fix(text): #8247 colour glyph review findings (clips, budgets, edge cases)

Works through the review threads on the colour glyph painter:

- Clips no longer leak into composites: a layer is painted without the
  clips around it, which apply once when the layer is merged, so a glyph
  around a SrcIn composite keeps its anti-aliased edges and Src/Clear
  composites leave pixels outside the clip alone.
- Layers, clip masks and fills count against shared budgets: the bytes
  left beside the accumulator and output (MAX_RASTER_BYTES) and the pixel
  work left of MAX_PASS_PIXELS. The fixed eight-layer cap that flattened
  deeper composites (dropping their modes) is gone.
- Clip boxes are drawn like the clip glyphs, so bent type bends them
  instead of dropping them; ink bounds take the intersection of the clips.
- Colour glyph clips follow the selected anti-alias mode.
- A colour glyph whose paint graph skrifa cannot walk draws its outline;
  one whose base glyph has no outline still paints its layers.
- Coincident colour stops switch at their offset; sweeps whose angles run
  past 360 degrees sample the right turn of the angle.
- Float documents keep extended text colours in colour glyphs.

Co-Authored-By: Aditya Vishwakarma <72960955+adi-1108@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed 2026-11-09 01:04:31 +01:00
1 parent 2eddd344eb
commit d9102e7e0a
3 files changed
+576 -173

No files matched your search

