Skip to content

Commit 9465b91

Browse files
committed
New API
1 parent 3076749 commit 9465b91

3 files changed

Lines changed: 168 additions & 232 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ png = ["image/png"]
1010
jpeg = ["image/jpeg"]
1111

1212
[dependencies]
13-
image = { package="image", version = "0.23", default-features = false, optional = true }
13+
image = { version = "0.23", default-features = false, optional = true }
1414
miniz_oxide = "0.4"
1515
pdf-writer = { git = "https://github.com/typst/pdf-writer", rev = "a8880b6" }
16-
usvg = "0.17"
16+
usvg = "0.19"

src/lib.rs

Lines changed: 134 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! Convert SVG files to PDFs.
22
3-
use std::borrow::Cow;
43
use std::collections::HashMap;
54

5+
use pdf_writer::types::ProcSet;
66
use pdf_writer::writers::{ColorSpace, ExponentialFunction, FormXObject, Resources};
77
use pdf_writer::{Content, Finish, Name, PdfWriter, Rect, Ref, TextStr, Writer};
88
use usvg::{NodeExt, NodeKind, Stop, Tree};
@@ -13,211 +13,54 @@ mod scale;
1313

1414
use defer::*;
1515
use render::*;
16-
pub use scale::*;
16+
use scale::*;
1717

1818
const SRGB: Name = Name(b"srgb");
1919

2020
/// Set size and scaling preferences for the conversion.
2121
#[derive(Debug, Clone)]
2222
pub struct Options {
23-
/// Specific dimensions the SVG can be forced to fill. This size will also
24-
/// be used if the SVG does not have a native size.
23+
/// Specific dimensions the SVG will be forced to fill in nominal SVG
24+
/// pixels. If this is `Some`, the resulting PDF will always have the
25+
/// corresponding size converted to PostScript points according to `dpi`. If
26+
/// it is `None`, the PDF will either take on the native size of the SVG or
27+
/// 100 by 100 if no native size was specified (i.e. there is no `viewBox`,
28+
/// no `width`, and no `height` attribute).
29+
///
30+
/// Normally, unsized SVGs will take on the size of the target viewport. In
31+
/// order to achieve the behavior in which your SVG will take its native
32+
/// size and the size of your viewport only if it has no native size, you
33+
/// need to create a usvg [`Tree`] for your file in your own code. You will
34+
/// then need to set the `default_size` field of the [`usvg::Options`]
35+
/// struct to your viewport size and set this field according to
36+
/// `tree.svg_node().size`.
37+
///
38+
/// _Default:_ `None`.
2539
pub viewport: Option<(f64, f64)>,
26-
/// Whether to respect the SVG's native size, even if the viewport is set.
27-
pub respect_native_size: bool,
28-
/// Override the scaling mode of the SVG within its viewport.
29-
pub aspect_ratio: Option<usvg::AspectRatio>,
30-
/// The Dots per Inch to assume for the conversion to PDF's printers points.
31-
/// Common values include `72.0` (1pt = 1px; Adobe and macOS) and `96.0`
32-
/// (Microsoft) for standard resolution screens and multiples of `300.0` for
33-
/// print quality.
40+
/// Override the scaling mode of the SVG within its viewport. Look
41+
/// [here][aspect] to learn about the different possible modes.
42+
///
43+
/// _Default:_ `None`.
44+
///
45+
/// [aspect]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio
46+
pub aspect: Option<usvg::AspectRatio>,
47+
/// The dots per inch to assume for the conversion to PDF's printer's
48+
/// points. Common values include `72.0` (1pt = 1px; Adobe and macOS) and
49+
/// `96.0` (Microsoft) for standard resolution screens and multiples of
50+
/// `300.0` for print quality.
51+
///
52+
/// This, of course, does not change the output quality (except for very
53+
/// high values, where precision might degrade due to floating point
54+
/// errors). Instead, it sets what the physical dimensions of one nominal
55+
/// pixel should be on paper when printed without scaling.
56+
///
57+
/// _Default:_ `72.0`.
3458
pub dpi: f64,
3559
}
3660

