use std::borrow::Cow;
use kurbo::Affine;
use pdfrum_page::{BlendMode, ColorSpace, Converted, ImageData, Pixels, Rgba8, Samples, Source};
use crate::color::Argb;
use crate::device::ImageQuality;
use crate::options::RenderOptions;
use crate::pixmap::{Pixmap, mul255};
pub const HUGE_IMAGE_SIZE: u64 = 60_000_000;
pub const MAX_IMAGE_VALUE: i64 = 256 * 1024 * 1024;
#[must_use]
pub fn use_interpolate_bilinear(
already_bilinear: bool,
no_smoothing: bool,
src_width: u32,
src_height: u32,
dest_width: i64,
dest_height: i64,
) -> bool {
if already_bilinear || no_smoothing {
return false;
}
let dw = dest_width.unsigned_abs();
if dw == 0 {
return false;
}
let dh = dest_height.unsigned_abs();
let lhs = dh / 8;
let rhs = (u64::from(src_width) * u64::from(src_height)) / dw;
lhs < rhs
}
#[must_use]
pub fn resample_quality(
image: &ImageData,
opts: &RenderOptions,
src_width: u32,
src_height: u32,
dest_width: i64,
dest_height: i64,
) -> ImageQuality {
if opts.no_image_smooth {
return ImageQuality::Nearest;
}
let mut bilinear = image.interpolate;
let bytes = u64::from(src_width)
.saturating_mul(u64::from(src_height))
.saturating_mul(image.samples.components() as u64);
if bytes > HUGE_IMAGE_SIZE && !opts.force_halftone {
bilinear = true;
}
if !bilinear
&& use_interpolate_bilinear(
bilinear,
opts.no_image_smooth,
src_width,
src_height,
dest_width,
dest_height,
)
{
bilinear = true;
}
if bilinear {
ImageQuality::Bilinear
} else {
ImageQuality::Nearest
}
}
#[must_use]
#[expect(
clippy::float_cmp,
reason = "exactness is the predicate: only a bit-exact identity-plus-integer \
-translation matrix triggers vello_cpu's nearest downgrade, so an \
epsilon here would claim a downgrade the backend does not make"
)]
#[expect(
clippy::many_single_char_names,
reason = "a..f are the six affine matrix coefficients, named as in the PDF `cm` operands"
)]
pub fn is_integer_translation(m: Affine) -> bool {
let [a, b, c, d, e, f] = m.as_coeffs();
a == 1.0
&& b == 0.0
&& c == 0.0
&& d == 1.0
&& e.fract() == 0.0
&& f.fract() == 0.0
&& e.is_finite()
&& f.is_finite()
}
#[must_use]
pub fn effective_quality(quality: ImageQuality, to_device: Affine) -> ImageQuality {
if is_integer_translation(to_device) {
ImageQuality::Nearest
} else {
quality
}
}
#[must_use]
#[expect(
clippy::float_cmp,
reason = "the `== 1.0f` alpha clauses are the upstream gate verbatim; an \
epsilon would apply the Darken approximation to files PDFium \
composites Normal, changing the corpus"
)]
pub fn overprint_blend(space: Option<&ColorSpace>, state: &pdfrum_page::GeneralState) -> BlendMode {
let subtractive = matches!(
space.map(ColorSpace::family),
Some(
pdfrum_page::Family::DeviceCmyk
| pdfrum_page::Family::Separation
| pdfrum_page::Family::DeviceN
)
);
let eligible = subtractive
&& state.fill_overprint
&& state.overprint_mode == 0
&& state.fill_alpha == 1.0
&& state.stroke_alpha == 1.0
&& matches!(state.blend, BlendMode::Normal | BlendMode::Compatible);
if eligible {
BlendMode::Darken
} else {
state.blend
}
}
#[must_use]
pub fn image_value_fits(v: f64) -> bool {
#[expect(
clippy::cast_precision_loss,
reason = "MAX_IMAGE_VALUE is 2^28, exactly representable in f64"
)]
let limit = MAX_IMAGE_VALUE as f64;
v.is_finite() && v.abs() < limit
}
#[must_use]
pub fn to_pixmap(
image: &ImageData,
stencil_color: Argb,
transfer: Option<&crate::transfer::TransferFunc<'_>>,
) -> Pixmap {
if !pdfrum_page::image_area_is_workable(image.width, image.height) {
return Pixmap::new(0, 0);
}
let Some(mut out) = Pixmap::try_new(image.width, image.height) else {
return Pixmap::new(0, 0);
};
let finish = RowFinish::new(image, stencil_color, transfer);
let mut rows = converted_rows(image);
let width = image.width as usize;
for y in 0..image.height {
let Some(row) = rows.next_row_with(|row| finish.apply(row, y)) else {
break;
};
let Some(dest) = out
.data_mut()
.get_mut((y as usize).saturating_mul(width).saturating_mul(4)..)
.and_then(|rest| rest.get_mut(..width.saturating_mul(4)))
else {
continue;
};
for (slot, px) in dest.as_chunks_mut::<4>().0.iter_mut().zip(row.pixels()) {
*slot = px.0;
}
}
out
}
pub(crate) fn converted_rows(image: &ImageData) -> Converted<'_> {
let palette = image.samples.palette().map(pdfrum_page::Palette::new);
Converted::new(
Source::new(&image.samples, image.width, image.height),
palette,
)
}
pub(crate) struct RowFinish<'a> {
fused: Option<&'a pdfrum_page::ImageMask>,
stencil: Option<Argb>,
matte: Option<[u8; 3]>,
transfer: Option<&'a crate::transfer::TransferFunc<'a>>,
}
impl<'a> RowFinish<'a> {
pub(crate) fn new(
image: &'a ImageData,
stencil_color: Argb,
transfer: Option<&'a crate::transfer::TransferFunc<'a>>,
) -> Self {
Self {
fused: image.mask.as_ref().filter(|m| is_coregistered(m, image)),
stencil: image.samples.is_stencil().then_some(stencil_color),
matte: image.matte.map(pdfrum_page::Rgb::to_bytes),
transfer: transfer.filter(|t| !t.is_identity()),
}
}
pub(crate) fn apply(&self, row: &mut [Rgba8], y: u32) {
for (x, slot) in (0_u32..).zip(row.iter_mut()) {
let alpha = self.fused.map_or(255, |m| m.alpha_at(x, y));
*slot = Rgba8(match self.stencil {
Some(color) if slot.0[0] == 0 => {
let a = mul255(color.a, alpha);
[
mul255(color.r, a),
mul255(color.g, a),
mul255(color.b, a),
a,
]
}
Some(_) => [0, 0, 0, 0],
None => self.sample(slot.0, alpha),
});
}
}
fn sample(&self, px: [u8; 4], alpha: u8) -> [u8; 4] {
let mut rgb = [px[0], px[1], px[2]];
if let Some(matte) = self.matte {
rgb = matte_source(rgb, alpha, matte);
}
if let Some(transfer) = self.transfer {
let [r, g, b] = rgb;
let mapped = transfer.translate(Argb { a: 255, r, g, b });
rgb = [mapped.r, mapped.g, mapped.b];
}
let [r, g, b] = rgb;
[mul255(r, alpha), mul255(g, alpha), mul255(b, alpha), alpha]
}
}
#[must_use]
pub fn is_coregistered(mask: &pdfrum_page::ImageMask, image: &ImageData) -> bool {
let pdfrum_page::ImageMask::Alpha { width, height, .. } = mask else {
return true;
};
*width == image.width && *height == image.height
}
#[must_use]
pub fn separate_mask(mask: &pdfrum_page::ImageMask) -> Option<(ImageData, Cow<'_, [u8]>)> {
let pdfrum_page::ImageMask::Alpha {
width,
height,
alpha,
stencil,
} = mask
else {
return None;
};
let (w, h) = (*width, *height);
if w == 0 || h == 0 {
return None;
}
let len = (w as usize).checked_mul(h as usize)?;
let plane = if !*stencil && alpha.len() == len {
Cow::Borrowed(&**alpha)
} else {
let mut plane = Vec::with_capacity(len);
for y in 0..h {
for x in 0..w {
plane.push(mask.alpha_at(x, y));
}
}
Cow::Owned(plane)
};
let dict = ImageData {
width: w,
height: h,
samples: Samples::Whole(Pixels::Gray8(Box::default())),
mask: None,
matte: None,
interpolate: false,
};
Some((dict, plane))
}
#[must_use]
pub fn mask_pixmap(plane: &[u8], width: u32, height: u32) -> Pixmap {
let mut out = Pixmap::new(width, height);
if plane.len() != (width as usize).saturating_mul(height as usize) {
return out;
}
for (slot, &a) in out
.data_mut()
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(plane.iter())
{
*slot = [a, a, a, a];
}
out
}
#[must_use]
pub fn reduced_mask_pixmap(
plane: &[u8],
width: u32,
height: u32,
to_device: kurbo::Affine,
dest_width: f64,
dest_height: f64,
) -> (Pixmap, kurbo::Affine) {
if let Some(snapped) = crate::stretch::snapped_reduction(to_device, width, height) {
let (new_w, new_h) = snapped.size();
let reduced = crate::stretch::reduce_gray_to(plane, width, height, new_w, new_h);
return (mask_pixmap(&reduced, new_w, new_h), snapped.transform());
}
match crate::stretch::reduction_for(width, height, dest_width, dest_height) {
Some((new_w, new_h)) => {
let reduced = crate::stretch::reduce_gray_to(plane, width, height, new_w, new_h);
(
mask_pixmap(&reduced, new_w, new_h),
crate::stretch::reduction_transform(to_device, width, height, new_w, new_h),
)
}
None => (mask_pixmap(plane, width, height), to_device),
}
}
#[must_use]
pub fn matte_source(sample: [u8; 3], mask: u8, matte: [u8; 3]) -> [u8; 3] {
if mask == 0 {
return sample;
}
let m = i32::from(mask);
let mut out = sample;
for i in 0..3 {
let (Some(&dest), Some(&mt)) = (sample.get(i), matte.get(i)) else {
continue;
};
let orig = (i32::from(dest) - i32::from(mt)) * 255 / m + i32::from(mt);
if let Some(slot) = out.get_mut(i) {
#[expect(
clippy::cast_sign_loss,
reason = "the clamp lower bound is 0, so the value fits u8 exactly"
)]
let byte = orig.clamp(0, 255) as u8;
*slot = byte;
}
}
out
}
#[cfg(test)]
mod tests {
use pdfrum_page::GeneralState;
use pdfrum_page::{BitImage, ImageMask, Rgb};
use super::*;
fn gray_image(w: u32, h: u32, interpolate: bool) -> ImageData {
ImageData {
width: w,
height: h,
samples: Samples::Whole(Pixels::Gray8(
vec![128; (w * h) as usize].into_boxed_slice(),
)),
mask: None,
matte: None,
interpolate,
}
}
#[test]
fn use_interpolate_bilinear_heuristic() {
assert!(
use_interpolate_bilinear(false, false, 100, 100, 50, 50),
"downsampling"
);
assert!(
!use_interpolate_bilinear(false, false, 4, 4, 100, 100),
"big enlargement"
);
assert!(!use_interpolate_bilinear(true, false, 100, 100, 50, 50));
assert!(!use_interpolate_bilinear(false, true, 100, 100, 50, 50));
assert!(!use_interpolate_bilinear(false, false, 100, 100, 0, 50));
}
#[test]
fn the_heuristic_divisions_truncate() {
assert!(use_interpolate_bilinear(false, false, 1, 1, 1, 7));
assert!(!use_interpolate_bilinear(false, false, 1, 1, 2, 800));
}
#[test]
fn no_smoothing_wins_over_interpolate() {
let img = gray_image(10, 10, true);
let opts = RenderOptions {
no_image_smooth: true,
..RenderOptions::default()
};
assert_eq!(
resample_quality(&img, &opts, 10, 10, 5, 5),
ImageQuality::Nearest
);
}
#[test]
fn huge_image_forces_bilinear_unless_halftoning() {
let mut img = gray_image(5000, 5000, false);
img.samples = Samples::Whole(Pixels::Rgb8(vec![0; 3].into_boxed_slice()));
let opts = RenderOptions::default();
assert_eq!(
resample_quality(&img, &opts, 5000, 5000, 100_000, 100_000),
ImageQuality::Bilinear
);
let halftone = RenderOptions {
force_halftone: true,
..RenderOptions::default()
};
assert_eq!(
resample_quality(&img, &halftone, 5000, 5000, 100_000, 100_000),
ImageQuality::Nearest,
"halftoning suppresses the forcing rule"
);
}
#[test]
fn cmyk_overprint_becomes_darken() {
let state = GeneralState {
fill_overprint: true,
overprint_mode: 0,
..Default::default()
};
let cmyk = ColorSpace::DeviceCmyk;
assert_eq!(overprint_blend(Some(&cmyk), &state), BlendMode::Darken);
let mut alpha = state.clone();
alpha.fill_alpha = 0.5;
assert_eq!(overprint_blend(Some(&cmyk), &alpha), BlendMode::Normal);
let mut stroke = state.clone();
stroke.stroke_alpha = 0.5;
assert_eq!(overprint_blend(Some(&cmyk), &stroke), BlendMode::Normal);
let mut mode = state.clone();
mode.overprint_mode = 1;
assert_eq!(overprint_blend(Some(&cmyk), &mode), BlendMode::Normal);
let mut off = state.clone();
off.fill_overprint = false;
assert_eq!(overprint_blend(Some(&cmyk), &off), BlendMode::Normal);
let mut blend = state.clone();
blend.blend = BlendMode::Multiply;
assert_eq!(overprint_blend(Some(&cmyk), &blend), BlendMode::Multiply);
assert_eq!(
overprint_blend(Some(&ColorSpace::DeviceRgb), &state),
BlendMode::Normal
);
assert_eq!(overprint_blend(None, &state), BlendMode::Normal);
}
#[test]
fn image_values_are_bounded() {
assert!(image_value_fits(1000.0));
assert!(!image_value_fits(300_000_000.0));
assert!(!image_value_fits(f64::NAN));
assert!(!image_value_fits(f64::INFINITY));
}
#[test]
fn integer_translations_are_detected() {
assert!(is_integer_translation(Affine::translate((3.0, -7.0))));
assert!(!is_integer_translation(Affine::translate((3.5, 0.0))));
assert!(!is_integer_translation(Affine::scale(2.0)));
assert_eq!(
effective_quality(ImageQuality::Bilinear, Affine::translate((3.0, 4.0))),
ImageQuality::Nearest
);
assert_eq!(
effective_quality(ImageQuality::Bilinear, Affine::scale(2.0)),
ImageQuality::Bilinear
);
}
#[test]
fn a_stencil_paints_its_set_bits_in_the_fill_colour() {
let bits = BitImage {
width: 2,
height: 1,
row_bytes: 1,
bits: vec![0b1000_0000],
};
let img = ImageData {
width: 2,
height: 1,
samples: Samples::Whole(Pixels::Stencil(bits)),
mask: None,
matte: None,
interpolate: false,
};
let p = to_pixmap(&img, Argb::opaque(255, 0, 0), None);
assert_eq!(p.pixel(0, 0), Some([255, 0, 0, 255]), "the set bit is ink");
assert_eq!(
p.pixel(1, 0),
Some([0, 0, 0, 0]),
"the clear bit paints nothing"
);
}
#[test]
fn a_mask_folds_into_the_alpha() {
let img = ImageData {
width: 1,
height: 1,
samples: Samples::Whole(Pixels::Rgb8(vec![255, 255, 255].into_boxed_slice())),
mask: Some(ImageMask::Alpha {
width: 1,
height: 1,
alpha: vec![128].into_boxed_slice(),
stencil: false,
}),
matte: None,
interpolate: false,
};
let p = to_pixmap(&img, Argb::BLACK, None);
assert_eq!(
p.pixel(0, 0),
Some([128, 128, 128, 128]),
"premultiplied by the mask"
);
}
#[test]
fn a_mask_on_its_own_grid_is_not_folded_into_the_samples() {
let mask = ImageMask::Alpha {
width: 2,
height: 2,
alpha: vec![0, 0, 0, 0].into_boxed_slice(),
stencil: false,
};
let img = ImageData {
width: 1,
height: 1,
samples: Samples::Whole(Pixels::Rgb8(vec![255, 255, 255].into_boxed_slice())),
mask: Some(mask.clone()),
matte: None,
interpolate: false,
};
assert!(!is_coregistered(&mask, &img), "2x2 mask over a 1x1 base");
let p = to_pixmap(&img, Argb::BLACK, None);
assert_eq!(
p.pixel(0, 0),
Some([255, 255, 255, 255]),
"the sample stays opaque; the mask is applied at device resolution"
);
let same = ImageMask::Alpha {
width: 1,
height: 1,
alpha: vec![64].into_boxed_slice(),
stencil: false,
};
let coreg = ImageData {
mask: Some(same.clone()),
..img
};
assert!(is_coregistered(&same, &coreg));
assert_eq!(
to_pixmap(&coreg, Argb::BLACK, None).pixel(0, 0),
Some([64; 4])
);
}
#[test]
fn a_separate_mask_carries_its_coverage_in_the_alpha() {
let mask = ImageMask::Alpha {
width: 2,
height: 1,
alpha: vec![0, 200].into_boxed_slice(),
stencil: false,
};
let (dict, plane) = separate_mask(&mask).expect("an alpha mask yields a plane");
assert_eq!((dict.width, dict.height), (2, 1));
assert_eq!(plane, vec![0, 200]);
let px = mask_pixmap(&plane, dict.width, dict.height);
assert_eq!(px.pixel(0, 0), Some([0, 0, 0, 0]));
assert_eq!(px.pixel(1, 0), Some([200, 200, 200, 200]));
let stencil = ImageMask::Alpha {
width: 1,
height: 1,
alpha: vec![0].into_boxed_slice(),
stencil: true,
};
let (d, plane) = separate_mask(&stencil).expect("a stencil yields a plane");
assert_eq!(
mask_pixmap(&plane, d.width, d.height).pixel(0, 0),
Some([255; 4]),
"a clear stencil bit is opaque"
);
}
#[test]
fn the_cmyk_arm_is_the_table_applied_row_by_row() {
for (w, h, len) in [(4_u32, 3_u32, 48_usize), (4, 3, 20), (4, 3, 100), (1, 1, 4)] {
let data: Box<[u8]> = (0..len)
.map(|i| u8::try_from(i * 53 % 256).unwrap_or(0))
.collect();
let image = ImageData {
width: w,
height: h,
samples: Samples::Whole(Pixels::Cmyk8(data.clone())),
mask: None,
matte: None,
interpolate: false,
};
let out = to_pixmap(&image, Argb::opaque(0, 0, 0), None);
for y in 0..h {
for x in 0..w {
let at = ((y as usize) * (w as usize) + x as usize) * 4;
let sample = |o: usize| data.get(at.saturating_add(o)).copied().unwrap_or(0);
let rgb =
pdfrum_page::adobe_cmyk_to_srgb(sample(0), sample(1), sample(2), sample(3));
assert_eq!(
out.pixel(x, y),
Some([rgb[0], rgb[1], rgb[2], 255]),
"{w}x{h}, {len} bytes, sample ({x}, {y})"
);
}
}
}
}
#[test]
fn the_borrowed_mask_plane_is_the_walked_one() {
let walked = |mask: &ImageMask, w: u32, h: u32| -> Vec<u8> {
(0..h)
.flat_map(|y| (0..w).map(move |x| (x, y)))
.map(|(x, y)| mask.alpha_at(x, y))
.collect()
};
for (w, h, len, stencil) in [
(4_u32, 3_u32, 12_usize, false),
(4, 3, 12, true),
(4, 3, 7, false),
(4, 3, 20, false),
(1, 1, 1, false),
] {
let mask = ImageMask::Alpha {
width: w,
height: h,
alpha: (0..len)
.map(|i| u8::try_from(i * 41 % 256).unwrap_or(0))
.collect(),
stencil,
};
let (_, plane) = separate_mask(&mask).expect("an alpha mask yields a plane");
assert_eq!(
&plane[..],
&walked(&mask, w, h)[..],
"{w}x{h}, {len} bytes, stencil {stencil}"
);
}
}
#[test]
fn the_separate_masks_descriptor_carries_shape_and_not_samples() {
let mask = ImageMask::Alpha {
width: 4,
height: 3,
alpha: vec![7; 12].into_boxed_slice(),
stencil: false,
};
let (dict, plane) = separate_mask(&mask).expect("an alpha mask yields a plane");
assert_eq!(
dict.samples.components(),
1,
"the component count is the use"
);
assert_eq!((dict.width, dict.height), (4, 3));
assert_eq!(plane.len(), 12, "the coverage is in the plane");
}
#[test]
fn a_mask_plane_of_the_wrong_length_is_transparent() {
let px = mask_pixmap(&[1, 2, 3], 4, 4);
assert_eq!((px.width(), px.height()), (4, 4));
assert!(px.data().iter().all(|b| *b == 0));
}
#[test]
#[expect(
clippy::float_cmp,
reason = "the placement transform must be bit-identical to the one \
`prescale` produced, not merely close: a caller compares it \
for equality nowhere, but a difference in the last bit would \
land the mask on a different device pixel"
)]
fn the_reduced_mask_pixmap_is_the_prescaled_expanded_one() {
for (w, h, dw, dh) in [
(64_u32, 40_u32, 8.0_f64, 5.0_f64),
(137, 85, 16.4, 10.2),
(1339, 81, 391.5, 10.2),
(9, 9, 9.0, 9.0),
(9, 9, 20.0, 20.0),
(13, 7, 1.0, 1.0),
] {
let plane: Vec<u8> = (0..w * h).map(|i| (i * 37 % 251) as u8).collect();
let at = kurbo::Affine::new([1.5, 0.0, 0.0, 2.5, 3.0, 4.0]);
let (got, got_at) = reduced_mask_pixmap(&plane, w, h, at, dw, dh);
let expanded = mask_pixmap(&plane, w, h);
let (want, want_at) = match crate::stretch::prescale(&expanded, at, dw, dh) {
Some(pair) => pair,
None => (expanded, at),
};
assert_eq!(
(got.width(), got.height()),
(want.width(), want.height()),
"{w}x{h} -> {dw}x{dh}"
);
assert_eq!(got.data(), want.data(), "{w}x{h} -> {dw}x{dh}");
assert_eq!(
got_at.as_coeffs(),
want_at.as_coeffs(),
"{w}x{h} -> {dw}x{dh}"
);
}
}
#[test]
fn a_reducing_axis_aligned_mask_lands_on_the_integer_rect() {
let (w, h) = (1572_u32, 85_u32);
let plane: Vec<u8> = (0..w * h).map(|i| (i * 37 % 251) as u8).collect();
let (sx, sy) = (393.1291 / f64::from(w), 21.2379 / f64::from(h));
let at = kurbo::Affine::new([sx, 0.0, 0.0, sy, 0.4837, 0.6594]);
let (pixmap, placed) = reduced_mask_pixmap(&plane, w, h, at, 393.1291, 21.2379);
assert_eq!((pixmap.width(), pixmap.height()), (394, 22));
assert_eq!(
crate::stretch::placement_for(placed, pixmap.width(), pixmap.height()),
crate::stretch::Placement::Exact { x: 0.0, y: 0.0 }
);
}
#[test]
fn only_an_unmirrored_axis_aligned_reduction_is_snapped() {
let (w, h) = (64_u32, 40_u32);
let reduce = kurbo::Affine::new([0.25, 0.0, 0.0, 0.25, 3.2, 4.7]);
assert!(crate::stretch::snapped_reduction(reduce, w, h).is_some());
let grow = kurbo::Affine::new([2.0, 0.0, 0.0, 2.0, 3.2, 4.7]);
assert!(crate::stretch::snapped_reduction(grow, w, h).is_none());
let mixed = kurbo::Affine::new([0.25, 0.0, 0.0, 2.0, 3.2, 4.7]);
assert!(crate::stretch::snapped_reduction(mixed, w, h).is_none());
let mirrored = kurbo::Affine::new([-0.25, 0.0, 0.0, 0.25, 3.2, 4.7]);
assert!(crate::stretch::snapped_reduction(mirrored, w, h).is_none());
let rotated = kurbo::Affine::new([0.2, 0.1, -0.1, 0.2, 3.2, 4.7]);
assert!(crate::stretch::snapped_reduction(rotated, w, h).is_none());
}
#[test]
fn a_transfer_function_reaches_the_images_own_samples() {
let invert: [u8; 256] = std::array::from_fn(|i| 255 - u8::try_from(i).unwrap_or(255));
let inverting = pdfrum_page::TransferFunc {
identity: false,
samples: Box::new([invert, invert, invert]),
};
let func = crate::transfer::TransferFunc::new(&inverting);
let img = ImageData {
width: 1,
height: 1,
samples: Samples::Whole(Pixels::Rgb8(vec![255, 255, 255].into_boxed_slice())),
mask: None,
matte: None,
interpolate: false,
};
assert_eq!(
to_pixmap(&img, Argb::BLACK, Some(&func)).pixel(0, 0),
Some([0, 0, 0, 255])
);
assert_eq!(
to_pixmap(&img, Argb::BLACK, None).pixel(0, 0),
Some([255, 255, 255, 255])
);
}
#[test]
fn matte_unpremultiply_skips_a_zero_mask() {
assert_eq!(
matte_source([100, 100, 100], 0, [50, 50, 50]),
[100, 100, 100]
);
}
#[test]
fn matte_unpremultiply_is_the_integer_inverse_blend() {
let expected = u8::try_from((100 - 50) * 255 / 128 + 50).expect("in range");
assert_eq!(
matte_source([100, 100, 100], 128, [50, 50, 50])[0],
expected
);
assert_eq!(
matte_source([37, 90, 200], 255, [50, 50, 50]),
[37, 90, 200]
);
}
#[test]
fn matte_unpremultiply_clamps_rather_than_wrapping() {
assert_eq!(matte_source([255, 255, 255], 1, [0, 0, 0]), [255, 255, 255]);
assert_eq!(matte_source([0, 0, 0], 1, [255, 255, 255]), [0, 0, 0]);
}
#[test]
fn a_matte_image_un_premultiplies_before_the_mask_becomes_alpha() {
let img = ImageData {
width: 1,
height: 1,
samples: Samples::Whole(Pixels::Gray8(vec![64].into_boxed_slice())),
mask: Some(ImageMask::Alpha {
width: 1,
height: 1,
alpha: vec![128].into_boxed_slice(),
stencil: false,
}),
matte: Some(Rgb::BLACK),
interpolate: false,
};
let p = to_pixmap(&img, Argb::BLACK, None);
let source = u8::try_from(64 * 255 / 128).expect("in range");
assert_eq!(
p.pixel(0, 0),
Some([
mul255(source, 128),
mul255(source, 128),
mul255(source, 128),
128
])
);
}
#[test]
fn an_image_without_a_matte_takes_its_samples_verbatim() {
let img = ImageData {
width: 1,
height: 1,
samples: Samples::Whole(Pixels::Gray8(vec![64].into_boxed_slice())),
mask: Some(ImageMask::Alpha {
width: 1,
height: 1,
alpha: vec![128].into_boxed_slice(),
stencil: false,
}),
matte: None,
interpolate: false,
};
let p = to_pixmap(&img, Argb::BLACK, None);
assert_eq!(
p.pixel(0, 0),
Some([mul255(64, 128), mul255(64, 128), mul255(64, 128), 128])
);
}
#[test]
fn indexed_pixels_read_their_palette() {
let img = ImageData {
width: 2,
height: 1,
samples: Samples::Whole(Pixels::Indexed {
indices: vec![0, 1].into_boxed_slice(),
palette: vec![
Rgb {
r: 1.0,
g: 0.0,
b: 0.0,
},
Rgb {
r: 0.0,
g: 0.0,
b: 1.0,
},
]
.into_boxed_slice(),
}),
mask: None,
matte: None,
interpolate: false,
};
let p = to_pixmap(&img, Argb::BLACK, None);
assert_eq!(p.pixel(0, 0), Some([255, 0, 0, 255]));
assert_eq!(p.pixel(1, 0), Some([0, 0, 255, 255]));
}
}