1mod image;
4mod paint;
5mod shape;
6mod text;
7
8pub use image::{convert_image_scaling, convert_image_to_base64_url};
9use rustc_hash::FxHashMap;
10use typst_library::introspection::Introspector;
11use typst_library::model::Destination;
12
13use std::fmt::{self, Display, Formatter, Write};
14
15use ecow::EcoString;
16use typst_library::layout::{
17 Abs, Frame, FrameItem, FrameKind, GroupItem, Page, PagedDocument, Point, Ratio, Size,
18 Transform,
19};
20use typst_library::visualize::{Geometry, Gradient, Tiling};
21use typst_utils::hash128;
22use xmlwriter::XmlWriter;
23
24use crate::paint::{GradientRef, SVGSubGradient, TilingRef};
25use crate::text::RenderedGlyph;
26
27#[typst_macros::time(name = "svg")]
29pub fn svg(page: &Page) -> String {
30 let mut renderer = SVGRenderer::new();
31 renderer.write_header(page.frame.size());
32
33 let state = State::new(page.frame.size(), Transform::identity());
34 renderer.render_page(&state, Transform::identity(), page);
35 renderer.finalize()
36}
37
38#[typst_macros::time(name = "svg frame")]
40pub fn svg_frame(frame: &Frame) -> String {
41 let mut renderer = SVGRenderer::new();
42 renderer.write_header(frame.size());
43
44 let state = State::new(frame.size(), Transform::identity());
45 renderer.render_frame(&state, frame);
46 renderer.finalize()
47}
48
49#[typst_macros::time(name = "svg html frame")]
51pub fn svg_html_frame(
52 frame: &Frame,
53 text_size: Abs,
54 id: Option<&str>,
55 link_points: &[(Point, EcoString)],
56 introspector: &Introspector,
57) -> String {
58 let mut renderer = SVGRenderer::with_options(
59 xmlwriter::Options {
60 indent: xmlwriter::Indent::None,
61 ..Default::default()
62 },
63 Some(introspector),
64 );
65 renderer.write_header_with_custom_attrs(frame.size(), |xml| {
66 if let Some(id) = id {
67 xml.write_attribute("id", id);
68 }
69 xml.write_attribute("class", "typst-frame");
70 xml.write_attribute_fmt(
71 "style",
72 format_args!(
73 "overflow: visible; width: {}em; height: {}em;",
74 frame.width() / text_size,
75 frame.height() / text_size,
76 ),
77 );
78 });
79
80 let state = State::new(frame.size(), Transform::identity());
81 renderer.render_frame(&state, frame);
82
83 for (pos, id) in link_points {
84 renderer.render_link_point(*pos, id);
85 }
86
87 renderer.finalize()
88}
89
90pub fn svg_merged(document: &PagedDocument, padding: Abs) -> String {
94 let width = 2.0 * padding
95 + document
96 .pages
97 .iter()
98 .map(|page| page.frame.width())
99 .max()
100 .unwrap_or_default();
101 let height = padding
102 + document
103 .pages
104 .iter()
105 .map(|page| page.frame.height() + padding)
106 .sum::<Abs>();
107
108 let mut renderer = SVGRenderer::new();
109 renderer.write_header(Size::new(width, height));
110
111 let [x, mut y] = [padding; 2];
112 for page in &document.pages {
113 let ts = Transform::translate(x, y);
114 let state = State::new(page.frame.size(), Transform::identity());
115 renderer.render_page(&state, ts, page);
116 y += page.frame.height() + padding;
117 }
118
119 renderer.finalize()
120}
121
122struct SVGRenderer<'a> {
124 xml: XmlWriter,
126 introspector: Option<&'a Introspector>,
128 glyphs: Deduplicator<RenderedGlyph>,
130 clip_paths: Deduplicator<EcoString>,
135 gradient_refs: Deduplicator<GradientRef>,
141 tiling_refs: Deduplicator<TilingRef>,
147 gradients: Deduplicator<(Gradient, Ratio)>,
154 tilings: Deduplicator<Tiling>,
160 conic_subgradients: Deduplicator<SVGSubGradient>,
162}
163
164#[derive(Copy, Clone)]
166struct State {
167 transform: Transform,
169 size: Size,
171}
172
173impl State {
174 fn new(size: Size, transform: Transform) -> Self {
175 Self { size, transform }
176 }
177
178 fn pre_translate(self, pos: Point) -> Self {
180 self.pre_concat(Transform::translate(pos.x, pos.y))
181 }
182
183 fn pre_concat(self, transform: Transform) -> Self {
185 Self {
186 transform: self.transform.pre_concat(transform),
187 ..self
188 }
189 }
190
191 fn with_size(self, size: Size) -> Self {
193 Self { size, ..self }
194 }
195
196 fn with_transform(self, transform: Transform) -> Self {
198 Self { transform, ..self }
199 }
200}
201
202impl<'a> SVGRenderer<'a> {
203 fn new() -> Self {
205 Self::with_options(Default::default(), None)
206 }
207
208 fn with_options(
210 options: xmlwriter::Options,
211 introspector: Option<&'a Introspector>,
212 ) -> Self {
213 SVGRenderer {
214 xml: XmlWriter::new(options),
215 introspector,
216 glyphs: Deduplicator::new('g'),
217 clip_paths: Deduplicator::new('c'),
218 gradient_refs: Deduplicator::new('g'),
219 gradients: Deduplicator::new('f'),
220 conic_subgradients: Deduplicator::new('s'),
221 tiling_refs: Deduplicator::new('p'),
222 tilings: Deduplicator::new('t'),
223 }
224 }
225
226 fn write_header(&mut self, size: Size) {
229 self.write_header_with_custom_attrs(size, |xml| {
230 xml.write_attribute("class", "typst-doc");
231 });
232 }
233
234 fn write_header_with_custom_attrs(
236 &mut self,
237 size: Size,
238 write_custom_attrs: impl FnOnce(&mut XmlWriter),
239 ) {
240 self.xml.start_element("svg");
241 write_custom_attrs(&mut self.xml);
242 self.xml.write_attribute_fmt(
243 "viewBox",
244 format_args!("0 0 {} {}", size.x.to_pt(), size.y.to_pt()),
245 );
246 self.xml
247 .write_attribute_fmt("width", format_args!("{}pt", size.x.to_pt()));
248 self.xml
249 .write_attribute_fmt("height", format_args!("{}pt", size.y.to_pt()));
250 self.xml.write_attribute("xmlns", "http://www.w3.org/2000/svg");
251 self.xml
252 .write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
253 self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml");
254 }
255
256 fn render_page(&mut self, state: &State, ts: Transform, page: &Page) {
258 if let Some(fill) = page.fill_or_white() {
259 let shape = Geometry::Rect(page.frame.size()).filled(fill);
260 self.render_shape(state, &shape);
261 }
262
263 if !ts.is_identity() {
264 self.xml.start_element("g");
265 self.xml.write_attribute("transform", &SvgMatrix(ts));
266 }
267
268 self.render_frame(state, &page.frame);
269
270 if !ts.is_identity() {
271 self.xml.end_element();
272 }
273 }
274
275 fn render_frame(&mut self, state: &State, frame: &Frame) {
277 self.xml.start_element("g");
278
279 for (pos, item) in frame.items() {
280 let state = state.pre_translate(*pos);
281 match item {
282 FrameItem::Group(group) => self.render_group(&state, group),
283 FrameItem::Text(text) => self.render_text(&state, text),
284 FrameItem::Shape(shape, _) => self.render_shape(&state, shape),
285 FrameItem::Image(image, size, _) => {
286 self.render_image(&state, image, size)
287 }
288 FrameItem::Link(dest, size) => self.render_link(&state, dest, *size),
289 FrameItem::Tag(_) => {}
290 };
291 }
292
293 self.xml.end_element();
294 }
295
296 fn render_group(&mut self, state: &State, group: &GroupItem) {
299 self.xml.start_element("g");
300 self.xml.write_attribute("class", "typst-group");
301
302 let state = match group.frame.kind() {
303 FrameKind::Soft => state.pre_concat(group.transform),
304 FrameKind::Hard => {
305 let transform = state.transform.pre_concat(group.transform);
306 if !transform.is_identity() {
307 self.xml.write_attribute("transform", &SvgMatrix(transform));
308 }
309 state
310 .with_transform(Transform::identity())
311 .with_size(group.frame.size())
312 }
313 };
314
315 if let Some(label) = group.label {
316 self.xml.write_attribute("data-typst-label", &label.resolve());
317 }
318
319 if let Some(clip_curve) = &group.clip {
320 let offset = Point::new(state.transform.tx, state.transform.ty);
321 let hash = hash128(&(&clip_curve, &offset));
322 let id = self
323 .clip_paths
324 .insert_with(hash, || shape::convert_curve(offset, clip_curve));
325 self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
326 }
327
328 self.render_frame(&state, &group.frame);
329 self.xml.end_element();
330 }
331
332 fn render_link(&mut self, state: &State, dest: &Destination, size: Size) {
334 self.xml.start_element("a");
335 if !state.transform.is_identity() {
336 self.xml.write_attribute("transform", &SvgMatrix(state.transform));
337 }
338
339 match dest {
340 Destination::Location(loc) => {
341 if let Some(introspector) = self.introspector
344 && let Some(id) = introspector.html_id(*loc)
345 {
346 self.xml.write_attribute_fmt("href", format_args!("#{id}"));
347 self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
348 }
349 }
350 Destination::Position(_) => {
351 }
353 Destination::Url(url) => {
354 self.xml.write_attribute("href", url.as_str());
355 self.xml.write_attribute("xlink:href", url.as_str());
356 }
357 }
358
359 self.xml.start_element("rect");
360 self.xml
361 .write_attribute_fmt("width", format_args!("{}", size.x.to_pt()));
362 self.xml
363 .write_attribute_fmt("height", format_args!("{}", size.y.to_pt()));
364 self.xml.write_attribute("fill", "transparent");
365 self.xml.write_attribute("stroke", "none");
366 self.xml.end_element();
367
368 self.xml.end_element();
369 }
370
371 fn render_link_point(&mut self, pos: Point, id: &str) {
373 self.xml.start_element("g");
374 self.xml.write_attribute("id", id);
375 self.xml.write_attribute_fmt(
376 "transform",
377 format_args!("translate({} {})", pos.x.to_pt(), pos.y.to_pt()),
378 );
379 self.xml.end_element();
380 }
381
382 fn finalize(mut self) -> String {
384 self.write_glyph_defs();
385 self.write_clip_path_defs();
386 self.write_gradients();
387 self.write_gradient_refs();
388 self.write_subgradients();
389 self.write_tilings();
390 self.write_tiling_refs();
391 self.xml.end_document()
392 }
393
394 fn write_clip_path_defs(&mut self) {
396 if self.clip_paths.is_empty() {
397 return;
398 }
399
400 self.xml.start_element("defs");
401 self.xml.write_attribute("id", "clip-path");
402
403 for (id, path) in self.clip_paths.iter() {
404 self.xml.start_element("clipPath");
405 self.xml.write_attribute("id", &id);
406 self.xml.start_element("path");
407 self.xml.write_attribute("d", &path);
408 self.xml.end_element();
409 self.xml.end_element();
410 }
411
412 self.xml.end_element();
413 }
414}
415
416#[derive(Debug, Clone)]
421struct Deduplicator<T> {
422 kind: char,
423 vec: Vec<(u128, T)>,
424 present: FxHashMap<u128, Id>,
425}
426
427impl<T> Deduplicator<T> {
428 fn new(kind: char) -> Self {
429 Self {
430 kind,
431 vec: Vec::new(),
432 present: FxHashMap::default(),
433 }
434 }
435
436 #[must_use = "returns the index of the inserted value"]
440 fn insert_with<F>(&mut self, hash: u128, f: F) -> Id
441 where
442 F: FnOnce() -> T,
443 {
444 *self.present.entry(hash).or_insert_with(|| {
445 let index = self.vec.len();
446 self.vec.push((hash, f()));
447 Id(self.kind, hash, index)
448 })
449 }
450
451 fn iter(&self) -> impl Iterator<Item = (Id, &T)> {
453 self.vec
454 .iter()
455 .enumerate()
456 .map(|(i, (id, v))| (Id(self.kind, *id, i), v))
457 }
458
459 fn is_empty(&self) -> bool {
461 self.vec.is_empty()
462 }
463}
464
465#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
467struct Id(char, u128, usize);
468
469impl Display for Id {
470 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
471 write!(f, "{}{:0X}", self.0, self.1)
472 }
473}
474
475struct SvgMatrix(Transform);
477
478impl Display for SvgMatrix {
479 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
480 write!(
483 f,
484 "matrix({} {} {} {} {} {})",
485 self.0.sx.get(),
486 self.0.ky.get(),
487 self.0.kx.get(),
488 self.0.sy.get(),
489 self.0.tx.to_pt(),
490 self.0.ty.to_pt()
491 )
492 }
493}
494
495struct SvgPathBuilder {
497 pub path: EcoString,
498 pub scale: Ratio,
499 pub last_close_point: Point,
500 pub last_point: Point,
501}
502
503impl SvgPathBuilder {
504 fn with_translate(pos: Point) -> Self {
505 Self {
507 path: EcoString::from(format!("M {} {}", pos.x.to_pt(), pos.y.to_pt())),
508 scale: Ratio::one(),
509 last_close_point: pos,
510 last_point: Point::zero(),
511 }
512 }
513
514 fn with_scale(scale: Ratio) -> Self {
515 Self {
516 path: EcoString::from("M 0 0"),
517 scale,
518 last_close_point: Point::zero(),
519 last_point: Point::zero(),
520 }
521 }
522
523 fn scale(&self) -> f32 {
524 self.scale.get() as f32
525 }
526
527 fn set_point(&mut self, x: f32, y: f32) {
528 let point = Point::new(
529 Abs::pt(f64::from(x * self.scale())),
530 Abs::pt(f64::from(y * self.scale())),
531 );
532
533 self.last_point = point;
534 }
535
536 fn map_x(&self, x: f32) -> f32 {
537 x * self.scale() - self.last_point.x.to_pt() as f32
538 }
539
540 fn map_y(&self, y: f32) -> f32 {
541 y * self.scale() - self.last_point.y.to_pt() as f32
542 }
543
544 fn rect(&mut self, width: f32, height: f32) {
547 self.move_to(0.0, 0.0);
548 self.line_to(0.0, height);
549 self.line_to(width, height);
550 self.line_to(width, 0.0);
551 self.close();
552 }
553
554 fn arc(
556 &mut self,
557 radius: (f32, f32),
558 x_axis_rot: f32,
559 large_arc_flag: u32,
560 sweep_flag: u32,
561 pos: (f32, f32),
562 ) {
563 let rx = self.map_x(radius.0);
564 let ry = self.map_y(radius.1);
565 let x = self.map_x(pos.0);
566 let y = self.map_y(pos.1);
567 write!(
568 &mut self.path,
569 "a {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} "
570 )
571 .unwrap();
572
573 self.set_point(x, y);
574 }
575
576 fn move_to(&mut self, x: f32, y: f32) {
577 let _x = self.map_x(x);
578 let _y = self.map_y(y);
579 if _x != 0.0 || _y != 0.0 {
580 write!(&mut self.path, "m {_x} {_y} ").unwrap();
581 }
582
583 self.set_point(x, y);
584 self.last_close_point = self.last_point;
585 }
586
587 fn line_to(&mut self, x: f32, y: f32) {
588 let _x = self.map_x(x);
589 let _y = self.map_y(y);
590
591 if _x != 0.0 && _y != 0.0 {
592 write!(&mut self.path, "l {_x} {_y} ").unwrap();
593 } else if _x != 0.0 {
594 write!(&mut self.path, "h {_x} ").unwrap();
595 } else if _y != 0.0 {
596 write!(&mut self.path, "v {_y} ").unwrap();
597 }
598
599 self.set_point(x, y);
600 }
601
602 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
603 let curve = format!(
604 "c {} {} {} {} {} {} ",
605 self.map_x(x1),
606 self.map_y(y1),
607 self.map_x(x2),
608 self.map_y(y2),
609 self.map_x(x),
610 self.map_y(y)
611 );
612 write!(&mut self.path, "{curve}").unwrap();
613 self.set_point(x, y);
614 }
615
616 fn close(&mut self) {
617 write!(&mut self.path, "Z ").unwrap();
618 self.last_point = self.last_close_point;
619 }
620}
621
622impl ttf_parser::OutlineBuilder for SvgPathBuilder {
624 fn move_to(&mut self, x: f32, y: f32) {
625 self.move_to(x, y);
626 }
627
628 fn line_to(&mut self, x: f32, y: f32) {
629 self.line_to(x, y);
630 }
631
632 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
633 let _x1 = self.map_x(x1);
634 let _y1 = self.map_y(y1);
635 let _x = self.map_x(x);
636 let _y = self.map_y(y);
637
638 write!(&mut self.path, "q {_x1} {_y1} {_x} {_y} ").unwrap();
639
640 self.set_point(x, y);
641 }
642
643 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
644 self.curve_to(x1, y1, x2, y2, x, y);
645 }
646
647 fn close(&mut self) {
648 self.close();
649 }
650}
651
652impl Default for SvgPathBuilder {
653 fn default() -> Self {
654 Self {
655 path: Default::default(),
656 scale: Ratio::one(),
657 last_close_point: Point::zero(),
658 last_point: Point::zero(),
659 }
660 }
661}