Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_file/src/office/odf/pkg.rs

7.8 KiB, 18 runs

created by r1870400018:22906, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The OpenDocument package: the four members every one of them has, and the manifest that lists
2//! them.
3//!
4//! Written once here rather than three times in the writers beside it, because the part that must not
5//! be got wrong -- `mimetype` first and stored -- is the part it would be easiest to get wrong
6//! separately in each.
7//!
8//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
9//! Anthropic Claude
10
11use crate::office::odf::{
12 NS_MANIFEST,
13 NS_OFFICE,
14 NS_STYLE,
15 NS_TEXT,
16};
17use crate::zip::{
18 Method,
19 Zip,
20};
21
22use oxedyne_fe2o3_core::prelude::*;
23use oxedyne_fe2o3_text::xml::write::Out;
24
25pub const VERSION: &str = "1.3"; // the OpenDocument version written
26
27/// **`mimetype` FIRST and STORED, and neither half is negotiable.** The format requires it so a
28/// reader can name the file from its opening bytes without inflating anything, which is exactly what
29/// `oxedyne_fe2o3_stds::media` now does. A package that deflates it, or writes it second, is a file
30/// every reader calls a plain ZIP -- and it fails that way silently, opening as an archive rather
31/// than refusing.
32pub fn start(media: &str) -> Zip {
33 let mut zip = Zip::new();
34 zip.set_first("mimetype", media.as_bytes().to_vec(), Method::Store);
35 zip
36}
37
38/// Called last, so the manifest lists what is actually there rather than what a caller intended. A
39/// manifest naming a member the archive does not hold is the OpenDocument equivalent of a content
40/// type override with no part behind it.
41pub fn finish(zip: &mut Zip, media: &str) -> Outcome<()> {
42 let mut out = Out::declared();
43 out.open("manifest:manifest", &[
44 ("xmlns:manifest", NS_MANIFEST),
45 ("manifest:version", VERSION),
46 ]);
47 out.empty("manifest:file-entry", &[
48 ("manifest:full-path", "/"),
49 ("manifest:version", VERSION),
50 ("manifest:media-type", media),
51 ]);
52 let names: Vec<String> = zip.names().iter().map(|n| n.to_string()).collect();
53 for name in names {
54 // `mimetype` is the package's own declaration and is not a member the manifest lists.
55 if name == "mimetype" {
56 continue;
57 }
58 out.empty("manifest:file-entry", &[
59 ("manifest:full-path", &name),
60 ("manifest:media-type", "text/xml"),
61 ]);
62 }
63 res!(out.close("manifest:manifest"));
64 zip.set("META-INF/manifest.xml", res!(out.finish()).into_bytes(), Method::Deflate);
65 Ok(())
66}
67
68/// The `styles.xml` every package carries.
69///
70/// Minimal on purpose. OpenDocument's own defaults are sensible and a reader applies its template
71/// where a document says nothing, so a writer that specified every font and every margin would be
72/// overriding the reader's choices rather than expressing the author's.
73pub fn styles() -> Outcome<String> {
74 styles_for("")
75}
76
77/// A presentation needs one thing the other two do not: a MASTER PAGE. Without it a reader treats
78/// every frame on a slide as a plain drawing box rather than as a placeholder, and
79/// `presentation:class="title"` becomes meaningless -- LibreOffice drops the attribute on re-save and
80/// the deck's titles stop being titles. The text still appears, so nothing looks broken until
81/// somebody tries to use an outline view. Measured, not assumed: it is what the fixture came back as
82/// before this was written.
83pub fn styles_for(media: &str) -> Outcome<String> {
84 let slides = media == "application/vnd.oasis.opendocument.presentation";
85 let mut out = Out::declared();
86 out.open("office:document-styles", &[
87 ("xmlns:office", NS_OFFICE),
88 ("xmlns:style", NS_STYLE),
89 ("xmlns:text", NS_TEXT),
90 ("xmlns:draw", "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"),
91 ("xmlns:fo", "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"),
92 ("office:version", VERSION),
93 ]);
94 out.open("office:styles", &[]);
95 // A quotation is indented and italic, which is the one thing a reader has no default for that
96 // the document tree can actually carry.
97 out.open("style:style", &[
98 ("style:name", "Quotations"),
99 ("style:family", "paragraph"),
100 ("style:parent-style-name", "Standard"),
101 ]);
102 res!(out.close("style:style"));
103 out.open("style:style", &[
104 ("style:name", "Preformatted_20_Text"),
105 ("style:display-name", "Preformatted Text"),
106 ("style:family", "paragraph"),
107 ("style:parent-style-name", "Standard"),
108 ]);
109 res!(out.close("style:style"));
110 // The three text styles the writers apply to a span. **A style name a document does not DEFINE
111 // is dropped by the reader**, span and all: without these, every bold word written here arrived
112 // in LibreOffice as plain text, and it was the round trip through it that showed so.
113 for (name, display, weight, style, font) in [
114 ("Strong_20_Emphasis", "Strong Emphasis", Some("bold"), None, None),
115 ("Emphasis", "Emphasis", None, Some("italic"), None),
116 ("Source_20_Text", "Source Text", None, None, Some("Liberation Mono")),
117 ] {
118 out.open("style:style", &[
119 ("style:name", name),
120 ("style:display-name", display),
121 ("style:family", "text"),
122 ]);
123 let mut props: Vec<(&str, &str)> = Vec::new();
124 if let Some(w) = weight {
125 props.push(("fo:font-weight", w));
126 props.push(("style:font-weight-asian", w));
127 props.push(("style:font-weight-complex", w));
128 }
129 if let Some(i) = style {
130 props.push(("fo:font-style", i));
131 props.push(("style:font-style-asian", i));
132 props.push(("style:font-style-complex", i));
133 }
134 if let Some(f) = font {
135 props.push(("style:font-name", f));
136 props.push(("fo:font-family", f));
137 }
138 out.empty("style:text-properties", &props);
139 res!(out.close("style:style"));
140 }
141 res!(out.close("office:styles"));
142 // AFTER `office:styles`, and both before `office:master-styles`. `office:document-styles` is a
143 // SEQUENCE -- `office:font-face-decls?`, `office:styles?`, `office:automatic-styles?`,
144 // `office:master-styles?` -- so where these go is not a matter of taste. The presentation branch
145 // used to open the automatic styles first, which put the two the wrong way round; `.odt` and `.ods`
146 // take neither branch and were always in order, which is why only the deck was wrong.
147 if slides {
148 out.open("office:automatic-styles", &[]);
149 out.open("style:page-layout", &[("style:name", "PM1")]);
150 out.empty("style:page-layout-properties", &[
151 ("fo:page-width", "28cm"),
152 ("fo:page-height", "15.75cm"),
153 ("style:print-orientation", "landscape"),
154 ("fo:margin-top", "0cm"), ("fo:margin-bottom", "0cm"),
155 ("fo:margin-left", "0cm"), ("fo:margin-right", "0cm"),
156 ]);
157 res!(out.close("style:page-layout"));
158 out.empty("style:style", &[("style:name", "dp1"), ("style:family", "drawing-page")]);
159 res!(out.close("office:automatic-styles"));
160 out.open("office:master-styles", &[]);
161 out.empty("style:master-page", &[
162 ("style:name", "Default"),
163 ("style:page-layout-name", "PM1"),
164 ("draw:style-name", "dp1"),
165 ]);
166 res!(out.close("office:master-styles"));
167 }
168 res!(out.close("office:document-styles"));
169 out.finish()
170}
171
172/// The `meta.xml` every package carries.
173///
174/// It names the generator and nothing else. No date: a document written twice from the same source
175/// must give the same bytes, and a timestamp is the one field that guarantees it will not.
176///
177/// **No `office:mimetype` here.** The grammar defines that attribute in `office-document-attrs`, and
178/// the only element referring to those is `office:document` -- the root of the FLAT single-file form,
179/// where there is no `mimetype` member to carry the fact instead. On `office:document-meta` it is
180/// simply not allowed, and the same mistake reached all three writers because they share this
181/// function. A package says what it is in its `mimetype` member; see [`start`].
182pub fn meta() -> Outcome<String> {
183 let mut out = Out::declared();
184 out.open("office:document-meta", &[
185 ("xmlns:office", NS_OFFICE),
186 ("xmlns:meta", "urn:oasis:names:tc:opendocument:xmlns:meta:1.0"),
187 ("office:version", VERSION),
188 ]);
189 out.open("office:meta", &[]);
190 out.leaf("meta:generator", &[], "Hematite/fe2o3_file");
191 res!(out.close("office:meta"));
192 res!(out.close("office:document-meta"));
193 out.finish()
194}