use serde::{Deserialize, Serialize};
use super::{Element, RenderContext};
use crate::compliance::ua::StructTag;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub enum ImageAlignment {
Left,
#[default]
Center,
Right,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageElement {
pub data: Vec<u8>,
pub width_mm: Option<f64>,
pub height_mm: Option<f64>,
pub alignment: ImageAlignment,
pub caption: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub width_percent: Option<f64>,
}
impl ImageElement {
pub fn new(data: Vec<u8>) -> Self {
Self {
data,
width_mm: None,
height_mm: None,
alignment: ImageAlignment::Center,
caption: None,
alt: None,
width_percent: None,
}
}
pub fn width(mut self, mm: f64) -> Self {
self.width_mm = Some(mm);
self
}
pub fn width_mm(mut self, mm: f64) -> Self {
self.width_mm = Some(mm);
self
}
pub fn height(mut self, mm: f64) -> Self {
self.height_mm = Some(mm);
self
}
pub fn align(mut self, alignment: ImageAlignment) -> Self {
self.alignment = alignment;
self
}
pub fn caption(mut self, caption: impl Into<String>) -> Self {
self.caption = Some(caption.into());
self
}
pub fn alt(mut self, text: impl Into<String>) -> Self {
self.alt = Some(text.into());
self
}
}
impl Element for ImageElement {
fn estimated_height_mm(&self) -> f64 {
let img_h = self.height_mm.unwrap_or(50.0);
img_h + if self.caption.is_some() { 5.0 } else { 0.0 }
}
fn render(&self, ctx: &mut RenderContext) -> crate::Result<super::RenderResult> {
if self.data.is_empty() {
ctx.flow.advance(self.estimated_height_mm());
return Ok(super::RenderResult::done());
}
let img = image::load_from_memory(&self.data)
.map_err(|e| crate::NormaxisPdfError::ImageLoadError(e.to_string()))?;
let (px_w, px_h) = (img.width() as f64, img.height() as f64);
let aspect = if px_w > 0.0 { px_h / px_w } else { 1.0 };
let content_w = ctx.layout.content_width_mm;
let render_w = if let Some(pct) = self.width_percent {
content_w * pct / 100.0
} else {
self.width_mm.unwrap_or(content_w)
};
let render_h = self.height_mm.unwrap_or(render_w * aspect);
let x_mm = match self.alignment {
ImageAlignment::Left => ctx.layout.content_x_mm,
ImageAlignment::Center => ctx.layout.content_x_mm + (content_w - render_w) / 2.0,
ImageAlignment::Right => ctx.layout.content_x_mm + content_w - render_w,
};
let y_mm = ctx.flow.cursor_y_mm - render_h;
if ctx.ua_enabled() {
match &self.alt {
Some(alt_text) => {
let mcid = ctx.ua_tag_element(StructTag::Figure, Some(alt_text.clone()));
ctx.backend.begin_tagged_content(b"Figure", mcid);
}
None => {
if ctx.ua_config.warn_missing_alt {
eprintln!(
"PDF/UA-2 WARNING: ImageElement without alt text — \
marking as Artifact. Add .alt() for accessible images."
);
}
ctx.backend.begin_artifact_content();
}
}
}
let img_ref = ctx.backend.embed_image(&self.data)?;
ctx.backend.draw_image(img_ref, x_mm, y_mm, render_w, render_h);
if ctx.ua_enabled() {
ctx.backend.end_tagged_content();
}
let caption_h = if self.caption.is_some() { 5.0 } else { 0.0 };
ctx.flow.advance(render_h + caption_h);
Ok(super::RenderResult::done())
}
}