diff --git a/crates/compose/src/cache.rs b/crates/compose/src/cache.rs index 72ef7fff4..585e4ab5c 100644 --- a/crates/compose/src/cache.rs +++ b/crates/compose/src/cache.rs @@ -86,6 +86,15 @@ fn doc_signature(doc: &Document) -> u64 { (Arc::as_ptr(&doc.styles) as usize).hash(&mut h); format!("{:?}", doc.text_variables).hash(&mut h); sections_hash(doc).hash(&mut h); + outlines_hash(doc).hash(&mut h); + h.finish() +} + +/// The Alpha Channel outlines text wraps around: a relink traces a new one while the items and +/// stories stay the same. +fn outlines_hash(doc: &Document) -> u64 { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); for o in &doc.alpha_outlines { o.serial.hash(&mut h); } @@ -409,12 +418,9 @@ fn signature(doc: &Document, story: &Arc) -> (Vec, Vec>) sections_hash(doc) as usize, doc.font_scope as usize, usize::from(g.relative_to == designcraft_doc::GridRelative::TopMargin), + outlines_hash(doc) as usize, ]; sig.extend([g.start, g.increment].into_iter().flat_map(bits)); - // Alpha Channel outlines: a relink traces one again without changing the story or the items - // that wrap it. - sig.extend(doc.alpha_outlines.iter().flat_map(|o| [(o.serial & 0xffff_ffff) as usize, (o.serial >> 32) as usize])); - sig.push(usize::MAX); let mut keep = Vec::new(); let mut spreads = Vec::new(); for f in &story.frames { diff --git a/crates/doc/src/item/text_frame.rs b/crates/doc/src/item/text_frame.rs index f31f14060..25a0ffb61 100644 --- a/crates/doc/src/item/text_frame.rs +++ b/crates/doc/src/item/text_frame.rs @@ -302,19 +302,18 @@ pub struct TextWrap { /// transparent holes it encloses (off: holes are blocked with the shape around them). #[serde(skip_serializing_if = "std::ops::Not::not")] pub inside_edges: bool, - /// Alpha Channel contour: pixels more opaque than this (0..254) count as the image. Saved only - /// when it isn't the default. + /// Alpha Channel contour: pixels more opaque than this (0..254) count as the image. #[serde(skip_serializing_if = "is_default_alpha_threshold")] pub alpha_threshold: u8, } +/// DesignCraft's default alpha threshold for Alpha Channel contour wraps. +pub const DEFAULT_ALPHA_THRESHOLD: u8 = 25; + fn is_default_alpha_threshold(t: &u8) -> bool { *t == DEFAULT_ALPHA_THRESHOLD } -/// DesignCraft's default alpha threshold for Alpha Channel contour wraps. -pub const DEFAULT_ALPHA_THRESHOLD: u8 = 25; - impl Default for TextWrap { fn default() -> Self { TextWrap { @@ -349,22 +348,3 @@ pub struct TextFrame { #[serde(default)] pub options: TextFrameOptions, } - -#[cfg(test)] -mod tests { - use super::*; - - /// The alpha threshold is saved only when it isn't the default, so a wrap saved before it - /// existed is saved the same way, and reads back as the default. - #[test] - fn the_default_alpha_threshold_is_not_saved() { - let wrap = TextWrap { mode: WrapMode::Contour, ..Default::default() }; - let v = serde_json::to_value(&wrap).unwrap(); - assert!(v.get("alphaThreshold").is_none(), "{v}"); - assert_eq!(serde_json::from_value::(v).unwrap(), wrap); - let set = TextWrap { alpha_threshold: 200, ..wrap }; - let v = serde_json::to_value(&set).unwrap(); - assert_eq!(v["alphaThreshold"], 200); - assert_eq!(serde_json::from_value::(v).unwrap(), set); - } -} diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index f26ef10ad..2b18c8f47 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -674,14 +674,13 @@ pub struct AlphaOutline { /// under the same id, and the outline is traced again. (Weak, so it doesn't keep old image /// data alive, and its address can't be reused by new data while it's held.) pub source: std::sync::Weak>, - /// A number no other outline made in this process has ([`AlphaOutline::next_serial`]). - /// Composition caches compare it: an outline traced again after a relink has a new one, - /// while a dropped outline's address can be reused. + /// A number no other outline traced in this process has ([`AlphaOutline::next_serial`]): + /// composition caches compare it to tell a new outline from the one they used. pub serial: u64, } impl AlphaOutline { - /// A serial number not given out before in this process. + /// A serial number for a new outline, unique within the process. pub fn next_serial() -> u64 { static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) diff --git a/crates/engine/src/cmd/alpha_wrap.rs b/crates/engine/src/cmd/alpha_wrap.rs index 1ee2919a1..585f29350 100644 --- a/crates/engine/src/cmd/alpha_wrap.rs +++ b/crates/engine/src/cmd/alpha_wrap.rs @@ -438,6 +438,40 @@ mod tests { assert_eq!((b.x0, b.x1), (24.0, 40.0), "the new image's outline"); } + /// A wrap with the default threshold saves as it did before thresholds were stored; another + /// threshold is saved. + #[test] + fn the_default_threshold_is_left_out_of_a_saved_wrap() { + let v = serde_json::to_value(designcraft_doc::TextWrap::default()).unwrap(); + assert!(v.get("alphaThreshold").is_none(), "{v}"); + let back: designcraft_doc::TextWrap = serde_json::from_value(v).unwrap(); + assert_eq!(back.alpha_threshold, designcraft_doc::DEFAULT_ALPHA_THRESHOLD); + let w = designcraft_doc::TextWrap { alpha_threshold: 200, ..Default::default() }; + assert_eq!(serde_json::to_value(w).unwrap()["alphaThreshold"], 200); + } + + /// A relink that changes only the image data recomposes the text around the new outline. + #[test] + fn text_is_composed_again_around_a_relinked_images_outline() { + let (mut s, image, story) = page(&png(|x, _| x < 32), [180.0, 150.0, 220.0, 178.0]); + s.execute("object.textWrap", &json!({"ids": [image], "contour": "alphaChannel"})).unwrap(); + assert!(!overset(&s, story), "the left half is visible: the caption on the right is set"); + let Some(designcraft_doc::Content::Graphic(g)) = + s.doc().unwrap().doc.item(designcraft_doc::ItemId(image.as_u64().unwrap())).map(|it| it.content.clone()) + else { + panic!("a graphic") + }; + let right = Arc::new(png(|x, _| x >= 32)); + s.edit(|d, _| { + let mut a = (**d.assets.get(&g.asset).unwrap()).clone(); + a.data = right.clone(); + d.assets.insert(g.asset, Arc::new(a)); + Ok(()) + }) + .unwrap(); + assert!(overset(&s, story), "the right half is visible now: it covers the caption"); + } + /// An image anchored in text keeps its outline: anchored objects wrap the lines after their /// anchor, and they are kept in the story, not on the spread. #[test] @@ -512,28 +546,4 @@ mod tests { assert!(s.execute("object.textWrap", &bad).is_err()); } } - - /// Replacing the image data under the same asset, as a relink or link update does, lays the - /// text out again around the new outline: neither the story nor the frames change. - #[test] - fn a_relinked_image_lays_the_text_out_again() { - let (mut s, image, story) = page(&png(|x, _| (8..20).contains(&x) || (44..56).contains(&x)), [150.0, 120.0, 178.0, 170.0]); - s.execute("object.textWrap", &json!({"ids": [image], "contour": "alphaChannel"})).unwrap(); - assert!(!overset(&s, story), "the gap between the legs stays open"); - let Some(designcraft_doc::Content::Graphic(g)) = - s.doc().unwrap().doc.item(designcraft_doc::ItemId(image.as_u64().unwrap())).map(|it| it.content.clone()) - else { - panic!("a graphic") - }; - // The new image has no gap: one band across both legs. - let solid = Arc::new(png(|x, _| (8..56).contains(&x))); - s.edit(|d, _| { - let mut a = (**d.assets.get(&g.asset).unwrap()).clone(); - a.data = solid.clone(); - d.assets.insert(g.asset, Arc::new(a)); - Ok(()) - }) - .unwrap(); - assert!(overset(&s, story), "the new image covers the caption"); - } } diff --git a/docs/typography-parity.md b/docs/typography-parity.md index 599c20b39..0b3c030cf 100644 --- a/docs/typography-parity.md +++ b/docs/typography-parity.md @@ -59,7 +59,7 @@ Fidelity: **unmeasured** unless stated. | Paragraph rules, borders, shading | ✓ | ✓ | ✓ | | | Balance ragged lines | ✓ | ✓ | ✓ | | | Optical margin alignment | per story with a size, Ignore Optical Margin per paragraph and style | ✓ (hang table of our own, scaled by the story's size; Story panel; IDML `StoryPreference`, `IgnoreEdgeAlignment`) | ~ | hang amounts are ours, InDesign's unknown (#947) | -| Text wrap | bounding box, shape, jump object/column, wrap sides (text on both sides of an object), contour options, on page items and anchored objects | ✓ | ~ | Alpha Channel contours follow the image's visible pixels within the frame (threshold, Include Inside Edges); Detect Edges and Photoshop Path contours follow the frame's path (composition doesn't read the image) | +| Text wrap | bounding box, shape, jump object/column, wrap sides (text on both sides of an object), contour options, on page items and anchored objects | ✓ | ~ | Alpha Channel contours follow the outline of the image's visible pixels; Detect Edges and Photoshop Path contours follow the frame's path | | Anchored objects | inline, above line, custom | ✓ | ~ | anchored objects in table cells not drawn (#163) | | Footnotes, endnotes | ✓ | ✓ | ~ | footnotes don't split across columns yet | | Type on a path | ✓ | ✓ | ✓ | |