use crate::content_editor::{
as_number, editor_for_page, multiply_matrix, write_editor_to_page, ContentEditor,
};
use crate::error::{ManipError, Result};
use crate::image_insert::create_image_xobject;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use lopdf::content::Operation;
use lopdf::{Dictionary, Document, Object, ObjectId, Stream};
use std::io::Write;
type Matrix = [f64; 6];
const IDENTITY: Matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
const MAX_FORM_DEPTH: usize = 8;
const MAX_FORM_INVOCATIONS: usize = 4096;
const MIN_EXTENT: f64 = 0.01;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PlacementKind {
XObject,
Inline,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct PlacementRef {
pub form_path: Vec<usize>,
pub op_index: usize,
pub token: Option<u64>,
}
impl PlacementRef {
pub fn new(form_path: Vec<usize>, op_index: usize) -> Self {
Self {
form_path,
op_index,
token: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImagePlacement {
pub at: PlacementRef,
pub form_names: Vec<String>,
pub kind: PlacementKind,
pub resource_name: Option<String>,
pub xobject: Option<ObjectId>,
pub ctm: [f64; 6],
pub bbox: [f64; 4],
pub pixel_width: u32,
pub pixel_height: u32,
pub has_smask: bool,
}
struct Fnv(u64);
impl Fnv {
fn new(seed: u64) -> Self {
let mut h = Fnv(0xcbf2_9ce4_8422_2325);
h.bytes(&seed.to_le_bytes());
h
}
fn bytes(&mut self, b: &[u8]) {
for x in b {
self.0 ^= u64::from(*x);
self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
}
}
}
impl std::fmt::Write for Fnv {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.bytes(s.as_bytes());
Ok(())
}
}
fn hash_ops(lineage: u64, ops: &[Operation]) -> u64 {
use std::fmt::Write as _;
let mut h = Fnv::new(lineage);
for op in ops {
let _ = write!(h, "{}{:?};", op.operator, op.operands);
}
h.0
}
fn mix(base: u64, index: usize, id: Option<ObjectId>) -> u64 {
let mut h = Fnv::new(base);
h.bytes(&(index as u64).to_le_bytes());
if let Some((n, g)) = id {
h.bytes(&n.to_le_bytes());
h.bytes(&g.to_le_bytes());
}
h.0
}
struct Found {
placement: ImagePlacement,
}
fn stale() -> ManipError {
ManipError::Other(
"the image placement is no longer at that position; list the page again".into(),
)
}
fn degenerate() -> ManipError {
ManipError::Image("the image placement has no area on the page".into())
}
fn invert(m: &Matrix) -> Option<Matrix> {
let det = m[0] * m[3] - m[1] * m[2];
if !det.is_finite() || det.abs() < 1e-12 {
return None;
}
Some([
m[3] / det,
-m[1] / det,
-m[2] / det,
m[0] / det,
(m[2] * m[5] - m[3] * m[4]) / det,
(m[1] * m[4] - m[0] * m[5]) / det,
])
}
fn bbox_of(ctm: &Matrix) -> [f64; 4] {
let mut r = [f64::MAX, f64::MAX, f64::MIN, f64::MIN];
for (x, y) in [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)] {
let px = x * ctm[0] + y * ctm[2] + ctm[4];
let py = x * ctm[1] + y * ctm[3] + ctm[5];
r = [r[0].min(px), r[1].min(py), r[2].max(px), r[3].max(py)];
}
r
}
fn matrix_of(operands: &[Object]) -> Option<Matrix> {
if operands.len() < 6 {
return None;
}
let mut m = IDENTITY;
for (slot, o) in m.iter_mut().zip(operands) {
*slot = as_number(o)?;
}
Some(m)
}
fn cm_op(m: &Matrix) -> Operation {
Operation::new("cm", m.iter().map(|v| Object::Real(*v as f32)).collect())
}
fn dict_of<'a>(doc: &'a Document, o: &'a Object) -> Option<&'a Dictionary> {
match o {
Object::Dictionary(d) => Some(d),
Object::Reference(id) => doc.get_object(*id).ok()?.as_dict().ok(),
_ => None,
}
}
pub(crate) fn inherited_resources(doc: &Document, page: ObjectId) -> Dictionary {
let mut node = page;
for _ in 0..64 {
let Ok(d) = doc.get_object(node).and_then(Object::as_dict) else {
break;
};
if let Some(r) = d.get(b"Resources").ok().and_then(|o| dict_of(doc, o)) {
return r.clone();
}
match d.get(b"Parent").and_then(Object::as_reference) {
Ok(p) => node = p,
Err(_) => break,
}
}
Dictionary::new()
}
fn page_resources(doc: &Document, page: ObjectId) -> Dictionary {
direct_xobjects(doc, inherited_resources(doc, page))
}
fn direct_xobjects(doc: &Document, mut res: Dictionary) -> Dictionary {
let x = res
.get(b"XObject")
.ok()
.and_then(|o| dict_of(doc, o))
.cloned()
.unwrap_or_default();
res.set("XObject", Object::Dictionary(x));
res
}
fn xobject_id(res: &Dictionary, name: &[u8]) -> Option<ObjectId> {
res.get(b"XObject")
.ok()?
.as_dict()
.ok()?
.get(name)
.ok()?
.as_reference()
.ok()
}
fn subtype(s: &Stream) -> Option<&[u8]> {
s.dict.get(b"Subtype").ok()?.as_name().ok()
}
fn dim(dict: &Dictionary, keys: [&[u8]; 2]) -> u32 {
keys.iter()
.find_map(|k| dict.get(k).ok().and_then(|o| o.as_i64().ok()))
.map_or(0, |v| v.clamp(0, i64::from(u32::MAX)) as u32)
}
fn matrix_from_object(doc: &Document, o: &Object) -> Option<Matrix> {
let items = match o {
Object::Array(a) => a,
Object::Reference(id) => doc.get_object(*id).ok()?.as_array().ok()?,
_ => return None,
};
matrix_of(items)
}
struct Walk<'a> {
doc: &'a Document,
out: Vec<Found>,
budget: usize,
lineage: u64,
}
impl<'a> Walk<'a> {
fn stream(
&mut self,
ops: &[Operation],
res: &Dictionary,
start: Matrix,
steps: &mut Vec<usize>,
names: &mut Vec<String>,
active: &mut Vec<ObjectId>,
) {
let doc = self.doc;
let base = hash_ops(self.lineage, ops);
let mut ctm = start;
let mut stack: Vec<Matrix> = Vec::new();
for (i, op) in ops.iter().enumerate() {
match op.operator.as_str() {
"q" => stack.push(ctm),
"Q" => {
if let Some(saved) = stack.pop() {
ctm = saved;
}
}
"cm" => {
if let Some(m) = matrix_of(&op.operands) {
ctm = multiply_matrix(&m, &ctm);
}
}
"BI" => {
let Some(Object::Stream(s)) = op.operands.first() else {
continue;
};
self.out.push(Found {
placement: ImagePlacement {
at: PlacementRef {
form_path: steps.clone(),
op_index: i,
token: Some(mix(base, i, None)),
},
form_names: names.clone(),
kind: PlacementKind::Inline,
resource_name: None,
xobject: None,
ctm,
bbox: bbox_of(&ctm),
pixel_width: dim(&s.dict, [b"W", b"Width"]),
pixel_height: dim(&s.dict, [b"H", b"Height"]),
has_smask: false,
},
});
}
"Do" => {
let Some(Ok(name)) = op.operands.first().map(Object::as_name) else {
continue;
};
let Some(id) = xobject_id(res, name) else {
continue;
};
let Ok(Object::Stream(s)) = doc.get_object(id) else {
continue;
};
let label = String::from_utf8_lossy(name).into_owned();
match subtype(s) {
Some(b"Image") => self.out.push(Found {
placement: ImagePlacement {
at: PlacementRef {
form_path: steps.clone(),
op_index: i,
token: Some(mix(base, i, Some(id))),
},
form_names: names.clone(),
kind: PlacementKind::XObject,
resource_name: Some(label),
xobject: Some(id),
ctm,
bbox: bbox_of(&ctm),
pixel_width: dim(&s.dict, [b"Width", b"W"]),
pixel_height: dim(&s.dict, [b"Height", b"H"]),
has_smask: s.dict.has(b"SMask"),
},
}),
Some(b"Form") => {
if self.budget == 0
|| active.len() >= MAX_FORM_DEPTH
|| active.contains(&id)
{
continue;
}
self.budget -= 1;
let Ok(data) = s.decompressed_content() else {
continue;
};
let Ok(inner) = ContentEditor::from_stream(&data) else {
continue;
};
let form_res = s
.dict
.get(b"Resources")
.ok()
.and_then(|o| dict_of(doc, o))
.map(|d| direct_xobjects(doc, d.clone()))
.unwrap_or_else(|| res.clone());
let fm = s
.dict
.get(b"Matrix")
.ok()
.and_then(|o| matrix_from_object(doc, o))
.unwrap_or(IDENTITY);
steps.push(i);
names.push(label);
active.push(id);
let outer =
std::mem::replace(&mut self.lineage, mix(base, i, Some(id)));
self.stream(
inner.operations(),
&form_res,
multiply_matrix(&fm, &ctm),
steps,
names,
active,
);
self.lineage = outer;
active.pop();
names.pop();
steps.pop();
}
_ => {}
}
}
_ => {}
}
}
}
}
fn collect(doc: &Document, page_num: u32) -> Result<(ObjectId, Vec<Found>)> {
let pages = doc.get_pages();
let page = *pages
.get(&page_num)
.ok_or(ManipError::PageOutOfRange(page_num as usize, pages.len()))?;
let editor = editor_for_page(doc, page_num)?;
let res = page_resources(doc, page);
let mut walk = Walk {
doc,
out: Vec::new(),
budget: MAX_FORM_INVOCATIONS,
lineage: 0,
};
walk.stream(
editor.operations(),
&res,
IDENTITY,
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
);
Ok((page, walk.out))
}
pub fn list_image_placements(doc: &Document, page_num: u32) -> Result<Vec<ImagePlacement>> {
Ok(collect(doc, page_num)?
.1
.into_iter()
.map(|f| f.placement)
.collect())
}
fn locate(doc: &Document, page_num: u32, at: &PlacementRef) -> Result<(ObjectId, Found)> {
let (page, found) = collect(doc, page_num)?;
let f = found
.into_iter()
.find(|f| {
let h = &f.placement.at;
h.form_path == at.form_path
&& h.op_index == at.op_index
&& at.token.is_none_or(|t| h.token == Some(t))
})
.ok_or_else(stale)?;
Ok((page, f))
}
fn handle_after(
doc: &Document,
page_num: u32,
form_path: &[usize],
op_index: usize,
) -> PlacementRef {
collect(doc, page_num)
.ok()
.and_then(|(_, found)| {
found
.into_iter()
.map(|f| f.placement.at)
.find(|a| a.form_path == form_path && a.op_index == op_index)
})
.unwrap_or(PlacementRef {
form_path: form_path.to_vec(),
op_index,
token: None,
})
}
#[derive(Clone, Copy)]
enum Container {
Page(ObjectId),
Form(ObjectId),
}
fn resources_of(doc: &Document, c: Container) -> Dictionary {
match c {
Container::Page(p) => page_resources(doc, p),
Container::Form(f) => {
let own = doc
.get_object(f)
.ok()
.and_then(|o| o.as_stream().ok())
.and_then(|s| s.dict.get(b"Resources").ok())
.and_then(|o| dict_of(doc, o))
.cloned()
.unwrap_or_default();
direct_xobjects(doc, own)
}
}
}
fn set_resources(doc: &mut Document, c: Container, res: Dictionary) {
match c {
Container::Page(p) => {
if let Some(Object::Dictionary(d)) = doc.objects.get_mut(&p) {
d.set("Resources", Object::Dictionary(res));
}
}
Container::Form(f) => {
if let Some(Object::Stream(s)) = doc.objects.get_mut(&f) {
s.dict.set("Resources", Object::Dictionary(res));
}
}
}
}
fn set_xobject(res: &mut Dictionary, name: &[u8], id: ObjectId) {
if let Ok(Object::Dictionary(x)) = res.get_mut(b"XObject") {
x.set(name.to_vec(), Object::Reference(id));
}
}
fn unique_name(res: &Dictionary, prefix: &str) -> String {
(1u32..)
.map(|n| format!("{prefix}{n}"))
.find(|n| xobject_id(res, n.as_bytes()).is_none())
.expect("u32 range is not exhausted")
}
fn compress(data: &[u8]) -> Option<Vec<u8>> {
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
e.write_all(data).ok()?;
e.finish().ok()
}
fn load_ops(doc: &Document, page_num: u32, c: Container) -> Result<ContentEditor> {
match c {
Container::Page(_) => editor_for_page(doc, page_num),
Container::Form(f) => {
let s = doc.get_object(f)?.as_stream()?;
ContentEditor::from_stream(&s.decompressed_content()?)
}
}
}
fn save_ops(doc: &mut Document, page_num: u32, c: Container, editor: &ContentEditor) -> Result<()> {
match c {
Container::Page(_) => write_editor_to_page(doc, page_num, editor),
Container::Form(f) => {
let raw = editor.encode()?;
let packed = compress(&raw).filter(|p| p.len() < raw.len());
let Some(Object::Stream(s)) = doc.objects.get_mut(&f) else {
return Err(stale());
};
s.dict.remove(b"DecodeParms");
match packed {
Some(p) => {
s.dict.set("Filter", Object::Name(b"FlateDecode".to_vec()));
s.set_content(p);
}
None => {
s.dict.remove(b"Filter");
s.set_content(raw);
}
}
Ok(())
}
}
}
fn count_in(o: &Object, id: ObjectId) -> usize {
match o {
Object::Reference(r) => usize::from(*r == id),
Object::Array(a) => a.iter().map(|x| count_in(x, id)).sum(),
Object::Dictionary(d) => d.iter().map(|(_, v)| count_in(v, id)).sum(),
Object::Stream(s) => s.dict.iter().map(|(_, v)| count_in(v, id)).sum(),
_ => 0,
}
}
fn reference_count(doc: &Document, id: ObjectId) -> usize {
let in_objects: usize = doc
.objects
.iter()
.filter(|(k, _)| **k != id)
.map(|(_, o)| count_in(o, id))
.sum();
let in_trailer: usize = doc.trailer.iter().map(|(_, v)| count_in(v, id)).sum();
in_objects + in_trailer
}
fn drop_if_orphan(doc: &mut Document, id: ObjectId) {
if reference_count(doc, id) != 0 {
return;
}
if let Some(Object::Stream(s)) = doc.objects.remove(&id) {
for key in [b"SMask".as_slice(), b"Mask".as_slice()] {
if let Ok(Object::Reference(m)) = s.dict.get(key) {
drop_if_orphan(doc, *m);
}
}
}
}
fn uses_name(ops: &[Operation], name: &[u8]) -> bool {
ops.iter().any(|o| {
o.operator == "Do" && o.operands.first().and_then(|n| n.as_name().ok()) == Some(name)
})
}
fn inherited_use(
doc: &Document,
res: &Dictionary,
ops: &[Operation],
name: &[u8],
seen: &mut Vec<ObjectId>,
) -> bool {
for o in ops.iter().filter(|o| o.operator == "Do") {
let Some(form) = o
.operands
.first()
.and_then(|n| n.as_name().ok())
.and_then(|n| xobject_id(res, n))
else {
continue;
};
let Ok(Object::Stream(s)) = doc.get_object(form) else {
continue;
};
if subtype(s) != Some(b"Form")
|| seen.contains(&form)
|| s.dict
.get(b"Resources")
.ok()
.and_then(|r| dict_of(doc, r))
.is_some()
{
continue;
}
let Ok(inner) = s
.decompressed_content()
.map_err(|_| ())
.and_then(|d| ContentEditor::from_stream(&d).map_err(|_| ()))
else {
continue;
};
if seen.len() >= MAX_FORM_DEPTH {
return true;
}
seen.push(form);
let hit = uses_name(inner.operations(), name)
|| inherited_use(doc, res, inner.operations(), name, seen);
seen.pop();
if hit {
return true;
}
}
false
}
fn release(doc: &mut Document, c: Container, name: &[u8], id: ObjectId, ops: &[Operation]) {
if uses_name(ops, name) {
return;
}
let mut res = resources_of(doc, c);
if inherited_use(doc, &res, ops, name, &mut Vec::new()) {
return;
}
if let Ok(Object::Dictionary(x)) = res.get_mut(b"XObject") {
x.remove(name);
}
set_resources(doc, c, res);
drop_if_orphan(doc, id);
}
fn private_container(
doc: &mut Document,
page_num: u32,
page: ObjectId,
steps: &[usize],
) -> Result<Container> {
let mut container = Container::Page(page);
for &do_index in steps {
let mut editor = load_ops(doc, page_num, container)?;
let name = editor
.operations()
.get(do_index)
.filter(|o| o.operator == "Do")
.and_then(|o| o.operands.first())
.and_then(|n| n.as_name().ok())
.map(<[u8]>::to_vec)
.ok_or_else(stale)?;
let mut res = resources_of(doc, container);
let old = xobject_id(&res, &name).ok_or_else(stale)?;
let mut copy = doc.get_object(old)?.as_stream()?.clone();
let form_res = copy
.dict
.get(b"Resources")
.ok()
.and_then(|o| dict_of(doc, o))
.map(|d| direct_xobjects(doc, d.clone()))
.unwrap_or_else(|| res.clone());
copy.dict.set("Resources", Object::Dictionary(form_res));
let new_id = doc.add_object(Object::Stream(copy));
let new_name = unique_name(&res, "PdfluentFm");
set_xobject(&mut res, new_name.as_bytes(), new_id);
set_resources(doc, container, res);
editor.operations_mut()[do_index].operands = vec![Object::Name(new_name.into_bytes())];
save_ops(doc, page_num, container, &editor)?;
release(doc, container, &name, old, editor.operations());
container = Container::Form(new_id);
}
Ok(container)
}
fn apply_unit_transform(ops: &mut Vec<Operation>, index: usize, t: &Matrix) -> Result<usize> {
let canonical = index >= 2
&& index + 1 < ops.len()
&& ops[index - 2].operator == "q"
&& ops[index - 1].operator == "cm"
&& matrix_of(&ops[index - 1].operands).is_some()
&& ops[index + 1].operator == "Q";
if canonical {
let a = matrix_of(&ops[index - 1].operands).ok_or_else(stale)?;
ops[index - 1] = cm_op(&multiply_matrix(t, &a));
Ok(index)
} else {
let placed = ops.get(index).cloned().ok_or_else(stale)?;
ops.splice(
index..=index,
[
Operation::new("q", vec![]),
cm_op(t),
placed,
Operation::new("Q", vec![]),
],
);
Ok(index + 2)
}
}
pub fn set_placement_rect(
doc: &mut Document,
page_num: u32,
at: &PlacementRef,
rect: [f64; 4],
) -> Result<PlacementRef> {
let (page, found) = locate(doc, page_num, at)?;
let p = &found.placement;
if !rect.iter().all(|v| v.is_finite()) {
return Err(ManipError::Image(
"the image rectangle is empty or not finite".into(),
));
}
let to = [
rect[0].min(rect[2]),
rect[1].min(rect[3]),
rect[0].max(rect[2]),
rect[1].max(rect[3]),
];
if !to.iter().all(|v| v.is_finite()) || to[2] - to[0] < MIN_EXTENT || to[3] - to[1] < MIN_EXTENT
{
return Err(ManipError::Image(
"the image rectangle is empty or not finite".into(),
));
}
let from = p.bbox;
let (ow, oh) = (from[2] - from[0], from[3] - from[1]);
if ow < 1e-9 || oh < 1e-9 {
return Err(degenerate());
}
let g_inv = invert(&p.ctm).ok_or_else(degenerate)?;
let (sx, sy) = ((to[2] - to[0]) / ow, (to[3] - to[1]) / oh);
let t_page = [sx, 0.0, 0.0, sy, to[0] - from[0] * sx, to[1] - from[1] * sy];
let t_unit = multiply_matrix(&multiply_matrix(&p.ctm, &t_page), &g_inv);
let container = private_container(doc, page_num, page, &at.form_path)?;
let mut editor = load_ops(doc, page_num, container)?;
let new_index = apply_unit_transform(editor.operations_mut(), at.op_index, &t_unit)?;
save_ops(doc, page_num, container, &editor)?;
Ok(handle_after(doc, page_num, &at.form_path, new_index))
}
pub fn replace_placement_image(
doc: &mut Document,
page_num: u32,
at: &PlacementRef,
data: &[u8],
) -> Result<PlacementRef> {
let (page, found) = locate(doc, page_num, at)?;
let p = &found.placement;
let (w, h) = (p.ctm[0].hypot(p.ctm[1]), p.ctm[2].hypot(p.ctm[3]));
if !(w.is_finite() && h.is_finite()) || w < 1e-9 || h < 1e-9 {
return Err(degenerate());
}
let (new_id, px, py) = create_image_xobject(doc, data)?;
let fit = (w / f64::from(px.max(1))).min(h / f64::from(py.max(1)));
let (sx, sy) = (f64::from(px) * fit / w, f64::from(py) * fit / h);
let t_unit = [sx, 0.0, 0.0, sy, (1.0 - sx) / 2.0, (1.0 - sy) / 2.0];
let container = private_container(doc, page_num, page, &at.form_path)?;
let mut res = resources_of(doc, container);
let new_name = unique_name(&res, "PdfluentIm");
set_xobject(&mut res, new_name.as_bytes(), new_id);
set_resources(doc, container, res);
let mut editor = load_ops(doc, page_num, container)?;
let do_op = Operation::new("Do", vec![Object::Name(new_name.into_bytes())]);
*editor
.operations_mut()
.get_mut(at.op_index)
.ok_or_else(stale)? = do_op;
let new_index = apply_unit_transform(editor.operations_mut(), at.op_index, &t_unit)?;
save_ops(doc, page_num, container, &editor)?;
if let (Some(name), Some(old)) = (&p.resource_name, p.xobject) {
release(doc, container, name.as_bytes(), old, editor.operations());
}
Ok(handle_after(doc, page_num, &at.form_path, new_index))
}
pub fn remove_placement(doc: &mut Document, page_num: u32, at: &PlacementRef) -> Result<()> {
let (page, found) = locate(doc, page_num, at)?;
let p = &found.placement;
let container = private_container(doc, page_num, page, &at.form_path)?;
let mut editor = load_ops(doc, page_num, container)?;
let i = at.op_index;
let ops = editor.operations();
let canonical = i >= 2
&& i + 1 < ops.len()
&& ops[i - 2].operator == "q"
&& ops[i - 1].operator == "cm"
&& ops[i + 1].operator == "Q";
if canonical {
editor.remove_range(i - 2..i + 2);
} else {
editor.remove_range(i..i + 1);
}
save_ops(doc, page_num, container, &editor)?;
if let (Some(name), Some(old)) = (&p.resource_name, p.xobject) {
release(doc, container, name.as_bytes(), old, editor.operations());
}
Ok(())
}
#[cfg(feature = "serde")]
pub mod json {
use super::{ImagePlacement, PlacementKind, PlacementRef};
use crate::error::{ManipError, Result};
use serde_json::{json, Value};
pub fn ref_json(at: &PlacementRef) -> Value {
let mut v = json!({ "formPath": at.form_path, "opIndex": at.op_index });
if let Some(t) = at.token {
v["token"] = json!(format!("{t:016x}"));
}
v
}
pub fn ref_from_json(text: &str) -> Result<PlacementRef> {
let bad = || {
ManipError::Other(
r#"image handle must be {"formPath":[...],"opIndex":n,"token":"..."}"#.into(),
)
};
let v: Value = serde_json::from_str(text).map_err(|_| bad())?;
let op_index = v.get("opIndex").and_then(Value::as_u64).ok_or_else(bad)? as usize;
let form_path = v
.get("formPath")
.and_then(Value::as_array)
.ok_or_else(bad)?
.iter()
.map(|x| x.as_u64().map(|n| n as usize).ok_or_else(bad))
.collect::<Result<Vec<_>>>()?;
let token = match v.get("token") {
None | Some(Value::Null) => None,
Some(t) => Some(
t.as_str()
.and_then(|s| u64::from_str_radix(s, 16).ok())
.ok_or_else(bad)?,
),
};
Ok(PlacementRef {
form_path,
op_index,
token,
})
}
pub fn placements_json(list: &[ImagePlacement]) -> String {
let rows: Vec<Value> = list
.iter()
.map(|p| {
let mut v = ref_json(&p.at);
v["kind"] = match p.kind {
PlacementKind::XObject => "xobject".into(),
PlacementKind::Inline => "inline".into(),
};
v["resourceName"] = json!(p.resource_name);
v["formNames"] = json!(p.form_names);
v["ctm"] = json!(p.ctm);
v["bbox"] = json!(p.bbox);
v["pixelWidth"] = json!(p.pixel_width);
v["pixelHeight"] = json!(p.pixel_height);
v["hasSmask"] = json!(p.has_smask);
v
})
.collect();
Value::Array(rows).to_string()
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_hand_built_handle_has_no_token() {
let at = PlacementRef::new(vec![2, 0], 7);
assert_eq!(
(at.form_path.clone(), at.op_index, at.token),
(vec![2, 0], 7, None)
);
}
use super::*;
use lopdf::dictionary;
const TOL: f64 = 0.02;
fn rgb_image(doc: &mut Document, w: i64, h: i64) -> ObjectId {
doc.add_object(Object::Stream(Stream::new(
dictionary! {
"Type" => "XObject", "Subtype" => "Image", "Width" => w, "Height" => h,
"ColorSpace" => "DeviceRGB", "BitsPerComponent" => 8_i64,
},
vec![200u8; (w * h * 3) as usize],
)))
}
fn png(w: u32, h: u32, alpha: bool) -> Vec<u8> {
let mut out = std::io::Cursor::new(Vec::new());
let img: image::DynamicImage = if alpha {
image::RgbaImage::from_pixel(w, h, image::Rgba([10, 20, 30, 128])).into()
} else {
image::RgbImage::from_pixel(w, h, image::Rgb([10, 20, 30])).into()
};
img.write_to(&mut out, image::ImageFormat::Png)
.expect("encode png");
out.into_inner()
}
struct Fixture {
doc: Document,
im_a: ObjectId,
im_b: ObjectId,
im_c: ObjectId,
form: ObjectId,
}
fn fixture() -> Fixture {
let mut doc = Document::with_version("1.7");
let im_a = rgb_image(&mut doc, 4, 2);
let im_b = rgb_image(&mut doc, 3, 3);
let im_c = rgb_image(&mut doc, 2, 2);
let form = doc.add_object(Object::Stream(Stream::new(
dictionary! {
"Type" => "XObject", "Subtype" => "Form",
"BBox" => vec![0.into(), 0.into(), 100.into(), 100.into()],
"Matrix" => vec![1.into(), 0.into(), 0.into(), 1.into(), 300.into(), 300.into()],
"Resources" => Object::Dictionary(dictionary! {
"XObject" => Object::Dictionary(dictionary! { "ImC" => Object::Reference(im_c) }),
}),
},
b"q 20 0 0 20 0 0 cm /ImC Do Q".to_vec(),
)));
let xobjects = |with_b: bool| {
let mut x =
dictionary! { "ImA" => Object::Reference(im_a), "FmC" => Object::Reference(form) };
if with_b {
x.set("ImB", Object::Reference(im_b));
}
Object::Dictionary(dictionary! { "XObject" => Object::Dictionary(x) })
};
let c1 = doc.add_object(Object::Stream(Stream::new(
dictionary! {},
b"q 100 0 0 50 50 600 cm /ImA Do Q \
q 1 0 0 1 100 100 cm 2 0 0 2 0 0 cm 10 0 0 10 0 0 cm /ImB Do Q \
/FmC Do \
q 40 0 0 40 400 400 cm BI /W 2 /H 2 /BPC 8 /CS /RGB ID abcdefghijkl EI Q"
.to_vec(),
)));
let c2 = doc.add_object(Object::Stream(Stream::new(
dictionary! {},
b"q 100 0 0 50 50 600 cm /ImA Do Q /FmC Do".to_vec(),
)));
let pages_id = doc.new_object_id();
let mut kids = Vec::new();
for (c, with_b) in [(c1, true), (c2, false)] {
let page = doc.add_object(Object::Dictionary(dictionary! {
"Type" => "Page", "Parent" => Object::Reference(pages_id),
"MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
"Contents" => Object::Reference(c),
"Resources" => xobjects(with_b),
}));
kids.push(Object::Reference(page));
}
doc.objects.insert(
pages_id,
Object::Dictionary(dictionary! { "Type" => "Pages", "Kids" => kids, "Count" => 2_i64 }),
);
let catalog = doc.add_object(Object::Dictionary(
dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) },
));
doc.trailer.set("Root", Object::Reference(catalog));
Fixture {
doc,
im_a,
im_b,
im_c,
form,
}
}
fn reopen(doc: &mut Document) -> Document {
let mut buf = Vec::new();
doc.save_to(&mut buf).expect("save");
Document::load_mem(&buf).expect("reopen")
}
fn assert_box(actual: [f64; 4], expected: [f64; 4], what: &str) {
for (a, e) in actual.iter().zip(expected) {
assert!((a - e).abs() < TOL, "{what}: {actual:?} != {expected:?}");
}
}
fn find(
doc: &Document,
page: u32,
kind: PlacementKind,
form: bool,
n: usize,
) -> ImagePlacement {
list_image_placements(doc, page)
.expect("list")
.into_iter()
.filter(|p| p.kind == kind && p.at.form_path.is_empty() != form)
.nth(n)
.expect("placement present")
}
#[test]
fn lists_nested_cm_form_and_inline_images() {
let f = fixture();
let list = list_image_placements(&f.doc, 1).unwrap();
assert_eq!(list.len(), 4);
assert_box(list[0].bbox, [50.0, 600.0, 150.0, 650.0], "ImA");
assert_eq!((list[0].pixel_width, list[0].pixel_height), (4, 2));
assert_eq!(list[0].xobject, Some(f.im_a));
assert_box(
list[1].bbox,
[100.0, 100.0, 120.0, 120.0],
"ImB (nested cm)",
);
assert_eq!(list[1].xobject, Some(f.im_b));
assert_box(list[2].bbox, [300.0, 300.0, 320.0, 320.0], "ImC (in form)");
assert_eq!(list[2].form_names, vec!["FmC".to_string()]);
assert_eq!(list[2].xobject, Some(f.im_c));
assert_eq!(list[3].kind, PlacementKind::Inline);
assert_box(list[3].bbox, [400.0, 400.0, 440.0, 440.0], "inline");
assert_eq!((list[3].pixel_width, list[3].pixel_height), (2, 2));
assert_eq!(list[3].resource_name, None);
assert!(!list[0].has_smask);
assert!(list_image_placements(&f.doc, 3).is_err());
}
#[test]
fn set_rect_moves_each_kind_and_survives_a_save() {
let target = [10.0, 20.0, 70.0, 50.0];
for n in 0..4 {
let mut f = fixture();
let before = list_image_placements(&f.doc, 1).unwrap();
let at = before[n].at.clone();
set_placement_rect(&mut f.doc, 1, &at, target).unwrap();
let mut doc = reopen(&mut f.doc);
let after = list_image_placements(&doc, 1).unwrap();
assert_eq!(after.len(), 4, "placement {n}");
assert_box(after[n].bbox, target, &format!("placement {n} moved"));
for (i, (b, a)) in before.iter().zip(&after).enumerate() {
if i != n {
assert_box(
a.bbox,
b.bbox,
&format!("placement {i} untouched by edit of {n}"),
);
}
}
set_placement_rect(&mut doc, 1, &after[n].at, [0.0, 0.0, 5.0, 5.0]).unwrap();
}
}
#[test]
fn form_edit_copies_the_form_and_leaves_other_pages_alone() {
let mut f = fixture();
let at = find(&f.doc, 1, PlacementKind::XObject, true, 0).at;
set_placement_rect(&mut f.doc, 1, &at, [0.0, 0.0, 10.0, 10.0]).unwrap();
let doc = reopen(&mut f.doc);
let p2 = list_image_placements(&doc, 2).unwrap();
assert_eq!(p2.len(), 2);
assert_box(
p2[1].bbox,
[300.0, 300.0, 320.0, 320.0],
"page 2 form image",
);
let p1_form = find(&doc, 1, PlacementKind::XObject, true, 0);
assert_box(p1_form.bbox, [0.0, 0.0, 10.0, 10.0], "page 1 form image");
assert_ne!(
p1_form.form_names,
vec!["FmC".to_string()],
"page 1 uses the private copy"
);
assert!(doc.objects.contains_key(&f.form));
}
#[test]
fn replace_fits_and_centres_and_drops_the_old_image() {
let mut f = fixture();
let at = list_image_placements(&f.doc, 1).unwrap()[1].at.clone(); let before = list_image_placements(&f.doc, 1).unwrap()[1].bbox;
replace_placement_image(&mut f.doc, 1, &at, &png(8, 4, true)).unwrap();
let doc = reopen(&mut f.doc);
let after = list_image_placements(&doc, 1).unwrap();
let cy = (before[1] + before[3]) / 2.0;
assert_box(
after[1].bbox,
[before[0], cy - 5.0, before[2], cy + 5.0],
"fit 2:1 into a square",
);
assert_eq!((after[1].pixel_width, after[1].pixel_height), (8, 4));
assert!(after[1].has_smask, "PNG alpha becomes a soft mask");
assert!(
!doc.objects.contains_key(&f.im_b),
"replaced image is no longer in the file"
);
}
#[test]
fn replace_inline_and_form_images() {
let mut f = fixture();
let inline = find(&f.doc, 1, PlacementKind::Inline, false, 0).at;
replace_placement_image(&mut f.doc, 1, &inline, &png(4, 4, false)).unwrap();
let form_img = find(&f.doc, 1, PlacementKind::XObject, true, 0).at;
replace_placement_image(&mut f.doc, 1, &form_img, &png(2, 4, false)).unwrap();
let doc = reopen(&mut f.doc);
let after = list_image_placements(&doc, 1).unwrap();
assert_eq!(after.len(), 4);
assert!(
after.iter().all(|p| p.kind == PlacementKind::XObject),
"no inline image left"
);
let sq = after
.iter()
.find(|p| p.pixel_width == 4 && p.pixel_height == 4)
.unwrap();
assert_box(
sq.bbox,
[400.0, 400.0, 440.0, 440.0],
"inline replaced in place",
);
let tall = after
.iter()
.find(|p| p.pixel_width == 2 && p.pixel_height == 4)
.unwrap();
assert_box(
tall.bbox,
[305.0, 300.0, 315.0, 320.0],
"2x4 fitted into the 20x20 form image",
);
assert_eq!(list_image_placements(&doc, 2).unwrap()[1].pixel_width, 2);
}
#[test]
fn remove_drops_unshared_images_and_keeps_shared_ones() {
let mut f = fixture();
let b = list_image_placements(&f.doc, 1).unwrap()[1].at.clone();
remove_placement(&mut f.doc, 1, &b).unwrap();
let a = list_image_placements(&f.doc, 1).unwrap()[0].at.clone();
remove_placement(&mut f.doc, 1, &a).unwrap();
let inline = find(&f.doc, 1, PlacementKind::Inline, false, 0).at;
remove_placement(&mut f.doc, 1, &inline).unwrap();
let form_img = find(&f.doc, 1, PlacementKind::XObject, true, 0).at;
remove_placement(&mut f.doc, 1, &form_img).unwrap();
let doc = reopen(&mut f.doc);
assert!(list_image_placements(&doc, 1).unwrap().is_empty());
assert!(!doc.objects.contains_key(&f.im_b));
assert!(doc.objects.contains_key(&f.im_a), "shared image survives");
let p2 = list_image_placements(&doc, 2).unwrap();
assert_eq!(p2.len(), 2, "page 2 keeps both placements");
assert_box(
p2[1].bbox,
[300.0, 300.0, 320.0, 320.0],
"page 2 form image intact",
);
assert!(doc.objects.contains_key(&f.im_c));
}
#[test]
fn stale_handles_and_bad_input_change_nothing() {
let mut f = fixture();
let stale_at = PlacementRef {
form_path: vec![],
op_index: 1,
token: None,
};
assert!(set_placement_rect(&mut f.doc, 1, &stale_at, [0.0, 0.0, 1.0, 1.0]).is_err());
assert!(remove_placement(&mut f.doc, 1, &stale_at).is_err());
let at = list_image_placements(&f.doc, 1).unwrap()[0].at.clone();
assert!(set_placement_rect(&mut f.doc, 1, &at, [5.0, 5.0, 5.0, 90.0]).is_err());
assert!(set_placement_rect(&mut f.doc, 1, &at, [0.0, 0.0, f64::NAN, 9.0]).is_err());
assert!(replace_placement_image(&mut f.doc, 1, &at, b"not an image").is_err());
assert!(set_placement_rect(&mut f.doc, 9, &at, [0.0, 0.0, 1.0, 1.0]).is_err());
let after = list_image_placements(&f.doc, 1).unwrap();
assert_eq!(after.len(), 4);
assert_box(after[0].bbox, [50.0, 600.0, 150.0, 650.0], "ImA untouched");
}
fn one_page(doc: &mut Document, content: &[u8], xobjects: Dictionary) {
let c = doc.add_object(Object::Stream(Stream::new(
dictionary! {},
content.to_vec(),
)));
let pages_id = doc.new_object_id();
let page = doc.add_object(Object::Dictionary(dictionary! {
"Type" => "Page", "Parent" => Object::Reference(pages_id),
"MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
"Contents" => Object::Reference(c),
"Resources" => Object::Dictionary(dictionary! { "XObject" => Object::Dictionary(xobjects) }),
}));
doc.objects.insert(
pages_id,
Object::Dictionary(dictionary! {
"Type" => "Pages", "Kids" => vec![Object::Reference(page)], "Count" => 1_i64,
}),
);
let catalog = doc.add_object(Object::Dictionary(
dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) },
));
doc.trailer.set("Root", Object::Reference(catalog));
}
fn three_images() -> (Document, [ObjectId; 3]) {
let mut doc = Document::with_version("1.7");
let ids = [
rgb_image(&mut doc, 2, 2),
rgb_image(&mut doc, 3, 3),
rgb_image(&mut doc, 4, 4),
];
one_page(
&mut doc,
b"q 10 0 0 10 0 0 cm /I1 Do Q q 10 0 0 10 50 0 cm /I2 Do Q q 10 0 0 10 100 0 cm /I3 Do Q",
dictionary! {
"I1" => Object::Reference(ids[0]),
"I2" => Object::Reference(ids[1]),
"I3" => Object::Reference(ids[2]),
},
);
(doc, ids)
}
#[test]
fn a_handle_from_before_an_earlier_removal_is_stale() {
for edit in 0..3 {
let (mut doc, ids) = three_images();
let list = list_image_placements(&doc, 1).unwrap();
remove_placement(&mut doc, 1, &list[0].at).unwrap();
let before = list_image_placements(&doc, 1).unwrap();
assert_eq!(before.len(), 2);
let err = match edit {
0 => set_placement_rect(&mut doc, 1, &list[1].at, [0.0, 0.0, 5.0, 5.0]).map(drop),
1 => replace_placement_image(&mut doc, 1, &list[1].at, &png(2, 2, false)).map(drop),
_ => remove_placement(&mut doc, 1, &list[1].at),
}
.expect_err("stale handle must be refused");
assert!(err.to_string().contains("list the page again"), "{err}");
let after = list_image_placements(&doc, 1).unwrap();
assert_eq!(
after, before,
"a refused edit changes nothing (edit {edit})"
);
assert!(doc.objects.contains_key(&ids[2]));
}
}
#[test]
fn the_handle_an_edit_returns_is_current_and_the_old_one_is_not() {
let (mut doc, _) = three_images();
let list = list_image_placements(&doc, 1).unwrap();
let fresh = set_placement_rect(&mut doc, 1, &list[1].at, [0.0, 0.0, 5.0, 5.0]).unwrap();
assert!(remove_placement(&mut doc, 1, &list[1].at).is_err());
assert_eq!(
list_image_placements(&doc, 1)
.unwrap()
.iter()
.find(|p| p.at.op_index == fresh.op_index)
.map(|p| &p.at),
Some(&fresh)
);
set_placement_rect(&mut doc, 1, &fresh, [1.0, 1.0, 6.0, 6.0]).unwrap();
}
#[test]
fn a_handle_without_a_token_matches_by_position() {
let (mut doc, _) = three_images();
let at = list_image_placements(&doc, 1).unwrap()[2].at.clone();
let bare = PlacementRef { token: None, ..at };
remove_placement(&mut doc, 1, &bare).unwrap();
assert_eq!(list_image_placements(&doc, 1).unwrap().len(), 2);
}
fn shared_name_fixture() -> (Document, ObjectId) {
let mut doc = Document::with_version("1.7");
let im = rgb_image(&mut doc, 2, 2);
let form = doc.add_object(Object::Stream(Stream::new(
dictionary! {
"Type" => "XObject", "Subtype" => "Form",
"BBox" => vec![0.into(), 0.into(), 100.into(), 100.into()],
},
b"q 20 0 0 20 0 0 cm /Im1 Do Q".to_vec(),
)));
one_page(
&mut doc,
b"q 10 0 0 10 200 200 cm /Im1 Do Q /Fm1 Do",
dictionary! { "Im1" => Object::Reference(im), "Fm1" => Object::Reference(form) },
);
(doc, im)
}
fn assert_inheriting_form_still_draws(doc: &Document, original: Option<ObjectId>) {
let list = list_image_placements(doc, 1).unwrap();
let nested: Vec<_> = list.iter().filter(|p| !p.at.form_path.is_empty()).collect();
assert_eq!(nested.len(), 1, "the form still draws its image: {list:?}");
assert_box(nested[0].bbox, [0.0, 0.0, 20.0, 20.0], "nested form image");
if let Some(id) = original {
assert!(doc.objects.contains_key(&id), "shared image survives");
}
}
fn deep_chain_fixture(depth: usize) -> (Document, ObjectId) {
let mut doc = Document::with_version("1.7");
let im = rgb_image(&mut doc, 2, 2);
let mut xobjects = dictionary! { "Im1" => Object::Reference(im) };
for n in 1..=depth {
let content = if n == depth {
"q 20 0 0 20 0 0 cm /Im1 Do Q".to_string()
} else {
format!("/Fm{} Do", n + 1)
};
let form = doc.add_object(Object::Stream(Stream::new(
dictionary! {
"Type" => "XObject", "Subtype" => "Form",
"BBox" => vec![0.into(), 0.into(), 100.into(), 100.into()],
},
content.into_bytes(),
)));
xobjects.set(format!("Fm{n}"), Object::Reference(form));
}
one_page(
&mut doc,
b"q 10 0 0 10 200 200 cm /Im1 Do Q /Fm1 Do",
xobjects,
);
(doc, im)
}
fn page_still_names_image(doc: &Document) -> bool {
let page = *doc.get_pages().get(&1).unwrap();
let res = inherited_resources(doc, page);
matches!(
res.get(b"XObject")
.and_then(|x| x.as_dict())
.and_then(|x| x.get(b"Im1")),
Ok(Object::Reference(_))
)
}
#[test]
fn an_inheriting_form_chain_past_the_depth_limit_keeps_the_image() {
let (mut doc, im) = deep_chain_fixture(MAX_FORM_DEPTH + 3);
let direct = list_image_placements(&doc, 1).unwrap()[0].at.clone();
assert!(direct.form_path.is_empty());
remove_placement(&mut doc, 1, &direct).unwrap();
assert!(
doc.objects.contains_key(&im),
"image deleted while a deep form draws it"
);
assert!(
page_still_names_image(&doc),
"page lost the name a deep form resolves"
);
}
#[test]
fn an_inheriting_form_chain_within_the_depth_limit_is_inspected_exactly() {
let (mut doc, im) = deep_chain_fixture(MAX_FORM_DEPTH - 2);
for (_, obj) in doc.objects.iter_mut() {
if let Object::Stream(s) = obj {
if s.content.ends_with(b"/Im1 Do Q") && s.dict.has(b"Subtype") {
s.content = b"q 20 0 0 20 0 0 cm /Other Do Q".to_vec();
}
}
}
let direct = list_image_placements(&doc, 1).unwrap()[0].at.clone();
remove_placement(&mut doc, 1, &direct).unwrap();
assert!(
!doc.objects.contains_key(&im),
"an unused image should still be released"
);
}
#[test]
fn removing_a_placement_keeps_an_image_an_inheriting_form_draws() {
let (mut doc, im) = shared_name_fixture();
let direct = list_image_placements(&doc, 1).unwrap()[0].at.clone();
assert!(direct.form_path.is_empty());
remove_placement(&mut doc, 1, &direct).unwrap();
assert_inheriting_form_still_draws(&doc, Some(im));
assert_inheriting_form_still_draws(&reopen(&mut doc), Some(im));
}
#[test]
fn replacing_a_placement_keeps_an_image_an_inheriting_form_draws() {
let (mut doc, im) = shared_name_fixture();
let direct = list_image_placements(&doc, 1).unwrap()[0].at.clone();
replace_placement_image(&mut doc, 1, &direct, &png(4, 4, false)).unwrap();
assert_inheriting_form_still_draws(&doc, Some(im));
assert_inheriting_form_still_draws(&reopen(&mut doc), Some(im));
}
}