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