mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-25 02:37:00 -06:00
PDF: a hidden layer missing from the placed file aborts vector export
Main now aborts an export when a stored hidden layer can no longer be applied (#876). Vector export follows it: a layer hidden in Object Layer Options that the placed PDF doesn't have, because the file has no layers or was relinked, fails with an error naming the file instead of exporting everything visible. The image-path tests for oversized images and undecodable content export as PDF/A-2b, which still rasterizes placed PDFs with hidden layers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
38d692089f
commit
a84faee829
@@ -118,11 +118,7 @@ fn exported(hidden: bool) -> Vec<u8> {
|
||||
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
|
||||
base64_decode(report["base64"].as_str().unwrap())
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
||||
@@ -185,7 +181,8 @@ fn oversized_image_does_not_silently_disappear_from_layered_export() {
|
||||
.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!({}));
|
||||
// PDF/A-2b has no optional content: placed PDFs with hidden layers go out as images there.
|
||||
let result = session.execute("file.exportPdf", &json!({"standard": "pdfa2b"}));
|
||||
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();
|
||||
@@ -317,7 +314,8 @@ fn failed_content_decode_aborts_layered_export_without_changing_the_document() {
|
||||
.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();
|
||||
// PDF/A-2b has no optional content: placed PDFs with hidden layers go out as images there.
|
||||
let error = session.execute("file.exportPdf", &json!({"standard": "pdfa2b"})).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}");
|
||||
}
|
||||
|
||||
@@ -1970,7 +1970,15 @@ mod image_content_cache_tests {
|
||||
let mut doc = Document::new(&Default::default());
|
||||
let cache = Cache::new();
|
||||
let opts = PdfOptions::default();
|
||||
let source = export_pdf(&doc, &cache, &opts).unwrap();
|
||||
// One layer, "Hidden", shown by default: hiding it needs a prepared copy, showing it doesn't.
|
||||
let content = "/OC /a BDC 0 0 1 rg 0 0 100 100 re f EMC";
|
||||
let source = registration_tests::assemble(&[
|
||||
"<</Type/Catalog/Pages 2 0 R/OCProperties<</OCGs[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!("<</Length {}>>\nstream\n{content}\nendstream", content.len()),
|
||||
"<</Type/OCG/Name(Hidden)>>".into(),
|
||||
]);
|
||||
let a = asset(&mut doc, source.clone(), "a.pdf");
|
||||
let b = asset(&mut doc, source, "b.pdf");
|
||||
let mut ex = exporter(&doc, &cache, &opts);
|
||||
@@ -1994,7 +2002,8 @@ mod image_content_cache_tests {
|
||||
assert_eq!(ex.pdf_preparations, count, "count actual preparations, not writer resource deduplication");
|
||||
}
|
||||
assert_eq!(ex.variants.len(), 3);
|
||||
assert_eq!(ex.pdfs.len(), 2);
|
||||
// a, b, and a with its layer hidden (a copy of its own).
|
||||
assert_eq!(ex.pdfs.len(), 3);
|
||||
let variant = ex.variants.get(&(a, Vec::new())).unwrap();
|
||||
assert!(Arc::ptr_eq(&variant.data, &doc.assets[&a].data));
|
||||
surface.finish();
|
||||
|
||||
@@ -11,6 +11,12 @@ fn invalid() -> PdfError {
|
||||
PdfError::Write("the placed PDF's optional-content state could not be preserved safely".into())
|
||||
}
|
||||
|
||||
/// A layer hidden in Object Layer Options that the placed file doesn't have (relinked, say):
|
||||
/// exporting would be a guess at what was meant to stay hidden.
|
||||
fn missing_layer() -> PdfError {
|
||||
PdfError::Write("a layer hidden in Object Layer Options is not in the placed PDF; relink it or reset its layer options".into())
|
||||
}
|
||||
|
||||
fn references(dict: &Dict<'_>, key: &[u8]) -> Result<Vec<ObjectIdentifier>> {
|
||||
if !dict.contains_key(key) {
|
||||
return Ok(Vec::new());
|
||||
@@ -128,6 +134,9 @@ pub(crate) fn prepare(bytes: &Arc<Vec<u8>>, hidden: &[String]) -> Result<Variant
|
||||
.objects()
|
||||
.into_iter()
|
||||
.any(|object| matches!(object, Object::Dict(dict) if dict.get::<Name<'_>>(b"Type").as_ref().map(AsRef::as_ref) == Some(b"OCG")));
|
||||
if !hidden.is_empty() {
|
||||
return Err(missing_layer());
|
||||
}
|
||||
return Ok(Variant { data: bytes.clone(), hidden: Vec::new(), warning: None, has_optional_content });
|
||||
}
|
||||
let properties = root.get::<Dict<'_>>(b"OCProperties").ok_or_else(invalid)?;
|
||||
@@ -165,6 +174,7 @@ pub(crate) fn prepare(bytes: &Arc<Vec<u8>>, hidden: &[String]) -> Result<Variant
|
||||
let mut changes = Vec::new();
|
||||
let mut effective = Vec::new();
|
||||
let mut renamed = false;
|
||||
let mut found = vec![false; hidden.len()];
|
||||
for id in groups {
|
||||
let group = xref.get::<Dict<'_>>(id).ok_or_else(invalid)?;
|
||||
if group.get::<Name<'_>>(b"Type").as_ref().map(AsRef::as_ref) != Some(b"OCG") {
|
||||
@@ -175,7 +185,13 @@ pub(crate) fn prepare(bytes: &Arc<Vec<u8>>, hidden: &[String]) -> Result<Variant
|
||||
(true, _) => !fallback && (off.contains(&id) || (base_off && !on.contains(&id))),
|
||||
(false, Some(name)) => {
|
||||
let name = String::from_utf8_lossy(name.as_bytes());
|
||||
hidden.iter().any(|n| n == name.as_ref())
|
||||
let mut is_hidden = false;
|
||||
for (n, found) in hidden.iter().zip(&mut found) {
|
||||
if n == name.as_ref() {
|
||||
(*found, is_hidden) = (true, true);
|
||||
}
|
||||
}
|
||||
is_hidden
|
||||
}
|
||||
(false, None) => return Err(invalid()),
|
||||
};
|
||||
@@ -200,6 +216,9 @@ pub(crate) fn prepare(bytes: &Arc<Vec<u8>>, hidden: &[String]) -> Result<Variant
|
||||
};
|
||||
changes.push((id, body));
|
||||
}
|
||||
if found.contains(&false) {
|
||||
return Err(missing_layer());
|
||||
}
|
||||
effective.sort();
|
||||
effective.dedup();
|
||||
let data = if changes.is_empty() { bytes.clone() } else { Arc::new(append(bytes, &changes, xref.root_id())?) };
|
||||
|
||||
@@ -45,7 +45,7 @@ fn unlayered_source() -> Vec<u8> {
|
||||
])
|
||||
}
|
||||
|
||||
fn assemble(objects: &[String]) -> Vec<u8> {
|
||||
pub(super) fn assemble(objects: &[String]) -> Vec<u8> {
|
||||
let mut bytes = b"%PDF-1.5\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (index, body) in objects.iter().enumerate() {
|
||||
|
||||
Reference in New Issue
Block a user