mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-10 10:02:53 +00:00
A vector's corner smoothing was stored, imported and exported, but its rounded points were drawn as plain circular arcs, and the Design panel hid the Corner smoothing control for vectors. A vector with a closed region now draws its rounded points with the layer's corner smoothing, on the canvas, in hit testing, in PNG, SVG and PDF exports, and in the fill and stroke geometry of .fig exports. Each point keeps its own radius; a point without one stays sharp. The corner uses the profile of smoothed polygon corners, and smoothing is reduced before the radius when a corner does not fit in half of each segment. FigCraft's own rules for cases the help page does not describe: only a point between two straight segments that both bound a closed region is smoothed; points on open branches and points next to a curved segment keep the plain arc; concave corners are smoothed like convex ones. The Design panel shows Corner smoothing and its iOS (60%) button for vectors with a closed region, and not for open paths.
353 lines
17 KiB
Rust
353 lines
17 KiB
Rust
use std::io::Read;
|
||
use std::sync::Arc;
|
||
|
||
use super::*;
|
||
use figcraft_color::{BlendMode, Color, Gradient, GradientKind, ImageScaleMode, Paint};
|
||
use figcraft_doc::{BoolOp, Effect, EffectKind, ImageData, NodeKind, Region, Segment, StrokeAlign, TextData, VectorNetwork, Vertex, WindingRule};
|
||
|
||
pub(crate) fn page_count(pdf: &[u8]) -> usize {
|
||
let s = String::from_utf8_lossy(pdf);
|
||
s.matches("/Type/Page").count() - s.matches("/Type/Pages").count()
|
||
}
|
||
|
||
/// Every stream inflated (krilla deflates content, fonts and CMaps), joined.
|
||
pub(crate) fn inflated(pdf: &[u8]) -> String {
|
||
let mut out = String::new();
|
||
let mut i = 0;
|
||
while let Some(off) = find(&pdf[i..], b"stream") {
|
||
let mut start = i + off + 6;
|
||
if pdf.get(start) == Some(&b'\r') {
|
||
start += 1;
|
||
}
|
||
if pdf.get(start) == Some(&b'\n') {
|
||
start += 1;
|
||
}
|
||
let Some(end) = find(&pdf[start..], b"endstream") else { break };
|
||
let mut z = flate2::read::ZlibDecoder::new(&pdf[start..start + end]);
|
||
let mut buf = Vec::new();
|
||
if z.read_to_end(&mut buf).is_ok() {
|
||
out.push_str(&String::from_utf8_lossy(&buf));
|
||
}
|
||
i = start + end + 9;
|
||
}
|
||
out
|
||
}
|
||
|
||
fn find(h: &[u8], n: &[u8]) -> Option<usize> {
|
||
h.windows(n.len()).position(|w| w == n)
|
||
}
|
||
|
||
pub(crate) fn png(w: u32, h: u32) -> Vec<u8> {
|
||
let mut p = figcraft_render::Pixmap::new(w, h).unwrap();
|
||
for (i, px) in p.data_mut().as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||
let (x, y) = (i as u32 % w, i as u32 / w);
|
||
let v = if (x / 4 + y / 4) % 2 == 0 { 255 } else { 40 };
|
||
*px = [v, 80, 255 - v, 255];
|
||
}
|
||
figcraft_render::encode_png(&p).unwrap()
|
||
}
|
||
|
||
pub(crate) fn has_inter() -> bool {
|
||
figcraft_text::find_face("Inter", "Regular").is_some()
|
||
}
|
||
|
||
#[test]
|
||
fn pdf_vector_region_paints_emit_independent_colors() {
|
||
let mut d = Document::new("regions");
|
||
let p = d.first_page().unwrap();
|
||
let network = VectorNetwork {
|
||
vertices: [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (10.0, 0.0), (20.0, 0.0), (20.0, 10.0), (10.0, 10.0)]
|
||
.into_iter()
|
||
.map(|(x, y)| Vertex::new(x, y))
|
||
.collect(),
|
||
segments: (0..8).map(|i| Segment::line(i, (i + 1) % 4 + (i / 4) * 4)).collect(),
|
||
regions: vec![
|
||
Region {
|
||
loops: vec![vec![0, 1, 2, 3]],
|
||
winding: WindingRule::NonZero,
|
||
fills: Some(vec![Paint::solid(Color::from_hex("FF0000").unwrap())]),
|
||
..Default::default()
|
||
},
|
||
Region {
|
||
loops: vec![vec![4, 5, 6, 7]],
|
||
winding: WindingRule::NonZero,
|
||
fills: Some(vec![Paint::solid(Color::from_hex("0000FF").unwrap())]),
|
||
..Default::default()
|
||
},
|
||
Region { loops: vec![vec![0, 1, 2, 3]], winding: WindingRule::NonZero, fills: Some(Vec::new()), ..Default::default() },
|
||
],
|
||
};
|
||
let id = d.create(p, NodeKind::Vector { network }, "V", 0.0, 0.0, 20.0, 10.0).unwrap();
|
||
let content = inflated(&export_pdf(&d, &[id], &PdfOptions::default()).unwrap());
|
||
assert!(content.contains("1 0 0 rg") && content.contains("0 0 1 rg"), "{content}");
|
||
}
|
||
|
||
#[test]
|
||
fn pdf_vector_region_paints_draw_smoothed_corners() {
|
||
// A closed vector with rounded points and paints on its region: corner smoothing changes the
|
||
// exported outline, as on the canvas.
|
||
let corners = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)].map(|(x, y)| figcraft_geom::Point::new(x, y));
|
||
let mut network = VectorNetwork::from_points(&corners, true);
|
||
for v in &mut network.vertices {
|
||
v.radius = 20.0;
|
||
}
|
||
network.regions[0].fills = Some(vec![Paint::solid(Color::from_hex("FF0000").unwrap())]);
|
||
let mut d = Document::new("smoothing");
|
||
let p = d.first_page().unwrap();
|
||
let id = d.create(p, NodeKind::Vector { network }, "V", 0.0, 0.0, 100.0, 100.0).unwrap();
|
||
let content = |d: &Document| inflated(&export_pdf(d, &[id], &PdfOptions::default()).unwrap());
|
||
let plain = content(&d);
|
||
d.get_mut(id).unwrap().corners.smoothing = 0.6;
|
||
let smoothed = content(&d);
|
||
assert!(smoothed.contains("1 0 0 rg"), "{smoothed}");
|
||
assert_ne!(smoothed, plain, "the region's outline takes the smoothing");
|
||
}
|
||
|
||
/// A page with two frames: shapes, gradients, an image, strokes, a mask, a boolean, effects, text.
|
||
pub(crate) fn sample_doc() -> (Document, Vec<NodeId>) {
|
||
let mut d = Document::new("sample");
|
||
let p = d.first_page().unwrap();
|
||
let f1 = d.create(p, NodeKind::Frame, "Card", 0.0, 0.0, 320.0, 240.0).unwrap();
|
||
let f2 = d.create(p, NodeKind::Frame, "Second", 400.0, 0.0, 200.0, 240.0).unwrap();
|
||
d.get_mut(f1).unwrap().fills = vec![Paint::solid(Color::from_hex("F4F1EA").unwrap())];
|
||
// Solid rect with an inside stroke and rounded corners.
|
||
let r = d.create(f1, NodeKind::Rectangle, "R", 16.0, 16.0, 120.0, 80.0).unwrap();
|
||
{
|
||
let n = d.get_mut(r).unwrap();
|
||
n.fills = vec![Paint::solid(Color::from_hex("3366FF").unwrap())];
|
||
n.strokes = vec![Paint::solid(Color::BLACK)];
|
||
n.stroke.weight = 6.0;
|
||
n.stroke.align = StrokeAlign::Inside;
|
||
n.corners.radii = [12.0; 4];
|
||
n.effects = vec![Effect::drop_shadow()];
|
||
}
|
||
// Linear gradient ellipse with an outside stroke.
|
||
let e = d.create(f1, NodeKind::Ellipse { start: 0.0, sweep: std::f64::consts::TAU, inner: 0.0 }, "E", 160.0, 16.0, 120.0, 80.0).unwrap();
|
||
{
|
||
let n = d.get_mut(e).unwrap();
|
||
n.fills = vec![Paint::gradient(Gradient::default_for(GradientKind::Linear, Color::from_hex("FF5500").unwrap()))];
|
||
n.strokes = vec![Paint::solid(Color::from_hex("222222").unwrap())];
|
||
n.stroke.weight = 4.0;
|
||
n.stroke.align = StrokeAlign::Outside;
|
||
}
|
||
// Radial gradient, rotated star.
|
||
let st = d.create(f1, NodeKind::Star { count: 5, inner_ratio: 0.4 }, "S", 30.0, 120.0, 90.0, 90.0).unwrap();
|
||
{
|
||
let n = d.get_mut(st).unwrap();
|
||
n.fills = vec![Paint::gradient(Gradient::default_for(GradientKind::Radial, Color::from_hex("00AA66").unwrap()))];
|
||
n.transform = Affine::translate((75.0, 165.0)) * Affine::rotate(0.4) * Affine::translate((-45.0, -45.0));
|
||
}
|
||
// Image fill.
|
||
let bytes = png(32, 24);
|
||
d.images.insert("img1".into(), ImageData { bytes: Arc::new(bytes), width: 32, height: 24 });
|
||
let im = d.create(f1, NodeKind::Rectangle, "Img", 140.0, 120.0, 80.0, 60.0).unwrap();
|
||
d.get_mut(im).unwrap().fills = vec![Paint::image("img1", ImageScaleMode::Fill)];
|
||
// Text.
|
||
let mut td = TextData::new("Hello PDF");
|
||
td.style.font_size = 24.0;
|
||
let t = d.create(f1, NodeKind::Text(td), "T", 16.0, 200.0, 280.0, 32.0).unwrap();
|
||
d.get_mut(t).unwrap().fills = vec![Paint::solid(Color::from_hex("111111").unwrap())];
|
||
// A child sticking out of the frame (clipped).
|
||
d.create(f1, NodeKind::Rectangle, "Out", 280.0, 200.0, 100.0, 100.0).unwrap();
|
||
|
||
// Second frame: mask, boolean, angular + diamond gradients, blend modes, inner shadow, blur.
|
||
let g = d.create(f2, NodeKind::Group, "Masked group", 0.0, 0.0, 100.0, 100.0).unwrap();
|
||
let m = d.create(g, NodeKind::Ellipse { start: 0.0, sweep: std::f64::consts::TAU, inner: 0.0 }, "Mask", 10.0, 10.0, 80.0, 80.0).unwrap();
|
||
d.get_mut(m).unwrap().is_mask = true;
|
||
let masked = d.create(g, NodeKind::Rectangle, "Masked", 0.0, 0.0, 100.0, 100.0).unwrap();
|
||
d.get_mut(masked).unwrap().fills = vec![Paint::gradient(Gradient::default_for(GradientKind::Angular, Color::from_hex("8844FF").unwrap()))];
|
||
let a = d.create(f2, NodeKind::Rectangle, "A", 110.0, 10.0, 60.0, 60.0).unwrap();
|
||
let b = d.create(f2, NodeKind::Ellipse { start: 0.0, sweep: std::f64::consts::TAU, inner: 0.0 }, "B", 140.0, 40.0, 50.0, 50.0).unwrap();
|
||
let bo = d.create(f2, NodeKind::BooleanOp { op: BoolOp::Subtract }, "Bool", 0.0, 0.0, 1.0, 1.0).unwrap();
|
||
d.reparent(a, bo, None).unwrap();
|
||
d.reparent(b, bo, None).unwrap();
|
||
d.fit_to_children(bo).unwrap();
|
||
d.get_mut(bo).unwrap().fills = vec![Paint::solid(Color::from_hex("CC2244").unwrap())];
|
||
let di = d.create(f2, NodeKind::Rectangle, "Diamond", 10.0, 120.0, 80.0, 80.0).unwrap();
|
||
{
|
||
let n = d.get_mut(di).unwrap();
|
||
n.fills = vec![Paint::gradient(Gradient::default_for(GradientKind::Diamond, Color::from_hex("FFAA00").unwrap()))];
|
||
n.effects = vec![Effect::inner_shadow()];
|
||
}
|
||
let bl = d.create(f2, NodeKind::Rectangle, "Blend", 100.0, 120.0, 80.0, 80.0).unwrap();
|
||
{
|
||
let n = d.get_mut(bl).unwrap();
|
||
n.fills = vec![Paint::solid(Color::from_hex("00CCFF").unwrap())];
|
||
n.blend = BlendMode::Multiply;
|
||
n.opacity = 0.7;
|
||
}
|
||
let blur = d.create(f2, NodeKind::Ellipse { start: 0.0, sweep: std::f64::consts::TAU, inner: 0.0 }, "Blurred", 120.0, 160.0, 60.0, 60.0).unwrap();
|
||
d.get_mut(blur).unwrap().effects = vec![Effect { kind: EffectKind::LayerBlur(figcraft_doc::Blur::uniform(6.0)), visible: true }];
|
||
let frames = d.top_frames(p);
|
||
(d, frames)
|
||
}
|
||
|
||
#[test]
|
||
fn multi_frame_document_one_page_each() {
|
||
let (d, frames) = sample_doc();
|
||
assert_eq!(frames.len(), 2);
|
||
let pdf = export_pdf(&d, &frames, &PdfOptions::default()).unwrap();
|
||
assert!(pdf.starts_with(b"%PDF"));
|
||
assert_eq!(page_count(&pdf), 2);
|
||
let s = String::from_utf8_lossy(&pdf);
|
||
assert!(s.contains("/MediaBox[0 0 320 240]"), "page size from the frame box");
|
||
assert!(s.contains("/MediaBox[0 0 200 240]"));
|
||
// Images: the PNG fill plus effect rasters.
|
||
assert!(s.contains("/Subtype/Image"));
|
||
// Shadings for the vector gradients.
|
||
assert!(s.contains("/ShadingType"));
|
||
}
|
||
|
||
#[test]
|
||
fn text_is_real_text() {
|
||
if !has_inter() {
|
||
return;
|
||
}
|
||
let (d, frames) = sample_doc();
|
||
let pdf = export_pdf(&d, &frames[..1], &PdfOptions::default()).unwrap();
|
||
let s = String::from_utf8_lossy(&pdf);
|
||
assert!(s.contains("/FontFile2") || s.contains("/FontFile3"), "an embedded font");
|
||
assert!(s.contains("/ToUnicode"));
|
||
let all = inflated(&pdf);
|
||
// "H" maps to U+0048 in the ToUnicode CMap; the content uses text operators.
|
||
assert!(all.contains("<0048>"), "ToUnicode for H");
|
||
assert!(all.contains("Tj") || all.contains("TJ"));
|
||
// Outlined text has no font.
|
||
let outlined = export_pdf(&d, &frames[..1], &PdfOptions { outline_text: true, ..Default::default() }).unwrap();
|
||
let so = String::from_utf8_lossy(&outlined);
|
||
assert!(!so.contains("/FontFile"));
|
||
}
|
||
|
||
#[test]
|
||
fn nodes_on_one_page() {
|
||
let (d, frames) = sample_doc();
|
||
let pdf = export_nodes_pdf(&d, &frames).unwrap();
|
||
assert_eq!(page_count(&pdf), 1);
|
||
assert!(String::from_utf8_lossy(&pdf).contains("/MediaBox[0 0 600 240]"));
|
||
}
|
||
|
||
#[test]
|
||
fn every_paint_and_scale_mode() {
|
||
let mut d = Document::new("p");
|
||
let p = d.first_page().unwrap();
|
||
d.images.insert("i".into(), ImageData { bytes: Arc::new(png(16, 16)), width: 16, height: 16 });
|
||
let f = d.create(p, NodeKind::Frame, "F", 0.0, 0.0, 400.0, 100.0).unwrap();
|
||
for (i, mode) in [ImageScaleMode::Fill, ImageScaleMode::Fit, ImageScaleMode::Tile, ImageScaleMode::Crop].into_iter().enumerate() {
|
||
let r = d.create(f, NodeKind::Rectangle, "R", i as f64 * 100.0, 0.0, 90.0, 60.0).unwrap();
|
||
let mut paint = Paint::image("i", mode);
|
||
if let figcraft_color::PaintKind::Image(img) = &mut paint.kind {
|
||
img.rotation = i as i32;
|
||
img.scale = 0.5;
|
||
img.filters.exposure = if i == 3 { 0.3 } else { 0.0 };
|
||
}
|
||
paint.opacity = 0.8;
|
||
d.get_mut(r).unwrap().fills = vec![paint];
|
||
}
|
||
// A missing image and a corrupt one draw nothing.
|
||
d.images.insert("bad".into(), ImageData { bytes: Arc::new(vec![0x89, b'P', b'N', b'G', 1, 2, 3]), width: 9, height: 9 });
|
||
let r = d.create(f, NodeKind::Rectangle, "Bad", 0.0, 70.0, 20.0, 20.0).unwrap();
|
||
d.get_mut(r).unwrap().fills = vec![Paint::image("missing", ImageScaleMode::Fill), Paint::image("bad", ImageScaleMode::Fill)];
|
||
let pdf = export_pdf(&d, &[f], &PdfOptions::default()).unwrap();
|
||
assert!(pdf.starts_with(b"%PDF"));
|
||
assert!(String::from_utf8_lossy(&pdf).contains("/PatternType 1"), "tiled image pattern");
|
||
if let Ok(stem) = std::env::var("FIGCRAFT_PDF_SAMPLE") {
|
||
std::fs::write(format!("{stem}-images.pdf"), &pdf).unwrap();
|
||
let pix = figcraft_render::render_node(&d, f, 2.0).unwrap();
|
||
std::fs::write(format!("{stem}-images.png"), figcraft_render::encode_png(&pix).unwrap()).unwrap();
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn hostile_inputs_error_or_skip() {
|
||
let mut d = Document::new("h");
|
||
let p = d.first_page().unwrap();
|
||
assert_eq!(export_pdf(&d, &[], &PdfOptions::default()), Err(PdfError::Empty));
|
||
assert_eq!(export_pdf(&d, &[NodeId(99_999)], &PdfOptions::default()), Err(PdfError::NoNode(NodeId(99_999))));
|
||
let huge = d.create(p, NodeKind::Frame, "Huge", 0.0, 0.0, 1e9, 10.0).unwrap();
|
||
assert!(matches!(export_pdf(&d, &[huge], &PdfOptions::default()), Err(PdfError::TooLarge(..))));
|
||
let f = d.create(p, NodeKind::Frame, "F", 0.0, 0.0, 100.0, 100.0).unwrap();
|
||
// NaN geometry, NaN transform, NaN opacity, empty vector, absurd stroke, NaN gradient.
|
||
let a = d.create(f, NodeKind::Rectangle, "NaN", 0.0, 0.0, 10.0, 10.0).unwrap();
|
||
d.get_mut(a).unwrap().width = f64::NAN;
|
||
let b = d.create(f, NodeKind::Rectangle, "NaNxf", 0.0, 0.0, 10.0, 10.0).unwrap();
|
||
d.get_mut(b).unwrap().transform = Affine::new([f64::NAN, 0.0, 0.0, 1.0, 0.0, 0.0]);
|
||
let c = d.create(f, NodeKind::Rectangle, "NaNop", 0.0, 0.0, 10.0, 10.0).unwrap();
|
||
d.get_mut(c).unwrap().opacity = f32::NAN;
|
||
d.create(f, NodeKind::Vector { network: Default::default() }, "Empty", 0.0, 0.0, 0.0, 0.0).unwrap();
|
||
let s = d.create(f, NodeKind::Rectangle, "Stroke", 10.0, 10.0, 10.0, 10.0).unwrap();
|
||
{
|
||
let n = d.get_mut(s).unwrap();
|
||
n.strokes = vec![Paint::solid(Color::BLACK)];
|
||
n.stroke.weight = 1e7;
|
||
n.stroke.align = StrokeAlign::Outside;
|
||
let mut g = Gradient::default_for(GradientKind::Linear, Color::BLACK);
|
||
g.to = (f64::NAN, 0.0);
|
||
n.fills = vec![Paint::gradient(g)];
|
||
n.effects = vec![Effect {
|
||
kind: EffectKind::DropShadow(figcraft_doc::Shadow { radius: f64::NAN, spread: 1e9, ..Default::default() }),
|
||
visible: true,
|
||
}];
|
||
}
|
||
let t = d.create(f, NodeKind::Text(TextData::new("x")), "T", 0.0, 0.0, 10.0, 10.0).unwrap();
|
||
if let NodeKind::Text(td) = &mut d.get_mut(t).unwrap().kind {
|
||
td.style.font_size = f64::NAN;
|
||
}
|
||
let pdf = export_pdf(&d, &[f], &PdfOptions { raster_scale: f64::NAN, ..Default::default() }).unwrap();
|
||
assert!(pdf.starts_with(b"%PDF"));
|
||
// Deep nesting beyond the depth limit must refuse the export, not return partial artwork.
|
||
let mut parent = f;
|
||
for _ in 0..400 {
|
||
parent = d.create(parent, NodeKind::Group, "g", 0.0, 0.0, 1.0, 1.0).unwrap();
|
||
}
|
||
d.create(parent, NodeKind::Rectangle, "deep", 0.0, 0.0, 5.0, 5.0).unwrap();
|
||
assert!(matches!(export_pdf(&d, &[f], &PdfOptions::default()), Err(PdfError::Depth(_))));
|
||
// A zero-size node still exports (as a tiny page); a NaN-sized frame errors or exports.
|
||
let z = d.create(p, NodeKind::Rectangle, "Zero", 0.0, 0.0, 0.0, 0.0).unwrap();
|
||
assert!(export_pdf(&d, &[z], &PdfOptions::default()).is_ok());
|
||
let nf = d.create(p, NodeKind::Frame, "NaNFrame", 0.0, 0.0, 10.0, 10.0).unwrap();
|
||
d.get_mut(nf).unwrap().height = f64::NAN;
|
||
let _ = export_pdf(&d, &[nf], &PdfOptions::default());
|
||
}
|
||
|
||
/// Writes a sample PDF and the renderer's PNG of the same frames for a visual comparison:
|
||
/// `FIGCRAFT_PDF_SAMPLE=/path/stem cargo test -p figcraft-pdf sample`.
|
||
#[test]
|
||
fn sample_files() {
|
||
let Ok(stem) = std::env::var("FIGCRAFT_PDF_SAMPLE") else { return };
|
||
let (d, frames) = sample_doc();
|
||
let pdf = export_pdf(&d, &frames, &PdfOptions::default()).unwrap();
|
||
std::fs::write(format!("{stem}.pdf"), pdf).unwrap();
|
||
for (i, f) in frames.iter().enumerate() {
|
||
let pix = figcraft_render::render_node(&d, *f, 2.0).unwrap();
|
||
std::fs::write(format!("{stem}-{}.png", i + 1), figcraft_render::encode_png(&pix).unwrap()).unwrap();
|
||
std::fs::write(format!("{stem}-{}.pdf", i + 1), export_pdf(&d, &[*f], &PdfOptions::default()).unwrap()).unwrap();
|
||
}
|
||
}
|
||
|
||
/// A tiny PNG whose header claims 30,000 × 30,000 RGBA (3.6 GB decoded) is refused when it is read,
|
||
/// before anything is decoded: the PDF writer is craft-libs' patched krilla.
|
||
#[test]
|
||
fn a_png_that_decodes_too_large_is_refused() {
|
||
fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
|
||
out.extend_from_slice(&u32::try_from(data.len()).unwrap().to_be_bytes());
|
||
let mut crc = flate2::Crc::new();
|
||
crc.update(kind);
|
||
crc.update(data);
|
||
out.extend_from_slice(kind);
|
||
out.extend_from_slice(data);
|
||
out.extend_from_slice(&crc.sum().to_be_bytes());
|
||
}
|
||
let mut ihdr = Vec::new();
|
||
ihdr.extend_from_slice(&30_000u32.to_be_bytes());
|
||
ihdr.extend_from_slice(&30_000u32.to_be_bytes());
|
||
ihdr.extend_from_slice(&[8, 6, 0, 0, 0]);
|
||
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
||
std::io::Write::write_all(&mut z, &[0; 64]).unwrap();
|
||
let mut png = b"\x89PNG\r\n\x1a\n".to_vec();
|
||
chunk(&mut png, b"IHDR", &ihdr);
|
||
chunk(&mut png, b"IDAT", &z.finish().unwrap());
|
||
chunk(&mut png, b"IEND", &[]);
|
||
const { assert!(krilla::CRAFT_UNWIND_SAFE_DROP) };
|
||
assert!(krilla::image::Image::from_png(krilla::Data::from(png), true).is_err());
|
||
}
|