From cef43389471d7ced83f0c0511726486ffd69a4c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 23:45:28 +0000 Subject: [PATCH] M5 #19: ODT page border padding beyond 31 pt kept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The padding inside an ODT page border (§20.210) was capped at Word's 31 pt border spacing when opening, so the text of WordCraft's own page borders (24 pt from the edge, 1 inch margins: 45 pt of padding) moved towards the border on reopening. In ODF the padding is the distance to the text, any length on the page. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC --- crates/formats/src/odt/import.rs | 3 ++- .../formats/src/odt/tests_more/page_setup.rs | 18 ++++++++++++++++++ docs/file-format-parity.md | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/formats/src/odt/import.rs b/crates/formats/src/odt/import.rs index 2e1f6323c9..7fdf17221d 100644 --- a/crates/formats/src/odt/import.rs +++ b/crates/formats/src/odt/import.rs @@ -1031,7 +1031,8 @@ fn apply_layout(a: &Attrs, sec: &mut SectionProps) { let b = get(a, &format!("fo:border-{s}")).or_else(|| get(a, "fo:border")).and_then(super::styles::parse_border)?; let pad = get(a, &format!("fo:padding-{s}")).or_else(|| get(a, "fo:padding")).and_then(length).filter(|v| v.is_finite()); (b.style != wordcraft_doc::props::BorderStyle::None) - .then(|| wordcraft_doc::props::Border { space: pad.map_or(0.0, |p| p.clamp(0.0, 31.0)), ..b }) + // Any distance on the page (Word's 31 pt limit is for its own measure, from the edge). + .then(|| wordcraft_doc::props::Border { space: pad.map_or(0.0, |p| p.clamp(0.0, 1584.0)), ..b }) }; // ODF's border is at the margin, the text inside its padding; WordCraft measures the border // from the page edge and the text margin from the edge too. diff --git a/crates/formats/src/odt/tests_more/page_setup.rs b/crates/formats/src/odt/tests_more/page_setup.rs index dae50ed477..313069fb67 100644 --- a/crates/formats/src/odt/tests_more/page_setup.rs +++ b/crates/formats/src/odt/tests_more/page_setup.rs @@ -229,3 +229,21 @@ fn a_page_background_picture_is_read_and_written() { assert_eq!(again.settings.page_picture.as_ref().map(|p| p.fill_type.as_str()), Some(fill)); } } + +#[test] +fn a_page_borders_padding_keeps_the_text_where_it_was() { + // Design › Page Borders around a page with 1 inch margins: the border 24 pt from the edge, + // 45 pt of padding inside it (more than Word's 31 pt border spacing, which is measured + // differently): the text stays 72 pt from the edge, written and read back. + let mut d = Document::new(); + d.body = vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text("x", Default::default()))]; + let edge = wordcraft_doc::props::Border { style: BorderStyle::Single, width: 3.0, color: Some(Rgb(0x2f, 0x55, 0x97)), space: 24.0 }; + d.last_section.page_borders = + Some(wordcraft_doc::props::Borders { top: Some(edge), bottom: Some(edge), left: Some(edge), right: Some(edge), ..Default::default() }); + let back = crate::odt::import(&crate::odt::export(&d).unwrap()).unwrap(); + let s = &back.last_section; + for m in [s.margin_top, s.margin_bottom, s.margin_left, s.margin_right] { + assert!((m - 72.0).abs() < 0.05, "{s:?}"); + } + assert_eq!(s.page_borders.and_then(|b| b.left).map(|b| (b.width, b.space)), Some((3.0, 24.0))); +} diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 21efc78273..2ad5f1ffa4 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -285,6 +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-03 | trivial | ODT page borders: the padding inside a page border was capped at Word's 31 pt border spacing on opening, so text inside a wide padding (WordCraft's own page borders around 1 inch margins) moved towards the border; it is now any distance on the page. | | 2026-11-03 | trivial | ODT watermarks: WordCraft's watermark (Design › Watermark) is saved as LibreOffice's and Word's are, a fontwork shape (`draw:text-path`) named PowerPlusWaterMarkObject behind the text in every header (one written for it alone, inside the top margin, when there is none), which LibreOffice draws; such a shape opens as the watermark (its text, colour, see-through, turn and size), not a shape. Before, the watermark was left out of ODT files. | | 2026-11-03 | trivial | ODT, found by saving WordCraft's own documents and opening them in LibreOffice: a table style's look (header and band fills, its cell borders, bold header text, its paragraph spacing) was left out of the saved file; each cell now states what the style gives it, under the paragraph's own styles as in the layout (ECMA-376 §17.7.2). Tables read from ODT no longer get Word's Table Grid style, whose single spacing changed their text. A columned section states whether its columns end balanced (`text:dont-balance-text-columns`, §20.428, whose value descriptions contradict its name: read as the name says); when a file doesn't say, the ODF interpretation profile decides (`balanceSectionColumns`). SmartArt diagrams (ODF has none) are saved as a group of their shapes and text, which LibreOffice draws as WordCraft does; WordCraft opens them back as a group. | | 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. |