From 166f706e3e73ae69ef15931b467d1c87bcbe5e98 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 27 Oct 2026 23:12:51 +0000 Subject: [PATCH] Refactor engine for strict Clippy checks --- crates/engine/src/doc_state.rs | 2 +- crates/engine/src/edit/labels.rs | 2 +- crates/engine/src/edit/run.rs | 5 +- crates/engine/src/session/editing.rs | 9 +- crates/engine/src/session/files.rs | 6 +- crates/engine/src/session/save.rs | 5 +- crates/engine/src/session/view.rs | 5 +- crates/engine/src/tests.rs | 149 +++++++++++++++------------ 8 files changed, 110 insertions(+), 73 deletions(-) diff --git a/crates/engine/src/doc_state.rs b/crates/engine/src/doc_state.rs index d25cc6503..d28429de6 100644 --- a/crates/engine/src/doc_state.rs +++ b/crates/engine/src/doc_state.rs @@ -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)] diff --git a/crates/engine/src/edit/labels.rs b/crates/engine/src/edit/labels.rs index 5e25ae465..ea2589806 100644 --- a/crates/engine/src/edit/labels.rs +++ b/crates/engine/src/edit/labels.rs @@ -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 diff --git a/crates/engine/src/edit/run.rs b/crates/engine/src/edit/run.rs index 7901a3b7d..7bf5467fc 100644 --- a/crates/engine/src/edit/run.rs +++ b/crates/engine/src/edit/run.rs @@ -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> { diff --git a/crates/engine/src/session/editing.rs b/crates/engine/src/session/editing.rs index 9b1a8340a..f8d5648a2 100644 --- a/crates/engine/src/session/editing.rs +++ b/crates/engine/src/session/editing.rs @@ -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. diff --git a/crates/engine/src/session/files.rs b/crates/engine/src/session/files.rs index 6d4e11d39..1e8cd9b08 100644 --- a/crates/engine/src/session/files.rs +++ b/crates/engine/src/session/files.rs @@ -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). diff --git a/crates/engine/src/session/save.rs b/crates/engine/src/session/save.rs index 23994aee3..d4156a2eb 100644 --- a/crates/engine/src/session/save.rs +++ b/crates/engine/src/session/save.rs @@ -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, diff --git a/crates/engine/src/session/view.rs b/crates/engine/src/session/view.rs index cef539189..055437f56 100644 --- a/crates/engine/src/session/view.rs +++ b/crates/engine/src/session/view.rs @@ -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 { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index e7e68a758..14a61b263 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -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 { 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>| { - 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>| { + 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}"); + } +}