use crate::{PdfLayerReference, Pt, Px, XObject, XObjectRef};
use lopdf::{Object, Stream};
use std::{error, fmt};
use usvg::TreeParsing;
#[derive(Debug, Clone)]
pub struct Svg {
svg_xobject: Stream,
pub width: Px,
pub height: Px,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SvgParseError {
Svg2PdfConversionError(String),
PdfParsingError,
NoContentStream,
InternalError,
}
impl fmt::Display for SvgParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Svg2PdfConversionError(inner) => write!(f, "svg2pdf conversion error: {inner}"),
Self::PdfParsingError => write!(f, "error parsing svg2pdf pdf data"),
Self::NoContentStream => write!(f, "svg2pdf returned no content stream"),
Self::InternalError => write!(f, "pdf returned by pdf2svg in unexpected form"),
}
}
}
impl error::Error for SvgParseError {}
#[derive(Debug, Copy, Clone, PartialEq, Default)]
pub struct SvgTransform {
pub translate_x: Option<Pt>,
pub translate_y: Option<Pt>,
pub rotate: Option<SvgRotation>,
pub scale_x: Option<f32>,
pub scale_y: Option<f32>,
pub dpi: Option<f32>,
}
#[derive(Debug, Copy, Clone, PartialEq, Default)]
pub struct SvgRotation {
pub angle_ccw_degrees: f32,
pub rotation_center_x: Pt,
pub rotation_center_y: Pt,
}
fn export_svg_to_xobject_pdf(svg: &str) -> Result<Stream, String> {
use pdf_writer::{Content, Finish, Name, Rect, Ref, Pdf};
let catalog_id = Ref::new(1);
let page_tree_id = Ref::new(2);
let page_id = Ref::new(3);
let content_id = Ref::new(4);
let svg_id = Ref::new(5);
let svg_name = Name(b"S1");
let mut writer = Pdf::new();
writer.catalog(catalog_id).pages(page_tree_id);
writer.pages(page_tree_id).kids([page_id]).count(1);
let mut page = writer.page(page_id);
page.media_box(Rect::new(0.0, 0.0, 595.0, 842.0));
page.parent(page_tree_id);
page.contents(content_id);
let mut resources = page.resources();
resources.x_objects().pair(svg_name, svg_id);
resources.finish();
page.finish();
let tree = usvg::Tree::from_str(svg, &usvg::Options::default())
.map_err(|err| format!("usvg parse: {err}"))?;
svg2pdf::convert_tree_into(&tree, svg2pdf::Options::default(), &mut writer, svg_id);
let content = Content::new();
writer.stream(content_id, &content.finish());
let bytes = writer.finish();
let document = lopdf::Document::load_mem(&bytes)
.map_err(|err| format!("lopdf load generated pdf: {err}"))?;
let svg_xobject = document
.get_object((5, 0))
.map_err(|err| format!("grab xobject from generated pdf: {err}"))?;
let object = svg_xobject.as_stream().unwrap();
Ok(object.clone())
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct SvgXObjectRef {
xobject_ref: XObjectRef,
pub width: Px,
pub height: Px,
}
impl SvgXObjectRef {
pub fn add_to_layer(self, layer: &PdfLayerReference, transform: SvgTransform) {
use crate::CurTransMat;
let width = self.width;
let height = self.height;
let dpi = transform.dpi.unwrap_or(300.0);
let scale_x = transform.scale_x.unwrap_or(1.0);
let scale_y = transform.scale_y.unwrap_or(1.0);
let image_w = width.into_pt(dpi).0 * scale_x;
let image_h = height.into_pt(dpi).0 * scale_y;
let mut transforms = Vec::new();
transforms.push(CurTransMat::Scale(image_w, image_h));
if let Some(rotate) = transform.rotate.as_ref() {
transforms.push(CurTransMat::Translate(
Pt(-rotate.rotation_center_x.0),
Pt(-rotate.rotation_center_y.0),
));
transforms.push(CurTransMat::Rotate(rotate.angle_ccw_degrees));
transforms.push(CurTransMat::Translate(
rotate.rotation_center_x,
rotate.rotation_center_y,
));
}
if transform.translate_x.is_some() || transform.translate_y.is_some() {
transforms.push(CurTransMat::Translate(
transform.translate_x.unwrap_or(Pt(0.0)),
transform.translate_y.unwrap_or(Pt(0.0)),
));
}
layer.use_xobject(self.xobject_ref, &transforms);
}
}
impl Svg {
pub fn parse(svg_string: &str) -> Result<Self, SvgParseError> {
let svg_xobject = export_svg_to_xobject_pdf(svg_string).map_err(|err| {
SvgParseError::Svg2PdfConversionError(format!("create xobject from svg: {err}"))
})?;
let bbox = svg_xobject
.dict
.get(b"BBox")
.map_err(|err| {
SvgParseError::Svg2PdfConversionError(format!("extract xobject bbox: {err}"))
})?
.as_array()
.map_err(|err| {
SvgParseError::Svg2PdfConversionError(format!("xobject bbox not an array: {err}"))
})?;
let width_px = match bbox.get(2) {
Some(Object::Integer(px)) => Ok(*px),
Some(Object::Real(px)) => Ok(px.ceil() as i64),
Some(obj) => Err(SvgParseError::Svg2PdfConversionError(format!(
"xobject bbox width not a number: {obj:?}"
))),
None => Err(SvgParseError::Svg2PdfConversionError(
"xobject bbox missing width field".to_string(),
)),
}?;
let height_px = match bbox.get(3) {
Some(Object::Integer(px)) => Ok(*px),
Some(Object::Real(px)) => Ok(px.ceil() as i64),
Some(obj) => Err(SvgParseError::Svg2PdfConversionError(format!(
"xobject bbox height not a number: {obj:?}"
))),
None => Err(SvgParseError::Svg2PdfConversionError(
"xobject bbox missing height field".to_string(),
)),
}?;
Ok(Self {
svg_xobject,
width: Px(width_px.max(0) as usize),
height: Px(height_px.max(0) as usize),
})
}
pub fn into_xobject(self, layer: &PdfLayerReference) -> SvgXObjectRef {
let width = self.width;
let height = self.height;
let xobject_ref = layer.add_xobject(XObject::External(self.svg_xobject));
SvgXObjectRef {
xobject_ref,
width,
height,
}
}
pub fn add_to_layer(self, layer: &PdfLayerReference, transform: SvgTransform) {
let xobject = self.into_xobject(layer);
xobject.add_to_layer(layer, transform);
}
}