The page the section before ends on is finished with its own margins and
footnote separators before the starting section's replace them, so the
footnotes pending on it are placed where the page they sit on says.
Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
* docx: keep a paragraph's text alignment
w:textAlignment (ECMA-376 17.3.1.39) was not read, so a save dropped it. Word sizes a line by it when a
list's bullet is larger than the text: without it, those lines grow and every page after them moves.
ParaProps gains text_alignment, read and carried through styles, and written back after w:jc. Kept for
the file only; the layout does not use it yet.
* contributors: add entropybeater
---------
Co-authored-by: Brian <255903314+entropybeater@users.noreply.github.com>
Spelling as you type now notes that a web or e-mail address after a
no-break, narrow no-break (U+202F), figure (U+2007) or ideographic space
is not marked while Ignore web and file addresses is on, as
address_after_a_wide_space_is_an_internet_token in craft-proof checks.
* doc, docx: keep the compound border lines
The nine thin and thick compound lines (thinThickSmallGap to thinThickThinLargeGap, ECMA-376 17.18.2)
were read as a single line and written back as one, on paragraphs, tables, cells and pages. BorderStyle
gains them; they are written back by name and drawn as a single line of the border's width, as before.
RTF export and import carry them too (the importer read four of them as double).
* contributors: add entropybeater
---------
Co-authored-by: Brian <255903314+entropybeater@users.noreply.github.com>
metafile_size() parsed the whole metafile, decoding every bitmap and building every path, to
read two numbers at picture insert and replace. A new wordcraft_metafile::size reads the frame
from the EMF header or the placeable WMF header, and falls back to the full parse for every
other file (plain WMF, an EMF without a usable frame, a file with no records), so it returns
what parse would. For an EMF with one 8000x5000 1 bpp bitmap the size now costs well under a
microsecond instead of about 280 ms.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
decode_clip matched the bit depth, bounds-checked and (for 16 and 32 bpp) divided per channel
for every pixel. The depth, palette and masks are now turned into a row decoder once per
bitmap: a 256-entry colour table for 1/4/8 bpp, plain channel swaps for 24 bpp and default
32 bpp, and precomputed shifts and scale tables for masked 16/32 bpp. Output is byte for byte
the same; the old per-pixel code stays in the tests as the reference.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
decode_clip only capped width x height at 40 MP. A 32 x 1,250,000 pixel 1 bpp bitmap (about
5 MB in the file) was decoded to 160 MB of RGBA and kept, then dropped by Item::take_pixels
because its side is over MAX_BITMAP_SIDE, while counting against the parse's 256 MB budget.
decode_clip now returns None when the clipped width or height is over the cap, so crops of a
large bitmap that fit still decode.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pin that a comment id past u32 is refused, not truncated (#3677)
`validate_comment_id` refuses an `id` that does not fit a `u32`, so the `as
u32` casts behind it can no longer reach another comment -- but nothing pinned
it, and the casts are still there to be reached if the guard moves.
The ids the test sends are the ones that would alias onto real comments if the
truncation came back: 2^32 truncates to 0 (and the first comment is 0), 2^32 +
id to that comment, 4294967301 to 5 as the report describes, and u64::MAX to
u32::MAX. Each of the four commands that take an id is tried with each, then
the comments are checked to be unchanged and unresolved, and a valid id is
accepted afterwards so the guard is shown to be a range check rather than a
refusal.
* Comment ids: the values past u32 go in the existing invalid-id test, refused by the range check
anchors_walk_dense_unrelated_markers_in_linear_work compared CPU times of
a few microseconds, so cache and timer noise could push the ratio past its
24x limit on a linear walk (43.52x in a CI run).
wordcraft-doc now counts, in tests only, the characters its paragraph
object lookups walk (objects_before, object_offsets and
objects_with_offsets), through a work::count helper behind the existing
test-support feature. The test asserts that 8x the markers take at most
12x the steps, as the layout linear-time tests do: the walk takes 2001 ->
16001 steps (8.0x), and the object_offsets + object_at walk it replaced
takes 2003001 -> 128024001 (63.9x).
update_generated now keeps what comes before a wrapped list's field, which can be
another list field; rebuild from the converted field's code instead of the
paragraph's first list field, and let index::rebuild_from take that code.
- update_generated keeps what precedes a wrapped list's field (r4468529243)
- from_doc keeps objects in hidden text when hidden text is written (r4469018820)
- clipped cell lines in a turned frame are drawn once, clipped, turned (r4469018900)
- kept::while_writing works out reply map and page fingerprint once per export (r4468529227)
- odt_charts fixture declares its namespaces (r4468529259)
- guard test for character styles based on a missing parent (r4469018843, not a bug)
* Arabic interface: mirrored right-to-left window and Arabic UI fonts
Adds the Arabic (`ar`) interface language's code: its catalogs are craft-i18n's
`locales/ar`, so this changes no catalog.
- `ar` is registered in `i18n::LANGUAGES` and `Lang::rtl()` says it is written
right to left; its translations are used as written (egui's text layout
orders each line's bidi text itself). A ribbon location reads from the right
with a leftwards mark.
- `rtl.rs`: the window is mirrored. Containers open in `rtl::root`; columns,
grids, checkboxes, radio buttons, menus, rows, panels, the dialog tab strip
and the ribbon's scrolling row follow the language's direction. Title bar,
status bar, ribbon tabs and groups, File pages (including Print, with the
settings on the right and a solid scrollbar gutter), dialogs, menus and the
task panes (their docking edge swaps) are laid out from the right. egui's
`Grid` panics in a right-aligned layout, so grids go through `rtl::grid`.
- Arabic UI font fallback: the craft-fonts Arabic face, else an installed
Arabic font, loaded when the interface is Arabic.
- The "Auto (200%)" size label keeps its percent sign inside the isolated
number.
- Tests: Arabic locale, saved preference and lookup order on fixture catalogs;
an interface audit (skipped without the craft-i18n Arabic catalog) that lays
the app out right to left at several widths, checks tab, group, pane, Print
and Go To positions and that nothing panics.
The Arabic work builds on Abdelhaq El Amraoui's mirrored-interface PR (#737).
Co-Authored-By: Abdelhaq El Amraoui <abdelhaqelamraoui@gmail.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: pin craft-i18n at a commit with the Arabic catalog; review fixes
- craft-i18n.rev and the Flatpak manifest pin 19358491 (craft-i18n main), the
first commit line that has locales/ar/wordcraft.tsv. The old pin had no
Arabic catalog, so `ar` was registered but not embedded and two i18n tests
(the_language_setting_is_a_ui_command_and_persists,
the_languages_offered_are_the_embedded_ones_of_the_list) failed in CI.
- The Arabic font fallback is requested for any right-to-left interface
(`Lang::rtl()`), not only after the language picker sets the flag, so an
automatic or saved Arabic gets the installed Arabic face.
- The file browser, the update window and Reading Mode's toolbar run their
bodies in `rtl::root`, so their far-side buttons and rows are mirrored.
- A group in a right-to-left ribbon row is measured from the right edge: the
cursor's left edge is negative infinity there, so its width was NaN and the
ribbon planner dropped every group, never compacting a narrow Arabic window.
A narrow window now folds groups into drop-downs.
- The mirrored checkbox and radio marks are labelled by their text, so screen
readers name them.
- Tests: group width in a right-to-left row; the Arabic interface audit checks
Reading Mode's toolbar.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: ask for the installed Arabic font separately from the CJK one
A previous commit made every right-to-left interface request the installed CJK
fallback font (`system_cjk`) so that the installed Arabic font would load. That
also made every Arabic launch read a large system CJK font file when no
craft-fonts input is embedded (the Flatpak build has none).
`install_ui_fonts` now passes a separate `system_arabic` flag (`Lang::rtl()`):
the installed Arabic font is read for it, and `system_cjk` is again set only
by a CJK interface or by CJK text on screen. `WordApp::fonts_system_arabic`
remembers what was installed, so a language change reinstalls the fonts.
Tests: the_installed_arabic_font_is_asked_for_by_arabic_alone (the Arabic flag
adds the installed font only when none is embedded, and the CJK request alone
adds no Arabic font). Checked a headless screenshot of the Arabic interface
without craft-fonts: the installed Arabic font draws the labels.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Fix the AutoSave switch knob: on is at the right, mirrored in Arabic
The title bar's AutoSave knob was placed with `lead_x(rtl == on, ..)`, which
reverses main's mapping in both directions: left to right it drew the knob at
the left when the switch was on, and right to left at the right. main draws it
at the right when on and at the left when off, so the mirrored interface must
draw it at the left when on: `rtl != on`.
The position is now `autosave_knob_x(rtl, on, track)`, pinned by a test for all
four cases. A headless screenshot of the off switch shows the knob at the left
in English and at the right in Arabic.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: file names and paths in right-to-left lines; kerning row order
Tester feedback on the Arabic interface:
- File > Open's Recent list, the built-in file browser and the title bar drew
file names and paths raw, from the left edge. In an Arabic line the marks of
a name ("(نهائي) 2.docx") and the folders of a path with Arabic parts were
scrambled. Rows now start at the side reading starts from (`rtl::lead_x`),
names go through `i18n::isolate` (a first-strong isolate, with a left-to-right
mark before the extension so `2.docx` stays `2.docx`) and paths through
`i18n::isolate_path` (a left-to-right isolate with a mark after every
separator, so neighbouring Arabic folders do not swap). Other languages get
the text untouched; saved and spoken text never carries the marks.
- Font dialog, Kerning row: the dialog's grid is drawn left to right inside a
right-to-left window, so "Points and above" came after its number; in an
Arabic interface it now comes first, so the line reads "14 نقطة فأكثر" from the
right. (The grid's columns themselves still keep their left-to-right order.)
Tests: names_and_paths_are_isolated_in_a_right_to_left_interface.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: mirror through craft_ui::direction and Style::right_to_left
The suite's craft-ui now has the direction helpers (`craft_ui::direction`) and
its egui fork a `Style::right_to_left` that mirrors egui's own chrome. Use them
instead of our parallel wrappers:
- `rtl::sync(ctx)` applies the interface direction every frame the language's
changes (`direction::apply`): it sets the shared direction state and
`Style::right_to_left` on every theme, which draws check boxes and radio
buttons with the mark at the reading start, mirrors submenus, scroll bars,
title bars and the combo boxes' arrows (the Print page's choice lists, the
font list) and opens combo menus from the right.
- `rtl::checkbox`, `radio_value`, `radio` and `indeterminate_checkbox` are
gone: about 200 call sites use plain `ui.checkbox`, `ui.radio_value`, ... and
egui does the mirroring (and names the control by its text, as a plain widget).
- `lead_x`, `lead_center`, `lead_top`, `start_panel` and `end_panel` delegate to
`craft_ui::direction`. `root`, `flows`, `island`, `grid`, `menu`, `vertical`,
`far_side` and the other layout helpers stay: the shared crate has no
equivalent that fits them.
Visual changes: the combo boxes' drop-down arrows and lists are mirrored in
Arabic; nothing changes in left-to-right languages (checked headless
screenshots of the English main window, Font dialog and Print page).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Recent files and the file browser: full path on hover, long names keep their start
Review feedback:
- Each row of File > Open's Recent list shows the document's full path as its
tooltip (the path line under the name can be longer than the row).
- A long name in the built-in file browser is laid out in one row, with an
ellipsis at its end, from the side reading starts from, instead of being
clipped at the row's edge: in a right-to-left interface the old text anchored
at the right edge cut the end of an Arabic name and the start of a Latin one.
The whole name is the row's tooltip when it was shortened.
Checked headless screenshots of the file browser with a long English and a long
Arabic name.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Font combos in a right-to-left row: the arrow joins its field on the left
The ribbon's font name and size boxes are drawn from a field and a separate
arrow box that share one outline: the field is square and the arrow's box is
rounded on the side where the arrow goes, painted a border width under the
field. That assumed the arrow is right of the field. In a right-to-left row
(the Arabic ribbon) the arrow comes after the field on its left, so the field
was square on the wrong side and the arrow box rounded on the side facing the
field: two boxes with a gap-like seam, the arrow looking outside the frame.
`arrow_box_shape(rtl, ..)` mirrors the radii and the underlap, and the field's
square corners are chosen by the row's direction (the nw/sw pair in right to
left). Left-to-right rows are drawn exactly as before (English before/after
crops are identical).
Test: the_arrow_box_is_rounded_away_from_its_field.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pin craft-i18n 4612850 (merged Arabic WordCraft rows, craft-i18n#76)
The pin and the Flatpak manifest commit move from 1935849 to 4612850, which is on
craft-i18n main and adds the 70 Arabic rows this branch relies on.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pin craft-i18n cac2cf0 (Arabic number agreement and status-bar rows)
Moves the pin and the Flatpak manifest commit from 4612850 to the current craft-i18n
main, which includes craft-i18n#95 (48bed03) and #81 (ef58ff3).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic: isolate paths shown beside labels; mirror the Print page zoom
File > Info shows Location as its own left-to-right path label (cut short with the full path on hover, not wrapped), and the open-failure, Exported and dictionary-file messages isolate their paths. The Print preview zoom puts minus at the slider's left end and plus at its right in Arabic, as the status bar does.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic: mirror the label/field rows of the Font, Info, Paragraph, Page Setup, Insert Table and Options forms
rtl::Table lays a form out as an egui Grid in a left-to-right interface (same id, columns and spacing, so the output is unchanged) and, where the interface is mirrored, with each row's cells running from the right, every column as wide as its widest cell. File > Info puts Properties at the right and Statistics after it, and the Font dialog's kerning row reads 14 points and above.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Docs: record the mirrored Arabic interface in the roadmap, gaps and localization parity
Progress log, the Arabic language row, gap 11 and the status lines and revision histories of the three docs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Insert Hyperlink, Word Count, Phonetic Guide, New Style, Modify Style, Encrypt, merge rule and Match Fields dialogs read right to left
Their label-and-field rows use rtl::Table, so in a mirrored interface the label is at the right with its field to its left. rtl::Table gains striped() for Match Fields. English renders are pixel-identical (only a caret-blink strip differs in an untouched dialog).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Line Numbers, Hyphenation, Drop Cap, Caption, Cross-reference, Index, Mark Index Entry and Source dialogs read right to left
Their label-and-field rows use rtl::Table (label at the right, field to its left). English renders are pixel-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Picture Size, Split Cells, Convert Text to Table, Footnote and Endnote, Table Properties, Cell Options and Custom Properties dialogs read right to left
Their label-and-field rows use rtl::Table. Picture Size's Height and Width pairs read Height first at the right. English renders are pixel-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Define New Multilevel List, Track Changes Options, content control properties, custom colours and table style dialogs read right to left
Label, field and swatch rows use rtl::Table. English renders are pixel-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Columns widths, Special Characters, Compare, AutoCorrect and About Models tables read right to left
Table columns now start at the right edge (rtl::Table gains top(), min_row_height() and an unset column count). About > Models stays a left-to-right scroll box with its columns listed from the right. English renders are pixel-identical (only the caret blink differs).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: table-of-contents styles, recipient filter and sort, and the ribbon Paragraph group read right to left
rtl::Table now stacks its rows top to bottom whatever way the area around it runs (a ribbon group lays out in a row). English renders are pixel-identical (caret blink and a status toast differ in untouched dialogs). The TOC options and recipient filter/sort rows are converted by pattern and not seen in an image.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Columns preset tiles and Preview label, Macros, Add/Remove arrows, Zoom slider and the Recipient List grid
A preset tile is as wide as its caption and centres it (the long Arabic captions ran into the next tile); the Preview column keeps its width so its label no longer wraps letter by letter; Macros puts the name box and list at the reading start with the buttons beside them; the Add >> and << Remove arrows swap in a mirrored interface so they point the way the items move; the Zoom slider is drawn left to right as in English; the Recipient List data grid scrolls left to right instead of running past the left edge. English renders are pixel-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: the chart type and SmartArt layout galleries list their tiles from the right
The chart type dialog, the Insert SmartArt dialog and the ribbon's SmartArt Layouts gallery use rtl::Table rows, so the first tile sits at the reading start. English renders are pixel-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Take main's panes/host.rs after the merge
The merge kept a use of mirrored() that main's pane-dock fix removed; this branch does not touch the panes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Columns preset captions are translated in the mirrored tile
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic: one Arabic and one French row in the localization parity table; About > Models columns read from the right
The merge left the Arabic and French rows twice in docs/localization-parity.md; each
is now a single row (main's current French text, the Arabic row with the dialogs
note). About > Models keeps main's fitted table but lists its columns, their
alignments and grow weights reversed when the interface is mirrored, so Company
starts at the reading edge. The credits.rs change comes from tareq's helper merge.
Co-authored-by: tareq <tareq.halaby@gmail.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* About > Models: the mirrored table keeps its own widths and scroll position
The columns are listed in reverse when the interface is mirrored, and egui keeps the
resizable column widths and the scroll offset by id. A language switched at run time
would apply one direction's widths to the other's columns, so the mirrored table uses
its own id.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: About > Models alignment mirrors as its comment says
The mirrored Models table reversed the per-column right-align flags but
did not negate them, so in Arabic the text columns stayed left-aligned and
the counts right-aligned, the opposite of the comment ("counts line up on
the right (left in RTL)"). Negate after reversing so the text sits at the
reading edge and the counts on the left. Keep this line when touching
models_ui.
* About > Models: a mirrored table that overflows opens at its reading edge
The reversed Arabic table puts Company in the rightmost column, but the
horizontal scroll area started at the left, so when the table was wider
than the dialog (narrow window, widened columns) Company and Model were
offscreen. Stick the scroll area to the right in a mirrored interface.
---------
Co-authored-by: Abdelhaq El Amraoui <abdelhaqelamraoui@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: email <email@email.com>
Co-authored-by: tareq <tareq.halaby@gmail.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
- odf.preset: word (Microsoft Word style, default) or libreoffice; wordcraft still read as word
- Options, CLI (WORDCRAFT_ODF), docs and ROADMAP follow the two styles
- CodeRabbit: kept groups and story fingerprints follow fill_objects; padding serde default;
list field depth; literal doubled quote; ASK/SET results not exported; dc:date offsets;
first_pages order; unique picture names; finite raise clamp; PDF rotated-text guard;
.ott in the password message; flat element bounded by depth; cell tile fallback;
ROADMAP and docs reconciled
- Remove clean-room wording from the ODT tests
* Gap 4: flag document fonts that aren't installed
The font box (ribbon, mini toolbar, both Font dialog boxes) shows a
warning mark when the current font isn't installed; its hover note
names the font drawn instead. The status bar says "N fonts not
installed" and lists each with the faces drawn instead, plus Add Font….
Agents get `font.missing` and `format.state.fontShownAs`.
Layout records, where it picks a face per text segment, the fonts the
shown text asks for that aren't installed and the faces drawn instead
(ParaLayout::substituted, DocLayout::substituted_fonts), so hidden
text, unused styles and orphaned stories never count; list labels,
field results and East Asian and complex-script text do, and so does a
missing roman font drawn in Cambria. Metric-compatible substitutes and
symbol fonts drawn as Unicode aren't flagged. Bounded for hostile
files: 64 pairs a paragraph, 1024 a document, names kept to 256
characters, 20 rows in the popup.
Ported from alehel/wordcraft#1 onto current main; English strings only
(translations belong in storytold/craft-i18n).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: missing-font review fixes
- Judge a padded font name by the face layout drew for it, and skip blank names.
- Name the face CJK text is drawn in for a missing East Asian font, not Cambria.
- Keep the missing-fonts list per window, so several windows don't rebuild it every frame.
- Expose the font box's warning to screen readers as its description.
- Count missing fonts with craft-i18n plurals (`i18n::tn`).
- One table of metric twins (`fonts::word::METRIC_TWINS`): a missing font is drawn in an
installed twin first, and the warning reads the same table.
- Find the status-bar item by part of its label in the click test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: warn for a font picked at the caret; drop fonts of hidden list items
- At a collapsed caret the font box warns for a missing font typing will use, also at the end
of text in another font (it waited for the first letter).
- A list item that shows nothing (#3982) reports no missing font: its number was shaped, and
its font recorded, before layout dropped it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: say where Add Font… is, and that it needs the font file
The font box's note said "Add Font… installs it": Add Font… opens a file picker at the end of
the list, so it only helps with a font file at hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: docs say the popup lists 20, and when the font box warns ahead
CodeRabbit review: the status-bar popup lists the first 20 missing fonts, then a count; the font
box also warns at a collapsed caret for a font no shown text uses yet; `fontShownAs` is the
font's substitute, fallback faces aside.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: the Font dialog warns for a missing font over a selection
CodeRabbit review: the dialog's boxes show the fonts OK applies, which no shown text may use
yet (an Asian text font over Latin text, a font typed into the dialog), so they name the
substitute whatever is selected. The ribbon's box over a selection still reads the list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: the Asian font box names the face CJK text gets
CodeRabbit review: the status bar's list is by font name, so for a font Latin text also uses
it named the Latin text's substitute. The Asian text box now always looks up the face layout
draws Chinese, Japanese and Korean text in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: gaps.md status on the current line, missing-font scope and twin wording
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: gaps.md says which missing fonts are flagged and what font.missing lists
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: font.missing lists what the current view lays out
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts kept both sides. Where the two branches met:
- Para.mark: both added it. It stays CharProps (ODT needs it); HTML's
`<p> </p>` converts its Fmt, and to_doc reads it in one place.
- Arrowheads: the PR's line ends (top left to bottom right) with main's
arrowhead styles.
- inline_rect: the PR's depth below the baseline with main's rule room.
- Spacing and Cell literals take main's new fields.
- The zip entry guard (#3660) also covers the encrypted ODT paths, which
parsed a package before it; regression test in tests_more/encryption.rs.
- chunks_exact to as_chunks in odt/frames.rs and odt/path.rs (clippy).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A comment, tracked change or index mark reaching from one paragraph
into another, or a text box continuing in another's, ties paragraphs
into a group: written as read while every paragraph of it is as read,
else all from the model, which names what ties them afresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Opening draws charts from their data, which changed their paragraphs'
fingerprints, so their markup never matched. Drawing goes through
Document::fill_objects, which moves the kept markup with them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A section in a table's cell is kept around the cell's paragraphs and
closed in the cell. One starting or ending at a paragraph with twins
(an empty one) is kept by that paragraph's own identity; the writer
opens each kept section once, so a copy can't open it again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Every element of the ODF 1.3 schema checked against both corpora and a
document of our own for each one no file held (docs/odt-element-audit.md).
Found and fixed: index and reference marks over text, plugins and
floating frames weren't kept as read, and a form's data source was
taken for a control. Left: password and grid form controls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found checking 220 ODT files from other applications (local only):
- What a section hidden with text:display="none" holds is read as
hidden text, so it stays hidden written without the section.
- A top-level list whose items are all as read is written as read
whole (label spacing, picture bullets, list headers); a list going on
with it names it; a section may begin or end with a list.
- A damaged package's media type isn't printed in its error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A kept part without a media type (LibreOffice's empty accelerator
configuration, a file the manifest didn't list) was written without an
entry, and LibreOffice refused the saved file. ODF 1.3 Part 2 §4.3
lists every file; its media type stays empty as read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Paragraphs and tables holding bookmarks are kept; the writer takes
the names kept markup begins and ends, so either end may be as read.
- Footnotes and endnotes: each note's text fingerprinted when read;
kept while it and the note numbering are as read.
- Formulas, charts and other objects in kept markup, with their files
and preview pictures (only while that markup is written).
- Page-anchored frames LibreOffice puts in the text before a paragraph
are kept before its markup.
- Styles of different families sharing a name are kept apart.
- The text keeps text:use-soft-page-breaks while kept markup holds them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The paragraphs a drawing's text holds count as its story's, so the
paragraph showing it can be kept. Each text box's and shape's story is
fingerprinted when read (with the boxes and notes it holds); one edited
sends its paragraph to the model. Linked boxes and a comment inside a
box's text stay with the model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A paragraph whose comments (range, replies, end) or tracked changes
(start and end) are all its own is kept as read. Each top-level comment
is fingerprinted with its replies when read; one edited, resolved,
answered or deleted sends its paragraph to the model, as a reply written
elsewhere in the file does. The change regions kept markup marks are
written as read (those their deleted content marks first), beside
WordCraft's own, which take the writer's prefix; one without a date is
written from the model. An older file's region gets its ODF 1.3 xml:id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A paragraph or table holding pictures and shapes without text is kept
as read while unchanged, with the pictures it shows (only those linked
from markup actually written, so a deleted picture stays deleted) and
its frames' styles. Text boxes, shapes with text, objects and controls
stay with the model. The file's Graphics and Frame styles are written as
read; WordCraft's own pictures don't take the file's picture names. The
flat writer inlines a picture whatever form its element has.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A section at a text box's top level keeps its wrapper, like the body's;
the box's story closes whatever it opened, so an edit inside the box
can't leave a section open past it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Kept definitions are keyed by name space (a list style may share a
paragraph style's name, §16.2), so unused list styles survive a save.
- Section wrappers kept as read go around tables as well as paragraphs,
nested ones too, balanced however the blocks are edited.
- An unchanged chart keeps its replacement picture.
- Note citations use the document's number format and first number.
- draw:z-index is read into the float's stacking order and written back.
- The no-edit gate counts elements, catching a section of three lost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The file's office:forms keeps its own head and each form:form with what
WordCraft doesn't model; controls are merged with the file's own. An
index whose generated entries nobody edited is written as read, with
its source settings and entry templates; an edited one is written from
the model. The no-edit gate saves nine samples of our own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The standard (§18.3.1, since ODF 1.2) reads an angle without a unit in
degrees. WordCraft read unitless gradient angles in tenths of a degree in
every file; in ODF 1.3 files it now follows the standard.
LibreOffice and OpenOffice wrote gradient and hatch angles in tenths of a
degree in their files before ODF 1.3 and still read them so; read by the
standard, every older LibreOffice document's gradients would turn the
wrong way. So in files older than 1.3 they are still read in tenths: a
deviation from the standard, out of necessity, now listed in
docs/odf-interpretation.md (a new rule and section for such deviations).
WordCraft always writes these angles with a unit.
The ODF 1.2 -> 1.3 changes (ODF 1.3 Part 3 Appendix G and their OASIS
issues) were checked: none other gives a value written for 1.2 another
meaning; the new defaults match how LibreOffice already read them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
More of what WordCraft doesn't change is written back as it was read:
tables; the document properties it doesn't model (template, statistics,
editing time, printing); the declarations opening the text and the
document's scripts; page layouts and master pages while the page setup
is unchanged, each section starting with the master page it started
with; named sections around unedited paragraphs. Package files and
folders kept markup names (a style's fill picture, a header's embedded
object) are written with it, with their manifest entries.
Files are saved as ODF 1.3; markup kept from an older file is brought to
1.3's meaning (a gradient's or hatch's unitless angle was tenths of a
degree). WordCraft's own drawings state style:flow-with-text, which the
standard leaves open.
odt::tests_more::lossless saves seven samples of our own without edits
and fails on anything lost (nothing is allowed), and checks that edits
keep the saved parts well-formed.
Over the 133-file corpus the round-trip difference falls from 191k to
51k pixels; 95 files come back pixel-identical (62 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A placeholder test used the label <Title>, which LibreOffice's letter
templates also use; it now uses an invented one. The kept-markup tests
say that every sample (and any sample file the no-edit loss gate adds)
is written by us, never taken from someone else's files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A paragraph nothing changed since it was read (by a fingerprint of how it
was modelled) is written with its own markup, unknown elements and
attributes included, together with the automatic styles, common styles,
font faces, namespaces and package files it names. While no style or list
definition changed, styles.xml's office:styles is the file's own (unused
list styles, note and line numbering configuration, attributes WordCraft
doesn't read); drawing definitions and the default graphic style stay
WordCraft's, as drawings are written from the model.
Paragraphs holding drawings, comments, tracked changes or ranged marks,
or starting a page, are written from the model, as is everything edited.
Kept markup lives in memory only (wordcraft_doc::kept, 64 MB at most) and
is never part of the document's content.
Over the 133-file corpus the round-trip difference in LibreOffice falls
from 528k to 191k pixels; 62 files come back pixel-identical (46 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice reads an author field without text:fixed as fixed (the
standard says not fixed) and writes false itself; an empty author field
stayed empty after a save instead of showing the author. Sender fields
are written as LibreOffice writes them, without the attribute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The default paragraph style's text colour (fo:color) is read into the
document's default and written back: Google Docs' grey résumé came back
black. What a covered table cell holds (not shown, §9.1.5) is skipped;
it went before the table as empty paragraphs, pushing ONLYOFFICE's table
down the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Widow and orphan control a file doesn't state is open in the standard
(it refers to XSL, whose initial value is 2); LibreOffice reads it as
off. It is now a profile case (widowControl), and WordCraft writes it on
its default style either way.
LibreOffice measures tab stops from the paragraph's indent when its
TabsRelativeToIndent setting is on (its default), default stops too
(measured). WordCraft's layout now counts default stops from the indent
for such documents, and writes them back measuring from the indent
instead of from the margin, which moved LibreOffice's default stops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Kerning turned on or off span by span (style:letter-kerning) is read and
written where it differs from the document's default; without it
LibreOffice set ONLYOFFICE's text wider and every page broke elsewhere
after a save. A note reference keeps its own formatting (a span around
text:note), and a note starting with a tab keeps it: only the space
WordCraft puts after the mark is left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A fixed date or time (text:fixed) opens as a locked field and is written
back fixed, with the value its text shows under its picture
(text:date-value / text:time-value), which LibreOffice needs to keep it.
One whose text can't be read back is written as text. Sender fields open
as SENDER fields and are written back as text:sender-*.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
User fields (text:user-field-decl, text:user-field-get: LibreOffice's
report templates, 58 documents of the corpus) were read as custom
document properties and written back as meta:user-defined properties
with text:user-defined fields: the look and values stayed, the kind
didn't. Custom properties now remember being user fields
(CustomProp::odf_user_field) and are written back as
text:user-field-decls with their values and text:user-field-get fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found by a loss check comparing what a file uses with what a WordCraft
save writes back (over the 133-document local corpus):
- Every font a file uses is declared (office:font-face-decls, §16.21:
family, generic family from its name, pitch) and named where it is
used (style:font-name and its Asian and complex forms beside the
fo:/style: font families), so a reader without the font picks a like
one. Lost in 132 documents.
- meta.xml keeps who changed the document last (dc:creator), when
(dc:date) and how often it was saved (meta:editing-cycles), read into
and written from CoreProps. Lost in 120.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with Google Docs' and ONLYOFFICE's ODT files (now in the local
corpus):
- A heading's look comes from its style, its outline level from its
element: a text:h without text:outline-level (level 1, §19.850.4) in a
style based on Heading 2 looks as Heading 2, as LibreOffice draws it,
and keeps outline level 1 (it became Heading 1). The level is carried
apart from the style (ParaProps::outline_level through the flow) and
written back as text:outline-level.
- Widow and orphan control is stated in the default paragraph style
(fo:widows/fo:orphans 2) when Normal has it: LibreOffice reads none
unsaid as off, so pages broke differently.
Google's 2011 Fundamentals export now round-trips with its 88 pages and
its text (1.27M -> 139k differing pixels).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found by comparing WordCraft's own documents in LibreOffice and
bisecting settings.xml (observed only, black-box):
- The settings set's name, ooo:configuration-settings, is a qualified
name: without xmlns:ooo declared LibreOffice ignored every setting
WordCraft wrote (its Word layout rules, tab measuring).
- A set LibreOffice reads must be complete: a setting missing from it
gets its old pre-OpenOffice value, which changes the layout. WordCraft
now writes LibreOffice's current layout defaults (as a new LibreOffice
document has them), overlaid with Word's layout rules for documents
WordCraft lays out as Word does (not for ones opened from ODF), and
its own choices.
- How the space between paragraphs adds up follows a file's
AddParaTableSpacing (false: the larger, as Word); unstated, the ODF
profile's new sumParagraphSpacing (WordCraft: the larger, LibreOffice:
the sum). WordCraft writes it as it lays the document out.
WordCraft-origin documents in LibreOffice: 142k -> 80k differing pixels
over 42 documents; the LibreOffice corpus: no regressions, 40k -> 39.6k.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice's elegant agenda puts a small gradient picture behind cells
(§17.3 style:background-image in a table-cell style). Cells can now
carry a background picture (CellProps::picture: media, own size,
repeat none/tile/stretch, alignment), which layout draws over the
cell's shading and under its text, clipped to the cell (Placed::Clip of
Placed::Image; tagged as an artifact in PDF). The ODT reader reads it
(style:repeat, style:position, the picture from the package) and the
writer writes it back with the picture in the package (once per
picture).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
WordCraft's watermark was left out of ODT files. It is now written as
LibreOffice and Word write theirs: a fontwork custom shape
(draw:text-path, draw:type fontwork-plain-text) named
PowerPlusWaterMarkObject, behind the text and centred on the page, in
every header (a header made for it alone fits inside the top margin, so
the text doesn't move). It doesn't grow to its text (draw:auto-grow-*
false), which LibreOffice needs to stretch the text to it. Such a shape
in a header, from LibreOffice or WordCraft, opens as the document's
watermark (text, colour, see-through, turn, size), not as a shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found by saving WordCraft's own documents as ODT and opening them in
LibreOffice (42 documents: templates, themes, one per feature).
- Table styles: ODF has no table styles of Word's kind, so each cell now
states what the style gives it (fills, its borders, bold header text,
its paragraph spacing), under the paragraph's own styles as layout
applies them (ECMA-376 §17.7.2; StyleSheet::table_para_patch and
table_char_patch). The style's border mapping moved to
styles::part_borders, shared with layout.
- Tables read from ODT no longer get Word's Table Grid style, whose
single spacing changed their text.
- Columned sections say whether their columns end balanced
(text:dont-balance-text-columns, §20.428, read as its name says; its
value descriptions contradict it). Unstated, the ODF profile decides
(balanceSectionColumns: LibreOffice balances, WordCraft as Word).
SectionProps::balance_columns carries it to layout.
- SmartArt diagrams are saved as a draw:g of their shapes and text (with
an invisible frame keeping the graphic's box), which LibreOffice draws
as WordCraft does.
LibreOffice round-trip corpus: 40k differing pixels over 125 documents
(was 76k), 42 identical; WordCraft-origin documents 277k -> 145k.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODF 1.3 §20.206–20.209 define page margins by referring to XSL's
(§7.10), without saying whether XSL's initial value 0 comes with the
reference; LibreOffice uses 2 cm. The margin a page layout leaves out
is now the profile's `pageMargin`: WordCraft 0 (XSL's), LibreOffice
2 cm, or custom (File › Options › Advanced › OpenDocument files,
`file.options` `odf.pageMargin`). A stated margin is the file's under
every preset. With the LibreOffice preset, LibreOffice's CV, sans-serif
business letter and one-page résumé templates match LibreOffice again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- File › Options › Advanced › OpenDocument files: the preset is a
drop-down like Measurement units and Region; shape line width is in
points (a 0.25 pt hairline showed as 0.00").
- WordCraft's preset keeps a header's or footer's stated spacing when
its style doesn't say whether it is dynamic: with no height stated,
growing into it always dropped the spacing the file states. Custom
profiles can still grow into it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
What ODF 1.3 defines, including XSL's initial values for fo: properties,
is followed whatever the settings: unstated page margins are now 0, and
padding without a border takes room in every ODT document. What the
standard leaves to the application (Part 3 §16.2) is an OdfProfile:
WordCraft's own choices by default, LibreOffice's, or custom values.
The profile fills in only what a file leaves unsaid; stated values win
under every preset (tests_more/profile.rs).
Cases: header height, how its height and spacing combine, dynamic
spacing, tabs from the indent, column gap, shape fill, line and
hairline, page frames against a paragraph, in-line frames against a
paragraph or page, and lines of only pictures.
- doc: odf_profile module; Settings::odf (OdfLayout) replaces odf_layout
- formats: import_with / import_flat_with take a profile
- engine: Prefs::odf, file.options `odf`, opened files follow it
- ui: File > Options > Advanced > OpenDocument files (presets, custom cases)
- cli: WORDCRAFT_ODF=wordcraft|libreoffice
- docs: odf-interpretation.md lists every case (law, open, writer
choices, Google Docs and ONLYOFFICE findings); parity docs, gaps,
ROADMAP updated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Form controls:
- Placement: a control hanging from the baseline (as LibreOffice places
those it makes) was saved with no placement. LibreOffice then puts it
at the line's top, so every control row shrank. Its raise is now kept
(`ContentControl::form_raise`, read as in-line frames are) and
written, standing on the baseline when it had none.
- Multi-line fields: these were saved without LibreOffice's `MultiLine`
property, so they showed one centred line. They now carry it.
- Text fields get LibreOffice's flat border.
- Also: `pt()` no longer writes "-0pt".
Custom shapes:
- A shape drawn from its own geometry was saved as WordCraft's path of
it, and a preset WordCraft draws as a rectangle (a cloud) was saved as
a rectangle. The geometry as read (type, path, equations, handles) is
now kept (`ShapeExtra::odf_geometry`) and written back, mirrored as
the shape now is.
- `shape.change` and the ink-to-shape conversion drop the kept geometry,
since the shape is then another kind. `ShapeExtra` holds it as an
`Arc<String>` to keep `Placed::Shape` small.
- The reader's `end` hands skipped elements to `end_skipped`.
Results:
- The forms document round-trips pixel-identical (2,949 pixels before),
with its text the same.
- The shape presets fall from 2,467 to 1,220. The rest is LibreOffice's
hairline outline, which WordCraft keeps as 0.5 pt.
- Across the corpus the round-trip difference falls from 45k to 41k
pixels. 42 of 125 documents round-trip pixel-identical, 92 with their
text the same, and none gets worse.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's style templates.
- An in-line frame placed against the text (`style:vertical-rel` "text"
or "char", with `middle`, `top` or `bottom`) stood on the baseline. It
is now raised or lowered against the text's height, from the size of
the text it sits in. Measured in LibreOffice, `middle` centres the
frame on the text: its middle half the ascent less the descent above
the baseline. Placement against the line still stands on the
baseline.
- An in-line picture or chart was saved with no placement style. In
LibreOffice that puts it at the line's top (measured: the title beside
the Finances logo moved up 19 pt). It is now saved standing on the
baseline ("top" to "baseline"), as WordCraft and Word show it.
Two templates' round-trip differences fall from 1,874 and 1,433 pixels
to 521 and 586. Across the corpus the difference falls from 47k to 45k,
and no document gets worse.
Left in these templates:
- template instantiation: LibreOffice refreshes a template's fixed dates
and fills its sender fields from the user's data when opening it;
- a first-page footer's frame placed against the page content;
- a page background picture centred in the margins rather than on the
page.
gaps.md row 19 lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Charts: on save, WordCraft wrote each chart anew from its chart model
(type, data, title, legend, labels), losing the look it doesn't model:
fonts and sizes, axis and grid lines.
- The chart object as read is now kept with the chart (`Chart::source`
in the flow model, `Graphic::source` in the document).
- `chart::redraw` keeps it when the chart is drawn again on open or
resize.
- The ODT writer writes it back whole while it still reads as the same
chart. An edited chart is written from the model, as before; DOCX
still writes charts from their model.
- The chart documents now round-trip pixel-identical (chartcol: 5,917
pixels to 0).
LibreOffice's serif business letter (4,223 pixels to 116):
- A text frame outlined by a drawn stroke (`draw:stroke`, a 0 width a
hairline) and no `fo:border` lost its outline. It is now the frame's
line; an explicit "none" border still wins.
- A header or footer whose style states no height, or that has no style,
was 0 tall. LibreOffice makes it 0.5 cm (measured by varying the
height), so the text had moved up 1.5 pt.
Across the corpus the round-trip difference falls from 60k to 47k
pixels, and 41 of 125 documents round-trip pixel-identical. No document
gets worse.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's agenda templates. Saving wrote a table's first
row as `table:table-header-rows`, which LibreOffice repeats at the top
of each page, so a table crossing a page showed its first row twice
("<bring>" twice). The cause: the flow model marks a first row as a
header when the table style's look has a header row, which Word's look
has by default. That only affects the style's formatting, and HTML and
Markdown still rely on it for heading cells.
- The flow table now carries how many rows repeat
(`TableExtra::header_rows`, Word's Repeat as header row).
- The ODT writer writes only those rows as header rows.
- That extra block is now always kept for ODT flows, where it had been
dropped from tables without an alignment, width or row height.
All 16 agenda templates now round-trip with their text the same; the
modern and simple ones fall from about 5,200 differing pixels to 145.
Across the corpus, documents with the same text after a round trip rise
from 75 to 91 of 125, and the round-trip difference falls from 82k to
60k pixels.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's report templates and one-page résumé.
Padding without a border:
- LibreOffice gives a paragraph's padding room on every side, with or
without a border; measured, 0.5 in of padding and no border moves the
text 0.5 in in and down. WordCraft dropped it.
- The reader now keeps it (an invisible border holding the space; a new
`Borders::takes_room()`), and the writer writes it back.
- The layout adds it to the spacing or indent under the document
setting, now renamed `Settings::odf_layout` to cover all of
LibreOffice's layout rules. `ResolvedPara::padding` carries it, so
Word documents are unchanged.
Columns:
- Unequal columns (`style:column` relative widths and indents, on pages
and sections) were made equal. They now keep their widths and are
written back as shares. The writer leaves out `fo:column-gap` beside
them, because LibreOffice ignores a page's column widths when a gap is
stated (checked).
- Columns with no stated gap have none, where they had Word's 0.5 in.
- A paragraph starting a column (`fo:break-before="column"`) now starts
with Word's column break, written back on the paragraph.
The report templates round-trip to 0-11 differing pixels (from up to
9,600), and the résumé to 24 (from 6,286) with its text the same.
Across the corpus the round-trip difference falls from 155k to 82k
pixels. WordCraft's own rendering difference falls from 469k to 428k,
and no document gets worse.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice sizes a line holding only in-line pictures by the pictures
alone. Its font's ascent and descent don't count: measured in
LibreOffice, a 3.6 pt rule's line is 3.6 pt, where WordCraft (following
Word) made it 15.5 pt.
- A new document setting, `Settings::object_lines_fit_objects`, makes
the layout size such lines by the pictures alone. The ODT reader turns
it on, so other documents keep Word's layout. An equation or any text
on the line keeps the font's height.
- A document with no page layout at all (no styles.xml) opened on Word's
Letter page with 1 inch margins. It now uses the region's paper with
LibreOffice's 2 cm margins.
WordCraft's own rendering of LibreOffice's elegant agenda comes close to
LibreOffice's: its pixel difference halves, from 17,428 to about 8,950.
Across the corpus WordCraft's rendering difference falls from 487k to
469k. No document gets worse, and round trips are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's elegant agenda:
- Paragraph size: a paragraph holding only pictures lost its own font
size, since the flow model's picture runs carry no formatting, so its
line grew. Such a paragraph (`Para::textless`) now keeps its formatting
as the paragraph mark's, which its pictures' runs take too. Read and
written as for empty paragraphs.
- Raise: an in-line frame's raise was dropped when a parent style gave
it a relation no in-line frame has (the Graphics style's
`style:vertical-rel="paragraph"`). It is now measured from the baseline,
as LibreOffice reads it.
The agenda's round-trip difference falls from 6,542 to 1,331 pixels, and
two business fax templates' from about 5,300 to 270 and 350. Across the
corpus it falls from 185k to 155k. No document changes page count.
WordCraft's own rendering of a picture-only line is still taller than
LibreOffice's, a layout difference left for later.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's report templates.
Tables:
- A table filling the margins (`table:align="margins"`) but wider than
them ran off the page. It is now fitted to the text width, its columns
in proportion. Checked in LibreOffice, which fits such a table with or
without relative column widths; the old test's claim otherwise is
corrected.
- A table's own space above and below (`fo:margin-top` and
`fo:margin-bottom` on the table) was dropped. It is now read into new
`TableProps::space_before`/`space_after`, laid out (added to the
spacing of the paragraphs around it, as LibreOffice does) and written
back.
Headers and footers:
- A header taller than its band lost its spacing from the body, moving
the text up. LibreOffice keeps the spacing below the content unless it
is dynamic (`style:dynamic-spacing`, which behaves as Word does). New
`SectionProps::header_gap`/`footer_gap` hold it. The layout keeps it
past a tall header or footer, and the writer writes it back. Word keeps
no gap, so DOCX documents are unchanged.
Across the 125-document corpus, the round-trip pixel difference falls
from 241k to 185k, and WordCraft's own rendering's difference from
LibreOffice's falls from 562k to 484k. No document changes page count or
gets worse; one report template falls from 7,646 to 602 pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A frame that doesn't state its anchor or position now takes them from
its graphic style chain (ODF lets `style:graphic-properties` carry
`text:anchor-type`, `svg:x` and `svg:y`).
LibreOffice's business card template relies on this:
- its Labels style holds the card in line;
- its Graphics style puts the credit at the top of the page.
WordCraft had floated the card, so a save scrambled the layout and added
a page.
Across the 125-document corpus, no document now changes page count
through a save (3 did), and the round-trip pixel difference falls from
333k to 241k. gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Saving LibreOffice's letter and fax templates added a page and moved
their logos. Fixes:
- Default font size: the writer always wrote the default style's size as
12 pt. It now writes Normal's size (11 pt in these letters).
- Page-anchored frames: a frame placed "from the paragraph" was moved by
the page margin. A page has no paragraph, so it is now placed from the
page, and "from the paragraph's content" from the page's text area, as
LibreOffice places it.
- Text box styles: a custom style used only by paragraphs in text boxes
(the fax's centred "Frame contents") was dropped. The style pass now
walks text boxes and groups.
- Empty paragraphs: an empty paragraph's own font size (its height) was
dropped. It is now kept as the paragraph mark's formatting, a new
field in the shared flow model that the ODT reader and writer use.
The marine, fax and bottle letters keep one page after a save. Over the
125-document corpus the round-trip pixel difference falls from 408k to
333k, and the business letters' differences roughly halve.
Also moved a doc comment that 48c60a5 left on the wrong function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
gaps.md row 19 now lists what's left in ODT:
- turned characters;
- conditional text;
- control looks;
- embedded spreadsheets' contents;
- dates in non-US locales.
The ROADMAP progress log has the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- The document's default font size (`fo:font-size` in
`style:default-style`, or the Standard style's: the document's
defaults) was ignored, leaving 12 pt; LibreOffice's letters are
10 pt. It is now the document's default. A named style with no parent
keeps the default style's size, as ODF inherits it.
- A date style ordered by the locale (`number:automatic-order`) showed
its parts as listed: "2026November2". It now shows in US English
order: "November 2, 2026", or "11/2/2026" with a numeric month, as
LibreOffice shows it here.
WordCraft's own rendering of LibreOffice's letter templates comes much
closer: one letter goes from 11,377 to 6,237 differing pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A page layout's background picture (`style:background-image`, §17.3:
LibreOffice's report templates' world map) was dropped. It is now
WordCraft's page picture (Design › Page Color › Fill Effects) and is
written back with its picture:
- repeated: tiled;
- stretched: stretched;
- not repeated: centred at its own size, a new fill that the screen
renderer and PDF export draw in the middle of the page. DOCX writes
it stretched, as VML has no centred fill.
LibreOffice's report template with the map goes from 11,719 to 3,602
differing pixels, and shows the map after a save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- A table starting a page (`fo:break-before="page"` on the table,
LibreOffice's agenda minutes) didn't. Its first paragraph now has a
page break before it, and it is written back on the table.
- Layout: a page break before the paragraph in a table's first row
now moves the whole table to the next page, as Word does. It was
ignored; this matters for Word documents too.
- ODT tables are now fixed layout (Word's `w:tblLayout`).
LibreOffice's columns never fit their content.
LibreOffice's 6orange agenda now round-trips with the same text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Word's Font › Emphasis mark (a mark over or under each character, used
to stress East Asian text) wasn't modelled. DOCX `w:em` and ODT
`style:text-emphasize` were dropped.
- Model: `CharProps::emphasis` (dot, comma, circle, dot below, or none
to turn off a style's).
- Layout: a mark is drawn over (or under) each character, and the line
is made taller to fit it.
- `format.emphasisMark {"mark": …}` sets it, and Format state reports
it. The catalog lists it under Home › Font.
- DOCX: `w:em` is read, and written in its place in `w:rPr` (after
`w:cs`, before `w:lang`).
- ODT: `style:text-emphasize` maps to it: an accent is the comma, a
disc the dot, a dot below the under-dot.
Also fixes two problems from the previous commit (ODT variables):
- Field updates blanked a SET field's stored result. The layout
already draws nothing for SET, so its result stays as it was, as
`ask_and_set_fields_draw_nothing` pins.
- A clippy lint (manual char comparison) failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- ODT variables (`text:variable-set`, `text:variable-get`, §7.4) were
read as their text. A set is now Word's SET field, followed by a REF
when it is displayed; a get is a REF. They are written back declared
(`text:variable-decls`), the sets hidden with a get after them.
- The engine now evaluates SET (ECMA-376 §17.16.5.58). It shows
nothing and gives its name a value. REF to a name no bookmark has
shows that value; a REF before the SET shows the last value set, as
Word's bookmark does after an update. This works for Word documents
too.
- Custom character styles now keep what they turn off under a parent
that turns it on (`fo:font-weight="normal"`…), as paragraph styles
already do.
A LibreOffice document with variables round-trips with the same text,
and the saved file validates against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
gaps.md row 19 now lists what's left in ODT after the template work:
- emphasis marks and turned characters, which have no WordCraft model;
- variable fields;
- control looks;
- embedded spreadsheets' contents;
- tables wider than the margins;
- what a character style turns off.
The ROADMAP progress log has the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
More from LibreOffice's bundled templates:
- WMF and EMF pictures (LibreOffice's fax and letter logos) were
dropped. They now open as pictures, at their own size (a placeable
WMF's or an EMF's frame) when the frame gives none, and are saved
with their media types.
- A floating picture inside a text box, saved by WordCraft, didn't
show in LibreOffice. Picture frames' styles now derive from the
Graphics style (declared, like Frame), which LibreOffice needs to
show it.
- A named paragraph style turning off its parent's bold (agenda
dates under bold titles) showed bold, both in WordCraft and in
LibreOffice after a save. What a style turns off (bold, italic,
underline, strike-through) is now kept and written.
- Columns' shares (`style:rel-column-width`) are now written only for
tables filling the margins. LibreOffice lays out an aligned table
oddly with them: WordCraft's sample table lost its proportions.
- Drawings waiting for a paragraph now join only a paragraph of the
text itself, never one in a text box or note.
With the harness's paper setting fixed (it had made LibreOffice draw
unsized pages as Letter), most of the 78 templates compare much closer
page by page. The CV template and the contour sample are now
pixel-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
More fixes from LibreOffice's bundled templates:
- A page margin the file leaves unsaid was 1 in, and a page layout with
no properties at all was ignored. Both now take LibreOffice's
defaults: 2 cm margins (observed black-box). LibreOffice's CV template
now lays out as LibreOffice draws it.
- Drawings anchored outside any paragraph (LibreOffice's
page-anchored frames before the first paragraph) made a paragraph of
their own. That paragraph started the document on the default page
style rather than the one the first paragraph asks for, which put the
running header on a letter's first page and pushed the letter onto
two pages. They now join the next paragraph; before a table or at the
end, they still get a paragraph of their own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice ships 78 text templates (letters, faxes, agendas, reports,
CVs). Round-tripping them through WordCraft exposed these problems:
- Placeholders (`text:placeholder`, "<Recipient's name>") were
printed text. They now open as plain-text content controls showing
them, so a click selects one and typing replaces it. Their
description becomes the control's title. They are written back as
placeholders of their type while they still show; what was typed is
written as text.
- A fixed date with neither a value nor text was lost. It is now a date
field.
- `text:author-name` opens as USERNAME, which the engine left empty.
USERNAME and USERINITIALS now show the user's name and initials
from File › Options; a quoted text after the name is shown instead.
- A "First Page" master followed by another (`style:next-style-name`)
put the running header on the first page. It is now a section with a
different first page: its own (empty) header on page 1, the next
master's after that.
- A page layout without a size was Letter. It is now the default
page's, the user's region's, as LibreOffice uses its default paper.
ODT import takes the regional page like the other importers.
- Style-only templates (no `content.xml`) failed to open. They now
open empty with their styles.
- Table columns are now written with their share of the table
(`style:rel-column-width`) too. A table filling the margins of a
narrower page keeps its proportions in LibreOffice.
All 78 templates now open and round-trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Hidden form controls (`form:hidden`: a name and value the form sends,
not shown) were dropped. They are now kept verbatim, with their
properties, and written back into the form with ids of their own.
- OpenDocument text templates (`.ott`) weren't opened. They now open
as documents, and save as templates: the template's media type goes
in `mimetype` and in the manifest's root entry (ODF 1.3 Part 2 §3.3).
This works with a password too. `.ott` is listed wherever `.odt` is:
open and save formats, file dialogs, export, the web app and its
media type.
A LibreOffice template round-trips with the same text. 300 fuzzed
documents exercising custom shapes, cell formulas, kept parts, turned
text and forms caused no crashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
After custom shapes, cell formulas, kept package parts, turned text,
and control sizes and buttons: ODT is ~90% in file-format-parity (20–35
h left). gaps.md row 19 lists what's left in ODT:
- emphasis marks;
- turned characters;
- conditional and variable fields;
- hidden controls and control looks;
- embedded spreadsheets' contents.
The ROADMAP progress log has the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review fix. A button kept verbatim was still written as the button
after its content control was made another kind (a check box, a list).
The kept form XML is now used only while the control is still the
plain text control it was read as.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
On save, form controls were sized to their text. Buttons and other
controls WordCraft has no kind for were dropped.
- Sizes: each control keeps the size of its `draw:control`
(`svg:width`, `svg:height`), stored on the content control.
- Buttons, image buttons, labels, file and value-range controls, frames
and generic controls now show their label, locked, as a content
control. Their form XML is kept verbatim and written back as it was,
so LibreOffice's button round-trips as a button.
- A kept control that names a namespace WordCraft doesn't declare is
left out. So is one longer than 64 KiB.
- The `ooo`, `oooc`, `script` and `dom` prefixes that LibreOffice's
controls use are declared, so the content validates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Text turned down or up opened across. This covered table cells and text
boxes (`style:writing-mode` §20.404, and LibreOffice's own
`loext:writing-mode="bt-lr"` for upward text).
- Table cells now open and save with WordCraft's cell text direction.
- Text boxes now open and save with their text direction.
LibreOffice's turned cells render as LibreOffice draws them. LibreOffice
reads WordCraft's turned text boxes back identically.
Turned characters (`style:text-rotation-angle`) still open upright:
WordCraft has no turned characters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Package parts WordCraft doesn't read were dropped on save. These
include LibreOffice Basic macros and dialogs (`Basic/`, `Dialogs/`),
`manifest.rdf` and other applications' files. They are now kept as
they are and written back with their manifest entries. LibreOffice
finds a round-tripped document's macro intact; WordCraft never runs
macros.
Dropped on save: the old thumbnail, and digital signatures
(`META-INF/*signatures*.xml`), because they sign the package as it
was. Also dropped: paths that climb out of the package, and paths
WordCraft writes itself (pictures, embedded objects).
Kept parts are bounded: 1000 files, 64 MiB in all. They survive
password protection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT cell formulas (`table:formula`) were lost, and only their value was
kept as text.
- Reading: LibreOffice's formulas, in its own syntax
(`ooow:sum <A1:A3>`, `<B1>*<B2>+<B3>`, `mean`, `min`, `max`,
`product`, `abs`, `sign`, `count`, `add`/`sub`/`mul`/`div`/`pow`, lists
joined by `|`), open as Word formula fields (`=SUM(A1:A3)`) showing
the cell's value. Update Field recalculates them.
- Writing: formula fields in cells are written back as cell formulas
with their value. ABOVE, LEFT, BELOW and RIGHT become the cells they
name.
- Cells holding a plain number are written as numbers, so LibreOffice
calculates with them. The `ooow` prefix is declared; without it,
LibreOffice reports "Expression is faulty".
A formula one side can't say keeps its value as text. A LibreOffice
table of sums round-trips with the same text, and the saved file
validates against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review fixes to the custom shape geometry:
- An equation that couldn't be worked out wasn't remembered. Equations
each referring twice to the next, ending in one that refers to
itself, took about 2^64 steps. Each equation is now worked out once,
failures and those being worked out included.
- Points far outside the view box (1e300) became infinite as f32. They
are now clamped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A custom shape whose type WordCraft has no kind for (hexagon, octagon,
smiley, can, moon, lightning, block arc, cross, pentagon, flowchart
shapes…) opened as a rectangle. It is now drawn from its own geometry
(ODF 1.3 §10.6.2) as a freeform:
- the enhanced path (§19.145): moves, lines, curves, ellipses (T, U),
arcs (A, B, W, V) and quarter ellipses (X, Y), arcs followed in
short straight steps;
- its values worked out from the shape's equations (§10.6.3) and
adjustment values: numbers, other equations, modifiers, the frame's
keywords, + - * /, abs, sqrt, sin, cos, tan, atan, atan2, min, max
and if.
Path length, equation count and nesting, and points are all bounded.
LibreOffice fills no part of a `draw:path` that has an open part. So a
freeform with both closed and open paths is now saved as a custom shape
whose path leaves the open ones unfilled (`F`).
LibreOffice's preset shapes render as LibreOffice draws them. The
saved file validates against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT citations (`text:bibliography-mark`) were read as their text, and
the sources they carry were lost. So an ODT bibliography had nothing
to update from.
- Reading: each mark is now a `CITATION` field of its source, which
becomes one of the document's bibliography sources (the first mark
of an identifier gives it). The identifier is the tag, the type the
kind, plus author, title, year, publisher, address, journal, volume,
pages, URL, editor, edition, number and month.
- Writing: a `CITATION` of a known source is written back as a mark
carrying the source.
References › Update rebuilds an ODT bibliography from them. A
LibreOffice citation and bibliography round-trip pixel-identical and
validate against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Linked text boxes (`draw:chain-next-name` on a `draw:text-box`, naming
the frame its text continues in) lost their link on open. The
following box's text was gone, since only the first box holds the
story.
- Reading: each box keeps its frame name and the name of the next box.
Once all boxes are made, names are resolved to WordCraft's link
(`TextBody::next`). A link to a missing box, to the box itself or
round in a loop is dropped.
- Writing: each box is named after its story, and a linked box names
the next one.
Flow model: `BoxLook::chain` (name, next).
A LibreOffice chain round-trips pixel-identical, and WordCraft flows
the text from the first box into the second. A linked box keeps its
own height where LibreOffice grows it to the rest of the text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Two picture properties were dropped.
- Crop: `fo:clip` (top, right, bottom, left, in lengths of the
picture's own size, its pixels at 96 per inch) is now WordCraft's
crop fractions. A picture's crop is written back as `fo:clip`, with a
graphic style even for an in-line picture.
- Contour wrapping: `style:wrap-contour` with
`style:wrap-contour-mode` `outside` is Word's Tight wrapping, and
`full` (the default) is Through. The frame's `draw:contour-polygon`
becomes the wrap polygon, scaled from its view box. A polygon whose
points lie well outside its view box is in another coordinate system
(LibreOffice writes one for some pictures), so the outline is
derived from the picture instead. All of this is written back, the
polygon last in the frame.
A cropped LibreOffice picture round-trips pixel-identical, and a
contour-wrapped one round-trips with WordCraft wrapping text as
LibreOffice does. Both validate against the schema. `Body::start`'s
frame parts moved to a helper (clippy's complexity limit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
This round added flow breaks, table placement, ruby, index marks, text
borders, connectors and custom properties. A 20,000-paragraph document
round-trips in about a second with nothing lost, and 400 fuzzed
documents opened without a crash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the custom property commit: an ODF date property may be a
bare date, while Word's `filetime` (which a DOCX save writes) wants a
UTC date and time. A bare date is now midnight UTC, a date and time
without a zone is taken as UTC, and a value that isn't a date is kept
as text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT custom document properties (`meta:user-defined` in `meta.xml`) were
dropped, and the fields showing them or a user field were read as
plain text.
- Properties: `meta:user-defined` values (string, number, true/false,
date) are now WordCraft's custom document properties, and so are
user field declarations (`text:user-field-decl`).
- Fields: `text:user-defined` and `text:user-field-get` are Word's
`DOCPROPERTY` fields, which WordCraft updates from those properties.
- Writing: custom properties go back into `meta.xml`, and a
`DOCPROPERTY` naming one is written as `text:user-defined`.
A LibreOffice document with a custom property and a user field
round-trips pixel-identical, valid against the schema. Its user field
comes back as a document property.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the connector commit: a connector inside a drawing group
wasn't converted. Its group's shapes weren't recorded, so its ends
lost their glue. Group members are now recorded for their kind (never
for placement, since they sit in the group's coordinates), and
connectors in groups have their glue points made their shapes' sites
too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT connectors (`draw:connector`) were left out. They now open as
WordCraft connectors: straight (`line`), curved (`curve`), or with
bends (`standard`, `lines`).
- Gluing: each end is glued to the shape it names (`draw:start-shape`,
`draw:end-shape`). The shape's four default glue points (§9.2.19:
top, right, bottom, left) become its connection sites. An end glued
to a glue point of the shape's own, or to a shape not read, is left
unglued.
- Placement: a connector's own coordinates are in LibreOffice's drawing
layer, not its anchor's. So a connector glued at both ends to shapes
anchored in its paragraph is placed between their sites.
- Writing: connectors are written back as `draw:connector` with their
type and glue, and the shapes they glue to get ids
(`xml:id`/`draw:id`). LibreOffice re-routes them from the glue.
LibreOffice's connector document round-trips with its connectors
drawn between the shapes, and WordCraft draws them between the right
sites. Other formats still get connectors as straight lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice writes a border around text as `loext:border` (or its
sides) with `loext:padding`; ODF 1.3 has no attribute for it. It was
dropped. It is now read as Word's text border (one line all round, the
first side with a line, the padding its distance) and written back
the same way, which LibreOffice draws. A document with a text border
is then extended ODF rather than strict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Gap 19's row now says what ODT reads and writes and what is left, and
its estimate drops from 18–27 h to 10–16 h. The roadmap's progress log
covers the ODT work since text boxes: shapes through password
protection, forms, ruby and flow breaks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT index marks (`text:alphabetical-index-mark`) were dropped. So
References › Update Index on an opened ODT index found no entries and
left the old list in place. Now:
- a mark at a point is Word's `XE` field, its `text:key1`/`text:key2`
the levels above its entry;
- a mark over text (`-mark-start` … `-mark-end`, in one paragraph) is
an `XE` field whose entry is the marked text, which stays text;
- `XE` fields are written back as point marks.
Engine: Update rebuilt only lists in WordCraft's own form, a paragraph
holding the field. A list held as a field around its entries, as ODT
and Word files hold an index, bibliography or table of figures, is now
first made the field's own paragraph, then rebuilt. That happens only
when the field starts its paragraph, so no document text is replaced.
A LibreOffice document with an index mark round-trips pixel-identical,
the mark kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Ruby (`text:ruby`, ODF 1.3 §6.4: a base text with ruby text over it,
furigana or pinyin) was read as its base and ruby text run together,
and the writer wrote any ruby as plain text (its comment wrongly said
ODF has none).
Ruby is now read as WordCraft's Phonetic Guide. It is written back as
ODF ruby, above its base and centred, with an automatic ruby style.
A ruby inside a tracked change is still written as its base text,
which the change marks, so the tracking isn't lost. LibreOffice shows
the round-tripped ruby over its base, and the content validates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Six features of LibreOffice documents were lost on open and save. A
two-page document came back as one page, because the page break after
a paragraph was dropped. Now read and written:
- a page break after a paragraph (`fo:break-after="page"`), as a page
break before the next paragraph, as Word has it;
- lines kept together (`fo:keep-together`);
- widow and orphan control (`fo:widows`/`fo:orphans`): on with any line
count, off with none;
- hyphenation (`fo:hyphenate`): a paragraph's in ODF, the document's in
WordCraft. A document with a hyphenated paragraph hyphenates, and its
other paragraphs opt out;
- a table's alignment and width (`table:align`, `style:rel-width`);
- its rows' heights (`style:row-height` exact, `style:min-row-height` at
least).
Flow model: `FTable::extra` (alignment, width, row heights), filled by
formats that keep them.
A LibreOffice document with all of these round-trips at 573 px
difference instead of 4,407, keeps its two pages, and validates
against the ODF 1.3 schema. No other test document changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A turned text box (free rotation) drew its frame turned and its text
upright. Its text now turns with it about the box's centre, as Word
draws it, on screen and in PNG and PDF export: upside down when the
box is flipped across, never mirrored by a side-to-side flip.
PDF export draws item by item, to tag each paragraph, so it turns each
of the box's lines the same way, and tags the turned lines as their
paragraph's text rather than as decoration.
Clicks and the caret inside a turned box still go by its upright text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Three causes of the largest remaining LibreOffice round-trip
differences:
- In-line frames and shapes centred on or hanging from the baseline
(`style:vertical-pos` `middle`, `bottom`, or `from-top` with
`svg:y`, relative to the baseline) were read as standing on it. They
are now read as their run's lowering, which the layout honours, and
written back as `from-top`.
- Leading: LibreOffice also leaves the leading after a paragraph's last
line. It is now added to the paragraph's space after.
- Text box frames didn't derive from LibreOffice's Frame style, without
which LibreOffice ignores a frame's `fo:border` and outlines it in
its default shape colour. They now do, and an empty Frame style is
declared.
LibreOffice round trips (pixel difference): shapes 13,864 → 1,273,
spacing 2,973 → 706, text box 736 → 0. No other test document changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
An in-line picture, shape or chart always stood on the baseline,
whatever its run's position. Word moves an in-line picture with its
run's position (`w:position`, Font › Advanced › Position):
- lowered, it reaches below the baseline (at most until its top is on
the baseline) and the line's descent grows to hold it;
- raised, it stands clear of the baseline at its own height.
A drawing's cluster now reaches `obj_h` above the baseline and `obj_d`
below it (negative when raised). It is painted, hit-tested and wrapped
from both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT form controls (ODF 1.3 §13: controls in `office:forms`, each
placed in the text by a `draw:control`) were dropped on open, values
and all. They now open as WordCraft content controls, each with its
value, entries and selection; its name becomes the tag and its label
the title:
- text fields and text areas: plain text controls;
- check boxes: check boxes;
- radio buttons: check boxes with round symbols;
- list boxes: drop-down lists;
- combo boxes: combo boxes;
- date fields: date controls.
Saved as DOCX they become Word content controls.
Content controls of those kinds in the body are written back as form
controls with a `draw:control` anchor; in headers and footers they stay
their text. LibreOffice shows the round-tripped controls with their
values, and the content validates against the ODF 1.3 schema.
Not read: buttons, images, hidden and other controls. Controls' sizes
and looks aren't kept.
Flow model: new `Inline::Control`, kept only by flows that ask for
boxes (ODT); other formats get its text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A password-protected ODT (ODF 1.3 Part 2 §3.4) failed to open. The
package is now decrypted with the password into a plain one, which the
reader opens as before.
- Supported: AES-256-CBC with a SHA-256 or SHA-1 start key, PBKDF2
with HMAC-SHA-1, and SHA-256 or SHA-1 1K checksums (LibreOffice's
encryption).
- Blowfish (the old default) is refused with a message naming it.
- A missing or wrong password gives the same errors as Word files, so
the app asks for the password.
- Hostile parameters are bounded: iteration count, IV size, ragged
data, inflated size.
Saving with a password (and saving a document opened with one) writes
the ODT encrypted as LibreOffice does. Every file but the `mimetype`
and manifest is deflated and encrypted, each with its own salt and IV,
and the manifest validates against the ODF manifest schema.
LibreOffice opens WordCraft's encrypted files with the password and
refuses a wrong one; WordCraft opens LibreOffice's.
No new dependencies: the AES, SHA, HMAC, miniz and getrandom crates
the workspace already uses for Word encryption.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the objects commit found two gaps.
- The header and footer reader wasn't given the package's entry list,
so embedded objects there were still lost on open. It now shares the
body reader's list and manifest, borrowed rather than copied.
- Headers and footers were flattened for writing without drawings, so
shapes, text boxes, charts and kept objects in them were dropped on
save. They now go out as in the body.
The full LibreOffice round-trip harness shows no change on the other
test documents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice writes an embedded object's replacement picture in its own
metafile format, which WordCraft can't decode. So an embedded
spreadsheet (`draw:object`, ODF 1.3 §10.4.6) or OLE object
(`draw:object-ole`, §10.4.7) vanished on open, and saving lost it.
Such objects are now kept whole: their files, media type (from the
manifest or the sub-document's body) and replacement picture.
- Display: the replacement picture when WordCraft can show it, else a
placeholder of the object's size drawn in code (ruled for a
spreadsheet).
- ODT save: written back as the same object, with its manifest entries
and original replacement. LibreOffice renders a round-tripped
spreadsheet pixel-identically.
- Document model: the files live in `passthrough` under `odf-object/`,
linked from the picture's `ole` by a marker. The DOCX writer skips
the marker and writes the picture alone.
Flat ODT, a fix to the chart commit: inline sub-documents lacked the
required `office:mimetype`, so the schema rejected flat files holding
charts. Sub-documents now go inline as full `office:document`s (media
type, styles, settings) and are split back into their files on
opening. Replacement pictures and OLE files go inline as base64. A
picture's data is matched to the element that holds it, no longer to
the last `draw:image` before it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the chart commit: the engine drew opened charts only at the
paragraph level, so a chart that was a member of a `draw:g` group
opened blank. The pass now goes into groups (bounded depth).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A chart object (`draw:object` whose sub-document is an `office:chart`,
ODF 1.3 §10.4.3, Part 3 §11) used to open as its replacement picture.
It is now read into WordCraft's chart model: column, bar (stacked or
not), line, area, pie, doughnut and scatter charts, with categories,
series names and values (gaps included) taken from the chart's own
table through the series' cell range addresses, plus title, legend
position and data labels. The engine draws opened charts in the
document's theme colours, and Chart Design's commands edit them.
Charts WordCraft can't edit as they are (3D, percent-stacked, mixed
plot types, a second value axis, radar and the like) keep their
picture.
WordCraft charts are written as ODF chart objects shaped like
LibreOffice's: a data table, each series and slice in its accent
colour, pies clockwise from the top. They validate against the ODF 1.3
schema, and LibreOffice draws them like WordCraft does. In flat
documents they are written inline as an `office:document`.
Bug fixed: flat ODT reading ended the body at the first nested
`</office:body>`, so an inline sub-document dropped the text after it.
Element lookup now follows nesting, with a bounded depth.
Flow model: new `Inline::Chart`, kept only by flows that ask for
boxes (ODT); other formats still get the alt text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice's leading (`style:line-spacing`, extra space between lines)
has no WordCraft line spacing mode. It was read as a multiple of a
12 pt line, which spreads the extra space differently from LibreOffice,
so the paragraph started too high. It is now read as at least a 12 pt
line and the leading: the extra space goes above each line, as
LibreOffice draws leading (it also leaves some after the last line,
which isn't followed).
A LibreOffice spacing document went from 6,190 px apart after a round
trip to 2,973, and WordCraft's drawing of it from 8,570 to 5,521.
Regression test: `leading_is_at_least_a_line_and_the_leading`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tracked formatting changes (`text:format-change` regions, ODF 1.3
§5.5.7, between `text:change-start`/`-end` marks) were dropped. The
text between the marks now gets a formatting revision with the change's
author and date. ODF records no old formatting, so the old formatting
is the text's own: the revision shows and can be accepted, and
rejecting it changes nothing (LibreOffice can't reject one after
reopening either). WordCraft's formatting revisions are written as
format changes around their text.
Fixed: changed regions were written with `text:id` only, but ODF 1.3
requires `xml:id` (which the change marks refer to), so any document
with tracked changes failed schema validation. Both are written now,
and a region named by `xml:id` alone is read.
A LibreOffice document with a tracked formatting change round-trips
pixel for pixel; documents with tracked insertions, deletions and
format changes validate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Drop caps (`style:drop-cap`, ODF 1.3 §17.9) are read into WordCraft's
drop cap (lines and distance; one line is none) and written back as
the paragraph style's `style:drop-cap`. WordCraft drops one letter, so
a drop cap of several letters or a word opens as its first letter.
Fixed: the paragraph style key kept its child elements after a
separator, and the background, borders, contextual spacing and
right-to-left direction were added after the tab stops, so a paragraph
with its own tab stops wrote them into the tab stops' place instead of
onto the paragraph properties, and lost them. Attributes and child
elements are now gathered apart
(`tab_stops_and_other_paragraph_properties_together`).
A LibreOffice document with a drop cap round-trips pixel for pixel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A flat ODT holds the whole document in one `office:document` element
(ODF 1.3 Part 3 §2.1.2), its pictures inline as base64
`office:binary-data` and its formulas inline in their `draw:object`.
It is opened by taking it apart into a package's files (content,
styles, meta, settings; pictures and formulas as entries, each part's
root keeping the document's namespaces) and reading that package, and
saved by putting a package's files together: one set of automatic
styles, every part's namespaces declared once on the root, pictures and
formulas inline without the link attributes.
`.fodt` opens and saves from Open, Save As, the CLI and the web app.
LibreOffice's own flat documents round-trip as their packages do, and
the written files validate against the ODF 1.3 schema.
The parity doc's ODT fidelity is re-estimated at ~80%, and its status
line, which hadn't been updated since the Asian fonts change (a text
replacement that matched nothing), now lists every change since.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Page number format: the page layout's `style:num-format`, and a page
number field's own (`PAGE \* Arabic`, `roman`, `ROMAN`,
`alphabetic`, `ALPHABETIC`), both ways.
- Page colour (the first page style's `fo:background-color`).
- Page borders: ODF's sit at the margin with the text inside their
padding; WordCraft measures a page border from the page edge and the
text margin from the edge, so the border's space is ODF's margin and
the margin takes in the border and padding, and back on save.
- Footnote and endnote numbering (`text:notes-configuration`: format,
first number, counted from 0 in ODF, restart, page foot or document
end); only what the document sets is written.
- Line numbering (`text:linenumbering-configuration`, on unless it says
otherwise, ODF 1.3 §19.845: interval, distance, restart on each page).
A LibreOffice document with all of these round-trips pixel for pixel;
WordCraft draws it 7,213 px from LibreOffice (24,025 with the border at
the page edge). The written styles validate against the ODF schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Headers and footers were put into the document under the ids their
sections named (1, 2…), assuming the body had made no stories of its
own; but notes, comments and text boxes now are stories made first, so
in a document with a footnote and a footer the footer took the note's
id and the note showed the footer's text (a page number field).
Headers and footers now take free ids and the sections are pointed at
them.
Regression test: `a_footnote_keeps_its_text_beside_a_footer`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Fields that were read as plain text now open as Word's, so Update Field
recomputes them, and are written back as ODF's:
- dates and times in their date or time style (`number:date-style`,
`number:time-style`) ↔ `DATE`/`TIME \@ "picture"`: day, month (by
number or name), year, weekday, hours (12-hour with AM/PM), minutes,
seconds and the text between, both ways; data styles are written as
automatic styles and the `number` namespace declared;
- sequences numbering figures and tables (`text:sequence`) ↔ `SEQ` with
its number format; on save the sequences are declared
(`text:sequence-decls`); on open a bookmark named by the sequence's
reference name covers the caption's label and number ("Figure 1"),
what references to it show;
- chapter fields (`text:chapter`) ↔ `STYLEREF "Heading n"`, by name or
number (`\n`);
- reference marks as bookmarks, and references to them and to sequences
(`text:reference-ref`, `text:sequence-ref`) as `REF`/`PAGEREF`.
Captions renumber, references to them and chapter fields refresh after
Update Field (engine test `odt_fields.rs`). A LibreOffice document with
all of them round-trips 13 px apart with the same text; the written
content and styles validate against the ODF 1.3 schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Word's date-time pictures (`DATE \@ "…"`) put literal text in single
quotes, two quotes in a row standing for one: `H' o''clock'` shows
"8 o'clock". The formatter read the quotes as text and the letters
inside as instructions (an `a` became AM or PM). Quoted text is now
shown as it is; an unclosed quote runs to the end.
Regression test: `date_pictures_keep_quoted_text`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A bookmark over text (`text:bookmark-start`…`text:bookmark-end`, in one
paragraph or across several) was read as a point at its start, and a
Word bookmark over text was written as a point: a reference to it then
updated to nothing. The flow model gets the end of a bookmark
(`Inline::AnchorEnd`, in flows asked for fields, as ODT's is), the
document gets the bookmark's start and end where they are, and the
writer writes a start and an end (a point, `text:bookmark`, when it
ends where it starts). A bookmark started twice, ended twice or ended
before it starts is kept once, as a point when it has no end after it.
References to bookmarks, LibreOffice's references to headings included,
now update to the bookmarked text (engine test `odt_fields.rs`, through
a save and reopen).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tables of contents, tables of figures (`text:illustration-index`,
`text:table-index`), alphabetical indexes and bibliographies open as
WordCraft's: Word's `TOC` (levels from the source's
`text:outline-level`; `\c` and the caption name for figures), `INDEX`
and `BIBLIOGRAPHY` fields over their entries as last generated. With a
title the field goes in the title paragraph, as References › Table of
Contents inserts one; without, it spans the entries (new flow inlines
`FieldStart`/`FieldEnd`, mirroring the document's range fields). Update
Table then regenerates them from the headings (engine test
`odt_toc.rs`, titled and untitled, through a save and reopen).
LibreOffice's Contents 1–10, Contents Heading and Figure Index 1 styles
are WordCraft's TOC 1–9, TOC Heading and Table of Figures, both ways,
also when a file names them without defining them. WordCraft's tables
are written as ODF indexes with their source and entry templates, so
LibreOffice updates them, the entries' links in the Index Link style.
User and object indexes are read as their paragraphs; index marks are
not read yet.
Found on the way, both shown by the table of contents' round trip:
- a style based on LibreOffice's generic Heading style (as its headings
and index headings are) lost that parent on save, and with it its
font and spacing;
- a paragraph's tab stops were measured from its own indent only, not
from an indent its style gives (LibreOffice's `TabsRelativeToIndent`),
and a named style's stops not from its indent at all.
A LibreOffice document with a table of contents, a table of figures and
an alphabetical index round-trips pixel for pixel (it was 29,717 px
apart and a page short). The parity doc's ODT fidelity is re-estimated
at ~72%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A pure move, no behaviour changed: `odt/body.rs` (1,720 lines) and
`odt/writer.rs` (1,716) are split into child modules by topic, the
methods of `Body` and `Writer` moved in `impl` blocks of their own.
- body.rs (930): paragraphs, text, lists, element dispatch;
body/drawing.rs frames, shapes, groups, formula objects;
body/notes.rs notes, comments, fields; body/tracked.rs tracked
changes; body/tables.rs tables.
- writer.rs (618): the writer, paragraphs, inlines, blocks, styles;
writer/props.rs text and paragraph style properties;
writer/drawing.rs pictures, text boxes, shapes, groups, formulas;
writer/tracked.rs tracked changes; writer/notes.rs notes and
comments; writer/tables.rs tables.
Items the rest of `odt` uses are re-exported where they were. Every
harness document (21) exports content.xml, styles.xml and settings.xml
byte for byte as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Polygons and polylines (`draw:points`), paths (`svg:d`) and regular
polygons and stars (`draw:corners`, `draw:concave`, `draw:sharpness`)
open as WordCraft freeforms in their view box (none: the box around
their points) instead of being skipped; circles (`draw:circle`, by size
or by centre and radius) open as ellipses.
The path data reader follows SVG path syntax: moves, lines, horizontal
and vertical lines, cubic and quadratic curves and their smooth forms,
and elliptical arcs (converted from endpoint to centre form), absolute
and relative, with repeated commands; curves are followed in 16 straight
steps, a subpath closed by `Z` is a closed path, and input is bounded
(numbers, points, paths, coordinates).
WordCraft's freeforms (ink included) are written as `draw:path` in a
view box of hundredths of a point (whole numbers, as ODF's view box is).
A LibreOffice document with a polygon, a polyline and a closed Bézier
curve (relative `c` and smooth `s` commands) round-trips 333 px apart,
and WordCraft draws it 529 px from LibreOffice; the written content
validates against the ODF 1.3 schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Graphic styles' arrowheads (`draw:marker-start`/`-end`), line dashes
(`draw:stroke-dash`) and gradient fills (`draw:gradient`), defined by
name in the styles, open as WordCraft's arrowheads, dash patterns and
gradients:
- dashes: up to two kinds of dash and one space, in line widths or
absolute lengths, matched to WordCraft's named dashes when equal;
- gradients: linear and axial along their angle (ODF's 0° runs top to
bottom, anticlockwise; WordCraft's left to right, clockwise; an angle
without a unit is tenths of a degree, as ODF 1.2 wrote it), radial,
ellipsoid, square and rectangular out from their centre (end colour in
the middle), with border and intensity.
They are written back as definitions in `styles.xml` (WordCraft's own
triangle arrowhead marker; equal definitions share a name; the header
writer's names are prefixed apart). Frames keep gradient fills too.
A line now keeps the direction it is drawn in (`svg:x1`/`y1` is its
start): one drawn leftwards or upwards is flipped that way, so its
arrowheads stay at their ends, and it is written from its start.
A LibreOffice document with arrowheads, dashes and gradients round-trips
215 px apart; WordCraft draws it 518 px from LibreOffice. Exported
styles.xml and content.xml validate against the ODF 1.3 schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The renderers draw a line from its frame's top left to its bottom right
(DrawingML's `line`; flips turn the frame), but the arrowhead code and
`end_direction` assumed a line from the bottom left to the top right, so
a sloped line's arrowheads were drawn at the other diagonal's corners,
apart from the line. Horizontal and vertical lines hid it. The
arrowheads now start from the line's own ends, pointing along it.
Regression test: `tests::a_lines_arrowheads_sit_at_its_ends`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Groups (`draw:g`) of shapes, text boxes and pictures open as one
WordCraft group instead of being skipped: anchored and wrapped as the
group is, over the box around its members, each member at its place in
it (from its `svg:x`/`svg:y`, its line ends or its `draw:transform`,
turned members turned); groups inside a group are flattened into it.
WordCraft's groups are saved as `draw:g`, members at their offsets with
no anchor of their own, nested groups nested (a canvas as a plain group).
A frame, shape or group whose styles state no `style:wrap` now has text
on both sides, as LibreOffice wraps it (ODF 1.3 §20.400 states no
default), not above and below only; `biggest` is read as the larger
side. A shape whose only paragraph is LibreOffice's empty `<text:p/>`
gets no empty text story.
A LibreOffice document with turned shapes and a group was 7,464 px apart
after a round trip; now 586, with the same text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Shapes and pictures turned by `draw:transform` (rotate, translate and
scale, applied in turn to the object's own coordinates) open turned: the
file's anticlockwise turn about the object's top left corner, followed by
a move, becomes WordCraft's clockwise turn about the centre of the
unrotated box at its offset, as Word places them. They are written back
the same way, in line as well as floating. LibreOffice writes no
`svg:x`/`svg:y` for a turned shape, so before, a turned shape lost both
its turn and its position.
Custom shapes keep their mirroring (`draw:mirror-horizontal`/`-vertical`
on their geometry), and a turned line is written by its turned ends. The
flow keeps the placement of an in-line object that is turned or mirrored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Drawing shapes (`draw:rect`, `draw:ellipse`, `draw:line`, and
`draw:custom-shape` of a predefined geometry named by its
`draw:enhanced-geometry` `draw:type`) open as WordCraft shapes instead of
being skipped: size, placement (in line or floating, as frames are),
fill and line from the graphic style chain and the default graphic style
(LibreOffice's solid fill and hairline where nothing is stated), their
text, and a line's direction from its two ends. LibreOffice's type names
and its `ooxml-` preset names map onto WordCraft's kinds; a geometry
WordCraft has no shape for opens as a rectangle.
WordCraft's shapes are saved the same way: a custom shape by its type
alone (LibreOffice rebuilds the geometry from it), a line by its ends,
connectors as lines. A shape is saved with `draw:auto-grow-*` off unless
it resizes to fit its text, so LibreOffice keeps its size.
The flow model's text box becomes `Inline::Shape` with a kind; text
boxes are the `TextBox` kind and are written as frames, as before.
Not yet: shapes in groups, polygons, paths, rotation, arrowheads,
dashes, gradients.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A text box frame's line (colour and width), fill and padding are read
from its graphic style and that style's parents (LibreOffice's Frame
style included) instead of a fixed thin black line on white, and written
back. ODF's defaults (no line, fill or padding) apply where the styles
say nothing. A frame without a height, or with a `fo:min-height` on its
text box, opens as a box that resizes to fit its text and is saved with
a least height again. A LibreOffice text-box document was 1,117 px apart
after a round trip; now 736.
Layout: a floating text box that resizes to fit its text now takes its
text's height (drawn and wrapped around), as inline ones already did;
before, it kept its stored height (an ODT frame without one showed as a
1 pt line with the text spilling over the paragraph).
A formula sub-document's content.xml stays a bare `math:math` root, as
ODF 1.3 Part 3 §2.2.9 B requires (pinned by a test), and the reader now
skips a wrapping `office:math` some writers add instead of taking it for
the MathML root.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The Asian and complex-script fonts (`style:font-name-asian`/`-complex`,
through their font-face declarations, or `style:font-family-*`), the
complex script's size, weight and posture, and the Asian and complex
languages (`style:language-*`, `style:country-*`) are read on runs and
from the default style, into the run's and the document's
character properties, and written back. Hebrew, Arabic and CJK text
lost its fonts on a save (an RTL test document: 2,043 px apart in
LibreOffice after a round trip, now 529).
A run's own complex-script weight or posture is written once, not also
copied from its Latin bold or italic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`style:writing-mode` `rl-tb` (or `rl`; ODF 1.3 §20.404) on paragraph
properties opens as a bidirectional paragraph, from automatic and custom
paragraph styles alike, and is written back on the paragraph's or the
custom style's properties. Right-to-left paragraphs opened left to right
and left-aligned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A `draw:frame` holding a `draw:text-box` (ODF 1.3 §10.4.3) opens as a
WordCraft text box: a text box shape of the frame's size, in line or
floating as the frame is anchored and placed, whose story holds the
box's paragraphs (read as a note's are, with the paragraph and frame
around them put aside meanwhile, so a picture inside the box is a frame
of its own). The text was dropped on open.
Export writes each text box as a frame with a thin border and white fill
(`draw:fill` as well as `fo:background-color`: LibreOffice fills a frame
without `draw:fill` with its default blue). The module documentation now
lists what the reader and writer handle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice gives a heading style the outline's numbering unless the
style names no list style (`style:list-style-name=""`, as LibreOffice
writes its own). Our heading styles named none, so a heading whose level
the outline doesn't number still took that level's default indent: a
Word-made document's headings sat indented after a save. Heading styles
of unnumbered levels now name no list style; numbered ones stay
attached. Round-trip of that document: 3,302 px apart, now 1,121.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Contextual spacing (`style:contextual-spacing`, ODF 1.3 §20.255: no
space between paragraphs of one style) is read from paragraph, custom
and kind styles and written back, and the List Paragraph style is
written like any other, so bulleted items keep their spacing; they were
written in Standard and spread apart.
LibreOffice switches many layout behaviours by `settings.xml`, and a file
without them is laid out its own way. A file's own `settings.xml` is now
kept through a save; a document from elsewhere gets the switches
LibreOffice uses for Word files, as WordCraft lays text out as Word
does. Tab stops are written from the margin either way. A Word-made
document saved as ODT drew 38,652 px apart from LibreOffice's own ODT of
it; now 3,302, with the same line breaks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A common text style other than LibreOffice's link, note-mark and
list-label styles (Emphasis, a user's own) opens as a document character
style with its display name, the custom character style it is based on
and its own formatting; the text in it names it, and only what the
spans' automatic styles add is direct formatting. Before, the style was
lost and its formatting copied onto the text. An id a paragraph style
already has gets "Char" added.
Export writes each custom character style the text uses, and those it is
based on, as common text styles; a span's automatic style is based on
its style and holds only the run's direct formatting. LibreOffice keeps
the style when it saves the file. Automatic styles of styles.xml are
never taken for custom styles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each list level's indent and hanging (`fo:margin-left` and
`fo:text-indent` of `style:list-level-label-alignment`) and its label's
alignment (`fo:text-align` of `style:list-level-properties`, ODF 1.3
§17.19-17.20) are read into the document's list levels, for lists and
heading numbering, and written back. Every list was written 36 pt a
level with left-aligned numbers, so LibreOffice's right-aligned numbers
and closer bullets moved on a save (lists test document: 658 px apart
in LibreOffice after a round trip, now 8).
The flow label keeps the position in twentieths of a point, its prefix
and suffix now up to 8 bytes ("Chapter " still fits), so list
paragraphs stay as small as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each paragraph's style chain was searched for its custom style by
building every custom style in it (cloning its tab stops and the rest)
just to see whether there was one. A cheap check now decides; the style
is built once, by the import that adds it to the document. A test opens
20,000 paragraphs on a 15-deep chain of styles with 60 tab stops each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A common paragraph style other than Standard, the paragraph kinds' and
the note texts' (LibreOffice's Text body, a user's own style) opens as a
document style with its display name, the style it is based on (a
custom parent, a kind's style or Normal) and its own formatting:
spacing, indents, line spacing, alignment, keep with next, borders,
background, tab stops and text formatting. Its paragraphs stay in it;
only what the automatic styles over it add is direct formatting. Before,
the style was lost and its formatting copied onto every paragraph.
Headers, footers, notes and comments are covered too.
Export writes each custom style the paragraphs use, and the custom
styles they are based on, as common styles named by an XML-safe form of
the style id and shown by the style's name; LibreOffice keeps them when
it saves the file. A custom style may not take a name the writer uses
for its own styles (Standard, Footnote, Endnote, the kinds'), so no
style is written twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each equation is written as an embedded formula object (ODF 1.3
§10.4.3): a frame in line with the text whose `draw:object` points at a
sub-document `Object N/` (declared in the manifest as a formula) whose
content.xml is the equation's MathML, with its linear text as an
annotation. The reader already opens LibreOffice's formula objects, so
equations round-trip. They were written as their linear text.
LibreOffice scales a formula to its frame, so the frame is sized from
the equation's structure (a box per node: fractions, scripts, radicals,
n-ary operators, delimiters, matrices) and centred on the line.
Checked in LibreOffice 24.2 with LibreOffice Math.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Export wrapped a heading numbered through its style in a `text:list`
with WordCraft's default list style, so LibreOffice laid its number out
with that list's indents: level 2's "1.1." sat 54 pt in from the margin
(measured against LibreOffice's own file). A heading numbered only
through its style is now a plain `text:h`, numbered by the outline
style. The outline levels also get label alignment followed by a tab,
and the heading styles no longer state zero indents, which in
LibreOffice override the numbering's placement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A frame anchored to a paragraph, character or page (`text:anchor-type`,
ODF 1.3 §19.11) opens as a floating picture: its graphic style's
`style:wrap` and `style:run-through` give the wrap (square on both
sides, left, right or the larger side; top and bottom; behind or in
front of text), `style:horizontal-pos`/`-rel` and `style:vertical-pos`/
`-rel` its alignment or, with `svg:x`/`svg:y`, its offset and what it is
relative to, and `fo:margin-*` its distance from the text. Every
picture used to open in line with the text.
Export writes floating pictures anchored the same way, placed by a
graphic style of their own; pictures in line stay `as-char`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Paragraph borders (`fo:border*`, ODF 1.3 §20.183; each side's
`fo:padding*` as its distance from the text) and background
(`fo:background-color`, `transparent` as none) are read from paragraph
styles, the nearest in the chain but the kind's style and Standard, and
written back on the paragraph's style. Only an empty paragraph with a
bottom border was kept before, as a horizontal rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`text:outline-style` (ODF 1.3 §16.34) numbers the headings of its
levels. On open its levels (format, prefix, suffix, levels shown, start)
become one list, each level linked to its heading style, and Heading
1-6 number their paragraphs through it, as Word's numbered headings do:
LibreOffice's "1. / 1.1." headings keep their numbers (they were lost).
Levels without a number stay unnumbered.
Export writes a `text:outline-style` from the numbering of the heading
styles; levels without one show no number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A `style:font-name` names a `style:font-face` in office:font-face-decls
(ODF 1.3 §16.23, §20.194); the font is that face's `svg:font-family`.
The reader took the face's name for the font, so LibreOffice's
`FreeSans1` opened as an unknown font "FreeSans1" instead of FreeSans.
Faces declared in content.xml and styles.xml both count; a name with no
face is still taken as the font.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Cell borders (`fo:border*`: width, style, colour; ODF 1.3 §20.183) and
padding (`fo:padding*`) are read from table-cell styles. LibreOffice
gives an edge two cells share to one of them only (the left cell's
right side `none`, the right cell's left side drawing it); on open the
line goes to both cells, as WordCraft's cells each draw their own sides.
A table whose cells all say "no line" opens without a grid.
Export writes each cell's padding (its own, its table's or Word's
default) and its borders with each shared edge once, as LibreOffice
does (left and bottom, the first row's top, the last column's right), so
lines no longer double; cells used a fixed 4 pt padding and a grid even
in tables without lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
On open, the file's own Heading 1-6, Title, Quotations and Preformatted
Text styles (named or displayed so, their whole chain but Standard)
give WordCraft's matching styles their font, size, bold, italic,
colour, spacing, indents, alignment and keep with next: a LibreOffice
heading is bold Liberation Sans, no longer WordCraft's blue heading.
`fo:font-size` percentages are read as a share of the inherited size.
On save, those styles are written from the document's own definitions
in full, instead of fixed ones, so a WordCraft heading looks the same in
LibreOffice. The styles test document now round-trips pixel-identically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tab stops (`style:tab-stops`, ODF 1.3 §17.7-17.8) open with their
alignment (left, centre, right, decimal) and leader (dot, hyphen,
underline, middle dot), and are written back on the paragraph's style;
the default tab interval (`style:tab-stop-distance`) round-trips.
LibreOffice measures tabs from the paragraph's indent when its
`TabsRelativeToIndent` setting is on (its default); opening follows the
setting, and saved files carry a settings.xml that turns it off, so
positions run from the margin as in Word and WordCraft.
The flow model carries the tabs in `Para::extra` only for flows that
ask (FlowOpts::paras).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`style:text-properties` beyond bold, italic and colour now open and
save: underline styles (double, thick, dotted, dashes, wave, words only)
and colour, double strike, caps, small caps, hidden text (kept, hidden),
letter spacing, scale, text raised or lowered at full size (a position,
not superscript), outline, shadow, emboss, engrave and language.
A monospaced font keeps its own name; it was read as "code" and written
as Courier New. The default style's pair kerning (`style:letter-kerning`)
is read into the document defaults and written back, so lines break
where LibreOffice breaks them (round-trip renders of the test documents
are now pixel-identical in LibreOffice).
The flow model carries this in `Fmt::extra` only for flows that ask
(FlowOpts::chars); the other formats are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each level of a list style (ODF 1.3 §16.30-16.33) is read with its
number format (1, i, I, a, A, none), prefix and suffix, levels shown,
start value and bullet character, and written back. Each `text:list` is
a list of its own unless it continues the one before it
(`text:continue-numbering`) or the one it names (`text:continue-list`);
an item's `text:start-value` restarts from there. A list inside a note
or table cell leaves the list around it alone.
Before, consecutive lists merged into one: three LibreOffice lists
numbered 1., i. and A. opened as one decimal list 1-7.
The flow model's ListInfo carries a list number and a compact label
(prefix and suffix up to 12 bytes, so "Chapter %1" fits) only for flows
that ask for them (FlowOpts::lists); the other formats are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Export wrote only the direct replies of a comment, so a reply to a reply
was lost on save. Every reply in a comment's thread is written now, each
answering the comment that starts the thread, as LibreOffice's threads
are flat. A reply chain that loops (hostile) is written once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`office:annotation` (ODF 1.3 §14.1) opens as a document comment with its
author, date, initials and text; `office:name` and
`office:annotation-end` (§14.2) give its range, across paragraphs too.
LibreOffice's resolved state (`loext:resolved`) and reply threads
(`loext:parent-name`) are kept: a reply is a comment of its parent with
no range of its own. An end with no comment is dropped, and a comment
whose end never comes covers nothing. LibreOffice's all-zero date is no
date. Before, comments were dropped on open.
Export writes each comment named, with its end where its range ends,
and its replies after it naming it; LibreOffice keeps the threads when
it saves the file again. Truncated XML no longer loses the paragraph a
note or comment sits in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`text:note` (ODF 1.3 §6.3) opens as a note story built as Word's: its
paragraphs in Footnote Text or Endnote Text, the first starting with the
note's own mark, the reference in the text in the reference style, and a
`text:label` custom mark kept. The paragraph a note sits in (with its
spans, list and table cell) is put aside while the note's content is
read, and goes on after it. Notes inside notes, in deleted text or in
frames are left out. Before, notes were dropped on open.
Export writes each note with a citation numbered as LibreOffice shows
it (footnotes 1, 2, 3; endnotes i, ii, iii) or its custom mark, its
paragraphs in Footnote or Endnote styles defined in styles.xml. An
insertion open around a note ends before it and goes on after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A LibreOffice header 2 cm high with 1 cm spacing puts the text 2 cm below
the page margin: `fo:min-height` takes in the `fo:margin-bottom` spacing.
Import added the spacing on top, so the text started a spacing too low
(measured: 113.9 pt in LibreOffice, 142.3 pt after a WordCraft save). The
spacing now counts only past the height.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Export writes each section's headers and footers into its master page
(style:header, -left for even pages, -first for a different first page;
an empty element where Word shows nothing), with their automatic styles
in styles.xml under names of their own. Word's page-edge header and
footer distances become ODF's page margin plus header/footer band
height, and import converts back, so headers sit where LibreOffice
draws them.
Text fields (ODF 1.3 §7: page number and count, date, time, document
information, bookmark references) open as WordCraft fields with their
shown text as the result, and are written back as ODF fields. Fixed
dates and fields without a Word equivalent stay text; Word fields ODF
lacks are written as their result. The flow model carries fields only
when asked (FlowOpts), so the other formats are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Read `fo:margin-*`, `fo:text-indent`, `fo:line-height` (percent, length,
normal), `style:line-height-at-least` and `style:line-spacing` (leading,
as a proportional approximation) from automatic and common paragraph
styles (ODF 1.3 §20.204-209, §20.225, §20.317-318), and write them back
from each paragraph's direct formatting.
The default paragraph style and Standard give the document defaults, on
ODF's initial values: an ODT paragraph without spacing has none, not
Word's 8 pt after and 1.15 lines. Export writes the document's Normal
spacing as the default paragraph style, so the copy reopens the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-10-31 22:58:55 +00:00
17 changed files with 278 additions and 22 deletions
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.