mirror of
https://github.com/typst/typst
synced 2025-08-17 16:38:31 +08:00
Compare commits
3 Commits
ceb5e76012
...
8c68584074
Author | SHA1 | Date | |
---|---|---|---|
|
8c68584074 | ||
|
9a6268050f | ||
|
ae187fa9c8 |
@ -109,10 +109,7 @@ fn handle(
|
|||||||
styles.chain(&style),
|
styles.chain(&style),
|
||||||
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
|
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
|
||||||
)?;
|
)?;
|
||||||
output.push(HtmlNode::Frame(HtmlFrame {
|
output.push(HtmlNode::Frame(HtmlFrame::new(frame, styles)));
|
||||||
inner: frame,
|
|
||||||
text_size: styles.resolve(TextElem::size),
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
engine.sink.warn(warning!(
|
engine.sink.warn(warning!(
|
||||||
child.span(),
|
child.span(),
|
||||||
|
@ -2,10 +2,11 @@ use std::fmt::{self, Debug, Display, Formatter};
|
|||||||
|
|
||||||
use ecow::{EcoString, EcoVec};
|
use ecow::{EcoString, EcoVec};
|
||||||
use typst_library::diag::{bail, HintedStrResult, StrResult};
|
use typst_library::diag::{bail, HintedStrResult, StrResult};
|
||||||
use typst_library::foundations::{cast, Dict, Repr, Str};
|
use typst_library::foundations::{cast, Dict, Repr, Str, StyleChain};
|
||||||
use typst_library::introspection::{Introspector, Tag};
|
use typst_library::introspection::{Introspector, Tag};
|
||||||
use typst_library::layout::{Abs, Frame};
|
use typst_library::layout::{Abs, Frame};
|
||||||
use typst_library::model::DocumentInfo;
|
use typst_library::model::DocumentInfo;
|
||||||
|
use typst_library::text::TextElem;
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::{PicoStr, ResolvedPicoStr};
|
use typst_utils::{PicoStr, ResolvedPicoStr};
|
||||||
|
|
||||||
@ -279,3 +280,10 @@ pub struct HtmlFrame {
|
|||||||
/// consistently.
|
/// consistently.
|
||||||
pub text_size: Abs,
|
pub text_size: Abs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HtmlFrame {
|
||||||
|
/// Wraps a laid-out frame.
|
||||||
|
pub fn new(inner: Frame, styles: StyleChain) -> Self {
|
||||||
|
Self { inner, text_size: styles.resolve(TextElem::size) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -121,6 +121,7 @@ fn write_children(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
|||||||
let pretty_inside = allows_pretty_inside(element.tag)
|
let pretty_inside = allows_pretty_inside(element.tag)
|
||||||
&& element.children.iter().any(|node| match node {
|
&& element.children.iter().any(|node| match node {
|
||||||
HtmlNode::Element(child) => wants_pretty_around(child.tag),
|
HtmlNode::Element(child) => wants_pretty_around(child.tag),
|
||||||
|
HtmlNode::Frame(_) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -305,14 +306,6 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
|
|||||||
|
|
||||||
/// Encode a laid out frame into the writer.
|
/// Encode a laid out frame into the writer.
|
||||||
fn write_frame(w: &mut Writer, frame: &HtmlFrame) {
|
fn write_frame(w: &mut Writer, frame: &HtmlFrame) {
|
||||||
// FIXME: This string replacement is obviously a hack.
|
let svg = typst_svg::svg_html_frame(&frame.inner, frame.text_size);
|
||||||
let svg = typst_svg::svg_frame(&frame.inner).replace(
|
|
||||||
"<svg class",
|
|
||||||
&format!(
|
|
||||||
"<svg style=\"overflow: visible; width: {}em; height: {}em;\" class",
|
|
||||||
frame.inner.width() / frame.text_size,
|
|
||||||
frame.inner.height() / frame.text_size,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
w.buf.push_str(&svg);
|
w.buf.push_str(&svg);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use base64::Engine;
|
|||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use image::{codecs::png::PngEncoder, ImageEncoder};
|
use image::{codecs::png::PngEncoder, ImageEncoder};
|
||||||
use typst_library::foundations::Smart;
|
use typst_library::foundations::Smart;
|
||||||
use typst_library::layout::{Abs, Axes};
|
use typst_library::layout::{Abs, Axes, Transform};
|
||||||
use typst_library::visualize::{
|
use typst_library::visualize::{
|
||||||
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
|
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
|
||||||
};
|
};
|
||||||
@ -11,10 +11,17 @@ use crate::SVGRenderer;
|
|||||||
|
|
||||||
impl SVGRenderer {
|
impl SVGRenderer {
|
||||||
/// Render an image element.
|
/// Render an image element.
|
||||||
pub(super) fn render_image(&mut self, image: &Image, size: &Axes<Abs>) {
|
pub(super) fn render_image(
|
||||||
|
&mut self,
|
||||||
|
transform: &Transform,
|
||||||
|
image: &Image,
|
||||||
|
size: &Axes<Abs>,
|
||||||
|
) {
|
||||||
let url = convert_image_to_base64_url(image);
|
let url = convert_image_to_base64_url(image);
|
||||||
self.xml.start_element("image");
|
self.xml.start_element("image");
|
||||||
self.xml.write_attribute("xlink:href", &url);
|
self.xml.write_attribute("xlink:href", &url);
|
||||||
|
self.xml.write_attribute("x", &transform.tx.to_pt());
|
||||||
|
self.xml.write_attribute("y", &transform.ty.to_pt());
|
||||||
self.xml.write_attribute("width", &size.x.to_pt());
|
self.xml.write_attribute("width", &size.x.to_pt());
|
||||||
self.xml.write_attribute("height", &size.y.to_pt());
|
self.xml.write_attribute("height", &size.y.to_pt());
|
||||||
self.xml.write_attribute("preserveAspectRatio", "none");
|
self.xml.write_attribute("preserveAspectRatio", "none");
|
||||||
|
@ -11,7 +11,6 @@ use std::collections::HashMap;
|
|||||||
use std::fmt::{self, Display, Formatter, Write};
|
use std::fmt::{self, Display, Formatter, Write};
|
||||||
|
|
||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use ttf_parser::OutlineBuilder;
|
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
Abs, Frame, FrameItem, FrameKind, GroupItem, Page, PagedDocument, Point, Ratio, Size,
|
Abs, Frame, FrameItem, FrameKind, GroupItem, Page, PagedDocument, Point, Ratio, Size,
|
||||||
Transform,
|
Transform,
|
||||||
@ -45,6 +44,30 @@ pub fn svg_frame(frame: &Frame) -> String {
|
|||||||
renderer.finalize()
|
renderer.finalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Export a frame into an SVG suitable for embedding into HTML.
|
||||||
|
#[typst_macros::time(name = "svg html frame")]
|
||||||
|
pub fn svg_html_frame(frame: &Frame, text_size: Abs) -> String {
|
||||||
|
let mut renderer = SVGRenderer::with_options(xmlwriter::Options {
|
||||||
|
indent: xmlwriter::Indent::None,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
renderer.write_header_with_custom_attrs(frame.size(), |xml| {
|
||||||
|
xml.write_attribute("class", "typst-frame");
|
||||||
|
xml.write_attribute_fmt(
|
||||||
|
"style",
|
||||||
|
format_args!(
|
||||||
|
"overflow: visible; width: {}em; height: {}em;",
|
||||||
|
frame.width() / text_size,
|
||||||
|
frame.height() / text_size,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let state = State::new(frame.size(), Transform::identity());
|
||||||
|
renderer.render_frame(state, Transform::identity(), frame);
|
||||||
|
renderer.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
/// Export a document with potentially multiple pages into a single SVG file.
|
/// Export a document with potentially multiple pages into a single SVG file.
|
||||||
///
|
///
|
||||||
/// The padding will be added around and between the individual frames.
|
/// The padding will be added around and between the individual frames.
|
||||||
@ -158,8 +181,13 @@ impl State {
|
|||||||
impl SVGRenderer {
|
impl SVGRenderer {
|
||||||
/// Create a new SVG renderer with empty glyph and clip path.
|
/// Create a new SVG renderer with empty glyph and clip path.
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
Self::with_options(Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new SVG renderer with the given configuration.
|
||||||
|
fn with_options(options: xmlwriter::Options) -> Self {
|
||||||
SVGRenderer {
|
SVGRenderer {
|
||||||
xml: XmlWriter::new(xmlwriter::Options::default()),
|
xml: XmlWriter::new(options),
|
||||||
glyphs: Deduplicator::new('g'),
|
glyphs: Deduplicator::new('g'),
|
||||||
clip_paths: Deduplicator::new('c'),
|
clip_paths: Deduplicator::new('c'),
|
||||||
gradient_refs: Deduplicator::new('g'),
|
gradient_refs: Deduplicator::new('g'),
|
||||||
@ -170,11 +198,22 @@ impl SVGRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the SVG header, including the `viewBox` and `width` and `height`
|
/// Write the default SVG header, including a `typst-doc` class, the
|
||||||
/// attributes.
|
/// `viewBox` and `width` and `height` attributes.
|
||||||
fn write_header(&mut self, size: Size) {
|
fn write_header(&mut self, size: Size) {
|
||||||
|
self.write_header_with_custom_attrs(size, |xml| {
|
||||||
|
xml.write_attribute("class", "typst-doc");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the SVG header with additional attributes and standard attributes.
|
||||||
|
fn write_header_with_custom_attrs(
|
||||||
|
&mut self,
|
||||||
|
size: Size,
|
||||||
|
write_custom_attrs: impl FnOnce(&mut XmlWriter),
|
||||||
|
) {
|
||||||
self.xml.start_element("svg");
|
self.xml.start_element("svg");
|
||||||
self.xml.write_attribute("class", "typst-doc");
|
write_custom_attrs(&mut self.xml);
|
||||||
self.xml.write_attribute_fmt(
|
self.xml.write_attribute_fmt(
|
||||||
"viewBox",
|
"viewBox",
|
||||||
format_args!("0 0 {} {}", size.x.to_pt(), size.y.to_pt()),
|
format_args!("0 0 {} {}", size.x.to_pt(), size.y.to_pt()),
|
||||||
@ -200,10 +239,16 @@ impl SVGRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render a frame with the given transform.
|
/// Render a frame with the given transform.
|
||||||
fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) {
|
fn render_frame(&mut self, mut state: State, ts: Transform, frame: &Frame) {
|
||||||
self.xml.start_element("g");
|
self.xml.start_element("g");
|
||||||
if !ts.is_identity() {
|
if !ts.is_identity() {
|
||||||
self.xml.write_attribute("transform", &SvgMatrix(ts));
|
// apply accumulated transform
|
||||||
|
self.xml.write_attribute(
|
||||||
|
"transform",
|
||||||
|
&SvgMatrix(ts.post_concat(state.transform)),
|
||||||
|
);
|
||||||
|
// reset transform accumulation
|
||||||
|
state = state.with_transform(Transform::identity());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (pos, item) in frame.items() {
|
for (pos, item) in frame.items() {
|
||||||
@ -213,12 +258,6 @@ impl SVGRenderer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let x = pos.x.to_pt();
|
|
||||||
let y = pos.y.to_pt();
|
|
||||||
self.xml.start_element("g");
|
|
||||||
self.xml
|
|
||||||
.write_attribute_fmt("transform", format_args!("translate({x} {y})"));
|
|
||||||
|
|
||||||
match item {
|
match item {
|
||||||
FrameItem::Group(group) => {
|
FrameItem::Group(group) => {
|
||||||
self.render_group(state.pre_translate(*pos), group)
|
self.render_group(state.pre_translate(*pos), group)
|
||||||
@ -229,12 +268,12 @@ impl SVGRenderer {
|
|||||||
FrameItem::Shape(shape, _) => {
|
FrameItem::Shape(shape, _) => {
|
||||||
self.render_shape(state.pre_translate(*pos), shape)
|
self.render_shape(state.pre_translate(*pos), shape)
|
||||||
}
|
}
|
||||||
FrameItem::Image(image, size, _) => self.render_image(image, size),
|
FrameItem::Image(image, size, _) => {
|
||||||
|
self.render_image(&state.pre_translate(*pos).transform, image, size)
|
||||||
|
}
|
||||||
FrameItem::Link(_, _) => unreachable!(),
|
FrameItem::Link(_, _) => unreachable!(),
|
||||||
FrameItem::Tag(_) => unreachable!(),
|
FrameItem::Tag(_) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.xml.end_element();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.xml.end_element();
|
self.xml.end_element();
|
||||||
@ -244,10 +283,8 @@ impl SVGRenderer {
|
|||||||
/// be created.
|
/// be created.
|
||||||
fn render_group(&mut self, state: State, group: &GroupItem) {
|
fn render_group(&mut self, state: State, group: &GroupItem) {
|
||||||
let state = match group.frame.kind() {
|
let state = match group.frame.kind() {
|
||||||
FrameKind::Soft => state.pre_concat(group.transform),
|
FrameKind::Soft => state,
|
||||||
FrameKind::Hard => state
|
FrameKind::Hard => state.with_size(group.frame.size()),
|
||||||
.with_transform(Transform::identity())
|
|
||||||
.with_size(group.frame.size()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.xml.start_element("g");
|
self.xml.start_element("g");
|
||||||
@ -259,8 +296,9 @@ impl SVGRenderer {
|
|||||||
|
|
||||||
if let Some(clip_curve) = &group.clip {
|
if let Some(clip_curve) = &group.clip {
|
||||||
let hash = hash128(&group);
|
let hash = hash128(&group);
|
||||||
let id =
|
let id = self
|
||||||
self.clip_paths.insert_with(hash, || shape::convert_curve(clip_curve));
|
.clip_paths
|
||||||
|
.insert_with(hash, || shape::convert_curve(&state.transform, clip_curve));
|
||||||
self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
|
self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,16 +415,37 @@ impl Display for SvgMatrix {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A builder for SVG path.
|
/// A builder for SVG path using relative coordinates.
|
||||||
struct SvgPathBuilder(pub EcoString, pub Ratio);
|
struct SvgRelativePathBuilder {
|
||||||
|
pub path: EcoString,
|
||||||
|
pub scale: Ratio,
|
||||||
|
pub last_point: Point,
|
||||||
|
}
|
||||||
|
|
||||||
impl SvgPathBuilder {
|
impl SvgRelativePathBuilder {
|
||||||
fn with_scale(scale: Ratio) -> Self {
|
fn with_translate(pos: Point) -> Self {
|
||||||
Self(EcoString::new(), scale)
|
// add initial M node to transform the entire path
|
||||||
|
Self {
|
||||||
|
path: EcoString::from(format!("M {} {}", pos.x.to_pt(), pos.y.to_pt())),
|
||||||
|
scale: Ratio::one(),
|
||||||
|
last_point: Point::zero(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scale(&self) -> f32 {
|
fn scale(&self) -> f32 {
|
||||||
self.1.get() as f32
|
self.scale.get() as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_point(&self) -> Point {
|
||||||
|
self.last_point
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_x(&self, x: f32) -> f32 {
|
||||||
|
x * self.scale() - self.last_point().x.to_pt() as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_y(&self, y: f32) -> f32 {
|
||||||
|
y * self.scale() - self.last_point().y.to_pt() as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a rectangle path. The rectangle is created with the top-left
|
/// Create a rectangle path. The rectangle is created with the top-left
|
||||||
@ -408,27 +467,92 @@ impl SvgPathBuilder {
|
|||||||
sweep_flag: u32,
|
sweep_flag: u32,
|
||||||
pos: (f32, f32),
|
pos: (f32, f32),
|
||||||
) {
|
) {
|
||||||
let scale = self.scale();
|
let rx = self.map_x(radius.0);
|
||||||
|
let ry = self.map_y(radius.1);
|
||||||
|
let x = self.map_x(pos.0);
|
||||||
|
let y = self.map_y(pos.1);
|
||||||
write!(
|
write!(
|
||||||
&mut self.0,
|
&mut self.path,
|
||||||
"A {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} ",
|
"a {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} "
|
||||||
rx = radius.0 * scale,
|
|
||||||
ry = radius.1 * scale,
|
|
||||||
x = pos.0 * scale,
|
|
||||||
y = pos.1 * scale,
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_to(&mut self, x: f32, y: f32) {
|
||||||
|
let scale = self.scale();
|
||||||
|
let _x = self.map_x(x);
|
||||||
|
let _y = self.map_y(y);
|
||||||
|
if _x != 0.0 || _y != 0.0 {
|
||||||
|
write!(&mut self.path, "m {x} {y} ").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_point =
|
||||||
|
Point::new(Abs::pt(f64::from(x * scale)), Abs::pt(f64::from(y * scale)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_to(&mut self, x: f32, y: f32) {
|
||||||
|
let scale = self.scale();
|
||||||
|
let _x = self.map_x(x);
|
||||||
|
let _y = self.map_y(y);
|
||||||
|
|
||||||
|
if _x != 0.0 && _y != 0.0 {
|
||||||
|
write!(&mut self.path, "l {_x} {_y} ").unwrap();
|
||||||
|
} else if _x != 0.0 {
|
||||||
|
write!(&mut self.path, "h {_x} ").unwrap();
|
||||||
|
} else if _y != 0.0 {
|
||||||
|
write!(&mut self.path, "v {_y} ").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_point =
|
||||||
|
Point::new(Abs::pt(f64::from(x * scale)), Abs::pt(f64::from(y * scale)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
|
||||||
|
let scale = self.scale();
|
||||||
|
let curve = format!(
|
||||||
|
"c {} {} {} {} {} {} ",
|
||||||
|
self.map_x(x1),
|
||||||
|
self.map_y(y1),
|
||||||
|
self.map_x(x2),
|
||||||
|
self.map_y(y2),
|
||||||
|
self.map_x(x),
|
||||||
|
self.map_y(y)
|
||||||
|
);
|
||||||
|
write!(&mut self.path, "{curve}").unwrap();
|
||||||
|
self.last_point =
|
||||||
|
Point::new(Abs::pt(f64::from(x * scale)), Abs::pt(f64::from(y * scale)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self) {
|
||||||
|
write!(&mut self.path, "Z ").unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SvgPathBuilder {
|
impl Default for SvgRelativePathBuilder {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(Default::default(), Ratio::one())
|
Self {
|
||||||
|
path: Default::default(),
|
||||||
|
scale: Ratio::one(),
|
||||||
|
last_point: Point::zero(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A builder for SVG path. This is used to build the path for a glyph.
|
/// A builder for SVG path. This is used to build the path for a glyph.
|
||||||
impl ttf_parser::OutlineBuilder for SvgPathBuilder {
|
struct SvgGlyphPathBuilder(pub EcoString, pub Ratio);
|
||||||
|
|
||||||
|
impl SvgGlyphPathBuilder {
|
||||||
|
fn with_scale(scale: Ratio) -> Self {
|
||||||
|
Self(EcoString::new(), scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scale(&self) -> f32 {
|
||||||
|
self.1.get() as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A builder for SVG path. This is used to build the path for a glyph.
|
||||||
|
impl ttf_parser::OutlineBuilder for SvgGlyphPathBuilder {
|
||||||
fn move_to(&mut self, x: f32, y: f32) {
|
fn move_to(&mut self, x: f32, y: f32) {
|
||||||
let scale = self.scale();
|
let scale = self.scale();
|
||||||
write!(&mut self.0, "M {} {} ", x * scale, y * scale).unwrap();
|
write!(&mut self.0, "M {} {} ", x * scale, y * scale).unwrap();
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
use std::f32::consts::TAU;
|
use std::f32::consts::TAU;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use ttf_parser::OutlineBuilder;
|
|
||||||
use typst_library::foundations::Repr;
|
use typst_library::foundations::Repr;
|
||||||
use typst_library::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform};
|
use typst_library::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform};
|
||||||
use typst_library::visualize::{Color, FillRule, Gradient, Paint, RatioOrAngle, Tiling};
|
use typst_library::visualize::{Color, FillRule, Gradient, Paint, RatioOrAngle, Tiling};
|
||||||
use typst_utils::hash128;
|
use typst_utils::hash128;
|
||||||
use xmlwriter::XmlWriter;
|
use xmlwriter::XmlWriter;
|
||||||
|
|
||||||
use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder};
|
use crate::{Id, SVGRenderer, State, SvgMatrix, SvgRelativePathBuilder};
|
||||||
|
|
||||||
/// The number of segments in a conic gradient.
|
/// The number of segments in a conic gradient.
|
||||||
/// This is a heuristic value that seems to work well.
|
/// This is a heuristic value that seems to work well.
|
||||||
@ -185,7 +184,7 @@ impl SVGRenderer {
|
|||||||
let theta2 = dtheta * (i + 1) as f32;
|
let theta2 = dtheta * (i + 1) as f32;
|
||||||
|
|
||||||
// Create the path for the segment.
|
// Create the path for the segment.
|
||||||
let mut builder = SvgPathBuilder::default();
|
let mut builder = SvgRelativePathBuilder::default();
|
||||||
builder.move_to(
|
builder.move_to(
|
||||||
correct_tiling_pos(center.0),
|
correct_tiling_pos(center.0),
|
||||||
correct_tiling_pos(center.1),
|
correct_tiling_pos(center.1),
|
||||||
@ -227,7 +226,7 @@ impl SVGRenderer {
|
|||||||
|
|
||||||
// Add the path to the pattern.
|
// Add the path to the pattern.
|
||||||
self.xml.start_element("path");
|
self.xml.start_element("path");
|
||||||
self.xml.write_attribute("d", &builder.0);
|
self.xml.write_attribute("d", &builder.path);
|
||||||
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
|
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
|
||||||
self.xml
|
self.xml
|
||||||
.write_attribute_fmt("stroke", format_args!("url(#{id})"));
|
.write_attribute_fmt("stroke", format_args!("url(#{id})"));
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use ttf_parser::OutlineBuilder;
|
use typst_library::layout::{Abs, Point, Ratio, Size, Transform};
|
||||||
use typst_library::layout::{Abs, Ratio, Size, Transform};
|
|
||||||
use typst_library::visualize::{
|
use typst_library::visualize::{
|
||||||
Curve, CurveItem, FixedStroke, Geometry, LineCap, LineJoin, Paint, RelativeTo, Shape,
|
Curve, CurveItem, FixedStroke, Geometry, LineCap, LineJoin, Paint, RelativeTo, Shape,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::paint::ColorEncode;
|
use crate::paint::ColorEncode;
|
||||||
use crate::{SVGRenderer, State, SvgPathBuilder};
|
use crate::{SVGRenderer, State, SvgRelativePathBuilder};
|
||||||
|
|
||||||
impl SVGRenderer {
|
impl SVGRenderer {
|
||||||
/// Render a shape element.
|
/// Render a shape element.
|
||||||
@ -33,7 +32,7 @@ impl SVGRenderer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = convert_geometry_to_path(&shape.geometry);
|
let path = convert_geometry_to_path(&state.transform, &shape.geometry);
|
||||||
self.xml.write_attribute("d", &path);
|
self.xml.write_attribute("d", &path);
|
||||||
self.xml.end_element();
|
self.xml.end_element();
|
||||||
}
|
}
|
||||||
@ -154,8 +153,10 @@ impl SVGRenderer {
|
|||||||
|
|
||||||
/// Convert a geometry to an SVG path.
|
/// Convert a geometry to an SVG path.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
|
fn convert_geometry_to_path(transform: &Transform, geometry: &Geometry) -> EcoString {
|
||||||
let mut builder = SvgPathBuilder::default();
|
let mut builder =
|
||||||
|
SvgRelativePathBuilder::with_translate(Point::new(transform.tx, transform.ty));
|
||||||
|
|
||||||
match geometry {
|
match geometry {
|
||||||
Geometry::Line(t) => {
|
Geometry::Line(t) => {
|
||||||
builder.move_to(0.0, 0.0);
|
builder.move_to(0.0, 0.0);
|
||||||
@ -166,13 +167,14 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
|
|||||||
let y = rect.y.to_pt() as f32;
|
let y = rect.y.to_pt() as f32;
|
||||||
builder.rect(x, y);
|
builder.rect(x, y);
|
||||||
}
|
}
|
||||||
Geometry::Curve(p) => return convert_curve(p),
|
Geometry::Curve(p) => return convert_curve(transform, p),
|
||||||
};
|
};
|
||||||
builder.0
|
builder.path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_curve(curve: &Curve) -> EcoString {
|
pub fn convert_curve(transform: &Transform, curve: &Curve) -> EcoString {
|
||||||
let mut builder = SvgPathBuilder::default();
|
let mut builder =
|
||||||
|
SvgRelativePathBuilder::with_translate(Point::new(transform.tx, transform.ty));
|
||||||
for item in &curve.0 {
|
for item in &curve.0 {
|
||||||
match item {
|
match item {
|
||||||
CurveItem::Move(m) => builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32),
|
CurveItem::Move(m) => builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32),
|
||||||
@ -188,5 +190,5 @@ pub fn convert_curve(curve: &Curve) -> EcoString {
|
|||||||
CurveItem::Close => builder.close(),
|
CurveItem::Close => builder.close(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.0
|
builder.path
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ use typst_library::visualize::{
|
|||||||
};
|
};
|
||||||
use typst_utils::hash128;
|
use typst_utils::hash128;
|
||||||
|
|
||||||
use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder};
|
use crate::{SVGRenderer, State, SvgGlyphPathBuilder, SvgMatrix};
|
||||||
|
|
||||||
impl SVGRenderer {
|
impl SVGRenderer {
|
||||||
/// Render a text item. The text is rendered as a group of glyphs. We will
|
/// Render a text item. The text is rendered as a group of glyphs. We will
|
||||||
@ -22,7 +22,14 @@ impl SVGRenderer {
|
|||||||
|
|
||||||
self.xml.start_element("g");
|
self.xml.start_element("g");
|
||||||
self.xml.write_attribute("class", "typst-text");
|
self.xml.write_attribute("class", "typst-text");
|
||||||
self.xml.write_attribute("transform", "scale(1, -1)");
|
self.xml.write_attribute(
|
||||||
|
"transform",
|
||||||
|
&format!(
|
||||||
|
"scale(1, -1) translate({} {})",
|
||||||
|
state.transform.tx.to_pt(),
|
||||||
|
-state.transform.ty.to_pt()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let mut x: f64 = 0.0;
|
let mut x: f64 = 0.0;
|
||||||
let mut y: f64 = 0.0;
|
let mut y: f64 = 0.0;
|
||||||
@ -247,7 +254,7 @@ fn convert_outline_glyph_to_path(
|
|||||||
id: GlyphId,
|
id: GlyphId,
|
||||||
scale: Ratio,
|
scale: Ratio,
|
||||||
) -> Option<EcoString> {
|
) -> Option<EcoString> {
|
||||||
let mut builder = SvgPathBuilder::with_scale(scale);
|
let mut builder = SvgGlyphPathBuilder::with_scale(scale);
|
||||||
font.ttf().outline_glyph(id, &mut builder)?;
|
font.ttf().outline_glyph(id, &mut builder)?;
|
||||||
Some(builder.0)
|
Some(builder.0)
|
||||||
}
|
}
|
||||||
|
11
tests/ref/html/html-frame.html
Normal file
11
tests/ref/html/html-frame.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A rectangle:</p>
|
||||||
|
<svg class="typst-frame" style="overflow: visible; width: 4.5em; height: 3em;" viewBox="0 0 45 30" width="45pt" height="30pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"><g><g transform="translate(-0 -0)"><path class="typst-shape" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 30 L 45 30 L 45 0 Z "/></g></g></svg>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
tests/ref/svg-relative-paths.png
Normal file
BIN
tests/ref/svg-relative-paths.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 794 B |
@ -1,5 +1,6 @@
|
|||||||
// No proper HTML tests here yet because we don't want to test SVG export just
|
--- html-frame html ---
|
||||||
// yet. We'll definitely add tests at some point.
|
A rectangle:
|
||||||
|
#html.frame(rect())
|
||||||
|
|
||||||
--- html-frame-in-layout ---
|
--- html-frame-in-layout ---
|
||||||
// Ensure that HTML frames are transparent in layout. This is less important for
|
// Ensure that HTML frames are transparent in layout. This is less important for
|
||||||
|
22
tests/suite/svg/relative-paths.typ
Normal file
22
tests/suite/svg/relative-paths.typ
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
--- svg-relative-paths ---
|
||||||
|
#block[
|
||||||
|
#rect(width: 10pt, height: 10pt)
|
||||||
|
#block(inset: 10pt)[
|
||||||
|
#rect(width: 10pt, height: 10pt)
|
||||||
|
#rotate(45deg,
|
||||||
|
block(inset: 10pt)[
|
||||||
|
#block(inset: 10pt)[
|
||||||
|
#rect(width: 10pt, height: 10pt)
|
||||||
|
#text("Hello world")
|
||||||
|
#rect(width: 10pt, height: 10pt, radius: 10pt)
|
||||||
|
#rotate(45deg,
|
||||||
|
block(inset: 10pt)[
|
||||||
|
#rect(width: 10pt, height: 10pt, radius: 10pt)
|
||||||
|
#rect(width: 10pt, height: 10pt, radius: 10pt)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user