diff --git a/crates/docx/src/write/parts.rs b/crates/docx/src/write/parts.rs index 39c2e58d24..f35a36fc91 100644 --- a/crates/docx/src/write/parts.rs +++ b/crates/docx/src/write/parts.rs @@ -294,8 +294,10 @@ fn write_document_body(doc: &Document, wr: &mut Writer<'_>, rels: &mut PartRels, wr.used_media.insert(p.media.clone()); w.open("v:background", &[("id", "_x0000_s1025"), ("o:bwmode", "white")]); let mut fa: Vec<(&str, &str)> = vec![("r:id", &rid)]; - if !p.fill_type.is_empty() { - fa.push(("type", &p.fill_type)); + // VML has no centred fill (an ODT page background's): stretched to the page. + let kind = if p.fill_type.eq_ignore_ascii_case("center") { "frame" } else { p.fill_type.as_str() }; + if !kind.is_empty() { + fa.push(("type", kind)); } if !p.title.is_empty() { fa.push(("o:title", &p.title)); diff --git a/crates/formats/src/odt/import.rs b/crates/formats/src/odt/import.rs index 21b26cb9f1..79366bf735 100644 --- a/crates/formats/src/odt/import.rs +++ b/crates/formats/src/odt/import.rs @@ -432,6 +432,8 @@ struct Parsed { kept: super::parts::Kept, /// Hidden form controls, kept for the next save. hidden_controls: Vec, + /// The page background picture's bytes and fill. + page_image: Option<(Vec, &'static str)>, } /// Hyphenation is a paragraph's in ODF (`fo:hyphenate`) and the document's in WordCraft (as in @@ -507,6 +509,8 @@ fn parse_tracked(bytes: &[u8], paper: (f32, f32)) -> Result { // A frame may point at any entry, as often as it likes: `Pictures` keeps that bounded. pictures.get(&mut z, path) }; + // The page background picture's bytes. + let page_image = pages.doc.page_image.clone().and_then(|(href, fill)| fetch(&href).map(|b| (b, fill))); let objects = (entries.as_slice(), &manifest); let mut parts = styles_xml.as_deref().map(|x| page_parts::read(x, &styles, &mut pages, &mut fetch, objects)).unwrap_or_default(); first_pages(&mut pages); @@ -567,6 +571,7 @@ fn parse_tracked(bytes: &[u8], paper: (f32, f32)) -> Result { sources, kept, hidden_controls, + page_image, }) } @@ -603,6 +608,7 @@ pub fn import_on(bytes: &[u8], page: &SectionProps) -> Result sources, kept, hidden_controls, + page_image, } = parse_tracked(bytes, (page.page_w, page.page_h))?; let mut doc = model::to_doc(&flow); kept.store(&mut doc); @@ -622,6 +628,13 @@ pub fn import_on(bytes: &[u8], page: &SectionProps) -> Result doc.settings.default_tab = t; } doc.settings.page_color = page_doc.page_color; + // The page background picture (Design › Page Color › Fill Effects › Picture). + if let Some((bytes, fill)) = page_image + && let Some(ext) = model::sniff_image(&bytes) + { + let media = doc.add_media(bytes, ext); + doc.settings.page_picture = Some(wordcraft_doc::PagePicture { media, fill_type: fill.to_string(), title: String::new() }); + } if let Some((fmt, props)) = page_doc.notes[0] { (doc.settings.footnote_format, doc.settings.footnote_pr) = (fmt, props); } @@ -670,6 +683,9 @@ pub(super) struct Pages { #[derive(Clone, Default)] pub(super) struct PageDoc { pub(super) page_color: Option, + /// The first page style's background picture (§17.3 `style:background-image`): its package + /// path and WordCraft's fill for its repeat (tiled, stretched, or centred at its size). + pub(super) page_image: Option<(String, &'static str)>, /// Footnotes', then endnotes', number format and options. pub(super) notes: [Option<(wordcraft_doc::section::NumFormat, wordcraft_doc::section::NoteProps)>; 2], } @@ -723,6 +739,8 @@ struct Band { #[derive(Default)] struct Layout { attrs: Attrs, + /// Its background picture: package path and fill (see [`PageDoc::page_image`]). + image: Option<(String, &'static str)>, columns: Option, header: Band, footer: Band, @@ -757,6 +775,20 @@ fn read_pages(xml: &[u8], paper: (f32, f32)) -> Pages { layouts.entry(n.clone()).or_default(); } } + // A page background picture (§17.3, in `style:page-layout-properties`): §20.249 + // `style:repeat` (repeat is the default) as WordCraft's tile, stretch, or centre. + "background-image" if q.starts_with("style:") => { + if let (Some(l), Some(href)) = + (cur.as_ref().and_then(|n| layouts.get_mut(n)), get(&a, "xlink:href").map(str::trim).filter(|h| !h.is_empty() && h.len() <= 1024)) + { + let fill = match get(&a, "style:repeat").map(str::trim) { + Some("no-repeat") => "center", + Some("stretch") => "frame", + _ => "tile", + }; + l.image = Some((href.trim_start_matches("./").to_string(), fill)); + } + } "header-style" => in_footer = false, "footer-style" => in_footer = true, "header-footer-properties" => { @@ -812,7 +844,7 @@ fn read_pages(xml: &[u8], paper: (f32, f32)) -> Pages { }); let mut pages = Pages { doc, next: nexts, ..Default::default() }; for (name, layout, header, footer) in masters { - let Some(Layout { attrs: a, columns: cols, header: hb, footer: fb }) = layouts.get(&layout) else { continue }; + let Some(Layout { attrs: a, columns: cols, header: hb, footer: fb, image }) = layouts.get(&layout) else { continue }; // What a layout leaves unsaid: the default paper, and LibreOffice's 2 cm margins. let mut sec = SectionProps { page_w: paper.0, @@ -827,6 +859,7 @@ fn read_pages(xml: &[u8], paper: (f32, f32)) -> Pages { sec.line_numbers.clone_from(&lines); if pages.first.is_none() { pages.doc.page_color = get(a, "fo:background-color").and_then(model::parse_color); + pages.doc.page_image.clone_from(image); } // ODF starts a header at the page margin and the text after the header's band; Word // measures the header's distance and the text's margin from the edge. The band's height diff --git a/crates/formats/src/odt/package.rs b/crates/formats/src/odt/package.rs index 01cba37df3..e78a55d0cb 100644 --- a/crates/formats/src/odt/package.rs +++ b/crates/formats/src/odt/package.rs @@ -657,7 +657,8 @@ fn export_as(doc: &Document, mime: &str) -> Result, String> { if w.indexes + hw.indexes > 0 { draw_xml.push_str(""); } - let styles = styles_xml(doc, &pages, &parts, &hw.style_defs(), &named).replacen( + let styles = page_background(doc, &mut w, styles_xml(doc, &pages, &parts, &hw.style_defs(), &named)); + let styles = styles.replacen( "", // The Frame style text boxes derive from (it states nothing: their own styles do). &format!("{draw_xml}{named_text_xml}"), @@ -837,3 +838,45 @@ fn variables(doc: &Document) -> std::collections::BTreeSet { } out } + +/// `styles` with the page background picture (Design › Page Color › Fill Effects › Picture) in +/// each page layout (§17.3 `style:background-image`, first in `style:page-layout-properties`), +/// the picture written into the package by `w`: tiled, stretched, or centred at its own size +/// (§20.249 `style:repeat`). +fn page_background(doc: &Document, w: &mut Writer, styles: String) -> String { + let Some(pic) = doc.settings.page_picture.as_ref() else { return styles }; + let Some(data) = doc.media.get(&pic.media) else { return styles }; + let Some(ext) = model::sniff_image(data) else { return styles }; + let name = format!("Pictures/page-background.{ext}"); + if !w.pictures.iter().any(|(n, _, _)| *n == name) { + w.pictures.push((name.clone(), data.clone(), model::mime_of(ext).to_string())); + } + let repeat = match pic.fill_type.to_ascii_lowercase().as_str() { + "tile" => "repeat", + "center" => "no-repeat", + _ => "stretch", + }; + let el = format!( + "", + x(&name) + ); + // After each layout's properties' opening tag. + let mut out = String::with_capacity(styles.len() + el.len() * 4); + let mut rest = styles.as_str(); + while let Some(at) = rest.find("')).map(|e| at + e) else { break }; + let self_closing = rest.get(end.saturating_sub(1)..end) == Some("/"); + if self_closing { + out.push_str(rest.get(..end.saturating_sub(1)).unwrap_or_default()); + out.push('>'); + out.push_str(&el); + out.push_str(""); + } else { + out.push_str(rest.get(..=end).unwrap_or_default()); + out.push_str(&el); + } + rest = rest.get(end + 1..).unwrap_or_default(); + } + out.push_str(rest); + out +} diff --git a/crates/formats/src/odt/tests_more/page_setup.rs b/crates/formats/src/odt/tests_more/page_setup.rs index 265fcda956..d283aebc03 100644 --- a/crates/formats/src/odt/tests_more/page_setup.rs +++ b/crates/formats/src/odt/tests_more/page_setup.rs @@ -161,3 +161,41 @@ fn a_page_layout_stating_nothing_has_libreoffices_margins() { let d = crate::import("odt", &crate::odt::tests::package(&content, Some(&one))).unwrap().unwrap(); assert_eq!((d.last_section.margin_left, d.last_section.margin_top), (72.0, cm2)); } + +#[test] +fn a_page_background_picture_is_read_and_written() { + // LibreOffice's report templates: a picture centred on the page, not repeated. + let png = { + let img = image::RgbImage::from_pixel(8, 4, image::Rgb([200, 30, 30])); + let mut out = std::io::Cursor::new(Vec::new()); + img.write_to(&mut out, image::ImageFormat::Png).unwrap(); + out.into_inner() + }; + let ns = crate::odt::tests::NS_T; + let layout = |repeat: &str| { + format!( + r#""# + ) + }; + let content = + format!("x"); + for (repeat, fill) in [(r#"style:repeat="no-repeat""#, "center"), (r#"style:repeat="stretch""#, "frame"), ("", "tile")] { + let styles = layout(repeat); + let pkg = crate::odt::tests::zip_of(&[("content.xml", content.as_bytes()), ("styles.xml", styles.as_bytes()), ("Pictures/map.png", &png)]); + let d = crate::odt::import(&pkg).unwrap(); + let pic = d.settings.page_picture.as_ref().expect("a page picture"); + assert_eq!(pic.fill_type, fill); + assert_eq!(d.media.get(&pic.media).map(|b| b.as_slice()), Some(png.as_slice())); + // Written back in the page layout, the picture in the package. + let saved = crate::odt::export(&d).unwrap(); + let s = crate::odt::tests::entry(&saved, "styles.xml"); + let want_repeat = match fill { + "center" => "no-repeat", + "frame" => "stretch", + _ => "repeat", + }; + assert!(s.contains(&format!(r#"style:repeat="{want_repeat}""#)) && s.contains("Pictures/page-background.png"), "{s}"); + let again = crate::odt::import(&saved).unwrap(); + assert_eq!(again.settings.page_picture.as_ref().map(|p| p.fill_type.as_str()), Some(fill)); + } +} diff --git a/crates/pdf/src/exporter/draw.rs b/crates/pdf/src/exporter/draw.rs index 73b6d5e1a5..f1f9adbc30 100644 --- a/crates/pdf/src/exporter/draw.rs +++ b/crates/pdf/src/exporter/draw.rs @@ -286,6 +286,12 @@ impl Exporter<'_> { .media .get(&pic.media) .and_then(|b| image::ImageReader::new(std::io::Cursor::new(b.as_slice())).with_guessed_format().ok()?.into_dimensions().ok()); + // Centred once at its natural size (an ODT page background that doesn't repeat). + if let Some((pw, ph)) = dims.filter(|_| pic.fill_type.eq_ignore_ascii_case("center")) { + let (iw, ih) = ((pw as f32 * 0.75).min(w * 4.0), (ph as f32 * 0.75).min(h * 4.0)); + self.picture(s, &Rect::new((w - iw) / 2.0, (h - ih) / 2.0, iw, ih), &pic.media, &[0.0; 4], 1.0); + return; + } if let Some((pw, ph)) = dims.filter(|_| pic.fill_type.eq_ignore_ascii_case("tile")) { let (tw, th) = (pw as f32 * 0.75, ph as f32 * 0.75); if tw >= 1.0 && th >= 1.0 && (w / tw).ceil() * (h / th).ceil() <= MAX_TILES { diff --git a/crates/render/src/media.rs b/crates/render/src/media.rs index 9e68d308f4..2e5e7b0759 100644 --- a/crates/render/src/media.rs +++ b/crates/render/src/media.rs @@ -338,6 +338,20 @@ pub(crate) fn draw_page_picture(ctx: &mut RenderContext, doc: &Document, page: & return; } let tile = pic.fill_type.eq_ignore_ascii_case("tile"); + // Centred once at its natural size (an ODT page background that doesn't repeat). + if pic.fill_type.eq_ignore_ascii_case("center") { + let (iw, ih) = (0.75 * nw, 0.75 * nh); + let (x0, y0) = ((w - iw) / 2.0, (h - ih) / 2.0); + ctx.set_transform(view); + ctx.set_paint(vello_cpu::Image { + image: vello_cpu::ImageSource::Pixmap(pm), + sampler: peniko::ImageSampler::default().with_quality(peniko::ImageQuality::Medium).with_extend(peniko::Extend::Pad), + }); + ctx.set_paint_transform(Affine::translate((x0, y0)) * Affine::scale_non_uniform(iw / pw, ih / ph)); + ctx.fill_rect(&kurbo::Rect::new(x0.max(0.0), y0.max(0.0), (x0 + iw).min(w), (y0 + ih).min(h))); + ctx.reset_paint_transform(); + return; + } // Tiles at the picture's natural size, whatever size its cached pixmap is kept at. let (sx, sy) = if tile { (0.75 * nw / pw, 0.75 * nh / ph) } else { (w / pw, h / ph) }; let extend = if tile { peniko::Extend::Repeat } else { peniko::Extend::Pad }; diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index e71488b1fa..71288bf3db 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -305,6 +305,16 @@ fn page_picture_is_painted_behind_the_text() { let i = ((300 * img.width + 300) * 4) as usize; assert_eq!(&img.pixels[i..i + 3], &[10, 200, 30], "{kind}"); } + // Centred once at its own size (8 px = 6 pt), the rest of the page paper. + d.settings.page_picture = Some(wordcraft_doc::PagePicture { media: "bg.png".into(), fill_type: "center".into(), title: String::new() }); + let l = layout(&d, &mut LayoutCache::new(), &LayoutOptions::default()); + let img = render_page(&d, &l.pages[0], 1.0, &RenderOptions::default()); + let px = |x: u32, y: u32| { + let i = ((y * img.width + x) * 4) as usize; + [img.pixels[i], img.pixels[i + 1], img.pixels[i + 2]] + }; + assert_eq!(px(img.width / 2, img.height / 2), [10, 200, 30], "centred"); + assert_eq!(px(5, 5), [255, 255, 255], "only in the middle"); // An undecodable picture leaves the paper alone. d.media.insert("bg.png".into(), Arc::new(b"junk".to_vec())); let l = layout(&d, &mut LayoutCache::new(), &LayoutOptions::default()); diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 87bcc2f2ef..25d6b75038 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity with Microsoft Word -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-02 · **Change:** trivial (ODT tables fixed layout; tables starting a page); previously trivial (emphasis marks: model, layout, DOCX `w:em`, ODT `style:text-emphasize`, `format.emphasisMark`); previously trivial (ODT variables as SET/REF fields; character styles keep what they turn off); previously trivial (ODT styles keep what they turn off; WMF/EMF pictures kept; pictures in text boxes shown by LibreOffice); previously trivial (ODT unstated page margins are LibreOffice's 2 cm; drawings outside a paragraph join the next one); previously trivial (ODT placeholders as content controls, template dates, user name fields, unsized pages, style-only templates, found with LibreOffice's own templates); previously trivial (ODT hidden form controls kept; `.ott` templates opened and saved); previously trivial (ODT fidelity re-estimated at ~90%; a kept form control made another kind is saved as that kind); previously trivial (ODT form control sizes and buttons kept); previously trivial (ODT turned text in cells and text boxes); previously trivial (ODT macros and unknown package parts kept); previously trivial (ODT table cell formulas as formula fields); previously trivial (ODT custom shapes drawn from their own geometry); previously trivial (ODT bibliography marks as citations of sources); previously trivial (ODT linked text boxes); previously trivial (ODT picture crops and contour wrapping); previously trivial (ODT fidelity re-estimated at ~85%); previously trivial (ODT custom document properties and user fields as DOCPROPERTY); previously trivial (ODT connectors read and written glued); previously trivial (ODT borders around text read and written); previously trivial (ODT index marks read and written as XE fields; an index held around its entries updates); previously trivial (ODT ruby read and written); previously trivial (ODT page breaks after a paragraph, kept lines, widow control, hyphenation, table alignment and width and row heights); previously trivial (in-line drawings raised or lowered by their run's position; ODT in-line frames keep their place on the baseline; leading's space after the last line; text box borders shown by LibreOffice); previously trivial (ODT form controls opened and saved as content controls); previously trivial (password-protected ODT opened and saved); previously trivial (ODT headers and footers keep their shapes, text boxes, charts and embedded objects on save); previously trivial (ODT embedded spreadsheets and OLE objects kept whole instead of lost; flat ODT sub-documents valid `office:document`s); previously trivial (ODT charts in drawing groups drawn on open); previously trivial (ODT charts opened and saved as editable charts; flat ODT sub-documents no longer end the body); previously trivial (ODT leading read as an at-least spacing); previously trivial (ODT tracked formatting changes; changed regions named by `xml:id`); previously trivial (ODT drop caps; tab stops no longer drop a paragraph's background); previously trivial (flat ODT `.fodt` opened and saved; ODT fidelity re-estimated at ~80%); previously trivial (ODT page settings; a footnote no longer takes a footer's text); previously trivial (ODT dates in their formats, sequences, chapters and references); previously trivial (ODT bookmarks over text); previously trivial (ODT tables of contents and indexes; ODT fidelity re-estimated); previously trivial (ODT polygons, polylines and paths); previously trivial (ODT arrowheads, dashes and gradients); previously trivial (ODT groups of shapes and pictures); previously trivial (ODT rotated and mirrored shapes and pictures); previously trivial (ODT drawing shapes read and written); previously trivial (ODT text box frames keep their line, fill, padding and fit); previously trivial (ODT Asian and complex-script fonts); previously trivial (ODT right-to-left paragraphs); previously trivial (ODT text boxes kept); previously trivial (ODT unnumbered headings detached from the outline); previously trivial (ODT contextual spacing and layout settings); previously trivial (ODT custom character styles kept); previously trivial (ODT list level positions); previously trivial (ODT custom paragraph styles kept); previously trivial (ODT equations written as formula objects); previously trivial (ODT numbered headings written as headings); previously trivial (ODT floating pictures); previously trivial (ODT paragraph borders and background); previously trivial (ODT heading numbering); previously trivial (ODT font-face names); previously trivial (ODT table cell borders and padding); previously trivial (ODT heading, title, quote and preformatted styles keep their look); previously trivial (ODT tab stops read and written); previously trivial (ODT character formatting read and written); previously trivial (ODT lists keep their number formats, bullets and identity); previously trivial (ODT comments read and written); previously trivial (ODT footnotes and endnotes read and written); previously trivial (ODT headers, footers and text fields written; fields read); previously trivial (ODT paragraph spacing, indents and line spacing read and written); previously trivial (a paragraph naming an undefined style is formatted as Normal, as in Word, #5077); previously trivial (a hyperlinked TOC writes its page numbers as `PAGEREF _Toc… \h` fields, as Word does, #5097); previously trivial (HTML import ends the empty comments and at once, #5040); previously trivial (chart and SmartArt text the font lacks is drawn in a fallback face, so Chinese labels no longer show boxes, #4720); previously trivial (bibliography sources keep a corporate author (`b:Corporate`): read, written, cited and listed whole, #4848); previously trivial (custom-property bookmark-link preservation, #4767); previously trivial (note conversion preserves content with an ordered reference/run walk, #4750); previously trivial (DOCX comment-ID save lookups); previously trivial (built-in heading styles have their outline level without `w:outlineLvl`, #4696); previously trivial (DOCX comment ranges inside footnotes and endnotes are kept on read, #4548); previously trivial (RTF import and export keep Windows and Enhanced Metafile pictures (`\wmetafile`, `\emfblip`), #4259); previously trivial (#4658 bounds nested Markdown reference-label work); previously trivial (HTML import keeps preformatted lines with CR or CRLF input newlines, #4810); previously trivial (DOCX parts stored under another casing keep their embedded fonts through a save, and a private save of a kept glossary escapes the attribute values it rewrites; previously trivial: the `.doc` reader logs what its copy budget leaves out, once a kind per document; importers bound hostile expansion: font names, links, ODS repeats, LaTeX wrappers, Markdown delimiters, #3692/#3713; previously trivial: repeated DOCX style IDs: save/reopen regression; previously trivial: Markdown code spans keep line-broken text literal, #3738; previously trivial: Arabic list labels: `arabicAlpha` and `arabicAbjad` draw the 28 Arabic letters (alphabetical and abjad order), doubling past the last letter; Hebrew, Hindi, Thai and Vietnamese still show decimal (`doc::section::tests::arabic_letters_count_in_both_orders`); previously trivial: localized TOC style ids read as the TOC styles, and a TOC in columns keeps its layout through Update Table, #3889; previously trivial: cu20we: outline caps; previously trivial: a relationship the markup names without the prefix is read as it wrote: an unqualified `id` on an element that carries a relationship names that relationship, and the writer keeps the attribute with the id the new package holds the part under; an ordinary `id` — a VML shape's, a drawing's — names no part; previously trivial: the reader takes the relationship ids the markup refers to from the relationship attributes of the main document it has already parsed, so a document with many relationships opens in the time its part takes, #3411; previously trivial: a table cell span from a `.json` file stays within 63 columns through Insert Column and Merge Cells, #3637; previously trivial: the file commands that change the document are checked on a protected document, the protection of a protected document needs an explicit stop, the session author can't change through `file.setAuthor` under protection (the CLI's `--author` uses the same protection check), and the post-check compares the settings and the metadata; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: `majorBidi`/`minorBidi` theme fonts follow the settings' theme font language, as measured in Word; previously trivial: Word signature lines are kept on save, still signable; previously trivial: Times New Roman keeps Liberation Serif under the roman-font rule; previously trivial: shape gradients and transparency, #1500; previously trivial: a folder that keeps no Unix modes, such as a NAS share mounted by GVFS, still takes a save, #3003; previously trivial: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: table style band sizes written, a style without them unbanded as in Word, #3415; previously trivial: HTML export writes ruby (furigana, phonetic guides) and HTML import reads it back, #1681; previously trivial: RTF import decodes the East Asian multibyte code pages, #1653; previously trivial: PDF export: lam-alef drawn as two glyphs at one x extracts in reading order, #2310; previously trivial: native .json stores pictures and kept parts as base64, #586 follow-up; previously trivial: ODT headers and footers read from each master page, #863; previously trivial: RTF import compares heading and code formatting with the file's stylesheet; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: Word XML and web pages saved as `.doc` open by what they hold, #2853; previously trivial: a VML shape in a header cell is always placed on the page, user-checked in Word for Mac, #1775; previously trivial: `w:sym` characters keep their symbol font when the text's font changes and are written back as `w:sym`, #2036; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: two lines in one and horizontal in vertical (`w:eastAsianLayout`) kept on save; previously trivial: PDF outline targets follow leading page breaks; previously trivial: positional tabs `w:ptab` read and kept on save, #2367; previously trivial: DOCX: mixed right-to-left runs split and marked `w:rtl` with their language, #1543; previously trivial: `w:doNotTrackMoves` round-trips and turns move tracking off; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: DOCX glossary documents (building blocks) read, kept and written; previously trivial: cached field results keep their visible run formatting; previously trivial: cached REF results retain hyperlink destinations; previously trivial: CLI conversions can explicitly refresh fields; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: a pasted HTML `
` is an indented paragraph, not the Quote style, #1270); previously trivial: every table style part round-trips, #714; previously trivial: VML freeforms read and kept on save; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: `flowChartOr` draws its circle and cross spokes, as in Word; previously trivial: RTF: Mac OS Roman and DOS text, #1653; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: PDF export draws horizontal and vertical lines inside groups, #1474; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-02 · **Change:** trivial (ODT page background pictures); previously trivial (ODT tables fixed layout; tables starting a page); previously trivial (emphasis marks: model, layout, DOCX `w:em`, ODT `style:text-emphasize`, `format.emphasisMark`); previously trivial (ODT variables as SET/REF fields; character styles keep what they turn off); previously trivial (ODT styles keep what they turn off; WMF/EMF pictures kept; pictures in text boxes shown by LibreOffice); previously trivial (ODT unstated page margins are LibreOffice's 2 cm; drawings outside a paragraph join the next one); previously trivial (ODT placeholders as content controls, template dates, user name fields, unsized pages, style-only templates, found with LibreOffice's own templates); previously trivial (ODT hidden form controls kept; `.ott` templates opened and saved); previously trivial (ODT fidelity re-estimated at ~90%; a kept form control made another kind is saved as that kind); previously trivial (ODT form control sizes and buttons kept); previously trivial (ODT turned text in cells and text boxes); previously trivial (ODT macros and unknown package parts kept); previously trivial (ODT table cell formulas as formula fields); previously trivial (ODT custom shapes drawn from their own geometry); previously trivial (ODT bibliography marks as citations of sources); previously trivial (ODT linked text boxes); previously trivial (ODT picture crops and contour wrapping); previously trivial (ODT fidelity re-estimated at ~85%); previously trivial (ODT custom document properties and user fields as DOCPROPERTY); previously trivial (ODT connectors read and written glued); previously trivial (ODT borders around text read and written); previously trivial (ODT index marks read and written as XE fields; an index held around its entries updates); previously trivial (ODT ruby read and written); previously trivial (ODT page breaks after a paragraph, kept lines, widow control, hyphenation, table alignment and width and row heights); previously trivial (in-line drawings raised or lowered by their run's position; ODT in-line frames keep their place on the baseline; leading's space after the last line; text box borders shown by LibreOffice); previously trivial (ODT form controls opened and saved as content controls); previously trivial (password-protected ODT opened and saved); previously trivial (ODT headers and footers keep their shapes, text boxes, charts and embedded objects on save); previously trivial (ODT embedded spreadsheets and OLE objects kept whole instead of lost; flat ODT sub-documents valid `office:document`s); previously trivial (ODT charts in drawing groups drawn on open); previously trivial (ODT charts opened and saved as editable charts; flat ODT sub-documents no longer end the body); previously trivial (ODT leading read as an at-least spacing); previously trivial (ODT tracked formatting changes; changed regions named by `xml:id`); previously trivial (ODT drop caps; tab stops no longer drop a paragraph's background); previously trivial (flat ODT `.fodt` opened and saved; ODT fidelity re-estimated at ~80%); previously trivial (ODT page settings; a footnote no longer takes a footer's text); previously trivial (ODT dates in their formats, sequences, chapters and references); previously trivial (ODT bookmarks over text); previously trivial (ODT tables of contents and indexes; ODT fidelity re-estimated); previously trivial (ODT polygons, polylines and paths); previously trivial (ODT arrowheads, dashes and gradients); previously trivial (ODT groups of shapes and pictures); previously trivial (ODT rotated and mirrored shapes and pictures); previously trivial (ODT drawing shapes read and written); previously trivial (ODT text box frames keep their line, fill, padding and fit); previously trivial (ODT Asian and complex-script fonts); previously trivial (ODT right-to-left paragraphs); previously trivial (ODT text boxes kept); previously trivial (ODT unnumbered headings detached from the outline); previously trivial (ODT contextual spacing and layout settings); previously trivial (ODT custom character styles kept); previously trivial (ODT list level positions); previously trivial (ODT custom paragraph styles kept); previously trivial (ODT equations written as formula objects); previously trivial (ODT numbered headings written as headings); previously trivial (ODT floating pictures); previously trivial (ODT paragraph borders and background); previously trivial (ODT heading numbering); previously trivial (ODT font-face names); previously trivial (ODT table cell borders and padding); previously trivial (ODT heading, title, quote and preformatted styles keep their look); previously trivial (ODT tab stops read and written); previously trivial (ODT character formatting read and written); previously trivial (ODT lists keep their number formats, bullets and identity); previously trivial (ODT comments read and written); previously trivial (ODT footnotes and endnotes read and written); previously trivial (ODT headers, footers and text fields written; fields read); previously trivial (ODT paragraph spacing, indents and line spacing read and written); previously trivial (a paragraph naming an undefined style is formatted as Normal, as in Word, #5077); previously trivial (a hyperlinked TOC writes its page numbers as `PAGEREF _Toc… \h` fields, as Word does, #5097); previously trivial (HTML import ends the empty comments and at once, #5040); previously trivial (chart and SmartArt text the font lacks is drawn in a fallback face, so Chinese labels no longer show boxes, #4720); previously trivial (bibliography sources keep a corporate author (`b:Corporate`): read, written, cited and listed whole, #4848); previously trivial (custom-property bookmark-link preservation, #4767); previously trivial (note conversion preserves content with an ordered reference/run walk, #4750); previously trivial (DOCX comment-ID save lookups); previously trivial (built-in heading styles have their outline level without `w:outlineLvl`, #4696); previously trivial (DOCX comment ranges inside footnotes and endnotes are kept on read, #4548); previously trivial (RTF import and export keep Windows and Enhanced Metafile pictures (`\wmetafile`, `\emfblip`), #4259); previously trivial (#4658 bounds nested Markdown reference-label work); previously trivial (HTML import keeps preformatted lines with CR or CRLF input newlines, #4810); previously trivial (DOCX parts stored under another casing keep their embedded fonts through a save, and a private save of a kept glossary escapes the attribute values it rewrites; previously trivial: the `.doc` reader logs what its copy budget leaves out, once a kind per document; importers bound hostile expansion: font names, links, ODS repeats, LaTeX wrappers, Markdown delimiters, #3692/#3713; previously trivial: repeated DOCX style IDs: save/reopen regression; previously trivial: Markdown code spans keep line-broken text literal, #3738; previously trivial: Arabic list labels: `arabicAlpha` and `arabicAbjad` draw the 28 Arabic letters (alphabetical and abjad order), doubling past the last letter; Hebrew, Hindi, Thai and Vietnamese still show decimal (`doc::section::tests::arabic_letters_count_in_both_orders`); previously trivial: localized TOC style ids read as the TOC styles, and a TOC in columns keeps its layout through Update Table, #3889; previously trivial: cu20we: outline caps; previously trivial: a relationship the markup names without the prefix is read as it wrote: an unqualified `id` on an element that carries a relationship names that relationship, and the writer keeps the attribute with the id the new package holds the part under; an ordinary `id` — a VML shape's, a drawing's — names no part; previously trivial: the reader takes the relationship ids the markup refers to from the relationship attributes of the main document it has already parsed, so a document with many relationships opens in the time its part takes, #3411; previously trivial: a table cell span from a `.json` file stays within 63 columns through Insert Column and Merge Cells, #3637; previously trivial: the file commands that change the document are checked on a protected document, the protection of a protected document needs an explicit stop, the session author can't change through `file.setAuthor` under protection (the CLI's `--author` uses the same protection check), and the post-check compares the settings and the metadata; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: `majorBidi`/`minorBidi` theme fonts follow the settings' theme font language, as measured in Word; previously trivial: Word signature lines are kept on save, still signable; previously trivial: Times New Roman keeps Liberation Serif under the roman-font rule; previously trivial: shape gradients and transparency, #1500; previously trivial: a folder that keeps no Unix modes, such as a NAS share mounted by GVFS, still takes a save, #3003; previously trivial: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: table style band sizes written, a style without them unbanded as in Word, #3415; previously trivial: HTML export writes ruby (furigana, phonetic guides) and HTML import reads it back, #1681; previously trivial: RTF import decodes the East Asian multibyte code pages, #1653; previously trivial: PDF export: lam-alef drawn as two glyphs at one x extracts in reading order, #2310; previously trivial: native .json stores pictures and kept parts as base64, #586 follow-up; previously trivial: ODT headers and footers read from each master page, #863; previously trivial: RTF import compares heading and code formatting with the file's stylesheet; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: Word XML and web pages saved as `.doc` open by what they hold, #2853; previously trivial: a VML shape in a header cell is always placed on the page, user-checked in Word for Mac, #1775; previously trivial: `w:sym` characters keep their symbol font when the text's font changes and are written back as `w:sym`, #2036; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: two lines in one and horizontal in vertical (`w:eastAsianLayout`) kept on save; previously trivial: PDF outline targets follow leading page breaks; previously trivial: positional tabs `w:ptab` read and kept on save, #2367; previously trivial: DOCX: mixed right-to-left runs split and marked `w:rtl` with their language, #1543; previously trivial: `w:doNotTrackMoves` round-trips and turns move tracking off; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: DOCX glossary documents (building blocks) read, kept and written; previously trivial: cached field results keep their visible run formatting; previously trivial: cached REF results retain hyperlink destinations; previously trivial: CLI conversions can explicitly refresh fields; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: a pasted HTML `
` is an indented paragraph, not the Quote style, #1270); previously trivial: every table style part round-trips, #714; previously trivial: VML freeforms read and kept on save; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: `flowChartOr` draws its circle and cross spokes, as in Word; previously trivial: RTF: Mac OS Roman and DOS text, #1653; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: PDF export draws horizontal and vertical lines inside groups, #1474; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats Every format Word opens or saves, what WordCraft does with it, and how it's tested. Fidelity is the share of a typical real-world file's content and formatting that survives (read: shows correctly; @@ -57,7 +57,7 @@ Weights are the share of a Word user's file traffic (estimate). Read/write: ✅ | PDF export | 8% | W | — | ✅ | ~80% | 6–10 | `crates/pdf/src/tests.rs` | krilla: embedded subsets, selectable text incl. field results (#133); Hebrew and Arabic copy and search as whole words (right-to-left glyphs written in visual order, #1383, also with fonts that draw lam-alef as two glyphs at one x, #2310); All Caps and Small Caps text extracts as the capitals shown, as from Word's PDFs, and never changes the case of other text (#1473), links, outline, tagging. Outline destinations follow leading page breaks to the heading text. Fonts it can't subset become outlines (#132). Print Markup (#589): comments in balloons beside each page (scaled to its paper) and tracked changes; HTML/Markdown/text copies end with a comment list. Horizontal and vertical lines inside groups are drawn (#1474). No PDF/A option, no "best for printing/online" choice | | PDF open (PDF Reflow, Word for Windows) | 1% | R | ❌ | — | 0% | 30–50 | — | Converting a PDF into an editable document. A sibling app (PdfCraft) may supply the parser | | `.rtf` Rich Text Format | 3% | R/W | 🟡 | 🟡 | ~50% | 10–15 | `crates/formats/src/tests.rs` | ~1,500 lines: text, character/paragraph formatting, styles, lists, tables, pictures (`\shppict`, #154; Windows and Enhanced Metafiles, `\wmetafile` and `\emfblip`, read and written, #4259), fields, bookmarks. 8-bit text in the document's code page (`\ansicpg`) or the current font's (`\fcharset`, `\cpg`): Cyrillic, Central European, Greek, Turkish, Hebrew, Arabic, Baltic, Vietnamese, Thai (was Windows-1252 only), Mac OS Roman (`\mac`, `\fcharset77`) and DOS code pages (`\pc` 437, `\pca` 850, `\cpg` 437/737/850/852/857/866, `\fcharset254`) (#1653). East Asian multibyte code pages decoded as runs: 932 Shift-JIS (`\fcharset128`, Japanese), 936 GBK (134, Simplified Chinese), 949 UHC/windows-949 (129, Korean) and 950 Big5 (136, Traditional Chinese) through `\ansicpg`, `\cpg` or the font's charset, escaped or raw, a lead byte and its trail byte together; invalid sequences read as U+FFFD, and a run that starts `EF BB BF` is text, not a BOM (#1653). Pasted from the system clipboard where the platform offers it (Windows, Wayland) when there is no HTML (#687). Tracked insertions and deletions read and written (`\revtbl`, `\revised`/`\deleted`, authors, `\revdttm` dates), so Accept/Reject All work after opening or a round trip; tracked paragraph marks (`\par` inside `\revised`/`\deleted` groups, including a `\sect` inside a deletion) and character and paragraph formatting revisions (`\crauth`/`\crdate` with `\oldcprops`, `\prauth`/`\prdate` with `\oldpprops`) read and written; section and table formatting revisions (`\oldsprops`, `\oldtprops`) not yet. Sections on import (#1378): each `\sect` ends a section with its own page size and orientation, margins, header/footer distance, columns, break type, page-number restart, start and format, title page and headers/footers (those it leaves out link to the previous); export writes every section too (break type, page size and orientation, margins, header/footer distance, columns, page numbering, title page; not headers/footers). Thin: comments, notes, RTL | -| `.odt` OpenDocument Text (and flat `.fodt`, templates `.ott`) | 2% | R/W | 🟡 | 🟡 | ~90% | 20–35 | `crates/formats/src/tests.rs`, `crates/formats/src/odt/tests.rs` and one file per feature in `crates/formats/src/odt/tests_more/`, `crates/engine/tests/it/odt_sections.rs`, `odt_toc.rs`, `odt_fields.rs`, `odt_charts.rs`, `odt_objects.rs`, `odt_password.rs`, `odt_forms.rs`; checked against LibreOffice's own documents (round trips compared page by page) and the ODF 1.3 schema | Ruby (`text:ruby`, §6.4: a base text with ruby text over it) is read as WordCraft's Phonetic Guide and written back as ODF ruby above its base, centred (one inside a tracked change is written as its base text, which the change marks); its own position and alignment aren't kept. A border around text (LibreOffice's `loext:border` or its sides, with `loext:padding`: ODF 1.3 has no attribute for it) is Word's text border, one line all round, and is written back so (a document with one is then extended ODF, not strict). Emphasis marks (`style:text-emphasize`) and rotated text (`style:text-rotation-angle`) aren't read. How text flows: a page break after a paragraph (`fo:break-after`, 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; the counts themselves aren't kept) and hyphenation (`fo:hyphenate`, a paragraph's in ODF and the document's in WordCraft: a document with a hyphenated paragraph hyphenates, its other paragraphs opting out) are read and written; a table's alignment and width (`table:align`, `style:rel-width`, or `style:width` for an aligned table) and its rows' heights (`style:row-height` exact, `style:min-row-height` at least) are read and written. Form controls (`office:forms` controls placed by `draw:control`, ODF 1.3 §13) open as content controls — text fields and text areas as plain text controls, check boxes as check boxes, radio buttons as check boxes with round symbols, list boxes as drop-down lists, combo boxes as combo boxes, date fields as date controls, each with its value, entries and selection, its name as the control's tag and a label as its title — and content controls of those kinds in the body are written back as form controls LibreOffice shows with their values (in headers and footers, as their text); a control keeps its size (`draw:control` `svg:width`/`svg:height`); buttons, image buttons, labels and the other controls WordCraft has no kind for show their label, locked, and are kept verbatim and written back as they were (one naming a namespace WordCraft doesn't declare is left out); hidden controls (`form:hidden`) are kept and written back with their form; a control's look (colours, font) isn't kept. Placeholders (`text:placeholder`, §7.7.2, as LibreOffice's templates use: "") open as plain text content controls showing them, so a click selects one and typing replaces it, and are written back as placeholders of their type (their description as the control's title) while they show it. A master page followed by another (`style:next-style-name`: LibreOffice's "First Page" before "Default") is a section with a different first page, its own headers and footers on the first page and the next one's after it. Tables are fixed layout (Word's `w:tblLayout`), as LibreOffice's columns never fit their content, and a table starting a page (`fo:break-before` on the table) starts it: its first paragraph has a page break before it, which moves the table, and is written back on the table. The columns of a table filling the margins are written with their share of the table (`style:rel-column-width`) too, so on a narrower page they keep their proportions in LibreOffice (an aligned table's aren't: LibreOffice lays one out oddly with shares). A fixed date or time with neither a value nor text (LibreOffice's templates) is a date field showing the day; `text:author-name` is `USERNAME`, which WordCraft now fills with the user's name. A page layout that states no size is the default page's (the user's region's, as LibreOffice uses its default paper), and a margin it leaves unsaid is LibreOffice's 2 cm (a layout with no properties at all included); Emphasis marks (`style:text-emphasize`, §20.370) open as Word's (dot, accent as comma, circle, disc as dot; below as dot below) and are written back. Variables (`text:variable-set`, `text:variable-get`, §7.4) open as Word's SET and REF fields (a displayed set as SET then REF), which Update Field works out as LibreOffice shows them, and are written back declared (`text:variable-decls`), sets hidden with a get after them. What a named paragraph or character style turns off that the style it is based on turns on (`fo:font-weight="normal"` under a bold parent; italic, underline and strike-through too) is kept and written back, so it doesn't show through. Windows metafile pictures (WMF, EMF: LibreOffice's clip art and logos) open as pictures, at their own size when the frame gives none, instead of being dropped; picture frames are written derived from the Graphics style, without which LibreOffice doesn't show a floating picture inside a text box; drawings anchored outside any paragraph (LibreOffice's page-anchored frames before the first one) join the next paragraph instead of making one of their own, which started the document on the default page style, and a template of styles only (no `content.xml`) opens empty with its styles. OpenDocument text templates (`.ott`) open as documents and save as templates (the template's media type in `mimetype` and the manifest), with a password too. Turned text (`style:writing-mode`, §20.404; LibreOffice's own `loext:writing-mode="bt-lr"` for text running upwards) in table cells and text boxes opens as WordCraft's cell and text box text direction, down or up, and is written back the same way; turned characters (`style:text-rotation-angle`) open upright, as WordCraft has no turned characters. Package parts WordCraft doesn't read — LibreOffice Basic macros and dialogs (`Basic/`, `Dialogs/`), `manifest.rdf`, other applications' files — are kept as they are and written back with their manifest entries, so LibreOffice still finds a document's macros (WordCraft never runs them); the old thumbnail and digital signatures (`META-INF/*signatures*.xml`, which sign the package as it was) are dropped on save, and paths that climb out of the package aren't written. Password-protected ODT (ODF 1.3 Part 2 §3.4) opens with its password — AES-256-CBC with a SHA-256 or SHA-1 start key and PBKDF2, as LibreOffice writes it; the old Blowfish encryption is refused with a message saying so — and a document opened with a password, or saved with one (Save with password), is written encrypted the way LibreOffice encrypts (every file but the `mimetype` and manifest; each with its own salt and initialisation vector; SHA-256 checksums), which LibreOffice opens with the password. Flat OpenDocument text (`.fodt`: the whole document in one `office:document`, pictures inline as base64 `office:binary-data`, formulas inline) is opened and saved as a package is, both validating against the schema. Tables of contents and other indexes (`text:table-of-content`, `text:illustration-index`/`text:table-index`, `text:alphabetical-index`, `text:bibliography`) open as Word's `TOC` (levels from the source's `text:outline-level`; `\c` with the caption name for a table of figures), `INDEX` and `BIBLIOGRAPHY` fields — in the title paragraph when the index has one, as WordCraft inserts a table, else around the entries — so References › Update Table regenerates them; LibreOffice's Contents 1–10, Contents Heading and Figure Index 1 styles are WordCraft's TOC 1–9, TOC Heading and Table of Figures; WordCraft's tables are written back as ODF indexes with their source and entry templates (LibreOffice updates them), entries' links in the Index Link style; user and object indexes are read as their paragraphs; index marks are Word's `XE` fields — a mark at a point (`text:alphabetical-index-mark`, its `text:key1`/`text:key2` the levels above its entry) and one over text (`-mark-start`…`-mark-end` in one paragraph, its entry the marked text) — and `XE` fields are written as point marks, so References › Update Index rebuilds an ODT index from its entries (an index or other generated list held as a field around its entries, as ODT and Word files hold it, is first made the field's own paragraph, as WordCraft inserts one). Custom character styles (a common text style other than LibreOffice's link, note-mark and list-label styles kept as a character style with its name, parent and own formatting, its text in it; written back as common text styles), custom paragraph styles (a common style other than Standard, the kinds' and the note texts' kept as a document style with its name, parent and own formatting, its paragraphs in it rather than given its formatting directly; written back as common styles, named so LibreOffice keeps them), paragraph borders (each side's line and padding) and background, heading numbering (`text:outline-style`: each level's format, prefix, suffix, levels shown and start number the headings through their styles, as Word's numbered headings, and are written back), styles (the file's own Heading 1–6, Title, Quotations and Preformatted Text definitions give those styles' look on open — font, size, percent sizes included, bold, colour, spacing, alignment, keep with next — and saving writes the document's own style definitions rather than fixed ones), tab stops (left, centre, right, decimal; dot, hyphen, underline and middle-dot leaders; LibreOffice's indent-relative positions converted, and a `settings.xml` written saying positions run from the margin) and the default tab interval, character formatting (underline styles and colour, double strike, caps, small caps, hidden text kept hidden, letter spacing, scale, raised and lowered text, outline, shadow, emboss, engrave, language, Asian and complex-script fonts, complex-script size, weight and posture and both script languages (per run and as document defaults), a monospaced font by its own name, a `style:font-name` read as the family its `style:font-face` declares, and the default style's pair kerning) both ways, lists (#87; each `text:list` a list of its own unless it continues one by `text:continue-numbering` or `text:continue-list`, an item's `text:start-value` restarting from it; each level's number format — 1, i, I, a, A, none —, prefix, suffix, levels shown, start value, bullet character, indent, hanging and number alignment read from its list style and written back), picture crops (`fo:clip`: top, right, bottom, left in lengths of the picture's own size, its pixels at 96 per inch ↔ WordCraft's crop fractions), floating pictures (a frame anchored to a paragraph, character or page: wrap — square on both sides, left, right or the larger side, top and bottom, behind or in front of text, along its outline (`style:wrap-contour`: `outside` Word's Tight, `full` Through, its `draw:contour-polygon` the wrap polygon; a polygon in another coordinate system than the frame's, as LibreOffice writes for some pictures, is worked out from the picture instead) —, horizontal and vertical alignment or offset and what it is relative to, distances from the text, read into the picture's placement and written back in a graphic style), tables (cell borders — width, style, colour, an edge LibreOffice gives one cell shared by both — and cell padding read and written, each shared edge written once so lines don't double, a table without lines written without; cell formulas — LibreOffice's `table:formula` in its own syntax, `ooow:sum ` — open as Word formula fields, `=SUM(A1:A3)`, showing the cell's value, so Update Field recalculates them, and formula fields in cells, `ABOVE`/`LEFT`/`BELOW`/`RIGHT` as the cells they name, are written back as cell formulas with their value; cells holding a plain number are written as numbers (`office:value-type="float"`) so formulas calculate with them; a formula one side can't say keeps its value as text), pictures, page breaks (#86), paragraph spacing, indents and line spacing (proportional, fixed, at least; leading read as an at-least spacing of a 12 pt line and the leading, which puts the extra space above each line as LibreOffice draws it, and as that much more space after the paragraph, where LibreOffice leaves it after the last line) both ways, with the default and Standard styles' spacing as the document defaults (ODT's are none, not Word's 8 pt after and 1.15 lines), headers and footers of each master page (every, left and first page) read on open and written on save, with the header and footer distances converted between Word's page-edge measures and ODF's header bands (#863), text fields (page number and count, date, time, title, subject, author, file name, word and character counts, bookmark references as `REF`/`PAGEREF`, to bookmarks kept over the text they mark — `text:bookmark-start`…`-end`, in a paragraph or across several — so a reference updates to that text) read as fields and written back as ODF fields; dates and times in their date style (`number:date-style`/`time-style` ↔ Word's `\@` picture, both ways), sequences numbering figures and tables (`text:sequence` ↔ `SEQ` with its number format, declared on save; a bookmark over the caption's label and number, which references to it show), chapter fields (`text:chapter` ↔ `STYLEREF "Heading n"`, by name or number), reference marks (as bookmarks) and references to them and to sequences (`text:reference-ref`, `text:sequence-ref` ↔ `REF`/`PAGEREF`); citations (`text:bibliography-mark`) are `CITATION` fields of the source each carries, which becomes one of the document's bibliography sources (its identifier the tag; its type, author, title, year, publisher, address, journal, volume, pages, URL, editor, edition, number and month), so a bibliography updates from them, and are written back as marks carrying their source; custom document properties (`meta:user-defined`: string, number, true/false and date) are WordCraft's custom document properties, read and written, and the fields showing one (`text:user-defined`) or a user field (`text:user-field-get`, its declaration a custom property too, so saved back as one) are `DOCPROPERTY` fields, which update from them and are written back as `text:user-defined`; fixed dates and fields without a Word equivalent (variables — `text:variable-set`/`-get`, whose value changes through the document —, references to a caption's text) read as their text, and Word fields ODF lacks written as their result, keep with next (#2813), an embedded formula object (`draw:object`) is read from its own `content.xml`'s MathML, same as HTML (#3099); charts (a `draw:object` whose sub-document is an `office:chart`: column, bar, stacked, line, area, pie, doughnut and scatter charts; categories, series names and values — gaps included — from the chart's own table through their cell range addresses, title, legend position, data labels) open as WordCraft charts (in a drawing group too), drawn in the theme's colours and edited with Chart Design's commands, and every WordCraft chart is written back as an ODF chart object shaped like LibreOffice's (its data table, each series in its accent colour, slices clockwise from the top), in line or floating, inline in a flat document; a chart WordCraft can't edit as it is (3D, percent-stacked, stacked lines or areas, radar, stock or bubble charts, mixed plot types, a second value axis, one with no series) keeps its replacement picture, and fonts and axis scaling aren't kept; other embedded objects (`draw:object` sub-documents such as spreadsheets, drawings and presentations, and `draw:object-ole` OLE files) are kept whole — their files, media type and replacement picture — and saved back as the same objects, which LibreOffice opens as before; WordCraft shows one by its replacement picture, or, when that is in LibreOffice's own metafile format, by a placeholder of its size it draws itself (ruled for a spreadsheet), and doesn't edit or draw its contents; DOCX and other formats get the picture alone; in a flat document each object goes inline with its replacement picture — a sub-document as an `office:document` of its media type, its styles and settings with it, an OLE file as base64 — and is taken apart into its files again on opening; export writes each equation as a formula object of its own (MathML, with its linear text as an annotation), its frame sized from the equation's structure and centred on the line, so LibreOffice shows and edits it, page size, orientation and margins (#810), page number format (the page style's, and a page number field's own as `PAGE \* Arabic`/`roman`/…), page colour, page borders (ODF's at the margin with the text inside their padding ↔ WordCraft's measured from the page edge), footnote and endnote numbering (`text:notes-configuration`: format, first number, restart, page foot or document end) and line numbering (`text:linenumbering-configuration`, on unless it says otherwise: every how many lines, distance, restart on each page), sections: one per master-page change on a paragraph or table (next page, with its own page size, orientation, margins, columns and page-numbering restart) and per columned `text:section` (continuous, with its columns and separator line) (#1379); export writes them back: a master page and page layout per new-page section (size, orientation, margins, columns, page-numbering restart on the first paragraph or table), a `text:section` with its columns per continuous section whose columns differ from the page's (one column included), a list cut by a section break goes on numbering. Limits: a continuous section's page-numbering restart isn't written (ODF applies it only with a master page); a continuous section with the page's columns merges into the section before it on reopening; even and odd page starts are written as plain new pages; a tracked deletion of a section break is lost; an empty last section is dropped; tracked insertions and deletions with author and date, inserted and deleted paragraph marks and text inserted then deleted included (ODF change tracking; format changes (`text:format-change`) as formatting revisions with their author and date — ODF records no old formatting, so the old is the text's own — and WordCraft's formatting revisions written as format changes; regions named by `xml:id` as ODF 1.3 requires; pictures and tables inside a tracked deletion are dropped). footnotes and endnotes (`text:note`) read into note stories as Word's (Footnote Text style, the note's own mark first, a custom mark kept) and written back with their citations; notes inside notes and in deleted text left out; comments (`office:annotation` with `office:annotation-end` ranges, author, date, initials, LibreOffice's resolved state and reply threads, `loext:resolved`/`loext:parent-name`) read and written, a comment whose end never comes covering nothing; comments in notes and in deleted text left out; text boxes (`draw:text-box` in a frame) open as WordCraft text boxes with their text, in line or floating, and are written back; linked text boxes (`draw:chain-next-name` naming the frame the text continues in) stay linked both ways (a link to a missing box, to itself or round in a loop is dropped; a linked box keeps its own height where LibreOffice grows it to the rest of the text), keeping the frame's line (colour and width), fill, padding (through the frame's graphic style and its parents, LibreOffice's Frame style included) and a frame without a height growing to its text (`fo:min-height`), which WordCraft lays out floating as well as in line. Right-to-left paragraphs (`style:writing-mode` `rl-tb`) read and written. Drop caps (`style:drop-cap`: lines and distance; WordCraft drops one letter, so a longer drop cap opens as its first letter) read and written. Drawing shapes (`draw:rect`, `draw:ellipse`, `draw:line`, and `draw:custom-shape` of a predefined geometry by its `draw:type`: rounded rectangle, triangles, diamond, the four block arrows, star, heart, flowchart or; LibreOffice's names and the `ooxml-` preset names) open as WordCraft shapes with their size, placement (in line or floating, as frames are), fill, line (colour and width; LibreOffice's default blue line and fill where the styles say nothing), text and a line's direction, and are written back, a custom shape by its type alone, which LibreOffice rebuilds the geometry from; a geometry WordCraft has no shape for (hexagon, octagon, smiley, can, moon, lightning, block arc, cross, pentagon, flowchart shapes and the rest) is drawn from its own path (`draw:enhanced-path`, its values worked out from the shape's `draw:equation` formulas and `draw:modifiers` adjustment values; arcs, ellipses and quarter ellipses followed in short straight steps) as a freeform, and only one with no path opens as a rectangle; a freeform with both closed and open paths is written as a custom shape whose path leaves the open ones unfilled (LibreOffice fills no part of a `draw:path` with an open part); shapes and pictures turned by `draw:transform` (rotate, translate and scale; anticlockwise about the corner in the file, clockwise about the centre in WordCraft) and custom shapes mirrored (`draw:mirror-horizontal`/`-vertical`) keep their turn and mirroring both ways, in line or floating, a turned line by its turned ends; groups (`draw:g`) of shapes, text boxes and pictures open as one WordCraft group anchored and wrapped as the group is, its members placed (and turned) as they are in it, groups in it flattened into it, and are written back as a group; a frame, shape or group whose styles state no wrap has text on both sides, as LibreOffice wraps it (§20.400 states no default), and `biggest` wraps on the larger side; arrowheads (`draw:marker-start`/`-end`, written with WordCraft's own triangle marker), dashes (`draw:stroke-dash`: up to two kinds of dash and a space, in line widths or lengths, matched to WordCraft's named dashes; a pattern with more kinds is written as its first two) and gradient fills (`draw:gradient`: linear and axial along their angle, radial, ellipsoid, square and rectangular out from their centre, border and intensity; more than two colours written as the first and last) are read and written, a line's direction (and so its arrowheads' ends) kept from its two ends; polygons, polylines, paths (SVG path data: moves, lines, cubic and quadratic curves, smooth ones and elliptical arcs, absolute and relative, curves followed in short straight steps) and regular polygons (stars too) open as WordCraft freeforms in their view box, circles as ellipses, and freeforms (ink too) are written as `draw:path`; connectors (`draw:connector`: straight, curved, or with bends) open as WordCraft connectors glued to the shapes their ends are glued to (a shape's four default glue points as its connection sites; a glue point of its own or a shape not read leaves that end unglued), placed between the shapes' sites when both are anchored in the connector's paragraph (a connector's own coordinates are LibreOffice's drawing layer's), and are written back glued, the shapes they glue to given ids, which LibreOffice re-routes; captions and measures are left out, a custom shape whose type WordCraft has no shape for opens as a rectangle; an in-line frame or shape stands on the baseline, is centred on it or hangs from it as its `style:vertical-pos` relative to the baseline says (`top`, `middle`, `bottom`, or its top `svg:y` from it with `from-top`), read as its run's raise or lowering (Word's `w:position`, which WordCraft now applies to in-line pictures and shapes too) and written back as `from-top`; placement relative to the character or line stays on the baseline; a text box's frame derives from LibreOffice's Frame style, so LibreOffice draws its border | +| `.odt` OpenDocument Text (and flat `.fodt`, templates `.ott`) | 2% | R/W | 🟡 | 🟡 | ~90% | 20–35 | `crates/formats/src/tests.rs`, `crates/formats/src/odt/tests.rs` and one file per feature in `crates/formats/src/odt/tests_more/`, `crates/engine/tests/it/odt_sections.rs`, `odt_toc.rs`, `odt_fields.rs`, `odt_charts.rs`, `odt_objects.rs`, `odt_password.rs`, `odt_forms.rs`; checked against LibreOffice's own documents (round trips compared page by page) and the ODF 1.3 schema | Ruby (`text:ruby`, §6.4: a base text with ruby text over it) is read as WordCraft's Phonetic Guide and written back as ODF ruby above its base, centred (one inside a tracked change is written as its base text, which the change marks); its own position and alignment aren't kept. A border around text (LibreOffice's `loext:border` or its sides, with `loext:padding`: ODF 1.3 has no attribute for it) is Word's text border, one line all round, and is written back so (a document with one is then extended ODF, not strict). Emphasis marks (`style:text-emphasize`) and rotated text (`style:text-rotation-angle`) aren't read. How text flows: a page break after a paragraph (`fo:break-after`, 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; the counts themselves aren't kept) and hyphenation (`fo:hyphenate`, a paragraph's in ODF and the document's in WordCraft: a document with a hyphenated paragraph hyphenates, its other paragraphs opting out) are read and written; a table's alignment and width (`table:align`, `style:rel-width`, or `style:width` for an aligned table) and its rows' heights (`style:row-height` exact, `style:min-row-height` at least) are read and written. Form controls (`office:forms` controls placed by `draw:control`, ODF 1.3 §13) open as content controls — text fields and text areas as plain text controls, check boxes as check boxes, radio buttons as check boxes with round symbols, list boxes as drop-down lists, combo boxes as combo boxes, date fields as date controls, each with its value, entries and selection, its name as the control's tag and a label as its title — and content controls of those kinds in the body are written back as form controls LibreOffice shows with their values (in headers and footers, as their text); a control keeps its size (`draw:control` `svg:width`/`svg:height`); buttons, image buttons, labels and the other controls WordCraft has no kind for show their label, locked, and are kept verbatim and written back as they were (one naming a namespace WordCraft doesn't declare is left out); hidden controls (`form:hidden`) are kept and written back with their form; a control's look (colours, font) isn't kept. Placeholders (`text:placeholder`, §7.7.2, as LibreOffice's templates use: "") open as plain text content controls showing them, so a click selects one and typing replaces it, and are written back as placeholders of their type (their description as the control's title) while they show it. A master page followed by another (`style:next-style-name`: LibreOffice's "First Page" before "Default") is a section with a different first page, its own headers and footers on the first page and the next one's after it. A page background picture (`style:background-image` in the page layout, §17.3) is WordCraft's page picture (Design › Page Color › Fill Effects): repeated as tiled, stretched as stretched, not repeated as centred at its own size (a fill WordCraft draws so; DOCX writes it stretched), and is written back with its picture. Tables are fixed layout (Word's `w:tblLayout`), as LibreOffice's columns never fit their content, and a table starting a page (`fo:break-before` on the table) starts it: its first paragraph has a page break before it, which moves the table, and is written back on the table. The columns of a table filling the margins are written with their share of the table (`style:rel-column-width`) too, so on a narrower page they keep their proportions in LibreOffice (an aligned table's aren't: LibreOffice lays one out oddly with shares). A fixed date or time with neither a value nor text (LibreOffice's templates) is a date field showing the day; `text:author-name` is `USERNAME`, which WordCraft now fills with the user's name. A page layout that states no size is the default page's (the user's region's, as LibreOffice uses its default paper), and a margin it leaves unsaid is LibreOffice's 2 cm (a layout with no properties at all included); Emphasis marks (`style:text-emphasize`, §20.370) open as Word's (dot, accent as comma, circle, disc as dot; below as dot below) and are written back. Variables (`text:variable-set`, `text:variable-get`, §7.4) open as Word's SET and REF fields (a displayed set as SET then REF), which Update Field works out as LibreOffice shows them, and are written back declared (`text:variable-decls`), sets hidden with a get after them. What a named paragraph or character style turns off that the style it is based on turns on (`fo:font-weight="normal"` under a bold parent; italic, underline and strike-through too) is kept and written back, so it doesn't show through. Windows metafile pictures (WMF, EMF: LibreOffice's clip art and logos) open as pictures, at their own size when the frame gives none, instead of being dropped; picture frames are written derived from the Graphics style, without which LibreOffice doesn't show a floating picture inside a text box; drawings anchored outside any paragraph (LibreOffice's page-anchored frames before the first one) join the next paragraph instead of making one of their own, which started the document on the default page style, and a template of styles only (no `content.xml`) opens empty with its styles. OpenDocument text templates (`.ott`) open as documents and save as templates (the template's media type in `mimetype` and the manifest), with a password too. Turned text (`style:writing-mode`, §20.404; LibreOffice's own `loext:writing-mode="bt-lr"` for text running upwards) in table cells and text boxes opens as WordCraft's cell and text box text direction, down or up, and is written back the same way; turned characters (`style:text-rotation-angle`) open upright, as WordCraft has no turned characters. Package parts WordCraft doesn't read — LibreOffice Basic macros and dialogs (`Basic/`, `Dialogs/`), `manifest.rdf`, other applications' files — are kept as they are and written back with their manifest entries, so LibreOffice still finds a document's macros (WordCraft never runs them); the old thumbnail and digital signatures (`META-INF/*signatures*.xml`, which sign the package as it was) are dropped on save, and paths that climb out of the package aren't written. Password-protected ODT (ODF 1.3 Part 2 §3.4) opens with its password — AES-256-CBC with a SHA-256 or SHA-1 start key and PBKDF2, as LibreOffice writes it; the old Blowfish encryption is refused with a message saying so — and a document opened with a password, or saved with one (Save with password), is written encrypted the way LibreOffice encrypts (every file but the `mimetype` and manifest; each with its own salt and initialisation vector; SHA-256 checksums), which LibreOffice opens with the password. Flat OpenDocument text (`.fodt`: the whole document in one `office:document`, pictures inline as base64 `office:binary-data`, formulas inline) is opened and saved as a package is, both validating against the schema. Tables of contents and other indexes (`text:table-of-content`, `text:illustration-index`/`text:table-index`, `text:alphabetical-index`, `text:bibliography`) open as Word's `TOC` (levels from the source's `text:outline-level`; `\c` with the caption name for a table of figures), `INDEX` and `BIBLIOGRAPHY` fields — in the title paragraph when the index has one, as WordCraft inserts a table, else around the entries — so References › Update Table regenerates them; LibreOffice's Contents 1–10, Contents Heading and Figure Index 1 styles are WordCraft's TOC 1–9, TOC Heading and Table of Figures; WordCraft's tables are written back as ODF indexes with their source and entry templates (LibreOffice updates them), entries' links in the Index Link style; user and object indexes are read as their paragraphs; index marks are Word's `XE` fields — a mark at a point (`text:alphabetical-index-mark`, its `text:key1`/`text:key2` the levels above its entry) and one over text (`-mark-start`…`-mark-end` in one paragraph, its entry the marked text) — and `XE` fields are written as point marks, so References › Update Index rebuilds an ODT index from its entries (an index or other generated list held as a field around its entries, as ODT and Word files hold it, is first made the field's own paragraph, as WordCraft inserts one). Custom character styles (a common text style other than LibreOffice's link, note-mark and list-label styles kept as a character style with its name, parent and own formatting, its text in it; written back as common text styles), custom paragraph styles (a common style other than Standard, the kinds' and the note texts' kept as a document style with its name, parent and own formatting, its paragraphs in it rather than given its formatting directly; written back as common styles, named so LibreOffice keeps them), paragraph borders (each side's line and padding) and background, heading numbering (`text:outline-style`: each level's format, prefix, suffix, levels shown and start number the headings through their styles, as Word's numbered headings, and are written back), styles (the file's own Heading 1–6, Title, Quotations and Preformatted Text definitions give those styles' look on open — font, size, percent sizes included, bold, colour, spacing, alignment, keep with next — and saving writes the document's own style definitions rather than fixed ones), tab stops (left, centre, right, decimal; dot, hyphen, underline and middle-dot leaders; LibreOffice's indent-relative positions converted, and a `settings.xml` written saying positions run from the margin) and the default tab interval, character formatting (underline styles and colour, double strike, caps, small caps, hidden text kept hidden, letter spacing, scale, raised and lowered text, outline, shadow, emboss, engrave, language, Asian and complex-script fonts, complex-script size, weight and posture and both script languages (per run and as document defaults), a monospaced font by its own name, a `style:font-name` read as the family its `style:font-face` declares, and the default style's pair kerning) both ways, lists (#87; each `text:list` a list of its own unless it continues one by `text:continue-numbering` or `text:continue-list`, an item's `text:start-value` restarting from it; each level's number format — 1, i, I, a, A, none —, prefix, suffix, levels shown, start value, bullet character, indent, hanging and number alignment read from its list style and written back), picture crops (`fo:clip`: top, right, bottom, left in lengths of the picture's own size, its pixels at 96 per inch ↔ WordCraft's crop fractions), floating pictures (a frame anchored to a paragraph, character or page: wrap — square on both sides, left, right or the larger side, top and bottom, behind or in front of text, along its outline (`style:wrap-contour`: `outside` Word's Tight, `full` Through, its `draw:contour-polygon` the wrap polygon; a polygon in another coordinate system than the frame's, as LibreOffice writes for some pictures, is worked out from the picture instead) —, horizontal and vertical alignment or offset and what it is relative to, distances from the text, read into the picture's placement and written back in a graphic style), tables (cell borders — width, style, colour, an edge LibreOffice gives one cell shared by both — and cell padding read and written, each shared edge written once so lines don't double, a table without lines written without; cell formulas — LibreOffice's `table:formula` in its own syntax, `ooow:sum ` — open as Word formula fields, `=SUM(A1:A3)`, showing the cell's value, so Update Field recalculates them, and formula fields in cells, `ABOVE`/`LEFT`/`BELOW`/`RIGHT` as the cells they name, are written back as cell formulas with their value; cells holding a plain number are written as numbers (`office:value-type="float"`) so formulas calculate with them; a formula one side can't say keeps its value as text), pictures, page breaks (#86), paragraph spacing, indents and line spacing (proportional, fixed, at least; leading read as an at-least spacing of a 12 pt line and the leading, which puts the extra space above each line as LibreOffice draws it, and as that much more space after the paragraph, where LibreOffice leaves it after the last line) both ways, with the default and Standard styles' spacing as the document defaults (ODT's are none, not Word's 8 pt after and 1.15 lines), headers and footers of each master page (every, left and first page) read on open and written on save, with the header and footer distances converted between Word's page-edge measures and ODF's header bands (#863), text fields (page number and count, date, time, title, subject, author, file name, word and character counts, bookmark references as `REF`/`PAGEREF`, to bookmarks kept over the text they mark — `text:bookmark-start`…`-end`, in a paragraph or across several — so a reference updates to that text) read as fields and written back as ODF fields; dates and times in their date style (`number:date-style`/`time-style` ↔ Word's `\@` picture, both ways), sequences numbering figures and tables (`text:sequence` ↔ `SEQ` with its number format, declared on save; a bookmark over the caption's label and number, which references to it show), chapter fields (`text:chapter` ↔ `STYLEREF "Heading n"`, by name or number), reference marks (as bookmarks) and references to them and to sequences (`text:reference-ref`, `text:sequence-ref` ↔ `REF`/`PAGEREF`); citations (`text:bibliography-mark`) are `CITATION` fields of the source each carries, which becomes one of the document's bibliography sources (its identifier the tag; its type, author, title, year, publisher, address, journal, volume, pages, URL, editor, edition, number and month), so a bibliography updates from them, and are written back as marks carrying their source; custom document properties (`meta:user-defined`: string, number, true/false and date) are WordCraft's custom document properties, read and written, and the fields showing one (`text:user-defined`) or a user field (`text:user-field-get`, its declaration a custom property too, so saved back as one) are `DOCPROPERTY` fields, which update from them and are written back as `text:user-defined`; fixed dates and fields without a Word equivalent (variables — `text:variable-set`/`-get`, whose value changes through the document —, references to a caption's text) read as their text, and Word fields ODF lacks written as their result, keep with next (#2813), an embedded formula object (`draw:object`) is read from its own `content.xml`'s MathML, same as HTML (#3099); charts (a `draw:object` whose sub-document is an `office:chart`: column, bar, stacked, line, area, pie, doughnut and scatter charts; categories, series names and values — gaps included — from the chart's own table through their cell range addresses, title, legend position, data labels) open as WordCraft charts (in a drawing group too), drawn in the theme's colours and edited with Chart Design's commands, and every WordCraft chart is written back as an ODF chart object shaped like LibreOffice's (its data table, each series in its accent colour, slices clockwise from the top), in line or floating, inline in a flat document; a chart WordCraft can't edit as it is (3D, percent-stacked, stacked lines or areas, radar, stock or bubble charts, mixed plot types, a second value axis, one with no series) keeps its replacement picture, and fonts and axis scaling aren't kept; other embedded objects (`draw:object` sub-documents such as spreadsheets, drawings and presentations, and `draw:object-ole` OLE files) are kept whole — their files, media type and replacement picture — and saved back as the same objects, which LibreOffice opens as before; WordCraft shows one by its replacement picture, or, when that is in LibreOffice's own metafile format, by a placeholder of its size it draws itself (ruled for a spreadsheet), and doesn't edit or draw its contents; DOCX and other formats get the picture alone; in a flat document each object goes inline with its replacement picture — a sub-document as an `office:document` of its media type, its styles and settings with it, an OLE file as base64 — and is taken apart into its files again on opening; export writes each equation as a formula object of its own (MathML, with its linear text as an annotation), its frame sized from the equation's structure and centred on the line, so LibreOffice shows and edits it, page size, orientation and margins (#810), page number format (the page style's, and a page number field's own as `PAGE \* Arabic`/`roman`/…), page colour, page borders (ODF's at the margin with the text inside their padding ↔ WordCraft's measured from the page edge), footnote and endnote numbering (`text:notes-configuration`: format, first number, restart, page foot or document end) and line numbering (`text:linenumbering-configuration`, on unless it says otherwise: every how many lines, distance, restart on each page), sections: one per master-page change on a paragraph or table (next page, with its own page size, orientation, margins, columns and page-numbering restart) and per columned `text:section` (continuous, with its columns and separator line) (#1379); export writes them back: a master page and page layout per new-page section (size, orientation, margins, columns, page-numbering restart on the first paragraph or table), a `text:section` with its columns per continuous section whose columns differ from the page's (one column included), a list cut by a section break goes on numbering. Limits: a continuous section's page-numbering restart isn't written (ODF applies it only with a master page); a continuous section with the page's columns merges into the section before it on reopening; even and odd page starts are written as plain new pages; a tracked deletion of a section break is lost; an empty last section is dropped; tracked insertions and deletions with author and date, inserted and deleted paragraph marks and text inserted then deleted included (ODF change tracking; format changes (`text:format-change`) as formatting revisions with their author and date — ODF records no old formatting, so the old is the text's own — and WordCraft's formatting revisions written as format changes; regions named by `xml:id` as ODF 1.3 requires; pictures and tables inside a tracked deletion are dropped). footnotes and endnotes (`text:note`) read into note stories as Word's (Footnote Text style, the note's own mark first, a custom mark kept) and written back with their citations; notes inside notes and in deleted text left out; comments (`office:annotation` with `office:annotation-end` ranges, author, date, initials, LibreOffice's resolved state and reply threads, `loext:resolved`/`loext:parent-name`) read and written, a comment whose end never comes covering nothing; comments in notes and in deleted text left out; text boxes (`draw:text-box` in a frame) open as WordCraft text boxes with their text, in line or floating, and are written back; linked text boxes (`draw:chain-next-name` naming the frame the text continues in) stay linked both ways (a link to a missing box, to itself or round in a loop is dropped; a linked box keeps its own height where LibreOffice grows it to the rest of the text), keeping the frame's line (colour and width), fill, padding (through the frame's graphic style and its parents, LibreOffice's Frame style included) and a frame without a height growing to its text (`fo:min-height`), which WordCraft lays out floating as well as in line. Right-to-left paragraphs (`style:writing-mode` `rl-tb`) read and written. Drop caps (`style:drop-cap`: lines and distance; WordCraft drops one letter, so a longer drop cap opens as its first letter) read and written. Drawing shapes (`draw:rect`, `draw:ellipse`, `draw:line`, and `draw:custom-shape` of a predefined geometry by its `draw:type`: rounded rectangle, triangles, diamond, the four block arrows, star, heart, flowchart or; LibreOffice's names and the `ooxml-` preset names) open as WordCraft shapes with their size, placement (in line or floating, as frames are), fill, line (colour and width; LibreOffice's default blue line and fill where the styles say nothing), text and a line's direction, and are written back, a custom shape by its type alone, which LibreOffice rebuilds the geometry from; a geometry WordCraft has no shape for (hexagon, octagon, smiley, can, moon, lightning, block arc, cross, pentagon, flowchart shapes and the rest) is drawn from its own path (`draw:enhanced-path`, its values worked out from the shape's `draw:equation` formulas and `draw:modifiers` adjustment values; arcs, ellipses and quarter ellipses followed in short straight steps) as a freeform, and only one with no path opens as a rectangle; a freeform with both closed and open paths is written as a custom shape whose path leaves the open ones unfilled (LibreOffice fills no part of a `draw:path` with an open part); shapes and pictures turned by `draw:transform` (rotate, translate and scale; anticlockwise about the corner in the file, clockwise about the centre in WordCraft) and custom shapes mirrored (`draw:mirror-horizontal`/`-vertical`) keep their turn and mirroring both ways, in line or floating, a turned line by its turned ends; groups (`draw:g`) of shapes, text boxes and pictures open as one WordCraft group anchored and wrapped as the group is, its members placed (and turned) as they are in it, groups in it flattened into it, and are written back as a group; a frame, shape or group whose styles state no wrap has text on both sides, as LibreOffice wraps it (§20.400 states no default), and `biggest` wraps on the larger side; arrowheads (`draw:marker-start`/`-end`, written with WordCraft's own triangle marker), dashes (`draw:stroke-dash`: up to two kinds of dash and a space, in line widths or lengths, matched to WordCraft's named dashes; a pattern with more kinds is written as its first two) and gradient fills (`draw:gradient`: linear and axial along their angle, radial, ellipsoid, square and rectangular out from their centre, border and intensity; more than two colours written as the first and last) are read and written, a line's direction (and so its arrowheads' ends) kept from its two ends; polygons, polylines, paths (SVG path data: moves, lines, cubic and quadratic curves, smooth ones and elliptical arcs, absolute and relative, curves followed in short straight steps) and regular polygons (stars too) open as WordCraft freeforms in their view box, circles as ellipses, and freeforms (ink too) are written as `draw:path`; connectors (`draw:connector`: straight, curved, or with bends) open as WordCraft connectors glued to the shapes their ends are glued to (a shape's four default glue points as its connection sites; a glue point of its own or a shape not read leaves that end unglued), placed between the shapes' sites when both are anchored in the connector's paragraph (a connector's own coordinates are LibreOffice's drawing layer's), and are written back glued, the shapes they glue to given ids, which LibreOffice re-routes; captions and measures are left out, a custom shape whose type WordCraft has no shape for opens as a rectangle; an in-line frame or shape stands on the baseline, is centred on it or hangs from it as its `style:vertical-pos` relative to the baseline says (`top`, `middle`, `bottom`, or its top `svg:y` from it with `from-top`), read as its run's raise or lowering (Word's `w:position`, which WordCraft now applies to in-line pictures and shapes too) and written back as `from-top`; placement relative to the character or line stays on the baseline; a text box's frame derives from LibreOffice's Frame style, so LibreOffice draws its border | | `.txt` Plain text | 2% | R/W | ✅ | ✅ | ~95% | 1–2 | unit tests | Encoding detection. Files with other extensions (`.m3u`/`.m3u8` playlists, logs, config files, no extension) open as text when they are text, Word and RTF files by their signature; binary files are refused (#569). A Word or RTF extension that contradicts the content also opens by signature, as in Word: RTF saved as `.doc` (hh.ru résumés, accounting exports), a `.docx` renamed `.doc`, a Word 97-2003 file renamed `.docx`; a Word XML Document (Word 2003 XML or Flat OPC) or a web page (``, ``) under a `.doc`, `.docx` or `.rtf` name opens as what it is (#2853, `tests::word_xml_and_web_pages_saved_as_doc_open_by_content`). Word's encoding choice dialog and line-ending options missing; such files save only through Save As | | `.htm/.html` Web Page and Web Page, Filtered | 1% | R/W | 🟡 | 🟡 | ~60% | 6–10 | `crates/formats/src/tests.rs` | Tables (#102), relative pictures embedded (#113), missing `` tolerated (#154); the empty comments `` and `` end at once instead of swallowing the rest of the page (#5040). Header cells (``) bold unless their CSS says otherwise; `text-align` on a cell, or inherited from its row, row group or table, aligns the cell's paragraphs (#1381). HTML gives no page size, so it opens on the region's paper (A4 or Letter), as Markdown and plain text do; `@page` isn't read (#1381). No Word "Web Page" (with `_files` folder) round-trip. The same reader takes HTML from the system clipboard: Paste keeps another program's formatting (a Word document, a web page; Windows' "HTML Format" header understood), Merge Formatting keeps its emphasis (#687). A pasted `
` (LibreOffice's paragraph indented on both sides) becomes a Normal paragraph indented by the page's `blockquote` margins, else 0.5 in each side; opening an .html file keeps it as the Quote style (#1270). Equations are written as MathML (with the linear format as a UnicodeMath annotation) and read back from MathML: our annotation, else the presentation elements, else a TeX annotation (#816). A copy of one equation alone also goes in the clipboard's MathML formats (Windows "MathML" and "MathML Presentation", macOS `public.mathml.presentation` and `public.mathml`; W3C MathML 3 §6.2.4), which Microsoft documents Word reading first when it pastes math (#1297; the paste into Word is not yet verified); Ruby (furigana, phonetic guides) is written as `basereading` in the base text's formatting and read back from ``/``/`` (`` fallback parentheses dropped; malformed markup keeps its text), so docx/doc → HTML → doc keeps the annotation; HTML states no alignment, size or raise, so the Phonetic Guide's defaults apply (#1681) CRLF and lone CR input newlines normalize before tokenization, preserving preformatted lines and leaving decoded character references separate (#4810). | | `.mht/.mhtml` Single File Web Page | <1% | R/W | ❌ | ❌ | 0% | 3–5 | — | | @@ -280,6 +280,7 @@ Custom document properties retain their bookmark link targets through DOCX saves | Date | Change | Summary | |---|---|---| +| 2026-11-02 | trivial | ODT: page background pictures (LibreOffice's report templates' world map) were dropped; they are page pictures, centred ones drawn at their own size in the middle of the page (on screen and in PDF), and written back (`page_setup::a_page_background_picture_is_read_and_written`, `render::tests::page_picture_is_painted_behind_the_text`) | | 2026-11-02 | trivial | ODT: a table starting a page (LibreOffice's agenda minutes) didn't; it does, in WordCraft and after a save, and LibreOffice's 6orange agenda round-trips with its text the same. ODT tables are fixed layout, as LibreOffice's columns never fit their content (`flow_breaks::a_table_starting_a_page_starts_it`, `a_tables_columns_keep_their_widths`) | | 2026-11-02 | trivial | Emphasis marks (Font › Emphasis mark) weren't modelled: DOCX `w:em` and ODT `style:text-emphasize` were dropped. They are read, drawn over (or under) each character with the line made taller for them, set by `format.emphasisMark`, and written back in both formats (`docx/tests/it/emphasis_marks.rs`, `odt::tests_more::chars::emphasis_marks_are_read_and_written`, `engine::tests::emphasis_marks_are_set_and_drawn`) | | 2026-11-02 | trivial | ODT: variables were their text; they are SET and REF fields (WordCraft now evaluates SET: it shows nothing and gives REF its value), written back as declared variables; a LibreOffice document with variables round-trips with its text the same. Character styles keep what they turn off too (`fields::variables_open_as_set_and_ref_fields`, `odt_fields.rs::odt_variables_update_as_set_and_ref`, `references::tests::set_gives_a_value_that_ref_shows`, `char_styles::what_a_character_style_turns_off_stays_off`) |