Files
wordcraft/crates/engine/tests/it/odt_objects.rs
T
Claude 275037932f Gap 19: ODT embedded spreadsheets and OLE objects kept whole
LibreOffice writes an embedded object's replacement picture in its own
metafile format, which WordCraft can't decode. So an embedded
spreadsheet (`draw:object`, ODF 1.3 §10.4.6) or OLE object
(`draw:object-ole`, §10.4.7) vanished on open, and saving lost it.

Such objects are now kept whole: their files, media type (from the
manifest or the sub-document's body) and replacement picture.
- Display: the replacement picture when WordCraft can show it, else a
  placeholder of the object's size drawn in code (ruled for a
  spreadsheet).
- ODT save: written back as the same object, with its manifest entries
  and original replacement. LibreOffice renders a round-tripped
  spreadsheet pixel-identically.
- Document model: the files live in `passthrough` under `odf-object/`,
  linked from the picture's `ole` by a marker. The DOCX writer skips
  the marker and writes the picture alone.

Flat ODT, a fix to the chart commit: inline sub-documents lacked the
required `office:mimetype`, so the schema rejected flat files holding
charts. Sub-documents now go inline as full `office:document`s (media
type, styles, settings) and are split back into their files on
opening. Replacement pictures and OLE files go inline as base64. A
picture's data is matched to the element that holds it, no longer to
the last `draw:image` before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 13:35:59 +00:00

54 lines
3.0 KiB
Rust

//! An embedded spreadsheet in an ODT file (ODF 1.3 §10.4.6) opens as a picture WordCraft keeps
//! the object behind: saved as ODT it is the spreadsheet again; saved as DOCX it is the picture
//! alone, never ODF markup. The ODT is original and synthetic, shaped like LibreOffice's.
use std::io::{Read, Write};
use wordcraft_doc::para::InlineObject;
use wordcraft_engine::io;
const NS: &str = r#"xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink""#;
fn odt() -> Vec<u8> {
let content = format!(
r#"<office:document-content {NS}><office:body><office:text><text:p>Costs <draw:frame svg:width="3in" svg:height="1in"><draw:object xlink:href="./Object 1"/><draw:image xlink:href="./ObjectReplacements/Object 1"/></draw:frame></text:p></office:text></office:body></office:document-content>"#
);
let sheet = r#"<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"><office:body><office:spreadsheet/></office:body></office:document-content>"#;
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
let entries: [(&str, &[u8]); 4] = [
("mimetype", b"application/vnd.oasis.opendocument.text"),
("content.xml", content.as_bytes()),
("Object 1/content.xml", sheet.as_bytes()),
("ObjectReplacements/Object 1", b"VCLMTF not a picture WordCraft draws"),
];
for (name, data) in entries {
z.start_file(name, o).unwrap();
z.write_all(data).unwrap();
}
z.finish().unwrap().into_inner()
}
fn entry(pkg: &[u8], name: &str) -> Option<String> {
let mut z = zip::ZipArchive::new(std::io::Cursor::new(pkg)).unwrap();
let mut s = String::new();
z.by_name(name).ok()?.read_to_string(&mut s).ok()?;
Some(s)
}
#[test]
fn an_embedded_spreadsheet_survives_odt_and_becomes_a_picture_in_docx() {
let doc = io::open_bytes("sheet.odt", &odt()).unwrap();
let pictures =
doc.body.iter().filter_map(|b| b.as_para()).flat_map(|p| p.objects.iter()).filter(|o| matches!(o, InlineObject::Image { .. })).count();
assert_eq!(pictures, 1, "shown, by a placeholder");
let odt = io::save_bytes("sheet.odt", &doc).unwrap();
assert!(entry(&odt, "Object 1/content.xml").is_some_and(|c| c.contains("office:spreadsheet")));
let docx = io::save_bytes("sheet.docx", &doc).unwrap();
let body = entry(&docx, "word/document.xml").unwrap();
assert!(!body.contains("odf-object") && body.contains("<w:drawing>"), "{body}");
// The DOCX opens with its picture.
let again = io::open_bytes("sheet.docx", &docx).unwrap();
assert!(again.body.iter().filter_map(|b| b.as_para()).flat_map(|p| p.objects.iter()).any(|o| matches!(o, InlineObject::Image { .. })));
}