mod image;
mod paint;
mod path;
mod shape;
mod text;
mod write;
use comemo::Tracked;
pub use image::{WebImage, convert_image_scaling};
use indexmap::IndexMap;
use rustc_hash::FxBuildHasher;
use typst_library::model::{Destination, LateLinkResolver};
use std::hash::Hash;
use ecow::EcoString;
use typst_layout::{Page, PagedDocument};
use typst_library::layout::{
Abs, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Sides, Size, Transform,
};
use typst_library::visualize::{Geometry, Gradient, Tiling};
use xmlwriter::XmlWriter;
use crate::paint::{GradientRef, SVGSubGradient, TilingRef};
use crate::text::RenderedGlyph;
use crate::write::{SvgDisplay, SvgElem, SvgTransform, SvgUrl, SvgWrite};
#[typst_macros::time(name = "svg")]
pub fn svg(page: &Page, opts: &SvgOptions) -> String {
let (size, ts) = page_bleed(page, opts);
let mut renderer = SVGRenderer::new();
let mut xml = XmlWriter::new(xml_options(opts.pretty));
let mut svg = svg_header(&mut xml, size);
let state = State::new(size);
renderer.render_page(&mut svg, &state, ts, page);
renderer.finalize(svg);
xml.end_document()
}
#[typst_macros::time(name = "svg in bundle")]
pub fn svg_in_bundle(
page: &Page,
opts: &SvgOptions,
anchors: &[(Point, EcoString)],
link_resolver: Tracked<LateLinkResolver>,
) -> String {
let (size, ts) = page_bleed(page, opts);
let mut renderer = SVGRenderer::with_options(Some(link_resolver));
let mut xml = XmlWriter::new(xml_options(opts.pretty));
let mut svg = svg_header(&mut xml, size);
let state = State::new(size);
renderer.render_page(&mut svg, &state, ts, page);
for (pos, id) in anchors {
renderer.render_anchor(&mut svg, *pos, id);
}
renderer.finalize(svg);
xml.end_document()
}
#[typst_macros::time(name = "svg in html")]
pub fn svg_in_html(
frame: &Frame,
text_size: Abs,
pretty: bool,
id: Option<&str>,
styles: &str,
anchors: &[(Point, EcoString)],
link_resolver: Tracked<LateLinkResolver>,
) -> String {
let mut renderer = SVGRenderer::with_options(Some(link_resolver));
let mut xml = XmlWriter::new(xmlwriter::Options {
indent: xmlwriter::Indent::None,
..xml_options(pretty)
});
let mut svg = svg_header_with_custom_attrs(&mut xml, frame.size(), |svg| {
if let Some(id) = id {
svg.attr("id", id);
}
svg.attr_with("style", |attr| {
attr.push_str("overflow: visible; width: ");
attr.push_num(frame.width() / text_size);
attr.push_str("em; height: ");
attr.push_num(frame.height() / text_size);
attr.push_str("em;");
if !styles.is_empty() {
attr.push_str(" ");
attr.push_str(styles);
}
});
});
let state = State::new(frame.size());
renderer.render_frame(&mut svg, &state, frame);
for (pos, id) in anchors {
renderer.render_anchor(&mut svg, *pos, id);
}
renderer.finalize(svg);
xml.end_document()
}
pub fn svg_merged(document: &PagedDocument, opts: &SvgOptions, gap: Abs) -> String {
let num_gaps = document.pages().len().saturating_sub(1) as f64;
let mut size = Size::new(Abs::zero(), num_gaps * gap);
for page in document.pages() {
let (page_size, _ts) = page_bleed(page, opts);
size.x.set_max(page_size.x);
size.y += page_size.y;
}
let mut renderer = SVGRenderer::new();
let mut xml = XmlWriter::new(xml_options(opts.pretty));
let mut svg = svg_header(&mut xml, size);
let mut y = Abs::zero();
for page in document.pages() {
let (page_size, bleed_ts) = page_bleed(page, opts);
let state = State::new(page_size);
renderer.render_page(
&mut svg,
&state,
Transform::translate(Abs::zero(), y).pre_concat(bleed_ts),
page,
);
y += page_size.y + gap;
}
renderer.finalize(svg);
xml.end_document()
}
fn page_bleed(page: &Page, opts: &SvgOptions) -> (Size, Transform) {
let bleed = if opts.render_bleed { page.bleed } else { Sides::default() };
let size = page.frame.size() + bleed.sum_by_axis();
let ts = Transform::translate(bleed.left, bleed.top);
(size, ts)
}
fn xml_options(pretty: bool) -> xmlwriter::Options {
xmlwriter::Options {
use_single_quote: false,
indent: if pretty {
xmlwriter::Indent::Spaces(2)
} else {
xmlwriter::Indent::None
},
attributes_indent: xmlwriter::Indent::None,
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct SvgOptions {
pub render_bleed: bool,
pub pretty: bool,
}
struct SVGRenderer<'a> {
link_resolver: Option<Tracked<'a, LateLinkResolver<'a>>>,
glyphs: Deduplicator<Option<RenderedGlyph>>,
clip_paths: Deduplicator<EcoString>,
gradients: Deduplicator<(Gradient, Ratio)>,
gradient_refs: Deduplicator<GradientRef>,
conic_subgradients: Deduplicator<SVGSubGradient>,
tilings: Deduplicator<Tiling>,
tiling_refs: Deduplicator<TilingRef>,
}
#[derive(Copy, Clone)]
struct State {
transform: Transform,
size: Size,
}
impl State {
fn new(size: Size) -> Self {
Self { size, transform: Transform::identity() }
}
fn pre_translate(self, pos: Point) -> Self {
self.pre_concat(Transform::translate(pos.x, pos.y))
}
fn pre_concat(self, transform: Transform) -> Self {
Self {
transform: self.transform.pre_concat(transform),
..self
}
}
fn with_size(self, size: Size) -> Self {
Self { size, ..self }
}
fn with_transform(self, transform: Transform) -> Self {
Self { transform, ..self }
}
}
impl<'a> SVGRenderer<'a> {
fn new() -> Self {
Self::with_options(None)
}
fn with_options(link_resolver: Option<Tracked<'a, LateLinkResolver<'a>>>) -> Self {
SVGRenderer {
link_resolver,
glyphs: Deduplicator::new('g'),
clip_paths: Deduplicator::new('c'),
gradients: Deduplicator::new('f'),
gradient_refs: Deduplicator::new('r'),
conic_subgradients: Deduplicator::new('s'),
tilings: Deduplicator::new('t'),
tiling_refs: Deduplicator::new('p'),
}
}
fn render_page(
&mut self,
svg: &mut SvgElem,
state: &State,
ts: Transform,
page: &Page,
) {
let mut svg = svg.lazy_elem("g");
if !ts.is_identity() {
svg.init().attr("transform", SvgTransform(ts));
}
if let Some(fill) = page.fill_or_white() {
let shape =
Geometry::Rect(page.frame.size() + page.bleed.sum_by_axis()).filled(fill);
let state =
&state.pre_translate(Point { x: -page.bleed.left, y: -page.bleed.top });
self.render_shape(svg.lazy(), state, &shape);
}
self.render_frame(svg.lazy(), state, &page.frame);
}
fn render_frame(&mut self, svg: &mut SvgElem, state: &State, frame: &Frame) {
for (pos, item) in frame.items() {
let state = state.pre_translate(*pos);
match item {
FrameItem::Group(group) => self.render_group(svg, &state, group),
FrameItem::Text(text) => self.render_text(svg, &state, text),
FrameItem::Shape(shape, _) => self.render_shape(svg, &state, shape),
FrameItem::Image(image, size, _) => {
self.render_image(svg, &state, image, size)
}
FrameItem::Link(dest, size) => self.render_link(svg, &state, dest, *size),
FrameItem::Tag(_) => {}
};
}
}
fn render_group(&mut self, svg: &mut SvgElem, state: &State, group: &GroupItem) {
let mut svg = svg.lazy_elem("g");
let state = match group.frame.kind() {
FrameKind::Soft => state.pre_concat(group.transform),
FrameKind::Hard => {
svg.init();
let transform = state.transform.pre_concat(group.transform);
if !transform.is_identity() {
svg.init().attr("transform", SvgTransform(transform));
}
state
.with_transform(Transform::identity())
.with_size(group.frame.size())
}
};
if let Some(label) = group.label {
svg.init().attr("data-typst-label", label.resolve());
}
if let Some(clip_curve) = &group.clip {
let offset = Point::new(state.transform.tx, state.transform.ty);
let id = self.clip_paths.insert_with((clip_curve, offset), || {
shape::convert_curve(offset, clip_curve)
});
svg.init().attr("clip-path", SvgUrl(id));
}
self.render_frame(svg.lazy(), &state, &group.frame);
}
fn render_link(
&mut self,
svg: &mut SvgElem,
state: &State,
dest: &Destination,
size: Size,
) {
let mut a = svg.elem("a");
if !state.transform.is_identity() {
a.attr("transform", SvgTransform(state.transform));
}
match dest {
Destination::Url(url) => {
a.attr("href", url.as_str());
a.attr("xlink:href", url.as_str());
}
Destination::Position(_) => {
}
Destination::Location(loc) => {
if let Some(resolver) = self.link_resolver
&& let Some(link) = resolver.resolve(*loc)
&& let Ok(uri) = link.into_relative_uri()
{
a.attr("href", &uri);
a.attr("xlink:href", &uri);
}
}
}
a.elem("rect")
.attr("width", size.x.to_pt())
.attr("height", size.y.to_pt())
.attr("fill", "transparent")
.attr("stroke", "none");
}
fn render_anchor(&mut self, svg: &mut SvgElem, pos: Point, id: &str) {
svg.elem("g")
.attr("id", id)
.attr("transform", SvgTransform(Transform::translate(pos.x, pos.y)));
}
fn finalize(mut self, mut svg: SvgElem) {
self.write_glyph_defs(&mut svg);
self.write_clip_path_defs(&mut svg);
self.write_gradients(&mut svg);
self.write_gradient_refs(&mut svg);
self.write_subgradients(&mut svg);
self.write_tilings(&mut svg);
self.write_tiling_refs(&mut svg);
}
fn write_clip_path_defs(&self, svg: &mut SvgElem) {
if self.clip_paths.is_empty() {
return;
}
let mut defs = svg.elem("defs");
for (id, path) in self.clip_paths.iter() {
defs.elem("clipPath").attr("id", id).with(|svg| {
svg.elem("path").attr("d", path);
});
}
}
}
fn svg_header(xml: &mut XmlWriter, size: Size) -> SvgElem<'_> {
svg_header_with_custom_attrs(xml, size, |_| {})
}
fn svg_header_with_custom_attrs(
xml: &mut XmlWriter,
size: Size,
write_custom_attrs: impl FnOnce(&mut SvgElem),
) -> SvgElem<'_> {
let size = size.max(Size::splat(Abs::pt(1.0)));
let mut svg = SvgElem::new(xml, "svg");
write_custom_attrs(&mut svg);
svg.attr_with("viewBox", |attr| {
attr.push_nums([0.0, 0.0, size.x.to_pt(), size.y.to_pt()])
});
svg.attr_with("width", |attr| {
attr.push_num(size.x.to_pt());
attr.push_str("pt");
});
svg.attr_with("height", |attr| {
attr.push_num(size.y.to_pt());
attr.push_str("pt");
});
svg.attr("xmlns", "http://www.w3.org/2000/svg");
svg.attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
svg.attr("xmlns:h5", "http://www.w3.org/1999/xhtml");
svg
}
#[derive(Debug, Default, Clone)]
struct Deduplicator<T> {
kind: char,
map: IndexMap<u128, T, FxBuildHasher>,
}
impl<T> Deduplicator<T> {
fn new(kind: char) -> Self {
Self { kind, map: IndexMap::default() }
}
#[must_use = "returns the id of the inserted value"]
fn insert_with<K, F>(&mut self, key: K, f: F) -> DedupId
where
K: Hash,
F: FnOnce() -> T,
{
self.insert_with_val(key, f).0
}
#[must_use]
fn insert_with_val<K, F>(&mut self, key: K, f: F) -> (DedupId, &mut T)
where
K: Hash,
F: FnOnce() -> T,
{
let hash = typst_utils::hash128(&key);
let val = self.map.entry(hash).or_insert_with(f);
(DedupId(self.kind, hash), val)
}
fn iter(&self) -> impl Iterator<Item = (DedupId, &T)> {
self.map.iter().map(|(hash, v)| (DedupId(self.kind, *hash), v))
}
fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
struct DedupId(char, u128);
impl SvgDisplay for DedupId {
fn fmt(&self, f: &mut impl SvgWrite) {
let Self(kind, hash) = *self;
f.push_char(kind);
let mut digits = [0; 32];
for (i, byte) in hash.to_be_bytes().into_iter().enumerate() {
digits[2 * i] = to_hex_digit((byte >> 4) & 0x0F);
digits[2 * i + 1] = to_hex_digit(byte & 0x0F);
}
let str = std::str::from_utf8(&digits).unwrap();
f.push_str(str.trim_start_matches('0'));
fn to_hex_digit(nibble: u8) -> u8 {
match nibble {
0..10 => b'0' + nibble,
_ => b'A' + (nibble - 10),
}
}
}
}