ppt_rs/generator/layouts/
common.rs1use crate::core::XmlWriter;
4use crate::generator::constants::{
5 SLIDE_WIDTH, SLIDE_HEIGHT,
6};
7use crate::generator::slide_content::BulletStyle;
8
9pub const XML_DECL: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#;
11pub const SLIDE_NS: &str = r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main""#;
12
13#[derive(Clone, Debug, Default)]
15pub struct ExtendedTextProps {
16 pub size: u32,
17 pub bold: bool,
18 pub italic: bool,
19 pub underline: bool,
20 pub strikethrough: bool,
21 pub subscript: bool,
22 pub superscript: bool,
23 pub color: Option<String>,
24 pub highlight: Option<String>,
25 pub font_family: Option<String>,
26}
27
28impl ExtendedTextProps {
29 pub fn new(size: u32) -> Self {
30 Self {
31 size,
32 ..Default::default()
33 }
34 }
35
36 pub fn with_basic(size: u32, bold: bool, italic: bool, underline: bool, color: Option<&str>) -> Self {
37 Self {
38 size,
39 bold,
40 italic,
41 underline,
42 color: color.map(|c| c.trim_start_matches('#').to_uppercase()),
43 ..Default::default()
44 }
45 }
46
47 pub fn to_xml(&self) -> String {
48 let mut attrs = format!(
49 r#"<a:rPr lang="en-US" sz="{}" b="{}" i="{}" dirty="0""#,
50 self.size,
51 if self.bold { "1" } else { "0" },
52 if self.italic { "1" } else { "0" }
53 );
54
55 if self.underline {
56 attrs.push_str(r#" u="sng""#);
57 }
58
59 if self.strikethrough {
60 attrs.push_str(r#" strike="sngStrike""#);
61 }
62
63 if self.subscript {
64 attrs.push_str(r#" baseline="-25000""#);
65 } else if self.superscript {
66 attrs.push_str(r#" baseline="30000""#);
67 }
68
69 attrs.push('>');
70
71 if let Some(ref hex_color) = self.color {
72 let clean_color = hex_color.trim_start_matches('#').to_uppercase();
73 attrs.push_str(&format!(
74 r#"<a:solidFill><a:srgbClr val="{clean_color}"/></a:solidFill>"#
75 ));
76 }
77
78 if let Some(ref highlight) = self.highlight {
79 let clean_color = highlight.trim_start_matches('#').to_uppercase();
80 attrs.push_str(&format!(
81 r#"<a:highlight><a:srgbClr val="{clean_color}"/></a:highlight>"#
82 ));
83 }
84
85 if let Some(ref font) = self.font_family {
86 attrs.push_str(&format!(
87 r#"<a:latin typeface="{font}"/><a:cs typeface="{font}"/>"#
88 ));
89 }
90
91 attrs.push_str("</a:rPr>");
92 attrs
93 }
94}
95
96pub fn generate_text_props(
98 size: u32,
99 bold: bool,
100 italic: bool,
101 underline: bool,
102 color: Option<&str>,
103) -> String {
104 ExtendedTextProps::with_basic(size, bold, italic, underline, color).to_xml()
105}
106
107pub fn generate_text_props_extended(props: &ExtendedTextProps) -> String {
109 props.to_xml()
110}
111
112#[derive(Clone, Copy, Debug)]
114pub struct ShapePosition {
115 pub x: u32,
116 pub y: u32,
117 pub cx: u32,
118 pub cy: u32,
119}
120
121impl ShapePosition {
122 pub fn new(x: u32, y: u32, cx: u32, cy: u32) -> Self {
123 Self { x, y, cx, cy }
124 }
125}
126
127#[derive(Clone, Debug)]
129pub struct TextContent<'a> {
130 pub text: &'a str,
131 pub props: &'a str,
132}
133
134impl<'a> TextContent<'a> {
135 pub fn new(text: &'a str, props: &'a str) -> Self {
136 Self { text, props }
137 }
138}
139
140pub struct SlideXmlBuilder {
142 writer: XmlWriter,
143}
144
145impl SlideXmlBuilder {
146 pub fn new() -> Self {
147 Self {
148 writer: XmlWriter::new(),
149 }
150 }
151
152 pub fn start_slide_with_bg(mut self) -> Self {
154 self.writer.raw(XML_DECL);
155 self.writer.raw("\n<p:sld ");
156 self.writer.raw(SLIDE_NS);
157 self.writer.raw(">\n<p:cSld>\n");
158 self.writer.raw("<p:bg><p:bgRef idx=\"1001\"><a:schemeClr val=\"bg1\"/></p:bgRef></p:bg>\n");
159 self
160 }
161
162 pub fn start_sp_tree(mut self) -> Self {
164 self.writer.raw("<p:spTree>\n");
165 self.writer.raw("<p:nvGrpSpPr><p:cNvPr id=\"1\" name=\"\"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>\n");
166 self.writer.raw(&format!(
167 "<p:grpSpPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"{SLIDE_WIDTH}\" cy=\"{SLIDE_HEIGHT}\"/><a:chOff x=\"0\" y=\"0\"/><a:chExt cx=\"{SLIDE_WIDTH}\" cy=\"{SLIDE_HEIGHT}\"/></a:xfrm></p:grpSpPr>\n"
168 ));
169 self
170 }
171
172 pub fn add_title(mut self, id: u32, position: ShapePosition, content: TextContent<'_>, ph_type: &str) -> Self {
174 self.writer.raw(&format!(
175 r#"<p:sp>
176<p:nvSpPr>
177<p:cNvPr id="{}" name="Title"/>
178<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
179<p:nvPr><p:ph type="{}"/></p:nvPr>
180</p:nvSpPr>
181<p:spPr>
182<a:xfrm><a:off x="{}" y="{}"/><a:ext cx="{}" cy="{}"/></a:xfrm>
183<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
184<a:noFill/>
185</p:spPr>
186<p:txBody>
187<a:bodyPr/>
188<a:lstStyle/>
189<a:p>
190<a:r>
191{}
192<a:t>{}</a:t>
193</a:r>
194</a:p>
195</p:txBody>
196</p:sp>
197"#,
198 id, ph_type, position.x, position.y, position.cx, position.cy, content.props, escape_xml(content.text)
199 ));
200 self
201 }
202
203 pub fn add_centered_title(mut self, id: u32, position: ShapePosition, content: TextContent<'_>) -> Self {
205 self.writer.raw(&format!(
206 r#"<p:sp>
207<p:nvSpPr>
208<p:cNvPr id="{}" name="Title"/>
209<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
210<p:nvPr><p:ph type="ctrTitle"/></p:nvPr>
211</p:nvSpPr>
212<p:spPr>
213<a:xfrm><a:off x="{}" y="{}"/><a:ext cx="{}" cy="{}"/></a:xfrm>
214<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
215<a:noFill/>
216</p:spPr>
217<p:txBody>
218<a:bodyPr/>
219<a:lstStyle/>
220<a:p>
221<a:pPr algn="ctr"/>
222<a:r>
223{}
224<a:t>{}</a:t>
225</a:r>
226</a:p>
227</p:txBody>
228</p:sp>
229"#,
230 id, position.x, position.y, position.cx, position.cy, content.props, escape_xml(content.text)
231 ));
232 self
233 }
234
235 pub fn start_content_body(mut self, id: u32, x: u32, y: u32, cx: u32, cy: u32) -> Self {
237 self.writer.raw(&format!(
238 r#"<p:sp>
239<p:nvSpPr>
240<p:cNvPr id="{}" name="Content"/>
241<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
242<p:nvPr><p:ph type="body" idx="1"/></p:nvPr>
243</p:nvSpPr>
244<p:spPr>
245<a:xfrm><a:off x="{}" y="{}"/><a:ext cx="{}" cy="{}"/></a:xfrm>
246<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
247<a:noFill/>
248</p:spPr>
249<p:txBody>
250<a:bodyPr/>
251<a:lstStyle/>
252"#,
253 id, x, y, cx, cy
254 ));
255 self
256 }
257
258 pub fn add_bullet(self, text: &str, props: &str, level: u32) -> Self {
260 self.add_bullet_with_style(text, props, level, BulletStyle::Bullet)
261 }
262
263 pub fn add_bullet_with_style(mut self, text: &str, props: &str, level: u32, style: BulletStyle) -> Self {
265 let indent = 457200 + (level * 457200); let margin_left = level * 457200 + indent;
267 let bullet_xml = style.to_xml();
268
269 self.writer.raw(&format!(
270 r#"<a:p>
271<a:pPr lvl="{}" marL="{}" indent="-{}">
272{}
273</a:pPr>
274<a:r>
275{}
276<a:t>{}</a:t>
277</a:r>
278</a:p>
279"#,
280 level, margin_left, indent, bullet_xml, props, escape_xml(text)
281 ));
282 self
283 }
284
285 pub fn end_content_body(mut self) -> Self {
287 self.writer.raw("</p:txBody>\n</p:sp>\n");
288 self
289 }
290
291 pub fn raw(mut self, xml: &str) -> Self {
293 self.writer.raw(xml);
294 self
295 }
296
297 pub fn end_sp_tree(mut self) -> Self {
299 self.writer.raw("</p:spTree>\n");
300 self
301 }
302
303 pub fn end_slide(mut self) -> Self {
305 self.writer.raw("</p:cSld>\n<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>\n</p:sld>");
306 self
307 }
308
309 pub fn build(self) -> String {
311 self.writer.finish()
312 }
313}
314
315impl Default for SlideXmlBuilder {
316 fn default() -> Self {
317 Self::new()
318 }
319}
320
321pub use crate::core::escape_xml;
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_generate_text_props() {
329 let props = generate_text_props(2400, true, false, false, Some("FF0000"));
330 assert!(props.contains("b=\"1\""));
331 assert!(props.contains("sz=\"2400\""));
332 assert!(props.contains("FF0000"));
333 }
334
335 #[test]
336 fn test_escape_xml() {
337 assert_eq!(escape_xml("a & b"), "a & b");
338 assert_eq!(escape_xml("<tag>"), "<tag>");
339 }
340
341 #[test]
342 fn test_slide_builder() {
343 let xml = SlideXmlBuilder::new()
344 .start_slide_with_bg()
345 .start_sp_tree()
346 .end_sp_tree()
347 .end_slide()
348 .build();
349
350 assert!(xml.contains("p:sld"));
351 assert!(xml.contains("p:spTree"));
352 }
353}