1use std::fmt;
2use std::io::Write;
3
4use oxml_core::OxmlError;
5use oxml_core::raw_xml::{capture_element, capture_empty_element};
6use oxml_core::xml::{local_name, matches_local_name};
7use quick_xml::events::{BytesEnd, BytesStart, Event};
8use quick_xml::{Reader, Writer, XmlVersion};
9
10use crate::color::ColorError;
11use crate::fill::FillError;
12use crate::namespace::reject_conflicting_a_prefix;
13use crate::order::OrderedRawChildren;
14
15const MAX_TEXT_SPACING_PERCENT: i32 = 13_200_000;
16
17#[derive(Debug)]
19pub enum TextError {
20 Xml(OxmlError),
21 Color(ColorError),
22 Fill(FillError),
23 UnexpectedElement(String),
24 MissingAttribute {
25 element: String,
26 attribute: String,
27 },
28 MissingBodyProperties,
29 MissingParagraph,
30 DuplicateElement(String),
31 InvalidAttribute {
32 element: String,
33 attribute: String,
34 value: String,
35 },
36}
37
38impl fmt::Display for TextError {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 Self::Xml(error) => error.fmt(formatter),
42 Self::Color(error) => error.fmt(formatter),
43 Self::Fill(error) => error.fmt(formatter),
44 Self::UnexpectedElement(element) => {
45 write!(formatter, "unexpected DrawingML text element: {element}")
46 }
47 Self::MissingAttribute { element, attribute } => {
48 write!(formatter, "DrawingML {element} requires @{attribute}")
49 }
50 Self::MissingBodyProperties => write!(formatter, "DrawingML txBody requires bodyPr"),
51 Self::MissingParagraph => write!(formatter, "DrawingML txBody requires at least one p"),
52 Self::DuplicateElement(element) => {
53 write!(formatter, "DrawingML text contains duplicate {element}")
54 }
55 Self::InvalidAttribute {
56 element,
57 attribute,
58 value,
59 } => write!(
60 formatter,
61 "DrawingML {element} has invalid @{attribute}: {value}"
62 ),
63 }
64 }
65}
66
67impl std::error::Error for TextError {
68 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69 match self {
70 Self::Xml(error) => Some(error),
71 Self::Color(error) => Some(error),
72 Self::Fill(error) => Some(error),
73 _ => None,
74 }
75 }
76}
77
78impl From<OxmlError> for TextError {
79 fn from(error: OxmlError) -> Self {
80 Self::Xml(error)
81 }
82}
83
84impl From<ColorError> for TextError {
85 fn from(error: ColorError) -> Self {
86 Self::Color(error)
87 }
88}
89
90impl From<FillError> for TextError {
91 fn from(error: FillError) -> Self {
92 Self::Fill(error)
93 }
94}
95
96pub type Result<T> = std::result::Result<T, TextError>;
97
98#[derive(Clone, Debug, Eq, PartialEq)]
100pub enum Coordinate32Value {
101 Emu(i32),
102 UniversalMeasure(String),
103}
104
105impl Coordinate32Value {
106 fn parse(element: &str, attribute: &str, value: String) -> Result<Self> {
107 if let Ok(value) = value.parse::<i32>() {
108 return Ok(Self::Emu(value));
109 }
110 if is_universal_measure(&value) {
111 return Ok(Self::UniversalMeasure(value));
112 }
113 Err(invalid_attribute(element, attribute, value))
114 }
115
116 fn as_xml(&self) -> Result<String> {
117 match self {
118 Self::Emu(value) => Ok(value.to_string()),
119 Self::UniversalMeasure(value) if is_universal_measure(value) => Ok(value.clone()),
120 Self::UniversalMeasure(value) => {
121 Err(invalid_attribute("bodyPr", "inset", value.clone()))
122 }
123 }
124 }
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub enum TextAnchor {
130 Top,
131 Center,
132 Bottom,
133 Justified,
134 Distributed,
135}
136
137impl TextAnchor {
138 pub(crate) fn parse(value: &str) -> Option<Self> {
139 match value {
140 "t" => Some(Self::Top),
141 "ctr" => Some(Self::Center),
142 "b" => Some(Self::Bottom),
143 "just" => Some(Self::Justified),
144 "dist" => Some(Self::Distributed),
145 _ => None,
146 }
147 }
148
149 pub(crate) const fn as_str(self) -> &'static str {
150 match self {
151 Self::Top => "t",
152 Self::Center => "ctr",
153 Self::Bottom => "b",
154 Self::Justified => "just",
155 Self::Distributed => "dist",
156 }
157 }
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum TextWrap {
163 None,
164 Square,
165}
166
167impl TextWrap {
168 fn parse(value: &str) -> Option<Self> {
169 match value {
170 "none" => Some(Self::None),
171 "square" => Some(Self::Square),
172 _ => None,
173 }
174 }
175
176 const fn as_str(self) -> &'static str {
177 match self {
178 Self::None => "none",
179 Self::Square => "square",
180 }
181 }
182}
183
184#[derive(Clone, Copy, Debug, Eq, PartialEq)]
186pub enum TextVertical {
187 Horizontal,
188 Vertical,
189 Vertical270,
190 WordArtVertical,
191 EastAsianVertical,
192 MongolianVertical,
193 WordArtVerticalRtl,
194}
195
196impl TextVertical {
197 fn parse(value: &str) -> Option<Self> {
198 match value {
199 "horz" => Some(Self::Horizontal),
200 "vert" => Some(Self::Vertical),
201 "vert270" => Some(Self::Vertical270),
202 "wordArtVert" => Some(Self::WordArtVertical),
203 "eaVert" => Some(Self::EastAsianVertical),
204 "mongolianVert" => Some(Self::MongolianVertical),
205 "wordArtVertRtl" => Some(Self::WordArtVerticalRtl),
206 _ => None,
207 }
208 }
209
210 const fn as_str(self) -> &'static str {
211 match self {
212 Self::Horizontal => "horz",
213 Self::Vertical => "vert",
214 Self::Vertical270 => "vert270",
215 Self::WordArtVertical => "wordArtVert",
216 Self::EastAsianVertical => "eaVert",
217 Self::MongolianVertical => "mongolianVert",
218 Self::WordArtVerticalRtl => "wordArtVertRtl",
219 }
220 }
221}
222
223#[derive(Clone, Debug, Default, Eq, PartialEq)]
225pub struct NormalAutofit {
226 pub font_scale: Option<String>,
227 pub line_spacing_reduction: Option<String>,
228}
229
230#[derive(Clone, Debug, Eq, PartialEq)]
232pub enum TextAutofit {
233 NoAutofit,
234 Normal(NormalAutofit),
235 ShapeAutofit,
236}
237
238impl TextAutofit {
239 fn from_xml(xml: &[u8]) -> Result<Self> {
240 let mut reader = Reader::from_reader(xml);
241 let mut buffer = Vec::new();
242 loop {
243 match reader
244 .read_event_into(&mut buffer)
245 .map_err(OxmlError::from)?
246 {
247 Event::Empty(element) => return Self::from_start(&element),
248 Event::Start(element) => {
249 let autofit = Self::from_start(&element)?;
250 ensure_empty_element(&mut reader, local_name(element.name().as_ref()))?;
251 return Ok(autofit);
252 }
253 Event::Eof => {
254 return Err(TextError::UnexpectedElement("EOF".to_owned()));
255 }
256 _ => {}
257 }
258 buffer.clear();
259 }
260 }
261
262 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
263 match local_name(start.name().as_ref()) {
264 b"noAutofit" => Ok(Self::NoAutofit),
265 b"spAutoFit" => Ok(Self::ShapeAutofit),
266 b"normAutofit" => {
267 let font_scale = get_body_attr(start, b"fontScale");
268 let line_spacing_reduction = get_body_attr(start, b"lnSpcReduction");
269 if let Some(value) = font_scale.as_deref() {
270 validate_autofit_percent("fontScale", value, PercentKind::FontScale)?;
271 }
272 if let Some(value) = line_spacing_reduction.as_deref() {
273 validate_autofit_percent("lnSpcReduction", value, PercentKind::LineSpacing)?;
274 }
275 Ok(Self::Normal(NormalAutofit {
276 font_scale,
277 line_spacing_reduction,
278 }))
279 }
280 _ => Err(TextError::UnexpectedElement(element_name(start))),
281 }
282 }
283
284 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
285 match self {
286 Self::NoAutofit => write_empty(writer, BytesStart::new("a:noAutofit")),
287 Self::ShapeAutofit => write_empty(writer, BytesStart::new("a:spAutoFit")),
288 Self::Normal(normal) => {
289 let mut start = BytesStart::new("a:normAutofit");
290 if let Some(value) = normal.font_scale.as_deref() {
291 validate_autofit_percent("fontScale", value, PercentKind::FontScale)?;
292 start.push_attribute(("fontScale", value));
293 }
294 if let Some(value) = normal.line_spacing_reduction.as_deref() {
295 validate_autofit_percent("lnSpcReduction", value, PercentKind::LineSpacing)?;
296 start.push_attribute(("lnSpcReduction", value));
297 }
298 write_empty(writer, start)
299 }
300 }
301 }
302}
303
304#[allow(non_camel_case_types)]
309#[derive(Clone, Debug, Default, Eq, PartialEq)]
310pub struct CT_TextBodyProperties {
311 pub left_inset: Option<Coordinate32Value>,
312 pub top_inset: Option<Coordinate32Value>,
313 pub right_inset: Option<Coordinate32Value>,
314 pub bottom_inset: Option<Coordinate32Value>,
315 pub anchor: Option<TextAnchor>,
316 pub wrap: Option<TextWrap>,
317 pub vertical: Option<TextVertical>,
318 pub space_first_last_paragraph: Option<bool>,
319 pub autofit: Option<TextAutofit>,
320 raw_attributes: Vec<(String, String)>,
321 raw_children: OrderedRawChildren,
322 pub(super) autofit_source: Option<Vec<u8>>,
323}
324
325impl CT_TextBodyProperties {
326 pub fn from_xml(xml: &[u8]) -> Result<Self> {
328 let mut reader = Reader::from_reader(xml);
329 let mut buffer = Vec::new();
330 loop {
331 match reader
332 .read_event_into(&mut buffer)
333 .map_err(OxmlError::from)?
334 {
335 Event::Start(element) if matches_local_name(element.name().as_ref(), b"bodyPr") => {
336 return Self::from_element(&mut reader, &element);
337 }
338 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"bodyPr") => {
339 return Self::from_start(&element);
340 }
341 Event::Start(element) | Event::Empty(element) => {
342 return Err(TextError::UnexpectedElement(element_name(&element)));
343 }
344 Event::Eof => {
345 return Err(TextError::Xml(OxmlError::MissingElement(
346 "DrawingML body properties".to_owned(),
347 )));
348 }
349 _ => {}
350 }
351 buffer.clear();
352 }
353 }
354
355 pub(crate) fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
356 let mut properties = Self::from_start(start)?;
357 let mut boundary = 0;
358 let mut occurrences = [false; 5];
359 let mut buffer = Vec::new();
360 loop {
361 match reader
362 .read_event_into(&mut buffer)
363 .map_err(OxmlError::from)?
364 {
365 Event::Start(element) => {
366 let name = local_name(element.name().as_ref()).to_vec();
367 let raw = capture_element(reader, &element)?;
368 properties.capture_child(&name, raw, &mut boundary, &mut occurrences)?;
369 }
370 Event::Empty(element) => {
371 let name = local_name(element.name().as_ref()).to_vec();
372 let raw = capture_empty_element(&element)?;
373 properties.capture_child(&name, raw, &mut boundary, &mut occurrences)?;
374 }
375 Event::End(element) if matches_local_name(element.name().as_ref(), b"bodyPr") => {
376 break;
377 }
378 Event::Eof => return Err(missing_end("bodyPr")),
379 _ => {}
380 }
381 buffer.clear();
382 }
383 Ok(properties)
384 }
385
386 pub(crate) fn from_start(start: &BytesStart<'_>) -> Result<Self> {
387 if !matches_local_name(start.name().as_ref(), b"bodyPr") {
388 return Err(TextError::UnexpectedElement(element_name(start)));
389 }
390 reject_conflicting_a_prefix(start)?;
393 Ok(Self {
394 left_inset: parse_coordinate(start, b"lIns")?,
395 top_inset: parse_coordinate(start, b"tIns")?,
396 right_inset: parse_coordinate(start, b"rIns")?,
397 bottom_inset: parse_coordinate(start, b"bIns")?,
398 anchor: parse_enum(start, b"anchor", TextAnchor::parse)?,
399 wrap: parse_enum(start, b"wrap", TextWrap::parse)?,
400 vertical: parse_enum(start, b"vert", TextVertical::parse)?,
401 space_first_last_paragraph: parse_optional_bool(start, b"spcFirstLastPara")?,
402 autofit: None,
403 raw_attributes: capture_raw_attributes(start)?,
404 raw_children: OrderedRawChildren::default(),
405 autofit_source: None,
406 })
407 }
408
409 fn capture_child(
410 &mut self,
411 name: &[u8],
412 raw: Vec<u8>,
413 boundary: &mut usize,
414 occurrences: &mut [bool; 5],
415 ) -> Result<()> {
416 if let Some(index) = schema_choice_index(name) {
417 if occurrences[index] {
418 return Err(TextError::DuplicateElement(
419 String::from_utf8_lossy(name).into_owned(),
420 ));
421 }
422 occurrences[index] = true;
423 }
424 if is_autofit(name) {
425 self.autofit = Some(TextAutofit::from_xml(&raw)?);
426 let mut reader = Reader::from_reader(raw.as_slice());
427 let has_unknown_attributes = match reader.read_event().map_err(OxmlError::from)? {
428 Event::Start(start) | Event::Empty(start) => start.attributes().any(|attribute| {
429 attribute.is_ok_and(|attribute| {
430 !matches!(attribute.key.as_ref(), b"fontScale" | b"lnSpcReduction")
431 })
432 }),
433 _ => false,
434 };
435 let has_comments = loop {
436 match reader.read_event().map_err(OxmlError::from)? {
437 Event::Comment(_) => break true,
438 Event::Eof => break false,
439 _ => {}
440 }
441 };
442 if has_unknown_attributes || has_comments {
445 self.autofit_source = Some(raw);
446 }
447 *boundary = (*boundary).max(2);
448 return Ok(());
449 }
450 let slot = raw_boundary_after(name).saturating_sub(1);
453 self.raw_children.push((*boundary).max(slot), raw);
454 *boundary = (*boundary).max(raw_boundary_after(name));
455 Ok(())
456 }
457
458 pub fn to_xml(&self) -> Result<Vec<u8>> {
460 let mut writer = Writer::new(Vec::new());
461 self.write_xml(&mut writer)?;
462 Ok(writer.into_inner())
463 }
464
465 pub(crate) fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
466 let mut start = BytesStart::new("a:bodyPr");
467 let left_inset = self
468 .left_inset
469 .as_ref()
470 .map(Coordinate32Value::as_xml)
471 .transpose()?;
472 let top_inset = self
473 .top_inset
474 .as_ref()
475 .map(Coordinate32Value::as_xml)
476 .transpose()?;
477 let right_inset = self
478 .right_inset
479 .as_ref()
480 .map(Coordinate32Value::as_xml)
481 .transpose()?;
482 let bottom_inset = self
483 .bottom_inset
484 .as_ref()
485 .map(Coordinate32Value::as_xml)
486 .transpose()?;
487 for (name, value) in [
488 ("lIns", left_inset.as_deref()),
489 ("tIns", top_inset.as_deref()),
490 ("rIns", right_inset.as_deref()),
491 ("bIns", bottom_inset.as_deref()),
492 ] {
493 if let Some(value) = value {
494 start.push_attribute((name, value));
495 }
496 }
497 if let Some(anchor) = self.anchor {
498 start.push_attribute(("anchor", anchor.as_str()));
499 }
500 if let Some(wrap) = self.wrap {
501 start.push_attribute(("wrap", wrap.as_str()));
502 }
503 if let Some(vertical) = self.vertical {
504 start.push_attribute(("vert", vertical.as_str()));
505 }
506 if let Some(value) = self.space_first_last_paragraph {
507 start.push_attribute(("spcFirstLastPara", if value { "1" } else { "0" }));
508 }
509 for (name, value) in &self.raw_attributes {
510 start.push_attribute((name.as_str(), value.as_str()));
511 }
512
513 if self.autofit.is_none() && self.raw_children.is_empty() {
514 return write_empty(writer, start);
515 }
516 writer
517 .write_event(Event::Start(start))
518 .map_err(OxmlError::from)?;
519 emit_raw(writer, self.raw_children.at(0))?;
520 emit_raw(writer, self.raw_children.at(1))?;
521 if let Some(autofit) = &self.autofit {
522 if let Some(source) = &self.autofit_source {
523 write_source_autofit(writer, autofit, source)?;
524 } else {
525 autofit.write_xml(writer)?;
526 }
527 }
528 for boundary in 2..=5 {
529 emit_raw(writer, self.raw_children.at(boundary))?;
530 }
531 writer
532 .write_event(Event::End(BytesEnd::new("a:bodyPr")))
533 .map_err(OxmlError::from)?;
534 Ok(())
535 }
536
537 pub fn raw_children(&self) -> &OrderedRawChildren {
538 &self.raw_children
539 }
540}
541
542fn write_source_autofit<W: Write>(
546 writer: &mut Writer<W>,
547 autofit: &TextAutofit,
548 source: &[u8],
549) -> Result<()> {
550 if TextAutofit::from_xml(source)?.eq(autofit) {
551 writer
552 .get_mut()
553 .write_all(source)
554 .map_err(OxmlError::from)?;
555 return Ok(());
556 }
557 let mut validation = Writer::new(Vec::new());
558 autofit.write_xml(&mut validation)?;
559 let mut reader = Reader::from_reader(source);
560 let event = reader.read_event().map_err(OxmlError::from)?;
561 let (start, empty) = match event {
562 Event::Start(start) => (start, false),
563 Event::Empty(start) => (start, true),
564 _ => return Err(TextError::UnexpectedElement("autofit source".to_owned())),
565 };
566 let old_name = start.name().as_ref().to_vec();
567 let local = match autofit {
568 TextAutofit::Normal(_) => b"normAutofit".as_slice(),
569 TextAutofit::NoAutofit => b"noAutofit".as_slice(),
570 TextAutofit::ShapeAutofit => b"spAutoFit".as_slice(),
571 };
572 let prefix_end = old_name
573 .iter()
574 .rposition(|byte| *byte == b':')
575 .map_or(0, |i| i + 1);
576 let mut name = old_name[..prefix_end].to_vec();
577 name.extend_from_slice(local);
578 let mut edits = vec![(1, 1 + old_name.len(), name.clone())];
579 let values = match autofit {
580 TextAutofit::Normal(normal) => [
581 normal.font_scale.as_deref(),
582 normal.line_spacing_reduction.as_deref(),
583 ],
584 _ => [None, None],
585 };
586 let keys = [b"fontScale".as_slice(), b"lnSpcReduction".as_slice()];
587 let mut found = [false; 2];
588 let content: &[u8] = start.as_ref();
589 for attribute in start.attributes() {
590 let attribute = attribute.map_err(OxmlError::from)?;
591 let Some(index) = keys.iter().position(|key| *key == attribute.key.as_ref()) else {
592 continue;
593 };
594 found[index] = true;
595 let key_at = (attribute.key.as_ref().as_ptr() as usize)
596 .checked_sub(content.as_ptr() as usize)
597 .filter(|offset| *offset + attribute.key.as_ref().len() <= content.len())
598 .ok_or_else(|| TextError::UnexpectedElement("autofit attribute source".to_owned()))?
599 + 1;
600 let value_at = (attribute.value.as_ptr() as usize)
601 .checked_sub(content.as_ptr() as usize)
602 .filter(|offset| *offset + attribute.value.len() <= content.len())
603 .ok_or_else(|| TextError::UnexpectedElement("autofit value source".to_owned()))?
604 + 1;
605 let value_end = value_at + attribute.value.len();
606 if let Some(value) = values[index] {
607 edits.push((value_at, value_end, value.as_bytes().to_vec()));
608 } else {
609 edits.push((key_at, value_end + 1, Vec::new()));
611 }
612 }
613 let mut added = Vec::new();
614 for (index, value) in values.iter().enumerate() {
615 if !found[index]
616 && let Some(value) = value
617 {
618 added.extend_from_slice(b" ");
619 added.extend_from_slice(keys[index]);
620 added.extend_from_slice(b"=\"");
621 added.extend_from_slice(value.as_bytes());
622 added.extend_from_slice(b"\"");
623 }
624 }
625 let start_end = reader.buffer_position() as usize;
626 let insertion = start_end - if empty { 2 } else { 1 };
627 edits.push((insertion, insertion, added));
628 if !empty {
629 let close = source
630 .windows(2)
631 .rposition(|bytes| bytes == b"</")
632 .ok_or_else(|| missing_end("autofit"))?
633 + 2;
634 edits.push((close, close + old_name.len(), name));
635 }
636 edits.sort_by_key(|edit| std::cmp::Reverse(edit.0));
637 let mut output = source.to_vec();
638 for (start, end, bytes) in edits {
639 output.splice(start..end, bytes);
640 }
641 writer
642 .get_mut()
643 .write_all(&output)
644 .map_err(OxmlError::from)?;
645 Ok(())
646}
647
648const MODELLED_ATTRIBUTES: [&[u8]; 8] = [
650 b"lIns",
651 b"tIns",
652 b"rIns",
653 b"bIns",
654 b"anchor",
655 b"wrap",
656 b"vert",
657 b"spcFirstLastPara",
658];
659
660fn capture_raw_attributes(start: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
661 let mut raw = Vec::new();
662 for attribute in start.attributes() {
663 let attribute = attribute.map_err(OxmlError::from)?;
664 let key = attribute.key.as_ref();
665 if MODELLED_ATTRIBUTES.contains(&key) {
666 continue;
667 }
668 let name = std::str::from_utf8(key)
669 .map_err(OxmlError::from)?
670 .to_owned();
671 let value = attribute
672 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
673 .map_err(OxmlError::from)?
674 .into_owned();
675 raw.push((name, value));
676 }
677 Ok(raw)
678}
679
680fn parse_coordinate(start: &BytesStart<'_>, attribute: &[u8]) -> Result<Option<Coordinate32Value>> {
681 get_body_attr(start, attribute)
682 .map(|value| Coordinate32Value::parse("bodyPr", &String::from_utf8_lossy(attribute), value))
683 .transpose()
684}
685
686fn parse_enum<T>(
687 start: &BytesStart<'_>,
688 attribute: &[u8],
689 parse: impl FnOnce(&str) -> Option<T>,
690) -> Result<Option<T>> {
691 let Some(value) = get_body_attr(start, attribute) else {
692 return Ok(None);
693 };
694 parse(&value)
695 .map(Some)
696 .ok_or_else(|| invalid_attribute("bodyPr", &String::from_utf8_lossy(attribute), value))
697}
698
699fn parse_optional_bool(start: &BytesStart<'_>, attribute: &[u8]) -> Result<Option<bool>> {
700 let Some(value) = get_body_attr(start, attribute) else {
701 return Ok(None);
702 };
703 match value.as_str() {
704 "1" | "true" => Ok(Some(true)),
705 "0" | "false" => Ok(Some(false)),
706 _ => Err(invalid_attribute(
707 "bodyPr",
708 &String::from_utf8_lossy(attribute),
709 value,
710 )),
711 }
712}
713
714fn get_body_attr(start: &BytesStart<'_>, name: &[u8]) -> Option<String> {
715 start
716 .attributes()
717 .flatten()
718 .find(|attribute| attribute.key.as_ref() == name)
719 .and_then(|attribute| {
720 std::str::from_utf8(&attribute.value)
721 .ok()
722 .map(str::to_owned)
723 })
724}
725
726fn is_autofit(name: &[u8]) -> bool {
727 matches!(name, b"noAutofit" | b"normAutofit" | b"spAutoFit")
728}
729
730fn schema_choice_index(name: &[u8]) -> Option<usize> {
731 match name {
732 b"prstTxWarp" => Some(0),
733 name if is_autofit(name) => Some(1),
734 b"scene3d" => Some(2),
735 b"sp3d" | b"flatTx" => Some(3),
736 b"extLst" => Some(4),
737 _ => None,
738 }
739}
740
741fn raw_boundary_after(name: &[u8]) -> usize {
742 match name {
743 b"prstTxWarp" => 1,
744 name if is_autofit(name) => 2,
745 b"scene3d" => 3,
746 b"sp3d" | b"flatTx" => 4,
747 b"extLst" => 5,
748 _ => 0,
749 }
750}
751
752enum PercentKind {
753 FontScale,
754 LineSpacing,
755}
756
757fn validate_autofit_percent(attribute: &str, value: &str, kind: PercentKind) -> Result<()> {
758 if is_percentage_string(value) {
759 return Ok(());
760 }
761 let integer = value
762 .parse::<i32>()
763 .map_err(|_| invalid_attribute("normAutofit", attribute, value.to_owned()))?;
764 let valid = match kind {
765 PercentKind::FontScale => (1_000..=100_000).contains(&integer),
766 PercentKind::LineSpacing => (0..=MAX_TEXT_SPACING_PERCENT).contains(&integer),
767 };
768 if valid {
769 Ok(())
770 } else {
771 Err(invalid_attribute(
772 "normAutofit",
773 attribute,
774 value.to_owned(),
775 ))
776 }
777}
778
779fn is_percentage_string(value: &str) -> bool {
780 let Some(number) = value.strip_suffix('%') else {
781 return false;
782 };
783 is_signed_decimal(number)
784}
785
786fn is_universal_measure(value: &str) -> bool {
787 if value.len() < 3 {
788 return false;
789 }
790 let (number, unit) = value.split_at(value.len() - 2);
791 matches!(unit, "mm" | "cm" | "in" | "pt" | "pc" | "pi") && is_signed_decimal(number)
792}
793
794fn is_signed_decimal(value: &str) -> bool {
795 let unsigned = value.strip_prefix('-').unwrap_or(value);
796 let mut parts = unsigned.split('.');
797 let Some(integer) = parts.next() else {
798 return false;
799 };
800 if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
801 return false;
802 }
803 if let Some(fraction) = parts.next()
804 && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
805 {
806 return false;
807 }
808 parts.next().is_none()
809}
810
811fn ensure_empty_element(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
812 let mut buffer = Vec::new();
813 loop {
814 match reader
815 .read_event_into(&mut buffer)
816 .map_err(OxmlError::from)?
817 {
818 Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
819 return Ok(());
820 }
821 Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
822 Event::Comment(_) => {}
823 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
824 Event::Start(element) | Event::Empty(element) => {
825 return Err(TextError::UnexpectedElement(element_name(&element)));
826 }
827 _ => {
828 return Err(TextError::UnexpectedElement(
829 String::from_utf8_lossy(expected).into_owned(),
830 ));
831 }
832 }
833 buffer.clear();
834 }
835}
836
837fn emit_raw<'a, W: Write>(
838 writer: &mut Writer<W>,
839 children: impl Iterator<Item = &'a [u8]>,
840) -> Result<()> {
841 for child in children {
842 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
843 }
844 Ok(())
845}
846
847fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
848 writer
849 .write_event(Event::Empty(start))
850 .map_err(OxmlError::from)?;
851 Ok(())
852}
853
854fn invalid_attribute(element: &str, attribute: &str, value: String) -> TextError {
855 TextError::InvalidAttribute {
856 element: element.to_owned(),
857 attribute: attribute.to_owned(),
858 value,
859 }
860}
861
862fn element_name(element: &BytesStart<'_>) -> String {
863 String::from_utf8_lossy(element.name().as_ref()).into_owned()
864}
865
866pub(crate) fn missing_end(element: &str) -> TextError {
867 TextError::Xml(OxmlError::MissingElement(format!(
868 "closing DrawingML {element}"
869 )))
870}
871
872#[cfg(test)]
873mod tests {
874 use std::panic;
875
876 use super::{CT_TextBodyProperties, Coordinate32Value, TextAnchor, TextAutofit};
877 use crate::text::CT_TextBody;
878
879 #[test]
880 fn every_body_property_autofit_form_round_trips_in_schema_order() {
881 let cases: &[(&[u8], &[u8])] = &[
882 (
883 br#"<q:bodyPr lIns="-2147483648" tIns="2147483647" rIns="1.25in" bIns="0" anchor="dist" wrap="square" vert="wordArtVertRtl" spcFirstLastPara="true"><x:warp/><q:prstTxWarp prst="textPlain"/><x:beforeFit/><q:noAutofit/></q:bodyPr>"#,
884 br#"<a:bodyPr lIns="-2147483648" tIns="2147483647" rIns="1.25in" bIns="0" anchor="dist" wrap="square" vert="wordArtVertRtl" spcFirstLastPara="1"><x:warp/><q:prstTxWarp prst="textPlain"/><x:beforeFit/><a:noAutofit/></a:bodyPr>"#,
885 ),
886 (
887 br#"<q:bodyPr><q:spAutoFit/></q:bodyPr>"#,
888 br#"<a:bodyPr><a:spAutoFit/></a:bodyPr>"#,
889 ),
890 (
891 br#"<q:bodyPr><q:normAutofit fontScale="62.500%" lnSpcReduction="20000"/></q:bodyPr>"#,
892 br#"<a:bodyPr><a:normAutofit fontScale="62.500%" lnSpcReduction="20000"/></a:bodyPr>"#,
893 ),
894 ];
895
896 for (xml, expected) in cases {
897 let parsed = CT_TextBodyProperties::from_xml(xml).unwrap();
898 let written = parsed.to_xml().unwrap();
899 assert_eq!(&written, expected);
900 assert_eq!(CT_TextBodyProperties::from_xml(&written).unwrap(), parsed);
901 }
902
903 let parsed = CT_TextBodyProperties::from_xml(cases[0].0).unwrap();
904 assert_eq!(parsed.left_inset, Some(Coordinate32Value::Emu(i32::MIN)));
905 assert_eq!(parsed.space_first_last_paragraph, Some(true));
906 assert!(matches!(parsed.autofit, Some(TextAutofit::NoAutofit)));
907 }
908
909 #[test]
910 fn every_body_property_attribute_survives_a_round_trip() {
911 let xml = br#"<q:bodyPr xmlns:x="urn:producer" rot="5400000" spcFirstLastPara="1" vertOverflow="clip" horzOverflow="overflow" vert="vert270" wrap="square" lIns="1" tIns="2" rIns="3" bIns="4" numCol="2" spcCol="91440" rtlCol="1" fromWordArt="1" anchor="ctr" anchorCtr="0" forceAA="1" upright="1" compatLnSpc="0" x:marker="kept"><q:normAutofit fontScale="90000"/></q:bodyPr>"#;
912 let parsed = CT_TextBodyProperties::from_xml(xml).unwrap();
913 let written = parsed.to_xml().unwrap();
914 assert_eq!(
917 written,
918 br#"<a:bodyPr lIns="1" tIns="2" rIns="3" bIns="4" anchor="ctr" wrap="square" vert="vert270" spcFirstLastPara="1" xmlns:x="urn:producer" rot="5400000" vertOverflow="clip" horzOverflow="overflow" numCol="2" spcCol="91440" rtlCol="1" fromWordArt="1" anchorCtr="0" forceAA="1" upright="1" compatLnSpc="0" x:marker="kept"><a:normAutofit fontScale="90000"/></a:bodyPr>"#
919 );
920 let reparsed = CT_TextBodyProperties::from_xml(&written).unwrap();
921 assert_eq!(reparsed, parsed);
922 assert_eq!(reparsed.to_xml().unwrap(), written);
923
924 let mut edited = CT_TextBodyProperties::from_xml(
925 br#"<q:bodyPr numCol="3" spcCol="45720" rot="-600000" anchorCtr="1"/>"#,
926 )
927 .unwrap();
928 edited.anchor = Some(TextAnchor::Bottom);
929 edited.left_inset = Some(Coordinate32Value::Emu(0));
930 assert_eq!(
931 edited.to_xml().unwrap(),
932 br#"<a:bodyPr lIns="0" anchor="b" numCol="3" spcCol="45720" rot="-600000" anchorCtr="1"/>"#
933 );
934 }
935
936 #[test]
937 fn foreign_attribute_with_modelled_local_name_is_retained() {
938 let parsed = CT_TextBodyProperties::from_xml(
939 br#"<q:bodyPr xmlns:x="urn:producer" x:wrap="foreign" wrap="square"/>"#,
940 )
941 .unwrap();
942 assert!(parsed.wrap.is_some());
943 assert_eq!(
944 parsed.to_xml().unwrap(),
945 br#"<a:bodyPr wrap="square" xmlns:x="urn:producer" x:wrap="foreign"/>"#
946 );
947 }
948
949 #[test]
950 fn body_properties_preserve_unknown_children_at_their_boundaries() {
951 let xml = br#"<q:bodyPr><x:before/><q:prstTxWarp prst="textPlain"><x:warp/></q:prstTxWarp><x:beforeFit/><q:normAutofit fontScale="62500"/><x:afterFit/><q:scene3d><x:scene/></q:scene3d><x:afterScene/><q:sp3d><x:shape/></q:sp3d><x:after3d/><q:extLst><x:ext/></q:extLst><x:afterExt/></q:bodyPr>"#;
952 let written = CT_TextBodyProperties::from_xml(xml)
953 .unwrap()
954 .to_xml()
955 .unwrap();
956 assert_eq!(written, br#"<a:bodyPr><x:before/><q:prstTxWarp prst="textPlain"><x:warp/></q:prstTxWarp><x:beforeFit/><a:normAutofit fontScale="62500"/><x:afterFit/><q:scene3d><x:scene/></q:scene3d><x:afterScene/><q:sp3d><x:shape/></q:sp3d><x:after3d/><q:extLst><x:ext/></q:extLst><x:afterExt/></a:bodyPr>"#);
957 }
958
959 #[test]
960 fn an_added_autofit_is_written_before_preserved_scene_and_extension_children() {
961 let mut properties = CT_TextBodyProperties::from_xml(
962 br#"<q:bodyPr><x:first/><q:scene3d><x:scene/></q:scene3d><q:flatTx/><q:extLst><x:ext/></q:extLst></q:bodyPr>"#,
963 )
964 .unwrap();
965 properties.autofit = Some(TextAutofit::ShapeAutofit);
966 let written = properties.to_xml().unwrap();
967 assert_eq!(
968 written,
969 br#"<a:bodyPr><x:first/><a:spAutoFit/><q:scene3d><x:scene/></q:scene3d><q:flatTx/><q:extLst><x:ext/></q:extLst></a:bodyPr>"#
970 );
971 assert_eq!(
972 CT_TextBodyProperties::from_xml(&written).unwrap(),
973 properties
974 );
975 }
976
977 #[test]
978 fn autofit_owned_updates_preserve_opaque_lexical_attributes_and_comments() {
979 let source = br#"<q:bodyPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main"><q:normAutofit xmlns:e='urn:example' e:proof='keep&!' e:fontScale='foreign' fontScale='62500' lnSpcReduction = "20000"><!--keep opaque--></q:normAutofit></q:bodyPr>"#;
980 let mut body = CT_TextBodyProperties::from_xml(source).unwrap();
981 let opaque = "xmlns:e='urn:example' e:proof='keep&!' e:fontScale='foreign'";
982 let unchanged = String::from_utf8(body.to_xml().unwrap()).unwrap();
983 assert!(unchanged.contains(opaque));
984 assert!(unchanged.contains("<!--keep opaque-->"));
985 body.autofit = Some(TextAutofit::Normal(super::NormalAutofit {
986 font_scale: Some("85000".to_owned()),
987 line_spacing_reduction: Some("10000".to_owned()),
988 }));
989 let updated = body.to_xml().unwrap();
990 let xml = String::from_utf8(updated.clone()).unwrap();
991 assert!(xml.contains(opaque));
992 assert!(xml.contains("fontScale='85000' lnSpcReduction = \"10000\""));
993 assert!(xml.contains("<!--keep opaque-->"));
994 assert_eq!(
995 CT_TextBodyProperties::from_xml(&updated).unwrap().autofit,
996 body.autofit
997 );
998 body.autofit = Some(TextAutofit::NoAutofit);
999 let removed = body.to_xml().unwrap();
1000 let xml = String::from_utf8(removed.clone()).unwrap();
1001 assert!(xml.contains("<q:noAutofit"));
1002 assert!(xml.contains(opaque));
1003 assert!(!xml.contains(" fontScale="));
1004 assert!(!xml.contains("lnSpcReduction"));
1005 assert!(xml.contains("<!--keep opaque--></q:noAutofit>"));
1006 let reopened = CT_TextBodyProperties::from_xml(&removed).unwrap();
1007 assert_eq!(reopened.autofit, Some(TextAutofit::NoAutofit));
1008 assert_eq!(removed, reopened.to_xml().unwrap());
1009 }
1010
1011 #[test]
1012 fn malformed_body_properties_return_errors_without_panicking() {
1013 let body_cases: &[&[u8]] = &[
1014 br#"<q:notBodyPr/>"#,
1015 br#"<q:bodyPr anchor="middle"/>"#,
1016 br#"<q:bodyPr wrap="tight"/>"#,
1017 br#"<q:bodyPr vert="sideways"/>"#,
1018 br#"<q:bodyPr lIns="2147483648"/>"#,
1019 br#"<q:bodyPr rIns="1.in"/>"#,
1020 br#"<q:bodyPr spcFirstLastPara="yes"/>"#,
1021 br#"<q:bodyPr><q:normAutofit fontScale="999"/></q:bodyPr>"#,
1022 br#"<q:bodyPr><q:normAutofit fontScale="100001"/></q:bodyPr>"#,
1023 br#"<q:bodyPr><q:normAutofit fontScale=".5%"/></q:bodyPr>"#,
1024 br#"<q:bodyPr><q:normAutofit lnSpcReduction="13200001"/></q:bodyPr>"#,
1025 br#"<q:bodyPr><q:noAutofit><x:child/></q:noAutofit></q:bodyPr>"#,
1026 br#"<q:bodyPr><q:noAutofit/><q:spAutoFit/></q:bodyPr>"#,
1027 br#"<q:bodyPr><q:sp3d/><q:flatTx/></q:bodyPr>"#,
1028 br#"<q:bodyPr xmlns:a="urn:producer" numCol="2"/>"#,
1029 ];
1030 for xml in body_cases {
1031 let result = panic::catch_unwind(|| CT_TextBodyProperties::from_xml(xml));
1032 assert!(result.is_ok(), "body-property parser panicked");
1033 assert!(result.unwrap().is_err(), "malformed body properties parsed");
1034 }
1035
1036 let shell_cases: &[&[u8]] = &[
1037 br#"<q:txBody><q:p/></q:txBody>"#,
1038 br#"<q:txBody/>"#,
1039 br#"<q:txBody><q:bodyPr/><q:bodyPr/><q:p/></q:txBody>"#,
1040 br#"<q:txBody><q:bodyPr/><q:lstStyle/><q:lstStyle/><q:p/></q:txBody>"#,
1041 ];
1042 for xml in shell_cases {
1043 let result = panic::catch_unwind(|| CT_TextBody::from_xml(xml));
1044 assert!(result.is_ok(), "text-body parser panicked");
1045 assert!(result.unwrap().is_err(), "malformed text body parsed");
1046 }
1047
1048 let empty = CT_TextBody::from_xml(br#"<q:txBody><q:bodyPr/></q:txBody>"#).unwrap();
1049 assert_eq!(empty.paragraph_count(), 0);
1050 assert!(empty.to_xml().is_ok());
1051
1052 let mut properties = CT_TextBodyProperties {
1053 left_inset: Some(Coordinate32Value::UniversalMeasure("NaNin".to_owned())),
1054 ..CT_TextBodyProperties::default()
1055 };
1056 assert!(properties.to_xml().is_err());
1057 properties.left_inset = Some(Coordinate32Value::Emu(i32::MAX));
1058 assert!(properties.to_xml().is_ok());
1059 }
1060}