use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex};
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use stet_tiny_skia::{
BlendMode, Color, FillRule as SkiaFillRule, LineCap as SkiaLineCap, LineJoin as SkiaLineJoin,
Mask, Paint, PathBuilder, Pixmap, Stroke, StrokeDash, Transform,
};
use stet_core::device::OutputDevice;
use stet_fonts::geometry::{Matrix, PathSegment, PsPath};
use stet_graphics::color::{DeviceColor, FillRule, LineCap, LineJoin};
use stet_graphics::device::{
AxialShadingParams, ClipParams, FillParams, ImageColorSpace, ImageParams, MeshShadingParams,
PageSinkFactory, PatchShadingParams, RadialShadingParams, ShadingColorSpace, ShadingVertex,
StrokeParams, TintLookupTable,
};
use stet_graphics::icc::IccCache;
#[derive(Clone, Copy)]
struct ClipRect {
x0: u32,
y0: u32, x1: u32,
y1: u32, }
impl ClipRect {
fn intersect(&self, other: &ClipRect) -> ClipRect {
ClipRect {
x0: self.x0.max(other.x0),
y0: self.y0.max(other.y0),
x1: self.x1.min(other.x1),
y1: self.y1.min(other.y1),
}
}
fn is_empty(&self) -> bool {
self.x0 >= self.x1 || self.y0 >= self.y1
}
fn is_full_page(&self, w: u32, h: u32) -> bool {
self.x0 == 0 && self.y0 == 0 && self.x1 == w && self.y1 == h
}
fn make_mask(self, w: u32, h: u32) -> Option<Mask> {
if self.is_empty() {
return None;
}
let mut mask = Mask::new(w, h)?;
let data = mask.data_mut();
let stride = w as usize;
for y in self.y0..self.y1 {
let row_start = y as usize * stride + self.x0 as usize;
let row_end = y as usize * stride + self.x1 as usize;
data[row_start..row_end].fill(255);
}
Some(mask)
}
}
enum ClipRegion {
Rect(ClipRect),
Mask(Mask),
}
pub struct SkiaDevice {
pixmap: Pixmap,
page_w: u32,
page_h: u32,
dpi: f64,
clip_region: Option<ClipRegion>,
clip_mask_cache: HashMap<u64, Mask>,
clip_mask_seen: HashSet<u64>,
spare_mask: Option<Mask>,
pending_render: Option<std::sync::mpsc::Receiver<Result<(), String>>>,
sink_factory: Box<dyn PageSinkFactory>,
system_cmyk_bytes: Option<std::sync::Arc<Vec<u8>>>,
render_icc_cache: Option<IccCache>,
no_aa: bool,
use_viewport_path: bool,
}
impl SkiaDevice {
pub fn new(width: u32, height: u32) -> Self {
Self::with_sink_factory(width, height, Box::new(crate::PngSinkFactory))
}
pub fn with_sink_factory(
width: u32,
height: u32,
sink_factory: Box<dyn PageSinkFactory>,
) -> Self {
let dpi = height as f64 * 72.0 / 792.0;
let pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
Self {
pixmap,
page_w: width,
page_h: height,
dpi,
clip_region: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
spare_mask: None,
pending_render: None,
sink_factory,
system_cmyk_bytes: None,
render_icc_cache: None,
no_aa: false,
use_viewport_path: false,
}
}
pub fn set_use_viewport_path(&mut self, on: bool) {
self.use_viewport_path = on;
}
fn ensure_full_pixmap(&mut self) {
if self.pixmap.width() != self.page_w || self.pixmap.height() != self.page_h {
self.pixmap =
Pixmap::new(self.page_w, self.page_h).expect("Failed to create page pixmap");
self.pixmap.fill(Color::WHITE);
}
}
pub fn pixmap(&self) -> &Pixmap {
&self.pixmap
}
pub fn set_system_cmyk_bytes(&mut self, bytes: std::sync::Arc<Vec<u8>>) {
self.system_cmyk_bytes = Some(bytes);
}
pub fn set_no_aa(&mut self, no_aa: bool) {
self.no_aa = no_aa;
}
}
fn to_transform(m: &Matrix) -> Transform {
Transform::from_row(
m.a as f32,
m.b as f32,
m.c as f32,
m.d as f32,
m.tx as f32,
m.ty as f32,
)
}
fn to_paint(color: &DeviceColor) -> Paint<'static> {
to_paint_alpha(color, 1.0, 0, false)
}
fn to_paint_alpha(color: &DeviceColor, alpha: f64, blend_mode: u8, no_aa: bool) -> Paint<'static> {
let mut paint = Paint::default();
let a = (alpha * 255.0).round().clamp(0.0, 255.0) as u8;
paint.set_color_rgba8(
(color.r * 255.0).round().clamp(0.0, 255.0) as u8,
(color.g * 255.0).round().clamp(0.0, 255.0) as u8,
(color.b * 255.0).round().clamp(0.0, 255.0) as u8,
a,
);
paint.anti_alias = !no_aa;
paint.blend_mode = u8_to_blend_mode(blend_mode);
paint
}
fn u8_to_blend_mode(mode: u8) -> BlendMode {
match mode {
1 => BlendMode::Multiply,
2 => BlendMode::Screen,
3 => BlendMode::Overlay,
4 => BlendMode::Darken,
5 => BlendMode::Lighten,
6 => BlendMode::ColorDodge,
7 => BlendMode::ColorBurn,
8 => BlendMode::HardLight,
9 => BlendMode::SoftLight,
10 => BlendMode::Difference,
11 => BlendMode::Exclusion,
12 => BlendMode::Hue,
13 => BlendMode::Saturation,
14 => BlendMode::Color,
15 => BlendMode::Luminosity,
_ => BlendMode::SourceOver,
}
}
const MAX_PATH_COORD: f32 = 1e6;
fn build_skia_path(path: &PsPath) -> Option<stet_tiny_skia::Path> {
let mut pb = PathBuilder::new();
for seg in &path.segments {
match seg {
PathSegment::MoveTo(x, y) => {
pb.move_to(*x as f32, *y as f32);
}
PathSegment::LineTo(x, y) => {
pb.line_to(*x as f32, *y as f32);
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => {
pb.cubic_to(
*x1 as f32, *y1 as f32, *x2 as f32, *y2 as f32, *x3 as f32, *y3 as f32,
);
}
PathSegment::ClosePath => {
pb.close();
}
}
}
let result = pb.finish()?;
let b = result.bounds();
if b.left().abs() > MAX_PATH_COORD
|| b.top().abs() > MAX_PATH_COORD
|| b.right().abs() > MAX_PATH_COORD
|| b.bottom().abs() > MAX_PATH_COORD
{
return None;
}
Some(result)
}
fn is_degenerate_fill(path: &PsPath) -> bool {
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for seg in &path.segments {
let (x, y) = match seg {
PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
PathSegment::CurveTo { .. } => return false,
PathSegment::ClosePath => continue,
};
x_min = x_min.min(x);
x_max = x_max.max(x);
y_min = y_min.min(y);
y_max = y_max.max(y);
}
if x_min > x_max {
return false; }
let w = x_max - x_min;
let h = y_max - y_min;
let eps = 1e-6;
(w < eps && h > eps) || (h < eps && w > eps)
}
fn to_fill_rule(rule: &FillRule) -> SkiaFillRule {
match rule {
FillRule::NonZeroWinding => SkiaFillRule::Winding,
FillRule::EvenOdd => SkiaFillRule::EvenOdd,
}
}
fn to_line_cap(cap: LineCap) -> SkiaLineCap {
match cap {
LineCap::Butt => SkiaLineCap::Butt,
LineCap::Round => SkiaLineCap::Round,
LineCap::Square => SkiaLineCap::Square,
}
}
fn to_line_join(join: LineJoin) -> SkiaLineJoin {
match join {
LineJoin::Miter => SkiaLineJoin::Miter,
LineJoin::Round => SkiaLineJoin::Round,
LineJoin::Bevel => SkiaLineJoin::Bevel,
}
}
fn detect_rect(path: &PsPath, page_w: u32, page_h: u32) -> Option<ClipRect> {
let segs = &path.segments;
let (move_to, lines, _has_close) = match segs.len() {
5 => {
if !matches!(segs[4], PathSegment::ClosePath) {
return None;
}
(&segs[0], &segs[1..4], true)
}
6 => {
if !matches!(segs[5], PathSegment::ClosePath) {
return None;
}
(&segs[0], &segs[1..5], true)
}
4 => {
(&segs[0], &segs[1..4], false)
}
_ => return None,
};
let PathSegment::MoveTo(mx, my) = move_to else {
return None;
};
let mut pts = vec![(*mx, *my)];
for seg in lines {
match seg {
PathSegment::LineTo(x, y) => pts.push((*x, *y)),
_ => return None,
}
}
if pts.len() == 5 {
let (fx, fy) = pts[0];
let (lx, ly) = pts[4];
if (fx - lx).abs() > 0.01 || (fy - ly).abs() > 0.01 {
return None;
}
pts.truncate(4);
}
for i in 0..4 {
let (x1, y1) = pts[i];
let (x2, y2) = pts[(i + 1) % 4];
let dx = (x2 - x1).abs();
let dy = (y2 - y1).abs();
if dx > 0.01 && dy > 0.01 {
return None; }
}
let min_x = pts.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
let min_y = pts.iter().map(|p| p.1).fold(f64::INFINITY, f64::min);
let max_x = pts.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max);
let max_y = pts.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max);
let x0 = (min_x.floor().max(0.0) as u32).min(page_w);
let y0 = (min_y.floor().max(0.0) as u32).min(page_h);
let x1 = (max_x.ceil().max(0.0) as u32).min(page_w);
let y1 = (max_y.ceil().max(0.0) as u32).min(page_h);
Some(ClipRect { x0, y0, x1, y1 })
}
fn intersect_mask_with_rect(mask: &mut Mask, rect: &ClipRect, w: u32, h: u32) {
let data = mask.data_mut();
let stride = w as usize;
if rect.y0 > 0 {
let end = (rect.y0 as usize * stride).min(data.len());
data[..end].fill(0);
}
if rect.y1 < h {
let start = (rect.y1 as usize * stride).min(data.len());
data[start..].fill(0);
}
for y in rect.y0..rect.y1.min(h) {
let row_start = y as usize * stride;
if rect.x0 > 0 {
let end = row_start + rect.x0 as usize;
data[row_start..end].fill(0);
}
if rect.x1 < w {
let start = row_start + rect.x1 as usize;
let end = row_start + stride;
data[start..end].fill(0);
}
}
}
fn resolve_clip_mask<'a>(
clip_region: &'a Option<ClipRegion>,
temp_mask: &'a mut Option<Mask>,
w: u32,
h: u32,
) -> Option<Option<&'a Mask>> {
match clip_region {
None => Some(None),
Some(ClipRegion::Mask(m)) => Some(Some(m)),
Some(ClipRegion::Rect(rect)) => {
if rect.is_empty() {
return None; }
if rect.is_full_page(w, h) {
return Some(None); }
*temp_mask = rect.make_mask(w, h);
Some(temp_mask.as_ref())
}
}
}
fn hash_clip_path(path: &PsPath, fill_rule: &FillRule) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
std::mem::discriminant(fill_rule).hash(&mut hasher);
for seg in &path.segments {
match seg {
PathSegment::MoveTo(x, y) => {
0u8.hash(&mut hasher);
x.to_bits().hash(&mut hasher);
y.to_bits().hash(&mut hasher);
}
PathSegment::LineTo(x, y) => {
1u8.hash(&mut hasher);
x.to_bits().hash(&mut hasher);
y.to_bits().hash(&mut hasher);
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => {
2u8.hash(&mut hasher);
x1.to_bits().hash(&mut hasher);
y1.to_bits().hash(&mut hasher);
x2.to_bits().hash(&mut hasher);
y2.to_bits().hash(&mut hasher);
x3.to_bits().hash(&mut hasher);
y3.to_bits().hash(&mut hasher);
}
PathSegment::ClosePath => {
3u8.hash(&mut hasher);
}
}
}
hasher.finish()
}
fn intersect_masks(dst: &mut Mask, src: &Mask) {
let dst_data = dst.data_mut();
let src_data = src.data();
for (d, s) in dst_data.iter_mut().zip(src_data.iter()) {
*d = ((*d as u16 * *s as u16 + 127) / 255) as u8;
}
}
use stet_graphics::display_list::{DisplayElement, DisplayList};
struct BandState {
clip_region: Option<ClipRegion>,
spare_mask: Option<Mask>,
clip_mask_cache: HashMap<u64, Mask>,
clip_mask_seen: HashSet<u64>,
mask_pool: Vec<Mask>,
cmyk_buffer: Option<Vec<f32>>,
op_bg_snapshot: Option<Vec<u8>>,
op_touched: Option<Vec<u8>>,
spot_mask: Option<Vec<u8>>,
}
const MAX_POOL_MASKS: usize = 8;
impl BandState {
#[allow(dead_code)]
fn recycle_cache(&mut self) {
for (_, mask) in self.clip_mask_cache.drain() {
if self.mask_pool.len() < MAX_POOL_MASKS {
self.mask_pool.push(mask);
}
}
}
fn recycle_mask(&mut self, mask: Mask) {
if self.mask_pool.len() < MAX_POOL_MASKS {
self.mask_pool.push(mask);
}
}
fn take_mask(&mut self, w: u32, h: u32) -> Mask {
self.spare_mask
.take()
.or_else(|| self.mask_pool.pop())
.unwrap_or_else(|| Mask::new(w, h).expect("Failed to create mask"))
}
fn take_op_buffers(&mut self, w: u32, h: u32) -> (Vec<u8>, Vec<u8>) {
let n = w as usize * h as usize;
let bg = self
.op_bg_snapshot
.take()
.unwrap_or_else(|| vec![0u8; n * 4]);
let touched = self.op_touched.take().unwrap_or_else(|| vec![0u8; n]);
(bg, touched)
}
fn restore_op_buffers(&mut self, bg: Vec<u8>, touched: Vec<u8>) {
self.op_bg_snapshot = Some(bg);
self.op_touched = Some(touched);
}
fn take_spot_mask(&mut self, w: u32, h: u32) -> Vec<u8> {
let n = w as usize * h as usize;
self.spot_mask.take().unwrap_or_else(|| vec![0u8; n])
}
fn restore_spot_mask(&mut self, mask: Vec<u8>) {
self.spot_mask = Some(mask);
}
#[allow(dead_code)]
fn invalidate_op_snapshot(
&mut self,
bbox_x0: usize,
bbox_y0: usize,
bbox_x1: usize,
bbox_y1: usize,
stride: usize,
) {
if let Some(touched) = self.op_touched.as_mut() {
for y in bbox_y0..bbox_y1 {
let row = y * stride;
for x in bbox_x0..bbox_x1 {
touched[row + x] = 0;
}
}
}
}
}
struct RenderContext<'a> {
vp_x: f32,
vp_y: f32,
scale_x: f32,
scale_y: f32,
out_w: u32,
out_h: u32,
effective_dpi: f64,
icc: Option<&'a IccCache>,
image_cache: Option<&'a ImageCache>,
preprocessed: Option<&'a [Option<PreprocessedImage>]>,
elem_idx: usize,
no_aa: bool,
opm_zero_transparent: bool,
knockout_painter_pass: KnockoutPainterPass,
parent_group_isolated: bool,
alpha_extraction_pass: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum KnockoutPainterPass {
None,
ColorPass,
CoveragePass,
}
impl RenderContext<'_> {
fn transform(&self, m: &Matrix) -> Transform {
viewport_transform(
to_transform(m),
self.vp_x,
self.vp_y,
self.scale_x,
self.scale_y,
)
}
}
struct YBBox {
y_min: f64,
y_max: f64,
}
struct ClipEpoch {
start_idx: usize,
end_idx: usize,
paint_bbox: Option<YBBox>,
has_erase_page: bool,
}
fn select_band_height(w: u32, h: u32) -> u32 {
if w == 0 || h == 0 {
return h;
}
let per_row = w as u64 * 6;
let budget = 2 * 1024 * 1024u64; let max_rows = budget / per_row;
let band = if max_rows >= h as u64 {
h
} else {
let mut p = 1u32;
while (p as u64) * 2 <= max_rows {
p *= 2;
}
p.clamp(128, h)
};
if h.div_ceil(band) <= 2 {
return h;
}
band
}
fn contains_clip_op(list: &DisplayList) -> bool {
list.elements().iter().any(|e| match e {
DisplayElement::Clip { .. } | DisplayElement::InitClip => true,
DisplayElement::OcgGroup { elements, .. } => contains_clip_op(elements),
DisplayElement::Group { elements, .. } => contains_clip_op(elements),
DisplayElement::SoftMasked { content, .. } => contains_clip_op(content),
_ => false,
})
}
fn precompute_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<YBBox>> {
list.elements()
.iter()
.map(|elem| match elem {
DisplayElement::Fill { path, params } => fill_device_y_bbox(path, ¶ms.ctm),
DisplayElement::Stroke { path, params } => stroke_device_y_bbox(path, params, dpi),
DisplayElement::Image { params, .. } => image_y_bbox(params),
DisplayElement::AxialShading { params } => {
shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
}
DisplayElement::RadialShading { params } => {
shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
}
DisplayElement::MeshShading { params } => {
shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
}
DisplayElement::PatchShading { params } => {
shading_y_bbox_from_bbox(¶ms.bbox, ¶ms.ctm)
}
DisplayElement::PatternFill { params } => pattern_fill_y_bbox(params),
DisplayElement::Group { params, .. } => Some(YBBox {
y_min: params.bbox[1],
y_max: params.bbox[3],
}),
DisplayElement::SoftMasked { params, .. } => Some(YBBox {
y_min: params.bbox[1],
y_max: params.bbox[3],
}),
DisplayElement::OcgGroup {
elements,
default_visible,
..
} => {
if !*default_visible && !contains_clip_op(elements) {
return None;
}
let child_bboxes = precompute_bboxes(elements, dpi);
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for cb in child_bboxes.into_iter().flatten() {
y_min = y_min.min(cb.y_min);
y_max = y_max.max(cb.y_max);
}
if y_min <= y_max {
Some(YBBox { y_min, y_max })
} else {
None
}
}
_ => None, })
.collect()
}
fn shading_y_bbox_from_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<YBBox> {
if let Some(bbox) = bbox {
let corners = [
(bbox[0], bbox[1]),
(bbox[2], bbox[1]),
(bbox[0], bbox[3]),
(bbox[2], bbox[3]),
];
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for (x, y) in &corners {
let (_, dy) = ctm.transform_point(*x, *y);
y_min = y_min.min(dy);
y_max = y_max.max(dy);
}
Some(YBBox { y_min, y_max })
} else {
Some(YBBox {
y_min: 0.0,
y_max: 1e9,
})
}
}
fn stroke_device_y_bbox(path: &PsPath, params: &StrokeParams, dpi: f64) -> Option<YBBox> {
let m = ¶ms.ctm;
let is_identity =
m.a == 1.0 && m.b == 0.0 && m.c == 0.0 && m.d == 1.0 && m.tx == 0.0 && m.ty == 0.0;
let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
if is_identity {
return path_y_bbox(path).map(|mut bbox| {
let expand = effective_lw * params.miter_limit * 0.5;
bbox.y_min -= expand;
bbox.y_max += expand;
bbox
});
}
let (mut x_min, mut x_max) = (f64::INFINITY, f64::NEG_INFINITY);
let (mut y_min, mut y_max) = (f64::INFINITY, f64::NEG_INFINITY);
for seg in &path.segments {
match seg {
PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
x_min = x_min.min(*x);
x_max = x_max.max(*x);
y_min = y_min.min(*y);
y_max = y_max.max(*y);
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => {
x_min = x_min.min(*x1).min(*x2).min(*x3);
x_max = x_max.max(*x1).max(*x2).max(*x3);
y_min = y_min.min(*y1).min(*y2).min(*y3);
y_max = y_max.max(*y1).max(*y2).max(*y3);
}
PathSegment::ClosePath => {}
}
}
if x_min > x_max {
return None;
}
let corners = [
(x_min, y_min),
(x_max, y_min),
(x_min, y_max),
(x_max, y_max),
];
let mut dev_y_min = f64::INFINITY;
let mut dev_y_max = f64::NEG_INFINITY;
for (x, y) in &corners {
let dy = m.b * x + m.d * y + m.ty;
dev_y_min = dev_y_min.min(dy);
dev_y_max = dev_y_max.max(dy);
}
let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
let expand = effective_lw * col_y_len * params.miter_limit * 0.5;
dev_y_min -= expand;
dev_y_max += expand;
Some(YBBox {
y_min: dev_y_min,
y_max: dev_y_max,
})
}
fn fill_device_y_bbox(path: &PsPath, ctm: &Matrix) -> Option<YBBox> {
let is_identity = ctm.a == 1.0
&& ctm.b == 0.0
&& ctm.c == 0.0
&& ctm.d == 1.0
&& ctm.tx == 0.0
&& ctm.ty == 0.0;
if is_identity {
return path_y_bbox(path);
}
let bbox = path_full_bbox(path)?;
let corners = [
(bbox.x_min, bbox.y_min),
(bbox.x_max, bbox.y_min),
(bbox.x_min, bbox.y_max),
(bbox.x_max, bbox.y_max),
];
let mut dev_y_min = f64::INFINITY;
let mut dev_y_max = f64::NEG_INFINITY;
for (x, y) in &corners {
let dy = ctm.b * x + ctm.d * y + ctm.ty;
dev_y_min = dev_y_min.min(dy);
dev_y_max = dev_y_max.max(dy);
}
Some(YBBox {
y_min: dev_y_min,
y_max: dev_y_max,
})
}
fn path_y_bbox(path: &PsPath) -> Option<YBBox> {
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for seg in &path.segments {
match seg {
PathSegment::MoveTo(_, y) | PathSegment::LineTo(_, y) => {
y_min = y_min.min(*y);
y_max = y_max.max(*y);
}
PathSegment::CurveTo { y1, y2, y3, .. } => {
y_min = y_min.min(*y1).min(*y2).min(*y3);
y_max = y_max.max(*y1).max(*y2).max(*y3);
}
PathSegment::ClosePath => {}
}
}
if y_min <= y_max {
Some(YBBox { y_min, y_max })
} else {
None
}
}
fn image_y_bbox(params: &ImageParams) -> Option<YBBox> {
let image_inv = params.image_matrix.invert()?;
let combined = params.ctm.concat(&image_inv);
let corners = [
(0.0, 0.0),
(params.width as f64, 0.0),
(params.width as f64, params.height as f64),
(0.0, params.height as f64),
];
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for (x, y) in &corners {
let (_, dy) = combined.transform_point(*x, *y);
y_min = y_min.min(dy);
y_max = y_max.max(dy);
}
Some(YBBox { y_min, y_max })
}
fn precompute_clip_seen(list: &DisplayList) -> HashSet<u64> {
let mut counts: HashMap<u64, u32> = HashMap::new();
for elem in list.elements() {
if let DisplayElement::Clip { path, params } = elem {
let hash = hash_clip_path(path, ¶ms.fill_rule);
*counts.entry(hash).or_insert(0) += 1;
}
}
counts
.into_iter()
.filter(|(_, c)| *c > 1)
.map(|(h, _)| h)
.collect()
}
fn build_clip_epochs(list: &DisplayList, bboxes: &[Option<YBBox>]) -> Vec<ClipEpoch> {
let elements = list.elements();
let mut epochs = Vec::new();
let mut epoch_start = 0;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
let mut has_erase = false;
for (i, element) in elements.iter().enumerate() {
if matches!(element, DisplayElement::InitClip) && i > epoch_start {
epochs.push(ClipEpoch {
start_idx: epoch_start,
end_idx: i,
paint_bbox: if y_min <= y_max {
Some(YBBox { y_min, y_max })
} else {
None
},
has_erase_page: has_erase,
});
epoch_start = i;
y_min = f64::INFINITY;
y_max = f64::NEG_INFINITY;
has_erase = false;
}
if matches!(element, DisplayElement::ErasePage) {
has_erase = true;
}
if let Some(ref bbox) = bboxes[i] {
y_min = y_min.min(bbox.y_min);
y_max = y_max.max(bbox.y_max);
}
}
if epoch_start < elements.len() {
epochs.push(ClipEpoch {
start_idx: epoch_start,
end_idx: elements.len(),
paint_bbox: if y_min <= y_max {
Some(YBBox { y_min, y_max })
} else {
None
},
has_erase_page: has_erase,
});
}
epochs
}
fn composite_onto_white(data: &mut [u8]) {
for pixel in data.chunks_exact_mut(4) {
let a = pixel[3] as u16;
if a == 255 {
continue; }
let inv_a = 255 - a;
pixel[0] = (pixel[0] as u16 + inv_a).min(255) as u8;
pixel[1] = (pixel[1] as u16 + inv_a).min(255) as u8;
pixel[2] = (pixel[2] as u16 + inv_a).min(255) as u8;
pixel[3] = 255;
}
}
fn composite_non_isolated_group_cropped(
target: &mut Pixmap,
source: &Pixmap,
backdrop: &[u8],
params: &stet_graphics::display_list::GroupParams,
clip_mask: Option<&stet_tiny_skia::Mask>,
crop_x: i32,
crop_y: i32,
) {
let cw = source.width();
let ch = source.height();
let Some(mut contribution) = Pixmap::new(cw, ch) else {
return;
};
let src_data = source.data();
let contrib_data = contribution.data_mut();
for (i, chunk) in contrib_data.chunks_exact_mut(4).enumerate() {
let off = i * 4;
if src_data[off] != backdrop[off]
|| src_data[off + 1] != backdrop[off + 1]
|| src_data[off + 2] != backdrop[off + 2]
|| src_data[off + 3] != backdrop[off + 3]
{
chunk.copy_from_slice(&src_data[off..off + 4]);
}
}
let paint = stet_tiny_skia::PixmapPaint {
opacity: params.alpha as f32,
blend_mode: u8_to_blend_mode(params.blend_mode),
quality: stet_tiny_skia::FilterQuality::Nearest,
};
target.draw_pixmap(
crop_x,
crop_y,
contribution.as_ref(),
&paint,
Transform::identity(),
clip_mask,
);
}
fn composite_non_isolated_extracted(
target: &mut Pixmap,
source: &Pixmap,
isolated: &Pixmap,
backdrop: &[u8],
params: &stet_graphics::display_list::GroupParams,
clip_mask: Option<&stet_tiny_skia::Mask>,
crop_x: i32,
crop_y: i32,
) {
let cw = source.width();
let ch = source.height();
let Some(mut contribution) = Pixmap::new(cw, ch) else {
return;
};
let src_data = source.data();
let iso_data = isolated.data();
let contrib_data = contribution.data_mut();
for i in 0..(cw as usize * ch as usize) {
let off = i * 4;
let alpha_g = iso_data[off + 3];
if alpha_g == 0 {
continue; }
let inv_alpha = 255 - alpha_g as i32;
for c in 0..3 {
let r = src_data[off + c] as i32;
let b = backdrop[off + c] as i32;
let raw = r - (b * inv_alpha + 127) / 255;
contrib_data[off + c] = raw.clamp(0, 255) as u8;
}
contrib_data[off + 3] = alpha_g;
}
let paint = stet_tiny_skia::PixmapPaint {
opacity: params.alpha as f32,
blend_mode: u8_to_blend_mode(params.blend_mode),
quality: stet_tiny_skia::FilterQuality::Nearest,
};
target.draw_pixmap(
crop_x,
crop_y,
contribution.as_ref(),
&paint,
Transform::identity(),
clip_mask,
);
}
fn viewport_transform(t: Transform, vp_x: f32, vp_y: f32, scale_x: f32, scale_y: f32) -> Transform {
Transform::from_row(
t.sx * scale_x,
t.ky * scale_y,
t.kx * scale_x,
t.sy * scale_y,
(t.tx - vp_x) * scale_x,
(t.ty - vp_y) * scale_y,
)
}
fn box_resample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
if dw == 0 || dh == 0 {
return Vec::new();
}
let (sw, sh, dw, dh) = (sw as usize, sh as usize, dw as usize, dh as usize);
let ratio_x = sw as f32 / dw as f32;
let mut tmp = vec![0.0f32; dw * sh * 4];
let tmp_stride = dw * 4;
for y in 0..sh {
let row_off = y * sw * 4;
let dst_row = y * tmp_stride;
for dx in 0..dw {
let left_f = dx as f32 * ratio_x;
let right_f = (dx + 1) as f32 * ratio_x;
let left = (left_f as usize).min(sw - 1);
let right = (right_f.ceil() as usize).min(sw);
let inv_area = 1.0 / (right_f - left_f);
let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
for sx in left..right {
let pixel_left = sx as f32;
let pixel_right = (sx + 1) as f32;
let w = pixel_right.min(right_f) - pixel_left.max(left_f);
let i = row_off + sx * 4;
r += src[i] as f32 * w;
g += src[i + 1] as f32 * w;
b += src[i + 2] as f32 * w;
a += src[i + 3] as f32 * w;
}
let di = dst_row + dx * 4;
tmp[di] = r * inv_area;
tmp[di + 1] = g * inv_area;
tmp[di + 2] = b * inv_area;
tmp[di + 3] = a * inv_area;
}
}
let ratio_y = sh as f32 / dh as f32;
let mut out = vec![0u8; dw * dh * 4];
let out_stride = dw * 4;
for dy in 0..dh {
let top_f = dy as f32 * ratio_y;
let bottom_f = (dy + 1) as f32 * ratio_y;
let top = (top_f as usize).min(sh - 1);
let bottom = (bottom_f.ceil() as usize).min(sh);
let inv_area = 1.0 / (bottom_f - top_f);
let n_rows = bottom - top;
let mut row_weights_buf: [(usize, f32); 8] = [(0, 0.0); 8];
let row_weights_vec: Vec<(usize, f32)>;
let row_weights: &[(usize, f32)] = if n_rows <= 8 {
for (i, sy) in (top..bottom).enumerate() {
let pixel_top = sy as f32;
let pixel_bottom = (sy + 1) as f32;
let w = pixel_bottom.min(bottom_f) - pixel_top.max(top_f);
row_weights_buf[i] = (sy, w);
}
&row_weights_buf[..n_rows]
} else {
row_weights_vec = (top..bottom)
.map(|sy| {
let pixel_top = sy as f32;
let pixel_bottom = (sy + 1) as f32;
let w = pixel_bottom.min(bottom_f) - pixel_top.max(top_f);
(sy, w)
})
.collect();
&row_weights_vec
};
let dst_row = dy * out_stride;
for dx in 0..dw {
let col = dx * 4;
let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
for &(sy, w) in row_weights {
let i = sy * tmp_stride + col;
r += tmp[i] * w;
g += tmp[i + 1] * w;
b += tmp[i + 2] * w;
a += tmp[i + 3] * w;
}
let di = dst_row + col;
out[di] = (r * inv_area + 0.5).clamp(0.0, 255.0) as u8;
out[di + 1] = (g * inv_area + 0.5).clamp(0.0, 255.0) as u8;
out[di + 2] = (b * inv_area + 0.5).clamp(0.0, 255.0) as u8;
out[di + 3] = (a * inv_area + 0.5).clamp(0.0, 255.0) as u8;
}
}
out
}
fn bicubic_resample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
if dw == 0 || dh == 0 {
return Vec::new();
}
let (sw, sh, dw, dh) = (sw as usize, sh as usize, dw as usize, dh as usize);
let ratio_x = sw as f32 / dw as f32;
let ratio_y = sh as f32 / dh as f32;
let mut tmp = vec![0.0f32; dw * sh * 4];
for y in 0..sh {
let src_row = y * sw * 4;
let dst_row = y * dw * 4;
for dx in 0..dw {
let sx = (dx as f32 + 0.5) * ratio_x - 0.5;
let sx_floor = sx.floor() as i32;
let fx = sx - sx_floor as f32;
let w0 = catmull_rom(fx + 1.0);
let w1 = catmull_rom(fx);
let w2 = catmull_rom(1.0 - fx);
let w3 = catmull_rom(2.0 - fx);
let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0, 0.0, 0.0);
for (k, w) in [
(sx_floor - 1, w0),
(sx_floor, w1),
(sx_floor + 1, w2),
(sx_floor + 2, w3),
] {
let px = k.clamp(0, sw as i32 - 1) as usize;
let i = src_row + px * 4;
r += src[i] as f32 * w;
g += src[i + 1] as f32 * w;
b += src[i + 2] as f32 * w;
a += src[i + 3] as f32 * w;
}
let di = dst_row + dx * 4;
tmp[di] = r;
tmp[di + 1] = g;
tmp[di + 2] = b;
tmp[di + 3] = a;
}
}
let mut out = vec![0u8; dw * dh * 4];
let tmp_stride = dw * 4;
let out_stride = dw * 4;
for dy in 0..dh {
let sy = (dy as f32 + 0.5) * ratio_y - 0.5;
let sy_floor = sy.floor() as i32;
let fy = sy - sy_floor as f32;
let w0 = catmull_rom(fy + 1.0);
let w1 = catmull_rom(fy);
let w2 = catmull_rom(1.0 - fy);
let w3 = catmull_rom(2.0 - fy);
let py0 = (sy_floor - 1).clamp(0, sh as i32 - 1) as usize * tmp_stride;
let py1 = sy_floor.clamp(0, sh as i32 - 1) as usize * tmp_stride;
let py2 = (sy_floor + 1).clamp(0, sh as i32 - 1) as usize * tmp_stride;
let py3 = (sy_floor + 2).clamp(0, sh as i32 - 1) as usize * tmp_stride;
let dst_row = dy * out_stride;
for dx in 0..dw {
let col = dx * 4;
let r = tmp[py0 + col] * w0
+ tmp[py1 + col] * w1
+ tmp[py2 + col] * w2
+ tmp[py3 + col] * w3;
let g = tmp[py0 + col + 1] * w0
+ tmp[py1 + col + 1] * w1
+ tmp[py2 + col + 1] * w2
+ tmp[py3 + col + 1] * w3;
let b = tmp[py0 + col + 2] * w0
+ tmp[py1 + col + 2] * w1
+ tmp[py2 + col + 2] * w2
+ tmp[py3 + col + 2] * w3;
let a = tmp[py0 + col + 3] * w0
+ tmp[py1 + col + 3] * w1
+ tmp[py2 + col + 3] * w2
+ tmp[py3 + col + 3] * w3;
let di = dst_row + col;
out[di] = r.round().clamp(0.0, 255.0) as u8;
out[di + 1] = g.round().clamp(0.0, 255.0) as u8;
out[di + 2] = b.round().clamp(0.0, 255.0) as u8;
out[di + 3] = a.round().clamp(0.0, 255.0) as u8;
}
}
out
}
#[inline]
fn catmull_rom(t: f32) -> f32 {
let t = t.abs();
if t < 1.0 {
(1.5 * t - 2.5) * t * t + 1.0
} else if t < 2.0 {
((-0.5 * t + 2.5) * t - 4.0) * t + 2.0
} else {
0.0
}
}
pub fn build_icc_cache_for_list(
list: &DisplayList,
system_cmyk_bytes: Option<&std::sync::Arc<Vec<u8>>>,
) -> IccCache {
let mut cache = IccCache::new();
let mut seen = HashSet::new();
if let Some(cmyk_bytes) = system_cmyk_bytes
&& let Some(hash) = cache.register_profile(cmyk_bytes)
{
seen.insert(hash);
cache.set_default_cmyk_hash(hash);
cache.prepare_reverse_cmyk();
}
fn scan_elements(
elements: &[DisplayElement],
seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
cache: &mut IccCache,
) {
for element in elements {
if let DisplayElement::Group { elements: sub, .. } = element {
scan_elements(sub.elements(), seen, cache);
}
if let DisplayElement::SoftMasked { content, mask, .. } = element {
scan_elements(content.elements(), seen, cache);
scan_elements(mask.elements(), seen, cache);
}
if let DisplayElement::OcgGroup { elements: sub, .. } = element {
scan_elements(sub.elements(), seen, cache);
}
let shading_cs = match element {
DisplayElement::AxialShading { params } => Some(¶ms.color_space),
DisplayElement::RadialShading { params } => Some(¶ms.color_space),
DisplayElement::MeshShading { params } => Some(¶ms.color_space),
DisplayElement::PatchShading { params } => Some(¶ms.color_space),
_ => None,
};
if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
n,
profile_hash,
profile_data,
}) = shading_cs
{
if seen.insert(*profile_hash) {
cache.register_profile_with_n(profile_data, Some(*n));
}
}
if let DisplayElement::Image { params, .. } = element {
match ¶ms.color_space {
ImageColorSpace::ICCBased {
n,
profile_hash,
profile_data,
} if seen.insert(*profile_hash) => {
cache.register_profile_with_n(profile_data, Some(*n));
}
ImageColorSpace::Indexed { base, .. }
if matches!(base.as_ref(), ImageColorSpace::ICCBased { .. }) =>
{
if let ImageColorSpace::ICCBased {
n,
profile_hash,
profile_data,
} = base.as_ref()
{
if seen.insert(*profile_hash) {
cache.register_profile_with_n(profile_data, Some(*n));
}
}
}
_ => {}
}
}
}
}
scan_elements(list.elements(), &mut seen, &mut cache);
cache
}
fn register_shading_icc_profiles(list: &DisplayList, cache: &mut IccCache) {
fn register_image_iccs(
cs: &ImageColorSpace,
seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
cache: &mut IccCache,
) {
match cs {
ImageColorSpace::ICCBased {
n,
profile_hash,
profile_data,
} => {
if seen.insert(*profile_hash) {
cache.register_profile_with_n(profile_data, Some(*n));
}
}
ImageColorSpace::Indexed { base, .. } => register_image_iccs(base, seen, cache),
ImageColorSpace::Separation { alt_space, .. }
| ImageColorSpace::DeviceN { alt_space, .. } => {
register_image_iccs(alt_space, seen, cache)
}
_ => {}
}
}
fn scan(
elements: &[DisplayElement],
seen: &mut HashSet<stet_graphics::icc::ProfileHash>,
cache: &mut IccCache,
) {
for element in elements {
if let DisplayElement::Group { elements: sub, .. } = element {
scan(sub.elements(), seen, cache);
}
if let DisplayElement::SoftMasked { content, mask, .. } = element {
scan(content.elements(), seen, cache);
scan(mask.elements(), seen, cache);
}
if let DisplayElement::OcgGroup { elements: sub, .. } = element {
scan(sub.elements(), seen, cache);
}
let shading_cs = match element {
DisplayElement::AxialShading { params } => Some(¶ms.color_space),
DisplayElement::RadialShading { params } => Some(¶ms.color_space),
DisplayElement::MeshShading { params } => Some(¶ms.color_space),
DisplayElement::PatchShading { params } => Some(¶ms.color_space),
_ => None,
};
if let Some(stet_graphics::device::ShadingColorSpace::ICCBased {
n,
profile_hash,
profile_data,
}) = shading_cs
&& seen.insert(*profile_hash)
{
cache.register_profile_with_n(profile_data, Some(*n));
}
if let DisplayElement::Image { params, .. } = element {
register_image_iccs(¶ms.color_space, seen, cache);
}
}
}
let mut seen = HashSet::new();
scan(list.elements(), &mut seen, cache);
}
fn samples_to_rgba(
data: &[u8],
params: &ImageParams,
icc: Option<&IccCache>,
opm_zero_transparent: bool,
) -> Vec<u8> {
let w = params.width as usize;
let h = params.height as usize;
let npixels = w * h;
let bpc = params.bits_per_component;
match ¶ms.color_space {
ImageColorSpace::PreconvertedRGBA => {
data.to_vec()
}
ImageColorSpace::DeviceGray => {
let mut rgba = vec![255u8; npixels * 4];
if bpc == 16 {
for i in 0..npixels {
let g = data.get(i * 2).copied().unwrap_or(0);
let pi = i * 4;
rgba[pi] = g;
rgba[pi + 1] = g;
rgba[pi + 2] = g;
}
} else {
for i in 0..npixels {
let g = data.get(i).copied().unwrap_or(0);
let pi = i * 4;
rgba[pi] = g;
rgba[pi + 1] = g;
rgba[pi + 2] = g;
}
}
rgba
}
ImageColorSpace::DeviceRGB => {
let mut rgba = vec![255u8; npixels * 4];
if bpc == 16 {
for i in 0..npixels {
let si = i * 6;
let pi = i * 4;
rgba[pi] = data.get(si).copied().unwrap_or(0);
rgba[pi + 1] = data.get(si + 2).copied().unwrap_or(0);
rgba[pi + 2] = data.get(si + 4).copied().unwrap_or(0);
}
} else {
for i in 0..npixels {
let si = i * 3;
let pi = i * 4;
rgba[pi] = data.get(si).copied().unwrap_or(0);
rgba[pi + 1] = data.get(si + 1).copied().unwrap_or(0);
rgba[pi + 2] = data.get(si + 2).copied().unwrap_or(0);
}
}
rgba
}
ImageColorSpace::DeviceCMYK => {
if let Some(cache) = icc
&& let Some(cmyk_hash) = cache.default_cmyk_hash()
{
let avail_pixels = data.len() / 4;
let icc_pixels = avail_pixels.min(npixels);
if icc_pixels > 0
&& let Some(rgb) = cache.convert_image_8bit(cmyk_hash, data, icc_pixels)
{
let mut rgba = vec![255u8; npixels * 4];
for i in 0..icc_pixels {
rgba[i * 4] = rgb[i * 3];
rgba[i * 4 + 1] = rgb[i * 3 + 1];
rgba[i * 4 + 2] = rgb[i * 3 + 2];
if opm_zero_transparent {
let si = i * 4;
if data[si] == 0
&& data[si + 1] == 0
&& data[si + 2] == 0
&& data[si + 3] == 0
{
rgba[i * 4 + 3] = 0;
}
}
}
return rgba;
}
}
let mut rgba = vec![255u8; npixels * 4];
for i in 0..npixels {
let si = i * 4;
let c = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
let m = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
let y = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
let k = data.get(si + 3).copied().unwrap_or(0) as f64 / 255.0;
let r = (1.0 - c.min(1.0)) * (1.0 - k.min(1.0));
let g = (1.0 - m.min(1.0)) * (1.0 - k.min(1.0));
let b = (1.0 - y.min(1.0)) * (1.0 - k.min(1.0));
let pi = i * 4;
rgba[pi] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
rgba[pi + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
rgba[pi + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
if opm_zero_transparent
&& data.get(si).copied().unwrap_or(0) == 0
&& data.get(si + 1).copied().unwrap_or(0) == 0
&& data.get(si + 2).copied().unwrap_or(0) == 0
&& data.get(si + 3).copied().unwrap_or(0) == 0
{
rgba[pi + 3] = 0;
}
}
rgba
}
ImageColorSpace::ICCBased {
n,
profile_hash,
profile_data,
} => {
if let Some(cache) = icc
&& cache.has_profile(profile_hash)
&& let Some(rgb) = cache.convert_image_8bit(profile_hash, data, npixels)
{
let mut rgba = vec![255u8; npixels * 4];
for i in 0..npixels {
rgba[i * 4] = rgb[i * 3];
rgba[i * 4 + 1] = rgb[i * 3 + 1];
rgba[i * 4 + 2] = rgb[i * 3 + 2];
if opm_zero_transparent && *n == 4 {
let si = i * *n as usize;
if si + 3 < data.len()
&& data[si] == 0
&& data[si + 1] == 0
&& data[si + 2] == 0
&& data[si + 3] == 0
{
rgba[i * 4 + 3] = 0;
}
}
}
return rgba;
}
let _ = (profile_hash, profile_data);
let fallback = match n {
1 => ImageColorSpace::DeviceGray,
4 => ImageColorSpace::DeviceCMYK,
_ => ImageColorSpace::DeviceRGB,
};
let p = ImageParams {
color_space: fallback,
bits_per_component: 8,
..params.clone()
};
samples_to_rgba(data, &p, icc, opm_zero_transparent)
}
ImageColorSpace::Indexed {
base,
hival,
lookup,
} => {
let base_ncomp = base.num_components() as usize;
let mut expanded = Vec::with_capacity(npixels * base_ncomp);
for i in 0..npixels {
let idx = data.get(i).copied().unwrap_or(0) as usize;
let idx = idx.min(*hival as usize);
let offset = idx * base_ncomp;
for c in 0..base_ncomp {
expanded.push(lookup.get(offset + c).copied().unwrap_or(0));
}
}
let p = ImageParams {
color_space: *base.clone(),
bits_per_component: 8,
..params.clone()
};
samples_to_rgba(&expanded, &p, icc, opm_zero_transparent)
}
ImageColorSpace::CIEBasedABC { params: cie_params } => {
let mut rgba = vec![255u8; npixels * 4];
for i in 0..npixels {
let si = i * 3;
let a = data.get(si).copied().unwrap_or(0) as f64 / 255.0;
let b = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0;
let c = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0;
let color = DeviceColor::from_cie_abc(a, b, c, cie_params);
let pi = i * 4;
rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
}
rgba
}
ImageColorSpace::CIEBasedA { params: cie_params } => {
let mut rgba = vec![255u8; npixels * 4];
for i in 0..npixels {
let val = data.get(i).copied().unwrap_or(0) as f64 / 255.0;
let color = DeviceColor::from_cie_a(val, cie_params);
let pi = i * 4;
rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
}
rgba
}
ImageColorSpace::Lab { range, .. } => {
let mut rgba = vec![255u8; npixels * 4];
let a_span = range[1] - range[0];
let b_span = range[3] - range[2];
for i in 0..npixels {
let si = i * 3;
let l = data.get(si).copied().unwrap_or(0) as f64 / 255.0 * 100.0;
let a = data.get(si + 1).copied().unwrap_or(0) as f64 / 255.0 * a_span + range[0];
let b = data.get(si + 2).copied().unwrap_or(0) as f64 / 255.0 * b_span + range[2];
let color = DeviceColor::from_lab(l, a, b, range);
let pi = i * 4;
rgba[pi] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
rgba[pi + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
rgba[pi + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
}
rgba
}
ImageColorSpace::Separation {
alt_space,
tint_table,
..
} => {
if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
&& let Some(rgba) = tint_separation_via_icc(data, npixels, tint_table, icc)
{
return rgba;
}
let mut rgba = vec![255u8; npixels * 4];
let no = tint_table.num_outputs as usize;
let mut alt_comps = vec![0.0f32; no];
for i in 0..npixels {
let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
tint_table.lookup_1d(tint, &mut alt_comps);
let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
let pi = i * 4;
rgba[pi] = r;
rgba[pi + 1] = g;
rgba[pi + 2] = b;
}
rgba
}
ImageColorSpace::DeviceN {
alt_space,
tint_table,
..
} => {
let ni = tint_table.num_inputs as usize;
let no = tint_table.num_outputs as usize;
if matches!(alt_space.as_ref(), ImageColorSpace::DeviceCMYK)
&& let Some(rgba) = tint_devicen_via_icc(data, npixels, ni, tint_table, icc)
{
return rgba;
}
let mut rgba = vec![255u8; npixels * 4];
let mut inputs = vec![0.0f32; ni];
let mut alt_comps = vec![0.0f32; no];
for i in 0..npixels {
let si = i * ni;
for (c, inp) in inputs.iter_mut().enumerate() {
*inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
}
tint_table.lookup_nd(&inputs, &mut alt_comps);
let (r, g, b) = alt_comps_to_rgb(&alt_comps, alt_space);
let pi = i * 4;
rgba[pi] = r;
rgba[pi + 1] = g;
rgba[pi + 2] = b;
}
rgba
}
ImageColorSpace::Mask { color, polarity } => {
let mut rgba = vec![0u8; npixels * 4];
let r = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
let g = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
let b = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
let bytes_per_row = (w).div_ceil(8);
for row in 0..h {
for col in 0..w {
let byte_idx = row * bytes_per_row + col / 8;
let bit_offset = 7 - (col % 8);
let bit = if byte_idx < data.len() {
(data[byte_idx] >> bit_offset) & 1
} else {
0
};
let paint = if *polarity { bit == 1 } else { bit == 0 };
if paint {
let pi = (row * w + col) * 4;
rgba[pi] = r;
rgba[pi + 1] = g;
rgba[pi + 2] = b;
rgba[pi + 3] = 255;
}
}
}
rgba
}
}
}
fn tint_separation_via_icc(
data: &[u8],
npixels: usize,
tint_table: &TintLookupTable,
icc: Option<&IccCache>,
) -> Option<Vec<u8>> {
let cache = icc?;
let cmyk_hash = cache.default_cmyk_hash()?;
let mut cmyk_data = vec![0u8; npixels * 4];
let mut alt_comps = [0.0f32; 4];
for i in 0..npixels {
let tint = data.get(i).copied().unwrap_or(0) as f32 / 255.0;
tint_table.lookup_1d(tint, &mut alt_comps);
let si = i * 4;
cmyk_data[si] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
cmyk_data[si + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
cmyk_data[si + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
cmyk_data[si + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
}
let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
let mut rgba = vec![255u8; npixels * 4];
for i in 0..npixels {
rgba[i * 4] = rgb[i * 3];
rgba[i * 4 + 1] = rgb[i * 3 + 1];
rgba[i * 4 + 2] = rgb[i * 3 + 2];
}
Some(rgba)
}
fn tint_devicen_via_icc(
data: &[u8],
npixels: usize,
ni: usize,
tint_table: &TintLookupTable,
icc: Option<&IccCache>,
) -> Option<Vec<u8>> {
let cache = icc?;
let cmyk_hash = cache.default_cmyk_hash()?;
let mut cmyk_data = vec![0u8; npixels * 4];
let mut inputs = vec![0.0f32; ni];
let mut alt_comps = [0.0f32; 4];
for i in 0..npixels {
let si = i * ni;
for (c, inp) in inputs.iter_mut().enumerate() {
*inp = data.get(si + c).copied().unwrap_or(0) as f32 / 255.0;
}
tint_table.lookup_nd(&inputs, &mut alt_comps);
let di = i * 4;
cmyk_data[di] = (alt_comps[0].clamp(0.0, 1.0) * 255.0).round() as u8;
cmyk_data[di + 1] = (alt_comps[1].clamp(0.0, 1.0) * 255.0).round() as u8;
cmyk_data[di + 2] = (alt_comps[2].clamp(0.0, 1.0) * 255.0).round() as u8;
cmyk_data[di + 3] = (alt_comps[3].clamp(0.0, 1.0) * 255.0).round() as u8;
}
let rgb = cache.convert_image_8bit(cmyk_hash, &cmyk_data, npixels)?;
let mut rgba = vec![255u8; npixels * 4];
for i in 0..npixels {
rgba[i * 4] = rgb[i * 3];
rgba[i * 4 + 1] = rgb[i * 3 + 1];
rgba[i * 4 + 2] = rgb[i * 3 + 2];
}
Some(rgba)
}
fn alt_comps_to_rgb(comps: &[f32], alt_space: &ImageColorSpace) -> (u8, u8, u8) {
match alt_space {
ImageColorSpace::DeviceGray => {
let g = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
(g, g, g)
}
ImageColorSpace::DeviceRGB => {
let r = (comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
let g = (comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
let b = (comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0) * 255.0).round() as u8;
(r, g, b)
}
ImageColorSpace::DeviceCMYK => {
let c = comps.first().copied().unwrap_or(0.0).clamp(0.0, 1.0);
let m = comps.get(1).copied().unwrap_or(0.0).clamp(0.0, 1.0);
let y = comps.get(2).copied().unwrap_or(0.0).clamp(0.0, 1.0);
let k = comps.get(3).copied().unwrap_or(0.0).clamp(0.0, 1.0);
let r = ((1.0 - (c + k).min(1.0)) * 255.0).round() as u8;
let g = ((1.0 - (m + k).min(1.0)) * 255.0).round() as u8;
let b = ((1.0 - (y + k).min(1.0)) * 255.0).round() as u8;
(r, g, b)
}
_ => (0, 0, 0),
}
}
fn apply_mask_color_rgba(rgba: &mut [u8], sample_data: &[u8], params: &ImageParams) {
let mask_color = match ¶ms.mask_color {
Some(mc) => mc,
None => return,
};
let ncomp = params.color_space.num_components() as usize;
let npixels = params.width as usize * params.height as usize;
let is_range = mask_color.len() == 2 * ncomp;
for i in 0..npixels {
let si = i * ncomp;
let matched = if is_range {
(0..ncomp).all(|c| {
let sample = sample_data.get(si + c).copied().unwrap_or(0);
let min_val = mask_color.get(c * 2).copied().unwrap_or(0);
let max_val = mask_color.get(c * 2 + 1).copied().unwrap_or(0);
sample >= min_val && sample <= max_val
})
} else {
(0..ncomp).all(|c| {
let sample = sample_data.get(si + c).copied().unwrap_or(0);
let target = mask_color.get(c).copied().unwrap_or(0);
sample == target
})
};
if matched {
let pi = i * 4;
if pi + 3 < rgba.len() {
rgba[pi] = 0;
rgba[pi + 1] = 0;
rgba[pi + 2] = 0;
rgba[pi + 3] = 0;
}
}
}
}
fn image_filter_quality(transform: Transform, interpolate: bool) -> stet_tiny_skia::FilterQuality {
let eff_sx = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
let eff_sy = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
let min_scale = eff_sx.min(eff_sy);
if (eff_sx - 1.0).abs() < 0.01 && (eff_sy - 1.0).abs() < 0.01 {
stet_tiny_skia::FilterQuality::Nearest
} else if !interpolate && min_scale >= 0.95 {
stet_tiny_skia::FilterQuality::Nearest
} else {
stet_tiny_skia::FilterQuality::Bilinear
}
}
fn prescale_image(
rgba_data: &[u8],
w: u32,
h: u32,
transform: Transform,
interpolate: bool,
) -> Option<(Vec<u8>, u32, u32, Transform)> {
let scale_x = (transform.sx * transform.sx + transform.ky * transform.ky).sqrt();
let scale_y = (transform.kx * transform.kx + transform.sy * transform.sy).sqrt();
let min_scale = scale_x.min(scale_y);
if min_scale > 1.05 {
if interpolate {
let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
if is_axis_aligned && w >= 2 && h >= 2 {
let dw = (w as f32 * transform.sx.abs()).round().max(1.0) as u32;
let dh = (h as f32 * transform.sy.abs()).round().max(1.0) as u32;
if dw > w || dh > h {
let resampled = bicubic_resample(rgba_data, w, h, dw, dh);
let new_sx = transform.sx * w as f32 / dw as f32;
let new_sy = transform.sy * h as f32 / dh as f32;
let adjusted = Transform::from_row(
new_sx,
transform.ky,
transform.kx,
new_sy,
transform.tx,
transform.ty,
);
return Some((resampled, dw, dh, adjusted));
}
}
}
return None;
}
if min_scale >= 0.95 {
return None;
}
let is_axis_aligned = transform.kx.abs() < 1e-4 && transform.ky.abs() < 1e-4;
if is_axis_aligned && w >= 2 && h >= 2 {
let dw = (w as f32 * transform.sx.abs()).ceil().max(1.0) as u32;
let dh = (h as f32 * transform.sy.abs()).ceil().max(1.0) as u32;
if dw < w || dh < h {
let resampled = box_resample(rgba_data, w, h, dw, dh);
let new_sx = transform.sx * w as f32 / dw as f32;
let new_sy = transform.sy * h as f32 / dh as f32;
let adjusted = Transform::from_row(
new_sx,
transform.ky,
transform.kx,
new_sy,
transform.tx,
transform.ty,
);
return Some((resampled, dw, dh, adjusted));
}
}
let factor = (1.0 / min_scale) as u32;
if factor < 2 || w < factor || h < factor {
return None;
}
let nw = w / factor;
let nh = h / factor;
if nw == 0 || nh == 0 {
return None;
}
let area = factor * factor;
let half = area / 2;
let stride = w as usize * 4;
let mut out = vec![0u8; (nw * nh * 4) as usize];
for dy in 0..nh {
for dx in 0..nw {
let (mut r, mut g, mut b, mut a) = (0u32, 0u32, 0u32, 0u32);
let sy0 = (dy * factor) as usize;
let sx0 = (dx * factor) as usize;
for iy in 0..factor as usize {
let row = (sy0 + iy) * stride + sx0 * 4;
for ix in 0..factor as usize {
let i = row + ix * 4;
r += rgba_data[i] as u32;
g += rgba_data[i + 1] as u32;
b += rgba_data[i + 2] as u32;
a += rgba_data[i + 3] as u32;
}
}
let di = (dy * nw + dx) as usize * 4;
out[di] = ((r + half) / area) as u8;
out[di + 1] = ((g + half) / area) as u8;
out[di + 2] = ((b + half) / area) as u8;
out[di + 3] = ((a + half) / area) as u8;
}
}
let f = factor as f32;
let adjusted = Transform::from_row(
transform.sx * f,
transform.ky * f,
transform.kx * f,
transform.sy * f,
transform.tx,
transform.ty,
);
Some((out, nw, nh, adjusted))
}
fn translate_clip_rect(rect: &ClipRect, y_start: u32, band_h: u32) -> ClipRect {
ClipRect {
x0: rect.x0,
y0: rect.y0.saturating_sub(y_start).min(band_h),
x1: rect.x1,
y1: rect.y1.saturating_sub(y_start).min(band_h),
}
}
fn enforce_min_image_size(transform: Transform, img_w: u32, img_h: u32) -> Transform {
let eff_w =
((transform.sx * img_w as f32).powi(2) + (transform.ky * img_w as f32).powi(2)).sqrt();
let eff_h =
((transform.kx * img_h as f32).powi(2) + (transform.sy * img_h as f32).powi(2)).sqrt();
if eff_w >= 1.0 && eff_h >= 1.0 {
return transform;
}
let ratio = eff_w.max(eff_h) / eff_w.min(eff_h).max(0.001);
if ratio < 3.0 {
return transform;
}
let mut t = transform;
if eff_w < 1.0 && eff_w > 0.001 {
let boost = 1.0 / eff_w;
t.sx *= boost;
t.ky *= boost;
}
if eff_h < 1.0 && eff_h > 0.001 {
let boost = 1.0 / eff_h;
t.kx *= boost;
t.sy *= boost;
}
t
}
fn hairline_min_width(ctm: &Matrix, dpi: f64) -> f64 {
let (a, b, c, d) = (ctm.a, ctm.b, ctm.c, ctm.d);
let sum_sq = a * a + b * b + c * c + d * d;
let diff = ((a * a + b * b - c * c - d * d).powi(2) + 4.0 * (a * c + b * d).powi(2)).sqrt();
let s_max = (0.5 * (sum_sq + diff)).max(0.0).sqrt();
let min_px = if dpi <= 150.0 { 0.5 } else { 1.0 };
if s_max > 1e-10 {
min_px / s_max
} else {
min_px
}
}
fn is_k_only_src(color: &DeviceColor) -> bool {
if let Some((c, m, y, _k)) = color.native_cmyk {
c == 0.0 && m == 0.0 && y == 0.0
} else {
false
}
}
fn needs_gray_promotion(
overprint: bool,
painted_channels: u8,
is_device_cmyk: bool,
color: &DeviceColor,
) -> Option<f64> {
if !overprint
|| painted_channels != 0
|| is_device_cmyk
|| color.native_cmyk.is_some()
|| color.process_cmyk.is_some()
{
return None;
}
let r = color.r;
if (r - color.g).abs() > f64::EPSILON || (r - color.b).abs() > f64::EPSILON {
return None;
}
Some(r.clamp(0.0, 1.0))
}
fn maybe_promote_gray_fill<'a>(
params: &'a FillParams,
buf: &'a mut Option<FillParams>,
) -> &'a FillParams {
if let Some(gray) = needs_gray_promotion(
params.overprint,
params.painted_channels,
params.is_device_cmyk,
¶ms.color,
) {
let mut promoted = params.clone();
promoted.is_device_cmyk = true;
promoted.painted_channels = stet_graphics::device::CMYK_K;
promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
*buf = Some(promoted);
return buf.as_ref().unwrap();
}
params
}
fn maybe_promote_gray_stroke<'a>(
params: &'a StrokeParams,
buf: &'a mut Option<StrokeParams>,
) -> &'a StrokeParams {
if let Some(gray) = needs_gray_promotion(
params.overprint,
params.painted_channels,
params.is_device_cmyk,
¶ms.color,
) {
let mut promoted = params.clone();
promoted.is_device_cmyk = true;
promoted.painted_channels = stet_graphics::device::CMYK_K;
promoted.color.native_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
promoted.color.process_cmyk = Some((0.0, 0.0, 0.0, 1.0 - gray));
*buf = Some(promoted);
return buf.as_ref().unwrap();
}
params
}
fn build_stroke(params: &StrokeParams, dpi: f64) -> Stroke {
let min_lw = hairline_min_width(¶ms.ctm, dpi);
let mut stroke = Stroke {
width: (params.line_width as f32).max(min_lw as f32),
line_cap: to_line_cap(params.line_cap),
line_join: to_line_join(params.line_join),
miter_limit: params.miter_limit as f32,
..Stroke::default()
};
if !params.dash_pattern.array.is_empty() {
let mut dash_array: Vec<f32> = params
.dash_pattern
.array
.iter()
.map(|&v| v as f32)
.collect();
if dash_array.len() % 2 == 1 {
let clone = dash_array.clone();
dash_array.extend_from_slice(&clone);
}
if let Some(dash) = StrokeDash::new(dash_array, params.dash_pattern.offset as f32) {
stroke.dash = Some(dash);
}
}
stroke
}
fn ctm_is_device_space(ctm: &Matrix) -> bool {
(ctm.a.abs() - 1.0).abs() < 0.01
&& ctm.b.abs() < 0.01
&& ctm.c.abs() < 0.01
&& (ctm.d.abs() - 1.0).abs() < 0.01
}
fn stroke_adjust_path_viewport(
path: &PsPath,
device_width: f64,
scale_x: f64,
scale_y: f64,
vp_x: f64,
vp_y: f64,
) -> PsPath {
let use_half_pixel = device_width < 1.5 || (device_width.round() as i32) % 2 == 1;
let snap_x = |v: f64| -> f64 {
let out = (v - vp_x) * scale_x;
let snapped = if use_half_pixel {
out.floor() + 0.5
} else {
out.round()
};
snapped / scale_x + vp_x
};
let snap_y = |v: f64| -> f64 {
let out = (v - vp_y) * scale_y;
let snapped = if use_half_pixel {
out.floor() + 0.5
} else {
out.round()
};
snapped / scale_y + vp_y
};
let mut result = PsPath::new();
let mut prev_x = 0.0_f64;
let mut prev_y = 0.0_f64;
for seg in &path.segments {
match *seg {
PathSegment::MoveTo(x, y) => {
prev_x = x;
prev_y = y;
result.segments.push(PathSegment::MoveTo(x, y));
}
PathSegment::LineTo(x, y) => {
let is_horizontal = (y - prev_y).abs() < 1e-6;
let is_vertical = (x - prev_x).abs() < 1e-6;
if is_horizontal {
let snapped_y = snap_y(y);
if let Some(PathSegment::MoveTo(_, ly) | PathSegment::LineTo(_, ly)) =
result.segments.last_mut()
{
*ly = snapped_y;
}
result.segments.push(PathSegment::LineTo(x, snapped_y));
prev_x = x;
prev_y = snapped_y;
} else if is_vertical {
let snapped_x = snap_x(x);
if let Some(PathSegment::MoveTo(lx, _) | PathSegment::LineTo(lx, _)) =
result.segments.last_mut()
{
*lx = snapped_x;
}
result.segments.push(PathSegment::LineTo(snapped_x, y));
prev_x = snapped_x;
prev_y = y;
} else {
result.segments.push(PathSegment::LineTo(x, y));
prev_x = x;
prev_y = y;
}
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => {
result.segments.push(PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x3,
y3,
});
prev_x = x3;
prev_y = y3;
}
PathSegment::ClosePath => {
result.segments.push(PathSegment::ClosePath);
}
}
}
result
}
fn render_element(
pixmap: &mut Pixmap,
band_state: &mut BandState,
element: &DisplayElement,
ctx: &RenderContext<'_>,
) {
match element {
DisplayElement::Fill { path, params } => {
let mut promoted_fill: Option<FillParams> = None;
let params = maybe_promote_gray_fill(params, &mut promoted_fill);
let painted = params.painted_channels;
let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
let custom_spot =
painted == 0 && !params.is_device_cmyk && params.color.native_cmyk.is_some();
let is_k_only_cmyk =
params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
let needs_overprint = params.overprint
&& band_state.cmyk_buffer.is_some()
&& params.blend_mode == 0
&& (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
if needs_overprint {
let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
render_overprint_fill(
pixmap,
&mut cmyk_buf,
&mut op_bg,
&mut op_touched,
&spot_mask,
band_state,
path,
params,
ctx.vp_x,
ctx.vp_y,
ctx.scale_x,
ctx.scale_y,
ctx.out_w,
ctx.out_h,
ctx.icc,
ctx.no_aa,
);
band_state.cmyk_buffer = Some(cmyk_buf);
band_state.restore_op_buffers(op_bg, op_touched);
band_state.restore_spot_mask(spot_mask);
} else {
let Some(skia_path) = build_skia_path(path) else {
return;
};
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(
&band_state.clip_region,
&mut temp_mask,
ctx.out_w,
ctx.out_h,
) else {
return;
};
let paint =
to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
let transform = ctx.transform(¶ms.ctm);
if is_degenerate_fill(path) {
let stroke = Stroke {
width: 1.0,
..Stroke::default()
};
pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
} else {
let fill_rule = to_fill_rule(¶ms.fill_rule);
pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
}
if band_state.cmyk_buffer.is_some() {
let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
update_cmyk_buffer_for_fill(
&mut cmyk_buf,
&mut spot_mask,
path,
params,
ctx.vp_x,
ctx.vp_y,
ctx.scale_x,
ctx.scale_y,
ctx.out_w,
ctx.out_h,
&band_state.clip_region,
ctx.no_aa,
ctx.icc,
);
band_state.cmyk_buffer = Some(cmyk_buf);
band_state.restore_spot_mask(spot_mask);
}
}
}
DisplayElement::Stroke { path, params } => {
let mut promoted_stroke: Option<StrokeParams> = None;
let params = maybe_promote_gray_stroke(params, &mut promoted_stroke);
let transform = ctx.transform(¶ms.ctm);
let vp_ctm = Matrix {
a: transform.sx as f64,
b: transform.ky as f64,
c: transform.kx as f64,
d: transform.sy as f64,
tx: 0.0,
ty: 0.0,
};
let vp_params = StrokeParams {
ctm: vp_ctm,
..params.clone()
};
let stroke = build_stroke(&vp_params, ctx.effective_dpi);
let adjusted;
let draw_path = if params.stroke_adjust
&& stroke.width <= 2.0
&& ctm_is_device_space(¶ms.ctm)
{
adjusted = stroke_adjust_path_viewport(
path,
stroke.width as f64,
ctx.scale_x as f64,
ctx.scale_y as f64,
ctx.vp_x as f64,
ctx.vp_y as f64,
);
&adjusted
} else {
path
};
let painted = params.painted_channels;
let subset_channels = painted != 0 && painted != stet_graphics::device::CMYK_ALL;
let opm1_cmyk = params.is_device_cmyk && params.overprint_mode == 1;
let custom_spot =
painted == 0 && !params.is_device_cmyk && params.color.native_cmyk.is_some();
let is_k_only_cmyk =
params.is_device_cmyk && params.overprint_mode == 0 && is_k_only_src(¶ms.color);
let needs_overprint = params.overprint
&& band_state.cmyk_buffer.is_some()
&& params.blend_mode == 0
&& (subset_channels || opm1_cmyk || custom_spot || is_k_only_cmyk);
let Some(skia_path) = build_skia_path(draw_path) else {
return;
};
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(
&band_state.clip_region,
&mut temp_mask,
ctx.out_w,
ctx.out_h,
) else {
return;
};
if needs_overprint {
let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
let spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
render_overprint_stroke(
pixmap,
&mut cmyk_buf,
&mut op_bg,
&mut op_touched,
&spot_mask,
band_state,
&skia_path,
&stroke,
transform,
params,
ctx.out_w,
ctx.out_h,
ctx.icc,
ctx.no_aa,
);
band_state.cmyk_buffer = Some(cmyk_buf);
band_state.restore_op_buffers(op_bg, op_touched);
band_state.restore_spot_mask(spot_mask);
} else {
let paint =
to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, ctx.no_aa);
pixmap.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
if band_state.cmyk_buffer.is_some() {
let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
let mut spot_mask = band_state.take_spot_mask(ctx.out_w, ctx.out_h);
update_cmyk_buffer_for_stroke(
&mut cmyk_buf,
&mut spot_mask,
draw_path,
params,
&stroke,
transform,
ctx.out_w,
ctx.out_h,
&band_state.clip_region,
ctx.no_aa,
ctx.icc,
);
band_state.cmyk_buffer = Some(cmyk_buf);
band_state.restore_spot_mask(spot_mask);
}
}
}
DisplayElement::Clip { path, params } => {
clip_path_unified(band_state, path, params, ctx);
}
DisplayElement::InitClip => {
if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
band_state.recycle_mask(mask);
}
band_state.clip_region = None;
}
DisplayElement::ErasePage => {
pixmap.fill(Color::TRANSPARENT);
if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
band_state.recycle_mask(mask);
}
band_state.clip_region = None;
}
DisplayElement::Image {
sample_data,
params,
} => {
let iw = params.width;
let ih = params.height;
if iw == 0 || ih == 0 {
return;
}
let needs_overprint = params.overprint
&& band_state.cmyk_buffer.is_some()
&& image_supports_overprint(¶ms.color_space);
if needs_overprint {
let mut cmyk_buf = band_state.cmyk_buffer.take().unwrap();
let (mut op_bg, mut op_touched) = band_state.take_op_buffers(ctx.out_w, ctx.out_h);
render_overprint_image(
pixmap,
&mut cmyk_buf,
&mut op_bg,
&mut op_touched,
band_state,
sample_data,
params,
ctx.vp_x,
ctx.vp_y,
ctx.scale_x,
ctx.scale_y,
ctx.out_w,
ctx.out_h,
ctx.icc,
);
band_state.cmyk_buffer = Some(cmyk_buf);
band_state.restore_op_buffers(op_bg, op_touched);
} else if let Some(pp) = ctx
.preprocessed
.and_then(|pp| pp.get(ctx.elem_idx))
.and_then(|e| e.as_ref())
{
let Some(image_inv) = params.image_matrix.invert() else {
return;
};
let combined = params.ctm.concat(&image_inv);
let raw_transform = ctx.transform(&combined);
let transform = Transform::from_row(
pp.adj_sx,
pp.adj_ky,
pp.adj_kx,
pp.adj_sy,
raw_transform.tx,
raw_transform.ty,
);
let Some(img_pixmap) =
stet_tiny_skia::PixmapRef::from_bytes(&pp.data, pp.width, pp.height)
else {
return;
};
#[allow(unused_assignments)]
let mut temp_mask = None;
let mask_ref = match &band_state.clip_region {
None => None,
Some(ClipRegion::Mask(m)) => Some(m as &Mask),
Some(ClipRegion::Rect(rect)) => {
if rect.is_empty() {
return;
} else if rect.is_full_page(ctx.out_w, ctx.out_h) {
None
} else {
temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
temp_mask.as_ref()
}
}
};
let img_paint = stet_tiny_skia::PixmapPaint {
quality: pp.quality,
opacity: params.alpha as f32,
blend_mode: u8_to_blend_mode(params.blend_mode),
};
pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
update_cmyk_buffer_for_image(
cmyk_buf,
sample_data,
pixmap.data(),
params,
ctx.vp_x,
ctx.vp_y,
ctx.scale_x,
ctx.scale_y,
ctx.out_w,
ctx.out_h,
&band_state.clip_region,
ctx.icc,
);
}
} else {
let owned_rgba;
let rgba_data: &[u8] = if let Some(cached) =
ctx.image_cache.and_then(|c| c.get(ctx.elem_idx))
{
cached
} else {
owned_rgba = {
let mut rgba =
samples_to_rgba(sample_data, params, ctx.icc, ctx.opm_zero_transparent);
if params.mask_color.is_some() {
apply_mask_color_rgba(&mut rgba, sample_data, params);
}
rgba
};
&owned_rgba
};
let expected = (iw * ih * 4) as usize;
if rgba_data.len() < expected {
return;
}
let Some(image_inv) = params.image_matrix.invert() else {
return;
};
let combined = params.ctm.concat(&image_inv);
let raw_transform = enforce_min_image_size(ctx.transform(&combined), iw, ih);
let prescaled =
prescale_image(rgba_data, iw, ih, raw_transform, params.interpolate);
let (img_data, img_w, img_h, transform) = match &prescaled {
Some((data, w, h, t)) => (data.as_slice(), *w, *h, *t),
None => (rgba_data, iw, ih, raw_transform),
};
let Some(img_pixmap) =
stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h)
else {
return;
};
#[allow(unused_assignments)]
let mut temp_mask = None;
let mask_ref = match &band_state.clip_region {
None => None,
Some(ClipRegion::Mask(m)) => Some(m as &Mask),
Some(ClipRegion::Rect(rect)) => {
if rect.is_empty() {
return;
} else if rect.is_full_page(ctx.out_w, ctx.out_h) {
None
} else {
temp_mask = rect.make_mask(ctx.out_w, ctx.out_h);
temp_mask.as_ref()
}
}
};
let img_paint = stet_tiny_skia::PixmapPaint {
quality: image_filter_quality(transform, params.interpolate),
opacity: params.alpha as f32,
blend_mode: u8_to_blend_mode(params.blend_mode),
};
pixmap.draw_pixmap(0, 0, img_pixmap, &img_paint, transform, mask_ref);
if let Some(ref mut cmyk_buf) = band_state.cmyk_buffer {
update_cmyk_buffer_for_image(
cmyk_buf,
sample_data,
pixmap.data(),
params,
ctx.vp_x,
ctx.vp_y,
ctx.scale_x,
ctx.scale_y,
ctx.out_w,
ctx.out_h,
&band_state.clip_region,
ctx.icc,
);
}
}
}
DisplayElement::AxialShading { params } => {
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(
&band_state.clip_region,
&mut temp_mask,
ctx.out_w,
ctx.out_h,
) else {
return;
};
render_axial_shading(
pixmap,
params,
ctx.vp_x,
ctx.vp_y,
ctx.scale_x,
ctx.scale_y,
mask_ref,
ctx.no_aa,
band_state.cmyk_buffer.as_deref_mut(),
ctx.icc,
);
}
DisplayElement::RadialShading { params } => {
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(
&band_state.clip_region,
&mut temp_mask,
ctx.out_w,
ctx.out_h,
) else {
return;
};
render_radial_shading(
pixmap,
params,
ctx.vp_x,
ctx.vp_y,
ctx.scale_x,
ctx.scale_y,
mask_ref,
ctx.no_aa,
band_state.cmyk_buffer.as_deref_mut(),
ctx.icc,
);
}
DisplayElement::MeshShading { params } => {
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(
&band_state.clip_region,
&mut temp_mask,
ctx.out_w,
ctx.out_h,
) else {
return;
};
render_mesh_shading(
pixmap,
params,
ctx.vp_x,
ctx.vp_y,
ctx.scale_x,
ctx.scale_y,
mask_ref,
band_state.cmyk_buffer.as_deref_mut(),
ctx.icc,
);
}
DisplayElement::PatchShading { params } => {
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(
&band_state.clip_region,
&mut temp_mask,
ctx.out_w,
ctx.out_h,
) else {
return;
};
render_patch_shading(
pixmap,
params,
ctx.vp_x,
ctx.vp_y,
ctx.scale_x,
ctx.scale_y,
mask_ref,
band_state.cmyk_buffer.as_deref_mut(),
ctx.icc,
);
}
DisplayElement::PatternFill { params } => {
render_pattern_fill(pixmap, band_state, params, ctx);
}
DisplayElement::Group { elements, params } => {
render_group(pixmap, band_state, elements, params, ctx);
}
DisplayElement::SoftMasked {
mask,
content,
params,
mask_cache,
} => {
render_soft_masked(pixmap, band_state, mask, content, params, mask_cache, ctx);
}
DisplayElement::Text { .. } => {} DisplayElement::OcgGroup {
elements,
default_visible,
..
} => {
let visible = *default_visible;
for (idx, elem) in elements.elements().iter().enumerate() {
if !visible
&& !matches!(elem, DisplayElement::Clip { .. } | DisplayElement::InitClip)
{
continue;
}
let elem_ctx = RenderContext {
elem_idx: idx,
..*ctx
};
render_element(pixmap, band_state, elem, &elem_ctx);
}
}
}
}
fn compute_group_crop(bbox: &[f64; 4], ctx: &RenderContext<'_>) -> Option<(i32, i32, u32, u32)> {
let px_min = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
let py_min = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
let px_max = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
let py_max = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
let x0 = px_min.max(0);
let y0 = py_min.max(0);
let x1 = px_max.min(ctx.out_w as i32);
let y1 = py_max.min(ctx.out_h as i32);
if x0 >= x1 || y0 >= y1 {
return None;
}
let crop_w = (x1 - x0) as u32;
let crop_h = (y1 - y0) as u32;
let crop_pixels = crop_w as u64 * crop_h as u64;
let full_pixels = ctx.out_w as u64 * ctx.out_h as u64;
if crop_pixels * 4 >= full_pixels * 3 {
return None;
}
Some((x0, y0, crop_w, crop_h))
}
fn blend_cmyk_separable_channel(cb: f64, cs: f64, mode: u8) -> f64 {
let cbi = 1.0 - cb;
let csi = 1.0 - cs;
let result_inv = match mode {
1 => cbi * csi, 2 => cbi + csi - cbi * csi, 3 => {
if cbi <= 0.5 {
2.0 * cbi * csi
} else {
1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
}
}
4 => cbi.min(csi), 5 => cbi.max(csi), 6 => {
if csi >= 1.0 {
1.0
} else {
(cbi / (1.0 - csi)).min(1.0)
}
}
7 => {
if csi <= 0.0 {
0.0
} else {
1.0 - ((1.0 - cbi) / csi).min(1.0)
}
}
8 => {
if csi <= 0.5 {
2.0 * cbi * csi
} else {
1.0 - 2.0 * (1.0 - cbi) * (1.0 - csi)
}
}
9 => {
let d = if cbi <= 0.25 {
((16.0 * cbi - 12.0) * cbi + 4.0) * cbi
} else {
cbi.sqrt()
};
if csi <= 0.5 {
cbi - (1.0 - 2.0 * csi) * cbi * (1.0 - cbi)
} else {
cbi + (2.0 * csi - 1.0) * (d - cbi)
}
}
10 => (cbi - csi).abs(), 11 => cbi + csi - 2.0 * cbi * csi, _ => csi, };
1.0 - result_inv.clamp(0.0, 1.0)
}
fn blend_cmyk_nonseparable(cb: [f64; 4], cs: [f64; 4], mode: u8) -> [f64; 4] {
fn lum(c: [f64; 3]) -> f64 {
0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
}
fn clip_color(mut c: [f64; 3]) -> [f64; 3] {
let l = lum(c);
let n = c[0].min(c[1]).min(c[2]);
let x = c[0].max(c[1]).max(c[2]);
if n < 0.0 {
for ci in c.iter_mut() {
*ci = l + (*ci - l) * l / (l - n);
}
}
if x > 1.0 {
for ci in c.iter_mut() {
*ci = l + (*ci - l) * (1.0 - l) / (x - l);
}
}
c
}
fn set_lum(c: [f64; 3], l: f64) -> [f64; 3] {
let d = l - lum(c);
clip_color([c[0] + d, c[1] + d, c[2] + d])
}
fn sat(c: [f64; 3]) -> f64 {
c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
}
fn set_sat(c: [f64; 3], s: f64) -> [f64; 3] {
let mut idx = [0usize, 1, 2];
idx.sort_by(|a, b| {
c[*a]
.partial_cmp(&c[*b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let (i_min, i_mid, i_max) = (idx[0], idx[1], idx[2]);
let mut out = c;
if c[i_max] > c[i_min] {
out[i_mid] = (c[i_mid] - c[i_min]) * s / (c[i_max] - c[i_min]);
out[i_max] = s;
} else {
out[i_mid] = 0.0;
out[i_max] = 0.0;
}
out[i_min] = 0.0;
out
}
let cb_rgb = [1.0 - cb[0], 1.0 - cb[1], 1.0 - cb[2]];
let cs_rgb = [1.0 - cs[0], 1.0 - cs[1], 1.0 - cs[2]];
let result_rgb = match mode {
12 => set_lum(set_sat(cs_rgb, sat(cb_rgb)), lum(cb_rgb)), 13 => set_lum(set_sat(cb_rgb, sat(cs_rgb)), lum(cb_rgb)), 14 => set_lum(cs_rgb, lum(cb_rgb)), 15 => set_lum(cb_rgb, lum(cs_rgb)), _ => cs_rgb,
};
let result_k = if mode == 15 { cs[3] } else { cb[3] };
[
(1.0 - result_rgb[0]).clamp(0.0, 1.0),
(1.0 - result_rgb[1]).clamp(0.0, 1.0),
(1.0 - result_rgb[2]).clamp(0.0, 1.0),
result_k,
]
}
fn ps_path_bbox(path: &PsPath) -> Option<(f64, f64, f64, f64)> {
let mut it = path.segments.iter().filter_map(|seg| match *seg {
PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => Some(vec![(x, y)]),
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => Some(vec![(x1, y1), (x2, y2), (x3, y3)]),
PathSegment::ClosePath => None,
});
let first = it.next()?.into_iter().next()?;
let (mut x0, mut y0) = first;
let (mut x1, mut y1) = first;
for seg_points in std::iter::once(vec![first]).chain(it) {
for (x, y) in seg_points {
x0 = x0.min(x);
y0 = y0.min(y);
x1 = x1.max(x);
y1 = y1.max(y);
}
}
Some((x0, y0, x1, y1))
}
fn bbox_contains(outer: (f64, f64, f64, f64), inner: (f64, f64, f64, f64), tolerance: f64) -> bool {
inner.0 >= outer.0 - tolerance
&& inner.1 >= outer.1 - tolerance
&& inner.2 <= outer.2 + tolerance
&& inner.3 <= outer.3 + tolerance
}
fn compute_obscured_fill_skips(elements: &DisplayList) -> Vec<usize> {
let mut skips = Vec::new();
let els = elements.elements();
for i in 0..els.len() {
let DisplayElement::Fill {
path: parent_path,
params: parent_params,
} = &els[i]
else {
continue;
};
if (parent_params.alpha - 1.0).abs() > 1e-6 || parent_params.blend_mode != 0 {
continue;
}
let Some(parent_bbox) = ps_path_bbox(parent_path) else {
continue;
};
let mut j = i + 1;
let mut clips_ok = true;
while j < els.len() {
match &els[j] {
DisplayElement::InitClip => {}
DisplayElement::Clip {
path: clip_path, ..
} => match ps_path_bbox(clip_path) {
Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
_ => {
clips_ok = false;
break;
}
},
_ => break,
}
j += 1;
}
if !clips_ok {
continue;
}
let Some(DisplayElement::Group {
elements: group_elements,
params: group_params,
}) = els.get(j)
else {
continue;
};
if !group_params.isolated
|| (group_params.alpha - 1.0).abs() > 1e-6
|| group_params.blend_mode != 0
{
continue;
}
let group_bbox = (
group_params.bbox[0],
group_params.bbox[1],
group_params.bbox[2],
group_params.bbox[3],
);
if !bbox_contains(group_bbox, parent_bbox, 0.5) {
continue;
}
let inner_els = group_elements.elements();
let mut k = 0;
let mut inner_clips_ok = true;
while k < inner_els.len() {
match &inner_els[k] {
DisplayElement::InitClip => {}
DisplayElement::Clip {
path: clip_path, ..
} => match ps_path_bbox(clip_path) {
Some(cb) if bbox_contains(cb, parent_bbox, 0.5) => {}
_ => {
inner_clips_ok = false;
break;
}
},
_ => break,
}
k += 1;
}
if !inner_clips_ok {
continue;
}
let Some(DisplayElement::Fill {
path: group_path,
params: group_fill_params,
}) = inner_els.get(k)
else {
continue;
};
if (group_fill_params.alpha - 1.0).abs() > 1e-6 || group_fill_params.blend_mode != 0 {
continue;
}
if paths_approximately_equal(parent_path, group_path, 0.5) {
skips.push(i);
}
}
skips
}
fn paths_approximately_equal(a: &PsPath, b: &PsPath, tolerance: f64) -> bool {
if a.segments.len() != b.segments.len() {
return false;
}
for (sa, sb) in a.segments.iter().zip(b.segments.iter()) {
let close_pair = |(x1, y1): (f64, f64), (x2, y2): (f64, f64)| -> bool {
(x1 - x2).abs() <= tolerance && (y1 - y2).abs() <= tolerance
};
match (sa, sb) {
(PathSegment::MoveTo(x1, y1), PathSegment::MoveTo(x2, y2)) => {
if !close_pair((*x1, *y1), (*x2, *y2)) {
return false;
}
}
(PathSegment::LineTo(x1, y1), PathSegment::LineTo(x2, y2)) => {
if !close_pair((*x1, *y1), (*x2, *y2)) {
return false;
}
}
(
PathSegment::CurveTo {
x1: ax1,
y1: ay1,
x2: ax2,
y2: ay2,
x3: ax3,
y3: ay3,
},
PathSegment::CurveTo {
x1: bx1,
y1: by1,
x2: bx2,
y2: by2,
x3: bx3,
y3: by3,
},
) => {
if !close_pair((*ax1, *ay1), (*bx1, *by1))
|| !close_pair((*ax2, *ay2), (*bx2, *by2))
|| !close_pair((*ax3, *ay3), (*bx3, *by3))
{
return false;
}
}
(PathSegment::ClosePath, PathSegment::ClosePath) => {}
_ => return false,
}
}
true
}
fn render_group(
pixmap: &mut Pixmap,
band_state: &mut BandState,
elements: &DisplayList,
params: &stet_graphics::display_list::GroupParams,
ctx: &RenderContext<'_>,
) {
if params.knockout {
render_knockout_group(pixmap, band_state, elements, params, ctx);
return;
}
let crop = compute_group_crop(¶ms.bbox, ctx);
let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
Some((cx, cy, cw, ch)) => (
cw,
ch,
cx,
cy,
ctx.vp_x + cx as f32 / ctx.scale_x,
ctx.vp_y + cy as f32 / ctx.scale_y,
),
None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
};
let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
return;
};
use stet_graphics::display_list::GroupColorSpace;
let needs_group_cmyk = has_overprint_elements(elements)
|| band_state.cmyk_buffer.is_some()
|| params.color_space == GroupColorSpace::DeviceCMYK
|| has_cmyk_group(elements);
let force_cmyk_compose =
std::env::var_os("STET_FORCE_CMYK_COMPOSITE_BACK").as_deref() == Some("1".as_ref());
let plan_cmyk_compose = match ctx.knockout_painter_pass {
KnockoutPainterPass::CoveragePass => false,
KnockoutPainterPass::ColorPass => {
!params.isolated
&& params.blend_mode != 0
&& needs_group_cmyk
&& band_state.cmyk_buffer.is_some()
&& group_content_is_native_cmyk(elements)
}
KnockoutPainterPass::None if force_cmyk_compose => {
!params.isolated
&& matches!(params.blend_mode, 10..=15)
&& needs_group_cmyk
&& band_state.cmyk_buffer.is_some()
&& group_content_is_native_cmyk(elements)
}
KnockoutPainterPass::None => {
let inversion_sensitive = !params.isolated
&& matches!(params.blend_mode, 10..=15)
&& group_only_native_cmyk_fills(elements);
let cmyk_group_blend = !params.isolated
&& ctx.parent_group_isolated
&& params.blend_mode != 0
&& params.color_space == GroupColorSpace::DeviceCMYK
&& needs_group_cmyk
&& band_state.cmyk_buffer.is_some()
&& group_content_is_native_cmyk(elements);
inversion_sensitive || cmyk_group_blend
}
};
let needs_alpha_extraction = !params.isolated
&& params.blend_mode != 0
&& !plan_cmyk_compose
&& !ctx.alpha_extraction_pass;
let needs_backdrop_preload =
!params.isolated && (params.blend_mode == 0 || plan_cmyk_compose || needs_alpha_extraction);
let backdrop = if needs_backdrop_preload {
let data = if crop.is_some() {
copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
} else {
pixmap.data().to_vec()
};
offscreen.data_mut().copy_from_slice(&data);
Some(data)
} else {
None
};
let group_cmyk = if needs_group_cmyk {
let buf_size = eff_w as usize * eff_h as usize * 4;
let mut buf = vec![0.0f32; buf_size];
if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
let parent_stride = ctx.out_w as usize * 4;
let group_stride = eff_w as usize * 4;
for gy in 0..eff_h as usize {
let py = crop_y as usize + gy;
if py < ctx.out_h as usize {
let p_start = py * parent_stride + crop_x as usize * 4;
let g_start = gy * group_stride;
let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
buf[g_start..g_start + copy_len]
.copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
}
}
}
Some(buf)
} else {
None
};
let backdrop_cmyk: Option<Vec<f32>> = if !params.isolated {
group_cmyk.clone()
} else {
None
};
let mut group_band = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
mask_pool: Vec::new(),
cmyk_buffer: group_cmyk,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
let group_ctx = RenderContext {
vp_x: eff_vp_x,
vp_y: eff_vp_y,
scale_x: ctx.scale_x,
scale_y: ctx.scale_y,
out_w: eff_w,
out_h: eff_h,
effective_dpi: ctx.effective_dpi,
icc: ctx.icc,
image_cache: None, preprocessed: None,
elem_idx: 0,
no_aa: ctx.no_aa,
opm_zero_transparent: ctx.opm_zero_transparent,
knockout_painter_pass: ctx.knockout_painter_pass,
parent_group_isolated: params.isolated,
alpha_extraction_pass: ctx.alpha_extraction_pass,
};
let skip_indices = compute_obscured_fill_skips(elements);
for (idx, elem) in elements.elements().iter().enumerate() {
if skip_indices.contains(&idx) {
continue;
}
let elem_ctx = RenderContext {
elem_idx: idx,
..group_ctx
};
render_element(&mut offscreen, &mut group_band, elem, &elem_ctx);
}
let alpha_offscreen = if needs_alpha_extraction {
let mut iso = Pixmap::new(eff_w, eff_h);
if let Some(ref mut iso_pm) = iso {
let mut iso_band = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
mask_pool: Vec::new(),
cmyk_buffer: None,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
let iso_ctx = RenderContext {
parent_group_isolated: true,
alpha_extraction_pass: true,
..group_ctx
};
for (idx, elem) in elements.elements().iter().enumerate() {
let elem_ctx = RenderContext {
elem_idx: idx,
..iso_ctx
};
render_element(iso_pm, &mut iso_band, elem, &elem_ctx);
}
}
iso
} else {
None
};
let mut temp_mask = None;
let mask_ref = match resolve_clip_mask(
&band_state.clip_region,
&mut temp_mask,
ctx.out_w,
ctx.out_h,
) {
None => return, Some(m) => m,
};
let coverage_params;
let effective_params: &stet_graphics::display_list::GroupParams =
if ctx.knockout_painter_pass == KnockoutPainterPass::CoveragePass {
coverage_params = stet_graphics::display_list::GroupParams {
alpha: 1.0,
blend_mode: 0,
..params.clone()
};
&coverage_params
} else {
params
};
let mut cmyk_compose_done = false;
if let Some(backdrop) = &backdrop {
let inner_cmyk = group_band.cmyk_buffer.as_deref();
let pre_cmyk = backdrop_cmyk.as_deref();
if plan_cmyk_compose && let (Some(inner), Some(pre)) = (inner_cmyk, pre_cmyk) {
composite_non_isolated_cmyk(
pixmap,
band_state.cmyk_buffer.as_deref_mut(),
&offscreen,
inner,
pre,
backdrop,
effective_params,
mask_ref,
crop_x,
crop_y,
ctx.icc,
);
cmyk_compose_done = true;
} else if let Some(ref alpha_os) = alpha_offscreen {
composite_non_isolated_extracted(
pixmap,
&offscreen,
alpha_os,
backdrop,
effective_params,
mask_ref,
crop_x,
crop_y,
);
} else {
composite_non_isolated_group_cropped(
pixmap,
&offscreen,
backdrop,
effective_params,
mask_ref,
crop_x,
crop_y,
);
}
} else {
let paint = stet_tiny_skia::PixmapPaint {
opacity: effective_params.alpha as f32,
blend_mode: u8_to_blend_mode(effective_params.blend_mode),
quality: stet_tiny_skia::FilterQuality::Nearest,
};
pixmap.draw_pixmap(
crop_x,
crop_y,
offscreen.as_ref(),
&paint,
Transform::identity(),
mask_ref,
);
}
if !cmyk_compose_done
&& let (Some(group_cmyk), Some(parent_cmyk)) =
(&group_band.cmyk_buffer, &mut band_state.cmyk_buffer)
{
copy_cmyk_buffer_to_parent(
parent_cmyk,
group_cmyk,
offscreen.data(),
crop_x as usize,
crop_y as usize,
eff_w as usize,
eff_h as usize,
ctx.out_w as usize,
ctx.out_h as usize,
);
}
}
#[allow(clippy::too_many_arguments)]
fn composite_non_isolated_cmyk(
target: &mut Pixmap,
parent_cmyk: Option<&mut [f32]>,
source: &Pixmap,
source_cmyk: &[f32],
backdrop_cmyk: &[f32],
backdrop_pixels: &[u8],
params: &stet_graphics::display_list::GroupParams,
clip_mask: Option<&stet_tiny_skia::Mask>,
crop_x: i32,
crop_y: i32,
icc: Option<&IccCache>,
) {
let cw = source.width() as usize;
let ch = source.height() as usize;
let target_w = target.width() as usize;
let target_h = target.height() as usize;
let opacity = params.alpha.clamp(0.0, 1.0);
let blend_mode = params.blend_mode;
let is_nonseparable = matches!(blend_mode, 12..=15);
let target_data = target.data_mut();
let target_stride = target_w * 4;
let group_stride = cw * 4;
let clip_data = clip_mask.map(|m| m.data());
for gy in 0..ch {
let ty = crop_y + gy as i32;
if ty < 0 || ty as usize >= target_h {
continue;
}
let ty = ty as usize;
let group_row = gy * group_stride;
let target_row = ty * target_stride;
for gx in 0..cw {
let tx = crop_x + gx as i32;
if tx < 0 || tx as usize >= target_w {
continue;
}
let tx = tx as usize;
let gi = group_row + gx * 4;
let ti = target_row + tx * 4;
let bc = backdrop_cmyk[gi] as f64;
let bm = backdrop_cmyk[gi + 1] as f64;
let by_ = backdrop_cmyk[gi + 2] as f64;
let bk = backdrop_cmyk[gi + 3] as f64;
let sc = source_cmyk[gi] as f64;
let sm = source_cmyk[gi + 1] as f64;
let sy_ = source_cmyk[gi + 2] as f64;
let sk = source_cmyk[gi + 3] as f64;
if (sc - bc).abs() < 1.0 / 255.0
&& (sm - bm).abs() < 1.0 / 255.0
&& (sy_ - by_).abs() < 1.0 / 255.0
&& (sk - bk).abs() < 1.0 / 255.0
{
continue;
}
let cov = if let Some(cd) = clip_data {
cd[ty * target_w + tx] as f64 / 255.0
} else {
1.0
};
if cov <= 0.0 {
continue;
}
let backdrop_alpha = backdrop_pixels[gi + 3];
let backdrop_transparent = backdrop_alpha == 0;
let mix = cov * opacity;
let dst_a = target_data[ti + 3] as f64 / 255.0;
if backdrop_transparent {
let src_data = source.data();
let src_a_pm = src_data[gi + 3] as f64 / 255.0;
if src_a_pm <= 0.0 {
continue;
}
let (full_r, full_g, full_b) = icc
.and_then(|i| i.convert_cmyk_readonly(sc, sm, sy_, sk))
.unwrap_or_else(|| cmyk_to_rgb_plrm(sc, sm, sy_, sk));
let alpha_s = mix;
let inv_sa = 1.0 - alpha_s;
let dst_r_pm = target_data[ti] as f64 / 255.0;
let dst_g_pm = target_data[ti + 1] as f64 / 255.0;
let dst_b_pm = target_data[ti + 2] as f64 / 255.0;
let out_r = full_r * alpha_s + dst_r_pm * inv_sa;
let out_g = full_g * alpha_s + dst_g_pm * inv_sa;
let out_b = full_b * alpha_s + dst_b_pm * inv_sa;
let out_a = alpha_s + dst_a * inv_sa;
target_data[ti] = (out_r * 255.0).round().clamp(0.0, 255.0) as u8;
target_data[ti + 1] = (out_g * 255.0).round().clamp(0.0, 255.0) as u8;
target_data[ti + 2] = (out_b * 255.0).round().clamp(0.0, 255.0) as u8;
target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
continue;
}
let (rc, rm, ry, rk) = if is_nonseparable {
let r = blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
(r[0], r[1], r[2], r[3])
} else {
(
blend_cmyk_separable_channel(bc, sc, blend_mode),
blend_cmyk_separable_channel(bm, sm, blend_mode),
blend_cmyk_separable_channel(by_, sy_, blend_mode),
blend_cmyk_separable_channel(bk, sk, blend_mode),
)
};
let (new_r, new_g, new_b) = icc
.and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
.unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
let alpha_s = mix;
let alpha_b = dst_a;
let out_a = alpha_s + alpha_b * (1.0 - alpha_s);
if out_a <= 0.0 {
continue;
}
let (dst_r, dst_g, dst_b) = if alpha_b > 0.0 {
let inv_a = 1.0 / alpha_b;
(
(target_data[ti] as f64 / 255.0) * inv_a,
(target_data[ti + 1] as f64 / 255.0) * inv_a,
(target_data[ti + 2] as f64 / 255.0) * inv_a,
)
} else {
(0.0, 0.0, 0.0)
};
let coef_b = alpha_s * alpha_b;
let coef_s = alpha_s * (1.0 - alpha_b);
let coef_d = (1.0 - alpha_s) * alpha_b;
let out_r = (coef_s * new_r + coef_b * new_r + coef_d * dst_r) / out_a;
let out_g = (coef_s * new_g + coef_b * new_g + coef_d * dst_g) / out_a;
let out_b = (coef_s * new_b + coef_b * new_b + coef_d * dst_b) / out_a;
target_data[ti] = (out_r * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
target_data[ti + 1] = (out_g * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
target_data[ti + 2] = (out_b * out_a * 255.0).round().clamp(0.0, 255.0) as u8;
target_data[ti + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
}
}
if let Some(parent_cmyk) = parent_cmyk {
for gy in 0..ch {
let ty = crop_y + gy as i32;
if ty < 0 || ty as usize >= target_h {
continue;
}
let ty = ty as usize;
let group_row = gy * group_stride;
let parent_row = ty * target_stride;
for gx in 0..cw {
let tx = crop_x + gx as i32;
if tx < 0 || tx as usize >= target_w {
continue;
}
let tx = tx as usize;
let gi = group_row + gx * 4;
let pi = parent_row + tx * 4;
let bc = backdrop_cmyk[gi] as f64;
let bm = backdrop_cmyk[gi + 1] as f64;
let by_ = backdrop_cmyk[gi + 2] as f64;
let bk = backdrop_cmyk[gi + 3] as f64;
let sc = source_cmyk[gi] as f64;
let sm = source_cmyk[gi + 1] as f64;
let sy_ = source_cmyk[gi + 2] as f64;
let sk = source_cmyk[gi + 3] as f64;
if (sc - bc).abs() < 1.0 / 255.0
&& (sm - bm).abs() < 1.0 / 255.0
&& (sy_ - by_).abs() < 1.0 / 255.0
&& (sk - bk).abs() < 1.0 / 255.0
{
continue;
}
let backdrop_transparent = backdrop_pixels[gi + 3] == 0;
let (rc, rm, ry, rk) = if backdrop_transparent {
(sc, sm, sy_, sk)
} else if is_nonseparable {
let r =
blend_cmyk_nonseparable([bc, bm, by_, bk], [sc, sm, sy_, sk], blend_mode);
(r[0], r[1], r[2], r[3])
} else {
(
blend_cmyk_separable_channel(bc, sc, blend_mode),
blend_cmyk_separable_channel(bm, sm, blend_mode),
blend_cmyk_separable_channel(by_, sy_, blend_mode),
blend_cmyk_separable_channel(bk, sk, blend_mode),
)
};
parent_cmyk[pi] = rc as f32;
parent_cmyk[pi + 1] = rm as f32;
parent_cmyk[pi + 2] = ry as f32;
parent_cmyk[pi + 3] = rk as f32;
}
}
}
}
fn render_knockout_group(
pixmap: &mut Pixmap,
band_state: &mut BandState,
elements: &DisplayList,
params: &stet_graphics::display_list::GroupParams,
ctx: &RenderContext<'_>,
) {
let crop = compute_group_crop(¶ms.bbox, ctx);
let (eff_w, eff_h, crop_x, crop_y, eff_vp_x, eff_vp_y) = match crop {
Some((cx, cy, cw, ch)) => (
cw,
ch,
cx,
cy,
ctx.vp_x + cx as f32 / ctx.scale_x,
ctx.vp_y + cy as f32 / ctx.scale_y,
),
None => (ctx.out_w, ctx.out_h, 0, 0, ctx.vp_x, ctx.vp_y),
};
let Some(mut offscreen) = Pixmap::new(eff_w, eff_h) else {
return;
};
let initial_backdrop = if !params.isolated {
if crop.is_some() {
copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h)
} else {
pixmap.data().to_vec()
}
} else {
vec![0u8; (eff_w * eff_h * 4) as usize]
};
let Some(mut accumulated) = Pixmap::new(eff_w, eff_h) else {
return;
};
accumulated.data_mut().copy_from_slice(&initial_backdrop);
let needs_cmyk = has_overprint_elements(elements) || band_state.cmyk_buffer.is_some();
let initial_cmyk = if needs_cmyk {
let buf_size = eff_w as usize * eff_h as usize * 4;
let mut buf = vec![0.0f32; buf_size];
if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
let parent_stride = ctx.out_w as usize * 4;
let group_stride = eff_w as usize * 4;
for gy in 0..eff_h as usize {
let py = crop_y as usize + gy;
if py < ctx.out_h as usize {
let p_start = py * parent_stride + crop_x as usize * 4;
let g_start = gy * group_stride;
let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
buf[g_start..g_start + copy_len]
.copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
}
}
}
Some(buf)
} else {
None
};
let mut accumulated_cmyk = initial_cmyk.clone();
let group_ctx = RenderContext {
vp_x: eff_vp_x,
vp_y: eff_vp_y,
scale_x: ctx.scale_x,
scale_y: ctx.scale_y,
out_w: eff_w,
out_h: eff_h,
effective_dpi: ctx.effective_dpi,
icc: ctx.icc,
image_cache: None,
preprocessed: None,
elem_idx: 0,
no_aa: true,
opm_zero_transparent: ctx.opm_zero_transparent,
knockout_painter_pass: ctx.knockout_painter_pass,
parent_group_isolated: true,
alpha_extraction_pass: false,
};
let mut ko_band = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
mask_pool: Vec::new(),
cmyk_buffer: None,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
let mut coverage_offscreen: Option<Pixmap> = None;
for elem in elements.elements() {
match elem {
DisplayElement::Clip { .. } | DisplayElement::InitClip => {
render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
}
DisplayElement::Group { .. } => {
let pass1_ctx = RenderContext {
knockout_painter_pass: KnockoutPainterPass::ColorPass,
..group_ctx
};
offscreen.data_mut().copy_from_slice(&initial_backdrop);
ko_band.cmyk_buffer = initial_cmyk.clone();
render_element(&mut offscreen, &mut ko_band, elem, &pass1_ctx);
let pass1_cmyk = ko_band.cmyk_buffer.take();
let cov = match coverage_offscreen.as_mut() {
Some(p) => {
p.data_mut().fill(0);
p
}
None => {
let Some(p) = Pixmap::new(eff_w, eff_h) else {
replace_changed_pixels(
accumulated.data_mut(),
offscreen.data(),
&initial_backdrop,
);
if let (Some(p1), Some(acc)) = (&pass1_cmyk, &mut accumulated_cmyk) {
replace_changed_cmyk(acc, p1, offscreen.data(), &initial_backdrop);
}
continue;
};
coverage_offscreen = Some(p);
coverage_offscreen.as_mut().unwrap()
}
};
ko_band.cmyk_buffer = None;
let coverage_ctx = RenderContext {
knockout_painter_pass: KnockoutPainterPass::CoveragePass,
..group_ctx
};
render_element(cov, &mut ko_band, elem, &coverage_ctx);
replace_with_coverage_mask(accumulated.data_mut(), offscreen.data(), cov.data());
if let (Some(p1_cmyk), Some(acc_cmyk)) = (&pass1_cmyk, &mut accumulated_cmyk) {
replace_cmyk_with_coverage_mask(acc_cmyk, p1_cmyk, cov.data());
}
ko_band.cmyk_buffer = None;
}
_ => {
offscreen.data_mut().copy_from_slice(&initial_backdrop);
ko_band.cmyk_buffer = initial_cmyk.clone();
render_element(&mut offscreen, &mut ko_band, elem, &group_ctx);
if let (Some(elem_cmyk), Some(acc_cmyk)) =
(&ko_band.cmyk_buffer, &mut accumulated_cmyk)
{
replace_changed_cmyk(acc_cmyk, elem_cmyk, offscreen.data(), &initial_backdrop);
}
ko_band.cmyk_buffer = None;
replace_changed_pixels(accumulated.data_mut(), offscreen.data(), &initial_backdrop);
}
}
}
let mut temp_mask = None;
let mask_ref = resolve_clip_mask(
&band_state.clip_region,
&mut temp_mask,
ctx.out_w,
ctx.out_h,
);
let mask_ref = match mask_ref {
None => return,
Some(m) => m,
};
composite_non_isolated_group_cropped(
pixmap,
&accumulated,
&initial_backdrop,
params,
mask_ref,
crop_x,
crop_y,
);
if let (Some(acc_cmyk), Some(parent_cmyk)) = (&accumulated_cmyk, &mut band_state.cmyk_buffer) {
copy_cmyk_buffer_to_parent(
parent_cmyk,
acc_cmyk,
accumulated.data(),
crop_x as usize,
crop_y as usize,
eff_w as usize,
eff_h as usize,
ctx.out_w as usize,
ctx.out_h as usize,
);
}
}
fn replace_with_coverage_mask(target: &mut [u8], source: &[u8], coverage: &[u8]) {
for i in (0..target.len()).step_by(4) {
let cov_a = coverage[i + 3];
if cov_a == 0 {
continue;
}
if cov_a == 255 {
target[i..i + 4].copy_from_slice(&source[i..i + 4]);
continue;
}
let a = cov_a as u32;
let inv = 255 - a;
for c in 0..4 {
let s = source[i + c] as u32;
let t = target[i + c] as u32;
target[i + c] = ((s * a + t * inv + 127) / 255) as u8;
}
}
}
fn replace_cmyk_with_coverage_mask(target: &mut [f32], source: &[f32], coverage: &[u8]) {
let pixel_count = target.len() / 4;
for i in 0..pixel_count {
let pi = i * 4;
let cov_a = coverage[pi + 3];
if cov_a == 0 {
continue;
}
if cov_a == 255 {
target[pi..pi + 4].copy_from_slice(&source[pi..pi + 4]);
continue;
}
let a = cov_a as f32 / 255.0;
let inv = 1.0 - a;
for c in 0..4 {
target[pi + c] = source[pi + c] * a + target[pi + c] * inv;
}
}
}
fn replace_changed_pixels(target: &mut [u8], source: &[u8], backdrop: &[u8]) {
for i in (0..target.len()).step_by(4) {
if source[i] != backdrop[i]
|| source[i + 1] != backdrop[i + 1]
|| source[i + 2] != backdrop[i + 2]
|| source[i + 3] != backdrop[i + 3]
{
target[i..i + 4].copy_from_slice(&source[i..i + 4]);
}
}
}
#[allow(clippy::too_many_arguments)]
fn copy_cmyk_buffer_to_parent(
parent_cmyk: &mut [f32],
group_cmyk: &[f32],
group_pixels: &[u8],
crop_x: usize,
crop_y: usize,
group_w: usize,
group_h: usize,
parent_w: usize,
parent_h: usize,
) {
let parent_stride = parent_w * 4;
let group_stride = group_w * 4;
for gy in 0..group_h {
let py = crop_y + gy;
if py >= parent_h {
break;
}
for gx in 0..group_w {
let px = crop_x + gx;
if px >= parent_w {
break;
}
let g_pixel_idx = (gy * group_w + gx) * 4;
let g_cmyk_idx = gy * group_stride + gx * 4;
if group_pixels[g_pixel_idx + 3] > 0
&& (group_cmyk[g_cmyk_idx] != 0.0
|| group_cmyk[g_cmyk_idx + 1] != 0.0
|| group_cmyk[g_cmyk_idx + 2] != 0.0
|| group_cmyk[g_cmyk_idx + 3] != 0.0)
{
let p_cmyk_idx = py * parent_stride + px * 4;
parent_cmyk[p_cmyk_idx..p_cmyk_idx + 4]
.copy_from_slice(&group_cmyk[g_cmyk_idx..g_cmyk_idx + 4]);
}
}
}
}
fn replace_changed_cmyk(
target_cmyk: &mut [f32],
source_cmyk: &[f32],
source_pixels: &[u8],
backdrop_pixels: &[u8],
) {
let pixel_count = target_cmyk.len() / 4;
for i in 0..pixel_count {
let pi = i * 4;
if source_pixels[pi] != backdrop_pixels[pi]
|| source_pixels[pi + 1] != backdrop_pixels[pi + 1]
|| source_pixels[pi + 2] != backdrop_pixels[pi + 2]
|| source_pixels[pi + 3] != backdrop_pixels[pi + 3]
{
target_cmyk[pi..pi + 4].copy_from_slice(&source_cmyk[pi..pi + 4]);
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_soft_masked(
pixmap: &mut Pixmap,
band_state: &mut BandState,
mask_list: &DisplayList,
content_list: &DisplayList,
params: &stet_graphics::display_list::SoftMaskParams,
mask_cache: &Arc<Mutex<Option<Option<stet_graphics::display_list::MaskRaster>>>>,
ctx: &RenderContext<'_>,
) {
let use_inline_mask = ctx.vp_x != 0.0;
let bbox = ¶ms.bbox;
let smask_px_x0 = ((bbox[0] as f32 - ctx.vp_x) * ctx.scale_x).floor() as i32;
let smask_px_y0 = ((bbox[1] as f32 - ctx.vp_y) * ctx.scale_y).floor() as i32;
let smask_px_x1 = ((bbox[2] as f32 - ctx.vp_x) * ctx.scale_x).ceil() as i32;
let smask_px_y1 = ((bbox[3] as f32 - ctx.vp_y) * ctx.scale_y).ceil() as i32;
let crop_x = smask_px_x0.max(0);
let crop_y = smask_px_y0.max(0);
let crop_x1 = smask_px_x1.min(ctx.out_w as i32);
let crop_y1 = smask_px_y1.min(ctx.out_h as i32);
if crop_x >= crop_x1 || crop_y >= crop_y1 {
return;
}
let eff_w = (crop_x1 - crop_x) as u32;
let eff_h = (crop_y1 - crop_y) as u32;
let eff_vp_x = ctx.vp_x + crop_x as f32 / ctx.scale_x;
let eff_vp_y = ctx.vp_y + crop_y as f32 / ctx.scale_y;
let sub_ctx = RenderContext {
vp_x: eff_vp_x,
vp_y: eff_vp_y,
scale_x: ctx.scale_x,
scale_y: ctx.scale_y,
out_w: eff_w,
out_h: eff_h,
effective_dpi: ctx.effective_dpi,
icc: ctx.icc,
image_cache: None,
preprocessed: None,
elem_idx: 0,
no_aa: ctx.no_aa,
opm_zero_transparent: ctx.opm_zero_transparent,
knockout_painter_pass: ctx.knockout_painter_pass,
parent_group_isolated: ctx.parent_group_isolated,
alpha_extraction_pass: false,
};
let mut mask_values_inline: Vec<u8> = Vec::new();
if use_inline_mask {
let Some(mut mask_pixmap) = Pixmap::new(eff_w, eff_h) else {
return;
};
let mut mask_band = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
mask_pool: Vec::new(),
cmyk_buffer: None,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
for (idx, elem) in mask_list.elements().iter().enumerate() {
let elem_ctx = RenderContext {
elem_idx: idx,
..sub_ctx
};
render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
}
if params.has_nested_mask_scope
&& params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
{
let bc = params.backdrop_color.as_ref();
let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
let a = chunk[3] as u16;
if a == 255 {
continue;
}
let inv_a = 255 - a;
chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
chunk[3] = 255;
}
}
mask_values_inline = vec![0u8; (eff_w * eff_h) as usize];
extract_soft_mask_values(mask_pixmap.data(), &mut mask_values_inline, params);
}
let raster_owned: Option<stet_graphics::display_list::MaskRaster> = if use_inline_mask {
None
} else {
let mut guard = mask_cache.lock().unwrap();
let needs_build = match guard.as_ref() {
None => true,
Some(None) => false, Some(Some(r)) => {
(r.scale_x - ctx.scale_x).abs() > 1e-4 || (r.scale_y - ctx.scale_y).abs() > 1e-4
}
};
if needs_build {
let built = rasterize_mask(
mask_list,
params,
ctx.icc,
ctx.no_aa,
ctx.effective_dpi,
ctx.scale_x,
ctx.scale_y,
);
*guard = Some(built);
}
guard.as_ref().and_then(|inner| inner.clone())
};
let fallback_mask = out_of_bounds_mask_value(params) as i32;
let Some(mut content_pixmap) = Pixmap::new(eff_w, eff_h) else {
return;
};
let backdrop = copy_backdrop_crop(pixmap, crop_x, crop_y, eff_w, eff_h);
content_pixmap.data_mut().copy_from_slice(&backdrop);
let content_cmyk = if has_overprint_elements(content_list) || band_state.cmyk_buffer.is_some() {
let buf_size = eff_w as usize * eff_h as usize * 4;
let mut buf = vec![0.0f32; buf_size];
if let Some(ref parent_cmyk) = band_state.cmyk_buffer {
let parent_stride = ctx.out_w as usize * 4;
let group_stride = eff_w as usize * 4;
for gy in 0..eff_h as usize {
let py = crop_y as usize + gy;
if py < ctx.out_h as usize {
let p_start = py * parent_stride + crop_x as usize * 4;
let g_start = gy * group_stride;
let copy_len = group_stride.min(parent_stride - crop_x as usize * 4);
buf[g_start..g_start + copy_len]
.copy_from_slice(&parent_cmyk[p_start..p_start + copy_len]);
}
}
}
Some(buf)
} else {
None
};
let backdrop_cmyk: Option<Vec<f32>> = content_cmyk.clone();
let mut content_band = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
mask_pool: Vec::new(),
cmyk_buffer: content_cmyk,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
for (idx, elem) in content_list.elements().iter().enumerate() {
let elem_ctx = RenderContext {
elem_idx: idx,
..sub_ctx
};
render_element(&mut content_pixmap, &mut content_band, elem, &elem_ctx);
}
let vp_x_pixels = (ctx.vp_x * ctx.scale_x).round() as i32;
let vp_y_pixels = (ctx.vp_y * ctx.scale_y).round() as i32;
let mut temp_mask = None;
let clip_ref = resolve_clip_mask(
&band_state.clip_region,
&mut temp_mask,
ctx.out_w,
ctx.out_h,
);
let clip_ref = match clip_ref {
None => return,
Some(m) => m,
};
let use_cmyk_blend = ctx.icc.is_some()
&& backdrop_cmyk.is_some()
&& content_band.cmyk_buffer.is_some()
&& content_list_is_simple_native_cmyk(content_list);
let content_data = content_pixmap.data();
let parent_data = pixmap.data_mut();
let parent_stride = ctx.out_w as usize * 4;
let content_stride = eff_w as usize * 4;
for y in 0..eff_h as usize {
let py = crop_y as usize + y;
if py >= ctx.out_h as usize {
break;
}
let ci_row = y * content_stride;
let pi_row = py * parent_stride;
let page_y = vp_y_pixels + crop_y + y as i32;
for x in 0..eff_w as usize {
let px = crop_x as usize + x;
if px >= ctx.out_w as usize {
break;
}
if let Some(clip) = clip_ref {
if clip.data()[py * ctx.out_w as usize + px] == 0 {
continue;
}
}
let m = if use_inline_mask {
mask_values_inline[y * eff_w as usize + x] as i32
} else if let Some(ref raster) = raster_owned {
let page_x = vp_x_pixels + crop_x + x as i32;
let mx = page_x - raster.origin_x;
let my = page_y - raster.origin_y;
if mx >= 0 && (mx as u32) < raster.width && my >= 0 && (my as u32) < raster.height {
raster.data[my as usize * raster.width as usize + mx as usize] as i32
} else {
fallback_mask
}
} else {
fallback_mask
};
if m == 0 {
continue;
}
let ci = ci_row + x * 4;
let pi = pi_row + px * 4;
let ci_cmyk = (y * eff_w as usize + x) * 4;
let cmyk_path_ok = use_cmyk_blend && {
let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
let icc_match = |cmyk: &[f32], rgb: &[u8]| -> bool {
let (r, g, b) = ctx
.icc
.and_then(|i| {
i.convert_cmyk_readonly(
cmyk[0] as f64,
cmyk[1] as f64,
cmyk[2] as f64,
cmyk[3] as f64,
)
})
.unwrap_or_else(|| {
cmyk_to_rgb_plrm(
cmyk[0] as f64,
cmyk[1] as f64,
cmyk[2] as f64,
cmyk[3] as f64,
)
});
let r = (r * 255.0).round() as i32;
let g = (g * 255.0).round() as i32;
let b = (b * 255.0).round() as i32;
(r - rgb[0] as i32).abs() <= 3
&& (g - rgb[1] as i32).abs() <= 3
&& (b - rgb[2] as i32).abs() <= 3
};
icc_match(bc_cmyk, &backdrop[ci..ci + 3])
&& icc_match(cc_cmyk, &content_data[ci..ci + 3])
};
if cmyk_path_ok {
let bc_cmyk = &backdrop_cmyk.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
let cc_cmyk = &content_band.cmyk_buffer.as_ref().unwrap()[ci_cmyk..ci_cmyk + 4];
let mf = m as f64 / 255.0;
let rc = bc_cmyk[0] as f64 + mf * (cc_cmyk[0] as f64 - bc_cmyk[0] as f64);
let rm = bc_cmyk[1] as f64 + mf * (cc_cmyk[1] as f64 - bc_cmyk[1] as f64);
let ry = bc_cmyk[2] as f64 + mf * (cc_cmyk[2] as f64 - bc_cmyk[2] as f64);
let rk = bc_cmyk[3] as f64 + mf * (cc_cmyk[3] as f64 - bc_cmyk[3] as f64);
let (fr, fg, fb) = ctx
.icc
.and_then(|i| i.convert_cmyk_readonly(rc, rm, ry, rk))
.unwrap_or_else(|| cmyk_to_rgb_plrm(rc, rm, ry, rk));
parent_data[pi] = (fr * 255.0).round().clamp(0.0, 255.0) as u8;
parent_data[pi + 1] = (fg * 255.0).round().clamp(0.0, 255.0) as u8;
parent_data[pi + 2] = (fb * 255.0).round().clamp(0.0, 255.0) as u8;
let content_a = content_data[ci + 3] as i32;
let backdrop_a = backdrop[ci + 3] as i32;
let delta = content_a - backdrop_a;
if delta != 0 {
let masked_delta = if delta > 0 {
(delta * m + 128) / 255
} else {
(delta * m - 128) / 255
};
let result = (parent_data[pi + 3] as i32 + masked_delta).clamp(0, 255);
parent_data[pi + 3] = result as u8;
}
} else {
for c in 0..4 {
let content_val = content_data[ci + c] as i32;
let backdrop_val = backdrop[ci + c] as i32;
let delta = content_val - backdrop_val;
if delta != 0 {
let masked_delta = if delta > 0 {
(delta * m + 128) / 255
} else {
(delta * m - 128) / 255
};
let result = (parent_data[pi + c] as i32 + masked_delta).clamp(0, 255);
parent_data[pi + c] = result as u8;
}
}
}
}
}
if !use_cmyk_blend {
if let (Some(content_cmyk), Some(parent_cmyk)) =
(&content_band.cmyk_buffer, &mut band_state.cmyk_buffer)
{
copy_cmyk_buffer_to_parent(
parent_cmyk,
content_cmyk,
content_pixmap.data(),
crop_x as usize,
crop_y as usize,
eff_w as usize,
eff_h as usize,
ctx.out_w as usize,
ctx.out_h as usize,
);
}
}
}
fn extract_soft_mask_values(
rgba: &[u8],
out: &mut [u8],
params: &stet_graphics::display_list::SoftMaskParams,
) {
use stet_graphics::display_list::SoftMaskSubtype;
let pixel_count = out.len();
match params.subtype {
SoftMaskSubtype::Alpha => {
for i in 0..pixel_count {
let a = rgba[i * 4 + 3]; out[i] = if params.transfer_invert { 255 - a } else { a };
}
}
SoftMaskSubtype::Luminosity => {
let backdrop_lum = if let Some(bc) = ¶ms.backdrop_color {
(0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
} else {
0.0 };
let backdrop_byte = (backdrop_lum * 255.0 + 0.5) as u8;
#[allow(clippy::needless_range_loop)]
for i in 0..pixel_count {
let off = i * 4;
let a = rgba[off + 3];
let lum_byte = if a == 0 {
backdrop_byte
} else if a < 255 {
let af = a as f64;
let bd = backdrop_lum * 255.0;
let r = rgba[off] as f64 + bd * (255.0 - af) / 255.0;
let g = rgba[off + 1] as f64 + bd * (255.0 - af) / 255.0;
let b = rgba[off + 2] as f64 + bd * (255.0 - af) / 255.0;
let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
(lum + 0.5).clamp(0.0, 255.0) as u8
} else {
let lum = 0.2126 * rgba[off] as f64
+ 0.7152 * rgba[off + 1] as f64
+ 0.0722 * rgba[off + 2] as f64;
(lum + 0.5).clamp(0.0, 255.0) as u8
};
out[i] = if params.transfer_invert {
255 - lum_byte
} else {
lum_byte
};
}
}
}
}
fn out_of_bounds_mask_value(params: &stet_graphics::display_list::SoftMaskParams) -> u8 {
use stet_graphics::display_list::SoftMaskSubtype;
let raw = match params.subtype {
SoftMaskSubtype::Alpha => 0u8,
SoftMaskSubtype::Luminosity => {
let lum = if let Some(bc) = ¶ms.backdrop_color {
(0.2126 * bc[0] + 0.7152 * bc[1] + 0.0722 * bc[2]).clamp(0.0, 1.0)
} else {
0.0
};
(lum * 255.0 + 0.5) as u8
}
};
if params.transfer_invert {
255 - raw
} else {
raw
}
}
const MAX_MASK_RASTER_PIXELS: u64 = 64 * 1024 * 1024;
fn rasterize_mask(
mask_list: &DisplayList,
params: &stet_graphics::display_list::SoftMaskParams,
icc: Option<&IccCache>,
no_aa: bool,
effective_dpi: f64,
scale_x: f32,
scale_y: f32,
) -> Option<stet_graphics::display_list::MaskRaster> {
let mut bounds = compute_paint_bounds(mask_list, effective_dpi)?;
if let Some(cap) = params.parent_clip_bbox {
let cap_bbox = BBox2D {
x_min: cap[0],
y_min: cap[1],
x_max: cap[2],
y_max: cap[3],
};
bounds = intersect_bbox(&bounds, &cap_bbox)?;
}
let px_x_min = (bounds.x_min as f32 * scale_x).floor() as i32 - 1;
let px_y_min = (bounds.y_min as f32 * scale_y).floor() as i32 - 1;
let px_x_max = (bounds.x_max as f32 * scale_x).ceil() as i32 + 1;
let px_y_max = (bounds.y_max as f32 * scale_y).ceil() as i32 + 1;
if px_x_min >= px_x_max || px_y_min >= px_y_max {
return None;
}
let raster_w = (px_x_max - px_x_min) as u32;
let raster_h = (px_y_max - px_y_min) as u32;
if raster_w == 0 || raster_h == 0 {
return None;
}
if (raster_w as u64) * (raster_h as u64) > MAX_MASK_RASTER_PIXELS {
return None;
}
let mut mask_pixmap = Pixmap::new(raster_w, raster_h)?;
let sub_ctx = RenderContext {
vp_x: px_x_min as f32 / scale_x,
vp_y: px_y_min as f32 / scale_y,
scale_x,
scale_y,
out_w: raster_w,
out_h: raster_h,
effective_dpi,
icc,
image_cache: None,
preprocessed: None,
elem_idx: 0,
no_aa,
opm_zero_transparent: false,
knockout_painter_pass: KnockoutPainterPass::None,
parent_group_isolated: false,
alpha_extraction_pass: false,
};
let mut mask_band = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
mask_pool: Vec::new(),
cmyk_buffer: None,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
for (idx, elem) in mask_list.elements().iter().enumerate() {
let elem_ctx = RenderContext {
elem_idx: idx,
..sub_ctx
};
render_element(&mut mask_pixmap, &mut mask_band, elem, &elem_ctx);
}
if params.has_nested_mask_scope
&& params.subtype == stet_graphics::display_list::SoftMaskSubtype::Luminosity
{
let bc = params.backdrop_color.as_ref();
let bd_r = bc.map_or(0u8, |c| (c[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
let bd_g = bc.map_or(0u8, |c| (c[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
let bd_b = bc.map_or(0u8, |c| (c[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
for chunk in mask_pixmap.data_mut().chunks_exact_mut(4) {
let a = chunk[3] as u16;
if a == 255 {
continue;
}
let inv_a = 255 - a;
chunk[0] = ((chunk[0] as u16 * 255 + bd_r as u16 * inv_a + 127) / 255) as u8;
chunk[1] = ((chunk[1] as u16 * 255 + bd_g as u16 * inv_a + 127) / 255) as u8;
chunk[2] = ((chunk[2] as u16 * 255 + bd_b as u16 * inv_a + 127) / 255) as u8;
chunk[3] = 255;
}
}
let pixel_count = (raster_w * raster_h) as usize;
let mut data = vec![0u8; pixel_count];
extract_soft_mask_values(mask_pixmap.data(), &mut data, params);
Some(stet_graphics::display_list::MaskRaster {
data,
width: raster_w,
height: raster_h,
origin_x: px_x_min,
origin_y: px_y_min,
scale_x,
scale_y,
})
}
fn transform_element_ctm(elem: &DisplayElement, pm: &Matrix) -> DisplayElement {
match elem {
DisplayElement::Fill { path, params } => {
let mut p = params.clone();
p.ctm = pm.concat(&p.ctm);
DisplayElement::Fill {
path: path.clone(),
params: p,
}
}
DisplayElement::Stroke { path, params } => {
let mut p = params.clone();
p.ctm = pm.concat(&p.ctm);
DisplayElement::Stroke {
path: path.clone(),
params: p,
}
}
DisplayElement::Clip { path, params } => {
let mut p = params.clone();
p.ctm = pm.concat(&p.ctm);
if let Some(ref mut sp) = p.stroke_params {
sp.ctm = pm.concat(&sp.ctm);
}
DisplayElement::Clip {
path: path.clone(),
params: p,
}
}
DisplayElement::Image {
sample_data,
params,
} => {
let mut p = params.clone();
p.ctm = pm.concat(&p.ctm);
DisplayElement::Image {
sample_data: sample_data.clone(),
params: p,
}
}
DisplayElement::MeshShading { params } => {
let mut p = params.clone();
p.ctm = pm.concat(&p.ctm);
DisplayElement::MeshShading { params: p }
}
DisplayElement::PatchShading { params } => {
let mut p = params.clone();
p.ctm = pm.concat(&p.ctm);
DisplayElement::PatchShading { params: p }
}
DisplayElement::AxialShading { params } => {
let mut p = params.clone();
p.ctm = pm.concat(&p.ctm);
DisplayElement::AxialShading { params: p }
}
DisplayElement::RadialShading { params } => {
let mut p = params.clone();
p.ctm = pm.concat(&p.ctm);
DisplayElement::RadialShading { params: p }
}
DisplayElement::Group { elements, params } => {
let mut t = DisplayList::new();
for child in elements.elements() {
t.push(transform_element_ctm(child, pm));
}
let mut p = params.clone();
let corners = [
pm.transform_point(p.bbox[0], p.bbox[1]),
pm.transform_point(p.bbox[2], p.bbox[1]),
pm.transform_point(p.bbox[0], p.bbox[3]),
pm.transform_point(p.bbox[2], p.bbox[3]),
];
p.bbox = [
corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
corners
.iter()
.map(|c| c.0)
.fold(f64::NEG_INFINITY, f64::max),
corners
.iter()
.map(|c| c.1)
.fold(f64::NEG_INFINITY, f64::max),
];
DisplayElement::Group {
elements: t,
params: p,
}
}
DisplayElement::SoftMasked {
mask,
content,
params,
..
} => {
let mut t_mask = DisplayList::new();
for child in mask.elements() {
t_mask.push(transform_element_ctm(child, pm));
}
let mut t_content = DisplayList::new();
for child in content.elements() {
t_content.push(transform_element_ctm(child, pm));
}
let mut p = params.clone();
let corners = [
pm.transform_point(p.bbox[0], p.bbox[1]),
pm.transform_point(p.bbox[2], p.bbox[1]),
pm.transform_point(p.bbox[0], p.bbox[3]),
pm.transform_point(p.bbox[2], p.bbox[3]),
];
p.bbox = [
corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min),
corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min),
corners
.iter()
.map(|c| c.0)
.fold(f64::NEG_INFINITY, f64::max),
corners
.iter()
.map(|c| c.1)
.fold(f64::NEG_INFINITY, f64::max),
];
if let Some(pcb) = p.parent_clip_bbox {
let pcb_corners = [
pm.transform_point(pcb[0], pcb[1]),
pm.transform_point(pcb[2], pcb[1]),
pm.transform_point(pcb[0], pcb[3]),
pm.transform_point(pcb[2], pcb[3]),
];
p.parent_clip_bbox = Some([
pcb_corners
.iter()
.map(|c| c.0)
.fold(f64::INFINITY, f64::min),
pcb_corners
.iter()
.map(|c| c.1)
.fold(f64::INFINITY, f64::min),
pcb_corners
.iter()
.map(|c| c.0)
.fold(f64::NEG_INFINITY, f64::max),
pcb_corners
.iter()
.map(|c| c.1)
.fold(f64::NEG_INFINITY, f64::max),
]);
}
DisplayElement::SoftMasked {
mask: t_mask,
content: t_content,
params: p,
mask_cache: Arc::new(Mutex::new(None)),
}
}
DisplayElement::PatternFill { params } => {
let mut p = params.clone();
p.pattern_matrix = pm.concat(&p.pattern_matrix);
p.path = transform_path_by_matrix(&p.path, pm);
if let Some(ref mut sp) = p.stroke_params {
sp.ctm = pm.concat(&sp.ctm);
}
DisplayElement::PatternFill { params: p }
}
DisplayElement::OcgGroup {
elements,
ocg_id,
default_visible,
} => {
let mut t = DisplayList::new();
for child in elements.elements() {
t.push(transform_element_ctm(child, pm));
}
DisplayElement::OcgGroup {
elements: t,
ocg_id: *ocg_id,
default_visible: *default_visible,
}
}
other => other.clone(),
}
}
fn transform_path_by_matrix(path: &PsPath, m: &Matrix) -> PsPath {
use stet_fonts::geometry::PathSegment;
let mut out = PsPath::new();
for seg in &path.segments {
out.segments.push(match *seg {
PathSegment::MoveTo(x, y) => {
let (nx, ny) = m.transform_point(x, y);
PathSegment::MoveTo(nx, ny)
}
PathSegment::LineTo(x, y) => {
let (nx, ny) = m.transform_point(x, y);
PathSegment::LineTo(nx, ny)
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => {
let (nx1, ny1) = m.transform_point(x1, y1);
let (nx2, ny2) = m.transform_point(x2, y2);
let (nx3, ny3) = m.transform_point(x3, y3);
PathSegment::CurveTo {
x1: nx1,
y1: ny1,
x2: nx2,
y2: ny2,
x3: nx3,
y3: ny3,
}
}
PathSegment::ClosePath => PathSegment::ClosePath,
});
}
out
}
fn bilinear_prescale(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
let mut dst = vec![0u8; (dw * dh * 4) as usize];
for dy in 0..dh {
let sy_f = (dy as f64 + 0.5) * sh as f64 / dh as f64 - 0.5;
let sy0 = sy_f.floor().max(0.0) as u32;
let sy1 = (sy0 + 1).min(sh - 1);
let fy = (sy_f - sy0 as f64) as f32;
let ify = 1.0 - fy;
for dx in 0..dw {
let sx_f = (dx as f64 + 0.5) * sw as f64 / dw as f64 - 0.5;
let sx0 = sx_f.floor().max(0.0) as u32;
let sx1 = (sx0 + 1).min(sw - 1);
let fx = (sx_f - sx0 as f64) as f32;
let ifx = 1.0 - fx;
let i00 = (sy0 * sw + sx0) as usize * 4;
let i10 = (sy0 * sw + sx1) as usize * 4;
let i01 = (sy1 * sw + sx0) as usize * 4;
let i11 = (sy1 * sw + sx1) as usize * 4;
let di = (dy * dw + dx) as usize * 4;
for c in 0..4 {
dst[di + c] = (src[i00 + c] as f32 * ifx * ify
+ src[i10 + c] as f32 * fx * ify
+ src[i01 + c] as f32 * ifx * fy
+ src[i11 + c] as f32 * fx * fy)
.round() as u8;
}
}
}
dst
}
fn render_pattern_fill(
pixmap: &mut Pixmap,
band_state: &mut BandState,
params: &stet_graphics::device::PatternFillParams,
ctx: &RenderContext<'_>,
) {
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(
&band_state.clip_region,
&mut temp_mask,
ctx.out_w,
ctx.out_h,
) else {
return;
};
let pm = ¶ms.pattern_matrix;
let (step_ux, step_uy) = pm.transform_delta(params.xstep, 0.0);
let (step_vx, step_vy) = pm.transform_delta(0.0, params.ystep);
let step_u_len = (step_ux * step_ux + step_uy * step_uy).sqrt();
let step_v_len = (step_vx * step_vx + step_vy * step_vy).sqrt();
if step_u_len < 0.01 || step_v_len < 0.01 {
return;
}
let origin_x = pm.tx;
let origin_y = pm.ty;
let dev_vp_x = ctx.vp_x as f64;
let dev_vp_y = ctx.vp_y as f64;
let dev_vp_w = ctx.out_w as f64 / ctx.scale_x as f64;
let dev_vp_h = ctx.out_h as f64 / ctx.scale_y as f64;
let (mut min_x, mut min_y, mut max_x, mut max_y) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for seg in ¶ms.path.segments {
let (x, y) = match seg {
PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => (*x, *y),
PathSegment::CurveTo { x3, y3, .. } => (*x3, *y3),
PathSegment::ClosePath => continue,
};
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
if let Some(ref sp) = params.stroke_params {
let ctm = &sp.ctm;
let corners = [
ctm.transform_point(min_x, min_y),
ctm.transform_point(max_x, min_y),
ctm.transform_point(min_x, max_y),
ctm.transform_point(max_x, max_y),
];
min_x = f64::MAX;
min_y = f64::MAX;
max_x = f64::MIN;
max_y = f64::MIN;
for (cx, cy) in &corners {
min_x = min_x.min(*cx);
min_y = min_y.min(*cy);
max_x = max_x.max(*cx);
max_y = max_y.max(*cy);
}
let half_w = sp.line_width
* 0.5
* (ctm.a * ctm.a + ctm.b * ctm.b)
.sqrt()
.max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
min_x -= half_w;
min_y -= half_w;
max_x += half_w;
max_y += half_w;
}
min_x = min_x.max(dev_vp_x);
min_y = min_y.max(dev_vp_y);
max_x = max_x.min(dev_vp_x + dev_vp_w);
max_y = max_y.min(dev_vp_y + dev_vp_h);
if min_x >= max_x || min_y >= max_y {
return;
}
let det = step_ux * step_vy - step_uy * step_vx;
if det.abs() < 1e-10 {
return;
}
let inv_det = 1.0 / det;
let mut tu_min = f64::MAX;
let mut tu_max = f64::MIN;
let mut tv_min = f64::MAX;
let mut tv_max = f64::MIN;
for &(cx, cy) in &[
(min_x, min_y),
(max_x, min_y),
(min_x, max_y),
(max_x, max_y),
] {
let dx = cx - origin_x;
let dy = cy - origin_y;
let tu = (dx * step_vy - dy * step_vx) * inv_det;
let tv = (-dx * step_uy + dy * step_ux) * inv_det;
tu_min = tu_min.min(tu);
tu_max = tu_max.max(tu);
tv_min = tv_min.min(tv);
tv_max = tv_max.max(tv);
}
let tile_x_start = tu_min.floor() as i32 - 1;
let tile_x_end = tu_max.ceil() as i32 + 1;
let tile_y_start = tv_min.floor() as i32 - 1;
let tile_y_end = tv_max.ceil() as i32 + 1;
let tile_count = (tile_x_end - tile_x_start) as i64 * (tile_y_end - tile_y_start) as i64;
if tile_count > 10000 {
return;
}
let Some(mut tile_buf) = Pixmap::new(ctx.out_w, ctx.out_h) else {
return;
};
let sx_f = ctx.scale_x as f64;
let sy_f = ctx.scale_y as f64;
if params.device_space_tile {
for tv in tile_y_start..tile_y_end {
for tu in tile_x_start..tile_x_end {
let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
let tile_ctx = RenderContext {
vp_x: ctx.vp_x - offset_x as f32,
vp_y: ctx.vp_y - offset_y as f32,
scale_x: ctx.scale_x,
scale_y: ctx.scale_y,
out_w: ctx.out_w,
out_h: ctx.out_h,
effective_dpi: ctx.effective_dpi,
icc: ctx.icc,
image_cache: None,
preprocessed: None,
elem_idx: 0,
no_aa: ctx.no_aa,
opm_zero_transparent: params.overprint_mode == 1,
knockout_painter_pass: ctx.knockout_painter_pass,
parent_group_isolated: ctx.parent_group_isolated,
alpha_extraction_pass: ctx.alpha_extraction_pass,
};
let mut tile_band = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
mask_pool: Vec::new(),
cmyk_buffer: None,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
for (idx, elem) in params.tile.elements().iter().enumerate() {
let elem_ctx = RenderContext {
elem_idx: idx,
..tile_ctx
};
render_element(&mut tile_buf, &mut tile_band, elem, &elem_ctx);
}
}
}
} else if params.tile.elements().iter().any(|e| {
!matches!(
e,
DisplayElement::Fill { .. }
| DisplayElement::Stroke { .. }
| DisplayElement::Image { .. }
| DisplayElement::Clip { .. }
| DisplayElement::InitClip
)
}) {
let bbox = ¶ms.bbox;
let corners_dev = [
pm.transform_point(bbox[0], bbox[1]),
pm.transform_point(bbox[2], bbox[1]),
pm.transform_point(bbox[0], bbox[3]),
pm.transform_point(bbox[2], bbox[3]),
];
let (mut td_x0, mut td_y0) = (f64::MAX, f64::MAX);
let (mut td_x1, mut td_y1) = (f64::MIN, f64::MIN);
for (x, y) in &corners_dev {
td_x0 = td_x0.min(*x);
td_y0 = td_y0.min(*y);
td_x1 = td_x1.max(*x);
td_y1 = td_y1.max(*y);
}
let tile_pw = ((td_x1 - td_x0) * sx_f).ceil().max(1.0) as u32;
let tile_ph = ((td_y1 - td_y0) * sy_f).ceil().max(1.0) as u32;
let tile_pw = tile_pw.min(8192);
let tile_ph = tile_ph.min(8192);
if let Some(mut one_tile) = Pixmap::new(tile_pw, tile_ph) {
let tile_render_ctx = RenderContext {
vp_x: td_x0 as f32,
vp_y: td_y0 as f32,
scale_x: ctx.scale_x,
scale_y: ctx.scale_y,
out_w: tile_pw,
out_h: tile_ph,
effective_dpi: ctx.effective_dpi,
icc: ctx.icc,
image_cache: None,
preprocessed: None,
elem_idx: 0,
no_aa: ctx.no_aa,
opm_zero_transparent: params.overprint_mode == 1,
knockout_painter_pass: ctx.knockout_painter_pass,
parent_group_isolated: ctx.parent_group_isolated,
alpha_extraction_pass: ctx.alpha_extraction_pass,
};
let mut tile_bs = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
mask_pool: Vec::new(),
cmyk_buffer: None,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
for (idx, elem) in params.tile.elements().iter().enumerate() {
let transformed = transform_element_ctm(elem, pm);
let elem_ctx = RenderContext {
elem_idx: idx,
..tile_render_ctx
};
render_element(&mut one_tile, &mut tile_bs, &transformed, &elem_ctx);
}
for tv in tile_y_start..tile_y_end {
for tu in tile_x_start..tile_x_end {
let offset_x = tu as f64 * step_ux + tv as f64 * step_vx;
let offset_y = tu as f64 * step_uy + tv as f64 * step_vy;
let px = ((td_x0 + offset_x - dev_vp_x) * sx_f) as i32;
let py = ((td_y0 + offset_y - dev_vp_y) * sy_f) as i32;
let paint = stet_tiny_skia::PixmapPaint {
opacity: 1.0,
blend_mode: BlendMode::SourceOver,
quality: stet_tiny_skia::FilterQuality::Nearest,
};
tile_buf.draw_pixmap(
px,
py,
one_tile.as_ref(),
&paint,
Transform::identity(),
None,
);
}
}
}
} else {
struct PreprocessedImage {
rgba: Vec<u8>,
width: u32,
height: u32,
img_transform: Transform,
}
let tile_elements = params.tile.elements();
let mut preprocessed: Vec<Option<PreprocessedImage>> =
Vec::with_capacity(tile_elements.len());
let tt_sx = (pm.a * sx_f) as f32;
let tt_sy = (pm.d * sy_f) as f32;
let tt_kx = (pm.c * sx_f) as f32;
let tt_ky = (pm.b * sy_f) as f32;
for elem in tile_elements {
if let DisplayElement::Image {
sample_data,
params: ip,
} = elem
{
let iw = ip.width;
let ih = ip.height;
if iw > 0 && ih > 0 {
let mut rgba =
samples_to_rgba(sample_data, ip, ctx.icc, ctx.opm_zero_transparent);
if ip.mask_color.is_some() {
apply_mask_color_rgba(&mut rgba, sample_data, ip);
}
let expected = (iw * ih * 4) as usize;
if rgba.len() >= expected {
if let Some(inv) = ip.image_matrix.invert() {
let combined_mat = ip.ctm.concat(&inv);
let t = to_transform(&combined_mat);
let test = t.post_concat(Transform::from_row(
tt_sx, tt_ky, tt_kx, tt_sy, 0.0, 0.0,
));
let eff_sx = (test.sx * test.sx + test.ky * test.ky).sqrt();
let eff_sy = (test.kx * test.kx + test.sy * test.sy).sqrt();
if eff_sx < 0.99 || eff_sy < 0.99 {
let tw = (iw as f32 * eff_sx).floor().max(1.0) as u32;
let th = (ih as f32 * eff_sy).floor().max(1.0) as u32;
let scaled = bilinear_prescale(&rgba, iw, ih, tw, th);
let adj = Transform::from_scale(
iw as f32 / tw as f32,
ih as f32 / th as f32,
);
preprocessed.push(Some(PreprocessedImage {
rgba: scaled,
width: tw,
height: th,
img_transform: t.pre_concat(adj),
}));
} else {
preprocessed.push(Some(PreprocessedImage {
rgba,
width: iw,
height: ih,
img_transform: t,
}));
}
} else {
preprocessed.push(None);
}
} else {
preprocessed.push(None);
}
} else {
preprocessed.push(None);
}
}
}
for tv in tile_y_start..tile_y_end {
for tu in tile_x_start..tile_x_end {
let pat_offset_x = tu as f64 * params.xstep;
let pat_offset_y = tv as f64 * params.ystep;
let tile_transform = Transform::from_row(
tt_sx,
tt_ky,
tt_kx,
tt_sy,
((pm.a * pat_offset_x + pm.c * pat_offset_y + pm.tx - dev_vp_x) * sx_f) as f32,
((pm.b * pat_offset_x + pm.d * pat_offset_y + pm.ty - dev_vp_y) * sy_f) as f32,
);
let bbox_clip = {
let bb = ¶ms.bbox;
let mut bp = stet_tiny_skia::PathBuilder::new();
bp.move_to(bb[0] as f32, bb[1] as f32);
bp.line_to(bb[2] as f32, bb[1] as f32);
bp.line_to(bb[2] as f32, bb[3] as f32);
bp.line_to(bb[0] as f32, bb[3] as f32);
bp.close();
bp.finish().and_then(|sp| {
let mut m = Mask::new(ctx.out_w, ctx.out_h)?;
m.fill_path(
&sp,
stet_tiny_skia::FillRule::Winding,
false,
tile_transform,
);
Some(m)
})
};
let mut tile_clip: Option<Mask> = bbox_clip;
let mut img_idx = 0usize;
for elem in tile_elements {
let clip_ref = tile_clip.as_ref();
match elem {
DisplayElement::Clip { path, params: cp } => {
if let Some(sp) = build_skia_path(path) {
let t = to_transform(&cp.ctm);
let combined = t.post_concat(tile_transform);
let mut mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
mask.fill_path(&sp, to_fill_rule(&cp.fill_rule), false, combined);
if let Some(prev) = tile_clip.take() {
intersect_masks(&mut mask, &prev);
}
tile_clip = Some(mask);
}
}
DisplayElement::InitClip => {
tile_clip = None;
}
DisplayElement::Fill { path, params: fp } => {
if let Some(sp) = build_skia_path(path) {
let mut paint = if params.paint_type == 1 {
to_paint(&fp.color)
} else {
to_paint(
params
.underlying_color
.as_ref()
.unwrap_or(&DeviceColor::black()),
)
};
paint.anti_alias = false;
let t = to_transform(&fp.ctm);
let combined = t.post_concat(tile_transform);
let fr = to_fill_rule(&fp.fill_rule);
tile_buf.fill_path(&sp, &paint, fr, combined, clip_ref);
}
}
DisplayElement::Stroke { path, params: sp } => {
if let Some(skp) = build_skia_path(path) {
let effective_ctm = pm.concat(&sp.ctm);
let mut sp_adj = sp.clone();
sp_adj.ctm = effective_ctm;
let stroke = build_stroke(&sp_adj, ctx.effective_dpi);
let paint = if params.paint_type == 1 {
to_paint(&sp.color)
} else {
to_paint(
params
.underlying_color
.as_ref()
.unwrap_or(&DeviceColor::black()),
)
};
let t = to_transform(&sp.ctm);
let combined = t.post_concat(tile_transform);
tile_buf.stroke_path(&skp, &paint, &stroke, combined, clip_ref);
}
}
DisplayElement::Image { .. } => {
if let Some(ref pi) = preprocessed[img_idx] {
let combined = pi.img_transform.post_concat(tile_transform);
if let Some(img_ref) = stet_tiny_skia::PixmapRef::from_bytes(
&pi.rgba, pi.width, pi.height,
) {
let paint = stet_tiny_skia::PixmapPaint {
opacity: 1.0,
blend_mode: BlendMode::SourceOver,
quality: stet_tiny_skia::FilterQuality::Nearest,
};
tile_buf.draw_pixmap(0, 0, img_ref, &paint, combined, clip_ref);
}
}
img_idx += 1;
}
_ => {}
}
}
}
}
}
let Some(fill_skia_path) = build_skia_path(¶ms.path) else {
return;
};
let fill_rule = to_fill_rule(¶ms.fill_rule);
let mut fill_mask = Mask::new(ctx.out_w, ctx.out_h).expect("mask");
let path_transform = viewport_transform(
Transform::identity(),
ctx.vp_x,
ctx.vp_y,
ctx.scale_x,
ctx.scale_y,
);
if let Some(ref sp) = params.stroke_params {
let stroke = build_stroke(sp, ctx.effective_dpi);
let ctm_transform = to_transform(&sp.ctm);
let combined = ctm_transform.post_concat(path_transform);
let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&combined);
let dashed;
let stroke_path = if let Some(ref dash) = stroke.dash {
dashed = fill_skia_path.dash(dash, res_scale);
match dashed.as_ref() {
Some(p) => p,
None => &fill_skia_path,
}
} else {
&fill_skia_path
};
if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
fill_mask.fill_path(
&outline,
stet_tiny_skia::FillRule::Winding,
!ctx.no_aa,
combined,
);
}
} else {
fill_mask.fill_path(&fill_skia_path, fill_rule, !ctx.no_aa, path_transform);
}
if let Some(clip_mask) = mask_ref {
intersect_masks(&mut fill_mask, clip_mask);
}
let img_paint = stet_tiny_skia::PixmapPaint::default();
pixmap.draw_pixmap(
0,
0,
tile_buf.as_ref(),
&img_paint,
Transform::identity(),
Some(&fill_mask),
);
}
fn clip_path_unified(
band_state: &mut BandState,
path: &PsPath,
params: &ClipParams,
ctx: &RenderContext<'_>,
) {
let is_unit_scale = ctx.scale_x == 1.0 && ctx.scale_y == 1.0;
if is_unit_scale {
let y_start = ctx.vp_y as u32;
let x_start = ctx.vp_x as u32;
if x_start == 0
&& params.stroke_params.is_none()
&& params.ctm.a == 1.0
&& params.ctm.d == 1.0
&& params.ctm.tx == 0.0
&& params.ctm.ty == 0.0
&& let Some(bbox) = path_y_bbox(path)
&& (bbox.y_max <= y_start as f64 || bbox.y_min >= (y_start + ctx.out_h) as f64)
{
if let Some(ClipRegion::Mask(mask)) = band_state.clip_region.take() {
band_state.recycle_mask(mask);
}
band_state.clip_region = Some(ClipRegion::Rect(ClipRect {
x0: 0,
y0: 0,
x1: 0,
y1: 0,
}));
return;
}
let ctm_is_identity = params.ctm.a == 1.0
&& params.ctm.b == 0.0
&& params.ctm.c == 0.0
&& params.ctm.d == 1.0
&& params.ctm.tx == 0.0
&& params.ctm.ty == 0.0;
if x_start == 0
&& ctm_is_identity
&& params.stroke_params.is_none()
&& let Some(dev_rect) = detect_rect(path, ctx.out_w, u32::MAX)
{
let new_rect = translate_clip_rect(&dev_rect, y_start, ctx.out_h);
match band_state.clip_region.take() {
None => {
band_state.clip_region = Some(ClipRegion::Rect(new_rect));
}
Some(ClipRegion::Rect(existing)) => {
band_state.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
}
Some(ClipRegion::Mask(mut mask)) => {
intersect_mask_with_rect(&mut mask, &new_rect, ctx.out_w, ctx.out_h);
band_state.clip_region = Some(ClipRegion::Mask(mask));
}
}
return;
}
}
let fill_rule = to_fill_rule(¶ms.fill_rule);
let path_hash = hash_clip_path(path, ¶ms.fill_rule);
let prev_region = band_state.clip_region.take();
let mut mask = band_state.take_mask(ctx.out_w, ctx.out_h);
let path_mask = if let Some(cached) = band_state.clip_mask_cache.get(&path_hash) {
mask.data_mut().copy_from_slice(cached.data());
mask
} else {
let Some(skia_path) = build_skia_path(path) else {
band_state.recycle_mask(mask);
band_state.clip_region = prev_region;
return;
};
mask.data_mut().fill(0);
if let Some(ref sp) = params.stroke_params {
let stroke = build_stroke(sp, ctx.effective_dpi);
let transform = ctx.transform(&sp.ctm);
let res_scale = stet_tiny_skia::PathStroker::compute_resolution_scale(&transform);
let dashed;
let stroke_path = if let Some(ref dash) = stroke.dash {
dashed = skia_path.dash(dash, res_scale);
match dashed.as_ref() {
Some(p) => p,
None => &skia_path,
}
} else {
&skia_path
};
if let Some(outline) = stroke_path.stroke(&stroke, res_scale) {
mask.fill_path(
&outline,
stet_tiny_skia::FillRule::Winding,
false,
transform,
);
}
} else {
let transform = ctx.transform(¶ms.ctm);
mask.fill_path(&skia_path, fill_rule, false, transform);
}
if !band_state.clip_mask_seen.insert(path_hash) {
band_state.clip_mask_cache.insert(path_hash, mask.clone());
}
mask
};
match prev_region {
None => {
band_state.clip_region = Some(ClipRegion::Mask(path_mask));
}
Some(ClipRegion::Rect(rect)) => {
if rect.is_empty() {
band_state.recycle_mask(path_mask);
band_state.clip_region = Some(ClipRegion::Rect(rect));
} else {
let mut mask = path_mask;
intersect_mask_with_rect(&mut mask, &rect, ctx.out_w, ctx.out_h);
band_state.clip_region = Some(ClipRegion::Mask(mask));
}
}
Some(ClipRegion::Mask(mut existing)) => {
intersect_masks(&mut existing, &path_mask);
band_state.recycle_mask(path_mask);
band_state.clip_region = Some(ClipRegion::Mask(existing));
}
}
}
impl OutputDevice for SkiaDevice {
fn fill_path(&mut self, path: &PsPath, params: &FillParams) {
self.ensure_full_pixmap();
let Some(skia_path) = build_skia_path(path) else {
return;
};
let (w, h) = (self.pixmap.width(), self.pixmap.height());
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
return; };
let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
let transform = to_transform(¶ms.ctm);
let fill_rule = to_fill_rule(¶ms.fill_rule);
self.pixmap
.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
}
fn stroke_path(&mut self, path: &PsPath, params: &StrokeParams) {
self.ensure_full_pixmap();
let stroke = build_stroke(params, self.dpi);
let adjusted;
let draw_path =
if params.stroke_adjust && stroke.width <= 2.0 && ctm_is_device_space(¶ms.ctm) {
adjusted =
stroke_adjust_path_viewport(path, stroke.width as f64, 1.0, 1.0, 0.0, 0.0);
&adjusted
} else {
path
};
let Some(skia_path) = build_skia_path(draw_path) else {
return;
};
let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, self.no_aa);
let transform = to_transform(¶ms.ctm);
let (w, h) = (self.pixmap.width(), self.pixmap.height());
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
return; };
self.pixmap
.stroke_path(&skia_path, &paint, &stroke, transform, mask_ref);
}
fn clip_path(&mut self, path: &PsPath, params: &ClipParams) {
self.ensure_full_pixmap();
let (w, h) = (self.pixmap.width(), self.pixmap.height());
if let Some(new_rect) = detect_rect(path, w, h) {
match self.clip_region.take() {
None => {
self.clip_region = Some(ClipRegion::Rect(new_rect));
}
Some(ClipRegion::Rect(existing)) => {
self.clip_region = Some(ClipRegion::Rect(existing.intersect(&new_rect)));
}
Some(ClipRegion::Mask(mut mask)) => {
intersect_mask_with_rect(&mut mask, &new_rect, w, h);
self.clip_region = Some(ClipRegion::Mask(mask));
}
}
return;
}
let fill_rule = to_fill_rule(¶ms.fill_rule);
let path_hash = hash_clip_path(path, ¶ms.fill_rule);
let prev_region = self.clip_region.take();
macro_rules! take_spare {
($self:expr, $w:expr, $h:expr) => {
$self
.spare_mask
.take()
.unwrap_or_else(|| Mask::new($w, $h).expect("Failed to create mask"))
};
}
let path_mask = if let Some(cached) = self.clip_mask_cache.get(&path_hash) {
let mut mask = take_spare!(self, w, h);
mask.data_mut().copy_from_slice(cached.data());
mask
} else {
let Some(skia_path) = build_skia_path(path) else {
self.clip_region = prev_region;
return;
};
let transform = to_transform(¶ms.ctm);
let mut mask = take_spare!(self, w, h);
mask.data_mut().fill(0); mask.fill_path(&skia_path, fill_rule, false, transform);
if !self.clip_mask_seen.insert(path_hash) {
self.clip_mask_cache.insert(path_hash, mask.clone());
}
mask
};
match prev_region {
None => {
self.clip_region = Some(ClipRegion::Mask(path_mask));
}
Some(ClipRegion::Rect(rect)) => {
if rect.is_empty() {
self.spare_mask = Some(path_mask); } else {
let mut mask = path_mask;
intersect_mask_with_rect(&mut mask, &rect, w, h);
self.clip_region = Some(ClipRegion::Mask(mask));
}
}
Some(ClipRegion::Mask(mut existing)) => {
intersect_masks(&mut existing, &path_mask);
self.spare_mask = Some(path_mask); self.clip_region = Some(ClipRegion::Mask(existing));
}
}
}
fn init_clip(&mut self) {
if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
self.spare_mask = Some(mask);
}
self.clip_region = None;
}
fn erase_page(&mut self) {
self.pixmap.fill(Color::WHITE);
if let Some(ClipRegion::Mask(mask)) = self.clip_region.take() {
self.spare_mask = Some(mask);
}
self.clip_region = None;
}
fn show_page(&mut self, output_path: &str) -> Result<(), String> {
let w = self.pixmap.width();
let h = self.pixmap.height();
composite_onto_white(self.pixmap.data_mut());
let mut sink = self.sink_factory.create_sink(output_path)?;
sink.begin_page(w, h)?;
sink.write_rows(self.pixmap.data(), h)?;
sink.end_page()
}
fn draw_image(&mut self, sample_data: &[u8], params: &ImageParams) {
self.ensure_full_pixmap();
let w = params.width;
let h = params.height;
if w == 0 || h == 0 {
return;
}
let mut rgba_data =
samples_to_rgba(sample_data, params, self.render_icc_cache.as_ref(), false);
if params.mask_color.is_some() {
apply_mask_color_rgba(&mut rgba_data, sample_data, params);
}
let expected = (w * h * 4) as usize;
if rgba_data.len() < expected {
return;
}
let Some(image_inv) = params.image_matrix.invert() else {
return;
};
let combined = params.ctm.concat(&image_inv);
let raw_transform = enforce_min_image_size(to_transform(&combined), w, h);
let prescaled = prescale_image(&rgba_data, w, h, raw_transform, params.interpolate);
let (img_data, img_w, img_h, transform) = match &prescaled {
Some((data, pw, ph, t)) => (data.as_slice(), *pw, *ph, *t),
None => (rgba_data.as_slice(), w, h, raw_transform),
};
let Some(img_pixmap) = stet_tiny_skia::PixmapRef::from_bytes(img_data, img_w, img_h) else {
return;
};
let (pw, ph) = (self.pixmap.width(), self.pixmap.height());
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, pw, ph) else {
return;
};
let paint = stet_tiny_skia::PixmapPaint {
quality: image_filter_quality(transform, params.interpolate),
opacity: params.alpha as f32,
blend_mode: u8_to_blend_mode(params.blend_mode),
};
self.pixmap
.draw_pixmap(0, 0, img_pixmap, &paint, transform, mask_ref);
}
fn paint_axial_shading(&mut self, params: &AxialShadingParams) {
self.ensure_full_pixmap();
let (w, h) = (self.pixmap.width(), self.pixmap.height());
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
return;
};
render_axial_shading(
&mut self.pixmap,
params,
0.0,
0.0,
1.0,
1.0,
mask_ref,
self.no_aa,
None,
None,
);
}
fn paint_radial_shading(&mut self, params: &RadialShadingParams) {
self.ensure_full_pixmap();
let (w, h) = (self.pixmap.width(), self.pixmap.height());
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
return;
};
render_radial_shading(
&mut self.pixmap,
params,
0.0,
0.0,
1.0,
1.0,
mask_ref,
self.no_aa,
None,
None,
);
}
fn paint_mesh_shading(&mut self, params: &MeshShadingParams) {
self.ensure_full_pixmap();
let (w, h) = (self.pixmap.width(), self.pixmap.height());
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
return;
};
render_mesh_shading(
&mut self.pixmap,
params,
0.0,
0.0,
1.0,
1.0,
mask_ref,
None,
None,
);
}
fn paint_patch_shading(&mut self, params: &PatchShadingParams) {
self.ensure_full_pixmap();
let (w, h) = (self.pixmap.width(), self.pixmap.height());
let mut temp_mask = None;
let Some(mask_ref) = resolve_clip_mask(&self.clip_region, &mut temp_mask, w, h) else {
return;
};
render_patch_shading(
&mut self.pixmap,
params,
0.0,
0.0,
1.0,
1.0,
mask_ref,
None,
None,
);
}
fn paint_pattern_fill(&mut self, params: &stet_graphics::device::PatternFillParams) {
self.ensure_full_pixmap();
let w = self.pixmap.width();
let h = self.pixmap.height();
let mut band_state = BandState {
clip_region: self.clip_region.take(),
spare_mask: self.spare_mask.take(),
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
mask_pool: Vec::new(),
cmyk_buffer: None,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
{
let ctx = RenderContext {
vp_x: 0.0,
vp_y: 0.0,
scale_x: 1.0,
scale_y: 1.0,
out_w: w,
out_h: h,
effective_dpi: self.dpi,
icc: None,
image_cache: None,
preprocessed: None,
elem_idx: 0,
no_aa: self.no_aa,
opm_zero_transparent: false,
knockout_painter_pass: KnockoutPainterPass::None,
parent_group_isolated: false,
alpha_extraction_pass: false,
};
render_pattern_fill(&mut self.pixmap, &mut band_state, params, &ctx);
}
self.clip_region = band_state.clip_region.take();
if let Some(mask) = band_state.spare_mask.take() {
self.spare_mask = Some(mask);
}
}
fn page_size(&self) -> (u32, u32) {
(self.page_w, self.page_h)
}
fn replay_and_show(&mut self, list: DisplayList, output_path: &str) -> Result<(), String> {
self.join_pending()?;
let (page_w, page_h) = self.page_size();
if self.use_viewport_path {
let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref());
let rgba = render_to_rgba_viewport(
&list,
page_w,
page_h,
self.dpi,
Some(&icc_cache),
self.no_aa,
);
let mut sink = self.sink_factory.create_sink(output_path)?;
sink.begin_page(page_w, page_h)?;
sink.write_rows(&rgba, page_h)?;
sink.end_page()?;
return Ok(());
}
let band_h = select_band_height(page_w, page_h);
let icc_cache = build_icc_cache_for_list(&list, self.system_cmyk_bytes.as_ref());
if band_h >= page_h {
self.ensure_full_pixmap();
let ctx = RenderContext {
vp_x: 0.0,
vp_y: 0.0,
scale_x: 1.0,
scale_y: 1.0,
out_w: page_w,
out_h: page_h,
effective_dpi: self.dpi,
icc: Some(&icc_cache),
image_cache: None,
preprocessed: None,
elem_idx: 0,
no_aa: self.no_aa,
opm_zero_transparent: false,
knockout_painter_pass: KnockoutPainterPass::None,
parent_group_isolated: false,
alpha_extraction_pass: false,
};
let mut band_state = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: HashSet::new(),
mask_pool: Vec::new(),
cmyk_buffer: None,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
for (idx, elem) in list.elements().iter().enumerate() {
let elem_ctx = RenderContext {
elem_idx: idx,
..ctx
};
render_element(&mut self.pixmap, &mut band_state, elem, &elem_ctx);
}
return self.show_page(output_path);
}
if self.pixmap.width() > 1 {
self.pixmap = Pixmap::new(1, 1).expect("Failed to create placeholder pixmap");
}
let mut sink = self.sink_factory.create_sink(output_path)?;
let dpi = self.dpi;
#[cfg(feature = "parallel")]
{
let no_aa = self.no_aa;
let (tx, rx) = std::sync::mpsc::sync_channel(1);
rayon::spawn(move || {
let result = render_banded_to_sink(
page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, no_aa,
);
let _ = tx.send(result);
});
self.pending_render = Some(rx);
}
#[cfg(not(feature = "parallel"))]
{
render_banded_to_sink(
page_w, page_h, band_h, dpi, &list, &mut *sink, &icc_cache, self.no_aa,
)?;
}
Ok(())
}
fn finish(&mut self) -> Result<(), String> {
self.join_pending()
}
}
impl Drop for SkiaDevice {
fn drop(&mut self) {
if let Some(rx) = self.pending_render.take() {
let _ = rx.recv();
}
}
}
impl SkiaDevice {
fn join_pending(&mut self) -> Result<(), String> {
if let Some(rx) = self.pending_render.take() {
match rx.recv() {
Ok(result) => result?,
Err(_) => return Err("Background render task failed".to_string()),
}
}
Ok(())
}
}
fn has_cmyk_group(list: &DisplayList) -> bool {
use stet_graphics::display_list::GroupColorSpace;
for elem in list.elements() {
match elem {
DisplayElement::Group { elements, params } => {
if params.color_space == GroupColorSpace::DeviceCMYK {
return true;
}
if has_cmyk_group(elements) {
return true;
}
}
DisplayElement::SoftMasked { content, mask, .. } => {
if has_cmyk_group(content) || has_cmyk_group(mask) {
return true;
}
}
DisplayElement::OcgGroup { elements, .. } => {
if has_cmyk_group(elements) {
return true;
}
}
_ => {}
}
}
false
}
fn group_only_native_cmyk_fills(elements: &DisplayList) -> bool {
let mut found_paint = false;
for elem in elements.elements() {
match elem {
DisplayElement::InitClip => continue,
DisplayElement::Clip { .. } => continue,
DisplayElement::Fill { params, .. } => {
if params.color.native_cmyk.is_none() {
return false;
}
found_paint = true;
}
DisplayElement::Stroke { params, .. } => {
if params.color.native_cmyk.is_none() {
return false;
}
found_paint = true;
}
_ => return false,
}
}
found_paint
}
fn group_content_is_native_cmyk(elements: &DisplayList) -> bool {
let mut found_paint = false;
for elem in elements.elements() {
match elem {
DisplayElement::InitClip => continue,
DisplayElement::Clip { .. } => continue,
DisplayElement::Text { .. } => continue,
DisplayElement::ErasePage => continue,
DisplayElement::Fill { params, .. } => {
if params.color.native_cmyk.is_none() {
return false;
}
found_paint = true;
}
DisplayElement::Stroke { params, .. } => {
if params.color.native_cmyk.is_none() {
return false;
}
found_paint = true;
}
DisplayElement::Image { params, .. } => {
if !is_cmyk_color_space(¶ms.color_space) {
return false;
}
found_paint = true;
}
DisplayElement::AxialShading { .. }
| DisplayElement::RadialShading { .. }
| DisplayElement::MeshShading { .. }
| DisplayElement::PatchShading { .. } => {
return false;
}
DisplayElement::PatternFill { .. } => {
return false;
}
DisplayElement::Group { elements: sub, .. } => {
if !group_content_is_native_cmyk(sub) {
return false;
}
found_paint = true;
}
DisplayElement::SoftMasked { .. } => {
return false;
}
DisplayElement::OcgGroup { elements: sub, .. } => {
if !group_content_is_native_cmyk(sub) {
return false;
}
found_paint = true;
}
}
}
found_paint
}
fn content_list_is_simple_native_cmyk(list: &DisplayList) -> bool {
let mut found_paint = false;
for elem in list.elements() {
match elem {
DisplayElement::InitClip
| DisplayElement::Clip { .. }
| DisplayElement::Text { .. }
| DisplayElement::ErasePage => continue,
DisplayElement::Fill { params, .. } => {
if params.color.native_cmyk.is_none() {
return false;
}
if params.blend_mode != 0 || params.alpha != 1.0 {
return false;
}
found_paint = true;
}
DisplayElement::Stroke { params, .. } => {
if params.color.native_cmyk.is_none() {
return false;
}
if params.blend_mode != 0 || params.alpha != 1.0 {
return false;
}
found_paint = true;
}
_ => return false,
}
}
found_paint
}
fn has_overprint_elements(list: &DisplayList) -> bool {
for elem in list.elements() {
match elem {
DisplayElement::Fill { params, .. } => {
if params.overprint {
return true;
}
}
DisplayElement::Stroke { params, .. } => {
if params.overprint {
return true;
}
}
DisplayElement::Image { params, .. } => {
if params.overprint {
return true;
}
}
DisplayElement::AxialShading { params } => {
if params.overprint {
return true;
}
}
DisplayElement::RadialShading { params } => {
if params.overprint {
return true;
}
}
DisplayElement::MeshShading { params } => {
if params.overprint {
return true;
}
}
DisplayElement::PatchShading { params } => {
if params.overprint {
return true;
}
}
DisplayElement::Group { elements, .. } => {
if has_overprint_elements(elements) {
return true;
}
}
DisplayElement::SoftMasked { content, mask, .. } => {
if has_overprint_elements(content) || has_overprint_elements(mask) {
return true;
}
}
DisplayElement::OcgGroup { elements, .. } => {
if has_overprint_elements(elements) {
return true;
}
}
_ => {}
}
}
false
}
#[allow(clippy::too_many_arguments)]
fn render_overprint_fill(
pixmap: &mut Pixmap,
cmyk_buf: &mut [f32],
op_bg: &mut [u8],
op_touched: &mut [u8],
spot_mask: &[u8],
band_state: &mut BandState,
path: &PsPath,
params: &FillParams,
vp_x: f32,
vp_y: f32,
scale_x: f32,
scale_y: f32,
out_w: u32,
out_h: u32,
icc: Option<&IccCache>,
no_aa: bool,
) {
let Some(skia_path) = build_skia_path(path) else {
return;
};
let fill_rule = to_fill_rule(¶ms.fill_rule);
let mut coverage_mask = match Mask::new(out_w, out_h) {
Some(m) => m,
None => return,
};
let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
path_device_bbox(&skia_path, transform, out_w, out_h);
let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
None => None,
Some(ClipRegion::Rect(r)) => {
let data = coverage_mask.data_mut();
let stride = out_w as usize;
for y in bbox_y0..bbox_y1 {
let row_start = y * stride;
for x in bbox_x0..bbox_x1 {
let yu = y as u32;
let xu = x as u32;
if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
data[row_start + x] = 0;
}
}
}
None
}
Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
};
let (src_c, src_m, src_y, src_k) = params.color.native_cmyk.unwrap_or_else(|| {
let r = params.color.r;
let g = params.color.g;
let b = params.color.b;
(1.0 - r, 1.0 - g, 1.0 - b, 0.0)
});
let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
let mut channels = params.painted_channels;
if channels == 0 {
channels = stet_graphics::device::CMYK_ALL;
}
if params.overprint_mode == 1
&& channels == stet_graphics::device::CMYK_ALL
&& params.is_device_cmyk
{
channels = 0;
if src_c != 0.0 {
channels |= stet_graphics::device::CMYK_C;
}
if src_m != 0.0 {
channels |= stet_graphics::device::CMYK_M;
}
if src_y != 0.0 {
channels |= stet_graphics::device::CMYK_Y;
}
if src_k != 0.0 {
channels |= stet_graphics::device::CMYK_K;
}
if channels == 0 && !params.opm_paired {
channels = stet_graphics::device::CMYK_ALL;
}
}
let is_k_only_cmyk = params.is_device_cmyk
&& params.overprint_mode == 0
&& src_c == 0.0
&& src_m == 0.0
&& src_y == 0.0;
if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
let cov_data = coverage_mask.data();
let stride = out_w as usize;
for y in bbox_y0..bbox_y1 {
for x in bbox_x0..bbox_x1 {
let mi = y * stride + x;
let mut cov = cov_data[mi] as f32 / 255.0;
if let Some(clip) = clip_coverage {
cov *= clip[mi] as f32 / 255.0;
}
if cov > 0.0 {
let ci = mi * 4;
cmyk_buf[ci] = src_c as f32;
cmyk_buf[ci + 1] = src_m as f32;
cmyk_buf[ci + 2] = src_y as f32;
cmyk_buf[ci + 3] = src_k as f32;
}
}
}
let mut temp_mask = None;
let Some(mask_ref) =
resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
else {
return;
};
let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
pixmap.fill_path(&skia_path, &paint, fill_rule, transform, mask_ref);
return;
}
let cov_data = coverage_mask.data();
let stride = out_w as usize;
let px_data = pixmap.data_mut();
let px_stride = out_w as usize * 4;
for y in bbox_y0..bbox_y1 {
for x in bbox_x0..bbox_x1 {
let mi = y * stride + x;
let mut cov = cov_data[mi] as f32 / 255.0;
if let Some(clip) = clip_coverage {
cov *= clip[mi] as f32 / 255.0;
}
if cov <= 0.0 {
continue;
}
let ci = mi * 4;
let pi = y * px_stride + x * 4;
if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
op_bg[pi] = px_data[pi];
op_bg[pi + 1] = px_data[pi + 1];
op_bg[pi + 2] = px_data[pi + 2];
op_bg[pi + 3] = px_data[pi + 3];
op_touched[mi] = 1;
}
let cur_c = cmyk_buf[ci] as f64;
let cur_m = cmyk_buf[ci + 1] as f64;
let cur_y = cmyk_buf[ci + 2] as f64;
let cur_k = cmyk_buf[ci + 3] as f64;
let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
let pixmap_has_colour = px_data[pi + 3] > 0
&& (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
&& channels == stet_graphics::device::CMYK_K
&& params.is_device_cmyk
&& src_c == 0.0
&& src_m == 0.0
&& src_y == 0.0;
let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
stet_graphics::device::CMYK_ALL
} else {
channels
};
let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
src_c
} else {
cur_c
};
let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
src_m
} else {
cur_m
};
let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
src_y
} else {
cur_y
};
let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
src_k
} else {
cur_k
};
if !is_custom_spot {
cmyk_buf[ci] = new_c as f32;
cmyk_buf[ci + 1] = new_m as f32;
cmyk_buf[ci + 2] = new_y as f32;
cmyk_buf[ci + 3] = new_k as f32;
}
let delta = (new_c - cur_c)
.abs()
.max((new_m - cur_m).abs())
.max((new_y - cur_y).abs())
.max((new_k - cur_k).abs());
if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
continue;
}
let (r, g, b) =
if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
(params.color.r, params.color.g, params.color.b)
} else if use_multiplicative {
let bg_r = px_data[pi] as f64 / 255.0;
let bg_g = px_data[pi + 1] as f64 / 255.0;
let bg_b = px_data[pi + 2] as f64 / 255.0;
let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
1.0 - src_c
} else {
1.0
};
let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
1.0 - src_m
} else {
1.0
};
let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
1.0 - src_y
} else {
1.0
};
let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
1.0 - src_k
} else {
1.0
};
(
(bg_r * over_r * k_fac).clamp(0.0, 1.0),
(bg_g * over_g * k_fac).clamp(0.0, 1.0),
(bg_b * over_b * k_fac).clamp(0.0, 1.0),
)
} else if let Some(icc_cache) = icc {
icc_cache
.convert_cmyk_readonly(new_c, new_m, new_y, new_k)
.unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
} else {
cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
};
let a = (cov * params.alpha as f32).min(1.0);
let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
let dr = (op_bg[pi] as f32 - new_r).abs();
let dg = (op_bg[pi + 1] as f32 - new_g).abs();
let db = (op_bg[pi + 2] as f32 - new_b).abs();
if dr.max(dg).max(db) <= 4.0 {
(op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
} else {
(
px_data[pi],
px_data[pi + 1],
px_data[pi + 2],
px_data[pi + 3],
)
}
} else {
(
px_data[pi],
px_data[pi + 1],
px_data[pi + 2],
px_data[pi + 3],
)
};
let dst_a = bk_a as f32 / 255.0;
let one_minus_a = 1.0 - a;
let out_a = a + dst_a * one_minus_a;
if out_a > 0.0 {
px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
.clamp(0.0, 255.0)
.round() as u8;
px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
.clamp(0.0, 255.0)
.round() as u8;
px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
.clamp(0.0, 255.0)
.round() as u8;
px_data[pi + 3] = (out_a * 255.0).round() as u8;
}
}
}
}
fn cmyk_to_rgb_plrm(c: f64, m: f64, y: f64, k: f64) -> (f64, f64, f64) {
(
1.0 - (c + k).min(1.0),
1.0 - (m + k).min(1.0),
1.0 - (y + k).min(1.0),
)
}
#[allow(clippy::too_many_arguments)]
fn path_device_bbox(
skia_path: &stet_tiny_skia::Path,
transform: Transform,
w: u32,
h: u32,
) -> (usize, usize, usize, usize) {
let b = skia_path.bounds();
let mut corners = [
stet_tiny_skia::Point {
x: b.left(),
y: b.top(),
},
stet_tiny_skia::Point {
x: b.right(),
y: b.top(),
},
stet_tiny_skia::Point {
x: b.right(),
y: b.bottom(),
},
stet_tiny_skia::Point {
x: b.left(),
y: b.bottom(),
},
];
transform.map_points(&mut corners);
let min_x = corners.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
let min_y = corners.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
let max_x = corners
.iter()
.map(|p| p.x)
.fold(f32::NEG_INFINITY, f32::max);
let max_y = corners
.iter()
.map(|p| p.y)
.fold(f32::NEG_INFINITY, f32::max);
let x0 = (min_x.floor() as i32 - 1).max(0) as usize;
let y0 = (min_y.floor() as i32 - 1).max(0) as usize;
let x1 = (max_x.ceil() as i32 + 1).clamp(0, w as i32) as usize;
let y1 = (max_y.ceil() as i32 + 1).clamp(0, h as i32) as usize;
(x0, y0, x1, y1)
}
fn update_cmyk_buffer_for_fill(
cmyk_buf: &mut [f32],
spot_mask: &mut [u8],
path: &PsPath,
params: &FillParams,
vp_x: f32,
vp_y: f32,
scale_x: f32,
scale_y: f32,
out_w: u32,
out_h: u32,
clip_region: &Option<ClipRegion>,
no_aa: bool,
icc: Option<&IccCache>,
) {
let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
|| matches!(
(params.color.native_cmyk, params.color.process_cmyk),
(Some(nat), Some(proc_))
if (nat.0 - proc_.0).abs() > 1e-6
|| (nat.1 - proc_.1).abs() > 1e-6
|| (nat.2 - proc_.2).abs() > 1e-6
|| (nat.3 - proc_.3).abs() > 1e-6
);
let (src_c, src_m, src_y, src_k) = if is_custom_spot {
(0.0, 0.0, 0.0, 0.0)
} else if let Some(c) = params.color.process_cmyk {
c
} else if let Some(c) = params.color.native_cmyk {
c
} else if let Some(cmyk) = icc.and_then(|i| {
i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
}) {
(cmyk[0], cmyk[1], cmyk[2], cmyk[3])
} else {
(
(1.0 - params.color.r).clamp(0.0, 1.0),
(1.0 - params.color.g).clamp(0.0, 1.0),
(1.0 - params.color.b).clamp(0.0, 1.0),
0.0,
)
};
let Some(skia_path) = build_skia_path(path) else {
return;
};
let mut coverage_mask = match Mask::new(out_w, out_h) {
Some(m) => m,
None => return,
};
let transform = viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
let fill_rule = to_fill_rule(¶ms.fill_rule);
coverage_mask.fill_path(&skia_path, fill_rule, !no_aa, transform);
let cov_data = coverage_mask.data();
let clip_data: Option<&[u8]> = match clip_region {
Some(ClipRegion::Mask(m)) => Some(m.data()),
_ => None,
};
let (mut bx0, mut by0, mut bx1, mut by1) =
path_device_bbox(&skia_path, transform, out_w, out_h);
if let Some(ClipRegion::Rect(r)) = clip_region {
bx0 = bx0.max(r.x0 as usize);
by0 = by0.max(r.y0 as usize);
bx1 = bx1.min(r.x1 as usize);
by1 = by1.min(r.y1 as usize);
}
let stride = out_w as usize;
for y in by0..by1 {
for x in bx0..bx1 {
let mi = y * stride + x;
let mut cov = cov_data[mi] as f32 / 255.0;
if let Some(clip) = clip_data {
cov *= clip[mi] as f32 / 255.0;
}
if cov > 0.0 {
let ci = mi * 4;
cmyk_buf[ci] = src_c as f32;
cmyk_buf[ci + 1] = src_m as f32;
cmyk_buf[ci + 2] = src_y as f32;
cmyk_buf[ci + 3] = src_k as f32;
if has_spot_contrib {
spot_mask[mi] = 1;
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_overprint_stroke(
pixmap: &mut Pixmap,
cmyk_buf: &mut [f32],
op_bg: &mut [u8],
op_touched: &mut [u8],
spot_mask: &[u8],
band_state: &mut BandState,
skia_path: &stet_tiny_skia::Path,
stroke: &Stroke,
transform: Transform,
params: &StrokeParams,
out_w: u32,
out_h: u32,
icc: Option<&IccCache>,
no_aa: bool,
) {
let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
.sqrt()
.max(1.0);
let dashed_op;
let stroke_src = if let Some(ref dash) = stroke.dash {
dashed_op = skia_path.dash(dash, resolution_scale);
match dashed_op.as_ref() {
Some(p) => p,
None => skia_path,
}
} else {
skia_path
};
let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
return;
};
let Some(stroked) = stroked_user.transform(transform) else {
return;
};
let mut coverage_mask = match Mask::new(out_w, out_h) {
Some(m) => m,
None => return,
};
coverage_mask.fill_path(
&stroked,
SkiaFillRule::Winding,
!no_aa,
Transform::identity(),
);
let (bbox_x0, bbox_y0, bbox_x1, bbox_y1) =
path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
let clip_coverage: Option<&[u8]> = match &band_state.clip_region {
None => None,
Some(ClipRegion::Rect(r)) => {
let data = coverage_mask.data_mut();
let stride = out_w as usize;
for y in bbox_y0..bbox_y1 {
let row_start = y * stride;
for x in bbox_x0..bbox_x1 {
let yu = y as u32;
let xu = x as u32;
if yu < r.y0 || yu >= r.y1 || xu < r.x0 || xu >= r.x1 {
data[row_start + x] = 0;
}
}
}
None
}
Some(ClipRegion::Mask(clip_mask)) => Some(clip_mask.data()),
};
let (src_c, src_m, src_y, src_k) = params.color.native_cmyk.unwrap_or_else(|| {
let r = params.color.r;
let g = params.color.g;
let b = params.color.b;
(1.0 - r, 1.0 - g, 1.0 - b, 0.0)
});
let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
let mut channels = params.painted_channels;
if channels == 0 {
channels = stet_graphics::device::CMYK_ALL;
}
if params.overprint_mode == 1
&& channels == stet_graphics::device::CMYK_ALL
&& params.is_device_cmyk
{
channels = 0;
if src_c != 0.0 {
channels |= stet_graphics::device::CMYK_C;
}
if src_m != 0.0 {
channels |= stet_graphics::device::CMYK_M;
}
if src_y != 0.0 {
channels |= stet_graphics::device::CMYK_Y;
}
if src_k != 0.0 {
channels |= stet_graphics::device::CMYK_K;
}
if channels == 0 && !params.opm_paired {
channels = stet_graphics::device::CMYK_ALL;
}
}
let is_k_only_cmyk = params.is_device_cmyk
&& params.overprint_mode == 0
&& src_c == 0.0
&& src_m == 0.0
&& src_y == 0.0;
if channels == stet_graphics::device::CMYK_ALL && !is_custom_spot && !is_k_only_cmyk {
let cov_data = coverage_mask.data();
let stride = out_w as usize;
for y in bbox_y0..bbox_y1 {
for x in bbox_x0..bbox_x1 {
let mi = y * stride + x;
let mut cov = cov_data[mi] as f32 / 255.0;
if let Some(clip) = clip_coverage {
cov *= clip[mi] as f32 / 255.0;
}
if cov > 0.0 {
let ci = mi * 4;
cmyk_buf[ci] = src_c as f32;
cmyk_buf[ci + 1] = src_m as f32;
cmyk_buf[ci + 2] = src_y as f32;
cmyk_buf[ci + 3] = src_k as f32;
}
}
}
let mut temp_mask = None;
let Some(mask_ref) =
resolve_clip_mask(&band_state.clip_region, &mut temp_mask, out_w, out_h)
else {
return;
};
let paint = to_paint_alpha(¶ms.color, params.alpha, params.blend_mode, no_aa);
pixmap.stroke_path(skia_path, &paint, stroke, transform, mask_ref);
return;
}
let cov_data = coverage_mask.data();
let stride = out_w as usize;
let px_data = pixmap.data_mut();
let px_stride = out_w as usize * 4;
for y in bbox_y0..bbox_y1 {
for x in bbox_x0..bbox_x1 {
let mi = y * stride + x;
let mut cov = cov_data[mi] as f32 / 255.0;
if let Some(clip) = clip_coverage {
cov *= clip[mi] as f32 / 255.0;
}
if cov <= 0.0 {
continue;
}
let ci = mi * 4;
let pi = y * px_stride + x * 4;
if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
op_bg[pi] = px_data[pi];
op_bg[pi + 1] = px_data[pi + 1];
op_bg[pi + 2] = px_data[pi + 2];
op_bg[pi + 3] = px_data[pi + 3];
op_touched[mi] = 1;
}
let cur_c = cmyk_buf[ci] as f64;
let cur_m = cmyk_buf[ci + 1] as f64;
let cur_y = cmyk_buf[ci + 2] as f64;
let cur_k = cmyk_buf[ci + 3] as f64;
let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
let pixmap_has_colour = px_data[pi + 3] > 0
&& (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
let is_promoted_gray = params.painted_channels == stet_graphics::device::CMYK_K
&& channels == stet_graphics::device::CMYK_K
&& params.is_device_cmyk
&& src_c == 0.0
&& src_m == 0.0
&& src_y == 0.0;
let effective_channels = if is_promoted_gray && spot_mask[mi] == 0 {
stet_graphics::device::CMYK_ALL
} else {
channels
};
let new_c = if effective_channels & stet_graphics::device::CMYK_C != 0 {
src_c
} else {
cur_c
};
let new_m = if effective_channels & stet_graphics::device::CMYK_M != 0 {
src_m
} else {
cur_m
};
let new_y = if effective_channels & stet_graphics::device::CMYK_Y != 0 {
src_y
} else {
cur_y
};
let new_k = if effective_channels & stet_graphics::device::CMYK_K != 0 {
src_k
} else {
cur_k
};
if !is_custom_spot {
cmyk_buf[ci] = new_c as f32;
cmyk_buf[ci + 1] = new_m as f32;
cmyk_buf[ci + 2] = new_y as f32;
cmyk_buf[ci + 3] = new_k as f32;
}
let delta = (new_c - cur_c)
.abs()
.max((new_m - cur_m).abs())
.max((new_y - cur_y).abs())
.max((new_k - cur_k).abs());
if delta < 1e-4 && spot_mask[mi] != 0 && pixmap_has_colour && !is_custom_spot {
continue;
}
let (r, g, b) =
if is_promoted_gray && effective_channels == stet_graphics::device::CMYK_ALL {
(params.color.r, params.color.g, params.color.b)
} else if use_multiplicative {
let bg_r = px_data[pi] as f64 / 255.0;
let bg_g = px_data[pi + 1] as f64 / 255.0;
let bg_b = px_data[pi + 2] as f64 / 255.0;
let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
1.0 - src_c
} else {
1.0
};
let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
1.0 - src_m
} else {
1.0
};
let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
1.0 - src_y
} else {
1.0
};
let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
1.0 - src_k
} else {
1.0
};
(
(bg_r * over_r * k_fac).clamp(0.0, 1.0),
(bg_g * over_g * k_fac).clamp(0.0, 1.0),
(bg_b * over_b * k_fac).clamp(0.0, 1.0),
)
} else if let Some(icc_cache) = icc {
icc_cache
.convert_cmyk_readonly(new_c, new_m, new_y, new_k)
.unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
} else {
cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
};
let a = (cov * params.alpha as f32).min(1.0);
let (bk_r, bk_g, bk_b, bk_a) = if op_touched[mi] != 0 {
let new_r = (r as f32 * 255.0).clamp(0.0, 255.0);
let new_g = (g as f32 * 255.0).clamp(0.0, 255.0);
let new_b = (b as f32 * 255.0).clamp(0.0, 255.0);
let dr = (op_bg[pi] as f32 - new_r).abs();
let dg = (op_bg[pi + 1] as f32 - new_g).abs();
let db = (op_bg[pi + 2] as f32 - new_b).abs();
if dr.max(dg).max(db) <= 4.0 {
(op_bg[pi], op_bg[pi + 1], op_bg[pi + 2], op_bg[pi + 3])
} else {
(
px_data[pi],
px_data[pi + 1],
px_data[pi + 2],
px_data[pi + 3],
)
}
} else {
(
px_data[pi],
px_data[pi + 1],
px_data[pi + 2],
px_data[pi + 3],
)
};
let dst_a = bk_a as f32 / 255.0;
let one_minus_a = 1.0 - a;
let out_a = a + dst_a * one_minus_a;
if out_a > 0.0 {
px_data[pi] = ((r as f32 * a + (bk_r as f32 / 255.0) * one_minus_a) * 255.0)
.clamp(0.0, 255.0)
.round() as u8;
px_data[pi + 1] = ((g as f32 * a + (bk_g as f32 / 255.0) * one_minus_a) * 255.0)
.clamp(0.0, 255.0)
.round() as u8;
px_data[pi + 2] = ((b as f32 * a + (bk_b as f32 / 255.0) * one_minus_a) * 255.0)
.clamp(0.0, 255.0)
.round() as u8;
px_data[pi + 3] = (out_a * 255.0).round() as u8;
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn update_cmyk_buffer_for_stroke(
cmyk_buf: &mut [f32],
spot_mask: &mut [u8],
path: &PsPath,
params: &StrokeParams,
stroke: &Stroke,
transform: Transform,
out_w: u32,
out_h: u32,
clip_region: &Option<ClipRegion>,
no_aa: bool,
icc: Option<&IccCache>,
) {
let is_custom_spot = params.painted_channels == 0 && !params.is_device_cmyk;
let has_spot_contrib = (is_custom_spot && params.color.native_cmyk.is_some())
|| matches!(
(params.color.native_cmyk, params.color.process_cmyk),
(Some(nat), Some(proc_))
if (nat.0 - proc_.0).abs() > 1e-6
|| (nat.1 - proc_.1).abs() > 1e-6
|| (nat.2 - proc_.2).abs() > 1e-6
|| (nat.3 - proc_.3).abs() > 1e-6
);
let (src_c, src_m, src_y, src_k) = if is_custom_spot {
(0.0, 0.0, 0.0, 0.0)
} else if let Some(c) = params.color.process_cmyk {
c
} else if let Some(c) = params.color.native_cmyk {
c
} else if let Some(cmyk) = icc.and_then(|i| {
i.convert_rgb_to_cmyk_readonly(params.color.r, params.color.g, params.color.b)
}) {
(cmyk[0], cmyk[1], cmyk[2], cmyk[3])
} else {
(
(1.0 - params.color.r).clamp(0.0, 1.0),
(1.0 - params.color.g).clamp(0.0, 1.0),
(1.0 - params.color.b).clamp(0.0, 1.0),
0.0,
)
};
let Some(skia_path) = build_skia_path(path) else {
return;
};
let resolution_scale = (transform.sx * transform.sx + transform.sy * transform.sy)
.sqrt()
.max(1.0);
let dashed_op;
let stroke_src = if let Some(ref dash) = stroke.dash {
dashed_op = skia_path.dash(dash, resolution_scale);
match dashed_op.as_ref() {
Some(p) => p,
None => &skia_path,
}
} else {
&skia_path
};
let Some(stroked_user) = stroke_src.stroke(stroke, resolution_scale) else {
return;
};
let Some(stroked) = stroked_user.transform(transform) else {
return;
};
let mut coverage_mask = match Mask::new(out_w, out_h) {
Some(m) => m,
None => return,
};
coverage_mask.fill_path(
&stroked,
SkiaFillRule::Winding,
!no_aa,
Transform::identity(),
);
let cov_data = coverage_mask.data();
let clip_data: Option<&[u8]> = match clip_region {
Some(ClipRegion::Mask(m)) => Some(m.data()),
_ => None,
};
let (mut bx0, mut by0, mut bx1, mut by1) =
path_device_bbox(&stroked, Transform::identity(), out_w, out_h);
if let Some(ClipRegion::Rect(r)) = clip_region {
bx0 = bx0.max(r.x0 as usize);
by0 = by0.max(r.y0 as usize);
bx1 = bx1.min(r.x1 as usize);
by1 = by1.min(r.y1 as usize);
}
let stride = out_w as usize;
for y in by0..by1 {
for x in bx0..bx1 {
let mi = y * stride + x;
let mut cov = cov_data[mi] as f32 / 255.0;
if let Some(clip) = clip_data {
cov *= clip[mi] as f32 / 255.0;
}
if cov > 0.0 {
let ci = mi * 4;
cmyk_buf[ci] = src_c as f32;
cmyk_buf[ci + 1] = src_m as f32;
cmyk_buf[ci + 2] = src_y as f32;
cmyk_buf[ci + 3] = src_k as f32;
if has_spot_contrib {
spot_mask[mi] = 1;
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_overprint_image(
pixmap: &mut Pixmap,
cmyk_buf: &mut [f32],
op_bg: &mut [u8],
op_touched: &mut [u8],
band_state: &mut BandState,
sample_data: &[u8],
params: &ImageParams,
vp_x: f32,
vp_y: f32,
scale_x: f32,
scale_y: f32,
out_w: u32,
out_h: u32,
icc: Option<&IccCache>,
) {
let iw = params.width as usize;
let ih = params.height as usize;
let Some(image_inv) = params.image_matrix.invert() else {
return;
};
let combined = params.ctm.concat(&image_inv);
let Some(inv_combined) = combined.invert() else {
return;
};
let px_data = pixmap.data_mut();
let stride = out_w as usize;
let inv_sx = 1.0 / scale_x as f64;
let inv_sy = 1.0 / scale_y as f64;
let clip_data: Option<&[u8]> = match &band_state.clip_region {
Some(ClipRegion::Mask(m)) => Some(m.data()),
_ => None,
};
let clip_rect = match &band_state.clip_region {
Some(ClipRegion::Rect(r)) => Some(*r),
_ => None,
};
let mask_info = if let ImageColorSpace::Mask { color, polarity } = ¶ms.color_space {
let (src_c, src_m, src_y, src_k) = color.native_cmyk.unwrap_or_else(|| {
let r = color.r;
let g = color.g;
let b = color.b;
(1.0 - r, 1.0 - g, 1.0 - b, 0.0)
});
Some((src_c, src_m, src_y, src_k, *polarity, iw.div_ceil(8)))
} else {
None
};
for by in 0..out_h as usize {
for bx in 0..out_w as usize {
if let Some(ref r) = clip_rect
&& ((by as u32) < r.y0
|| (by as u32) >= r.y1
|| (bx as u32) < r.x0
|| (bx as u32) >= r.x1)
{
continue;
}
if let Some(clip) = clip_data {
let ci_clip = by * stride + bx;
if clip[ci_clip] == 0 {
let bh = out_h as usize;
let has_neighbor = (bx > 0 && clip[ci_clip - 1] != 0)
|| (bx + 1 < stride && clip[ci_clip + 1] != 0)
|| (by > 0 && clip[ci_clip - stride] != 0)
|| (by + 1 < bh && clip[ci_clip + stride] != 0)
|| (bx > 0 && by > 0 && clip[ci_clip - stride - 1] != 0)
|| (bx + 1 < stride && by > 0 && clip[ci_clip - stride + 1] != 0)
|| (bx > 0 && by + 1 < bh && clip[ci_clip + stride - 1] != 0)
|| (bx + 1 < stride && by + 1 < bh && clip[ci_clip + stride + 1] != 0);
if !has_neighbor {
continue;
}
}
}
let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
let col = ix.floor() as i64;
let row = iy.floor() as i64;
if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
continue;
}
let col = col as usize;
let row = row as usize;
let (src_c, src_m, src_y, src_k) =
if let Some((mc, mm, my, mk, polarity, bytes_per_row)) = mask_info {
let byte_idx = row * bytes_per_row + col / 8;
let bit_offset = 7 - (col % 8);
let bit = if byte_idx < sample_data.len() {
(sample_data[byte_idx] >> bit_offset) & 1
} else {
0
};
let paint = if polarity { bit == 1 } else { bit == 0 };
if !paint {
continue;
}
(mc, mm, my, mk)
} else if let Some(cmyk) =
sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
{
cmyk
} else {
continue;
};
let mi = by * stride + bx;
let ci = mi * 4;
let pi = mi * 4;
if image_cs_has_spot_tint_transform(¶ms.color_space) {
let cur_c = cmyk_buf[ci] as f64;
let cur_m = cmyk_buf[ci + 1] as f64;
let cur_y = cmyk_buf[ci + 2] as f64;
let cur_k = cmyk_buf[ci + 3] as f64;
let cur_is_zero = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
let named = params.painted_channels;
let opm1 = params.overprint_mode == 1;
let (new_c, new_m, new_y, new_k) = if cur_is_zero {
(src_c, src_m, src_y, src_k)
} else {
let nc =
if named & stet_graphics::device::CMYK_C != 0 && !(opm1 && src_c == 0.0) {
src_c
} else {
cur_c
};
let nm =
if named & stet_graphics::device::CMYK_M != 0 && !(opm1 && src_m == 0.0) {
src_m
} else {
cur_m
};
let ny =
if named & stet_graphics::device::CMYK_Y != 0 && !(opm1 && src_y == 0.0) {
src_y
} else {
cur_y
};
let nk =
if named & stet_graphics::device::CMYK_K != 0 && !(opm1 && src_k == 0.0) {
src_k
} else {
cur_k
};
(nc, nm, ny, nk)
};
cmyk_buf[ci] = new_c as f32;
cmyk_buf[ci + 1] = new_m as f32;
cmyk_buf[ci + 2] = new_y as f32;
cmyk_buf[ci + 3] = new_k as f32;
let (r, g, b) = if let Some(icc_cache) = icc {
icc_cache
.convert_cmyk_readonly(new_c, new_m, new_y, new_k)
.unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
} else {
cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
};
if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
op_bg[pi] = px_data[pi];
op_bg[pi + 1] = px_data[pi + 1];
op_bg[pi + 2] = px_data[pi + 2];
op_bg[pi + 3] = px_data[pi + 3];
op_touched[mi] = 1;
}
px_data[pi] = (r * 255.0).round() as u8;
px_data[pi + 1] = (g * 255.0).round() as u8;
px_data[pi + 2] = (b * 255.0).round() as u8;
px_data[pi + 3] = 255;
continue;
}
let mut channels = params.painted_channels;
if channels == 0 {
channels = stet_graphics::device::CMYK_ALL;
}
let is_direct_cmyk = matches!(
¶ms.color_space,
ImageColorSpace::DeviceCMYK
| ImageColorSpace::ICCBased { n: 4, .. }
| ImageColorSpace::Mask { .. }
);
let is_custom_spot = params.painted_channels == 0
&& !is_cmyk_color_space(¶ms.color_space)
&& match ¶ms.color_space {
ImageColorSpace::Mask { color, .. } => color.native_cmyk.is_some(),
_ => true,
};
if params.overprint_mode == 1
&& channels == stet_graphics::device::CMYK_ALL
&& is_direct_cmyk
{
channels = 0;
if src_c != 0.0 {
channels |= stet_graphics::device::CMYK_C;
}
if src_m != 0.0 {
channels |= stet_graphics::device::CMYK_M;
}
if src_y != 0.0 {
channels |= stet_graphics::device::CMYK_Y;
}
if src_k != 0.0 {
channels |= stet_graphics::device::CMYK_K;
}
}
let cur_c = cmyk_buf[ci] as f64;
let cur_m = cmyk_buf[ci + 1] as f64;
let cur_y = cmyk_buf[ci + 2] as f64;
let cur_k = cmyk_buf[ci + 3] as f64;
let cur_is_clean = cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
let pixmap_has_colour = px_data[pi + 3] > 0
&& (px_data[pi] < 250 || px_data[pi + 1] < 250 || px_data[pi + 2] < 250);
let use_multiplicative = (is_custom_spot || cur_is_clean) && pixmap_has_colour;
let new_c = if channels & stet_graphics::device::CMYK_C != 0 {
src_c
} else {
cur_c
};
let new_m = if channels & stet_graphics::device::CMYK_M != 0 {
src_m
} else {
cur_m
};
let new_y = if channels & stet_graphics::device::CMYK_Y != 0 {
src_y
} else {
cur_y
};
let new_k = if channels & stet_graphics::device::CMYK_K != 0 {
src_k
} else {
cur_k
};
if !is_custom_spot {
cmyk_buf[ci] = new_c as f32;
cmyk_buf[ci + 1] = new_m as f32;
cmyk_buf[ci + 2] = new_y as f32;
cmyk_buf[ci + 3] = new_k as f32;
}
let (r, g, b) = if use_multiplicative {
let bg_r = px_data[pi] as f64 / 255.0;
let bg_g = px_data[pi + 1] as f64 / 255.0;
let bg_b = px_data[pi + 2] as f64 / 255.0;
let over_r = if channels & stet_graphics::device::CMYK_C != 0 {
1.0 - src_c
} else {
1.0
};
let over_g = if channels & stet_graphics::device::CMYK_M != 0 {
1.0 - src_m
} else {
1.0
};
let over_b = if channels & stet_graphics::device::CMYK_Y != 0 {
1.0 - src_y
} else {
1.0
};
let k_fac = if channels & stet_graphics::device::CMYK_K != 0 {
1.0 - src_k
} else {
1.0
};
(
(bg_r * over_r * k_fac).clamp(0.0, 1.0),
(bg_g * over_g * k_fac).clamp(0.0, 1.0),
(bg_b * over_b * k_fac).clamp(0.0, 1.0),
)
} else if let Some(icc_cache) = icc {
icc_cache
.convert_cmyk_readonly(new_c, new_m, new_y, new_k)
.unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
} else {
cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
};
if op_touched[mi] == 0 && px_data[pi + 3] > 0 {
op_bg[pi] = px_data[pi];
op_bg[pi + 1] = px_data[pi + 1];
op_bg[pi + 2] = px_data[pi + 2];
op_bg[pi + 3] = px_data[pi + 3];
op_touched[mi] = 1;
}
px_data[pi] = (r * 255.0).round() as u8;
px_data[pi + 1] = (g * 255.0).round() as u8;
px_data[pi + 2] = (b * 255.0).round() as u8;
px_data[pi + 3] = 255;
}
}
}
#[allow(clippy::too_many_arguments)]
fn update_cmyk_buffer_for_image(
cmyk_buf: &mut [f32],
sample_data: &[u8],
pixmap_rgba: &[u8],
params: &ImageParams,
vp_x: f32,
vp_y: f32,
scale_x: f32,
scale_y: f32,
out_w: u32,
out_h: u32,
clip_region: &Option<ClipRegion>,
icc: Option<&IccCache>,
) {
let iw = params.width as usize;
let ih = params.height as usize;
let Some(image_inv) = params.image_matrix.invert() else {
return;
};
let combined = params.ctm.concat(&image_inv);
let Some(inv_combined) = combined.invert() else {
return;
};
let stride = out_w as usize;
let inv_sx = 1.0 / scale_x as f64;
let inv_sy = 1.0 / scale_y as f64;
let mask_info = if let ImageColorSpace::Mask { color, polarity } = ¶ms.color_space {
let Some((c, m, y, k)) = color.native_cmyk else {
return;
};
Some((
c as f32,
m as f32,
y as f32,
k as f32,
*polarity,
iw.div_ceil(8),
))
} else {
None
};
let clip_data: Option<&[u8]> = match clip_region {
Some(ClipRegion::Mask(m)) => Some(m.data()),
_ => None,
};
let clip_rect = match clip_region {
Some(ClipRegion::Rect(r)) => Some(*r),
_ => None,
};
for by in 0..out_h as usize {
for bx in 0..out_w as usize {
if let Some(ref r) = clip_rect
&& ((by as u32) < r.y0
|| (by as u32) >= r.y1
|| (bx as u32) < r.x0
|| (bx as u32) >= r.x1)
{
continue;
}
if let Some(clip) = clip_data
&& clip[by * stride + bx] == 0
{
continue;
}
let dx = (bx as f64 + 0.5) * inv_sx + vp_x as f64;
let dy = (by as f64 + 0.5) * inv_sy + vp_y as f64;
let ix = inv_combined.a * dx + inv_combined.c * dy + inv_combined.tx;
let iy = inv_combined.b * dx + inv_combined.d * dy + inv_combined.ty;
let col = ix.floor() as i64;
let row = iy.floor() as i64;
if col < 0 || col >= iw as i64 || row < 0 || row >= ih as i64 {
continue;
}
let col = col as usize;
let row = row as usize;
let ci = (by * stride + bx) * 4;
if let Some((sc, sm, sy, sk, polarity, bytes_per_row)) = mask_info {
let byte_idx = row * bytes_per_row + col / 8;
let bit_offset = 7 - (col % 8);
let bit = if byte_idx < sample_data.len() {
(sample_data[byte_idx] >> bit_offset) & 1
} else {
0
};
let paint = if polarity { bit == 1 } else { bit == 0 };
if paint {
cmyk_buf[ci] = sc;
cmyk_buf[ci + 1] = sm;
cmyk_buf[ci + 2] = sy;
cmyk_buf[ci + 3] = sk;
}
} else if let Some((sc, sm, sy, sk)) =
sample_pixel_cmyk(sample_data, ¶ms.color_space, iw, row, col)
{
cmyk_buf[ci] = sc as f32;
cmyk_buf[ci + 1] = sm as f32;
cmyk_buf[ci + 2] = sy as f32;
cmyk_buf[ci + 3] = sk as f32;
} else if ci + 3 < pixmap_rgba.len() && pixmap_rgba[ci + 3] > 0 {
let r = pixmap_rgba[ci] as f64 / 255.0;
let g = pixmap_rgba[ci + 1] as f64 / 255.0;
let b = pixmap_rgba[ci + 2] as f64 / 255.0;
let cmyk =
if let Some(c) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
c
} else {
[
(1.0 - r).clamp(0.0, 1.0),
(1.0 - g).clamp(0.0, 1.0),
(1.0 - b).clamp(0.0, 1.0),
0.0,
]
};
cmyk_buf[ci] = cmyk[0] as f32;
cmyk_buf[ci + 1] = cmyk[1] as f32;
cmyk_buf[ci + 2] = cmyk[2] as f32;
cmyk_buf[ci + 3] = cmyk[3] as f32;
}
}
}
}
fn image_supports_overprint(cs: &ImageColorSpace) -> bool {
match cs {
ImageColorSpace::Mask { .. } => true,
ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => true,
ImageColorSpace::Separation { alt_space, .. }
| ImageColorSpace::DeviceN { alt_space, .. } => {
matches!(
alt_space.as_ref(),
ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
)
}
ImageColorSpace::Indexed { base, .. } => image_supports_overprint(base),
_ => false,
}
}
fn is_cmyk_color_space(cs: &ImageColorSpace) -> bool {
match cs {
ImageColorSpace::DeviceCMYK => true,
ImageColorSpace::ICCBased { n: 4, .. } => true,
ImageColorSpace::Indexed { base, .. } => is_cmyk_color_space(base),
_ => false,
}
}
fn image_cs_has_spot_tint_transform(cs: &ImageColorSpace) -> bool {
use stet_graphics::device::cmyk_channel_for_name;
match cs {
ImageColorSpace::Separation {
name, alt_space, ..
} => {
cmyk_channel_for_name(name) == 0
&& matches!(
alt_space.as_ref(),
ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
)
}
ImageColorSpace::DeviceN {
names, alt_space, ..
} => {
matches!(
alt_space.as_ref(),
ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
) && names.iter().any(|n| cmyk_channel_for_name(n) == 0)
}
ImageColorSpace::Indexed { base, .. } => image_cs_has_spot_tint_transform(base),
_ => false,
}
}
fn sample_pixel_cmyk(
sample_data: &[u8],
cs: &ImageColorSpace,
iw: usize,
row: usize,
col: usize,
) -> Option<(f64, f64, f64, f64)> {
match cs {
ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. } => {
let si = (row * iw + col) * 4;
if si + 3 < sample_data.len() {
Some((
sample_data[si] as f64 / 255.0,
sample_data[si + 1] as f64 / 255.0,
sample_data[si + 2] as f64 / 255.0,
sample_data[si + 3] as f64 / 255.0,
))
} else {
None
}
}
ImageColorSpace::Separation {
alt_space,
tint_table,
..
} => {
if !matches!(
alt_space.as_ref(),
ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
) {
return None;
}
let si = row * iw + col;
if si >= sample_data.len() {
return None;
}
let tint = sample_data[si] as f32 / 255.0;
let mut alt = [0.0f32; 4];
tint_table.lookup_1d(tint, &mut alt);
Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64))
}
ImageColorSpace::DeviceN {
alt_space,
tint_table,
..
} => {
if !matches!(
alt_space.as_ref(),
ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
) {
return None;
}
let ni = tint_table.num_inputs as usize;
let si = (row * iw + col) * ni;
if si + ni > sample_data.len() {
return None;
}
let mut inputs = vec![0.0f32; ni];
for (c, inp) in inputs.iter_mut().enumerate() {
*inp = sample_data[si + c] as f32 / 255.0;
}
let mut alt = [0.0f32; 4];
tint_table.lookup_nd(&inputs, &mut alt);
Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64))
}
ImageColorSpace::Indexed {
base,
hival,
lookup,
} => {
let pi = row * iw + col;
if pi >= sample_data.len() {
return None;
}
let idx = sample_data[pi] as usize;
let idx = idx.min(*hival as usize);
let base_ncomp = base.num_components() as usize;
let li = idx * base_ncomp;
if is_cmyk_color_space(base) && base_ncomp == 4 && li + 3 < lookup.len() {
return Some((
lookup[li] as f64 / 255.0,
lookup[li + 1] as f64 / 255.0,
lookup[li + 2] as f64 / 255.0,
lookup[li + 3] as f64 / 255.0,
));
}
if li + base_ncomp <= lookup.len() {
match base.as_ref() {
ImageColorSpace::Separation {
alt_space,
tint_table,
..
} if matches!(
alt_space.as_ref(),
ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
) =>
{
let tint = lookup[li] as f32 / 255.0;
let mut alt = [0.0f32; 4];
tint_table.lookup_1d(tint, &mut alt);
return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
}
ImageColorSpace::DeviceN {
alt_space,
tint_table,
..
} if matches!(
alt_space.as_ref(),
ImageColorSpace::DeviceCMYK | ImageColorSpace::ICCBased { n: 4, .. }
) =>
{
let ni = tint_table.num_inputs as usize;
let mut inputs = vec![0.0f32; ni];
for (c, inp) in inputs.iter_mut().enumerate() {
if c < base_ncomp {
*inp = lookup[li + c] as f32 / 255.0;
}
}
let mut alt = [0.0f32; 4];
tint_table.lookup_nd(&inputs, &mut alt);
return Some((alt[0] as f64, alt[1] as f64, alt[2] as f64, alt[3] as f64));
}
_ => {}
}
}
None
}
_ => None,
}
}
#[allow(clippy::too_many_arguments)]
fn render_banded_to_sink(
page_w: u32,
page_h: u32,
band_h: u32,
dpi: f64,
list: &DisplayList,
sink: &mut dyn stet_graphics::device::PageSink,
icc_cache: &IccCache,
no_aa: bool,
) -> Result<(), String> {
let bboxes = precompute_bboxes(list, dpi);
let epochs = build_clip_epochs(list, &bboxes);
let clip_seen = precompute_clip_seen(list);
use stet_graphics::display_list::GroupColorSpace;
let needs_cmyk_buffer = has_overprint_elements(list)
|| list.page_group_color_space() == GroupColorSpace::DeviceCMYK
|| has_cmyk_group(list);
let preprocessed_images = preprocess_images_for_bands(list, Some(icc_cache));
const BAND_OVERLAP: u32 = 6;
let render_h = band_h + 2 * BAND_OVERLAP;
sink.begin_page(page_w, page_h)?;
let num_bands = page_h.div_ceil(band_h);
let elements = list.elements();
let row_bytes = page_w as usize * 4;
let icc_ref = Some(icc_cache);
let render_band = |band_idx: u32| -> Vec<u8> {
let y_start = band_idx * band_h;
let actual_h = (page_h - y_start).min(band_h);
let render_y_start = y_start.saturating_sub(BAND_OVERLAP);
let render_y_end_f = ((y_start + actual_h + BAND_OVERLAP).min(page_h)) as f64;
let band_offset = y_start - render_y_start;
let mut band_pixmap = Pixmap::new(page_w, render_h).expect("Failed to create band pixmap");
band_pixmap.as_mut().data_mut().fill(0x00);
let cmyk_buf = if needs_cmyk_buffer {
Some(vec![0.0f32; page_w as usize * render_h as usize * 4])
} else {
None
};
let mut band_state = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: clip_seen.clone(),
mask_pool: Vec::new(),
cmyk_buffer: cmyk_buf,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
for epoch in &epochs {
if !epoch.has_erase_page {
match epoch.paint_bbox {
Some(ref pb)
if pb.y_max <= render_y_start as f64 || pb.y_min >= render_y_end_f =>
{
continue;
}
None => continue,
_ => {}
}
}
for i in epoch.start_idx..epoch.end_idx {
let force_process = matches!(
&elements[i],
DisplayElement::OcgGroup { elements: inner, .. }
if contains_clip_op(inner)
);
if !force_process
&& let Some(ref bbox) = bboxes[i]
&& (bbox.y_max <= render_y_start as f64 || bbox.y_min >= render_y_end_f)
{
continue;
}
let ctx = RenderContext {
vp_x: 0.0,
vp_y: render_y_start as f32,
scale_x: 1.0,
scale_y: 1.0,
out_w: page_w,
out_h: render_h,
effective_dpi: dpi,
icc: icc_ref,
image_cache: None,
preprocessed: Some(&preprocessed_images),
elem_idx: i,
no_aa,
opm_zero_transparent: false,
knockout_painter_pass: KnockoutPainterPass::None,
parent_group_isolated: false,
alpha_extraction_pass: false,
};
render_element(&mut band_pixmap, &mut band_state, &elements[i], &ctx);
}
}
composite_onto_white(band_pixmap.data_mut());
let start_byte = band_offset as usize * row_bytes;
let total_bytes = actual_h as usize * row_bytes;
band_pixmap.data()[start_byte..start_byte + total_bytes].to_vec()
};
#[cfg(feature = "parallel")]
{
let chunk_size = rayon::current_num_threads().max(1);
for chunk_start in (0..num_bands).step_by(chunk_size) {
let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
.into_par_iter()
.map(&render_band)
.collect();
for (i, band_data) in rendered.iter().enumerate() {
let band_idx = chunk_start + i as u32;
let y_start = band_idx * band_h;
let actual_h = (page_h - y_start).min(band_h);
sink.write_rows(band_data, actual_h)?;
}
}
}
#[cfg(not(feature = "parallel"))]
{
for band_idx in 0..num_bands {
let band_data = render_band(band_idx);
let y_start = band_idx * band_h;
let actual_h = (page_h - y_start).min(band_h);
sink.write_rows(&band_data, actual_h)?;
}
}
sink.end_page()
}
#[derive(Clone, Copy)]
struct BBox2D {
x_min: f64,
y_min: f64,
x_max: f64,
y_max: f64,
}
fn precompute_full_bboxes(list: &DisplayList, dpi: f64) -> Vec<Option<BBox2D>> {
list.elements()
.iter()
.map(|elem| match elem {
DisplayElement::Fill { path, params } => fill_device_full_bbox(path, ¶ms.ctm),
DisplayElement::Stroke { path, params } => {
path_full_bbox(path).map(|mut bbox| {
let effective_lw = params.line_width.max(hairline_min_width(¶ms.ctm, dpi));
let expand = effective_lw * params.miter_limit * 0.5;
let m = ¶ms.ctm;
let is_identity = m.a == 1.0
&& m.b == 0.0
&& m.c == 0.0
&& m.d == 1.0
&& m.tx == 0.0
&& m.ty == 0.0;
if is_identity {
bbox.x_min -= expand;
bbox.x_max += expand;
bbox.y_min -= expand;
bbox.y_max += expand;
} else {
let col_x_len = (m.a * m.a + m.b * m.b).sqrt().max(1.0);
let col_y_len = (m.c * m.c + m.d * m.d).sqrt().max(1.0);
let expand_x = effective_lw * col_x_len * params.miter_limit * 0.5;
let expand_y = effective_lw * col_y_len * params.miter_limit * 0.5;
bbox.x_min -= expand_x;
bbox.x_max += expand_x;
bbox.y_min -= expand_y;
bbox.y_max += expand_y;
let corners = [
(
m.a * bbox.x_min + m.c * bbox.y_min + m.tx,
m.b * bbox.x_min + m.d * bbox.y_min + m.ty,
),
(
m.a * bbox.x_max + m.c * bbox.y_min + m.tx,
m.b * bbox.x_max + m.d * bbox.y_min + m.ty,
),
(
m.a * bbox.x_min + m.c * bbox.y_max + m.tx,
m.b * bbox.x_min + m.d * bbox.y_max + m.ty,
),
(
m.a * bbox.x_max + m.c * bbox.y_max + m.tx,
m.b * bbox.x_max + m.d * bbox.y_max + m.ty,
),
];
bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
bbox.x_max = corners
.iter()
.map(|c| c.0)
.fold(f64::NEG_INFINITY, f64::max);
bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
bbox.y_max = corners
.iter()
.map(|c| c.1)
.fold(f64::NEG_INFINITY, f64::max);
}
bbox
})
}
DisplayElement::Image { params, .. } => image_full_bbox(params),
DisplayElement::AxialShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
DisplayElement::RadialShading { params } => {
shading_full_bbox(¶ms.bbox, ¶ms.ctm)
}
DisplayElement::MeshShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
DisplayElement::PatchShading { params } => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
DisplayElement::PatternFill { params } => pattern_fill_full_bbox(params),
DisplayElement::Group { params, .. } => Some(BBox2D {
x_min: params.bbox[0],
y_min: params.bbox[1],
x_max: params.bbox[2],
y_max: params.bbox[3],
}),
DisplayElement::SoftMasked { params, .. } => Some(BBox2D {
x_min: params.bbox[0],
y_min: params.bbox[1],
x_max: params.bbox[2],
y_max: params.bbox[3],
}),
DisplayElement::OcgGroup {
elements,
default_visible,
..
} => {
if !*default_visible && !contains_clip_op(elements) {
return None;
}
let child_bboxes = precompute_full_bboxes(elements, dpi);
let mut x_min = f64::INFINITY;
let mut y_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_max = f64::NEG_INFINITY;
for cb in child_bboxes.into_iter().flatten() {
x_min = x_min.min(cb.x_min);
y_min = y_min.min(cb.y_min);
x_max = x_max.max(cb.x_max);
y_max = y_max.max(cb.y_max);
}
if x_min <= x_max && y_min <= y_max {
Some(BBox2D {
x_min,
y_min,
x_max,
y_max,
})
} else {
None
}
}
_ => None, })
.collect()
}
fn clip_path_bbox(path: &PsPath, params: &ClipParams) -> Option<BBox2D> {
let mut bbox = path_full_bbox(path)?;
let ctm = ¶ms.ctm;
let is_identity = ctm.a == 1.0
&& ctm.b == 0.0
&& ctm.c == 0.0
&& ctm.d == 1.0
&& ctm.tx == 0.0
&& ctm.ty == 0.0;
if !is_identity {
let corners = [
ctm.transform_point(bbox.x_min, bbox.y_min),
ctm.transform_point(bbox.x_max, bbox.y_min),
ctm.transform_point(bbox.x_min, bbox.y_max),
ctm.transform_point(bbox.x_max, bbox.y_max),
];
bbox.x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
bbox.x_max = corners
.iter()
.map(|c| c.0)
.fold(f64::NEG_INFINITY, f64::max);
bbox.y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
bbox.y_max = corners
.iter()
.map(|c| c.1)
.fold(f64::NEG_INFINITY, f64::max);
}
if let Some(sp) = ¶ms.stroke_params {
let scale = (ctm.a * ctm.a + ctm.b * ctm.b)
.sqrt()
.max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt())
.max(1.0);
let expand = sp.line_width * 0.5 * scale;
bbox.x_min -= expand;
bbox.x_max += expand;
bbox.y_min -= expand;
bbox.y_max += expand;
}
Some(bbox)
}
fn intersect_bbox(a: &BBox2D, b: &BBox2D) -> Option<BBox2D> {
let x_min = a.x_min.max(b.x_min);
let y_min = a.y_min.max(b.y_min);
let x_max = a.x_max.min(b.x_max);
let y_max = a.y_max.min(b.y_max);
if x_min < x_max && y_min < y_max {
Some(BBox2D {
x_min,
y_min,
x_max,
y_max,
})
} else {
None
}
}
fn compute_paint_bounds(list: &DisplayList, _dpi: f64) -> Option<BBox2D> {
let mut clip_stack: Vec<BBox2D> = Vec::new();
let mut union: Option<BBox2D> = None;
let push_paint = |union: &mut Option<BBox2D>, clip_stack: &[BBox2D], bbox: BBox2D| {
let visible = match clip_stack.last() {
Some(clip) => match intersect_bbox(clip, &bbox) {
Some(b) => b,
None => return,
},
None => bbox,
};
*union = Some(match union.take() {
None => visible,
Some(u) => BBox2D {
x_min: u.x_min.min(visible.x_min),
y_min: u.y_min.min(visible.y_min),
x_max: u.x_max.max(visible.x_max),
y_max: u.y_max.max(visible.y_max),
},
});
};
for elem in list.elements() {
match elem {
DisplayElement::Clip { path, params } => {
if let Some(cb) = clip_path_bbox(path, params) {
let new_top = match clip_stack.last() {
Some(prev) => match intersect_bbox(prev, &cb) {
Some(b) => b,
None => BBox2D {
x_min: 0.0,
y_min: 0.0,
x_max: 0.0,
y_max: 0.0,
},
},
None => cb,
};
clip_stack.push(new_top);
}
}
DisplayElement::InitClip | DisplayElement::ErasePage => {
clip_stack.clear();
}
DisplayElement::Fill { path, .. } => {
if let Some(b) = path_full_bbox(path) {
push_paint(&mut union, &clip_stack, b);
}
}
DisplayElement::Stroke { path, params } => {
if let Some(mut b) = path_full_bbox(path) {
let expand = params.line_width * params.miter_limit * 0.5;
b.x_min -= expand;
b.x_max += expand;
b.y_min -= expand;
b.y_max += expand;
push_paint(&mut union, &clip_stack, b);
}
}
DisplayElement::Image { params, .. } => {
if let Some(b) = image_full_bbox(params) {
push_paint(&mut union, &clip_stack, b);
}
}
DisplayElement::AxialShading { params } => {
let b = match ¶ms.bbox {
Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
None => clip_stack.last().copied(),
};
if let Some(b) = b {
push_paint(&mut union, &clip_stack, b);
}
}
DisplayElement::RadialShading { params } => {
let b = match ¶ms.bbox {
Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
None => clip_stack.last().copied(),
};
if let Some(b) = b {
push_paint(&mut union, &clip_stack, b);
}
}
DisplayElement::MeshShading { params } => {
let b = match ¶ms.bbox {
Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
None => clip_stack.last().copied(),
};
if let Some(b) = b {
push_paint(&mut union, &clip_stack, b);
}
}
DisplayElement::PatchShading { params } => {
let b = match ¶ms.bbox {
Some(_) => shading_full_bbox(¶ms.bbox, ¶ms.ctm),
None => clip_stack.last().copied(),
};
if let Some(b) = b {
push_paint(&mut union, &clip_stack, b);
}
}
DisplayElement::PatternFill { params } => {
if let Some(b) = pattern_fill_full_bbox(params) {
push_paint(&mut union, &clip_stack, b);
}
}
DisplayElement::Group { params, .. } => {
push_paint(
&mut union,
&clip_stack,
BBox2D {
x_min: params.bbox[0],
y_min: params.bbox[1],
x_max: params.bbox[2],
y_max: params.bbox[3],
},
);
}
DisplayElement::SoftMasked { params, .. } => {
push_paint(
&mut union,
&clip_stack,
BBox2D {
x_min: params.bbox[0],
y_min: params.bbox[1],
x_max: params.bbox[2],
y_max: params.bbox[3],
},
);
}
DisplayElement::Text { .. } => {} DisplayElement::OcgGroup { .. } => {
}
}
}
union
}
fn fill_device_full_bbox(path: &PsPath, ctm: &Matrix) -> Option<BBox2D> {
let bbox = path_full_bbox(path)?;
let is_identity = ctm.a == 1.0
&& ctm.b == 0.0
&& ctm.c == 0.0
&& ctm.d == 1.0
&& ctm.tx == 0.0
&& ctm.ty == 0.0;
if is_identity {
return Some(bbox);
}
let corners = [
(bbox.x_min, bbox.y_min),
(bbox.x_max, bbox.y_min),
(bbox.x_min, bbox.y_max),
(bbox.x_max, bbox.y_max),
];
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for (x, y) in &corners {
let dx = ctm.a * x + ctm.c * y + ctm.tx;
let dy = ctm.b * x + ctm.d * y + ctm.ty;
x_min = x_min.min(dx);
x_max = x_max.max(dx);
y_min = y_min.min(dy);
y_max = y_max.max(dy);
}
Some(BBox2D {
x_min,
y_min,
x_max,
y_max,
})
}
fn path_full_bbox(path: &PsPath) -> Option<BBox2D> {
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for seg in &path.segments {
match seg {
PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
x_min = x_min.min(*x);
x_max = x_max.max(*x);
y_min = y_min.min(*y);
y_max = y_max.max(*y);
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => {
x_min = x_min.min(*x1).min(*x2).min(*x3);
x_max = x_max.max(*x1).max(*x2).max(*x3);
y_min = y_min.min(*y1).min(*y2).min(*y3);
y_max = y_max.max(*y1).max(*y2).max(*y3);
}
PathSegment::ClosePath => {}
}
}
if x_min <= x_max {
Some(BBox2D {
x_min,
y_min,
x_max,
y_max,
})
} else {
None
}
}
fn pattern_fill_full_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<BBox2D> {
if let Some(ref sp) = params.stroke_params {
let bbox = path_full_bbox(¶ms.path)?;
let ctm = &sp.ctm;
let corners = [
ctm.transform_point(bbox.x_min, bbox.y_min),
ctm.transform_point(bbox.x_max, bbox.y_min),
ctm.transform_point(bbox.x_min, bbox.y_max),
ctm.transform_point(bbox.x_max, bbox.y_max),
];
let mut dev_bbox = BBox2D {
x_min: f64::INFINITY,
y_min: f64::INFINITY,
x_max: f64::NEG_INFINITY,
y_max: f64::NEG_INFINITY,
};
for (x, y) in &corners {
dev_bbox.x_min = dev_bbox.x_min.min(*x);
dev_bbox.y_min = dev_bbox.y_min.min(*y);
dev_bbox.x_max = dev_bbox.x_max.max(*x);
dev_bbox.y_max = dev_bbox.y_max.max(*y);
}
let half_w = sp.line_width
* 0.5
* (ctm.a * ctm.a + ctm.b * ctm.b)
.sqrt()
.max((ctm.c * ctm.c + ctm.d * ctm.d).sqrt());
dev_bbox.x_min -= half_w;
dev_bbox.y_min -= half_w;
dev_bbox.x_max += half_w;
dev_bbox.y_max += half_w;
Some(dev_bbox)
} else {
path_full_bbox(¶ms.path)
}
}
fn pattern_fill_y_bbox(params: &stet_graphics::device::PatternFillParams) -> Option<YBBox> {
let bbox = pattern_fill_full_bbox(params)?;
Some(YBBox {
y_min: bbox.y_min,
y_max: bbox.y_max,
})
}
fn image_full_bbox(params: &ImageParams) -> Option<BBox2D> {
let m = ¶ms.ctm;
let im = ¶ms.image_matrix;
let im_inv = im.invert()?;
let combined = m.concat(&im_inv);
let w = params.width as f64;
let h = params.height as f64;
let corners = [
combined.transform_point(0.0, 0.0),
combined.transform_point(w, 0.0),
combined.transform_point(0.0, h),
combined.transform_point(w, h),
];
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for (x, y) in &corners {
x_min = x_min.min(*x);
x_max = x_max.max(*x);
y_min = y_min.min(*y);
y_max = y_max.max(*y);
}
Some(BBox2D {
x_min,
y_min,
x_max,
y_max,
})
}
fn shading_full_bbox(bbox: &Option<[f64; 4]>, ctm: &Matrix) -> Option<BBox2D> {
if let Some(bbox) = bbox {
let corners = [
ctm.transform_point(bbox[0], bbox[1]),
ctm.transform_point(bbox[2], bbox[1]),
ctm.transform_point(bbox[0], bbox[3]),
ctm.transform_point(bbox[2], bbox[3]),
];
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for (x, y) in &corners {
x_min = x_min.min(*x);
x_max = x_max.max(*x);
y_min = y_min.min(*y);
y_max = y_max.max(*y);
}
Some(BBox2D {
x_min,
y_min,
x_max,
y_max,
})
} else {
Some(BBox2D {
x_min: 0.0,
y_min: 0.0,
x_max: 1e9,
y_max: 1e9,
})
}
}
fn build_viewport_epochs(list: &DisplayList, bboxes: &[Option<BBox2D>]) -> Vec<ViewportEpoch> {
let elements = list.elements();
let mut epochs = Vec::new();
let mut epoch_start = 0;
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
let mut has_erase = false;
for (i, element) in elements.iter().enumerate() {
if matches!(element, DisplayElement::InitClip) && i > epoch_start {
epochs.push(ViewportEpoch {
start_idx: epoch_start,
end_idx: i,
paint_bbox: if x_min <= x_max {
Some(BBox2D {
x_min,
y_min,
x_max,
y_max,
})
} else {
None
},
has_erase_page: has_erase,
});
epoch_start = i;
x_min = f64::INFINITY;
x_max = f64::NEG_INFINITY;
y_min = f64::INFINITY;
y_max = f64::NEG_INFINITY;
has_erase = false;
}
if matches!(element, DisplayElement::ErasePage) {
has_erase = true;
}
if let Some(ref bbox) = bboxes[i] {
x_min = x_min.min(bbox.x_min);
x_max = x_max.max(bbox.x_max);
y_min = y_min.min(bbox.y_min);
y_max = y_max.max(bbox.y_max);
}
}
if epoch_start < elements.len() {
epochs.push(ViewportEpoch {
start_idx: epoch_start,
end_idx: elements.len(),
paint_bbox: if x_min <= x_max {
Some(BBox2D {
x_min,
y_min,
x_max,
y_max,
})
} else {
None
},
has_erase_page: has_erase,
});
}
epochs
}
struct ViewportEpoch {
start_idx: usize,
end_idx: usize,
paint_bbox: Option<BBox2D>,
has_erase_page: bool,
}
pub struct PreparedDisplayList {
bboxes: Vec<Option<BBox2D>>,
epochs: Vec<ViewportEpoch>,
clip_seen: HashSet<u64>,
}
pub fn prepare_display_list(list: &DisplayList) -> PreparedDisplayList {
let bboxes = precompute_full_bboxes(list, 72.0);
let epochs = build_viewport_epochs(list, &bboxes);
let clip_seen = precompute_clip_seen(list);
PreparedDisplayList {
bboxes,
epochs,
clip_seen,
}
}
struct PreprocessedImage {
data: Vec<u8>,
width: u32,
height: u32,
adj_sx: f32,
adj_ky: f32,
adj_kx: f32,
adj_sy: f32,
quality: stet_tiny_skia::FilterQuality,
}
pub struct ImageCache {
entries: Vec<Option<Vec<u8>>>,
}
impl ImageCache {
pub fn build(list: &DisplayList, icc: Option<&IccCache>) -> Self {
let entries = list
.elements()
.iter()
.map(|elem| {
if let DisplayElement::Image {
sample_data,
params,
} = elem
{
if params.width == 0 || params.height == 0 {
return None;
}
let mut rgba = samples_to_rgba(sample_data, params, icc, false);
if params.mask_color.is_some() {
apply_mask_color_rgba(&mut rgba, sample_data, params);
}
Some(rgba)
} else {
None
}
})
.collect();
Self { entries }
}
pub fn get(&self, index: usize) -> Option<&[u8]> {
self.entries.get(index).and_then(|e| e.as_deref())
}
}
fn preprocess_images_for_bands(
list: &DisplayList,
icc: Option<&IccCache>,
) -> Vec<Option<PreprocessedImage>> {
list.elements()
.iter()
.map(|elem| {
let DisplayElement::Image {
sample_data,
params,
} = elem
else {
return None;
};
let iw = params.width;
let ih = params.height;
if iw == 0 || ih == 0 {
return None;
}
if params.overprint {
return None;
}
let mut rgba = samples_to_rgba(sample_data, params, icc, false);
if params.mask_color.is_some() {
apply_mask_color_rgba(&mut rgba, sample_data, params);
}
let image_inv = params.image_matrix.invert()?;
let combined = params.ctm.concat(&image_inv);
let base_transform = enforce_min_image_size(to_transform(&combined), iw, ih);
let (data, width, height, adj_t) =
match prescale_image(&rgba, iw, ih, base_transform, params.interpolate) {
Some((d, w, h, t)) => {
drop(rgba); (d, w, h, t)
}
None => (rgba, iw, ih, base_transform),
};
let quality = image_filter_quality(adj_t, params.interpolate);
Some(PreprocessedImage {
data,
width,
height,
adj_sx: adj_t.sx,
adj_ky: adj_t.ky,
adj_kx: adj_t.kx,
adj_sy: adj_t.sy,
quality,
})
})
.collect()
}
#[allow(clippy::too_many_arguments)]
pub fn render_region_prepared(
list: &DisplayList,
prepared: &PreparedDisplayList,
vp_x: f64,
vp_y: f64,
vp_w: f64,
vp_h: f64,
pixel_w: u32,
pixel_h: u32,
dpi: f64,
icc: Option<&IccCache>,
image_cache: Option<&ImageCache>,
no_aa: bool,
) -> Vec<u8> {
if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
}
let scale_x = pixel_w as f64 / vp_w;
let scale_y = pixel_h as f64 / vp_h;
let effective_dpi = dpi * scale_x;
const OVERLAP: u32 = 6;
let render_h = pixel_h + 2 * OVERLAP;
let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
pixmap.fill(Color::TRANSPARENT);
let cmyk_buf = if has_overprint_elements(list)
|| list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
|| has_cmyk_group(list)
{
Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
} else {
None
};
let mut state = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: prepared.clip_seen.clone(),
mask_pool: Vec::new(),
cmyk_buffer: cmyk_buf,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
let elements = list.elements();
let vp_x_f = vp_x as f32;
let vp_y_f = vp_y as f32;
let sx = scale_x as f32;
let sy = scale_y as f32;
let vp_x_max = vp_x + vp_w;
let vp_y_max = vp_y + vp_h;
for epoch in &prepared.epochs {
if !epoch.has_erase_page {
match epoch.paint_bbox {
Some(ref pb)
if pb.x_max <= vp_x
|| pb.x_min >= vp_x_max
|| pb.y_max <= vp_y
|| pb.y_min >= vp_y_max =>
{
continue;
}
None => continue,
_ => {}
}
}
#[allow(clippy::needless_range_loop)]
for i in epoch.start_idx..epoch.end_idx {
let force_process = matches!(
&elements[i],
DisplayElement::OcgGroup { elements: inner, .. }
if contains_clip_op(inner)
);
if !force_process
&& let Some(ref bbox) = prepared.bboxes[i]
&& (bbox.x_max <= vp_x
|| bbox.x_min >= vp_x_max
|| bbox.y_max <= vp_y
|| bbox.y_min >= vp_y_max)
{
continue;
}
let ctx = RenderContext {
vp_x: vp_x_f,
vp_y: vp_y_f,
scale_x: sx,
scale_y: sy,
out_w: pixel_w,
out_h: render_h,
effective_dpi,
icc,
image_cache,
preprocessed: None,
elem_idx: i,
no_aa,
opm_zero_transparent: false,
knockout_painter_pass: KnockoutPainterPass::None,
parent_group_isolated: false,
alpha_extraction_pass: false,
};
render_element(&mut pixmap, &mut state, &elements[i], &ctx);
}
}
composite_onto_white(pixmap.data_mut());
let row_bytes = pixel_w as usize * 4;
let end = pixel_h as usize * row_bytes;
pixmap.data()[..end].to_vec()
}
pub fn viewport_band_count(pixel_w: u32, pixel_h: u32) -> (u32, u32) {
let band_h = select_band_height(pixel_w, pixel_h);
let num_bands = if band_h >= pixel_h {
1
} else {
pixel_h.div_ceil(band_h)
};
(num_bands, band_h)
}
#[allow(clippy::too_many_arguments)]
pub fn render_region_single_band(
list: &DisplayList,
prepared: &PreparedDisplayList,
vp_x: f64,
vp_y: f64,
vp_w: f64,
vp_h: f64,
pixel_w: u32,
pixel_h: u32,
band_idx: u32,
band_h: u32,
num_bands: u32,
dpi: f64,
icc: Option<&IccCache>,
image_cache: Option<&ImageCache>,
no_aa: bool,
) -> Vec<u8> {
if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
let actual_h = if band_idx < num_bands - 1 {
band_h
} else {
pixel_h - band_idx * band_h
};
return vec![0xFF; pixel_w as usize * actual_h as usize * 4];
}
let scale_x = pixel_w as f64 / vp_w;
let scale_y = pixel_h as f64 / vp_h;
let effective_dpi = dpi * scale_x;
let out_y_start = band_idx * band_h;
let actual_h = if band_idx < num_bands - 1 {
band_h
} else {
pixel_h - out_y_start
};
const OVERLAP: u32 = 6;
let render_y_start = out_y_start.saturating_sub(OVERLAP);
let render_y_end = (out_y_start + actual_h + OVERLAP).min(pixel_h);
let render_h = band_h + 2 * OVERLAP;
let overlap_top = out_y_start - render_y_start;
let src_y_min = vp_y + render_y_start as f64 / scale_y;
let src_y_max = vp_y + render_y_end as f64 / scale_y;
let band_vp_y = vp_y + render_y_start as f64 / scale_y;
let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create band pixmap");
pixmap.fill(Color::TRANSPARENT);
let cmyk_buf = if has_overprint_elements(list)
|| list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
|| has_cmyk_group(list)
{
Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
} else {
None
};
let mut state = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: prepared.clip_seen.clone(),
mask_pool: Vec::new(),
cmyk_buffer: cmyk_buf,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
let elements = list.elements();
let vp_x_f = vp_x as f32;
let band_vp_y_f = band_vp_y as f32;
let sx = scale_x as f32;
let sy = scale_y as f32;
let vp_x_max = vp_x + vp_w;
for epoch in &prepared.epochs {
if !epoch.has_erase_page {
match epoch.paint_bbox {
Some(ref pb)
if pb.x_max <= vp_x
|| pb.x_min >= vp_x_max
|| pb.y_max <= src_y_min
|| pb.y_min >= src_y_max =>
{
continue;
}
None => continue,
_ => {}
}
}
#[allow(clippy::needless_range_loop)]
for i in epoch.start_idx..epoch.end_idx {
let force_process = matches!(
&elements[i],
DisplayElement::OcgGroup { elements: inner, .. }
if contains_clip_op(inner)
);
if !force_process
&& let Some(ref bbox) = prepared.bboxes[i]
&& (bbox.x_max <= vp_x
|| bbox.x_min >= vp_x_max
|| bbox.y_max <= src_y_min
|| bbox.y_min >= src_y_max)
{
continue;
}
let ctx = RenderContext {
vp_x: vp_x_f,
vp_y: band_vp_y_f,
scale_x: sx,
scale_y: sy,
out_w: pixel_w,
out_h: render_h,
effective_dpi,
icc,
image_cache,
preprocessed: None,
elem_idx: i,
no_aa,
opm_zero_transparent: false,
knockout_painter_pass: KnockoutPainterPass::None,
parent_group_isolated: false,
alpha_extraction_pass: false,
};
render_element(&mut pixmap, &mut state, &elements[i], &ctx);
}
}
composite_onto_white(pixmap.data_mut());
let row_bytes = pixel_w as usize * 4;
let start = overlap_top as usize * row_bytes;
let end = start + actual_h as usize * row_bytes;
pixmap.data()[start..end].to_vec()
}
#[allow(clippy::too_many_arguments)]
pub fn render_region_prepared_parallel(
list: &DisplayList,
prepared: &PreparedDisplayList,
vp_x: f64,
vp_y: f64,
vp_w: f64,
vp_h: f64,
pixel_w: u32,
pixel_h: u32,
dpi: f64,
icc: Option<&IccCache>,
image_cache: Option<&ImageCache>,
no_aa: bool,
) -> Vec<u8> {
let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
if num_bands <= 1 {
return render_region_prepared(
list,
prepared,
vp_x,
vp_y,
vp_w,
vp_h,
pixel_w,
pixel_h,
dpi,
icc,
image_cache,
no_aa,
);
}
let render_band = |band_idx: u32| -> Vec<u8> {
render_region_single_band(
list,
prepared,
vp_x,
vp_y,
vp_w,
vp_h,
pixel_w,
pixel_h,
band_idx,
band_h,
num_bands,
dpi,
icc,
image_cache,
no_aa,
)
};
let row_bytes = pixel_w as usize * 4;
let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
#[cfg(feature = "parallel")]
{
let chunk_size = rayon::current_num_threads().max(1);
for chunk_start in (0..num_bands).step_by(chunk_size) {
let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
.into_par_iter()
.map(&render_band)
.collect();
for (i, band_data) in rendered.iter().enumerate() {
let band_idx = chunk_start + i as u32;
let y_start = (band_idx * band_h) as usize;
let dest_start = y_start * row_bytes;
let len = band_data.len();
result[dest_start..dest_start + len].copy_from_slice(band_data);
}
}
}
#[cfg(not(feature = "parallel"))]
{
for band_idx in 0..num_bands {
let band_data = render_band(band_idx);
let y_start = (band_idx * band_h) as usize;
let dest_start = y_start * row_bytes;
let len = band_data.len();
result[dest_start..dest_start + len].copy_from_slice(&band_data);
}
}
result
}
#[allow(clippy::too_many_arguments)]
pub fn render_region_prepared_parallel_with_progress(
list: &DisplayList,
prepared: &PreparedDisplayList,
vp_x: f64,
vp_y: f64,
vp_w: f64,
vp_h: f64,
pixel_w: u32,
pixel_h: u32,
dpi: f64,
icc: Option<&IccCache>,
image_cache: Option<&ImageCache>,
no_aa: bool,
progress: &std::sync::atomic::AtomicU32,
) -> Vec<u8> {
let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
if num_bands <= 1 {
let result = render_region_prepared(
list,
prepared,
vp_x,
vp_y,
vp_w,
vp_h,
pixel_w,
pixel_h,
dpi,
icc,
image_cache,
no_aa,
);
progress.store(1, std::sync::atomic::Ordering::Relaxed);
return result;
}
let render_band = |band_idx: u32| -> Vec<u8> {
render_region_single_band(
list,
prepared,
vp_x,
vp_y,
vp_w,
vp_h,
pixel_w,
pixel_h,
band_idx,
band_h,
num_bands,
dpi,
icc,
image_cache,
no_aa,
)
};
let row_bytes = pixel_w as usize * 4;
let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
#[cfg(feature = "parallel")]
{
let chunk_size = rayon::current_num_threads().max(1);
for chunk_start in (0..num_bands).step_by(chunk_size) {
let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
.into_par_iter()
.map(&render_band)
.collect();
for (i, band_data) in rendered.iter().enumerate() {
let band_idx = chunk_start + i as u32;
let y_start = (band_idx * band_h) as usize;
let dest_start = y_start * row_bytes;
let len = band_data.len();
result[dest_start..dest_start + len].copy_from_slice(band_data);
}
progress.store(chunk_end, std::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(not(feature = "parallel"))]
{
for band_idx in 0..num_bands {
let band_data = render_band(band_idx);
let y_start = (band_idx * band_h) as usize;
let dest_start = y_start * row_bytes;
let len = band_data.len();
result[dest_start..dest_start + len].copy_from_slice(&band_data);
progress.store(band_idx + 1, std::sync::atomic::Ordering::Relaxed);
}
}
result
}
#[allow(clippy::too_many_arguments)]
pub fn render_region_prepared_parallel_cancellable(
list: &DisplayList,
prepared: &PreparedDisplayList,
vp_x: f64,
vp_y: f64,
vp_w: f64,
vp_h: f64,
pixel_w: u32,
pixel_h: u32,
dpi: f64,
icc: Option<&IccCache>,
image_cache: Option<&ImageCache>,
no_aa: bool,
cancelled: &std::sync::atomic::AtomicBool,
) -> Option<Vec<u8>> {
if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
return None;
}
let (num_bands, band_h) = viewport_band_count(pixel_w, pixel_h);
if num_bands <= 1 {
return Some(render_region_prepared(
list,
prepared,
vp_x,
vp_y,
vp_w,
vp_h,
pixel_w,
pixel_h,
dpi,
icc,
image_cache,
no_aa,
));
}
let render_band = |band_idx: u32| -> Vec<u8> {
render_region_single_band(
list,
prepared,
vp_x,
vp_y,
vp_w,
vp_h,
pixel_w,
pixel_h,
band_idx,
band_h,
num_bands,
dpi,
icc,
image_cache,
no_aa,
)
};
let row_bytes = pixel_w as usize * 4;
let mut result = vec![0u8; pixel_w as usize * pixel_h as usize * 4];
#[cfg(feature = "parallel")]
{
let chunk_size = rayon::current_num_threads().max(1);
for chunk_start in (0..num_bands).step_by(chunk_size) {
if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
return None;
}
let chunk_end = (chunk_start + chunk_size as u32).min(num_bands);
let rendered: Vec<Vec<u8>> = (chunk_start..chunk_end)
.into_par_iter()
.map(&render_band)
.collect();
for (i, band_data) in rendered.iter().enumerate() {
let band_idx = chunk_start + i as u32;
let y_start = (band_idx * band_h) as usize;
let dest_start = y_start * row_bytes;
let len = band_data.len();
result[dest_start..dest_start + len].copy_from_slice(band_data);
}
}
}
#[cfg(not(feature = "parallel"))]
{
for band_idx in 0..num_bands {
if cancelled.load(std::sync::atomic::Ordering::Relaxed) {
return None;
}
let band_data = render_band(band_idx);
let y_start = (band_idx * band_h) as usize;
let dest_start = y_start * row_bytes;
let len = band_data.len();
result[dest_start..dest_start + len].copy_from_slice(&band_data);
}
}
Some(result)
}
pub fn render_to_rgba(
list: &DisplayList,
pixel_w: u32,
pixel_h: u32,
dpi: f64,
icc: Option<&IccCache>,
no_aa: bool,
) -> Vec<u8> {
if pixel_w == 0 || pixel_h == 0 {
return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
}
let mut icc_cache = match icc {
Some(c) => c.clone(),
None => IccCache::new(),
};
register_shading_icc_profiles(list, &mut icc_cache);
let mut sink = MemorySink {
data: Vec::new(),
width: 0,
};
let band_h = select_band_height(pixel_w, pixel_h);
if let Err(e) = render_banded_to_sink(
pixel_w, pixel_h, band_h, dpi, list, &mut sink, &icc_cache, no_aa,
) {
eprintln!("render_to_rgba: banded render failed: {e}");
return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
}
sink.data
}
pub fn render_to_rgba_viewport(
list: &DisplayList,
pixel_w: u32,
pixel_h: u32,
dpi: f64,
icc: Option<&IccCache>,
no_aa: bool,
) -> Vec<u8> {
if pixel_w == 0 || pixel_h == 0 {
return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
}
let mut icc_cache = match icc {
Some(c) => c.clone(),
None => IccCache::new(),
};
register_shading_icc_profiles(list, &mut icc_cache);
let prepared = prepare_display_list(list);
render_region_prepared_parallel(
list,
&prepared,
0.0,
0.0,
pixel_w as f64,
pixel_h as f64,
pixel_w,
pixel_h,
dpi,
Some(&icc_cache),
None,
no_aa,
)
}
fn debug_bbox_lines(list: &DisplayList, dpi: f64, depth: usize, out: &mut Vec<String>) {
let y_bboxes = precompute_bboxes(list, dpi);
let full_bboxes = precompute_full_bboxes(list, dpi);
let elements = list.elements();
let indent = " ".repeat(depth);
for (i, elem) in elements.iter().enumerate() {
let kind = match elem {
DisplayElement::Fill { .. } => "Fill",
DisplayElement::Stroke { .. } => "Stroke",
DisplayElement::Image { .. } => "Image",
DisplayElement::AxialShading { .. } => "AxialShading",
DisplayElement::RadialShading { .. } => "RadialShading",
DisplayElement::MeshShading { .. } => "MeshShading",
DisplayElement::PatchShading { .. } => "PatchShading",
DisplayElement::PatternFill { .. } => "PatternFill",
DisplayElement::Group { .. } => "Group",
DisplayElement::SoftMasked { .. } => "SoftMasked",
DisplayElement::OcgGroup { .. } => "OcgGroup",
DisplayElement::Clip { .. } => "Clip",
DisplayElement::InitClip => "InitClip",
DisplayElement::ErasePage => "ErasePage",
DisplayElement::Text { .. } => "Text",
};
let yb = &y_bboxes[i];
let fb = &full_bboxes[i];
let mut diff = false;
if yb.is_some() != fb.is_some() {
diff = true;
}
if let (Some(yb), Some(fb)) = (yb, fb)
&& ((yb.y_min - fb.y_min).abs() > 1e-9 || (yb.y_max - fb.y_max).abs() > 1e-9)
{
diff = true;
}
let yb_s = match yb {
Some(b) => format!("Y[{:8.3}..{:8.3}]", b.y_min, b.y_max),
None => "Y[None]".to_string(),
};
let fb_s = match fb {
Some(b) => format!(
"2D[x {:8.3}..{:8.3} y {:8.3}..{:8.3}]",
b.x_min, b.x_max, b.y_min, b.y_max
),
None => "2D[None]".to_string(),
};
out.push(format!(
"{}{:4} {:15} {:30} {:55} {}",
indent,
i,
kind,
yb_s,
fb_s,
if diff { "DIFF" } else { "" }
));
if let DisplayElement::Stroke { path, params } = elem {
let rp = path_full_bbox(path);
let m = ¶ms.ctm;
out.push(format!(
"{} ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] lw={:.4} miter={:.4} raw={}",
indent,
m.a,
m.b,
m.c,
m.d,
m.tx,
m.ty,
params.line_width,
params.miter_limit,
match rp {
Some(b) => format!(
"x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
b.x_min, b.x_max, b.y_min, b.y_max
),
None => "None".to_string(),
}
));
}
if let DisplayElement::Clip { path, params } = elem {
let rp = path_full_bbox(path);
let m = ¶ms.ctm;
out.push(format!(
"{} clip ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] rule={:?} raw={}",
indent,
m.a,
m.b,
m.c,
m.d,
m.tx,
m.ty,
params.fill_rule,
match rp {
Some(b) => format!(
"x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
b.x_min, b.x_max, b.y_min, b.y_max
),
None => "None".to_string(),
}
));
}
if let DisplayElement::PatchShading { params } = elem {
out.push(format!(
"{} patch ctm=[{:.4} {:.4} {:.4} {:.4} {:.4} {:.4}] bbox={:?} patches={}",
indent,
params.ctm.a,
params.ctm.b,
params.ctm.c,
params.ctm.d,
params.ctm.tx,
params.ctm.ty,
params.bbox,
params.patches.len()
));
if !params.patches.is_empty() {
let patch = ¶ms.patches[0];
let mut x_min = f64::INFINITY;
let mut y_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_max = f64::NEG_INFINITY;
for &(px, py) in &patch.points {
let (dx, dy) = params.ctm.transform_point(px, py);
x_min = x_min.min(dx);
y_min = y_min.min(dy);
x_max = x_max.max(dx);
y_max = y_max.max(dy);
}
out.push(format!(
"{} patch[0] pts={} dev x[{:.3}..{:.3}] y[{:.3}..{:.3}]",
indent,
patch.points.len(),
x_min,
x_max,
y_min,
y_max
));
}
}
if let DisplayElement::Group {
elements: inner,
params,
} = elem
{
out.push(format!(
"{} group bbox={:?} iso={} ko={} alpha={} bm={} cs={:?}",
indent,
params.bbox,
params.isolated,
params.knockout,
params.alpha,
params.blend_mode,
params.color_space
));
debug_bbox_lines(inner, dpi, depth + 1, out);
}
if let DisplayElement::SoftMasked {
content, params, ..
} = elem
{
out.push(format!(
"{} softmasked bbox={:?}",
indent, params.bbox
));
debug_bbox_lines(content, dpi, depth + 1, out);
}
if let DisplayElement::OcgGroup {
elements: inner,
default_visible,
..
} = elem
{
out.push(format!(
"{} ocg default_visible={}",
indent, default_visible
));
debug_bbox_lines(inner, dpi, depth + 1, out);
}
}
}
pub fn debug_bbox_comparison(list: &DisplayList, dpi: f64) -> Vec<String> {
let mut out = Vec::new();
debug_bbox_lines(list, dpi, 0, &mut out);
out
}
struct MemorySink {
data: Vec<u8>,
width: u32,
}
impl stet_graphics::device::PageSink for MemorySink {
fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String> {
self.width = width;
self.data.reserve(width as usize * height as usize * 4);
Ok(())
}
fn write_rows(&mut self, rgba_rows: &[u8], _num_rows: u32) -> Result<(), String> {
self.data.extend_from_slice(rgba_rows);
Ok(())
}
fn end_page(&mut self) -> Result<(), String> {
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
pub fn render_region(
list: &DisplayList,
vp_x: f64,
vp_y: f64,
vp_w: f64,
vp_h: f64,
pixel_w: u32,
pixel_h: u32,
dpi: f64,
icc: Option<&IccCache>,
image_cache: Option<&ImageCache>,
no_aa: bool,
) -> Vec<u8> {
if pixel_w == 0 || pixel_h == 0 || vp_w <= 0.0 || vp_h <= 0.0 {
return vec![0xFF; pixel_w as usize * pixel_h as usize * 4];
}
let scale_x = pixel_w as f64 / vp_w;
let scale_y = pixel_h as f64 / vp_h;
let effective_dpi = dpi * scale_x;
let bboxes = precompute_full_bboxes(list, effective_dpi);
let epochs = build_viewport_epochs(list, &bboxes);
let clip_seen = precompute_clip_seen(list);
const OVERLAP: u32 = 6;
let render_h = pixel_h + 2 * OVERLAP;
let mut pixmap = Pixmap::new(pixel_w, render_h).expect("Failed to create viewport pixmap");
pixmap.fill(Color::TRANSPARENT);
let cmyk_buf = if has_overprint_elements(list)
|| list.page_group_color_space() == stet_graphics::display_list::GroupColorSpace::DeviceCMYK
|| has_cmyk_group(list)
{
Some(vec![0.0f32; pixel_w as usize * render_h as usize * 4])
} else {
None
};
let mut state = BandState {
clip_region: None,
spare_mask: None,
clip_mask_cache: HashMap::new(),
clip_mask_seen: clip_seen,
mask_pool: Vec::new(),
cmyk_buffer: cmyk_buf,
op_bg_snapshot: None,
op_touched: None,
spot_mask: None,
};
let elements = list.elements();
let vp_x_f = vp_x as f32;
let vp_y_f = vp_y as f32;
let sx = scale_x as f32;
let sy = scale_y as f32;
let vp_x_max = vp_x + vp_w;
let vp_y_max = vp_y + vp_h;
for epoch in &epochs {
if !epoch.has_erase_page {
match epoch.paint_bbox {
Some(ref pb)
if pb.x_max <= vp_x
|| pb.x_min >= vp_x_max
|| pb.y_max <= vp_y
|| pb.y_min >= vp_y_max =>
{
continue;
}
None => continue,
_ => {}
}
}
for i in epoch.start_idx..epoch.end_idx {
let force_process = matches!(
&elements[i],
DisplayElement::OcgGroup { elements: inner, .. }
if contains_clip_op(inner)
);
if !force_process
&& let Some(ref bbox) = bboxes[i]
&& (bbox.x_max <= vp_x
|| bbox.x_min >= vp_x_max
|| bbox.y_max <= vp_y
|| bbox.y_min >= vp_y_max)
{
continue;
}
let ctx = RenderContext {
vp_x: vp_x_f,
vp_y: vp_y_f,
scale_x: sx,
scale_y: sy,
out_w: pixel_w,
out_h: render_h,
effective_dpi,
icc,
image_cache,
preprocessed: None,
elem_idx: i,
no_aa,
opm_zero_transparent: false,
knockout_painter_pass: KnockoutPainterPass::None,
parent_group_isolated: false,
alpha_extraction_pass: false,
};
render_element(&mut pixmap, &mut state, &elements[i], &ctx);
}
}
composite_onto_white(pixmap.data_mut());
let row_bytes = pixel_w as usize * 4;
let end = pixel_h as usize * row_bytes;
pixmap.data()[..end].to_vec()
}
fn copy_backdrop_crop(
parent: &Pixmap,
crop_x: i32,
crop_y: i32,
crop_w: u32,
crop_h: u32,
) -> Vec<u8> {
let pw = parent.width() as usize;
let src = parent.data();
let cw = crop_w as usize;
let ch = crop_h as usize;
let cx = crop_x as usize;
let cy = crop_y as usize;
let mut backdrop = vec![0u8; cw * ch * 4];
for row in 0..ch {
let src_off = ((cy + row) * pw + cx) * 4;
let dst_off = row * cw * 4;
backdrop[dst_off..dst_off + cw * 4].copy_from_slice(&src[src_off..src_off + cw * 4]);
}
backdrop
}
fn clip_polygon_halfplane(
poly: &[(f32, f32)],
nx: f32,
ny: f32,
px: f32,
py: f32,
) -> Vec<(f32, f32)> {
if poly.is_empty() {
return vec![];
}
let dot = |x: f32, y: f32| nx * (x - px) + ny * (y - py);
let mut out = Vec::with_capacity(poly.len() + 1);
let n = poly.len();
for i in 0..n {
let (ax, ay) = poly[i];
let (bx, by) = poly[(i + 1) % n];
let da = dot(ax, ay);
let db = dot(bx, by);
if da >= 0.0 {
out.push((ax, ay));
}
if (da >= 0.0) != (db >= 0.0) {
let t = da / (da - db);
out.push((ax + t * (bx - ax), ay + t * (by - ay)));
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn render_axial_shading(
pixmap: &mut Pixmap,
params: &AxialShadingParams,
vp_x: f32,
vp_y: f32,
scale_x: f32,
scale_y: f32,
clip_mask: Option<&Mask>,
no_aa: bool,
cmyk_buf: Option<&mut [f32]>,
icc: Option<&IccCache>,
) {
let pw = pixmap.width();
let ph = pixmap.height();
if params.color_stops.is_empty() || pw == 0 || ph == 0 {
return;
}
let (mut rx_min, mut ry_min, mut rx_max, mut ry_max) = if let Some(bbox) = ¶ms.bbox {
let corners = [
params.ctm.transform_point(bbox[0], bbox[1]),
params.ctm.transform_point(bbox[2], bbox[1]),
params.ctm.transform_point(bbox[0], bbox[3]),
params.ctm.transform_point(bbox[2], bbox[3]),
];
let x_min = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min);
let y_min = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min);
let x_max = corners
.iter()
.map(|c| c.0)
.fold(f64::NEG_INFINITY, f64::max);
let y_max = corners
.iter()
.map(|c| c.1)
.fold(f64::NEG_INFINITY, f64::max);
(
((x_min as f32 - vp_x) * scale_x).max(0.0),
((y_min as f32 - vp_y) * scale_y).max(0.0),
((x_max as f32 - vp_x) * scale_x).min(pw as f32),
((y_max as f32 - vp_y) * scale_y).min(ph as f32),
)
} else {
(0.0, 0.0, pw as f32, ph as f32)
};
if rx_max <= rx_min || ry_max <= ry_min {
return;
}
let (dx0, dy0) = params.ctm.transform_point(params.x0, params.y0);
let (dx1, dy1) = params.ctm.transform_point(params.x1, params.y1);
let needs_perpendicular_clip = (!params.extend_start || !params.extend_end) && {
let axis_x = dx1 - dx0;
let axis_y = dy1 - dy0;
axis_x.abs() > 1e-6 && axis_y.abs() > 1e-6
};
let bbox_is_rotated =
params.bbox.is_some() && (params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10);
if needs_perpendicular_clip {
let stops = build_gradient_stops(¶ms.color_stops);
if stops.is_empty() {
return;
}
let start = stet_tiny_skia::Point::from_xy(params.x0 as f32, params.y0 as f32);
let end = stet_tiny_skia::Point::from_xy(params.x1 as f32, params.y1 as f32);
let gradient_transform =
viewport_transform(to_transform(¶ms.ctm), vp_x, vp_y, scale_x, scale_y);
let Some(gradient) = stet_tiny_skia::LinearGradient::new(
start,
end,
stops,
stet_tiny_skia::SpreadMode::Pad,
gradient_transform,
) else {
return;
};
let paint = Paint {
shader: gradient,
anti_alias: !no_aa,
..Paint::default()
};
let mut poly: Vec<(f32, f32)> = if bbox_is_rotated {
let bbox = params.bbox.as_ref().unwrap();
let corners = [
params.ctm.transform_point(bbox[0], bbox[1]),
params.ctm.transform_point(bbox[2], bbox[1]),
params.ctm.transform_point(bbox[2], bbox[3]),
params.ctm.transform_point(bbox[0], bbox[3]),
];
corners
.iter()
.map(|(x, y)| ((*x as f32 - vp_x) * scale_x, (*y as f32 - vp_y) * scale_y))
.collect()
} else {
vec![
(rx_min, ry_min),
(rx_max, ry_min),
(rx_max, ry_max),
(rx_min, ry_max),
]
};
let ax = (dx1 - dx0) as f32 * scale_x;
let ay = (dy1 - dy0) as f32 * scale_y;
if !params.extend_start {
let px = (dx0 as f32 - vp_x) * scale_x;
let py = (dy0 as f32 - vp_y) * scale_y;
poly = clip_polygon_halfplane(&poly, ax, ay, px, py);
}
if !params.extend_end {
let px = (dx1 as f32 - vp_x) * scale_x;
let py = (dy1 as f32 - vp_y) * scale_y;
poly = clip_polygon_halfplane(&poly, -ax, -ay, px, py);
}
if poly.len() >= 3 {
let mut pb = PathBuilder::new();
pb.move_to(poly[0].0, poly[0].1);
for &(x, y) in &poly[1..] {
pb.line_to(x, y);
}
pb.close();
if let Some(path) = pb.finish() {
pixmap.fill_path(
&path,
&paint,
SkiaFillRule::Winding,
Transform::identity(),
clip_mask,
);
}
}
} else {
if !params.extend_start || !params.extend_end {
let axis_x = dx1 - dx0;
let axis_y = dy1 - dy0;
let gx0 = (dx0 as f32 - vp_x) * scale_x;
let gy0 = (dy0 as f32 - vp_y) * scale_y;
let gx1 = (dx1 as f32 - vp_x) * scale_x;
let gy1 = (dy1 as f32 - vp_y) * scale_y;
if axis_x.abs() >= axis_y.abs() {
if !params.extend_start {
if axis_x >= 0.0 {
rx_min = rx_min.max(gx0);
} else {
rx_max = rx_max.min(gx0);
}
}
if !params.extend_end {
if axis_x >= 0.0 {
rx_max = rx_max.min(gx1);
} else {
rx_min = rx_min.max(gx1);
}
}
} else {
if !params.extend_start {
if axis_y >= 0.0 {
ry_min = ry_min.max(gy0);
} else {
ry_max = ry_max.min(gy0);
}
}
if !params.extend_end {
if axis_y >= 0.0 {
ry_max = ry_max.min(gy1);
} else {
ry_min = ry_min.max(gy1);
}
}
}
if rx_max <= rx_min || ry_max <= ry_min {
return;
}
}
let ax = params.x1 - params.x0;
let ay = params.y1 - params.y0;
let axis_sq = ax * ax + ay * ay;
if axis_sq < 1e-20 {
return;
}
let pixel_dx = (dx1 - dx0) * scale_x as f64;
let pixel_dy = (dy1 - dy0) * scale_y as f64;
let pixel_axis_len = (pixel_dx * pixel_dx + pixel_dy * pixel_dy).sqrt();
let lut_size = (pixel_axis_len as usize)
.max(params.color_stops.len())
.max(256)
.min(16384);
let lut = build_gradient_lut(¶ms.color_stops, lut_size);
let Some(inv) = params.ctm.invert() else {
return;
};
let inv_sx = 1.0 / scale_x as f64;
let inv_sy = 1.0 / scale_y as f64;
let dev_origin_x = vp_x as f64;
let dev_origin_y = vp_y as f64;
let sx_base = inv.a * dev_origin_x + inv.c * dev_origin_y + inv.tx;
let sy_base = inv.b * dev_origin_x + inv.d * dev_origin_y + inv.ty;
let dsx_dx = inv.a * inv_sx;
let dsx_dy = inv.c * inv_sy;
let dsy_dx = inv.b * inv_sx;
let dsy_dy = inv.d * inv_sy;
let inv_axis_sq = 1.0 / axis_sq;
let t_origin = ((sx_base - params.x0) * ax + (sy_base - params.y0) * ay) * inv_axis_sq;
let dt_dx = (dsx_dx * ax + dsy_dx * ay) * inv_axis_sq;
let dt_dy = (dsx_dy * ax + dsy_dy * ay) * inv_axis_sq;
let bbox_pixel_clip = if bbox_is_rotated {
let bbox = params.bbox.as_ref().unwrap();
let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
Some((
dsx_dx, dsx_dy, sx_base, dsy_dx, dsy_dy, sy_base, bx0, by0, bx1, by1,
))
} else {
None
};
let ix_min = rx_min.floor() as u32;
let ix_max = rx_max.ceil().min(pw as f32) as u32;
let iy_min = ry_min.floor() as u32;
let iy_max = ry_max.ceil().min(ph as f32) as u32;
let stride = pw as usize * 4;
let data = pixmap.data_mut();
let mask_data = clip_mask.map(|m| m.data());
let alpha = (params.alpha.clamp(0.0, 1.0) * 255.0 + 0.5) as u16;
for py in iy_min..iy_max {
let t_row = t_origin + dt_dy * py as f64;
let row_offset = py as usize * stride;
let (ux_row, uy_row) =
if let Some((_, dux_dy, ux_base, _, duy_dy, uy_base, ..)) = &bbox_pixel_clip {
(ux_base + dux_dy * py as f64, uy_base + duy_dy * py as f64)
} else {
(0.0, 0.0)
};
for px in ix_min..ix_max {
if let Some(md) = mask_data {
if md[py as usize * pw as usize + px as usize] == 0 {
continue;
}
}
if let Some((dux_dx, _, _, duy_dx, _, _, bx0, by0, bx1, by1)) = &bbox_pixel_clip {
let ux = ux_row + dux_dx * px as f64;
let uy = uy_row + duy_dx * px as f64;
if ux < *bx0 || ux > *bx1 || uy < *by0 || uy > *by1 {
continue;
}
}
let t = t_row + dt_dx * px as f64;
let t_clamped = t.clamp(0.0, 1.0);
let idx = (t_clamped * (lut_size - 1) as f64 + 0.5) as usize;
let [r, g, b, _] = lut[idx.min(lut_size - 1)];
let offset = row_offset + px as usize * 4;
if alpha >= 255 {
data[offset] = r;
data[offset + 1] = g;
data[offset + 2] = b;
data[offset + 3] = 255;
} else {
let a = alpha as u16;
let inv_a = 255 - a;
data[offset] = ((r as u16 * a + data[offset] as u16 * inv_a + 127) / 255) as u8;
data[offset + 1] =
((g as u16 * a + data[offset + 1] as u16 * inv_a + 127) / 255) as u8;
data[offset + 2] =
((b as u16 * a + data[offset + 2] as u16 * inv_a + 127) / 255) as u8;
data[offset + 3] = ((a + data[offset + 3] as u16 * inv_a / 255).min(255)) as u8;
}
}
}
}
if let Some(buf) = cmyk_buf {
let pw = pixmap.width();
let inv_sx = 1.0 / scale_x as f64;
let inv_sy = 1.0 / scale_y as f64;
let axis_x = params.x1 - params.x0;
let axis_y = params.y1 - params.y0;
let axis_len_sq = axis_x * axis_x + axis_y * axis_y;
let Some(inv_ctm) = params.ctm.invert() else {
return;
};
let iy_min = ry_min.floor() as u32;
let iy_max = ry_max.ceil().min(pixmap.height() as f32) as u32;
let ix_min = rx_min.floor() as u32;
let ix_max = rx_max.ceil().min(pw as f32) as u32;
for py in iy_min..iy_max {
let dev_y = py as f64 * inv_sy + vp_y as f64;
for px in ix_min..ix_max {
let dev_x = px as f64 * inv_sx + vp_x as f64;
let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
let t = if axis_len_sq > 1e-10 {
((ux - params.x0) * axis_x + (uy - params.y0) * axis_y) / axis_len_sq
} else {
0.0
};
if t < 0.0 && !params.extend_start {
continue;
}
if t > 1.0 && !params.extend_end {
continue;
}
let clamped = t.clamp(0.0, 1.0);
if let Some(mask) = clip_mask {
let mi = py as usize * pw as usize + px as usize;
if mask.data()[mi] == 0 {
continue;
}
}
let color = interpolate_color_stops(¶ms.color_stops, clamped);
let cmyk = interpolate_cmyk_from_stops(
¶ms.color_stops,
¶ms.color_space,
clamped,
&color,
icc,
);
let ci = (py as usize * pw as usize + px as usize) * 4;
if ci + 3 < buf.len() {
if params.spot_tint_blend && params.overprint {
let cur_c = buf[ci] as f64;
let cur_m = buf[ci + 1] as f64;
let cur_y = buf[ci + 2] as f64;
let cur_k = buf[ci + 3] as f64;
let cur_is_zero =
cur_c == 0.0 && cur_m == 0.0 && cur_y == 0.0 && cur_k == 0.0;
let named = params.painted_channels;
if cur_is_zero {
if named & stet_graphics::device::CMYK_C != 0 {
buf[ci] = cmyk.0 as f32;
}
if named & stet_graphics::device::CMYK_M != 0 {
buf[ci + 1] = cmyk.1 as f32;
}
if named & stet_graphics::device::CMYK_Y != 0 {
buf[ci + 2] = cmyk.2 as f32;
}
if named & stet_graphics::device::CMYK_K != 0 {
buf[ci + 3] = cmyk.3 as f32;
}
} else {
let new_c = if named & stet_graphics::device::CMYK_C != 0 {
cmyk.0
} else {
cur_c
};
let new_m = if named & stet_graphics::device::CMYK_M != 0 {
cmyk.1
} else {
cur_m
};
let new_y = if named & stet_graphics::device::CMYK_Y != 0 {
cmyk.2
} else {
cur_y
};
let new_k = if named & stet_graphics::device::CMYK_K != 0 {
cmyk.3
} else {
cur_k
};
buf[ci] = new_c as f32;
buf[ci + 1] = new_m as f32;
buf[ci + 2] = new_y as f32;
buf[ci + 3] = new_k as f32;
let (rv, gv, bv) = if let Some(icc_cache) = icc {
icc_cache
.convert_cmyk_readonly(new_c, new_m, new_y, new_k)
.unwrap_or_else(|| cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k))
} else {
cmyk_to_rgb_plrm(new_c, new_m, new_y, new_k)
};
let stride = pixmap.data().len() / pixmap.height() as usize;
let offset = py as usize * stride + px as usize * 4;
let data = pixmap.data_mut();
data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
}
} else if params.overprint
&& params.painted_channels != stet_graphics::device::CMYK_ALL
{
if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
buf[ci] = cmyk.0 as f32;
}
if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
buf[ci + 1] = cmyk.1 as f32;
}
if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
buf[ci + 2] = cmyk.2 as f32;
}
if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
buf[ci + 3] = cmyk.3 as f32;
}
let c = buf[ci] as f64;
let m = buf[ci + 1] as f64;
let y = buf[ci + 2] as f64;
let k = buf[ci + 3] as f64;
let (rv, gv, bv) = if let Some(icc_cache) = icc {
icc_cache
.convert_cmyk_readonly(c, m, y, k)
.unwrap_or_else(|| cmyk_to_rgb_plrm(c, m, y, k))
} else {
cmyk_to_rgb_plrm(c, m, y, k)
};
let stride = pixmap.data().len() / pixmap.height() as usize;
let offset = py as usize * stride + px as usize * 4;
let data = pixmap.data_mut();
data[offset] = (rv * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 1] = (gv * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 2] = (bv * 255.0).round().clamp(0.0, 255.0) as u8;
} else {
buf[ci] = cmyk.0 as f32;
buf[ci + 1] = cmyk.1 as f32;
buf[ci + 2] = cmyk.2 as f32;
buf[ci + 3] = cmyk.3 as f32;
}
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_radial_shading(
pixmap: &mut Pixmap,
params: &RadialShadingParams,
vp_x: f32,
vp_y: f32,
scale_x: f32,
scale_y: f32,
clip_mask: Option<&Mask>,
_no_aa: bool,
mut cmyk_buf: Option<&mut [f32]>,
icc: Option<&IccCache>,
) {
let pw = pixmap.width();
let ph = pixmap.height();
if params.color_stops.is_empty() || pw == 0 || ph == 0 {
return;
}
let Some(inv_ctm) = params.ctm.invert() else {
return;
};
let (px_min, py_min, px_max, py_max) = if let Some(bbox) = ¶ms.bbox {
let corners = [
params.ctm.transform_point(bbox[0], bbox[1]),
params.ctm.transform_point(bbox[2], bbox[1]),
params.ctm.transform_point(bbox[0], bbox[3]),
params.ctm.transform_point(bbox[2], bbox[3]),
];
let x_min = corners
.iter()
.map(|c| c.0 as f32)
.fold(f32::INFINITY, f32::min);
let y_min = corners
.iter()
.map(|c| c.1 as f32)
.fold(f32::INFINITY, f32::min);
let x_max = corners
.iter()
.map(|c| c.0 as f32)
.fold(f32::NEG_INFINITY, f32::max);
let y_max = corners
.iter()
.map(|c| c.1 as f32)
.fold(f32::NEG_INFINITY, f32::max);
(
((x_min - vp_x) * scale_x).max(0.0) as u32,
((y_min - vp_y) * scale_y).max(0.0) as u32,
(((x_max - vp_x) * scale_x).ceil() as u32).min(pw),
(((y_max - vp_y) * scale_y).ceil() as u32).min(ph),
)
} else {
(0, 0, pw, ph)
};
let inv_sx = 1.0 / scale_x as f64;
let inv_sy = 1.0 / scale_y as f64;
let rotated_bbox = if let Some(bbox) = ¶ms.bbox {
if params.ctm.b.abs() > 1e-10 || params.ctm.c.abs() > 1e-10 {
let (bx0, bx1) = (bbox[0].min(bbox[2]), bbox[0].max(bbox[2]));
let (by0, by1) = (bbox[1].min(bbox[3]), bbox[1].max(bbox[3]));
Some((bx0, by0, bx1, by1))
} else {
None
}
} else {
None
};
let data = pixmap.data_mut();
let stride = pw as usize * 4;
for py in py_min..py_max {
let dev_y = py as f64 * inv_sy + vp_y as f64;
for px in px_min..px_max {
let dev_x = px as f64 * inv_sx + vp_x as f64;
let (ux, uy) = inv_ctm.transform_point(dev_x, dev_y);
if let Some((bx0, by0, bx1, by1)) = rotated_bbox {
if ux < bx0 || ux > bx1 || uy < by0 || uy > by1 {
continue;
}
}
let t = solve_radial_t(
ux,
uy,
params.x0,
params.y0,
params.r0,
params.x1,
params.y1,
params.r1,
params.extend_start,
params.extend_end,
);
if let Some(t) = t {
let clamped = t.clamp(0.0, 1.0);
let color = interpolate_color_stops(¶ms.color_stops, clamped);
let clipped = clip_mask
.is_some_and(|mask| mask.data()[py as usize * pw as usize + px as usize] == 0);
if clipped {
continue;
}
let cmyk = interpolate_cmyk_from_stops(
¶ms.color_stops,
¶ms.color_space,
clamped,
&color,
icc,
);
let ci = (py as usize * pw as usize + px as usize) * 4;
let buffer_clean = if let Some(ref buf) = cmyk_buf {
if ci + 3 < buf.len() {
buf[ci] == 0.0
&& buf[ci + 1] == 0.0
&& buf[ci + 2] == 0.0
&& buf[ci + 3] == 0.0
} else {
false
}
} else {
false
};
let offset_for_check = py as usize * stride + px as usize * 4;
let pixmap_has_colour = data[offset_for_check + 3] > 0
&& (data[offset_for_check] < 250
|| data[offset_for_check + 1] < 250
|| data[offset_for_check + 2] < 250);
let use_multiplicative = params.overprint
&& params.painted_channels != stet_graphics::device::CMYK_ALL
&& buffer_clean
&& pixmap_has_colour;
if let Some(ref mut buf) = cmyk_buf
&& ci + 3 < buf.len()
{
if params.overprint
&& params.painted_channels != stet_graphics::device::CMYK_ALL
{
if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
buf[ci] = cmyk.0 as f32;
}
if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
buf[ci + 1] = cmyk.1 as f32;
}
if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
buf[ci + 2] = cmyk.2 as f32;
}
if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
buf[ci + 3] = cmyk.3 as f32;
}
} else {
buf[ci] = cmyk.0 as f32;
buf[ci + 1] = cmyk.1 as f32;
buf[ci + 2] = cmyk.2 as f32;
buf[ci + 3] = cmyk.3 as f32;
}
}
let offset = py as usize * stride + px as usize * 4;
if use_multiplicative {
let bg_r = data[offset] as f64 / 255.0;
let bg_g = data[offset + 1] as f64 / 255.0;
let bg_b = data[offset + 2] as f64 / 255.0;
let over_r = if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
1.0 - cmyk.0
} else {
1.0
};
let over_g = if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
1.0 - cmyk.1
} else {
1.0
};
let over_b = if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
1.0 - cmyk.2
} else {
1.0
};
let k_fac = if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
1.0 - cmyk.3
} else {
1.0
};
data[offset] = ((bg_r * over_r * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
data[offset + 1] =
((bg_g * over_g * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
data[offset + 2] =
((bg_b * over_b * k_fac).clamp(0.0, 1.0) * 255.0).round() as u8;
data[offset + 3] = 255;
} else {
data[offset] = (color.r * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 1] = (color.g * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 2] = (color.b * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 3] = 255;
if params.overprint
&& params.painted_channels != stet_graphics::device::CMYK_ALL
&& matches!(params.color_space, ShadingColorSpace::DeviceCMYK)
&& let Some(ref mut buf) = cmyk_buf
&& ci + 3 < buf.len()
&& let Some(icc_cache) = icc
{
let c = buf[ci] as f64;
let m = buf[ci + 1] as f64;
let y = buf[ci + 2] as f64;
let k = buf[ci + 3] as f64;
if let Some((r, g, b)) = icc_cache.convert_cmyk_readonly(c, m, y, k) {
data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
}
}
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn solve_radial_t(
px: f64,
py: f64,
x0: f64,
y0: f64,
r0: f64,
x1: f64,
y1: f64,
r1: f64,
extend_start: bool,
extend_end: bool,
) -> Option<f64> {
let cdx = x1 - x0;
let cdy = y1 - y0;
let dr = r1 - r0;
let a = cdx * cdx + cdy * cdy - dr * dr;
let dpx = px - x0;
let dpy = py - y0;
let b = -2.0 * (dpx * cdx + dpy * cdy + r0 * dr);
let c = dpx * dpx + dpy * dpy - r0 * r0;
let in_domain = |t: f64| -> bool {
(0.0..=1.0).contains(&t) || (t < 0.0 && extend_start) || (t > 1.0 && extend_end)
};
if a.abs() < 1e-10 {
if b.abs() < 1e-10 {
return None;
}
let t = -c / b;
let radius = r0 + t * dr;
if radius >= 0.0 && in_domain(t) {
return Some(t);
}
return None;
}
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return None;
}
let sqrt_d = discriminant.sqrt();
let t1 = (-b + sqrt_d) / (2.0 * a);
let t2 = (-b - sqrt_d) / (2.0 * a);
let mut best: Option<f64> = None;
for t in [t1, t2] {
let radius = r0 + t * dr;
if radius >= 0.0 && in_domain(t) {
best = Some(match best {
Some(prev) => prev.max(t),
None => t,
});
}
}
best
}
#[allow(clippy::too_many_arguments)]
fn render_mesh_shading(
pixmap: &mut Pixmap,
params: &MeshShadingParams,
vp_x: f32,
vp_y: f32,
scale_x: f32,
scale_y: f32,
clip_mask: Option<&Mask>,
mut cmyk_buf: Option<&mut [f32]>,
icc: Option<&IccCache>,
) {
let pw = pixmap.width() as usize;
let ph = pixmap.height() as usize;
if pw == 0 || ph == 0 {
return;
}
let data = pixmap.data_mut();
let stride = pw * 4;
let lut = params.color_lut.as_deref();
for tri in ¶ms.triangles {
let (dx0, dy0) = params.ctm.transform_point(tri.v0.x, tri.v0.y);
let (dx1, dy1) = params.ctm.transform_point(tri.v1.x, tri.v1.y);
let (dx2, dy2) = params.ctm.transform_point(tri.v2.x, tri.v2.y);
let x0 = (dx0 as f32 - vp_x) * scale_x;
let y0 = (dy0 as f32 - vp_y) * scale_y;
let x1 = (dx1 as f32 - vp_x) * scale_x;
let y1 = (dy1 as f32 - vp_y) * scale_y;
let x2 = (dx2 as f32 - vp_x) * scale_x;
let y2 = (dy2 as f32 - vp_y) * scale_y;
let min_x = (x0.min(x1).min(x2).floor().max(0.0)) as usize;
let max_x = (x0.max(x1).max(x2).ceil() as usize).min(pw);
let min_y = (y0.min(y1).min(y2).floor().max(0.0)) as usize;
let max_y = (y0.max(y1).max(y2).ceil() as usize).min(ph);
if min_x >= max_x || min_y >= max_y {
continue;
}
let x0 = x0 as f64;
let y0 = y0 as f64;
let x1 = x1 as f64;
let y1 = y1 as f64;
let x2 = x2 as f64;
let y2 = y2 as f64;
let denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2);
if denom.abs() < 1e-10 {
continue;
}
let (x1, y1, x2, y2) = if denom < 0.0 {
(x2, y2, x1, y1)
} else {
(x1, y1, x2, y2)
};
let (v1_ref, v2_ref) = if denom < 0.0 {
(&tri.v2, &tri.v1)
} else {
(&tri.v1, &tri.v2)
};
let denom = denom.abs();
let inv_denom = 1.0 / denom;
for py in min_y..max_y {
for px in min_x..max_x {
let pxf = px as f64 + 0.5;
let pyf = py as f64 + 0.5;
let w0 = ((y1 - y2) * (pxf - x2) + (x2 - x1) * (pyf - y2)) * inv_denom;
let w1 = ((y2 - y0) * (pxf - x2) + (x0 - x2) * (pyf - y2)) * inv_denom;
let w2 = 1.0 - w0 - w1;
if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
continue;
}
let clipped = clip_mask.is_some_and(|mask| mask.data()[py * pw + px] == 0);
let w0c = w0.max(0.0);
let w1c = w1.max(0.0);
let w2c = w2.max(0.0);
let wsum = w0c + w1c + w2c;
let w0n = w0c / wsum;
let w1n = w1c / wsum;
let w2n = w2c / wsum;
let (r, g, b) = if let Some(lut) = lut {
let raw = w0n * tri.v0.raw_components[0]
+ w1n * v1_ref.raw_components[0]
+ w2n * v2_ref.raw_components[0];
let raw = raw.clamp(0.0, 1.0);
let fi = raw * (lut.len() - 1) as f64;
let i0 = (fi as usize).min(lut.len().saturating_sub(2));
let frac = fi - i0 as f64;
let c0 = &lut[i0];
let c1 = &lut[i0 + 1];
(
c0.r + frac * (c1.r - c0.r),
c0.g + frac * (c1.g - c0.g),
c0.b + frac * (c1.b - c0.b),
)
} else {
(
w0n * tri.v0.color.r + w1n * v1_ref.color.r + w2n * v2_ref.color.r,
w0n * tri.v0.color.g + w1n * v1_ref.color.g + w2n * v2_ref.color.g,
w0n * tri.v0.color.b + w1n * v1_ref.color.b + w2n * v2_ref.color.b,
)
};
if let Some(ref mut buf) = cmyk_buf {
let ci = (py * pw + px) * 4;
if ci + 3 < buf.len() {
let cmyk = interpolate_cmyk_from_vertices(
&tri.v0,
v1_ref,
v2_ref,
w0n,
w1n,
w2n,
¶ms.color_space,
r,
g,
b,
icc,
);
if params.overprint
&& params.painted_channels != stet_graphics::device::CMYK_ALL
{
if !clipped {
if params.painted_channels & stet_graphics::device::CMYK_C != 0 {
buf[ci] = cmyk.0 as f32;
}
if params.painted_channels & stet_graphics::device::CMYK_M != 0 {
buf[ci + 1] = cmyk.1 as f32;
}
if params.painted_channels & stet_graphics::device::CMYK_Y != 0 {
buf[ci + 2] = cmyk.2 as f32;
}
if params.painted_channels & stet_graphics::device::CMYK_K != 0 {
buf[ci + 3] = cmyk.3 as f32;
}
}
} else {
buf[ci] = cmyk.0 as f32;
buf[ci + 1] = cmyk.1 as f32;
buf[ci + 2] = cmyk.2 as f32;
buf[ci + 3] = cmyk.3 as f32;
}
}
}
if clipped {
continue;
}
let offset = py * stride + px * 4;
data[offset] = (r * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 1] = (g * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 2] = (b * 255.0).round().clamp(0.0, 255.0) as u8;
data[offset + 3] = 255;
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_patch_shading(
pixmap: &mut Pixmap,
params: &PatchShadingParams,
vp_x: f32,
vp_y: f32,
scale_x: f32,
scale_y: f32,
clip_mask: Option<&Mask>,
cmyk_buf: Option<&mut [f32]>,
icc: Option<&IccCache>,
) {
let mut triangles = Vec::new();
let scale = scale_x.max(scale_y) as f64;
for patch in ¶ms.patches {
if patch.points.len() >= 12 {
let mut x_min = f64::INFINITY;
let mut y_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_max = f64::NEG_INFINITY;
for &(px, py) in &patch.points {
let (dx, dy) = params.ctm.transform_point(px, py);
x_min = x_min.min(dx);
y_min = y_min.min(dy);
x_max = x_max.max(dx);
y_max = y_max.max(dy);
}
let extent = (x_max - x_min).max(y_max - y_min).abs() * scale;
let n = (extent / 2.0).ceil().clamp(8.0, 64.0) as usize;
let icc_profile_hash = match ¶ms.color_space {
stet_graphics::device::ShadingColorSpace::ICCBased { profile_hash, .. } => {
Some(profile_hash)
}
_ => None,
};
subdivide_patch_to_triangles(patch, &mut triangles, n, icc_profile_hash, icc);
}
}
if !triangles.is_empty() {
let mesh_params = MeshShadingParams {
triangles,
ctm: params.ctm,
bbox: params.bbox,
color_space: params.color_space.clone(),
overprint: params.overprint,
painted_channels: params.painted_channels,
color_lut: params.color_lut.clone(),
};
render_mesh_shading(
pixmap,
&mesh_params,
vp_x,
vp_y,
scale_x,
scale_y,
clip_mask,
cmyk_buf,
icc,
);
}
}
fn subdivide_patch_to_triangles(
patch: &stet_graphics::device::ShadingPatch,
triangles: &mut Vec<stet_graphics::device::ShadingTriangle>,
n: usize,
icc_profile_hash: Option<&stet_graphics::icc::ProfileHash>,
icc_cache: Option<&IccCache>,
) {
let mut grid: Vec<(f64, f64, DeviceColor, Vec<f64>)> = Vec::with_capacity((n + 1) * (n + 1));
let use_tensor = patch.points.len() >= 16;
let has_raw = !patch.raw_colors[0].is_empty();
let use_icc_interp = has_raw && icc_profile_hash.is_some() && icc_cache.is_some();
for row in 0..=n {
let v = row as f64 / n as f64;
for col in 0..=n {
let u = col as f64 / n as f64;
let (x, y) = if use_tensor {
eval_tensor_patch(patch, u, v)
} else {
eval_coons_patch(patch, u, v)
};
let raw = if has_raw {
bilinear_raw(&patch.raw_colors, u, v)
} else {
vec![]
};
let color = if use_icc_interp {
if let Some((r, g, b)) = icc_cache
.unwrap()
.convert_color_readonly(icc_profile_hash.unwrap(), &raw)
{
DeviceColor::from_rgb(r, g, b)
} else {
bilinear_color(&patch.colors, u, v)
}
} else {
bilinear_color(&patch.colors, u, v)
};
grid.push((x, y, color, raw));
}
}
let cols = n + 1;
for row in 0..n {
for col in 0..n {
let i00 = row * cols + col;
let i10 = i00 + 1;
let i01 = i00 + cols;
let i11 = i01 + 1;
let (x00, y00, c00, r00) = &grid[i00];
let (x10, y10, c10, r10) = &grid[i10];
let (x01, y01, c01, r01) = &grid[i01];
let (x11, y11, c11, r11) = &grid[i11];
use stet_graphics::device::ShadingVertex;
triangles.push(stet_graphics::device::ShadingTriangle {
v0: ShadingVertex {
x: *x00,
y: *y00,
color: c00.clone(),
raw_components: r00.clone(),
},
v1: ShadingVertex {
x: *x10,
y: *y10,
color: c10.clone(),
raw_components: r10.clone(),
},
v2: ShadingVertex {
x: *x01,
y: *y01,
color: c01.clone(),
raw_components: r01.clone(),
},
});
triangles.push(stet_graphics::device::ShadingTriangle {
v0: ShadingVertex {
x: *x10,
y: *y10,
color: c10.clone(),
raw_components: r10.clone(),
},
v1: ShadingVertex {
x: *x11,
y: *y11,
color: c11.clone(),
raw_components: r11.clone(),
},
v2: ShadingVertex {
x: *x01,
y: *y01,
color: c01.clone(),
raw_components: r01.clone(),
},
});
}
}
}
fn eval_coons_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
let pts = &patch.points;
if pts.len() < 12 {
return (0.0, 0.0);
}
let c0 = eval_cubic_bezier(pts[0], pts[1], pts[2], pts[3], u);
let c2 = eval_cubic_bezier(pts[6], pts[7], pts[8], pts[9], 1.0 - u);
let d0 = eval_cubic_bezier(pts[0], pts[11], pts[10], pts[9], v);
let d1 = eval_cubic_bezier(pts[3], pts[4], pts[5], pts[6], v);
let p00 = pts[0];
let p10 = pts[3];
let p01 = pts[9];
let p11 = pts[6];
let bx = (1.0 - u) * (1.0 - v) * p00.0
+ u * (1.0 - v) * p10.0
+ (1.0 - u) * v * p01.0
+ u * v * p11.0;
let by = (1.0 - u) * (1.0 - v) * p00.1
+ u * (1.0 - v) * p10.1
+ (1.0 - u) * v * p01.1
+ u * v * p11.1;
let x = (1.0 - v) * c0.0 + v * c2.0 + (1.0 - u) * d0.0 + u * d1.0 - bx;
let y = (1.0 - v) * c0.1 + v * c2.1 + (1.0 - u) * d0.1 + u * d1.1 - by;
(x, y)
}
fn eval_tensor_patch(patch: &stet_graphics::device::ShadingPatch, u: f64, v: f64) -> (f64, f64) {
let pts = &patch.points;
let grid: [[usize; 4]; 4] = [[0, 1, 2, 3], [11, 12, 13, 4], [10, 15, 14, 5], [9, 8, 7, 6]];
let su = 1.0 - u;
let bu = [su * su * su, 3.0 * su * su * u, 3.0 * su * u * u, u * u * u];
let sv = 1.0 - v;
let bv = [sv * sv * sv, 3.0 * sv * sv * v, 3.0 * sv * v * v, v * v * v];
let mut x = 0.0;
let mut y = 0.0;
for j in 0..4 {
for i in 0..4 {
let w = bu[i] * bv[j];
let p = pts[grid[j][i]];
x += w * p.0;
y += w * p.1;
}
}
(x, y)
}
fn eval_cubic_bezier(
p0: (f64, f64),
p1: (f64, f64),
p2: (f64, f64),
p3: (f64, f64),
t: f64,
) -> (f64, f64) {
let s = 1.0 - t;
let s2 = s * s;
let t2 = t * t;
let b0 = s2 * s;
let b1 = 3.0 * s2 * t;
let b2 = 3.0 * s * t2;
let b3 = t2 * t;
(
b0 * p0.0 + b1 * p1.0 + b2 * p2.0 + b3 * p3.0,
b0 * p0.1 + b1 * p1.1 + b2 * p2.1 + b3 * p3.1,
)
}
fn bilinear_color(colors: &[DeviceColor; 4], u: f64, v: f64) -> DeviceColor {
let r = (1.0 - u) * (1.0 - v) * colors[0].r
+ u * (1.0 - v) * colors[1].r
+ (1.0 - u) * v * colors[3].r
+ u * v * colors[2].r;
let g = (1.0 - u) * (1.0 - v) * colors[0].g
+ u * (1.0 - v) * colors[1].g
+ (1.0 - u) * v * colors[3].g
+ u * v * colors[2].g;
let b = (1.0 - u) * (1.0 - v) * colors[0].b
+ u * (1.0 - v) * colors[1].b
+ (1.0 - u) * v * colors[3].b
+ u * v * colors[2].b;
DeviceColor::from_rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
}
fn bilinear_raw(raw_colors: &[Vec<f64>; 4], u: f64, v: f64) -> Vec<f64> {
let n = raw_colors[0].len();
let mut result = vec![0.0; n];
for i in 0..n {
result[i] = (1.0 - u) * (1.0 - v) * raw_colors[0][i]
+ u * (1.0 - v) * raw_colors[1][i]
+ (1.0 - u) * v * raw_colors[3][i]
+ u * v * raw_colors[2][i];
}
result
}
fn build_gradient_lut(stops: &[stet_graphics::device::ColorStop], size: usize) -> Vec<[u8; 4]> {
let size = size.max(2);
let mut lut = vec![[0u8; 4]; size];
if stops.is_empty() {
return lut;
}
let mut si = 0usize; let last = (size - 1) as f64;
for i in 0..size {
let t = i as f64 / last;
while si + 1 < stops.len() && stops[si + 1].position < t {
si += 1;
}
let (r, g, b) = if si + 1 >= stops.len() {
let c = &stops[stops.len() - 1].color;
(c.r, c.g, c.b)
} else if t <= stops[si].position {
let c = &stops[si].color;
(c.r, c.g, c.b)
} else {
let t0 = stops[si].position;
let t1 = stops[si + 1].position;
let frac = if (t1 - t0).abs() < 1e-10 {
0.0
} else {
(t - t0) / (t1 - t0)
};
let c0 = &stops[si].color;
let c1 = &stops[si + 1].color;
(
c0.r + frac * (c1.r - c0.r),
c0.g + frac * (c1.g - c0.g),
c0.b + frac * (c1.b - c0.b),
)
};
lut[i] = [
(r * 255.0).round().clamp(0.0, 255.0) as u8,
(g * 255.0).round().clamp(0.0, 255.0) as u8,
(b * 255.0).round().clamp(0.0, 255.0) as u8,
255,
];
}
lut
}
fn build_gradient_stops(
stops: &[stet_graphics::device::ColorStop],
) -> Vec<stet_tiny_skia::GradientStop> {
let mut result = Vec::with_capacity(stops.len());
for stop in stops {
let r = (stop.color.r * 255.0).round().clamp(0.0, 255.0) as u8;
let g = (stop.color.g * 255.0).round().clamp(0.0, 255.0) as u8;
let b = (stop.color.b * 255.0).round().clamp(0.0, 255.0) as u8;
result.push(stet_tiny_skia::GradientStop::new(
stop.position as f32,
Color::from_rgba8(r, g, b, 255),
));
}
result
}
fn interpolate_color_stops(
stops: &[stet_graphics::device::ColorStop],
position: f64,
) -> DeviceColor {
if stops.is_empty() {
return DeviceColor::from_gray(0.0);
}
if stops.len() == 1 || position <= stops[0].position {
return stops[0].color.clone();
}
if position >= stops.last().unwrap().position {
return stops.last().unwrap().color.clone();
}
for i in 1..stops.len() {
if position <= stops[i].position {
let t0 = stops[i - 1].position;
let t1 = stops[i].position;
let frac = if (t1 - t0).abs() < 1e-10 {
0.0
} else {
(position - t0) / (t1 - t0)
};
let c0 = &stops[i - 1].color;
let c1 = &stops[i].color;
return DeviceColor::from_rgb(
(c0.r + frac * (c1.r - c0.r)).clamp(0.0, 1.0),
(c0.g + frac * (c1.g - c0.g)).clamp(0.0, 1.0),
(c0.b + frac * (c1.b - c0.b)).clamp(0.0, 1.0),
);
}
}
stops.last().unwrap().color.clone()
}
fn interpolate_cmyk_from_stops(
stops: &[stet_graphics::device::ColorStop],
cs: &ShadingColorSpace,
t: f64,
color: &DeviceColor,
icc: Option<&IccCache>,
) -> (f64, f64, f64, f64) {
let rgb_to_cmyk = |c: &DeviceColor| -> (f64, f64, f64, f64) {
if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(c.r, c.g, c.b)) {
(cmyk[0], cmyk[1], cmyk[2], cmyk[3])
} else {
(
(1.0 - c.r).clamp(0.0, 1.0),
(1.0 - c.g).clamp(0.0, 1.0),
(1.0 - c.b).clamp(0.0, 1.0),
0.0,
)
}
};
match cs {
ShadingColorSpace::DeviceCMYK => {
if stops.len() == 1 {
let rc = &stops[0].raw_components;
if rc.len() >= 4 {
return (rc[0], rc[1], rc[2], rc[3]);
}
}
let mut lo = &stops[0];
let mut hi = stops.last().unwrap();
for i in 0..stops.len() - 1 {
if stops[i + 1].position >= t {
lo = &stops[i];
hi = &stops[i + 1];
break;
}
}
let span = hi.position - lo.position;
let frac = if span > 1e-10 {
(t - lo.position) / span
} else {
0.0
};
let frac = frac.clamp(0.0, 1.0);
if lo.raw_components.len() >= 4 && hi.raw_components.len() >= 4 {
(
lo.raw_components[0] + frac * (hi.raw_components[0] - lo.raw_components[0]),
lo.raw_components[1] + frac * (hi.raw_components[1] - lo.raw_components[1]),
lo.raw_components[2] + frac * (hi.raw_components[2] - lo.raw_components[2]),
lo.raw_components[3] + frac * (hi.raw_components[3] - lo.raw_components[3]),
)
} else {
rgb_to_cmyk(color)
}
}
_ => rgb_to_cmyk(color),
}
}
#[allow(clippy::too_many_arguments)]
fn interpolate_cmyk_from_vertices(
v0: &ShadingVertex,
v1: &ShadingVertex,
v2: &ShadingVertex,
w0: f64,
w1: f64,
w2: f64,
cs: &ShadingColorSpace,
r: f64,
g: f64,
b: f64,
icc: Option<&IccCache>,
) -> (f64, f64, f64, f64) {
let rgb_to_cmyk = |r: f64, g: f64, b: f64| -> (f64, f64, f64, f64) {
if let Some(cmyk) = icc.and_then(|i| i.convert_rgb_to_cmyk_readonly(r, g, b)) {
(cmyk[0], cmyk[1], cmyk[2], cmyk[3])
} else {
(
(1.0 - r).clamp(0.0, 1.0),
(1.0 - g).clamp(0.0, 1.0),
(1.0 - b).clamp(0.0, 1.0),
0.0,
)
}
};
match cs {
ShadingColorSpace::DeviceCMYK => {
if v0.raw_components.len() >= 4
&& v1.raw_components.len() >= 4
&& v2.raw_components.len() >= 4
{
(
w0 * v0.raw_components[0]
+ w1 * v1.raw_components[0]
+ w2 * v2.raw_components[0],
w0 * v0.raw_components[1]
+ w1 * v1.raw_components[1]
+ w2 * v2.raw_components[1],
w0 * v0.raw_components[2]
+ w1 * v1.raw_components[2]
+ w2 * v2.raw_components[2],
w0 * v0.raw_components[3]
+ w1 * v1.raw_components[3]
+ w2 * v2.raw_components[3],
)
} else {
rgb_to_cmyk(r, g, b)
}
}
_ => rgb_to_cmyk(r, g, b),
}
}
#[cfg(test)]
mod tests {
use super::*;
use stet_graphics::color::DashPattern;
use stet_graphics::device::{BgUcrState, HalftoneState, TransferState};
#[test]
fn test_create_device() {
let dev = SkiaDevice::new(100, 100);
assert_eq!(dev.page_size(), (100, 100));
}
#[test]
fn test_fill_rect() {
let mut dev = SkiaDevice::new(100, 100);
let mut path = PsPath::new();
path.segments.push(PathSegment::MoveTo(10.0, 10.0));
path.segments.push(PathSegment::LineTo(90.0, 10.0));
path.segments.push(PathSegment::LineTo(90.0, 90.0));
path.segments.push(PathSegment::LineTo(10.0, 90.0));
path.segments.push(PathSegment::ClosePath);
let params = FillParams {
color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
fill_rule: FillRule::NonZeroWinding,
ctm: Matrix::identity(),
is_text_glyph: false,
overprint: false,
overprint_mode: 0,
opm_paired: false,
painted_channels: 0,
is_device_cmyk: false,
spot_color: None,
rendering_intent: 0,
transfer: TransferState::default(),
halftone: HalftoneState::default(),
bg_ucr: BgUcrState::default(),
alpha: 1.0,
blend_mode: 0,
};
dev.fill_path(&path, ¶ms);
let pixel = dev.pixmap().pixel(50, 50).unwrap();
assert_eq!(pixel.red(), 255);
assert_eq!(pixel.green(), 0);
assert_eq!(pixel.blue(), 0);
}
#[test]
fn test_stroke_line() {
let mut dev = SkiaDevice::new(100, 100);
let mut path = PsPath::new();
path.segments.push(PathSegment::MoveTo(10.0, 50.0));
path.segments.push(PathSegment::LineTo(90.0, 50.0));
let params = StrokeParams {
color: DeviceColor::from_rgb(0.0, 0.0, 1.0),
line_width: 4.0,
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
miter_limit: 10.0,
dash_pattern: DashPattern::solid(),
ctm: Matrix::identity(),
stroke_adjust: false,
is_text_glyph: false,
overprint: false,
overprint_mode: 0,
opm_paired: false,
painted_channels: 0,
is_device_cmyk: false,
spot_color: None,
rendering_intent: 0,
transfer: TransferState::default(),
halftone: HalftoneState::default(),
bg_ucr: BgUcrState::default(),
alpha: 1.0,
blend_mode: 0,
};
dev.stroke_path(&path, ¶ms);
let pixel = dev.pixmap().pixel(50, 50).unwrap();
assert_eq!(pixel.blue(), 255);
}
#[test]
fn test_clip() {
let mut dev = SkiaDevice::new(100, 100);
let mut clip_path = PsPath::new();
clip_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
clip_path.segments.push(PathSegment::LineTo(50.0, 0.0));
clip_path.segments.push(PathSegment::LineTo(50.0, 100.0));
clip_path.segments.push(PathSegment::LineTo(0.0, 100.0));
clip_path.segments.push(PathSegment::ClosePath);
let clip_params = ClipParams {
fill_rule: FillRule::NonZeroWinding,
ctm: Matrix::identity(),
stroke_params: None,
};
dev.clip_path(&clip_path, &clip_params);
let mut fill_path = PsPath::new();
fill_path.segments.push(PathSegment::MoveTo(0.0, 0.0));
fill_path.segments.push(PathSegment::LineTo(100.0, 0.0));
fill_path.segments.push(PathSegment::LineTo(100.0, 100.0));
fill_path.segments.push(PathSegment::LineTo(0.0, 100.0));
fill_path.segments.push(PathSegment::ClosePath);
let fill_params = FillParams {
color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
fill_rule: FillRule::NonZeroWinding,
ctm: Matrix::identity(),
is_text_glyph: false,
overprint: false,
overprint_mode: 0,
opm_paired: false,
painted_channels: 0,
is_device_cmyk: false,
spot_color: None,
rendering_intent: 0,
transfer: TransferState::default(),
halftone: HalftoneState::default(),
bg_ucr: BgUcrState::default(),
alpha: 1.0,
blend_mode: 0,
};
dev.fill_path(&fill_path, &fill_params);
let left_pixel = dev.pixmap().pixel(25, 50).unwrap();
assert_eq!(left_pixel.red(), 255);
let right_pixel = dev.pixmap().pixel(75, 50).unwrap();
assert_eq!(right_pixel.red(), 255);
assert_eq!(right_pixel.green(), 255); }
#[test]
fn test_erase_page() {
let mut dev = SkiaDevice::new(100, 100);
let mut path = PsPath::new();
path.segments.push(PathSegment::MoveTo(0.0, 0.0));
path.segments.push(PathSegment::LineTo(100.0, 0.0));
path.segments.push(PathSegment::LineTo(100.0, 100.0));
path.segments.push(PathSegment::LineTo(0.0, 100.0));
path.segments.push(PathSegment::ClosePath);
let params = FillParams {
color: DeviceColor::from_rgb(1.0, 0.0, 0.0),
fill_rule: FillRule::NonZeroWinding,
ctm: Matrix::identity(),
is_text_glyph: false,
overprint: false,
overprint_mode: 0,
opm_paired: false,
painted_channels: 0,
is_device_cmyk: false,
spot_color: None,
rendering_intent: 0,
transfer: TransferState::default(),
halftone: HalftoneState::default(),
bg_ucr: BgUcrState::default(),
alpha: 1.0,
blend_mode: 0,
};
dev.fill_path(&path, ¶ms);
dev.erase_page();
let pixel = dev.pixmap().pixel(50, 50).unwrap();
assert_eq!(pixel.red(), 255);
assert_eq!(pixel.green(), 255);
assert_eq!(pixel.blue(), 255);
}
#[test]
fn test_show_page() {
let mut dev = SkiaDevice::new(10, 10);
let path = std::env::temp_dir().join("stet_test_output.png");
let path_str = path.to_string_lossy();
let result = dev.show_page(&path_str);
assert!(result.is_ok());
assert!(path.exists());
std::fs::remove_file(&path).ok();
}
#[test]
fn test_transform() {
let mut dev = SkiaDevice::new(200, 200);
let mut path = PsPath::new();
path.segments.push(PathSegment::MoveTo(0.0, 0.0));
path.segments.push(PathSegment::LineTo(10.0, 0.0));
path.segments.push(PathSegment::LineTo(10.0, 10.0));
path.segments.push(PathSegment::LineTo(0.0, 10.0));
path.segments.push(PathSegment::ClosePath);
let params = FillParams {
color: DeviceColor::from_rgb(0.0, 1.0, 0.0),
fill_rule: FillRule::NonZeroWinding,
ctm: Matrix::translate(100.0, 100.0),
is_text_glyph: false,
overprint: false,
overprint_mode: 0,
opm_paired: false,
painted_channels: 0,
is_device_cmyk: false,
spot_color: None,
rendering_intent: 0,
transfer: TransferState::default(),
halftone: HalftoneState::default(),
bg_ucr: BgUcrState::default(),
alpha: 1.0,
blend_mode: 0,
};
dev.fill_path(&path, ¶ms);
let pixel = dev.pixmap().pixel(105, 105).unwrap();
assert_eq!(pixel.green(), 255);
assert_eq!(pixel.red(), 0);
}
fn make_test_fill_at(x: f64, y: f64, w: f64, h: f64) -> DisplayElement {
let mut path = PsPath::new();
path.segments.push(PathSegment::MoveTo(x, y));
path.segments.push(PathSegment::LineTo(x + w, y));
path.segments.push(PathSegment::LineTo(x + w, y + h));
path.segments.push(PathSegment::LineTo(x, y + h));
path.segments.push(PathSegment::ClosePath);
DisplayElement::Fill {
path,
params: FillParams {
color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
fill_rule: FillRule::NonZeroWinding,
ctm: Matrix::identity(),
is_text_glyph: false,
overprint: false,
overprint_mode: 0,
opm_paired: false,
painted_channels: 0,
is_device_cmyk: false,
spot_color: None,
rendering_intent: 0,
transfer: TransferState::default(),
halftone: HalftoneState::default(),
bg_ucr: BgUcrState::default(),
alpha: 1.0,
blend_mode: 0,
},
}
}
#[test]
fn test_compute_paint_bounds_two_fills() {
let mut list = DisplayList::new();
list.push(make_test_fill_at(10.0, 20.0, 30.0, 40.0)); list.push(make_test_fill_at(100.0, 50.0, 50.0, 25.0));
let bounds = compute_paint_bounds(&list, 72.0).expect("expected union bounds");
assert!(
(bounds.x_min - 10.0).abs() < 1e-9,
"x_min was {}",
bounds.x_min
);
assert!(
(bounds.y_min - 20.0).abs() < 1e-9,
"y_min was {}",
bounds.y_min
);
assert!(
(bounds.x_max - 150.0).abs() < 1e-9,
"x_max was {}",
bounds.x_max
);
assert!(
(bounds.y_max - 75.0).abs() < 1e-9,
"y_max was {}",
bounds.y_max
);
}
#[test]
fn test_compute_paint_bounds_empty_list() {
let list = DisplayList::new();
assert!(compute_paint_bounds(&list, 72.0).is_none());
}
#[test]
fn test_compute_paint_bounds_only_clip_returns_none() {
let mut list = DisplayList::new();
list.push(DisplayElement::InitClip);
assert!(compute_paint_bounds(&list, 72.0).is_none());
}
#[test]
fn test_rasterize_mask_anchors_to_paint_bounds() {
use stet_graphics::display_list::{SoftMaskParams, SoftMaskSubtype};
let mut mask = DisplayList::new();
let mut path = PsPath::new();
path.segments.push(PathSegment::MoveTo(200.0, 300.0));
path.segments.push(PathSegment::LineTo(250.0, 300.0));
path.segments.push(PathSegment::LineTo(250.0, 340.0));
path.segments.push(PathSegment::LineTo(200.0, 340.0));
path.segments.push(PathSegment::ClosePath);
mask.push(DisplayElement::Fill {
path,
params: FillParams {
color: DeviceColor::from_rgb(1.0, 1.0, 1.0),
fill_rule: FillRule::NonZeroWinding,
ctm: Matrix::identity(),
is_text_glyph: false,
overprint: false,
overprint_mode: 0,
opm_paired: false,
painted_channels: 0,
is_device_cmyk: false,
spot_color: None,
rendering_intent: 0,
transfer: TransferState::default(),
halftone: HalftoneState::default(),
bg_ucr: BgUcrState::default(),
alpha: 1.0,
blend_mode: 0,
},
});
let params = SoftMaskParams {
subtype: SoftMaskSubtype::Luminosity,
bbox: [0.0, 0.0, 100.0, 100.0],
backdrop_color: None, transfer_invert: false,
has_nested_mask_scope: false,
parent_clip_bbox: None,
};
let raster =
rasterize_mask(&mask, ¶ms, None, false, 72.0, 1.0, 1.0).expect("expected raster");
assert_eq!(raster.origin_x, 199);
assert_eq!(raster.origin_y, 299);
assert_eq!(raster.width, 52);
assert_eq!(raster.height, 42);
assert_eq!(raster.scale_x, 1.0);
assert_eq!(raster.scale_y, 1.0);
let mx = 225 - raster.origin_x;
let my = 320 - raster.origin_y;
assert!(mx >= 0 && (mx as u32) < raster.width);
assert!(my >= 0 && (my as u32) < raster.height);
let center_value = raster.data[(my as usize) * raster.width as usize + mx as usize];
assert_eq!(
center_value, 255,
"center of painted mask should be opaque white (lum=255)"
);
let mx_out = 300 - raster.origin_x;
let in_bounds = mx_out >= 0 && (mx_out as u32) < raster.width;
assert!(!in_bounds, "page x=300 should be outside the mask raster");
assert_eq!(
out_of_bounds_mask_value(¶ms),
0,
"black backdrop → out-of-bounds = 0"
);
}
#[test]
fn test_band_local_to_mask_formula() {
let raster_origin_x = 200i32;
let raster_origin_y = 300i32;
let sample = |vp_x_dev: f32,
vp_y_dev: f32,
scale: f32,
crop_x: i32,
crop_y: i32,
x: i32,
y: i32|
-> (i32, i32) {
let vp_x_pixels = (vp_x_dev * scale).round() as i32;
let vp_y_pixels = (vp_y_dev * scale).round() as i32;
let page_x = vp_x_pixels + crop_x + x;
let page_y = vp_y_pixels + crop_y + y;
let mx = page_x - raster_origin_x;
let my = page_y - raster_origin_y;
(mx, my)
};
let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 0, 0);
assert_eq!((mx, my), (20, 10), "top band: smask top-left");
let (mx, my) = sample(0.0, 0.0, 1.0, 220, 310, 5, 5);
assert_eq!((mx, my), (25, 15), "top band: 5px into smask");
let (mx, my) = sample(0.0, 305.0, 1.0, 220, 5, 0, 0);
assert_eq!((mx, my), (20, 10), "mid band: smask top-left");
let vp_x_pixels = (100.0_f32 * 2.0).round() as i32;
let crop_x = ((220.0_f32 - 100.0) * 2.0).floor() as i32;
let page_x_for_x_zero = vp_x_pixels + crop_x;
assert_eq!(page_x_for_x_zero, 440, "viewport scale-2: page-x at x=0");
}
fn x_path() -> PsPath {
let mut p = PsPath::new();
p.segments.push(PathSegment::MoveTo(10.0, 10.0));
p.segments.push(PathSegment::LineTo(20.0, 20.0));
p.segments.push(PathSegment::LineTo(30.0, 10.0));
p.segments.push(PathSegment::LineTo(20.0, 0.0));
p.segments.push(PathSegment::ClosePath);
p
}
fn x_path_perturbed() -> PsPath {
let mut p = PsPath::new();
p.segments.push(PathSegment::MoveTo(10.001, 10.0));
p.segments.push(PathSegment::LineTo(20.0, 19.999));
p.segments.push(PathSegment::LineTo(30.002, 10.001));
p.segments.push(PathSegment::LineTo(19.999, 0.0));
p.segments.push(PathSegment::ClosePath);
p
}
fn fill(path: PsPath, alpha: f64, blend: u8) -> DisplayElement {
DisplayElement::Fill {
path,
params: FillParams {
color: DeviceColor::from_rgb(0.0, 0.0, 0.0),
fill_rule: FillRule::NonZeroWinding,
ctm: Matrix::identity(),
is_text_glyph: false,
overprint: false,
overprint_mode: 0,
opm_paired: false,
painted_channels: 0,
is_device_cmyk: false,
spot_color: None,
rendering_intent: 0,
transfer: TransferState::default(),
halftone: HalftoneState::default(),
bg_ucr: BgUcrState::default(),
alpha,
blend_mode: blend,
},
}
}
fn rect_path(x0: f64, y0: f64, x1: f64, y1: f64) -> PsPath {
let mut p = PsPath::new();
p.segments.push(PathSegment::MoveTo(x0, y0));
p.segments.push(PathSegment::LineTo(x1, y0));
p.segments.push(PathSegment::LineTo(x1, y1));
p.segments.push(PathSegment::LineTo(x0, y1));
p.segments.push(PathSegment::ClosePath);
p
}
fn clip_elem(path: PsPath) -> DisplayElement {
DisplayElement::Clip {
path,
params: ClipParams {
fill_rule: FillRule::NonZeroWinding,
ctm: Matrix::identity(),
stroke_params: None,
},
}
}
fn group_elem(
inner: Vec<DisplayElement>,
bbox: [f64; 4],
isolated: bool,
alpha: f64,
blend: u8,
) -> DisplayElement {
let mut dl = DisplayList::new();
for e in inner {
dl.push(e);
}
DisplayElement::Group {
elements: dl,
params: stet_graphics::display_list::GroupParams {
bbox,
isolated,
knockout: false,
blend_mode: blend,
alpha,
color_space: stet_graphics::display_list::GroupColorSpace::Inherited,
},
}
}
fn dl(elements: Vec<DisplayElement>) -> DisplayList {
let mut d = DisplayList::new();
for e in elements {
d.push(e);
}
d
}
#[test]
fn obscured_skip_fires_on_matching_fill_plus_iso_group() {
let parent = fill(x_path(), 1.0, 0);
let inner = vec![fill(x_path_perturbed(), 1.0, 0)];
let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
let d = dl(vec![
parent,
clip_elem(rect_path(0.0, -5.0, 40.0, 30.0)),
grp,
]);
assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
}
#[test]
fn obscured_skip_does_not_fire_on_non_isolated_group() {
let parent = fill(x_path(), 1.0, 0);
let inner = vec![fill(x_path(), 1.0, 0)];
let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], false, 1.0, 0);
let d = dl(vec![parent, grp]);
assert!(compute_obscured_fill_skips(&d).is_empty());
}
#[test]
fn obscured_skip_does_not_fire_on_partial_alpha_group() {
let parent = fill(x_path(), 1.0, 0);
let inner = vec![fill(x_path(), 1.0, 0)];
let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 0.5, 0);
let d = dl(vec![parent, grp]);
assert!(compute_obscured_fill_skips(&d).is_empty());
}
#[test]
fn obscured_skip_does_not_fire_on_non_normal_blend() {
let parent = fill(x_path(), 1.0, 0);
let inner = vec![fill(x_path(), 1.0, 0)];
let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 10);
let d = dl(vec![parent, grp]);
assert!(compute_obscured_fill_skips(&d).is_empty());
}
#[test]
fn obscured_skip_does_not_fire_when_paths_differ() {
let parent = fill(rect_path(0.0, 0.0, 5.0, 5.0), 1.0, 0);
let inner = vec![fill(x_path(), 1.0, 0)];
let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
let d = dl(vec![parent, grp]);
assert!(compute_obscured_fill_skips(&d).is_empty());
}
#[test]
fn obscured_skip_does_not_fire_when_group_bbox_too_small() {
let big = rect_path(0.0, 0.0, 100.0, 100.0);
let parent = fill(big.clone(), 1.0, 0);
let inner = vec![fill(big, 1.0, 0)];
let grp = group_elem(inner, [0.0, 0.0, 10.0, 10.0], true, 1.0, 0);
let d = dl(vec![parent, grp]);
assert!(compute_obscured_fill_skips(&d).is_empty());
}
#[test]
fn obscured_skip_does_not_fire_when_intervening_clip_too_small() {
let parent = fill(x_path(), 1.0, 0);
let narrow_clip = clip_elem(rect_path(12.0, 5.0, 18.0, 15.0));
let inner = vec![fill(x_path(), 1.0, 0)];
let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
let d = dl(vec![parent, narrow_clip, grp]);
assert!(compute_obscured_fill_skips(&d).is_empty());
}
#[test]
fn obscured_skip_does_not_fire_when_inner_clip_too_small() {
let parent = fill(x_path(), 1.0, 0);
let inner = vec![
clip_elem(rect_path(12.0, 5.0, 18.0, 15.0)),
fill(x_path(), 1.0, 0),
];
let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
let d = dl(vec![parent, grp]);
assert!(compute_obscured_fill_skips(&d).is_empty());
}
#[test]
fn obscured_skip_fires_when_inner_clip_is_wider_than_parent_path() {
let parent = fill(x_path(), 1.0, 0);
let inner = vec![
clip_elem(rect_path(-10.0, -10.0, 40.0, 30.0)),
fill(x_path_perturbed(), 1.0, 0),
];
let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
let d = dl(vec![parent, grp]);
assert_eq!(compute_obscured_fill_skips(&d), vec![0]);
}
#[test]
fn obscured_skip_does_not_fire_on_partial_alpha_parent() {
let parent = fill(x_path(), 0.5, 0);
let inner = vec![fill(x_path(), 1.0, 0)];
let grp = group_elem(inner, [0.0, -5.0, 40.0, 30.0], true, 1.0, 0);
let d = dl(vec![parent, grp]);
assert!(compute_obscured_fill_skips(&d).is_empty());
}
}