mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 17:01:23 +00:00
A new `model_add` automation tool and `Edit::AddModel3d` embed a U3D or
PRC file as a 3D annotation (ISO 32000-2 13.6.2) in a chosen rectangle:
- The model goes into a Flate-compressed 3D stream (13.6.3) with /Type /3D
and /Subtype U3D or PRC, read from the file's header (`U3D` and a zero
byte, or `PRC`); anything else is refused before the document changes.
Models over 256 MiB (MAX_MODEL_BYTES) are refused. model_add checks the
file's size (from the file system) and its header before reading the
rest, so a large file that is not a model is never loaded; the path goes
through the read root, and `read_model` joins the helpers the
file-access test knows take a resolved path.
- /3DD references the stream, so the annotation has its own instance.
- /3DA states /A: click (XA, the standard's default and the tool's),
page_open (PO) or page_visible (PV).
- /AP /N is the poster readers print and show before activation: a chosen
image or PDF page fitted inside the box keeping its shape and centred (the
custom-stamp picture path: `mark_source`, `fit_picture`, and the stamp's
placement matrix now shared as `appearance::picture_place`), or, with no
picture, a light frame labelled "3D model". It is drawn in user space and
not counter-rotated, so on a page with /Rotate it turns with the page, as
the model does.
- /Contents carries the name (the file stem by default) as the alternate
description (12.5.2).
- A document older than the model needs gets /Version in its catalog
(7.7.2): 1.6 for U3D, 1.7 for PRC, as the optimizer does for JBIG2.
Adobe's extension level for PRC in 1.7 is not written.
- The 3D rectangle has its own MAX_MODEL_SIDE (14400 pt).
The edit is undoable ("Add 3D model"), needs the comments permission and
rebuilds only the comments view. A new parity entry, misc.3d-add, is
partial: no poster rendered from the model, no views (/VA, /3DV), no
replacing the poster, no UI command, no /Extensions. misc.3d-view is
unchanged. ROADMAP.md progress log line.
Adapted from pdfcer (MIT, same author), commit 1ca5ece, rewritten to this
repository's types and rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
84 lines
3.8 KiB
Rust
84 lines
3.8 KiB
Rust
//! pdfcraft-annot — comments (annotations), ISO 32000-2 §12.5, execution plan M5.1–M5.3.
|
||
//!
|
||
//! Builders for the comment types Acrobat's commenting tools create (sticky note, highlight,
|
||
//! underline, strikethrough, squiggly, rectangle, oval, line/arrow, freehand ink, text box),
|
||
//! appearance streams for them ([`appearance`]), and the edits a comment goes through: reply,
|
||
//! change its text, recolour, move, resize and delete.
|
||
//!
|
||
//! Addressing: a comment is `(page, index)`, its position in the page's `/Annots` array, which
|
||
//! is what `pdfcraft_render::Annotation::index` reports. Inline annotation dictionaries are
|
||
//! promoted to indirect objects when they are edited (replies need a reference to point at).
|
||
//!
|
||
//! Every edit mutates a `pdfcraft_cos::Document` (copy-on-write); callers snapshot it first
|
||
//! for undo. Keys we do not understand are left alone.
|
||
|
||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||
|
||
use std::collections::{HashMap, HashSet};
|
||
|
||
use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream};
|
||
|
||
mod create;
|
||
mod draw;
|
||
mod info;
|
||
mod lookup;
|
||
mod modify;
|
||
mod threed;
|
||
mod types;
|
||
|
||
#[cfg(test)]
|
||
use create::num_array;
|
||
pub use create::{add_annotation, callout_attach, orient_image_stamp};
|
||
pub use draw::{AppearanceCache, set_appearance, with_missing_appearances, with_missing_appearances_cached};
|
||
pub use info::{Props, Summary, is_comment_subtype, props, set_info, summaries};
|
||
#[cfg(test)]
|
||
use lookup::{ANNOTS_ENTRIES_COPIED, PageWalk, annot_dict, annot_ref};
|
||
use lookup::{annots, page_ref};
|
||
pub use lookup::{
|
||
has_visible_fill_sign, is_fill_sign, isolate_annotation, isolate_annotations, page_refs, picture_rotation, set_annots, signature_image,
|
||
};
|
||
pub(crate) use modify::n;
|
||
pub use modify::{
|
||
add_reply, add_text_replacement, deletable, delete_annotation, erase_ink, fillable, goes_with, lift_annotation, move_annotation, set_contents,
|
||
set_fill, set_font_size, set_locked, set_marked, set_points, set_rect, set_review_state, set_style, with_replies_and_popups,
|
||
};
|
||
pub use threed::{Activation as Activation3d, MAX_MODEL_BYTES, ModelFormat, NewModel3d, PosterPicture, add_3d_model, check_model};
|
||
pub use types::{
|
||
AttachIcon, FillMark, LineEnding, Markup, Meta, NOTE_SIZE, NewAnnotation, NoteIcon, OverlayFont, OverlayLook, ReviewState, Rgb, Shape,
|
||
StampGroup, StampKind, Style, StyleChange, TextReplacement, rect_quad,
|
||
};
|
||
|
||
pub mod appearance;
|
||
pub mod links;
|
||
|
||
#[cfg(test)]
|
||
mod tests;
|
||
|
||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||
pub enum AnnotError {
|
||
#[error("the document has no page tree")]
|
||
NoPageTree,
|
||
/// Holds the 0-based index; the message names the 1-based page a user counts (#1550).
|
||
#[error("page {} does not exist", .0.saturating_add(1))]
|
||
NoSuchPage(usize),
|
||
#[error("there is no comment {index} on page {page}")]
|
||
NoSuchAnnotation { page: usize, index: usize },
|
||
#[error("{0}")]
|
||
Invalid(String),
|
||
#[error("{0} comments can't be restyled yet (their appearance can't be regenerated)")]
|
||
Unsupported(String),
|
||
/// Text the standard fonts can't draw where `?` would be permanent or replace a good drawing:
|
||
/// a redaction's overlay text, or an edit that would redraw a comment another program drew
|
||
/// with a font that has those characters. The message names the character.
|
||
#[error("{0}")]
|
||
Undrawable(String),
|
||
/// A 3D model file that is neither U3D (starts with `U3D` and a zero byte) nor PRC (starts `PRC`).
|
||
#[error("not a 3D model: the file must be U3D (ECMA-363) or PRC (ISO 14739-1)")]
|
||
NotAModel,
|
||
/// A 3D model larger than `MAX_MODEL_BYTES`.
|
||
#[error("the 3D model is {size} bytes; the largest accepted is {max}")]
|
||
ModelTooLarge { size: usize, max: usize },
|
||
#[error("{0}")]
|
||
Cos(#[from] pdfcraft_cos::CosError),
|
||
}
|