mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 04:58:34 -06:00
244 lines
10 KiB
Rust
244 lines
10 KiB
Rust
use super::info::text_value;
|
|
use super::types::{FLAG_HIDDEN, FLAG_NO_ROTATE, FLAG_NO_VIEW, Shape};
|
|
use super::{AnnotError, Dict, Document, ObjRef, Object, appearance};
|
|
|
|
// ── pages and /Annots ───────────────────────────────────────────────────────────────────────
|
|
|
|
/// The page object of each leaf page, in order.
|
|
pub fn page_refs(doc: &Document) -> Result<Vec<ObjRef>, AnnotError> {
|
|
let root = doc.root().ok_or(AnnotError::NoPageTree)?;
|
|
let pages = doc.get(root).as_dict().and_then(|d| d.reference(b"Pages")).ok_or(AnnotError::NoPageTree)?;
|
|
Ok(PageWalk::new(doc, pages).collect())
|
|
}
|
|
|
|
/// Depth-first walk over the leaf pages under `top`. It keeps one cursor per open `/Kids` array,
|
|
/// never one entry per child, so a child listed many times or shared between nodes is skipped by
|
|
/// the seen check without being queued first (`pdfcraft_model::pages` walks the same way).
|
|
/// Cycles end at the seen check, a `/Kids` array object shared by several nodes is read once,
|
|
/// and at most a million nodes and four million `/Kids` entries are read.
|
|
pub(crate) struct PageWalk<'a> {
|
|
doc: &'a Document,
|
|
seen: std::collections::HashSet<ObjRef>,
|
|
pub(crate) pending: Vec<(std::sync::Arc<Object>, usize)>,
|
|
/// `/Kids` entries read so far.
|
|
pub(crate) examined: usize,
|
|
}
|
|
|
|
impl<'a> PageWalk<'a> {
|
|
pub(crate) fn new(doc: &'a Document, top: ObjRef) -> Self {
|
|
let top = std::sync::Arc::new(Object::Array(vec![Object::Ref(top)]));
|
|
Self { doc, seen: Default::default(), pending: vec![(top, 0)], examined: 0 }
|
|
}
|
|
}
|
|
|
|
impl Iterator for PageWalk<'_> {
|
|
type Item = ObjRef;
|
|
|
|
fn next(&mut self) -> Option<ObjRef> {
|
|
while let Some((kids, next)) = self.pending.last_mut() {
|
|
let Some(kid) = kids.as_array().and_then(|a| a.get(*next)) else {
|
|
self.pending.pop();
|
|
continue;
|
|
};
|
|
let node = kid.as_ref();
|
|
*next += 1;
|
|
self.examined += 1;
|
|
let Some(node) = node else { continue };
|
|
if self.seen.len() >= 1_000_000 || self.examined > 4_000_000 {
|
|
self.pending.clear();
|
|
return None;
|
|
}
|
|
if !self.seen.insert(node) {
|
|
continue;
|
|
}
|
|
let obj = self.doc.get(node);
|
|
let Some(d) = obj.as_dict() else { continue };
|
|
match d.get(b"Kids") {
|
|
// A `/Kids` array object shared by several nodes is read under the first: under
|
|
// the others it would only list nodes already seen.
|
|
Some(Object::Ref(array)) if d.name(b"Type") != Some(b"Page") => {
|
|
if self.seen.insert(*array) {
|
|
self.pending.push((self.doc.get(*array), 0));
|
|
}
|
|
}
|
|
Some(kids) if d.name(b"Type") != Some(b"Page") => self.pending.push((self.doc.resolve(kids), 0)),
|
|
_ => return Some(node),
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) fn page_ref(doc: &Document, page: usize) -> Result<ObjRef, AnnotError> {
|
|
page_refs(doc)?.get(page).copied().ok_or(AnnotError::NoSuchPage(page))
|
|
}
|
|
|
|
/// The entries of a page's `/Annots` (references or inline dictionaries).
|
|
pub(crate) fn annots(doc: &Document, page: ObjRef) -> Vec<Object> {
|
|
let obj = doc.get(page);
|
|
let Some(a) = obj.as_dict().and_then(|d| d.get(b"Annots")) else { return Vec::new() };
|
|
let list = doc.resolve(a).as_array().cloned().unwrap_or_default();
|
|
#[cfg(test)]
|
|
ANNOTS_ENTRIES_COPIED.with(|n| n.set(n.get() + list.len()));
|
|
list
|
|
}
|
|
|
|
#[cfg(test)]
|
|
thread_local! {
|
|
/// Entries copied out of pages' `/Annots` arrays.
|
|
pub(crate) static ANNOTS_ENTRIES_COPIED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
}
|
|
|
|
/// Replace a page's `/Annots`, writing into the shared array object when it is indirect. An empty
|
|
/// list removes a direct `/Annots`.
|
|
pub fn set_annots(doc: &mut Document, page: ObjRef, list: Vec<Object>) -> Result<(), pdfcraft_cos::CosError> {
|
|
let existing = doc.get(page).as_dict().and_then(|d| d.reference(b"Annots"));
|
|
match existing {
|
|
Some(r) if doc.get(r).as_array().is_some() => doc.set(r, Object::Array(list)),
|
|
_ => doc.update_dict(page, |d| {
|
|
if list.is_empty() {
|
|
d.remove(b"Annots");
|
|
} else {
|
|
d.set(b"Annots".to_vec(), Object::Array(list));
|
|
}
|
|
})?,
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The annotation at `(page, index)` as an indirect object (inline dictionaries are promoted).
|
|
pub(crate) fn annot_ref(doc: &mut Document, page: usize, index: usize) -> Result<(ObjRef, ObjRef), AnnotError> {
|
|
let p = page_ref(doc, page)?;
|
|
let mut list = annots(doc, p);
|
|
let entry = list.get(index).cloned().ok_or(AnnotError::NoSuchAnnotation { page, index })?;
|
|
let r = match entry {
|
|
Object::Ref(r) if doc.get(r).as_dict().is_some() => r,
|
|
Object::Dict(d) => {
|
|
let r = doc.add(Object::Dict(d));
|
|
list[index] = Object::Ref(r);
|
|
set_annots(doc, p, list)?;
|
|
r
|
|
}
|
|
_ => return Err(AnnotError::NoSuchAnnotation { page, index }),
|
|
};
|
|
Ok((p, r))
|
|
}
|
|
|
|
pub(crate) fn annot_dict(doc: &Document, r: ObjRef) -> Dict {
|
|
doc.get(r).as_dict().cloned().unwrap_or_default()
|
|
}
|
|
|
|
/// A Fill & Sign item: typed text, a check/cross/dot/line, or a typed, drawn or image signature.
|
|
///
|
|
/// New items carry `/PCFillSign`. Older files are recognised from the dictionaries PdfCraft
|
|
/// already writes (`/IT /FreeTextTypeWriter`, stamp `/Name`, drawn ink whose subject is
|
|
/// "Signature").
|
|
pub fn is_fill_sign(doc: &Document, d: &Dict) -> bool {
|
|
if matches!(d.get(b"PCFillSign").map(|o| doc.resolve(o)).as_deref(), Some(Object::Bool(true))) {
|
|
return true;
|
|
}
|
|
let subtype = d.name(b"Subtype").unwrap_or_default();
|
|
if subtype == b"FreeText" && d.name(b"IT") == Some(b"FreeTextTypeWriter") {
|
|
return true;
|
|
}
|
|
if subtype == b"Stamp" {
|
|
if matches!(d.name(b"Name"), Some(b"PCCheck" | b"PCCross" | b"PCDot" | b"PCLine" | b"PCTypedSignature")) {
|
|
return true;
|
|
}
|
|
if matches!(d.name(b"Name"), Some(b"PCCustomSignature" | b"PCCustomInitials"))
|
|
&& matches!(d.get(b"PCPictureImage").map(|o| doc.resolve(o)).as_deref(), Some(Object::Bool(true)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
subtype == b"Ink" && text_value(doc, d, b"Subj").as_deref() == Some("Signature")
|
|
}
|
|
|
|
pub(crate) fn annotation_flags(doc: &Document, d: &Dict) -> i64 {
|
|
d.get(b"F").and_then(|f| doc.resolve(f).as_int()).unwrap_or(0)
|
|
}
|
|
|
|
/// Whether the renderer keeps the annotation upright on a turned page: the NoRotate flag, with
|
|
/// `/F` read as the renderer reads it (a real truncated, a value that isn't a `u32` no flags).
|
|
pub(crate) fn drawn_upright(doc: &Document, d: &Dict) -> bool {
|
|
let f = match d.get(b"F").map(|f| doc.resolve(f)).as_deref() {
|
|
Some(Object::Int(i)) => *i,
|
|
Some(Object::Real(r)) => *r as i64,
|
|
_ => 0,
|
|
};
|
|
u32::try_from(f).is_ok_and(|f| i64::from(f) & FLAG_NO_ROTATE != 0)
|
|
}
|
|
|
|
/// Whether any page has a Fill & Sign annotation that flatten would bake in (not hidden).
|
|
pub fn has_visible_fill_sign(doc: &Document) -> bool {
|
|
let Ok(pages) = page_refs(doc) else { return false };
|
|
for page in pages {
|
|
for entry in annots(doc, page) {
|
|
let obj = doc.resolve(&entry);
|
|
let Some(d) = obj.as_dict() else { continue };
|
|
let flags = annotation_flags(doc, d);
|
|
if flags & (FLAG_HIDDEN | FLAG_NO_VIEW) != 0 {
|
|
continue;
|
|
}
|
|
if is_fill_sign(doc, d) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
pub(crate) fn fill_sign_shape(shape: &Shape) -> bool {
|
|
match shape {
|
|
Shape::Typewriter { .. } | Shape::Mark { .. } | Shape::Signature { .. } | Shape::TypedSignature { .. } => true,
|
|
Shape::CustomStamp { name, image: true, .. } => name == "Signature" || name == "Initials",
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// The embedded image of a Fill & Sign image signature or initials (0-based target).
|
|
/// Other stamps have appearances that can't be represented by this image alone.
|
|
pub fn signature_image(doc: &Document, page: usize, index: usize) -> Result<Option<ObjRef>, AnnotError> {
|
|
let p = page_ref(doc, page)?;
|
|
let list = annots(doc, p);
|
|
let entry = list.get(index).ok_or(AnnotError::NoSuchAnnotation { page, index })?;
|
|
let obj = doc.resolve(entry);
|
|
let Some(d) = obj.as_dict() else { return Ok(None) };
|
|
Ok((d.name(b"Subtype") == Some(b"Stamp")
|
|
&& matches!(d.name(b"Name"), Some(b"PCCustomSignature" | b"PCCustomInitials"))
|
|
&& matches!(d.get(b"PCPictureImage"), Some(Object::Bool(true))))
|
|
.then(|| d.reference(b"PCPicture"))
|
|
.flatten())
|
|
}
|
|
|
|
/// The page `/Rotate` the picture of the image signature at `(page, index)` is drawn turned back
|
|
/// by, so it reads upright on a page shown that way: 0 for one added to an unturned page, or by
|
|
/// another app. The picture appears turned by the page's current rotation less this.
|
|
pub fn picture_rotation(doc: &Document, page: usize, index: usize) -> Result<i64, AnnotError> {
|
|
let p = page_ref(doc, page)?;
|
|
let list = annots(doc, p);
|
|
let entry = list.get(index).ok_or(AnnotError::NoSuchAnnotation { page, index })?;
|
|
Ok(doc.resolve(entry).as_dict().map_or(0, appearance::picture_turn))
|
|
}
|
|
|
|
/// Leave page `page` of (a copy of) a document showing only its annotation at `index`, without
|
|
/// the page's content or other annotations, so a render draws that annotation alone: to lay it
|
|
/// over the page while it is dragged. `false`, with `doc` unchanged, when the annotation has no
|
|
/// normal appearance (`/AP /N`) for that render to draw.
|
|
pub fn isolate_annotation(doc: &mut Document, page: usize, index: usize) -> Result<bool, AnnotError> {
|
|
let p = page_ref(doc, page)?;
|
|
let entry = annots(doc, p).get(index).cloned().ok_or(AnnotError::NoSuchAnnotation { page, index })?;
|
|
let has_normal = {
|
|
let obj = doc.resolve(&entry);
|
|
obj.as_dict().and_then(|d| d.get(b"AP")).is_some_and(|ap| doc.resolve(ap).as_dict().is_some_and(|ap| ap.contains(b"N")))
|
|
};
|
|
if !has_normal {
|
|
return Ok(false);
|
|
}
|
|
set_annots(doc, p, vec![entry])?;
|
|
doc.update_dict(p, |d| {
|
|
d.remove(b"Contents");
|
|
})?;
|
|
Ok(true)
|
|
}
|