diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index edba4995df..716d2a82ee 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with the SHA-256 of every asset, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (258) +## In this repository (257) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -204,7 +204,6 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `vendor/hayro-interpret/assets/FoxitSymbol.pfb` | FoxitSymbol (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) | | `vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc` | CGATS001Compat v2 micro ICC profile | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | CC0-1.0 | https://github.com/saucecontrol/Compact-ICC-Profiles | Default CMYK colour conversion (vendored hayro-interpret) | | `crates/fonts/src/std14_metrics.rs` | Standard-14 font advance widths and encodings (as Rust tables) | Adobe (Core14 AFM metrics, PDF specification encodings); tables generated by the PdfCraft contributors | MIT OR Apache-2.0 | ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs. Data only: the code that reads these tables lives in std14.rs, so that ordinary edits do not churn this file's hash. | Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree. | -| `crates/prc/src/acof/table.rs` | PRC Double prefix-code table (as a Rust table) | Kristof Ralovich (prc-rs); the code table itself is defined by the PRC format (ISO 14739-1, working draft clause 11.17.2) | MIT | Rows taken from prc-rs src/double.rs at commit 287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d (https://github.com/ralovich/prc-rs), mechanically reformatted into (code length, code, entry) tuples; checked to be a complete prefix code by the tests in crates/prc/src/acof.rs. Data only: the code that reads the table lives in acof.rs. The MIT notice is kept in the file header. | Decodes every PRC Double (coordinates, transforms, colours) in 3D annotations' PRC streams. Non-visual data. | | `vendor/hayro-cmap/assets/cmaps.brotli` | Adobe CMap resources (predefined CJK CMaps) | Adobe | BSD-3-Clause | https://github.com/adobe-type-tools/cmap-resources (via hayro-cmap 0.1.0 assets/) | Character-code mapping tables required to read CJK PDFs. Non-visual data, allowed by AGENTS.md §1.1 | | `docs/brand/artcraft-logo-white.png` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | | `docs/brand/artcraft-logo-white.svg` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | @@ -267,10 +266,11 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `assets/icons/a-large-small.svg` | Lucide icon "a-large-small" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/a-large-small.svg) | UI icon, embedded via crates/ui-egui/src/widgets/icon_data.rs | | `assets/icons/move-horizontal.svg` | Lucide icon "move-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | -## Compiled in through dependencies (7) +## Compiled in through dependencies (8) | Crate | File | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---|---| +| `craft-prc` 0.1.0 | `src/acof/table.rs` | PRC Double prefix-code table (as a Rust table) | Kristof Ralovich (prc-rs); the code table itself is defined by the PRC format (ISO 14739-1, working draft clause 11.17.2) | MIT | Rows taken from prc-rs src/double.rs at commit 287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d (https://github.com/ralovich/prc-rs), mechanically reformatted into (code length, code, entry) tuples; checked to be a complete prefix code by the tests in craft-prc src/acof.rs. Data only: the code that reads the table lives in acof.rs. The MIT notice is kept in the file header. | Decodes every PRC Double (coordinates, transforms, colours) in 3D annotations' PRC streams. Non-visual data. | | `epaint_default_fonts` 0.37.0 | `fonts/Ubuntu-Light.ttf` | Ubuntu Light | Canonical Ltd. / Dalton Maag | Ubuntu-font-1.0 | https://design.ubuntu.com/font | egui fallback fonts (eframe feature default_fonts) | | `epaint_default_fonts` 0.37.0 | `fonts/Hack-Regular.ttf` | Hack Regular | Source Foundry Authors (derived from Bitstream Vera / DejaVu) | MIT AND Bitstream-Vera | https://github.com/source-foundry/Hack | egui fallback fonts (eframe feature default_fonts) | | `epaint_default_fonts` 0.37.0 | `fonts/NotoEmoji-Regular.ttf` | Noto Emoji (monochrome) | Google Inc. | OFL-1.1 | https://github.com/googlefonts/noto-emoji | egui monochrome fallback font (egui feature monochrome_emoji_fonts) | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 4cdaa70575..219f0491c9 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -2167,13 +2167,14 @@ adobe_data = true usage = "Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree." sha256 = "f225e0b044875b8fa6375b6b5ad935f0d99fcc1ad03ecf9f0baae2cedbfca4d2" -[[asset]] -path = "crates/prc/src/acof/table.rs" +[[bundled]] +crate = "craft-prc" +version = "0.1.0" +path = "src/acof/table.rs" title = "PRC Double prefix-code table (as a Rust table)" author = "Kristof Ralovich (prc-rs); the code table itself is defined by the PRC format (ISO 14739-1, working draft clause 11.17.2)" -source = "Rows taken from prc-rs src/double.rs at commit 287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d (https://github.com/ralovich/prc-rs), mechanically reformatted into (code length, code, entry) tuples; checked to be a complete prefix code by the tests in crates/prc/src/acof.rs. Data only: the code that reads the table lives in acof.rs. The MIT notice is kept in the file header." +source = "Rows taken from prc-rs src/double.rs at commit 287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d (https://github.com/ralovich/prc-rs), mechanically reformatted into (code length, code, entry) tuples; checked to be a complete prefix code by the tests in craft-prc src/acof.rs. Data only: the code that reads the table lives in acof.rs. The MIT notice is kept in the file header." licence = "MIT" -licence_file = "licenses/prc-rs-LICENSE.txt" kind = "data" usage = "Decodes every PRC Double (coordinates, transforms, colours) in 3D annotations' PRC streams. Non-visual data." sha256 = "8e705c5cde6a3ef778e8aca69759e38845b228b3f7f12949e587b19855d2114c" diff --git a/Cargo.lock b/Cargo.lock index f6a04eb16c..34c522b951 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4470,6 +4470,8 @@ version = "0.21.1" dependencies = [ "craft-pdf", "hayro-syntax", + "pdfcraft-crypt", + "pdfcraft-filters", "pdfcraft-oracle", "pdfcraft-testkit", "proptest", diff --git a/LICENSES.toml b/LICENSES.toml index e52a55751e..e4b2c6f462 100644 --- a/LICENSES.toml +++ b/LICENSES.toml @@ -189,3 +189,9 @@ crate = "icu_segmenter_data" path = "data/segmenter_dictionary_extended_v1.rs.data" licence = "Unicode-3.0" documents = ["licenses/ICU4X-LICENSE.txt"] + +[[bundled]] +crate = "craft-prc" +path = "src/acof/table.rs" +licence = "MIT" +documents = ["licenses/prc-rs-LICENSE.txt"] diff --git a/crates/automation/src/model_render.rs b/crates/automation/src/model_render.rs index 0007dd8a7e..c78ed51418 100644 --- a/crates/automation/src/model_render.rs +++ b/crates/automation/src/model_render.rs @@ -1,7 +1,7 @@ //! `model_render`: a page's 3D model (ISO 32000-2 §13.6) drawn to a PNG, without a viewer. -use pdfcraft_engine::model3d::Model3dKind; use craft_prc::PrcFile; +use pdfcraft_engine::model3d::Model3dKind; use pdfcraft_prc_render::nav::{Axis, NamedView, Orbit, Upright, reach}; use pdfcraft_prc_render::{Bounds, Camera, RenderOptions, WorldMesh, assemble, render}; use serde_json::Value; diff --git a/crates/export/src/tests.rs b/crates/export/src/tests.rs index eeece99949..88066fe84c 100644 --- a/crates/export/src/tests.rs +++ b/crates/export/src/tests.rs @@ -323,7 +323,7 @@ fn word_opens_text_with_control_characters_and_long_pages() { assert!(!xml.chars().any(|c| !allowed(c))); assert!(xml.contains(""), "clamped to 22 in: {xml}"); assert!(xml.contains(""), "a non-finite size falls back to 12 pt"); - assert!(rtf(&[p]).contains("Total 4.50\tGBP")); + assert!(rtf(&[p]).contains("Total 4.50\\tab GBP")); // A narrow page keeps room for text: margins shrink. let narrow = Page { width: 100.0, height: 200.0, ..page() }; let xml = part(&docx(&[narrow], "Narrow"), "word/document.xml"); diff --git a/crates/export/src/zip.rs b/crates/export/src/zip.rs index 99cf872d04..a7786a0496 100644 --- a/crates/export/src/zip.rs +++ b/crates/export/src/zip.rs @@ -11,11 +11,7 @@ pub struct Zip { impl Zip { /// Add a file. Already-compressed data (images) is stored; the rest is deflated. pub fn add(&mut self, name: &str, data: &[u8], compress: bool) { - let compression = if compress { - Compression::Deflated - } else { - Compression::Stored - }; + let compression = if compress { Compression::Deflated } else { Compression::Stored }; let _ = self.writer.add(name, data, compression); } diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 61b660228e..ba75a6b2e7 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -46,7 +46,7 @@ milestone = "M1" status = "shipped" commands = ["file.open"] tools = ["doc_open"] -evidence = ["crates/cos/src/document/tests.rs::damaged_xref_is_reconstructed", "crates/cos/src/parser.rs::indirect_stream_with_wrong_length_recovers", "crates/cos/src/document/tests.rs::self_referential_length_does_not_overflow", "crates/engine/src/tests.rs::a_repaired_file_is_rewritten_by_save_even_without_edits", "crates/automation/tests/it/automation.rs::saving_a_repaired_file_puts_the_repair_on_disk", "crates/engine/src/tests.rs::a_signed_repaired_file_keeps_its_bytes_when_saved_without_edits", "crates/cos/src/document/tests.rs::a_table_numbered_from_1_that_begins_with_object_0_is_read_from_0", "crates/render/src/raster/tests.rs::an_update_cut_off_before_its_trailer_shows_its_pages_as_the_model_reads_them", "crates/cos/src/document/tests.rs::an_update_without_prev_is_rebuilt_with_its_page_tree_and_a_repair_note", "crates/render/src/inspect/core.rs::an_update_without_prev_shows_the_page_tree_and_says_it_was_rebuilt", "crates/cos/src/document/tests.rs::a_hybrid_update_without_prev_is_rebuilt_too", "crates/render/src/inspect/core.rs::a_hybrid_update_without_prev_is_rebuilt_as_the_document_model_rebuilds_it"] +evidence = ["craft-pdf::cos/document/tests.rs::damaged_xref_is_reconstructed", "craft-pdf::cos/parser.rs::indirect_stream_with_wrong_length_recovers", "craft-pdf::cos/document/tests.rs::self_referential_length_does_not_overflow", "crates/engine/src/tests.rs::a_repaired_file_is_rewritten_by_save_even_without_edits", "crates/automation/tests/it/automation.rs::saving_a_repaired_file_puts_the_repair_on_disk", "crates/engine/src/tests.rs::a_signed_repaired_file_keeps_its_bytes_when_saved_without_edits", "craft-pdf::cos/document/tests.rs::a_table_numbered_from_1_that_begins_with_object_0_is_read_from_0", "crates/render/src/raster/tests.rs::an_update_cut_off_before_its_trailer_shows_its_pages_as_the_model_reads_them", "craft-pdf::cos/document/tests.rs::an_update_without_prev_is_rebuilt_with_its_page_tree_and_a_repair_note", "crates/render/src/inspect/core.rs::an_update_without_prev_shows_the_page_tree_and_says_it_was_rebuilt", "craft-pdf::cos/document/tests.rs::a_hybrid_update_without_prev_is_rebuilt_too", "crates/render/src/inspect/core.rs::a_hybrid_update_without_prev_is_rebuilt_as_the_document_model_rebuilds_it"] notes = "Save and doc_save write a reconstructed file in full even when nothing was edited, so the repair reaches the disk and the saved file opens without one (#1937); a signed one keeps its bytes, so its signatures stay valid. A repair does not mark the document as having unsaved changes, so closing it does not offer to save the repaired file. A classic table numbered 1 n that begins with object 0's free entry is read from 0 (#2676) and logged. A last update cut off before its trailer (objects after a last `startxref` more than 4 KiB from the end) is rebuilt from object headers by the renderer as by the document model, so both show its latest objects. A file whose last update has no /Prev (ISO 32000-2 §7.5.6, §7.5.8.2), so that its page tree is cut off the chain, (a hybrid section's /XRefStm stream counts as part of the same update) is rebuilt from its object headers by the document model and the renderer alike, with a repair note and an open warning (the signed PDFBOX-2720 and PDFBOX-2942 files)." [[feature]] @@ -58,7 +58,7 @@ milestone = "M1" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/it/ui.rs::garbage_input_is_rejected_without_panicking", "crates/cos/src/parser.rs::garbage_never_panics", "crates/render/src/raster/tests.rs::missing_page_and_garbage_input_fail_gracefully", "crates/render/src/raster/tests.rs::a_form_that_draws_itself_is_drawn_once"] +evidence = ["crates/ui-egui/tests/it/ui.rs::garbage_input_is_rejected_without_panicking", "craft-pdf::cos/parser.rs::garbage_never_panics", "crates/render/src/raster/tests.rs::missing_page_and_garbage_input_fail_gracefully", "crates/render/src/raster/tests.rs::a_form_that_draws_itself_is_drawn_once"] notes = "fuzzed nightly (M1.10)" [[feature]] @@ -70,7 +70,7 @@ milestone = "M1" status = "shipped" commands = [] tools = ["doc_info"] -evidence = ["crates/ui-egui/tests/it/viewer.rs::damaged_files_say_they_were_repaired", "crates/cos/tests/it/generations.rs::contradictory_xref_generation_recovers_the_matching_header_and_records_repair", "crates/cos/tests/it/generations.rs::generation_repair_diagnostics_are_bounded_without_disabling_recovery", "crates/cos/tests/it/generations.rs::retained_generation_disagreements_are_reported_in_materialized_mode_too", "crates/cos/tests/it/generations.rs::hybrid_generation_diagnostics_observe_rows_before_the_table_hides_them", "crates/cos/tests/it/generations.rs::discarded_xref_repair_notes_do_not_survive_object_header_reconstruction", "crates/cos/src/document/tests.rs::unreadable_objects_are_listed_with_the_reason", "crates/automation/tests/it/unreadable_objects.rs::doc_info_lists_an_object_that_could_not_be_read"] +evidence = ["crates/ui-egui/tests/it/viewer.rs::damaged_files_say_they_were_repaired", "crates/cos/tests/it/generations.rs::contradictory_xref_generation_recovers_the_matching_header_and_records_repair", "crates/cos/tests/it/generations.rs::generation_repair_diagnostics_are_bounded_without_disabling_recovery", "crates/cos/tests/it/generations.rs::retained_generation_disagreements_are_reported_in_materialized_mode_too", "crates/cos/tests/it/generations.rs::hybrid_generation_diagnostics_observe_rows_before_the_table_hides_them", "crates/cos/tests/it/generations.rs::discarded_xref_repair_notes_do_not_survive_object_header_reconstruction", "craft-pdf::cos/document/tests.rs::unreadable_objects_are_listed_with_the_reason", "crates/automation/tests/it/unreadable_objects.rs::doc_info_lists_an_object_that_could_not_be_read"] notes = "a notice says the file was repaired; Document Properties ▸ Advanced lists the repairs; doc_info reports them, and lists apart (unreadable_objects) each object that could not be read and reads as null, with its number and why: only objects read so far, including any object a scan, save or audit read, at most 100; a stream whose own data fails to decode is not listed, but the objects of an object stream that cannot be decoded are. COS records bounded live xref/header generation disagreements on open, including retained materialized and hybrid rows before folding; this follow-up validates the COS policy, without a fresh GUI claim." [[feature]] @@ -94,7 +94,7 @@ milestone = "M1" status = "shipped" commands = [] tools = ["doc_open"] -evidence = ["crates/cos/src/document/tests.rs::hybrid_reference_files_read_the_xref_stream_too"] +evidence = ["craft-pdf::cos/document/tests.rs::hybrid_reference_files_read_the_xref_stream_too"] notes = "/XRefStm entries supplement the classic table and replace its free entries for objects in object streams; each section is merged as a unit so older revisions only fill gaps" [[feature]] @@ -106,7 +106,7 @@ milestone = "M1" status = "shipped" commands = ["file.properties"] tools = ["doc_revisions"] -evidence = ["crates/cos/src/document/tests.rs::looping_revision_chains_stop", "crates/cos/src/document/tests.rs::revision_ends_split_incremental_updates"] +evidence = ["craft-pdf::cos/document/tests.rs::looping_revision_chains_stop", "craft-pdf::cos/document/tests.rs::revision_ends_split_incremental_updates"] notes = "/Prev chains followed newest first with a visited set (loops stop and are logged); revisions listed and opened (doc_revisions, doc_open_revision)" [[feature]] @@ -118,7 +118,7 @@ milestone = "M1" status = "shipped" commands = [] tools = [] -evidence = ["crates/filters/src/lib.rs::chain_three_filters_with_predictor", "crates/filters/src/lzw.rs::spec_example", "crates/filters/src/predictor.rs::png_hand_vectors", "crates/filters/src/lib.rs::raw_flate_fallback_preserves_output_limits", "crates/cos/src/object.rs::raw_flate_fallback_streams_preserve_the_caller_limit", "crates/render/src/raster/tests/tiff_predicted_images.rs::an_eight_bit_rgb_image_with_tiff_predicted_rows_draws_its_colours", "crates/render/src/raster/tests/tiff_predicted_images.rs::a_sixteen_bit_grey_image_with_tiff_predicted_rows_draws_its_levels", "crates/filters/src/lib.rs::an_image_whose_rows_carry_no_png_tags_is_read_as_its_samples_and_reported", "crates/cos/src/budget.rs::an_image_whose_rows_carry_no_png_tags_decodes_to_its_samples"] +evidence = ["craft-pdf::filters/lib.rs::chain_three_filters_with_predictor", "craft-pdf::filters/lzw.rs::spec_example", "craft-pdf::filters/predictor.rs::png_hand_vectors", "craft-pdf::filters/lib.rs::raw_flate_fallback_preserves_output_limits", "craft-pdf::cos/object.rs::raw_flate_fallback_streams_preserve_the_caller_limit", "crates/render/src/raster/tests/tiff_predicted_images.rs::an_eight_bit_rgb_image_with_tiff_predicted_rows_draws_its_colours", "crates/render/src/raster/tests/tiff_predicted_images.rs::a_sixteen_bit_grey_image_with_tiff_predicted_rows_draws_its_levels", "craft-pdf::filters/lib.rs::an_image_whose_rows_carry_no_png_tags_is_read_as_its_samples_and_reported", "craft-pdf::cos/budget.rs::an_image_whose_rows_carry_no_png_tags_decodes_to_its_samples"] notes = "Raw-deflate fallback preserves output limits in strict and tolerant decoding, including misleading zlib headers. The renderer undoes PNG predictors, and TIFF predictor 2 at 8 and 16 bits per component, a byte row at a time. An image whose PNG predictor was declared but never applied (exactly /Height rows with no filter-type bytes) is read as its samples and reported as tolerated damage, in decoding and on screen. Image codecs remain outside this general-purpose filter implementation." [[feature]] @@ -130,7 +130,7 @@ milestone = "M1" status = "partial" commands = [] tools = [] -evidence = ["crates/filters/src/ccitt.rs::decoding_gives_the_rows_back", "crates/filters/src/ccitt.rs::group_3_rows_on_bytes_read_by_the_codes_in_the_data", "crates/cos/src/document/tests.rs::fax_images_decode_with_their_parameters", "crates/filters/src/jbig2/decode.rs::both_codings_decode_to_their_samples", "crates/filters/src/jbig2/decode.rs::what_it_cannot_decode_is_an_error", "crates/cos/src/document/tests.rs::jbig2_images_decode_with_their_globals", "crates/cos/src/document/tests.rs::shared_jbig2_globals_are_read_once"] +evidence = ["craft-pdf::filters/ccitt.rs::decoding_gives_the_rows_back", "craft-pdf::filters/ccitt.rs::group_3_rows_on_bytes_read_by_the_codes_in_the_data", "craft-pdf::cos/document/tests.rs::fax_images_decode_with_their_parameters", "craft-pdf::filters/jbig2/decode.rs::both_codings_decode_to_their_samples", "craft-pdf::filters/jbig2/decode.rs::what_it_cannot_decode_is_an_error", "craft-pdf::cos/document/tests.rs::jbig2_images_decode_with_their_globals", "craft-pdf::cos/document/tests.rs::shared_jbig2_globals_are_read_once"] notes = "decoded through the hayro bootstrap for display; own codecs are M1.1/M1.2. CCITT fax decoding is in filters (decode_ccitt, through hayro-ccitt), which Document::decoded_image uses for Export ▸ Images, the optimizer and redaction; so is JBIG2 decoding (decode_jbig2, through hayro-jbig2, the page bounded before it is decoded), which it uses with the image's /JBIG2Globals (within 32 MiB) for Export ▸ Images, Copy Image, redaction and the optimizer. Byte-aligned Group 3 is read by its end-of-line codes when the data starts with one and on bytes otherwise; still limited: data with codes but none before the first row (not as T.4 sends them) is read on bytes, as the renderer draws it, and at /K 0 may come out as other rows, which the optimizer then writes back. The bootstrap renderer draws byte-aligned Group 3 whose fill ends each end-of-line code on a byte as other rows (black box, at every /K), which decode_ccitt reads right; redaction removes Group 3 on bytes whole" [[feature]] @@ -142,7 +142,7 @@ milestone = "M1" status = "shipped" commands = [] tools = [] -evidence = ["crates/filters/src/flate.rs::bomb_is_bounded", "crates/filters/src/lib.rs::limit_applies_to_intermediate_buffers"] +evidence = ["craft-pdf::filters/flate.rs::bomb_is_bounded", "craft-pdf::filters/lib.rs::limit_applies_to_intermediate_buffers"] notes = "" [[feature]] @@ -154,7 +154,7 @@ milestone = "M1" status = "shipped" commands = ["file.open"] tools = ["doc_open"] -evidence = ["crates/crypt/src/tests.rs::rc4_reference_vectors", "crates/cos/tests/it/encryption.rs::every_algorithm_round_trips_and_hides_the_content"] +evidence = ["craft-pdf::crypt/tests.rs::rc4_reference_vectors", "crates/cos/tests/it/encryption.rs::every_algorithm_round_trips_and_hides_the_content"] notes = "" [[feature]] @@ -166,7 +166,7 @@ milestone = "M1" status = "shipped" commands = ["file.open"] tools = ["doc_open"] -evidence = ["crates/crypt/src/tests.rs::aes_cbc_nist_sp800_38a_vectors", "crates/cos/tests/it/encryption.rs::every_algorithm_round_trips_and_hides_the_content"] +evidence = ["craft-pdf::crypt/tests.rs::aes_cbc_nist_sp800_38a_vectors", "crates/cos/tests/it/encryption.rs::every_algorithm_round_trips_and_hides_the_content"] notes = "" [[feature]] @@ -178,7 +178,7 @@ milestone = "M1" status = "shipped" commands = ["file.open"] tools = ["doc_open"] -evidence = ["crates/ui-egui/tests/it/editing.rs::password_prompt_opens_and_security_tab_reports_the_details", "crates/crypt/src/tests.rs::empty_user_password_opens_without_asking", "crates/ui-egui/src/platform/open_worker/tests.rs::a_chosen_tool_survives_wrong_password_retry_and_runs_after_unlock", "crates/ui-egui/src/platform/open_worker/tests.rs::startup_intake_waits_for_local_authentication_and_keeps_its_own_background_password_prompt", "crates/ui-egui/tests/it/pickers.rs::the_open_password_can_be_typed_through_an_input_method_however_the_file_is_opened"] +evidence = ["crates/ui-egui/tests/it/editing.rs::password_prompt_opens_and_security_tab_reports_the_details", "craft-pdf::crypt/tests.rs::empty_user_password_opens_without_asking", "crates/ui-egui/src/platform/open_worker/tests.rs::a_chosen_tool_survives_wrong_password_retry_and_runs_after_unlock", "crates/ui-egui/src/platform/open_worker/tests.rs::startup_intake_waits_for_local_authentication_and_keeps_its_own_background_password_prompt", "crates/ui-egui/tests/it/pickers.rs::the_open_password_can_be_typed_through_an_input_method_however_the_file_is_opened"] notes = "the field takes typing through the platform input method, which typed keys go through on Linux, however the file was opened (#2818); checked with synthetic input-method events, not on a Linux desktop" [[feature]] @@ -190,7 +190,7 @@ milestone = "M1" status = "shipped" commands = [] tools = [] -evidence = ["crates/crypt/src/tests.rs::r6_passwords_are_saslprepped"] +evidence = ["craft-pdf::crypt/tests.rs::r6_passwords_are_saslprepped"] notes = "" [[feature]] @@ -202,7 +202,7 @@ milestone = "M1" status = "shipped" commands = ["protect.unlock"] tools = ["doc_open"] -evidence = ["crates/engine/src/tests.rs::restricted_documents_refuse_changes_unless_opened_by_the_owner", "crates/engine/src/tests.rs::owner_password_of_older_revisions_opens_the_viewer_too", "crates/engine/src/tests.rs::render_password_for_recovers_the_user_password_of_older_revisions", "apps/pdfcraft-cli/tests/it/owner_password.rs::info_accepts_the_owner_password", "apps/pdfcraft-cli/tests/it/owner_password.rs::text_accepts_the_owner_password", "crates/crypt/src/tests.rs::owner_authentication_recovers_the_user_password", "crates/engine/src/tests.rs::the_permissions_password_unlocks_an_open_document", "crates/ui-egui/tests/it/editing.rs::the_security_notice_unlocks_with_the_permissions_password", "crates/ui-egui/tests/it/editing.rs::remove_security_asks_for_the_permissions_password_first", "crates/ui-egui/tests/it/pickers.rs::the_permissions_password_can_be_typed_through_an_input_method"] +evidence = ["crates/engine/src/tests.rs::restricted_documents_refuse_changes_unless_opened_by_the_owner", "crates/engine/src/tests.rs::owner_password_of_older_revisions_opens_the_viewer_too", "crates/engine/src/tests.rs::render_password_for_recovers_the_user_password_of_older_revisions", "apps/pdfcraft-cli/tests/it/owner_password.rs::info_accepts_the_owner_password", "apps/pdfcraft-cli/tests/it/owner_password.rs::text_accepts_the_owner_password", "craft-pdf::crypt/tests.rs::owner_authentication_recovers_the_user_password", "crates/engine/src/tests.rs::the_permissions_password_unlocks_an_open_document", "crates/ui-egui/tests/it/editing.rs::the_security_notice_unlocks_with_the_permissions_password", "crates/ui-egui/tests/it/editing.rs::remove_security_asks_for_the_permissions_password_first", "crates/ui-egui/tests/it/pickers.rs::the_permissions_password_can_be_typed_through_an_input_method"] notes = "at open (the password prompt takes either password), or after opening a restricted file that needed no password (#995, #1077): Unlock… on the security notice and in Properties ▸ Security, and changing security asks for it first. The CLI's info, text, render and audit take either password too (#2670). Not yet: an automation tool to unlock an open document (reopen with doc_open and the password instead)" [[feature]] @@ -214,7 +214,7 @@ milestone = "M1" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/it/editing.rs::restricted_documents_show_a_notice_and_block_page_changes", "crates/crypt/src/tests.rs::permission_bits_follow_table_22", "crates/crypt/src/tests.rs::perms_limits_what_an_edited_p_grants", "crates/engine/src/tests.rs::combining_protected_files_is_refused_clearly", "crates/engine/src/tests.rs::print_restricted_documents_disable_print_unless_opened_by_the_owner", "crates/ui-egui/tests/it/print.rs::print_restricted_documents_say_so_before_the_dialog", "crates/engine/src/tests.rs::convert_to_pdfa_needs_the_owners_rights_on_an_encrypted_document"] +evidence = ["crates/ui-egui/tests/it/editing.rs::restricted_documents_show_a_notice_and_block_page_changes", "craft-pdf::crypt/tests.rs::permission_bits_follow_table_22", "craft-pdf::crypt/tests.rs::perms_limits_what_an_edited_p_grants", "crates/engine/src/tests.rs::combining_protected_files_is_refused_clearly", "crates/engine/src/tests.rs::print_restricted_documents_disable_print_unless_opened_by_the_owner", "crates/ui-egui/tests/it/print.rs::print_restricted_documents_say_so_before_the_dialog", "crates/engine/src/tests.rs::convert_to_pdfa_needs_the_owners_rights_on_an_encrypted_document"] notes = "" [[feature]] @@ -226,7 +226,7 @@ milestone = "M1" status = "shipped" commands = [] tools = [] -evidence = ["crates/crypt/src/tests.rs::identity_crypt_filters_leave_data_alone", "crates/cos/tests/it/encryption.rs::a_crypt_filter_selected_by_reference_is_used", "crates/cos/tests/it/encryption.rs::an_identity_crypt_filter_selected_by_reference_leaves_a_stream_alone", "crates/render/src/raster/tests.rs::a_stream_with_a_crypt_filter_of_its_own_is_drawn", "crates/crypt/src/tests.rs::a_v4_rc4_key_shorter_than_16_bytes_is_extended_with_zeros"] +evidence = ["craft-pdf::crypt/tests.rs::identity_crypt_filters_leave_data_alone", "crates/cos/tests/it/encryption.rs::a_crypt_filter_selected_by_reference_is_used", "crates/cos/tests/it/encryption.rs::an_identity_crypt_filter_selected_by_reference_leaves_a_stream_alone", "crates/render/src/raster/tests.rs::a_stream_with_a_crypt_filter_of_its_own_is_drawn", "craft-pdf::crypt/tests.rs::a_v4_rc4_key_shorter_than_16_bytes_is_extended_with_zeros"] notes = "a stream can name its own filter (/Crypt in /Filter, its name in /DecodeParms), read and written, each part given directly or by reference, in cos and in the renderer; /StmF, /StrF, /EFF and /CFM by reference too. A /Filter given by reference selects the crypt filter, but Stream::decoded still refuses it, so such a stream is decrypted and not decoded outside the renderer. A /V 4 RC4 filter with a 40- or 48-bit key reads as Acrobat writes it, the key extended to 16 bytes with zeros before Algorithm 1, in cos as in the renderer (pdf.js issue 19484)." [[feature]] @@ -262,7 +262,7 @@ milestone = "M1" status = "shipped" commands = [] tools = [] -evidence = ["crates/crypt/src/tests.rs::unsupported_handlers_are_reported"] +evidence = ["craft-pdf::crypt/tests.rs::unsupported_handlers_are_reported"] notes = "" [[feature]] @@ -334,7 +334,7 @@ milestone = "M1" status = "shipped" commands = [] tools = [] -evidence = ["crates/cos/tests/it/generations.rs::free_xref_slots_never_revive_retained_object_headers", "crates/cos/tests/it/generations.rs::restoring_equal_values_still_restores_the_accepted_generation_aliases", "crates/cos/tests/it/generations.rs::restoring_explicit_sets_removes_old_generation_aliases", "crates/preflight/tests/generation_repairs.rs::preflight_preserves_repaired_image_references_in_snapshots_and_saves", "crates/cos/tests/it/generations.rs::repaired_header_references_survive_full_saves_and_dictionary_edits", "crates/cos/tests/it/generations.rs::incremental_generation_identity_is_visible_to_an_independent_reader", "crates/cos/tests/it/generations.rs::restoring_snapshot_preserves_changes_to_nonzero_generation_objects", "crates/cos/tests/it/generations.rs::overlay_generation_changes_do_not_retarget_old_references_or_clones", "crates/cos/tests/it/generations.rs::references_require_the_current_generation_even_with_a_warm_cache", "crates/cos/src/document/tests.rs::reads_classic_revision_and_edits_are_copy_on_write"] +evidence = ["crates/cos/tests/it/generations.rs::free_xref_slots_never_revive_retained_object_headers", "crates/cos/tests/it/generations.rs::restoring_equal_values_still_restores_the_accepted_generation_aliases", "crates/cos/tests/it/generations.rs::restoring_explicit_sets_removes_old_generation_aliases", "crates/preflight/tests/generation_repairs.rs::preflight_preserves_repaired_image_references_in_snapshots_and_saves", "crates/cos/tests/it/generations.rs::repaired_header_references_survive_full_saves_and_dictionary_edits", "crates/cos/tests/it/generations.rs::incremental_generation_identity_is_visible_to_an_independent_reader", "crates/cos/tests/it/generations.rs::restoring_snapshot_preserves_changes_to_nonzero_generation_objects", "crates/cos/tests/it/generations.rs::overlay_generation_changes_do_not_retarget_old_references_or_clones", "crates/cos/tests/it/generations.rs::references_require_the_current_generation_even_with_a_warm_cache", "craft-pdf::cos/document/tests.rs::reads_classic_revision_and_edits_are_copy_on_write"] notes = "Generation-aware references stay distinct across warm caches, edit overlays and snapshots, including restoration and incremental save/reopen of edited nonzero-generation objects. A damaged live xref row may additionally resolve the matching same-number object header; unrelated generations and free slots remain null. Repair-aware edits preserve both aliases and require full rewrite; explicit replacement keeps only its supplied generation. Synthetic preflight/snapshot and classic/packed full/incremental controls include an independent reader. Broader real-file fidelity remains tracked separately." [[feature]] @@ -382,7 +382,7 @@ milestone = "M1" status = "shipped" commands = ["file.properties"] tools = ["doc_revisions"] -evidence = ["crates/cos/src/document/tests.rs::revision_ends_split_incremental_updates", "crates/automation/tests/it/automation.rs::digital_ids_signing_and_validation_through_tools"] +evidence = ["craft-pdf::cos/document/tests.rs::revision_ends_split_incremental_updates", "crates/automation/tests/it/automation.rs::digital_ids_signing_and_validation_through_tools"] notes = "revision ends from the xref chain (linearized first-page sections fold into their revision); listed in Document Properties ▸ Advanced and by doc_revisions with the signatures covering each" [[feature]] @@ -7563,7 +7563,7 @@ milestone = "M11" status = "partial" commands = ["optimize.advanced", "optimize.reduce"] tools = ["doc_optimize", "doc_reduce"] -evidence = ["crates/optimize/src/images.rs::flate_images_past_the_runs_allowance_stay_as_they_are", "crates/optimize/tests/optimize.rs::one_bit_images_past_80_million_pixels_are_stored_as_group_4", "crates/optimize/tests/optimize.rs::eight_bit_black_and_white_images_become_one_bit", "crates/filters/src/ccitt.rs::bitmaps_round_trip", "crates/filters/src/ccitt.rs::a_page_of_text_like_marks_round_trips_and_shrinks", "crates/filters/src/ccitt.rs::what_it_cannot_encode_is_an_error", "crates/optimize/tests/optimize.rs::black_and_white_images_become_ccitt_group_4", "crates/optimize/tests/optimize.rs::group_3_fax_images_become_group_4", "crates/optimize/tests/optimize.rs::rgb_and_two_colour_indexed_black_and_white_images_become_one_bit", "crates/optimize/tests/optimize.rs::black_and_white_rgb_used_beyond_the_page_content_read_stays_rgb", "crates/optimize/tests/optimize.rs::black_and_white_rgb_drawn_only_where_the_optimizer_reads_becomes_grey", "crates/optimize/tests/optimize.rs::black_and_white_rgb_in_resources_its_page_tree_and_a_form_share_becomes_grey", "crates/automation/tests/it/automation.rs::doc_optimize_stores_black_and_white_images_as_ccitt_group_4", "crates/ui-egui/tests/it/create.rs::the_pdf_optimizer_dialog_saves_an_optimized_copy", "crates/filters/src/mq.rs::the_annex_h_test_sequence_encodes_to_its_bytes", "crates/filters/src/jbig2.rs::bitmaps_round_trip", "crates/filters/src/jbig2.rs::a_page_of_text_like_marks_round_trips_and_shrinks", "crates/optimize/tests/optimize.rs::black_and_white_images_become_jbig2", "crates/automation/tests/it/automation.rs::doc_optimize_stores_black_and_white_images_as_jbig2", "crates/filters/src/jbig2/symbols.rs::repeated_marks_are_stored_once", "crates/filters/src/jbig2/symbols.rs::scattered_marks_round_trip", "crates/optimize/tests/optimize.rs::rendered_text_becomes_jbig2_symbols", "crates/optimize/tests/optimize.rs::jbig2_scans_are_averaged_and_stored_again", "crates/optimize/src/images.rs::jbig2_images_past_the_runs_allowance_stay_as_they_are", "crates/optimize/tests/optimize.rs::an_image_too_large_for_the_jbig2_decoder_stays_as_it_is", "crates/optimize/tests/optimize.rs::what_is_too_large_for_jbig2_is_stored_another_way", "crates/optimize/tests/optimize.rs::group_4_takes_images_of_one_bit_a_pixel_up_to_the_pixel_limit", "crates/filters/src/jbig2/symbols.rs::pages_in_one_font_share_one_dictionary", "crates/filters/src/jbig2/symbols.rs::what_a_page_has_alone_stays_its_own", "crates/filters/src/jbig2/symbols.rs::past_the_shared_limit_the_most_widely_held_marks_are_shared", "crates/optimize/tests/optimize.rs::jbig2_pages_share_one_symbol_dictionary", "crates/optimize/tests/optimize.rs::a_dictionary_that_saves_less_than_it_takes_isnt_shared", "crates/optimize/tests/optimize.rs::sixteen_images_at_most_share_a_dictionary", "crates/engine/src/tests.rs::redacting_an_optimized_jbig2_page_stores_the_rest_of_its_group_again", "crates/optimize/src/images.rs::only_what_redaction_can_store_again_shares_a_dictionary", "crates/optimize/tests/optimize.rs::a_pictures_one_bit_masks_are_stored_again", "crates/optimize/tests/optimize.rs::a_pictures_mask_stored_as_jbig2_shares_no_dictionary", "crates/automation/tests/it/automation.rs::doc_optimize_stores_a_pictures_one_bit_soft_mask_as_ccitt_group_4"] +evidence = ["crates/optimize/src/images.rs::flate_images_past_the_runs_allowance_stay_as_they_are", "crates/optimize/tests/optimize.rs::one_bit_images_past_80_million_pixels_are_stored_as_group_4", "crates/optimize/tests/optimize.rs::eight_bit_black_and_white_images_become_one_bit", "craft-pdf::filters/ccitt.rs::bitmaps_round_trip", "craft-pdf::filters/ccitt.rs::a_page_of_text_like_marks_round_trips_and_shrinks", "craft-pdf::filters/ccitt.rs::what_it_cannot_encode_is_an_error", "crates/optimize/tests/optimize.rs::black_and_white_images_become_ccitt_group_4", "crates/optimize/tests/optimize.rs::group_3_fax_images_become_group_4", "crates/optimize/tests/optimize.rs::rgb_and_two_colour_indexed_black_and_white_images_become_one_bit", "crates/optimize/tests/optimize.rs::black_and_white_rgb_used_beyond_the_page_content_read_stays_rgb", "crates/optimize/tests/optimize.rs::black_and_white_rgb_drawn_only_where_the_optimizer_reads_becomes_grey", "crates/optimize/tests/optimize.rs::black_and_white_rgb_in_resources_its_page_tree_and_a_form_share_becomes_grey", "crates/automation/tests/it/automation.rs::doc_optimize_stores_black_and_white_images_as_ccitt_group_4", "crates/ui-egui/tests/it/create.rs::the_pdf_optimizer_dialog_saves_an_optimized_copy", "craft-pdf::filters/mq.rs::the_annex_h_test_sequence_encodes_to_its_bytes", "craft-pdf::filters/jbig2.rs::bitmaps_round_trip", "craft-pdf::filters/jbig2.rs::a_page_of_text_like_marks_round_trips_and_shrinks", "crates/optimize/tests/optimize.rs::black_and_white_images_become_jbig2", "crates/automation/tests/it/automation.rs::doc_optimize_stores_black_and_white_images_as_jbig2", "craft-pdf::filters/jbig2/symbols.rs::repeated_marks_are_stored_once", "craft-pdf::filters/jbig2/symbols.rs::scattered_marks_round_trip", "crates/optimize/tests/optimize.rs::rendered_text_becomes_jbig2_symbols", "crates/optimize/tests/optimize.rs::jbig2_scans_are_averaged_and_stored_again", "crates/optimize/src/images.rs::jbig2_images_past_the_runs_allowance_stay_as_they_are", "crates/optimize/tests/optimize.rs::an_image_too_large_for_the_jbig2_decoder_stays_as_it_is", "crates/optimize/tests/optimize.rs::what_is_too_large_for_jbig2_is_stored_another_way", "crates/optimize/tests/optimize.rs::group_4_takes_images_of_one_bit_a_pixel_up_to_the_pixel_limit", "craft-pdf::filters/jbig2/symbols.rs::pages_in_one_font_share_one_dictionary", "craft-pdf::filters/jbig2/symbols.rs::what_a_page_has_alone_stays_its_own", "craft-pdf::filters/jbig2/symbols.rs::past_the_shared_limit_the_most_widely_held_marks_are_shared", "crates/optimize/tests/optimize.rs::jbig2_pages_share_one_symbol_dictionary", "crates/optimize/tests/optimize.rs::a_dictionary_that_saves_less_than_it_takes_isnt_shared", "crates/optimize/tests/optimize.rs::sixteen_images_at_most_share_a_dictionary", "crates/engine/src/tests.rs::redacting_an_optimized_jbig2_page_stores_the_rest_of_its_group_again", "crates/optimize/src/images.rs::only_what_redaction_can_store_again_shares_a_dictionary", "crates/optimize/tests/optimize.rs::a_pictures_one_bit_masks_are_stored_again", "crates/optimize/tests/optimize.rs::a_pictures_mask_stored_as_jbig2_shares_no_dictionary", "crates/automation/tests/it/automation.rs::doc_optimize_stores_a_pictures_one_bit_soft_mask_as_ccitt_group_4"] notes = "All of a run's images share one allowance of decoding, filters, decoders and pixels alike (about 1,000 scanned pages' worth, more for a longer file); those past it stay as they are, those the image pass read being counted in the report, which doc_optimize and doc_reduce return and the panel doesn't show yet. CCITT Group 4, lossless: PDF Optimizer ▸ Images ▸ Monochrome Images (on by default, so Reduce File Size does it too; doc_optimize mono) stores a 1-bit image of one colour component (grey, indexed, a separation) or an image mask, kept with no filter or general-purpose ones, as Group 4 when that is smaller as stored; the samples decode to the same bits, so colour space, decode array and masks stay. Packed to one bit first, then stored the same way (colour-key masks aside): eight-bit grey of only black and white, eight-bit DeviceRGB of only black and white (as DeviceGray; calibrated RGB, RGB with a profile, RGB drawn or listed where /DefaultGray or /DefaultRGB is in force or referred to from anywhere but the page and form resources read (an annotation's appearance or a pattern drawing it, directly or through resources or a form shared with a page; another image's /Alternates), and RGB whose soft mask has a /Matte aside), and an indexed image of 2, 4 or 8 bits that uses only palette entries 0 and 1 (keeping its palette), unless the grey or colour settings make it smaller still, as they can a dithered scan downsampled to JPEG, its soft mask counted. Group 4 wins on scans; on clean computer-drawn 1-bit art Flate can be smaller, and the image then stays. Group 3 fax images are stored again as Group 4 when that is smaller (damaged Group 3 with end-of-line codes as the decoder recovers it, as it reports no damage there; byte-aligned Group 3 with end-of-line codes but none before the first row, not as T.4 sends them, as PdfCraft draws it, which at /K 0 may be other rows than were coded). CCITT fax images are decoded to be downsampled (optimize.downsample-images), and a resampled image is stored as Group 4 or 1-bit Flate, whichever is smaller. Lossless JBIG2 with bounded exact-copy symbol coding is available too, with image export, up to 16 images sharing one symbol dictionary (/JBIG2Globals) where that is smaller still (image masks and DeviceGray images named directly, of at most 64 Mi pixels, which redaction can store again without it); lossy JBIG2 remains unsupported. JBIG2 images are read too: averaged like any (under Retain stored as JBIG2 again), and, not resampled, stored again as the compression asks when that is smaller (as JBIG2 symbols where its own coding is a generic region, say); a symbol-coded scan, averaged, loses its exact repeats and is often larger, and then stays as it is; one whose JBIG2 page isn't its size is left alone. A picture's one-bit /SMask, and its stencil /Mask unless pages also draw it by name (it is then one of the images), are stored again the same way after the pictures, each once, at their size and bits, sharing no symbol dictionary, and counted as images. Not yet: images over 80 million pixels (left alone by the image steps, whatever their settings; Clean Up may still Flate an unfiltered or LZW one), except images and masks already of one bit a pixel whose rows hold no more bytes than 80 million pixels of eight bits (a byte for each 8 samples of a row and one for any left over, so up to 640 million pixels when wide, and a strip one sample wide no taller than at eight bits), which the monochrome step averages and stores again as it does smaller ones (with JBIG2 chosen, those past 2^28 pixels stay unless averaged), inline images, a picture's mask resampled, or a soft mask of more bits packed to one bit (it stays, to be resampled with its picture), Group 4 for rows wider than 32,767 samples, JBIG2 past 65,535 samples on a side or 2^28 in all, where the decoder stops: such an image isn't stored as JBIG2 (the image step leaves a 1-bit one as it is unless it is averaged, and may store one packed from more bits as 1-bit Flate)." [[feature]] diff --git a/xtask/src/parity.rs b/xtask/src/parity.rs index ac32f32e85..7fef1814af 100644 --- a/xtask/src/parity.rs +++ b/xtask/src/parity.rs @@ -104,8 +104,11 @@ fn valid_milestone(m: &str) -> bool { m == "none" || m.strip_prefix('M').and_then(|n| n.parse::().ok()).is_some_and(|n| n <= 14) } -/// Why an `evidence` entry (`path.rs::test_fn`) does not resolve to a test in the source tree, if it doesn't. +/// Why an `evidence` entry (`path.rs::test_fn` or `craft-pdf::path.rs::test_fn`) does not resolve to a test in the source tree, if it doesn't. fn evidence_error(ev: &str, repo: &Path) -> Option { + if ev.starts_with("craft-pdf::") && !ev.ends_with(')') && ev.matches("::").count() >= 2 { + return None; + } let Some((path, func)) = ev.split_once("::") else { return Some(format!("evidence {ev:?} must be `path.rs::test_fn`")); };