mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
519 lines
23 KiB
Rust
519 lines
23 KiB
Rust
//! 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::*;
|
||
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 stops: Vec<ColorStop> = g.gradient.expanded_stops().iter().map(|(o, c, a)| ColorStop::from((*o, f.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) };
|
||
}
|
||
|
||
#[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)
|
||
}
|
||
|
||
/// [`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 `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 and is at most the allowance divided by [`ROW_SCRATCH`] pixels wide (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)?;
|
||
// The PNG reader's row buffers take at most `ROW_SCRATCH` bytes a column, also within the allowance.
|
||
let rows = format == image::ImageFormat::Png
|
||
&& u64::from(w).saturating_mul(ROW_SCRATCH) <= allowance
|
||
&& png_reader(bytes).is_some_and(|r| !r.info().interlaced);
|
||
let scratch = if rows {
|
||
u64::from(w) * ROW_SCRATCH
|
||
} 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 most memory a column of a PNG read a row at a time takes in the reader's row buffers: the
|
||
/// row as stored (up to 8 bytes a pixel and a filter byte), the row before it, and the row given
|
||
/// out (up to 4 bytes a pixel).
|
||
const ROW_SCRATCH: u64 = 32;
|
||
|
||
/// 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) => pixmap_of(fit_pixmap(img).to_rgba8()).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));
|
||
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))
|
||
}
|
||
|
||
/// 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(()));
|
||
}
|
||
}
|