Skip to content

Commit

Permalink
Switch to Result-based API. (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
LaurenzV authored Jun 7, 2024
1 parent 5779739 commit 43aa468
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 107 deletions.
3 changes: 2 additions & 1 deletion cli/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ pub fn convert_(

let tree = usvg::Tree::from_str(&svg, &options).map_err(|err| err.to_string())?;

let pdf = svg2pdf::to_pdf(&tree, conversion_options, page_options);
let pdf = svg2pdf::to_pdf(&tree, conversion_options, page_options)
.map_err(|e| format!("Failed to convert PDF file: {e}"))?;

std::fs::write(output, pdf).map_err(|_| "Failed to write PDF file")?;

Expand Down
62 changes: 50 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ let mut options = svg2pdf::usvg::Options::default();
options.fontdb_mut().load_system_fonts();
let tree = svg2pdf::usvg::Tree::from_str(&svg, &options)?;
let pdf = svg2pdf::to_pdf(&tree, ConversionOptions::default(), PageOptions::default());
let pdf = svg2pdf::to_pdf(&tree, ConversionOptions::default(), PageOptions::default()).unwrap();
std::fs::write(output, pdf)?;
# Ok(()) }
```
Expand Down Expand Up @@ -56,8 +56,11 @@ comprehensive list.
mod render;
mod util;

use std::fmt;
use std::fmt::{Display, Formatter};
pub use usvg;

use crate::ConversionError::UnknownError;
use once_cell::sync::Lazy;
use pdf_writer::{Chunk, Content, Filter, Finish, Pdf, Ref, TextStr};
use usvg::{Size, Transform, Tree};
Expand Down Expand Up @@ -88,6 +91,38 @@ impl Default for PageOptions {
}
}

/// A error that can appear during conversion.
#[derive(Copy, Clone, Debug)]
pub enum ConversionError {
/// The SVG image contains an unrecognized type of image.
InvalidImage,
/// An unknown error occurred during the conversion. This could indicate a bug in the
/// svg2pdf.
UnknownError,
/// An error occurred while subsetting a font.
#[cfg(feature = "text")]
SubsetError(fontdb::ID),
/// An error occurred while reading a font.
#[cfg(feature = "text")]
InvalidFont(fontdb::ID),
}

impl Display for ConversionError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::InvalidImage => f.write_str("An unknown type of image appears in the SVG."),
Self::UnknownError => f.write_str("An unknown error occurred during the conversion. This could indicate a bug in svg2pdf"),
#[cfg(feature = "text")]
Self::SubsetError(_) => f.write_str("An error occurred while subsetting a font."),
#[cfg(feature = "text")]
Self::InvalidFont(_) => f.write_str("An error occurred while reading a font."),
}
}
}

/// The result type for everything.
type Result<T> = std::result::Result<T, ConversionError>;

/// Options for the PDF conversion.
#[derive(Copy, Clone)]
pub struct ConversionOptions {
Expand Down Expand Up @@ -146,23 +181,23 @@ impl Default for ConversionOptions {
/// let mut tree = svg2pdf::usvg::Tree::from_str(&svg, &options)?;
///
///
/// let pdf = svg2pdf::to_pdf(&tree, ConversionOptions::default(), PageOptions::default());
/// let pdf = svg2pdf::to_pdf(&tree, ConversionOptions::default(), PageOptions::default()).unwrap();
/// std::fs::write(output, pdf)?;
/// # Ok(()) }
/// ```
pub fn to_pdf(
tree: &Tree,
conversion_options: ConversionOptions,
page_options: PageOptions,
) -> Vec<u8> {
) -> Result<Vec<u8>> {
let mut ctx = Context::new(tree, conversion_options);
let mut pdf = Pdf::new();

let dpi_ratio = 72.0 / page_options.dpi;
let dpi_transform = Transform::from_scale(dpi_ratio, dpi_ratio);
let page_size =
Size::from_wh(tree.size().width() * dpi_ratio, tree.size().height() * dpi_ratio)
.unwrap();
.ok_or(UnknownError)?;

let catalog_ref = ctx.alloc_ref();
let page_tree_ref = ctx.alloc_ref();
Expand All @@ -177,7 +212,7 @@ pub fn to_pdf(
let mut content = Content::new();
content.save_state();
content.transform(dpi_transform.to_pdf_transform());
tree_to_stream(tree, &mut pdf, &mut content, &mut ctx, &mut rc);
tree_to_stream(tree, &mut pdf, &mut content, &mut ctx, &mut rc)?;
content.restore_state();
let content_stream = ctx.finish_content(content);
let mut stream = pdf.stream(content_ref, &content_stream);
Expand All @@ -203,12 +238,12 @@ pub fn to_pdf(
page.contents(content_ref);
page.finish();

ctx.write_global_objects(&mut pdf);
ctx.write_global_objects(&mut pdf)?;

let document_info_id = ctx.alloc_ref();
pdf.document_info(document_info_id).producer(TextStr("svg2pdf"));

pdf.finish()
Ok(pdf.finish())
}

/// Convert a [Tree] into a [`Chunk`].
Expand Down Expand Up @@ -251,7 +286,7 @@ pub fn to_pdf(
/// let mut options = svg2pdf::usvg::Options::default();
/// options.fontdb_mut().load_system_fonts();
/// let tree = svg2pdf::usvg::Tree::from_str(&svg, &options)?;
/// let (mut svg_chunk, svg_id) = svg2pdf::to_chunk(&tree, svg2pdf::ConversionOptions::default());
/// let (mut svg_chunk, svg_id) = svg2pdf::to_chunk(&tree, svg2pdf::ConversionOptions::default()).unwrap();
///
/// // Renumber the chunk so that we can embed it into our existing workflow, and also make sure
/// // to update `svg_id`.
Expand Down Expand Up @@ -306,11 +341,14 @@ pub fn to_pdf(
/// std::fs::write("target/embedded.pdf", pdf.finish())?;
/// # Ok(()) }
/// ```
pub fn to_chunk(tree: &Tree, conversion_options: ConversionOptions) -> (Chunk, Ref) {
pub fn to_chunk(
tree: &Tree,
conversion_options: ConversionOptions,
) -> Result<(Chunk, Ref)> {
let mut chunk = Chunk::new();

let mut ctx = Context::new(tree, conversion_options);
let x_ref = tree_to_xobject(tree, &mut chunk, &mut ctx);
ctx.write_global_objects(&mut chunk);
(chunk, x_ref)
let x_ref = tree_to_xobject(tree, &mut chunk, &mut ctx)?;
ctx.write_global_objects(&mut chunk)?;
Ok((chunk, x_ref))
}
15 changes: 9 additions & 6 deletions src/render/clip_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use super::path::draw_path;
use crate::util::context::Context;
use crate::util::helper::{bbox_to_non_zero_rect, NameExt, RectExt, TransformExt};
use crate::util::resources::ResourceContainer;
use crate::Result;

/// Render a clip path into a content stream.
pub fn render(
Expand All @@ -17,7 +18,7 @@ pub fn render(
content: &mut Content,
ctx: &mut Context,
rc: &mut ResourceContainer,
) {
) -> Result<()> {
// Unfortunately, clip paths are a bit tricky to deal with, the reason being that clip paths in
// SVGs can be much more complex than in PDF. In SVG, clip paths can have transforms, as well as
// nested clip paths. The objects inside of the clip path can have transforms as well, making it
Expand Down Expand Up @@ -54,10 +55,12 @@ pub fn render(
clip_rules.first().copied().unwrap_or(FillRule::NonZero),
);
} else {
let clip_path_ref = create_complex_clip_path(group, clip_path, chunk, ctx);
let clip_path_ref = create_complex_clip_path(group, clip_path, chunk, ctx)?;
let clip_path_name = rc.add_graphics_state(clip_path_ref);
content.set_parameters(clip_path_name.to_pdf_name());
}

Ok(())
}

fn is_simple_clip_path(group: &Group) -> bool {
Expand Down Expand Up @@ -173,15 +176,15 @@ fn create_complex_clip_path(
clip_path: &ClipPath,
chunk: &mut Chunk,
ctx: &mut Context,
) -> Ref {
) -> Result<Ref> {
let mut rc = ResourceContainer::new();
let x_ref = ctx.alloc_ref();

let mut content = Content::new();
content.save_state();

if let Some(clip_path) = clip_path.clip_path() {
render(parent, clip_path, chunk, &mut content, ctx, &mut rc);
render(parent, clip_path, chunk, &mut content, ctx, &mut rc)?;
}

content.transform(clip_path.transform().to_pdf_transform());
Expand All @@ -196,7 +199,7 @@ fn create_complex_clip_path(
Transform::default(),
None,
&mut rc,
);
)?;
content.restore_state();

let content_stream = ctx.finish_content(content);
Expand Down Expand Up @@ -224,5 +227,5 @@ fn create_complex_clip_path(
let mut gs = chunk.ext_graphics(gs_ref);
gs.soft_mask().subtype(MaskType::Alpha).group(x_ref);

gs_ref
Ok(gs_ref)
}
21 changes: 14 additions & 7 deletions src/render/filter.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::render::image;
use crate::util::context::Context;
use crate::util::resources::ResourceContainer;
use crate::ConversionError::UnknownError;
use crate::Result;
use pdf_writer::{Chunk, Content};
use std::sync::Arc;
use tiny_skia::{Size, Transform};
Expand All @@ -13,18 +15,23 @@ pub fn render(
content: &mut Content,
ctx: &mut Context,
rc: &mut ResourceContainer,
) -> Option<()> {
) -> Result<()> {
// TODO: Add a check so that huge regions don't crash svg2pdf (see huge-region.svg test case)
let layer_bbox = group.layer_bounding_box().transform(group.transform())?;
let layer_bbox = group
.layer_bounding_box()
.transform(group.transform())
.ok_or(UnknownError)?;
let pixmap_size = Size::from_wh(
layer_bbox.width() * ctx.options.raster_scale,
layer_bbox.height() * ctx.options.raster_scale,
)?;
)
.ok_or(UnknownError)?;

let mut pixmap = tiny_skia::Pixmap::new(
pixmap_size.width().round() as u32,
pixmap_size.height().round() as u32,
)?;
)
.ok_or(UnknownError)?;

let initial_transform =
Transform::from_scale(ctx.options.raster_scale, ctx.options.raster_scale)
Expand All @@ -43,7 +50,7 @@ pub fn render(
&mut pixmap.as_mut(),
);

let encoded_image = pixmap.encode_png().ok()?;
let encoded_image = pixmap.encode_png().map_err(|_| UnknownError)?;

image::render(
true,
Expand All @@ -53,7 +60,7 @@ pub fn render(
content,
ctx,
rc,
);
)?;

Some(())
Ok(())
}
33 changes: 20 additions & 13 deletions src/render/group.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::ConversionError::UnknownError;
use pdf_writer::{Chunk, Content, Filter, Finish, Ref};
use std::ops::Mul;
use usvg::{Opacity, Transform};
Expand All @@ -8,6 +9,7 @@ use super::{clip_path, mask, Render};
use crate::util::context::Context;
use crate::util::helper::{BlendModeExt, GroupExt, NameExt, RectExt, TransformExt};
use crate::util::resources::ResourceContainer;
use crate::Result;

/// Render a group into a content stream.
pub fn render(
Expand All @@ -18,11 +20,10 @@ pub fn render(
accumulated_transform: Transform,
initial_opacity: Option<Opacity>,
rc: &mut ResourceContainer,
) {
) -> Result<()> {
#[cfg(feature = "filters")]
if !group.filters().is_empty() {
filter::render(group, chunk, content, ctx, rc);
return;
return filter::render(group, chunk, content, ctx, rc);
}

#[cfg(not(feature = "filters"))]
Expand Down Expand Up @@ -51,21 +52,25 @@ pub fn render(
// hack of setting and then reversing the transform.
if let Some(mask) = group.mask() {
content.transform(group.transform().to_pdf_transform());
mask::render(group, mask, chunk, content, ctx, rc);
content.transform(group.transform().invert().unwrap().to_pdf_transform());
mask::render(group, mask, chunk, content, ctx, rc)?;
content.transform(
group.transform().invert().ok_or(UnknownError)?.to_pdf_transform(),
);
}

// We don't need to pass the accumulated transform here because if a pattern appears in a
// XObject, it will be mapped to the coordinate space of where the XObject was invoked, meaning
// that it will also be affected by the transforms in the content stream. If we passed on the
// accumulated transform, they would be applied twice.
let x_ref = create_x_object(group, chunk, ctx, Transform::default());
let x_ref = create_x_object(group, chunk, ctx, Transform::default())?;
let x_name = rc.add_x_object(x_ref);
content.x_object(x_name.to_pdf_name());
content.restore_state();
} else {
create_to_stream(group, chunk, content, ctx, accumulated_transform, rc);
create_to_stream(group, chunk, content, ctx, accumulated_transform, rc)?;
}

Ok(())
}

/// Turn a group into an XObject.
Expand All @@ -74,7 +79,7 @@ fn create_x_object(
chunk: &mut Chunk,
ctx: &mut Context,
accumulated_transform: Transform,
) -> Ref {
) -> Result<Ref> {
let x_ref = ctx.alloc_ref();
let mut rc = ResourceContainer::new();

Expand All @@ -86,7 +91,7 @@ fn create_x_object(

let mut content = Content::new();

create_to_stream(group, chunk, &mut content, ctx, accumulated_transform, &mut rc);
create_to_stream(group, chunk, &mut content, ctx, accumulated_transform, &mut rc)?;

let content_stream = ctx.finish_content(content);

Expand All @@ -108,7 +113,7 @@ fn create_x_object(
x_object.bbox(pdf_bbox);
x_object.finish();

x_ref
Ok(x_ref)
}

/// Write a group into a content stream. Opacities will be ignored. If opacities are needed,
Expand All @@ -120,18 +125,20 @@ fn create_to_stream(
ctx: &mut Context,
accumulated_transform: Transform,
rc: &mut ResourceContainer,
) {
) -> Result<()> {
content.save_state();
content.transform(group.transform().to_pdf_transform());
let accumulated_transform = accumulated_transform.pre_concat(group.transform());

if let Some(clip_path) = &group.clip_path() {
clip_path::render(group, clip_path, chunk, content, ctx, rc);
clip_path::render(group, clip_path, chunk, content, ctx, rc)?;
}

for child in group.children() {
child.render(chunk, content, ctx, accumulated_transform, rc);
child.render(chunk, content, ctx, accumulated_transform, rc)?;
}

content.restore_state();

Ok(())
}
Loading

0 comments on commit 43aa468

Please sign in to comment.