use std::collections::{HashMap, HashSet};
use std::io::Write as IoWrite;
use crate::font_embedder;
use crate::font_tracker::FontTracker;
use crate::image_ops::{self, ImageXObject};
use crate::pdf_objects::PdfObj;
use crate::text_ops;
use stet_core::context::Context;
use stet_fonts::geometry::{Matrix, PsPath};
use stet_graphics::color::{DeviceColor, FillRule, LineCap, LineJoin};
use stet_graphics::device::{
AxialShadingParams, BgUcrState, HalftoneState, MeshShadingParams, PatchShadingParams,
PatternFillParams, RadialShadingParams, SpotColor, SpotColorSpace, StrokeParams, TextParams,
TransferState,
};
use stet_graphics::display_list::{DisplayElement, DisplayList};
pub struct ContentStreamResult {
pub content: Vec<u8>,
pub images: Vec<ImageXObject>,
pub shading_refs: Vec<ShadingRef>,
pub used_font_names: Vec<String>,
pub ext_gstate_dicts: Vec<ExtGStateDict>,
pub color_spaces: Vec<(String, SpotColorSpace)>,
pub pattern_refs: Vec<PatternRef>,
pub pattern_cs_entries: Vec<(String, PdfObj)>,
pub transfer_refs: Vec<TransferFunctionRef>,
pub halftone_refs: Vec<HalftoneRef>,
pub bg_ucr_refs: Vec<BgUcrRef>,
}
pub struct TransferFunctionRef {
pub ext_gstate_idx: usize,
pub tables: Vec<Option<std::sync::Arc<Vec<f64>>>>,
pub is_color: bool,
}
pub struct HalftoneRef {
pub ext_gstate_idx: usize,
pub state: HalftoneState,
}
pub struct BgUcrRef {
pub ext_gstate_idx: usize,
pub state: BgUcrState,
}
pub struct PatternRef {
pub tile: DisplayList,
pub pattern_matrix: Matrix,
pub bbox: [f64; 4],
pub xstep: f64,
pub ystep: f64,
pub paint_type: i32,
}
pub struct ExtGStateDict {
pub entries: Vec<(Vec<u8>, PdfObj)>,
}
pub enum ShadingRef {
Axial(AxialShadingParams),
Radial(RadialShadingParams),
Mesh(MeshShadingParams),
Patch(PatchShadingParams),
}
struct GState {
fill_color: Option<PdfColor>,
stroke_color: Option<PdfColor>,
line_width: f64,
line_cap: i32,
line_join: i32,
miter_limit: f64,
dash_array: Vec<f64>,
dash_offset: f64,
overprint: bool,
fill_cs_name: Option<String>,
stroke_cs_name: Option<String>,
rendering_intent: u8,
transfer_key: Vec<u8>,
halftone_key: Vec<u8>,
bg_ucr_key: Vec<u8>,
}
impl GState {
fn new() -> Self {
Self {
fill_color: None,
stroke_color: None,
line_width: -1.0, line_cap: -1,
line_join: -1,
miter_limit: -1.0,
dash_array: Vec::new(),
dash_offset: -1.0,
overprint: false,
fill_cs_name: None,
stroke_cs_name: None,
rendering_intent: 0,
transfer_key: Vec::new(),
halftone_key: Vec::new(),
bg_ucr_key: Vec::new(),
}
}
fn reset(&mut self) {
*self = Self::new();
}
}
#[derive(Clone, Debug, PartialEq)]
enum PdfColor {
Gray(u16), Rgb(u16, u16, u16), Cmyk(u16, u16, u16, u16),
}
fn quantize(v: f64) -> u16 {
(v.clamp(0.0, 1.0) * 10000.0) as u16
}
fn color_to_pdf(c: &DeviceColor) -> PdfColor {
if let Some((c_val, m, y, k)) = c.native_cmyk {
PdfColor::Cmyk(quantize(c_val), quantize(m), quantize(y), quantize(k))
} else if c.r == c.g && c.g == c.b {
PdfColor::Gray(quantize(c.r))
} else {
PdfColor::Rgb(quantize(c.r), quantize(c.g), quantize(c.b))
}
}
pub fn build_content_stream(
list: &DisplayList,
page_w: u32,
page_h: u32,
dpi: f64,
ctx: Option<&Context>,
font_tracker: &mut FontTracker,
) -> ContentStreamResult {
let scale = 72.0 / dpi;
let page_h_pts = page_h as f64 * scale;
let mut buf = Vec::with_capacity(4096);
let mut images: Vec<ImageXObject> = Vec::new();
let mut shading_refs: Vec<ShadingRef> = Vec::new();
let mut clip_depth: u32 = 0;
let mut gs = GState::new();
let mut ext_gstates: Vec<ExtGStateDict> = Vec::new();
let mut ext_gstate_map: HashMap<Vec<u8>, usize> = HashMap::new();
let mut color_spaces: Vec<(String, SpotColorSpace)> = Vec::new();
let mut cs_name_map: HashMap<Vec<u8>, String> = HashMap::new();
let mut pattern_refs: Vec<PatternRef> = Vec::new();
let mut pattern_map: HashMap<u32, usize> = HashMap::new();
let mut pattern_cs_names: Vec<(String, PdfObj)> = Vec::new();
let mut pattern_cs_set: HashSet<String> = HashSet::new();
let mut transfer_refs: Vec<TransferFunctionRef> = Vec::new();
let mut halftone_refs: Vec<HalftoneRef> = Vec::new();
let mut bg_ucr_refs: Vec<BgUcrRef> = Vec::new();
let mut page_font_names: HashSet<String> = HashSet::new();
for element in list.elements() {
if let DisplayElement::Text { params } = element {
let name = font_tracker.track(params).to_string();
page_font_names.insert(name);
}
}
if let Some(c) = ctx {
for usage in font_tracker.fonts_mut() {
if usage.widths.is_empty() {
usage.widths = font_embedder::extract_widths(usage, c);
}
}
}
fmt_num(&mut buf, scale);
buf.extend(b" 0 0 ");
fmt_num(&mut buf, -scale);
buf.extend(b" 0 ");
fmt_num(&mut buf, page_h_pts);
buf.extend(b" cm\n");
buf.extend(b"0 0 ");
fmt_num(&mut buf, page_w as f64);
buf.push(b' ');
fmt_num(&mut buf, page_h as f64);
buf.extend(b" re W n\n");
let mut text_batch: Vec<&TextParams> = Vec::new();
let mut batch_font: Option<u32> = None;
for element in list.elements() {
match element {
DisplayElement::Text { params } => {
if batch_font == Some(params.font_entity) {
text_batch.push(params);
} else {
flush_text_batch(&text_batch, font_tracker, &mut buf, &mut gs);
text_batch.clear();
text_batch.push(params);
batch_font = Some(params.font_entity);
}
continue;
}
DisplayElement::Fill { params, .. } if params.is_text_glyph => continue,
DisplayElement::Stroke { params, .. } if params.is_text_glyph => continue,
_ => {
flush_text_batch(&text_batch, font_tracker, &mut buf, &mut gs);
text_batch.clear();
batch_font = None;
}
}
match element {
DisplayElement::ErasePage => {
buf.extend(b"1 g 0 0 ");
fmt_num(&mut buf, page_w as f64);
buf.push(b' ');
fmt_num(&mut buf, page_h as f64);
buf.extend(b" re f\n");
gs.fill_color = Some(PdfColor::Gray(10000));
}
DisplayElement::Fill { path, params } => {
if params.is_text_glyph {
continue;
}
emit_transfer(
&mut buf,
¶ms.transfer,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut transfer_refs,
);
emit_halftone(
&mut buf,
¶ms.halftone,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut halftone_refs,
);
emit_bg_ucr(
&mut buf,
¶ms.bg_ucr,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut bg_ucr_refs,
);
emit_rendering_intent(&mut buf, params.rendering_intent, &mut gs);
emit_overprint(
&mut buf,
params.overprint,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
);
if let Some(spot) = ¶ms.spot_color {
emit_fill_color_spot(
&mut buf,
spot,
&mut gs,
&mut cs_name_map,
&mut color_spaces,
);
} else {
if gs.fill_cs_name.is_some() {
gs.fill_cs_name = None;
gs.fill_color = None;
}
emit_fill_color(&mut buf, ¶ms.color, &mut gs);
}
emit_path(&mut buf, path);
if params.fill_rule == FillRule::EvenOdd {
buf.extend(b"f*\n");
} else {
buf.extend(b"f\n");
}
}
DisplayElement::Stroke { path, params } => {
if params.is_text_glyph {
continue;
}
let has_ctm = !is_identity(¶ms.ctm);
if has_ctm {
buf.extend(b"q\n");
emit_cm(&mut buf, ¶ms.ctm);
}
emit_transfer(
&mut buf,
¶ms.transfer,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut transfer_refs,
);
emit_halftone(
&mut buf,
¶ms.halftone,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut halftone_refs,
);
emit_bg_ucr(
&mut buf,
¶ms.bg_ucr,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut bg_ucr_refs,
);
emit_rendering_intent(&mut buf, params.rendering_intent, &mut gs);
emit_overprint(
&mut buf,
params.overprint,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
);
if let Some(spot) = ¶ms.spot_color {
emit_stroke_color_spot(
&mut buf,
spot,
&mut gs,
&mut cs_name_map,
&mut color_spaces,
);
} else {
if gs.stroke_cs_name.is_some() {
gs.stroke_cs_name = None;
gs.stroke_color = None;
}
emit_stroke_color(&mut buf, ¶ms.color, &mut gs);
}
emit_line_state(&mut buf, params, &mut gs);
emit_path(&mut buf, path);
buf.extend(b"S\n");
if has_ctm {
buf.extend(b"Q\n");
gs = GState::new();
}
}
DisplayElement::Clip { path, params } => {
buf.extend(b"q\n");
emit_path(&mut buf, path);
if params.fill_rule == FillRule::EvenOdd {
buf.extend(b"W* n\n");
} else {
buf.extend(b"W n\n");
}
clip_depth += 1;
}
DisplayElement::InitClip => {
for _ in 0..clip_depth {
buf.extend(b"Q\n");
}
clip_depth = 0;
gs.reset();
}
DisplayElement::Image {
sample_data,
params,
} => {
let img_idx = images.len();
let xobj = image_ops::convert_image(sample_data, params);
let m = compute_image_matrix(params);
buf.extend(b"q ");
emit_matrix(&mut buf, &m);
buf.extend(b" cm ");
if xobj.is_imagemask {
if let Some((r, g, b)) = xobj.mask_color {
emit_fill_color_rgb(&mut buf, r, g, b);
}
}
writeln!(buf, "/Im{} Do Q", img_idx).unwrap();
images.push(xobj);
}
DisplayElement::AxialShading { params } => {
let sh_idx = shading_refs.len();
buf.extend(b"q ");
if !is_identity(¶ms.ctm) {
emit_matrix(&mut buf, ¶ms.ctm);
buf.extend(b" cm ");
}
writeln!(buf, "/Sh{} sh Q", sh_idx).unwrap();
shading_refs.push(ShadingRef::Axial(params.clone()));
}
DisplayElement::RadialShading { params } => {
let sh_idx = shading_refs.len();
buf.extend(b"q ");
if !is_identity(¶ms.ctm) {
emit_matrix(&mut buf, ¶ms.ctm);
buf.extend(b" cm ");
}
writeln!(buf, "/Sh{} sh Q", sh_idx).unwrap();
shading_refs.push(ShadingRef::Radial(params.clone()));
}
DisplayElement::MeshShading { params } => {
let sh_idx = shading_refs.len();
buf.extend(b"q ");
if !is_identity(¶ms.ctm) {
emit_matrix(&mut buf, ¶ms.ctm);
buf.extend(b" cm ");
}
writeln!(buf, "/Sh{} sh Q", sh_idx).unwrap();
shading_refs.push(ShadingRef::Mesh(params.clone()));
}
DisplayElement::PatchShading { params } => {
let sh_idx = shading_refs.len();
buf.extend(b"q ");
if !is_identity(¶ms.ctm) {
emit_matrix(&mut buf, ¶ms.ctm);
buf.extend(b" cm ");
}
writeln!(buf, "/Sh{} sh Q", sh_idx).unwrap();
shading_refs.push(ShadingRef::Patch(params.clone()));
}
DisplayElement::PatternFill { params } => {
emit_pattern_fill(
&mut buf,
params,
&mut gs,
&mut pattern_refs,
&mut pattern_map,
&mut pattern_cs_names,
&mut pattern_cs_set,
);
}
DisplayElement::Text { .. } => unreachable!(), DisplayElement::Group { .. }
| DisplayElement::SoftMasked { .. }
| DisplayElement::OcgGroup { .. } => {} }
}
flush_text_batch(&text_batch, font_tracker, &mut buf, &mut gs);
for _ in 0..clip_depth {
buf.extend(b"Q\n");
}
let initial_cm = Matrix::new(scale, 0.0, 0.0, -scale, 0.0, page_h_pts);
for pat_ref in &mut pattern_refs {
pat_ref.pattern_matrix = initial_cm.concat(&pat_ref.pattern_matrix);
}
let pattern_cs_entries = pattern_cs_names;
ContentStreamResult {
content: buf,
images,
shading_refs,
used_font_names: page_font_names.into_iter().collect(),
ext_gstate_dicts: ext_gstates,
color_spaces,
pattern_refs,
pattern_cs_entries,
transfer_refs,
halftone_refs,
bg_ucr_refs,
}
}
pub fn build_tile_content_stream(
list: &DisplayList,
font_tracker: &mut FontTracker,
) -> ContentStreamResult {
let mut buf = Vec::with_capacity(1024);
let mut images: Vec<ImageXObject> = Vec::new();
let mut shading_refs: Vec<ShadingRef> = Vec::new();
let mut clip_depth: u32 = 0;
let mut gs = GState::new();
let mut ext_gstates: Vec<ExtGStateDict> = Vec::new();
let mut ext_gstate_map: HashMap<Vec<u8>, usize> = HashMap::new();
let mut color_spaces: Vec<(String, SpotColorSpace)> = Vec::new();
let mut cs_name_map: HashMap<Vec<u8>, String> = HashMap::new();
let mut pattern_refs: Vec<PatternRef> = Vec::new();
let mut pattern_map: HashMap<u32, usize> = HashMap::new();
let mut pattern_cs_names: Vec<(String, PdfObj)> = Vec::new();
let mut pattern_cs_set: HashSet<String> = HashSet::new();
let mut transfer_refs: Vec<TransferFunctionRef> = Vec::new();
let mut halftone_refs: Vec<HalftoneRef> = Vec::new();
let mut bg_ucr_refs: Vec<BgUcrRef> = Vec::new();
let mut page_font_names: HashSet<String> = HashSet::new();
for element in list.elements() {
if let DisplayElement::Text { params } = element {
let name = font_tracker.track(params).to_string();
page_font_names.insert(name);
}
}
for element in list.elements() {
match element {
DisplayElement::ErasePage => {} DisplayElement::Fill { path, params } => {
if params.is_text_glyph {
continue;
}
emit_transfer(
&mut buf,
¶ms.transfer,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut transfer_refs,
);
emit_halftone(
&mut buf,
¶ms.halftone,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut halftone_refs,
);
emit_bg_ucr(
&mut buf,
¶ms.bg_ucr,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut bg_ucr_refs,
);
emit_rendering_intent(&mut buf, params.rendering_intent, &mut gs);
emit_overprint(
&mut buf,
params.overprint,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
);
if let Some(spot) = ¶ms.spot_color {
emit_fill_color_spot(
&mut buf,
spot,
&mut gs,
&mut cs_name_map,
&mut color_spaces,
);
} else {
if gs.fill_cs_name.is_some() {
gs.fill_cs_name = None;
gs.fill_color = None;
}
emit_fill_color(&mut buf, ¶ms.color, &mut gs);
}
emit_path(&mut buf, path);
if params.fill_rule == FillRule::EvenOdd {
buf.extend(b"f*\n");
} else {
buf.extend(b"f\n");
}
}
DisplayElement::Stroke { path, params } => {
if params.is_text_glyph {
continue;
}
let has_ctm = !is_identity(¶ms.ctm);
if has_ctm {
buf.extend(b"q\n");
emit_cm(&mut buf, ¶ms.ctm);
}
emit_transfer(
&mut buf,
¶ms.transfer,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut transfer_refs,
);
emit_halftone(
&mut buf,
¶ms.halftone,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut halftone_refs,
);
emit_bg_ucr(
&mut buf,
¶ms.bg_ucr,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
&mut bg_ucr_refs,
);
emit_rendering_intent(&mut buf, params.rendering_intent, &mut gs);
emit_overprint(
&mut buf,
params.overprint,
&mut gs,
&mut ext_gstates,
&mut ext_gstate_map,
);
if let Some(spot) = ¶ms.spot_color {
emit_stroke_color_spot(
&mut buf,
spot,
&mut gs,
&mut cs_name_map,
&mut color_spaces,
);
} else {
if gs.stroke_cs_name.is_some() {
gs.stroke_cs_name = None;
gs.stroke_color = None;
}
emit_stroke_color(&mut buf, ¶ms.color, &mut gs);
}
emit_line_state(&mut buf, params, &mut gs);
emit_path(&mut buf, path);
buf.extend(b"S\n");
if has_ctm {
buf.extend(b"Q\n");
gs = GState::new();
}
}
DisplayElement::Clip { path, params } => {
buf.extend(b"q\n");
emit_path(&mut buf, path);
if params.fill_rule == FillRule::EvenOdd {
buf.extend(b"W* n\n");
} else {
buf.extend(b"W n\n");
}
clip_depth += 1;
}
DisplayElement::InitClip => {
for _ in 0..clip_depth {
buf.extend(b"Q\n");
}
clip_depth = 0;
gs.reset();
}
DisplayElement::Image {
sample_data,
params,
} => {
let img_idx = images.len();
let xobj = image_ops::convert_image(sample_data, params);
let m = compute_image_matrix(params);
buf.extend(b"q ");
emit_matrix(&mut buf, &m);
buf.extend(b" cm ");
if xobj.is_imagemask
&& let Some((r, g, b)) = xobj.mask_color
{
emit_fill_color_rgb(&mut buf, r, g, b);
}
writeln!(buf, "/Im{} Do Q", img_idx).unwrap();
images.push(xobj);
}
DisplayElement::AxialShading { params } => {
let sh_idx = shading_refs.len();
buf.extend(b"q ");
if !is_identity(¶ms.ctm) {
emit_matrix(&mut buf, ¶ms.ctm);
buf.extend(b" cm ");
}
writeln!(buf, "/Sh{} sh Q", sh_idx).unwrap();
shading_refs.push(ShadingRef::Axial(params.clone()));
}
DisplayElement::RadialShading { params } => {
let sh_idx = shading_refs.len();
buf.extend(b"q ");
if !is_identity(¶ms.ctm) {
emit_matrix(&mut buf, ¶ms.ctm);
buf.extend(b" cm ");
}
writeln!(buf, "/Sh{} sh Q", sh_idx).unwrap();
shading_refs.push(ShadingRef::Radial(params.clone()));
}
DisplayElement::MeshShading { params } => {
let sh_idx = shading_refs.len();
buf.extend(b"q ");
if !is_identity(¶ms.ctm) {
emit_matrix(&mut buf, ¶ms.ctm);
buf.extend(b" cm ");
}
writeln!(buf, "/Sh{} sh Q", sh_idx).unwrap();
shading_refs.push(ShadingRef::Mesh(params.clone()));
}
DisplayElement::PatchShading { params } => {
let sh_idx = shading_refs.len();
buf.extend(b"q ");
if !is_identity(¶ms.ctm) {
emit_matrix(&mut buf, ¶ms.ctm);
buf.extend(b" cm ");
}
writeln!(buf, "/Sh{} sh Q", sh_idx).unwrap();
shading_refs.push(ShadingRef::Patch(params.clone()));
}
DisplayElement::PatternFill { params } => {
emit_pattern_fill(
&mut buf,
params,
&mut gs,
&mut pattern_refs,
&mut pattern_map,
&mut pattern_cs_names,
&mut pattern_cs_set,
);
}
DisplayElement::Text { params } => {
let name = font_tracker.track(params).to_string();
page_font_names.insert(name);
text_ops::emit_text_batch(&mut buf, &[params], font_tracker);
gs.fill_color = None;
}
DisplayElement::Group { .. }
| DisplayElement::SoftMasked { .. }
| DisplayElement::OcgGroup { .. } => {} }
}
for _ in 0..clip_depth {
buf.extend(b"Q\n");
}
ContentStreamResult {
content: buf,
images,
shading_refs,
used_font_names: page_font_names.into_iter().collect(),
ext_gstate_dicts: ext_gstates,
color_spaces,
pattern_refs,
pattern_cs_entries: pattern_cs_names,
transfer_refs,
halftone_refs,
bg_ucr_refs,
}
}
fn flush_text_batch(
batch: &[&TextParams],
font_tracker: &FontTracker,
buf: &mut Vec<u8>,
gs: &mut GState,
) {
if batch.is_empty() {
return;
}
text_ops::emit_text_batch(buf, batch, font_tracker);
gs.fill_color = None;
gs.fill_cs_name = None;
gs.stroke_color = None;
gs.stroke_cs_name = None;
}
fn emit_fill_color(buf: &mut Vec<u8>, color: &DeviceColor, gs: &mut GState) {
let pc = color_to_pdf(color);
if gs.fill_color.as_ref() == Some(&pc) {
return;
}
match &pc {
PdfColor::Cmyk(c, m, y, k) => {
fmt_num(buf, *c as f64 / 10000.0);
buf.push(b' ');
fmt_num(buf, *m as f64 / 10000.0);
buf.push(b' ');
fmt_num(buf, *y as f64 / 10000.0);
buf.push(b' ');
fmt_num(buf, *k as f64 / 10000.0);
buf.extend(b" k\n");
}
PdfColor::Gray(g) => {
fmt_num(buf, *g as f64 / 10000.0);
buf.extend(b" g\n");
}
PdfColor::Rgb(r, g, b) => {
fmt_num(buf, *r as f64 / 10000.0);
buf.push(b' ');
fmt_num(buf, *g as f64 / 10000.0);
buf.push(b' ');
fmt_num(buf, *b as f64 / 10000.0);
buf.extend(b" rg\n");
}
}
gs.fill_color = Some(pc);
}
fn emit_fill_color_rgb(buf: &mut Vec<u8>, r: f64, g: f64, b: f64) {
fmt_num(buf, r);
buf.push(b' ');
fmt_num(buf, g);
buf.push(b' ');
fmt_num(buf, b);
buf.extend(b" rg ");
}
fn emit_stroke_color(buf: &mut Vec<u8>, color: &DeviceColor, gs: &mut GState) {
let pc = color_to_pdf(color);
if gs.stroke_color.as_ref() == Some(&pc) {
return;
}
match &pc {
PdfColor::Cmyk(c, m, y, k) => {
fmt_num(buf, *c as f64 / 10000.0);
buf.push(b' ');
fmt_num(buf, *m as f64 / 10000.0);
buf.push(b' ');
fmt_num(buf, *y as f64 / 10000.0);
buf.push(b' ');
fmt_num(buf, *k as f64 / 10000.0);
buf.extend(b" K\n");
}
PdfColor::Gray(g) => {
fmt_num(buf, *g as f64 / 10000.0);
buf.extend(b" G\n");
}
PdfColor::Rgb(r, g, b) => {
fmt_num(buf, *r as f64 / 10000.0);
buf.push(b' ');
fmt_num(buf, *g as f64 / 10000.0);
buf.push(b' ');
fmt_num(buf, *b as f64 / 10000.0);
buf.extend(b" RG\n");
}
}
gs.stroke_color = Some(pc);
}
fn spot_cs_key(spot: &SpotColor) -> Vec<u8> {
match &spot.color_space {
SpotColorSpace::Separation { name, .. } => {
let mut key = b"Sep:".to_vec();
key.extend(name);
key
}
SpotColorSpace::DeviceN { names, .. } => {
let mut key = b"DN:".to_vec();
let mut sorted: Vec<&Vec<u8>> = names.iter().collect();
sorted.sort();
for (i, n) in sorted.iter().enumerate() {
if i > 0 {
key.push(b',');
}
key.extend(*n);
}
key
}
}
}
fn get_or_create_cs_name(
spot: &SpotColor,
cs_map: &mut HashMap<Vec<u8>, String>,
color_spaces: &mut Vec<(String, SpotColorSpace)>,
) -> String {
let key = spot_cs_key(spot);
if let Some(name) = cs_map.get(&key) {
return name.clone();
}
let name = format!("CS{}", color_spaces.len());
cs_map.insert(key, name.clone());
color_spaces.push((name.clone(), spot.color_space.clone()));
name
}
fn emit_fill_color_spot(
buf: &mut Vec<u8>,
spot: &SpotColor,
gs: &mut GState,
cs_map: &mut HashMap<Vec<u8>, String>,
color_spaces: &mut Vec<(String, SpotColorSpace)>,
) {
let cs_name = get_or_create_cs_name(spot, cs_map, color_spaces);
if gs.fill_cs_name.as_deref() != Some(&cs_name) {
writeln!(buf, "/{} cs", cs_name).unwrap();
gs.fill_cs_name = Some(cs_name);
gs.fill_color = None; }
for v in &spot.tint_values {
fmt_num(buf, *v);
buf.push(b' ');
}
buf.extend(b"scn\n");
}
fn emit_stroke_color_spot(
buf: &mut Vec<u8>,
spot: &SpotColor,
gs: &mut GState,
cs_map: &mut HashMap<Vec<u8>, String>,
color_spaces: &mut Vec<(String, SpotColorSpace)>,
) {
let cs_name = get_or_create_cs_name(spot, cs_map, color_spaces);
if gs.stroke_cs_name.as_deref() != Some(&cs_name) {
writeln!(buf, "/{} CS", cs_name).unwrap();
gs.stroke_cs_name = Some(cs_name);
gs.stroke_color = None;
}
for v in &spot.tint_values {
fmt_num(buf, *v);
buf.push(b' ');
}
buf.extend(b"SCN\n");
}
fn emit_line_state(buf: &mut Vec<u8>, params: &StrokeParams, gs: &mut GState) {
if gs.line_width != params.line_width {
fmt_num(buf, params.line_width);
buf.extend(b" w\n");
gs.line_width = params.line_width;
}
let lc = match params.line_cap {
LineCap::Butt => 0,
LineCap::Round => 1,
LineCap::Square => 2,
};
if gs.line_cap != lc {
writeln!(buf, "{} J", lc).unwrap();
gs.line_cap = lc;
}
let lj = match params.line_join {
LineJoin::Miter => 0,
LineJoin::Round => 1,
LineJoin::Bevel => 2,
};
if gs.line_join != lj {
writeln!(buf, "{} j", lj).unwrap();
gs.line_join = lj;
}
if gs.miter_limit != params.miter_limit {
fmt_num(buf, params.miter_limit);
buf.extend(b" M\n");
gs.miter_limit = params.miter_limit;
}
let dash = ¶ms.dash_pattern;
if gs.dash_array != dash.array || gs.dash_offset != dash.offset {
buf.push(b'[');
for (i, &d) in dash.array.iter().enumerate() {
if i > 0 {
buf.push(b' ');
}
fmt_num(buf, d);
}
buf.extend(b"] ");
fmt_num(buf, dash.offset);
buf.extend(b" d\n");
gs.dash_array.clone_from(&dash.array);
gs.dash_offset = dash.offset;
}
}
fn emit_overprint(
buf: &mut Vec<u8>,
overprint: bool,
gs: &mut GState,
ext_gstates: &mut Vec<ExtGStateDict>,
ext_gstate_map: &mut HashMap<Vec<u8>, usize>,
) {
if gs.overprint == overprint {
return;
}
gs.overprint = overprint;
let key = format!("OP{}", overprint as u8).into_bytes();
let idx = if let Some(&idx) = ext_gstate_map.get(&key) {
idx
} else {
let idx = ext_gstates.len();
let mut entries = vec![
(b"Type".to_vec(), PdfObj::name("ExtGState")),
(b"OP".to_vec(), PdfObj::Bool(overprint)),
(b"op".to_vec(), PdfObj::Bool(overprint)),
];
if overprint {
entries.push((b"OPM".to_vec(), PdfObj::Int(1)));
}
ext_gstates.push(ExtGStateDict { entries });
ext_gstate_map.insert(key, idx);
idx
};
writeln!(buf, "/GS{} gs", idx).unwrap();
}
fn emit_rendering_intent(buf: &mut Vec<u8>, intent: u8, gs: &mut GState) {
if gs.rendering_intent == intent {
return;
}
gs.rendering_intent = intent;
let name = match intent {
0 => b"RelativeColorimetric" as &[u8],
1 => b"AbsoluteColorimetric",
2 => b"Perceptual",
3 => b"Saturation",
_ => return,
};
buf.push(b'/');
buf.extend_from_slice(name);
buf.extend(b" ri\n");
}
fn build_transfer_key(transfer: &TransferState) -> Vec<u8> {
use std::sync::Arc;
let mut key = Vec::new();
if let Some(ref color) = transfer.color {
key.extend(b"C");
for table in color {
if let Some(t) = table {
let ptr = Arc::as_ptr(t) as usize;
key.extend(ptr.to_le_bytes());
} else {
key.extend(b"I"); }
}
} else if let Some(ref gray) = transfer.gray {
key.extend(b"G");
let ptr = Arc::as_ptr(gray) as usize;
key.extend(ptr.to_le_bytes());
}
key
}
fn emit_transfer(
buf: &mut Vec<u8>,
transfer: &TransferState,
gs: &mut GState,
ext_gstates: &mut Vec<ExtGStateDict>,
ext_gstate_map: &mut HashMap<Vec<u8>, usize>,
transfer_refs: &mut Vec<TransferFunctionRef>,
) {
let key = build_transfer_key(transfer);
if key == gs.transfer_key {
return;
}
gs.transfer_key = key.clone();
if key.is_empty() {
return;
}
if let Some(&idx) = ext_gstate_map.get(&key) {
writeln!(buf, "/GS{} gs", idx).unwrap();
return;
}
let idx = ext_gstates.len();
let entries = vec![(b"Type".to_vec(), PdfObj::name("ExtGState"))];
ext_gstates.push(ExtGStateDict { entries });
ext_gstate_map.insert(key, idx);
let (tables, is_color) = if let Some(ref color) = transfer.color {
(color.to_vec(), true)
} else if let Some(ref gray) = transfer.gray {
(vec![Some(gray.clone())], false)
} else {
(vec![], false)
};
transfer_refs.push(TransferFunctionRef {
ext_gstate_idx: idx,
tables,
is_color,
});
writeln!(buf, "/GS{} gs", idx).unwrap();
}
fn build_halftone_key(halftone: &HalftoneState) -> Vec<u8> {
use std::sync::Arc;
let mut key = Vec::new();
if let Some(ref color) = halftone.color {
key.extend(b"C");
for screen in color {
if let Some(s) = screen {
let ptr = Arc::as_ptr(s) as usize;
key.extend(ptr.to_le_bytes());
} else {
key.extend(b"D"); }
}
} else if let Some(ref gray) = halftone.gray {
key.extend(b"G");
let ptr = Arc::as_ptr(gray) as usize;
key.extend(ptr.to_le_bytes());
}
key
}
fn emit_halftone(
buf: &mut Vec<u8>,
halftone: &HalftoneState,
gs: &mut GState,
ext_gstates: &mut Vec<ExtGStateDict>,
ext_gstate_map: &mut HashMap<Vec<u8>, usize>,
halftone_refs: &mut Vec<HalftoneRef>,
) {
let key = build_halftone_key(halftone);
if key == gs.halftone_key {
return;
}
gs.halftone_key = key.clone();
if key.is_empty() {
return;
}
let mut map_key = b"HT:".to_vec();
map_key.extend(&key);
if let Some(&idx) = ext_gstate_map.get(&map_key) {
writeln!(buf, "/GS{} gs", idx).unwrap();
return;
}
let idx = ext_gstates.len();
let entries = vec![(b"Type".to_vec(), PdfObj::name("ExtGState"))];
ext_gstates.push(ExtGStateDict { entries });
ext_gstate_map.insert(map_key, idx);
halftone_refs.push(HalftoneRef {
ext_gstate_idx: idx,
state: halftone.clone(),
});
writeln!(buf, "/GS{} gs", idx).unwrap();
}
fn build_bg_ucr_key(state: &BgUcrState) -> Vec<u8> {
use std::sync::Arc;
let mut key = Vec::new();
if let Some(ref bg) = state.bg {
key.extend(b"B");
let ptr = Arc::as_ptr(bg) as usize;
key.extend(ptr.to_le_bytes());
}
if let Some(ref ucr) = state.ucr {
key.extend(b"U");
let ptr = Arc::as_ptr(ucr) as usize;
key.extend(ptr.to_le_bytes());
}
key
}
fn emit_bg_ucr(
buf: &mut Vec<u8>,
state: &BgUcrState,
gs: &mut GState,
ext_gstates: &mut Vec<ExtGStateDict>,
ext_gstate_map: &mut HashMap<Vec<u8>, usize>,
bg_ucr_refs: &mut Vec<BgUcrRef>,
) {
let key = build_bg_ucr_key(state);
if key == gs.bg_ucr_key {
return;
}
gs.bg_ucr_key = key.clone();
if key.is_empty() {
return;
}
let mut map_key = b"BU:".to_vec();
map_key.extend(&key);
if let Some(&idx) = ext_gstate_map.get(&map_key) {
writeln!(buf, "/GS{} gs", idx).unwrap();
return;
}
let idx = ext_gstates.len();
let entries = vec![(b"Type".to_vec(), PdfObj::name("ExtGState"))];
ext_gstates.push(ExtGStateDict { entries });
ext_gstate_map.insert(map_key, idx);
bg_ucr_refs.push(BgUcrRef {
ext_gstate_idx: idx,
state: state.clone(),
});
writeln!(buf, "/GS{} gs", idx).unwrap();
}
fn emit_path(buf: &mut Vec<u8>, path: &PsPath) {
use stet_fonts::geometry::PathSegment;
for seg in &path.segments {
match seg {
PathSegment::MoveTo(x, y) => {
fmt_num(buf, *x);
buf.push(b' ');
fmt_num(buf, *y);
buf.extend(b" m\n");
}
PathSegment::LineTo(x, y) => {
fmt_num(buf, *x);
buf.push(b' ');
fmt_num(buf, *y);
buf.extend(b" l\n");
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x3,
y3,
} => {
fmt_num(buf, *x1);
buf.push(b' ');
fmt_num(buf, *y1);
buf.push(b' ');
fmt_num(buf, *x2);
buf.push(b' ');
fmt_num(buf, *y2);
buf.push(b' ');
fmt_num(buf, *x3);
buf.push(b' ');
fmt_num(buf, *y3);
buf.extend(b" c\n");
}
PathSegment::ClosePath => {
buf.extend(b"h\n");
}
}
}
}
fn compute_image_matrix(params: &stet_graphics::device::ImageParams) -> Matrix {
let m_scale = Matrix::new(
params.width as f64,
0.0,
0.0,
params.height as f64,
0.0,
0.0,
);
let inv_im = params.image_matrix.invert().unwrap_or(Matrix::identity());
let flip_v = Matrix::new(1.0, 0.0, 0.0, -1.0, 0.0, 1.0);
params.ctm.concat(&inv_im).concat(&m_scale).concat(&flip_v)
}
fn emit_matrix(buf: &mut Vec<u8>, m: &Matrix) {
fmt_num(buf, m.a);
buf.push(b' ');
fmt_num(buf, m.b);
buf.push(b' ');
fmt_num(buf, m.c);
buf.push(b' ');
fmt_num(buf, m.d);
buf.push(b' ');
fmt_num(buf, m.tx);
buf.push(b' ');
fmt_num(buf, m.ty);
}
fn emit_cm(buf: &mut Vec<u8>, m: &Matrix) {
emit_matrix(buf, m);
buf.extend(b" cm\n");
}
fn is_identity(m: &Matrix) -> bool {
(m.a - 1.0).abs() < 1e-10
&& m.b.abs() < 1e-10
&& m.c.abs() < 1e-10
&& (m.d - 1.0).abs() < 1e-10
&& m.tx.abs() < 1e-10
&& m.ty.abs() < 1e-10
}
fn emit_pattern_fill(
buf: &mut Vec<u8>,
params: &PatternFillParams,
gs: &mut GState,
pattern_refs: &mut Vec<PatternRef>,
pattern_map: &mut HashMap<u32, usize>,
pattern_cs_names: &mut Vec<(String, PdfObj)>,
pattern_cs_set: &mut HashSet<String>,
) {
let pat_idx = if let Some(&idx) = pattern_map.get(¶ms.pattern_id) {
idx
} else {
let idx = pattern_refs.len();
pattern_refs.push(PatternRef {
tile: params.tile.clone(),
pattern_matrix: params.pattern_matrix,
bbox: params.bbox,
xstep: params.xstep,
ystep: params.ystep,
paint_type: params.paint_type,
});
pattern_map.insert(params.pattern_id, idx);
idx
};
let pat_name = format!("P{}", pat_idx);
if params.paint_type == 2 {
let base_cs = match ¶ms.underlying_color {
Some(c) if c.native_cmyk.is_some() => "DeviceCMYK",
Some(c) if c.r == c.g && c.g == c.b => "DeviceGray",
_ => "DeviceRGB",
};
let cs_name = format!("CSP{}", pat_idx);
if !pattern_cs_set.contains(&cs_name) {
pattern_cs_names.push((
cs_name.clone(),
PdfObj::Array(vec![PdfObj::name("Pattern"), PdfObj::name(base_cs)]),
));
pattern_cs_set.insert(cs_name.clone());
}
if gs.fill_cs_name.as_deref() != Some(&cs_name) {
writeln!(buf, "/{} cs", cs_name).unwrap();
gs.fill_cs_name = Some(cs_name);
gs.fill_color = None;
}
if let Some(color) = ¶ms.underlying_color {
if let Some((c, m, y, k)) = color.native_cmyk {
fmt_num(buf, c);
buf.push(b' ');
fmt_num(buf, m);
buf.push(b' ');
fmt_num(buf, y);
buf.push(b' ');
fmt_num(buf, k);
} else if color.r == color.g && color.g == color.b {
fmt_num(buf, color.r);
} else {
fmt_num(buf, color.r);
buf.push(b' ');
fmt_num(buf, color.g);
buf.push(b' ');
fmt_num(buf, color.b);
}
buf.push(b' ');
}
writeln!(buf, "/{} scn", pat_name).unwrap();
} else {
if gs.fill_cs_name.as_deref() != Some("Pattern") {
buf.extend(b"/Pattern cs\n");
gs.fill_cs_name = Some("Pattern".to_string());
gs.fill_color = None;
}
writeln!(buf, "/{} scn", pat_name).unwrap();
}
emit_path(buf, ¶ms.path);
if params.fill_rule == FillRule::EvenOdd {
buf.extend(b"f*\n");
} else {
buf.extend(b"f\n");
}
}
pub(crate) fn fmt_num(buf: &mut Vec<u8>, v: f64) {
if v == 0.0 {
buf.push(b'0');
} else if v == v.round() && v.abs() < 2_147_483_647.0 {
write!(buf, "{}", v as i64).unwrap();
} else {
let s = format!("{:.4}", v);
let s = s.trim_end_matches('0');
let s = s.trim_end_matches('.');
buf.extend(s.as_bytes());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fmt_num() {
let mut buf = Vec::new();
fmt_num(&mut buf, 0.0);
assert_eq!(&buf, b"0");
buf.clear();
fmt_num(&mut buf, 1.0);
assert_eq!(&buf, b"1");
buf.clear();
fmt_num(&mut buf, -42.0);
assert_eq!(&buf, b"-42");
buf.clear();
fmt_num(&mut buf, 1.5);
assert_eq!(&buf, b"1.5");
buf.clear();
fmt_num(&mut buf, 0.001);
assert_eq!(&buf, b"0.001");
}
#[test]
fn test_color_to_pdf() {
let gray = DeviceColor::from_gray(0.5);
assert_eq!(color_to_pdf(&gray), PdfColor::Gray(5000));
let rgb = DeviceColor::from_rgb(1.0, 0.0, 0.5);
assert_eq!(color_to_pdf(&rgb), PdfColor::Rgb(10000, 0, 5000));
}
}