hayro_svg/
lib.rs

1/*!
2A crate for converting PDF pages to SVG files.
3
4This is the pendant to [`hayro`](https://crates.io/crates/hayro), but allows you to export to
5SVG instead of bitmap images. See the description of that crate for more information on the
6supported features and limitations.
7
8## Safety
9This crate forbids unsafe code via a crate-level attribute.
10
11## Cargo features
12This crate has one optional feature:
13- `embed-fonts`: See the description of [`hayro-interpret`](https://docs.rs/hayro-interpret/latest/hayro_interpret/#cargo-features) for more information.
14*/
15
16#![forbid(unsafe_code)]
17#![deny(missing_docs)]
18
19use crate::clip::CachedClipPath;
20use crate::glyph::{CachedOutlineGlyph, CachedType3Glyph};
21use crate::mask::MaskKind;
22use crate::paint::{CachedShading, CachedShadingPattern, CachedTilingPattern};
23use hayro_interpret::font::Glyph;
24use hayro_interpret::hayro_syntax::page::Page;
25use hayro_interpret::util::{Float32Ext, PageExt};
26use hayro_interpret::{
27    BlendMode, CacheKey, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterSettings,
28    Paint, PathDrawMode, SoftMask, StrokeProps, interpret_page,
29};
30use kurbo::{Affine, BezPath, Cap, Join, Rect};
31use siphasher::sip128::{Hasher128, SipHasher13};
32use std::collections::HashMap;
33use std::fmt;
34use std::fmt::{Display, Formatter};
35use std::hash::Hash;
36use xmlwriter::{Options, XmlWriter};
37
38pub use hayro_interpret;
39pub use hayro_interpret::hayro_syntax;
40
41mod clip;
42mod glyph;
43pub(crate) mod image;
44mod mask;
45pub(crate) mod paint;
46mod path;
47
48/// Convert the given page into an SVG string.
49pub fn convert(page: &Page<'_>, interpreter_settings: &InterpreterSettings) -> String {
50    let mut state = Context::new(
51        page.initial_transform(true),
52        Rect::new(
53            0.0,
54            0.0,
55            page.render_dimensions().0 as f64,
56            page.render_dimensions().1 as f64,
57        ),
58        page.xref(),
59        interpreter_settings.clone(),
60    );
61    let mut device = SvgRenderer::new(page);
62    device.write_header(page.render_dimensions());
63
64    interpret_page(page, &mut state, &mut device);
65
66    device.finish()
67}
68
69pub(crate) struct SvgRenderer<'a> {
70    pub(crate) xml: XmlWriter,
71    pub(crate) outline_glyphs: Deduplicator<CachedOutlineGlyph>,
72    pub(crate) type3_glyphs: Deduplicator<CachedType3Glyph<'a>>,
73    pub(crate) clip_paths: Deduplicator<CachedClipPath>,
74    pub(crate) masks: Deduplicator<MaskKind<'a>>,
75    pub(crate) shadings: Deduplicator<CachedShading>,
76    pub(crate) shading_patterns: Deduplicator<CachedShadingPattern>,
77    pub(crate) tiling_patterns: Deduplicator<CachedTilingPattern<'a>>,
78    pub(crate) dimensions: (f32, f32),
79    pub(crate) cur_mask: Option<SoftMask<'a>>,
80    pub(crate) cur_blend_mode: BlendMode,
81}
82
83impl<'a> SvgRenderer<'a> {
84    pub(crate) fn write_transform(&mut self, transform: Affine) {
85        let c = transform.as_coeffs();
86        let has_scale = !(c[0] as f32).is_nearly_equal(1.0) || !(c[3] as f32).is_nearly_equal(1.0);
87        let has_skew = !(c[1] as f32).is_nearly_equal(0.0) || !(c[2] as f32).is_nearly_equal(0.0);
88        let has_translate =
89            !(c[4] as f32).is_nearly_equal(0.0) || !(c[5] as f32).is_nearly_equal(0.0);
90        let is_identity = !has_scale && !has_skew && !has_translate;
91
92        if !is_identity {
93            let transform = match (has_scale, has_skew, has_translate) {
94                (true, false, false) => {
95                    format!("scale({} {})", c[0] as f32, c[3] as f32)
96                }
97                (false, false, true) => {
98                    format!("translate({} {})", c[4] as f32, c[5] as f32)
99                }
100                _ => {
101                    format!("matrix({})", &convert_transform(&transform))
102                }
103            };
104
105            self.xml.write_attribute("transform", &transform);
106        }
107    }
108
109    fn push_transparency_group_inner(
110        &mut self,
111        opacity: f32,
112        mask: Option<MaskKind<'a>>,
113        blend_mode: BlendMode,
114    ) {
115        let mask_id = mask.map(|m| self.get_mask_id(m));
116
117        self.xml.start_element("g");
118
119        if let Some(mask_id) = mask_id {
120            self.xml
121                .write_attribute_fmt("mask", format_args!("url(#{mask_id})"));
122        }
123
124        if blend_mode != BlendMode::Normal {
125            let bm_name = match blend_mode {
126                BlendMode::Normal => "normal",
127                BlendMode::Multiply => "multiply",
128                BlendMode::Screen => "screen",
129                BlendMode::Overlay => "overlay",
130                BlendMode::Darken => "darken",
131                BlendMode::Lighten => "lighten",
132                BlendMode::ColorDodge => "color-dodge",
133                BlendMode::ColorBurn => "color-burn",
134                BlendMode::HardLight => "hard-light",
135                BlendMode::SoftLight => "soft-light",
136                BlendMode::Difference => "difference",
137                BlendMode::Exclusion => "exclusion",
138                BlendMode::Hue => "hue",
139                BlendMode::Saturation => "saturation",
140                BlendMode::Color => "color",
141                BlendMode::Luminosity => "luminosity",
142            };
143
144            self.xml
145                .write_attribute("style", &format!("mix-blend-mode:{}", bm_name));
146        }
147
148        if !opacity.is_nearly_equal(1.0) {
149            self.xml.write_attribute("opacity", &opacity.to_string());
150        }
151    }
152
153    pub(crate) fn write_stroke_properties(&mut self, stroke_props: &StrokeProps) {
154        if !stroke_props.line_width.is_nearly_equal(1.0) {
155            self.xml
156                .write_attribute("stroke-width", &stroke_props.line_width);
157        }
158
159        match stroke_props.line_cap {
160            Cap::Butt => {}
161            Cap::Square => self.xml.write_attribute("stroke-linecap", "square"),
162            Cap::Round => self.xml.write_attribute("stroke-linecap", "round"),
163        }
164
165        match stroke_props.line_join {
166            Join::Bevel => self.xml.write_attribute("stroke-linejoin", "bevel"),
167            Join::Miter => {}
168            Join::Round => self.xml.write_attribute("stroke-linejoin", "round"),
169        }
170
171        if !stroke_props.miter_limit.is_nearly_equal(4.0) {
172            self.xml
173                .write_attribute("stroke-miterlimit", &stroke_props.miter_limit);
174        }
175
176        if !stroke_props.dash_offset.is_nearly_equal(0.0) {
177            self.xml
178                .write_attribute("stroke-dashoffset", &stroke_props.dash_offset);
179        }
180
181        if !stroke_props.dash_array.is_empty() {
182            self.xml.write_attribute(
183                "stroke-dasharray",
184                &stroke_props
185                    .dash_array
186                    .iter()
187                    .map(|v| v.to_string())
188                    .collect::<Vec<String>>()
189                    .join(","),
190            );
191        }
192    }
193
194    fn with_group(&mut self, func: impl FnOnce(&mut Self)) {
195        let push_group = self.cur_mask.is_some() || self.cur_blend_mode != BlendMode::Normal;
196
197        if push_group {
198            self.push_transparency_group(1.0, self.cur_mask.clone(), self.cur_blend_mode);
199        }
200
201        func(self);
202
203        if push_group {
204            self.pop_transparency_group();
205        }
206    }
207}
208
209impl<'a> Device<'a> for SvgRenderer<'a> {
210    fn set_soft_mask(&mut self, mask: Option<SoftMask<'a>>) {
211        self.cur_mask = mask;
212    }
213
214    fn set_blend_mode(&mut self, blend_mode: BlendMode) {
215        self.cur_blend_mode = blend_mode;
216    }
217
218    fn draw_path(
219        &mut self,
220        path: &BezPath,
221        transform: Affine,
222        paint: &Paint<'a>,
223        draw_mode: &PathDrawMode,
224    ) {
225        self.with_group(|r| {
226            Self::draw_path(r, path, transform, paint, draw_mode);
227        });
228    }
229
230    fn push_clip_path(&mut self, clip_path: &ClipPath) {
231        let clip_id = self
232            .clip_paths
233            .insert_with(clip_path.cache_key(), || CachedClipPath {
234                path: clip_path.path.clone(),
235                fill_rule: clip_path.fill,
236            });
237
238        self.xml.start_element("g");
239        self.xml
240            .write_attribute_fmt("clip-path", format_args!("url(#{clip_id})"));
241    }
242
243    fn push_transparency_group(
244        &mut self,
245        opacity: f32,
246        mask: Option<SoftMask<'a>>,
247        blend_mode: BlendMode,
248    ) {
249        self.push_transparency_group_inner(opacity, mask.map(MaskKind::SoftMask), blend_mode);
250    }
251
252    fn draw_glyph(
253        &mut self,
254        glyph: &Glyph<'a>,
255        transform: Affine,
256        glyph_transform: Affine,
257        paint: &Paint<'a>,
258        draw_mode: &GlyphDrawMode,
259    ) {
260        self.with_group(|r| {
261            Self::draw_glyph(r, glyph, transform, glyph_transform, paint, draw_mode);
262        });
263    }
264
265    fn draw_image(&mut self, image: Image<'a, '_>, mut transform: Affine) {
266        // TODO: Use Self::group
267        match image {
268            Image::Stencil(s) => {
269                s.with_stencil(
270                    |s, paint| {
271                        transform *= Affine::scale_non_uniform(
272                            s.scale_factors.0 as f64,
273                            s.scale_factors.1 as f64,
274                        );
275                        Self::draw_stencil_image(self, s, transform, paint);
276                    },
277                    None,
278                );
279            }
280            Image::Raster(r) => {
281                r.with_rgba(
282                    |rgb, alpha| {
283                        transform *= Affine::scale_non_uniform(
284                            rgb.scale_factors.0 as f64,
285                            rgb.scale_factors.1 as f64,
286                        );
287                        Self::draw_rgba_image(self, rgb, transform, alpha);
288                    },
289                    None,
290                );
291            }
292        }
293    }
294
295    fn pop_clip_path(&mut self) {
296        self.xml.end_element();
297    }
298
299    fn pop_transparency_group(&mut self) {
300        self.xml.end_element();
301    }
302}
303
304impl<'a> SvgRenderer<'a> {
305    pub(crate) fn new(page: &'a Page<'a>) -> Self {
306        Self {
307            xml: XmlWriter::new(Options::default()),
308            outline_glyphs: Deduplicator::new('g'),
309            type3_glyphs: Deduplicator::new('e'),
310            clip_paths: Deduplicator::new('c'),
311            masks: Deduplicator::new('m'),
312            shadings: Deduplicator::new('s'),
313            shading_patterns: Deduplicator::new('v'),
314            tiling_patterns: Deduplicator::new('t'),
315            cur_mask: None,
316            dimensions: page.render_dimensions(),
317            cur_blend_mode: BlendMode::default(),
318        }
319    }
320
321    pub(crate) fn write_header(&mut self, size: (f32, f32)) {
322        self.xml.start_element("svg");
323        self.xml
324            .write_attribute_fmt("viewBox", format_args!("0 0 {} {}", size.0, size.1));
325        self.xml
326            .write_attribute_fmt("width", format_args!("{}", size.0));
327        self.xml
328            .write_attribute_fmt("height", format_args!("{}", size.1));
329        self.xml
330            .write_attribute("xmlns", "http://www.w3.org/2000/svg");
331        self.xml
332            .write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
333    }
334
335    // We need this because we have a small problem. `xmlwriter` doesn't allow us to write sub-streams
336    // of XML while we are writing our main stream. This means that objects that need to be interpreted
337    // (like patterns or mask) all need to be written to the XML in the end. On the other hand, once
338    // we get to `finish` all of the registerd resources must already have been registered. This isn't
339    // the case if masks or patterns use new resources that haven't been registered before. As a result,
340    pub(crate) fn with_dummy(&mut self, f: impl FnOnce(&mut Self)) {
341        let mut old_xml = std::mem::replace(&mut self.xml, XmlWriter::new(Options::default()));
342        f(self);
343        std::mem::swap(&mut self.xml, &mut old_xml);
344    }
345
346    pub(crate) fn finish(mut self) -> String {
347        self.write_glyph_defs();
348        self.write_mask_defs();
349        self.write_clip_path_defs();
350        self.write_shading_defs();
351        self.write_shading_pattern_defs();
352        self.write_tiling_pattern_defs();
353        // Close the `svg` element.
354        self.xml.end_element();
355        self.xml.end_document()
356    }
357}
358
359pub(crate) fn convert_transform(transform: &Affine) -> String {
360    transform
361        .as_coeffs()
362        .iter()
363        .map(|c| (*c as f32).to_string())
364        .collect::<Vec<String>>()
365        .join(" ")
366}
367
368#[derive(Debug, Clone)]
369pub(crate) struct Deduplicator<T> {
370    kind: char,
371    vec: Vec<T>,
372    present: HashMap<u128, Id>,
373}
374
375impl<T> Default for Deduplicator<T> {
376    fn default() -> Self {
377        Self::new('-')
378    }
379}
380
381impl<T> Deduplicator<T> {
382    fn new(kind: char) -> Self {
383        Self {
384            kind,
385            vec: Vec::new(),
386            present: HashMap::new(),
387        }
388    }
389
390    pub(crate) fn contains(&self, hash: u128) -> bool {
391        self.present.contains_key(&hash)
392    }
393
394    pub(crate) fn insert_with<F>(&mut self, hash: u128, f: F) -> Id
395    where
396        F: FnOnce() -> T,
397    {
398        *self.present.entry(hash).or_insert_with(|| {
399            let index = self.vec.len();
400            self.vec.push(f());
401            Id(self.kind, index as u64)
402        })
403    }
404
405    pub(crate) fn insert(&mut self, value: T) -> Id {
406        let index = self.vec.len();
407        self.vec.push(value);
408        Id(self.kind, index as u64)
409    }
410
411    pub(crate) fn iter(&self) -> impl Iterator<Item = (Id, &T)> {
412        self.vec
413            .iter()
414            .enumerate()
415            .map(|(i, v)| (Id(self.kind, i as u64), v))
416    }
417
418    pub(crate) fn is_empty(&self) -> bool {
419        self.vec.is_empty()
420    }
421}
422
423#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
424pub(crate) struct Id(char, u64);
425
426impl Display for Id {
427    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
428        write!(f, "{}{}", self.0, self.1)
429    }
430}
431
432pub(crate) fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
433    let mut state = SipHasher13::new();
434    value.hash(&mut state);
435    state.finish128().as_u128()
436}