mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
* Add craft-wordmodel: WordCraft's document model without its editing code WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX, RTF, ODT and other text-format crates can read into and write from one model. Module paths, type names and serde attributes are unchanged: an app that re-exports the crate keeps its native file format. Left in the app: range editing (edit.rs), IME text and equation editing. Not model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs. * craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs The DOCX reader and writer walk an equation's runs. The walk and the child accessors it uses were in WordCraft's equation editor (math_edit.rs), which stays in the app; they are structure, not editing. * craft-wordmodel: keep the document operations (edit.rs) with the model The DOCX and RTF readers use them: alternative-format chunks are merged into a document with adopt_blocks, and a reader formats ranges with format_range and inserts objects. They operate on the document, not on a view, so they belong beside it. The tests that drove them come back with them; the paragraph walks stay in walk.rs and edit re-exports them. base64 0.23, as WordCraft and craft-offcrypto use. * Add craft-flow: the flow model the text formats map to, on craft-wordmodel WordCraft's crates/formats/src/model (07f97737) moved as it is: Flow, FBlock, Para, Inline, Fmt, tables and cells, pictures, to_doc and from_doc, picture sniffing, colours, base64 and the Dropped report. Only the paths changed (wordcraft_doc is craft_wordmodel); the report helpers are public so the format crates can call them; the image re-encoding helper stays with WordCraft's RTF writer.
73 lines
3.0 KiB
Rust
73 lines
3.0 KiB
Rust
//! ODF embedded objects ([`OdfObject`]) in a document: their files in
|
|
//! [`Document::passthrough`] under `odf-object/<n>/`, linked from the picture showing them by an
|
|
//! [`Embedded`] whose markup only names the object's media type, and taken back out for the ODT
|
|
//! writer. Other formats see the picture alone.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use craft_wordmodel::Document;
|
|
use craft_wordmodel::graphic::{Embedded, EmbeddedRef};
|
|
|
|
use super::types::OdfObject;
|
|
|
|
/// Relationship kind of an object's file, and of its replacement picture.
|
|
const FILE: &str = "file";
|
|
const REPLACEMENT: &str = "replacement";
|
|
/// Most objects a document keeps, and most files in one.
|
|
pub const MAX_OBJECTS: usize = 1000;
|
|
pub const MAX_FILES: usize = 256;
|
|
|
|
fn esc(s: &str) -> String {
|
|
s.replace('&', "&").replace('"', """).replace('<', "<").replace('>', ">")
|
|
}
|
|
|
|
fn unesc(s: &str) -> String {
|
|
s.replace(""", "\"").replace("<", "<").replace(">", ">").replace("&", "&")
|
|
}
|
|
|
|
/// Keep `obj` in `doc`: its files as passthrough parts, and the link to them. `None` when the
|
|
/// document already holds as many objects as it keeps.
|
|
pub fn store(doc: &mut Document, obj: &OdfObject) -> Option<Arc<Embedded>> {
|
|
let taken = |n: usize| {
|
|
let prefix = format!("odf-object/{n}/");
|
|
doc.passthrough.range(prefix.clone()..).next().is_some_and(|(k, _)| k.starts_with(&prefix))
|
|
};
|
|
let n = (0..MAX_OBJECTS).find(|&n| !taken(n))?;
|
|
let mut refs = Vec::new();
|
|
for (k, (path, bytes)) in obj.files.iter().take(MAX_FILES).enumerate() {
|
|
let target = format!("odf-object/{n}/{k}");
|
|
doc.passthrough.insert(target.clone(), bytes.clone());
|
|
refs.push(EmbeddedRef { id: path.clone(), kind: FILE.into(), target, external: false });
|
|
}
|
|
if let Some(r) = &obj.replacement {
|
|
let target = format!("odf-object/{n}/replacement");
|
|
doc.passthrough.insert(target.clone(), r.clone());
|
|
refs.push(EmbeddedRef { id: String::new(), kind: REPLACEMENT.into(), target, external: false });
|
|
}
|
|
let xml = format!("{} mime=\"{}\" ole=\"{}\"/>", Embedded::ODF_OBJECT, esc(&obj.mime), obj.ole);
|
|
Some(Arc::new(Embedded { xml, refs }))
|
|
}
|
|
|
|
/// The object `e` links to in `doc`, when it is an ODF object whose files are all there.
|
|
pub fn load(doc: &Document, e: &Embedded) -> Option<Arc<OdfObject>> {
|
|
if !e.is_odf_object() {
|
|
return None;
|
|
}
|
|
let attr = |k: &str| {
|
|
let pat = format!(" {k}=\"");
|
|
let from = e.xml.find(&pat)? + pat.len();
|
|
let len = e.xml.get(from..)?.find('"')?;
|
|
e.xml.get(from..from + len).map(unesc)
|
|
};
|
|
let mut obj = OdfObject { mime: attr("mime").unwrap_or_default(), ole: attr("ole").as_deref() == Some("true"), ..Default::default() };
|
|
for r in e.refs.iter().take(MAX_FILES + 1) {
|
|
let bytes = doc.passthrough.get(&r.target)?.clone();
|
|
match r.kind.as_str() {
|
|
FILE => obj.files.push((r.id.clone(), bytes)),
|
|
REPLACEMENT => obj.replacement = Some(bytes),
|
|
_ => {}
|
|
}
|
|
}
|
|
(!obj.files.is_empty()).then(|| Arc::new(obj))
|
|
}
|