#![allow(
clippy::field_reassign_with_default,
clippy::ptr_arg,
clippy::only_used_in_recursion
)]
use std::collections::HashMap;
use std::sync::Arc;
use tiny_skia::{FillRule, Mask, PathBuilder, Pixmap, Transform};
use crate::content::graphics_state::{GraphicsState, GraphicsStateStack, Matrix};
use crate::content::operators::{Operator, TextElement};
use crate::content::parser::parse_content_stream;
use crate::document::PdfDocument;
use crate::error::{Error, Result};
use crate::fonts::FontInfo;
use crate::object::Object;
use super::ext_gstate::{parse_ext_g_state_inner, ParsedExtGState};
use super::resolution::{
InkName, PaintBackend, PaintIntent, PaintKind, PaintSide, ResolutionContext,
ResolutionPipeline, SeparationBackend, SeparationSurface,
};
use super::text_rasterizer::TextRasterizer;
use crate::rendering::resolution::{DeviceColor, LogicalColor};
use smallvec::SmallVec;
#[derive(Debug, Clone)]
pub struct SeparationPlate {
pub ink_name: String,
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
}
pub fn render_separations(
doc: &PdfDocument,
page_num: usize,
dpi: u32,
) -> Result<Vec<SeparationPlate>> {
let inks = collect_page_inks(doc, page_num)?;
if inks.is_empty() {
return Ok(Vec::new());
}
let referenced = collect_referenced_inks(doc, page_num)?;
render_plates_for_inks(doc, page_num, dpi, &inks, &referenced)
}
pub fn render_separation(
doc: &PdfDocument,
page_num: usize,
ink_name: &str,
dpi: u32,
) -> Result<SeparationPlate> {
let inks = vec![ink_name.to_string()];
let referenced = inks.clone();
let mut plates = render_plates_for_inks(doc, page_num, dpi, &inks, &referenced)?;
plates
.pop()
.ok_or_else(|| Error::InvalidPdf("render_separation: no plate produced".to_string()))
}
fn render_plates_for_inks(
doc: &PdfDocument,
page_num: usize,
dpi: u32,
inks: &[String],
referenced: &[String],
) -> Result<Vec<SeparationPlate>> {
let resources = doc.get_page_resources(page_num)?;
if crate::rendering::sidecar::page_declares_transparency(doc, &resources) {
return render_plates_via_composite(doc, page_num, dpi, inks, referenced);
}
let (width, height, base_transform) = compute_page_extent(doc, page_num, dpi)?;
let mut render_indices: Vec<usize> = Vec::new();
let mut empty_indices: Vec<usize> = Vec::new();
for (i, ink) in inks.iter().enumerate() {
if referenced.iter().any(|r| r == ink) {
render_indices.push(i);
} else {
empty_indices.push(i);
}
}
let mut pixmaps: Vec<Pixmap> = Vec::with_capacity(render_indices.len());
for _ in &render_indices {
let pixmap = Pixmap::new(width, height)
.ok_or_else(|| Error::InvalidPdf("Failed to create separation pixmap".to_string()))?;
pixmaps.push(pixmap);
}
let target_inks: Vec<&str> = render_indices.iter().map(|&i| inks[i].as_str()).collect();
if !pixmaps.is_empty() {
let color_spaces = load_color_spaces(doc, &resources)?;
let fonts = load_fonts(doc, &resources);
let text_rasterizer = TextRasterizer::new();
let content_data = doc.get_page_content_data(page_num)?;
let operators = parse_content_stream(&content_data)?;
let mut ctx = SeparationContext {
doc,
text_rasterizer: &text_rasterizer,
fonts: &fonts,
};
execute_separation_operators(
&mut pixmaps,
base_transform,
&operators,
&mut ctx,
&resources,
&color_spaces,
None,
&target_inks,
)?;
}
let pixel_count = (width as usize) * (height as usize);
let mut result: Vec<Option<SeparationPlate>> = (0..inks.len()).map(|_| None).collect();
for (k, &i) in render_indices.iter().enumerate() {
let mut data = vec![0u8; pixel_count];
let rgba = pixmaps[k].data();
for j in 0..pixel_count {
data[j] = rgba[j * 4];
}
result[i] = Some(SeparationPlate {
ink_name: inks[i].clone(),
data,
width,
height,
});
}
for &i in &empty_indices {
result[i] = Some(SeparationPlate {
ink_name: inks[i].clone(),
data: vec![0u8; pixel_count],
width,
height,
});
}
Ok(result
.into_iter()
.map(|o| o.expect("plate filled"))
.collect())
}
fn render_plates_via_composite(
doc: &PdfDocument,
page_num: usize,
dpi: u32,
inks: &[String],
referenced: &[String],
) -> Result<Vec<SeparationPlate>> {
use crate::rendering::page_renderer::{PageRenderer, RenderOptions};
let mut renderer = PageRenderer::new(RenderOptions::with_dpi(dpi).as_raw());
renderer.force_cmyk_sidecar = true;
let rendered = renderer.render_page(doc, page_num)?;
let width = rendered.width;
let height = rendered.height;
let pixel_count = (width as usize) * (height as usize);
let sidecar = renderer.take_cmyk_sidecar();
let mut plates: Vec<SeparationPlate> = Vec::with_capacity(inks.len());
for ink in inks {
let mut data = vec![0u8; pixel_count];
if !referenced.iter().any(|r| r == ink) {
plates.push(SeparationPlate {
ink_name: ink.clone(),
data,
width,
height,
});
continue;
}
if let Some(s) = sidecar.as_ref() {
if matches!(ink.as_str(), "Cyan" | "Magenta" | "Yellow" | "Black") {
if let Some(plate) = s.process_plate(ink) {
data = plate;
}
} else if let Some(lane) = s.spot_plate(ink) {
data = lane.to_vec();
}
}
plates.push(SeparationPlate {
ink_name: ink.clone(),
data,
width,
height,
});
}
Ok(plates)
}
fn collect_page_inks(doc: &PdfDocument, page_num: usize) -> Result<Vec<String>> {
let mut inks = vec![
"Cyan".to_string(),
"Magenta".to_string(),
"Yellow".to_string(),
"Black".to_string(),
];
let spot_inks = doc.get_page_inks_deep(page_num)?;
for ink in spot_inks {
if !inks.contains(&ink) {
inks.push(ink);
}
}
Ok(inks)
}
fn collect_referenced_inks(doc: &PdfDocument, page_num: usize) -> Result<Vec<String>> {
let resources = doc.get_page_resources(page_num)?;
let color_spaces = load_color_spaces(doc, &resources)?;
let content_data = doc.get_page_content_data(page_num)?;
let operators = parse_content_stream(&content_data)?;
let mut referenced: Vec<String> = Vec::new();
let mut visited: Vec<String> = Vec::new();
scan_operators_for_inks(
&operators,
doc,
&resources,
&color_spaces,
&mut referenced,
&mut visited,
)?;
Ok(referenced)
}
fn scan_operators_for_inks(
operators: &[Operator],
doc: &PdfDocument,
resources: &Object,
color_spaces: &HashMap<String, Object>,
referenced: &mut Vec<String>,
visited: &mut Vec<String>,
) -> Result<()> {
let xobjects = match resources {
Object::Dictionary(rd) => rd.get("XObject").and_then(|o| doc.resolve_object(o).ok()),
_ => None,
};
let push = |list: &mut Vec<String>, name: &str| {
if !list.iter().any(|s| s == name) {
list.push(name.to_string());
}
};
for op in operators {
match op {
Operator::SetFillCmyk { .. } | Operator::SetStrokeCmyk { .. } => {
push(referenced, "Cyan");
push(referenced, "Magenta");
push(referenced, "Yellow");
push(referenced, "Black");
},
Operator::SetFillColorSpace { name } | Operator::SetStrokeColorSpace { name } => {
inks_from_space(name, color_spaces, resources, doc, referenced);
},
Operator::Do { name } => {
if visited.iter().any(|s| s == name) {
continue;
}
visited.push(name.clone());
if let Some(xobj_dict) = xobjects.as_ref().and_then(|o| o.as_dict()) {
if let Some(xobj_ref_obj) = xobj_dict.get(name) {
if let Ok(xobj) = doc.resolve_object(xobj_ref_obj) {
if let Object::Stream { ref dict, .. } = xobj {
let subtype = dict.get("Subtype").and_then(|o| o.as_name());
if subtype == Some("Form") {
let stream_data = if let Some(r) = xobj_ref_obj.as_reference() {
doc.decode_stream_with_encryption(&xobj, r)?
} else {
xobj.decode_stream_data()?
};
let form_resources = if let Some(res) = dict.get("Resources") {
doc.resolve_object(res)?
} else {
resources.clone()
};
let form_cs = load_color_spaces(doc, &form_resources)?;
let mut merged_cs = color_spaces.clone();
merged_cs.extend(form_cs);
if let Ok(form_ops) = parse_content_stream(&stream_data) {
scan_operators_for_inks(
&form_ops,
doc,
&form_resources,
&merged_cs,
referenced,
visited,
)?;
}
} else if subtype == Some("Image") {
let resolved = resolve_image_color_space(
dict,
color_spaces,
resources,
doc,
);
match resolved {
ResolvedSpace::Cmyk | ResolvedSpace::IccCmyk => {
push(referenced, "Cyan");
push(referenced, "Magenta");
push(referenced, "Yellow");
push(referenced, "Black");
},
ResolvedSpace::Separation(ink) => {
if ink != "None" && !ink.is_empty() {
if ink == "All" {
push(referenced, "Cyan");
push(referenced, "Magenta");
push(referenced, "Yellow");
push(referenced, "Black");
} else {
push(referenced, &ink);
}
}
},
ResolvedSpace::DeviceN(names) => {
for n in names {
if n != "None" && !n.is_empty() {
if n == "All" {
push(referenced, "Cyan");
push(referenced, "Magenta");
push(referenced, "Yellow");
push(referenced, "Black");
} else {
push(referenced, &n);
}
}
}
},
_ => {},
}
}
}
}
}
}
},
_ => {},
}
}
Ok(())
}
fn inks_from_space(
space_name: &str,
color_spaces: &HashMap<String, Object>,
resources: &Object,
doc: &PdfDocument,
out: &mut Vec<String>,
) {
let space = resolve_color_space(space_name, color_spaces, resources, doc);
match space {
ResolvedSpace::Cmyk | ResolvedSpace::IccCmyk => {
for ink in ["Cyan", "Magenta", "Yellow", "Black"] {
if !out.iter().any(|s| s == ink) {
out.push(ink.to_string());
}
}
},
ResolvedSpace::Separation(name) => {
if name == "All" {
for ink in ["Cyan", "Magenta", "Yellow", "Black"] {
if !out.iter().any(|s| s == ink) {
out.push(ink.to_string());
}
}
} else if name != "None" && !out.iter().any(|s| s == &name) {
out.push(name);
}
},
ResolvedSpace::DeviceN(names) => {
for n in names {
if n == "All" {
for ink in ["Cyan", "Magenta", "Yellow", "Black"] {
if !out.iter().any(|s| s == ink) {
out.push(ink.to_string());
}
}
} else if n != "None" && !out.iter().any(|s| s == &n) {
out.push(n);
}
}
},
ResolvedSpace::Rgb
| ResolvedSpace::Gray
| ResolvedSpace::IccRgb
| ResolvedSpace::IccGray
| ResolvedSpace::Unknown => {},
}
}
fn compute_page_extent(
doc: &PdfDocument,
page_num: usize,
dpi: u32,
) -> Result<(u32, u32, Transform)> {
let page_info = doc.get_page_info(page_num)?;
let media_box = page_info.media_box;
let rotation = page_info.rotation % 360;
let (page_w, page_h) = if rotation == 90 || rotation == 270 {
(media_box.height, media_box.width)
} else {
(media_box.width, media_box.height)
};
let scale = dpi as f32 / 72.0;
let width = (page_w * scale).ceil() as u32;
let height = (page_h * scale).ceil() as u32;
let base_transform = match rotation {
90 => Transform::from_translate(-media_box.x, -media_box.y)
.post_concat(Transform::from_row(0.0, scale, scale, 0.0, 0.0, 0.0)),
180 => Transform::from_translate(-media_box.x, -media_box.y)
.post_scale(-scale, scale)
.post_translate(media_box.width * scale, 0.0),
270 => Transform::from_translate(-media_box.x, -media_box.y).post_concat(
Transform::from_row(0.0, scale, -scale, 0.0, media_box.height * scale, 0.0),
),
_ => Transform::from_translate(-media_box.x, -media_box.y)
.post_scale(scale, -scale)
.post_translate(0.0, page_h * scale),
};
Ok((width, height, base_transform))
}
#[derive(Debug, Clone)]
enum ResolvedSpace {
Cmyk,
Rgb,
Gray,
Separation(String),
DeviceN(Vec<String>),
IccCmyk,
IccRgb,
IccGray,
Unknown,
}
fn resolve_color_space(
space_name: &str,
color_spaces: &HashMap<String, Object>,
resources: &Object,
doc: &PdfDocument,
) -> ResolvedSpace {
let default_key = match space_name {
"DeviceCMYK" | "CMYK" => Some("DefaultCMYK"),
"DeviceRGB" | "RGB" => Some("DefaultRGB"),
"DeviceGray" | "G" => Some("DefaultGray"),
_ => None,
};
if let Some(key) = default_key {
if let Some(default) = color_spaces.get(key) {
return classify_resolved(default, color_spaces, resources, doc);
}
return match key {
"DefaultCMYK" => ResolvedSpace::Cmyk,
"DefaultRGB" => ResolvedSpace::Rgb,
_ => ResolvedSpace::Gray,
};
}
if let Some(cs_obj) = color_spaces.get(space_name) {
classify_resolved(cs_obj, color_spaces, resources, doc)
} else {
ResolvedSpace::Unknown
}
}
fn classify_resolved(
cs_obj: &Object,
color_spaces: &HashMap<String, Object>,
resources: &Object,
doc: &PdfDocument,
) -> ResolvedSpace {
if let Some(name) = cs_obj.as_name() {
return match name {
"DeviceCMYK" | "CMYK" => ResolvedSpace::Cmyk,
"DeviceRGB" | "RGB" => ResolvedSpace::Rgb,
"DeviceGray" | "G" => ResolvedSpace::Gray,
_ => resolve_color_space(name, color_spaces, resources, doc),
};
}
let arr = match cs_obj.as_array() {
Some(a) => a,
None => return ResolvedSpace::Unknown,
};
let type_name = match arr.first().and_then(|o| o.as_name()) {
Some(n) => n,
None => return ResolvedSpace::Unknown,
};
match type_name {
"DeviceCMYK" | "CMYK" => ResolvedSpace::Cmyk,
"DeviceRGB" | "RGB" => ResolvedSpace::Rgb,
"DeviceGray" | "G" => ResolvedSpace::Gray,
"Pattern" => {
match arr.get(1) {
Some(underlying) => {
let resolved = doc
.resolve_object(underlying)
.unwrap_or_else(|_| underlying.clone());
classify_resolved(&resolved, color_spaces, resources, doc)
},
None => ResolvedSpace::Unknown,
}
},
"Separation" => {
let ink = arr
.get(1)
.and_then(|o| o.as_name())
.map(|s| s.to_string())
.unwrap_or_default();
ResolvedSpace::Separation(ink)
},
"DeviceN" => {
if let Some(Object::Array(ink_names)) = arr.get(1) {
let names = ink_names
.iter()
.filter_map(|o| o.as_name().map(|s| s.to_string()))
.collect();
ResolvedSpace::DeviceN(names)
} else {
ResolvedSpace::Unknown
}
},
"ICCBased" => {
if let Some(stream_obj) = arr.get(1) {
if let Ok(resolved) = doc.resolve_object(stream_obj) {
if let Object::Stream { ref dict, .. } = resolved {
if let Some(n) = dict.get("N").and_then(|o| o.as_integer()) {
return match n {
4 => ResolvedSpace::IccCmyk,
3 => ResolvedSpace::IccRgb,
1 => ResolvedSpace::IccGray,
_ => ResolvedSpace::Unknown,
};
}
}
}
}
ResolvedSpace::Unknown
},
_ => ResolvedSpace::Unknown,
}
}
fn load_color_spaces(doc: &PdfDocument, resources: &Object) -> Result<HashMap<String, Object>> {
let mut color_spaces = HashMap::new();
if let Object::Dictionary(res_dict) = resources {
if let Some(cs_obj) = res_dict.get("ColorSpace") {
let cs_dict_obj = doc.resolve_object(cs_obj)?;
if let Some(cs_dict) = cs_dict_obj.as_dict() {
for (name, o) in cs_dict {
if let Ok(resolved_cs) = doc.resolve_object(o) {
color_spaces.insert(name.clone(), resolved_cs);
}
}
}
}
}
Ok(color_spaces)
}
fn load_fonts(doc: &PdfDocument, resources: &Object) -> HashMap<String, Arc<FontInfo>> {
let mut fonts = HashMap::new();
if let Object::Dictionary(res_dict) = resources {
if let Some(font_obj) = res_dict.get("Font") {
if let Ok(font_dict_obj) = doc.resolve_object(font_obj) {
if let Some(font_dict) = font_dict_obj.as_dict() {
for (name, f_obj) in font_dict {
if let Ok(info) = doc.get_or_load_font_for_rendering(f_obj) {
fonts.insert(name.clone(), info);
}
}
}
}
}
}
fonts
}
enum PaintAction {
Paint(f32),
Skip,
}
fn tint_for_ink(
fill: bool,
gs: &GraphicsState,
color_spaces: &HashMap<String, Object>,
resources: &Object,
doc: &PdfDocument,
target_ink: &str,
fill_components: &[f32],
stroke_components: &[f32],
) -> PaintAction {
let space_name = if fill {
&gs.fill_color_space
} else {
&gs.stroke_color_space
};
let components = if fill {
fill_components
} else {
stroke_components
};
let overprint = if fill {
gs.fill_overprint
} else {
gs.stroke_overprint
};
let opm = gs.overprint_mode;
let other_plate_action = if overprint {
PaintAction::Skip
} else {
PaintAction::Paint(0.0)
};
let resolved = resolve_color_space(space_name, color_spaces, resources, doc);
match resolved {
ResolvedSpace::Cmyk | ResolvedSpace::IccCmyk => {
let cmyk_state = if fill {
gs.fill_color_cmyk
} else {
gs.stroke_color_cmyk
};
let (c, m, y, k) = if let Some(v) = cmyk_state {
v
} else if components.len() >= 4 {
(components[0], components[1], components[2], components[3])
} else {
return PaintAction::Skip;
};
let tint = match target_ink {
"Cyan" => c,
"Magenta" => m,
"Yellow" => y,
"Black" => k,
_ => return other_plate_action,
};
if overprint && opm == 1 && tint == 0.0 {
PaintAction::Skip
} else {
PaintAction::Paint(tint)
}
},
ResolvedSpace::Rgb
| ResolvedSpace::Gray
| ResolvedSpace::IccRgb
| ResolvedSpace::IccGray => {
PaintAction::Skip
},
ResolvedSpace::Separation(ink) => {
if components.is_empty() || ink == "None" {
return PaintAction::Skip;
}
if ink == "All" {
return PaintAction::Paint(components[0]);
}
if ink == target_ink {
PaintAction::Paint(components[0])
} else {
other_plate_action
}
},
ResolvedSpace::DeviceN(names) => {
for (i, n) in names.iter().enumerate() {
if n == "None" {
continue;
}
if (n == "All" || n == target_ink) && i < components.len() {
return PaintAction::Paint(components[i]);
}
}
other_plate_action
},
ResolvedSpace::Unknown => PaintAction::Skip,
}
}
fn logical_color_for_side<'a>(
fill: bool,
gs: &'a GraphicsState,
cs: &'a SeparationColorState,
color_spaces: &'a HashMap<String, Object>,
) -> Option<LogicalColor<'a>> {
let space_name = if fill {
&gs.fill_color_space
} else {
&gs.stroke_color_space
};
let components = if fill {
&cs.fill_components
} else {
&cs.stroke_components
};
let cmyk_state = if fill {
gs.fill_color_cmyk
} else {
gs.stroke_color_cmyk
};
match space_name.as_str() {
"DeviceCMYK" | "CMYK" => {
let (c, m, y, k) = cmyk_state.or_else(|| {
if components.len() >= 4 {
Some((components[0], components[1], components[2], components[3]))
} else {
None
}
})?;
return Some(LogicalColor::Device(DeviceColor::Cmyk(c, m, y, k)));
},
"DeviceRGB" | "RGB" => {
if components.len() >= 3 {
return Some(LogicalColor::Device(DeviceColor::Rgb(
components[0],
components[1],
components[2],
)));
}
return None;
},
"DeviceGray" | "G" => {
if !components.is_empty() {
return Some(LogicalColor::Device(DeviceColor::Gray(components[0])));
}
return None;
},
_ => {},
}
let space = color_spaces.get(space_name)?;
let comps: SmallVec<[f32; 8]> = components.iter().copied().collect();
Some(LogicalColor::Spaced {
space,
components: comps,
})
}
#[allow(clippy::too_many_arguments)]
fn paint_through_pipeline(
fill: bool,
fill_rule: Option<FillRule>,
path: &tiny_skia::Path,
pixmaps: &mut [Pixmap],
target_inks: &[InkName],
base_transform: Transform,
gs: &GraphicsState,
cs: &SeparationColorState,
color_spaces: &HashMap<String, Object>,
resources: &Object,
doc: &PdfDocument,
clip: Option<&Mask>,
pipeline: &ResolutionPipeline,
backend: &mut SeparationBackend,
) -> Result<()> {
let _ = resources; let Some(logical) = logical_color_for_side(fill, gs, cs, color_spaces) else {
return Ok(());
};
let side = if fill {
PaintSide::Fill
} else {
PaintSide::Stroke
};
let intent = PaintIntent {
kind: PaintKind::Path {
path,
fill_rule: fill_rule.unwrap_or(FillRule::Winding),
},
side,
gs,
color: logical,
ctm: gs.ctm,
};
let output_intent = doc.output_intent_cmyk_profile();
let ctx = ResolutionContext::new(doc, color_spaces)
.with_output_intent(output_intent.as_ref())
.with_rendering_intent(crate::color::RenderingIntent::from_pdf_name(&gs.rendering_intent))
.with_defaults(
color_spaces.get("DefaultGray"),
color_spaces.get("DefaultRGB"),
color_spaces.get("DefaultCMYK"),
);
let cmd = pipeline.resolve(&intent, &ctx, None)?;
let surface = SeparationSurface {
pixmaps,
inks: target_inks,
base_transform,
};
let cmd = if let Some(mask) = clip {
let mut new = cmd;
new.clip = crate::rendering::resolution::ClipPlan::Mask(std::sync::Arc::new(mask.clone()));
new
} else {
cmd
};
backend.paint(&cmd, surface)?;
Ok(())
}
fn side_uses_pipeline(
fill: bool,
gs: &GraphicsState,
color_spaces: &HashMap<String, Object>,
resources: &Object,
doc: &PdfDocument,
) -> bool {
let space_name = if fill {
&gs.fill_color_space
} else {
&gs.stroke_color_space
};
if matches!(
space_name.as_str(),
"DeviceCMYK" | "CMYK" | "DeviceRGB" | "RGB" | "DeviceGray" | "G"
) {
return false;
}
matches!(
resolve_color_space(space_name, color_spaces, resources, doc),
ResolvedSpace::Separation(_)
| ResolvedSpace::DeviceN(_)
| ResolvedSpace::IccCmyk
| ResolvedSpace::IccRgb
| ResolvedSpace::IccGray
)
}
struct SeparationContext<'a> {
doc: &'a PdfDocument,
text_rasterizer: &'a TextRasterizer,
fonts: &'a HashMap<String, Arc<FontInfo>>,
}
#[derive(Clone, Debug)]
struct SeparationColorState {
fill_components: Vec<f32>,
stroke_components: Vec<f32>,
}
impl SeparationColorState {
fn new() -> Self {
Self {
fill_components: Vec::new(),
stroke_components: Vec::new(),
}
}
}
fn initial_components_for_space(
space_name: &str,
color_spaces: &HashMap<String, Object>,
resources: &Object,
doc: &PdfDocument,
) -> (Vec<f32>, Option<(f32, f32, f32, f32)>) {
let resolved = resolve_color_space(space_name, color_spaces, resources, doc);
match resolved {
ResolvedSpace::Cmyk | ResolvedSpace::IccCmyk => {
(vec![0.0, 0.0, 0.0, 1.0], Some((0.0, 0.0, 0.0, 1.0)))
},
ResolvedSpace::Rgb | ResolvedSpace::IccRgb => (vec![0.0, 0.0, 0.0], None),
ResolvedSpace::Gray | ResolvedSpace::IccGray => (vec![0.0], None),
ResolvedSpace::Separation(_) => (vec![1.0], None),
ResolvedSpace::DeviceN(names) => {
let n = names.len().max(1);
(vec![1.0; n], None)
},
ResolvedSpace::Unknown => (Vec::new(), None),
}
}
struct InheritedState {
fill_color_space: String,
stroke_color_space: String,
fill_color_cmyk: Option<(f32, f32, f32, f32)>,
stroke_color_cmyk: Option<(f32, f32, f32, f32)>,
fill_components: Vec<f32>,
stroke_components: Vec<f32>,
fill_overprint: bool,
stroke_overprint: bool,
overprint_mode: u8,
}
#[allow(clippy::too_many_arguments)]
fn execute_separation_operators(
pixmaps: &mut [Pixmap],
base_transform: Transform,
operators: &[Operator],
ctx: &mut SeparationContext<'_>,
resources: &Object,
color_spaces: &HashMap<String, Object>,
inherited: Option<&InheritedState>,
target_inks: &[&str],
) -> Result<()> {
debug_assert_eq!(pixmaps.len(), target_inks.len());
let mut gs_stack = GraphicsStateStack::new();
{
let gs = gs_stack.current_mut();
if let Some(inh) = inherited {
gs.fill_color_space = inh.fill_color_space.clone();
gs.stroke_color_space = inh.stroke_color_space.clone();
gs.fill_color_cmyk = inh.fill_color_cmyk;
gs.stroke_color_cmyk = inh.stroke_color_cmyk;
gs.fill_overprint = inh.fill_overprint;
gs.stroke_overprint = inh.stroke_overprint;
gs.overprint_mode = inh.overprint_mode;
} else {
gs.fill_color_space = "DeviceGray".to_string();
gs.stroke_color_space = "DeviceGray".to_string();
}
gs.fill_color_rgb = (0.0, 0.0, 0.0);
gs.stroke_color_rgb = (0.0, 0.0, 0.0);
}
let initial_cs = if let Some(inh) = inherited {
SeparationColorState {
fill_components: inh.fill_components.clone(),
stroke_components: inh.stroke_components.clone(),
}
} else {
SeparationColorState::new()
};
let mut color_state_stack: Vec<SeparationColorState> = vec![initial_cs];
let mut current_path = PathBuilder::new();
let mut pending_clip: Option<(tiny_skia::Path, FillRule)> = None;
let mut clip_stack: Vec<Option<Mask>> = vec![None];
let mut in_text_object = false;
let ext_g_state_resolved: Option<Object> = match resources {
Object::Dictionary(rd) => rd
.get("ExtGState")
.and_then(|o| ctx.doc.resolve_object(o).ok()),
_ => None,
};
let ext_g_states: Option<&HashMap<String, Object>> =
ext_g_state_resolved.as_ref().and_then(|o| o.as_dict());
let mut ext_g_state_cache: HashMap<String, ParsedExtGState> = HashMap::new();
let xobjects_resolved: Option<Object> = match resources {
Object::Dictionary(rd) => rd
.get("XObject")
.and_then(|o| ctx.doc.resolve_object(o).ok()),
_ => None,
};
let pixmap_width = pixmaps.first().map(|p| p.width()).unwrap_or(0);
let pixmap_height = pixmaps.first().map(|p| p.height()).unwrap_or(0);
let pipeline = ResolutionPipeline::new();
let mut backend = SeparationBackend::new();
let target_inks_owned: Vec<InkName> = target_inks.iter().map(|s| InkName::new(*s)).collect();
for op in operators {
match op {
Operator::SaveState => {
gs_stack.save();
let cs = color_state_stack
.last()
.cloned()
.unwrap_or_else(SeparationColorState::new);
color_state_stack.push(cs);
clip_stack.push(clip_stack.last().cloned().unwrap_or(None));
},
Operator::RestoreState => {
gs_stack.restore();
if color_state_stack.len() > 1 {
color_state_stack.pop();
}
if clip_stack.len() > 1 {
clip_stack.pop();
}
},
Operator::Cm { a, b, c, d, e, f } => {
let current = gs_stack.current_mut();
let new_matrix = Matrix {
a: *a,
b: *b,
c: *c,
d: *d,
e: *e,
f: *f,
};
current.ctm = new_matrix.multiply(¤t.ctm);
},
Operator::SetFillRgb { r, g, b } => {
let gs = gs_stack.current_mut();
gs.fill_color_rgb = (*r, *g, *b);
gs.fill_color_space = "DeviceRGB".to_string();
gs.fill_color_cmyk = None;
if let Some(cs) = color_state_stack.last_mut() {
cs.fill_components = vec![*r, *g, *b];
}
},
Operator::SetStrokeRgb { r, g, b } => {
let gs = gs_stack.current_mut();
gs.stroke_color_rgb = (*r, *g, *b);
gs.stroke_color_space = "DeviceRGB".to_string();
gs.stroke_color_cmyk = None;
if let Some(cs) = color_state_stack.last_mut() {
cs.stroke_components = vec![*r, *g, *b];
}
},
Operator::SetFillGray { gray } => {
let g = *gray;
let gs = gs_stack.current_mut();
gs.fill_color_rgb = (g, g, g);
gs.fill_color_space = "DeviceGray".to_string();
gs.fill_color_cmyk = None;
if let Some(cs) = color_state_stack.last_mut() {
cs.fill_components = vec![g];
}
},
Operator::SetStrokeGray { gray } => {
let g = *gray;
let gs = gs_stack.current_mut();
gs.stroke_color_rgb = (g, g, g);
gs.stroke_color_space = "DeviceGray".to_string();
gs.stroke_color_cmyk = None;
if let Some(cs) = color_state_stack.last_mut() {
cs.stroke_components = vec![g];
}
},
Operator::SetFillCmyk { c, m, y, k } => {
let gs = gs_stack.current_mut();
gs.fill_color_cmyk = Some((*c, *m, *y, *k));
gs.fill_color_space = "DeviceCMYK".to_string();
if let Some(cs) = color_state_stack.last_mut() {
cs.fill_components = vec![*c, *m, *y, *k];
}
},
Operator::SetStrokeCmyk { c, m, y, k } => {
let gs = gs_stack.current_mut();
gs.stroke_color_cmyk = Some((*c, *m, *y, *k));
gs.stroke_color_space = "DeviceCMYK".to_string();
if let Some(cs) = color_state_stack.last_mut() {
cs.stroke_components = vec![*c, *m, *y, *k];
}
},
Operator::SetFillColorSpace { name } => {
let (components, cmyk) =
initial_components_for_space(name, color_spaces, resources, ctx.doc);
let gs = gs_stack.current_mut();
gs.fill_color_space = name.clone();
gs.fill_color_cmyk = cmyk;
if let Some(cs) = color_state_stack.last_mut() {
cs.fill_components = components;
}
},
Operator::SetStrokeColorSpace { name } => {
let (components, cmyk) =
initial_components_for_space(name, color_spaces, resources, ctx.doc);
let gs = gs_stack.current_mut();
gs.stroke_color_space = name.clone();
gs.stroke_color_cmyk = cmyk;
if let Some(cs) = color_state_stack.last_mut() {
cs.stroke_components = components;
}
},
Operator::SetFillColor { components } | Operator::SetFillColorN { components, .. } => {
let gs = gs_stack.current_mut();
let space = gs.fill_color_space.clone();
match space.as_str() {
"DeviceCMYK" | "CMYK" if components.len() >= 4 => {
gs.fill_color_cmyk =
Some((components[0], components[1], components[2], components[3]));
},
_ => {},
}
if let Some(cs) = color_state_stack.last_mut() {
cs.fill_components = components.clone();
}
},
Operator::SetStrokeColor { components }
| Operator::SetStrokeColorN { components, .. } => {
let gs = gs_stack.current_mut();
let space = gs.stroke_color_space.clone();
match space.as_str() {
"DeviceCMYK" | "CMYK" if components.len() >= 4 => {
gs.stroke_color_cmyk =
Some((components[0], components[1], components[2], components[3]));
},
_ => {},
}
if let Some(cs) = color_state_stack.last_mut() {
cs.stroke_components = components.clone();
}
},
Operator::SetLineWidth { width } => {
gs_stack.current_mut().line_width = *width;
},
Operator::SetLineCap { cap_style } => {
gs_stack.current_mut().line_cap = *cap_style;
},
Operator::SetLineJoin { join_style } => {
gs_stack.current_mut().line_join = *join_style;
},
Operator::SetMiterLimit { limit } => {
gs_stack.current_mut().miter_limit = *limit;
},
Operator::SetDash { array, phase } => {
gs_stack.current_mut().dash_pattern = (array.clone(), *phase);
},
Operator::SetRenderingIntent { intent } => {
gs_stack.current_mut().rendering_intent = intent.clone();
},
Operator::MoveTo { x, y } => {
current_path.move_to(*x, *y);
},
Operator::LineTo { x, y } => {
current_path.line_to(*x, *y);
},
Operator::CurveTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => {
current_path.cubic_to(*x1, *y1, *x2, *y2, *x3, *y3);
},
Operator::CurveToV { x2, y2, x3, y3 } => {
if let Some(last) = current_path.last_point() {
current_path.cubic_to(last.x, last.y, *x2, *y2, *x3, *y3);
}
},
Operator::CurveToY { x1, y1, x3, y3 } => {
current_path.cubic_to(*x1, *y1, *x3, *y3, *x3, *y3);
},
Operator::Rectangle {
x,
y,
width,
height,
} => {
let (nx, nw) = if *width < 0.0 {
(x + width, -width)
} else {
(*x, *width)
};
let (ny, nh) = if *height < 0.0 {
(y + height, -height)
} else {
(*y, *height)
};
if let Some(rect) = tiny_skia::Rect::from_xywh(nx, ny, nw, nh) {
current_path.push_rect(rect);
}
},
Operator::ClosePath => {
current_path.close();
},
Operator::Stroke => {
apply_separation_clip(
&mut pending_clip,
&mut clip_stack,
pixmap_width,
pixmap_height,
base_transform,
&gs_stack,
);
if let Some(path) = current_path.finish() {
let gs = gs_stack.current();
let empty = SeparationColorState::new();
let cs = color_state_stack.last().unwrap_or(&empty);
let transform = combine_transforms(base_transform, &gs.ctm);
let clip = clip_stack.last().and_then(|c| c.as_ref());
if side_uses_pipeline(false, gs, color_spaces, resources, ctx.doc) {
paint_through_pipeline(
false,
None,
&path,
pixmaps,
&target_inks_owned,
base_transform,
gs,
cs,
color_spaces,
resources,
ctx.doc,
clip,
&pipeline,
&mut backend,
)?;
} else {
for (i, &ink) in target_inks.iter().enumerate() {
if let PaintAction::Paint(tint) = tint_for_ink(
false,
gs,
color_spaces,
resources,
ctx.doc,
ink,
&cs.fill_components,
&cs.stroke_components,
) {
stroke_separation(
&mut pixmaps[i],
&path,
transform,
gs,
tint,
clip,
);
}
}
}
}
current_path = PathBuilder::new();
},
Operator::Fill => {
apply_separation_clip(
&mut pending_clip,
&mut clip_stack,
pixmap_width,
pixmap_height,
base_transform,
&gs_stack,
);
if let Some(path) = current_path.finish() {
let gs = gs_stack.current();
let empty = SeparationColorState::new();
let cs = color_state_stack.last().unwrap_or(&empty);
let transform = combine_transforms(base_transform, &gs.ctm);
let clip = clip_stack.last().and_then(|c| c.as_ref());
if side_uses_pipeline(true, gs, color_spaces, resources, ctx.doc) {
paint_through_pipeline(
true,
Some(FillRule::Winding),
&path,
pixmaps,
&target_inks_owned,
base_transform,
gs,
cs,
color_spaces,
resources,
ctx.doc,
clip,
&pipeline,
&mut backend,
)?;
} else {
for (i, &ink) in target_inks.iter().enumerate() {
if let PaintAction::Paint(tint) = tint_for_ink(
true,
gs,
color_spaces,
resources,
ctx.doc,
ink,
&cs.fill_components,
&cs.stroke_components,
) {
fill_separation(
&mut pixmaps[i],
&path,
transform,
tint,
FillRule::Winding,
clip,
);
}
}
}
}
current_path = PathBuilder::new();
},
Operator::FillEvenOdd => {
apply_separation_clip(
&mut pending_clip,
&mut clip_stack,
pixmap_width,
pixmap_height,
base_transform,
&gs_stack,
);
if let Some(path) = current_path.finish() {
let gs = gs_stack.current();
let empty = SeparationColorState::new();
let cs = color_state_stack.last().unwrap_or(&empty);
let transform = combine_transforms(base_transform, &gs.ctm);
let clip = clip_stack.last().and_then(|c| c.as_ref());
if side_uses_pipeline(true, gs, color_spaces, resources, ctx.doc) {
paint_through_pipeline(
true,
Some(FillRule::EvenOdd),
&path,
pixmaps,
&target_inks_owned,
base_transform,
gs,
cs,
color_spaces,
resources,
ctx.doc,
clip,
&pipeline,
&mut backend,
)?;
} else {
for (i, &ink) in target_inks.iter().enumerate() {
if let PaintAction::Paint(tint) = tint_for_ink(
true,
gs,
color_spaces,
resources,
ctx.doc,
ink,
&cs.fill_components,
&cs.stroke_components,
) {
fill_separation(
&mut pixmaps[i],
&path,
transform,
tint,
FillRule::EvenOdd,
clip,
);
}
}
}
}
current_path = PathBuilder::new();
},
Operator::FillStroke | Operator::CloseFillStroke => {
apply_separation_clip(
&mut pending_clip,
&mut clip_stack,
pixmap_width,
pixmap_height,
base_transform,
&gs_stack,
);
if let Some(path) = current_path.finish() {
let gs = gs_stack.current();
let empty = SeparationColorState::new();
let cs = color_state_stack.last().unwrap_or(&empty);
let transform = combine_transforms(base_transform, &gs.ctm);
let clip = clip_stack.last().and_then(|c| c.as_ref());
if side_uses_pipeline(true, gs, color_spaces, resources, ctx.doc) {
paint_through_pipeline(
true,
Some(FillRule::Winding),
&path,
pixmaps,
&target_inks_owned,
base_transform,
gs,
cs,
color_spaces,
resources,
ctx.doc,
clip,
&pipeline,
&mut backend,
)?;
} else {
for (i, &ink) in target_inks.iter().enumerate() {
if let PaintAction::Paint(tint) = tint_for_ink(
true,
gs,
color_spaces,
resources,
ctx.doc,
ink,
&cs.fill_components,
&cs.stroke_components,
) {
fill_separation(
&mut pixmaps[i],
&path,
transform,
tint,
FillRule::Winding,
clip,
);
}
}
}
if side_uses_pipeline(false, gs, color_spaces, resources, ctx.doc) {
paint_through_pipeline(
false,
None,
&path,
pixmaps,
&target_inks_owned,
base_transform,
gs,
cs,
color_spaces,
resources,
ctx.doc,
clip,
&pipeline,
&mut backend,
)?;
} else {
for (i, &ink) in target_inks.iter().enumerate() {
if let PaintAction::Paint(tint) = tint_for_ink(
false,
gs,
color_spaces,
resources,
ctx.doc,
ink,
&cs.fill_components,
&cs.stroke_components,
) {
stroke_separation(
&mut pixmaps[i],
&path,
transform,
gs,
tint,
clip,
);
}
}
}
}
current_path = PathBuilder::new();
},
Operator::FillStrokeEvenOdd | Operator::CloseFillStrokeEvenOdd => {
apply_separation_clip(
&mut pending_clip,
&mut clip_stack,
pixmap_width,
pixmap_height,
base_transform,
&gs_stack,
);
if let Some(path) = current_path.finish() {
let gs = gs_stack.current();
let empty = SeparationColorState::new();
let cs = color_state_stack.last().unwrap_or(&empty);
let transform = combine_transforms(base_transform, &gs.ctm);
let clip = clip_stack.last().and_then(|c| c.as_ref());
if side_uses_pipeline(true, gs, color_spaces, resources, ctx.doc) {
paint_through_pipeline(
true,
Some(FillRule::EvenOdd),
&path,
pixmaps,
&target_inks_owned,
base_transform,
gs,
cs,
color_spaces,
resources,
ctx.doc,
clip,
&pipeline,
&mut backend,
)?;
} else {
for (i, &ink) in target_inks.iter().enumerate() {
if let PaintAction::Paint(tint) = tint_for_ink(
true,
gs,
color_spaces,
resources,
ctx.doc,
ink,
&cs.fill_components,
&cs.stroke_components,
) {
fill_separation(
&mut pixmaps[i],
&path,
transform,
tint,
FillRule::EvenOdd,
clip,
);
}
}
}
if side_uses_pipeline(false, gs, color_spaces, resources, ctx.doc) {
paint_through_pipeline(
false,
None,
&path,
pixmaps,
&target_inks_owned,
base_transform,
gs,
cs,
color_spaces,
resources,
ctx.doc,
clip,
&pipeline,
&mut backend,
)?;
} else {
for (i, &ink) in target_inks.iter().enumerate() {
if let PaintAction::Paint(tint) = tint_for_ink(
false,
gs,
color_spaces,
resources,
ctx.doc,
ink,
&cs.fill_components,
&cs.stroke_components,
) {
stroke_separation(
&mut pixmaps[i],
&path,
transform,
gs,
tint,
clip,
);
}
}
}
}
current_path = PathBuilder::new();
},
Operator::EndPath => {
apply_separation_clip(
&mut pending_clip,
&mut clip_stack,
pixmap_width,
pixmap_height,
base_transform,
&gs_stack,
);
current_path = PathBuilder::new();
},
Operator::ClipNonZero => {
if let Some(path) = current_path.clone().finish() {
pending_clip = Some((path, FillRule::Winding));
}
},
Operator::ClipEvenOdd => {
if let Some(path) = current_path.clone().finish() {
pending_clip = Some((path, FillRule::EvenOdd));
}
},
Operator::BeginText => {
in_text_object = true;
let gs = gs_stack.current_mut();
gs.text_matrix = Matrix::identity();
gs.text_line_matrix = Matrix::identity();
},
Operator::EndText => {
in_text_object = false;
},
Operator::Tc { char_space } => {
gs_stack.current_mut().char_space = *char_space;
},
Operator::Tw { word_space } => {
gs_stack.current_mut().word_space = *word_space;
},
Operator::Tz { scale } => {
gs_stack.current_mut().horizontal_scaling = *scale;
},
Operator::TL { leading } => {
gs_stack.current_mut().leading = *leading;
},
Operator::Ts { rise } => {
gs_stack.current_mut().text_rise = *rise;
},
Operator::Tr { render } => {
gs_stack.current_mut().render_mode = *render;
},
Operator::Tf { font, size } => {
let wmode = ctx.fonts.get(font).map(|f| f.wmode).unwrap_or(0);
let gs = gs_stack.current_mut();
gs.font_name = Some(font.clone());
gs.font_size = *size;
gs.text_wmode = wmode;
},
Operator::Td { tx, ty } => {
if in_text_object {
let gs = gs_stack.current_mut();
let translation = Matrix::translation(*tx, *ty);
gs.text_line_matrix = translation.multiply(&gs.text_line_matrix);
gs.text_matrix = gs.text_line_matrix;
}
},
Operator::TD { tx, ty } => {
if in_text_object {
let gs = gs_stack.current_mut();
gs.leading = -(*ty);
let translation = Matrix::translation(*tx, *ty);
gs.text_line_matrix = translation.multiply(&gs.text_line_matrix);
gs.text_matrix = gs.text_line_matrix;
}
},
Operator::Tm { a, b, c, d, e, f } => {
if in_text_object {
let gs = gs_stack.current_mut();
gs.text_matrix = Matrix {
a: *a,
b: *b,
c: *c,
d: *d,
e: *e,
f: *f,
};
gs.text_line_matrix = gs.text_matrix;
}
},
Operator::TStar => {
if in_text_object {
let gs = gs_stack.current_mut();
let leading = gs.leading;
let translation = Matrix::translation(0.0, -leading);
gs.text_line_matrix = translation.multiply(&gs.text_line_matrix);
gs.text_matrix = gs.text_line_matrix;
}
},
Operator::Tj { text } => {
if in_text_object {
let advance = render_text_to_plate(
pixmaps,
text,
base_transform,
&mut gs_stack,
&color_state_stack,
color_spaces,
resources,
ctx,
clip_stack.last().and_then(|c| c.as_ref()),
target_inks,
)?;
gs_stack.current_mut().advance_text_matrix(advance);
}
},
Operator::TJ { array } => {
if in_text_object {
let advance = render_tj_to_plate(
pixmaps,
array,
base_transform,
&mut gs_stack,
&color_state_stack,
color_spaces,
resources,
ctx,
clip_stack.last().and_then(|c| c.as_ref()),
target_inks,
)?;
gs_stack.current_mut().advance_text_matrix(advance);
}
},
Operator::Quote { text } => {
if in_text_object {
let gs_mut = gs_stack.current_mut();
let leading = gs_mut.leading;
let translation = Matrix::translation(0.0, -leading);
gs_mut.text_line_matrix = translation.multiply(&gs_mut.text_line_matrix);
gs_mut.text_matrix = gs_mut.text_line_matrix;
let advance = render_text_to_plate(
pixmaps,
text,
base_transform,
&mut gs_stack,
&color_state_stack,
color_spaces,
resources,
ctx,
clip_stack.last().and_then(|c| c.as_ref()),
target_inks,
)?;
gs_stack.current_mut().advance_text_matrix(advance);
}
},
Operator::DoubleQuote {
word_space,
char_space,
text,
} => {
if in_text_object {
let gs_mut = gs_stack.current_mut();
gs_mut.word_space = *word_space;
gs_mut.char_space = *char_space;
let leading = gs_mut.leading;
let translation = Matrix::translation(0.0, -leading);
gs_mut.text_line_matrix = translation.multiply(&gs_mut.text_line_matrix);
gs_mut.text_matrix = gs_mut.text_line_matrix;
let advance = render_text_to_plate(
pixmaps,
text,
base_transform,
&mut gs_stack,
&color_state_stack,
color_spaces,
resources,
ctx,
clip_stack.last().and_then(|c| c.as_ref()),
target_inks,
)?;
gs_stack.current_mut().advance_text_matrix(advance);
}
},
Operator::SetExtGState { dict_name } => {
let entry = ext_g_state_cache
.entry(dict_name.clone())
.or_insert_with(|| {
if let Some(states) = ext_g_states {
if let Some(state_obj) = states.get(dict_name) {
return parse_ext_g_state_inner(state_obj, ctx.doc)
.unwrap_or_default();
}
}
ParsedExtGState::default()
});
entry.apply(gs_stack.current_mut());
},
Operator::Do { name } => {
if let Some(xobjects) = xobjects_resolved.as_ref().and_then(|o| o.as_dict()) {
if let Some(xobj_ref_obj) = xobjects.get(name) {
if let Ok(xobj) = ctx.doc.resolve_object(xobj_ref_obj) {
if let Object::Stream { ref dict, .. } = xobj {
if let Some(subtype) = dict.get("Subtype").and_then(|o| o.as_name())
{
if subtype == "Image" {
let xobj_ref = xobj_ref_obj.as_reference();
paint_image_to_plates(
pixmaps,
name,
&xobj,
xobj_ref,
base_transform,
&gs_stack,
color_state_stack.last(),
color_spaces,
resources,
ctx,
clip_stack.last().and_then(|c| c.as_ref()),
target_inks,
)?;
} else if subtype == "Form" {
let xobj_ref = xobj_ref_obj.as_reference();
let stream_data = if let Some(r) = xobj_ref {
ctx.doc.decode_stream_with_encryption(&xobj, r)?
} else {
xobj.decode_stream_data()?
};
let form_resources =
if let Some(res) = dict.get("Resources") {
ctx.doc.resolve_object(res)?
} else {
resources.clone()
};
let form_cs = load_color_spaces(ctx.doc, &form_resources)?;
let mut merged_cs = color_spaces.clone();
merged_cs.extend(form_cs);
let form_matrix = parse_form_matrix(dict);
let gs = gs_stack.current();
let combined = combine_transforms(base_transform, &gs.ctm)
.pre_concat(form_matrix);
let empty = SeparationColorState::new();
let cs = color_state_stack.last().unwrap_or(&empty);
let inherited = InheritedState {
fill_color_space: gs.fill_color_space.clone(),
stroke_color_space: gs.stroke_color_space.clone(),
fill_color_cmyk: gs.fill_color_cmyk,
stroke_color_cmyk: gs.stroke_color_cmyk,
fill_components: cs.fill_components.clone(),
stroke_components: cs.stroke_components.clone(),
fill_overprint: gs.fill_overprint,
stroke_overprint: gs.stroke_overprint,
overprint_mode: gs.overprint_mode,
};
let form_ops = parse_content_stream(&stream_data)?;
execute_separation_operators(
pixmaps,
combined,
&form_ops,
ctx,
&form_resources,
&merged_cs,
Some(&inherited),
target_inks,
)?;
}
}
}
}
}
}
},
_ => {},
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn render_text_to_plate(
pixmaps: &mut [Pixmap],
text: &[u8],
base_transform: Transform,
gs_stack: &mut GraphicsStateStack,
color_state_stack: &[SeparationColorState],
color_spaces: &HashMap<String, Object>,
resources: &Object,
ctx: &mut SeparationContext<'_>,
clip: Option<&Mask>,
target_inks: &[&str],
) -> Result<f32> {
let gs = gs_stack.current();
let empty = SeparationColorState::new();
let cs = color_state_stack.last().unwrap_or(&empty);
if gs.render_mode == 3 {
return measure_text_advance(text, gs, ctx.fonts);
}
let transform = combine_transforms(base_transform, &gs.ctm);
let mut painted_advance: Option<f32> = None;
for (i, &ink) in target_inks.iter().enumerate() {
let tint = match tint_for_ink(
true,
gs,
color_spaces,
resources,
ctx.doc,
ink,
&cs.fill_components,
&cs.stroke_components,
) {
PaintAction::Paint(t) => t,
PaintAction::Skip => continue,
};
let mut faux = gs.clone();
faux.fill_color_rgb = (tint, tint, tint);
faux.fill_alpha = 1.0;
faux.blend_mode = "Normal".to_string();
let advance = ctx.text_rasterizer.render_text(
&mut pixmaps[i],
text,
transform,
&faux,
None,
resources,
ctx.doc,
clip,
ctx.fonts,
)?;
painted_advance = Some(advance);
}
match painted_advance {
Some(a) => Ok(a),
None => measure_text_advance(text, gs, ctx.fonts),
}
}
#[allow(clippy::too_many_arguments)]
fn render_tj_to_plate(
pixmaps: &mut [Pixmap],
array: &[TextElement],
base_transform: Transform,
gs_stack: &mut GraphicsStateStack,
color_state_stack: &[SeparationColorState],
color_spaces: &HashMap<String, Object>,
resources: &Object,
ctx: &mut SeparationContext<'_>,
clip: Option<&Mask>,
target_inks: &[&str],
) -> Result<f32> {
let mut total_advance = 0.0;
for element in array {
match element {
TextElement::String(text) => {
let advance = render_text_to_plate(
pixmaps,
text,
base_transform,
gs_stack,
color_state_stack,
color_spaces,
resources,
ctx,
clip,
target_inks,
)?;
gs_stack.current_mut().advance_text_matrix(advance);
total_advance += advance;
},
TextElement::Offset(offset) => {
let shift = (-*offset / 1000.0) * gs_stack.current().font_size;
gs_stack.current_mut().advance_text_matrix(shift);
total_advance += shift;
},
}
}
Ok(total_advance)
}
fn measure_text_advance(
text: &[u8],
gs: &GraphicsState,
fonts: &HashMap<String, Arc<FontInfo>>,
) -> Result<f32> {
let font_info = gs
.font_name
.as_ref()
.and_then(|n| fonts.get(n))
.map(Arc::clone);
let mut units: f32 = 0.0;
let mut count: usize = 0;
if let Some(info) = font_info.as_ref() {
if info.subtype != "Type0" {
for &b in text {
units += info.get_glyph_width(b as u16);
count += 1;
}
} else {
let mut i = 0;
while i + 1 < text.len() {
let code = ((text[i] as u16) << 8) | text[i + 1] as u16;
units += info.get_glyph_width(code);
count += 1;
i += 2;
}
}
} else {
for _ in text {
units += 500.0;
count += 1;
}
}
let advance = units * gs.font_size / 1000.0 + (count as f32) * gs.char_space;
Ok(advance)
}
pub(crate) fn fill_separation(
pixmap: &mut Pixmap,
path: &tiny_skia::Path,
transform: Transform,
tint: f32,
fill_rule: FillRule,
clip: Option<&Mask>,
) {
let gray = (tint.clamp(0.0, 1.0) * 255.0).round() as u8;
let color = tiny_skia::Color::from_rgba8(gray, gray, gray, 255);
let mut paint = tiny_skia::Paint::default();
paint.set_color(color);
paint.anti_alias = true;
paint.blend_mode = tiny_skia::BlendMode::SourceOver;
pixmap.fill_path(path, &paint, fill_rule, transform, clip);
}
fn stroke_separation(
pixmap: &mut Pixmap,
path: &tiny_skia::Path,
transform: Transform,
gs: &GraphicsState,
tint: f32,
clip: Option<&Mask>,
) {
let gray = (tint.clamp(0.0, 1.0) * 255.0).round() as u8;
let color = tiny_skia::Color::from_rgba8(gray, gray, gray, 255);
let mut paint = tiny_skia::Paint::default();
paint.set_color(color);
paint.anti_alias = true;
let mut stroke = tiny_skia::Stroke::default();
stroke.width = gs.line_width;
stroke.line_cap = match gs.line_cap {
1 => tiny_skia::LineCap::Round,
2 => tiny_skia::LineCap::Square,
_ => tiny_skia::LineCap::Butt,
};
stroke.line_join = match gs.line_join {
1 => tiny_skia::LineJoin::Round,
2 => tiny_skia::LineJoin::Bevel,
_ => tiny_skia::LineJoin::Miter,
};
stroke.miter_limit = gs.miter_limit;
if !gs.dash_pattern.0.is_empty() {
stroke.dash = tiny_skia::StrokeDash::new(gs.dash_pattern.0.clone(), gs.dash_pattern.1);
}
pixmap.stroke_path(path, &paint, &stroke, transform, clip);
}
fn apply_separation_clip(
pending: &mut Option<(tiny_skia::Path, FillRule)>,
clip_stack: &mut Vec<Option<Mask>>,
pixmap_width: u32,
pixmap_height: u32,
base_transform: Transform,
gs_stack: &GraphicsStateStack,
) {
if let Some((path, fill_rule)) = pending.take() {
if pixmap_width == 0 || pixmap_height == 0 {
return;
}
let gs = gs_stack.current();
let transform = combine_transforms(base_transform, &gs.ctm);
if let Some(path_transformed) = path.transform(transform) {
let mut new_mask = Mask::new(pixmap_width, pixmap_height).unwrap();
new_mask.fill_path(&path_transformed, fill_rule, true, Transform::identity());
if let Some(Some(current_mask)) = clip_stack.last() {
let mut combined = current_mask.clone();
let combined_data = combined.data_mut();
let new_data = new_mask.data();
for i in 0..combined_data.len() {
combined_data[i] = ((combined_data[i] as u32 * new_data[i] as u32) / 255) as u8;
}
*clip_stack.last_mut().unwrap() = Some(combined);
} else {
*clip_stack.last_mut().unwrap() = Some(new_mask);
}
}
}
}
fn parse_form_matrix(dict: &HashMap<String, Object>) -> Transform {
if let Some(Object::Array(arr)) = dict.get("Matrix") {
let get_f32 = |i: usize| -> f32 {
match arr.get(i) {
Some(Object::Real(v)) => *v as f32,
Some(Object::Integer(v)) => *v as f32,
_ => {
if i == 0 || i == 3 {
1.0
} else {
0.0
}
},
}
};
Transform::from_row(get_f32(0), get_f32(1), get_f32(2), get_f32(3), get_f32(4), get_f32(5))
} else {
Transform::identity()
}
}
fn combine_transforms(base: Transform, ctm: &Matrix) -> Transform {
base.pre_concat(Transform::from_row(ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f))
}
fn resolve_image_color_space(
image_dict: &HashMap<String, Object>,
color_spaces: &HashMap<String, Object>,
resources: &Object,
doc: &PdfDocument,
) -> ResolvedSpace {
let cs_obj = match image_dict.get("ColorSpace") {
Some(o) => o,
None => return ResolvedSpace::Unknown,
};
let resolved_obj = match cs_obj.as_reference() {
Some(r) => match doc.load_object(r) {
Ok(o) => o,
Err(_) => return ResolvedSpace::Unknown,
},
None => cs_obj.clone(),
};
if let Some(name) = resolved_obj.as_name() {
return resolve_color_space(name, color_spaces, resources, doc);
}
classify_resolved(&resolved_obj, color_spaces, resources, doc)
}
fn image_channel_for_ink(space: &ResolvedSpace, ink: &str) -> Option<usize> {
match space {
ResolvedSpace::Cmyk | ResolvedSpace::IccCmyk => match ink {
"Cyan" => Some(0),
"Magenta" => Some(1),
"Yellow" => Some(2),
"Black" => Some(3),
_ => None,
},
ResolvedSpace::Separation(name) => {
if name == "None" {
None
} else if name == "All" || name == ink {
Some(0)
} else {
None
}
},
ResolvedSpace::DeviceN(names) => names
.iter()
.position(|n| n.as_str() != "None" && (n == "All" || n == ink)),
ResolvedSpace::Rgb
| ResolvedSpace::Gray
| ResolvedSpace::IccRgb
| ResolvedSpace::IccGray
| ResolvedSpace::Unknown => None,
}
}
fn extract_image_channel(
samples: &[u8],
pixel_count: usize,
stride: usize,
target_channel: usize,
) -> Vec<u8> {
let mut plane = Vec::with_capacity(pixel_count);
for p in 0..pixel_count {
let off = p * stride + target_channel;
plane.push(samples.get(off).copied().unwrap_or(0));
}
plane
}
fn blit_image_plane_to_plate(
dst: &mut Pixmap,
plane: &[u8],
src_w: u32,
src_h: u32,
transform: Transform,
clip: Option<&Mask>,
) {
let n = (src_w as usize) * (src_h as usize);
if plane.len() < n {
return;
}
let mut rgba = Vec::with_capacity(n * 4);
for &v in &plane[..n] {
rgba.extend_from_slice(&[v, v, v, 255]);
}
let Some(size) = tiny_skia::IntSize::from_wh(src_w, src_h) else {
return;
};
let Some(src) = Pixmap::from_vec(rgba, size) else {
return;
};
let image_transform = transform
.pre_translate(0.0, 1.0)
.pre_scale(1.0 / src_w as f32, -1.0 / src_h as f32);
let mut paint = tiny_skia::PixmapPaint::default();
paint.blend_mode = tiny_skia::BlendMode::SourceOver;
paint.quality = tiny_skia::FilterQuality::Bilinear;
dst.draw_pixmap(0, 0, src.as_ref(), &paint, image_transform, clip);
}
fn image_has_unsupported_filter(image_dict: &HashMap<String, Object>) -> bool {
let filter = match image_dict.get("Filter") {
Some(f) => f,
None => return false,
};
let names: Vec<&str> = match filter {
Object::Name(n) => vec![n.as_str()],
Object::Array(arr) => arr.iter().filter_map(|o| o.as_name()).collect(),
_ => vec![],
};
names.iter().any(|f| matches!(*f, "JPXDecode" | "J2"))
}
#[allow(clippy::too_many_arguments)]
fn paint_image_to_plates(
pixmaps: &mut [Pixmap],
name: &str,
xobject: &Object,
obj_ref: Option<crate::object::ObjectRef>,
base_transform: Transform,
gs_stack: &GraphicsStateStack,
color_state: Option<&SeparationColorState>,
color_spaces: &HashMap<String, Object>,
resources: &Object,
ctx: &SeparationContext<'_>,
clip: Option<&Mask>,
target_inks: &[&str],
) -> Result<()> {
use crate::extractors::images::{
extract_image_from_xobject, ColorSpace as PdfCs, ImageData, PixelFormat,
};
let dict = match xobject {
Object::Stream { dict, .. } => dict,
_ => return Ok(()),
};
let is_image_mask = dict
.get("ImageMask")
.map(|o| matches!(o, Object::Boolean(true)))
.unwrap_or(false);
if is_image_mask {
return paint_image_mask_to_plates(
pixmaps,
name,
xobject,
obj_ref,
base_transform,
gs_stack,
color_state,
color_spaces,
resources,
ctx,
clip,
target_inks,
);
}
if image_has_unsupported_filter(dict) {
log::warn!(
"Skipping image XObject '{name}' on separation plates: \
unsupported filter (JPXDecode — JPEG 2000 decoder not bundled)"
);
return Ok(());
}
let resolved_space = resolve_image_color_space(dict, color_spaces, resources, ctx.doc);
let needs_4ch = matches!(resolved_space, ResolvedSpace::Cmyk | ResolvedSpace::IccCmyk);
let needs_separation = matches!(resolved_space, ResolvedSpace::Separation(_));
let needs_devicen = matches!(resolved_space, ResolvedSpace::DeviceN(_));
if !(needs_4ch || needs_separation || needs_devicen) {
log::debug!(
"Skipping image XObject '{name}' on separation plates: \
source colour space has no subtractive-ink intent"
);
return Ok(());
}
let pdf_image =
match extract_image_from_xobject(Some(ctx.doc), xobject, obj_ref, Some(color_spaces)) {
Ok(img) => img,
Err(e) => {
log::warn!("Skipping image XObject '{name}': {e}");
return Ok(());
},
};
let w = pdf_image.width() as usize;
let h = pdf_image.height() as usize;
let pixel_count = w * h;
if pixel_count == 0 {
return Ok(());
}
let bpc = pdf_image.bits_per_component();
if bpc != 8 {
log::warn!(
"Skipping image XObject '{name}' on separation plates: \
BitsPerComponent={bpc} not supported (only 8-bpc channel \
routing is implemented; 1/2/4/16-bpc expansion pending)"
);
return Ok(());
}
let extractor_cs = pdf_image.color_space();
let (samples, stride) = match (resolved_space.clone(), extractor_cs, pdf_image.data()) {
(
ResolvedSpace::Cmyk | ResolvedSpace::IccCmyk,
PdfCs::DeviceCMYK | PdfCs::ICCBased(4),
ImageData::Raw {
pixels,
format: PixelFormat::CMYK,
},
) => (pixels.clone(), 4usize),
(
ResolvedSpace::Cmyk | ResolvedSpace::IccCmyk,
PdfCs::DeviceCMYK | PdfCs::ICCBased(4),
ImageData::Jpeg(bytes),
) => (crate::extractors::images::decode_cmyk_jpeg_to_raw_cmyk(bytes)?, 4),
(ResolvedSpace::Separation(_), PdfCs::Separation, ImageData::Raw { pixels, .. }) => {
(pixels.clone(), 1)
},
(ResolvedSpace::DeviceN(ref names), PdfCs::DeviceN, ImageData::Raw { pixels, .. }) => {
(pixels.clone(), names.len().max(1))
},
_ => {
log::debug!(
"Image XObject '{name}': shape mismatch between resolved colour space \
and extractor sample format; skipping"
);
return Ok(());
},
};
let _ = color_state;
let decode = read_decode_array(dict, stride);
let gs = gs_stack.current();
let transform = combine_transforms(base_transform, &gs.ctm);
for (i, &ink) in target_inks.iter().enumerate() {
let Some(channel_idx) = image_channel_for_ink(&resolved_space, ink) else {
continue;
};
if channel_idx >= stride {
continue;
}
let mut plane = extract_image_channel(&samples, pixel_count, stride, channel_idx);
if let Some(decode_pairs) = decode.as_ref() {
if let Some(&(dmin, dmax)) = decode_pairs.get(channel_idx) {
apply_decode_to_plane(&mut plane, dmin, dmax);
}
}
blit_image_plane_to_plate(&mut pixmaps[i], &plane, w as u32, h as u32, transform, clip);
}
Ok(())
}
fn expand_1bpc_to_8bpc(packed: &[u8], width: u32, height: u32) -> Vec<u8> {
let row_bytes = width.div_ceil(8) as usize;
let w = width as usize;
let h = height as usize;
let mut out = Vec::with_capacity(w * h);
for row in 0..h {
let row_start = row * row_bytes;
for col in 0..w {
let byte_idx = row_start + col / 8;
let bit_idx = 7 - (col % 8);
let bit = packed
.get(byte_idx)
.map(|b| (*b >> bit_idx) & 1)
.unwrap_or(0);
out.push(if bit == 1 { 255 } else { 0 });
}
}
out
}
fn read_decode_array(
dict: &HashMap<String, Object>,
num_components: usize,
) -> Option<Vec<(f32, f32)>> {
let decode = dict.get("Decode")?;
let arr = decode.as_array()?;
if arr.len() < num_components * 2 {
return None;
}
let to_f32 = |o: &Object| -> Option<f32> {
match o {
Object::Real(r) => Some(*r as f32),
Object::Integer(i) => Some(*i as f32),
_ => None,
}
};
let mut out = Vec::with_capacity(num_components);
for i in 0..num_components {
let dmin = to_f32(&arr[i * 2])?;
let dmax = to_f32(&arr[i * 2 + 1])?;
out.push((dmin, dmax));
}
Some(out)
}
fn apply_decode_to_plane(plane: &mut [u8], dmin: f32, dmax: f32) {
if dmin == 0.0 && dmax == 1.0 {
return;
}
for byte in plane.iter_mut() {
let raw = *byte as f32 / 255.0;
let decoded = (dmin + raw * (dmax - dmin)).clamp(0.0, 1.0);
*byte = (decoded * 255.0).round() as u8;
}
}
#[allow(clippy::too_many_arguments)]
fn paint_image_mask_to_plates(
pixmaps: &mut [Pixmap],
name: &str,
xobject: &Object,
obj_ref: Option<crate::object::ObjectRef>,
base_transform: Transform,
gs_stack: &GraphicsStateStack,
color_state: Option<&SeparationColorState>,
color_spaces: &HashMap<String, Object>,
resources: &Object,
ctx: &SeparationContext<'_>,
clip: Option<&Mask>,
target_inks: &[&str],
) -> Result<()> {
let dict = match xobject {
Object::Stream { dict, .. } => dict,
_ => return Ok(()),
};
let w = dict.get("Width").and_then(|o| o.as_integer()).unwrap_or(0) as usize;
let h = dict.get("Height").and_then(|o| o.as_integer()).unwrap_or(0) as usize;
let pixel_count = w * h;
if pixel_count == 0 {
return Ok(());
}
let bpc = dict
.get("BitsPerComponent")
.and_then(|o| o.as_integer())
.unwrap_or(1) as u8;
if bpc != 1 {
log::warn!(
"Skipping image mask '{name}': BitsPerComponent={bpc} out of spec \
(§8.9.6.2 mandates 1-bpc)"
);
return Ok(());
}
let packed = if let Some(r) = obj_ref {
ctx.doc.decode_stream_with_encryption(xobject, r)?
} else {
xobject.decode_stream_data()?
};
let mut stencil = expand_1bpc_to_8bpc(&packed, w as u32, h as u32);
if stencil.len() < pixel_count {
return Ok(());
}
if let Some(decode_pairs) = read_decode_array(dict, 1) {
if let Some(&(dmin, dmax)) = decode_pairs.first() {
apply_decode_to_plane(&mut stencil, dmin, dmax);
}
}
for byte in stencil.iter_mut() {
*byte = 255 - *byte;
}
let gs = gs_stack.current();
let transform = combine_transforms(base_transform, &gs.ctm);
let empty = SeparationColorState::new();
let cs = color_state.unwrap_or(&empty);
for (i, &ink) in target_inks.iter().enumerate() {
let PaintAction::Paint(tint) = tint_for_ink(
true,
gs,
color_spaces,
resources,
ctx.doc,
ink,
&cs.fill_components,
&cs.stroke_components,
) else {
continue;
};
let gray = (tint.clamp(0.0, 1.0) * 255.0).round() as u8;
let mut rgba = Vec::with_capacity(pixel_count * 4);
for &alpha in &stencil[..pixel_count] {
rgba.extend_from_slice(&[gray, gray, gray, alpha]);
}
let Some(size) = tiny_skia::IntSize::from_wh(w as u32, h as u32) else {
continue;
};
let Some(src) = Pixmap::from_vec(rgba, size) else {
continue;
};
let image_transform = transform
.pre_translate(0.0, 1.0)
.pre_scale(1.0 / w as f32, -1.0 / h as f32);
let mut paint = tiny_skia::PixmapPaint::default();
paint.blend_mode = tiny_skia::BlendMode::SourceOver;
paint.quality = tiny_skia::FilterQuality::Bilinear;
pixmaps[i].draw_pixmap(0, 0, src.as_ref(), &paint, image_transform, clip);
}
Ok(())
}