Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions crates/rdocx-layout/src/block.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,39 @@
//! Block-level layout: paragraphs and tables as positioned blocks.

use rdocx_oxml::borders::CT_PBdr;
use rdocx_oxml::drawing::{ST_RelativeFromH, ST_RelativeFromV};
use rdocx_oxml::shared::ST_Jc;

use crate::line::LayoutLine;
use crate::output::Color;
use crate::table::TableBlock;

/// A floating drawing anchored to a paragraph.
///
/// Offsets are kept in points alongside the frame they are measured from. A
/// `wp:anchor` offset is meaningless on its own: the same number means a
/// different place depending on whether it is relative to the page, the
/// margin, the text column or the paragraph.
#[derive(Debug, Clone)]
pub struct AnchoredDrawing {
/// Render underneath the text rather than on top of it.
pub behind_doc: bool,
/// Frame the horizontal offset is measured from.
pub rel_h: ST_RelativeFromH,
/// Horizontal offset in points.
pub off_h: f64,
/// Frame the vertical offset is measured from.
pub rel_v: ST_RelativeFromV,
/// Vertical offset in points.
pub off_v: f64,
/// Width in points.
pub width: f64,
/// Height in points.
pub height: f64,
/// Relationship ID of the image part.
pub embed_id: String,
}

/// A laid-out block element (paragraph or table).
#[derive(Debug, Clone)]
pub enum LayoutBlock {
Expand Down Expand Up @@ -79,6 +106,12 @@ impl LayoutBlock {
pub struct ParagraphBlock {
/// Laid-out lines.
pub lines: Vec<LayoutLine>,
/// Floating drawings anchored to this paragraph.
///
/// These travel with the paragraph so the paginator can resolve a
/// paragraph-relative or line-relative offset once it knows where the
/// paragraph actually landed.
pub anchored: Vec<AnchoredDrawing>,
/// Space before the paragraph in points.
pub space_before: f64,
/// Space after the paragraph in points.
Expand Down Expand Up @@ -141,6 +174,7 @@ pub fn build_paragraph_block(
) -> ParagraphBlock {
ParagraphBlock {
lines,
anchored: Vec::new(),
space_before,
space_after,
borders,
Expand All @@ -164,6 +198,7 @@ mod tests {
#[test]
fn paragraph_block_height() {
let block = ParagraphBlock {
anchored: Vec::new(),
lines: vec![
LayoutLine {
items: vec![],
Expand Down
105 changes: 32 additions & 73 deletions crates/rdocx-layout/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,6 @@ impl Engine {
// Post-pagination pass: apply page background color
apply_page_background(&mut pages, input);

// Post-pagination pass: resolve anchor (background) images
resolve_anchor_images(&mut pages, input);

// Post-pagination pass: resolve inline image data
resolve_inline_images(&mut pages, input);

Expand Down Expand Up @@ -252,74 +249,6 @@ fn extract_background_color(xml: &str) -> Option<Color> {
None
}

/// Resolve anchor (floating) images from the document and inject them into page frames.
///
/// For `behind_doc=true` images: inserts at the START of page elements (renders underneath).
/// For `behind_doc=false` images: inserts at the END (renders on top).
fn resolve_anchor_images(pages: &mut [PageFrame], input: &LayoutInput) {
use crate::output::Rect;
use rdocx_oxml::text::RunContent;

// Collect all anchor drawings from body content
let mut anchor_images: Vec<(bool, f64, f64, f64, f64, String)> = Vec::new();

for content in &input.document.body.content {
if let BodyContent::Paragraph(p) = content {
for run in &p.runs {
for rc in &run.content {
if let RunContent::Drawing(drawing) = rc
&& let Some(ref anchor) = drawing.anchor
{
let behind = anchor.behind_doc;
// Convert EMU positions and extents to points
let x = anchor.pos_h_offset.to_pt();
let y = anchor.pos_v_offset.to_pt();
let w = anchor.extent_cx.to_pt();
let h = anchor.extent_cy.to_pt();
anchor_images.push((behind, x, y, w, h, anchor.embed_id.clone()));
}
}
}
}
}

if anchor_images.is_empty() {
return;
}

// For each anchor image, resolve image data and add to pages
for (behind, x, y, w, h, embed_id) in &anchor_images {
let (data, content_type) = if let Some(img) = input.images.get(embed_id) {
(img.data.clone(), img.content_type.clone())
} else {
continue;
};

let element = PositionedElement::Image {
rect: Rect {
x: *x,
y: *y,
width: *w,
height: *h,
},
data,
content_type,
embed_id: None, // Already resolved
};

if *behind {
// Behind-doc images go on the first page only
// (proper page association would require paragraph-to-page mapping)
if let Some(page) = pages.first_mut() {
page.elements.insert(0, element);
}
} else if let Some(page) = pages.first_mut() {
// Foreground anchor images go on the first page only
page.elements.push(element);
}
}
}

/// Resolve inline image data from input.images by embed_id.
///
/// During pagination, inline images are created with empty data and an embed_id.
Expand Down Expand Up @@ -870,7 +799,7 @@ pub fn layout_paragraph(

let lines = line::break_into_lines(&inline_items, &line_params, fm)?;

Ok(block::build_paragraph_block(
let mut result = block::build_paragraph_block(
lines,
space_before,
space_after,
Expand All @@ -883,7 +812,37 @@ pub fn layout_paragraph(
keep_lines,
page_break_before,
widow_control,
))
);
result.anchored = collect_anchored_drawings(para);
Ok(result)
}

/// Collect the floating drawings anchored to a paragraph.
///
/// The offsets stay paired with the frame they are measured from. Resolving
/// them here is not possible: a paragraph-relative offset needs the laid-out
/// position of the paragraph, which only the paginator knows.
fn collect_anchored_drawings(para: &CT_P) -> Vec<block::AnchoredDrawing> {
let mut out = Vec::new();
for run in &para.runs {
for rc in &run.content {
if let RunContent::Drawing(drawing) = rc
&& let Some(ref anchor) = drawing.anchor
{
out.push(block::AnchoredDrawing {
behind_doc: anchor.behind_doc,
rel_h: anchor.pos_h_relative_from,
off_h: anchor.pos_h_offset.to_pt(),
rel_v: anchor.pos_v_relative_from,
off_v: anchor.pos_v_offset.to_pt(),
width: anchor.extent_cx.to_pt(),
height: anchor.extent_cy.to_pt(),
embed_id: anchor.embed_id.clone(),
});
}
}
}
out
}

/// Merge direct paragraph properties (only fields explicitly set in the XML).
Expand Down
Loading