use crate::table::TableBlock;
use oxml_layout::{
Align, Color, GroupElement, InlineItem, LayoutLine, LineBreakParams, MediaId, SourceNodeId,
StructureId,
};
use rdocx_oxml::borders::CT_PBdr;
use rdocx_oxml::drawing::{
AnchorAlignH, AnchorAlignV, ST_RelativeFromH, ST_RelativeFromV, WrapType,
};
use std::ops::Deref;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct AnchoredDrawing {
pub behind_doc: bool,
pub rel_h: ST_RelativeFromH,
pub off_h: f64,
pub rel_v: ST_RelativeFromV,
pub off_v: f64,
pub width: f64,
pub height: f64,
pub wrap: WrapType,
pub dist_top: f64,
pub dist_bottom: f64,
pub dist_left: f64,
pub dist_right: f64,
pub align_h: Option<AnchorAlignH>,
pub align_v: Option<AnchorAlignV>,
pub content: AnchoredContent,
pub alternate_text: Option<String>,
pub structure_id: Option<StructureId>,
}
#[derive(Debug, Clone)]
pub enum AnchoredContent {
Image { media_id: MediaId },
Group(GroupElement),
Shape {
preset: ShapePreset,
fill: Option<Color>,
text: Vec<ParagraphBlock>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShapePreset {
Rect,
Line,
Unsupported,
}
impl ShapePreset {
pub fn from_prst(prst: Option<&str>) -> Self {
match prst {
Some("rect") => ShapePreset::Rect,
Some("line") | Some("straightConnector1") => ShapePreset::Line,
_ => ShapePreset::Unsupported,
}
}
}
#[derive(Debug, Clone)]
pub enum LayoutBlock {
Paragraph(ParagraphBlock),
Table(TableBlock),
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ParagraphSemantics {
pub source_node: Option<SourceNodeId>,
pub structure_id: Option<StructureId>,
}
#[derive(Debug, Clone)]
pub(crate) enum CellBlockSemantics {
Paragraph(ParagraphSemantics),
Table(TableSemantics),
}
#[derive(Debug, Clone)]
pub(crate) struct CellSemantics {
pub blocks: Vec<CellBlockSemantics>,
}
#[derive(Debug, Clone)]
pub(crate) struct RowSemantics {
pub cells: Vec<CellSemantics>,
}
#[derive(Debug, Clone)]
pub(crate) struct TableSemantics {
pub rows: Vec<RowSemantics>,
}
#[derive(Debug, Clone)]
pub(crate) enum SharedLayoutBlock {
Owned(Box<LayoutBlock>),
Paragraph {
block: Arc<ParagraphBlock>,
semantics: ParagraphSemantics,
},
Table {
block: Arc<TableBlock>,
semantics: TableSemantics,
},
}
#[derive(Clone, Copy)]
pub(crate) struct ParagraphView<'a> {
pub block: &'a ParagraphBlock,
pub semantics: Option<&'a ParagraphSemantics>,
}
impl Deref for ParagraphView<'_> {
type Target = ParagraphBlock;
fn deref(&self) -> &Self::Target {
self.block
}
}
impl ParagraphView<'_> {
pub fn source_node(self) -> Option<Option<SourceNodeId>> {
self.semantics.map(|semantics| semantics.source_node)
}
pub fn structure_id(self) -> Option<StructureId> {
self.semantics
.map_or(self.block.structure_id, |semantics| semantics.structure_id)
}
}
#[derive(Clone, Copy)]
pub(crate) struct TableView<'a> {
pub block: &'a TableBlock,
pub semantics: Option<&'a TableSemantics>,
}
impl Deref for TableView<'_> {
type Target = TableBlock;
fn deref(&self) -> &Self::Target {
self.block
}
}
pub(crate) trait LayoutBlockLike {
fn paragraph(&self) -> Option<ParagraphView<'_>>;
fn table(&self) -> Option<TableView<'_>>;
fn content_height(&self) -> f64 {
self.paragraph().map_or_else(
|| self.table().unwrap().content_height(),
|p| p.content_height(),
)
}
fn space_before(&self) -> f64 {
self.paragraph()
.map_or(0.0, |paragraph| paragraph.space_before)
}
fn space_after(&self) -> f64 {
self.paragraph()
.map_or(0.0, |paragraph| paragraph.space_after)
}
fn page_break_before(&self) -> bool {
self.paragraph()
.is_some_and(|paragraph| paragraph.page_break_before)
}
}
impl LayoutBlockLike for LayoutBlock {
fn paragraph(&self) -> Option<ParagraphView<'_>> {
match self {
Self::Paragraph(block) => Some(ParagraphView {
block,
semantics: None,
}),
Self::Table(_) => None,
}
}
fn table(&self) -> Option<TableView<'_>> {
match self {
Self::Paragraph(_) => None,
Self::Table(block) => Some(TableView {
block,
semantics: None,
}),
}
}
}
impl LayoutBlockLike for SharedLayoutBlock {
fn paragraph(&self) -> Option<ParagraphView<'_>> {
match self {
Self::Owned(block) => block.paragraph(),
Self::Paragraph { block, semantics } => Some(ParagraphView {
block,
semantics: Some(semantics),
}),
Self::Table { .. } => None,
}
}
fn table(&self) -> Option<TableView<'_>> {
match self {
Self::Owned(block) => block.table(),
Self::Paragraph { .. } => None,
Self::Table { block, semantics } => Some(TableView {
block,
semantics: Some(semantics),
}),
}
}
}
impl LayoutBlock {
pub fn total_height(&self) -> f64 {
match self {
LayoutBlock::Paragraph(p) => p.total_height(),
LayoutBlock::Table(t) => t.total_height(),
}
}
pub fn content_height(&self) -> f64 {
match self {
LayoutBlock::Paragraph(p) => p.content_height(),
LayoutBlock::Table(t) => t.content_height(),
}
}
pub fn space_before(&self) -> f64 {
match self {
LayoutBlock::Paragraph(p) => p.space_before,
LayoutBlock::Table(_) => 0.0,
}
}
pub fn space_after(&self) -> f64 {
match self {
LayoutBlock::Paragraph(p) => p.space_after,
LayoutBlock::Table(_) => 0.0,
}
}
pub fn keep_next(&self) -> bool {
match self {
LayoutBlock::Paragraph(p) => p.keep_next,
LayoutBlock::Table(_) => false,
}
}
pub fn keep_lines(&self) -> bool {
match self {
LayoutBlock::Paragraph(p) => p.keep_lines,
LayoutBlock::Table(_) => false,
}
}
pub fn page_break_before(&self) -> bool {
match self {
LayoutBlock::Paragraph(p) => p.page_break_before,
LayoutBlock::Table(_) => false,
}
}
pub fn widow_control(&self) -> bool {
match self {
LayoutBlock::Paragraph(p) => p.widow_control,
LayoutBlock::Table(_) => false,
}
}
}
#[derive(Debug, Clone)]
pub struct ParagraphReflow {
pub items: Vec<InlineItem>,
pub params: LineBreakParams,
}
#[derive(Debug, Clone)]
pub struct ParagraphBlock {
pub lines: Vec<LayoutLine>,
pub has_visible_revision: bool,
pub anchored: Vec<AnchoredDrawing>,
pub space_before: f64,
pub space_after: f64,
pub borders: Option<CT_PBdr>,
pub shading: Option<Color>,
pub indent_left: f64,
pub indent_right: f64,
pub jc: Option<Align>,
pub keep_next: bool,
pub keep_lines: bool,
pub page_break_before: bool,
pub widow_control: bool,
pub heading_level: Option<u32>,
pub heading_text: Option<String>,
pub list: Option<(u32, u8)>,
pub structure_id: Option<StructureId>,
pub reflow: Option<Box<ParagraphReflow>>,
pub content_offset_top: f64,
}
impl ParagraphBlock {
pub fn content_height(&self) -> f64 {
self.content_offset_top + self.lines.iter().map(|l| l.height).sum::<f64>()
}
pub fn total_height(&self) -> f64 {
self.space_before + self.content_height() + self.space_after
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
}
pub fn build_paragraph_block(
lines: Vec<LayoutLine>,
space_before: f64,
space_after: f64,
borders: Option<CT_PBdr>,
shading: Option<Color>,
indent_left: f64,
indent_right: f64,
jc: Option<Align>,
keep_next: bool,
keep_lines: bool,
page_break_before: bool,
widow_control: bool,
) -> ParagraphBlock {
ParagraphBlock {
lines,
has_visible_revision: false,
anchored: Vec::new(),
space_before,
space_after,
borders,
shading,
indent_left,
indent_right,
jc,
keep_next,
keep_lines,
page_break_before,
widow_control,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paragraph_block_height() {
let block = ParagraphBlock {
anchored: Vec::new(),
has_visible_revision: false,
lines: vec![
LayoutLine {
items: vec![],
width: 0.0,
ascent: 10.0,
descent: 3.0,
line_gap: 0.0,
height: 13.0,
indent_left: 0.0,
available_width: 468.0,
is_last: false,
},
LayoutLine {
items: vec![],
width: 0.0,
ascent: 10.0,
descent: 3.0,
line_gap: 0.0,
height: 13.0,
indent_left: 0.0,
available_width: 468.0,
is_last: true,
},
],
space_before: 6.0,
space_after: 8.0,
borders: None,
shading: None,
indent_left: 0.0,
indent_right: 0.0,
jc: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
widow_control: true,
heading_level: None,
heading_text: None,
list: None,
structure_id: None,
reflow: None,
content_offset_top: 0.0,
};
assert!((block.content_height() - 26.0).abs() < 0.01);
assert!((block.total_height() - 40.0).abs() < 0.01);
}
#[test]
fn shape_presets_map_to_what_we_can_draw() {
assert_eq!(ShapePreset::from_prst(Some("rect")), ShapePreset::Rect);
assert_eq!(ShapePreset::from_prst(Some("line")), ShapePreset::Line);
assert_eq!(
ShapePreset::from_prst(Some("straightConnector1")),
ShapePreset::Line
);
assert_eq!(
ShapePreset::from_prst(Some("roundRect")),
ShapePreset::Unsupported,
"an unhandled preset must not silently draw as a plain rectangle"
);
assert_eq!(ShapePreset::from_prst(None), ShapePreset::Unsupported);
}
}