use crate::content::graphics_state::{GraphicsState, SoftMaskForm, SoftMaskSubtype};
use crate::document::PdfDocument;
use crate::error::Result;
use crate::object::Object;
#[derive(Clone, Debug)]
pub(crate) enum SoftMaskValue {
None,
Form(SoftMaskForm),
}
#[derive(Clone, Debug, Default)]
pub(crate) struct ParsedExtGState {
pub(crate) fill_alpha: Option<f32>,
pub(crate) stroke_alpha: Option<f32>,
pub(crate) blend_mode: Option<String>,
pub(crate) stroke_overprint: Option<bool>,
pub(crate) fill_overprint: Option<bool>,
pub(crate) overprint_mode: Option<u8>,
pub(crate) smask: Option<SoftMaskValue>,
}
impl ParsedExtGState {
pub(crate) fn apply(&self, gs: &mut GraphicsState) {
if let Some(a) = self.fill_alpha {
gs.fill_alpha = a;
}
if let Some(a) = self.stroke_alpha {
gs.stroke_alpha = a;
}
if let Some(ref m) = self.blend_mode {
gs.blend_mode = m.clone();
}
if let Some(v) = self.fill_overprint {
gs.fill_overprint = v;
}
if let Some(v) = self.stroke_overprint {
gs.stroke_overprint = v;
}
if let Some(v) = self.overprint_mode {
gs.overprint_mode = v;
}
if let Some(ref sm) = self.smask {
match sm {
SoftMaskValue::None => {
gs.smask = None;
}
SoftMaskValue::Form(f) => {
gs.smask = Some(f.clone());
}
}
}
}
}
pub(crate) fn parse_ext_g_state_inner(state_obj: &Object, doc: &PdfDocument) -> Result<ParsedExtGState> {
let mut out = ParsedExtGState::default();
let state_resolved = doc.resolve_object(state_obj)?;
let state_dict = match state_resolved.as_dict() {
Some(d) => d,
None => return Ok(out),
};
let read = |key: &str| -> Option<Object> {
let raw = state_dict.get(key)?;
doc.resolve_object(raw).ok()
};
if let Some(ca) = read("ca") {
out.fill_alpha = ca
.as_real()
.map(|v| v as f32)
.or_else(|| ca.as_integer().map(|v| v as f32));
}
if let Some(ca_upper) = read("CA") {
out.stroke_alpha = ca_upper
.as_real()
.map(|v| v as f32)
.or_else(|| ca_upper.as_integer().map(|v| v as f32));
}
if let Some(bm) = read("BM") {
out.blend_mode = Some(parse_blend_mode(&bm, doc));
}
let op_stroke = read("OP").and_then(|v| v.as_bool());
let op_fill = read("op").and_then(|v| v.as_bool());
out.stroke_overprint = op_stroke;
out.fill_overprint = op_fill.or(op_stroke);
if let Some(opm) = read("OPM").and_then(|v| v.as_integer()) {
out.overprint_mode = Some(if opm == 1 { 1 } else { 0 });
}
if let Some(smask_obj) = state_dict.get("SMask") {
out.smask = parse_smask(smask_obj, doc);
}
Ok(out)
}
fn parse_blend_mode(bm: &Object, doc: &PdfDocument) -> String {
match bm {
Object::Name(n) => n.clone(),
Object::Array(arr) => arr
.iter()
.filter_map(|elem| doc.resolve_object(elem).ok())
.filter_map(|elem| elem.as_name().map(str::to_string))
.find(|name| crate::rendering::sidecar::is_recognised_mode(name))
.unwrap_or_else(|| "Normal".to_string()),
_ => "Normal".to_string(),
}
}
fn parse_smask(smask_obj: &Object, doc: &PdfDocument) -> Option<SoftMaskValue> {
let resolved = doc.resolve_object(smask_obj).unwrap_or(smask_obj.clone());
let mask_dict = match &resolved {
Object::Name(n) if n == "None" => return Some(SoftMaskValue::None),
Object::Dictionary(mask_dict) => mask_dict,
_ => return None,
};
let resolve_in_smask = |key: &str| -> Option<Object> {
let raw = mask_dict.get(key)?;
doc.resolve_object(raw).ok()
};
let subtype = match resolve_in_smask("S").as_ref().and_then(Object::as_name) {
Some("Alpha") => SoftMaskSubtype::Alpha,
Some("Luminosity") => SoftMaskSubtype::Luminosity,
_ => SoftMaskSubtype::Alpha,
};
let form_ref = mask_dict.get("G").and_then(|o| match o {
Object::Reference(r) => Some(*r),
_ => None,
})?;
let backdrop = if subtype == SoftMaskSubtype::Luminosity {
resolve_in_smask("BC").and_then(|o| {
o.as_array().map(|arr| {
arr.iter()
.filter_map(|v| doc.resolve_object(v).ok())
.filter_map(|v| {
v.as_real()
.map(|r| r as f32)
.or_else(|| v.as_integer().map(|i| i as f32))
})
.collect::<Vec<f32>>()
})
})
} else {
None
};
let transfer = resolve_in_smask("TR");
Some(SoftMaskValue::Form(SoftMaskForm {
form_ref,
subtype,
backdrop,
transfer,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn fixture_doc() -> PdfDocument {
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(b"%PDF-1.4\n");
let cat_off = buf.len();
buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let pages_off = buf.len();
buf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
let xref_off = buf.len();
buf.extend_from_slice(b"xref\n0 3\n0000000000 65535 f \n");
buf.extend_from_slice(format!("{:010} 00000 n \n", cat_off).as_bytes());
buf.extend_from_slice(format!("{:010} 00000 n \n", pages_off).as_bytes());
buf.extend_from_slice(
format!("trailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{}\n%%EOF\n", xref_off).as_bytes(),
);
PdfDocument::from_bytes(buf).expect("fixture PDF parses")
}
fn dict(entries: &[(&str, Object)]) -> Object {
let mut m = HashMap::new();
for (k, v) in entries {
m.insert((*k).to_string(), v.clone());
}
Object::Dictionary(m)
}
#[test]
fn parses_op_op_opm_from_extgstate_dict() {
let obj = dict(&[
("OP", Object::Boolean(true)),
("op", Object::Boolean(false)),
("OPM", Object::Integer(1)),
]);
let doc = fixture_doc();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.stroke_overprint, Some(true));
assert_eq!(parsed.fill_overprint, Some(false));
assert_eq!(parsed.overprint_mode, Some(1));
}
#[test]
fn op_without_op_sets_both_overprints() {
let obj = dict(&[("OP", Object::Boolean(true))]);
let doc = fixture_doc();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.stroke_overprint, Some(true));
assert_eq!(parsed.fill_overprint, Some(true));
}
#[test]
fn op_without_op_uppercase_only_does_not_affect_stroke() {
let obj = dict(&[("op", Object::Boolean(true))]);
let doc = fixture_doc();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.stroke_overprint, None);
assert_eq!(parsed.fill_overprint, Some(true));
}
#[test]
fn opm_clamps_unknown_values_to_zero() {
let obj = dict(&[("OPM", Object::Integer(42))]);
let doc = fixture_doc();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.overprint_mode, Some(0));
}
#[test]
fn missing_overprint_keys_leave_options_none() {
let obj = dict(&[]);
let doc = fixture_doc();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.stroke_overprint, None);
assert_eq!(parsed.fill_overprint, None);
assert_eq!(parsed.overprint_mode, None);
}
fn fixture_doc_with_indirect_values() -> PdfDocument {
use crate::object::ObjectRef;
let _ = ObjectRef::new(0, 0);
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(b"%PDF-1.4\n");
let mut offsets: Vec<usize> = Vec::new();
let mut emit = |buf: &mut Vec<u8>, body: &str| {
let off = buf.len();
buf.extend_from_slice(body.as_bytes());
offsets.push(off);
};
emit(&mut buf, "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
emit(&mut buf, "2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
emit(&mut buf, "3 0 obj\n0.5\nendobj\n");
emit(&mut buf, "4 0 obj\ntrue\nendobj\n");
emit(&mut buf, "5 0 obj\n1\nendobj\n");
emit(&mut buf, "6 0 obj\n/Multiply\nendobj\n");
emit(&mut buf, "7 0 obj\n[/Multiply]\nendobj\n");
let xref_off = buf.len();
buf.extend_from_slice(b"xref\n0 8\n0000000000 65535 f \n");
for off in &offsets {
buf.extend_from_slice(format!("{:010} 00000 n \n", off).as_bytes());
}
buf.extend_from_slice(
format!("trailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n{}\n%%EOF\n", xref_off).as_bytes(),
);
PdfDocument::from_bytes(buf).expect("fixture PDF parses")
}
fn obj_ref(num: u32) -> Object {
use crate::object::ObjectRef;
Object::Reference(ObjectRef::new(num, 0))
}
#[test]
fn resolves_indirect_fill_alpha() {
let obj = dict(&[("ca", obj_ref(3))]);
let doc = fixture_doc_with_indirect_values();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.fill_alpha, Some(0.5_f32));
}
#[test]
fn resolves_indirect_stroke_alpha() {
let obj = dict(&[("CA", obj_ref(3))]);
let doc = fixture_doc_with_indirect_values();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.stroke_alpha, Some(0.5_f32));
}
#[test]
fn resolves_indirect_blend_mode_name() {
let obj = dict(&[("BM", obj_ref(6))]);
let doc = fixture_doc_with_indirect_values();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.blend_mode.as_deref(), Some("Multiply"));
}
#[test]
fn resolves_indirect_blend_mode_array() {
let obj = dict(&[("BM", obj_ref(7))]);
let doc = fixture_doc_with_indirect_values();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.blend_mode.as_deref(), Some("Multiply"));
}
#[test]
fn resolves_indirect_op_op_opm() {
let obj = dict(&[("OP", obj_ref(4)), ("op", obj_ref(4)), ("OPM", obj_ref(5))]);
let doc = fixture_doc_with_indirect_values();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.stroke_overprint, Some(true));
assert_eq!(parsed.fill_overprint, Some(true));
assert_eq!(parsed.overprint_mode, Some(1));
}
#[test]
fn resolves_indirect_blend_mode_array_with_indirect_elements() {
use crate::object::ObjectRef;
let array_with_indirect_name = Object::Array(vec![Object::Reference(ObjectRef::new(6, 0))]);
let obj = dict(&[("BM", array_with_indirect_name)]);
let doc = fixture_doc_with_indirect_values();
let parsed = parse_ext_g_state_inner(&obj, &doc).expect("parses");
assert_eq!(parsed.blend_mode.as_deref(), Some("Multiply"));
}
}