3761
impl Default for Options {
3862
fn default() -> Self {
39-
Options {
40-
viewport: None,
41-
respect_native_size: true,
42-
aspect_ratio: None,
43-
dpi: 72.0,
44-
}
45-
}
46-
}
47-
48-
/// Holds the SVG tree to be converted. Can be used to query the final file
49-
/// dimensions before converting.
50-
#[derive(Clone)]
51-
pub struct SvgConversion<'a> {
52-
/// The tree of the input SVG.
53-
tree: Cow<'a, Tree>,
54-
/// The user-set preferences.
55-
options: Options,
56-
/// The bounding box of the PDF file.
57-
bbox: Rect,
58-
/// The coordinate conversion
59-
c: CoordToPdf,
60-
}
61-
62-
impl<'a> SvgConversion<'a> {
63-
/// Create a converter from a source string.
64-
pub fn from_str(src: &str, options: Options) -> Option<Self> {
65-
let mut usvg_opts = usvg::Options::default();
66-
if let Some((width, height)) = options.viewport {
67-
usvg_opts.default_size = usvg::Size::new(width, height)?;
68-
}
69-
let tree = Tree::from_str(src, &usvg_opts.to_ref()).ok()?;
70-
Some(Self::new(tree, options))
71-
}
72-
73-
/// Create a converter from a usvg tree.
74-
pub fn new(tree: Tree, options: Options) -> Self {
75-
let (c, bbox) = Self::new_impl(&tree, &options);
76-
Self { tree: Cow::Owned(tree), options, bbox, c }
77-
}
78-
79-
/// Create a converter from a usvg tree reference.
80-
pub fn from_tree_ref(tree: &'a Tree, options: Options) -> Self {
81-
let (c, bbox) = Self::new_impl(&tree, &options);
82-
Self {
83-
tree: Cow::Borrowed(tree),
84-
options,
85-
bbox,
86-
c,
87-
}
88-
}
89-
90-
fn new_impl(tree: &Tree, options: &Options) -> (CoordToPdf, Rect) {
91-
let native_size = tree.svg_node().size;
92-
let viewport = if let Some((width, height)) = options.viewport {
93-
if options.respect_native_size {
94-
(native_size.width(), native_size.height())
95-
} else {
96-
(width, height)
97-
}
98-
} else {
99-
(native_size.width(), native_size.height())
100-
};
101-
102-
let c = CoordToPdf::new(
103-
viewport,
104-
options.dpi,
105-
tree.svg_node().view_box,
106-
options.aspect_ratio,
107-
);
108-
109-
(
110-
c,
111-
Rect::new(0.0, 0.0, c.px_to_pt(viewport.0), c.px_to_pt(viewport.1)),
112-
)
113-
}
114-
115-
/// Perform the conversion.
116-
pub fn convert(&self) -> Vec<u8> {
117-
let mut ctx = Context::new(&self.tree, &self.bbox, self.c);
118-
119-
let mut writer = PdfWriter::new();
120-
let catalog_id = ctx.alloc_ref();
121-
let page_tree_id = ctx.alloc_ref();
122-
let page_id = ctx.alloc_ref();
123-
let content_id = ctx.alloc_ref();
124-
125-
writer.catalog(catalog_id).pages(page_tree_id);
126-
writer.pages(page_tree_id).count(1).kids([page_id]);
127-
128-
for element in self.tree.defs().children() {
129-
match *element.borrow() {
130-
NodeKind::LinearGradient(ref lg) => {
131-
register_functions(&mut writer, &mut ctx, &lg.id, &lg.base.stops);
132-
}
133-
NodeKind::RadialGradient(ref rg) => {
134-
register_functions(&mut writer, &mut ctx, &rg.id, &rg.base.stops);
135-
}
136-
_ => {}
137-
}
138-
}
139-
140-
ctx.push();
141-
let content = content_stream(&self.tree.root(), &mut writer, &mut ctx);
142-
143-
for (id, gp) in ctx.pending_groups.clone() {
144-
let mask_node = self.tree.defs_by_id(&id).unwrap();
145-
let borrowed = mask_node.borrow();
146-
147-
if let NodeKind::Mask(_) = *borrowed {
148-
ctx.push();
149-
ctx.initial_mask = gp.initial_mask;
150-
151-
let content = content_stream(&mask_node, &mut writer, &mut ctx);
152-
153-
let mut group =
154-
form_xobject(&mut writer, gp.reference, &content, gp.bbox, true);
155-
156-
if let Some(matrix) = gp.matrix {
157-
group.matrix(matrix);
158-
}
159-
160-
let mut resources = group.resources();
161-
ctx.pop(&mut resources);
162-
resources.finish();
163-
}
164-
}
165-
166-
ctx.initial_mask = None;
167-
168-
let mut page = writer.page(page_id);
169-
page.media_box(self.bbox);
170-
page.parent(page_tree_id);
171-
page.contents(content_id);
172-
173-
let mut resources = page.resources();
174-
ctx.pop(&mut resources);
175-
176-
resources.finish();
177-
page.finish();
178-
179-
writer.stream(content_id, &content);
180-
writer.document_info(ctx.alloc_ref()).producer(TextStr("svg2pdf"));
181-
182-
writer.finish()
183-
}
184-
185-
/// Get a reference to the SVG tree.
186-
pub fn tree(&self) -> &Tree {
187-
&self.tree
188-
}
189-
190-
/// Get a mutable reference to the SVG tree.
191-
pub fn tree_mut(&mut self) -> &mut Tree {
192-
self.tree.to_mut()
193-
}
194-
195-
/// The width of the final image in PostScript points.
196-
pub fn width_pt(&self) -> f32 {
197-
self.bbox.x2 - self.bbox.x1
198-
}
199-
200-
/// The height of the final image in PostScript points.
201-
pub fn height_pt(&self) -> f32 {
202-
self.bbox.y2 - self.bbox.y1
203-
}
204-
205-
/// The ratio between width and height of an image.
206-
///
207-
/// A ratio greater than one means that the image is wide, a ratio smaller
208-
/// than one means that it is tall.
209-
pub fn aspect_ratio(&self) -> f32 {
210-
self.width_pt() / self.height_pt()
211-
}
212-
213-
/// The width of the final image in pixels, as specified in the SVG source.
214-
pub fn width_px(&self) -> f32 {
215-
self.c.pt_to_px(self.width_pt()) as f32
216-
}
217-
218-
/// The height of the final image in pixels, as specified in the SVG source.
219-
pub fn height_px(&self) -> f32 {
220-
self.c.pt_to_px(self.height_pt()) as f32
63+
Options { viewport: None, aspect: None, dpi: 72.0 }
22164
}
22265
}
22366

