mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Merge 397f4871ff5b14998333e0dad3a720ac6ba7047e into 9b09146a6b5e936966ed7ee73bce9dd2df3810ae
This commit is contained in:
commit
e14de2ec43
@ -2,7 +2,7 @@ use base64::Engine;
|
||||
use ecow::{eco_format, EcoString};
|
||||
use image::{codecs::png::PngEncoder, ImageEncoder};
|
||||
use typst_library::foundations::Smart;
|
||||
use typst_library::layout::{Abs, Axes};
|
||||
use typst_library::layout::{Abs, Axes, Transform};
|
||||
use typst_library::visualize::{
|
||||
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
|
||||
};
|
||||
@ -11,10 +11,17 @@ use crate::SVGRenderer;
|
||||
|
||||
impl SVGRenderer {
|
||||
/// 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);
|
||||
self.xml.start_element("image");
|
||||
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("height", &size.y.to_pt());
|
||||
self.xml.write_attribute("preserveAspectRatio", "none");
|
||||
|
@ -9,7 +9,6 @@ use std::collections::HashMap;
|
||||
use std::fmt::{self, Display, Formatter, Write};
|
||||
|
||||
use ecow::EcoString;
|
||||
use ttf_parser::OutlineBuilder;
|
||||
use typst_library::layout::{
|
||||
Abs, Frame, FrameItem, FrameKind, GroupItem, Page, PagedDocument, Point, Ratio, Size,
|
||||
Transform,
|
||||
@ -211,12 +210,6 @@ impl SVGRenderer {
|
||||
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 {
|
||||
FrameItem::Group(group) => {
|
||||
self.render_group(state.pre_translate(*pos), group)
|
||||
@ -227,12 +220,12 @@ impl SVGRenderer {
|
||||
FrameItem::Shape(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::Tag(_) => unreachable!(),
|
||||
};
|
||||
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
self.xml.end_element();
|
||||
@ -244,7 +237,7 @@ impl SVGRenderer {
|
||||
let state = match group.frame.kind() {
|
||||
FrameKind::Soft => state.pre_concat(group.transform),
|
||||
FrameKind::Hard => state
|
||||
.with_transform(Transform::identity())
|
||||
.with_transform(Transform::identity().pre_concat(state.transform))
|
||||
.with_size(group.frame.size()),
|
||||
};
|
||||
|
||||
@ -257,8 +250,9 @@ impl SVGRenderer {
|
||||
|
||||
if let Some(clip_curve) = &group.clip {
|
||||
let hash = hash128(&group);
|
||||
let id =
|
||||
self.clip_paths.insert_with(hash, || shape::convert_curve(clip_curve));
|
||||
let id = self
|
||||
.clip_paths
|
||||
.insert_with(hash, || shape::convert_curve(&state.transform, clip_curve));
|
||||
self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
|
||||
}
|
||||
|
||||
@ -375,18 +369,31 @@ impl Display for SvgMatrix {
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for SVG path.
|
||||
struct SvgPathBuilder(pub EcoString, pub Ratio);
|
||||
/// A builder for SVG path using relative coordinates.
|
||||
struct SvgRelativePathBuilder(pub EcoString, pub Ratio, pub Point);
|
||||
|
||||
impl SvgPathBuilder {
|
||||
fn with_scale(scale: Ratio) -> Self {
|
||||
Self(EcoString::new(), scale)
|
||||
impl SvgRelativePathBuilder {
|
||||
fn with_translate(pos: Point) -> Self {
|
||||
// add initial M node to transform the entire path
|
||||
Self(
|
||||
EcoString::from(format!("M {} {}", pos.x.to_pt(), pos.y.to_pt())),
|
||||
Ratio::one(),
|
||||
Point::zero(),
|
||||
)
|
||||
}
|
||||
|
||||
fn scale(&self) -> f32 {
|
||||
self.1.get() as f32
|
||||
}
|
||||
|
||||
fn map_x(&self, x: f32) -> f32 {
|
||||
x * self.scale() - self.2.x.to_pt() as f32
|
||||
}
|
||||
|
||||
fn map_y(&self, y: f32) -> f32 {
|
||||
y * self.scale() - self.2.y.to_pt() as f32
|
||||
}
|
||||
|
||||
/// Create a rectangle path. The rectangle is created with the top-left
|
||||
/// corner at (0, 0). The width and height are the size of the rectangle.
|
||||
fn rect(&mut self, width: f32, height: f32) {
|
||||
@ -406,27 +413,85 @@ impl SvgPathBuilder {
|
||||
sweep_flag: u32,
|
||||
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!(
|
||||
&mut self.0,
|
||||
"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,
|
||||
"a {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} "
|
||||
)
|
||||
.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.0, "m {x} {y} ").unwrap();
|
||||
}
|
||||
|
||||
self.2 = Point::new(Abs::pt((x * scale) as f64), Abs::pt((y * scale) as f64));
|
||||
}
|
||||
|
||||
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.0, "l {_x} {_y} ").unwrap();
|
||||
} else if _x != 0.0 {
|
||||
write!(&mut self.0, "h {_x} ").unwrap();
|
||||
} else if _y != 0.0 {
|
||||
write!(&mut self.0, "v {_y} ").unwrap();
|
||||
}
|
||||
|
||||
self.2 = Point::new(Abs::pt((x * scale) as f64), Abs::pt((y * scale) as f64));
|
||||
}
|
||||
|
||||
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.0, "{curve}").unwrap();
|
||||
self.2 = Point::new(Abs::pt((x * scale) as f64), Abs::pt((y * scale) as f64));
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
write!(&mut self.0, "Z ").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SvgPathBuilder {
|
||||
impl Default for SvgRelativePathBuilder {
|
||||
fn default() -> Self {
|
||||
Self(Default::default(), Ratio::one())
|
||||
Self(Default::default(), Ratio::one(), Point::zero())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
let scale = self.scale();
|
||||
write!(&mut self.0, "M {} {} ", x * scale, y * scale).unwrap();
|
||||
|
@ -1,14 +1,13 @@
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use ttf_parser::OutlineBuilder;
|
||||
use typst_library::foundations::Repr;
|
||||
use typst_library::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform};
|
||||
use typst_library::visualize::{Color, FillRule, Gradient, Paint, RatioOrAngle, Tiling};
|
||||
use typst_utils::hash128;
|
||||
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.
|
||||
/// This is a heuristic value that seems to work well.
|
||||
@ -185,7 +184,7 @@ impl SVGRenderer {
|
||||
let theta2 = dtheta * (i + 1) as f32;
|
||||
|
||||
// Create the path for the segment.
|
||||
let mut builder = SvgPathBuilder::default();
|
||||
let mut builder = SvgRelativePathBuilder::default();
|
||||
builder.move_to(
|
||||
correct_tiling_pos(center.0),
|
||||
correct_tiling_pos(center.1),
|
||||
|
@ -1,12 +1,11 @@
|
||||
use ecow::EcoString;
|
||||
use ttf_parser::OutlineBuilder;
|
||||
use typst_library::layout::{Abs, Ratio, Size, Transform};
|
||||
use typst_library::layout::{Abs, Point, Ratio, Size, Transform};
|
||||
use typst_library::visualize::{
|
||||
Curve, CurveItem, FixedStroke, Geometry, LineCap, LineJoin, Paint, RelativeTo, Shape,
|
||||
};
|
||||
|
||||
use crate::paint::ColorEncode;
|
||||
use crate::{SVGRenderer, State, SvgPathBuilder};
|
||||
use crate::{SVGRenderer, State, SvgRelativePathBuilder};
|
||||
|
||||
impl SVGRenderer {
|
||||
/// 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.end_element();
|
||||
}
|
||||
@ -154,8 +153,10 @@ impl SVGRenderer {
|
||||
|
||||
/// Convert a geometry to an SVG path.
|
||||
#[comemo::memoize]
|
||||
fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
|
||||
let mut builder = SvgPathBuilder::default();
|
||||
fn convert_geometry_to_path(transform: &Transform, geometry: &Geometry) -> EcoString {
|
||||
let mut builder =
|
||||
SvgRelativePathBuilder::with_translate(Point::new(transform.tx, transform.ty));
|
||||
|
||||
match geometry {
|
||||
Geometry::Line(t) => {
|
||||
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;
|
||||
builder.rect(x, y);
|
||||
}
|
||||
Geometry::Curve(p) => return convert_curve(p),
|
||||
Geometry::Curve(p) => return convert_curve(transform, p),
|
||||
};
|
||||
builder.0
|
||||
}
|
||||
|
||||
pub fn convert_curve(curve: &Curve) -> EcoString {
|
||||
let mut builder = SvgPathBuilder::default();
|
||||
pub fn convert_curve(transform: &Transform, curve: &Curve) -> EcoString {
|
||||
let mut builder =
|
||||
SvgRelativePathBuilder::with_translate(Point::new(transform.tx, transform.ty));
|
||||
for item in &curve.0 {
|
||||
match item {
|
||||
CurveItem::Move(m) => builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32),
|
||||
|
@ -11,7 +11,7 @@ use typst_library::visualize::{
|
||||
};
|
||||
use typst_utils::hash128;
|
||||
|
||||
use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder};
|
||||
use crate::{SVGRenderer, State, SvgGlyphPathBuilder, SvgMatrix};
|
||||
|
||||
impl SVGRenderer {
|
||||
/// 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.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;
|
||||
for glyph in &text.glyphs {
|
||||
@ -234,7 +241,7 @@ fn convert_outline_glyph_to_path(
|
||||
id: GlyphId,
|
||||
scale: Ratio,
|
||||
) -> Option<EcoString> {
|
||||
let mut builder = SvgPathBuilder::with_scale(scale);
|
||||
let mut builder = SvgGlyphPathBuilder::with_scale(scale);
|
||||
font.ttf().outline_glyph(id, &mut builder)?;
|
||||
Some(builder.0)
|
||||
}
|
||||
|
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: 246 B |
13
tests/suite/svg/relative-paths.typ
Normal file
13
tests/suite/svg/relative-paths.typ
Normal file
@ -0,0 +1,13 @@
|
||||
--- svg-relative-paths ---
|
||||
#block[
|
||||
#rect(width: 10pt, height: 10pt)
|
||||
#block(inset: 10pt)[
|
||||
#rect(width: 10pt, height: 10pt)
|
||||
#block(inset: 10pt)[
|
||||
#block(inset: 10pt)[
|
||||
#rect(width: 10pt, height: 10pt)
|
||||
#rect(width: 10pt, height: 10pt, radius: 2pt)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user