use crate::binary::Reader;
use crate::frame::Frame;
use crate::media::MediaResolver;
use crate::object::read_bbox;
use crate::shape::{NativePathCommand, read_shape_base, visit_path};
use crate::{
BoundingBox, DiagnosticCode, Error, ObjectMetadata, ObjectSpanLayoutConstraint,
ObjectSpanLayoutOption, ObjectType, ParseReport, PlacedImage, Result, RichTextBox,
RichTextObjectContent, ShapePaint,
};
pub(crate) struct DecodedImage {
pub(crate) image: PlacedImage,
pub(crate) unsupported: Vec<&'static str>,
visible: bool,
}
impl DecodedImage {
pub(crate) fn resolve(
mut self,
media: &MediaResolver,
archive_entry: &str,
location: &str,
report: &mut ParseReport,
) -> Option<PlacedImage> {
if !self.visible {
return None;
}
if !self.unsupported.is_empty() {
report.warning(
DiagnosticCode::UnsupportedImageFeature,
Some(archive_entry.to_owned()),
format!(
"{location}: incomplete support for {}",
self.unsupported.join(", ")
),
);
}
match media.resolve(self.image.media_id) {
Ok((index, inferred)) => {
self.image.media_index = Some(index);
if inferred {
report.warning(DiagnosticCode::InferredImageMediaReference, Some(archive_entry.to_owned()), format!("{location}: media/mediaInfo.dat is absent; resolved media ID {} using a unique numeric filename prefix", self.image.media_id.unwrap()));
}
}
Err(message) => report.warning(
DiagnosticCode::UnresolvedImageMedia,
Some(archive_entry.to_owned()),
format!("{location}: {message}"),
),
}
Some(self.image)
}
}
pub(crate) fn decode_text_images(
text: &mut RichTextBox,
media: &MediaResolver,
archive_entry: &str,
report: &mut ParseReport,
) -> Result<()> {
decode_text_images_in_context(text, media, archive_entry, report, false)
}
fn decode_text_images_in_context(
text: &mut RichTextBox,
media: &MediaResolver,
archive_entry: &str,
report: &mut ParseReport,
nested: bool,
) -> Result<()> {
for span in &mut text.object_spans {
if span.object_type == ObjectType::Image {
let location = format!("embedded image at UTF-16 index {}", span.text_index_utf16);
let mut decoded = decode_image(&span.object_data)?;
if nested {
decoded
.unsupported
.push("image layout inside another embedded text object");
}
if span.layout_option != ObjectSpanLayoutOption::Block
|| span.layout_constraint != ObjectSpanLayoutConstraint::Normal
{
decoded
.unsupported
.push("inline, alternate-margin or cross-page image layout");
}
span.content = decoded
.resolve(media, archive_entry, &location, report)
.map(Box::new)
.map(RichTextObjectContent::Image);
}
match span.content.as_mut() {
Some(RichTextObjectContent::Table(table)) => {
for row in &mut table.rows {
for cell in &mut row.cells {
decode_text_images_in_context(
&mut cell.content,
media,
archive_entry,
report,
true,
)?;
}
}
}
Some(RichTextObjectContent::CodeBlock(code)) => {
for text in code.title.iter_mut().chain(code.body.iter_mut()) {
decode_text_images_in_context(text, media, archive_entry, report, true)?;
}
}
_ => {}
}
}
Ok(())
}
pub(crate) fn decode_image(data: &[u8]) -> Result<DecodedImage> {
let mut reader = Reader::new(data, "image object");
let base = ObjectMetadata::read(&mut reader)?;
let shape_base = Frame::read(&mut reader)?;
shape_base.expect_kind(6)?;
let shape = Frame::read(&mut reader)?;
shape.expect_kind(7)?;
let tail = Frame::read(&mut reader)?;
tail.expect_kind(3)?;
let mut unsupported = Vec::new();
read_image_outline(&shape_base, &mut unsupported)?;
let mut fixed = Reader::new(shape.fixed, "image shape fixed data");
let shape_type = fixed.read_u32("shape type")?;
let geometry_bbox = read_bbox(&mut fixed)?;
let shape_rotation = finite_f32(&mut fixed, "shape rotation")?;
let path_size = fixed.read_u32("shape path size")? as usize;
let path = fixed.read_bytes(path_size, "shape path")?;
let rectangular_path = path.is_empty()
|| (is_placement_rectangle(path, base.bbox, base.rotation_degrees.unwrap_or(0.0))?
&& matches_placement(geometry_bbox, base.bbox));
let point_count = usize::from(fixed.read_u8("control point count")?);
fixed.skip(point_count * 16, "control points")?;
if shape_type != 4
|| (shape_rotation != 0.0
&& f64::from(shape_rotation) != base.rotation_degrees.unwrap_or(0.0))
|| !rectangular_path
|| point_count != 0
|| fixed.remaining() != 0
|| shape.properties.has_other_bits(0)
{
unsupported.push("image shape geometry or properties");
}
let mut fields = Reader::new(shape.flexible, "image shape fields");
if shape.fields.contains(0) {
skip_sized(&mut fields, "image text common")?;
unsupported.push("image text content");
}
if shape.fields.contains(1) {
fields.read_u8("text area type")?;
}
if shape.fields.contains(2) {
fields.read_i32("shape pen name ID")?;
}
let media_id = if shape.fields.contains(3) {
unsupported.push("unknown field before the image fill");
None
} else {
if shape.fields.contains(4) {
fields.read_i32("shape pen settings ID")?;
}
if shape.fields.contains(5) {
let size = fields.read_u32("fill size")? as usize;
let kind = fields.read_u8("fill type")?;
let fill = fields.read_bytes(size, "fill payload")?;
if kind == 2 {
parse_image_fill(fill, &mut unsupported)?
} else {
unsupported.push("non-image fill effect");
None
}
} else {
None
}
};
if shape.fields.has_other_bits(0x35) || fields.remaining() != 0 {
unsupported.push("additional shape fields");
}
let mut image = PlacedImage {
bbox: base.bbox,
rotation_degrees: base.rotation_degrees,
media_id,
media_index: None,
crop_rect: None,
original_bbox: None,
border_media_id: None,
original_media_id: None,
};
let mut fields = Reader::new(tail.flexible, "image settings");
for bit in 0..=19 {
if !tail.fields.contains(bit) {
continue;
}
match bit {
1 => image.crop_rect = Some(read_rect(&mut fields)?),
3 => {
fields.read_u32("border color")?;
}
4 => {
finite_f32(&mut fields, "border width")?;
}
5 => {
fields.read_u16("border type")?;
}
9 => image.border_media_id = read_media_id(&mut fields)?,
10 => {
read_rect(&mut fields)?;
}
11 => {
for _ in 0..4 {
finite_f32(&mut fields, "border widths")?;
}
}
12 => {
fields.read_u32("border nine-patch width")?;
}
17 => {
image.original_bbox = Some(read_bbox(&mut fields)?);
}
18 => image.original_media_id = read_media_id(&mut fields)?,
19 => {
skip_sized(&mut fields, "image path")?;
fields.skip(32, "image path rectangles")?;
}
_ => break,
}
}
if tail.fields.has_other_bits((1 << 1) | (1 << 17))
|| (image.crop_rect.is_some() && image.original_bbox.is_none())
|| tail.properties.has_other_bits(0)
|| !tail.fixed.is_empty()
|| fields.remaining() != 0
{
unsupported.push("crop, border, original-image or extension settings");
}
if reader.remaining() != 0 {
while reader.remaining() != 0 {
Frame::read(&mut reader)?;
}
unsupported.push("additional image frames");
}
Ok(DecodedImage {
image,
unsupported,
visible: base.visible,
})
}
fn read_image_outline(frame: &Frame<'_>, unsupported: &mut Vec<&'static str>) -> Result<()> {
if frame.fixed.is_empty() {
if frame.fields.has_other_bits(0)
|| frame.properties.has_other_bits(0)
|| !frame.flexible.is_empty()
{
unsupported.push("inherited shape settings");
}
return Ok(());
}
let style = read_shape_base(frame, unsupported)?.style;
let invisible = match style.paint {
ShapePaint::None => true,
ShapePaint::Solid(argb) => argb >> 24 == 0,
ShapePaint::Gradient | ShapePaint::Unsupported { .. } => false,
};
if style.width != 0.0 && !invisible {
unsupported.push("image outlines");
}
Ok(())
}
fn matches_placement(a: BoundingBox, b: BoundingBox) -> bool {
a.x_min == b.x_min && a.y_min == b.y_min && a.x_max == b.x_max && a.y_max == b.y_max
}
fn is_placement_rectangle(data: &[u8], bbox: BoundingBox, rotation: f64) -> Result<bool> {
let mut points = [[0.0; 2]; 4];
let mut commands = 0;
let mut rectangular = true;
let (consumed, supported) = visit_path(data, |command| {
match (commands, command) {
(0, NativePathCommand::Move(point)) | (1..=3, NativePathCommand::Line(point)) => {
points[commands] = point;
}
(4, NativePathCommand::Close) => {}
_ => rectangular = false,
}
commands += 1;
})?;
if !supported || !rectangular || commands != 5 || consumed != data.len() {
return Ok(false);
}
let cx = (bbox.x_min + bbox.x_max) / 2.0;
let cy = (bbox.y_min + bbox.y_max) / 2.0;
let (sin, cos) = rotation.to_radians().sin_cos();
let corners = [
[bbox.x_min, bbox.y_min],
[bbox.x_max, bbox.y_min],
[bbox.x_max, bbox.y_max],
[bbox.x_min, bbox.y_max],
];
let coordinate_scale = corners
.iter()
.flatten()
.fold(1.0_f64, |scale, value| scale.max(value.abs()));
let tolerance = coordinate_scale * f64::from(f32::EPSILON) * 8.0;
Ok(points.iter().zip(corners).all(|(point, [x, y])| {
let expected = [
cx + cos * (x - cx) - sin * (y - cy),
cy + sin * (x - cx) + cos * (y - cy),
];
point.iter().zip(expected).all(|(actual, expected)| {
expected.is_finite() && (actual - expected).abs() <= tolerance
})
}))
}
fn parse_image_fill(data: &[u8], unsupported: &mut Vec<&'static str>) -> Result<Option<u32>> {
if data.len() > 62 {
unsupported.push("alternate or extended image-fill encoding");
return Ok(None);
}
let mut reader = Reader::new(data, "image fill");
let fill_type = reader.read_u8("image fill mode")?;
let media_id = read_media_id(&mut reader)?;
let mut settings = [0.0; 9];
for value in &mut settings {
*value = finite_f32(&mut reader, "image fill settings")?;
}
let rotatable = reader.read_u8("fill rotatable flag")?;
let nine_patch = read_rect(&mut reader)?;
reader.read_i32("nine-patch width")?;
if fill_type != 0
|| settings[..6].iter().any(|value| *value != 0.0)
|| settings[6..8].iter().any(|value| *value != 100.0)
|| settings[8] != 0.0
|| rotatable != 0
|| nine_patch != [0; 4]
{
unsupported.push("image fill transforms, tiling, transparency or nine-patch");
}
Ok(media_id)
}
fn read_media_id(reader: &mut Reader<'_>) -> Result<Option<u32>> {
let id = reader.read_i32("media bind ID")?;
Ok(u32::try_from(id).ok())
}
fn read_rect(reader: &mut Reader<'_>) -> Result<[i32; 4]> {
Ok([
reader.read_i32("rectangle left")?,
reader.read_i32("rectangle top")?,
reader.read_i32("rectangle right")?,
reader.read_i32("rectangle bottom")?,
])
}
fn finite_f32(reader: &mut Reader<'_>, field: &'static str) -> Result<f32> {
let value = reader.read_f32(field)?;
if !value.is_finite() {
return Err(Error::Format(format!("image: non-finite {field}")));
}
Ok(value)
}
fn skip_sized(reader: &mut Reader<'_>, field: &'static str) -> Result<()> {
let size = reader.read_u32(field)? as usize;
reader.skip(size, field)
}