oxedyne/fe2o3/fe2o3_file/src/office/pptx/parts.rs
8.1 KiB, 11 runs
created by r1870400018:22876, 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 three parts a `.pptx` needs before it can hold a slide: a theme, a slide master and a slide |
| 2 | //! layout. |
| 3 | //! |
| 4 | //! None of this is content and all of it is required. A slide points at a layout, a layout at a |
| 5 | //! master, and a master at a theme; PowerPoint refuses the file if any link is missing, and refuses |
| 6 | //! it with a repair prompt that names neither the part nor the reason. That is the worst kind of |
| 7 | //! failure to debug, so the whole chain is written whether or not a deck uses any of it. |
| 8 | //! |
| 9 | //! It is generated rather than held as a blob of literal XML because it is almost entirely |
| 10 | //! repetition -- twelve colours, three fill styles, three line styles, three effect styles, three |
| 11 | //! background fills -- and a blob is where a typo in the eleventh colour waits. |
| 12 | //! |
| 13 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 14 | //! Anthropic Claude |
| 15 | |
| 16 | use crate::office::pptx::{ |
| 17 | MARGIN, |
| 18 | NS_A, |
| 19 | NS_P, |
| 20 | SLIDE_H, |
| 21 | SLIDE_W, |
| 22 | TITLE_H, |
| 23 | }; |
| 24 | use crate::office::opc::NS_R; |
| 25 | |
| 26 | use oxedyne_fe2o3_core::prelude::*; |
| 27 | use oxedyne_fe2o3_text::xml::write::Out; |
| 28 | |
| 29 | const LAYOUT_TYPE: &str = "tx"; // ST_SlideLayoutType for "Title and Text" |
| 30 | |
| 31 | /// The twelve entries of a colour scheme, in the order the schema requires them. |
| 32 | /// |
| 33 | /// The order is not decoration: a reader takes the first as dark 1, the second as light 1, and so on |
| 34 | /// down, so a scheme written in another order is a deck whose text comes out the colour of its |
| 35 | /// background. |
| 36 | const SCHEME: [(&str, &str); 12] = [ |
| 37 | ("dk1", "000000"), |
| 38 | ("lt1", "FFFFFF"), |
| 39 | ("dk2", "44546A"), |
| 40 | ("lt2", "E7E6E6"), |
| 41 | ("accent1", "4472C4"), |
| 42 | ("accent2", "ED7D31"), |
| 43 | ("accent3", "A5A5A5"), |
| 44 | ("accent4", "FFC000"), |
| 45 | ("accent5", "5B9BD5"), |
| 46 | ("accent6", "70AD47"), |
| 47 | ("hlink", "0563C1"), |
| 48 | ("folHlink", "954F72"), |
| 49 | ]; |
| 50 | |
| 51 | /// The theme: a colour scheme, a font scheme, and the format scheme PowerPoint insists on. |
| 52 | pub fn theme() -> Outcome<String> { |
| 53 | let mut out = Out::declared(); |
| 54 | out.open("a:theme", &[("xmlns:a", NS_A), ("name", "Daimond")]); |
| 55 | out.open("a:themeElements", &[]); |
| 56 | |
| 57 | out.open("a:clrScheme", &[("name", "Daimond")]); |
| 58 | for (name, rgb) in SCHEME { |
| 59 | out.open(&fmt!("a:{}", name), &[]); |
| 60 | // The first two are system colours in every theme Office writes, and a reader that expects |
| 61 | // one and finds an RGB still renders. `srgbClr` throughout keeps this one file readable. |
| 62 | out.empty("a:srgbClr", &[("val", rgb)]); |
| 63 | res!(out.close(&fmt!("a:{}", name))); |
| 64 | } |
| 65 | res!(out.close("a:clrScheme")); |
| 66 | |
| 67 | out.open("a:fontScheme", &[("name", "Daimond")]); |
| 68 | for which in ["a:majorFont", "a:minorFont"] { |
| 69 | out.open(which, &[]); |
| 70 | out.empty("a:latin", &[("typeface", "Calibri")]); |
| 71 | out.empty("a:ea", &[("typeface", "")]); |
| 72 | out.empty("a:cs", &[("typeface", "")]); |
| 73 | res!(out.close(which)); |
| 74 | } |
| 75 | res!(out.close("a:fontScheme")); |
| 76 | |
| 77 | // Three of each, always. The schema requires exactly three subtle-to-intense variants in each of |
| 78 | // the four lists, and a deck with two opens with a repair prompt. |
| 79 | out.open("a:fmtScheme", &[("name", "Daimond")]); |
| 80 | out.open("a:fillStyleLst", &[]); |
| 81 | for _ in 0..3 { |
| 82 | out.open("a:solidFill", &[]); |
| 83 | out.empty("a:schemeClr", &[("val", "phClr")]); |
| 84 | res!(out.close("a:solidFill")); |
| 85 | } |
| 86 | res!(out.close("a:fillStyleLst")); |
| 87 | out.open("a:lnStyleLst", &[]); |
| 88 | for w in ["6350", "12700", "19050"] { |
| 89 | out.open("a:ln", &[("w", w), ("cap", "flat"), ("cmpd", "sng"), ("algn", "ctr")]); |
| 90 | out.open("a:solidFill", &[]); |
| 91 | out.empty("a:schemeClr", &[("val", "phClr")]); |
| 92 | res!(out.close("a:solidFill")); |
| 93 | out.empty("a:prstDash", &[("val", "solid")]); |
| 94 | res!(out.close("a:ln")); |
| 95 | } |
| 96 | res!(out.close("a:lnStyleLst")); |
| 97 | out.open("a:effectStyleLst", &[]); |
| 98 | for _ in 0..3 { |
| 99 | out.open("a:effectStyle", &[]); |
| 100 | out.open("a:effectLst", &[]); |
| 101 | res!(out.close("a:effectLst")); |
| 102 | res!(out.close("a:effectStyle")); |
| 103 | } |
| 104 | res!(out.close("a:effectStyleLst")); |
| 105 | out.open("a:bgFillStyleLst", &[]); |
| 106 | for _ in 0..3 { |
| 107 | out.open("a:solidFill", &[]); |
| 108 | out.empty("a:schemeClr", &[("val", "phClr")]); |
| 109 | res!(out.close("a:solidFill")); |
| 110 | } |
| 111 | res!(out.close("a:bgFillStyleLst")); |
| 112 | res!(out.close("a:fmtScheme")); |
| 113 | |
| 114 | res!(out.close("a:themeElements")); |
| 115 | res!(out.close("a:theme")); |
| 116 | out.finish() |
| 117 | } |
| 118 | |
| 119 | /// The slide master: the shape tree every slide inherits, and the map from scheme colours to roles. |
| 120 | pub fn master() -> Outcome<String> { |
| 121 | let mut out = Out::declared(); |
| 122 | out.open("p:sldMaster", &[("xmlns:a", NS_A), ("xmlns:r", NS_R), ("xmlns:p", NS_P)]); |
| 123 | res!(shape_tree(&mut out, true)); |
| 124 | // Which scheme entry plays which role. Getting this wrong is how a deck comes out with white |
| 125 | // text on a white background, so it is written explicitly rather than left to a default. |
| 126 | out.empty("p:clrMap", &[ |
| 127 | ("bg1", "lt1"), ("tx1", "dk1"), ("bg2", "lt2"), ("tx2", "dk2"), |
| 128 | ("accent1", "accent1"), ("accent2", "accent2"), ("accent3", "accent3"), |
| 129 | ("accent4", "accent4"), ("accent5", "accent5"), ("accent6", "accent6"), |
| 130 | ("hlink", "hlink"), ("folHlink", "folHlink"), |
| 131 | ]); |
| 132 | out.open("p:sldLayoutIdLst", &[]); |
| 133 | out.empty("p:sldLayoutId", &[("id", "2147483649"), ("r:id", "rId1")]); |
| 134 | res!(out.close("p:sldLayoutIdLst")); |
| 135 | res!(out.close("p:sldMaster")); |
| 136 | out.finish() |
| 137 | } |
| 138 | |
| 139 | /// The one slide layout: a title and a body, which is the only shape a generated deck needs. |
| 140 | /// |
| 141 | /// `type="tx"` is "Title and Text", and it is the name for this shape however wrong it reads. |
| 142 | /// `ST_SlideLayoutType` is a closed list of 36 tokens and `titleAndBody` -- the obvious guess, and what |
| 143 | /// stood here -- is not one of them; `tx` and `obj` are the two that mean title-plus-body. LibreOffice |
| 144 | /// converted the deck without a murmur, so only the schema was ever going to catch it. |
| 145 | pub fn layout() -> Outcome<String> { |
| 146 | let mut out = Out::declared(); |
| 147 | out.open("p:sldLayout", &[ |
| 148 | ("xmlns:a", NS_A), ("xmlns:r", NS_R), ("xmlns:p", NS_P), |
| 149 | ("type", LAYOUT_TYPE), ("preserve", "1"), |
| 150 | ]); |
| 151 | res!(shape_tree(&mut out, false)); |
| 152 | res!(out.close("p:sldLayout")); |
| 153 | out.finish() |
| 154 | } |
| 155 | |
| 156 | /// The common shell of a master's or a layout's shape tree: the two placeholders and the group that |
| 157 | /// holds them. |
| 158 | /// |
| 159 | /// Identical in both but for the element name around it, so it is written once. The placeholders |
| 160 | /// carry explicit geometry rather than inheriting it, because a placeholder with no `xfrm` anywhere |
| 161 | /// up its chain is a shape a reader puts at the origin with no size. |
| 162 | fn shape_tree(out: &mut Out, master: bool) -> Outcome<()> { |
| 163 | out.open("p:cSld", &[]); |
| 164 | out.open("p:spTree", &[]); |
| 165 | out.open("p:nvGrpSpPr", &[]); |
| 166 | out.empty("p:cNvPr", &[("id", "1"), ("name", "")]); |
| 167 | out.empty("p:cNvGrpSpPr", &[]); |
| 168 | out.empty("p:nvPr", &[]); |
| 169 | res!(out.close("p:nvGrpSpPr")); |
| 170 | out.open("p:grpSpPr", &[]); |
| 171 | out.open("a:xfrm", &[]); |
| 172 | out.empty("a:off", &[("x", "0"), ("y", "0")]); |
| 173 | out.empty("a:ext", &[("cx", "0"), ("cy", "0")]); |
| 174 | out.empty("a:chOff", &[("x", "0"), ("y", "0")]); |
| 175 | out.empty("a:chExt", &[("cx", "0"), ("cy", "0")]); |
| 176 | res!(out.close("a:xfrm")); |
| 177 | res!(out.close("p:grpSpPr")); |
| 178 | res!(placeholder(out, 2, "Title", "title", None, MARGIN, MARGIN, SLIDE_W - 2 * MARGIN, TITLE_H)); |
| 179 | res!(placeholder(out, 3, "Body", "body", Some("1"), |
| 180 | MARGIN, MARGIN + TITLE_H, SLIDE_W - 2 * MARGIN, SLIDE_H - TITLE_H - 2 * MARGIN)); |
| 181 | res!(out.close("p:spTree")); |
| 182 | res!(out.close("p:cSld")); |
| 183 | let _ = master; |
| 184 | Ok(()) |
| 185 | } |
| 186 | |
| 187 | /// One placeholder shape, with its geometry written out. |
| 188 | pub fn placeholder( |
| 189 | out: &mut Out, |
| 190 | id: u32, |
| 191 | name: &str, |
| 192 | kind: &str, |
| 193 | idx: Option<&str>, |
| 194 | x: i64, |
| 195 | y: i64, |
| 196 | cx: i64, |
| 197 | cy: i64, |
| 198 | ) |
| 199 | -> Outcome<()> |
| 200 | { |
| 201 | out.open("p:sp", &[]); |
| 202 | out.open("p:nvSpPr", &[]); |
| 203 | out.empty("p:cNvPr", &[("id", &fmt!("{}", id)), ("name", name)]); |
| 204 | out.open("p:cNvSpPr", &[]); |
| 205 | out.empty("a:spLocks", &[("noGrp", "1")]); |
| 206 | res!(out.close("p:cNvSpPr")); |
| 207 | out.open("p:nvPr", &[]); |
| 208 | match idx { |
| 209 | Some(i) => out.empty("p:ph", &[("type", kind), ("idx", i)]), |
| 210 | None => out.empty("p:ph", &[("type", kind)]), |
| 211 | } |
| 212 | res!(out.close("p:nvPr")); |
| 213 | res!(out.close("p:nvSpPr")); |
| 214 | out.open("p:spPr", &[]); |
| 215 | out.open("a:xfrm", &[]); |
| 216 | out.empty("a:off", &[("x", &fmt!("{}", x)), ("y", &fmt!("{}", y))]); |
| 217 | out.empty("a:ext", &[("cx", &fmt!("{}", cx)), ("cy", &fmt!("{}", cy))]); |
| 218 | res!(out.close("a:xfrm")); |
| 219 | out.empty("a:prstGeom", &[("prst", "rect")]); |
| 220 | res!(out.close("p:spPr")); |
| 221 | out.open("p:txBody", &[]); |
| 222 | out.empty("a:bodyPr", &[]); |
| 223 | out.empty("a:lstStyle", &[]); |
| 224 | out.open("a:p", &[]); |
| 225 | res!(out.close("a:p")); |
| 226 | res!(out.close("p:txBody")); |
| 227 | res!(out.close("p:sp")); |
| 228 | Ok(()) |
| 229 | } |