1use std::hash::{Hash, Hasher};
2use std::sync::{Arc, Mutex};
3
4use comemo::Tracked;
5use ecow::{EcoString, eco_format};
6use rustc_hash::FxHashMap;
7use siphasher::sip128::{Hasher128, SipHasher13};
8use typst_syntax::FileId;
9
10use crate::World;
11use crate::diag::{FileError, LoadError, LoadResult, ReportPos, format_xml_like_error};
12use crate::foundations::Bytes;
13use crate::layout::Axes;
14use crate::visualize::VectorFormat;
15use crate::visualize::image::raster::{ExchangeFormat, RasterFormat};
16use crate::visualize::image::{ImageFormat, determine_format_from_path};
17
18use crate::text::{
19 Font, FontBook, FontFlags, FontStretch, FontStyle, FontVariant, FontWeight,
20};
21
22#[derive(Clone, Hash)]
24pub struct SvgImage(Arc<Repr>);
25
26struct Repr {
28 data: Bytes,
29 size: Axes<f64>,
30 font_hash: u128,
31 tree: usvg::Tree,
32}
33
34impl SvgImage {
35 #[comemo::memoize]
37 #[typst_macros::time(name = "load svg")]
38 pub fn new(data: Bytes) -> LoadResult<SvgImage> {
39 let tree =
40 usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?;
41 Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree })))
42 }
43
44 #[comemo::memoize]
46 #[typst_macros::time(name = "load svg")]
47 pub fn with_fonts_images(
48 data: Bytes,
49 world: Tracked<dyn World + '_>,
50 families: &[&str],
51 svg_file: Option<FileId>,
52 ) -> LoadResult<SvgImage> {
53 let book = world.book();
54 let font_resolver = Mutex::new(FontResolver::new(world, book, families));
55 let image_resolver = Mutex::new(ImageResolver::new(world, svg_file));
56 let tree = usvg::Tree::from_data(
57 &data,
58 &usvg::Options {
59 font_resolver: usvg::FontResolver {
60 select_font: Box::new(|font, db| {
61 font_resolver.lock().unwrap().select_font(font, db)
62 }),
63 select_fallback: Box::new(|c, exclude_fonts, db| {
64 font_resolver.lock().unwrap().select_fallback(
65 c,
66 exclude_fonts,
67 db,
68 )
69 }),
70 },
71 image_href_resolver: usvg::ImageHrefResolver {
72 resolve_data: usvg::ImageHrefResolver::default_data_resolver(),
73 resolve_string: Box::new(|href, _opts| {
74 image_resolver.lock().unwrap().load(href)
75 }),
76 },
77 ..base_options()
78 },
79 )
80 .map_err(format_usvg_error)?;
81 if let Some(err) = image_resolver.into_inner().unwrap().error {
82 return Err(err);
83 }
84 let font_hash = font_resolver.into_inner().unwrap().finish();
85 Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash, tree })))
86 }
87
88 pub fn data(&self) -> &Bytes {
90 &self.0.data
91 }
92
93 pub fn width(&self) -> f64 {
95 self.0.size.x
96 }
97
98 pub fn height(&self) -> f64 {
100 self.0.size.y
101 }
102
103 pub fn tree(&self) -> &usvg::Tree {
105 &self.0.tree
106 }
107}
108
109impl Hash for Repr {
110 fn hash<H: Hasher>(&self, state: &mut H) {
111 self.data.hash(state);
115 self.font_hash.hash(state);
116 }
117}
118
119fn base_options() -> usvg::Options<'static> {
122 usvg::Options {
123 font_family: String::new(),
125
126 resources_dir: None,
133 image_href_resolver: usvg::ImageHrefResolver {
134 resolve_data: usvg::ImageHrefResolver::default_data_resolver(),
135 resolve_string: Box::new(|_, _| None),
136 },
137
138 ..Default::default()
139 }
140}
141
142fn tree_size(tree: &usvg::Tree) -> Axes<f64> {
144 Axes::new(tree.size().width() as f64, tree.size().height() as f64)
145}
146
147fn format_usvg_error(error: usvg::Error) -> LoadError {
149 let error = match error {
150 usvg::Error::NotAnUtf8Str => "file is not valid utf-8",
151 usvg::Error::MalformedGZip => "file is not compressed correctly",
152 usvg::Error::ElementsLimitReached => "file is too large",
153 usvg::Error::InvalidSize => "width, height, or viewbox is invalid",
154 usvg::Error::ParsingFailed(error) => return format_xml_like_error("SVG", error),
155 };
156 LoadError::new(ReportPos::None, "failed to parse SVG", error)
157}
158
159struct FontResolver<'a> {
161 book: &'a FontBook,
163 world: Tracked<'a, dyn World + 'a>,
165 families: &'a [&'a str],
167 to_id: FxHashMap<usize, Option<fontdb::ID>>,
169 from_id: FxHashMap<fontdb::ID, Font>,
171 hasher: SipHasher13,
173}
174
175impl<'a> FontResolver<'a> {
176 fn new(
178 world: Tracked<'a, dyn World + 'a>,
179 book: &'a FontBook,
180 families: &'a [&'a str],
181 ) -> Self {
182 Self {
183 book,
184 world,
185 families,
186 to_id: FxHashMap::default(),
187 from_id: FxHashMap::default(),
188 hasher: SipHasher13::new(),
189 }
190 }
191
192 fn finish(self) -> u128 {
194 self.hasher.finish128().as_u128()
195 }
196}
197
198impl FontResolver<'_> {
199 fn select_font(
201 &mut self,
202 font: &usvg::Font,
203 db: &mut Arc<fontdb::Database>,
204 ) -> Option<fontdb::ID> {
205 let variant = FontVariant {
206 style: font.style().into(),
207 weight: FontWeight::from_number(font.weight()),
208 stretch: font.stretch().into(),
209 };
210
211 font.families()
213 .iter()
214 .filter_map(|family| match family {
215 usvg::FontFamily::Named(named) => Some(named.as_str()),
216 _ => None,
218 })
219 .chain(self.families.iter().copied())
220 .filter_map(|named| self.book.select(&named.to_lowercase(), variant))
221 .find_map(|index| self.get_or_load(index, db))
222 }
223
224 fn select_fallback(
226 &mut self,
227 c: char,
228 exclude_fonts: &[fontdb::ID],
229 db: &mut Arc<fontdb::Database>,
230 ) -> Option<fontdb::ID> {
231 let like = exclude_fonts
233 .first()
234 .and_then(|first| self.from_id.get(first))
235 .map(|font| font.info());
236
237 let variant = like.map(|info| info.variant).unwrap_or_default();
242
243 let index =
245 self.book.select_fallback(like, variant, c.encode_utf8(&mut [0; 4]))?;
246
247 self.get_or_load(index, db)
248 }
249
250 fn get_or_load(
253 &mut self,
254 index: usize,
255 db: &mut Arc<fontdb::Database>,
256 ) -> Option<fontdb::ID> {
257 self.to_id
258 .get(&index)
259 .copied()
260 .unwrap_or_else(|| self.load(index, db))
261 }
262
263 fn load(
266 &mut self,
267 index: usize,
268 db: &mut Arc<fontdb::Database>,
269 ) -> Option<fontdb::ID> {
270 let font = self.world.font(index)?;
271 let info = font.info();
272 let variant = info.variant;
273 let id = Arc::make_mut(db).push_face_info(fontdb::FaceInfo {
274 id: fontdb::ID::dummy(),
275 source: fontdb::Source::Binary(Arc::new(font.data().clone())),
276 index: font.index(),
277 families: vec![(
278 info.family.clone(),
279 ttf_parser::Language::English_UnitedStates,
280 )],
281 post_script_name: String::new(),
282 style: match variant.style {
283 FontStyle::Normal => fontdb::Style::Normal,
284 FontStyle::Italic => fontdb::Style::Italic,
285 FontStyle::Oblique => fontdb::Style::Oblique,
286 },
287 weight: fontdb::Weight(variant.weight.to_number()),
288 stretch: match variant.stretch.round() {
289 FontStretch::ULTRA_CONDENSED => ttf_parser::Width::UltraCondensed,
290 FontStretch::EXTRA_CONDENSED => ttf_parser::Width::ExtraCondensed,
291 FontStretch::CONDENSED => ttf_parser::Width::Condensed,
292 FontStretch::SEMI_CONDENSED => ttf_parser::Width::SemiCondensed,
293 FontStretch::NORMAL => ttf_parser::Width::Normal,
294 FontStretch::SEMI_EXPANDED => ttf_parser::Width::SemiExpanded,
295 FontStretch::EXPANDED => ttf_parser::Width::Expanded,
296 FontStretch::EXTRA_EXPANDED => ttf_parser::Width::ExtraExpanded,
297 FontStretch::ULTRA_EXPANDED => ttf_parser::Width::UltraExpanded,
298 _ => unreachable!(),
299 },
300 monospaced: info.flags.contains(FontFlags::MONOSPACE),
301 });
302
303 font.hash(&mut self.hasher);
304
305 self.to_id.insert(index, Some(id));
306 self.from_id.insert(id, font);
307
308 Some(id)
309 }
310}
311
312struct ImageResolver<'a> {
315 world: Tracked<'a, dyn World + 'a>,
317 svg_file: Option<FileId>,
319 error: Option<LoadError>,
321}
322
323impl<'a> ImageResolver<'a> {
324 fn new(world: Tracked<'a, dyn World + 'a>, svg_file: Option<FileId>) -> Self {
325 Self { world, svg_file, error: None }
326 }
327
328 fn load(&mut self, href: &str) -> Option<usvg::ImageKind> {
332 if self.error.is_some() {
333 return None;
334 }
335 match self.load_or_error(href) {
336 Ok(image) => Some(image),
337 Err(err) => {
338 self.error = Some(LoadError::new(
339 ReportPos::None,
340 eco_format!("failed to load linked image {} in SVG", href),
341 err,
342 ));
343 None
344 }
345 }
346 }
347
348 fn load_or_error(&mut self, href: &str) -> Result<usvg::ImageKind, EcoString> {
350 let href = href.strip_prefix("file://").unwrap_or(href);
352
353 if href.starts_with("/") {
356 return Err("absolute paths are not allowed".into());
357 }
358
359 if let Some(pos) = href.find("://") {
361 let scheme = &href[..pos];
362 if scheme
363 .chars()
364 .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
365 {
366 return Err("URLs are not allowed".into());
367 }
368 }
369
370 if self.svg_file.is_none() {
372 return Err("cannot access file system from here".into());
373 }
374 let href_file = self.svg_file.unwrap().join(href);
376
377 match self.world.file(href_file) {
379 Ok(bytes) => {
380 let arc_data = Arc::new(bytes.to_vec());
381 let format = match determine_format_from_path(href) {
382 Some(format) => Some(format),
383 None => ImageFormat::detect(&arc_data),
384 };
385 match format {
386 Some(ImageFormat::Vector(vector_format)) => match vector_format {
387 VectorFormat::Svg => {
388 Err("SVG images are not supported yet".into())
389 }
390 VectorFormat::Pdf => {
391 Err("PDF documents are not supported".into())
392 }
393 },
394 Some(ImageFormat::Raster(raster_format)) => match raster_format {
395 RasterFormat::Exchange(exchange_format) => {
396 match exchange_format {
397 ExchangeFormat::Gif => Ok(usvg::ImageKind::GIF(arc_data)),
398 ExchangeFormat::Jpg => {
399 Ok(usvg::ImageKind::JPEG(arc_data))
400 }
401 ExchangeFormat::Png => Ok(usvg::ImageKind::PNG(arc_data)),
402 ExchangeFormat::Webp => {
403 Ok(usvg::ImageKind::WEBP(arc_data))
404 }
405 }
406 }
407 RasterFormat::Pixel(_) => {
408 Err("pixel formats are not supported".into())
409 }
410 },
411 None => Err("unknown image format".into()),
412 }
413 }
414 Err(err) => Err(match err {
416 FileError::NotFound(path) => {
417 eco_format!("file not found, searched at {}", path.display())
418 }
419 FileError::AccessDenied => "access denied".into(),
420 FileError::IsDirectory => "is a directory".into(),
421 FileError::Other(Some(msg)) => msg,
422 FileError::Other(None) => "unspecified error".into(),
423 _ => eco_format!("unexpected error: {}", err),
424 }),
425 }
426 }
427}