mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 02:06:54 -06:00
Integrate current main (d8d78851) without rewriting the contributor's commits, retaining craft-libs 9f75d3dc.
Accept literal null trailer entries while rejecting actual or ambiguous encryption/hybrid markers. Structurally inspect bounded xref/trailer history instead of adopting trailer-looking comments. Preflight all selected source variants before editing/undo; preserve reset-to-defaults. Suppress stale unrewritable canvas overrides rather than revealing source content. Keep conservative all-content crop fallback without overcropping layerless/null stubs.
Validation: cargo xtask ci, all 9 gates passed; 3062 tests passed, 6 skipped; all 16 WASM-safe crates checked. 17 supported variants passed independent qpdf, logical preservation and Poppler pixel checks; 6 unsupported/repair-only cases refused. Direct-harness Claude Opus 5 reviewed the exact main-relative delta with no blocking findings.
No native-format changes or unrelated layer features. Synthetic regression fixtures are generated in test code.
352 lines
18 KiB
Rust
352 lines
18 KiB
Rust
//! Synthetic placement/export regressions for PDF backend updates.
|
|
|
|
use serde_json::json;
|
|
|
|
use designcraft_engine::Session;
|
|
use designcraft_engine::cmd::{base64_decode, base64_encode};
|
|
|
|
fn pdf(objects: &[String]) -> Vec<u8> {
|
|
let mut bytes = b"%PDF-1.7\n".to_vec();
|
|
let mut offsets = vec![0];
|
|
for (i, object) in objects.iter().enumerate() {
|
|
offsets.push(bytes.len());
|
|
bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes());
|
|
}
|
|
let xref = bytes.len();
|
|
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f\r\n", offsets.len()).as_bytes());
|
|
for offset in offsets.iter().skip(1) {
|
|
bytes.extend_from_slice(format!("{offset:010} 00000 n\r\n").as_bytes());
|
|
}
|
|
bytes.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", offsets.len()).as_bytes());
|
|
bytes
|
|
}
|
|
|
|
fn layered_pdf(second_name: &str) -> Vec<u8> {
|
|
layered_pdf_with_box(second_name, "")
|
|
}
|
|
|
|
fn layered_pdf_with_box(second_name: &str, page_box: &str) -> Vec<u8> {
|
|
let content = "/OC /a BDC 0 0 1 rg 10 10 35 80 re f EMC /OC /b BDC 1 0 0 rg 55 10 35 80 re f EMC";
|
|
pdf(&[
|
|
"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [5 0 R 6 0 R] /D << /Order [5 0 R 6 0 R] >> >> >>".into(),
|
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
|
format!(
|
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] {page_box} /Resources << /Properties << /a 5 0 R /b 6 0 R >> >> /Contents 4 0 R >>"
|
|
),
|
|
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
|
|
"<< /Type /OCG /Name (Blue) >>".into(),
|
|
format!("<< /Type /OCG /Name ({second_name}) >>"),
|
|
])
|
|
}
|
|
|
|
fn rendered(bytes: &[u8]) -> designcraft_render::Rendered {
|
|
let pm = designcraft_render::render_pdf_page(bytes, 0, 100).expect("render synthetic PDF");
|
|
designcraft_render::Rendered {
|
|
width: pm.width().into(),
|
|
height: pm.height().into(),
|
|
pixels: pm.data().iter().flat_map(|p| [p.r, p.g, p.b, p.a]).collect(),
|
|
}
|
|
}
|
|
|
|
// Optional local evidence output; no binary fixtures are checked in.
|
|
fn evidence(name: &str, bytes: &[u8]) {
|
|
if let Some(dir) = std::env::var_os("CRAFT_PDF_EVIDENCE_DIR") {
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
std::fs::write(std::path::Path::new(&dir).join(name), bytes).unwrap();
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn placed_pdf_preserves_transparent_background_and_layer_choices() {
|
|
let bytes = layered_pdf("Red");
|
|
let img = rendered(&bytes);
|
|
assert_eq!(img.pixel(2, 2), [0, 0, 0, 0]);
|
|
assert_eq!(img.pixel(25, 50), [0, 0, 255, 255]);
|
|
assert_eq!(img.pixel(75, 50), [255, 0, 0, 255]);
|
|
let hidden = designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).unwrap();
|
|
let img = rendered(&hidden);
|
|
assert_eq!(img.pixel(75, 50), [0, 0, 0, 0]);
|
|
assert_eq!(img.pixel(25, 50), [0, 0, 255, 255]);
|
|
evidence("layers-hidden.png", &img.to_png());
|
|
evidence("layers-source.pdf", &bytes);
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn duplicate_layer_names_keep_name_based_hiding() {
|
|
let bytes = layered_pdf("Blue");
|
|
assert_eq!(designcraft_render::pdf_layers(&bytes).len(), 2);
|
|
let hidden = designcraft_render::pdf_hide_layers(&bytes, &["Blue".into()]).unwrap();
|
|
let img = rendered(&hidden);
|
|
assert_eq!(img.pixel(25, 50), [0, 0, 0, 0]);
|
|
assert_eq!(img.pixel(75, 50), [0, 0, 0, 0]);
|
|
// A second render of the original must not inherit the hidden configuration.
|
|
assert_eq!(rendered(&bytes).pixel(25, 50), [0, 0, 255, 255]);
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn crop_box_and_rotation_keep_placed_dimensions() {
|
|
let bytes = pdf(&[
|
|
"<< /Type /Catalog /Pages 2 0 R >>".into(),
|
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 120 100] /CropBox [10 20 110 80] /Rotate 90 >>".into(),
|
|
]);
|
|
assert_eq!(designcraft_render::pdf_page_count(&bytes), Some(1));
|
|
assert_eq!(designcraft_render::pdf_page_size(&bytes, 0), Some((60.0, 100.0)));
|
|
let img = rendered(&bytes);
|
|
assert_eq!((img.width, img.height), (60, 100));
|
|
assert_eq!(img.pixel(20, 20), [0, 0, 0, 0]);
|
|
}
|
|
|
|
fn exported(hidden: bool) -> Vec<u8> {
|
|
let mut session = Session::new();
|
|
session.execute("file.new", &json!({"width": 100, "height": 100, "pages": 1})).unwrap();
|
|
session.execute("edit.transparencyBlendSpace", &json!({"space": "rgb"})).unwrap();
|
|
session.execute("frame.create", &json!({"rect": [0, 0, 100, 100]})).unwrap();
|
|
session.execute("object.fill", &json!({"swatch": "[Black]"})).unwrap();
|
|
let placed = session
|
|
.execute(
|
|
"file.place",
|
|
&json!({
|
|
"base64": base64_encode(&layered_pdf("Red")), "name": "layers.pdf", "x": 0, "y": 0, "width": 100
|
|
}),
|
|
)
|
|
.unwrap();
|
|
if hidden {
|
|
session.execute("object.layerOptions", &json!({"ids": [placed["id"]], "hidden": ["Red"]})).unwrap();
|
|
}
|
|
let report = session.execute("file.exportPdf", &json!({"compress": false})).unwrap();
|
|
let bytes = base64_decode(report["base64"].as_str().unwrap());
|
|
if hidden {
|
|
assert!(report["warnings"].to_string().contains("hidden layers"));
|
|
}
|
|
bytes
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn hidden_layer_export_composites_over_underlying_artwork() {
|
|
let bytes = exported(true);
|
|
let img = rendered(&bytes);
|
|
let right = img.pixel(75, 50);
|
|
assert!(right[..3].iter().all(|c| *c < 45), "hidden area must show the black backdrop, not opaque white: {right:?}");
|
|
let left = img.pixel(25, 50);
|
|
assert!(left[2] > 180 && left[0] < 80, "visible blue layer: {left:?}");
|
|
evidence("hidden-export.pdf", &bytes);
|
|
evidence("hidden-export.png", &img.to_png());
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn ordinary_placed_pages_still_embed_as_vectors() {
|
|
use hayro_syntax::object::Name;
|
|
let bytes = exported(false);
|
|
let parsed = hayro_syntax::Pdf::new(bytes.clone()).unwrap();
|
|
let subtypes: Vec<_> = parsed
|
|
.objects()
|
|
.into_iter()
|
|
.filter_map(|o| o.into_stream())
|
|
.filter_map(|s| s.dict().get::<Name>("Subtype").map(|n| n.as_str().to_owned()))
|
|
.collect();
|
|
assert!(subtypes.iter().any(|s| s == "Form"), "placed PDF form: {subtypes:?}");
|
|
assert!(!subtypes.iter().any(|s| s == "Image"), "no rasterization: {subtypes:?}");
|
|
let img = rendered(&bytes);
|
|
let right = img.pixel(75, 50);
|
|
assert!(right[0] > 180 && right[1] < 80, "visible red vector layer: {right:?}");
|
|
evidence("vector-export.pdf", &bytes);
|
|
evidence("vector-export.png", &img.to_png());
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn oversized_image_does_not_silently_disappear_from_layered_export() {
|
|
// Just above the backend's 2^28-pixel cap. The stream is one byte; no large
|
|
// fixture or large image allocation is needed to exercise the refusal.
|
|
let content = "/OC /a BDC 0 0 1 rg 10 10 35 80 re f EMC q 100 0 0 100 0 0 cm /Im Do Q";
|
|
let bytes = pdf(&[
|
|
"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [6 0 R] /D << /Order [6 0 R] >> >> >>".into(),
|
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << /Properties << /a 6 0 R >> /XObject << /Im 5 0 R >> >> /Contents 4 0 R >>".into(),
|
|
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
|
|
"<< /Type /XObject /Subtype /Image /Width 32769 /Height 8192 /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 1 >>\nstream\nX\nendstream".into(),
|
|
"<< /Type /OCG /Name (Blue) >>".into(),
|
|
]);
|
|
let mut session = Session::new();
|
|
session.execute("file.new", &json!({"width": 100, "height": 100, "pages": 1})).unwrap();
|
|
let placed = session
|
|
.execute(
|
|
"file.place",
|
|
&json!({
|
|
"base64": base64_encode(&bytes), "name": "oversized-scan.pdf", "x": 0, "y": 0, "width": 100
|
|
}),
|
|
)
|
|
.unwrap();
|
|
session.execute("object.layerOptions", &json!({"ids": [placed["id"]], "hidden": ["Blue"]})).unwrap();
|
|
let before = session.execute("file.serialize", &json!({})).unwrap();
|
|
let result = session.execute("file.exportPdf", &json!({}));
|
|
assert!(result.is_err(), "export must report the image omitted by the backend's resource limit");
|
|
assert_eq!(session.execute("file.serialize", &json!({})).unwrap(), before, "a failed export must leave the document unchanged");
|
|
let error = result.unwrap_err().to_string();
|
|
assert!(error.contains("oversized-scan.pdf") && error.contains("image"), "{error}");
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn failed_layer_rewrite_aborts_export_instead_of_revealing_hidden_content() {
|
|
// A stored layer choice can outlive relinking to a PDF with no layers.
|
|
let bytes = pdf(&[
|
|
"<< /Type /Catalog /Pages 2 0 R >>".into(),
|
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>".into(),
|
|
]);
|
|
assert!(designcraft_render::render_pdf_page(&bytes, 0, 100).is_some());
|
|
assert!(designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).is_none());
|
|
let mut session = Session::new();
|
|
session.execute("file.new", &json!({"width": 100, "height": 100, "pages": 1})).unwrap();
|
|
let placed =
|
|
session.execute("file.place", &json!({"base64": base64_encode(&bytes), "name": "missing-layers.pdf", "x": 0, "y": 0, "width": 100})).unwrap();
|
|
// Commands now reject unrewritable choices. Simulate stale persisted state after relinking.
|
|
let id: designcraft_doc::ItemId = serde_json::from_value(placed["id"].clone()).unwrap();
|
|
std::sync::Arc::make_mut(&mut session.doc_mut().unwrap().doc).item_mut(id).unwrap().pdf_hidden_layers = vec!["Red".into()];
|
|
let before = session.execute("file.serialize", &json!({})).unwrap();
|
|
let result = session.execute("file.exportPdf", &json!({}));
|
|
assert!(result.is_err(), "failed layer hiding must not export the original visible layers");
|
|
let error = result.unwrap_err().to_string();
|
|
assert!(error.contains("missing-layers.pdf") && error.contains("layer"), "{error}");
|
|
assert_eq!(session.execute("file.serialize", &json!({})).unwrap(), before);
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn placed_standard_font_text_renders_ink_on_a_transparent_background() {
|
|
let content = "BT /F1 18 Tf 10 40 Td (PDF text) Tj ET";
|
|
let bytes = pdf(&[
|
|
"<< /Type /Catalog /Pages 2 0 R >>".into(),
|
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>".into(),
|
|
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
|
|
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".into(),
|
|
]);
|
|
let img = rendered(&bytes);
|
|
let ink = (40..65).flat_map(|y| (5..95).map(move |x| (x, y))).filter(|(x, y)| img.pixel(*x, *y)[3] > 100).count();
|
|
assert!(ink > 100, "standard-font glyphs must paint visible ink: {ink}");
|
|
assert_eq!(img.pixel(2, 2), [0, 0, 0, 0]);
|
|
assert_eq!(img.pixel(50, 80), [0, 0, 0, 0]);
|
|
assert!(designcraft_render::pdf_page_png_checked(&bytes, 0, 100, &[], None).is_ok());
|
|
evidence("standard-font-text.png", &img.to_png());
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn unavailable_placed_page_reports_the_page_and_recovery_action() {
|
|
let error = designcraft_render::pdf_page_png_checked(&layered_pdf("Red"), 2, 100, &["Red".into()], None).unwrap_err();
|
|
assert!(matches!(error, designcraft_render::PdfRasterError::MissingPage { page: 3 }));
|
|
assert!(error.to_string().contains("place the replacement PDF again"));
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn hidden_layers_work_with_an_xref_stream_and_compressed_catalog() {
|
|
// Reuse the tiny synthetic objects, but put the catalog in an object stream
|
|
// and index everything with a binary cross-reference stream.
|
|
let classic = String::from_utf8(layered_pdf("Red")).unwrap();
|
|
let objects: Vec<&str> = (1..=6).map(|i| classic.split_once(&format!("{i} 0 obj\n")).unwrap().1.split_once("\nendobj").unwrap().0).collect();
|
|
let mut bytes = b"%PDF-1.7\n".to_vec();
|
|
let mut offsets = [0u32; 9];
|
|
for (i, object) in objects.iter().enumerate().skip(1) {
|
|
offsets[i + 1] = u32::try_from(bytes.len()).unwrap();
|
|
bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes());
|
|
}
|
|
offsets[7] = u32::try_from(bytes.len()).unwrap();
|
|
let catalog = format!("1 0 {}", objects[0]);
|
|
bytes.extend_from_slice(
|
|
format!("7 0 obj\n<< /Type /ObjStm /N 1 /First 4 /Length {} >>\nstream\n{catalog}\nendstream\nendobj\n", catalog.len()).as_bytes(),
|
|
);
|
|
offsets[8] = u32::try_from(bytes.len()).unwrap();
|
|
let mut entries = Vec::new();
|
|
for (i, offset) in offsets.iter().enumerate() {
|
|
let (kind, field, generation): (u8, u32, u16) = match i {
|
|
0 => (0, 0, 65535),
|
|
1 => (2, 7, 0),
|
|
_ => (1, *offset, 0),
|
|
};
|
|
entries.push(kind);
|
|
entries.extend_from_slice(&field.to_be_bytes());
|
|
entries.extend_from_slice(&generation.to_be_bytes());
|
|
}
|
|
bytes.extend_from_slice(format!("8 0 obj\n<< /Type /XRef /Size 9 /Root 1 0 R /W [1 4 2] /Length {} >>\nstream\n", entries.len()).as_bytes());
|
|
bytes.extend_from_slice(&entries);
|
|
bytes.extend_from_slice(format!("\nendstream\nendobj\nstartxref\n{}\n%%EOF\n", offsets[8]).as_bytes());
|
|
assert_eq!(rendered(&bytes).pixel(75, 50), [255, 0, 0, 255]);
|
|
let hidden = designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).unwrap();
|
|
let img = rendered(&hidden);
|
|
assert_eq!(img.pixel(75, 50), [0, 0, 0, 0]);
|
|
assert_eq!(img.pixel(25, 50), [0, 0, 255, 255]);
|
|
assert!(designcraft_render::pdf_page_png_checked(&bytes, 0, 100, &["Red".into()], None).is_ok());
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn trailing_whitespace_does_not_prevent_layer_hiding() {
|
|
let mut bytes = layered_pdf("Red");
|
|
bytes.extend_from_slice(&[b' '; 96]);
|
|
let hidden = designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).expect("trailing whitespace is accepted by the layer rewriter");
|
|
let img = rendered(&hidden);
|
|
assert_eq!(img.pixel(75, 50), [0, 0, 0, 0]);
|
|
assert_eq!(img.pixel(25, 50), [0, 0, 255, 255]);
|
|
assert!(designcraft_render::pdf_page_png_checked(&bytes, 0, 100, &["Red".into()], None).is_ok());
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn failed_content_decode_aborts_layered_export_without_changing_the_document() {
|
|
for filter in ["FlateDecode", "DCTDecode"] {
|
|
let bytes = pdf(&[
|
|
"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [5 0 R] /D << /Order [5 0 R] >> >> >>".into(),
|
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << /Properties << /a 5 0 R >> >> /Contents 4 0 R >>".into(),
|
|
format!("<< /Filter /{filter} /Length 1 >>\nstream\nx\nendstream"),
|
|
"<< /Type /OCG /Name (Blue) >>".into(),
|
|
]);
|
|
let mut session = Session::new();
|
|
session.execute("file.new", &json!({"width": 100, "height": 100, "pages": 1})).unwrap();
|
|
let placed = session
|
|
.execute("file.place", &json!({"base64": base64_encode(&bytes), "name": "broken-content.pdf", "x": 0, "y": 0, "width": 100}))
|
|
.unwrap();
|
|
session.execute("object.layerOptions", &json!({"ids": [placed["id"]], "hidden": ["Blue"]})).unwrap();
|
|
let before = session.execute("file.serialize", &json!({})).unwrap();
|
|
let error = session.execute("file.exportPdf", &json!({})).unwrap_err().to_string();
|
|
assert!(error.contains("broken-content.pdf") && error.contains("content"), "{filter}: {error}");
|
|
assert_eq!(session.execute("file.serialize", &json!({})).unwrap(), before, "{filter}");
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
|
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
|
fn hidden_layer_export_keeps_the_placed_trim_box() {
|
|
let bytes = layered_pdf_with_box("Red", "/TrimBox [0 0 50 100]");
|
|
let mut session = Session::new();
|
|
session.execute("file.new", &json!({"width": 50, "height": 100, "pages": 1})).unwrap();
|
|
session.execute("edit.transparencyBlendSpace", &json!({"space": "rgb"})).unwrap();
|
|
let placed = session
|
|
.execute(
|
|
"file.place",
|
|
&json!({"base64": base64_encode(&bytes), "name": "trimmed-layers.pdf", "pdfCrop": "trim", "x": 0, "y": 0, "width": 100}),
|
|
)
|
|
.unwrap();
|
|
session.execute("object.layerOptions", &json!({"ids": [placed["id"]], "hidden": ["Red"]})).unwrap();
|
|
let before = session.execute("file.serialize", &json!({})).unwrap();
|
|
let report = session.execute("file.exportPdf", &json!({"compress": false})).unwrap();
|
|
let exported = base64_decode(report["base64"].as_str().unwrap());
|
|
let img = rendered(&exported);
|
|
assert_eq!((img.width, img.height), (50, 100));
|
|
// Omitting the shown box squeezes the whole page into the trimmed graphic,
|
|
// moving the blue rectangle's right edge from x=45 to x=22.5.
|
|
let blue = img.pixel(35, 50);
|
|
assert!(blue[2] > 180 && blue[0] < 80, "the trim box keeps the visible layer at its placed coordinates: {blue:?}");
|
|
assert_eq!(session.execute("file.serialize", &json!({})).unwrap(), before);
|
|
evidence("trimmed-layer-export.pdf", &exported);
|
|
}
|