Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ This changelog also contains important changes in dependencies.

## [Unreleased]

### Added

- `usvg::Tree::compute_flattened_text`, which flattens all text nodes in the tree
upfront while sharing a glyph cache between them.

### Changed

- Text flattening (converting positioned glyphs into paths) is now performed lazily.
Parsing an SVG only performs text layout; outlines are computed on the first access
to `usvg::Text::flattened` or via `usvg::Tree::compute_flattened_text`.
- `usvg::Text::stroke_bounding_box` is now calculated from the per-glyph ink bounding
boxes stored in the font instead of the flattened outlines, which may result in
slightly different (approximate) bounds for non-outline (e.g. color) glyphs.

## [0.48.1] 2026-08-02

This release has an MSRV of 1.85.0 for `usvg` and `resvg` and the C API.
Expand Down
7 changes: 7 additions & 0 deletions crates/resvg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ pub fn render(
transform: tiny_skia::Transform,
pixmap: &mut tiny_skia::PixmapMut,
) {
// Flatten all text nodes upfront so that a glyph cache
// can be shared between them.
#[cfg(feature = "text")]
if tree.has_text_nodes() {
tree.compute_flattened_text();
}

let max_bbox = max_filter_bbox(pixmap.width(), pixmap.height());

let ctx = render::Context { max_bbox };
Expand Down
96 changes: 0 additions & 96 deletions crates/usvg/src/parser/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,15 @@ use std::hash::{Hash, Hasher};
use std::str::FromStr;
use std::sync::Arc;

#[cfg(feature = "text")]
use crate::{FontVariation, GlyphId};
#[cfg(feature = "text")]
use fontdb::Database;
#[cfg(feature = "text")]
use fontdb::ID;
use svgtypes::{Length, LengthUnit as Unit, PaintOrderKind, TransformOrigin};
use tiny_skia_path::PathBuilder;

use super::svgtree::{self, AId, EId, FromValue, SvgNode};
use super::units::{self, convert_length};
use super::{Error, Options, marker};
#[cfg(feature = "text")]
use crate::flatten::BitmapImage;
use crate::parser::paint_server::process_paint;
#[cfg(feature = "text")]
use crate::text::flatten::DatabaseExt;
use crate::*;

#[derive(Clone)]
Expand All @@ -49,17 +41,6 @@ pub struct Cache {
#[cfg(feature = "text")]
pub fontdb: Arc<Database>,

#[cfg(feature = "text")]
cache_outline: HashMap<(ID, GlyphId, Vec<FontVariation>), Option<tiny_skia_path::Path>>,
#[cfg(feature = "text")]
cache_colr: HashMap<(ID, GlyphId, Vec<FontVariation>), Option<Tree>>,
#[cfg(feature = "text")]
cache_svg: HashMap<(ID, GlyphId), Option<Node>>,
#[cfg(feature = "text")]
cache_raster: HashMap<(ID, GlyphId), Option<BitmapImage>>,
#[cfg(feature = "text")]
cache_has_opsz: HashMap<ID, bool>,

pub clip_paths: HashMap<String, Arc<ClipPath>>,
pub masks: HashMap<String, Arc<Mask>>,
pub filters: HashMap<String, Arc<filter::Filter>>,
Expand All @@ -76,40 +57,12 @@ pub struct Cache {
image_index: usize,
}

macro_rules! font_lookup {
($method_name:ident, $cache_map:ident, $font_variant:ident, $return_type:ty) => {
#[cfg(feature = "text")]
pub(crate) fn $method_name(&mut self, font: ID, glyph: GlyphId) -> Option<$return_type> {
let key = (font, glyph);
match self.$cache_map.get(&key) {
Some(cache_hit) => cache_hit.clone(),
None => {
let lookup = self.fontdb.$font_variant(font, glyph);
self.$cache_map.insert(key, lookup.clone());
lookup
}
}
}
};
}

impl Cache {
pub(crate) fn new(#[cfg(feature = "text")] fontdb: Arc<Database>) -> Self {
Self {
#[cfg(feature = "text")]
fontdb,

#[cfg(feature = "text")]
cache_outline: HashMap::new(),
#[cfg(feature = "text")]
cache_colr: HashMap::new(),
#[cfg(feature = "text")]
cache_svg: HashMap::new(),
#[cfg(feature = "text")]
cache_raster: HashMap::new(),
#[cfg(feature = "text")]
cache_has_opsz: HashMap::new(),

clip_paths: HashMap::new(),
masks: HashMap::new(),
filters: HashMap::new(),
Expand Down Expand Up @@ -203,55 +156,6 @@ impl Cache {
}
}
}

font_lookup!(fontdb_svg, cache_svg, svg, Node);
font_lookup!(fontdb_raster, cache_raster, raster, BitmapImage);

#[cfg(feature = "text")]
pub(crate) fn fontdb_outline(
&mut self,
font: ID,
glyph: GlyphId,
variations: &[FontVariation],
) -> Option<tiny_skia_path::Path> {
let key = (font, glyph, variations.to_vec());
match self.cache_outline.get(&key) {
Some(cache_hit) => cache_hit.clone(),
None => {
let lookup = self.fontdb.outline(font, glyph, variations);
self.cache_outline.insert(key, lookup.clone());
lookup
}
}
}

#[cfg(feature = "text")]
pub(crate) fn fontdb_colr(
&mut self,
font: ID,
glyph: GlyphId,
variations: &[FontVariation],
) -> Option<Tree> {
let key = (font, glyph, variations.to_vec());
match self.cache_colr.get(&key) {
Some(cache_hit) => cache_hit.clone(),
None => {
let lookup = self.fontdb.colr(font, glyph, variations);
self.cache_colr.insert(key, lookup.clone());
lookup
}
}
}

#[cfg(feature = "text")]
pub(crate) fn has_opsz_axis(&mut self, font: ID) -> bool {
if let Some(&cached) = self.cache_has_opsz.get(&font) {
return cached;
}
let has_opsz = self.fontdb.has_opsz_axis(font);
self.cache_has_opsz.insert(font, has_opsz);
has_opsz
}
}

// TODO: is there a simpler way?
Expand Down
16 changes: 5 additions & 11 deletions crates/usvg/src/parser/paint_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,10 +664,13 @@ fn node_to_user_coordinates(
// paint servers.
let bbox = text.bounding_box;

// We need to update three things:
// We need to update two things:
// 1. The fills/strokes of the original elements in the usvg tree.
// 2. The fills/strokes of the layouted elements of the text.
// 3. The fills/strokes of the outlined text.
//
// The outlined text is generated lazily after parsing and clones
// the (already processed) fills/strokes of the layouted elements,
// so it doesn't need to be processed here.

// 1.
for chunk in &mut text.chunks {
Expand Down Expand Up @@ -745,15 +748,6 @@ fn node_to_user_coordinates(
process_decoration(path);
}
}

// 3.
update_paint_servers(
&mut text.flattened,
context_transform,
context_bbox,
Some(bbox),
cache,
);
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/usvg/src/parser/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,9 @@ pub(crate) fn convert(
abs_bounding_box: dummy,
stroke_bounding_box: dummy,
abs_stroke_bounding_box: dummy,
flattened: Box::new(Group::empty()),
flattened: std::sync::OnceLock::new(),
layouted: vec![],
fontdb: cache.fontdb.clone(),
};

if text::convert(&mut text, &state.opt.font_resolver, cache).is_none() {
Expand Down
Loading
Loading