1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use std::collections::BTreeMap;
use svg2pdf::{usvg, ConversionOptions, PageOptions};
use crate::{
xobject::ExternalXObject, ColorSpace, DictItem, ExternalStream, PdfResources, PdfWarnMsg,
};
/// SVG - wrapper around an `XObject` to allow for more
/// control within the library.
///
/// When placing multiple copies of the same SVG on the
/// same layer, it is better to use the `into_xobject`
/// method to get a reference, rather than a clone
#[derive(Debug, Clone)]
pub struct Svg {}
impl Svg {
/// Parses the SVG string, converts it to a PDF XObject
pub fn parse(
svg_string: &str,
warnings: &mut Vec<PdfWarnMsg>,
) -> Result<ExternalXObject, String> {
Self::parse_with_fonts(svg_string, &BTreeMap::new(), warnings)
}
/// Same as [`Svg::parse`], but `<text>` elements resolve against fonts supplied
/// by the caller, in addition to the system fonts where those exist. On wasm this
/// is the only way to get SVG text rendered at all — there is no system font
/// database to scan there. Map keys are informational; family names are read from
/// the font data itself.
pub fn parse_with_fonts(
svg_string: &str,
fonts: &BTreeMap<String, Vec<u8>>,
warnings: &mut Vec<PdfWarnMsg>,
) -> Result<ExternalXObject, String> {
// Parses the SVG, converts it to a PDF document using the svg2pdf crate,
// parses the resulting PDF again, then extracts the first pages PDF content operations.
// Let's first convert the SVG into an independent chunk.
let mut options = usvg::Options::default();
#[cfg(not(target_arch = "wasm32"))]
options.fontdb_mut().load_system_fonts();
for bytes in fonts.values() {
options.fontdb_mut().load_font_data(bytes.clone());
}
let dpi = 300.0;
let tree = usvg::Tree::from_str(svg_string, &usvg::Options { dpi, ..options })
.map_err(|err| format!("usvg parse: {err}"))?;
let mut co = ConversionOptions::default();
co.compress = false;
co.embed_text = false; // TODO!
let po = PageOptions { dpi };
let pdf_bytes = svg2pdf::to_pdf(&tree, co, po)
.map_err(|err| format!("convert svg tree to pdf: {err}"))?;
let pdf = crate::deserialize::parse_pdf_from_bytes(
&pdf_bytes,
&crate::PdfParseOptions {
fail_on_error: false,
},
warnings,
)
.map_err(|err| format!("convert svg tree to pdf: parse pdf: {err}"))?;
let page = pdf
.pages
.get(0)
.ok_or_else(|| format!("convert svg tree to pdf: no page rendered"))?;
let stream = crate::serialize::translate_operations(
&page.ops,
&crate::serialize::prepare_fonts_for_serialization(&PdfResources::default(), &[], false, warnings).0,
&BTreeMap::new(),
true,
warnings,
);
// Scale the PDF content down to a 1:1 unit square,
// so that it behaves like an image
let sx = 1.0 / page.media_box.width.0;
let sy = 1.0 / page.media_box.height.0;
let dict = [
("Type", DictItem::Name("XObject".into())),
("Subtype", DictItem::Name("Form".into())),
(
"ProcSet",
DictItem::Array(vec![
DictItem::Name("PDF".into()),
DictItem::Name("Text".into()),
DictItem::Name("ImageC".into()),
DictItem::Name("ImageB".into()),
]),
),
(
"Resources",
DictItem::Dict {
map: [(
"ColorSpace".to_string(),
DictItem::Name(ColorSpace::Rgb.as_string().into()),
)]
.into_iter()
.collect(),
},
),
(
"BBox",
DictItem::Array(vec![
DictItem::Real(0.0),
DictItem::Real(0.0),
DictItem::Real(page.media_box.width.0),
DictItem::Real(page.media_box.height.0),
]),
),
(
"Matrix",
DictItem::Array(vec![
DictItem::Real(sx),
DictItem::Real(0.0),
DictItem::Real(0.0),
DictItem::Real(sy),
DictItem::Real(0.0),
DictItem::Real(0.0),
]),
),
];
Ok(ExternalXObject {
stream: ExternalStream {
dict: dict.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
content: stream,
compress: false,
},
width: Some(page.media_box.width.into_px(dpi)),
height: Some(page.media_box.height.into_px(dpi)),
dpi: Some(dpi),
})
}
}