1use crate::table::TableBlock;
4use oxml_layout::{
5 Align, Color, GroupElement, InlineItem, LayoutLine, LineBreakParams, MediaId, SourceNodeId,
6 StructureId, TextDirection,
7};
8use rdocx_oxml::borders::CT_PBdr;
9use rdocx_oxml::drawing::{
10 AnchorAlignH, AnchorAlignV, ST_RelativeFromH, ST_RelativeFromV, WrapType,
11};
12use std::ops::Deref;
13use std::sync::Arc;
14
15#[derive(Debug, Clone)]
22pub struct AnchoredDrawing {
23 pub behind_doc: bool,
25 pub rel_h: ST_RelativeFromH,
27 pub off_h: f64,
29 pub rel_v: ST_RelativeFromV,
31 pub off_v: f64,
33 pub width: f64,
35 pub height: f64,
37 pub wrap: WrapType,
39 pub dist_top: f64,
42 pub dist_bottom: f64,
43 pub dist_left: f64,
44 pub dist_right: f64,
45 pub align_h: Option<AnchorAlignH>,
47 pub align_v: Option<AnchorAlignV>,
49 pub content: AnchoredContent,
51 pub alternate_text: Option<String>,
53 pub structure_id: Option<StructureId>,
55}
56
57#[derive(Debug, Clone)]
59pub enum AnchoredContent {
60 Image { media_id: MediaId },
62 Group(GroupElement),
64 Shape {
69 preset: ShapePreset,
71 fill: Option<Color>,
74 text: Vec<ParagraphBlock>,
76 },
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum ShapePreset {
82 Rect,
84 Line,
86 Unsupported,
88}
89
90impl ShapePreset {
91 pub fn from_prst(prst: Option<&str>) -> Self {
93 match prst {
94 Some("rect") => ShapePreset::Rect,
95 Some("line") | Some("straightConnector1") => ShapePreset::Line,
96 _ => ShapePreset::Unsupported,
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
103pub enum LayoutBlock {
104 Paragraph(ParagraphBlock),
105 Table(TableBlock),
106}
107
108#[derive(Debug, Clone, Copy)]
109pub(crate) struct ParagraphSemantics {
110 pub source_node: Option<SourceNodeId>,
111 pub structure_id: Option<StructureId>,
112 pub reflow_direction: TextDirection,
113}
114
115#[derive(Debug, Clone)]
116pub(crate) enum CellBlockSemantics {
117 Paragraph(ParagraphSemantics),
118 Table(TableSemantics),
119}
120
121#[derive(Debug, Clone)]
122pub(crate) struct CellSemantics {
123 pub blocks: Vec<CellBlockSemantics>,
124}
125
126#[derive(Debug, Clone)]
127pub(crate) struct RowSemantics {
128 pub cells: Vec<CellSemantics>,
129}
130
131#[derive(Debug, Clone)]
132pub(crate) struct TableSemantics {
133 pub rows: Vec<RowSemantics>,
134}
135
136#[derive(Debug, Clone)]
137pub(crate) enum SharedLayoutBlock {
138 Owned {
139 block: Box<LayoutBlock>,
140 reflow_direction: TextDirection,
141 },
142 Paragraph {
143 block: Arc<ParagraphBlock>,
144 semantics: ParagraphSemantics,
145 },
146 Table {
147 block: Arc<TableBlock>,
148 semantics: TableSemantics,
149 },
150}
151
152#[derive(Clone, Copy)]
153pub(crate) struct ParagraphView<'a> {
154 pub block: &'a ParagraphBlock,
155 pub semantics: Option<&'a ParagraphSemantics>,
156 pub reflow_direction: TextDirection,
157 pub reflow_allowed: bool,
158}
159
160impl Deref for ParagraphView<'_> {
161 type Target = ParagraphBlock;
162
163 fn deref(&self) -> &Self::Target {
164 self.block
165 }
166}
167
168impl ParagraphView<'_> {
169 pub fn source_node(self) -> Option<Option<SourceNodeId>> {
170 self.semantics.map(|semantics| semantics.source_node)
171 }
172
173 pub fn structure_id(self) -> Option<StructureId> {
174 self.semantics
175 .map_or(self.block.structure_id, |semantics| semantics.structure_id)
176 }
177}
178
179#[derive(Clone, Copy)]
180pub(crate) struct TableView<'a> {
181 pub block: &'a TableBlock,
182 pub semantics: Option<&'a TableSemantics>,
183}
184
185impl Deref for TableView<'_> {
186 type Target = TableBlock;
187
188 fn deref(&self) -> &Self::Target {
189 self.block
190 }
191}
192
193pub(crate) trait LayoutBlockLike {
194 fn paragraph(&self) -> Option<ParagraphView<'_>>;
195 fn table(&self) -> Option<TableView<'_>>;
196
197 fn content_height(&self) -> f64 {
198 self.paragraph().map_or_else(
199 || self.table().unwrap().content_height(),
200 |p| p.content_height(),
201 )
202 }
203
204 fn space_before(&self) -> f64 {
205 self.paragraph()
206 .map_or(0.0, |paragraph| paragraph.space_before)
207 }
208
209 fn space_after(&self) -> f64 {
210 self.paragraph()
211 .map_or(0.0, |paragraph| paragraph.space_after)
212 }
213
214 fn page_break_before(&self) -> bool {
215 self.paragraph()
216 .is_some_and(|paragraph| paragraph.page_break_before)
217 }
218}
219
220impl LayoutBlockLike for LayoutBlock {
221 fn paragraph(&self) -> Option<ParagraphView<'_>> {
222 match self {
223 Self::Paragraph(block) => Some(ParagraphView {
224 block,
225 semantics: None,
226 reflow_direction: TextDirection::Auto,
227 reflow_allowed: true,
228 }),
229 Self::Table(_) => None,
230 }
231 }
232
233 fn table(&self) -> Option<TableView<'_>> {
234 match self {
235 Self::Paragraph(_) => None,
236 Self::Table(block) => Some(TableView {
237 block,
238 semantics: None,
239 }),
240 }
241 }
242}
243
244impl LayoutBlockLike for SharedLayoutBlock {
245 fn paragraph(&self) -> Option<ParagraphView<'_>> {
246 match self {
247 Self::Owned {
248 block,
249 reflow_direction,
250 } => block.paragraph().map(|mut paragraph| {
251 paragraph.reflow_direction = *reflow_direction;
252 paragraph
253 }),
254 Self::Paragraph { block, semantics } => Some(ParagraphView {
255 block,
256 semantics: Some(semantics),
257 reflow_direction: semantics.reflow_direction,
258 reflow_allowed: true,
259 }),
260 Self::Table { .. } => None,
261 }
262 }
263
264 fn table(&self) -> Option<TableView<'_>> {
265 match self {
266 Self::Owned { block, .. } => block.table(),
267 Self::Paragraph { .. } => None,
268 Self::Table { block, semantics } => Some(TableView {
269 block,
270 semantics: Some(semantics),
271 }),
272 }
273 }
274}
275
276impl LayoutBlock {
277 pub fn total_height(&self) -> f64 {
279 match self {
280 LayoutBlock::Paragraph(p) => p.total_height(),
281 LayoutBlock::Table(t) => t.total_height(),
282 }
283 }
284
285 pub fn content_height(&self) -> f64 {
287 match self {
288 LayoutBlock::Paragraph(p) => p.content_height(),
289 LayoutBlock::Table(t) => t.content_height(),
290 }
291 }
292
293 pub fn space_before(&self) -> f64 {
294 match self {
295 LayoutBlock::Paragraph(p) => p.space_before,
296 LayoutBlock::Table(_) => 0.0,
297 }
298 }
299
300 pub fn space_after(&self) -> f64 {
301 match self {
302 LayoutBlock::Paragraph(p) => p.space_after,
303 LayoutBlock::Table(_) => 0.0,
304 }
305 }
306
307 pub fn keep_next(&self) -> bool {
308 match self {
309 LayoutBlock::Paragraph(p) => p.keep_next,
310 LayoutBlock::Table(_) => false,
311 }
312 }
313
314 pub fn keep_lines(&self) -> bool {
315 match self {
316 LayoutBlock::Paragraph(p) => p.keep_lines,
317 LayoutBlock::Table(_) => false,
318 }
319 }
320
321 pub fn page_break_before(&self) -> bool {
322 match self {
323 LayoutBlock::Paragraph(p) => p.page_break_before,
324 LayoutBlock::Table(_) => false,
325 }
326 }
327
328 pub fn widow_control(&self) -> bool {
329 match self {
330 LayoutBlock::Paragraph(p) => p.widow_control,
331 LayoutBlock::Table(_) => false,
332 }
333 }
334}
335
336#[derive(Debug, Clone)]
346pub struct ParagraphReflow {
347 pub items: Vec<InlineItem>,
348 pub params: LineBreakParams,
349}
350
351#[derive(Debug, Clone)]
353pub struct ParagraphBlock {
354 pub lines: Vec<LayoutLine>,
356 pub has_visible_revision: bool,
358 pub anchored: Vec<AnchoredDrawing>,
364 pub space_before: f64,
366 pub space_after: f64,
368 pub borders: Option<CT_PBdr>,
370 pub shading: Option<Color>,
372 pub indent_left: f64,
374 pub indent_right: f64,
376 pub jc: Option<Align>,
378 pub keep_next: bool,
380 pub keep_lines: bool,
382 pub page_break_before: bool,
384 pub widow_control: bool,
386 pub heading_level: Option<u32>,
388 pub heading_text: Option<String>,
390 pub list: Option<(u32, u8)>,
392 pub structure_id: Option<StructureId>,
394 pub reflow: Option<Box<ParagraphReflow>>,
398 pub content_offset_top: f64,
401}
402
403impl ParagraphBlock {
404 pub fn content_height(&self) -> f64 {
406 self.content_offset_top + self.lines.iter().map(|l| l.height).sum::<f64>()
407 }
408
409 pub fn total_height(&self) -> f64 {
411 self.space_before + self.content_height() + self.space_after
412 }
413
414 pub fn line_count(&self) -> usize {
416 self.lines.len()
417 }
418}
419
420pub fn build_paragraph_block(
422 lines: Vec<LayoutLine>,
423 space_before: f64,
424 space_after: f64,
425 borders: Option<CT_PBdr>,
426 shading: Option<Color>,
427 indent_left: f64,
428 indent_right: f64,
429 jc: Option<Align>,
430 keep_next: bool,
431 keep_lines: bool,
432 page_break_before: bool,
433 widow_control: bool,
434) -> ParagraphBlock {
435 ParagraphBlock {
436 lines,
437 has_visible_revision: false,
438 anchored: Vec::new(),
439 space_before,
440 space_after,
441 borders,
442 shading,
443 indent_left,
444 indent_right,
445 jc,
446 keep_next,
447 keep_lines,
448 page_break_before,
449 widow_control,
450 heading_level: None,
451 heading_text: None,
452 list: None,
453 structure_id: None,
454 reflow: None,
455 content_offset_top: 0.0,
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[test]
464 fn paragraph_block_height() {
465 let block = ParagraphBlock {
466 anchored: Vec::new(),
467 has_visible_revision: false,
468 lines: vec![
469 LayoutLine {
470 items: vec![],
471 width: 0.0,
472 ascent: 10.0,
473 descent: 3.0,
474 line_gap: 0.0,
475 height: 13.0,
476 indent_left: 0.0,
477 available_width: 468.0,
478 is_last: false,
479 },
480 LayoutLine {
481 items: vec![],
482 width: 0.0,
483 ascent: 10.0,
484 descent: 3.0,
485 line_gap: 0.0,
486 height: 13.0,
487 indent_left: 0.0,
488 available_width: 468.0,
489 is_last: true,
490 },
491 ],
492 space_before: 6.0,
493 space_after: 8.0,
494 borders: None,
495 shading: None,
496 indent_left: 0.0,
497 indent_right: 0.0,
498 jc: None,
499 keep_next: false,
500 keep_lines: false,
501 page_break_before: false,
502 widow_control: true,
503 heading_level: None,
504 heading_text: None,
505 list: None,
506 structure_id: None,
507 reflow: None,
508 content_offset_top: 0.0,
509 };
510 assert!((block.content_height() - 26.0).abs() < 0.01);
511 assert!((block.total_height() - 40.0).abs() < 0.01);
512 }
513
514 #[test]
517 fn shape_presets_map_to_what_we_can_draw() {
518 assert_eq!(ShapePreset::from_prst(Some("rect")), ShapePreset::Rect);
519 assert_eq!(ShapePreset::from_prst(Some("line")), ShapePreset::Line);
520 assert_eq!(
521 ShapePreset::from_prst(Some("straightConnector1")),
522 ShapePreset::Line
523 );
524 assert_eq!(
525 ShapePreset::from_prst(Some("roundRect")),
526 ShapePreset::Unsupported,
527 "an unhandled preset must not silently draw as a plain rectangle"
528 );
529 assert_eq!(ShapePreset::from_prst(None), ShapePreset::Unsupported);
530 }
531}