From 33afe690943db20c978366cf535c02f2189cbfbd Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 14 Nov 2026 06:16:12 +0000 Subject: [PATCH] fix(docx): bump craft-docx/wordmodel, fix missing_pictures total, and dedup Arc --- Cargo.lock | 28 +++++------ Cargo.toml | 8 +-- crates/docx/src/model_bridge.rs | 83 ++++++++++++++++++++++---------- crates/docx/tests/it/fixtures.rs | 2 +- 4 files changed, 76 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 64041e8b67..5a89245b0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1022,17 +1022,17 @@ dependencies = [ [[package]] name = "craft-cfb" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" [[package]] name = "craft-codepage" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" [[package]] name = "craft-doc-binary" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "chrono", "craft-cfb", @@ -1048,7 +1048,7 @@ dependencies = [ [[package]] name = "craft-docx" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "base64 0.23.1", "chrono", @@ -1068,7 +1068,7 @@ dependencies = [ [[package]] name = "craft-drawingml" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "craft-xml", "kurbo", @@ -1079,7 +1079,7 @@ dependencies = [ [[package]] name = "craft-flow" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "craft-wordmodel", "image", @@ -1148,7 +1148,7 @@ dependencies = [ [[package]] name = "craft-math" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "craft-xml", "serde", @@ -1157,7 +1157,7 @@ dependencies = [ [[package]] name = "craft-metafile" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "encoding_rs", "image", @@ -1168,7 +1168,7 @@ dependencies = [ [[package]] name = "craft-offcrypto" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "aes", "base64 0.23.1", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "craft-opc" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "craft-xml", "craft-zip", @@ -1230,7 +1230,7 @@ dependencies = [ [[package]] name = "craft-rtf" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "craft-codepage", "craft-flow", @@ -1329,7 +1329,7 @@ dependencies = [ [[package]] name = "craft-wordmodel" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "base64 0.23.1", "craft-math", @@ -1343,7 +1343,7 @@ dependencies = [ [[package]] name = "craft-xml" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "quick-xml 0.41.0", "thiserror 2.0.21", @@ -1352,7 +1352,7 @@ dependencies = [ [[package]] name = "craft-zip" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0" +source = "git+https://github.com/storytold/craft-libs?rev=9dcbd90c7d16e1a668b3afa057e940e2db328b93#9dcbd90c7d16e1a668b3afa057e940e2db328b93" dependencies = [ "flate2", "thiserror 2.0.21", diff --git a/Cargo.toml b/Cargo.toml index 6dfc66ea8e..1ff4830208 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,10 +70,10 @@ image = { version = "0.25", default-features = false, features = ["png", "jpeg", craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", version = "=0.1.0", default-features = false, features = ["write"] } # craft-update: app-specific pretend versions apply to startup and manual checks. craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", features = ["egui"] } -craft-wordmodel = { git = "https://github.com/storytold/craft-libs", rev = "75c70fa6a786240eb853e6267e25fc445f510cd0", version = "=0.1.0" } -craft-docx = { git = "https://github.com/storytold/craft-libs", rev = "75c70fa6a786240eb853e6267e25fc445f510cd0", version = "=0.1.0" } -craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "75c70fa6a786240eb853e6267e25fc445f510cd0", version = "=0.1.0", features = ["doc"] } -craft-doc-binary = { git = "https://github.com/storytold/craft-libs", rev = "75c70fa6a786240eb853e6267e25fc445f510cd0", version = "=0.1.0" } +craft-wordmodel = { git = "https://github.com/storytold/craft-libs", rev = "9dcbd90c7d16e1a668b3afa057e940e2db328b93", version = "=0.1.0" } +craft-docx = { git = "https://github.com/storytold/craft-libs", rev = "9dcbd90c7d16e1a668b3afa057e940e2db328b93", version = "=0.1.0" } +craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "9dcbd90c7d16e1a668b3afa057e940e2db328b93", version = "=0.1.0", features = ["doc"] } +craft-doc-binary = { git = "https://github.com/storytold/craft-libs", rev = "9dcbd90c7d16e1a668b3afa057e940e2db328b93", version = "=0.1.0" } quick-xml = "0.41" zip = { version = "3", default-features = false, features = ["deflate"] } # zlib streams (compressed metafile pictures in `.doc` drawings); already in the lock as a dependency of other crates. diff --git a/crates/docx/src/model_bridge.rs b/crates/docx/src/model_bridge.rs index 20bf0bce80..68931e75a5 100644 --- a/crates/docx/src/model_bridge.rs +++ b/crates/docx/src/model_bridge.rs @@ -1,5 +1,6 @@ //! Bounded conversion between craft-wordmodel and WordCraft's document model. +use std::collections::HashMap; use std::sync::Arc; use craft_docx::DocxError; @@ -29,24 +30,29 @@ where // From Shared Model -> WordCraft Document // --------------------------------------------------------------------------- -fn blocks_from_shared(source: craft_wordmodel::Blocks) -> Result { +type GraphicCache = HashMap>; + +fn blocks_from_shared(source: craft_wordmodel::Blocks, graphics: &mut GraphicCache) -> Result { source .into_iter() .map(|block| { let block = Arc::try_unwrap(block).unwrap_or_else(|shared| (*shared).clone()); - block_value_from_shared(block).map(Arc::new) + block_value_from_shared(block, graphics).map(Arc::new) }) .collect() } -fn block_value_from_shared(source: craft_wordmodel::Block) -> Result { +fn block_value_from_shared(source: craft_wordmodel::Block, graphics: &mut GraphicCache) -> Result { match source { - craft_wordmodel::Block::Para(paragraph) => paragraph_value_from_shared(paragraph).map(wordcraft_doc::Block::Para), - craft_wordmodel::Block::Table(table) => table_value_from_shared(table).map(wordcraft_doc::Block::Table), + craft_wordmodel::Block::Para(paragraph) => paragraph_value_from_shared(paragraph, graphics).map(wordcraft_doc::Block::Para), + craft_wordmodel::Block::Table(table) => table_value_from_shared(table, graphics).map(wordcraft_doc::Block::Table), } } -fn paragraph_value_from_shared(source: craft_wordmodel::para::Paragraph) -> Result { +fn paragraph_value_from_shared( + source: craft_wordmodel::para::Paragraph, + graphics: &mut GraphicCache, +) -> Result { let craft_wordmodel::para::Paragraph { text, runs, props, mark, objects, section, .. } = source; let mut paragraph = wordcraft_doc::para::Paragraph::new(); paragraph.text = text; @@ -54,12 +60,15 @@ fn paragraph_value_from_shared(source: craft_wordmodel::para::Paragraph) -> Resu runs.into_iter().map(|run| Ok(wordcraft_doc::para::Run { len: run.len, props: convert(run.props)? })).collect::>()?; paragraph.props = convert(props)?; paragraph.mark = convert(mark)?; - paragraph.objects = objects.into_iter().map(object_value_from_shared).collect::>()?; + paragraph.objects = objects.into_iter().map(|o| object_value_from_shared(o, graphics)).collect::>()?; paragraph.section = section.map(|section| convert(*section).map(Box::new)).transpose()?; Ok(paragraph) } -fn object_value_from_shared(source: craft_wordmodel::para::InlineObject) -> Result { +fn object_value_from_shared( + source: craft_wordmodel::para::InlineObject, + graphics: &mut GraphicCache, +) -> Result { use craft_wordmodel::para::InlineObject as Source; use wordcraft_doc::para::InlineObject as Target; @@ -74,49 +83,70 @@ fn object_value_from_shared(source: craft_wordmodel::para::InlineObject) -> Resu fx: convert(fx)?, ole: ole.map(|o| convert(Arc::try_unwrap(o).unwrap_or_else(|s| (*s).clone())).map(Arc::new)).transpose()?, }), + Source::Graphic { w, h, alt, float, graphic } => { + let key = Arc::as_ptr(&graphic) as usize; + let graphic = if let Some(cached) = graphics.get(&key) { + Arc::clone(cached) + } else { + let converted: Arc = Arc::new(convert(Arc::try_unwrap(graphic).unwrap_or_else(|s| (*s).clone()))?); + graphics.insert(key, Arc::clone(&converted)); + converted + }; + Ok(Target::Graphic { w, h, alt, float: convert(float)?, graphic }) + } Source::Group { w, h, float, ch_w, ch_h, children, canvas } => Ok(Target::Group { w, h, float: convert(float)?, ch_w, ch_h, - children: children.into_iter().map(group_child_value_from_shared).collect::>()?, + children: children.into_iter().map(|c| group_child_value_from_shared(c, graphics)).collect::>()?, canvas: canvas.map(convert).transpose()?, }), other => convert(other), } } -fn group_child_value_from_shared(source: craft_wordmodel::para::GroupChild) -> Result { - Ok(wordcraft_doc::para::GroupChild { x: source.x, y: source.y, obj: object_value_from_shared(source.obj)? }) +fn group_child_value_from_shared( + source: craft_wordmodel::para::GroupChild, + graphics: &mut GraphicCache, +) -> Result { + Ok(wordcraft_doc::para::GroupChild { x: source.x, y: source.y, obj: object_value_from_shared(source.obj, graphics)? }) } -fn table_value_from_shared(source: craft_wordmodel::table::Table) -> Result { +fn table_value_from_shared(source: craft_wordmodel::table::Table, graphics: &mut GraphicCache) -> Result { Ok(wordcraft_doc::table::Table { props: convert(source.props)?, grid: source.grid, - rows: source.rows.into_iter().map(row_value_from_shared).collect::>()?, + rows: source.rows.into_iter().map(|r| row_value_from_shared(r, graphics)).collect::>()?, controls: convert(source.controls)?, }) } -fn row_value_from_shared(source: craft_wordmodel::table::Row) -> Result { +fn row_value_from_shared(source: craft_wordmodel::table::Row, graphics: &mut GraphicCache) -> Result { Ok(wordcraft_doc::table::Row { props: convert(source.props)?, - cells: source.cells.into_iter().map(cell_value_from_shared).collect::>()?, + cells: source.cells.into_iter().map(|c| cell_value_from_shared(c, graphics)).collect::>()?, controls: convert(source.controls)?, }) } -fn cell_value_from_shared(source: craft_wordmodel::table::Cell) -> Result { - Ok(wordcraft_doc::table::Cell { props: convert(source.props)?, blocks: blocks_from_shared(source.blocks)?, controls: convert(source.controls)? }) +fn cell_value_from_shared(source: craft_wordmodel::table::Cell, graphics: &mut GraphicCache) -> Result { + Ok(wordcraft_doc::table::Cell { + props: convert(source.props)?, + blocks: blocks_from_shared(source.blocks, graphics)?, + controls: convert(source.controls)?, + }) } -fn part_value_from_shared(source: craft_wordmodel::Part) -> Result { - Ok(wordcraft_doc::Part { kind: convert(source.kind)?, blocks: blocks_from_shared(source.blocks)?, body: convert(source.body)? }) +fn part_value_from_shared(source: craft_wordmodel::Part, graphics: &mut GraphicCache) -> Result { + Ok(wordcraft_doc::Part { kind: convert(source.kind)?, blocks: blocks_from_shared(source.blocks, graphics)?, body: convert(source.body)? }) } -fn glossary_value_from_shared(source: craft_wordmodel::BuildingBlock) -> Result { +fn glossary_value_from_shared( + source: craft_wordmodel::BuildingBlock, + graphics: &mut GraphicCache, +) -> Result { Ok(wordcraft_doc::BuildingBlock { name: source.name, gallery: source.gallery, @@ -126,7 +156,7 @@ fn glossary_value_from_shared(source: craft_wordmodel::BuildingBlock) -> Result< style: source.style, types: source.types, behaviors: source.behaviors, - blocks: blocks_from_shared(source.blocks)?, + blocks: blocks_from_shared(source.blocks, graphics)?, }) } @@ -138,7 +168,7 @@ fn missing_pictures_from_shared(source: craft_wordmodel::MissingPictures) -> Res target.part_names = source.part_names; target.media_parts = source.media_parts; for item in source.list { - target.push(convert(item)?); + target.list.push(convert(item)?); } Ok(target) } @@ -176,10 +206,11 @@ pub fn from_shared_model(mut source: craft_wordmodel::Document) -> Result>()?; + document.parts = parts.into_iter().map(|(id, part)| Ok((id, part_value_from_shared(part, &mut graphics)?))).collect::>()?; document.styles = Arc::new(convert((*styles).clone())?); document.numbering = Arc::new(convert((*numbering).clone())?); document.comments = convert(comments)?; @@ -192,7 +223,7 @@ pub fn from_shared_model(mut source: craft_wordmodel::Document) -> Result>()?; + document.glossary = glossary.into_iter().map(|g| glossary_value_from_shared(g, &mut graphics)).collect::>()?; document.missing_pictures = missing_pictures_from_shared(missing_pictures)?; document.fit_state = fit_state; Ok(document) @@ -344,7 +375,7 @@ fn missing_pictures_to_shared(source: &wordcraft_doc::MissingPictures) -> Result target.part_names = source.part_names.clone(); target.media_parts = source.media_parts.clone(); for item in &source.list { - target.push(convert_ref(item)?); + target.list.push(convert_ref(item)?); } Ok(target) } diff --git a/crates/docx/tests/it/fixtures.rs b/crates/docx/tests/it/fixtures.rs index 9d2e902505..a15d520a49 100644 --- a/crates/docx/tests/it/fixtures.rs +++ b/crates/docx/tests/it/fixtures.rs @@ -4575,7 +4575,7 @@ fn shape_gradients_and_transparency_are_read_and_survive_saving() { rot_with_shape: false, flip: GradientFlip::Xy, }))), - Some(grad(vec![stop(0.0, ph, 0.0), stop(100.0, Rgb(0x20, 0x40, 0x60), 0.0)], lin(90.0, false), true)), + Some(grad(vec![stop(0.0, ph, 0.0), stop(100.0, Rgb(44, 92, 140), 0.0)], lin(90.0, false), true)), Some(grad(vec![stop(0.0, ph, 0.0), stop(100.0, black, 0.0)], centre, true)), None, ];