use std::rc::Rc;
use crate::model::dimension::{Dimension, ThousandthPercent};
use crate::model::{GraphicContent, Image, ImageFormat, RelId, RelativeRect};
use crate::render::dimension::Pt;
use crate::render::geometry::PtRect;
#[derive(Clone, Debug)]
pub struct MediaEntry {
pub data: Rc<[u8]>,
pub format: ImageFormat,
}
pub fn extract_image_rel_id(image: &Image) -> Option<&RelId> {
match image.graphic.as_ref()? {
GraphicContent::Picture(pic) => pic.blip_fill.blip.as_ref()?.embed.as_ref(),
GraphicContent::WordProcessingShape(_) => None,
}
}
pub(crate) fn relative_rect_to_fraction(rel: &RelativeRect) -> Option<PtRect> {
let frac = |d: Option<Dimension<ThousandthPercent>>| d.map_or(0.0, |v| v.to_fraction());
let (left, top, right, bottom) = (
frac(rel.left),
frac(rel.top),
frac(rel.right),
frac(rel.bottom),
);
if left == 0.0 && top == 0.0 && right == 0.0 && bottom == 0.0 {
return None;
}
let (visible_w, visible_h) = (1.0 - left - right, 1.0 - top - bottom);
if visible_w <= 0.0 || visible_h <= 0.0 {
return None;
}
Some(PtRect::from_xywh(
Pt::new(left),
Pt::new(top),
Pt::new(visible_w),
Pt::new(visible_h),
))
}
pub fn extract_src_rect(image: &Image) -> Option<PtRect> {
let GraphicContent::Picture(pic) = image.graphic.as_ref()? else {
return None;
};
relative_rect_to_fraction(pic.blip_fill.src_rect.as_ref()?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::dimension::Dimension;
use crate::model::geometry::{EdgeInsets, Size};
use crate::model::*;
fn make_image_with_blip(rel_id: &str) -> Image {
Image {
extent: Size::new(Dimension::new(0), Dimension::new(0)),
effect_extent: None,
doc_properties: DocProperties {
id: 1,
name: "img".into(),
description: None,
hidden: None,
title: None,
},
graphic_frame_locks: None,
graphic: Some(GraphicContent::Picture(Picture {
nv_pic_pr: NvPicProperties {
cnv_pr: DocProperties {
id: 1,
name: "pic".into(),
description: None,
hidden: None,
title: None,
},
cnv_pic_pr: None,
},
blip_fill: BlipFill {
rotate_with_shape: None,
dpi: None,
blip: Some(Blip {
embed: Some(RelId::new(rel_id)),
link: None,
compression: None,
}),
src_rect: None,
fill_kind: BlipFillKind::Unspecified,
},
shape_properties: None,
})),
placement: ImagePlacement::Inline {
distance: EdgeInsets::new(
Dimension::new(0),
Dimension::new(0),
Dimension::new(0),
Dimension::new(0),
),
},
}
}
#[test]
fn extracts_rel_id_from_picture_blip() {
let img = make_image_with_blip("rId5");
let rel_id = extract_image_rel_id(&img);
assert_eq!(rel_id.map(|r| r.as_str()), Some("rId5"));
}
#[test]
fn no_graphic_returns_none() {
let img = Image {
extent: Size::new(Dimension::new(0), Dimension::new(0)),
effect_extent: None,
doc_properties: DocProperties {
id: 1,
name: "img".into(),
description: None,
hidden: None,
title: None,
},
graphic_frame_locks: None,
graphic: None,
placement: ImagePlacement::Inline {
distance: EdgeInsets::new(
Dimension::new(0),
Dimension::new(0),
Dimension::new(0),
Dimension::new(0),
),
},
};
assert!(extract_image_rel_id(&img).is_none());
}
#[test]
fn no_blip_returns_none() {
let img = Image {
extent: Size::new(Dimension::new(0), Dimension::new(0)),
effect_extent: None,
doc_properties: DocProperties {
id: 1,
name: "img".into(),
description: None,
hidden: None,
title: None,
},
graphic_frame_locks: None,
graphic: Some(GraphicContent::Picture(Picture {
nv_pic_pr: NvPicProperties {
cnv_pr: DocProperties {
id: 1,
name: "pic".into(),
description: None,
hidden: None,
title: None,
},
cnv_pic_pr: None,
},
blip_fill: BlipFill {
rotate_with_shape: None,
dpi: None,
blip: None,
src_rect: None,
fill_kind: BlipFillKind::Unspecified,
},
shape_properties: None,
})),
placement: ImagePlacement::Inline {
distance: EdgeInsets::new(
Dimension::new(0),
Dimension::new(0),
Dimension::new(0),
Dimension::new(0),
),
},
};
assert!(extract_image_rel_id(&img).is_none());
}
#[test]
fn word_processing_shape_returns_none() {
let img = Image {
extent: Size::new(Dimension::new(0), Dimension::new(0)),
effect_extent: None,
doc_properties: DocProperties {
id: 1,
name: "img".into(),
description: None,
hidden: None,
title: None,
},
graphic_frame_locks: None,
graphic: Some(GraphicContent::WordProcessingShape(WordProcessingShape {
cnv_pr: None,
shape_properties: None,
style_line_ref: None,
style_effect_ref: None,
body_pr: None,
txbx_content: vec![],
})),
placement: ImagePlacement::Inline {
distance: EdgeInsets::new(
Dimension::new(0),
Dimension::new(0),
Dimension::new(0),
Dimension::new(0),
),
},
};
assert!(extract_image_rel_id(&img).is_none());
}
#[test]
fn src_rect_none_when_no_crop() {
assert!(extract_src_rect(&make_image_with_blip("rId1")).is_none());
}
#[test]
fn src_rect_converts_thousandth_percent_crop() {
let mut img = make_image_with_blip("rId1");
if let Some(GraphicContent::Picture(pic)) = img.graphic.as_mut() {
pic.blip_fill.src_rect = Some(RelativeRect {
left: None,
top: Some(Dimension::new(15883)),
right: Some(Dimension::new(14520)),
bottom: Some(Dimension::new(17647)),
});
}
let r = extract_src_rect(&img).expect("crop present");
assert!((r.origin.x.raw() - 0.0).abs() < 1e-4, "left crop = 0");
assert!((r.origin.y.raw() - 0.15883).abs() < 1e-4, "top crop");
assert!((r.size.width.raw() - 0.85480).abs() < 1e-4, "visible width");
assert!(
(r.size.height.raw() - 0.66470).abs() < 1e-4,
"visible height"
);
}
#[test]
fn src_rect_none_when_horizontal_crop_collapses_region() {
let mut img = make_image_with_blip("rId1");
if let Some(GraphicContent::Picture(pic)) = img.graphic.as_mut() {
pic.blip_fill.src_rect = Some(RelativeRect {
left: Some(Dimension::new(60000)),
top: None,
right: Some(Dimension::new(60000)),
bottom: None,
});
}
assert!(extract_src_rect(&img).is_none());
}
#[test]
fn src_rect_none_when_vertical_crop_collapses_region() {
let mut img = make_image_with_blip("rId1");
if let Some(GraphicContent::Picture(pic)) = img.graphic.as_mut() {
pic.blip_fill.src_rect = Some(RelativeRect {
left: None,
top: Some(Dimension::new(100000)),
right: None,
bottom: Some(Dimension::new(5000)),
});
}
assert!(extract_src_rect(&img).is_none());
}
#[test]
fn relative_rect_to_fraction_is_the_shared_converter() {
let rel = RelativeRect {
left: Some(Dimension::new(25000)),
top: None,
right: Some(Dimension::new(10000)),
bottom: Some(Dimension::new(10000)),
};
let r = relative_rect_to_fraction(&rel).expect("crop present");
assert!((r.origin.x.raw() - 0.25).abs() < 1e-5);
assert!((r.size.width.raw() - 0.65).abs() < 1e-5);
assert!((r.size.height.raw() - 0.90).abs() < 1e-5);
}
#[test]
fn relative_rect_to_fraction_preserves_negative_insets_for_padding() {
let rel = RelativeRect {
left: Some(Dimension::new(-20000)),
top: None,
right: Some(Dimension::new(-20000)),
bottom: None,
};
let r = relative_rect_to_fraction(&rel).expect("crop present");
assert!(
(r.origin.x.raw() + 0.2).abs() < 1e-5,
"negative origin kept"
);
assert!((r.size.width.raw() - 1.4).abs() < 1e-5, "width > 1 kept");
}
}