11//! Convert SVG files to PDFs.
22
3- use std:: borrow:: Cow ;
43use std:: collections:: HashMap ;
54
5+ use pdf_writer:: types:: ProcSet ;
66use pdf_writer:: writers:: { ColorSpace , ExponentialFunction , FormXObject , Resources } ;
77use pdf_writer:: { Content , Finish , Name , PdfWriter , Rect , Ref , TextStr , Writer } ;
88use usvg:: { NodeExt , NodeKind , Stop , Tree } ;
@@ -13,211 +13,54 @@ mod scale;
1313
1414use defer:: * ;
1515use render:: * ;
16- pub use scale:: * ;
16+ use scale:: * ;
1717
1818const SRGB : Name = Name ( b"srgb" ) ;
1919
2020/// Set size and scaling preferences for the conversion.
2121#[ derive( Debug , Clone ) ]
2222pub 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
3761impl 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