mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 12:27:11 -06:00
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Brandon Thomas
parent
e347ef203e
commit
7fbcd0643a
@@ -24,6 +24,7 @@ pub mod rt {
|
||||
pub const NUMBERING: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
|
||||
pub const SETTINGS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
|
||||
pub const ATTACHED_TEMPLATE: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/attachedTemplate";
|
||||
pub const FONT_TABLE: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable";
|
||||
pub const THEME: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
|
||||
pub const FOOTNOTES: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
|
||||
pub const ENDNOTES: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//! reaches through relationships (chart, embedded workbook, colours and style; diagram data, layout,
|
||||
//! quick style, colours and drawing; OLE embedding and its picture…) as opaque bytes in
|
||||
//! `Document::passthrough`, with their content types and relationships in an
|
||||
//! [`EmbeddedManifest`](crate::package::EmbeddedManifest).
|
||||
//! [`EmbeddedManifest`](crate::package::EmbeddedManifest). The font table and the fonts it embeds
|
||||
//! are kept the same way.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -73,7 +74,7 @@ impl Reader<'_> {
|
||||
|
||||
/// Keep part `root` and every part it reaches through its relationships. False when `root`
|
||||
/// itself can't be kept.
|
||||
fn keep_part_tree(&mut self, root: &str) -> bool {
|
||||
pub(super) fn keep_part_tree(&mut self, root: &str) -> bool {
|
||||
if self.embedded.parts.contains_key(root) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -161,6 +161,10 @@ pub(crate) fn read_opened(pkg: &Package) -> Result<Document, DocxError> {
|
||||
r.pc.minor_font = r.doc.settings.minor_font.clone();
|
||||
let part = |t: &str| rels.by_type(t).filter(|x| !x.external).map(|x| x.target.clone());
|
||||
|
||||
// The font table and the fonts embedded through it ride along as opaque parts.
|
||||
if let Some(p) = part(rt::FONT_TABLE) {
|
||||
r.keep_part_tree(&p);
|
||||
}
|
||||
if let Some(p) = part(rt::THEME) {
|
||||
r.read_theme(&p);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ const PART_COST: u64 = 4096;
|
||||
/// Where the stories' parts live: every story this writer makes is a part in `word/`.
|
||||
const STORY_PART: &str = "word/document.xml";
|
||||
|
||||
/// The font table kept from a file (read by `Reader::keep_part_tree`).
|
||||
const FONT_TABLE_PART: &str = "word/fontTable.xml";
|
||||
|
||||
/// The embedded-object parts of a package being written.
|
||||
pub(crate) struct EmbedWriter<'d> {
|
||||
manifest: EmbeddedManifest,
|
||||
@@ -90,6 +93,14 @@ impl<'d> EmbedWriter<'d> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the font table kept from a file, with the fonts it embeds, relating it from `rels`.
|
||||
pub fn font_table(&mut self, rels: &mut PartRels) {
|
||||
let mut written = Vec::new();
|
||||
if let Some(name) = self.copy_tree(FONT_TABLE_PART, &mut HashMap::new(), &mut written) {
|
||||
rels.add(rt::FONT_TABLE, &relative(STORY_PART, &name), false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reserve the names of the media files this writer writes.
|
||||
pub fn reserve_media<'a>(&mut self, files: impl Iterator<Item = &'a String>) {
|
||||
for f in files {
|
||||
|
||||
@@ -368,6 +368,8 @@ pub(crate) fn write_parts(doc: &Document, flavor: Flavor) -> Result<Vec<(String,
|
||||
PartRels::default(),
|
||||
);
|
||||
|
||||
wr.embeds.font_table(&mut rels);
|
||||
|
||||
// Media referenced by some drawing (orphan parts would make the package suspect).
|
||||
let mut media_types: BTreeMap<String, &str> = BTreeMap::new();
|
||||
for (key, file) in &wr.media_files {
|
||||
|
||||
@@ -1514,6 +1514,33 @@ fn vml_lines_and_rectangles() {
|
||||
assert!(matches!(&objs[1], InlineObject::Shape { kind: ShapeKind::Rectangle, fill: Some(Rgb(255, 0, 0)), stroke: None, w: 30.0, h: 20.0, .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_fonts_survive_a_round_trip() {
|
||||
let table = r#"<w:fonts xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:font w:name="Roboto"><w:embedRegular r:id="rId1"/><w:embedBold r:id="rId2"/></w:font></w:fonts>"#;
|
||||
let table_rels = rels(&[("rId1", "font", "fonts/Roboto-regular.ttf"), ("rId2", "font", "fonts/Roboto-bold.ttf")]);
|
||||
let bytes = docx(
|
||||
"<w:p><w:r><w:t>Hi</w:t></w:r></w:p>",
|
||||
&[("rId9", "fontTable", "fontTable.xml")],
|
||||
&[
|
||||
("word/fontTable.xml", table),
|
||||
("word/_rels/fontTable.xml.rels", &table_rels),
|
||||
("word/fonts/Roboto-regular.ttf", "regular font bytes"),
|
||||
("word/fonts/Roboto-bold.ttf", "bold font bytes"),
|
||||
],
|
||||
);
|
||||
let out = wordcraft_docx::write(&wordcraft_docx::read(&bytes).unwrap()).unwrap();
|
||||
let files = unzip(&out);
|
||||
assert_eq!(text(&files, "word/fontTable.xml"), table);
|
||||
assert_eq!(text(&files, "word/fonts/Roboto-regular.ttf"), "regular font bytes");
|
||||
assert_eq!(text(&files, "word/fonts/Roboto-bold.ttf"), "bold font bytes");
|
||||
let table_rels = text(&files, "word/_rels/fontTable.xml.rels");
|
||||
assert!(table_rels.contains(r#"Id="rId1""#) && table_rels.contains("fonts/Roboto-regular.ttf"), "{table_rels}");
|
||||
assert!(text(&files, "word/_rels/document.xml.rels").contains("fontTable.xml"));
|
||||
// A second save keeps them too.
|
||||
let again = unzip(&wordcraft_docx::write(&wordcraft_docx::read(&out).unwrap()).unwrap());
|
||||
assert_eq!(text(&again, "word/fonts/Roboto-bold.ttf"), "bold font bytes");
|
||||
}
|
||||
|
||||
// ---- native Word ink: opaque preservation, never re-encoded as WordCraft freeforms ----
|
||||
|
||||
const INK_NS: &str = "http://schemas.microsoft.com/office/word/2010/wordprocessingInk";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** trivial (formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (font table and embedded fonts kept on save, #770; previously trivial: native save temporary-file ownership and failure protection; previously trivial: Print Markup: comments in PDF/print/copies, #589; previously trivial: SmartArt written from WordCraft's model, #497; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats
|
||||
|
||||
Every format Word opens or saves, what WordCraft does with it, and how it's tested. Fidelity is the
|
||||
share of a typical real-world file's content and formatting that survives (read: shows correctly;
|
||||
@@ -70,7 +70,7 @@ Measured by grepping `crates/docx/src/read` and `src/write` for the OOXML elemen
|
||||
| Native Word ink (`w14:contentPart`, InkML) | 🟡 | 🟡 | Drawing-level ink kept opaque with its InkML part, relationship and content type; run-level `mc:AlternateContent` keeps the native branch and VML picture fallback. Shows the preview or a missing-picture box. Repeated saves, copies, resize, rotation and namespace aliases tested with original fixtures. No native stroke rendering, eraser, conversion or replay; direct run-level content parts and ink nested in groups/canvases remain unsupported |
|
||||
| 3D models | ❌ | ❌ | Dropped on read, not preserved on save |
|
||||
| Equations (OMML `m:oMath`) | ✅ | ✅ | #191; manual breaks (`m:brk`) and the settings' `m:mathPr` wrapping options (#326) |
|
||||
| Themes, font table, settings, compatibility mode | ✅ | ✅ | Embedded fonts (`w:embedRegular`) not read; attached template (`w:attachedTemplate` and its external relationship) and `w:linkStyles` round-trip (#377). The template path comes from the file, so opening never reads it: styles update from it only on an explicit Update Styles |
|
||||
| Themes, font table, settings, compatibility mode | ✅ | ✅ | Embedded fonts (`w:embedRegular`) not used for rendering, but the font table, its relationships and the embedded font parts are written back unchanged (#770); attached template (`w:attachedTemplate` and its external relationship) and `w:linkStyles` round-trip (#377). The template path comes from the file, so opening never reads it: styles update from it only on an explicit Update Styles |
|
||||
| Custom XML parts, document properties, bibliography sources | 🟡 | 🟡 | Custom XML stores reached from the package/main document preserve opaque data, properties and relationships; binding evaluation remains absent. Custom properties round-trip; bibliography model I/O remains separate (#532 open) |
|
||||
| Section text direction (`w:textDirection` in `w:sectPr`, §17.6.20) | ✅ | ✅ | `tbRl`, `btLr` (and `tbRlV`/`tbLrV`, read as `tbRl`); `lrTbV` and unknown values read as horizontal (#491) |
|
||||
| Ruby (`w:ruby`: `w:rubyPr`, `w:rt`, `w:rubyBase`) | ✅ | ✅ | #288; base and ruby text with their run formatting, alignment, sizes, raise, language |
|
||||
@@ -189,6 +189,7 @@ raise readiness estimates. No Word-derived files are fixtures.
|
||||
| 2026-10-10 | trivial | Right-to-left tables (`w:bidiVisual`) read and written (#362) |
|
||||
| 2026-10-10 | trivial | Drawing canvas (`wpc:wpc`) read and written (#344); group shapes row brought up to date (#267) |
|
||||
| 2026-10-11 | trivial | Spreadsheets (.xlsx/.xlsm/.ods) read as mail-merge recipient lists (#334) |
|
||||
| 2026-10-12 | trivial | Font table (`word/fontTable.xml`), its relationships and embedded fonts (`word/fonts/*`) written back unchanged (#770) |
|
||||
| 2026-10-12 | minor | Preserve opaque custom XML stores and related properties; bindings and control support remain separate |
|
||||
| 2026-10-11 | trivial | Native saves protect existing temporary-name entries and originals on pre-replacement failure; synthetic macOS evidence, explicit durability/platform limits; readiness unchanged |
|
||||
| 2026-10-11 | minor | Preserve imported TOC ranges independently of paragraph styles, with explicit evaluation limits |
|
||||
|
||||
Reference in New Issue
Block a user