pdfrum_doc/annot/mod.rs
1//! Annotations (ISO 32000-1 §12.5): the subtype table, the flag word, and
2//! the record every reader in this crate works from.
3
4pub(crate) mod appearance;
5mod list;
6pub(crate) mod quad;
7
8pub use appearance::{ApMode, annot_ap, has_appearance};
9pub use list::AnnotList;
10pub use quad::{quad_point_count, rect_from_quad_points};
11
12use kurbo::Rect;
13use pdfrum_object::{Array, Dict, Resolve};
14
15use crate::names;
16
17/// An annotation's `/Subtype`.
18///
19/// The variant order is API: it is the numbering the public annotation
20/// interface exposes, and the `--annot` dump's subtype names come from the
21/// same table. Three spellings differ from their variant names — `ThreeD` is
22/// `3D`, `XfaWidget` is `XFAWidget`, and `PolyLine` keeps its capital L.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum Subtype {
25 /// A `/Subtype` matching none of the known spellings, or none at all.
26 #[default]
27 Unknown,
28 /// A sticky note.
29 Text,
30 /// A hyperlink or destination jump.
31 Link,
32 /// Free text drawn directly on the page.
33 FreeText,
34 /// A straight line.
35 Line,
36 /// A rectangle.
37 Square,
38 /// An ellipse.
39 Circle,
40 /// A closed polygon.
41 Polygon,
42 /// An open polyline.
43 PolyLine,
44 /// Highlighted text.
45 Highlight,
46 /// Underlined text.
47 Underline,
48 /// Text with a wavy underline.
49 Squiggly,
50 /// Struck-out text.
51 StrikeOut,
52 /// A rubber stamp.
53 Stamp,
54 /// An editorial caret.
55 Caret,
56 /// Freehand ink strokes.
57 Ink,
58 /// A pop-up note attached to another annotation.
59 Popup,
60 /// An attached file.
61 FileAttachment,
62 /// A sound clip.
63 Sound,
64 /// A movie clip.
65 Movie,
66 /// A form field's on-page control.
67 Widget,
68 /// An embedded media screen.
69 Screen,
70 /// A printer's mark.
71 PrinterMark,
72 /// A trapping network.
73 TrapNet,
74 /// A watermark.
75 Watermark,
76 /// Three-dimensional artwork.
77 ThreeD,
78 /// Rich media.
79 RichMedia,
80 /// An XFA form control.
81 XfaWidget,
82 /// A redaction region.
83 Redact,
84}
85
86/// The spelling table, used by both directions and by the `--annot` dump.
87const SUBTYPES: [(Subtype, &[u8]); 28] = [
88 (Subtype::Text, b"Text"),
89 (Subtype::Link, b"Link"),
90 (Subtype::FreeText, b"FreeText"),
91 (Subtype::Line, b"Line"),
92 (Subtype::Square, b"Square"),
93 (Subtype::Circle, b"Circle"),
94 (Subtype::Polygon, b"Polygon"),
95 (Subtype::PolyLine, b"PolyLine"),
96 (Subtype::Highlight, b"Highlight"),
97 (Subtype::Underline, b"Underline"),
98 (Subtype::Squiggly, b"Squiggly"),
99 (Subtype::StrikeOut, b"StrikeOut"),
100 (Subtype::Stamp, b"Stamp"),
101 (Subtype::Caret, b"Caret"),
102 (Subtype::Ink, b"Ink"),
103 (Subtype::Popup, b"Popup"),
104 (Subtype::FileAttachment, b"FileAttachment"),
105 (Subtype::Sound, b"Sound"),
106 (Subtype::Movie, b"Movie"),
107 (Subtype::Widget, b"Widget"),
108 (Subtype::Screen, b"Screen"),
109 (Subtype::PrinterMark, b"PrinterMark"),
110 (Subtype::TrapNet, b"TrapNet"),
111 (Subtype::Watermark, b"Watermark"),
112 (Subtype::ThreeD, b"3D"),
113 (Subtype::RichMedia, b"RichMedia"),
114 (Subtype::XfaWidget, b"XFAWidget"),
115 (Subtype::Redact, b"Redact"),
116];
117
118impl Subtype {
119 /// Reads a `/Subtype` spelling. Anything unrecognized is
120 /// [`Subtype::Unknown`].
121 ///
122 /// ```
123 /// use pdfrum_doc::Subtype;
124 ///
125 /// assert_eq!(Subtype::from_bytes(b"Widget"), Subtype::Widget);
126 /// // Matching is exact: no case folding, and `3D` is spelled `3D`.
127 /// assert_eq!(Subtype::from_bytes(b"widget"), Subtype::Unknown);
128 /// assert_eq!(Subtype::from_bytes(b"3D"), Subtype::ThreeD);
129 /// ```
130 #[must_use]
131 pub fn from_bytes(bytes: &[u8]) -> Subtype {
132 SUBTYPES
133 .iter()
134 .find(|(_, spelling)| *spelling == bytes)
135 .map_or(Subtype::Unknown, |(subtype, _)| *subtype)
136 }
137
138 /// The spelling. [`Subtype::Unknown`] has none and answers empty.
139 ///
140 /// ```
141 /// use pdfrum_doc::Subtype;
142 ///
143 /// assert_eq!(Subtype::XfaWidget.as_bytes(), b"XFAWidget");
144 /// assert_eq!(Subtype::Unknown.as_bytes(), b"");
145 /// ```
146 #[must_use]
147 pub fn as_bytes(self) -> &'static [u8] {
148 SUBTYPES
149 .iter()
150 .find(|(subtype, _)| *subtype == self)
151 .map_or(&b""[..], |(_, spelling)| spelling)
152 }
153
154 /// The four text markup subtypes, whose drawing rectangle comes from
155 /// their quadrilaterals once an appearance has been generated.
156 ///
157 /// This is **not** the same set as [`Subtype::has_attachment_points`],
158 /// which the `--annot` dump uses and which also admits links.
159 ///
160 /// ```
161 /// use pdfrum_doc::Subtype;
162 ///
163 /// assert!(Subtype::Highlight.is_text_markup());
164 /// assert!(!Subtype::Link.is_text_markup());
165 /// ```
166 #[must_use]
167 pub fn is_text_markup(self) -> bool {
168 matches!(
169 self,
170 Subtype::Highlight | Subtype::Squiggly | Subtype::StrikeOut | Subtype::Underline
171 )
172 }
173
174 /// Whether the dump prints a quadpoint count for this subtype.
175 ///
176 /// Links are in the set, which is why every link in the corpus prints
177 /// `Number of quadpoints sets: 0`.
178 ///
179 /// ```
180 /// use pdfrum_doc::Subtype;
181 ///
182 /// assert!(Subtype::Link.has_attachment_points());
183 /// assert!(!Subtype::Square.has_attachment_points());
184 /// ```
185 #[must_use]
186 pub fn has_attachment_points(self) -> bool {
187 matches!(self, Subtype::Link) || self.is_text_markup()
188 }
189}
190
191/// An annotation's `/F` flag word (ISO 32000-1 table 165).
192///
193/// A hand-rolled newtype rather than a `bitflags` dependency, for two reasons
194/// the crate would get wrong: **unknown bits round-trip**
195/// ([`AnnotFlags::from_bits`] keeps the whole word), and the dump needs a
196/// **fixed name order** — [`AnnotFlags::names`] is that order, and the tenth
197/// defined bit, `LOCKED_CONTENTS`, has no printed name at all.
198///
199/// ```
200/// use pdfrum_doc::AnnotFlags;
201///
202/// let f = AnnotFlags::PRINT | AnnotFlags::NO_ZOOM;
203/// assert!(f.contains(AnnotFlags::PRINT));
204/// assert_eq!(f.names(), ["Print", "NoZoom"]);
205/// assert_eq!(AnnotFlags::from_bits(1 << 20).bits(), 1 << 20);
206/// ```
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
208pub struct AnnotFlags(i64);
209
210/// The nine flags the dump prints, in bit order. The tenth defined bit
211/// (`LockedContents`) has no printed name.
212const FLAG_NAMES: [(AnnotFlags, &str); 9] = [
213 (AnnotFlags::INVISIBLE, "Invisible"),
214 (AnnotFlags::HIDDEN, "Hidden"),
215 (AnnotFlags::PRINT, "Print"),
216 (AnnotFlags::NO_ZOOM, "NoZoom"),
217 (AnnotFlags::NO_ROTATE, "NoRotate"),
218 (AnnotFlags::NO_VIEW, "NoView"),
219 (AnnotFlags::READ_ONLY, "ReadOnly"),
220 (AnnotFlags::LOCKED, "Locked"),
221 (AnnotFlags::TOGGLE_NO_VIEW, "ToggleNoView"),
222];
223
224impl AnnotFlags {
225 /// Bit 1: an annotation whose subtype the viewer does not handle is not
226 /// drawn at all, rather than falling back to its appearance stream.
227 ///
228 /// Only widgets consult it here — `CPDFSDK_BAAnnot::IsVisible` adds it,
229 /// and Pass A never tests it.
230 ///
231 /// ```
232 /// use pdfrum_doc::AnnotFlags;
233 ///
234 /// assert_eq!(AnnotFlags::INVISIBLE.bits(), 1);
235 /// assert!(!AnnotFlags::INVISIBLE.is_visible());
236 /// ```
237 pub const INVISIBLE: Self = Self(1 << 0);
238 /// Bit 2: not displayed and not printed at all.
239 ///
240 /// ```
241 /// use pdfrum_doc::AnnotFlags;
242 ///
243 /// assert!(AnnotFlags::HIDDEN.is_hidden());
244 /// ```
245 pub const HIDDEN: Self = Self(1 << 1);
246 /// Bit 3: appears in printed output.
247 ///
248 /// ```
249 /// use pdfrum_doc::AnnotFlags;
250 ///
251 /// assert!(AnnotFlags::PRINT.prints());
252 /// ```
253 pub const PRINT: Self = Self(1 << 2);
254 /// Bit 4: the annotation keeps its size when the page is zoomed.
255 ///
256 /// ```
257 /// use pdfrum_doc::AnnotFlags;
258 ///
259 /// assert!(!AnnotFlags::NO_ZOOM.zooms());
260 /// ```
261 pub const NO_ZOOM: Self = Self(1 << 3);
262 /// Bit 5: the annotation ignores the page's rotation.
263 ///
264 /// ```
265 /// use pdfrum_doc::AnnotFlags;
266 ///
267 /// assert!(AnnotFlags::NO_ROTATE.no_rotate());
268 /// ```
269 pub const NO_ROTATE: Self = Self(1 << 4);
270 /// Bit 6: suppressed on screen, though it may still print.
271 ///
272 /// ```
273 /// use pdfrum_doc::AnnotFlags;
274 ///
275 /// assert!(AnnotFlags::NO_VIEW.no_view());
276 /// ```
277 pub const NO_VIEW: Self = Self(1 << 5);
278 /// Bit 7: the annotation does not interact with the user.
279 ///
280 /// ```
281 /// use pdfrum_doc::AnnotFlags;
282 ///
283 /// assert_eq!(AnnotFlags::READ_ONLY.names(), ["ReadOnly"]);
284 /// ```
285 pub const READ_ONLY: Self = Self(1 << 6);
286 /// Bit 8: the annotation may not be deleted, moved or resized.
287 ///
288 /// ```
289 /// use pdfrum_doc::AnnotFlags;
290 ///
291 /// assert_eq!(AnnotFlags::LOCKED.names(), ["Locked"]);
292 /// ```
293 pub const LOCKED: Self = Self(1 << 7);
294 /// Bit 9: [`AnnotFlags::NO_VIEW`]'s sense is inverted for the viewer's
295 /// own idea of "selected".
296 ///
297 /// ```
298 /// use pdfrum_doc::AnnotFlags;
299 ///
300 /// assert_eq!(AnnotFlags::TOGGLE_NO_VIEW.names(), ["ToggleNoView"]);
301 /// ```
302 pub const TOGGLE_NO_VIEW: Self = Self(1 << 8);
303 /// Bit 10: the annotation's contents may not be changed.
304 ///
305 /// The one defined bit the dump has no printed name for.
306 ///
307 /// ```
308 /// use pdfrum_doc::AnnotFlags;
309 ///
310 /// // The one defined bit with no printed name.
311 /// assert!(AnnotFlags::LOCKED_CONTENTS.names().is_empty());
312 /// ```
313 pub const LOCKED_CONTENTS: Self = Self(1 << 9);
314
315 /// No bit set.
316 ///
317 /// ```
318 /// use pdfrum_doc::AnnotFlags;
319 ///
320 /// assert!(AnnotFlags::NONE.is_empty());
321 /// assert!(AnnotFlags::NONE.names().is_empty());
322 /// ```
323 pub const NONE: Self = Self(0);
324
325 /// The raw `/F` word, including any bit this type does not name.
326 ///
327 /// ```
328 /// use pdfrum_doc::AnnotFlags;
329 ///
330 /// assert_eq!((AnnotFlags::PRINT | AnnotFlags::LOCKED).bits(), 4 | 128);
331 /// ```
332 #[must_use]
333 pub const fn bits(self) -> i64 {
334 self.0
335 }
336
337 /// The word as written in the file. **Unknown bits are retained**: a
338 /// reserved bit a damaged file sets is kept, not dropped.
339 ///
340 /// ```
341 /// use pdfrum_doc::AnnotFlags;
342 ///
343 /// let f = AnnotFlags::from_bits((1 << 20) | 4);
344 /// assert!(f.prints());
345 /// assert_eq!(f.bits(), (1 << 20) | 4);
346 /// ```
347 #[must_use]
348 pub const fn from_bits(bits: i64) -> Self {
349 Self(bits)
350 }
351
352 /// Whether every bit of `other` is set here.
353 ///
354 /// [`AnnotFlags::NONE`] is contained in everything, so `contains` is the
355 /// wrong question to ask about "no flags at all" — use
356 /// `== AnnotFlags::NONE`.
357 ///
358 /// ```
359 /// use pdfrum_doc::AnnotFlags;
360 ///
361 /// let f = AnnotFlags::PRINT | AnnotFlags::NO_VIEW;
362 /// assert!(f.contains(AnnotFlags::PRINT));
363 /// assert!(!f.contains(AnnotFlags::PRINT | AnnotFlags::LOCKED));
364 /// // Every flag word contains the empty one.
365 /// assert!(f.contains(AnnotFlags::NONE));
366 /// ```
367 #[must_use]
368 pub const fn contains(self, other: Self) -> bool {
369 self.0 & other.0 == other.0
370 }
371
372 /// Both sets of bits.
373 ///
374 /// ```
375 /// use pdfrum_doc::AnnotFlags;
376 ///
377 /// let f = AnnotFlags::PRINT.union(AnnotFlags::LOCKED);
378 /// assert_eq!(f.names(), ["Print", "Locked"]);
379 /// ```
380 #[must_use]
381 pub const fn union(self, other: Self) -> Self {
382 Self(self.0 | other.0)
383 }
384
385 /// A copy with `other`'s bits set. An alias for [`AnnotFlags::union`].
386 ///
387 /// ```
388 /// use pdfrum_doc::AnnotFlags;
389 ///
390 /// assert_eq!(AnnotFlags::NONE.with(AnnotFlags::LOCKED), AnnotFlags::LOCKED);
391 /// ```
392 #[must_use]
393 pub const fn with(self, other: Self) -> Self {
394 self.union(other)
395 }
396
397 /// The bits of `self` that are not in `other`.
398 ///
399 /// ```
400 /// use pdfrum_doc::AnnotFlags;
401 ///
402 /// let f = AnnotFlags::PRINT | AnnotFlags::NO_VIEW;
403 /// assert!(f.without(AnnotFlags::NO_VIEW).views());
404 /// ```
405 #[must_use]
406 pub const fn without(self, other: Self) -> Self {
407 Self(self.0 & !other.0)
408 }
409
410 /// Whether no bit at all is set.
411 ///
412 /// ```
413 /// use pdfrum_doc::AnnotFlags;
414 ///
415 /// assert!(AnnotFlags::NONE.is_empty());
416 /// assert!(!AnnotFlags::PRINT.is_empty());
417 /// ```
418 #[must_use]
419 pub const fn is_empty(self) -> bool {
420 self.0 == 0
421 }
422
423 /// The annotation is drawn even when its subtype has no handler.
424 ///
425 /// The positive reading of the spec's `Invisible` bit, and **the bit
426 /// alone**: whether an annotation is actually drawn also depends on
427 /// [`AnnotFlags::is_hidden`], [`AnnotFlags::no_view`] and the subtype.
428 ///
429 /// ```
430 /// use pdfrum_doc::AnnotFlags;
431 ///
432 /// assert!(AnnotFlags::PRINT.is_visible());
433 /// assert!(!AnnotFlags::INVISIBLE.is_visible());
434 /// ```
435 #[must_use]
436 #[doc(alias = "Invisible")]
437 pub const fn is_visible(self) -> bool {
438 !self.contains(Self::INVISIBLE)
439 }
440
441 /// The annotation is not displayed and not printed at all.
442 ///
443 /// ```
444 /// use pdfrum_doc::AnnotFlags;
445 ///
446 /// assert!(AnnotFlags::HIDDEN.is_hidden());
447 /// assert!(!AnnotFlags::PRINT.is_hidden());
448 /// ```
449 #[must_use]
450 pub const fn is_hidden(self) -> bool {
451 self.contains(Self::HIDDEN)
452 }
453
454 /// The annotation appears in printed output.
455 ///
456 /// ```
457 /// use pdfrum_doc::AnnotFlags;
458 ///
459 /// assert!(AnnotFlags::PRINT.prints());
460 /// assert!(!AnnotFlags::NONE.prints());
461 /// ```
462 #[must_use]
463 pub const fn prints(self) -> bool {
464 self.contains(Self::PRINT)
465 }
466
467 /// The annotation is shown on screen.
468 ///
469 /// The positive reading of the spec's `NoView` bit.
470 ///
471 /// ```
472 /// use pdfrum_doc::AnnotFlags;
473 ///
474 /// assert!(AnnotFlags::PRINT.views());
475 /// assert!(!AnnotFlags::NO_VIEW.views());
476 /// ```
477 #[must_use]
478 #[doc(alias = "NoView")]
479 pub const fn views(self) -> bool {
480 !self.contains(Self::NO_VIEW)
481 }
482
483 /// The annotation is suppressed on screen.
484 ///
485 /// ```
486 /// use pdfrum_doc::AnnotFlags;
487 ///
488 /// assert!(AnnotFlags::NO_VIEW.no_view());
489 /// assert!(!AnnotFlags::PRINT.no_view());
490 /// ```
491 #[must_use]
492 #[doc(alias = "NoView")]
493 pub const fn no_view(self) -> bool {
494 self.contains(Self::NO_VIEW)
495 }
496
497 /// The annotation scales with the page.
498 ///
499 /// The positive reading of the spec's `NoZoom` bit.
500 ///
501 /// ```
502 /// use pdfrum_doc::AnnotFlags;
503 ///
504 /// assert!(AnnotFlags::PRINT.zooms());
505 /// assert!(!AnnotFlags::NO_ZOOM.zooms());
506 /// ```
507 #[must_use]
508 #[doc(alias = "NoZoom")]
509 pub const fn zooms(self) -> bool {
510 !self.contains(Self::NO_ZOOM)
511 }
512
513 /// The annotation turns with the page.
514 ///
515 /// The positive reading of the spec's `NoRotate` bit.
516 ///
517 /// ```
518 /// use pdfrum_doc::AnnotFlags;
519 ///
520 /// assert!(AnnotFlags::PRINT.rotates());
521 /// assert!(!AnnotFlags::NO_ROTATE.rotates());
522 /// ```
523 #[must_use]
524 #[doc(alias = "NoRotate")]
525 pub const fn rotates(self) -> bool {
526 !self.contains(Self::NO_ROTATE)
527 }
528
529 /// The annotation ignores the page's rotation.
530 ///
531 /// ```
532 /// use pdfrum_doc::AnnotFlags;
533 ///
534 /// assert!(AnnotFlags::NO_ROTATE.no_rotate());
535 /// assert!(!AnnotFlags::PRINT.no_rotate());
536 /// ```
537 #[must_use]
538 #[doc(alias = "NoRotate")]
539 pub const fn no_rotate(self) -> bool {
540 self.contains(Self::NO_ROTATE)
541 }
542
543 /// The set flags' names, in bit order.
544 ///
545 /// **The order is the dump's** — `--annot` prints exactly this sequence,
546 /// and [`AnnotFlags::LOCKED_CONTENTS`] never appears because it has no
547 /// printed name.
548 ///
549 /// ```
550 /// use pdfrum_doc::AnnotFlags;
551 ///
552 /// let f = AnnotFlags::from_bits(4 | 8 | 16);
553 /// assert_eq!(f.names(), ["Print", "NoZoom", "NoRotate"]);
554 /// ```
555 #[must_use]
556 pub fn names(self) -> Vec<&'static str> {
557 FLAG_NAMES
558 .iter()
559 .filter(|(bit, _)| self.contains(*bit))
560 .map(|(_, name)| *name)
561 .collect()
562 }
563}
564
565impl std::ops::BitOr for AnnotFlags {
566 type Output = Self;
567
568 fn bitor(self, rhs: Self) -> Self {
569 self.union(rhs)
570 }
571}
572
573/// One annotation, as every reader in this crate sees it.
574#[derive(Debug, Clone, PartialEq)]
575pub struct Annotation {
576 /// The `/Subtype`.
577 pub subtype: Subtype,
578 /// `/Rect` **exactly as written**: not normalized, and not corrected for
579 /// an inverted ordering. The dump prints these numbers verbatim.
580 pub rect: Rect,
581 /// `/F`.
582 pub flags: AnnotFlags,
583 /// The source dictionary, for the long tail of per-subtype keys.
584 pub dict: Dict,
585 /// `/QuadPoints`, when present.
586 pub quad_points: Option<Array>,
587}
588
589impl Annotation {
590 /// Reads one annotation dictionary.
591 #[must_use]
592 pub fn read<R: Resolve>(dict: &Dict, r: &R) -> Annotation {
593 Annotation {
594 // `/Subtype` is read coercively here: a *string* subtype names an
595 // annotation just as a name does. The form-field reader uses a
596 // name-typed accessor for the same key, so a string `(Widget)` is
597 // an annotation widget but not a form widget.
598 subtype: Subtype::from_bytes(&dict.byte_string(names::SUBTYPE, r).unwrap_or_default()),
599 rect: dict.rect(names::RECT, r),
600 flags: AnnotFlags::from_bits(dict.int(names::F, r).unwrap_or(0)),
601 dict: dict.clone(),
602 quad_points: dict.array(names::QUAD_POINTS, r),
603 }
604 }
605
606 /// The rectangle an appearance is drawn into.
607 ///
608 /// A text markup annotation whose appearance *we* generated draws at its
609 /// quadrilaterals' bounding box rather than at its `/Rect`; everything
610 /// else draws at `/Rect`.
611 #[must_use]
612 pub fn rect_for_drawing(&self, has_generated_ap: bool) -> Rect {
613 if self.subtype.is_text_markup() && has_generated_ap {
614 quad::bounding_rect_from_quad_points(self.quad_points.as_ref())
615 } else {
616 self.rect
617 }
618 }
619}
620
621/// Whether a `/Subtype` value names a pop-up, read coercively.
622#[must_use]
623pub(crate) fn is_popup<R: Resolve>(dict: &Dict, r: &R) -> bool {
624 dict.byte_string(names::SUBTYPE, r).as_deref() == Some(b"Popup")
625}
626
627#[cfg(test)]
628mod tests {
629 use super::{AnnotFlags, Subtype};
630
631 #[test]
632 fn three_spellings_differ_from_their_variant_names() {
633 assert_eq!(Subtype::ThreeD.as_bytes(), b"3D");
634 assert_eq!(Subtype::XfaWidget.as_bytes(), b"XFAWidget");
635 assert_eq!(Subtype::PolyLine.as_bytes(), b"PolyLine");
636 assert_eq!(Subtype::from_bytes(b"3D"), Subtype::ThreeD);
637 assert_eq!(Subtype::from_bytes(b"XFAWidget"), Subtype::XfaWidget);
638 }
639
640 #[test]
641 fn matching_is_exact_and_case_sensitive() {
642 assert_eq!(Subtype::from_bytes(b"Widget"), Subtype::Widget);
643 assert_eq!(Subtype::from_bytes(b"widget"), Subtype::Unknown);
644 assert_eq!(Subtype::from_bytes(b""), Subtype::Unknown);
645 assert_eq!(Subtype::Unknown.as_bytes(), b"");
646 }
647
648 #[test]
649 fn every_spelling_round_trips() {
650 for subtype in [
651 Subtype::Text,
652 Subtype::Link,
653 Subtype::FreeText,
654 Subtype::Line,
655 Subtype::Square,
656 Subtype::Circle,
657 Subtype::Polygon,
658 Subtype::PolyLine,
659 Subtype::Highlight,
660 Subtype::Underline,
661 Subtype::Squiggly,
662 Subtype::StrikeOut,
663 Subtype::Stamp,
664 Subtype::Caret,
665 Subtype::Ink,
666 Subtype::Popup,
667 Subtype::FileAttachment,
668 Subtype::Sound,
669 Subtype::Movie,
670 Subtype::Widget,
671 Subtype::Screen,
672 Subtype::PrinterMark,
673 Subtype::TrapNet,
674 Subtype::Watermark,
675 Subtype::ThreeD,
676 Subtype::RichMedia,
677 Subtype::XfaWidget,
678 Subtype::Redact,
679 ] {
680 assert_eq!(Subtype::from_bytes(subtype.as_bytes()), subtype);
681 }
682 }
683
684 #[test]
685 fn text_markup_and_attachment_points_are_different_sets() {
686 assert!(!Subtype::Link.is_text_markup());
687 assert!(Subtype::Link.has_attachment_points());
688 assert!(Subtype::Highlight.is_text_markup());
689 assert!(Subtype::Highlight.has_attachment_points());
690 assert!(!Subtype::Square.has_attachment_points());
691 }
692
693 #[test]
694 fn flag_names_come_out_in_bit_order() {
695 assert_eq!(
696 AnnotFlags::from_bits(4 | 8 | 16).names(),
697 ["Print", "NoZoom", "NoRotate"]
698 );
699 assert!(AnnotFlags::NONE.names().is_empty());
700 // The tenth bit has no printed name.
701 assert!(AnnotFlags::LOCKED_CONTENTS.names().is_empty());
702 assert!(AnnotFlags::HIDDEN.is_hidden());
703 }
704
705 /// The dump's `Flags set:` line is byte-compared by the conformance
706 /// harness, so the name order is behaviour, not presentation. Every
707 /// printed name, in the one order they may appear in.
708 #[test]
709 fn the_dump_order_is_fixed() {
710 let all = AnnotFlags::from_bits(0x3FF);
711 assert_eq!(
712 all.names().join(", "),
713 "Invisible, Hidden, Print, NoZoom, NoRotate, NoView, ReadOnly, Locked, ToggleNoView"
714 );
715 // Setting the unnamed tenth bit, or a reserved one, changes nothing.
716 assert_eq!(
717 AnnotFlags::from_bits(0x3FF | (1 << 20)).names().join(", "),
718 "Invisible, Hidden, Print, NoZoom, NoRotate, NoView, ReadOnly, Locked, ToggleNoView"
719 );
720 }
721
722 #[test]
723 fn unknown_bits_round_trip() {
724 let f = AnnotFlags::from_bits((1 << 20) | AnnotFlags::PRINT.bits());
725 assert_eq!(f.bits(), (1 << 20) | 4);
726 assert!(f.contains(AnnotFlags::PRINT));
727 assert!(!f.contains(AnnotFlags::HIDDEN));
728 }
729
730 #[test]
731 fn set_algebra_and_the_positive_predicates() {
732 let f = AnnotFlags::PRINT | AnnotFlags::NO_VIEW | AnnotFlags::NO_ROTATE;
733 assert!(f.contains(AnnotFlags::PRINT | AnnotFlags::NO_VIEW));
734 assert!(f.contains(AnnotFlags::NONE));
735 assert!(!f.contains(AnnotFlags::PRINT | AnnotFlags::LOCKED));
736 assert!(!f.views() && f.no_view());
737 assert!(!f.rotates() && f.no_rotate());
738 assert!(f.zooms());
739 assert!(f.is_visible());
740 assert!(!AnnotFlags::INVISIBLE.is_visible());
741
742 let cleared = f.without(AnnotFlags::NO_VIEW);
743 assert!(cleared.views());
744 assert!(cleared.contains(AnnotFlags::PRINT));
745 assert!(AnnotFlags::NONE.is_empty());
746 assert_eq!(
747 AnnotFlags::NONE.with(AnnotFlags::LOCKED),
748 AnnotFlags::LOCKED
749 );
750 }
751}