Gap 19: ODT headers and footers keep their drawings on save

Review of the objects commit found two gaps.
- The header and footer reader wasn't given the package's entry list,
  so embedded objects there were still lost on open. It now shares the
  body reader's list and manifest, borrowed rather than copied.
- Headers and footers were flattened for writing without drawings, so
  shapes, text boxes, charts and kept objects in them were dropped on
  save. They now go out as in the body.

The full LibreOffice round-trip harness shows no change on the other
test documents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
This commit is contained in:
Claude committed 2026-11-01 13:42:25 +00:00
1 parent 275037932f
commit 16f0b6ed25
7 files changed
+44 -13

No files matched your search

+5 -5
View File
@@ -27,8 +27,8 @@ pub(super) struct Body<'a> {
styles: &'a Styles,
zip: &'a mut dyn FnMut(&str) -> Option<Vec<u8>>,
/// The package's entry names and its manifest's media types, for embedded objects.
pub(super) entries: Vec<String>,
pub(super) manifest: HashMap<String, String>,
pub(super) entries: &'a [String],
pub(super) manifest: Option<&'a HashMap<String, String>>,
containers: Vec<Vec<FBlock>>,
fmt: Vec<Fmt>,
para: Option<Para>,
@@ -121,8 +121,8 @@ impl<'a> Body<'a> {
Body {
styles,
zip,
entries: Vec::new(),
manifest: HashMap::new(),
entries: &[],
manifest: None,
containers: vec![Vec::new()],
fmt: Vec::new(),
para: None,
@@ -759,7 +759,7 @@ impl Body<'_> {
}
}
if fr.4.is_none() && fr.5.is_none() {
fr.6 = super::objects::gather(href, ole, &self.entries, &self.manifest, &mut *self.zip);
fr.6 = super::objects::gather(href, ole, self.entries, self.manifest, &mut *self.zip);
// A replacement picture read before the object.
if let Some(o) = &mut fr.6 {
o.replacement = fr.2.clone().map(std::sync::Arc::new);
+3 -3
View File
@@ -432,10 +432,10 @@ fn parse_tracked(bytes: &[u8]) -> Result<Parsed, String> {
// A frame may point at any entry, as often as it likes: `Pictures` keeps that bounded.
pictures.get(&mut z, path)
};
let mut parts = styles_xml.as_deref().map(|x| page_parts::read(x, &styles, &mut pages, &mut fetch)).unwrap_or_default();
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();
let mut body = Body::new(&styles, &pages, &mut fetch);
body.entries = entries;
body.manifest = manifest;
(body.entries, body.manifest) = (objects.0, Some(objects.1));
events(&content, |ev| body.feed(ev));
let mut blocks = body.take_blocks();
let (mut named_odt, mut named) = custom_styles(&mut blocks, &styles, Vec::new());
+2 -2
View File
@@ -43,7 +43,7 @@ pub(in crate::odt) fn gather(
href: &str,
ole: bool,
entries: &[String],
manifest: &HashMap<String, String>,
manifest: Option<&HashMap<String, String>>,
fetch: &mut dyn FnMut(&str) -> Option<Vec<u8>>,
) -> Option<OdfObject> {
let path = inside(href)?;
@@ -64,7 +64,7 @@ pub(in crate::odt) fn gather(
if files.is_empty() {
return None;
}
let mime = manifest.get(&format!("{path}/")).or_else(|| manifest.get(path)).cloned().unwrap_or_else(|| {
let mime = manifest.and_then(|m| m.get(&format!("{path}/")).or_else(|| m.get(path))).cloned().unwrap_or_else(|| {
if ole {
OLE_MIME.to_string()
} else {
+2 -1
View File
@@ -238,7 +238,8 @@ fn page_parts(doc: &Document, sec: &SectionProps, w: &mut Writer) -> PageParts {
let blocks = model::part_flow(
doc,
&part.blocks,
model::FlowOpts { fields: true, lists: true, chars: true, paras: true, ..Default::default() },
// Pictures, shapes, text boxes, charts and objects too, as in the body.
model::FlowOpts { fields: true, lists: true, chars: true, paras: true, boxes: true, ..Default::default() },
);
w.blocks(&blocks, &mut body, 0);
}
+9 -1
View File
@@ -66,7 +66,14 @@ impl Parts {
/// Read each visible header and footer of the master pages in `pages`, and name it on its master
/// page's section as part id 1, 2, … in order, so the sections the body starts carry them.
/// [`add`] puts those parts into the document.
pub(super) fn read(xml: &[u8], styles: &Styles, pages: &mut Pages, fetch: &mut dyn FnMut(&str) -> Option<Vec<u8>>) -> Parts {
/// `objects`: the package's entry names and manifest media types, for embedded objects.
pub(super) fn read(
xml: &[u8],
styles: &Styles,
pages: &mut Pages,
fetch: &mut dyn FnMut(&str) -> Option<Vec<u8>>,
objects: (&[String], &std::collections::HashMap<String, String>),
) -> Parts {
let mut found: Vec<(String, Slot, Vec<Ev>)> = Vec::new();
let mut master: Option<String> = None;
// The slot being read, its nesting depth and whether it is kept.
@@ -116,6 +123,7 @@ pub(super) fn read(xml: &[u8], styles: &Styles, pages: &mut Pages, fetch: &mut d
for (name, kind, evs) in found {
let mut reader = Body::new(styles, &none, fetch);
reader.in_text = true;
(reader.entries, reader.manifest) = (objects.0, Some(objects.1));
for ev in evs {
reader.feed(ev);
}
@@ -132,3 +132,24 @@ fn a_flat_document_holds_its_objects_and_their_pictures_inline() {
let ole = crate::odt::tests::entry(&saved, "content.xml");
assert!(ole.contains("<draw:object-ole"), "{ole}");
}
#[test]
fn an_object_in_a_header_is_kept_too() {
let styles = format!(
r#"<office:document-styles {NS_T} {NS_D}><office:automatic-styles><style:page-layout style:name="pm1"><style:page-layout-properties fo:page-width="21cm" fo:page-height="29.7cm" fo:margin-top="2cm"/><style:header-style><style:header-footer-properties fo:min-height="1.5cm"/></style:header-style></style:page-layout></office:automatic-styles><office:master-styles><style:master-page style:name="Standard" style:page-layout-name="pm1"><style:header><text:p>Logo <draw:frame svg:width="1in" svg:height="0.5in"><draw:object xlink:href="./Object 1"/><draw:image xlink:href="./ObjectReplacements/Object 1"/></draw:frame></text:p></style:header></style:master-page></office:master-styles></office:document-styles>"#
);
let content = format!(
r#"<office:document-content {NS_T}><office:body><office:text><text:p>Body.</text:p></office:text></office:body></office:document-content>"#
);
let pkg = zip_of(&[
("META-INF/manifest.xml", MANIFEST.as_bytes()),
("content.xml", content.as_bytes()),
("styles.xml", styles.as_bytes()),
("Object 1/content.xml", SHEET.as_bytes()),
("ObjectReplacements/Object 1", METAFILE),
]);
let saved = export(&import(&pkg).unwrap()).unwrap();
let z = zip::ZipArchive::new(std::io::Cursor::new(&saved)).unwrap();
assert!(z.file_names().any(|n| n.ends_with("/content.xml") && n.starts_with("Object M")), "{:?}", z.file_names().collect::<Vec<_>>());
assert!(entry(&saved, "styles.xml").contains("<draw:object xlink:href=\"./Object M"));
}
+2 -1
View File
@@ -1,6 +1,6 @@
# File-format parity with Microsoft Word
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-01 · **Change:** 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 througLine truncated
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-01 · **Change:** 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`);Line truncated
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;
@@ -279,6 +279,7 @@ Custom document properties retain their bookmark link targets through DOCX saves
| Date | Change | Summary |
|---|---|---|
| 2026-11-01 | trivial | ODT: shapes, text boxes, charts and embedded objects in headers and footers were dropped on save (header and footer text was written without drawings other than pictures), and embedded objects there were lost on open; both now as in the body (`odt::tests_more::objects::an_object_in_a_header_is_kept_too`) |
| 2026-11-01 | trivial | Flat ODT: an inline sub-document (a chart since the chart change) lacked the `office:mimetype` an `office:document` must have, and the schema rejected the file; sub-documents now go inline with their media type, styles and settings, objects' replacement pictures inline rather than linked to files a flat document doesn't have, OLE objects as base64 (`odt::tests_more::objects::a_flat_document_holds_its_objects_and_their_pictures_inline`) |
| 2026-11-01 | trivial | ODT: embedded objects WordCraft doesn't edit — spreadsheets and other sub-documents (`draw:object`), OLE files (`draw:object-ole`) — were lost on open when their replacement picture was in LibreOffice's own metafile format (as LibreOffice writes it); they are now kept whole with their files, media type and replacement picture, shown by that picture or a placeholder WordCraft draws, and saved back as the same objects (LibreOffice draws a round-tripped spreadsheet identically); DOCX gets the picture alone (`odt::tests_more::objects`, `odt_objects.rs`) |
| 2026-11-01 | trivial | ODT: a chart that is a member of a drawing group (`draw:g`) is drawn on open instead of showing blank (`odt_charts.rs::a_chart_in_a_group_opens_drawn`) |