#![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::text_rasterizer::TextRasterizer;
#[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 (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 resources = doc.get_page_resources(page_num)?;
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 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(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,
)?;
}
}
}
}
}
}
},
_ => {},
}
}
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,
"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
}
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],
) -> Option<f32> {
let space_name = if fill {
&gs.fill_color_space
} else {
&gs.stroke_color_space
};
let components = if fill {
fill_components
} else {
stroke_components
};
let resolved = resolve_color_space(space_name, color_spaces, resources, doc);
match resolved {
ResolvedSpace::Cmyk => {
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 None;
};
match target_ink {
"Cyan" => Some(c),
"Magenta" => Some(m),
"Yellow" => Some(y),
"Black" => Some(k),
_ => None,
}
},
ResolvedSpace::Rgb
| ResolvedSpace::Gray
| ResolvedSpace::IccRgb
| ResolvedSpace::IccGray => {
None
},
ResolvedSpace::Separation(ink) => {
if ink == "None" || components.is_empty() {
None
} else if ink == "All" || ink == target_ink {
Some(components[0])
} else {
None
}
},
ResolvedSpace::DeviceN(names) => {
for (i, n) in names.iter().enumerate() {
if n == "None" {
continue;
}
if (n == "All" || n == target_ink) && i < components.len() {
return Some(components[i]);
}
}
None
},
ResolvedSpace::IccCmyk => {
if components.len() >= 4 {
match target_ink {
"Cyan" => Some(components[0]),
"Magenta" => Some(components[1]),
"Yellow" => Some(components[2]),
"Black" => Some(components[3]),
_ => None,
}
} else {
None
}
},
ResolvedSpace::Unknown => None,
}
}
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>,
}
#[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;
} 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);
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::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());
for (i, &ink) in target_inks.iter().enumerate() {
if let Some(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());
for (i, &ink) in target_inks.iter().enumerate() {
if let Some(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());
for (i, &ink) in target_inks.iter().enumerate() {
if let Some(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());
for (i, &ink) in target_inks.iter().enumerate() {
if let Some(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 let Some(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());
for (i, &ink) in target_inks.iter().enumerate() {
if let Some(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 let Some(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 gs = gs_stack.current_mut();
gs.font_name = Some(font.clone());
gs.font_size = *size;
},
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,
)?;
let gs_mut = gs_stack.current_mut();
let advance_matrix = Matrix::translation(advance, 0.0);
gs_mut.text_matrix = advance_matrix.multiply(&gs_mut.text_matrix);
}
},
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,
)?;
let gs_mut = gs_stack.current_mut();
let advance_matrix = Matrix::translation(advance, 0.0);
gs_mut.text_matrix = advance_matrix.multiply(&gs_mut.text_matrix);
}
},
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,
)?;
let gs_mut = gs_stack.current_mut();
let advance_matrix = Matrix::translation(advance, 0.0);
gs_mut.text_matrix = advance_matrix.multiply(&gs_mut.text_matrix);
}
},
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,
)?;
let gs_mut = gs_stack.current_mut();
let advance_matrix = Matrix::translation(advance, 0.0);
gs_mut.text_matrix = advance_matrix.multiply(&gs_mut.text_matrix);
}
},
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 == "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(),
};
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,
) {
Some(t) => t,
None => 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,
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,
)?;
let gs_mut = gs_stack.current_mut();
let advance_matrix = Matrix::translation(advance, 0.0);
gs_mut.text_matrix = advance_matrix.multiply(&gs_mut.text_matrix);
total_advance += advance;
},
TextElement::Offset(offset) => {
let gs = gs_stack.current();
let shift = (-*offset / 1000.0) * gs.font_size;
let advance_matrix = Matrix::translation(shift, 0.0);
let gs_mut = gs_stack.current_mut();
gs_mut.text_matrix = advance_matrix.multiply(&gs_mut.text_matrix);
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)
}
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))
}