Compare commits

...

3 Commits

Author SHA1 Message Date
Martin Šlachta
a6ddd3bcdd
Merge ae187fa9c8412f9e2d332448609b307a66dad1b9 into 0264534928864c7aed0466d670824ac0ce5ca1a8 2025-07-10 18:19:18 +02:00
Said A.
0264534928
Fix regression in reference autocomplete (#6586) 2025-07-10 15:02:23 +00:00
Martin Slachta
ae187fa9c8 SVG Export: Removed groups around every single element to reduce size.
Every element, like path, text, etc., had a group around them, that defined it's
transform. These changes accumulate the transformations of these groups and release
them to the element itself. This reduces the overall size of the exported SVG,
because those group elements can be removed.

Added new SVG path builder using relative coordinates. The previous with
global coordinates is still used for glyph paths. Using relative
coordinates allows to transform the entire element without changing the entire path.
2025-05-31 14:39:25 +02:00
8 changed files with 212 additions and 62 deletions

View File

@ -130,7 +130,14 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool {
return true; return true;
} }
// Start of a reference: "@|" or "@he|". // Start of a reference: "@|".
if ctx.leaf.kind() == SyntaxKind::Text && ctx.before.ends_with("@") {
ctx.from = ctx.cursor;
ctx.label_completions();
return true;
}
// An existing reference: "@he|".
if ctx.leaf.kind() == SyntaxKind::RefMarker { if ctx.leaf.kind() == SyntaxKind::RefMarker {
ctx.from = ctx.leaf.offset() + 1; ctx.from = ctx.leaf.offset() + 1;
ctx.label_completions(); ctx.label_completions();
@ -1644,6 +1651,19 @@ mod tests {
test_with_doc(world, pos, doc.as_ref()) test_with_doc(world, pos, doc.as_ref())
} }
#[track_caller]
fn test_with_addition(
initial_text: &str,
addition: &str,
pos: impl FilePos,
) -> Response {
let mut world = TestWorld::new(initial_text);
let doc = typst::compile(&world).output.ok();
let end = world.main.text().len();
world.main.edit(end..end, addition);
test_with_doc(&world, pos, doc.as_ref())
}
#[track_caller] #[track_caller]
fn test_with_doc( fn test_with_doc(
world: impl WorldLike, world: impl WorldLike,
@ -1709,15 +1729,24 @@ mod tests {
.must_exclude(["bib"]); .must_exclude(["bib"]);
} }
#[test]
fn test_autocomplete_ref_function() {
test_with_addition("x<test>", " #ref(<)", -2).must_include(["test"]);
}
#[test]
fn test_autocomplete_ref_shorthand() {
test_with_addition("x<test>", " @", -1).must_include(["test"]);
}
#[test]
fn test_autocomplete_ref_shorthand_with_partial_identifier() {
test_with_addition("x<test>", " @te", -1).must_include(["test"]);
}
#[test] #[test]
fn test_autocomplete_ref_identical_labels_returns_single_completion() { fn test_autocomplete_ref_identical_labels_returns_single_completion() {
let mut world = TestWorld::new("x<test> y<test>"); let result = test_with_addition("x<test> y<test>", " @t", -1);
let doc = typst::compile(&world).output.ok();
let end = world.main.text().len();
world.main.edit(end..end, " @t");
let result = test_with_doc(&world, -1, doc.as_ref());
let completions = result.completions(); let completions = result.completions();
let label_count = let label_count =
completions.iter().filter(|c| c.kind == CompletionKind::Label).count(); completions.iter().filter(|c| c.kind == CompletionKind::Label).count();

View File

@ -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");

View File

@ -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,
@ -200,10 +199,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 +218,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 +228,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 +243,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 +256,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 +375,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 +427,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();

View File

@ -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})"));

View File

@ -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
} }

View File

@ -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)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

View 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)
]
)
]
]
)
]
]