use anyhow::Result;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use usvg::roxmltree::Document;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq)]
pub enum ImageType {
Png,
Jpeg,
Svg,
}
#[derive(Debug, Clone)]
pub struct Image {
pub path: String,
pub alt_text: String,
pub width: u32,
pub height: u32,
pub image_type: ImageType,
pub data: Vec<u8>,
}
impl Image {
pub fn new(path: String, alt_text: String, width: u32, height: u32) -> Result<Self> {
let path_obj = Path::new(&path);
let extension = path_obj
.extension()
.and_then(|ext| ext.to_str())
.ok_or_else(|| anyhow::anyhow!("Unable to determine file extension"))?
.to_lowercase();
let image_type = match extension.as_str() {
"png" => ImageType::Png,
"jpg" | "jpeg" => ImageType::Jpeg,
"svg" => ImageType::Svg,
_ => {
return Err(anyhow::anyhow!(
"Unsupported image format: {}. Only PNG, JPEG and SVG are supported.",
extension
))
}
};
let mut file = File::open(&path)?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
Ok(Self {
path,
alt_text,
width,
height,
image_type,
data,
})
}
pub fn from_svg_string(svg_code: String, alt_text: String) -> Result<Self> {
let (width, height) = get_svg_size(&svg_code);
let data = svg_code.into_bytes();
let path = format!("svg_{}.svg", Uuid::new_v4());
Ok(Self {
path,
alt_text,
width: width as u32,
height: height as u32,
image_type: ImageType::Svg,
data,
})
}
pub fn from_png_bytes(
data: Vec<u8>,
alt_text: String,
width: u32,
height: u32,
) -> Result<Self> {
let path = format!("png_{}.png", Uuid::new_v4());
Ok(Self {
path,
alt_text,
width,
height,
image_type: ImageType::Png,
data,
})
}
pub fn get_mime_type(&self) -> &'static str {
match self.image_type {
ImageType::Png => "image/png",
ImageType::Jpeg => "image/jpeg",
ImageType::Svg => "image/svg+xml",
}
}
pub fn get_extension(&self) -> &'static str {
match self.image_type {
ImageType::Png => "png",
ImageType::Jpeg => "jpeg",
ImageType::Svg => "svg",
}
}
}
pub fn generate_image_drawing_xml(
relationship_id: usize,
width: u32,
height: u32,
alt_text: &str,
image_type: &ImageType,
) -> String {
let page_width_emu = (21.0 - 2.0 * 2.0) * 360000.0; let orig_width_emu = width * 9525;
let orig_height_emu = height * 9525;
let ratio = page_width_emu / orig_width_emu as f64;
let emu_width = page_width_emu as u32;
let emu_height = (orig_height_emu as f64 * ratio) as u32;
let drawing_id = 1000 + relationship_id;
let blip_ext = match image_type {
ImageType::Svg => {
format!(
r#"
<a:extLst>
<a:ext uri="{{96DAC541-7B7A-43D3-8B79-37D633B846F1}}">
<asvg:svgBlip xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main" r:embed="rId{}"/>
</a:ext>
</a:extLst>"#,
relationship_id
)
}
ImageType::Png | ImageType::Jpeg => {
r#"
<a:extLst>
<a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
<a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/>
</a:ext>
</a:extLst>"#.to_string()
}
};
format!(
r#"
<w:p>
<w:r>
<w:drawing>
<wp:inline distT="0" distB="0" distL="0" distR="0">
<wp:extent cx="{emu_width}" cy="{emu_height}"/>
<wp:effectExtent l="0" t="0" r="0" b="0"/>
<wp:docPr id="{drawing_id}" name="图形 {drawing_id}"/>
<wp:cNvGraphicFramePr>
<a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
</wp:cNvGraphicFramePr>
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:nvPicPr>
<pic:cNvPr id="{drawing_id}" name="图形 {drawing_id}"/>
<pic:cNvPicPr><a:picLocks noChangeAspect="1" /></pic:cNvPicPr>
</pic:nvPicPr>
<pic:blipFill>
<a:blip r:embed="rId{relationship_id}">{blip_ext}
</a:blip>
<a:stretch>
<a:fillRect/>
</a:stretch>
</pic:blipFill>
<pic:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="{emu_width}" cy="{emu_height}"/>
</a:xfrm>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
</pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
</w:r>
</w:p>"#,
emu_width = emu_width,
emu_height = emu_height,
drawing_id = drawing_id,
relationship_id = relationship_id,
blip_ext = blip_ext
)
}
fn get_svg_size(svg_code: &str) -> (f64, f64) {
let doc = Document::parse(svg_code).unwrap();
let root = doc.root_element();
if let (Some(w), Some(h)) = (root.attribute("width"), root.attribute("height")) {
if let (Some(width), Some(height)) = (parse_svg_length(w), parse_svg_length(h)) {
return (width, height);
}
}
if let Some(viewbox) = root.attribute("viewBox") {
let parts: Vec<&str> = viewbox
.split(|ch: char| ch.is_whitespace() || ch == ',')
.filter(|part| !part.is_empty())
.collect();
if parts.len() == 4 {
if let (Ok(width), Ok(height)) = (parts[2].parse::<f64>(), parts[3].parse::<f64>()) {
return (width, height);
}
}
}
(100.0, 100.0)
}
fn parse_svg_length(value: &str) -> Option<f64> {
let value = value.trim();
if value.is_empty() || value.ends_with('%') {
return None;
}
let units = ["px", "pt", "pc", "cm", "mm", "in"];
let (number_part, scale) = if let Some(unit) = units.iter().find(|unit| value.ends_with(**unit))
{
let number_part = value.strip_suffix(unit)?;
let scale = match *unit {
"px" => 1.0,
"pt" => 96.0 / 72.0,
"pc" => 16.0,
"cm" => 96.0 / 2.54,
"mm" => 96.0 / 25.4,
"in" => 96.0,
_ => return None,
};
(number_part, scale)
} else {
(value, 1.0)
};
number_part.trim().parse::<f64>().ok().map(|v| v * scale)
}
#[cfg(test)]
mod tests {
use super::{get_svg_size, parse_svg_length};
#[test]
fn parses_svg_lengths_with_supported_units() {
assert_eq!(parse_svg_length("100"), Some(100.0));
assert_eq!(parse_svg_length("100px"), Some(100.0));
assert_eq!(parse_svg_length("2.54cm"), Some(96.0));
assert_eq!(parse_svg_length("100%"), None);
}
#[test]
fn falls_back_to_viewbox_when_svg_uses_percent_dimensions() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 640 360"></svg>"#;
assert_eq!(get_svg_size(svg), (640.0, 360.0));
}
}