Files
vectorcraft/crates/render/src/paint.rs
T
dotCooCoo 0a65178696 Images: a PNG read a row at a time counts the reader's buffer while it grows
The png reader's stream buffer grows by doubling its reservation, and
while it grows the old reservation and the new one are both allocated.
The room estimate counted the buffer twice, for the doubled
reservation, but not the moment both are held, so a wide PNG could be
approved for a row-at-a-time decode with less room than reading it
takes. The estimate now counts the stream buffer three times.
2026-11-04 23:45:05 +07:00

561 lines
25 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Paint conversion (solid, gradients, patterns) and image decoding.
use vectorcraft_color::{GradientKind, Paint};
use vectorcraft_geom::{Rect, Vec2};
use vello_cpu::RenderContext;
use vello_cpu::peniko::{self, ColorStop};
use super::{BlendMode, Compose, FillRule, Mix, vello_cpu};
use crate::Frame;
/// Set the context paint (colours as frame `f` paints them). Returns false if nothing should be
/// drawn.
pub(crate) fn set_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, f: &Frame) -> bool {
match p {
Paint::None => false,
Paint::Solid { color: c, .. } => {
ctx.set_paint(f.ink.color(c, 1.0));
true
}
Paint::Gradient(g) if g.gradient.kind == GradientKind::Freeform => crate::freeform::set_freeform_paint(ctx, g, bounds, f.ink),
Paint::Gradient(g) => {
let geom = g.resolve(bounds);
let adjusted = f.ink.gradient(&g.gradient);
let gradient = adjusted.as_ref().unwrap_or(&g.gradient);
let ink = if adjusted.is_some() { f.ink.base() } else { f.ink };
let stops: Vec<ColorStop> = gradient.expanded_stops().iter().map(|(o, c, a)| ColorStop::from((*o, ink.color(c, *a)))).collect();
if stops.is_empty() {
return false;
}
let grad = match g.gradient.kind {
GradientKind::Radial => {
let r = geom.length().max(1e-6) as f32;
// The aspect ratio squashes the circle across the gradient vector.
let squash = geom.radial_squash();
ctx.set_paint_transform(squash);
let g = match geom.focal {
// An off-centre focal point: a two-point radial from it to the extent circle.
Some(f) => peniko::Gradient::new_two_point_radial(squash.inverse() * f, 0.0, geom.start, r),
None => peniko::Gradient::new_radial(geom.start, r),
};
g.with_stops(stops.as_slice())
}
_ => {
ctx.reset_paint_transform();
let (s, e) =
if geom.start.distance(geom.end) < 1e-9 { (geom.start, geom.start + Vec2::new(1.0, 0.0)) } else { (geom.start, geom.end) };
peniko::Gradient::new_linear(s, e).with_stops(stops.as_slice())
}
};
// Colour and opacity blend each on its own, as the model samples them and as the PDF
// and SVG exports draw them (peniko's default premultiplies).
ctx.set_paint(grad.with_interpolation_alpha_space(peniko::InterpolationAlphaSpace::Unpremultiplied));
true
}
Paint::Pattern { pattern, xf } => crate::pattern::set_pattern_paint(ctx, pattern, *xf, f),
}
}
/// [`set_paint`] for a fill: a dithered gradient (dither applies to fills only) with stops is
/// drawn as pixels with fine noise ([`crate::dither`]).
pub(crate) fn set_fill_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, f: &Frame) -> bool {
match p {
Paint::Gradient(g) if g.gradient.dither && g.gradient.kind != GradientKind::Freeform && !g.gradient.stops.is_empty() => {
crate::dither::set_dithered_paint(ctx, g, bounds, f.ink)
}
_ => set_paint(ctx, p, bounds, f),
}
}
/// The most memory decoding one image may take: print rasters run to gigabytes (a 300 ppi roll,
/// 6614 x 59055 px, is 1.56 GB as RGBA), past the decoder's 512 MB default. The web keeps it.
const MAX_DECODE_ALLOC: u64 = if cfg!(target_arch = "wasm32") { 512 << 20 } else { 2 << 30 };
thread_local! {
/// Images that couldn't be decoded (and so drew nothing) on this thread, counted for exports to
/// report.
static DECODE_FAILURES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
/// Images decoded, or tried, on this thread ([`image_decodes`]).
static DECODES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
thread_local! {
/// The decode allowance of one image on this thread in place of [`MAX_DECODE_ALLOC`], so a
/// test can draw an image past it without decoding gigabytes.
pub(crate) static TEST_ALLOWANCE: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
/// The bytes [`fits_beside_pinned`] finds room for on this thread, in place of what the images
/// paints hold in the shared store leave.
pub(crate) static TEST_ROOM: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
/// The images [`decode_or_reduction`] has tried to decode on this thread.
pub(crate) static DECODE_ATTEMPTS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
/// The most memory decoding one image may take on this thread: [`MAX_DECODE_ALLOC`].
fn allowance() -> u64 {
#[cfg(test)]
if let Some(a) = TEST_ALLOWANCE.with(std::cell::Cell::get) {
return a;
}
MAX_DECODE_ALLOC
}
/// Why an image has no pixels to draw ([`decode_pixmap_or_why`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Undecoded {
/// Its pixels can't be read, at their own size or at [`reduced_size`]: decoding them again
/// fails again.
Unreadable,
/// The images being painted leave no room for it within twice [`MAX_DECODE_ALLOC`]: it may
/// be decoded once they are done.
NoRoom,
}
/// How many images have failed to decode on this thread so far: an export compares the count
/// before and after it draws to say it left images out. Images that other threads (another export,
/// the canvas, thumbnails) fail to decode meanwhile are not counted.
pub fn decode_failures() -> u64 {
DECODE_FAILURES.with(std::cell::Cell::get)
}
/// How many images [`decode_pixmap_or_why`] and [`decode_pixmap_fit`] have decoded on this thread so
/// far, failed decodes included. Images other threads decode are not counted.
pub fn image_decodes() -> u64 {
DECODES.with(std::cell::Cell::get)
}
/// [`decode_in_room`]'s image or error.
#[cfg(test)]
pub(crate) fn decode_within_allowance(bytes: &[u8]) -> image::ImageResult<image::DynamicImage> {
decode_in_room(bytes).map_err(|(e, _)| e)
}
/// `bytes` decoded within [`MAX_DECODE_ALLOC`], counted as RGBA: the image's size is read from its
/// header first, and an image of more than that as RGBA gets a limit error before anything is
/// decoded (#1717). A failure comes with why the image gave no pixels ([`room_for`]).
fn decode_in_room(bytes: &[u8]) -> Result<image::DynamicImage, (image::ImageError, Undecoded)> {
let allowance = allowance();
let unreadable = |e| (e, Undecoded::Unreadable);
let reader = || -> image::ImageResult<_> {
let mut r = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format()?;
let mut limits = image::Limits::default();
limits.max_alloc = Some(allowance);
r.limits(limits);
Ok(r)
};
let (w, h) = reader().and_then(|r| r.into_dimensions()).map_err(unreadable)?;
let bytes = (u64::from(w) * u64::from(h)).saturating_mul(4);
let pinned = crate::images::store().pinned_bytes() as u64;
if let Err(why) = room_for(bytes, pinned) {
return Err((image::ImageError::Limits(image::error::LimitError::from_kind(image::error::LimitErrorKind::InsufficientMemory)), why));
}
reader().and_then(|r| r.decode()).map_err(unreadable)
}
/// Whether an image of `rgba` bytes may be decoded while the images being painted hold `pinned`
/// bytes: within the allowance itself, and within twice [`MAX_DECODE_ALLOC`] with them. An image
/// over the allowance may still be decoded at [`reduced_size`].
fn room_for(rgba: u64, pinned: u64) -> Result<(), Undecoded> {
if rgba > allowance() {
Err(Undecoded::Unreadable)
} else if rgba.saturating_add(pinned) > 2 * MAX_DECODE_ALLOC {
Err(Undecoded::NoRoom)
} else {
Ok(())
}
}
/// Whether `w`×`h` pixels may be decoded whole ([`room_for`]) beside the images paints hold now.
pub(crate) fn within_allowance(w: u32, h: u32) -> bool {
let bytes = (u64::from(w) * u64::from(h)).saturating_mul(4);
room_for(bytes, crate::images::store().pinned_bytes() as u64).is_ok()
}
/// Whether `bytes` of decoded pixels fit beside the images paints hold, which can't be evicted from
/// the cache: a new image is decoded only while those and it fit twice [`MAX_DECODE_ALLOC`].
fn fits_beside_pinned(bytes: u64) -> bool {
#[cfg(test)]
if let Some(room) = TEST_ROOM.with(std::cell::Cell::get) {
return bytes <= room;
}
let pinned = crate::images::store().pinned_bytes() as u64;
bytes.saturating_add(pinned) <= 2 * MAX_DECODE_ALLOC
}
/// The size at which [`decode_pixmap_or_why`] decodes encoded image `bytes` that take more than
/// [`MAX_DECODE_ALLOC`] as RGBA, or whose decoded samples do (16 bits a sample): the largest that
/// fits it as RGBA, keeping its proportions, at most the image's own size, with sides of at most
/// `u16::MAX` pixels. `None` for an image within the allowance and for one that isn't decoded at
/// all: an image that isn't a PNG, JPEG, GIF or WebP; an image of more pixels than the allowance
/// has bytes (2,147,483,648 pixels, or 536,870,912 in the web app; 46,341 × 46,341 is more); and an
/// image whose decoded samples take more than the allowance, unless it is a PNG that isn't
/// interlaced whose reader's buffers ([`png_row_scratch`]) fit the allowance (read a row at a time).
pub fn reduced_size(bytes: &[u8]) -> Option<(u32, u32)> {
reduction(bytes, allowance()).map(|r| r.to)
}
/// How [`decode_pixmap_or_why`] reads an image past the decode allowance ([`reduced_size`]).
struct Reduction {
/// The image's size in pixels.
size: (u32, u32),
/// The size it is decoded at.
to: (u32, u32),
/// Read a row at a time (a PNG that isn't interlaced) rather than decoded whole first.
rows: bool,
/// The bytes reading the image takes besides the result: the PNG reader's row buffers, or the
/// image decoded whole and a band of its rows as RGBA.
scratch: u64,
}
/// [`reduced_size`]'s reading of `bytes` within `allowance`.
fn reduction(bytes: &[u8], allowance: u64) -> Option<Reduction> {
use image::ImageDecoder as _;
let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
let format = reader.format()?;
if !matches!(format, image::ImageFormat::Png | image::ImageFormat::Jpeg | image::ImageFormat::Gif | image::ImageFormat::WebP) {
return None;
}
let mut limits = image::Limits::default();
limits.max_alloc = Some(allowance);
reader.limits(limits);
let d = reader.into_decoder().ok()?;
let (w, h) = d.dimensions();
let pixels = u64::from(w) * u64::from(h);
// An image of more pixels than the allowance has bytes (four times what it holds as RGBA) is
// not read.
if pixels == 0 || pixels > allowance || (pixels.saturating_mul(4) <= allowance && d.total_bytes() <= allowance) {
return None;
}
let to = fitted_dims(w, h, allowance)?;
// A PNG that isn't interlaced is read a row at a time when the reader's buffers fit the allowance.
let row_scratch = if format == image::ImageFormat::Png {
png_reader(bytes).filter(|r| !r.info().interlaced).and_then(|r| png_row_scratch(&r)).filter(|&s| s <= allowance)
} else {
None
};
let rows = row_scratch.is_some();
let scratch = row_scratch.unwrap_or_else(|| {
let band = u64::from(BAND) * u64::from(w) * (u64::from(d.color_type().bytes_per_pixel()) + 4);
d.total_bytes().saturating_add(band)
});
(rows || d.total_bytes() <= allowance).then_some(Reduction { size: (w, h), to, rows, scratch })
}
/// How many rows of an image decoded whole [`reduced_pixmap`] converts to RGBA at a time.
const BAND: u32 = 16;
/// The bytes of rows already read that png 0.18's reader keeps in its stream buffer, at least,
/// before it moves the rest of the buffer back to the start.
const PNG_KEPT_ROWS: u64 = 128 << 10;
/// The deflate window png 0.18's reader keeps after the rows it has unfiltered, and the step its
/// stream buffer grows by.
const PNG_WINDOW_AND_STEP: u64 = (32 << 10) + (8 << 10);
/// The most memory `r` takes in its buffers to read its image a row at a time: the stream buffer
/// holds the rows already read up to [`PNG_KEPT_ROWS`] or four rows, whichever is more, the previous
/// and current rows, and [`PNG_WINDOW_AND_STEP`]. The buffer grows by doubling its reservation,
/// and while it grows its old reservation and the new one, up to twice the size it holds, are both
/// allocated: three times that size at most. A copy of a stored row and the row given out come on
/// top. `None` when a row given out doesn't fit memory.
fn png_row_scratch(r: &png::Reader<std::io::Cursor<&[u8]>>) -> Option<u64> {
let info = r.info();
// A filter byte and the row's samples, in whole bytes.
let stored = 1 + (u64::from(info.width) * info.color_type.samples() as u64 * info.bit_depth as u64).div_ceil(8);
let out = u64::try_from(r.output_line_size(info.width)?).ok()?;
let kept = (stored * 4).next_multiple_of(64).max(PNG_KEPT_ROWS);
let stream = kept + stored * 2 + PNG_WINDOW_AND_STEP;
Some(stream * 3 + stored + out)
}
/// The largest size of at most `w`×`h` pixels, keeping their proportions, whose pixels take at most
/// `allowance` bytes as RGBA, with sides of at most `u16::MAX` pixels.
fn fitted_dims(w: u32, h: u32, allowance: u64) -> Option<(u32, u32)> {
let pixels = u64::from(w) * u64::from(h);
let s = ((allowance / 4) as f64 / pixels as f64).sqrt().min(u16::MAX as f64 / w.max(h) as f64).min(1.0);
let side = |n: u32| ((n as f64 * s).floor() as u32).clamp(1, u16::MAX as u32);
let (mut nw, mut nh) = (side(w), side(h));
// Rounding can leave the product a pixel over: take a pixel off the longer side.
while u64::from(nw) * u64::from(nh) * 4 > allowance && (nw > 1 || nh > 1) {
if nw >= nh { nw -= 1 } else { nh -= 1 }
}
(u64::from(nw) * u64::from(nh) * 4 <= allowance).then_some((nw, nh))
}
/// A PNG reader of `bytes` that gives 8-bit gray, gray with alpha, RGB or RGBA rows, with its
/// header and the chunks before the pixels read.
fn png_reader(bytes: &[u8]) -> Option<png::Reader<std::io::Cursor<&[u8]>>> {
let mut d = png::Decoder::new(std::io::Cursor::new(bytes));
d.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
d.read_info().ok()
}
/// `bytes` decoded at the size [`reduction`] gives. Each pixel of the result is the mean of the
/// image's pixels it covers. [`Undecoded::Unreadable`] when they can't be read, and
/// [`Undecoded::NoRoom`] when the result and what reading takes ([`Reduction::scratch`]) don't fit
/// beside the images paints hold.
fn reduced_pixmap(bytes: &[u8], r: &Reduction) -> Result<vello_cpu::Pixmap, Undecoded> {
let (tw, th) = r.to;
if !fits_beside_pinned((u64::from(tw) * u64::from(th) * 4).saturating_add(r.scratch)) {
return Err(Undecoded::NoRoom);
}
shrunk(bytes, r).ok_or(Undecoded::Unreadable)
}
/// `bytes` read and shrunk as [`reduced_pixmap`] says.
fn shrunk(bytes: &[u8], r: &Reduction) -> Option<vello_cpu::Pixmap> {
let mut shrink = Shrink::new(r.size, r.to);
if r.rows {
let mut reader = png_reader(bytes)?;
let (color, _) = reader.output_color_type();
while let Some(row) = reader.next_row().ok()? {
let data = row.data();
match color {
png::ColorType::Grayscale => shrink.row(data.iter().map(|&g| [g, g, g, 255])),
png::ColorType::GrayscaleAlpha => shrink.row(data.as_chunks::<2>().0.iter().map(|&[g, a]| [g, g, g, a])),
png::ColorType::Rgb => shrink.row(data.as_chunks::<3>().0.iter().map(|&[r, g, b]| [r, g, b, 255])),
png::ColorType::Rgba => shrink.row(data.as_chunks::<4>().0.iter().copied()),
png::ColorType::Indexed => return None,
}
}
} else {
let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
let mut limits = image::Limits::default();
limits.max_alloc = Some(allowance());
reader.limits(limits);
let img = reader.decode().ok()?;
let (w, h) = (img.width(), img.height());
for y in (0..h).step_by(BAND as usize) {
let band = img.crop_imm(0, y, w, BAND.min(h - y)).to_rgba8();
band.as_raw().as_chunks::<4>().0.chunks_exact(w as usize).for_each(|row| shrink.row(row.iter().copied()));
}
}
shrink.finish()
}
/// An image shrunk as its RGBA rows are given ([`Shrink::row`]): each pixel of the result is the
/// mean of the image's pixels it covers, premultiplied.
struct Shrink {
/// The image's size.
size: (u32, u32),
/// The result's size.
to: (u32, u32),
/// The sums of the result row being made, a pixel each: red, green and blue times alpha, and
/// alpha.
sums: Vec<[u64; 4]>,
/// How many of the image's pixels each pixel of that row has.
counts: Vec<u64>,
/// The result row being made, and the next row of the image.
at: (u32, u32),
/// The finished rows.
out: Vec<vello_cpu::color::PremulRgba8>,
}
impl Shrink {
fn new(size: (u32, u32), to: (u32, u32)) -> Self {
let n = to.0 as usize;
Self { size, to, sums: vec![[0; 4]; n], counts: vec![0; n], at: (0, 0), out: Vec::with_capacity(n * to.1 as usize) }
}
/// Add the image's next row of RGBA pixels (pixels past its width are left out).
fn row(&mut self, rgba: impl IntoIterator<Item = [u8; 4]>) {
if self.at.1 >= self.size.1 {
return;
}
let ty = (u64::from(self.at.1) * u64::from(self.to.1) / u64::from(self.size.1)) as u32;
if ty != self.at.0 {
self.flush();
self.at.0 = ty;
}
let (w, tw) = (u64::from(self.size.0), u64::from(self.to.0));
for (x, p) in (0..w).zip(rgba) {
// `tx` is the result column that the image's column `x` falls in.
let tx = (x * tw / w) as usize;
let a = u64::from(p[3]);
if let Some(s) = self.sums.get_mut(tx) {
s[0] += u64::from(p[0]) * a;
s[1] += u64::from(p[1]) * a;
s[2] += u64::from(p[2]) * a;
s[3] += a;
}
if let Some(c) = self.counts.get_mut(tx) {
*c += 1;
}
}
self.at.1 += 1;
}
/// Finish the result row being made.
fn flush(&mut self) {
for (s, n) in self.sums.iter_mut().zip(&mut self.counts) {
let k = (*n).max(1);
let mean = |v: u64, d: u64| ((v + d / 2) / d).min(255) as u8;
self.out.push(vello_cpu::color::PremulRgba8 { r: mean(s[0], 255 * k), g: mean(s[1], 255 * k), b: mean(s[2], 255 * k), a: mean(s[3], k) });
*s = [0; 4];
*n = 0;
}
}
/// The shrunk image once every row of the image was given.
fn finish(mut self) -> Option<vello_cpu::Pixmap> {
if self.at.1 < self.size.1 {
return None;
}
self.flush();
let (w, h) = (u16::try_from(self.to.0).ok()?, u16::try_from(self.to.1).ok()?);
(self.out.len() == w as usize * h as usize).then(|| vello_cpu::Pixmap::from_parts(self.out, w, h))
}
}
/// `img` shrunk, keeping its proportions, to fit a pixmap's sides (at most `u16::MAX` pixels).
fn fit_pixmap(img: image::DynamicImage) -> image::DynamicImage {
let (w, h) = (img.width(), img.height());
let max = u16::MAX as u32;
if w <= max && h <= max {
return img;
}
let s = max as f64 / w.max(h) as f64;
let (nw, nh) = (((w as f64 * s).round() as u32).clamp(1, max), ((h as f64 * s).round() as u32).clamp(1, max));
img.resize_exact(nw, nh, image::imageops::FilterType::Triangle)
}
/// [`decode_pixmap_or_why`]'s pixels.
#[cfg(test)]
pub fn decode_pixmap(bytes: &[u8]) -> Option<vello_cpu::Pixmap> {
decode_pixmap_or_why(bytes).ok()
}
/// Decode encoded image bytes into a premultiplied pixmap (shrunk to fit a pixmap's sides), or say
/// why there are no pixels. An image that takes more than the allowance as RGBA is decoded at
/// [`reduced_size`] when it has one; a failure is counted in [`decode_failures`].
pub(crate) fn decode_pixmap_or_why(bytes: &[u8]) -> Result<vello_cpu::Pixmap, Undecoded> {
counted(match decode_or_reduction(bytes) {
Ok(img) => decoded_pixmap(img).ok_or(Undecoded::Unreadable),
Err(r) => r.and_then(|r| reduced_pixmap(bytes, &r)),
})
}
/// `bytes` decoded within the allowance ([`decode_in_room`]), or, when it refuses them for their
/// size, how [`reduced_pixmap`] reads them past it; otherwise why there is no image.
fn decode_or_reduction(bytes: &[u8]) -> Result<image::DynamicImage, Result<Reduction, Undecoded>> {
#[cfg(test)]
DECODE_ATTEMPTS.with(|n| n.set(n.get() + 1));
DECODES.with(|n| n.set(n.get() + 1));
match decode_in_room(bytes) {
Ok(img) => Ok(img),
Err((image::ImageError::Limits(_), Undecoded::Unreadable)) => Err(reduction(bytes, allowance()).ok_or(Undecoded::Unreadable)),
Err((_, why)) => Err(Err(why)),
}
}
/// `pm`, with a failure counted in [`decode_failures`] when there is none.
fn counted(pm: Result<vello_cpu::Pixmap, Undecoded>) -> Result<vello_cpu::Pixmap, Undecoded> {
if pm.is_err() {
add_decode_failures(1);
}
pm
}
/// [`decode_pixmap_or_why`] shrunk to `w`×`h` pixels when the image is larger (never enlarged). An
/// image past the decode allowance is decoded straight at `w`×`h` when that is no larger on either
/// side and fits the allowance, otherwise at [`reduced_size`].
pub fn decode_pixmap_fit(bytes: &[u8], w: u16, h: u16) -> Option<vello_cpu::Pixmap> {
let img = match decode_or_reduction(bytes) {
Ok(img) => img,
Err(r) => {
return counted(r.and_then(|mut r| {
let fit = (u32::from(w).max(1), u32::from(h).max(1));
if fit.0 <= r.to.0 && fit.1 <= r.to.1 {
r.to = fit;
}
reduced_pixmap(bytes, &r)
}))
.ok();
}
};
let img = if img.width() > w as u32 || img.height() > h as u32 {
img.resize_exact(w as u32, h as u32, image::imageops::FilterType::Triangle)
} else {
img
};
pixmap_of(img.to_rgba8())
}
/// Count `n` images that couldn't be decoded on this thread ([`decode_failures`]).
/// [`decode_pixmap_or_why`] and [`decode_pixmap_fit`] count each failure, and a raster kept in a cache
/// counts the failures it was drawn with again each time it is drawn.
pub(crate) fn add_decode_failures(n: u64) {
DECODE_FAILURES.with(|c| c.set(c.get() + n));
}
/// An RGBA image as a premultiplied pixmap.
fn pixmap_of(img: image::RgbaImage) -> Option<vello_cpu::Pixmap> {
let (w, h) = img.dimensions();
if w == 0 || h == 0 || w > u16::MAX as u32 || h > u16::MAX as u32 {
return None;
}
let data: Vec<vello_cpu::color::PremulRgba8> = img
.pixels()
.map(|p| {
let a = p[3] as u16;
let m = |c: u8| ((c as u16 * a + 127) / 255) as u8;
vello_cpu::color::PremulRgba8 { r: m(p[0]), g: m(p[1]), b: m(p[2]), a: p[3] }
})
.collect();
Some(vello_cpu::Pixmap::from_parts(data, w as u16, h as u16))
}
/// Decoded image `img` as [`decode_pixmap_or_why`] makes it a pixmap.
pub(crate) fn decoded_pixmap(img: image::DynamicImage) -> Option<vello_cpu::Pixmap> {
pixmap_of(fit_pixmap(img).to_rgba8())
}
/// The substitution highlights' pink: Preferences › Highlight Substituted Fonts and Highlight
/// Alternate Glyphs, and Document Setup's Highlight Substituted Glyphs (screen only).
pub(crate) const SUBSTITUTED: peniko::Color = peniko::Color::from_rgba8(255, 120, 190, 110);
pub(crate) fn fill_rule(r: FillRule) -> peniko::Fill {
match r {
FillRule::NonZero => peniko::Fill::NonZero,
FillRule::EvenOdd => peniko::Fill::EvenOdd,
}
}
pub(crate) fn blend_mode(b: vectorcraft_color::BlendMode) -> BlendMode {
use vectorcraft_color::BlendMode as B;
let mix = match b {
B::Normal => Mix::Normal,
B::Darken => Mix::Darken,
B::Multiply => Mix::Multiply,
B::ColorBurn => Mix::ColorBurn,
B::Lighten => Mix::Lighten,
B::Screen => Mix::Screen,
B::ColorDodge => Mix::ColorDodge,
B::Overlay => Mix::Overlay,
B::SoftLight => Mix::SoftLight,
B::HardLight => Mix::HardLight,
B::Difference => Mix::Difference,
B::Exclusion => Mix::Exclusion,
B::Hue => Mix::Hue,
B::Saturation => Mix::Saturation,
B::Color => Mix::Color,
B::Luminosity => Mix::Luminosity,
};
BlendMode::new(mix, Compose::SrcOver)
}
#[cfg(test)]
mod tests {
use super::*;
/// An image over the allowance as RGBA never fits, so it is noted as undecodable; one that only
/// the images being painted leave no room for is not, and is tried again later.
#[test]
fn only_the_room_painted_images_take_is_tried_again() {
assert_eq!(room_for(MAX_DECODE_ALLOC + 1, 0), Err(Undecoded::Unreadable));
assert_eq!(room_for(MAX_DECODE_ALLOC + 1, 2 * MAX_DECODE_ALLOC), Err(Undecoded::Unreadable));
assert_eq!(room_for(MAX_DECODE_ALLOC, MAX_DECODE_ALLOC + 1), Err(Undecoded::NoRoom));
assert_eq!(room_for(MAX_DECODE_ALLOC, MAX_DECODE_ALLOC), Ok(()));
}
}