use crate::model::{self, Paragraph};
use crate::render::dimension::Pt;
use crate::render::geometry::PtSize;
use crate::render::layout::section::{FloatingImage, FloatingImageY, FloatingShape};
use crate::render::resolve::shape_geometry::build_geometry;
use crate::render::resolve::shape_visuals::resolve_shape_visuals;
use super::{BuildContext, BuildState};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum AnchorFrame {
Page,
Stack,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ShapeAnchorClass {
All,
ParagraphAnchored,
PageAnchored,
}
pub(super) fn extract_floating_images(
para: &Paragraph,
ctx: &BuildContext,
state: &BuildState,
frame: AnchorFrame,
) -> Vec<FloatingImage> {
use crate::model::{GraphicContent, ImagePlacement, Inline};
fn find_anchor_images<'a>(inlines: &'a [Inline], out: &mut Vec<&'a crate::model::Image>) {
for inline in inlines {
match inline {
Inline::Image(img)
if matches!(img.placement, ImagePlacement::Anchor(_))
&& !matches!(img.graphic, Some(GraphicContent::WordProcessingShape(_))) =>
{
out.push(img);
}
Inline::Hyperlink(link) => find_anchor_images(&link.content, out),
Inline::Field(f) => find_anchor_images(&f.content, out),
Inline::AlternateContent(ac) => {
if let Some(ref fb) = ac.fallback {
find_anchor_images(fb, out);
}
}
_ => {}
}
}
}
let mut anchor_imgs = Vec::new();
find_anchor_images(¶.content, &mut anchor_imgs);
let mut images = Vec::new();
for img in anchor_imgs {
let ImagePlacement::Anchor(ref anchor) = img.placement else {
continue;
};
let Some(rel_id) = crate::render::resolve::images::extract_image_rel_id(img) else {
continue;
};
let Some(image_data) = ctx.resolved.media.get(rel_id).cloned() else {
log::warn!(
"anchor image: rel_id={} missing from media table ({} entries)",
rel_id.as_str(),
ctx.resolved.media.len(),
);
continue;
};
let w = Pt::from(img.extent.width);
let h = Pt::from(img.extent.height);
let (x, y) = resolve_anchor_position(anchor, w, h, state, frame);
images.push(FloatingImage {
image_data,
size: PtSize::new(w, h),
x,
y,
wrap_mode: crate::render::layout::section::WrapMode::from_model(&anchor.wrap),
dist_left: Pt::from(anchor.distance.left),
dist_right: Pt::from(anchor.distance.right),
behind_doc: anchor.behind_text,
});
}
extract_vml_floating_images(¶.content, state, frame, ctx, &mut images);
images
}
pub(super) fn extract_floating_shapes(
para: &Paragraph,
ctx: &BuildContext,
state: &mut BuildState,
frame: AnchorFrame,
restrict: ShapeAnchorClass,
) -> Vec<FloatingShape> {
use crate::model::{GraphicContent, ImagePlacement, Inline};
fn find_anchor_shapes<'a>(inlines: &'a [Inline], out: &mut Vec<&'a crate::model::Image>) {
for inline in inlines {
match inline {
Inline::Image(img)
if matches!(img.placement, ImagePlacement::Anchor(_))
&& matches!(img.graphic, Some(GraphicContent::WordProcessingShape(_))) =>
{
out.push(img);
}
Inline::Hyperlink(link) => find_anchor_shapes(&link.content, out),
Inline::Field(f) => find_anchor_shapes(&f.content, out),
Inline::AlternateContent(ac) => {
let before = out.len();
for choice in &ac.choices {
find_anchor_shapes(&choice.content, out);
}
if out.len() == before {
if let Some(ref fb) = ac.fallback {
find_anchor_shapes(fb, out);
}
}
}
_ => {}
}
}
}
let mut shape_imgs = Vec::new();
find_anchor_shapes(¶.content, &mut shape_imgs);
let mut shapes = Vec::new();
for img in shape_imgs {
let ImagePlacement::Anchor(ref anchor) = img.placement else {
continue;
};
let class_match = match restrict {
ShapeAnchorClass::All => true,
ShapeAnchorClass::ParagraphAnchored => anchors_to_paragraph(anchor),
ShapeAnchorClass::PageAnchored => !anchors_to_paragraph(anchor),
};
if !class_match {
continue;
}
let wsp = match img.graphic.as_ref() {
Some(GraphicContent::WordProcessingShape(w)) => w,
_ => continue,
};
let shape_props = wsp.shape_properties.as_ref();
let geometry = match shape_props.and_then(|p| p.geometry.as_ref()) {
Some(g) => g,
None => continue, };
let w = Pt::from(img.extent.width);
let h = Pt::from(img.extent.height);
let extent = PtSize::new(w, h);
let shape_path = match build_geometry(geometry, extent) {
Some(p) => p,
None => continue, };
let visuals = resolve_shape_visuals(
shape_props,
wsp.style_line_ref.as_ref(),
wsp.style_effect_ref.as_ref(),
ctx.resolved.theme.as_ref(),
);
let (rotation, flip_h, flip_v) = shape_props
.and_then(|p| p.transform.as_ref())
.map(|t| {
(
t.rotation
.unwrap_or_else(|| crate::model::dimension::Dimension::new(0)),
t.flip_h.unwrap_or(false),
t.flip_v.unwrap_or(false),
)
})
.unwrap_or((crate::model::dimension::Dimension::new(0), false, false));
let (x, y) = resolve_anchor_position(anchor, w, h, state, frame);
let text_commands = build_shape_text_commands(wsp, extent, ctx, state);
shapes.push(FloatingShape {
x,
y,
size: extent,
rotation,
flip_h,
flip_v,
wrap_mode: crate::render::layout::section::WrapMode::from_model(&anchor.wrap),
dist_left: Pt::from(anchor.distance.left),
dist_right: Pt::from(anchor.distance.right),
behind_doc: anchor.behind_text,
paths: shape_path.paths,
fill: visuals.fill,
stroke: visuals.stroke,
effects: visuals.effects,
text_commands,
});
}
extract_vml_primitive_shapes(¶.content, state, frame, &mut shapes);
shapes
}
fn extract_vml_floating_images(
inlines: &[crate::model::Inline],
state: &BuildState,
frame: AnchorFrame,
ctx: &BuildContext,
out: &mut Vec<FloatingImage>,
) {
use crate::model::Inline;
for inline in inlines {
match inline {
Inline::Pict(pict) => {
for primitive in &pict.primitives {
extract_vml_primitive_image(primitive, state, frame, ctx, out);
}
}
Inline::Hyperlink(link) => {
extract_vml_floating_images(&link.content, state, frame, ctx, out)
}
Inline::Field(f) => extract_vml_floating_images(&f.content, state, frame, ctx, out),
Inline::AlternateContent(_) => {}
_ => {}
}
}
}
fn extract_vml_primitive_image(
primitive: &model::VmlPrimitive,
state: &BuildState,
frame: AnchorFrame,
ctx: &BuildContext,
out: &mut Vec<FloatingImage>,
) {
use crate::model::VmlPrimitive;
match primitive {
VmlPrimitive::Image(img) => {
if let Some(fi) = build_vml_floating_image(&img.common, state, frame, ctx) {
out.push(fi);
}
}
VmlPrimitive::Shape(s) if s.common.image_data.is_some() && s.common.text_box.is_none() => {
if let Some(fi) = build_vml_floating_image(&s.common, state, frame, ctx) {
out.push(fi);
}
}
VmlPrimitive::Group(g) => {
for child in &g.children {
extract_vml_primitive_image(child, state, frame, ctx, out);
}
}
_ => {}
}
}
fn build_vml_floating_image(
common: &model::VmlCommonAttrs,
state: &BuildState,
frame: AnchorFrame,
ctx: &BuildContext,
) -> Option<FloatingImage> {
use crate::render::layout::section::WrapMode;
let rel_id = common.image_data.as_ref()?.rel_id.as_ref()?;
let image_data = ctx.resolved.media.get(rel_id).cloned()?;
let (page_x, y) = vml_absolute_position(&common.style)?;
let x = match frame {
AnchorFrame::Page => page_x,
AnchorFrame::Stack => page_x - state.page_config.margins.left,
};
let width = common.style.width.map(vml_length_to_pt)?;
let height = common.style.height.map(vml_length_to_pt)?;
if width <= Pt::ZERO || height <= Pt::ZERO {
return None;
}
Some(FloatingImage {
image_data,
size: PtSize::new(width, height),
x,
y: FloatingImageY::RelativeToParagraph(y),
wrap_mode: WrapMode::None,
dist_left: Pt::ZERO,
dist_right: Pt::ZERO,
behind_doc: false,
})
}
fn extract_vml_primitive_shapes(
inlines: &[crate::model::Inline],
state: &BuildState,
frame: AnchorFrame,
out: &mut Vec<FloatingShape>,
) {
use crate::model::Inline;
for inline in inlines {
match inline {
Inline::Pict(pict) => {
for primitive in &pict.primitives {
extract_vml_primitive(primitive, state, frame, out);
}
}
Inline::Hyperlink(link) => {
extract_vml_primitive_shapes(&link.content, state, frame, out)
}
Inline::Field(f) => extract_vml_primitive_shapes(&f.content, state, frame, out),
Inline::AlternateContent(_) => {}
_ => {}
}
}
}
fn extract_vml_primitive(
primitive: &model::VmlPrimitive,
state: &BuildState,
frame: AnchorFrame,
out: &mut Vec<FloatingShape>,
) {
use crate::model::VmlPrimitive;
match primitive {
VmlPrimitive::Rect(r) => {
if let Some(shape) = build_vml_rect_shape(&r.common, state, frame) {
out.push(shape);
}
}
VmlPrimitive::RoundRect(r) => {
if let Some(shape) = build_vml_rect_shape(&r.common, state, frame) {
out.push(shape);
}
}
VmlPrimitive::Group(g) => {
for child in &g.children {
extract_vml_primitive(child, state, frame, out);
}
}
VmlPrimitive::Image(_) => {}
VmlPrimitive::Shape(_)
| VmlPrimitive::Oval(_)
| VmlPrimitive::Line(_)
| VmlPrimitive::PolyLine(_)
| VmlPrimitive::Arc(_)
| VmlPrimitive::Curve(_) => {}
}
}
fn build_vml_rect_shape(
common: &model::VmlCommonAttrs,
state: &BuildState,
frame: AnchorFrame,
) -> Option<FloatingShape> {
use crate::render::geometry::PtOffset;
use crate::render::resolve::shape_geometry::{PathVerb, SubPath};
let (page_x, y) = vml_absolute_position(&common.style)?;
let x = match frame {
AnchorFrame::Page => page_x,
AnchorFrame::Stack => page_x - state.page_config.margins.left,
};
let width = common.style.width.map(vml_length_to_pt)?;
let height = common.style.height.map(vml_length_to_pt)?;
if width <= Pt::ZERO || height <= Pt::ZERO {
return None;
}
let extent = PtSize::new(width, height);
let fill = resolve_vml_solid_fill(common);
let paths = vec![SubPath {
verbs: vec![
PathVerb::MoveTo(PtOffset::new(Pt::ZERO, Pt::ZERO)),
PathVerb::LineTo(PtOffset::new(extent.width, Pt::ZERO)),
PathVerb::LineTo(PtOffset::new(extent.width, extent.height)),
PathVerb::LineTo(PtOffset::new(Pt::ZERO, extent.height)),
PathVerb::Close,
],
fill_mode: crate::model::PathFillMode::Norm,
stroked: matches!(common.stroked, Some(true)),
}];
let y_image = FloatingImageY::RelativeToParagraph(y);
Some(FloatingShape {
x,
y: y_image,
size: extent,
rotation: crate::model::dimension::Dimension::new(0),
flip_h: false,
flip_v: false,
wrap_mode: crate::render::layout::section::WrapMode::None,
dist_left: Pt::ZERO,
dist_right: Pt::ZERO,
behind_doc: false,
paths,
fill,
stroke: None,
effects: vec![],
text_commands: Vec::new(),
})
}
fn resolve_anchor_position(
anchor: &crate::model::AnchorProperties,
content_w: Pt,
content_h: Pt,
state: &BuildState,
frame: AnchorFrame,
) -> (Pt, FloatingImageY) {
let x = resolve_anchor_x(anchor, content_w, state, frame);
let y = resolve_anchor_y(anchor, content_h, state, frame);
(x, y)
}
fn resolve_anchor_x(
anchor: &crate::model::AnchorProperties,
content_w: Pt,
state: &BuildState,
frame: AnchorFrame,
) -> Pt {
use crate::model::{AnchorAlignment, AnchorPosition, AnchorRelativeFrom};
let pc = &state.page_config;
let (page_width, margin_left, margin_right) = match frame {
AnchorFrame::Page => (pc.page_size.width, pc.margins.left, pc.margins.right),
AnchorFrame::Stack => (Pt::ZERO, Pt::ZERO, Pt::ZERO),
};
let content_width = (page_width - margin_left - margin_right).max(Pt::ZERO);
let page_anchor_offset = match frame {
AnchorFrame::Page => Pt::ZERO,
AnchorFrame::Stack => -pc.margins.left,
};
match &anchor.horizontal_position {
AnchorPosition::Offset {
relative_from,
offset,
} => match relative_from {
AnchorRelativeFrom::Page => page_anchor_offset + Pt::from(*offset),
_ => margin_left + Pt::from(*offset),
},
AnchorPosition::Align {
relative_from,
alignment,
} => {
let (area_left, area_width) = match relative_from {
AnchorRelativeFrom::Page => {
(page_anchor_offset, page_width.max(pc.page_size.width))
}
AnchorRelativeFrom::Margin | AnchorRelativeFrom::Column => {
(margin_left, content_width)
}
_ => (margin_left, content_width),
};
match alignment {
AnchorAlignment::Left => area_left,
AnchorAlignment::Right => area_left + area_width - content_w,
AnchorAlignment::Center => area_left + (area_width - content_w) * 0.5,
_ => area_left,
}
}
}
}
fn resolve_anchor_y(
anchor: &crate::model::AnchorProperties,
content_h: Pt,
state: &BuildState,
frame: AnchorFrame,
) -> FloatingImageY {
use crate::model::{AnchorAlignment, AnchorPosition, AnchorRelativeFrom};
let pc = &state.page_config;
match &anchor.vertical_position {
AnchorPosition::Offset {
relative_from,
offset,
} => match frame {
AnchorFrame::Stack => FloatingImageY::RelativeToParagraph(Pt::from(*offset)),
AnchorFrame::Page => match relative_from {
AnchorRelativeFrom::Page => FloatingImageY::Absolute(Pt::from(*offset)),
AnchorRelativeFrom::Margin => {
FloatingImageY::Absolute(pc.margins.top + Pt::from(*offset))
}
AnchorRelativeFrom::TopMargin => FloatingImageY::Absolute(Pt::from(*offset)),
AnchorRelativeFrom::BottomMargin => FloatingImageY::Absolute(
pc.page_size.height - pc.margins.bottom + Pt::from(*offset),
),
AnchorRelativeFrom::Paragraph | AnchorRelativeFrom::Line => {
FloatingImageY::RelativeToParagraph(Pt::from(*offset))
}
_ => FloatingImageY::Absolute(pc.margins.top + Pt::from(*offset)),
},
},
AnchorPosition::Align {
relative_from,
alignment,
} => {
let (margin_top, page_height, margin_bottom) = match frame {
AnchorFrame::Page => (pc.margins.top, pc.page_size.height, pc.margins.bottom),
AnchorFrame::Stack => (Pt::ZERO, Pt::ZERO, Pt::ZERO),
};
let (area_top, area_height) = match relative_from {
AnchorRelativeFrom::Page => (Pt::ZERO, page_height),
AnchorRelativeFrom::Margin => (
margin_top,
(page_height - margin_top - margin_bottom).max(Pt::ZERO),
),
AnchorRelativeFrom::TopMargin => (Pt::ZERO, margin_top),
AnchorRelativeFrom::BottomMargin => (page_height - margin_bottom, margin_bottom),
_ => (
margin_top,
(page_height - margin_top - margin_bottom).max(Pt::ZERO),
),
};
let y_pos = match alignment {
AnchorAlignment::Top => area_top,
AnchorAlignment::Bottom => area_top + area_height - content_h,
AnchorAlignment::Center => area_top + (area_height - content_h) * 0.5,
_ => area_top,
};
FloatingImageY::Absolute(y_pos)
}
}
}
pub(super) fn find_vml_absolute_position(inline: &model::Inline) -> Option<(Pt, Pt)> {
match inline {
model::Inline::Pict(pict) => find_vml_pos_in_pict(pict),
model::Inline::AlternateContent(ac) => {
if let Some(ref fallback) = ac.fallback {
for inner in fallback {
if let Some(pos) = find_vml_absolute_position(inner) {
return Some(pos);
}
}
}
None
}
_ => None,
}
}
fn find_vml_pos_in_pict(pict: &model::Pict) -> Option<(Pt, Pt)> {
for shape in pict.shapes() {
if shape.common.text_box.is_some() {
if let Some(pos) = vml_absolute_position(&shape.common.style) {
return Some(pos);
}
}
}
None
}
fn vml_absolute_position(style: &model::VmlStyle) -> Option<(Pt, Pt)> {
use crate::model::CssPosition;
if style.position != Some(CssPosition::Absolute) {
return None;
}
let x = style.margin_left.map(vml_length_to_pt)?;
let y = style.margin_top.map(vml_length_to_pt)?;
Some((x, y))
}
fn resolve_vml_solid_fill(
common: &model::VmlCommonAttrs,
) -> crate::render::layout::draw_command::ResolvedFill {
use crate::model::{VmlColor, VmlFillType};
use crate::render::layout::draw_command::ResolvedFill;
use crate::render::resolve::drawing_color::Rgba;
let to_solid = |c: &VmlColor| -> Option<ResolvedFill> {
match c {
VmlColor::Rgb(r, g, b) => Some(ResolvedFill::Solid(Rgba {
r: *r as f32 / 255.0,
g: *g as f32 / 255.0,
b: *b as f32 / 255.0,
a: 1.0,
})),
VmlColor::Named(_) => None,
}
};
if let Some(ref fill) = common.fill {
match fill.fill_type {
VmlFillType::Solid => {
if let Some(c) = fill.color.as_ref().and_then(to_solid) {
return c;
}
}
VmlFillType::Gradient
| VmlFillType::GradientRadial
| VmlFillType::Tile
| VmlFillType::Frame
| VmlFillType::Pattern => {
log::warn!(
"vml: unsupported fill type {:?} — rendering as no-fill",
fill.fill_type
);
return ResolvedFill::None;
}
}
}
common
.fill_color
.as_ref()
.and_then(to_solid)
.unwrap_or(ResolvedFill::None)
}
fn anchors_to_paragraph(anchor: &crate::model::AnchorProperties) -> bool {
use crate::model::{AnchorPosition, AnchorRelativeFrom};
let relative_from = match &anchor.vertical_position {
AnchorPosition::Offset { relative_from, .. } => relative_from,
AnchorPosition::Align { relative_from, .. } => relative_from,
};
matches!(
relative_from,
AnchorRelativeFrom::Paragraph | AnchorRelativeFrom::Line
)
}
pub(super) fn build_shape_text_commands(
wsp: &crate::model::WordProcessingShape,
extent: PtSize,
ctx: &BuildContext,
state: &BuildState,
) -> Vec<crate::render::layout::draw_command::DrawCommand> {
if wsp.txbx_content.is_empty() {
return Vec::new();
}
let default_lr = Pt::new(91440.0 / 12700.0); let default_tb = Pt::new(45720.0 / 12700.0); let (left_inset, top_inset, right_inset, _bot_inset) =
wsp.body_pr
.as_ref()
.map_or((default_lr, default_tb, default_lr, default_tb), |bp| {
(
bp.left_inset.map_or(default_lr, Pt::from),
bp.top_inset.map_or(default_tb, Pt::from),
bp.right_inset.map_or(default_lr, Pt::from),
bp.bottom_inset.map_or(default_tb, Pt::from),
)
});
let content_width = (extent.width - left_inset - right_inset).max(Pt::ZERO);
if content_width <= Pt::ZERO {
return Vec::new();
}
let mut sub_state = BuildState {
page_config: state.page_config.clone(),
footnote_counter: 0,
endnote_counter: 0,
list_counters: std::collections::HashMap::new(),
field_ctx: state.field_ctx,
};
let hf = super::build_header_footer_content(&wsp.txbx_content, ctx, &mut sub_state);
let line_height = super::default_line_height(ctx);
let result =
crate::render::layout::section::stack_blocks(&hf.blocks, content_width, line_height, None);
let mut commands = Vec::with_capacity(result.commands.len());
for mut cmd in result.commands {
cmd.shift(left_inset, top_inset);
commands.push(cmd);
}
commands
}
fn vml_length_to_pt(len: model::VmlLength) -> Pt {
use crate::model::VmlLengthUnit;
let value = len.value as f32;
Pt::new(match len.unit {
VmlLengthUnit::Pt => value,
VmlLengthUnit::In => value * 72.0,
VmlLengthUnit::Cm => value * 72.0 / 2.54,
VmlLengthUnit::Mm => value * 72.0 / 25.4,
VmlLengthUnit::Px => value * 0.75, VmlLengthUnit::None => value / 914400.0 * 72.0, _ => value, })
}