@@ -298,6 +141,7 @@ impl<'a> Context<'a> {
298141
/// dictionary.
299142
fn pop(&mut self, resources: &mut Resources) {
300143
resources.color_spaces().insert(SRGB).start::<ColorSpace>().srgb();
144+
resources.proc_sets([ProcSet::Pdf, ProcSet::ImageColor, ProcSet::ImageGrayscale]);
301145

302146
let [gradients, patterns, graphics, xobjects] = self.checkpoints.pop().unwrap();
303147

@@ -353,14 +197,103 @@ impl<'a> Context<'a> {
353197
}
354198
}
355199

356-
/// Convenience method to convert an SVG source string to a PDF buffer.
357-
pub fn convert(src: &str, options: Options) -> Option<Vec<u8>> {
358-
Some(SvgConversion::from_str(src, options)?.convert())
200+
/// Convert an SVG source string to a PDF buffer.
201+
///
202+
/// Returns an error if the SVG string is malformed.
203+
pub fn convert_str(src: &str, options: Options) -> Result<Vec<u8>, usvg::Error> {
204+
let mut usvg_opts = usvg::Options::default();
205+
if let Some((width, height)) = options.viewport {
206+
usvg_opts.default_size =
207+
usvg::Size::new(width.max(1.0), height.max(1.0)).unwrap();
208+
}
209+
let tree = Tree::from_str(src, &usvg_opts.to_ref())?;
210+
Ok(convert_tree(&tree, options))
359211
}
360212

361-
/// Convenience method to convert an usvg source tree to a PDF buffer.
362-
pub fn from_tree(tree: Tree, options: Options) -> Vec<u8> {
363-
SvgConversion::new(tree, options).convert()
213+
/// Convert a [`usvg` tree](Tree) to a PDF buffer.
214+
pub fn convert_tree(tree: &Tree, options: Options) -> Vec<u8> {
215+
let native_size = tree.svg_node().size;
216+
let viewport = if let Some((width, height)) = options.viewport {
217+
(width, height)
218+
} else {
219+
(native_size.width(), native_size.height())
220+
};
221+
222+
let c = CoordToPdf::new(
223+
viewport,
224+
options.dpi,
225+
tree.svg_node().view_box,
226+
options.aspect,
227+
);
228+
229+
let bbox = Rect::new(0.0, 0.0, c.px_to_pt(viewport.0), c.px_to_pt(viewport.1));
230+
231+
let mut ctx = Context::new(&tree, &bbox, c);
232+
233+
let mut writer = PdfWriter::new();
234+
let catalog_id = ctx.alloc_ref();
235+
let page_tree_id = ctx.alloc_ref();
236+
let page_id = ctx.alloc_ref();
237+
let content_id = ctx.alloc_ref();
238+
239+
writer.catalog(catalog_id).pages(page_tree_id);
240+
writer.pages(page_tree_id).count(1).kids([page_id]);
241+
242+
for element in tree.defs().children() {
243+
match *element.borrow() {
244+
NodeKind::LinearGradient(ref lg) => {
245+
register_functions(&mut writer, &mut ctx, &lg.id, &lg.base.stops);
246+
}
247+
NodeKind::RadialGradient(ref rg) => {
248+
register_functions(&mut writer, &mut ctx, &rg.id, &rg.base.stops);
249+
}
250+
_ => {}
251+
}
252+
}
253+
254+
ctx.push();
255+
let content = content_stream(&tree.root(), &mut writer, &mut ctx);
256+
257+
for (id, gp) in ctx.pending_groups.clone() {
258+
let mask_node = tree.defs_by_id(&id).unwrap();
259+
let borrowed = mask_node.borrow();
260+
261+
if let NodeKind::Mask(_) = *borrowed {
262+
ctx.push();
263+
ctx.initial_mask = gp.initial_mask;
264+
265+
let content = content_stream(&mask_node, &mut writer, &mut ctx);
266+
267+
let mut group =
268+
form_xobject(&mut writer, gp.reference, &content, gp.bbox, true);
269+
270+
if let Some(matrix) = gp.matrix {
271+
group.matrix(matrix);
272+
}
273+
274+
let mut resources = group.resources();
275+
ctx.pop(&mut resources);
276+
resources.finish();
277+
}
278+
}
279+
280+
ctx.initial_mask = None;
281+
282+
let mut page = writer.page(page_id);
283+
page.media_box(bbox);
284+
page.parent(page_tree_id);
285+
page.contents(content_id);
286+
287+
let mut resources = page.resources();
288+
ctx.pop(&mut resources);
289+
290+
resources.finish();
291+
page.finish();
292+
293+
writer.stream(content_id, &content);
294+
writer.document_info(ctx.alloc_ref()).producer(TextStr("svg2pdf"));
295+
296+
writer.finish()
364297
}
365298

366299
/// Write a content stream for a node.
@@ -665,7 +598,7 @@ mod tests {
665598
let doc = fs::read_to_string(path.path()).unwrap();
666599
let mut options = Options::default();
667600
options.dpi = 72.0;
668-
let buf = convert(&doc, options).unwrap();
601+
let buf = convert_str(&doc, options).unwrap();
669602

670603
let len = base_name.len();
671604
let file_name = format!("{}.pdf", &base_name[0 .. len - 4]);

0 commit comments

Comments
 (0)