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
2026-11-01 13:42:25 +00:00
parent 275037932f
commit 16f0b6ed25
7 changed files with 44 additions and 13 deletions
+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"));
}
File diff suppressed because one or more lines are too long