use crate::error::{RedactError, Result};
use crate::redact::RedactionArea;
use lopdf::content::Operation;
use lopdf::{dictionary, Dictionary, Document, Object, ObjectId, Stream, StringFormat};
use pdf_manip::content_editor::ContentEditor;
use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::rc::Rc;
type Matrix = [f64; 6];
type Point = (f64, f64);
const IDENTITY: Matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
const UNKNOWN_WIDTH_MAX: f64 = 1200.0;
const GLYPH_OVERLAP_SHARE: f64 = 0.1;
const MAX_FORM_DEPTH: usize = 32;
const TEXT_PROPERTY_KEYS: [&[u8]; 3] = [b"ActualText", b"Alt", b"E"];
fn refuse(what: impl std::fmt::Display) -> RedactError {
RedactError::Other(format!("area redaction refused: {what}"))
}
fn mul(a: Matrix, b: Matrix) -> Matrix {
[
a[0] * b[0] + a[1] * b[2],
a[0] * b[1] + a[1] * b[3],
a[2] * b[0] + a[3] * b[2],
a[2] * b[1] + a[3] * b[3],
a[4] * b[0] + a[5] * b[2] + b[4],
a[4] * b[1] + a[5] * b[3] + b[5],
]
}
fn translate(tx: f64, ty: f64) -> Matrix {
[1.0, 0.0, 0.0, 1.0, tx, ty]
}
fn apply(m: &Matrix, x: f64, y: f64) -> Point {
(m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5])
}
fn quad(m: &Matrix, x0: f64, y0: f64, x1: f64, y1: f64) -> [Point; 4] {
[
apply(m, x0, y0),
apply(m, x1, y0),
apply(m, x1, y1),
apply(m, x0, y1),
]
}
fn bounds(points: &[Point]) -> [f64; 4] {
let mut b = [
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
];
for &(x, y) in points {
b[0] = b[0].min(x);
b[1] = b[1].min(y);
b[2] = b[2].max(x);
b[3] = b[3].max(y);
}
b
}
fn bounds_touch(a: &[f64; 4], r: &[f64; 4]) -> bool {
a[0] <= r[2] && a[2] >= r[0] && a[1] <= r[3] && a[3] >= r[1]
}
fn intersect(a: Option<[f64; 4]>, b: Option<[f64; 4]>) -> Option<[f64; 4]> {
match (a, b) {
(None, x) | (x, None) => x,
(Some(a), Some(b)) => Some([
a[0].max(b[0]),
a[1].max(b[1]),
a[2].min(b[2]),
a[3].min(b[3]),
]),
}
}
fn polygon_area(points: &[Point]) -> f64 {
if points.len() < 3 {
return 0.0;
}
let mut sum = 0.0;
for i in 0..points.len() {
let (x0, y0) = points[i];
let (x1, y1) = points[(i + 1) % points.len()];
sum += x0 * y1 - x1 * y0;
}
(sum / 2.0).abs()
}
fn clipped_area(points: &[Point], r: &[f64; 4]) -> f64 {
let mut poly: Vec<Point> = points.to_vec();
let edges = [
(0, r[0], true),
(0, r[2], false),
(1, r[1], true),
(1, r[3], false),
];
for (axis, bound, keep_ge) in edges {
if poly.is_empty() {
return 0.0;
}
let inside = |p: &Point| {
let v = if axis == 0 { p.0 } else { p.1 };
if keep_ge {
v >= bound
} else {
v <= bound
}
};
let mut out = Vec::with_capacity(poly.len() + 4);
for i in 0..poly.len() {
let cur = poly[i];
let prev = poly[(i + poly.len() - 1) % poly.len()];
let (cin, pin) = (inside(&cur), inside(&prev));
if cin != pin {
let (pc, pp) = if axis == 0 {
(cur.0, prev.0)
} else {
(cur.1, prev.1)
};
let t = (bound - pp) / (pc - pp);
out.push((prev.0 + t * (cur.0 - prev.0), prev.1 + t * (cur.1 - prev.1)));
}
if cin {
out.push(cur);
}
}
poly = out;
}
polygon_area(&poly)
}
struct Areas(Vec<[f64; 4]>);
impl Areas {
fn new(areas: &[&RedactionArea]) -> Self {
Areas(
areas
.iter()
.map(|a| {
let [x0, y0, x1, y1] = a.rect;
[x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)]
})
.collect(),
)
}
fn touches(&self, points: &[Point]) -> bool {
let b = bounds(points);
self.0.iter().any(|r| bounds_touch(&b, r))
}
fn takes_glyph(&self, glyph: &[Point; 4], exact: bool) -> bool {
let area = polygon_area(glyph);
if area < 1e-9 {
return self.touches(glyph);
}
self.0.iter().any(|r| {
let overlap = clipped_area(glyph, r);
if exact {
overlap >= GLYPH_OVERLAP_SHARE * area
} else {
overlap > 0.0
}
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Coding {
OneByte,
TwoByte,
Opaque,
}
struct StandardWidths {
ascii: &'static [u16; 95],
quotes: u16,
}
const HELVETICA: [u16; 95] = [
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667,
611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500,
222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
];
const HELVETICA_BOLD: [u16; 95] = [
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667,
611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
667, 611, 333, 278, 333, 584, 556, 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556,
278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
];
const TIMES_ROMAN: [u16; 95] = [
250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278, 500, 500, 500,
500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444, 921, 722, 667, 667, 722, 611,
556, 722, 722, 333, 389, 722, 611, 889, 722, 722, 556, 722, 667, 556, 611, 722, 722, 944, 722,
722, 611, 333, 278, 333, 469, 500, 333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500,
278, 778, 500, 500, 500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541,
];
const TIMES_BOLD: [u16; 95] = [
250, 333, 555, 500, 500, 1000, 833, 278, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500,
500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 930, 722, 667, 722, 722, 667,
611, 778, 778, 389, 500, 778, 667, 944, 722, 778, 611, 778, 722, 556, 667, 722, 722, 1000, 722,
722, 667, 333, 278, 333, 581, 500, 333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556,
278, 833, 556, 500, 556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520,
];
const TIMES_ITALIC: [u16; 95] = [
250, 333, 420, 500, 500, 833, 778, 214, 333, 333, 500, 675, 250, 333, 250, 278, 500, 500, 500,
500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500, 920, 611, 611, 667, 722, 611,
611, 722, 722, 333, 444, 667, 556, 833, 667, 722, 611, 722, 611, 500, 556, 722, 611, 833, 611,
556, 556, 389, 278, 389, 422, 500, 333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444,
278, 722, 500, 500, 500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541,
];
const TIMES_BOLD_ITALIC: [u16; 95] = [
250, 389, 555, 500, 500, 833, 778, 278, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500,
500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 832, 667, 667, 667, 722, 667,
667, 722, 778, 389, 500, 667, 611, 889, 722, 722, 611, 722, 667, 556, 611, 722, 667, 889, 667,
611, 611, 333, 278, 333, 570, 500, 333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500,
278, 778, 556, 500, 500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570,
];
const COURIER: [u16; 95] = [600; 95];
fn standard_widths(base_font: &str) -> Option<StandardWidths> {
let (ascii, quotes): (&'static [u16; 95], u16) = match base_font {
"Helvetica" | "Helvetica-Oblique" => (&HELVETICA, 222),
"Helvetica-Bold" | "Helvetica-BoldOblique" => (&HELVETICA_BOLD, 278),
"Times-Roman" => (&TIMES_ROMAN, 333),
"Times-Bold" => (&TIMES_BOLD, 333),
"Times-Italic" => (&TIMES_ITALIC, 333),
"Times-BoldItalic" => (&TIMES_BOLD_ITALIC, 333),
"Courier" | "Courier-Bold" | "Courier-Oblique" | "Courier-BoldOblique" => (&COURIER, 600),
_ => return None,
};
Some(StandardWidths { ascii, quotes })
}
struct FontMetrics {
coding: Coding,
widths: Option<(u32, Vec<f64>)>,
missing_width: Option<f64>,
standard: Option<StandardWidths>,
remapped: HashSet<u32>,
cid_widths: HashMap<u32, f64>,
default_width: Option<f64>,
max_width: f64,
ascent: f64,
descent: f64,
refusal: Option<String>,
}
impl FontMetrics {
fn unknown() -> Self {
FontMetrics {
coding: Coding::OneByte,
widths: None,
missing_width: None,
standard: None,
remapped: HashSet::new(),
cid_widths: HashMap::new(),
default_width: None,
max_width: UNKNOWN_WIDTH_MAX,
ascent: 950.0,
descent: -250.0,
refusal: None,
}
}
fn width(&self, code: u32) -> (f64, f64) {
let unknown = (0.0, self.max_width);
match self.coding {
Coding::Opaque => unknown,
Coding::TwoByte => match self.cid_widths.get(&code).copied().or(self.default_width) {
Some(w) => (w, w),
None => unknown,
},
Coding::OneByte => {
if let Some((first, ref widths)) = self.widths {
if let Some(&w) = code.checked_sub(first).and_then(|i| widths.get(i as usize)) {
return (w, w);
}
return match self.missing_width {
Some(w) => (w, w),
None => unknown,
};
}
if let Some(ref std) = self.standard {
if (32..=126).contains(&code) && !self.remapped.contains(&code) {
let w = std.ascii[(code - 32) as usize] as f64;
if code == 0x27 || code == 0x60 {
let q = std.quotes as f64;
return (w.min(q), w.max(q));
}
return (w, w);
}
}
unknown
}
}
}
}
fn resolve<'a>(doc: &'a Document, obj: &'a Object) -> &'a Object {
let mut cur = obj;
for _ in 0..8 {
match cur {
Object::Reference(id) => match doc.get_object(*id) {
Ok(o) => cur = o,
Err(_) => return cur,
},
_ => return cur,
}
}
cur
}
fn dict_of<'a>(doc: &'a Document, obj: &'a Object) -> Option<&'a Dictionary> {
match resolve(doc, obj) {
Object::Dictionary(d) => Some(d),
Object::Stream(s) => Some(&s.dict),
_ => None,
}
}
fn number(doc: &Document, obj: &Object) -> Option<f64> {
match resolve(doc, obj) {
Object::Integer(i) => Some(*i as f64),
Object::Real(r) => Some(*r as f64),
_ => None,
}
}
fn get<'a>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> Option<&'a Object> {
dict.get(key).ok().map(|o| resolve(doc, o))
}
fn vertical_extent(
doc: &Document,
descriptor: Option<&Dictionary>,
font: &Dictionary,
) -> (f64, f64) {
let defaults = (950.0, -250.0);
let from_bbox = |d: &Dictionary| -> Option<(f64, f64)> {
match get(doc, d, b"FontBBox")? {
Object::Array(a) if a.len() == 4 => Some((number(doc, &a[3])?, number(doc, &a[1])?)),
_ => None,
}
};
let stated = descriptor.and_then(|d| {
let ascent = get(doc, d, b"Ascent").and_then(|o| number(doc, o));
let descent = get(doc, d, b"Descent").and_then(|o| number(doc, o));
match (ascent, descent) {
(Some(a), Some(d)) => Some((a, d)),
_ => None,
}
});
let (ascent, descent) = stated
.or_else(|| descriptor.and_then(from_bbox))
.or_else(|| from_bbox(font))
.unwrap_or(defaults);
if ascent <= descent || ascent <= 0.0 {
return defaults;
}
(ascent, descent.min(0.0))
}
fn build_font(doc: &Document, font: &Dictionary) -> FontMetrics {
let mut m = FontMetrics::unknown();
let subtype = get(doc, font, b"Subtype").and_then(|o| match o {
Object::Name(n) => Some(n.as_slice()),
_ => None,
});
match subtype {
Some(b"Type3") => {
m.refusal = Some(
"text in a Type3 font: its glyphs are content streams the filter does not place"
.into(),
);
m
}
Some(b"Type0") => {
m.coding = Coding::Opaque;
match get(doc, font, b"Encoding") {
Some(Object::Name(n)) if n.as_slice() == b"Identity-H" => {
m.coding = Coding::TwoByte
}
Some(Object::Name(n)) if n.ends_with(b"-V") => {
m.refusal = Some(format!(
"vertical writing (CMap {}) is not supported",
String::from_utf8_lossy(n)
));
}
Some(Object::Stream(s)) => {
if s.dict.get(b"WMode").ok().and_then(|o| number(doc, o)) == Some(1.0) {
m.refusal = Some(
"vertical writing (embedded CMap, WMode 1) is not supported".into(),
);
}
}
_ => {}
}
let descendant = match get(doc, font, b"DescendantFonts") {
Some(Object::Array(a)) => a.first().and_then(|o| dict_of(doc, o)),
_ => None,
};
let descriptor = descendant
.and_then(|d| get(doc, d, b"FontDescriptor"))
.and_then(|o| match o {
Object::Dictionary(d) => Some(d),
_ => None,
});
if let Some(cid) = descendant {
m.default_width = Some(
get(doc, cid, b"DW")
.and_then(|o| number(doc, o))
.unwrap_or(1000.0),
);
if let Some(Object::Array(w)) = get(doc, cid, b"W") {
parse_cid_widths(doc, w, &mut m.cid_widths);
}
let (a, d) = vertical_extent(doc, descriptor, cid);
m.ascent = a;
m.descent = d;
}
let widest = m
.cid_widths
.values()
.copied()
.chain(m.default_width)
.fold(0.0_f64, f64::max);
m.max_width = widest.max(UNKNOWN_WIDTH_MAX);
m
}
_ => {
let descriptor = get(doc, font, b"FontDescriptor").and_then(|o| match o {
Object::Dictionary(d) => Some(d),
_ => None,
});
if let Some(Object::Array(widths)) = get(doc, font, b"Widths") {
let first = get(doc, font, b"FirstChar")
.and_then(|o| number(doc, o))
.unwrap_or(0.0)
.max(0.0) as u32;
let ws: Vec<f64> = widths
.iter()
.map(|o| number(doc, o).unwrap_or(UNKNOWN_WIDTH_MAX))
.collect();
m.widths = Some((first, ws));
m.missing_width = descriptor
.and_then(|d| get(doc, d, b"MissingWidth"))
.and_then(|o| number(doc, o));
} else {
let embedded = descriptor.is_some_and(|d| {
d.has(b"FontFile") || d.has(b"FontFile2") || d.has(b"FontFile3")
});
let base = get(doc, font, b"BaseFont")
.and_then(|o| match o {
Object::Name(n) => Some(String::from_utf8_lossy(n).into_owned()),
_ => None,
})
.unwrap_or_default();
let base = match base.split_once('+') {
Some((prefix, rest)) if prefix.len() == 6 => rest.to_string(),
_ => base,
};
let encoding_fits = match get(doc, font, b"Encoding") {
None => true,
Some(Object::Name(n)) => is_latin_base_encoding(n),
Some(Object::Dictionary(d)) => {
let base_ok = match get(doc, d, b"BaseEncoding") {
None => true,
Some(Object::Name(n)) => is_latin_base_encoding(n),
Some(_) => false,
};
if let Some(Object::Array(diffs)) = get(doc, d, b"Differences") {
let mut code = 0u32;
for item in diffs {
match resolve(doc, item) {
Object::Integer(i) => code = (*i).max(0) as u32,
Object::Name(_) => {
m.remapped.insert(code);
code = code.saturating_add(1);
}
_ => {}
}
}
}
base_ok
}
Some(_) => false,
};
if !embedded && encoding_fits {
m.standard = standard_widths(&base);
}
}
let widest = m
.widths
.iter()
.flat_map(|(_, w)| w.iter().copied())
.chain(m.missing_width)
.fold(0.0_f64, f64::max);
m.max_width = widest.max(UNKNOWN_WIDTH_MAX);
let (a, d) = vertical_extent(doc, descriptor, font);
m.ascent = a;
m.descent = d;
m
}
}
}
fn is_latin_base_encoding(name: &[u8]) -> bool {
matches!(
name,
b"StandardEncoding" | b"WinAnsiEncoding" | b"MacRomanEncoding"
)
}
fn parse_cid_widths(doc: &Document, w: &[Object], out: &mut HashMap<u32, f64>) {
const MAX_ENTRIES: usize = 1 << 17;
let mut i = 0;
while i + 1 < w.len() && out.len() < MAX_ENTRIES {
let Some(first) = number(doc, &w[i]) else {
return;
};
let first = first.max(0.0) as u32;
match resolve(doc, &w[i + 1]) {
Object::Array(ws) => {
for (k, o) in ws.iter().enumerate() {
if let Some(v) = number(doc, o) {
out.insert(first + k as u32, v);
}
}
i += 2;
}
other => {
let (Some(last), Some(v)) = (
number(doc, other),
w.get(i + 2).and_then(|o| number(doc, o)),
) else {
return;
};
let last = (last.max(0.0) as u32).min(first.saturating_add(MAX_ENTRIES as u32));
for c in first..=last {
out.insert(c, v);
}
i += 3;
}
}
}
}
struct Container {
resources: Dictionary,
xobjects: Dictionary,
xobjects_changed: bool,
redirected: HashSet<Vec<u8>>,
fonts: HashMap<Vec<u8>, Rc<FontMetrics>>,
owner_page: Option<ObjectId>,
black_images: HashMap<ObjectId, Vec<u8>>,
shadings_dropped: HashSet<Vec<u8>>,
}
impl Container {
fn new(doc: &Document, resources: Dictionary, owner_page: Option<ObjectId>) -> Self {
let xobjects = resources
.get(b"XObject")
.ok()
.and_then(|o| dict_of(doc, o))
.cloned()
.unwrap_or_default();
Container {
resources,
xobjects,
xobjects_changed: false,
redirected: HashSet::new(),
fonts: HashMap::new(),
owner_page,
black_images: HashMap::new(),
shadings_dropped: HashSet::new(),
}
}
fn sub_dict<'a>(&'a self, doc: &'a Document, key: &[u8]) -> Option<&'a Dictionary> {
self.resources.get(key).ok().and_then(|o| dict_of(doc, o))
}
fn font(&mut self, doc: &Document, name: &[u8]) -> Rc<FontMetrics> {
if let Some(f) = self.fonts.get(name) {
return f.clone();
}
let metrics = self
.sub_dict(doc, b"Font")
.and_then(|fonts| fonts.get(name).ok())
.and_then(|o| dict_of(doc, o))
.map(|d| build_font(doc, d))
.unwrap_or_else(FontMetrics::unknown);
let metrics = Rc::new(metrics);
self.fonts.insert(name.to_vec(), metrics.clone());
metrics
}
fn add_xobject(&mut self, base: &[u8], id: ObjectId) -> Vec<u8> {
let mut n = 1;
let name = loop {
let mut candidate = base.to_vec();
candidate.extend_from_slice(format!("_R{n}").as_bytes());
if !self.xobjects.has(&candidate) {
break candidate;
}
n += 1;
};
self.xobjects.set(name.clone(), Object::Reference(id));
self.xobjects_changed = true;
name
}
fn finish(mut self, doc: &Document, ops: &[Operation]) -> (Option<Dictionary>, Vec<ObjectId>) {
let used = |operator: &str| -> HashSet<Vec<u8>> {
ops.iter()
.filter(|op| op.operator == operator)
.filter_map(|op| match op.operands.first() {
Some(Object::Name(n)) => Some(n.clone()),
_ => None,
})
.collect()
};
let mut changed = false;
let mut dropped = Vec::new();
if self.xobjects_changed {
let used = used("Do");
for name in &self.redirected {
if !used.contains(name) {
self.xobjects.remove(name);
}
}
self.resources
.set("XObject", Object::Dictionary(self.xobjects));
changed = true;
}
if !self.shadings_dropped.is_empty() {
let used = used("sh");
let mut shadings = self
.resources
.get(b"Shading")
.ok()
.and_then(|o| dict_of(doc, o))
.cloned()
.unwrap_or_default();
for name in &self.shadings_dropped {
if used.contains(name) {
continue;
}
if let Ok(Object::Reference(id)) = shadings.get(name) {
dropped.push(*id);
}
shadings.remove(name);
}
self.resources.set("Shading", Object::Dictionary(shadings));
changed = true;
}
(changed.then_some(self.resources), dropped)
}
}
#[derive(Clone)]
struct GState {
ctm: Matrix,
clip: Option<[f64; 4]>,
line_width: f64,
font: Rc<FontMetrics>,
font_size: f64,
char_spacing: f64,
word_spacing: f64,
horizontal_scaling: f64,
leading: f64,
rise: f64,
}
impl GState {
fn new(ctm: Matrix) -> Self {
GState {
ctm,
clip: None,
line_width: 1.0,
font: Rc::new(FontMetrics::unknown()),
font_size: 0.0,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 1.0,
leading: 0.0,
rise: 0.0,
}
}
}
struct TextPos {
tm: Matrix,
tlm: Matrix,
pos_lo: f64,
pos_hi: f64,
}
impl TextPos {
fn new() -> Self {
TextPos {
tm: IDENTITY,
tlm: IDENTITY,
pos_lo: 0.0,
pos_hi: 0.0,
}
}
fn set_line(&mut self, tlm: Matrix) {
self.tlm = tlm;
self.tm = tlm;
self.pos_lo = 0.0;
self.pos_hi = 0.0;
}
fn next_line(&mut self, leading: f64) {
self.set_line(mul(translate(0.0, -leading), self.tlm));
}
}
struct Filtered {
ops: Vec<Operation>,
changed: bool,
removed: usize,
}
struct Filter<'a> {
areas: &'a Areas,
orphans: Vec<ObjectId>,
form_stack: Vec<ObjectId>,
location: String,
text_hits: usize,
}
fn nums(op: &Operation, n: usize) -> Option<Vec<f64>> {
if op.operands.len() < n {
return None;
}
op.operands[op.operands.len() - n..]
.iter()
.map(|o| match o {
Object::Integer(i) => Some(*i as f64),
Object::Real(r) => Some(*r as f64),
_ => None,
})
.collect()
}
fn matrix_operands(op: &Operation) -> Option<Matrix> {
let v = nums(op, 6)?;
Some([v[0], v[1], v[2], v[3], v[4], v[5]])
}
fn op(name: &str, operands: Vec<Object>) -> Operation {
Operation::new(name, operands)
}
fn real(v: f64) -> Object {
Object::Real(v as f32)
}
fn is_path_construction(op: &str) -> bool {
matches!(op, "m" | "l" | "c" | "v" | "y" | "h" | "re")
}
fn is_path_painting(op: &str) -> bool {
matches!(
op,
"S" | "s" | "f" | "F" | "f*" | "B" | "B*" | "b" | "b*" | "n"
)
}
fn strokes(op: &str) -> bool {
matches!(op, "S" | "s" | "B" | "B*" | "b" | "b*")
}
fn black_image_dict() -> Dictionary {
dictionary! {
"Type" => "XObject",
"Subtype" => "Image",
"Width" => 1_i64,
"Height" => 1_i64,
"ColorSpace" => "DeviceGray",
"BitsPerComponent" => 8_i64,
}
}
fn compress(data: &[u8]) -> Vec<u8> {
let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
if encoder.write_all(data).is_ok() {
if let Ok(out) = encoder.finish() {
return out;
}
}
data.to_vec()
}
fn flate_stream(mut dict: Dictionary, content: &[u8]) -> Stream {
dict.remove(b"DecodeParms");
dict.remove(b"Length");
dict.set("Filter", Object::Name(b"FlateDecode".to_vec()));
let compressed = compress(content);
Stream::new(dict, compressed)
}
impl<'a> Filter<'a> {
fn refuse(&self, what: impl std::fmt::Display) -> RedactError {
refuse(format!("{what} ({})", self.location))
}
fn filter(
&mut self,
doc: &mut Document,
ops: &[Operation],
container: &mut Container,
initial: GState,
) -> Result<Filtered> {
let mut out: Vec<Operation> = Vec::with_capacity(ops.len());
let mut changed = false;
let mut removed = 0usize;
let mut gs = initial;
let mut stack: Vec<GState> = Vec::new();
let mut text = TextPos::new();
let mut path: Vec<Operation> = Vec::new();
let mut path_points: Vec<Point> = Vec::new();
let mut path_clips = false;
let mut marked: Vec<Option<usize>> = Vec::new();
let mut tainted: HashSet<usize> = HashSet::new();
macro_rules! taint {
() => {
for idx in marked.iter().flatten() {
tainted.insert(*idx);
}
};
}
for o in ops {
let name = o.operator.as_str();
if !path.is_empty()
&& !is_path_construction(name)
&& !is_path_painting(name)
&& name != "W"
&& name != "W*"
{
out.append(&mut path);
path_points.clear();
path_clips = false;
}
match name {
"q" => {
stack.push(gs.clone());
out.push(o.clone());
}
"Q" => {
if let Some(saved) = stack.pop() {
gs = saved;
}
out.push(o.clone());
}
"cm" => {
let m = matrix_operands(o)
.ok_or_else(|| self.refuse("a cm operator without six numbers"))?;
gs.ctm = mul(m, gs.ctm);
out.push(o.clone());
}
"w" => {
if let Some(v) = nums(o, 1) {
gs.line_width = v[0];
}
out.push(o.clone());
}
"gs" => {
if let Some(Object::Name(n)) = o.operands.first() {
self.apply_ext_gstate(doc, container, n, &mut gs);
}
out.push(o.clone());
}
"Tf" => {
if let (Some(Object::Name(n)), Some(size)) = (o.operands.first(), nums(o, 1)) {
gs.font = container.font(doc, n);
gs.font_size = size[0];
}
out.push(o.clone());
}
"Tc" | "Tw" | "Tz" | "TL" | "Ts" => {
if let Some(v) = nums(o, 1) {
let v = v[0];
match name {
"Tc" => gs.char_spacing = v,
"Tw" => gs.word_spacing = v,
"Tz" => gs.horizontal_scaling = v / 100.0,
"TL" => gs.leading = v,
_ => gs.rise = v,
}
}
out.push(o.clone());
}
"BT" => {
text = TextPos::new();
out.push(o.clone());
}
"Td" | "TD" => {
if let Some(v) = nums(o, 2) {
if name == "TD" {
gs.leading = -v[1];
}
text.set_line(mul(translate(v[0], v[1]), text.tlm));
}
out.push(o.clone());
}
"Tm" => {
if let Some(m) = matrix_operands(o) {
text.set_line(m);
}
out.push(o.clone());
}
"T*" => {
text.next_line(gs.leading);
out.push(o.clone());
}
"Tj" | "TJ" | "'" | "\"" => {
if name == "\"" {
if let Some(v) = o.operands.first().and_then(|x| match x {
Object::Integer(i) => Some(*i as f64),
Object::Real(r) => Some(*r as f64),
_ => None,
}) {
gs.word_spacing = v;
}
if let Some(v) = o.operands.get(1).and_then(|x| match x {
Object::Integer(i) => Some(*i as f64),
Object::Real(r) => Some(*r as f64),
_ => None,
}) {
gs.char_spacing = v;
}
}
if name == "'" || name == "\"" {
text.next_line(gs.leading);
}
let elements: Vec<Object> = match name {
"TJ" => match o.operands.first() {
Some(Object::Array(a)) => a.clone(),
_ => Vec::new(),
},
"\"" => o.operands.get(2).cloned().into_iter().collect(),
_ => o.operands.first().cloned().into_iter().collect(),
};
if let Some(reason) = &gs.font.refusal {
return Err(self.refuse(reason));
}
match self.show(&gs, &mut text, &elements) {
None => out.push(o.clone()),
Some(new_array) => {
match name {
"'" => out.push(op("T*", vec![])),
"\"" => {
out.push(op("Tw", vec![o.operands[0].clone()]));
out.push(op("Tc", vec![o.operands[1].clone()]));
out.push(op("T*", vec![]));
}
_ => {}
}
out.push(op("TJ", vec![Object::Array(new_array)]));
changed = true;
removed += 1;
self.text_hits += 1;
taint!();
}
}
}
n if is_path_construction(n) => {
self.path_points(o, &gs.ctm, &mut path_points);
path.push(o.clone());
}
"W" | "W*" => {
path_clips = true;
path.push(o.clone());
}
n if is_path_painting(n) => {
let new_clip = (path_clips && !path_points.is_empty())
.then(|| intersect(gs.clip, Some(bounds(&path_points))));
if path.is_empty() || n == "n" {
out.append(&mut path);
out.push(o.clone());
} else {
let mut b = bounds(&path_points);
if strokes(n) {
let scale =
(gs.ctm[0].hypot(gs.ctm[1])).max(gs.ctm[2].hypot(gs.ctm[3]));
let half = (gs.line_width.abs() * scale) / 2.0 + 1.0;
b = [b[0] - half, b[1] - half, b[2] + half, b[3] + half];
}
let hit = self.areas.0.iter().any(|r| bounds_touch(&b, r));
if !hit {
out.append(&mut path);
out.push(o.clone());
} else {
if path_clips {
out.append(&mut path);
out.push(op("n", vec![]));
} else {
path.clear();
}
changed = true;
removed += 1;
taint!();
}
}
if let Some(clip) = new_clip {
gs.clip = clip;
}
path.clear();
path_points.clear();
path_clips = false;
}
"sh" => {
let Some(Object::Name(sname)) = o.operands.first() else {
out.push(o.clone());
continue;
};
if self.shading_touches(doc, container, sname, &gs) {
container.shadings_dropped.insert(sname.clone());
changed = true;
removed += 1;
taint!();
} else {
out.push(o.clone());
}
}
"BI" => {
let unit = quad(&gs.ctm, 0.0, 0.0, 1.0, 1.0);
if self.areas.touches(&unit) {
let black = dictionary! {
"W" => 1_i64,
"H" => 1_i64,
"CS" => "G",
"BPC" => 8_i64,
};
out.push(op(
"BI",
vec![Object::Stream(Stream::new(black, vec![0u8]))],
));
changed = true;
removed += 1;
taint!();
} else {
out.push(o.clone());
}
}
"Do" => {
let Some(Object::Name(xname)) = o.operands.first() else {
out.push(o.clone());
continue;
};
match self.do_xobject(doc, container, xname, &gs)? {
DoOutcome::Keep => out.push(o.clone()),
DoOutcome::InPlace => {
removed += 1;
taint!();
}
DoOutcome::Replace(new_name, n) => {
out.push(op("Do", vec![Object::Name(new_name)]));
changed = true;
removed += n;
taint!();
}
}
}
"BDC" => {
marked.push(Some(out.len()));
out.push(o.clone());
}
"BMC" => {
marked.push(None);
out.push(o.clone());
}
"EMC" => {
marked.pop();
out.push(o.clone());
}
_ => out.push(o.clone()),
}
}
out.append(&mut path);
for idx in tainted {
if self.strip_text_properties(doc, container, &mut out[idx]) {
changed = true;
}
}
Ok(Filtered {
ops: out,
changed,
removed,
})
}
fn apply_ext_gstate(
&mut self,
doc: &Document,
container: &mut Container,
name: &[u8],
gs: &mut GState,
) {
let Some(ext) = container
.sub_dict(doc, b"ExtGState")
.and_then(|d| d.get(name).ok())
.and_then(|o| dict_of(doc, o))
.cloned()
else {
return;
};
if let Some(lw) = get(doc, &ext, b"LW").and_then(|o| number(doc, o)) {
gs.line_width = lw;
}
if let Some(Object::Array(font)) = get(doc, &ext, b"Font") {
if let (Some(f), Some(size)) = (
font.first().and_then(|o| dict_of(doc, o)),
font.get(1).and_then(|o| number(doc, o)),
) {
gs.font = Rc::new(build_font(doc, f));
gs.font_size = size;
}
}
}
fn shading_touches(
&self,
doc: &Document,
container: &Container,
name: &[u8],
gs: &GState,
) -> bool {
let own_box = container
.sub_dict(doc, b"Shading")
.and_then(|d| d.get(name).ok())
.map(|o| resolve(doc, o))
.and_then(|o| match o {
Object::Dictionary(d) => Some(d),
Object::Stream(s) => Some(&s.dict),
_ => None,
})
.and_then(|d| get(doc, d, b"BBox"))
.and_then(|o| match o {
Object::Array(a) if a.len() == 4 => {
let v: Vec<f64> = a.iter().filter_map(|x| number(doc, x)).collect();
(v.len() == 4).then(|| bounds(&quad(&gs.ctm, v[0], v[1], v[2], v[3])))
}
_ => None,
});
match intersect(gs.clip, own_box) {
None => true,
Some(region) => {
region[0] <= region[2]
&& region[1] <= region[3]
&& self.areas.0.iter().any(|r| bounds_touch(®ion, r))
}
}
}
fn path_points(&self, o: &Operation, ctm: &Matrix, points: &mut Vec<Point>) {
let v: Vec<f64> = o
.operands
.iter()
.filter_map(|x| match x {
Object::Integer(i) => Some(*i as f64),
Object::Real(r) => Some(*r as f64),
_ => None,
})
.collect();
if o.operator == "re" && v.len() >= 4 {
points.extend(quad(ctm, v[0], v[1], v[0] + v[2], v[1] + v[3]));
return;
}
for pair in v.chunks_exact(2) {
points.push(apply(ctm, pair[0], pair[1]));
}
}
fn show(&self, gs: &GState, text: &mut TextPos, elements: &[Object]) -> Option<Vec<Object>> {
let font = &gs.font;
let tfs = gs.font_size;
let th = gs.horizontal_scaling;
let to_page = mul(text.tm, gs.ctm);
let y_lo =
(font.descent / 1000.0 * tfs + gs.rise).min(font.ascent / 1000.0 * tfs + gs.rise);
let y_hi =
(font.descent / 1000.0 * tfs + gs.rise).max(font.ascent / 1000.0 * tfs + gs.rise);
let mut out: Vec<Object> = Vec::new();
let mut current: Option<(Vec<u8>, StringFormat)> = None;
let mut pending = 0.0_f64;
let mut dropped = false;
let flush = |out: &mut Vec<Object>, current: &mut Option<(Vec<u8>, StringFormat)>| {
if let Some((bytes, format)) = current.take() {
if !bytes.is_empty() {
out.push(Object::String(bytes, format));
}
}
};
for element in elements {
match element {
Object::Integer(_) | Object::Real(_) => {
let n = match element {
Object::Integer(i) => *i as f64,
Object::Real(r) => *r as f64,
_ => 0.0,
};
flush(&mut out, &mut current);
if pending != 0.0 {
out.push(real(pending));
pending = 0.0;
}
out.push(element.clone());
let shift = -n / 1000.0 * tfs * th;
text.pos_lo += shift;
text.pos_hi += shift;
}
Object::String(bytes, format) => {
for (start, len, code) in units(font.coding, bytes) {
let (w_lo, w_hi) = match font.coding {
Coding::Opaque => (0.0, font.max_width * len as f64),
_ => font.width(code),
};
let codes = if font.coding == Coding::Opaque {
len as f64
} else {
1.0
};
let spaces = match font.coding {
Coding::OneByte => (code == 32) as u8 as f64,
Coding::Opaque => bytes[start..start + len]
.iter()
.filter(|&&b| b == 32)
.count() as f64,
Coding::TwoByte => 0.0,
};
let opaque = font.coding == Coding::Opaque;
let ink = w_hi / 1000.0 * tfs * th;
let x0 = text.pos_lo + ink.min(0.0);
let x1 = text.pos_hi + ink.max(0.0);
let exact = text.pos_lo == text.pos_hi && w_lo == w_hi && !opaque;
let glyph = quad(&to_page, x0, y_lo, x1, y_hi);
let hit = self.areas.takes_glyph(&glyph, exact);
let spacing_lo = if opaque {
(gs.char_spacing * codes).min(0.0) + (gs.word_spacing * spaces).min(0.0)
} else {
gs.char_spacing + gs.word_spacing * spaces
};
let spacing_hi = if opaque {
(gs.char_spacing * codes).max(0.0) + (gs.word_spacing * spaces).max(0.0)
} else {
spacing_lo
};
let a = w_lo / 1000.0 * tfs;
let b = w_hi / 1000.0 * tfs;
let adv_lo = a.min(b) + spacing_lo;
let adv_hi = a.max(b) + spacing_hi;
let (s_lo, s_hi) = (
(adv_lo * th).min(adv_hi * th),
(adv_lo * th).max(adv_hi * th),
);
if hit {
flush(&mut out, &mut current);
if tfs != 0.0 {
let estimate = (adv_lo + adv_hi) / 2.0;
pending += -estimate * 1000.0 / tfs;
}
dropped = true;
} else {
if pending != 0.0 {
flush(&mut out, &mut current);
out.push(real(pending));
pending = 0.0;
}
current
.get_or_insert_with(|| (Vec::new(), *format))
.0
.extend_from_slice(&bytes[start..start + len]);
}
text.pos_lo += s_lo;
text.pos_hi += s_hi;
}
flush(&mut out, &mut current);
}
other => {
flush(&mut out, &mut current);
out.push(other.clone());
}
}
}
flush(&mut out, &mut current);
if pending != 0.0 {
out.push(real(pending));
}
dropped.then_some(out)
}
fn do_xobject(
&mut self,
doc: &mut Document,
container: &mut Container,
xname: &[u8],
gs: &GState,
) -> Result<DoOutcome> {
let Some(Object::Reference(id)) = container.xobjects.get(xname).ok().cloned() else {
if let Ok(Object::Stream(_)) = container.xobjects.get(xname) {
return Err(self.refuse(format!(
"XObject /{} is a direct stream",
String::from_utf8_lossy(xname)
)));
}
return Ok(DoOutcome::Keep);
};
let (subtype, dict) = match doc.get_object(id) {
Ok(Object::Stream(s)) => (
s.dict
.get(b"Subtype")
.ok()
.and_then(|o| match o {
Object::Name(n) => Some(n.clone()),
_ => None,
})
.unwrap_or_default(),
s.dict.clone(),
),
_ => return Ok(DoOutcome::Keep),
};
match subtype.as_slice() {
b"Image" => {
let unit = quad(&gs.ctm, 0.0, 0.0, 1.0, 1.0);
if !self.areas.touches(&unit) {
return Ok(DoOutcome::Keep);
}
crate::redact::check_image_filter(&dict)?;
if let Some(page) = container.owner_page {
if uniquely_owned_by(doc, id, page) {
if let Ok(Object::Stream(s)) = doc.get_object_mut(id) {
*s = flate_stream(black_image_dict(), &[0u8]);
}
return Ok(DoOutcome::InPlace);
}
}
let new_name = match container.black_images.get(&id) {
Some(n) => n.clone(),
None => {
let black = doc
.add_object(Object::Stream(flate_stream(black_image_dict(), &[0u8])));
let n = container.add_xobject(xname, black);
container.black_images.insert(id, n.clone());
n
}
};
container.redirected.insert(xname.to_vec());
self.orphans.push(id);
Ok(DoOutcome::Replace(new_name, 1))
}
b"Form" => {
let matrix = match dict.get(b"Matrix").ok().map(|o| resolve(doc, o)) {
Some(Object::Array(a)) if a.len() == 6 => {
let v: Vec<f64> = a.iter().filter_map(|o| number(doc, o)).collect();
if v.len() != 6 {
return Err(
self.refuse("a Form XObject /Matrix that is not six numbers")
);
}
[v[0], v[1], v[2], v[3], v[4], v[5]]
}
None => IDENTITY,
Some(_) => {
return Err(self.refuse("a Form XObject /Matrix that is not six numbers"))
}
};
let form_ctm = mul(matrix, gs.ctm);
if let Some(b) = form_bbox(doc, &dict) {
let corners = quad(&form_ctm, b[0], b[1], b[2], b[3]);
if !self.areas.touches(&corners) {
return Ok(DoOutcome::Keep);
}
}
match self.filter_form(doc, id, &dict, form_ctm, gs, &container.resources)? {
None => Ok(DoOutcome::Keep),
Some((clone_id, n)) => {
let new_name = container.add_xobject(xname, clone_id);
container.redirected.insert(xname.to_vec());
self.orphans.push(id);
Ok(DoOutcome::Replace(new_name, n))
}
}
}
_ => Ok(DoOutcome::Keep),
}
}
fn filter_form(
&mut self,
doc: &mut Document,
id: ObjectId,
dict: &Dictionary,
form_ctm: Matrix,
gs: &GState,
fallback: &Dictionary,
) -> Result<Option<(ObjectId, usize)>> {
if self.form_stack.contains(&id) {
return Err(self.refuse(format!("Form XObject {} {} R draws itself", id.0, id.1)));
}
if self.form_stack.len() >= MAX_FORM_DEPTH {
return Err(self.refuse("Form XObjects nested deeper than 32 levels"));
}
let content = match doc.get_object(id) {
Ok(Object::Stream(s)) => content_bytes(doc, s).map_err(|e| {
self.refuse(format!(
"Form XObject {} {} R cannot be decoded: {e}",
id.0, id.1
))
})?,
_ => return Ok(None),
};
let ops = ContentEditor::from_stream(&content)
.map_err(|e| {
self.refuse(format!(
"Form XObject {} {} R cannot be parsed: {e}",
id.0, id.1
))
})?
.operations()
.to_vec();
let resources = dict
.get(b"Resources")
.ok()
.and_then(|o| dict_of(doc, o))
.cloned()
.unwrap_or_else(|| fallback.clone());
let mut child = Container::new(doc, resources, None);
let mut child_gs = gs.clone();
child_gs.ctm = form_ctm;
child_gs.clip = intersect(
gs.clip,
form_bbox(doc, dict).map(|b| bounds(&quad(&form_ctm, b[0], b[1], b[2], b[3]))),
);
self.form_stack.push(id);
let inner = format!("{}, Form XObject {} {} R", self.location, id.0, id.1);
let saved = std::mem::replace(&mut self.location, inner);
let result = self.filter(doc, &ops, &mut child, child_gs);
self.location = saved;
self.form_stack.pop();
let result = result?;
if !result.changed {
return Ok(None);
}
let encoded = ContentEditor::from_operations(result.ops.clone())
.encode()
.map_err(|e| self.refuse(format!("re-encoding a Form XObject: {e}")))?;
let mut new_dict = dict.clone();
let (res, dropped) = child.finish(doc, &result.ops);
if let Some(res) = res {
new_dict.set("Resources", Object::Dictionary(res));
}
self.orphans.extend(dropped);
let clone_id = doc.add_object(Object::Stream(flate_stream(new_dict, &encoded)));
Ok(Some((clone_id, result.removed.max(1))))
}
fn strip_text_properties(
&self,
doc: &Document,
container: &Container,
bdc: &mut Operation,
) -> bool {
let Some(props) = bdc.operands.get(1) else {
return false;
};
let mut dict = match props {
Object::Dictionary(d) => d.clone(),
Object::Name(n) => match container
.sub_dict(doc, b"Properties")
.and_then(|p| p.get(n).ok())
.and_then(|o| dict_of(doc, o))
{
Some(d) => d.clone(),
None => return false,
},
_ => return false,
};
if !TEXT_PROPERTY_KEYS.iter().any(|k| dict.has(k)) {
return false;
}
for k in TEXT_PROPERTY_KEYS {
dict.remove(k);
}
bdc.operands[1] = Object::Dictionary(dict);
true
}
}
fn form_bbox(doc: &Document, dict: &Dictionary) -> Option<[f64; 4]> {
match dict.get(b"BBox").ok().map(|o| resolve(doc, o)) {
Some(Object::Array(a)) if a.len() == 4 => {
let v: Vec<f64> = a.iter().filter_map(|o| number(doc, o)).collect();
(v.len() == 4).then(|| {
[
v[0].min(v[2]),
v[1].min(v[3]),
v[0].max(v[2]),
v[1].max(v[3]),
]
})
}
_ => None,
}
}
enum DoOutcome {
Keep,
InPlace,
Replace(Vec<u8>, usize),
}
fn units(coding: Coding, bytes: &[u8]) -> Vec<(usize, usize, u32)> {
match coding {
Coding::OneByte => bytes
.iter()
.enumerate()
.map(|(i, &b)| (i, 1, b as u32))
.collect(),
Coding::TwoByte => {
let mut v = Vec::with_capacity(bytes.len() / 2 + 1);
let mut i = 0;
while i + 1 < bytes.len() {
v.push((i, 2, (bytes[i] as u32) << 8 | bytes[i + 1] as u32));
i += 2;
}
if i < bytes.len() {
v.push((i, 1, u32::MAX));
}
v
}
Coding::Opaque => {
if bytes.is_empty() {
Vec::new()
} else {
vec![(0, bytes.len(), 0)]
}
}
}
}
fn count_refs(obj: &Object, target: ObjectId) -> usize {
match obj {
Object::Reference(id) => (*id == target) as usize,
Object::Array(a) => a.iter().map(|o| count_refs(o, target)).sum(),
Object::Dictionary(d) => d.iter().map(|(_, o)| count_refs(o, target)).sum(),
Object::Stream(s) => s.dict.iter().map(|(_, o)| count_refs(o, target)).sum(),
_ => 0,
}
}
fn referrers(doc: &Document, target: ObjectId) -> Vec<Option<ObjectId>> {
let mut out = Vec::new();
for (id, obj) in &doc.objects {
for _ in 0..count_refs(obj, target) {
out.push(Some(*id));
}
}
for (_, obj) in doc.trailer.iter() {
for _ in 0..count_refs(obj, target) {
out.push(None);
}
}
out
}
fn uniquely_owned_by(doc: &Document, id: ObjectId, page: ObjectId) -> bool {
let mut current = id;
for _ in 0..4 {
let refs = referrers(doc, current);
match refs.as_slice() {
[Some(owner)] if *owner == page => return true,
[Some(owner)] => current = *owner,
_ => return false,
}
}
false
}
pub(crate) fn drop_orphans(doc: &mut Document, candidates: &[ObjectId]) {
if candidates.is_empty() {
return;
}
let mut reachable: HashSet<ObjectId> = HashSet::new();
let mut queue: Vec<ObjectId> = Vec::new();
fn collect(obj: &Object, queue: &mut Vec<ObjectId>) {
match obj {
Object::Reference(id) => queue.push(*id),
Object::Array(a) => a.iter().for_each(|o| collect(o, queue)),
Object::Dictionary(d) => d.iter().for_each(|(_, o)| collect(o, queue)),
Object::Stream(s) => s.dict.iter().for_each(|(_, o)| collect(o, queue)),
_ => {}
}
}
for (_, obj) in doc.trailer.iter() {
collect(obj, &mut queue);
}
while let Some(id) = queue.pop() {
if !reachable.insert(id) {
continue;
}
if let Some(obj) = doc.objects.get(&id) {
collect(obj, &mut queue);
}
}
for id in candidates {
if !reachable.contains(id) {
doc.objects.remove(id);
}
}
}
pub(crate) fn drop_unreachable(doc: &mut Document) {
let all: Vec<ObjectId> = doc.objects.keys().copied().collect();
drop_orphans(doc, &all);
}
const MAX_DECODED_CONTENT: usize = 256 << 20;
fn lopdf_strict(
doc: &Document,
filter: &str,
data: Vec<u8>,
params: Option<&Object>,
) -> std::result::Result<Vec<u8>, String> {
let mut dict = dictionary! { "Filter" => filter };
if let Some(Object::Dictionary(p)) = params {
let mut resolved = Dictionary::new();
for (key, value) in p.iter() {
resolved.set(key.clone(), resolve(doc, value).clone());
}
dict.set("DecodeParms", Object::Dictionary(resolved));
}
Stream::new(dict, data)
.decompressed_content_strict_with_limit(MAX_DECODED_CONTENT)
.map_err(|e| e.to_string())
}
fn ascii_hex_strict(input: &[u8]) -> std::result::Result<Vec<u8>, String> {
let end = input
.iter()
.position(|&b| b == b'>')
.ok_or("ASCIIHex data ends without its > end marker")?;
let mut out = Vec::with_capacity(end / 2 + 1);
let mut hi: Option<u8> = None;
for &ch in &input[..end] {
if ch.is_ascii_whitespace() {
continue;
}
let nibble = (ch as char)
.to_digit(16)
.ok_or_else(|| format!("ASCIIHex data holds the character 0x{ch:02x}"))?
as u8;
match hi.take() {
None => hi = Some(nibble),
Some(h) => out.push(h << 4 | nibble),
}
}
if let Some(h) = hi {
out.push(h << 4);
}
Ok(out)
}
fn run_length_strict(input: &[u8]) -> std::result::Result<Vec<u8>, String> {
let mut out = Vec::new();
let mut i = 0;
while i < input.len() {
let len = input[i] as usize;
i += 1;
match len {
128 => return Ok(out),
0..=127 => {
let run = input
.get(i..i + len + 1)
.ok_or("RunLength data ends inside a literal run")?;
out.extend_from_slice(run);
i += len + 1;
}
_ => {
let b = *input
.get(i)
.ok_or("RunLength data ends inside a repeat run")?;
out.extend(std::iter::repeat_n(b, 257 - len));
i += 1;
}
}
if out.len() > MAX_DECODED_CONTENT {
return Err(format!("decodes to more than {MAX_DECODED_CONTENT} bytes"));
}
}
Err("RunLength data ends without its end-of-data byte".into())
}
fn content_bytes(doc: &Document, s: &Stream) -> std::result::Result<Vec<u8>, String> {
let filters: Vec<Vec<u8>> = match s.dict.get(b"Filter").ok().map(|o| resolve(doc, o)) {
None => return Ok(s.content.clone()),
Some(Object::Name(n)) => vec![n.clone()],
Some(Object::Array(a)) => a
.iter()
.map(|o| match resolve(doc, o) {
Object::Name(n) => Ok(n.clone()),
_ => Err("a /Filter entry that is not a name".to_string()),
})
.collect::<std::result::Result<_, _>>()?,
Some(_) => return Err("a /Filter that is not a name or an array".into()),
};
let params_for = |i: usize| -> Option<Object> {
match s.dict.get(b"DecodeParms").ok().map(|o| resolve(doc, o)) {
Some(Object::Dictionary(d)) if i == 0 => Some(Object::Dictionary(d.clone())),
Some(Object::Array(a)) => a.get(i).map(|o| resolve(doc, o).clone()),
_ => None,
}
.filter(|p| !matches!(p, Object::Null))
};
let mut data = s.content.clone();
for (i, filter) in filters.iter().enumerate() {
let params = params_for(i);
data = match filter.as_slice() {
b"FlateDecode" | b"Fl" => lopdf_strict(doc, "FlateDecode", data, params.as_ref())?,
b"LZWDecode" | b"LZW" => lopdf_strict(doc, "LZWDecode", data, params.as_ref())?,
b"ASCII85Decode" | b"A85" => lopdf_strict(doc, "ASCII85Decode", data, None)?,
b"ASCIIHexDecode" | b"AHx" => ascii_hex_strict(&data)?,
b"RunLengthDecode" | b"RL" => run_length_strict(&data)?,
other => {
return Err(format!(
"the {} filter does not belong on content",
String::from_utf8_lossy(other)
))
}
};
}
Ok(data)
}
fn content_stream_ids(doc: &Document, page_num: u32, page_id: ObjectId) -> Result<Vec<ObjectId>> {
let page = doc.get_dictionary(page_id)?;
let contents = match page.get(b"Contents") {
Ok(c) => c,
Err(_) => return Ok(Vec::new()),
};
let items: Vec<Object> = match contents {
Object::Reference(id) => match doc.get_object(*id) {
Ok(Object::Array(a)) => a.clone(),
Ok(Object::Stream(_)) => vec![Object::Reference(*id)],
_ => {
return Err(refuse(format!(
"page {page_num}: /Contents {} {} R is not a content stream",
id.0, id.1
)))
}
},
Object::Array(a) => a.clone(),
_ => {
return Err(refuse(format!(
"page {page_num}: /Contents is not a stream"
)))
}
};
items
.iter()
.map(|item| match item {
Object::Reference(id) => match doc.get_object(*id) {
Ok(Object::Stream(_)) => Ok(*id),
_ => Err(refuse(format!(
"page {page_num}: content stream {} {} R is missing",
id.0, id.1
))),
},
_ => Err(refuse(format!(
"page {page_num}: a /Contents entry is not a reference"
))),
})
.collect()
}
pub(crate) fn page_content_strict(
doc: &Document,
page_num: u32,
page_id: ObjectId,
) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
for id in content_stream_ids(doc, page_num, page_id)? {
if let Ok(Object::Stream(s)) = doc.get_object(id) {
let plain = content_bytes(doc, s).map_err(|e| {
refuse(format!(
"page {page_num}: content stream {} {} R cannot be decoded: {e}",
id.0, id.1
))
})?;
if !bytes.is_empty() {
bytes.push(b'\n');
}
bytes.extend_from_slice(&plain);
}
}
Ok(bytes)
}
pub(crate) fn check_page_decodes(
doc: &Document,
page_num: u32,
page_id: ObjectId,
resources: &Dictionary,
) -> Result<()> {
for id in content_stream_ids(doc, page_num, page_id)? {
if let Ok(Object::Stream(s)) = doc.get_object(id) {
content_bytes(doc, s).map_err(|e| {
refuse(format!(
"page {page_num}: content stream {} {} R cannot be decoded: {e}",
id.0, id.1
))
})?;
}
}
let mut seen: HashSet<ObjectId> = HashSet::new();
let mut queue: Vec<(Dictionary, usize)> = vec![(resources.clone(), 0)];
while let Some((res, depth)) = queue.pop() {
let Some(xobjects) = get(doc, &res, b"XObject").and_then(|o| dict_of(doc, o)) else {
continue;
};
for (_, entry) in xobjects.iter() {
let Object::Reference(id) = entry else {
continue;
};
if !seen.insert(*id) {
continue;
}
let Ok(Object::Stream(s)) = doc.get_object(*id) else {
continue;
};
if !matches!(s.dict.get(b"Subtype"), Ok(Object::Name(n)) if n == b"Form") {
continue;
}
content_bytes(doc, s).map_err(|e| {
refuse(format!(
"page {page_num}: Form XObject {} {} R cannot be decoded: {e}",
id.0, id.1
))
})?;
if depth < MAX_FORM_DEPTH {
if let Some(inner) = get(doc, &s.dict, b"Resources").and_then(|o| dict_of(doc, o)) {
queue.push((inner.clone(), depth + 1));
}
}
}
}
Ok(())
}
pub(crate) fn redact_page(
doc: &mut Document,
page_num: u32,
page_id: ObjectId,
resources: Dictionary,
areas: &[&RedactionArea],
) -> Result<(usize, Vec<ObjectId>)> {
redact_page_counting(doc, page_num, page_id, resources, areas).map(|(n, o, _)| (n, o))
}
pub(crate) fn text_left_under(
doc: &Document,
page_num: u32,
page_id: ObjectId,
resources: Dictionary,
areas: &[&RedactionArea],
) -> Result<usize> {
let mut probe = doc.clone();
redact_page_counting(&mut probe, page_num, page_id, resources, areas).map(|(_, _, hits)| hits)
}
fn redact_page_counting(
doc: &mut Document,
page_num: u32,
page_id: ObjectId,
resources: Dictionary,
areas: &[&RedactionArea],
) -> Result<(usize, Vec<ObjectId>, usize)> {
let areas = Areas::new(areas);
let ids = content_stream_ids(doc, page_num, page_id)?;
if ids.is_empty() {
return Ok((0, Vec::new(), 0));
}
let mut bytes = Vec::new();
for &id in &ids {
let Ok(Object::Stream(s)) = doc.get_object(id) else {
unreachable!("content_stream_ids checked every entry");
};
let plain = content_bytes(doc, s).map_err(|e| {
refuse(format!(
"page {page_num}: content stream {} {} R cannot be decoded: {e}",
id.0, id.1
))
})?;
if !bytes.is_empty() {
bytes.push(b'\n');
}
bytes.extend_from_slice(&plain);
}
let editor = ContentEditor::from_stream(&bytes)
.map_err(|e| refuse(format!("page {page_num}: content cannot be parsed: {e}")))?;
let mut container = Container::new(doc, resources, Some(page_id));
let mut filter = Filter {
areas: &areas,
orphans: Vec::new(),
form_stack: Vec::new(),
location: format!("page {page_num}"),
text_hits: 0,
};
let result = filter.filter(
doc,
editor.operations(),
&mut container,
GState::new(IDENTITY),
)?;
let mut orphans = filter.orphans;
let filter_text_hits = filter.text_hits;
if result.changed {
let encoded = ContentEditor::from_operations(result.ops.clone())
.encode()
.map_err(|e| refuse(format!("page {page_num}: re-encoding content: {e}")))?;
let new_id = doc.add_object(Object::Stream(flate_stream(Dictionary::new(), &encoded)));
if let Ok(Object::Dictionary(page)) = doc.get_object_mut(page_id) {
page.set("Contents", Object::Reference(new_id));
}
orphans.extend(ids);
}
let (res, dropped) = container.finish(doc, &result.ops);
orphans.extend(dropped);
if let Some(res) = res {
if let Ok(Object::Dictionary(page)) = doc.get_object_mut(page_id) {
page.set("Resources", Object::Dictionary(res));
}
}
Ok((result.removed, orphans, filter_text_hits))
}
const ANNOT_TEXT_KEYS: [&[u8]; 4] = [b"Contents", b"RC", b"T", b"Subj"];
const FIELD_VALUE_KEYS: [&[u8]; 3] = [b"V", b"DV", b"RV"];
fn appearance_matrix(doc: &Document, dict: &Dictionary, rect: [f64; 4]) -> Result<Option<Matrix>> {
let matrix = match dict.get(b"Matrix").ok().map(|o| resolve(doc, o)) {
None => IDENTITY,
Some(Object::Array(a)) if a.len() == 6 => {
let v: Vec<f64> = a.iter().filter_map(|o| number(doc, o)).collect();
if v.len() != 6 {
return Err(refuse("an appearance /Matrix that is not six numbers"));
}
[v[0], v[1], v[2], v[3], v[4], v[5]]
}
Some(_) => return Err(refuse("an appearance /Matrix that is not six numbers")),
};
let Some(b) = form_bbox(doc, dict) else {
return Ok(None);
};
let t = bounds(&quad(&matrix, b[0], b[1], b[2], b[3]));
let (w, h) = (t[2] - t[0], t[3] - t[1]);
if w <= 0.0 || h <= 0.0 || !w.is_finite() || !h.is_finite() {
return Ok(None);
}
let sx = (rect[2] - rect[0]) / w;
let sy = (rect[3] - rect[1]) / h;
let fit = [sx, 0.0, 0.0, sy, rect[0] - sx * t[0], rect[1] - sy * t[1]];
Ok(Some(mul(matrix, fit)))
}
fn annot_rect(doc: &Document, dict: &Dictionary) -> Option<[f64; 4]> {
match get(doc, dict, b"Rect") {
Some(Object::Array(a)) if a.len() == 4 => {
let v: Vec<f64> = a.iter().filter_map(|o| number(doc, o)).collect();
(v.len() == 4).then(|| {
[
v[0].min(v[2]),
v[1].min(v[3]),
v[0].max(v[2]),
v[1].max(v[3]),
]
})
}
_ => None,
}
}
fn page_annot_ids(doc: &Document, page_id: ObjectId) -> Vec<ObjectId> {
let Ok(page) = doc.get_dictionary(page_id) else {
return Vec::new();
};
match get(doc, page, b"Annots") {
Some(Object::Array(a)) => a.iter().filter_map(|o| o.as_reference().ok()).collect(),
_ => Vec::new(),
}
}
pub(crate) fn redact_annotations(
doc: &mut Document,
page_num: u32,
page_id: ObjectId,
areas: &[&RedactionArea],
) -> Result<Vec<ObjectId>> {
let areas = Areas::new(areas);
let mut orphans = Vec::new();
let mut off_page: HashSet<ObjectId> = HashSet::new();
for annot_id in page_annot_ids(doc, page_id) {
let Ok(dict) = doc.get_dictionary(annot_id).cloned() else {
continue;
};
let Some(rect) = annot_rect(doc, &dict) else {
continue;
};
if !areas.0.iter().any(|r| bounds_touch(&rect, r)) {
continue;
}
let is_widget =
matches!(get(doc, &dict, b"Subtype"), Some(Object::Name(n)) if n == b"Widget");
let mut filter = Filter {
areas: &areas,
orphans: Vec::new(),
form_stack: Vec::new(),
location: format!(
"page {page_num}, annotation {} {} R",
annot_id.0, annot_id.1
),
text_hits: 0,
};
let mut new_ap = match get(doc, &dict, b"AP") {
Some(Object::Dictionary(ap)) => Some(ap.clone()),
_ => None,
};
if let Some(ap) = new_ap.as_mut() {
for key in [&b"N"[..], b"R", b"D"] {
let Ok(entry) = ap.get(key).cloned() else {
continue;
};
let replaced = match resolve(doc, &entry).clone() {
Object::Stream(_) => match entry {
Object::Reference(id) => {
redact_appearance(doc, &mut filter, id, rect)?.map(Object::Reference)
}
_ => None,
},
Object::Dictionary(states) => {
let mut states = states.clone();
let mut any = false;
let names: Vec<Vec<u8>> = states.iter().map(|(k, _)| k.clone()).collect();
for name in names {
if let Ok(Object::Reference(id)) = states.get(&name).cloned() {
if let Some(new_id) = redact_appearance(doc, &mut filter, id, rect)?
{
states.set(name, Object::Reference(new_id));
any = true;
}
}
}
any.then_some(Object::Dictionary(states))
}
_ => None,
};
if let Some(obj) = replaced {
ap.set(key.to_vec(), obj);
}
}
}
orphans.extend(filter.orphans);
if let Ok(Object::Dictionary(d)) = doc.get_object_mut(annot_id) {
if let Some(ap) = new_ap {
d.set("AP", Object::Dictionary(ap));
}
for key in ANNOT_TEXT_KEYS {
d.remove(key);
}
if is_widget {
for key in FIELD_VALUE_KEYS {
d.remove(key);
}
}
}
if is_widget {
let mut parent = dict.get(b"Parent").ok().and_then(|o| o.as_reference().ok());
for _ in 0..MAX_FORM_DEPTH {
let Some(pid) = parent else { break };
let next = doc
.get_dictionary(pid)
.ok()
.and_then(|p| p.get(b"Parent").ok())
.and_then(|o| o.as_reference().ok());
if let Ok(Object::Dictionary(p)) = doc.get_object_mut(pid) {
for key in FIELD_VALUE_KEYS {
p.remove(key);
}
}
parent = next;
}
}
let inside = areas
.0
.iter()
.any(|r| r[0] <= rect[0] && r[1] <= rect[1] && r[2] >= rect[2] && r[3] >= rect[3]);
if inside && !is_widget {
off_page.insert(annot_id);
orphans.push(annot_id);
if let Some(popup) = dict.get(b"Popup").ok().and_then(|o| o.as_reference().ok()) {
off_page.insert(popup);
orphans.push(popup);
}
}
}
if !off_page.is_empty() {
let kept: Vec<Object> = page_annot_ids(doc, page_id)
.into_iter()
.filter(|id| !off_page.contains(id))
.map(Object::Reference)
.collect();
if let Ok(Object::Dictionary(page)) = doc.get_object_mut(page_id) {
page.set("Annots", Object::Array(kept));
}
}
Ok(orphans)
}
fn redact_appearance(
doc: &mut Document,
filter: &mut Filter<'_>,
id: ObjectId,
rect: [f64; 4],
) -> Result<Option<ObjectId>> {
let Ok(Object::Stream(s)) = doc.get_object(id) else {
return Ok(None);
};
let dict = s.dict.clone();
let Some(m) = appearance_matrix(doc, &dict, rect)? else {
let empty = doc.add_object(Object::Stream(flate_stream(dict, b"")));
filter.orphans.push(id);
return Ok(Some(empty));
};
match filter.filter_form(
doc,
id,
&dict,
m,
&GState::new(IDENTITY),
&Dictionary::new(),
)? {
None => Ok(None),
Some((new_id, _)) => {
filter.orphans.push(id);
Ok(Some(new_id))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mul_applies_the_first_matrix_first() {
let m = mul([2.0, 0.0, 0.0, 2.0, 0.0, 0.0], translate(10.0, 0.0));
assert_eq!(apply(&m, 1.0, 1.0), (12.0, 2.0));
let m = mul(translate(10.0, 0.0), [2.0, 0.0, 0.0, 2.0, 0.0, 0.0]);
assert_eq!(apply(&m, 1.0, 1.0), (22.0, 2.0));
}
#[test]
fn clipped_area_of_a_half_covered_square() {
let square = [(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0)];
assert!((clipped_area(&square, &[1.0, -1.0, 5.0, 5.0]) - 2.0).abs() < 1e-9);
assert_eq!(clipped_area(&square, &[3.0, 3.0, 5.0, 5.0]), 0.0);
}
#[test]
fn a_sliver_of_a_known_glyph_does_not_take_it_but_an_uncertain_one_does() {
let areas = Areas(vec![[9.5, 0.0, 20.0, 10.0]]);
let glyph = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
assert!(!areas.takes_glyph(&glyph, true));
assert!(areas.takes_glyph(&glyph, false));
}
}