mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 20:00:11 -06:00
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:
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user