Refactor engine for strict Clippy checks

This commit is contained in:
Dylan M. Taylor
2026-10-27 23:12:51 +00:00
parent de73c35e17
commit 166f706e3e
8 changed files with 110 additions and 73 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Arc, EditError, Hidden, SignatureInfo, TrustStore, optimize};
/// A change to an existing page image.
#[derive(Clone, Debug, PartialEq)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Edit, FillMark, ImageEdit, Label, LineEnding, MarkKind, Markup, PageBox, ReviewState, Shape, field_table};
/// Every wording [`Edit::label`] can produce, for an interface to translate. `Edit::Batch` is not
/// here: its wording is only the hole, because a batch carries its name as text (see
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
AddedContent, Arc, Edit, EditError, FieldValue, ImageEdit, NewAnnotation, Shape, a11y, copy_image, decode_data_file, embed_image, fit_picture,
image_content_type, is_xfa_data_file, js, mark_source, measure, open_source, strip_xfa, unrestricted, xfa, xfa_after_data_change,
};
/// Whether the opening password allows an edit (§7.6.4.2, Table 22).
pub(crate) fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Result<(), EditError> {
+8 -1
View File
@@ -1,4 +1,11 @@
use super::*;
#[cfg(test)]
use super::check_refresh_panic;
use super::{
Arc, DocId, Document, Edit, EditCtx, EditError, Label, MAX_UNDO, ObjectKind, OnceLock, RenderPool, SaveOptions, Scope, Session,
XFA_REMOVED_PREFIX, XFA_SCRIPTS_OFF, check_permission, comment_list, display_bytes, guard, info_fields, inspect, js, keys_after, note_warnings,
render_threads, run_edit, scan_colours, scope_of, shown_under_redactions, signatures_of, sync_xfa_state, undrawn_notices, uses_scripts,
write_incremental, xfa, xfa_sync_datasets,
};
impl Session {
/// Apply an edit. On success the previous state is undoable and the view data is refreshed.
+5 -1
View File
@@ -1,4 +1,8 @@
use super::*;
use super::{
Arc, CombineSource, Combined, DataFormat, DocId, EditError, Hidden, ImageResolution, LOW_RESOLUTION_PRINT_DPI, OptimizeReport, SaveOptions,
Session, SourceKind, SplitPart, decode_text, guard, open_source, open_source_with, optimize, print, print_raster, set_default_layer_state,
source_kind, writable, write_full,
};
impl Session {
/// The page count of another PDF (Replace Pages, Insert Pages dialogs).
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Arc, DocId, EditError, Keys, OpenError, RecoverySnapshot, RenderPool, SaveOptions, Session, SignOptions, TrustStore, display_bytes, guard,
render_threads, signatures_of, writable, write_full, write_incremental, xfa_sync_datasets,
};
impl Session {
/// The bytes to write for Save: an incremental update of the file as opened/last saved,
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Arc, BookmarkPage, BookmarkRow, DocId, Document, EditError, LayerOp, OpenError, ReadingColors, RenderConfig, RenderPool, Session, SummarySort,
apply_layer_state, comment_summary, local_utc_offset, outline_rows, parse_ymd, render_threads, use_layer_choices,
};
impl Session {
pub fn new() -> Self {
+83 -66
View File
@@ -3068,7 +3068,12 @@ fn secondary_indexes_load_once_and_follow_edit_scopes() {
assert!(doc.links().is_empty());
assert_eq!(secondary_scan_counts(), (1, 1), "repeated reads reuse the link list");
}
metadata_and_comment_edits_keep_the_indexes(&mut session, id);
link_edits_rebuild_only_the_link_list(&mut session, id);
content_and_marker_edits_rebuild_the_markers(&mut session, id);
}
fn metadata_and_comment_edits_keep_the_indexes(session: &mut Session, id: DocId) {
session.apply(id, Edit::SetInfo { key: "Title".into(), value: "Changed".into() }).unwrap();
assert!(session.get(id).unwrap().links().is_empty());
assert!(session.get(id).unwrap().marks().is_empty());
@@ -3077,7 +3082,9 @@ fn secondary_indexes_load_once_and_follow_edit_scopes() {
session.apply(id, rect_comment(0, [20.0, 20.0, 60.0, 60.0])).unwrap();
assert!(session.get(id).unwrap().links().is_empty());
assert_eq!(secondary_scan_counts(), (1, 2), "comment edits invalidate the annotation-position link index");
}
fn link_edits_rebuild_only_the_link_list(session: &mut Session, id: DocId) {
session
.apply(
id,
@@ -3101,7 +3108,9 @@ fn secondary_indexes_load_once_and_follow_edit_scopes() {
session.apply(id, Edit::DeleteAnnotation { page: 0, index: 0 }).unwrap();
assert_eq!(session.get(id).unwrap().links()[0].index, 0, "deleting an earlier annotation shifts link indexes");
assert_eq!(secondary_scan_counts(), (1, 6), "comment deletion rebuilds annotation-position indexes");
}
fn content_and_marker_edits_rebuild_the_markers(session: &mut Session, id: DocId) {
session
.apply(id, Edit::AddText { page: 0, text: AddedText { rect: [20.0, 250.0, 100.0, 280.0], text: "Added".into(), ..Default::default() } })
.unwrap();
@@ -8106,7 +8115,12 @@ fn undo_and_redo_roll_back_on_a_refresh_panic_or_error() {
// Redo now succeeds cleanly.
assert_eq!(s.redo(id).unwrap().as_str(), "Rotate page");
assert_eq!(s.get(id).unwrap().info.pages[0].rotation, 90);
rolls_back_metadata_scope(&mut s, id);
rolls_back_protection_keys(&mut s, id);
}
/// Undo/redo of a metadata edit rolled back after a refresh panic.
fn rolls_back_metadata_scope(s: &mut Session, id: DocId) {
// 5. Metadata scope: a panic after `doc.info.title` was written rolls back `info.title` and keeps `edit_notices`.
s.apply(id, Edit::SetInfo { key: "Title".into(), value: "Draft".into() }).unwrap();
let notice = TextNotice { field: None, page: 0, ch: '☺', redrawn_elsewhere: false };
@@ -8124,7 +8138,10 @@ fn undo_and_redo_roll_back_on_a_refresh_panic_or_error() {
assert_eq!(s.get(id).unwrap().info.title, None);
assert_eq!(s.redo(id).unwrap().as_str(), "Change Title");
assert_eq!(s.get(id).unwrap().info.title.as_deref(), Some("Draft"));
}
/// Undo/redo of a protection edit rolled back after a refresh panic.
fn rolls_back_protection_keys(s: &mut Session, id: DocId) {
// 6. Protection keys: undo/redo rollback restores `editor.keys` and `doc.password` before re-inspecting.
let protect = Protection { open_password: Some("u".into()), permissions_password: Some("o".into()), ..Protection::default() };
s.apply(id, Edit::Protect(protect)).unwrap();
@@ -8720,69 +8737,69 @@ fn pdf_with_annots(annots: &[&[u8]]) -> Vec<u8> {
bytes
}
/// ISO 32000-2 Table 224 (`/NeedAppearances`) and Table 192 (`/MK /CA`): a form that leaves its
/// appearances to the viewer shows its check boxes and radio buttons drawn from `/MK`, over the
/// filled squares the file carries: a checked box its check mark (ZapfDingbats `4`), an unchecked
/// one its empty border, a radio button its dot (`l`), on screen and printed as an image. Without
/// `/NeedAppearances` the file's own squares show. Saving writes the file as it was.
#[test]
fn need_appearances_buttons_are_displayed_from_their_captions() {
let pdf = |need: bool| {
let flag = if need { "/NeedAppearances true " } else { "" };
format!(
"%PDF-1.7\n\
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >> endobj\n\
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 400 200] /Annots [6 0 R 7 0 R 8 0 R] >> endobj\n\
4 0 obj << /Fields [6 0 R 7 0 R 8 0 R] {flag}/DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 5 0 R >> >> >> endobj\n\
5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> endobj\n\
6 0 obj << /Type /Annot /Subtype /Widget /FT /Btn /T (on) /V /Yes /AS /Yes /DA (/Helv 0 Tf 0 g) /MK << /BC [0 0 0] /CA (4) >> /Rect [20 100 60 140] /F 4 /P 3 0 R /AP << /N << /Yes 9 0 R /Off 9 0 R >> >> >> endobj\n\
7 0 obj << /Type /Annot /Subtype /Widget /FT /Btn /T (off) /V /Off /AS /Off /DA (/Helv 0 Tf 0 g) /MK << /BC [0 0 0] /CA (4) >> /Rect [100 100 140 140] /F 4 /P 3 0 R /AP << /N << /Yes 9 0 R /Off 9 0 R >> >> >> endobj\n\
8 0 obj << /Type /Annot /Subtype /Widget /FT /Btn /Ff 32768 /T (radio) /V /B /AS /B /DA (/Helv 0 Tf 0 g) /MK << /BC [0 0 0] /CA (l) >> /Rect [180 100 220 140] /F 4 /P 3 0 R /AP << /N << /B 9 0 R /Off 9 0 R >> >> >> endobj\n\
9 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 40 40] /Length 18 >> stream\n0 g 4 4 32 32 re f\nendstream endobj\n\
trailer << /Root 1 0 R >>\n%%EOF"
)
.into_bytes()
};
let render = |bytes: Arc<Vec<u8>>| {
let mut r = pdfcraft_render::PageRenderer::new(bytes, pdfcraft_render::RenderConfig::default());
r.render(pdfcraft_render::RenderRequest { page: 0, scale: 1.0, ..Default::default() })
};
// Dark pixels of the page (drawn at scale 1, 400 x 200) in the user-space box x0..x1, y0..y1.
let ink = |page: &pdfcraft_render::RenderedPage, (x0, y0, x1, y1): (u32, u32, u32, u32)| {
let (px, _) = page.rgba.as_chunks::<4>();
(200 - y1..200 - y0)
.flat_map(|y| (x0..x1).map(move |x| (x, y)))
.filter(|&(x, y)| px.get((y * page.width + x) as usize).is_some_and(|p| p[0] < 128 && p[1] < 128 && p[2] < 128))
.count()
};
// In a widget whose lower-left corner is (x, y): its whole inside, the top-left of its inside
// (where a check mark leaves the box empty), and the lower-left corner (outside a dot).
let inside = |x: u32, y: u32| (x + 6, y + 6, x + 34, y + 34);
let top_left = |x: u32, y: u32| (x + 6, y + 26, x + 14, y + 34);
let corner = |x: u32, y: u32| (x + 5, y + 5, x + 9, y + 9);
let mut s = Session::new();
let id = s.open("form.pdf", None, Arc::new(pdf(true)), None).unwrap();
let shown = render(s.get(id).unwrap().display.clone());
let raster = pdfcraft_print::Raster { dpi: 72.0, grayscale: false };
let pictures = crate::print_raster::rasterize(s.get(id).unwrap(), &[0], &raster, pdfcraft_print::Content::Document).unwrap();
let printed = render(Arc::new(pictures));
for (page, how) in [(&shown, "shown"), (&printed, "printed")] {
let (check, check_corner) = (ink(page, inside(20, 100)), ink(page, top_left(20, 100)));
assert!(check > 100 && check < 600, "{how}: a check mark, not the 28 x 28 square: {check}");
assert_eq!(check_corner, 0, "{how}: the check mark leaves the top left empty");
assert_eq!(ink(page, inside(100, 100)), 0, "{how}: the unchecked box is empty");
assert!(ink(page, (100, 100, 140, 102)) > 30, "{how}: and keeps its border");
let dot = ink(page, (188, 108, 212, 132));
assert!(dot > 300 && ink(page, corner(180, 100)) == 0, "{how}: the radio button's dot: {dot}");
}
let saved = s.save_bytes(id).unwrap();
assert!(saved.windows(18).any(|w| w == b"0 g 4 4 32 32 re f"), "the file keeps its own appearances");
// Without /NeedAppearances the file's filled squares are shown.
let id = s.open("plain.pdf", None, Arc::new(pdf(false)), None).unwrap();
let shown = render(s.get(id).unwrap().display.clone());
for x in [20, 100, 180] {
assert!(ink(&shown, inside(x, 100)) > 700, "the file's square at {x}");
}
}
/// ISO 32000-2 Table 224 (`/NeedAppearances`) and Table 192 (`/MK /CA`): a form that leaves its
/// appearances to the viewer shows its check boxes and radio buttons drawn from `/MK`, over the
/// filled squares the file carries: a checked box its check mark (ZapfDingbats `4`), an unchecked
/// one its empty border, a radio button its dot (`l`), on screen and printed as an image. Without
/// `/NeedAppearances` the file's own squares show. Saving writes the file as it was.
#[test]
fn need_appearances_buttons_are_displayed_from_their_captions() {
let pdf = |need: bool| {
let flag = if need { "/NeedAppearances true " } else { "" };
format!(
"%PDF-1.7\n\
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >> endobj\n\
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 400 200] /Annots [6 0 R 7 0 R 8 0 R] >> endobj\n\
4 0 obj << /Fields [6 0 R 7 0 R 8 0 R] {flag}/DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 5 0 R >> >> >> endobj\n\
5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> endobj\n\
6 0 obj << /Type /Annot /Subtype /Widget /FT /Btn /T (on) /V /Yes /AS /Yes /DA (/Helv 0 Tf 0 g) /MK << /BC [0 0 0] /CA (4) >> /Rect [20 100 60 140] /F 4 /P 3 0 R /AP << /N << /Yes 9 0 R /Off 9 0 R >> >> >> endobj\n\
7 0 obj << /Type /Annot /Subtype /Widget /FT /Btn /T (off) /V /Off /AS /Off /DA (/Helv 0 Tf 0 g) /MK << /BC [0 0 0] /CA (4) >> /Rect [100 100 140 140] /F 4 /P 3 0 R /AP << /N << /Yes 9 0 R /Off 9 0 R >> >> >> endobj\n\
8 0 obj << /Type /Annot /Subtype /Widget /FT /Btn /Ff 32768 /T (radio) /V /B /AS /B /DA (/Helv 0 Tf 0 g) /MK << /BC [0 0 0] /CA (l) >> /Rect [180 100 220 140] /F 4 /P 3 0 R /AP << /N << /B 9 0 R /Off 9 0 R >> >> >> endobj\n\
9 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 40 40] /Length 18 >> stream\n0 g 4 4 32 32 re f\nendstream endobj\n\
trailer << /Root 1 0 R >>\n%%EOF"
)
.into_bytes()
};
let render = |bytes: Arc<Vec<u8>>| {
let mut r = pdfcraft_render::PageRenderer::new(bytes, pdfcraft_render::RenderConfig::default());
r.render(pdfcraft_render::RenderRequest { page: 0, scale: 1.0, ..Default::default() })
};
// Dark pixels of the page (drawn at scale 1, 400 x 200) in the user-space box x0..x1, y0..y1.
let ink = |page: &pdfcraft_render::RenderedPage, (x0, y0, x1, y1): (u32, u32, u32, u32)| {
let (px, _) = page.rgba.as_chunks::<4>();
(200 - y1..200 - y0)
.flat_map(|y| (x0..x1).map(move |x| (x, y)))
.filter(|&(x, y)| px.get((y * page.width + x) as usize).is_some_and(|p| p[0] < 128 && p[1] < 128 && p[2] < 128))
.count()
};
// In a widget whose lower-left corner is (x, y): its whole inside, the top-left of its inside
// (where a check mark leaves the box empty), and the lower-left corner (outside a dot).
let inside = |x: u32, y: u32| (x + 6, y + 6, x + 34, y + 34);
let top_left = |x: u32, y: u32| (x + 6, y + 26, x + 14, y + 34);
let corner = |x: u32, y: u32| (x + 5, y + 5, x + 9, y + 9);
let mut s = Session::new();
let id = s.open("form.pdf", None, Arc::new(pdf(true)), None).unwrap();
let shown = render(s.get(id).unwrap().display.clone());
let raster = pdfcraft_print::Raster { dpi: 72.0, grayscale: false };
let pictures = crate::print_raster::rasterize(s.get(id).unwrap(), &[0], &raster, pdfcraft_print::Content::Document).unwrap();
let printed = render(Arc::new(pictures));
for (page, how) in [(&shown, "shown"), (&printed, "printed")] {
let (check, check_corner) = (ink(page, inside(20, 100)), ink(page, top_left(20, 100)));
assert!(check > 100 && check < 600, "{how}: a check mark, not the 28 x 28 square: {check}");
assert_eq!(check_corner, 0, "{how}: the check mark leaves the top left empty");
assert_eq!(ink(page, inside(100, 100)), 0, "{how}: the unchecked box is empty");
assert!(ink(page, (100, 100, 140, 102)) > 30, "{how}: and keeps its border");
let dot = ink(page, (188, 108, 212, 132));
assert!(dot > 300 && ink(page, corner(180, 100)) == 0, "{how}: the radio button's dot: {dot}");
}
let saved = s.save_bytes(id).unwrap();
assert!(saved.windows(18).any(|w| w == b"0 g 4 4 32 32 re f"), "the file keeps its own appearances");
// Without /NeedAppearances the file's filled squares are shown.
let id = s.open("plain.pdf", None, Arc::new(pdf(false)), None).unwrap();
let shown = render(s.get(id).unwrap().display.clone());
for x in [20, 100, 180] {
assert!(ink(&shown, inside(x, 100)) > 700, "the file's square at {x}");
}
}