pub(crate) mod appearance;
mod list;
pub(crate) mod quad;
pub use appearance::{ApMode, annot_ap, has_appearance};
pub use list::AnnotList;
pub use quad::{quad_point_count, rect_from_quad_points};
use kurbo::Rect;
use pdfrum_object::{Array, Dict, Resolve};
use crate::names;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Subtype {
#[default]
Unknown,
Text,
Link,
FreeText,
Line,
Square,
Circle,
Polygon,
PolyLine,
Highlight,
Underline,
Squiggly,
StrikeOut,
Stamp,
Caret,
Ink,
Popup,
FileAttachment,
Sound,
Movie,
Widget,
Screen,
PrinterMark,
TrapNet,
Watermark,
ThreeD,
RichMedia,
XfaWidget,
Redact,
}
const SUBTYPES: [(Subtype, &[u8]); 28] = [
(Subtype::Text, b"Text"),
(Subtype::Link, b"Link"),
(Subtype::FreeText, b"FreeText"),
(Subtype::Line, b"Line"),
(Subtype::Square, b"Square"),
(Subtype::Circle, b"Circle"),
(Subtype::Polygon, b"Polygon"),
(Subtype::PolyLine, b"PolyLine"),
(Subtype::Highlight, b"Highlight"),
(Subtype::Underline, b"Underline"),
(Subtype::Squiggly, b"Squiggly"),
(Subtype::StrikeOut, b"StrikeOut"),
(Subtype::Stamp, b"Stamp"),
(Subtype::Caret, b"Caret"),
(Subtype::Ink, b"Ink"),
(Subtype::Popup, b"Popup"),
(Subtype::FileAttachment, b"FileAttachment"),
(Subtype::Sound, b"Sound"),
(Subtype::Movie, b"Movie"),
(Subtype::Widget, b"Widget"),
(Subtype::Screen, b"Screen"),
(Subtype::PrinterMark, b"PrinterMark"),
(Subtype::TrapNet, b"TrapNet"),
(Subtype::Watermark, b"Watermark"),
(Subtype::ThreeD, b"3D"),
(Subtype::RichMedia, b"RichMedia"),
(Subtype::XfaWidget, b"XFAWidget"),
(Subtype::Redact, b"Redact"),
];
impl Subtype {
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Subtype {
SUBTYPES
.iter()
.find(|(_, spelling)| *spelling == bytes)
.map_or(Subtype::Unknown, |(subtype, _)| *subtype)
}
#[must_use]
pub fn as_bytes(self) -> &'static [u8] {
SUBTYPES
.iter()
.find(|(subtype, _)| *subtype == self)
.map_or(&b""[..], |(_, spelling)| spelling)
}
#[must_use]
pub fn is_text_markup(self) -> bool {
matches!(
self,
Subtype::Highlight | Subtype::Squiggly | Subtype::StrikeOut | Subtype::Underline
)
}
#[must_use]
pub fn has_attachment_points(self) -> bool {
matches!(self, Subtype::Link) || self.is_text_markup()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct AnnotFlags(i64);
const FLAG_NAMES: [(AnnotFlags, &str); 9] = [
(AnnotFlags::INVISIBLE, "Invisible"),
(AnnotFlags::HIDDEN, "Hidden"),
(AnnotFlags::PRINT, "Print"),
(AnnotFlags::NO_ZOOM, "NoZoom"),
(AnnotFlags::NO_ROTATE, "NoRotate"),
(AnnotFlags::NO_VIEW, "NoView"),
(AnnotFlags::READ_ONLY, "ReadOnly"),
(AnnotFlags::LOCKED, "Locked"),
(AnnotFlags::TOGGLE_NO_VIEW, "ToggleNoView"),
];
impl AnnotFlags {
pub const INVISIBLE: Self = Self(1 << 0);
pub const HIDDEN: Self = Self(1 << 1);
pub const PRINT: Self = Self(1 << 2);
pub const NO_ZOOM: Self = Self(1 << 3);
pub const NO_ROTATE: Self = Self(1 << 4);
pub const NO_VIEW: Self = Self(1 << 5);
pub const READ_ONLY: Self = Self(1 << 6);
pub const LOCKED: Self = Self(1 << 7);
pub const TOGGLE_NO_VIEW: Self = Self(1 << 8);
pub const LOCKED_CONTENTS: Self = Self(1 << 9);
pub const NONE: Self = Self(0);
#[must_use]
pub const fn bits(self) -> i64 {
self.0
}
#[must_use]
pub const fn from_bits(bits: i64) -> Self {
Self(bits)
}
#[must_use]
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
#[must_use]
pub const fn with(self, other: Self) -> Self {
self.union(other)
}
#[must_use]
pub const fn without(self, other: Self) -> Self {
Self(self.0 & !other.0)
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
#[must_use]
#[doc(alias = "Invisible")]
pub const fn is_visible(self) -> bool {
!self.contains(Self::INVISIBLE)
}
#[must_use]
pub const fn is_hidden(self) -> bool {
self.contains(Self::HIDDEN)
}
#[must_use]
pub const fn prints(self) -> bool {
self.contains(Self::PRINT)
}
#[must_use]
#[doc(alias = "NoView")]
pub const fn views(self) -> bool {
!self.contains(Self::NO_VIEW)
}
#[must_use]
#[doc(alias = "NoView")]
pub const fn no_view(self) -> bool {
self.contains(Self::NO_VIEW)
}
#[must_use]
#[doc(alias = "NoZoom")]
pub const fn zooms(self) -> bool {
!self.contains(Self::NO_ZOOM)
}
#[must_use]
#[doc(alias = "NoRotate")]
pub const fn rotates(self) -> bool {
!self.contains(Self::NO_ROTATE)
}
#[must_use]
#[doc(alias = "NoRotate")]
pub const fn no_rotate(self) -> bool {
self.contains(Self::NO_ROTATE)
}
#[must_use]
pub fn names(self) -> Vec<&'static str> {
FLAG_NAMES
.iter()
.filter(|(bit, _)| self.contains(*bit))
.map(|(_, name)| *name)
.collect()
}
}
impl std::ops::BitOr for AnnotFlags {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
self.union(rhs)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Annotation {
pub subtype: Subtype,
pub rect: Rect,
pub flags: AnnotFlags,
pub dict: Dict,
pub quad_points: Option<Array>,
}
impl Annotation {
#[must_use]
pub fn read<R: Resolve>(dict: &Dict, r: &R) -> Annotation {
Annotation {
subtype: Subtype::from_bytes(&dict.byte_string(names::SUBTYPE, r).unwrap_or_default()),
rect: dict.rect(names::RECT, r),
flags: AnnotFlags::from_bits(dict.int(names::F, r).unwrap_or(0)),
dict: dict.clone(),
quad_points: dict.array(names::QUAD_POINTS, r),
}
}
#[must_use]
pub fn rect_for_drawing(&self, has_generated_ap: bool) -> Rect {
if self.subtype.is_text_markup() && has_generated_ap {
quad::bounding_rect_from_quad_points(self.quad_points.as_ref())
} else {
self.rect
}
}
}
#[must_use]
pub(crate) fn is_popup<R: Resolve>(dict: &Dict, r: &R) -> bool {
dict.byte_string(names::SUBTYPE, r).as_deref() == Some(b"Popup")
}
#[cfg(test)]
mod tests {
use super::{AnnotFlags, Subtype};
#[test]
fn three_spellings_differ_from_their_variant_names() {
assert_eq!(Subtype::ThreeD.as_bytes(), b"3D");
assert_eq!(Subtype::XfaWidget.as_bytes(), b"XFAWidget");
assert_eq!(Subtype::PolyLine.as_bytes(), b"PolyLine");
assert_eq!(Subtype::from_bytes(b"3D"), Subtype::ThreeD);
assert_eq!(Subtype::from_bytes(b"XFAWidget"), Subtype::XfaWidget);
}
#[test]
fn matching_is_exact_and_case_sensitive() {
assert_eq!(Subtype::from_bytes(b"Widget"), Subtype::Widget);
assert_eq!(Subtype::from_bytes(b"widget"), Subtype::Unknown);
assert_eq!(Subtype::from_bytes(b""), Subtype::Unknown);
assert_eq!(Subtype::Unknown.as_bytes(), b"");
}
#[test]
fn every_spelling_round_trips() {
for subtype in [
Subtype::Text,
Subtype::Link,
Subtype::FreeText,
Subtype::Line,
Subtype::Square,
Subtype::Circle,
Subtype::Polygon,
Subtype::PolyLine,
Subtype::Highlight,
Subtype::Underline,
Subtype::Squiggly,
Subtype::StrikeOut,
Subtype::Stamp,
Subtype::Caret,
Subtype::Ink,
Subtype::Popup,
Subtype::FileAttachment,
Subtype::Sound,
Subtype::Movie,
Subtype::Widget,
Subtype::Screen,
Subtype::PrinterMark,
Subtype::TrapNet,
Subtype::Watermark,
Subtype::ThreeD,
Subtype::RichMedia,
Subtype::XfaWidget,
Subtype::Redact,
] {
assert_eq!(Subtype::from_bytes(subtype.as_bytes()), subtype);
}
}
#[test]
fn text_markup_and_attachment_points_are_different_sets() {
assert!(!Subtype::Link.is_text_markup());
assert!(Subtype::Link.has_attachment_points());
assert!(Subtype::Highlight.is_text_markup());
assert!(Subtype::Highlight.has_attachment_points());
assert!(!Subtype::Square.has_attachment_points());
}
#[test]
fn flag_names_come_out_in_bit_order() {
assert_eq!(
AnnotFlags::from_bits(4 | 8 | 16).names(),
["Print", "NoZoom", "NoRotate"]
);
assert!(AnnotFlags::NONE.names().is_empty());
assert!(AnnotFlags::LOCKED_CONTENTS.names().is_empty());
assert!(AnnotFlags::HIDDEN.is_hidden());
}
#[test]
fn the_dump_order_is_fixed() {
let all = AnnotFlags::from_bits(0x3FF);
assert_eq!(
all.names().join(", "),
"Invisible, Hidden, Print, NoZoom, NoRotate, NoView, ReadOnly, Locked, ToggleNoView"
);
assert_eq!(
AnnotFlags::from_bits(0x3FF | (1 << 20)).names().join(", "),
"Invisible, Hidden, Print, NoZoom, NoRotate, NoView, ReadOnly, Locked, ToggleNoView"
);
}
#[test]
fn unknown_bits_round_trip() {
let f = AnnotFlags::from_bits((1 << 20) | AnnotFlags::PRINT.bits());
assert_eq!(f.bits(), (1 << 20) | 4);
assert!(f.contains(AnnotFlags::PRINT));
assert!(!f.contains(AnnotFlags::HIDDEN));
}
#[test]
fn set_algebra_and_the_positive_predicates() {
let f = AnnotFlags::PRINT | AnnotFlags::NO_VIEW | AnnotFlags::NO_ROTATE;
assert!(f.contains(AnnotFlags::PRINT | AnnotFlags::NO_VIEW));
assert!(f.contains(AnnotFlags::NONE));
assert!(!f.contains(AnnotFlags::PRINT | AnnotFlags::LOCKED));
assert!(!f.views() && f.no_view());
assert!(!f.rotates() && f.no_rotate());
assert!(f.zooms());
assert!(f.is_visible());
assert!(!AnnotFlags::INVISIBLE.is_visible());
let cleared = f.without(AnnotFlags::NO_VIEW);
assert!(cleared.views());
assert!(cleared.contains(AnnotFlags::PRINT));
assert!(AnnotFlags::NONE.is_empty());
assert_eq!(
AnnotFlags::NONE.with(AnnotFlags::LOCKED),
AnnotFlags::LOCKED
);
}
}