mod copy;
mod inherit;
mod nup;
mod range;
mod viewer;
use pdfrum_common::PageIndex;
use pdfrum_object::{Array, Dict, Name, ObjRef, Object, Resolve, names};
use pdfrum_parser::Document;
use crate::doc::EditDoc;
use crate::error::Error;
use crate::names as edit_names;
use copy::ObjectMap;
use inherit::inheritable;
use nup::{NupGrid, sub_page_fragment};
pub use range::PageRange;
const LETTER: [i64; 4] = [0, 0, 612, 792];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ImportOptions {
pub at: PageIndex,
pub viewer_preferences: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NUpOptions {
pub sheet: (f32, f32),
pub grid: (u32, u32),
}
impl Default for NUpOptions {
fn default() -> Self {
Self {
sheet: (612.0, 792.0),
grid: (2, 1),
}
}
}
pub(crate) fn init_dest(dest: &mut EditDoc<'_>) -> Result<u32, Error> {
let catalog_ref = dest
.base()
.trailer()
.reference(names::ROOT)
.ok_or(Error::NoDestinationCatalog)?;
let catalog = dest
.fetch(catalog_ref)
.ok()
.and_then(|o| o.as_dict().cloned())
.ok_or(Error::NoDestinationCatalog)?;
let mut catalog = catalog;
if catalog
.name(names::TYPE)
.is_none_or(|n| n.as_bytes().is_empty())
{
set(
&mut catalog,
names::TYPE,
Object::Name(edit_names::CATALOG.clone()),
);
}
let pages_ref = match catalog.raw(names::PAGES) {
Some(Object::Ref(r)) if dest.fetch(*r).is_ok_and(|o| o.as_dict().is_some()) => *r,
_ => {
let fresh = fresh_pages_node(dest);
set(&mut catalog, names::PAGES, Object::Ref(fresh));
fresh
}
};
let mut pages = dest
.fetch(pages_ref)
.ok()
.and_then(|o| o.as_dict().cloned())
.unwrap_or_default();
if pages
.name(names::TYPE)
.is_none_or(|n| n.as_bytes().is_empty())
{
set(&mut pages, names::TYPE, Object::Name(names::PAGES.clone()));
}
if !matches!(pages.raw(names::KIDS), Some(Object::Array(_))) {
set(&mut pages, names::KIDS, Object::Array(Array::new()));
set(&mut pages, names::COUNT, Object::Int(0));
}
dest.replace(pages_ref, Object::Dict(pages));
dest.replace(catalog_ref, Object::Dict(catalog));
Ok(pages_ref.num)
}
fn fresh_pages_node(dest: &mut EditDoc<'_>) -> ObjRef {
dest.add(Object::Dict(Dict::from_pairs([
(names::TYPE.clone(), Object::Name(names::PAGES.clone())),
(names::COUNT.clone(), Object::Int(0)),
(names::KIDS.clone(), Object::Array(Array::new())),
])))
}
pub fn import_pages(
dest: &mut EditDoc<'_>,
src: &Document,
pages: &PageRange,
opts: &ImportOptions,
) -> Result<(), Error> {
for index in pages.indices() {
if index.get() >= src.page_count() {
return Err(Error::PageIndexOutOfRange(*index));
}
}
let pages_node = init_dest(dest)?;
let mut map = ObjectMap::new();
let mut created = Vec::new();
for index in pages.indices() {
let page = src
.page(*index)
.map_err(|_| Error::PageIndexOutOfRange(*index))?;
let dest_page = dest.add(Object::Null);
if let Some(source_ref) = page.reference {
map.record(source_ref.num, dest_page.num);
}
let built = build_page(dest, src, &page.dict, pages_node, &mut map, dest_page);
dest.replace(dest_page, Object::Dict(built));
created.push(dest_page);
}
insert_into_tree(dest, pages_node, &created, opts.at.get());
if opts.viewer_preferences
&& let Ok(catalog) = src.catalog()
&& let Some(prefs) = viewer::filtered(&catalog, src)
{
copy_viewer_preferences(dest, prefs);
}
Ok(())
}
fn build_page(
dest: &mut EditDoc<'_>,
src: &Document,
source: &Dict,
pages_node: u32,
map: &mut ObjectMap,
self_ref: ObjRef,
) -> Dict {
let mut out = Dict::from_pairs([
(names::TYPE.clone(), Object::Name(names::PAGE.clone())),
(
names::PARENT.clone(),
Object::Ref(ObjRef::new(pages_node, 0)),
),
]);
for (key, value) in source.iter() {
if key == names::TYPE || key == names::PARENT {
continue;
}
let mut value = value.clone();
if rewrite_value(dest, src, &mut value, map, pages_node) {
out.push(key.clone(), value);
}
}
flatten_inherited(dest, src, source, &mut out, map, pages_node);
let _ = self_ref;
out
}
fn flatten_inherited(
dest: &mut EditDoc<'_>,
src: &Document,
source: &Dict,
out: &mut Dict,
map: &mut ObjectMap,
pages_node: u32,
) {
if !copy_inherited(dest, src, source, out, names::MEDIA_BOX, map, pages_node)
&& !copy_inherited(dest, src, source, out, names::CROP_BOX, map, pages_node)
{
set(
out,
names::MEDIA_BOX,
Object::Array(Array::of(LETTER.map(Object::Int))),
);
} else if !out.contains_key(names::MEDIA_BOX) {
if let Some(crop) = out.raw(names::CROP_BOX).cloned() {
set(out, names::MEDIA_BOX, crop);
}
}
if !copy_inherited(dest, src, source, out, names::RESOURCES, map, pages_node) {
set(out, names::RESOURCES, Object::Dict(Dict::new()));
}
copy_inherited(dest, src, source, out, names::CROP_BOX, map, pages_node);
copy_inherited(dest, src, source, out, names::ROTATE, map, pages_node);
}
fn copy_inherited(
dest: &mut EditDoc<'_>,
src: &Document,
source: &Dict,
out: &mut Dict,
key: &Name,
map: &mut ObjectMap,
pages_node: u32,
) -> bool {
if out.contains_key(key) {
return true;
}
let Some(mut value) = inheritable(source, key, src) else {
return false;
};
if !rewrite_value(dest, src, &mut value, map, pages_node) {
return false;
}
out.push(key.clone(), value);
true
}
fn rewrite_value(
dest: &mut EditDoc<'_>,
src: &Document,
value: &mut Object,
map: &mut ObjectMap,
pages_node: u32,
) -> bool {
copy::rewrite_in_place(dest, src, value, map, pages_node)
}
pub(crate) fn insert_into_tree(
dest: &mut EditDoc<'_>,
pages_node: u32,
created: &[ObjRef],
at: u32,
) {
let node_ref = ObjRef::new(pages_node, 0);
let mut node = dest
.fetch(node_ref)
.ok()
.and_then(|o| o.as_dict().cloned())
.unwrap_or_default();
let existing: Vec<Object> = node
.raw(names::KIDS)
.and_then(Object::as_array)
.map(|a| a.iter().cloned().collect())
.unwrap_or_default();
let split = (at as usize).min(existing.len());
let mut kids = Array::new();
for value in existing.get(..split).unwrap_or_default() {
kids.push(value.clone());
}
for page in created {
kids.push(Object::Ref(*page));
}
for value in existing.get(split..).unwrap_or_default() {
kids.push(value.clone());
}
let count = i64::try_from(kids.len()).unwrap_or(i64::MAX);
set(&mut node, names::KIDS, Object::Array(kids));
set(&mut node, names::COUNT, Object::Int(count));
dest.replace(node_ref, Object::Dict(node));
}
fn copy_viewer_preferences(dest: &mut EditDoc<'_>, prefs: Dict) {
let Some(catalog_ref) = dest.base().trailer().reference(names::ROOT) else {
return;
};
let Some(mut catalog) = dest
.fetch(catalog_ref)
.ok()
.and_then(|o| o.as_dict().cloned())
else {
return;
};
set(&mut catalog, names::VIEWER_PREFERENCES, Object::Dict(prefs));
dest.replace(catalog_ref, Object::Dict(catalog));
}
pub fn n_page_to_one(
dest: &mut EditDoc<'_>,
src: &Document,
pages: &PageRange,
opts: &NUpOptions,
) -> Result<(), Error> {
let (x, y) = opts.grid;
let (width, height) = opts.sheet;
if x == 0 || y == 0 || width <= 0.0 || height <= 0.0 {
return Err(Error::BadNupParams);
}
for index in pages.indices() {
if index.get() >= src.page_count() {
return Err(Error::PageIndexOutOfRange(*index));
}
}
let pages_node = init_dest(dest)?;
let grid = NupGrid {
sheet_width: width,
sheet_height: height,
x,
y,
};
let mut map = ObjectMap::new();
let mut forms: Vec<(u32, ObjRef)> = Vec::new();
let mut created = Vec::new();
for chunk in pages.indices().chunks(grid.per_sheet().max(1) as usize) {
let mut content = String::new();
let mut xobjects = Dict::new();
for (slot, index) in chunk.iter().enumerate() {
let page = src
.page(*index)
.map_err(|_| Error::PageIndexOutOfRange(*index))?;
let key = page.reference.map_or(u32::MAX - index.get(), |r| r.num);
let form = if let Some((_, existing)) = forms.iter().find(|(k, _)| *k == key) {
*existing
} else {
let made = make_form(dest, src, &page.dict, pages_node, &mut map);
forms.push((key, made));
made
};
let (page_w, page_h) = page_size(&page.dict, src);
#[expect(
clippy::cast_possible_truncation,
reason = "a slot index is bounded by the grid, which is a u32"
)]
let edit = grid.edit(slot as u32, page_w, page_h);
let name = format!("X{}", xobjects.len() + 1);
content.push_str(&sub_page_fragment(&name, edit));
xobjects.push(Name::from(name.as_str()), Object::Ref(form));
}
created.push(make_sheet(
dest, pages_node, &content, xobjects, width, height,
));
}
insert_into_tree(dest, pages_node, &created, 0);
Ok(())
}
fn make_form(
dest: &mut EditDoc<'_>,
src: &Document,
page: &Dict,
pages_node: u32,
map: &mut ObjectMap,
) -> ObjRef {
let content = assemble_content(page, src);
let mut resources = Dict::new();
let mut carrier = Dict::new();
if copy_inherited(
dest,
src,
page,
&mut carrier,
names::RESOURCES,
map,
pages_node,
) && let Some(found) = carrier.raw(names::RESOURCES).and_then(Object::as_dict)
{
resources = found.clone();
}
let (media, crop) = boxes(page, src);
let bbox = intersect(media, crop);
let dict = Dict::from_pairs([
(names::RESOURCES.clone(), Object::Dict(resources)),
(
edit_names::BBOX.clone(),
Object::Array(Array::of(bbox.map(Object::Real))),
),
(
edit_names::MATRIX.clone(),
Object::Array(Array::of(
[1.0, 0.0, 0.0, 1.0, -bbox[0], -bbox[1]].map(Object::Real),
)),
),
(
names::TYPE.clone(),
Object::Name(edit_names::XOBJECT.clone()),
),
(
names::SUBTYPE.clone(),
Object::Name(edit_names::FORM.clone()),
),
(edit_names::FORM_TYPE.clone(), Object::Int(1)),
(
names::LENGTH.clone(),
Object::Int(i64::try_from(content.len()).unwrap_or(0)),
),
]);
dest.add(Object::Stream(Box::new(pdfrum_object::Stream::new(
dict,
pdfrum_object::ByteSpan::from(content),
))))
}
fn make_sheet(
dest: &mut EditDoc<'_>,
pages_node: u32,
content: &str,
xobjects: Dict,
width: f32,
height: f32,
) -> ObjRef {
let stream = dest.add(Object::Stream(Box::new(pdfrum_object::Stream::new(
Dict::from_pairs([(
names::LENGTH.clone(),
Object::Int(i64::try_from(content.len()).unwrap_or(0)),
)]),
pdfrum_object::ByteSpan::from(content.as_bytes().to_vec()),
))));
dest.add(Object::Dict(Dict::from_pairs([
(names::TYPE.clone(), Object::Name(names::PAGE.clone())),
(
names::PARENT.clone(),
Object::Ref(ObjRef::new(pages_node, 0)),
),
(
names::MEDIA_BOX.clone(),
Object::Array(Array::of([
Object::Int(0),
Object::Int(0),
Object::Real(width),
Object::Real(height),
])),
),
(
names::RESOURCES.clone(),
Object::Dict(Dict::from_pairs([(
edit_names::XOBJECT.clone(),
Object::Dict(xobjects),
)])),
),
(names::CONTENTS.clone(), Object::Ref(stream)),
])))
}
fn assemble_content(page: &Dict, src: &Document) -> Vec<u8> {
let limits = pdfrum_common::Limits::default();
let mut diags = pdfrum_common::Diagnostics::default();
let mut out = Vec::new();
match page.get(names::CONTENTS, src).as_deref() {
Some(Object::Stream(s)) => {
out = pdfrum_parser::decoded_stream(s, src, &limits, &mut diags);
}
Some(Object::Array(a)) => {
for i in 0..a.len() {
let Some(s) = a.stream_at(i, src) else {
continue;
};
out.extend_from_slice(&pdfrum_parser::decoded_stream(&s, src, &limits, &mut diags));
out.push(b'\n');
}
}
_ => {}
}
out
}
fn boxes(page: &Dict, src: &Document) -> ([f32; 4], Option<[f32; 4]>) {
let read = |key: &Name| -> Option<[f32; 4]> {
let a = match inheritable(page, key, src) {
Some(Object::Array(a)) => a,
_ => page.array(key, src)?,
};
if a.len() < 4 {
return None;
}
Some(normalize([
a.number_at_or_zero(0),
a.number_at_or_zero(1),
a.number_at_or_zero(2),
a.number_at_or_zero(3),
]))
};
let media = read(names::MEDIA_BOX).unwrap_or([0.0, 0.0, 612.0, 792.0]);
(media, read(names::CROP_BOX))
}
fn normalize(b: [f32; 4]) -> [f32; 4] {
[
b[0].min(b[2]),
b[1].min(b[3]),
b[0].max(b[2]),
b[1].max(b[3]),
]
}
fn intersect(media: [f32; 4], crop: Option<[f32; 4]>) -> [f32; 4] {
let Some(crop) = crop else {
return media;
};
[
media[0].max(crop[0]),
media[1].max(crop[1]),
media[2].min(crop[2]),
media[3].min(crop[3]),
]
}
fn page_size(page: &Dict, src: &Document) -> (f32, f32) {
let (media, crop) = boxes(page, src);
let b = intersect(media, crop);
let (w, h) = (b[2] - b[0], b[3] - b[1]);
let quarter = matches!(
inheritable(page, names::ROTATE, src)
.or_else(|| page.raw(names::ROTATE).cloned())
.and_then(|o| o.as_int())
.map(|v| ((v / 90) % 4 + 4) % 4),
Some(1 | 3)
);
if quarter { (h, w) } else { (w, h) }
}
fn set(dict: &mut Dict, key: &Name, value: Object) {
if dict.contains_key(key) {
*dict = Dict::from_pairs(dict.iter().map(|(k, v)| {
if k == key {
(k.clone(), value.clone())
} else {
(k.clone(), v.clone())
}
}));
} else {
dict.push(key.clone(), value);
}
}