diff --git a/ROADMAP.md b/ROADMAP.md index 976e432b95..ddf112c76a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -140,7 +140,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm ## Progress log -- 2026-11-02: ODT files follow their standard wherever it says something, and a setting wherever it doesn't (gap 19). File › Options › Advanced › OpenDocument files (and `file.options` `odf`) chooses WordCraft's own behaviour (the default), LibreOffice's, or custom values for each of eleven cases the standard leaves open: header heights and spacing, column gaps, shape colours and hairlines, where tab stops are measured from, frame relations the standard doesn't define, and lines of only pictures. A page layout that states no margins now has XSL's 0, as the standard says. Every case is listed in docs/odf-interpretation.md; Google Docs and ONLYOFFICE files checked, and neither needs a preset of its own yet. +- 2026-11-02: ODT files follow their standard wherever it says something, and a setting wherever it doesn't (gap 19). File › Options › Advanced › OpenDocument files (and `file.options` `odf`) chooses WordCraft's own behaviour (the default), LibreOffice's, or custom values for each of twelve cases the standard leaves open: header heights and spacing, column gaps, shape colours and hairlines, where tab stops are measured from, frame relations the standard doesn't define, and lines of only pictures. A page layout that states no margins follows the setting too: 0 for WordCraft (XSL's initial value), 2 cm for LibreOffice. Every case is listed in docs/odf-interpretation.md; Google Docs and ONLYOFFICE files checked, and neither needs a preset of its own yet. - 2026-11-02: ODT form controls keep their place on the line and multi-line text fields their lines; custom shapes keep their own geometry (LibreOffice draws its own shape after a save). In-line pictures placed against the text are raised from its size, and saved standing on the baseline. The corpus round-trip difference falls to 41k pixels, 90% below this morning's 408k; 42 of 125 documents round-trip pixel-identical (gap 19). Readiness unchanged. - 2026-11-02: ODT charts keep their whole look through a save: the chart object as read is written back while the chart is unchanged (edited, it's written from WordCraft's chart). Frames outlined by a drawn stroke keep it, and a header with no stated height is LibreOffice's 0.5 cm. The corpus round-trip difference falls to 47k pixels (408k this morning); 41 of 125 documents round-trip pixel-identical (gap 19). Readiness unchanged. - 2026-11-02: ODT: saving no longer makes a table's first row repeat on every page (a table style's header row had been taken for Repeat as header row), so LibreOffice's 16 agenda templates round-trip with their text the same; 91 of the 125 corpus documents now do, and the round-trip difference falls to 60k pixels (408k this morning) (gap 19). Readiness unchanged. diff --git a/crates/doc/src/odf_profile.rs b/crates/doc/src/odf_profile.rs index 802846f165..0e11e0f576 100644 --- a/crates/doc/src/odf_profile.rs +++ b/crates/doc/src/odf_profile.rs @@ -51,6 +51,10 @@ pub const MAX_LENGTH: f32 = 1584.0; #[serde(default, rename_all = "camelCase")] pub struct OdfProfile { pub preset: OdfPreset, + /// A page margin a page layout doesn't state, points. §20.206–20.209 refer to XSL's margins + /// (§7.10), whose initial value is 0; whether that initial value comes with the reference + /// the standard doesn't say (LibreOffice uses 2 cm). + pub page_margin: f32, /// The height of a header or footer whose style states none (§20.212.2 `fo:min-height` /// has no default), points. pub header_min_height: f32, @@ -100,6 +104,7 @@ impl OdfProfile { pub fn wordcraft() -> OdfProfile { OdfProfile { preset: OdfPreset::WordCraft, + page_margin: 0.0, header_min_height: 0.0, header_height_includes_spacing: false, dynamic_spacing: false, @@ -118,6 +123,7 @@ impl OdfProfile { pub fn libreoffice() -> OdfProfile { OdfProfile { preset: OdfPreset::LibreOffice, + page_margin: 2.0 * 72.0 / 2.54, header_min_height: 0.5 * 72.0 / 2.54, header_height_includes_spacing: true, dynamic_spacing: false, @@ -146,6 +152,7 @@ impl OdfProfile { let length = |v: f32, least: f32| if v.is_finite() { v.clamp(least, MAX_LENGTH) } else { least }; match self.preset { OdfPreset::Custom => OdfProfile { + page_margin: length(self.page_margin, 0.0), header_min_height: length(self.header_min_height, 0.0), column_gap: length(self.column_gap, 0.0), hairline: length(self.hairline, 0.05), diff --git a/crates/engine/src/cmd/options.rs b/crates/engine/src/cmd/options.rs index 88de5c5882..f61c28c218 100644 --- a/crates/engine/src/cmd/options.rs +++ b/crates/engine/src/cmd/options.rs @@ -23,7 +23,7 @@ pub fn specs() -> Vec { vec![ CommandSpec::new("file.options", "Options", "File", options) .params( - r#"{"pane"?: "general|display|proofing|save|language|accessibility|advanced|agents", "author"?: string, "initials"?: string, "units"?: "inches|centimeters|millimeters|points|picas|auto" (auto: the region's), "region"?: string (a locale tag like pl-PL, saved: the paper File › New starts on, the default units and the decimal separator; auto: the system's), "typingReplacesSelection"?: bool, "insertKeyOvertype"?: bool, "overtype"?: bool, "marks"?: {"tabs"?: bool, "spaces"?: bool, "paragraphs"?: bool, "hidden"?: bool}, "checkSpelling"?: bool, "markGrammar"?: bool, "ignoreUppercase"?: bool, "ignoreNumbers"?: bool, "ignoreInternet"?: bool, "dictionariesFolder"?: string (a folder of Hunspell .aff/.dic files searched first, saved; auto: the system locations only), "userTemplatesFolder"?: string (the folder File › New lists personal templates from, saved; auto: the app's own Templates folder; not a network folder), "addWords"?: [string], "removeWords"?: [string], "odf"?: {"preset"?: "wordcraft|libreoffice|custom", "headerMinHeight"?: pt, "headerHeightIncludesSpacing"?: bool, "dynamicSpacing"?: bool, "tabsFromIndent"?: bool, "columnGap"?: pt, "shapeFill"?: "RRGGBB", "shapeLine"?: "RRGGBB", "hairline"?: pt, "pageFrameParagraphIsPage"?: bool, "inlineInvalidRelationIsBaseline"?: bool, "pictureLinesFitPictures"?: bool} (how OpenDocument files opened from now on fill in what their standard leaves open, docs/odf-interpretation.md; a value without a preset makes it custom)} — with any setting it changes them and opens nothing; without, it opens File › Options (at `pane`). Returns the settings."#, + r#"{"pane"?: "general|display|proofing|save|language|accessibility|advanced|agents", "author"?: string, "initials"?: string, "units"?: "inches|centimeters|millimeters|points|picas|auto" (auto: the region's), "region"?: string (a locale tag like pl-PL, saved: the paper File › New starts on, the default units and the decimal separator; auto: the system's), "typingReplacesSelection"?: bool, "insertKeyOvertype"?: bool, "overtype"?: bool, "marks"?: {"tabs"?: bool, "spaces"?: bool, "paragraphs"?: bool, "hidden"?: bool}, "checkSpelling"?: bool, "markGrammar"?: bool, "ignoreUppercase"?: bool, "ignoreNumbers"?: bool, "ignoreInternet"?: bool, "dictionariesFolder"?: string (a folder of Hunspell .aff/.dic files searched first, saved; auto: the system locations only), "userTemplatesFolder"?: string (the folder File › New lists personal templates from, saved; auto: the app's own Templates folder; not a network folder), "addWords"?: [string], "removeWords"?: [string], "odf"?: {"preset"?: "wordcraft|libreoffice|custom", "pageMargin"?: pt, "headerMinHeight"?: pt, "headerHeightIncludesSpacing"?: bool, "dynamicSpacing"?: bool, "tabsFromIndent"?: bool, "columnGap"?: pt, "shapeFill"?: "RRGGBB", "shapeLine"?: "RRGGBB", "hairline"?: pt, "pageFrameParagraphIsPage"?: bool, "inlineInvalidRelationIsBaseline"?: bool, "pictureLinesFitPictures"?: bool} (how OpenDocument files opened from now on fill in what their standard leaves open, docs/odf-interpretation.md; a value without a preset makes it custom)} — with any setting it changes them and opens nothing; without, it opens File › Options (at `pane`). Returns the settings."#, ) .pure(), CommandSpec::new("text.overtype", "Overtype", "File › Options › Advanced", |s, v| { diff --git a/crates/formats/src/odt/import.rs b/crates/formats/src/odt/import.rs index 27fbae352d..2e1f6323c9 100644 --- a/crates/formats/src/odt/import.rs +++ b/crates/formats/src/odt/import.rs @@ -474,10 +474,6 @@ fn hyphenation(blocks: &mut [FBlock]) -> bool { /// The content of a package that has none: an empty text. const EMPTY_CONTENT: &str = r#""#; -/// The margin a page layout leaves unsaid: XSL's initial value, 0 (ODF 1.3 §20.206–20.209 are -/// XSL's §7.10 margins; LibreOffice's own pages have 2 cm, but always say so). -const DEFAULT_MARGIN: f32 = 0.0; - /// The page size of a page layout that states none: WordCraft's default page's. fn default_paper() -> (f32, f32) { let d = SectionProps::default(); @@ -508,16 +504,16 @@ fn parse_tracked(bytes: &[u8], paper: (f32, f32), profile: &OdfProfile) -> Resul styles.read(s, false); pages = read_pages(s, paper, &profile); } - // No page layout at all (no styles.xml, or no master page): the default paper, with XSL's - // margins (0). + // No page layout at all (no styles.xml, or no master page): the default paper, with the + // profile's margins. if pages.first.is_none() { pages.first = Some(SectionProps { page_w: paper.0, page_h: paper.1, - margin_top: DEFAULT_MARGIN, - margin_bottom: DEFAULT_MARGIN, - margin_left: DEFAULT_MARGIN, - margin_right: DEFAULT_MARGIN, + margin_top: profile.page_margin, + margin_bottom: profile.page_margin, + margin_left: profile.page_margin, + margin_right: profile.page_margin, ..SectionProps::default() }); } @@ -923,14 +919,15 @@ fn read_pages(xml: &[u8], paper: (f32, f32), profile: &OdfProfile) -> Pages { let mut pages = Pages { doc, next: nexts, ..Default::default() }; for (name, layout, header, footer) in masters { let Some(Layout { attrs: a, columns: cols, column_specs, header: hb, footer: fb, image }) = layouts.get(&layout) else { continue }; - // What a layout leaves unsaid: the default paper, and XSL's margins (0). + // What a layout leaves unsaid: the default paper, and the profile's margins (§20.206–20.209 + // refer to XSL's margins; whether XSL's initial 0 comes with them the standard doesn't say). let mut sec = SectionProps { page_w: paper.0, page_h: paper.1, - margin_top: DEFAULT_MARGIN, - margin_bottom: DEFAULT_MARGIN, - margin_left: DEFAULT_MARGIN, - margin_right: DEFAULT_MARGIN, + margin_top: profile.page_margin, + margin_bottom: profile.page_margin, + margin_left: profile.page_margin, + margin_right: profile.page_margin, ..SectionProps::default() }; apply_layout(a, &mut sec); diff --git a/crates/formats/src/odt/tests_more/page_setup.rs b/crates/formats/src/odt/tests_more/page_setup.rs index 5b34d0a7d0..dae50ed477 100644 --- a/crates/formats/src/odt/tests_more/page_setup.rs +++ b/crates/formats/src/odt/tests_more/page_setup.rs @@ -143,10 +143,10 @@ fn a_page_layout_without_a_size_is_the_default_pages_size() { } #[test] -fn a_document_without_a_page_layout_has_the_default_paper_and_xsl_margins() { +fn a_document_without_a_page_layout_has_the_default_paper_and_the_profiles_margins() { // No styles.xml at all: the region's paper (the standard leaves the size to the - // application), and XSL's margins, 0: the standard's (LibreOffice's own pages have 2 cm, but - // always say so). It is laid out as an ODF document. + // application), and the profile's margins (the standard leaves them open too): WordCraft's + // 0, XSL's initial value; LibreOffice's 2 cm. It is laid out as an ODF document. let content = format!( "x", crate::odt::tests::NS_T @@ -157,12 +157,17 @@ fn a_document_without_a_page_layout_has_the_default_paper_and_xsl_margins() { assert_eq!((s.page_w, s.page_h), (595.3, 841.9)); assert_eq!([s.margin_top, s.margin_bottom, s.margin_left, s.margin_right], [0.0; 4], "{s:?}"); assert!(d.settings.odf.is_some()); + let lo = crate::import_with("odt", &crate::odt::tests::package(&content, None), &a4, &wordcraft_doc::odf_profile::OdfProfile::libreoffice()) + .unwrap() + .unwrap(); + let cm2 = 2.0 * 72.0 / 2.54; + assert_eq!([lo.last_section.margin_top, lo.last_section.margin_left], [cm2; 2]); } #[test] -fn a_page_layout_stating_nothing_has_xsl_margins() { - // LibreOffice's CV template: a page layout with no properties at all. Its margins are XSL's - // initial value, 0 (the standard's, whatever the profile; LibreOffice draws 2 cm). +fn a_page_layout_stating_nothing_has_the_profiles_margins() { + // LibreOffice's CV template: a page layout with no properties at all. Its margins are the + // profile's: WordCraft's 0 (XSL's initial value), LibreOffice's 2 cm. let ns = crate::odt::tests::NS_T; let styles = format!( r#""# @@ -176,6 +181,15 @@ fn a_page_layout_stating_nothing_has_xsl_margins() { let one = styles.replace("", r#""#); let d = crate::import("odt", &crate::odt::tests::package(&content, Some(&one))).unwrap().unwrap(); assert_eq!((d.last_section.margin_left, d.last_section.margin_top), (72.0, 0.0)); + let lo = |styles: &str| { + crate::odt::tests::import_as(&crate::odt::tests::package(&content, Some(styles)), &wordcraft_doc::odf_profile::OdfProfile::libreoffice()) + .last_section + }; + let cm2 = 2.0 * 72.0 / 2.54; + let s = lo(&styles); + assert_eq!([s.margin_top, s.margin_bottom, s.margin_left, s.margin_right], [cm2; 4]); + let s = lo(&one); + assert_eq!((s.margin_left, s.margin_top), (72.0, cm2), "the stated one is the file's under every preset"); } #[test] diff --git a/crates/formats/src/odt/tests_more/profile.rs b/crates/formats/src/odt/tests_more/profile.rs index e19484992d..3885c9e28f 100644 --- a/crates/formats/src/odt/tests_more/profile.rs +++ b/crates/formats/src/odt/tests_more/profile.rs @@ -39,6 +39,7 @@ fn values_a_file_states_are_kept_under_every_preset() { let bytes = z.finish().unwrap().into_inner(); let odd = OdfProfile { preset: OdfPreset::Custom, + page_margin: 99.0, header_min_height: 99.0, dynamic_spacing: true, tabs_from_indent: true, @@ -54,6 +55,7 @@ fn values_a_file_states_are_kept_under_every_preset() { let s = &d.last_section; assert!(near(s.header_gap, cm(0.5)), "{:?}: the stated spacing, kept: {}", profile.preset, s.header_gap); assert!(s.margin_top >= cm(3.0) - 0.05, "{:?}: at least the stated height under the margin: {}", profile.preset, s.margin_top); + assert!(near(s.margin_left, cm(2.0)) && near(s.margin_bottom, cm(2.0)), "{:?}: the stated margins", profile.preset); assert_eq!(s.columns.count, 2); assert!(near(s.columns.space, cm(1.0)), "{:?}: {}", profile.preset, s.columns.space); let p = d.body[0].as_para().unwrap(); diff --git a/crates/ui-egui/src/options.rs b/crates/ui-egui/src/options.rs index 76d68e69e7..b6bb8c26eb 100644 --- a/crates/ui-egui/src/options.rs +++ b/crates/ui-egui/src/options.rs @@ -652,9 +652,11 @@ fn odf_files(app: &mut WordApp, ui: &mut Ui) { let u = unit(); let k = u.pt_per_unit(); egui::Grid::new("options_odf").num_columns(2).spacing(vec2(8.0, 6.0)).show(ui, |ui| { - for (key, label, pt) in - [("headerMinHeight", "Header and footer height:", e.header_min_height), ("columnGap", "Gap between columns:", e.column_gap)] - { + for (key, label, pt) in [ + ("pageMargin", "Page margins:", e.page_margin), + ("headerMinHeight", "Header and footer height:", e.header_min_height), + ("columnGap", "Gap between columns:", e.column_gap), + ] { let id = ui.label(tl!(label)).id; let mut v = pt / k; if ui.add(egui::DragValue::new(&mut v).speed(0.5 / k).range(0.0..=MAX_LENGTH / k).measure(u).max_decimals(2)).labelled_by(id).changed() { diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index bf2cf4a923..399a2e383d 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity with Microsoft Word -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-02 · **Change:** trivial (ODT interpretation profile: the standard is law, what it leaves open follows WordCraft, LibreOffice or custom settings; unstated margins are XSL's 0); previously trivial (ODT form controls keep their place on the line and text areas their lines; custom shapes keep their geometry); previously trivial (ODT in-line frames placed against the text; in-line pictures saved standing on the baseline); previously trivial (ODT charts written back as they were while unchanged; frames' drawn outline; default header height); previously trivial (ODT header rows only where they repeat); previously trivial (ODT padding without a border; unequal columns; column breaks); previously trivial (ODT lines of only pictures laid out as LibreOffice does; default page without a layout); previously trivial (ODT pictures alone in a paragraph keep its size; in-line frames raised under an inherited paragraph relation); previously trivial (ODT tables fit the margins and keep their space above and below; header spacing kept); previously trivial (ODT frames anchored and placed by their style); previously trivial (ODT default font size saved; page-anchored frames placed from the page; text box paragraph styles kept; empty paragraphs keep their size); previously trivial (ODT default font size; locale-ordered dates); previously trivial (ODT page background pictures); previously trivial (ODT tables fixed layout; tables starting a page); previously trivial (emphasis marks: model, layout, DOCX `w:em`, ODT `style:text-emphasize`, `format.emphasisMark`); previously trivial (ODT variables as SET/REF fields; character styles keep what they turn off); previously trivial (ODT styles keep what they turn off; WMF/EMF pictures kept; pictures in text boxes shown by LibreOffice); previously trivial (ODT unstated page margins are LibreOffice's 2 cm; drawings outside a paragraph join the next one); previously trivial (ODT placeholders as content controls, template dates, user name fields, unsized pages, style-only templates, found with LibreOffice's own templates); previously trivial (ODT hidden form controls kept; `.ott` templates opened and saved); previously trivial (ODT fidelity re-estimated at ~90%; a kept form control made another kind is saved as that kind); previously trivial (ODT form control sizes and buttons kept); previously trivial (ODT turned text in cells and text boxes); previously trivial (ODT macros and unknown package parts kept); previously trivial (ODT table cell formulas as formula fields); previously trivial (ODT custom shapes drawn from their own geometry); previously trivial (ODT bibliography marks as citations of sources); previously trivial (ODT linked text boxes); previously trivial (ODT picture crops and contour wrapping); previously trivial (ODT fidelity re-estimated at ~85%); previously trivial (ODT custom document properties and user fields as DOCPROPERTY); previously trivial (ODT connectors read and written glued); previously trivial (ODT borders around text read and written); previously trivial (ODT index marks read and written as XE fields; an index held around its entries updates); previously trivial (ODT ruby read and written); previously trivial (ODT page breaks after a paragraph, kept lines, widow control, hyphenation, table alignment and width and row heights); previously trivial (in-line drawings raised or lowered by their run's position; ODT in-line frames keep their place on the baseline; leading's space after the last line; text box borders shown by LibreOffice); previously trivial (ODT form controls opened and saved as content controls); previously trivial (password-protected ODT opened and saved); previously trivial (ODT headers and footers keep their shapes, text boxes, charts and embedded objects on save); previously trivial (ODT embedded spreadsheets and OLE objects kept whole instead of lost; flat ODT sub-documents valid `office:document`s); previously trivial (ODT charts in drawing groups drawn on open); previously trivial (ODT charts opened and saved as editable charts; flat ODT sub-documents no longer end the body); previously trivial (ODT leading read as an at-least spacing); previously trivial (ODT tracked formatting changes; changed regions named by `xml:id`); previously trivial (ODT drop caps; tab stops no longer drop a paragraph's background); previously trivial (flat ODT `.fodt` opened and saved; ODT fidelity re-estimated at ~80%); previously trivial (ODT page settings; a footnote no longer takes a footer's text); previously trivial (ODT dates in their formats, sequences, chapters and references); previously trivial (ODT bookmarks over text); previously trivial (ODT tables of contents and indexes; ODT fidelity re-estimated); previously trivial (ODT polygons, polylines and paths); previously trivial (ODT arrowheads, dashes and gradients); previously trivial (ODT groups of shapes and pictures); previously trivial (ODT rotated and mirrored shapes and pictures); previously trivial (ODT drawing shapes read and written); previously trivial (ODT text box frames keep their line, fill, padding and fit); previously trivial (ODT Asian and complex-script fonts); previously trivial (ODT right-to-left paragraphs); previously trivial (ODT text boxes kept); previously trivial (ODT unnumbered headings detached from the outline); previously trivial (ODT contextual spacing and layout settings); previously trivial (ODT custom character styles kept); previously trivial (ODT list level positions); previously trivial (ODT custom paragraph styles kept); previously trivial (ODT equations written as formula objects); previously trivial (ODT numbered headings written as headings); previously trivial (ODT floating pictures); previously trivial (ODT paragraph borders and background); previously trivial (ODT heading numbering); previously trivial (ODT font-face names); previously trivial (ODT table cell borders and padding); previously trivial (ODT heading, title, quote and preformatted styles keep their look); previously trivial (ODT tab stops read and written); previously trivial (ODT character formatting read and written); previously trivial (ODT lists keep their number formats, bullets and identity); previously trivial (ODT comments read and written); previously trivial (ODT footnotes and endnotes read and written); previously trivial (ODT headers, footers and text fields written; fields read); previously trivial (ODT paragraph spacing, indents and line spacing read and written); previously trivial (a paragraph naming an undefined style is formatted as Normal, as in Word, #5077); previously trivial (a hyperlinked TOC writes its page numbers as `PAGEREF _Toc… \h` fields, as Word does, #5097); previously trivial (HTML import ends the empty comments and at once, #5040); previously trivial (chart and SmartArt text the font lacks is drawn in a fallback face, so Chinese labels no longer show boxes, #4720); previously trivial (bibliography sources keep a corporate author (`b:Corporate`): read, written, cited and listed whole, #4848); previously trivial (custom-property bookmark-link preservation, #4767); previously trivial (note conversion preserves content with an ordered reference/run walk, #4750); previously trivial (DOCX comment-ID save lookups); previously trivial (built-in heading styles have their outline level without `w:outlineLvl`, #4696); previously trivial (DOCX comment ranges inside footnotes and endnotes are kept on read, #4548); previously trivial (RTF import and export keep Windows and Enhanced Metafile pictures (`\wmetafile`, `\emfblip`), #4259); previously trivial (#4658 bounds nested Markdown reference-label work); previously trivial (HTML import keeps preformatted lines with CR or CRLF input newlines, #4810); previously trivial (DOCX parts stored under another casing keep their embedded fonts through a save, and a private save of a kept glossary escapes the attribute values it rewrites; previously trivial: the `.doc` reader logs what its copy budget leaves out, once a kind per document; importers bound hostile expansion: font names, links, ODS repeats, LaTeX wrappers, Markdown delimiters, #3692/#3713; previously trivial: repeated DOCX style IDs: save/reopen regression; previously trivial: Markdown code spans keep line-broken text literal, #3738; previously trivial: Arabic list labels: `arabicAlpha` and `arabicAbjad` draw the 28 Arabic letters (alphabetical and abjad order), doubling past the last letter; Hebrew, Hindi, Thai and Vietnamese still show decimal (`doc::section::tests::arabic_letters_count_in_both_orders`); previously trivial: localized TOC style ids read as the TOC styles, and a TOC in columns keeps its layout through Update Table, #3889; previously trivial: cu20we: outline caps; previously trivial: a relationship the markup names without the prefix is read as it wrote: an unqualified `id` on an element that carries a relationship names that relationship, and the writer keeps the attribute with the id the new package holds the part under; an ordinary `id` — a VML shape's, a drawing's — names no part; previously trivial: the reader takes the relationship ids the markup refers to from the relationship attributes of the main document it has already parsed, so a document with many relationships opens in the time its part takes, #3411; previously trivial: a table cell span from a `.json` file stays within 63 columns through Insert Column and Merge Cells, #3637; previously trivial: the file commands that change the document are checked on a protected document, the protection of a protected document needs an explicit stop, the session author can't change through `file.setAuthor` under protection (the CLI's `--author` uses the same protection check), and the post-check compares the settings and the metadata; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: `majorBidi`/`minorBidi` theme fonts follow the settings' theme font language, as measured in Word; previously trivial: Word signature lines are kept on save, still signable; previously trivial: Times New Roman keeps Liberation Serif under the roman-font rule; previously trivial: shape gradients and transparency, #1500; previously trivial: a folder that keeps no Unix modes, such as a NAS share mounted by GVFS, still takes a save, #3003; previously trivial: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: table style band sizes written, a style without them unbanded as in Word, #3415; previously trivial: HTML export writes ruby (furigana, phonetic guides) and HTML import reads it back, #1681; previously trivial: RTF import decodes the East Asian multibyte code pages, #1653; previously trivial: PDF export: lam-alef drawn as two glyphs at one x extracts in reading order, #2310; previously trivial: native .json stores pictures and kept parts as base64, #586 follow-up; previously trivial: ODT headers and footers read from each master page, #863; previously trivial: RTF import compares heading and code formatting with the file's stylesheet; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: Word XML and web pages saved as `.doc` open by what they hold, #2853; previously trivial: a VML shape in a header cell is always placed on the page, user-checked in Word for Mac, #1775; previously trivial: `w:sym` characters keep their symbol font when the text's font changes and are written back as `w:sym`, #2036; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: two lines in one and horizontal in vertical (`w:eastAsianLayout`) kept on save; previously trivial: PDF outline targets follow leading page breaks; previously trivial: positional tabs `w:ptab` read and kept on save, #2367; previously trivial: DOCX: mixed right-to-left runs split and marked `w:rtl` with their language, #1543; previously trivial: `w:doNotTrackMoves` round-trips and turns move tracking off; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: DOCX glossary documents (building blocks) read, kept and written; previously trivial: cached field results keep their visible run formatting; previously trivial: cached REF results retain hyperlink destinations; previously trivial: CLI conversions can explicitly refresh fields; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: a pasted HTML `
` is an indented paragraph, not the Quote style, #1270); previously trivial: every table style part round-trips, #714; previously trivial: VML freeforms read and kept on save; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: `flowChartOr` draws its circle and cross spokes, as in Word; previously trivial: RTF: Mac OS Roman and DOS text, #1653; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: PDF export draws horizontal and vertical lines inside groups, #1474; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously 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-11 · **Last updated:** 2026-11-02 · **Change:** trivial (ODT interpretation profile: the standard is law, what it leaves open (including unstated page margins) follows WordCraft, LibreOffice or custom settings); previously trivial (ODT form controls keep their place on the line and text areas their lines; custom shapes keep their geometry); previously trivial (ODT in-line frames placed against the text; in-line pictures saved standing on the baseline); previously trivial (ODT charts written back as they were while unchanged; frames' drawn outline; default header height); previously trivial (ODT header rows only where they repeat); previously trivial (ODT padding without a border; unequal columns; column breaks); previously trivial (ODT lines of only pictures laid out as LibreOffice does; default page without a layout); previously trivial (ODT pictures alone in a paragraph keep its size; in-line frames raised under an inherited paragraph relation); previously trivial (ODT tables fit the margins and keep their space above and below; header spacing kept); previously trivial (ODT frames anchored and placed by their style); previously trivial (ODT default font size saved; page-anchored frames placed from the page; text box paragraph styles kept; empty paragraphs keep their size); previously trivial (ODT default font size; locale-ordered dates); previously trivial (ODT page background pictures); previously trivial (ODT tables fixed layout; tables starting a page); previously trivial (emphasis marks: model, layout, DOCX `w:em`, ODT `style:text-emphasize`, `format.emphasisMark`); previously trivial (ODT variables as SET/REF fields; character styles keep what they turn off); previously trivial (ODT styles keep what they turn off; WMF/EMF pictures kept; pictures in text boxes shown by LibreOffice); previously trivial (ODT unstated page margins are LibreOffice's 2 cm; drawings outside a paragraph join the next one); previously trivial (ODT placeholders as content controls, template dates, user name fields, unsized pages, style-only templates, found with LibreOffice's own templates); previously trivial (ODT hidden form controls kept; `.ott` templates opened and saved); previously trivial (ODT fidelity re-estimated at ~90%; a kept form control made another kind is saved as that kind); previously trivial (ODT form control sizes and buttons kept); previously trivial (ODT turned text in cells and text boxes); previously trivial (ODT macros and unknown package parts kept); previously trivial (ODT table cell formulas as formula fields); previously trivial (ODT custom shapes drawn from their own geometry); previously trivial (ODT bibliography marks as citations of sources); previously trivial (ODT linked text boxes); previously trivial (ODT picture crops and contour wrapping); previously trivial (ODT fidelity re-estimated at ~85%); previously trivial (ODT custom document properties and user fields as DOCPROPERTY); previously trivial (ODT connectors read and written glued); previously trivial (ODT borders around text read and written); previously trivial (ODT index marks read and written as XE fields; an index held around its entries updates); previously trivial (ODT ruby read and written); previously trivial (ODT page breaks after a paragraph, kept lines, widow control, hyphenation, table alignment and width and row heights); previously trivial (in-line drawings raised or lowered by their run's position; ODT in-line frames keep their place on the baseline; leading's space after the last line; text box borders shown by LibreOffice); previously trivial (ODT form controls opened and saved as content controls); previously trivial (password-protected ODT opened and saved); previously trivial (ODT headers and footers keep their shapes, text boxes, charts and embedded objects on save); previously trivial (ODT embedded spreadsheets and OLE objects kept whole instead of lost; flat ODT sub-documents valid `office:document`s); previously trivial (ODT charts in drawing groups drawn on open); previously trivial (ODT charts opened and saved as editable charts; flat ODT sub-documents no longer end the body); previously trivial (ODT leading read as an at-least spacing); previously trivial (ODT tracked formatting changes; changed regions named by `xml:id`); previously trivial (ODT drop caps; tab stops no longer drop a paragraph's background); previously trivial (flat ODT `.fodt` opened and saved; ODT fidelity re-estimated at ~80%); previously trivial (ODT page settings; a footnote no longer takes a footer's text); previously trivial (ODT dates in their formats, sequences, chapters and references); previously trivial (ODT bookmarks over text); previously trivial (ODT tables of contents and indexes; ODT fidelity re-estimated); previously trivial (ODT polygons, polylines and paths); previously trivial (ODT arrowheads, dashes and gradients); previously trivial (ODT groups of shapes and pictures); previously trivial (ODT rotated and mirrored shapes and pictures); previously trivial (ODT drawing shapes read and written); previously trivial (ODT text box frames keep their line, fill, padding and fit); previously trivial (ODT Asian and complex-script fonts); previously trivial (ODT right-to-left paragraphs); previously trivial (ODT text boxes kept); previously trivial (ODT unnumbered headings detached from the outline); previously trivial (ODT contextual spacing and layout settings); previously trivial (ODT custom character styles kept); previously trivial (ODT list level positions); previously trivial (ODT custom paragraph styles kept); previously trivial (ODT equations written as formula objects); previously trivial (ODT numbered headings written as headings); previously trivial (ODT floating pictures); previously trivial (ODT paragraph borders and background); previously trivial (ODT heading numbering); previously trivial (ODT font-face names); previously trivial (ODT table cell borders and padding); previously trivial (ODT heading, title, quote and preformatted styles keep their look); previously trivial (ODT tab stops read and written); previously trivial (ODT character formatting read and written); previously trivial (ODT lists keep their number formats, bullets and identity); previously trivial (ODT comments read and written); previously trivial (ODT footnotes and endnotes read and written); previously trivial (ODT headers, footers and text fields written; fields read); previously trivial (ODT paragraph spacing, indents and line spacing read and written); previously trivial (a paragraph naming an undefined style is formatted as Normal, as in Word, #5077); previously trivial (a hyperlinked TOC writes its page numbers as `PAGEREF _Toc… \h` fields, as Word does, #5097); previously trivial (HTML import ends the empty comments and at once, #5040); previously trivial (chart and SmartArt text the font lacks is drawn in a fallback face, so Chinese labels no longer show boxes, #4720); previously trivial (bibliography sources keep a corporate author (`b:Corporate`): read, written, cited and listed whole, #4848); previously trivial (custom-property bookmark-link preservation, #4767); previously trivial (note conversion preserves content with an ordered reference/run walk, #4750); previously trivial (DOCX comment-ID save lookups); previously trivial (built-in heading styles have their outline level without `w:outlineLvl`, #4696); previously trivial (DOCX comment ranges inside footnotes and endnotes are kept on read, #4548); previously trivial (RTF import and export keep Windows and Enhanced Metafile pictures (`\wmetafile`, `\emfblip`), #4259); previously trivial (#4658 bounds nested Markdown reference-label work); previously trivial (HTML import keeps preformatted lines with CR or CRLF input newlines, #4810); previously trivial (DOCX parts stored under another casing keep their embedded fonts through a save, and a private save of a kept glossary escapes the attribute values it rewrites; previously trivial: the `.doc` reader logs what its copy budget leaves out, once a kind per document; importers bound hostile expansion: font names, links, ODS repeats, LaTeX wrappers, Markdown delimiters, #3692/#3713; previously trivial: repeated DOCX style IDs: save/reopen regression; previously trivial: Markdown code spans keep line-broken text literal, #3738; previously trivial: Arabic list labels: `arabicAlpha` and `arabicAbjad` draw the 28 Arabic letters (alphabetical and abjad order), doubling past the last letter; Hebrew, Hindi, Thai and Vietnamese still show decimal (`doc::section::tests::arabic_letters_count_in_both_orders`); previously trivial: localized TOC style ids read as the TOC styles, and a TOC in columns keeps its layout through Update Table, #3889; previously trivial: cu20we: outline caps; previously trivial: a relationship the markup names without the prefix is read as it wrote: an unqualified `id` on an element that carries a relationship names that relationship, and the writer keeps the attribute with the id the new package holds the part under; an ordinary `id` — a VML shape's, a drawing's — names no part; previously trivial: the reader takes the relationship ids the markup refers to from the relationship attributes of the main document it has already parsed, so a document with many relationships opens in the time its part takes, #3411; previously trivial: a table cell span from a `.json` file stays within 63 columns through Insert Column and Merge Cells, #3637; previously trivial: the file commands that change the document are checked on a protected document, the protection of a protected document needs an explicit stop, the session author can't change through `file.setAuthor` under protection (the CLI's `--author` uses the same protection check), and the post-check compares the settings and the metadata; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: `majorBidi`/`minorBidi` theme fonts follow the settings' theme font language, as measured in Word; previously trivial: Word signature lines are kept on save, still signable; previously trivial: Times New Roman keeps Liberation Serif under the roman-font rule; previously trivial: shape gradients and transparency, #1500; previously trivial: a folder that keeps no Unix modes, such as a NAS share mounted by GVFS, still takes a save, #3003; previously trivial: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: table style band sizes written, a style without them unbanded as in Word, #3415; previously trivial: HTML export writes ruby (furigana, phonetic guides) and HTML import reads it back, #1681; previously trivial: RTF import decodes the East Asian multibyte code pages, #1653; previously trivial: PDF export: lam-alef drawn as two glyphs at one x extracts in reading order, #2310; previously trivial: native .json stores pictures and kept parts as base64, #586 follow-up; previously trivial: ODT headers and footers read from each master page, #863; previously trivial: RTF import compares heading and code formatting with the file's stylesheet; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: Word XML and web pages saved as `.doc` open by what they hold, #2853; previously trivial: a VML shape in a header cell is always placed on the page, user-checked in Word for Mac, #1775; previously trivial: `w:sym` characters keep their symbol font when the text's font changes and are written back as `w:sym`, #2036; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: two lines in one and horizontal in vertical (`w:eastAsianLayout`) kept on save; previously trivial: PDF outline targets follow leading page breaks; previously trivial: positional tabs `w:ptab` read and kept on save, #2367; previously trivial: DOCX: mixed right-to-left runs split and marked `w:rtl` with their language, #1543; previously trivial: `w:doNotTrackMoves` round-trips and turns move tracking off; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: DOCX glossary documents (building blocks) read, kept and written; previously trivial: cached field results keep their visible run formatting; previously trivial: cached REF results retain hyperlink destinations; previously trivial: CLI conversions can explicitly refresh fields; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: a pasted HTML `
` is an indented paragraph, not the Quote style, #1270); previously trivial: every table style part round-trips, #714; previously trivial: VML freeforms read and kept on save; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: `flowChartOr` draws its circle and cross spokes, as in Word; previously trivial: RTF: Mac OS Roman and DOS text, #1653; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: PDF export draws horizontal and vertical lines inside groups, #1474; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously 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 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; @@ -285,7 +285,7 @@ Custom document properties retain their bookmark link targets through DOCX saves | 2026-11-02 | trivial | ODT: a chart lost the look WordCraft doesn't model (its fonts and sizes, axis and grid lines) on save, written anew from WordCraft's chart. The chart object as read is kept (through drawing it again when it opens or is resized), and written back whole while the chart reads the same; an edited chart is written from WordCraft's chart as before. The chart documents round-trip pixel-identical (`charts::an_unchanged_chart_is_written_back_as_it_was_and_an_edited_one_anew`, `engine::tests::redrawing_a_chart_keeps_the_chart_as_read`). Found with LibreOffice's serif business letter: a text frame outlined by its drawn stroke (`draw:stroke`, no `fo:border`) lost its outline; it's the frame's line. A header or footer whose style states no height (or that has none) was 0 tall; it's LibreOffice's 0.5 cm (measured), so the text no longer moves up. The letter round-trips to 116 differing pixels (from 4,223) (`text_boxes::a_frames_drawn_stroke_is_its_line_when_it_has_no_border`, `headers::a_header_without_a_stated_height_is_half_a_centimetre`) | | 2026-11-02 | trivial | ODT, found with LibreOffice's agendas: saving wrote every table's first row as a header row (`table:table-header-rows`), repeated at the top of each page, because a table style's header row (on by default in Word's table look) was taken for Repeat as header row; a table crossing a page showed its first row twice. Only rows set to repeat are written so. The agendas round-trip to about 145 differing pixels (from 5,269 and 5,086), their text the same (`tables::only_rows_set_to_repeat_are_written_as_header_rows`) | | 2026-11-02 | trivial | ODT, found with LibreOffice's report templates and one-page résumé. A paragraph's padding on a side without a border (measured: LibreOffice gives it room) was dropped; it's kept, laid out (see layout-parity) and written back. Unequal columns (`style:column` `style:rel-width` and indents, on pages and sections) were made equal; they keep their widths, written back as shares, without the gap LibreOffice ignores them beside. Columns with no stated gap have none (it had been Word's 0.5 in). A paragraph starting a column (`fo:break-before="column"`) didn't; it starts with Word's column break, written back on the paragraph. The report templates round-trip to 0–11 differing pixels (from 9,600), the résumé to 24 (from 6,286) (`para_borders::padding_without_a_border_keeps_its_room`, `odt::tests::unequal_columns_keep_their_widths_and_columns_without_a_gap_have_none`, `odt::tests::a_paragraph_starting_a_column_starts_with_a_column_break`) | -| 2026-11-02 | trivial | ODT: what ODF 1.3 defines (including XSL's initial values for `fo:` properties) is followed whatever the settings; what it leaves to the application (Part 3 §16.2) is an interpretation profile, WordCraft's by default, LibreOffice's or custom, in File › Options › Advanced › OpenDocument files and `file.options` `odf`. Every case is in [odf-interpretation.md](odf-interpretation.md). Unstated page margins are now XSL's 0 (not LibreOffice's 2 cm); unstated header heights, column gaps, shape colours and hairlines, tab measuring, frame relations the standard doesn't define and picture-only line heights follow the profile. Google Docs and ONLYOFFICE files checked: no preset of their own needed yet. | +| 2026-11-02 | trivial | ODT: what ODF 1.3 defines (including XSL's initial values for `fo:` properties) is followed whatever the settings; what it leaves to the application (Part 3 §16.2) is an interpretation profile, WordCraft's by default, LibreOffice's or custom, in File › Options › Advanced › OpenDocument files and `file.options` `odf`. Every case is in [odf-interpretation.md](odf-interpretation.md). Unstated page margins (WordCraft 0, XSL's initial value; LibreOffice 2 cm), header heights, column gaps, shape colours and hairlines, tab measuring, frame relations the standard doesn't define and picture-only line heights follow the profile. Google Docs and ONLYOFFICE files checked: no preset of their own needed yet. | | 2026-11-02 | trivial | ODT: a document opens with LibreOffice's layout for lines of only pictures (as tall as the pictures; see layout-parity), so WordCraft draws LibreOffice's rules and logos at their height. A document with no page layout at all (no styles.xml) had Word's Letter page and 1 inch margins; it has the region's paper and LibreOffice's 2 cm margins (`page_setup::a_document_without_a_page_layout_has_libreoffices_default_page`) | | 2026-11-02 | trivial | ODT, found with LibreOffice's elegant agenda: a paragraph holding only pictures lost its own font size (the pictures' runs had none), so its line grew; the size is the paragraph mark's and the pictures' runs', read and written. An in-line frame's raise was dropped when a parent style gave it a relation no in-line frame has (`style:vertical-rel="paragraph"`); it's measured from the baseline, as LibreOffice reads it. The agenda's round-trip difference falls from 6,542 to 1,331 pixels (`floats::a_picture_alone_in_its_paragraph_keeps_its_raise_and_the_paragraphs_size`) | | 2026-11-02 | trivial | ODT, found with LibreOffice's report templates: a table filling the margins (`table:align="margins"`) wider than them ran off the page; it's fitted to them, as LibreOffice fits it. A table's own space above and below (`fo:margin-top`/`fo:margin-bottom`) was dropped; it's kept, laid out and written back. A header or footer taller than its band lost its spacing from the body, moving the text up; it's kept unless the spacing is dynamic (`style:dynamic-spacing`). One report template's round-trip difference falls from 7,646 to 602 pixels (`flow_breaks::a_tables_own_space_above_and_below_is_kept`, `a_tables_columns_keep_their_widths`, `headers::a_headers_spacing_stays_below_content_taller_than_it`) | diff --git a/docs/odf-interpretation.md b/docs/odf-interpretation.md index 0afe380aab..8ddb70adef 100644 --- a/docs/odf-interpretation.md +++ b/docs/odf-interpretation.md @@ -11,8 +11,10 @@ knows of, so none gets lost. ## The rule 1. **What the standard defines is law.** WordCraft follows it whatever the settings say. That - includes `fo:` properties, which the standard defines by referring to XSL 1.0. Where it does, - XSL's initial value is the default. + includes `fo:` properties, which the standard defines by referring to XSL 1.0, for their + meaning. Whether such a reference also brings XSL's initial value as the default the standard + doesn't say; where an application's choice differs from XSL's (page margins), the case is a + setting, with XSL's value as WordCraft's. 2. **What the standard leaves open is a setting.** Each such case is one value of the ODF interpretation profile (`OdfProfile`, `crates/doc/src/odf_profile.rs`): - **WordCraft**, the default: what WordCraft does for its own and Word documents. @@ -41,6 +43,7 @@ Where to set it: | Case | Standard | Setting (`file.options` `odf.*`) | WordCraft | LibreOffice | |---|---|---|---|---| +| Page margins a page layout doesn't state | §20.206–20.209 refer to XSL §7.10 (initial value 0); whether that default comes with the reference is not said | `pageMargin` | 0 (XSL's initial value) | 2 cm (its own files always state theirs; some of its templates don't) | | Height of a header or footer whose style states none | §20.212.2 `fo:min-height`: no default | `headerMinHeight` | 0 (as tall as its text) | 0.5 cm | | *(meaning)* Whether a header's stated minimum height includes its stated spacing from the body | §20.212.2, §20.207: not said | `headerHeightIncludesSpacing` | no: spacing comes after the height | yes | | A header taller than its height with `style:dynamic-spacing` unstated | §20.264: no default | `dynamicSpacing` | off: the spacing is kept (on, with no height stated, would always drop the spacing the file states) | off | @@ -57,7 +60,6 @@ Where to set it: | Case | Rule | Note | |---|---|---| -| Page margins a page layout doesn't state | `fo:margin-*` (§20.207 et seq.) refer to XSL §7.10.1: initial value 0 | LibreOffice draws 2 cm, but its own files always state their margins. A file without them (some templates) opens with 0 margins. | | Paragraph padding on a side without a border | `fo:padding` (§20.217) is XSL's box model: padding takes room whether or not there is a border | Word ignores padding without a border. ODF documents don't. | | A frame's `style:run-through` when unstated | §20.351: `foreground` | | | `style:dynamic-spacing="true"/"false"` when stated | §20.264 | WordCraft writes `false` when a header or footer has spacing, so every reader keeps it. | @@ -109,4 +111,5 @@ one more `OdfPreset` with its values, and the cases above stay the same. | Date | Change | |---|---| -| 2026-11-02 | First version: the profile, the eleven open cases, law cases, writer choices, Google Docs and ONLYOFFICE findings. Stated values are never replaced by a preset; the cases about the meaning of stated values are marked. | +| 2026-11-02 | Unstated page margins are a setting (`pageMargin`: WordCraft 0, XSL's initial value; LibreOffice 2 cm), not law: the standard refers to XSL's margins without saying whether XSL's default comes too. | +| 2026-11-02 | First version: the profile, the open cases, law cases, writer choices, Google Docs and ONLYOFFICE findings. Stated values are never replaced by a preset; the cases about the meaning of stated values are marked. |