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 | |
| 11 | use crate::office::odf::{ |
| 12 | NS_MANIFEST, |
| 13 | NS_OFFICE, |
| 14 | NS_STYLE, |
| 15 | NS_TEXT, |
| 16 | }; |
| 17 | use crate::zip::{ |
| 18 | Method, |
| 19 | Zip, |
| 20 | }; |
| 21 | |
| 22 | use oxedyne_fe2o3_core::prelude::*; |
| 23 | use oxedyne_fe2o3_text::xml::write::Out; |
| 24 | |
| 25 | pub 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. |
| 32 | pub 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. |
| 41 | pub 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. |
| 73 | pub 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. |
| 83 | pub 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`]. |
| 182 | pub 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 | } |