use ttf_parser::colr::{
ClipBox, CompositeMode, GradientExtend, LinearGradient as TtfLinear, Paint, Painter,
RadialGradient as TtfRadial,
};
use ttf_parser::{GlyphId, RgbaColor, Transform as TtfTransform};
use tiny_skia::{
BlendMode, Color, FillRule, GradientStop, LinearGradient, Mask, Paint as SkPaint,
Path, PathBuilder, Pixmap, PixmapPaint, Point, RadialGradient, Rect, Shader,
SpreadMode, Transform,
};
use crate::font::glyf_decode;
pub struct RasterizedPayload {
pub data: Vec<u8>,
pub width: u16,
pub height: u16,
pub left: i32,
pub top: i32,
pub is_color: bool,
}
pub fn rasterize_payload(
payload: &crate::font::glyph_registry::StoredPayload,
upm: u16,
pixel_size: u16,
foreground_rgba: [u8; 4],
) -> Option<RasterizedPayload> {
use crate::font::glyph_registry::StoredPayload;
match payload {
StoredPayload::Glyf { glyf } => rasterize_mono(glyf, upm, pixel_size),
StoredPayload::ColrV0 { glyphs, colr, cpal }
| StoredPayload::ColrV1 { glyphs, colr, cpal } => {
rasterize(glyphs, colr, cpal, upm, pixel_size, foreground_rgba)
}
}
}
fn rasterize_mono(glyf: &[u8], upm: u16, pixel_size: u16) -> Option<RasterizedPayload> {
if pixel_size == 0 || upm == 0 {
return None;
}
let outline = glyf_decode::decode(glyf).ok()?;
let scale = pixel_size as f32 / upm as f32;
let pad = 1.0_f32;
let pix_w = (((outline.x_max - outline.x_min) as f32 * scale).ceil() + pad * 2.0)
.max(1.0) as u32;
let pix_h = (((outline.y_max - outline.y_min) as f32 * scale).ceil() + pad * 2.0)
.max(1.0) as u32;
let cmds = outline.walk(1, 1.0);
if cmds.is_empty() {
return None;
}
let mut pb = PathBuilder::new();
for cmd in &cmds {
match *cmd {
glyf_decode::PathCmd::MoveTo { x, y } => pb.move_to(x, y),
glyf_decode::PathCmd::LineTo { x, y } => pb.line_to(x, y),
glyf_decode::PathCmd::QuadTo { cx, cy, x, y } => pb.quad_to(cx, cy, x, y),
glyf_decode::PathCmd::Close => pb.close(),
}
}
let path = pb.finish()?;
let mut pixmap = Pixmap::new(pix_w, pix_h)?;
let ctm = Transform::from_row(
scale,
0.0,
0.0,
scale,
pad - outline.x_min as f32 * scale,
pad,
);
let mut paint = SkPaint::default();
paint.set_color_rgba8(0xFF, 0xFF, 0xFF, 0xFF);
paint.anti_alias = true;
pixmap.fill_path(&path, &paint, FillRule::Winding, ctm, None);
let data: Vec<u8> = pixmap.pixels().iter().map(|p| p.alpha()).collect();
let left = (outline.x_min as f32 * scale - pad).floor() as i32;
let top = (outline.y_max as f32 * scale + pad).ceil() as i32;
Some(RasterizedPayload {
data,
width: pix_w as u16,
height: pix_h as u16,
left,
top,
is_color: false,
})
}
pub(super) fn rasterize(
glyphs: &[Vec<u8>],
colr_bytes: &[u8],
cpal_bytes: &[u8],
upm: u16,
pixel_size: u16,
foreground: [u8; 4],
) -> Option<RasterizedPayload> {
if pixel_size == 0 || upm == 0 {
return None;
}
const EMPTY_CPAL: [u8; 12] = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C,
];
let cpal_source: &[u8] = if cpal_bytes.is_empty() {
&EMPTY_CPAL
} else {
cpal_bytes
};
let cpal = ttf_parser::cpal::Table::parse(cpal_source)?;
let colr = ttf_parser::colr::Table::parse(cpal, colr_bytes)?;
let base_gid = first_base_glyph_id(colr_bytes, glyphs)?;
let (x_min, y_min, x_max, y_max): (i32, i32, i32, i32) =
match colr.clip_box(GlyphId(base_gid), &[]) {
Some(cb) => (
cb.x_min.floor() as i32,
cb.y_min.floor() as i32,
cb.x_max.ceil() as i32,
cb.y_max.ceil() as i32,
),
None => {
let (a, b, c, d) = glyf_bbox(glyphs.get(base_gid as usize)?)?;
(a as i32, b as i32, c as i32, d as i32)
}
};
let scale = pixel_size as f32 / upm as f32;
let pad = 1.0_f32;
let pix_w = (((x_max - x_min) as f32 * scale).ceil() + pad * 2.0).max(1.0) as u32;
let pix_h = (((y_max - y_min) as f32 * scale).ceil() + pad * 2.0).max(1.0) as u32;
let base_pixmap = Pixmap::new(pix_w, pix_h)?;
let base_ctm = Transform::from_row(
scale,
0.0,
0.0,
-scale,
-(x_min as f32) * scale + pad,
(y_max as f32) * scale + pad,
);
let mut raster = ColorRaster {
layers: vec![Layer {
pixmap: base_pixmap,
mode: CompositeMode::SourceOver,
}],
transforms: vec![base_ctm],
clips: vec![None],
current_path: None,
glyphs,
};
let fg = RgbaColor::new(foreground[0], foreground[1], foreground[2], foreground[3]);
colr.paint(GlyphId(base_gid), 0, &mut raster, &[], fg)?;
debug_assert_eq!(raster.layers.len(), 1, "layer stack should drain");
let final_pixmap = raster.layers.pop().unwrap().pixmap;
Some(RasterizedPayload {
data: pixmap_to_rgba(&final_pixmap),
width: pix_w as u16,
height: pix_h as u16,
left: (x_min as f32 * scale - pad).floor() as i32,
top: (y_max as f32 * scale + pad).ceil() as i32,
is_color: true,
})
}
struct Layer {
pixmap: Pixmap,
mode: CompositeMode,
}
struct ColorRaster<'a> {
layers: Vec<Layer>,
transforms: Vec<Transform>,
clips: Vec<Option<Mask>>,
current_path: Option<Path>,
glyphs: &'a [Vec<u8>],
}
impl ColorRaster<'_> {
fn top_ctm(&self) -> Transform {
*self.transforms.last().unwrap_or(&Transform::identity())
}
fn top_clip(&self) -> Option<&Mask> {
self.clips.last().and_then(|c| c.as_ref())
}
fn top_pixmap(&mut self) -> &mut Pixmap {
&mut self.layers.last_mut().unwrap().pixmap
}
fn fill_current(&mut self, paint: SkPaint) {
let Some(path) = self.current_path.clone() else {
return;
};
let ctm = self.top_ctm();
let clip = self.top_clip().cloned();
let pixmap = self.top_pixmap();
pixmap.fill_path(&path, &paint, FillRule::Winding, ctm, clip.as_ref());
}
}
impl<'a> Painter<'a> for ColorRaster<'a> {
fn outline_glyph(&mut self, glyph_id: GlyphId) {
let idx = glyph_id.0 as usize;
let Some(bytes) = self.glyphs.get(idx) else {
self.current_path = None;
return;
};
if bytes.is_empty() {
self.current_path = None;
return;
}
self.current_path = build_path(bytes);
}
fn paint(&mut self, paint: Paint<'a>) {
match paint {
Paint::Solid(color) => {
let p = SkPaint {
shader: Shader::SolidColor(rgba_to_color(color)),
anti_alias: true,
..SkPaint::default()
};
self.fill_current(p);
}
Paint::LinearGradient(lg) => {
if let Some(shader) = linear_gradient_shader(&lg) {
let p = SkPaint {
shader,
anti_alias: true,
..SkPaint::default()
};
self.fill_current(p);
}
}
Paint::RadialGradient(rg) => {
if let Some(shader) = radial_gradient_shader(&rg) {
let p = SkPaint {
shader,
anti_alias: true,
..SkPaint::default()
};
self.fill_current(p);
}
}
Paint::SweepGradient(sg) => {
if let Some(first) = sg.stops(0, &[]).next() {
let p = SkPaint {
shader: Shader::SolidColor(rgba_to_color(first.color)),
anti_alias: true,
..SkPaint::default()
};
self.fill_current(p);
}
}
}
}
fn push_clip(&mut self) {
let Some(path) = self.current_path.clone() else {
self.clips.push(self.top_clip().cloned());
return;
};
let ctm = self.top_ctm();
let parent = self.top_clip().cloned();
let (pw, ph) = {
let p = self.top_pixmap();
(p.width(), p.height())
};
let Some(mut mask) = Mask::new(pw, ph) else {
self.clips.push(parent);
return;
};
mask.fill_path(&path, FillRule::Winding, true, ctm);
if let Some(par) = parent {
intersect_masks(&mut mask, &par);
}
self.clips.push(Some(mask));
}
fn push_clip_box(&mut self, clipbox: ClipBox) {
let parent = self.top_clip().cloned();
let (pw, ph) = {
let p = self.top_pixmap();
(p.width(), p.height())
};
let Some(rect) =
Rect::from_ltrb(clipbox.x_min, clipbox.y_min, clipbox.x_max, clipbox.y_max)
else {
self.clips.push(parent);
return;
};
let path = PathBuilder::from_rect(rect);
let ctm = self.top_ctm();
let Some(mut mask) = Mask::new(pw, ph) else {
self.clips.push(parent);
return;
};
mask.fill_path(&path, FillRule::Winding, true, ctm);
if let Some(par) = parent {
intersect_masks(&mut mask, &par);
}
self.clips.push(Some(mask));
}
fn pop_clip(&mut self) {
self.clips.pop();
}
fn push_layer(&mut self, mode: CompositeMode) {
let (w, h) = {
let base = self.top_pixmap();
(base.width(), base.height())
};
let Some(pixmap) = Pixmap::new(w, h) else {
self.layers.push(Layer {
pixmap: Pixmap::new(1, 1).unwrap(),
mode,
});
self.clips.push(self.top_clip().cloned());
return;
};
self.layers.push(Layer { pixmap, mode });
self.clips.push(self.top_clip().cloned());
}
fn pop_layer(&mut self) {
let Some(top) = self.layers.pop() else { return };
self.clips.pop();
let blend = composite_mode_to_blend(top.mode);
let Some(parent) = self.layers.last_mut() else {
return;
};
parent.pixmap.draw_pixmap(
0,
0,
top.pixmap.as_ref(),
&PixmapPaint {
opacity: 1.0,
blend_mode: blend,
quality: tiny_skia::FilterQuality::Nearest,
},
Transform::identity(),
None,
);
}
fn push_transform(&mut self, transform: TtfTransform) {
let t = Transform::from_row(
transform.a,
transform.b,
transform.c,
transform.d,
transform.e,
transform.f,
);
let ctm = self.top_ctm().pre_concat(t);
self.transforms.push(ctm);
}
fn pop_transform(&mut self) {
self.transforms.pop();
}
}
fn first_base_glyph_id(colr: &[u8], glyphs: &[Vec<u8>]) -> Option<u16> {
if colr.len() < 8 {
return None;
}
let is_non_empty =
|gid: u16| -> bool { glyphs.get(gid as usize).is_some_and(|g| !g.is_empty()) };
if colr.len() >= 18 {
let v1_off =
u32::from_be_bytes([colr[14], colr[15], colr[16], colr[17]]) as usize;
if v1_off != 0 && v1_off + 4 <= colr.len() {
let num_records = u32::from_be_bytes([
colr[v1_off],
colr[v1_off + 1],
colr[v1_off + 2],
colr[v1_off + 3],
]) as usize;
let mut first_gid = None;
for i in 0..num_records {
let rec_off = v1_off + 4 + i * 6;
if rec_off + 2 > colr.len() {
break;
}
let gid = u16::from_be_bytes([colr[rec_off], colr[rec_off + 1]]);
first_gid.get_or_insert(gid);
if is_non_empty(gid) {
return Some(gid);
}
}
if let Some(g) = first_gid {
return Some(g);
}
}
}
let num_v0 = u16::from_be_bytes([colr[2], colr[3]]) as usize;
let v0_off = u32::from_be_bytes([colr[4], colr[5], colr[6], colr[7]]) as usize;
let mut first_gid = None;
for i in 0..num_v0 {
let rec_off = v0_off + i * 6;
if rec_off + 2 > colr.len() {
break;
}
let gid = u16::from_be_bytes([colr[rec_off], colr[rec_off + 1]]);
first_gid.get_or_insert(gid);
if is_non_empty(gid) {
return Some(gid);
}
}
first_gid
}
fn glyf_bbox(bytes: &[u8]) -> Option<(i16, i16, i16, i16)> {
if bytes.len() < 10 {
return None;
}
let xmin = i16::from_be_bytes([bytes[2], bytes[3]]);
let ymin = i16::from_be_bytes([bytes[4], bytes[5]]);
let xmax = i16::from_be_bytes([bytes[6], bytes[7]]);
let ymax = i16::from_be_bytes([bytes[8], bytes[9]]);
Some((xmin, ymin, xmax, ymax))
}
fn build_path(bytes: &[u8]) -> Option<Path> {
let outline = glyf_decode::decode(bytes).ok()?;
let y_max = outline.y_max as f32;
let cmds = outline.walk(1, 1.0);
if cmds.is_empty() {
return None;
}
let unflip = |y: f32| y_max - y;
let mut pb = PathBuilder::new();
for cmd in &cmds {
match *cmd {
glyf_decode::PathCmd::MoveTo { x, y } => pb.move_to(x, unflip(y)),
glyf_decode::PathCmd::LineTo { x, y } => pb.line_to(x, unflip(y)),
glyf_decode::PathCmd::QuadTo { cx, cy, x, y } => {
pb.quad_to(cx, unflip(cy), x, unflip(y))
}
glyf_decode::PathCmd::Close => pb.close(),
}
}
pb.finish()
}
fn pixmap_to_rgba(pixmap: &Pixmap) -> Vec<u8> {
let pixels = pixmap.pixels();
let mut out = Vec::with_capacity(pixels.len() * 4);
for p in pixels {
out.push(p.red());
out.push(p.green());
out.push(p.blue());
out.push(p.alpha());
}
out
}
fn rgba_to_color(c: RgbaColor) -> Color {
Color::from_rgba8(c.red, c.green, c.blue, c.alpha)
}
fn collect_stops(
iter: impl Iterator<Item = ttf_parser::colr::ColorStop>,
) -> Vec<(f32, Color)> {
let mut stops: Vec<(f32, Color)> = iter
.map(|s| (s.stop_offset, rgba_to_color(s.color)))
.collect();
stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
stops
}
fn stops_to_tiny_skia(stops: &[(f32, Color)]) -> Vec<GradientStop> {
stops
.iter()
.map(|&(o, c)| GradientStop::new(o, c))
.collect()
}
fn extend_to_spread(e: GradientExtend) -> SpreadMode {
match e {
GradientExtend::Pad => SpreadMode::Pad,
GradientExtend::Repeat => SpreadMode::Repeat,
GradientExtend::Reflect => SpreadMode::Reflect,
}
}
fn project_p3(p0: (f32, f32), p1: (f32, f32), p2: (f32, f32)) -> Option<(f32, f32)> {
let dx = p2.0 - p0.0;
let dy = p2.1 - p0.1;
let len_sq = dx * dx + dy * dy;
if !len_sq.is_finite() || len_sq < 1e-6 {
return None;
}
let bx = p1.0 - p0.0;
let by = p1.1 - p0.1;
let t = (bx * dx + by * dy) / len_sq;
Some((p1.0 - t * dx, p1.1 - t * dy))
}
fn linear_gradient_shader(lg: &TtfLinear<'_>) -> Option<Shader<'static>> {
let p0 = (lg.x0, lg.y0);
let p1 = (lg.x1, lg.y1);
let p2 = (lg.x2, lg.y2);
let (p3x, p3y) = project_p3(p0, p1, p2)?;
let stops = collect_stops(lg.stops(0, &[]));
if stops.len() < 2 {
return stops.into_iter().next().map(|(_, c)| Shader::SolidColor(c));
}
LinearGradient::new(
Point::from_xy(p0.0, p0.1),
Point::from_xy(p3x, p3y),
stops_to_tiny_skia(&stops),
extend_to_spread(lg.extend),
Transform::identity(),
)
}
fn radial_gradient_shader(rg: &TtfRadial<'_>) -> Option<Shader<'static>> {
let stops = collect_stops(rg.stops(0, &[]));
if stops.len() < 2 {
return stops.into_iter().next().map(|(_, c)| Shader::SolidColor(c));
}
RadialGradient::new(
Point::from_xy(rg.x0, rg.y0),
rg.r0.max(0.0),
Point::from_xy(rg.x1, rg.y1),
rg.r1.max(0.1),
stops_to_tiny_skia(&stops),
extend_to_spread(rg.extend),
Transform::identity(),
)
}
fn composite_mode_to_blend(mode: CompositeMode) -> BlendMode {
use CompositeMode::*;
match mode {
Clear => BlendMode::Clear,
Source => BlendMode::Source,
Destination => BlendMode::Destination,
SourceOver => BlendMode::SourceOver,
DestinationOver => BlendMode::DestinationOver,
SourceIn => BlendMode::SourceIn,
DestinationIn => BlendMode::DestinationIn,
SourceOut => BlendMode::SourceOut,
DestinationOut => BlendMode::DestinationOut,
SourceAtop => BlendMode::SourceAtop,
DestinationAtop => BlendMode::DestinationAtop,
Xor => BlendMode::Xor,
Plus => BlendMode::Plus,
Screen => BlendMode::Screen,
Overlay => BlendMode::Overlay,
Darken => BlendMode::Darken,
Lighten => BlendMode::Lighten,
ColorDodge => BlendMode::ColorDodge,
ColorBurn => BlendMode::ColorBurn,
HardLight => BlendMode::HardLight,
SoftLight => BlendMode::SoftLight,
Difference => BlendMode::Difference,
Exclusion => BlendMode::Exclusion,
Multiply => BlendMode::Multiply,
Hue => BlendMode::Hue,
Saturation => BlendMode::Saturation,
Color => BlendMode::Color,
Luminosity => BlendMode::Luminosity,
}
}
fn intersect_masks(dst: &mut Mask, src: &Mask) {
if dst.width() != src.width() || dst.height() != src.height() {
return;
}
let dst_bytes = dst.data_mut();
let src_bytes = src.data();
for (d, &s) in dst_bytes.iter_mut().zip(src_bytes.iter()) {
*d = ((*d as u16 * s as u16) / 255) as u8;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dot(a: (f32, f32), b: (f32, f32)) -> f32 {
a.0 * b.0 + a.1 * b.1
}
fn glyf_top_strip(em_top: i16, strip_height: i16) -> Vec<u8> {
let strip_bottom = em_top - strip_height;
let mut v = Vec::new();
v.extend_from_slice(&1i16.to_be_bytes()); v.extend_from_slice(&0i16.to_be_bytes()); v.extend_from_slice(&0i16.to_be_bytes()); v.extend_from_slice(&em_top.to_be_bytes()); v.extend_from_slice(&em_top.to_be_bytes()); v.extend_from_slice(&3u16.to_be_bytes()); v.extend_from_slice(&0u16.to_be_bytes()); v.extend_from_slice(&[0x01; 4]); let xs = [0i16, em_top, 0, -em_top];
let ys = [strip_bottom, 0, strip_height, 0];
for x in &xs {
v.extend_from_slice(&x.to_be_bytes());
}
for y in &ys {
v.extend_from_slice(&y.to_be_bytes());
}
v
}
#[test]
fn rasterize_mono_top_pixels_filled_bottom_pixels_empty() {
let upm = 100i16;
let bytes = glyf_top_strip(upm, upm / 4);
let r = rasterize_mono(&bytes, upm as u16, upm as u16)
.expect("rasterize succeeds for valid simple glyph");
let w = r.width as usize;
let h = r.height as usize;
assert!(w > 4 && h > 4, "bitmap should be larger than the padding");
let mid_x = w / 2;
let top_y = 2;
let bot_y = h - 2;
let top_alpha = r.data[top_y * w + mid_x];
let bot_alpha = r.data[bot_y * w + mid_x];
assert!(
top_alpha > 0,
"top of bitmap should be inked (got alpha {top_alpha})"
);
assert!(
bot_alpha == 0,
"bottom of bitmap should be empty (got alpha {bot_alpha})"
);
assert!(!r.is_color, "glyf path produces an alpha mask");
}
#[test]
fn rasterize_mono_rejects_zero_pixel_size() {
let bytes = glyf_top_strip(100, 25);
assert!(rasterize_mono(&bytes, 100, 0).is_none());
assert!(rasterize_mono(&bytes, 0, 16).is_none());
}
#[test]
fn project_p3_is_perpendicular_to_p0p2_axis_through_p0() {
let cases = [
((0.0, 0.0), (10.0, 5.0), (20.0, 0.0)),
((100.0, 100.0), (150.0, 200.0), (200.0, 100.0)),
((0.0, 0.0), (3.0, 4.0), (5.0, 0.0)),
((-50.0, 25.0), (0.0, 75.0), (50.0, 25.0)),
];
for (p0, p1, p2) in cases {
let (p3x, p3y) = project_p3(p0, p1, p2).unwrap();
let p0p3 = (p3x - p0.0, p3y - p0.1);
let p0p2 = (p2.0 - p0.0, p2.1 - p0.1);
let d = dot(p0p3, p0p2);
assert!(
d.abs() < 1e-3,
"P0P3 · P0P2 = {d} for p0={p0:?} p1={p1:?} p2={p2:?}",
);
}
}
#[test]
fn project_p3_matches_skrifa_formulation() {
let p0 = (10.0, 20.0);
let p1 = (50.0, 80.0);
let p2 = (100.0, 20.0);
let perp_x = p2.1 - p0.1; let perp_y = -(p2.0 - p0.0);
let b = (p1.0 - p0.0, p1.1 - p0.1);
let perp_len_sq = perp_x * perp_x + perp_y * perp_y;
let k = (b.0 * perp_x + b.1 * perp_y) / perp_len_sq;
let skrifa_p3 = (p0.0 + k * perp_x, p0.1 + k * perp_y);
let (our_p3x, our_p3y) = project_p3(p0, p1, p2).unwrap();
assert!((our_p3x - skrifa_p3.0).abs() < 1e-3);
assert!((our_p3y - skrifa_p3.1).abs() < 1e-3);
}
#[test]
fn project_p3_rejects_degenerate_axis() {
assert!(project_p3((10.0, 20.0), (50.0, 50.0), (10.0, 20.0)).is_none());
assert!(
project_p3((10.0, 20.0), (50.0, 50.0), (10.0 + 1e-4, 20.0 + 1e-4)).is_none()
);
}
#[test]
fn project_p3_p1_already_on_perpendicular_returns_p1() {
let p0 = (0.0, 0.0);
let p2 = (10.0, 0.0);
let p1 = (0.0, 5.0); let (p3x, p3y) = project_p3(p0, p1, p2).unwrap();
assert!((p3x - p1.0).abs() < 1e-6);
assert!((p3y - p1.1).abs() < 1e-6);
}
#[test]
fn glyf_bbox_reads_signed_bbox() {
let bytes = [
0x00, 0x01, 0xFF, 0x9C, 0xFF, 0x38, 0x01, 0x2C, 0x02, 0xBC, ];
assert_eq!(glyf_bbox(&bytes), Some((-100, -200, 300, 700)));
}
#[test]
fn glyf_bbox_rejects_short_input() {
assert_eq!(glyf_bbox(&[]), None);
assert_eq!(glyf_bbox(&[0; 9]), None);
}
fn build_colr_v1(base_glyph_ids: &[u16]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
let list_off: u32 = 34;
out.extend_from_slice(&list_off.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
assert_eq!(out.len(), list_off as usize);
out.extend_from_slice(&(base_glyph_ids.len() as u32).to_be_bytes());
for &gid in base_glyph_ids {
out.extend_from_slice(&gid.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
}
out
}
#[test]
fn first_base_glyph_id_picks_first_non_empty() {
let colr = build_colr_v1(&[0, 1]);
let glyphs: Vec<Vec<u8>> = vec![
vec![], vec![0xA, 0xB, 0xC], ];
assert_eq!(first_base_glyph_id(&colr, &glyphs), Some(1));
}
#[test]
fn first_base_glyph_id_honours_record_order() {
let colr = build_colr_v1(&[3, 1, 7]);
let glyphs: Vec<Vec<u8>> = vec![vec![1]; 10]; assert_eq!(first_base_glyph_id(&colr, &glyphs), Some(3));
}
#[test]
fn first_base_glyph_id_falls_back_to_first_record_when_all_empty() {
let colr = build_colr_v1(&[5, 10]);
let glyphs: Vec<Vec<u8>> = vec![vec![]; 20];
assert_eq!(first_base_glyph_id(&colr, &glyphs), Some(5));
}
#[test]
fn first_base_glyph_id_handles_empty_colr_table() {
assert_eq!(first_base_glyph_id(&[], &[]), None);
assert_eq!(first_base_glyph_id(&[0, 0, 0, 0, 0, 0, 0, 0], &[]), None);
}
#[test]
fn composite_mode_to_blend_covers_every_variant() {
use CompositeMode::*;
assert_eq!(composite_mode_to_blend(SourceOver), BlendMode::SourceOver);
assert_eq!(composite_mode_to_blend(Clear), BlendMode::Clear);
assert_eq!(composite_mode_to_blend(Xor), BlendMode::Xor);
assert_eq!(composite_mode_to_blend(Plus), BlendMode::Plus);
assert_eq!(composite_mode_to_blend(Multiply), BlendMode::Multiply);
assert_eq!(composite_mode_to_blend(Luminosity), BlendMode::Luminosity);
}
#[test]
fn extend_to_spread_maps_all_three_modes() {
assert_eq!(extend_to_spread(GradientExtend::Pad), SpreadMode::Pad);
assert_eq!(extend_to_spread(GradientExtend::Repeat), SpreadMode::Repeat);
assert_eq!(
extend_to_spread(GradientExtend::Reflect),
SpreadMode::Reflect
);
}
#[test]
fn intersect_masks_multiplies_alpha_channels() {
let mut dst = Mask::new(2, 2).unwrap();
let mut src = Mask::new(2, 2).unwrap();
dst.data_mut().copy_from_slice(&[255, 128, 64, 0]);
src.data_mut().copy_from_slice(&[128, 255, 128, 255]);
intersect_masks(&mut dst, &src);
assert_eq!(dst.data(), &[128, 128, 32, 0]);
}
#[test]
fn intersect_masks_ignores_mismatched_sizes() {
let mut dst = Mask::new(2, 2).unwrap();
dst.data_mut().copy_from_slice(&[0x55; 4]);
let src = Mask::new(4, 2).unwrap();
intersect_masks(&mut dst, &src);
assert_eq!(dst.data(), &[0x55; 4]);
}
}