mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-07 03:01:32 +00:00
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
54 lines
3.0 KiB
Rust
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 { .. })));
|
|
}
|