Skip to main content

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}