diff --git a/resources/testdata/color_icon_composite_screen.svg b/resources/testdata/color_icon_composite_screen.svg
new file mode 100644
index 0000000..3b6f51f
--- /dev/null
+++ b/resources/testdata/color_icon_composite_screen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/testdata/color_icon_composite_src_in.svg b/resources/testdata/color_icon_composite_src_in.svg
new file mode 100644
index 0000000..85064a2
--- /dev/null
+++ b/resources/testdata/color_icon_composite_src_in.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/testdata/composite_mode.png b/resources/testdata/composite_mode.png
new file mode 100644
index 0000000..21adbff
Binary files /dev/null and b/resources/testdata/composite_mode.png differ
diff --git a/src/draw_icon/icon2svg.rs b/src/draw_icon/icon2svg.rs
index b061495..2c21d33 100644
--- a/src/draw_icon/icon2svg.rs
+++ b/src/draw_icon/icon2svg.rs
@@ -5,7 +5,7 @@ use super::{draw_glyph, get_pen, DrawOptions, DrawingInstructions, GlyphType};
use crate::{
error::DrawSvgError,
pathstyle::SvgPathStyle,
- pens::{ColorFill, ColorStop, GlyphPainter, Paint},
+ pens::{ColorDraw, ColorStop, GlyphPainter, Paint},
xml_element::{HexColor, TruncatedFloat, XmlElement},
};
use kurbo::Affine;
@@ -76,50 +76,27 @@ fn draw_color_glyph(
));
}
- to_svg(painter.into_fills()?, &options.style)
+ let draws = painter.into_draws()?;
+ to_svg(draws, &options.style)
}
-fn to_svg(fills: Vec, style: &SvgPathStyle) -> Result {
- let mut group = Vec::new();
+fn to_svg(draws: Vec, style: &SvgPathStyle) -> Result {
let mut clips_cache = ClipsCache::default();
let mut fill_cache = PaintCache::default();
- for fill in fills.iter() {
- // Path
- let Some(shape) = fill.clip_paths.last() else {
- continue;
- };
- let mut path = XmlElement::new("path").with_attribute("d", style.write_svg_path(shape));
-
- // Fill
- fill_cache.add_fill(&mut path, &fill.paint)?;
-
- // Clip
- let mut clip_parent_id = None;
- if fill.clip_paths.len() > 1 {
- for clip in &fill.clip_paths[0..fill.clip_paths.len() - 1] {
- let id = clips_cache.get_id(clip_parent_id, style.write_svg_path(clip).to_string());
- clip_parent_id = Some(id);
- }
- }
- if let Some(id) = clip_parent_id {
- path.add_attribute("clip-path", format!("url(#{})", id));
- }
-
- // Offset
- if fill.offset_x != 0.0 || fill.offset_y != 0.0 {
- path.add_attribute(
- "transform",
- format!("translate({} {})", fill.offset_x, fill.offset_y),
- );
- }
-
- group.push(path);
- }
-
- if !fill_cache.is_empty() || !clips_cache.is_empty() {
+ let mut masks_cache = MasksCache::default();
+ let mut group = draws_to_svg_elements(
+ &draws,
+ style,
+ &mut clips_cache,
+ &mut fill_cache,
+ &mut masks_cache,
+ )?;
+
+ if !fill_cache.is_empty() || !clips_cache.is_empty() || !masks_cache.is_empty() {
group.push(
XmlElement::new("defs")
.with_children(clips_cache.into_svg())
+ .with_children(masks_cache.into_svg())
.with_children(fill_cache.into_svg()),
);
}
@@ -131,6 +108,158 @@ fn to_svg(fills: Vec, style: &SvgPathStyle) -> Result Result, DrawSvgError> {
+ let mut elements = Vec::new();
+ for draw in draws {
+ match draw {
+ ColorDraw::Fill(fill) => {
+ let [clips @ .., shape] = &fill.clip_paths[..] else {
+ continue;
+ };
+ let mut path =
+ XmlElement::new("path").with_attribute("d", style.write_svg_path(shape));
+
+ fill_cache.add_fill(&mut path, &fill.paint)?;
+
+ let mut clip_parent_id = None;
+ for clip in clips {
+ let id =
+ clips_cache.get_id(clip_parent_id, style.write_svg_path(clip).to_string());
+ clip_parent_id = Some(id);
+ }
+ if let Some(id) = clip_parent_id {
+ path.add_attribute("clip-path", format!("url(#{})", id));
+ }
+
+ if fill.offset_x != 0.0 || fill.offset_y != 0.0 {
+ path.add_attribute(
+ "transform",
+ format!("translate({} {})", fill.offset_x, fill.offset_y),
+ );
+ }
+
+ elements.push(path);
+ }
+ ColorDraw::Layer {
+ mode: skrifa::color::CompositeMode::SrcIn,
+ draws: source_draws,
+ } => {
+ let mask_id = masks_cache.get_id(&elements);
+ let child_elements = draws_to_svg_elements(
+ source_draws,
+ style,
+ clips_cache,
+ fill_cache,
+ masks_cache,
+ )?;
+ let mut group = XmlElement::new("g").with_children(child_elements);
+ group.add_attribute("mask", format!("url(#{mask_id})"));
+ elements.push(group);
+ }
+ ColorDraw::Layer { mode, draws } => {
+ let child_elements =
+ draws_to_svg_elements(draws, style, clips_cache, fill_cache, masks_cache)?;
+ let mut group = XmlElement::new("g").with_children(child_elements);
+ if *mode != skrifa::color::CompositeMode::SrcOver {
+ let blend_mode = composite_mode_to_svg_blend_mode(*mode)?;
+ group.add_attribute(
+ "style",
+ format!("mix-blend-mode: {blend_mode}; isolation: isolate"),
+ );
+ }
+ elements.push(group);
+ }
+ }
+ }
+ Ok(elements)
+}
+
+fn composite_mode_to_svg_blend_mode(
+ mode: skrifa::color::CompositeMode,
+) -> Result<&'static str, DrawSvgError> {
+ use skrifa::color::CompositeMode;
+ match mode {
+ CompositeMode::SrcOver => Ok("normal"),
+ CompositeMode::Multiply => Ok("multiply"),
+ CompositeMode::Screen => Ok("screen"),
+ CompositeMode::Overlay => Ok("overlay"),
+ CompositeMode::Darken => Ok("darken"),
+ CompositeMode::Lighten => Ok("lighten"),
+ CompositeMode::ColorDodge => Ok("color-dodge"),
+ CompositeMode::ColorBurn => Ok("color-burn"),
+ CompositeMode::HardLight => Ok("hard-light"),
+ CompositeMode::SoftLight => Ok("soft-light"),
+ CompositeMode::Difference => Ok("difference"),
+ CompositeMode::Exclusion => Ok("exclusion"),
+ CompositeMode::HslHue => Ok("hue"),
+ CompositeMode::HslSaturation => Ok("saturation"),
+ CompositeMode::HslColor => Ok("color"),
+ CompositeMode::HslLuminosity => Ok("luminosity"),
+ unsupported => Err(DrawSvgError::CompositeModeNotSupported(unsupported)),
+ }
+}
+
+/// Unique identifier for a mask.
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+struct MaskId(usize);
+
+impl std::fmt::Display for MaskId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "m{}", self.0)
+ }
+}
+
+/// Caches and manages SVG masks in ``.
+#[derive(Default)]
+struct MasksCache {
+ masks_to_id: HashMap,
+}
+
+impl MasksCache {
+ fn get_id(&mut self, mask_content: &[XmlElement]) -> MaskId {
+ fn to_mask_element(el: &XmlElement) -> XmlElement {
+ let mut mask_el = XmlElement::new(el.tag());
+ for (k, v) in el.attributes() {
+ if k == "fill" {
+ mask_el.add_attribute("fill", "#ffffff");
+ } else {
+ mask_el.add_attribute(k, v);
+ }
+ }
+ if !el.attributes().iter().any(|(k, _)| k == "fill") {
+ mask_el.add_attribute("fill", "#ffffff");
+ }
+ for child in el.children() {
+ mask_el.add_child(to_mask_element(child));
+ }
+ mask_el
+ }
+
+ let children: Vec = mask_content.iter().map(to_mask_element).collect();
+ let mask_el = XmlElement::new("mask").with_children(children);
+ let next_id = MaskId(self.masks_to_id.len());
+ *self.masks_to_id.entry(mask_el).or_insert(next_id)
+ }
+
+ fn into_svg(self) -> impl Iterator- {
+ let mut masks: Vec<_> = self.masks_to_id.into_iter().collect();
+ masks.sort_unstable_by_key(|(_, id)| *id);
+ masks
+ .into_iter()
+ .map(|(mask, id)| mask.with_attribute("id", id))
+ }
+
+ fn is_empty(&self) -> bool {
+ self.masks_to_id.is_empty()
+ }
+}
+
/// Caches and manages SVG clip paths to avoid duplicates in the `` section.
#[derive(Default)]
struct ClipsCache {
@@ -574,4 +703,39 @@ mod tests {
Err(DrawSvgError::SweepGradientNotSupported)
);
}
+
+ #[test]
+ fn color_icon_composite_screen() {
+ let font = FontRef::new(testdata::COLR_FONT).unwrap();
+ let svg = font
+ .draw_icon(&test_options_bounding_box(IconIdentifier::Codepoint(
+ 0xf0a0d,
+ )))
+ .unwrap();
+ assert_file_eq!(svg, "color_icon_composite_screen.svg");
+ }
+
+ #[test]
+ fn color_icon_composite_src_in() {
+ let font = FontRef::new(testdata::COLR_FONT).unwrap();
+ let svg = font
+ .draw_icon(&test_options_bounding_box(IconIdentifier::Codepoint(
+ 0xf0a05,
+ )))
+ .unwrap();
+ assert_file_eq!(svg, "color_icon_composite_src_in.svg");
+ }
+
+ #[test]
+ fn icon_with_unsupported_composite_mode_produces_error() {
+ let font = FontRef::new(testdata::COLR_FONT).unwrap();
+ assert_matches!(
+ font.draw_icon(&test_options_bounding_box(IconIdentifier::Codepoint(
+ 0xf0a00
+ ))),
+ Err(DrawSvgError::CompositeModeNotSupported(
+ skrifa::color::CompositeMode::Clear
+ ))
+ );
+ }
}
diff --git a/src/error.rs b/src/error.rs
index 18a67bf..d3f8053 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -26,6 +26,8 @@ pub enum DrawSvgError {
ColorGlyphNotSupported(GlyphId),
#[error("Unexpected draw type: {0}")]
UnExpectedDrawType(String),
+ #[error("Unsupported SVG feature: composite mode {0:?}")]
+ CompositeModeNotSupported(skrifa::color::CompositeMode),
}
#[derive(Debug, Error)]
diff --git a/src/pens.rs b/src/pens.rs
index 2549a27..67bc356 100644
--- a/src/pens.rs
+++ b/src/pens.rs
@@ -124,6 +124,18 @@ pub enum Paint {
},
}
+/// A drawing operation produced by exercising a color glyph.
+#[derive(Debug, Clone)]
+pub enum ColorDraw {
+ /// A single fill operation.
+ Fill(ColorFill),
+ /// A layer with a composite mode and child drawing operations.
+ Layer {
+ mode: CompositeMode,
+ draws: Vec,
+ },
+}
+
/// Error that occurs when trying to use a color painter.
#[derive(Error, Debug)]
pub enum GlyphPainterError {
@@ -133,9 +145,13 @@ pub enum GlyphPainterError {
UnsupportedFontFeature(&'static str),
#[error("{0}")]
DrawError(#[from] DrawError),
+ #[error("Layer stack underflow")]
+ LayerStackUnderflow,
+ #[error("Layer stack not empty")]
+ LayerStackNotEmpty,
}
-/// A [ColorPainter] that generates a series of [ColorFill]s.
+/// A [ColorPainter] that generates a series of [ColorDraw]s.
pub struct GlyphPainter<'a> {
/// The x-offset for the next fill operation.
pub x: f64,
@@ -155,8 +171,10 @@ struct ColorFillsBuilder {
/// The path for the next fill.
paths: Vec,
transforms: Vec,
- /// All the fills that have been finalized.
- fills: Vec,
+ /// The current layer that is being drawn.
+ current_layer: Vec,
+ /// Stack of open drawing layers.
+ layer_stack: Vec>,
}
/// TODO: Make this into a const once has been
@@ -200,14 +218,19 @@ impl<'a> GlyphPainter<'a> {
builder: Ok(ColorFillsBuilder {
paths: Vec::new(),
transforms: Vec::new(),
- fills: Vec::new(),
+ layer_stack: vec![],
+ current_layer: Vec::new(),
}),
}
}
- /// Returns the completed color fills, or an error if one occurred.
- pub fn into_fills(self) -> Result, GlyphPainterError> {
- self.builder.map(|i| i.fills)
+ /// Returns the completed color drawing operations, or an error if one occurred.
+ pub fn into_draws(self) -> Result, GlyphPainterError> {
+ let builder = self.builder?;
+ if !builder.layer_stack.is_empty() {
+ return Err(GlyphPainterError::LayerStackNotEmpty);
+ }
+ Ok(builder.current_layer)
}
fn set_err(&mut self, err: GlyphPainterError) {
@@ -401,15 +424,107 @@ impl<'a> ColorPainter for GlyphPainter<'a> {
transform,
},
};
- builder.fills.push(ColorFill {
+ builder.current_layer.push(ColorDraw::Fill(ColorFill {
paint,
clip_paths: builder.paths.clone(),
offset_x: self.x,
offset_y: self.y,
- });
+ }));
}
- fn push_layer(&mut self, _: CompositeMode) {
- self.set_err(GlyphPainterError::UnsupportedFontFeature("colr layers"));
+ fn push_layer(&mut self, _mode: CompositeMode) {
+ let Ok(builder) = self.builder.as_mut() else {
+ return;
+ };
+ let parent = std::mem::take(&mut builder.current_layer);
+ builder.layer_stack.push(parent);
+ }
+
+ fn pop_layer_with_mode(&mut self, mode: CompositeMode) {
+ let Ok(builder) = self.builder.as_mut() else {
+ return;
+ };
+ let Some(parent) = builder.layer_stack.pop() else {
+ self.set_err(GlyphPainterError::LayerStackUnderflow);
+ return;
+ };
+ let child = std::mem::replace(&mut builder.current_layer, parent);
+ builder
+ .current_layer
+ .push(ColorDraw::Layer { mode, draws: child });
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use skrifa::{prelude::LocationRef, raw::FontRef};
+
+ #[test]
+ fn painter_composite_glyph() {
+ // Glyph 0xf0a0d in colr.ttf is composite_SCREEN
+ let font = FontRef::new(crate::testdata::COLR_FONT).unwrap();
+ let cmap = font.cmap().unwrap();
+ let glyph_id = cmap.map_codepoint(0xf0a0d_u32).unwrap();
+ let color_glyph = font.color_glyphs().get(glyph_id).unwrap();
+
+ let mut painter = GlyphPainter::new(
+ &font,
+ LocationRef::default(),
+ Color::BLACK,
+ Size::unscaled(),
+ );
+ color_glyph
+ .paint(LocationRef::default(), &mut painter)
+ .unwrap();
+ let draws = painter.into_draws().unwrap();
+
+ assert!(!draws.is_empty());
+ fn has_layer_mode(draws: &[ColorDraw], expected: CompositeMode) -> bool {
+ for draw in draws {
+ if let ColorDraw::Layer { mode, draws } = draw {
+ if *mode == expected || has_layer_mode(draws, expected) {
+ return true;
+ }
+ }
+ }
+ false
+ }
+ assert!(
+ has_layer_mode(&draws, CompositeMode::Screen),
+ "Expected at least one Layer with Screen mode in draws: {draws:?}"
+ );
+ }
+
+ #[test]
+ fn painter_pop_layer_empty_stack_produces_error() {
+ let font = FontRef::new(crate::testdata::COLR_FONT).unwrap();
+ let mut painter = GlyphPainter::new(
+ &font,
+ LocationRef::default(),
+ Color::BLACK,
+ Size::unscaled(),
+ );
+ painter.pop_layer_with_mode(CompositeMode::Screen);
+ assert!(matches!(
+ painter.into_draws(),
+ Err(GlyphPainterError::LayerStackUnderflow)
+ ));
+ }
+
+ #[test]
+ fn painter_unclosed_layer_produces_error() {
+ let font = FontRef::new(crate::testdata::COLR_FONT).unwrap();
+ let mut painter = GlyphPainter::new(
+ &font,
+ LocationRef::default(),
+ Color::BLACK,
+ Size::unscaled(),
+ );
+ painter.push_layer(CompositeMode::Screen);
+ assert!(matches!(
+ painter.into_draws(),
+ Err(GlyphPainterError::LayerStackNotEmpty)
+ ));
}
}
diff --git a/src/text2png.rs b/src/text2png.rs
index e30b718..d6ace2a 100644
--- a/src/text2png.rs
+++ b/src/text2png.rs
@@ -5,7 +5,7 @@ use crate::{
};
use kurbo::{Affine, BezPath, PathEl, Rect, Shape, Vec2};
use skrifa::{
- color::{ColorPainter, Extend, PaintError},
+ color::{ColorPainter, CompositeMode, Extend, PaintError},
prelude::{LocationRef, Size},
raw::{FontRef, ReadError},
MetadataProvider,
@@ -13,7 +13,7 @@ use skrifa::{
use thiserror::Error;
use tiny_skia::{
Color, FillRule, GradientStop, LinearGradient, Mask, Paint as SkiaPaint, PathBuilder, Pixmap,
- Point as SkiaPoint, RadialGradient, Shader, SpreadMode, SweepGradient, Transform,
+ PixmapPaint, Point as SkiaPoint, RadialGradient, Shader, SpreadMode, SweepGradient, Transform,
};
/// Errors encountered during the text-to-PNG rendering process.
@@ -35,6 +35,8 @@ pub enum TextToPngError {
GlyphPainterError(#[from] GlyphPainterError),
#[error("Malformed gradient")]
MalformedGradient,
+ #[error("{0} not supported")]
+ NotSupported(&'static str),
}
// TODO: From can be autoderived with `#[from]` once
@@ -138,7 +140,8 @@ pub fn text2png(text: &str, options: &Text2PngOptions) -> Result, TextTo
}
let expected_height =
(options.line_spacing * options.font_size * text.lines().count() as f32) as f64;
- let pixmap = to_pixmap(&painter.into_fills()?, options.background, expected_height)?;
+ let draws = painter.into_draws()?;
+ let pixmap = to_pixmap(&draws, options.background, expected_height)?;
let bytes = encode_png(pixmap)?;
Ok(bytes)
}
@@ -187,17 +190,23 @@ fn clip_bounds(paths: &[BezPath]) -> Option {
.reduce(|a, b| a.intersect(b))
}
-/// Computes the union of bounding boxes for all provided color fills,
-/// considering their respective offsets and clip paths.
-fn compute_bounds(fills: &[crate::pens::ColorFill]) -> Rect {
- fills
- .iter()
- .filter_map(|fill| {
- let add_offset = |b| b + Vec2::new(fill.offset_x, fill.offset_y);
- clip_bounds(&fill.clip_paths).map(add_offset)
- })
- .reduce(|a, b| a.union(b))
- .unwrap_or_default()
+/// Computes the union of bounding boxes for all provided color fills, considering their respective
+/// offsets and clip paths.
+fn compute_bounds(draws: &[crate::pens::ColorDraw]) -> Rect {
+ fn compute(draws: &[crate::pens::ColorDraw]) -> Option {
+ draws
+ .iter()
+ .filter_map(|draw| match draw {
+ crate::pens::ColorDraw::Fill(fill) => {
+ let add_offset = |b| b + Vec2::new(fill.offset_x, fill.offset_y);
+ clip_bounds(&fill.clip_paths).map(add_offset)
+ }
+ crate::pens::ColorDraw::Layer { draws, .. } => compute(draws),
+ })
+ .reduce(|a, b| a.union(b))
+ }
+
+ compute(draws).unwrap_or_default()
}
/// Create a mask from the intersection of all `paths`. If there are
@@ -232,17 +241,68 @@ fn to_mask(
}
}
+/// Recursively renders color draw operations into a Pixmap.
+fn render_draws(
+ draws: &[crate::pens::ColorDraw],
+ pixmap: &mut Pixmap,
+ x_offset: f64,
+ y_offset: f64,
+) -> Result<(), TextToPngError> {
+ for draw in draws {
+ match draw {
+ crate::pens::ColorDraw::Fill(fill) => {
+ let transform = Transform::from_translate(
+ (fill.offset_x + x_offset) as f32,
+ (fill.offset_y + y_offset) as f32,
+ );
+ let [clips @ .., path] = fill.clip_paths.as_slice() else {
+ continue;
+ };
+ let mask = to_mask(clips, (pixmap.width(), pixmap.height()), transform)?;
+ pixmap.fill_path(
+ &path.to_tinyskia().ok_or(TextToPngError::PathBuildError)?,
+ &fill
+ .paint
+ .to_tinyskia()
+ .ok_or(TextToPngError::MalformedGradient)?,
+ FILL_RULE,
+ transform,
+ mask.as_ref(),
+ );
+ }
+ crate::pens::ColorDraw::Layer { mode, draws } => {
+ let mut layer_pixmap = Pixmap::new(pixmap.width(), pixmap.height())
+ .ok_or(TextToPngError::TextTooSmall)?;
+ render_draws(draws, &mut layer_pixmap, x_offset, y_offset)?;
+ let paint = PixmapPaint {
+ blend_mode: mode.to_tinyskia(),
+ ..PixmapPaint::default()
+ };
+ pixmap.draw_pixmap(
+ 0,
+ 0,
+ layer_pixmap.as_ref(),
+ &paint,
+ Transform::identity(),
+ None,
+ );
+ }
+ }
+ }
+ Ok(())
+}
+
/// Creates a Pixmap from a collection of color fills, centering them
/// vertically within the given height.
///
/// The Pixmap's width is determined automatically based on the
/// bounding box of the fills.
fn to_pixmap(
- fills: &[crate::pens::ColorFill],
+ draws: &[crate::pens::ColorDraw],
background: Color,
height: f64,
) -> Result {
- let bounds = compute_bounds(fills);
+ let bounds = compute_bounds(draws);
let width = bounds.width();
let mut pixmap = Pixmap::new(width.ceil() as u32, height.ceil() as u32)
@@ -251,31 +311,7 @@ fn to_pixmap(
let x_offset = -bounds.min_x();
let y_offset_for_centering = (height - bounds.height()) / 2.0;
let y_offset = y_offset_for_centering - bounds.min_y();
- for fill in fills {
- let transform = Transform::from_translate(
- (fill.offset_x + x_offset) as f32,
- (fill.offset_y + y_offset) as f32,
- );
- let Some(path) = fill.clip_paths.last() else {
- continue;
- };
- let mask = to_mask(
- // OK: Guaranteed to be at least length 1 in above statement.
- &fill.clip_paths[0..fill.clip_paths.len() - 1],
- (pixmap.width(), pixmap.height()),
- transform,
- )?;
- pixmap.fill_path(
- &path.to_tinyskia().ok_or(TextToPngError::PathBuildError)?,
- &fill
- .paint
- .to_tinyskia()
- .ok_or(TextToPngError::MalformedGradient)?,
- FILL_RULE,
- transform,
- mask.as_ref(),
- );
- }
+ render_draws(draws, &mut pixmap, x_offset, y_offset)?;
Ok(pixmap)
}
@@ -411,6 +447,44 @@ impl ToTinySkia for Vec {
}
}
+impl ToTinySkia for CompositeMode {
+ type T = tiny_skia::BlendMode;
+
+ fn to_tinyskia(&self) -> tiny_skia::BlendMode {
+ match self {
+ CompositeMode::Clear => tiny_skia::BlendMode::Clear,
+ CompositeMode::Src => tiny_skia::BlendMode::Source,
+ CompositeMode::Dest => tiny_skia::BlendMode::Destination,
+ CompositeMode::SrcOver => tiny_skia::BlendMode::SourceOver,
+ CompositeMode::DestOver => tiny_skia::BlendMode::DestinationOver,
+ CompositeMode::SrcIn => tiny_skia::BlendMode::SourceIn,
+ CompositeMode::DestIn => tiny_skia::BlendMode::DestinationIn,
+ CompositeMode::SrcOut => tiny_skia::BlendMode::SourceOut,
+ CompositeMode::DestOut => tiny_skia::BlendMode::DestinationOut,
+ CompositeMode::SrcAtop => tiny_skia::BlendMode::SourceAtop,
+ CompositeMode::DestAtop => tiny_skia::BlendMode::DestinationAtop,
+ CompositeMode::Xor => tiny_skia::BlendMode::Xor,
+ CompositeMode::Plus => tiny_skia::BlendMode::Plus,
+ CompositeMode::Screen => tiny_skia::BlendMode::Screen,
+ CompositeMode::Overlay => tiny_skia::BlendMode::Overlay,
+ CompositeMode::Darken => tiny_skia::BlendMode::Darken,
+ CompositeMode::Lighten => tiny_skia::BlendMode::Lighten,
+ CompositeMode::ColorDodge => tiny_skia::BlendMode::ColorDodge,
+ CompositeMode::ColorBurn => tiny_skia::BlendMode::ColorBurn,
+ CompositeMode::HardLight => tiny_skia::BlendMode::HardLight,
+ CompositeMode::SoftLight => tiny_skia::BlendMode::SoftLight,
+ CompositeMode::Difference => tiny_skia::BlendMode::Difference,
+ CompositeMode::Exclusion => tiny_skia::BlendMode::Exclusion,
+ CompositeMode::Multiply => tiny_skia::BlendMode::Multiply,
+ CompositeMode::HslHue => tiny_skia::BlendMode::Hue,
+ CompositeMode::HslSaturation => tiny_skia::BlendMode::Saturation,
+ CompositeMode::HslColor => tiny_skia::BlendMode::Color,
+ CompositeMode::HslLuminosity => tiny_skia::BlendMode::Luminosity,
+ _ => tiny_skia::BlendMode::SourceOver,
+ }
+ }
+}
+
impl ToTinySkia for kurbo::Point {
type T = SkiaPoint;
@@ -483,6 +557,17 @@ mod tests {
assert_file_eq!(png_bytes, "sweep_gradient.png");
}
+ #[test]
+ fn composite_mode() {
+ let composite_mode_text = "\u{f0a00}\u{f0a01}\u{f0a02}\u{f0a03}\u{f0a04}\u{f0a05}\u{f0a06}\u{f0a07}\u{f0a08}\u{f0a09}\u{f0a0a}\u{f0a0b}\u{f0a0c}\u{f0a0d}\u{f0a0e}\u{f0a0f}\n\u{f0a10}\u{f0a11}\u{f0a12}\u{f0a13}\u{f0a14}\u{f0a15}\u{f0a16}\u{f0a17}\u{f0a18}\u{f0a19}\u{f0a1a}\u{f0a1b}";
+ let png_bytes = text2png(
+ composite_mode_text,
+ &Text2PngOptions::new(testdata::COLR_FONT, 64.0),
+ )
+ .unwrap();
+ assert_file_eq!(png_bytes, "composite_mode.png");
+ }
+
#[test]
fn complex_emoji() {
// TODO: Improve the centering algorithm.
diff --git a/src/xml_element.rs b/src/xml_element.rs
index 1d633e5..4ecc4e6 100644
--- a/src/xml_element.rs
+++ b/src/xml_element.rs
@@ -144,6 +144,21 @@ impl XmlElement {
self.add_children(children);
self
}
+
+ /// Returns the tag name of the element.
+ pub fn tag(&self) -> &str {
+ &self.tag
+ }
+
+ /// Returns the attributes of the element.
+ pub fn attributes(&self) -> &[(String, String)] {
+ &self.attributes
+ }
+
+ /// Returns the children of the element.
+ pub fn children(&self) -> &[XmlElement] {
+ &self.children
+ }
}
/// Formats the `XmlElement` as an XML string.