Oregami
Repositories/oxedyne/fe2o3

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
16use crate::office::pptx::{
17 MARGIN,
18 NS_A,
19 NS_P,
20 SLIDE_H,
21 SLIDE_W,
22 TITLE_H,
23};
24use crate::office::opc::NS_R;
25
26use oxedyne_fe2o3_core::prelude::*;
27use oxedyne_fe2o3_text::xml::write::Out;
28
29const 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.
36const 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.
52pub 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.
120pub 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.
145pub 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.
162fn 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.
188pub 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}