diff --git a/ROADMAP.md b/ROADMAP.md index 406e7a304..97b22b499 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -127,7 +127,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - Object Layer Options for placed PDFs (`object.pdfLayers`, `object.layerOptions`, Object ▸ Object Layer Options…): the file's optional-content layers listed and shown or hidden per frame on screen; in exported PDFs such graphics go out as 300 ppi images of their visible layers. - Effects: Inner Glow (edge or centre), Bevel and Emboss (inner bevel lit from the light angle), Satin (`object.innerGlow`, `object.bevel`, `object.satin`); PDF export now keeps soft effects: objects with shadows, glows, feathers, bevels or satin go out as 300 ppi transparent images of their appearance (threaded text frames excepted). - Directional Feather (`object.directionalFeather {widths: [top, left, bottom, right]}`): each side fades over its own width. -- View ▸ Flattener Preview (`view.flattenerPreview`): objects involving transparency highlighted in red; knockout groups export as images (PDF output has no knockout groups). +- View ▸ Flattener Preview (`view.flattenerPreview`): objects involving transparency highlighted in red; PDF export writes Knockout Group as a knockout transparency group (`/K true`) and Isolate Blending as `/I true`. - Live captions (`object.caption {live: true}`, Object ▸ Captions ▸ Generate Live Caption): the caption text is re-filled from the source object's metadata after every edit (same undo step). - Style Export Tagging (`style.exportTag`, Paragraph Style Options ▸ Export Tagging): paragraph and character styles choose their HTML element and class in EPUB/HTML. - Tagged Text: export (`file.exportText {format: "tagged"}`) and place (detected by its header; ASCII, UTF-8 or UTF-16) with paragraph and character styles, font, typeface and size, and `<0x…>` characters. diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs index 1396c2b06..62233c7a6 100644 --- a/crates/engine/src/cmd/export.rs +++ b/crates/engine/src/cmd/export.rs @@ -513,9 +513,7 @@ fn threaded(d: &designcraft_doc::Document, it: &designcraft_doc::Item) -> bool { /// Soft effects other than the gradient feather (which PDF export draws itself). fn raster_effects(it: &designcraft_doc::Item) -> bool { let e = &it.effects; - // Knockout groups too: PDF output here has no knockout transparency groups. - it.knockout - || e.drop_shadow.on + e.drop_shadow.on || e.feather > 0.0 || e.inner_shadow.on || e.outer_glow.on @@ -795,7 +793,7 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result { } } if fx_count > 0 { - r.warnings.push(format!("{fx_count} object(s) with soft effects (shadows, glows, feathers, bevels) or knockout groups exported as images")); + r.warnings.push(format!("{fx_count} object(s) with soft effects (shadows, glows, feathers, bevels) exported as images")); } if layer_count > 0 { r.warnings.push(format!("{layer_count} placed PDF(s) with hidden layers exported as images")); diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 82f33a6d8..25f008e53 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -4195,13 +4195,14 @@ mod fitting_tests { let df = shot(&s); assert!(df.pixel(102, 200)[0] > 200, "left edge faded to paper: {:?}", df.pixel(102, 200)); assert_eq!(df.pixel(298, 200), plain.pixel(298, 200), "right edge untouched"); - // A knockout group exports as an image too. + // A knockout group stays vector, as a knockout transparency group. let other = s.execute("frame.create", &json!({"rect": [400, 100, 450, 150]})).unwrap()["id"].clone(); let other2 = s.execute("frame.create", &json!({"rect": [420, 120, 470, 170]})).unwrap()["id"].clone(); let g = s.execute("object.group", &json!({"ids": [other, other2]})).unwrap()["id"].clone(); s.execute("object.transparencyGroup", &json!({"ids": [g], "knockout": true})).unwrap(); let r = s.execute("file.exportPdf", &json!({})).unwrap(); - assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("2 object(s)")), "{r}"); + assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("1 object(s)")), "{r}"); + assert!(String::from_utf8_lossy(&super::super::file::base64_decode(r["base64"].as_str().unwrap())).contains("/K true")); // PDF export carries effects (as an image of the object). let r = s.execute("file.exportPdf", &json!({})).unwrap(); assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("soft effects")), "{r}"); diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index 4c155bb1a..977929f4b 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -749,11 +749,10 @@ impl Exporter<'_> { n } - /// Isolate Blending / Knockout groups: an isolated transparency group (knockout is drawn - /// isolated too). + /// Isolate Blending / Knockout Group: a transparency group with `/I` and `/K` as set. fn push_isolation(s: &mut Surface, it: &Item) -> usize { if it.isolate || it.knockout { - s.push_isolated(); + s.push_group(krilla::blend::GroupOptions { isolated: it.isolate, knockout: it.knockout }); 1 } else { 0 diff --git a/crates/pdf/src/print_tests.rs b/crates/pdf/src/print_tests.rs index 69f63501f..a3f8e22c3 100644 --- a/crates/pdf/src/print_tests.rs +++ b/crates/pdf/src/print_tests.rs @@ -240,3 +240,38 @@ fn ruby_overprint_fill_is_written() { assert_eq!(overprint_states(&r.bytes), [(true, false, 1)]); assert!(!r.warnings.iter().any(|w| w.contains("overprint")), "{:?}", r.warnings); } + +/// Knockout Group writes `/K true` and Isolate Blending `/I true`, each on its own. +#[test] +fn knockout_and_isolation_are_group_attributes() { + let grouped = |knockout: bool, isolate: bool| { + let mut d = Document::new(&NewDocument::default()); + let lid = d.default_layer(); + let child = |d: &mut Document, r: Rect, swatch: &str| { + let mut b = Item::new(ItemId(d.alloc()), lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(r)); + b.fill = Fill::swatch(swatch); + std::sync::Arc::new(b) + }; + let items = vec![ + child(&mut d, Rect::new(36.0, 36.0, 136.0, 136.0), "C=100 M=0 Y=0 K=0"), + child(&mut d, Rect::new(86.0, 86.0, 186.0, 186.0), "C=0 M=100 Y=0 K=0"), + ]; + let mut g = Item::new( + ItemId(d.alloc()), + lid, + designcraft_doc::Shape::Group, + designcraft_geom::shapes::rectangle(Rect::new(36.0, 36.0, 186.0, 186.0)), + ); + g.content = designcraft_doc::Content::Group { items }; + g.knockout = knockout; + g.isolate = isolate; + d.insert_item(SpreadRef::Doc(0), g, None).unwrap(); + form_groups(&exported(&d)) + }; + let attributes = + |groups: Vec<(Option, bool, bool)>| groups.into_iter().map(|(_, i, k)| (i, k)).filter(|ik| *ik != (false, false)).collect::>(); + assert_eq!(attributes(grouped(true, false)), [(false, true)]); + assert_eq!(attributes(grouped(false, true)), [(true, false)]); + assert_eq!(attributes(grouped(true, true)), [(true, true)]); + assert_eq!(attributes(grouped(false, false)), []); +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7de3521c4..9294cc04e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -389,7 +389,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level - Object Layer Options for placed PDFs (`object.pdfLayers`, `object.layerOptions`, Object ▸ Object Layer Options…): the file's optional-content layers listed and shown or hidden per frame on screen; in exported PDFs such graphics go out as 300 ppi images of their visible layers. - Effects: Inner Glow (edge or centre), Bevel and Emboss (inner bevel lit from the light angle), Satin (`object.innerGlow`, `object.bevel`, `object.satin`); PDF export now keeps soft effects: objects with shadows, glows, feathers, bevels or satin go out as 300 ppi transparent images of their appearance (threaded text frames excepted). - Directional Feather (`object.directionalFeather {widths: [top, left, bottom, right]}`): each side fades over its own width. -- View ▸ Flattener Preview (`view.flattenerPreview`): objects involving transparency highlighted in red; knockout groups export as images (PDF output has no knockout groups). +- View ▸ Flattener Preview (`view.flattenerPreview`): objects involving transparency highlighted in red; PDF export writes Knockout Group as a knockout transparency group (`/K true`) and Isolate Blending as `/I true`. - Live captions (`object.caption {live: true}`, Object ▸ Captions ▸ Generate Live Caption): the caption text is re-filled from the source object's metadata after every edit (same undo step). - Style Export Tagging (`style.exportTag`, Paragraph Style Options ▸ Export Tagging): paragraph and character styles choose their HTML element and class in EPUB/HTML. - Tagged Text: export (`file.exportText {format: "tagged"}`) and place (detected by its header; ASCII, UTF-8 or UTF-16) with paragraph and character styles, font, typeface and size, and `<0x…>` characters.