diff --git a/exporter/canvas/src/lib.rs b/exporter/canvas/src/lib.rs index 7e5214d4..b0fde61a 100644 --- a/exporter/canvas/src/lib.rs +++ b/exporter/canvas/src/lib.rs @@ -178,7 +178,7 @@ impl CanvasElem for CanvasPathElem { if fill { // todo: canvas gradient - if fill_color.starts_with("url") { + if fill_color.starts_with('@') { fill_color = "black".into() } canvas.set_fill_style(&fill_color.as_ref().into()); @@ -187,7 +187,7 @@ impl CanvasElem for CanvasPathElem { if stroke && stroke_width.abs() > 1e-5 { // todo: canvas gradient - if stroke_color.starts_with("url") { + if stroke_color.starts_with('@') { stroke_color = "black".into() } diff --git a/exporter/svg/src/backend/mod.rs b/exporter/svg/src/backend/mod.rs index 7b23233f..af105540 100644 --- a/exporter/svg/src/backend/mod.rs +++ b/exporter/svg/src/backend/mod.rs @@ -20,7 +20,7 @@ use typst_ts_core::{ mod escape; use escape::{PcDataEscapes, TextContentDataEscapes}; -use crate::utils::ToCssExt; +use crate::{frontend::HasGradient, utils::ToCssExt}; pub trait BuildClipPath { fn build_clip_path(&mut self, path: &ir::PathItem) -> Fingerprint; @@ -586,8 +586,10 @@ impl< } /// See [`FlatGroupContext`]. -impl<'m, C: FlatIncrRenderVm<'m, Resultant = Arc, Group = SvgTextBuilder>> - FlatIncrGroupContext for SvgTextBuilder +impl< + 'm, + C: FlatIncrRenderVm<'m, Resultant = Arc, Group = SvgTextBuilder> + HasGradient, + > FlatIncrGroupContext for SvgTextBuilder { fn render_diff_item_ref_at( &mut self, @@ -597,7 +599,8 @@ impl<'m, C: FlatIncrRenderVm<'m, Resultant = Arc, Group = SvgTextBu item: &Fingerprint, prev_item: &Fingerprint, ) { - let content = if item == prev_item { + let has_gradient = ctx.has_gradient(item); + let content = if item == prev_item && !has_gradient { // todo: update transform vec![] } else { @@ -612,6 +615,9 @@ impl<'m, C: FlatIncrRenderVm<'m, Resultant = Arc, Group = SvgTextBu }; attributes.push(("data-tid", item.as_svg_id("p"))); attributes.push(("data-reuse-from", prev_item.as_svg_id("p"))); + if has_gradient { + attributes.push(("data-bad-equality", "1".to_owned())); + } self.content.push(SvgText::Content(Arc::new(SvgTextNode { attributes, @@ -631,7 +637,7 @@ fn render_path(path: &ir::PathItem, state: RenderState, abs_ref: &Fingerprint) - for style in &path.styles { match style { PathStyle::Fill(color) => { - fill_color = if color.starts_with("url") { + fill_color = if color.starts_with('@') { ft = format!(r#"url(#{})"#, state.at(abs_ref).as_svg_id("pf")); &ft } else { @@ -640,7 +646,7 @@ fn render_path(path: &ir::PathItem, state: RenderState, abs_ref: &Fingerprint) - } PathStyle::Stroke(color) => { // compress the stroke color - p.push(if color.starts_with("url") { + p.push(if color.starts_with('@') { let ps = state.at(abs_ref).as_svg_id("ps"); format!(r##"stroke="url(#{})" "##, &ps) } else { diff --git a/exporter/svg/src/frontend/context.rs b/exporter/svg/src/frontend/context.rs index 6e8abed6..8b94aa34 100644 --- a/exporter/svg/src/frontend/context.rs +++ b/exporter/svg/src/frontend/context.rs @@ -8,7 +8,7 @@ use typst_ts_core::{ flat_vm::{FlatGroupContext, FlatIncrRenderVm, FlatRenderVm}, ir::{ self, BuildGlyph, FontIndice, FontRef, GlyphHashStablizer, GlyphIndice, GlyphItem, - GlyphPackBuilder, GlyphRef, ImmutStr, PathItem, Scalar, StyleNs, + GlyphPackBuilder, GlyphRef, ImmutStr, PathItem, PathStyle, Scalar, StyleNs, }, vm::GroupContext, vm::{RenderState, RenderVm}, @@ -21,9 +21,12 @@ use crate::{ BuildClipPath, BuildFillStyleClass, DynExportFeature, NotifyPaint, SvgText, SvgTextBuilder, SvgTextNode, }, + utils::MemorizeFree, ExportFeature, GlyphProvider, SvgGlyphBuilder, }; +use super::HasGradient; + /// Maps the style name to the style definition. /// See [`StyleNs`]. pub(crate) type StyleDefMap = HashMap<(StyleNs, ImmutStr), String>; @@ -146,6 +149,36 @@ impl<'m, 't, Feat: ExportFeature> BuildClipPath for RenderContext<'m, 't, Feat> } } +#[comemo::memoize] +fn has_gradient<'m, 't, Feat: ExportFeature>( + ctx: &MemorizeFree>, + x: &Fingerprint, +) -> bool { + let Some(item) = ctx.0.get_item(x) else { + // overestimated + return true; + }; + + use FlatSvgItem::*; + match item { + Gradient(..) => true, + Image(..) | Link(..) | None => false, + Item(t) => has_gradient(ctx, &t.1), + Group(g, ..) => g.0.iter().any(|(_, x)| has_gradient(ctx, x)), + Path(p) => p.styles.iter().any(|s| match s { + PathStyle::Fill(color) | PathStyle::Stroke(color) => color.starts_with('@'), + _ => false, + }), + Text(p) => p.shape.fill.starts_with('@'), + } +} + +impl<'m, 't, Feat: ExportFeature> HasGradient for RenderContext<'m, 't, Feat> { + fn has_gradient(&self, a: &Fingerprint) -> bool { + has_gradient(&MemorizeFree(self), a) + } +} + impl<'m, 't, Feat: ExportFeature> NotifyPaint for RenderContext<'m, 't, Feat> { fn notify_paint(&mut self, url_ref: ImmutStr) -> (u8, Fingerprint, Option) { if let Some(f) = self.gradients.get(&url_ref) { @@ -232,20 +265,10 @@ impl<'m, 't, Feat: ExportFeature> RenderVm for RenderContext<'m, 't, Feat> { let mut group_ctx = text.shape.add_transform(self, group_ctx, upem); - if let Some(fill) = &group_ctx.text_fill { + let width = if let Some(fill) = &group_ctx.text_fill { // clip path rect let clip_id = fill.as_svg_id("tc"); - group_ctx.content.push(SvgText::Plain(format!( - r#""#, - clip_id - ))); - } - let width = text.render_glyphs(upem, |x, g| { - group_ctx.render_glyph(self, x, g); - }); - if let Some(fill) = &group_ctx.text_fill { let fill_id = fill.as_svg_id("tf"); - let clip_id = fill.as_svg_id("tc"); // because the text is already scaled by the font size, // we need to scale it back to the original size. @@ -256,16 +279,26 @@ impl<'m, 't, Feat: ExportFeature> RenderVm for RenderContext<'m, 't, Feat> { .descender .at(TypstAbs::raw(upem.0 as f64)) .to_pt() as f32; - let width = width.0 * upem.0 / text.shape.size.0; + + group_ctx.content.push(SvgText::Plain(format!( + r#""#, + clip_id + ))); + + let width = text.render_glyphs(upem, |x, g| { + group_ctx.render_glyph(self, x, g); + group_ctx.content.push(SvgText::Plain("".into())); + }); group_ctx .content .push(SvgText::Plain(r#""#.to_owned())); // clip path rect + let scaled_width = width.0 * upem.0 / text.shape.size.0; group_ctx.content.push(SvgText::Plain(format!( r##""##, - fill_id, width, upem.0, descender, clip_id + fill_id, scaled_width, upem.0, descender, clip_id ))); // image glyphs @@ -276,7 +309,13 @@ impl<'m, 't, Feat: ExportFeature> RenderVm for RenderContext<'m, 't, Feat> { } group_ctx.render_glyph(self, x, g); }); - } + + width + } else { + text.render_glyphs(upem, |x, g| { + group_ctx.render_glyph(self, x, g); + }) + }; if self.should_render_text_element() { group_ctx.render_text_semantics_inner( @@ -346,35 +385,35 @@ impl<'m, 't, Feat: ExportFeature> FlatRenderVm<'m> for RenderContext<'m, 't, Fea group_ctx = text.shape.add_transform(self, group_ctx, upem); - if let Some(fill) = &group_ctx.text_fill { + let width = if let Some(fill) = &group_ctx.text_fill { // clip path rect let clip_id = fill.as_svg_id("tc"); - group_ctx.content.push(SvgText::Plain(format!( - r#""#, - clip_id - ))); - } - let width = text.render_glyphs(upem, |x, g| { - group_ctx.render_glyph_ref(self, x, g); - }); - if let Some(fill) = &group_ctx.text_fill { let fill_id = fill.as_svg_id("tf"); - let clip_id = fill.as_svg_id("tc"); // because the text is already scaled by the font size, // we need to scale it back to the original size. // todo: infinite multiplication let descender = font.descender.0 * upem.0; - let width = width.0 * upem.0 / text.shape.size.0; + + group_ctx.content.push(SvgText::Plain(format!( + r#""#, + clip_id + ))); + + let width = text.render_glyphs(upem, |x, g| { + group_ctx.render_glyph_ref(self, x, g); + group_ctx.content.push(SvgText::Plain("".into())); + }); group_ctx .content .push(SvgText::Plain(r#""#.to_owned())); // clip path rect + let scaled_width = width.0 * upem.0 / text.shape.size.0; group_ctx.content.push(SvgText::Plain(format!( r##""##, - fill_id, width, upem.0, descender, clip_id + fill_id, scaled_width, upem.0, descender, clip_id ))); // image glyphs @@ -388,7 +427,13 @@ impl<'m, 't, Feat: ExportFeature> FlatRenderVm<'m> for RenderContext<'m, 't, Fea } group_ctx.render_glyph_ref(self, x, g); }); - } + + width + } else { + text.render_glyphs(upem, |x, g| { + group_ctx.render_glyph_ref(self, x, g); + }) + }; if self.should_render_text_element() { group_ctx.render_text_semantics_inner( diff --git a/exporter/svg/src/frontend/mod.rs b/exporter/svg/src/frontend/mod.rs index 3763f6b1..66e68460 100644 --- a/exporter/svg/src/frontend/mod.rs +++ b/exporter/svg/src/frontend/mod.rs @@ -752,3 +752,7 @@ impl std::fmt::Display for RatioRepr { write!(f, "{:.3}%", self.0 * 100.0) } } + +pub trait HasGradient { + fn has_gradient(&self, f: &Fingerprint) -> bool; +} diff --git a/exporter/svg/src/utils.rs b/exporter/svg/src/utils.rs index 059ac6d1..0f104664 100644 --- a/exporter/svg/src/utils.rs +++ b/exporter/svg/src/utils.rs @@ -60,3 +60,18 @@ impl ToCssExt for ir::Transform { ) } } + +#[derive(Clone, Copy)] +pub(crate) struct MemorizeFree<'a, T>(pub &'a T); + +impl<'a, T> std::hash::Hash for MemorizeFree<'a, T> { + fn hash(&self, _state: &mut H) {} +} + +impl<'a, T> std::cmp::PartialEq for MemorizeFree<'a, T> { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl<'a, T> std::cmp::Eq for MemorizeFree<'a, T> {} diff --git a/fuzzers/corpora/visualize/gradient-official_00.typ b/fuzzers/corpora/visualize/gradient-official_00.typ new file mode 100644 index 00000000..9e39bd65 --- /dev/null +++ b/fuzzers/corpora/visualize/gradient-official_00.typ @@ -0,0 +1,10 @@ + + +#set text(fill: gradient.linear(red, blue), font: "Open Sans") +#let rainbow(content) = { + set text(fill: gradient.linear(..color.map.rainbow)) + box(content) +} + +This is a gradient on text, but with a #rainbow[twist]! +