pub mod axial;
pub mod function;
pub mod gouraud;
pub mod patch;
pub mod radial;
pub mod steps;
use kurbo::Affine;
use pdfrum_page::Shading;
use pdfrum_page::{Geometry, ShadingKind};
use crate::color::Argb;
use crate::device::RenderDevice;
use crate::options::{ColorMode, RenderOptions};
use crate::path::IntRect;
use crate::pixmap::Pixmap;
use steps::ColorSteps;
#[must_use]
pub fn background_color(shading: &Shading) -> Option<Argb> {
shading.background.map(|rgb| {
let [r, g, b] = rgb.to_bytes_truncating();
Argb { a: 255, r, g, b }
})
}
#[must_use]
pub fn draw_to_pixmap(
shading: &Shading,
rect: IntRect,
to_device: Affine,
alpha: u8,
opts: &RenderOptions,
) -> Option<Pixmap> {
let (w, h) = (
u32::try_from(rect.width()).ok()?,
u32::try_from(rect.height()).ok()?,
);
if w == 0 || h == 0 {
return None;
}
let mut buffer = match background_color(shading) {
Some(bg) => Pixmap::filled(w, h, bg.to_peniko()),
None => Pixmap::new(w, h),
};
let to_bitmap = Affine::translate((-f64::from(rect.left), -f64::from(rect.top))) * to_device;
match &shading.geometry {
Geometry::Axial(a) => {
let ramp = ColorSteps::sample(shading, a.t_min, a.t_max, alpha)?;
axial::draw(&mut buffer, a, &ramp, to_bitmap);
}
Geometry::Radial(r) => {
let ramp = ColorSteps::sample(shading, r.t_min, r.t_max, alpha)?;
radial::draw(&mut buffer, r, &ramp, to_bitmap);
}
Geometry::FunctionBased(f) => {
function::draw(&mut buffer, shading, f, alpha, to_bitmap);
}
Geometry::Mesh { kind, mesh } => match kind {
ShadingKind::FreeFormMesh | ShadingKind::LatticeMesh => {
let [t_min, t_max] = mesh.component_range;
let ramp = (!shading.functions.is_empty())
.then(|| ColorSteps::sample(shading, t_min, t_max, alpha))
.flatten();
gouraud::draw(
&mut buffer,
&mesh.triangles,
ramp.as_ref(),
mesh.component_range,
alpha,
to_bitmap,
);
}
#[expect(
clippy::match_same_arms,
reason = "the two arms decline for different reasons — patches \
are drawn by `draw_patches`, while the three \
non-mesh kinds cannot appear under `Geometry::Mesh` \
at all. Merging them would lose that, and the day \
patches gain an in-buffer path only one arm changes."
)]
ShadingKind::CoonsMesh | ShadingKind::TensorMesh => return None,
ShadingKind::FunctionBased | ShadingKind::Axial | ShadingKind::Radial => return None,
},
_ => return None,
}
post_process(&mut buffer, opts.color_mode);
Some(buffer)
}
pub fn draw_patches(
scratch: &mut dyn RenderDevice,
shading: &Shading,
rect: IntRect,
to_device: Affine,
tensor: bool,
) {
let Geometry::Mesh { mesh, .. } = &shading.geometry else {
return;
};
let to_bitmap = Affine::translate((-f64::from(rect.left), -f64::from(rect.top))) * to_device;
let [t_min, t_max] = mesh.component_range;
let ramp = (!shading.functions.is_empty())
.then(|| ColorSteps::sample(shading, t_min, t_max, 255))
.flatten();
let (Ok(w), Ok(h)) = (u32::try_from(rect.width()), u32::try_from(rect.height())) else {
return;
};
for p in &mesh.patches {
if patch::patch_is_offscreen(p, to_bitmap, w, h) {
continue;
}
patch::draw_patch(scratch, p, ramp.as_ref(), [t_min, t_max], to_bitmap, tensor);
}
}
fn post_process(buffer: &mut Pixmap, mode: ColorMode) {
match mode {
ColorMode::Alpha => {
for chunk in buffer.data_mut().as_chunks_mut::<4>().0 {
chunk[0] = chunk[3];
}
}
ColorMode::Gray => {
for chunk in buffer.data_mut().as_chunks_mut::<4>().0 {
let [r, g, b, _] = *chunk;
let v = crate::color::rgb_to_gray(r, g, b);
(chunk[0], chunk[1], chunk[2]) = (v, v, v);
}
}
ColorMode::Normal | ColorMode::Forced(_) => {}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use kurbo::Point;
use pdfrum_page::Axial;
use pdfrum_page::{ColorSpace, Rgb};
use super::*;
fn axial_shading(background: Option<Rgb>) -> Shading {
Shading {
geometry: Geometry::Axial(Axial {
start: Point::new(0.0, 0.0),
end: Point::new(8.0, 0.0),
t_min: 0.0,
t_max: 1.0,
extend_start: false,
extend_end: false,
}),
space: Arc::new(ColorSpace::DeviceGray),
functions: Box::new([]),
background,
bbox: None,
}
}
fn rect(w: i32, h: i32) -> IntRect {
IntRect {
left: 0,
top: 0,
right: w,
bottom: h,
}
}
#[test]
fn background_is_truncated_not_rounded() {
let bg = background_color(&axial_shading(Some(Rgb {
r: 0.5,
g: 0.5,
b: 0.5,
})));
assert_eq!(
bg.map(|c| c.r),
Some(127),
"the /Background conversion truncates"
);
}
#[test]
fn background_clears_the_buffer_before_rasterizing() {
let shading = axial_shading(Some(Rgb {
r: 1.0,
g: 0.0,
b: 0.0,
}));
let p = draw_to_pixmap(
&shading,
rect(4, 1),
Affine::translate((100.0, 0.0)),
255,
&RenderOptions::default(),
)
.expect("rasterizes");
assert_eq!(p.pixel(0, 0), Some([255, 0, 0, 255]));
}
#[test]
fn an_empty_rect_produces_nothing() {
let shading = axial_shading(None);
assert!(
draw_to_pixmap(
&shading,
rect(0, 4),
Affine::IDENTITY,
255,
&RenderOptions::default()
)
.is_none()
);
}
#[test]
fn gray_mode_grayscales_without_inverting() {
let mut p = Pixmap::filled(1, 1, peniko::Color::from_rgba8(255, 0, 0, 255));
post_process(&mut p, ColorMode::Gray);
assert_eq!(
p.pixel(0, 0),
Some([76, 76, 76, 255]),
"grayscaled, not inverted"
);
}
#[test]
fn alpha_mode_sets_red_from_alpha() {
let mut p = Pixmap::filled(1, 1, peniko::Color::from_rgba8(0, 0, 0, 128));
post_process(&mut p, ColorMode::Alpha);
assert_eq!(p.pixel(0, 0).map(|px| px[0]), Some(128));
}
#[test]
fn patches_are_declined_by_the_pixmap_path() {
let shading = Shading {
geometry: Geometry::Mesh {
kind: ShadingKind::CoonsMesh,
mesh: Box::new(pdfrum_page::Mesh::default()),
},
..axial_shading(None)
};
assert!(
draw_to_pixmap(
&shading,
rect(4, 4),
Affine::IDENTITY,
255,
&RenderOptions::default()
)
.is_none()
);
}
}