mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Add support for COLRv1 emojis (#4371)
This commit is contained in:
parent
9a45d948f9
commit
1110b93564
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -2600,6 +2600,7 @@ dependencies = [
|
||||
"unscanny",
|
||||
"usvg",
|
||||
"wasmi",
|
||||
"xmlwriter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2657,7 +2658,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "typst-dev-assets"
|
||||
version = "0.11.0"
|
||||
source = "git+https://github.com/typst/typst-dev-assets?rev=ee8ae61cca138dc92f9d818fc7f2fc046d0148c5#ee8ae61cca138dc92f9d818fc7f2fc046d0148c5"
|
||||
source = "git+https://github.com/typst/typst-dev-assets?rev=48a924d9de82b631bc775124a69384c8d860db04#48a924d9de82b631bc775124a69384c8d860db04"
|
||||
|
||||
[[package]]
|
||||
name = "typst-docs"
|
||||
|
@ -27,7 +27,7 @@ typst-syntax = { path = "crates/typst-syntax", version = "0.11.0" }
|
||||
typst-timing = { path = "crates/typst-timing", version = "0.11.0" }
|
||||
typst-utils = { path = "crates/typst-utils", version = "0.11.0" }
|
||||
typst-assets = "0.11.0"
|
||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "ee8ae61cca138dc92f9d818fc7f2fc046d0148c5" }
|
||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "48a924d9de82b631bc775124a69384c8d860db04" }
|
||||
az = "1.2"
|
||||
base64 = "0.22"
|
||||
bitflags = { version = "2", features = ["serde"] }
|
||||
|
@ -68,6 +68,7 @@ unicode-script = { workspace = true }
|
||||
unicode-segmentation = { workspace = true }
|
||||
unscanny = { workspace = true }
|
||||
usvg = { workspace = true }
|
||||
xmlwriter = { workspace = true }
|
||||
wasmi = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
@ -2,13 +2,14 @@
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use ecow::EcoString;
|
||||
use ttf_parser::GlyphId;
|
||||
use ttf_parser::{GlyphId, RgbaColor};
|
||||
use usvg::tiny_skia_path;
|
||||
use xmlwriter::XmlWriter;
|
||||
|
||||
use crate::layout::{Abs, Axes, Em, Frame, FrameItem, Point, Size};
|
||||
use crate::layout::{Abs, Axes, Frame, FrameItem, Point, Size};
|
||||
use crate::syntax::Span;
|
||||
use crate::text::{Font, Glyph, Lang, TextItem};
|
||||
use crate::visualize::{Color, Image, Rgb};
|
||||
use crate::text::{Font, Glyph};
|
||||
use crate::visualize::Image;
|
||||
|
||||
/// Tells if a glyph is a color glyph or not in a given font.
|
||||
pub fn is_color_glyph(font: &Font, g: &Glyph) -> bool {
|
||||
@ -33,15 +34,81 @@ pub fn frame_for_glyph(font: &Font, glyph_id: u16) -> Frame {
|
||||
|
||||
if let Some(raster_image) = ttf.glyph_raster_image(glyph_id, u16::MAX) {
|
||||
draw_raster_glyph(&mut frame, font, upem, raster_image);
|
||||
} else if ttf.is_color_glyph(glyph_id) {
|
||||
draw_colr_glyph(&mut frame, upem, ttf, glyph_id);
|
||||
} else if ttf.glyph_svg_image(glyph_id).is_some() {
|
||||
draw_svg_glyph(&mut frame, upem, font, glyph_id);
|
||||
} else if ttf.is_color_glyph(glyph_id) {
|
||||
draw_colr_glyph(&mut frame, font, glyph_id);
|
||||
}
|
||||
|
||||
frame
|
||||
}
|
||||
|
||||
fn draw_colr_glyph(
|
||||
frame: &mut Frame,
|
||||
upem: Abs,
|
||||
ttf: &ttf_parser::Face,
|
||||
glyph_id: GlyphId,
|
||||
) -> Option<()> {
|
||||
let mut svg = XmlWriter::new(xmlwriter::Options::default());
|
||||
|
||||
let width = ttf.global_bounding_box().width() as f64;
|
||||
let height = ttf.global_bounding_box().height() as f64;
|
||||
let x_min = ttf.global_bounding_box().x_min as f64;
|
||||
let y_max = ttf.global_bounding_box().y_max as f64;
|
||||
let tx = -x_min;
|
||||
let ty = -y_max;
|
||||
|
||||
svg.start_element("svg");
|
||||
svg.write_attribute("xmlns", "http://www.w3.org/2000/svg");
|
||||
svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
||||
svg.write_attribute("width", &width);
|
||||
svg.write_attribute("height", &height);
|
||||
svg.write_attribute_fmt("viewBox", format_args!("0 0 {width} {height}"));
|
||||
|
||||
let mut path_buf = String::with_capacity(256);
|
||||
let gradient_index = 1;
|
||||
let clip_path_index = 1;
|
||||
|
||||
svg.start_element("g");
|
||||
svg.write_attribute_fmt(
|
||||
"transform",
|
||||
format_args!("matrix(1 0 0 -1 0 0) matrix(1 0 0 1 {tx} {ty})"),
|
||||
);
|
||||
|
||||
let mut glyph_painter = GlyphPainter {
|
||||
face: ttf,
|
||||
svg: &mut svg,
|
||||
path_buf: &mut path_buf,
|
||||
gradient_index,
|
||||
clip_path_index,
|
||||
palette_index: 0,
|
||||
transform: ttf_parser::Transform::default(),
|
||||
outline_transform: ttf_parser::Transform::default(),
|
||||
transforms_stack: vec![ttf_parser::Transform::default()],
|
||||
};
|
||||
|
||||
ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)
|
||||
.unwrap();
|
||||
svg.end_element();
|
||||
|
||||
let data = svg.end_document().into_bytes();
|
||||
|
||||
let image = Image::new(
|
||||
data.into(),
|
||||
typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let y_shift = Abs::raw(upem.to_raw() - y_max);
|
||||
|
||||
let position = Point::new(Abs::raw(x_min), y_shift);
|
||||
let size = Axes::new(Abs::pt(width), Abs::pt(height));
|
||||
frame.push(position, FrameItem::Image(image, size, Span::detached()));
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Draws a raster glyph in a frame.
|
||||
fn draw_raster_glyph(
|
||||
frame: &mut Frame,
|
||||
@ -74,77 +141,6 @@ fn draw_raster_glyph(
|
||||
frame.push(position, FrameItem::Image(image, size, Span::detached()));
|
||||
}
|
||||
|
||||
/// Draws a COLR glyph in a frame.
|
||||
fn draw_colr_glyph(frame: &mut Frame, font: &Font, glyph_id: GlyphId) {
|
||||
let mut painter = ColrPainter { font, current_glyph: glyph_id, frame };
|
||||
let black = ttf_parser::RgbaColor::new(0, 0, 0, 255);
|
||||
font.ttf().paint_color_glyph(glyph_id, 0, black, &mut painter);
|
||||
}
|
||||
|
||||
/// Draws COLR glyphs in a frame.
|
||||
struct ColrPainter<'f, 't> {
|
||||
/// The frame in which to draw.
|
||||
frame: &'f mut Frame,
|
||||
/// The font of the text.
|
||||
font: &'t Font,
|
||||
/// The glyph that will be drawn the next time `ColrPainter::paint` is called.
|
||||
current_glyph: GlyphId,
|
||||
}
|
||||
|
||||
impl<'f, 't> ttf_parser::colr::Painter<'_> for ColrPainter<'f, 't> {
|
||||
fn outline_glyph(&mut self, glyph_id: GlyphId) {
|
||||
self.current_glyph = glyph_id;
|
||||
}
|
||||
|
||||
fn paint(&mut self, paint: ttf_parser::colr::Paint) {
|
||||
let ttf_parser::colr::Paint::Solid(color) = paint else { return };
|
||||
let color = Color::Rgb(Rgb::new(
|
||||
color.red as f32 / 255.0,
|
||||
color.green as f32 / 255.0,
|
||||
color.blue as f32 / 255.0,
|
||||
color.alpha as f32 / 255.0,
|
||||
));
|
||||
|
||||
self.frame.push(
|
||||
// With images, the position corresponds to the top-left corner, but
|
||||
// in the case of text it matches the baseline-left point. Here, we
|
||||
// move the glyph one unit down to compensate for that.
|
||||
Point::new(Abs::zero(), Abs::pt(self.font.units_per_em())),
|
||||
FrameItem::Text(TextItem {
|
||||
font: self.font.clone(),
|
||||
size: Abs::pt(self.font.units_per_em()),
|
||||
fill: color.into(),
|
||||
stroke: None,
|
||||
lang: Lang::ENGLISH,
|
||||
region: None,
|
||||
text: EcoString::new(),
|
||||
glyphs: vec![Glyph {
|
||||
id: self.current_glyph.0,
|
||||
// Advance is not relevant here as we will draw glyph on top
|
||||
// of each other anyway
|
||||
x_advance: Em::zero(),
|
||||
x_offset: Em::zero(),
|
||||
range: 0..0,
|
||||
span: (Span::detached(), 0),
|
||||
}],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// These are not implemented.
|
||||
fn push_clip(&mut self) {}
|
||||
fn push_clip_box(&mut self, _: ttf_parser::colr::ClipBox) {}
|
||||
fn pop_clip(&mut self) {}
|
||||
fn push_layer(&mut self, _: ttf_parser::colr::CompositeMode) {}
|
||||
fn pop_layer(&mut self) {}
|
||||
fn push_translate(&mut self, _: f32, _: f32) {}
|
||||
fn push_scale(&mut self, _: f32, _: f32) {}
|
||||
fn push_rotate(&mut self, _: f32) {}
|
||||
fn push_skew(&mut self, _: f32, _: f32) {}
|
||||
fn push_transform(&mut self, _: ttf_parser::Transform) {}
|
||||
fn pop_transform(&mut self) {}
|
||||
}
|
||||
|
||||
/// Draws an SVG glyph in a frame.
|
||||
fn draw_svg_glyph(
|
||||
frame: &mut Frame,
|
||||
@ -152,6 +148,8 @@ fn draw_svg_glyph(
|
||||
font: &Font,
|
||||
glyph_id: GlyphId,
|
||||
) -> Option<()> {
|
||||
// TODO: Our current conversion of the SVG table works for Twitter Color Emoji,
|
||||
// but might not work for others. See also: https://github.com/RazrFalcon/resvg/pull/776
|
||||
let mut data = font.ttf().glyph_svg_image(glyph_id)?.data;
|
||||
|
||||
// Decompress SVGZ.
|
||||
@ -266,3 +264,342 @@ fn make_svg_unsized(svg: &mut String) {
|
||||
svg.replace_range(range, "");
|
||||
}
|
||||
}
|
||||
|
||||
struct ColrBuilder<'a>(&'a mut String);
|
||||
|
||||
impl ColrBuilder<'_> {
|
||||
fn finish(&mut self) {
|
||||
if !self.0.is_empty() {
|
||||
self.0.pop(); // remove trailing space
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ttf_parser::OutlineBuilder for ColrBuilder<'_> {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
use std::fmt::Write;
|
||||
write!(self.0, "M {x} {y} ").unwrap()
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
use std::fmt::Write;
|
||||
write!(self.0, "L {x} {y} ").unwrap()
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
|
||||
use std::fmt::Write;
|
||||
write!(self.0, "Q {x1} {y1} {x} {y} ").unwrap()
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
|
||||
use std::fmt::Write;
|
||||
write!(self.0, "C {x1} {y1} {x2} {y2} {x} {y} ").unwrap()
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.0.push_str("Z ")
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: This is only a best-effort translation of COLR into SVG. It's not feature-complete
|
||||
// and it's also not possible to make it feature-complete using just raw SVG features.
|
||||
pub(crate) struct GlyphPainter<'a> {
|
||||
pub(crate) face: &'a ttf_parser::Face<'a>,
|
||||
pub(crate) svg: &'a mut xmlwriter::XmlWriter,
|
||||
pub(crate) path_buf: &'a mut String,
|
||||
pub(crate) gradient_index: usize,
|
||||
pub(crate) clip_path_index: usize,
|
||||
pub(crate) palette_index: u16,
|
||||
pub(crate) transform: ttf_parser::Transform,
|
||||
pub(crate) outline_transform: ttf_parser::Transform,
|
||||
pub(crate) transforms_stack: Vec<ttf_parser::Transform>,
|
||||
}
|
||||
|
||||
impl<'a> GlyphPainter<'a> {
|
||||
fn write_gradient_stops(&mut self, stops: ttf_parser::colr::GradientStopsIter) {
|
||||
for stop in stops {
|
||||
self.svg.start_element("stop");
|
||||
self.svg.write_attribute("offset", &stop.stop_offset);
|
||||
self.write_color_attribute("stop-color", stop.color);
|
||||
let opacity = f32::from(stop.color.alpha) / 255.0;
|
||||
self.svg.write_attribute("stop-opacity", &opacity);
|
||||
self.svg.end_element();
|
||||
}
|
||||
}
|
||||
|
||||
fn write_color_attribute(&mut self, name: &str, color: ttf_parser::RgbaColor) {
|
||||
self.svg.write_attribute_fmt(
|
||||
name,
|
||||
format_args!("rgb({}, {}, {})", color.red, color.green, color.blue),
|
||||
);
|
||||
}
|
||||
|
||||
fn write_transform_attribute(&mut self, name: &str, ts: ttf_parser::Transform) {
|
||||
if ts.is_default() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.svg.write_attribute_fmt(
|
||||
name,
|
||||
format_args!("matrix({} {} {} {} {} {})", ts.a, ts.b, ts.c, ts.d, ts.e, ts.f),
|
||||
);
|
||||
}
|
||||
|
||||
fn write_spread_method_attribute(
|
||||
&mut self,
|
||||
extend: ttf_parser::colr::GradientExtend,
|
||||
) {
|
||||
self.svg.write_attribute(
|
||||
"spreadMethod",
|
||||
match extend {
|
||||
ttf_parser::colr::GradientExtend::Pad => &"pad",
|
||||
ttf_parser::colr::GradientExtend::Repeat => &"repeat",
|
||||
ttf_parser::colr::GradientExtend::Reflect => &"reflect",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn paint_solid(&mut self, color: ttf_parser::RgbaColor) {
|
||||
self.svg.start_element("path");
|
||||
self.write_color_attribute("fill", color);
|
||||
let opacity = f32::from(color.alpha) / 255.0;
|
||||
self.svg.write_attribute("fill-opacity", &opacity);
|
||||
self.write_transform_attribute("transform", self.outline_transform);
|
||||
self.svg.write_attribute("d", self.path_buf);
|
||||
self.svg.end_element();
|
||||
}
|
||||
|
||||
fn paint_linear_gradient(&mut self, gradient: ttf_parser::colr::LinearGradient<'a>) {
|
||||
let gradient_id = format!("lg{}", self.gradient_index);
|
||||
self.gradient_index += 1;
|
||||
|
||||
let gradient_transform = paint_transform(self.outline_transform, self.transform);
|
||||
|
||||
// TODO: We ignore x2, y2. Have to apply them somehow.
|
||||
// TODO: The way spreadMode works in ttf and svg is a bit different. In SVG, the spreadMode
|
||||
// will always be applied based on x1/y1 and x2/y2. However, in TTF the spreadMode will
|
||||
// be applied from the first/last stop. So if we have a gradient with x1=0 x2=1, and
|
||||
// a stop at x=0.4 and x=0.6, then in SVG we will always see a padding, while in ttf
|
||||
// we will see the actual spreadMode. We need to account for that somehow.
|
||||
self.svg.start_element("linearGradient");
|
||||
self.svg.write_attribute("id", &gradient_id);
|
||||
self.svg.write_attribute("x1", &gradient.x0);
|
||||
self.svg.write_attribute("y1", &gradient.y0);
|
||||
self.svg.write_attribute("x2", &gradient.x1);
|
||||
self.svg.write_attribute("y2", &gradient.y1);
|
||||
self.svg.write_attribute("gradientUnits", &"userSpaceOnUse");
|
||||
self.write_spread_method_attribute(gradient.extend);
|
||||
self.write_transform_attribute("gradientTransform", gradient_transform);
|
||||
self.write_gradient_stops(
|
||||
gradient.stops(self.palette_index, self.face.variation_coordinates()),
|
||||
);
|
||||
self.svg.end_element();
|
||||
|
||||
self.svg.start_element("path");
|
||||
self.svg
|
||||
.write_attribute_fmt("fill", format_args!("url(#{gradient_id})"));
|
||||
self.write_transform_attribute("transform", self.outline_transform);
|
||||
self.svg.write_attribute("d", self.path_buf);
|
||||
self.svg.end_element();
|
||||
}
|
||||
|
||||
fn paint_radial_gradient(&mut self, gradient: ttf_parser::colr::RadialGradient<'a>) {
|
||||
let gradient_id = format!("rg{}", self.gradient_index);
|
||||
self.gradient_index += 1;
|
||||
|
||||
let gradient_transform = paint_transform(self.outline_transform, self.transform);
|
||||
|
||||
self.svg.start_element("radialGradient");
|
||||
self.svg.write_attribute("id", &gradient_id);
|
||||
self.svg.write_attribute("cx", &gradient.x1);
|
||||
self.svg.write_attribute("cy", &gradient.y1);
|
||||
self.svg.write_attribute("r", &gradient.r1);
|
||||
self.svg.write_attribute("fr", &gradient.r0);
|
||||
self.svg.write_attribute("fx", &gradient.x0);
|
||||
self.svg.write_attribute("fy", &gradient.y0);
|
||||
self.svg.write_attribute("gradientUnits", &"userSpaceOnUse");
|
||||
self.write_spread_method_attribute(gradient.extend);
|
||||
self.write_transform_attribute("gradientTransform", gradient_transform);
|
||||
self.write_gradient_stops(
|
||||
gradient.stops(self.palette_index, self.face.variation_coordinates()),
|
||||
);
|
||||
self.svg.end_element();
|
||||
|
||||
self.svg.start_element("path");
|
||||
self.svg
|
||||
.write_attribute_fmt("fill", format_args!("url(#{gradient_id})"));
|
||||
self.write_transform_attribute("transform", self.outline_transform);
|
||||
self.svg.write_attribute("d", self.path_buf);
|
||||
self.svg.end_element();
|
||||
}
|
||||
|
||||
fn paint_sweep_gradient(&mut self, _: ttf_parser::colr::SweepGradient<'a>) {}
|
||||
}
|
||||
|
||||
fn paint_transform(
|
||||
outline_transform: ttf_parser::Transform,
|
||||
transform: ttf_parser::Transform,
|
||||
) -> ttf_parser::Transform {
|
||||
let outline_transform = tiny_skia_path::Transform::from_row(
|
||||
outline_transform.a,
|
||||
outline_transform.b,
|
||||
outline_transform.c,
|
||||
outline_transform.d,
|
||||
outline_transform.e,
|
||||
outline_transform.f,
|
||||
);
|
||||
|
||||
let gradient_transform = tiny_skia_path::Transform::from_row(
|
||||
transform.a,
|
||||
transform.b,
|
||||
transform.c,
|
||||
transform.d,
|
||||
transform.e,
|
||||
transform.f,
|
||||
);
|
||||
|
||||
let gradient_transform = outline_transform
|
||||
.invert()
|
||||
// In theory, we should error out. But the transform shouldn't ever be uninvertible, so let's ignore it.
|
||||
.unwrap_or_default()
|
||||
.pre_concat(gradient_transform);
|
||||
|
||||
ttf_parser::Transform {
|
||||
a: gradient_transform.sx,
|
||||
b: gradient_transform.ky,
|
||||
c: gradient_transform.kx,
|
||||
d: gradient_transform.sy,
|
||||
e: gradient_transform.tx,
|
||||
f: gradient_transform.ty,
|
||||
}
|
||||
}
|
||||
|
||||
impl GlyphPainter<'_> {
|
||||
fn clip_with_path(&mut self, path: &str) {
|
||||
let clip_id = format!("cp{}", self.clip_path_index);
|
||||
self.clip_path_index += 1;
|
||||
|
||||
self.svg.start_element("clipPath");
|
||||
self.svg.write_attribute("id", &clip_id);
|
||||
self.svg.start_element("path");
|
||||
self.write_transform_attribute("transform", self.outline_transform);
|
||||
self.svg.write_attribute("d", &path);
|
||||
self.svg.end_element();
|
||||
self.svg.end_element();
|
||||
|
||||
self.svg.start_element("g");
|
||||
self.svg
|
||||
.write_attribute_fmt("clip-path", format_args!("url(#{clip_id})"));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ttf_parser::colr::Painter<'a> for GlyphPainter<'a> {
|
||||
fn outline_glyph(&mut self, glyph_id: ttf_parser::GlyphId) {
|
||||
self.path_buf.clear();
|
||||
let mut builder = ColrBuilder(self.path_buf);
|
||||
match self.face.outline_glyph(glyph_id, &mut builder) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
builder.finish();
|
||||
|
||||
// We have to write outline using the current transform.
|
||||
self.outline_transform = self.transform;
|
||||
}
|
||||
|
||||
fn push_layer(&mut self, mode: ttf_parser::colr::CompositeMode) {
|
||||
self.svg.start_element("g");
|
||||
|
||||
use ttf_parser::colr::CompositeMode;
|
||||
// TODO: Need to figure out how to represent the other blend modes
|
||||
// in SVG.
|
||||
let mode = match mode {
|
||||
CompositeMode::SourceOver => "normal",
|
||||
CompositeMode::Screen => "screen",
|
||||
CompositeMode::Overlay => "overlay",
|
||||
CompositeMode::Darken => "darken",
|
||||
CompositeMode::Lighten => "lighten",
|
||||
CompositeMode::ColorDodge => "color-dodge",
|
||||
CompositeMode::ColorBurn => "color-burn",
|
||||
CompositeMode::HardLight => "hard-light",
|
||||
CompositeMode::SoftLight => "soft-light",
|
||||
CompositeMode::Difference => "difference",
|
||||
CompositeMode::Exclusion => "exclusion",
|
||||
CompositeMode::Multiply => "multiply",
|
||||
CompositeMode::Hue => "hue",
|
||||
CompositeMode::Saturation => "saturation",
|
||||
CompositeMode::Color => "color",
|
||||
CompositeMode::Luminosity => "luminosity",
|
||||
_ => "normal",
|
||||
};
|
||||
self.svg.write_attribute_fmt(
|
||||
"style",
|
||||
format_args!("mix-blend-mode: {mode}; isolation: isolate"),
|
||||
);
|
||||
}
|
||||
|
||||
fn pop_layer(&mut self) {
|
||||
self.svg.end_element(); // g
|
||||
}
|
||||
|
||||
fn push_translate(&mut self, tx: f32, ty: f32) {
|
||||
self.push_transform(ttf_parser::Transform::new(1.0, 0.0, 0.0, 1.0, tx, ty));
|
||||
}
|
||||
|
||||
fn push_scale(&mut self, sx: f32, sy: f32) {
|
||||
self.push_transform(ttf_parser::Transform::new(sx, 0.0, 0.0, sy, 0.0, 0.0));
|
||||
}
|
||||
|
||||
fn push_rotate(&mut self, angle: f32) {
|
||||
let cc = (angle * std::f32::consts::PI).cos();
|
||||
let ss = (angle * std::f32::consts::PI).sin();
|
||||
self.push_transform(ttf_parser::Transform::new(cc, ss, -ss, cc, 0.0, 0.0));
|
||||
}
|
||||
|
||||
fn push_skew(&mut self, skew_x: f32, skew_y: f32) {
|
||||
let x = (-skew_x * std::f32::consts::PI).tan();
|
||||
let y = (skew_y * std::f32::consts::PI).tan();
|
||||
self.push_transform(ttf_parser::Transform::new(1.0, y, x, 1.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
fn push_transform(&mut self, transform: ttf_parser::Transform) {
|
||||
self.transforms_stack.push(self.transform);
|
||||
self.transform = ttf_parser::Transform::combine(self.transform, transform);
|
||||
}
|
||||
|
||||
fn paint(&mut self, paint: ttf_parser::colr::Paint<'a>) {
|
||||
match paint {
|
||||
ttf_parser::colr::Paint::Solid(color) => self.paint_solid(color),
|
||||
ttf_parser::colr::Paint::LinearGradient(lg) => self.paint_linear_gradient(lg),
|
||||
ttf_parser::colr::Paint::RadialGradient(rg) => self.paint_radial_gradient(rg),
|
||||
ttf_parser::colr::Paint::SweepGradient(sg) => self.paint_sweep_gradient(sg),
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_transform(&mut self) {
|
||||
if let Some(ts) = self.transforms_stack.pop() {
|
||||
self.transform = ts
|
||||
}
|
||||
}
|
||||
|
||||
fn push_clip(&mut self) {
|
||||
self.clip_with_path(&self.path_buf.clone());
|
||||
}
|
||||
|
||||
fn pop_clip(&mut self) {
|
||||
self.svg.end_element();
|
||||
}
|
||||
|
||||
fn push_clip_box(&mut self, clipbox: ttf_parser::colr::ClipBox) {
|
||||
let x_min = clipbox.x_min;
|
||||
let x_max = clipbox.x_max;
|
||||
let y_min = clipbox.y_min;
|
||||
let y_max = clipbox.y_max;
|
||||
|
||||
let clip_path = format!(
|
||||
"M {x_min} {y_min} L {x_max} {y_min} L {x_max} {y_max} L {x_min} {y_max} Z"
|
||||
);
|
||||
|
||||
self.clip_with_path(&clip_path);
|
||||
}
|
||||
}
|
||||
|
BIN
tests/ref/issue-3733-dpi-svg.png
Normal file
BIN
tests/ref/issue-3733-dpi-svg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 124 B |
Binary file not shown.
Before Width: | Height: | Size: 948 B After Width: | Height: | Size: 800 B |
Binary file not shown.
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.4 KiB |
@ -120,3 +120,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
||||
--- issue-2051-new-cm-svg ---
|
||||
#set text(font: "New Computer Modern")
|
||||
#image("/assets/images/diagram.svg")
|
||||
|
||||
--- issue-3733-dpi-svg ---
|
||||
#set page(width: 200pt, height: 200pt, margin: 0pt)
|
||||
#image("/assets/images/relative.svg")
|
||||
|
Loading…
x
Reference in New Issue
Block a user