Three defects found by auditing the ADR merge criteria against the code rather than against memory. 1. The scheduled fuzz job ran five hardcoded targets. Four had been added since and were never fuzzed: parse_revision_chain, decrypt, eval_function and parse_colorspace. eval_function is the sharpest of those - it executes PostScript taken verbatim from an untrusted file. The list now comes from `cargo fuzz list` and the job fails rather than passing vacuously if it comes back empty. `cargo fuzz list` reads the manifest, so a target file added without its [[bin]] entry would still be skipped silently. The engine job, which runs on every push, now checks the two agree. Both directions of that guard were exercised before committing. 2. ADR 0004 rule 3 promises an annotation whose appearance cannot be generated "keeps its original /AP and is reported as skipped". The keeping worked - to_dict clones the source dictionary - but nothing reported it: SaveReport only tracked skipped appearances for form fields. A caller who moved a stamp was never told its artwork still showed the old position. Adds SaveReport::annotation_appearances_skipped and appearance_is_generated, which enumerates the out-of-scope types explicitly so a new AnnotationType fails to compile until classified. 3. SetContents and SetFlags had no round-trip test. Both were implemented and unit-tested against the in-memory model, but neither was ever reparsed from written bytes - the assertion ADR 0004 calls central. New fixture annotations/stamp.pdf carries real /AP artwork for a Stamp (undrawable: must be preserved and reported) beside a Square (drawable: must not be reported), so the reporting cannot pass by reporting everything. The stamp test was mutation-checked: it fails when the reporting line is removed. ADR 0003 and 0004 merge criteria are now ticked. 0003's were genuine paperwork - every box traced to an existing named test. 0004's were not, and its ADR now records what was missing rather than implying it always worked. TEST_TARGET=pdf 447 -> 451 passing. rustfmt and clippy -D warnings clean.
553 lines
19 KiB
Rust
553 lines
19 KiB
Rust
//! Annotation editing acceptance tests.
|
|
//!
|
|
//! The merge criteria from `REVIEWS/adr/0004-pdf-annotation-editing.md`.
|
|
//! As with form saving, the central assertion reparses **from the written
|
|
//! bytes**: an edit that only survives in memory has not been saved.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use nigig_pdf_cos::{ObjRef, PdfDict, PdfObj};
|
|
use nigig_pdf_document::annotation_edit::{
|
|
AnnotationColor, AnnotationEdit, AnnotationEditor, AnnotationError, PageAnnotations,
|
|
};
|
|
use nigig_pdf_document::annotations::{AnnotationFlags, AnnotationType};
|
|
use nigig_pdf_document::save::save_annotation_edits;
|
|
use nigig_pdf_document::PdfDocument;
|
|
|
|
const PAGE: usize = 0;
|
|
|
|
fn corpus(relative: &str) -> Vec<u8> {
|
|
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../tests/corpus")
|
|
.join(relative);
|
|
std::fs::read(&path).unwrap_or_else(|e| panic!("missing {}: {e}", path.display()))
|
|
}
|
|
|
|
/// Everything a save needs: the page's annotations, its reference and dict.
|
|
fn open(data: &[u8]) -> (PdfDict, PageAnnotations, ObjRef, PdfDict) {
|
|
let mut doc = PdfDocument::parse(data).expect("parses");
|
|
let annots = doc.page_annotations(PAGE).expect("annotations");
|
|
let trailer = doc.trailer().clone();
|
|
let page_ref = doc.page_object_ref(PAGE).expect("page ref");
|
|
let page_dict = doc
|
|
.resolve_ref(page_ref)
|
|
.expect("page object")
|
|
.as_dict()
|
|
.cloned()
|
|
.expect("page is a dict");
|
|
(
|
|
trailer,
|
|
PageAnnotations::new(PAGE, annots),
|
|
page_ref,
|
|
page_dict,
|
|
)
|
|
}
|
|
|
|
/// The reference of the first annotation carrying a URI action.
|
|
fn link_ref(page: &PageAnnotations) -> ObjRef {
|
|
page.live()
|
|
.find(|a| a.annotation.action().is_some())
|
|
.and_then(|a| a.annotation.obj_ref)
|
|
.expect("the fixture has a link")
|
|
}
|
|
|
|
#[test]
|
|
fn corpus_annotations_carry_their_object_reference() {
|
|
// Without this an edit has nothing stable to address.
|
|
let (_, page, _, _) = open(&corpus("annotations/links.pdf"));
|
|
assert!(!page.is_empty());
|
|
for item in page.live() {
|
|
assert!(
|
|
item.annotation.obj_ref.is_some(),
|
|
"every corpus annotation is an indirect object"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The core round trip: move an annotation, save, reparse, see it moved.
|
|
#[test]
|
|
fn a_moved_annotation_survives_a_save_and_reparse() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (trailer, mut page, page_ref, page_dict) = open(&data);
|
|
let target = link_ref(&page);
|
|
let before = page.get(target).expect("annotation").annotation.rect;
|
|
|
|
AnnotationEditor::new(&mut page)
|
|
.move_to(target, 42.0, 84.0)
|
|
.expect("move");
|
|
|
|
let (out, report) =
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).expect("save");
|
|
assert!(report.annotations_written.contains(&target.num));
|
|
assert!(out.starts_with(&data), "an incremental save appends");
|
|
|
|
let mut reloaded = PdfDocument::parse(&out).expect("saved file reparses");
|
|
let annots = reloaded.page_annotations(PAGE).expect("annotations");
|
|
let moved = annots
|
|
.iter()
|
|
.find(|a| a.obj_ref == Some(target))
|
|
.expect("annotation survives");
|
|
|
|
assert_ne!(moved.rect, before, "the rectangle must have changed");
|
|
assert_eq!(moved.rect[0], 42.0);
|
|
assert_eq!(moved.rect[1], 84.0);
|
|
// Size is preserved by a move.
|
|
assert_eq!(moved.width(), (before[2] - before[0]).abs());
|
|
assert_eq!(moved.height(), (before[3] - before[1]).abs());
|
|
}
|
|
|
|
#[test]
|
|
fn a_resize_survives_a_save_and_reparse() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (trailer, mut page, page_ref, page_dict) = open(&data);
|
|
let target = link_ref(&page);
|
|
|
|
AnnotationEditor::new(&mut page)
|
|
.resize(target, [10.0, 20.0, 110.0, 70.0])
|
|
.expect("resize");
|
|
let (out, _) =
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).expect("save");
|
|
|
|
let mut reloaded = PdfDocument::parse(&out).expect("reparses");
|
|
let annots = reloaded.page_annotations(PAGE).expect("annotations");
|
|
let resized = annots
|
|
.iter()
|
|
.find(|a| a.obj_ref == Some(target))
|
|
.expect("annotation");
|
|
assert_eq!(resized.rect, [10.0, 20.0, 110.0, 70.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn restyling_survives_a_save_and_reparse() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (trailer, mut page, page_ref, page_dict) = open(&data);
|
|
let target = link_ref(&page);
|
|
|
|
{
|
|
let mut editor = AnnotationEditor::new(&mut page);
|
|
editor
|
|
.apply(
|
|
target,
|
|
AnnotationEdit::SetColor(AnnotationColor::new(1.0, 0.0, 0.0)),
|
|
)
|
|
.expect("colour");
|
|
editor
|
|
.apply(target, AnnotationEdit::SetBorderWidth(2.5))
|
|
.expect("border");
|
|
editor
|
|
.apply(target, AnnotationEdit::SetContents("edited note".into()))
|
|
.expect("contents");
|
|
}
|
|
|
|
let (out, _) =
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).expect("save");
|
|
let mut reloaded = PdfDocument::parse(&out).expect("reparses");
|
|
let annots = reloaded.page_annotations(PAGE).expect("annotations");
|
|
let styled = annots
|
|
.iter()
|
|
.find(|a| a.obj_ref == Some(target))
|
|
.expect("annotation");
|
|
|
|
let colour = styled.raw_dict.get_array("C").expect("/C survived");
|
|
assert_eq!(colour[0].as_f64(), Some(1.0));
|
|
assert_eq!(
|
|
styled
|
|
.raw_dict
|
|
.get_dict("BS")
|
|
.and_then(|bs| bs.get_f64("W")),
|
|
Some(2.5)
|
|
);
|
|
assert_eq!(
|
|
styled.raw_dict.get_str("Contents"),
|
|
Some(&b"edited note"[..])
|
|
);
|
|
}
|
|
|
|
/// ADR 0004 rule 2: a deleted annotation must leave the page's `/Annots`,
|
|
/// not just the in-memory model.
|
|
#[test]
|
|
fn a_deleted_annotation_is_gone_from_the_reparsed_page() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (trailer, mut page, page_ref, page_dict) = open(&data);
|
|
let before = page.len();
|
|
let target = link_ref(&page);
|
|
|
|
AnnotationEditor::new(&mut page)
|
|
.delete(target)
|
|
.expect("delete");
|
|
|
|
let (out, report) =
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).expect("save");
|
|
assert!(report.annotations_deleted.contains(&target.num));
|
|
assert!(
|
|
report.pages_written.contains(&page_ref.num),
|
|
"the page must be rewritten so /Annots loses the reference"
|
|
);
|
|
|
|
let mut reloaded = PdfDocument::parse(&out).expect("reparses");
|
|
let annots = reloaded.page_annotations(PAGE).expect("annotations");
|
|
assert_eq!(annots.len(), before - 1);
|
|
assert!(
|
|
!annots.iter().any(|a| a.obj_ref == Some(target)),
|
|
"the deleted annotation must not come back"
|
|
);
|
|
|
|
// And no dangling reference is left behind in the array.
|
|
let page_obj = reloaded.resolve_ref(page_ref).expect("page");
|
|
let refs: Vec<ObjRef> = page_obj
|
|
.as_dict()
|
|
.and_then(|d| d.get_array("Annots"))
|
|
.map(|arr| arr.iter().filter_map(|o| o.as_ref().copied()).collect())
|
|
.unwrap_or_default();
|
|
assert!(!refs.contains(&target), "/Annots still references it");
|
|
}
|
|
|
|
#[test]
|
|
fn the_other_annotations_survive_a_deletion() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (trailer, mut page, page_ref, page_dict) = open(&data);
|
|
let target = link_ref(&page);
|
|
let survivors: Vec<ObjRef> = page
|
|
.live()
|
|
.filter_map(|a| a.annotation.obj_ref)
|
|
.filter(|r| *r != target)
|
|
.collect();
|
|
assert!(!survivors.is_empty(), "the fixture has several annotations");
|
|
|
|
AnnotationEditor::new(&mut page)
|
|
.delete(target)
|
|
.expect("delete");
|
|
let (out, _) =
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).expect("save");
|
|
|
|
let mut reloaded = PdfDocument::parse(&out).expect("reparses");
|
|
let annots = reloaded.page_annotations(PAGE).expect("annotations");
|
|
for survivor in survivors {
|
|
assert!(
|
|
annots.iter().any(|a| a.obj_ref == Some(survivor)),
|
|
"annotation {survivor} must survive an unrelated deletion"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn saving_with_no_edits_returns_the_document_unchanged() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (trailer, page, page_ref, page_dict) = open(&data);
|
|
let (out, report) =
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).expect("save");
|
|
assert!(report.unchanged);
|
|
assert!(!report.wrote_anything());
|
|
assert_eq!(out, data);
|
|
}
|
|
|
|
#[test]
|
|
fn a_hidden_annotation_can_be_unhidden_and_saved() {
|
|
let data = corpus("annotations/hidden.pdf");
|
|
let (trailer, mut page, page_ref, page_dict) = open(&data);
|
|
let target = page
|
|
.live()
|
|
.filter_map(|a| a.annotation.obj_ref)
|
|
.next()
|
|
.expect("a hidden annotation");
|
|
assert!(!page
|
|
.get(target)
|
|
.expect("annotation")
|
|
.annotation
|
|
.is_visible());
|
|
|
|
AnnotationEditor::new(&mut page)
|
|
.apply(
|
|
target,
|
|
AnnotationEdit::SetFlags(AnnotationFlags {
|
|
print: true,
|
|
..Default::default()
|
|
}),
|
|
)
|
|
.expect("unhide");
|
|
|
|
let (out, _) =
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).expect("save");
|
|
let mut reloaded = PdfDocument::parse(&out).expect("reparses");
|
|
let annots = reloaded.page_annotations(PAGE).expect("annotations");
|
|
let shown = annots
|
|
.iter()
|
|
.find(|a| a.obj_ref == Some(target))
|
|
.expect("annotation");
|
|
assert!(shown.is_visible(), "the annotation should now be visible");
|
|
}
|
|
|
|
#[test]
|
|
fn several_edits_to_one_annotation_produce_one_object() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (trailer, mut page, page_ref, page_dict) = open(&data);
|
|
let target = link_ref(&page);
|
|
{
|
|
let mut editor = AnnotationEditor::new(&mut page);
|
|
editor.move_to(target, 1.0, 2.0).expect("move");
|
|
editor
|
|
.resize(target, [1.0, 2.0, 51.0, 22.0])
|
|
.expect("resize");
|
|
editor
|
|
.apply(target, AnnotationEdit::SetOpacity(0.5))
|
|
.expect("opacity");
|
|
}
|
|
|
|
let (_, report) =
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).expect("save");
|
|
assert_eq!(
|
|
report.annotations_written,
|
|
vec![target.num],
|
|
"one annotation edited means one object written"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn two_successive_annotation_saves_chain_correctly() {
|
|
let mut data = corpus("annotations/links.pdf");
|
|
for (x, y) in [(10.0, 10.0), (20.0, 20.0)] {
|
|
let (trailer, mut page, page_ref, page_dict) = open(&data);
|
|
let target = link_ref(&page);
|
|
AnnotationEditor::new(&mut page)
|
|
.move_to(target, x, y)
|
|
.expect("move");
|
|
let (out, _) =
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).expect("save");
|
|
assert!(out.starts_with(&data), "each revision appends");
|
|
data = out;
|
|
}
|
|
|
|
let mut reloaded = PdfDocument::parse(&data).expect("reparses");
|
|
let annots = reloaded.page_annotations(PAGE).expect("annotations");
|
|
let moved = annots
|
|
.iter()
|
|
.find(|a| a.action().is_some())
|
|
.expect("the link");
|
|
assert_eq!(
|
|
(moved.rect[0], moved.rect[1]),
|
|
(20.0, 20.0),
|
|
"the newest revision must win"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn saving_an_encrypted_document_is_refused() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (mut trailer, mut page, page_ref, page_dict) = open(&data);
|
|
trailer.set("Encrypt", PdfObj::Ref(ObjRef { num: 99, gen: 0 }));
|
|
let target = link_ref(&page);
|
|
AnnotationEditor::new(&mut page)
|
|
.move_to(target, 1.0, 1.0)
|
|
.expect("move");
|
|
|
|
assert!(
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).is_err(),
|
|
"an encrypted document must not be written"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_refused_edit_leaves_the_document_savable_and_unchanged() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (trailer, mut page, page_ref, page_dict) = open(&data);
|
|
let target = link_ref(&page);
|
|
|
|
let err = AnnotationEditor::new(&mut page)
|
|
.resize(target, [0.0, 0.0, 0.0, 0.0])
|
|
.expect_err("degenerate");
|
|
assert!(matches!(err, AnnotationError::DegenerateRect { .. }));
|
|
|
|
let (out, report) =
|
|
save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict).expect("save");
|
|
assert!(report.unchanged);
|
|
assert_eq!(out, data);
|
|
}
|
|
|
|
/// Editing and saving must not become a new way to crash on hostile input.
|
|
#[test]
|
|
fn malformed_fixtures_never_panic_an_annotation_save() {
|
|
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../tests/corpus/malformed");
|
|
for entry in std::fs::read_dir(&dir).expect("malformed corpus").flatten() {
|
|
let path = entry.path();
|
|
if path.extension().is_none_or(|e| e != "pdf") {
|
|
continue;
|
|
}
|
|
let Ok(data) = std::fs::read(&path) else {
|
|
continue;
|
|
};
|
|
let Ok(mut doc) = PdfDocument::parse(&data) else {
|
|
continue;
|
|
};
|
|
let Ok(annots) = doc.page_annotations(0) else {
|
|
continue;
|
|
};
|
|
let trailer = doc.trailer().clone();
|
|
let Some(page_ref) = doc.page_object_ref(0) else {
|
|
continue;
|
|
};
|
|
let page_dict = doc
|
|
.resolve_ref(page_ref)
|
|
.ok()
|
|
.and_then(|o| o.as_dict().cloned())
|
|
.unwrap_or_default();
|
|
|
|
let mut page = PageAnnotations::new(0, annots);
|
|
let targets: Vec<ObjRef> = page.live().filter_map(|a| a.annotation.obj_ref).collect();
|
|
for target in targets {
|
|
let mut editor = AnnotationEditor::new(&mut page);
|
|
let _ = editor.move_to(target, 5.0, 5.0);
|
|
let _ = editor.delete(target);
|
|
}
|
|
// Whatever happened, saving returns.
|
|
let _ = save_annotation_edits(&data, &trailer, &page, page_ref, &page_dict);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// ADR 0004 criteria that the original commit left unverified: the
|
|
// contents/flags edits, and the "kept its /AP and said so" reporting.
|
|
// ---------------------------------------------------------------------
|
|
|
|
/// The `/AP` of a type this crate cannot draw must survive an edit *and*
|
|
/// be reported, so a caller knows the artwork still shows the old state.
|
|
#[test]
|
|
fn a_stamp_keeps_its_appearance_and_is_reported_as_skipped() {
|
|
let data = corpus("annotations/stamp.pdf");
|
|
let (trailer, mut annots, page_ref, page_dict) = open(&data);
|
|
|
|
let stamp = annots
|
|
.live()
|
|
.find(|a| matches!(a.annotation.annot_type, AnnotationType::Stamp { .. }))
|
|
.and_then(|a| a.annotation.obj_ref)
|
|
.expect("the fixture has a stamp");
|
|
|
|
// Capture the artwork reference before the edit.
|
|
let ap_before = annots
|
|
.get(stamp)
|
|
.and_then(|a| a.annotation.raw_dict.get("AP").cloned())
|
|
.expect("the stamp has an /AP");
|
|
|
|
AnnotationEditor::new(&mut annots)
|
|
.move_to(stamp, 100.0, 500.0)
|
|
.expect("a stamp can be moved");
|
|
|
|
let (out, report) =
|
|
save_annotation_edits(&data, &trailer, &annots, page_ref, &page_dict).expect("save");
|
|
|
|
assert!(
|
|
report.annotation_appearances_skipped.contains(&stamp.num),
|
|
"the stamp's appearance was kept but not reported: {report:?}"
|
|
);
|
|
|
|
// Reparse from the written bytes: the artwork must still be there.
|
|
let mut doc = PdfDocument::parse(&out).expect("reparses");
|
|
let reparsed = doc.page_annotations(PAGE).expect("annotations");
|
|
let saved = reparsed
|
|
.iter()
|
|
.find(|a| a.obj_ref == Some(stamp))
|
|
.expect("the stamp survived");
|
|
assert_eq!(
|
|
saved.raw_dict.get("AP"),
|
|
Some(&ap_before),
|
|
"ADR 0004 rule 3: a stamp's artwork must never be replaced"
|
|
);
|
|
assert_eq!(saved.rect[0], 100.0, "and the move still applied");
|
|
}
|
|
|
|
/// The control: a type this crate *can* draw is not reported as skipped.
|
|
/// Without this, the test above would pass if everything were reported.
|
|
#[test]
|
|
fn a_square_is_not_reported_as_skipped() {
|
|
let data = corpus("annotations/stamp.pdf");
|
|
let (trailer, mut annots, page_ref, page_dict) = open(&data);
|
|
|
|
let square = annots
|
|
.live()
|
|
.find(|a| matches!(a.annotation.annot_type, AnnotationType::Square))
|
|
.and_then(|a| a.annotation.obj_ref)
|
|
.expect("the fixture has a square");
|
|
|
|
AnnotationEditor::new(&mut annots)
|
|
.move_to(square, 50.0, 50.0)
|
|
.expect("a square can be moved");
|
|
|
|
let (_, report) =
|
|
save_annotation_edits(&data, &trailer, &annots, page_ref, &page_dict).expect("save");
|
|
|
|
assert!(
|
|
!report.annotation_appearances_skipped.contains(&square.num),
|
|
"a drawable type must not be reported as skipped: {report:?}"
|
|
);
|
|
}
|
|
|
|
/// `SetContents` is one of the six edits the ADR promises; it had no
|
|
/// round-trip test.
|
|
#[test]
|
|
fn edited_contents_survive_a_save_and_reparse() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (trailer, mut annots, page_ref, page_dict) = open(&data);
|
|
|
|
let target = annots
|
|
.live()
|
|
.find(|a| matches!(a.annotation.annot_type, AnnotationType::FreeText { .. }))
|
|
.and_then(|a| a.annotation.obj_ref)
|
|
.expect("the fixture has a free-text note");
|
|
|
|
AnnotationEditor::new(&mut annots)
|
|
.apply(target, AnnotationEdit::SetContents("Revised note".into()))
|
|
.expect("contents can be set");
|
|
|
|
let (out, _) =
|
|
save_annotation_edits(&data, &trailer, &annots, page_ref, &page_dict).expect("save");
|
|
|
|
let mut doc = PdfDocument::parse(&out).expect("reparses");
|
|
let reparsed = doc.page_annotations(PAGE).expect("annotations");
|
|
let saved = reparsed
|
|
.iter()
|
|
.find(|a| a.obj_ref == Some(target))
|
|
.expect("the annotation survived");
|
|
assert_eq!(
|
|
saved
|
|
.raw_dict
|
|
.get_str("Contents")
|
|
.map(String::from_utf8_lossy),
|
|
Some(std::borrow::Cow::Borrowed("Revised note")),
|
|
"the new contents must be readable from the written bytes"
|
|
);
|
|
}
|
|
|
|
/// `SetFlags` likewise: written as `/F` bits and readable after a reparse.
|
|
#[test]
|
|
fn edited_flags_survive_a_save_and_reparse() {
|
|
let data = corpus("annotations/links.pdf");
|
|
let (trailer, mut annots, page_ref, page_dict) = open(&data);
|
|
|
|
let target = annots
|
|
.live()
|
|
.next()
|
|
.and_then(|a| a.annotation.obj_ref)
|
|
.expect("a reference");
|
|
|
|
let flags = AnnotationFlags {
|
|
print: true,
|
|
no_zoom: true,
|
|
..AnnotationFlags::default()
|
|
};
|
|
|
|
AnnotationEditor::new(&mut annots)
|
|
.apply(target, AnnotationEdit::SetFlags(flags))
|
|
.expect("flags can be set");
|
|
|
|
let (out, _) =
|
|
save_annotation_edits(&data, &trailer, &annots, page_ref, &page_dict).expect("save");
|
|
|
|
let mut doc = PdfDocument::parse(&out).expect("reparses");
|
|
let reparsed = doc.page_annotations(PAGE).expect("annotations");
|
|
let saved = reparsed
|
|
.iter()
|
|
.find(|a| a.obj_ref == Some(target))
|
|
.expect("the annotation survived");
|
|
assert!(saved.flags.print, "Print must round-trip");
|
|
assert!(saved.flags.no_zoom, "NoZoom must round-trip");
|
|
assert!(!saved.flags.hidden, "Hidden must not have been invented");
|
|
}
|