+445 -145
View File
@@ -6,42 +6,81 @@
//! the text raster in premultiplied sRGB, [`OutlineBounds`] only collects where it would paint.
use photocraft_color::blend::{BlendMode, blend_rgb};
use photocraft_doc::text::AntiAlias;
use skrifa::GlyphId;
use skrifa::color::{Brush, ColorPainter, ColorStop, CompositeMode, Extend, Transform};
use skrifa::raw::types::BoundingBox;
use crate::raster::{Coverage, LineSink, Xform, rect_to};
use crate::raster::{Bounds, Coverage, Xform};
use crate::render::shape_antialias_coverage;
/// Palette entry standing for the text colour.
const FOREGROUND: u16 = 0xFFFF;
/// Layers a glyph may stack (each is a window-sized buffer); deeper ones paint straight into the
/// layer below. skrifa itself stops at 64 nested paints.
const MAX_LAYERS: usize = 8;
fn xform(t: &Transform) -> Xform {
Xform([t.xx as f64, t.yx as f64, t.xy as f64, t.yy as f64, t.dx as f64, t.dy as f64])
}
/// Collects the outlines a colour glyph paints within (its clip glyphs and clip boxes) into a
/// [`LineSink`], for the ink bounds. `outline` draws a glyph (font units, through the given paint
/// transform) into the sink; `to_sink` maps font units to the sink for clip boxes.
pub(crate) struct OutlineBounds<'a, S: LineSink, F: FnMut(GlyphId, &Xform, &mut S)> {
pub sink: &'a mut S,
pub outline: F,
pub to_sink: Xform,
xforms: Vec<Xform>,
/// What a colour glyph clips to: a glyph outline or a COLR clip box, both in font units through
/// the paint transform. The caller draws either one the same way (bent with the type, for
/// warped text), so a clip box follows the glyph it crops.
#[derive(Clone, Copy)]
pub(crate) enum Shape {
Glyph(GlyphId),
Box(BoundingBox<f32>),
}
impl<'a, S: LineSink, F: FnMut(GlyphId, &Xform, &mut S)> OutlineBounds<'a, S, F> {
pub fn new(sink: &'a mut S, outline: F, to_sink: Xform) -> Self {
Self { sink, outline, to_sink, xforms: Vec::new() }
/// A painter that ignores everything, to check that a colour glyph's paint graph can be walked
/// before the glyph is drawn in colour ([`crate::render`] draws its outline otherwise).
pub(crate) struct NoPaint;
impl ColorPainter for NoPaint {
fn push_transform(&mut self, _: Transform) {}
fn pop_transform(&mut self) {}
fn push_clip_glyph(&mut self, _: GlyphId) {}
fn push_clip_box(&mut self, _: BoundingBox<f32>) {}
fn pop_clip(&mut self) {}
fn fill(&mut self, _: Brush<'_>) {}
fn push_layer(&mut self, _: CompositeMode) {}
}
/// Collects where a colour glyph paints, for the ink bounds: every fill adds the intersection of
/// the clips around it, so a large clip box around small artwork does not widen the bounds.
/// `outline` draws a [`Shape`] (through the given paint transform) into a [`Bounds`] in sink
/// space. A fill outside every clip is unbounded and adds nothing.
pub(crate) struct OutlineBounds<'a, F: FnMut(Shape, &Xform, &mut Bounds)> {
pub sink: &'a mut Bounds,
pub outline: F,
xforms: Vec<Xform>,
/// The intersection of the clips so far; `None` when it is empty.
clips: Vec<Option<[f64; 4]>>,
}
impl<'a, F: FnMut(Shape, &Xform, &mut Bounds)> OutlineBounds<'a, F> {
pub fn new(sink: &'a mut Bounds, outline: F) -> Self {
Self { sink, outline, xforms: Vec::new(), clips: Vec::new() }
}
fn current(&self) -> Xform {
self.xforms.last().copied().unwrap_or(Xform::IDENTITY)
}
fn push(&mut self, shape: Shape) {
let mut b = Bounds::default();
let m = self.current();
(self.outline)(shape, &m, &mut b);
let r = match (self.clips.last(), b.rect) {
(_, None) => None,
(None, r) => r,
(Some(None), _) => None,
(Some(Some(o)), Some(r)) => {
let i = [o[0].max(r[0]), o[1].max(r[1]), o[2].min(r[2]), o[3].min(r[3])];
(i[0] <= i[2] && i[1] <= i[3]).then_some(i)
}
};
self.clips.push(r);
}
}
impl<S: LineSink, F: FnMut(GlyphId, &Xform, &mut S)> ColorPainter for OutlineBounds<'_, S, F> {
impl<F: FnMut(Shape, &Xform, &mut Bounds)> ColorPainter for OutlineBounds<'_, F> {
fn push_transform(&mut self, t: Transform) {
self.xforms.push(self.current().mul(&xform(&t)));
}
@@ -49,15 +88,20 @@ impl<S: LineSink, F: FnMut(GlyphId, &Xform, &mut S)> ColorPainter for OutlineBou
self.xforms.pop();
}
fn push_clip_glyph(&mut self, id: GlyphId) {
let m = self.current();
(self.outline)(id, &m, self.sink);
self.push(Shape::Glyph(id));
}
fn push_clip_box(&mut self, b: BoundingBox<f32>) {
let m = self.to_sink.mul(&self.current());
rect_to(self.sink, &m, b.x_min as f64, b.y_min as f64, b.x_max as f64, b.y_max as f64);
self.push(Shape::Box(b));
}
fn pop_clip(&mut self) {
self.clips.pop();
}
fn fill(&mut self, _: Brush<'_>) {
if let Some(Some([x0, y0, x1, y1])) = self.clips.last().copied() {
self.sink.add((x0, y0));
self.sink.add((x1, y1));
}
}
fn pop_clip(&mut self) {}
fn fill(&mut self, _: Brush<'_>) {}
fn push_layer(&mut self, _: CompositeMode) {}
}
@@ -81,20 +125,19 @@ impl ColorLine {
}
_ => t,
};
if t.is_nan() || t <= first.0 {
if t.is_nan() || t < first.0 {
return first.1;
}
if t >= last.0 {
return last.1;
}
for pair in self.stops.windows(2) {
let [(o0, c0), (o1, c1)] = [pair[0], pair[1]];
if t <= o1 {
let k = if o1 > o0 { (t - o0) / (o1 - o0) } else { 1.0 };
return std::array::from_fn(|i| c0[i] + (c1[i] - c0[i]) * k);
}
}
last.1
// The last stop at or before `t`: where several stops share an offset, the last of them
// holds from that offset on (OpenType COLR, colour lines), so a pixel landing exactly on
// a hard transition takes the colour after it. The next stop then lies strictly after.
let i = self.stops.partition_point(|s| s.0 <= t);
let (Some(&(o0, c0)), Some(&(o1, c1))) = (i.checked_sub(1).and_then(|i| self.stops.get(i)), self.stops.get(i)) else { return last.1 };
let k = if o1 > o0 { (t - o0) / (o1 - o0) } else { 1.0 };
std::array::from_fn(|i| c0[i] + (c1[i] - c0[i]) * k)
}
}
@@ -136,7 +179,13 @@ impl Paint {
Paint::Sweep { c, start, end, line } => {
// skrifa hands the angles clockwise (360° minus the font's counter-clockwise ones).
let ccw = (v - c.1).atan2(u - c.0).to_degrees();
let cw = (360.0 - ccw).rem_euclid(360.0);
// skrifa's start and end may lie outside 0..360 (a sweep from 270° to 450°
// crosses the positive x axis): take the turn of the angle that falls inside them.
let mut cw = (360.0 - ccw).rem_euclid(360.0);
let turn = cw + 360.0 * ((start - cw) / 360.0).ceil();
if turn.is_finite() && turn <= *end {
cw = turn;
}
let span = end - start;
let t = if span.abs() > 1e-6 {
(cw - start) / span
@@ -202,29 +251,46 @@ fn composite(mode: CompositeMode, b: [f32; 4], s: [f32; 4]) -> [f32; 4] {
std::array::from_fn(|i| (s[i] * fs + b[i] * fb).min(1.0))
}
/// Paints one colour glyph into a `w × h` window, premultiplied sRGB. `outline` adds a glyph's
/// coverage (font units through the given paint transform) to a window-sized [`Coverage`];
/// `to_window` maps font units into the window (gradients and clip boxes).
pub(crate) struct Painter<'a, F: FnMut(GlyphId, &Xform, &mut Coverage)> {
/// What one render may still spend on colour glyphs, shared by all of them: `bytes` of buffers
/// live at once (the window layers and clip masks), and `work`, pixels touched by fills, clips
/// and composites, against the same cumulative budget as the plain colour passes. A glyph that
/// would overrun either one fails, and the text renders nothing, as with the other budgets.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Limits {
pub bytes: u64,
pub work: u64,
}
/// The colours a glyph paints with.
pub(crate) struct Ink<'a> {
pub palette: &'a [skrifa::color::Color],
/// The text colour, straight sRGB and alpha.
pub foreground: [f32; 4],
}
/// A window-sized layer and how many clips were already pushed when it was opened: clips from
/// outside a layer do not shape what is painted into it, they apply once when it is merged down.
struct Layer {
px: Vec<[f32; 4]>,
clip_base: usize,
}
/// Paints one colour glyph into a `w × h` window, premultiplied sRGB. `outline` adds a
/// [`Shape`]'s coverage (through the given paint transform) to a window-sized [`Coverage`];
/// `to_window` maps font units into the window (gradients).
pub(crate) struct Painter<'a, F: FnMut(Shape, &Xform, &mut Coverage)> {
w: usize,
h: usize,
outline: F,
to_window: Xform,
palette: &'a [skrifa::color::Color],
/// The text colour, straight sRGB and alpha.
foreground: [f32; 4],
/// Anti-aliasing off: coverage is 0 or 1.
hard: bool,
ink: Ink<'a>,
antialias: AntiAlias,
limits: Limits,
xforms: Vec<Xform>,
clips: Vec<Vec<f32>>,
/// Whether COLR clip boxes clip. Off for bent type: the outlines are bent but a clip box
/// would stay a straight rectangle and cut them off. Gradients under a bend are laid out
/// unbent either way.
clip_boxes: bool,
layers: Vec<Vec<[f32; 4]>>,
/// `push_layer` calls past [`MAX_LAYERS`], painted into the layer below.
flat: usize,
/// A buffer could not be reserved: [`Painter::finish`] returns `None`.
layers: Vec<Layer>,
/// A buffer could not be reserved or a budget ran out: [`Painter::finish`] returns `None`.
/// Placeholders (empty buffers) keep pushes and pops balanced and paint nothing.
failed: bool,
}
@@ -236,50 +302,100 @@ fn filled<T: Clone>(len: usize, v: T) -> Option<Vec<T>> {
Some(out)
}
impl<'a, F: FnMut(GlyphId, &Xform, &mut Coverage)> Painter<'a, F> {
pub fn new(w: usize, h: usize, outline: F, to_window: Xform, palette: &'a [skrifa::color::Color], foreground: [f32; 4], hard: bool) -> Self {
let base = w.checked_mul(h).and_then(|n| filled(n, [0.0; 4]));
let failed = base.is_none();
let layers = base.into_iter().collect();
Self { w, h, outline, to_window, palette, foreground, hard, xforms: Vec::new(), clips: Vec::new(), clip_boxes: true, layers, flat: 0, failed }
impl<'a, F: FnMut(Shape, &Xform, &mut Coverage)> Painter<'a, F> {
pub fn new(w: usize, h: usize, outline: F, to_window: Xform, ink: Ink<'a>, antialias: AntiAlias, limits: Limits) -> Self {
let mut p = Self { w, h, outline, to_window, ink, antialias, limits, xforms: Vec::new(), clips: Vec::new(), layers: Vec::new(), failed: false };
let base = p.new_layer().unwrap_or_default();
p.layers.push(Layer { px: base, clip_base: 0 });
p
}
/// Leaves COLR clip boxes out (bent type); see [`Painter::clip_boxes`].
pub fn with_clip_boxes(mut self, on: bool) -> Self {
self.clip_boxes = on;
self
/// The pixel work left after this glyph; its buffers' bytes come back once it is done.
pub fn work_left(&self) -> u64 {
self.limits.work
}
/// The painted glyph, premultiplied sRGB, row-major, or `None` when a buffer could not be
/// reserved.
/// reserved or a budget ran out.
pub fn finish(mut self) -> Option<Vec<[f32; 4]>> {
if self.failed {
return None;
}
self.layers.truncate(1);
self.layers.pop()
self.layers.pop().map(|l| l.px)
}
/// Records a buffer that could not be reserved; the empty clip keeps push/pop balanced and
/// paints nothing.
fn fail_clip(&mut self) {
self.failed = true;
self.clips.push(Vec::new());
fn pixels(&self) -> Option<u64> {
(self.w as u64).checked_mul(self.h as u64)
}
/// Takes `bytes` of buffers and, with `work`, one window's worth of pixel work from the
/// budgets; `false` (and the painter failed) when they don't fit or it failed already.
fn spend(&mut self, bytes: Option<u64>, work: bool) -> bool {
let work = if work { self.pixels() } else { Some(0) };
match (bytes.and_then(|b| self.limits.bytes.checked_sub(b)), work.and_then(|n| self.limits.work.checked_sub(n))) {
(Some(b), Some(w)) if !self.failed => {
self.limits = Limits { bytes: b, work: w };
true
}
_ => {
self.failed = true;
false
}
}
}
/// Returns the bytes of `len` elements of `each` bytes to the budget.
fn give_back(&mut self, len: usize, each: usize) {
let b = (len as u64).saturating_mul(each as u64);
self.limits.bytes = self.limits.bytes.saturating_add(b);
}
/// A transparent window-sized layer, its bytes taken from the budget.
fn new_layer(&mut self) -> Option<Vec<[f32; 4]>> {
let bytes = self.pixels().and_then(|n| n.checked_mul(size_of::<[f32; 4]>() as u64));
if !self.spend(bytes, false) {
return None;
}
let v = self.w.checked_mul(self.h).and_then(|n| filled(n, [0.0; 4]));
self.failed |= v.is_none();
v
}
fn current(&self) -> Xform {
self.xforms.last().copied().unwrap_or(Xform::IDENTITY)
}
fn push_coverage(&mut self, cov: Coverage) {
let Some(mut c) = cov.finish() else {
self.fail_clip();
/// The innermost clip pushed inside the current layer, if any.
fn layer_clip(&self) -> Option<&Vec<f32>> {
let base = self.layers.last().map_or(0, |l| l.clip_base);
if self.clips.len() > base { self.clips.last() } else { None }
}
/// Rasterises `shape` into a clip mask, intersected with the clip around it in this layer.
fn push_shape(&mut self, shape: Shape) {
// A mask is kept at one f32 per pixel; while it is built, the coverage accumulator and
// the finished vector are live side by side.
let mask = self.pixels().and_then(|n| n.checked_mul(size_of::<f32>() as u64));
let cov = if self.spend(mask.and_then(|m| m.checked_mul(2)), true) { Coverage::try_new(self.w, self.h) } else { None };
let Some(mut cov) = cov else {
self.failed = true;
self.clips.push(Vec::new());
return;
};
if self.hard {
c.iter_mut().for_each(|v| *v = if *v >= 0.5 { 1.0 } else { 0.0 });
}
if let Some(outer) = self.clips.last() {
let m = self.current();
(self.outline)(shape, &m, &mut cov);
let c = cov.finish();
// The accumulator is gone again: its half of the reservation comes back.
self.give_back(self.w.saturating_mul(self.h), size_of::<f32>());
let Some(mut c) = c else {
self.failed = true;
self.clips.push(Vec::new());
return;
};
let mode = self.antialias;
c.iter_mut().for_each(|v| *v = shape_antialias_coverage(*v, mode));
if let Some(outer) = self.layer_clip() {
c.iter_mut().zip(outer).for_each(|(v, o)| *v *= o);
}
self.clips.push(c);
@@ -288,9 +404,9 @@ impl<'a, F: FnMut(GlyphId, &Xform, &mut Coverage)> Painter<'a, F> {
/// A palette entry (or the text colour), premultiplied, times `alpha`.
fn color(&self, index: u16, alpha: f32) -> [f32; 4] {
let [r, g, b, a] = if index == FOREGROUND {
self.foreground
self.ink.foreground
} else {
match self.palette.get(usize::from(index)) {
match self.ink.palette.get(usize::from(index)) {
Some(c) => [c.red, c.green, c.blue, c.alpha].map(|v| f32::from(v) / 255.0),
None => return [0.0; 4],
}
@@ -333,7 +449,7 @@ impl<'a, F: FnMut(GlyphId, &Xform, &mut Coverage)> Painter<'a, F> {
}
}
impl<F: FnMut(GlyphId, &Xform, &mut Coverage)> ColorPainter for Painter<'_, F> {
impl<F: FnMut(Shape, &Xform, &mut Coverage)> ColorPainter for Painter<'_, F> {
fn push_transform(&mut self, t: Transform) {
self.xforms.push(self.current().mul(&xform(&t)));
}
@@ -341,47 +457,27 @@ impl<F: FnMut(GlyphId, &Xform, &mut Coverage)> ColorPainter for Painter<'_, F> {
self.xforms.pop();
}
fn push_clip_glyph(&mut self, id: GlyphId) {
let Some(mut cov) = Coverage::try_new(self.w, self.h) else {
self.fail_clip();
return;
};
let m = self.current();
(self.outline)(id, &m, &mut cov);
self.push_coverage(cov);
self.push_shape(Shape::Glyph(id));
}
fn push_clip_box(&mut self, b: BoundingBox<f32>) {
if !self.clip_boxes {
// Keep push/pop balanced: the clip stays what it was.
let same = match self.clips.last() {
Some(c) => filled(c.len(), 0.0).map(|mut v| {
v.copy_from_slice(c);
v
}),
None => self.w.checked_mul(self.h).and_then(|n| filled(n, 1.0)),
};
match same {
Some(same) => self.clips.push(same),
None => self.fail_clip(),
}
return;
}
let Some(mut cov) = Coverage::try_new(self.w, self.h) else {
self.fail_clip();
return;
};
rect_to(&mut cov, &self.to_window.mul(&self.current()), b.x_min as f64, b.y_min as f64, b.x_max as f64, b.y_max as f64);
self.push_coverage(cov);
self.push_shape(Shape::Box(b));
}
fn pop_clip(&mut self) {
self.clips.pop();
if let Some(c) = self.clips.pop() {
self.give_back(c.len(), size_of::<f32>());
}
}
fn fill(&mut self, brush: Brush<'_>) {
if !self.spend(Some(0), true) {
return;
}
let Some(paint) = self.paint(brush) else { return };
let to_paint = self.to_window.mul(&self.current()).invert();
let w = self.w;
let clip = self.clips.last();
let base = self.layers.last().map_or(0, |l| l.clip_base);
let clip = if self.clips.len() > base { self.clips.last() } else { None };
let Some(layer) = self.layers.last_mut() else { return };
for (i, px) in layer.iter_mut().enumerate() {
for (i, px) in layer.px.iter_mut().enumerate() {
let k = clip.map_or(1.0, |c| c.get(i).copied().unwrap_or(0.0));
if k <= 0.0 {
continue;
@@ -402,28 +498,30 @@ impl<F: FnMut(GlyphId, &Xform, &mut Coverage)> ColorPainter for Painter<'_, F> {
}
}
fn push_layer(&mut self, _: CompositeMode) {
if self.layers.len() >= MAX_LAYERS || self.flat > 0 {
self.flat += 1;
} else if let Some(layer) = self.w.checked_mul(self.h).and_then(|n| filled(n, [0.0; 4])) {
self.layers.push(layer);
} else {
// Painted into the layer below, and the result is dropped by `finish`.
self.failed = true;
self.flat += 1;
}
// No fixed depth: skrifa stops at 64 nested paints, and the byte budget bounds the rest.
let px = self.new_layer().unwrap_or_default();
self.layers.push(Layer { px, clip_base: self.clips.len() });
}
fn pop_layer_with_mode(&mut self, mode: CompositeMode) {
if self.flat > 0 {
self.flat -= 1;
return;
}
if self.layers.len() < 2 {
return;
}
let Some(src) = self.layers.pop() else { return };
self.give_back(src.px.len(), size_of::<[f32; 4]>());
if !self.spend(Some(0), true) {
return;
}
// The clips around the merged layer (inside the one below) apply once, to the result.
let base = self.layers.last().map_or(0, |l| l.clip_base);
let clip = if self.clips.len() > base { self.clips.last() } else { None };
let Some(dst) = self.layers.last_mut() else { return };
for (b, s) in dst.iter_mut().zip(&src) {
*b = composite(mode, *b, *s);
for (i, (b, s)) in dst.px.iter_mut().zip(&src.px).enumerate() {
let k = clip.map_or(1.0, |c| c.get(i).copied().unwrap_or(0.0));
if k <= 0.0 {
continue;
}
let c = composite(mode, *b, *s);
*b = std::array::from_fn(|j| b[j] + (c[j] - b[j]) * k);
}
}
}
@@ -431,6 +529,7 @@ impl<F: FnMut(GlyphId, &Xform, &mut Coverage)> ColorPainter for Painter<'_, F> {
#[cfg(test)]
mod tests {
use super::*;
use crate::raster::rect_to;
const RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
const BLUE: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
@@ -467,45 +566,246 @@ mod tests {
assert_eq!(wide.at(1.5), BLUE);
}
const RED_PAL: [skrifa::color::Color; 2] =
[skrifa::color::Color { red: 255, green: 0, blue: 0, alpha: 255 }, skrifa::color::Color { red: 0, green: 0, blue: 255, alpha: 255 }];
const PLENTY: Limits = Limits { bytes: u64::MAX, work: u64::MAX };
fn ink() -> Ink<'static> {
Ink { palette: &RED_PAL, foreground: [0.0, 0.0, 0.0, 1.0] }
}
/// Draws every shape as given, glyph `n` as the rectangle 0..n x 0..8 (window pixels).
fn shapes(shape: Shape, m: &Xform, c: &mut Coverage) {
match shape {
Shape::Glyph(id) => rect_to(c, m, 0.0, 0.0, f64::from(id.to_u32()) * 0.5, 8.0),
Shape::Box(b) => rect_to(c, m, f64::from(b.x_min), f64::from(b.y_min), f64::from(b.x_max), f64::from(b.y_max)),
}
}
fn solid(index: u16) -> Brush<'static> {
Brush::Solid { palette_index: index, alpha: 1.0 }
}
#[test]
fn clip_boxes_can_be_left_out_for_bent_type() {
// Under a bending warp the outlines are bent but a clip box would stay a straight
// rectangle and cut the bent glyph off, so bent type leaves clip boxes out.
let palette = [skrifa::color::Color { red: 255, green: 0, blue: 0, alpha: 255 }];
let paint = |clip_boxes: bool| {
let full = |_: GlyphId, _: &Xform, c: &mut Coverage| rect_to(c, &Xform::IDENTITY, 0.0, 0.0, 8.0, 8.0);
let mut p = Painter::new(8, 8, full, Xform::IDENTITY, &palette, [0.0, 0.0, 0.0, 1.0], false).with_clip_boxes(clip_boxes);
p.push_clip_box(BoundingBox { x_min: 0.0, y_min: 0.0, x_max: 2.0, y_max: 2.0 });
p.push_clip_glyph(GlyphId::new(1));
p.fill(Brush::Solid { palette_index: 0, alpha: 1.0 });
p.pop_clip();
p.pop_clip();
p.finish().unwrap_or_default()
};
let (clipped, bent) = (paint(true), paint(false));
// Pixel (5, 5) lies outside the 2x2 clip box.
assert_eq!(clipped.get(5 * 8 + 5).map(|c| c[3]), Some(0.0));
assert_eq!(bent.get(5 * 8 + 5).map(|c| c[3]), Some(1.0));
assert_eq!(bent.first().map(|c| c[3]), Some(1.0));
fn clip_boxes_are_drawn_the_way_the_glyphs_are() {
// Clip boxes go through the same drawing as the clip glyphs, so bent type bends its clip
// boxes too instead of dropping them. A stand-in "bend" here shifts everything 4 px right.
let bent = |shape: Shape, m: &Xform, c: &mut Coverage| shapes(shape, &Xform([1.0, 0.0, 0.0, 1.0, 4.0, 0.0]).mul(m), c);
let mut p = Painter::new(8, 8, bent, Xform::IDENTITY, ink(), AntiAlias::Smooth, PLENTY);
p.push_clip_box(BoundingBox { x_min: 0.0, y_min: 0.0, x_max: 2.0, y_max: 2.0 });
p.push_clip_glyph(GlyphId::new(16));
p.fill(solid(0));
p.pop_clip();
p.pop_clip();
let px = p.finish().unwrap_or_default();
// The box moved with the shapes: 4..6 is painted, 0..2 and the rest are not.
assert_eq!(px.get(8 + 5).map(|c| c[3]), Some(1.0));
assert_eq!(px.get(8 + 1).map(|c| c[3]), Some(0.0));
assert_eq!(px.get(5 * 8 + 5).map(|c| c[3]), Some(0.0));
}
#[test]
fn a_window_too_large_to_reserve_paints_nothing() {
// Coverage buffers are fallible on main (#4979); a colour glyph whose window cannot be
// reserved gives `None` instead of aborting, and its clips stay balanced.
let palette = [skrifa::color::Color { red: 255, green: 0, blue: 0, alpha: 255 }];
let full = |_: GlyphId, _: &Xform, c: &mut Coverage| rect_to(c, &Xform::IDENTITY, 0.0, 0.0, 8.0, 8.0);
let mut p = Painter::new(usize::MAX / 2, 4, full, Xform::IDENTITY, &palette, [0.0, 0.0, 0.0, 1.0], false);
let mut p = Painter::new(usize::MAX / 2, 4, shapes, Xform::IDENTITY, ink(), AntiAlias::Smooth, PLENTY);
p.push_clip_box(BoundingBox { x_min: 0.0, y_min: 0.0, x_max: 2.0, y_max: 2.0 });
p.push_clip_glyph(GlyphId::new(1));
p.push_layer(CompositeMode::SrcOver);
p.fill(Brush::Solid { palette_index: 0, alpha: 1.0 });
p.fill(solid(0));
p.pop_layer_with_mode(CompositeMode::SrcOver);
p.pop_clip();
p.pop_clip();
assert!(p.finish().is_none());
}
#[test]
fn layers_and_clips_count_against_the_byte_budget() {
// An 8x8 window: 1 KiB per layer, 256 bytes per clip mask (512 while it is built).
let run = |bytes: u64| {
let mut p = Painter::new(8, 8, shapes, Xform::IDENTITY, ink(), AntiAlias::Smooth, Limits { bytes, work: u64::MAX });
p.push_clip_glyph(GlyphId::new(16));
for _ in 0..3 {
p.push_layer(CompositeMode::SrcOver);
}
p.fill(solid(0));
for _ in 0..3 {
p.pop_layer_with_mode(CompositeMode::SrcOver);
}
p.pop_clip();
p.finish()
};
// Base layer, mask and three layers live at once.
assert!(run(4 * 1024 + 512).is_some());
assert!(run(4 * 1024 + 255).is_none());
// Buffers come back when popped: one more layer later on fits in the same budget.
let mut p = Painter::new(8, 8, shapes, Xform::IDENTITY, ink(), AntiAlias::Smooth, Limits { bytes: 2 * 1024, work: u64::MAX });
for _ in 0..5 {
p.push_layer(CompositeMode::SrcOver);
p.fill(solid(0));
p.pop_layer_with_mode(CompositeMode::SrcOver);
}
assert!(p.finish().is_some());
}
#[test]
fn colour_glyph_work_is_capped() {
// Each fill, clip and merge touches the whole 8x8 window (64 px of work).
let run = |work: u64| {
let mut p = Painter::new(8, 8, shapes, Xform::IDENTITY, ink(), AntiAlias::Smooth, Limits { bytes: u64::MAX, work });
for _ in 0..3 {
p.fill(solid(0));
}
let left = p.work_left();
(p.finish().is_some(), left)
};
assert_eq!(run(3 * 64), (true, 0));
assert!(!run(3 * 64 - 1).0);
}
#[test]
fn deep_composites_keep_their_modes() {
// Past what used to be an eight-layer cap, a nested Clear still clears: the red fill
// underneath must not show.
let mut p = Painter::new(4, 4, shapes, Xform::IDENTITY, ink(), AntiAlias::Smooth, PLENTY);
for _ in 0..9 {
p.push_layer(CompositeMode::SrcOver);
}
p.fill(solid(0));
p.push_layer(CompositeMode::Clear);
p.fill(solid(1));
p.pop_layer_with_mode(CompositeMode::Clear);
for _ in 0..9 {
p.pop_layer_with_mode(CompositeMode::SrcOver);
}
let px = p.finish().unwrap_or_default();
assert_eq!(px.len(), 16);
assert!(px.iter().all(|c| c[3] == 0.0), "{:?}", px.first());
}
#[test]
fn a_glyph_clip_applies_once_to_a_composite_inside_it() {
// A PaintGlyph around a SrcIn composite: the edge pixel half covered by the glyph ends
// up half opaque, not a quarter (the clip on both the backdrop and the source).
let mut p = Painter::new(
2,
1,
|s: Shape, m: &Xform, c: &mut Coverage| {
if let Shape::Glyph(_) = s {
rect_to(c, m, 0.0, 0.0, 1.5, 1.0);
}
},
Xform::IDENTITY,
ink(),
AntiAlias::Smooth,
PLENTY,
);
p.push_clip_glyph(GlyphId::new(1));
p.push_layer(CompositeMode::SrcOver);
p.fill(solid(0));
p.push_layer(CompositeMode::SrcIn);
p.fill(solid(1));
p.pop_layer_with_mode(CompositeMode::SrcIn);
p.pop_layer_with_mode(CompositeMode::SrcOver);
p.pop_clip();
let px = p.finish().unwrap_or_default();
assert_eq!(px.first().map(|c| c[3]), Some(1.0));
let edge = px.get(1).copied().unwrap_or_default();
assert!((edge[3] - 0.5).abs() < 1e-5 && (edge[2] - 0.5).abs() < 1e-5, "{edge:?}");
}
#[test]
fn a_composite_leaves_pixels_outside_its_clip_alone() {
// Red everywhere, then a Src composite of blue inside a clip over the left pixel only:
// Src replaces what is below, but only where the clip lets it.
let mut p = Painter::new(
2,
1,
|s: Shape, m: &Xform, c: &mut Coverage| {
if let Shape::Glyph(_) = s {
rect_to(c, m, 0.0, 0.0, 1.0, 1.0);
}
},
Xform::IDENTITY,
ink(),
AntiAlias::Smooth,
PLENTY,
);
p.fill(solid(0));
p.push_clip_glyph(GlyphId::new(1));
p.push_layer(CompositeMode::Src);
p.fill(solid(1));
p.pop_layer_with_mode(CompositeMode::Src);
p.pop_clip();
let px = p.finish().unwrap_or_default();
assert_eq!(px, vec![BLUE, RED]);
}
#[test]
fn colour_glyph_edges_follow_the_anti_alias_mode() {
// A clip covering 0.5 of a pixel: Smooth keeps 0.5, Sharp thins it as the plain passes do.
let paint = |mode| {
let mut p = Painter::new(1, 1, |_: Shape, m: &Xform, c: &mut Coverage| rect_to(c, m, 0.0, 0.0, 0.5, 1.0), Xform::IDENTITY, ink(), mode, PLENTY);
p.push_clip_glyph(GlyphId::new(1));
p.fill(solid(0));
p.pop_clip();
p.finish().unwrap_or_default().first().map_or(0.0, |c| c[3])
};
assert!((paint(AntiAlias::Smooth) - 0.5).abs() < 1e-5);
assert!((paint(AntiAlias::Sharp) - shape_antialias_coverage(0.5, AntiAlias::Sharp)).abs() < 1e-5);
assert!(paint(AntiAlias::Sharp) < 0.3);
assert_eq!(paint(AntiAlias::None), 1.0);
}
#[test]
fn ink_bounds_take_the_intersection_of_the_clips() {
// A small glyph inside a huge clip box: the bounds are the glyph's, not the box's.
let mut b = Bounds::default();
let to = |s: Shape, m: &Xform, b: &mut Bounds| match s {
Shape::Glyph(_) => rect_to(b, m, 0.0, 0.0, 8.0, 8.0),
Shape::Box(r) => rect_to(b, m, f64::from(r.x_min), f64::from(r.y_min), f64::from(r.x_max), f64::from(r.y_max)),
};
let mut o = OutlineBounds::new(&mut b, to);
o.push_clip_box(BoundingBox { x_min: -1e6, y_min: -1e6, x_max: 1e6, y_max: 1e6 });
o.push_clip_glyph(GlyphId::new(1));
o.fill(solid(0));
o.pop_clip();
// Clips that don't overlap paint nothing.
o.push_clip_box(BoundingBox { x_min: 100.0, y_min: 100.0, x_max: 200.0, y_max: 200.0 });
o.push_clip_glyph(GlyphId::new(1));
o.fill(solid(0));
o.pop_clip();
o.pop_clip();
o.pop_clip();
assert_eq!(b.rect, Some([0.0, 0.0, 8.0, 8.0]));
}
#[test]
fn coincident_stops_switch_at_their_offset() {
// A hard red-to-blue step at 0.5: exactly at 0.5 the colour after the step holds.
let step = ColorLine { stops: vec![(0.0, RED), (0.5, RED), (0.5, BLUE), (1.0, BLUE)], extend: Extend::Pad };
assert_eq!(step.at(0.5), BLUE);
assert_eq!(step.at(0.499), RED);
assert_eq!(step.at(0.0), RED);
// Several stops at the very start: the last of them holds from there.
let start = ColorLine { stops: vec![(0.0, RED), (0.0, BLUE), (1.0, BLUE)], extend: Extend::Pad };
assert_eq!(start.at(0.0), BLUE);
assert_eq!(start.at(-1.0), RED);
}
#[test]
fn sweep_angles_past_360_reach_the_positive_x_axis() {
// skrifa's 270°..450° (clockwise) crosses the positive x axis at 360°, the midpoint.
let p = Paint::Sweep { c: (0.0, 0.0), start: 270.0, end: 450.0, line: line(Extend::Pad) };
let mid = p.at(1.0, 0.0).unwrap_or_default();
assert!((mid[0] - 0.5).abs() < 1e-4 && (mid[2] - 0.5).abs() < 1e-4, "{mid:?}");
// A sweep starting below 0°: 350° is -10°, a sixth of the way through -30°..30°.
let p = Paint::Sweep { c: (0.0, 0.0), start: -30.0, end: 30.0, line: line(Extend::Pad) };
let ccw10 = 10f32.to_radians();
let c = p.at(ccw10.cos(), ccw10.sin()).unwrap_or_default();
assert!((c[2] - 1.0 / 3.0).abs() < 1e-3, "{c:?}");
}
#[test]
fn radial_gradient_is_the_two_point_cone() {
// Concentric circles, radius 0 to 10: t is the distance over 10, red at the centre.
+69
View File
@@ -236,3 +236,72 @@ fn colr_v1_gradient_runs_across_the_glyph() {
}
}
}
/// A COLR v1 table whose `base` paints itself (PaintColrGlyph), a cycle skrifa refuses to walk.
fn colr_v1_cycle(base: u16) -> Vec<u8> {
let mut t = Vec::new();
for v in [1u16, 0] {
t.extend_from_slice(&v.to_be_bytes());
}
t.extend_from_slice(&0u32.to_be_bytes());
t.extend_from_slice(&0u32.to_be_bytes());
t.extend_from_slice(&0u16.to_be_bytes());
t.extend_from_slice(&34u32.to_be_bytes());
for _ in 0..4 {
t.extend_from_slice(&0u32.to_be_bytes());
}
t.extend_from_slice(&1u32.to_be_bytes());
t.extend_from_slice(&base.to_be_bytes());
t.extend_from_slice(&10u32.to_be_bytes());
// PaintColrGlyph (format 11) of itself.
t.push(11);
t.extend_from_slice(&base.to_be_bytes());
t
}
#[test]
fn a_colour_glyph_skrifa_cannot_walk_draws_its_outline() {
// A paint graph that loops back on itself fails in skrifa; the glyph keeps its plain
// outline in the text colour instead of vanishing.
let inter = fonts::INTER_REGULAR.as_slice();
let h = gid(inter, 'H');
let font = with_tables(inter, &[(*b"COLR", colr_v1_cycle(h)), (*b"CPAL", cpal(&[RED]))]);
let green = Color::rgb(0.0, 1.0, 0.0);
let px = pixels(&layout_with("H", font, green), PixelFormat::RGBA8);
let solid: Vec<_> = px.iter().filter(|p| p[3] > 0.99).collect();
assert!(solid.len() > 100, "{} pixels", solid.len());
assert!(solid.iter().all(|p| p[1] > 0.98 && p[0] < 0.02), "{:?}", solid.first());
}
#[test]
fn a_colour_glyph_without_a_base_outline_still_paints() {
// The base glyph has no outline of its own (no glyf entry); its colour layer does.
let inter = fonts::INTER_REGULAR.as_slice();
let h = gid(inter, 'H');
let base = 60000u16;
let font = with_tables(inter, &[(*b"COLR", colr_v0(base, &[(h, 0)])), (*b"CPAL", cpal(&[RED]))]);
assert!(FontRef::new(&font).is_ok_and(|f| f.outline_glyphs().get(skrifa::GlyphId::new(base.into())).is_none()));
let mut l = layout_with("H", font, Color::BLACK);
for g in &mut l.glyphs {
g.id = base.into();
}
let px = pixels(&l, PixelFormat::RGBA8);
let red = px.iter().filter(|p| p[3] > 0.99 && p[0] > 0.98 && p[1] < 0.02).count();
assert!(red > 100, "{red} red pixels");
}
#[test]
fn colour_glyphs_keep_extended_text_colours_in_float_documents() {
// A layer in the text colour (palette entry 0xFFFF) keeps HDR and signed components in a
// float document, as plain glyphs do, and stays bounded in integer ones.
let inter = fonts::INTER_REGULAR.as_slice();
let h = gid(inter, 'H');
let font = with_tables(inter, &[(*b"COLR", colr_v0(h, &[(h, 0xFFFF)])), (*b"CPAL", cpal(&[RED]))]);
let color = Color::rgba(2.5, -0.25, 0.5, 1.0);
let float = pixels(&layout_with("H", font.clone(), color), PixelFormat::RGBA32F);
let covered: Vec<_> = float.iter().filter(|p| p[3] > 0.01).collect();
assert!(covered.len() > 100);
assert!(covered.iter().all(|p| (p[0] - 2.5).abs() < 1e-4 && (p[1] + 0.25).abs() < 1e-4), "{:?}", covered.first());
let int = pixels(&layout_with("H", font, color), PixelFormat::RGBA8);
assert!(int.iter().filter(|p| p[3] > 0.01).all(|p| p[0] == 1.0 && p[1] == 0.0));
}
+62 -28
View File
@@ -8,7 +8,7 @@ use skrifa::instance::{LocationRef, NormalizedCoord, Size};
use skrifa::outline::DrawSettings;
use skrifa::{GlyphId, MetadataProvider};
use crate::color_glyphs::{OutlineBounds, Painter};
use crate::color_glyphs::{Ink, Limits, NoPaint, OutlineBounds, Painter, Shape};
use crate::layout::{DecorationRect, GlyphOrient, PlacedGlyph, TextLayout};
use crate::raster::{Bounds, Coverage, LineSink, Pen, Xform, rect_to};
use crate::warp::{Placement, Warp};
@@ -62,7 +62,7 @@ fn nothing(format: PixelFormat) -> Rendered {
/// Maps raw area-coverage to the opacity Photoshop's anti-alias modes imply. Exponents above 1 thin
/// the fringe (sharper); below 1 widen it (stronger smoothing).
fn shape_antialias_coverage(cv: f32, mode: AntiAlias) -> f32 {
pub(crate) fn shape_antialias_coverage(cv: f32, mode: AntiAlias) -> f32 {
if !cv.is_finite() {
return 0.0;
}
@@ -124,14 +124,15 @@ fn glyph_is_placed(layout: &TextLayout, g: &PlacedGlyph, warp: Option<&Warp>) ->
let Ok(font) = skrifa::FontRef::from_index(face.font.data.as_ref(), face.font.index) else {
return false;
};
if font.outline_glyphs().get(GlyphId::new(g.id)).is_none() {
let coords: Vec<NormalizedCoord> = face.coords.iter().map(|&c| NormalizedCoord::from_bits(c)).collect();
// A colour glyph paints its layers, so it counts even when its base glyph has no outline.
if font.outline_glyphs().get(GlyphId::new(g.id)).is_none() && color_glyph(&font, GlyphId::new(g.id), &coords).is_none() {
return false;
}
if let Some(w) = warp {
let coords: Vec<NormalizedCoord> = face.coords.iter().map(|&c| NormalizedCoord::from_bits(c)).collect();
if matches!(w.place_glyph(glyph_center(layout, g, &font, face.size_px, &coords)), Placement::Hidden) {
return false;
}
if let Some(w) = warp
&& matches!(w.place_glyph(glyph_center(layout, g, &font, face.size_px, &coords)), Placement::Hidden)
{
return false;
}
true
}
@@ -201,13 +202,17 @@ fn draw(layout: &TextLayout, transform: &Xform, sink: &mut impl LineSink, only:
let coords: Vec<NormalizedCoord> = face.coords.iter().map(|&c| NormalizedCoord::from_bits(c)).collect();
// Colour glyphs paint in their own colours ([`paint_color_glyphs`]); only their extent
// counts here.
if let Some(color) = font.color_glyphs().get(GlyphId::new(g.id)) {
if let Some(color) = color_glyph(&font, GlyphId::new(g.id), &coords) {
if only.is_none()
&& let Some((glyph, bend)) = place(layout, g, &font, face.size_px, &coords, warp)
{
let unit = font_units(&font, face.size_px, &coords, glyph);
let outline = |id: GlyphId, m: &Xform, s: &mut _| outline_into(&font, &coords, id, unit.mul(m), transform, bend, s);
let _ = color.paint(LocationRef::new(&coords), &mut OutlineBounds::new(&mut *sink, outline, transform.mul(&unit)));
let outline = |shape: Shape, m: &Xform, s: &mut Bounds| shape_into(&font, &coords, shape, unit.mul(m), transform, bend, s);
let mut b = Bounds::default();
let _ = color.paint(LocationRef::new(&coords), &mut OutlineBounds::new(&mut b, outline));
if let Some([x0, y0, x1, y1]) = b.rect {
rect_to(sink, &Xform::IDENTITY, x0, y0, x1, y1);
}
}
continue;
}
@@ -308,26 +313,47 @@ fn draw_outline(
}
}
/// Glyph `id`'s unscaled outline through `local` (font units → text space) into `sink`, for
/// colour glyph layers.
fn outline_into(
/// The colour glyph `id` paints as, when the font has one and skrifa can walk its paint graph.
/// A graph it cannot walk (too deep, a cycle, a broken offset) leaves the glyph to its plain
/// outline in the text colour instead of dropping it.
fn color_glyph<'a>(font: &skrifa::FontRef<'a>, id: GlyphId, coords: &[NormalizedCoord]) -> Option<skrifa::color::ColorGlyph<'a>> {
let color = font.color_glyphs().get(id)?;
color.paint(LocationRef::new(coords), &mut NoPaint).is_ok().then_some(color)
}
/// A colour glyph's clip [`Shape`], unscaled font units through `local` (font units → text
/// space), into `sink`: a glyph's outline, or a clip box, bent by `bend` like the outlines so it
/// still crops what it was meant to.
fn shape_into(
font: &skrifa::FontRef,
coords: &[NormalizedCoord],
id: GlyphId,
shape: Shape,
local: Xform,
transform: &Xform,
bend: Option<&Warp>,
sink: &mut impl LineSink,
) {
if let Some(outline) = font.outline_glyphs().get(id) {
draw_outline(&outline, DrawSettings::unhinted(Size::unscaled(), LocationRef::new(coords)), local, transform, bend, sink);
match shape {
Shape::Glyph(id) => {
if let Some(outline) = font.outline_glyphs().get(id) {
draw_outline(&outline, DrawSettings::unhinted(Size::unscaled(), LocationRef::new(coords)), local, transform, bend, sink);
}
}
Shape::Box(b) => {
let (x0, y0, x1, y1) = (f64::from(b.x_min), f64::from(b.y_min), f64::from(b.x_max), f64::from(b.y_max));
match bend {
Some(w) => rect_to(&mut WarpSink { inner: sink, warp: w, post: *transform }, &local, x0, y0, x1, y1),
None => rect_to(sink, &transform.mul(&local), x0, y0, x1, y1),
}
}
}
}
/// Paints the colour glyphs of `layout` in their own colours over the premultiplied accumulator
/// (`w × h`, `format`'s colour channels then alpha): their palette's sRGB converted to the
/// document's colour model, times the text colour's opacity. `None` when a glyph's buffers could
/// not be reserved.
/// not be reserved or `limits` ran out (see [`Limits`]).
#[allow(clippy::too_many_arguments)]
fn paint_color_glyphs(
layout: &TextLayout,
xf: &Xform,
@@ -336,21 +362,24 @@ fn paint_color_glyphs(
antialias: AntiAlias,
acc: &mut [f32],
(w, h): (usize, usize),
mut limits: Limits,
) -> Option<()> {
let n = format.mode.color_channels();
let stride = n + 1;
let opaque = PixelFormat { alpha: false, ..*format };
// Float documents keep extended (HDR, signed) colours, as the plain passes do.
let keep_range = |v: f32| if format.sample == SampleType::F32 && v.is_finite() { v } else { v.clamp(0.0, 1.0) };
for g in &layout.glyphs {
let (Some(st), Some(face)) = (layout.styles.get(g.style as usize), layout.faces.get(g.face as usize)) else { continue };
let Ok(font) = skrifa::FontRef::from_index(face.font.data.as_ref(), face.font.index) else { continue };
let Some(color) = font.color_glyphs().get(GlyphId::new(g.id)) else { continue };
let coords: Vec<NormalizedCoord> = face.coords.iter().map(|&c| NormalizedCoord::from_bits(c)).collect();
let Some(color) = color_glyph(&font, GlyphId::new(g.id), &coords) else { continue };
let Some((glyph, bend)) = place(layout, g, &font, face.size_px, &coords, warp) else { continue };
let unit = font_units(&font, face.size_px, &coords, glyph);
// The window the glyph can paint: its layers' outlines and clip boxes.
// The window the glyph can paint: its fills within their clips.
let mut b = Bounds::default();
let outline = |id: GlyphId, m: &Xform, s: &mut Bounds| outline_into(&font, &coords, id, unit.mul(m), xf, bend, s);
if color.paint(LocationRef::new(&coords), &mut OutlineBounds::new(&mut b, outline, xf.mul(&unit))).is_err() {
let outline = |shape: Shape, m: &Xform, s: &mut Bounds| shape_into(&font, &coords, shape, unit.mul(m), xf, bend, s);
if color.paint(LocationRef::new(&coords), &mut OutlineBounds::new(&mut b, outline)).is_err() {
continue;
}
let Some([x0, y0, x1, y1]) = b.rect else { continue };
@@ -364,19 +393,20 @@ fn paint_color_glyphs(
let palettes = font.color_palettes();
let palette = palettes.get(0);
let [r, gr, bl] = st.color.to_rgb();
let outline = |id: GlyphId, m: &Xform, s: &mut Coverage| outline_into(&font, &coords, id, unit.mul(m), &to_window, bend, s);
let colors = palette.as_ref().map_or(&[][..], |p| p.colors());
let mut painter =
Painter::new(ww, wh, outline, to_window.mul(&unit), colors, [r, gr, bl, 1.0], antialias == AntiAlias::None).with_clip_boxes(bend.is_none());
let outline = |shape: Shape, m: &Xform, s: &mut Coverage| shape_into(&font, &coords, shape, unit.mul(m), &to_window, bend, s);
let ink = Ink { palette: palette.as_ref().map_or(&[][..], |p| p.colors()), foreground: [r, gr, bl, 1.0] };
let mut painter = Painter::new(ww, wh, outline, to_window.mul(&unit), ink, antialias, limits);
if color.paint(LocationRef::new(&coords), &mut painter).is_err() {
continue;
}
// Compositing the window into the text is one more pass over it.
limits.work = painter.work_left().checked_sub((ww as u64).checked_mul(wh as u64)?)?;
let opacity = st.color.alpha.clamp(0.0, 1.0);
for (i, s) in painter.finish()?.iter().enumerate() {
let a = s[3] * opacity;
let o = ((wy0 + i / ww) * w + wx0 + i % ww) * stride;
let (true, Some(px)) = (a > 0.0, acc.get_mut(o..o + stride)) else { continue };
let rgb = [s[0] / s[3], s[1] / s[3], s[2] / s[3]].map(|v| v.clamp(0.0, 1.0));
let rgb = [s[0] / s[3], s[1] / s[3], s[2] / s[3]].map(keep_range);
let comps = if format.mode == ColorMode::Rgb { rgb.to_vec() } else { photocraft_raster::from_rgba(&opaque, [rgb[0], rgb[1], rgb[2], 1.0]) };
let keep = 1.0 - a;
for (p, c) in px.iter_mut().zip(&comps).take(n) {
@@ -575,7 +605,11 @@ pub(crate) fn rasterize_passes(
let a = c.alpha.clamp(0.0, 1.0);
composite(&mut acc, w, sub, &cov, stride, &comps, a, antialias);
}
if paint_color_glyphs(layout, &xf, warp, &format, antialias, &mut acc, (w, h)).is_none() {
// Colour glyphs get what the plain passes left of both budgets: the bytes beside the
// accumulator and the output surface ([`fits_raster_budget`]), and the pixel work.
let deep = (w as u64).saturating_mul(h as u64).saturating_mul(stride as u64).saturating_mul(size_of::<f32>() as u64 + format.sample.bytes() as u64);
let limits = Limits { bytes: MAX_RASTER_BYTES.saturating_sub(deep), work: MAX_PASS_PIXELS.saturating_sub(pass_pixels) };
if paint_color_glyphs(layout, &xf, warp, &format, antialias, &mut acc, (w, h), limits).is_none() {
return nothing(format);
}
// Unpremultiply.