mirror of
https://github.com/penpot/penpot.git
synced 2026-09-21 11:26:15 +00:00
137 lines
5.1 KiB
Rust
137 lines
5.1 KiB
Rust
use skia_safe::Canvas;
|
|
|
|
use crate::error::Result;
|
|
use crate::shapes::{Shape, StrokeKind};
|
|
|
|
use super::document::SvgLayerCanvas;
|
|
use super::masks::push_inverse_glyph_mask;
|
|
use crate::render::vector::{ExportState, VectorRenderer, VectorTarget};
|
|
|
|
/// Emits each semi-transparent *center* text stroke as a `<g opacity>` wrapper
|
|
/// around the fully-opaque stroke geometry, matching the GPU/PDF opacity-layer
|
|
/// result without a `save_layer` (which `SkSVGDevice` would drop). Inner strokes
|
|
/// are handled by `render_text_inner_strokes` and outer strokes (any
|
|
/// opacity) by `render_text_outer_strokes`.
|
|
pub(super) fn render_text_alpha_strokes(
|
|
builder: &mut SvgLayerCanvas,
|
|
shared: &mut ExportState,
|
|
element: &Shape,
|
|
scale: f32,
|
|
) -> Result<()> {
|
|
let matrix = element.centered_transform();
|
|
for stroke in element.visible_strokes() {
|
|
if stroke.render_kind(false) != StrokeKind::Center {
|
|
continue;
|
|
}
|
|
let opacity = stroke.fill.opacity();
|
|
if opacity >= 1.0 {
|
|
// Opaque strokes were already drawn by `render_leaf_content`.
|
|
continue;
|
|
}
|
|
|
|
builder.open_group(&format!("opacity=\"{opacity}\""));
|
|
{
|
|
let canvas = builder.canvas();
|
|
canvas.save();
|
|
canvas.concat(&matrix);
|
|
let mut renderer = VectorRenderer::new(canvas, shared, scale, VectorTarget::Svg);
|
|
renderer.draw_text_stroke_opaque(element, stroke)?;
|
|
canvas.restore();
|
|
}
|
|
builder.close_group();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Emits each inner text stroke as a `<g clip-path>` (glyph-silhouette clip)
|
|
/// wrapping the fully-opaque double-width stroke, plus a `<g opacity>` when the
|
|
/// stroke is semi-transparent. Reproduces the GPU/PDF mask + `SrcIn` + `DstOver`
|
|
/// inner-stroke composition, which `SkSVGDevice` drops (it lives inside
|
|
/// `save_layer`s). Inner strokes vanish from SVG regardless of opacity, so all
|
|
/// of them are handled here.
|
|
pub(super) fn render_text_inner_strokes(
|
|
builder: &mut SvgLayerCanvas,
|
|
shared: &mut ExportState,
|
|
element: &Shape,
|
|
scale: f32,
|
|
) -> Result<()> {
|
|
let matrix = element.centered_transform();
|
|
for stroke in element.visible_strokes() {
|
|
if stroke.render_kind(false) != StrokeKind::Inner {
|
|
continue;
|
|
}
|
|
|
|
// Clip path from the opaque glyph silhouette: clipping the double-width
|
|
// stroke to the glyph interior keeps only its inner half.
|
|
let clip_id = builder.unique("tclip");
|
|
{
|
|
let canvas = builder.new_fragment();
|
|
{
|
|
let cv: &Canvas = &*canvas;
|
|
cv.concat(&matrix);
|
|
let mut renderer = VectorRenderer::new(cv, shared, scale, VectorTarget::Svg);
|
|
renderer.draw_text_glyph_silhouette(element)?;
|
|
}
|
|
builder.finish_clip_path_fragment(&clip_id, canvas);
|
|
}
|
|
|
|
let opacity = stroke.fill.opacity();
|
|
let mut attrs = format!("clip-path=\"url(#{clip_id})\"");
|
|
if opacity < 1.0 {
|
|
attrs.push_str(&format!(" opacity=\"{opacity}\""));
|
|
}
|
|
builder.open_group(&attrs);
|
|
{
|
|
let canvas = builder.canvas();
|
|
canvas.save();
|
|
canvas.concat(&matrix);
|
|
let mut renderer = VectorRenderer::new(canvas, shared, scale, VectorTarget::Svg);
|
|
renderer.draw_text_stroke_opaque(element, stroke)?;
|
|
canvas.restore();
|
|
}
|
|
builder.close_group();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Emits each *outer* text stroke as a `<g mask>` (glyph-exterior mask) wrapping
|
|
/// the fully-opaque double-width stroke, plus a `<g opacity>` when the stroke is
|
|
/// semi-transparent. The shared renderer draws the outer stroke at double width
|
|
/// centered on the glyph outline and relies on a `save_layer` + `Clear` (keep
|
|
/// only the outer half) that `SkSVGDevice` drops, so on SVG it is skipped there
|
|
/// and re-emitted here masked to the glyph exterior (an inverse-of-glyph
|
|
/// luminance mask: white canvas minus the glyphs, since `<clipPath>` cannot
|
|
/// subtract), keeping only the outer half and matching the GPU/PDF width.
|
|
pub(super) fn render_text_outer_strokes(
|
|
builder: &mut SvgLayerCanvas,
|
|
shared: &mut ExportState,
|
|
element: &Shape,
|
|
scale: f32,
|
|
) -> Result<()> {
|
|
let matrix = element.centered_transform();
|
|
for stroke in element.visible_strokes() {
|
|
if stroke.render_kind(false) != StrokeKind::Outer {
|
|
continue;
|
|
}
|
|
|
|
let mask_id = push_inverse_glyph_mask(builder, shared, element, &matrix, scale)?;
|
|
|
|
let opacity = stroke.fill.opacity();
|
|
let mut attrs = format!("mask=\"url(#{mask_id})\"");
|
|
if opacity < 1.0 {
|
|
attrs.push_str(&format!(" opacity=\"{opacity}\""));
|
|
}
|
|
builder.open_group(&attrs);
|
|
{
|
|
let canvas = builder.canvas();
|
|
canvas.save();
|
|
canvas.concat(&matrix);
|
|
let mut renderer = VectorRenderer::new(canvas, shared, scale, VectorTarget::Svg);
|
|
renderer.draw_text_stroke_opaque(element, stroke)?;
|
|
canvas.restore();
|
|
}
|
|
builder.close_group();
|
|
}
|
|
Ok(())
|
|
}
|