fix(pdf): indexed membership in page ranges, optional content and PDF/X scans (#2207)

This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-03 22:32:10 -05:00
1 parent 36da81e03b
commit 7faf7029fb
5 files changed
+28 -8

No files matched your search

+4 -1
View File
@@ -215,6 +215,8 @@ pub fn parse_page_range(s: &str, count: usize) -> Result<Vec<usize>> {
let bad = || PdfError::BadRange(s.to_string());
let num = |v: &str| v.trim().parse::<usize>().map_err(|_| bad());
let mut out = Vec::new();
// The pages selected so far, for ranges that overlap (`1-9, 2-8, …`): a set, not a search.
let mut selected = vec![false; count];
for part in t.split(',').map(str::trim).filter(|p| !p.is_empty()) {
let (a, b) = match part.split_once('-') {
Some((a, b)) => {
@@ -234,7 +236,8 @@ pub fn parse_page_range(s: &str, count: usize) -> Result<Vec<usize>> {
if p > count {
return Err(PdfError::BadPage(p));
}
if !out.contains(&(p - 1)) {
if let Some(new) = selected.get_mut(p - 1).filter(|s| !**s) {
*new = true;
out.push(p - 1);
}
}
+5 -2
View File
@@ -1,6 +1,7 @@
use hayro_syntax::Pdf;
use hayro_syntax::object::{Array, Dict, MaybeRef, Name, Object, ObjectIdentifier, Stream};
use hayro_syntax::reader::{Reader, ReaderExt};
use std::collections::HashSet;
use std::sync::Arc;
use crate::{PdfError, Result};
@@ -218,10 +219,12 @@ pub(crate) fn prepare(bytes: &Arc<Vec<u8>>, choice: Option<&[String]>) -> Result
}
Err(error) => return Err(error),
};
if on.iter().any(|id| off.contains(id)) {
let off_set: HashSet<_> = off.iter().copied().collect();
if on.iter().any(|id| off_set.contains(id)) {
return Err(invalid());
}
let incomplete = on.iter().chain(&off).any(|id| !groups.contains(id));
let group_set: HashSet<_> = groups.iter().copied().collect();
let incomplete = on.iter().chain(&off).any(|id| !group_set.contains(id));
groups.extend(on.iter().chain(&off).copied());
groups.sort();
groups.dedup();
+10 -4
View File
@@ -3,7 +3,7 @@
//! krilla's PDF 1.6 output, and a check of the PDF/X-4 requirements this exporter is responsible
//! for.
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use hayro_syntax::content::UntypedIter;
use hayro_syntax::object::{Array, Dict, Name, Object, ObjectIdentifier, Stream};
@@ -217,6 +217,7 @@ fn device_rgb(file: Option<&hayro_syntax::Pdf>) -> Vec<String> {
return Vec::new();
}
let mut found: Vec<(String, Vec<usize>)> = Vec::new();
let mut at: HashMap<String, usize> = HashMap::new();
for (i, page) in file.pages().iter().enumerate() {
let mut scan = RgbScan::default();
let res = page.resources();
@@ -227,9 +228,12 @@ fn device_rgb(file: Option<&hayro_syntax::Pdf>) -> Vec<String> {
scan.content(content, res, 0);
}
for what in scan.found {
match found.iter_mut().find(|(w, _)| *w == what) {
match at.get(&what).and_then(|k| found.get_mut(*k)) {
Some((_, pages)) => pages.push(i + 1),
None => found.push((what, vec![i + 1])),
None => {
at.insert(what.clone(), found.len());
found.push((what, vec![i + 1]));
}
}
}
}
@@ -274,14 +278,16 @@ pub(crate) const MAX_DEPTH: usize = 16;
/// What one page uses in device RGB, in the order found.
#[derive(Default)]
struct RgbScan {
/// What was found, in the order found; `known` is the same as a set.
found: Vec<String>,
known: HashSet<String>,
/// Forms, images and patterns already looked at on this page.
seen: HashSet<ObjectIdentifier>,
}
impl RgbScan {
fn add(&mut self, what: String) {
if !self.found.contains(&what) {
if self.known.insert(what.clone()) {
self.found.push(what);
}
}
+3 -1
View File
@@ -520,7 +520,9 @@ fn disallowed(cs: &Object<'_>, res: &Resources<'_>, depth: usize) -> Option<Stri
/// What one page has that PDF/X-1a doesn't allow, in the order found.
#[derive(Default)]
struct Scan {
/// What was found, in the order found; `known` is the same as a set.
found: Vec<String>,
known: HashSet<String>,
font_issues: Vec<String>,
seen: HashSet<ObjectIdentifier>,
fonts: HashSet<ObjectIdentifier>,
@@ -528,7 +530,7 @@ struct Scan {
impl Scan {
fn add(&mut self, what: String) {
if !self.found.contains(&what) {
if self.known.insert(what.clone()) {
self.found.push(what);
}
}
+6
View File
@@ -178,6 +178,12 @@ fn spreads_and_ranges() {
assert_eq!(parse_page_range("1-2, 4", 4).unwrap(), vec![0, 1, 3]);
assert_eq!(parse_page_range("3-", 4).unwrap(), vec![2, 3]);
assert_eq!(parse_page_range("", 2).unwrap(), vec![0, 1]);
// Ranges that overlap many times over: each page is selected once, in the order first named.
let overlapping = std::iter::repeat_n("1-20000, 5-19000", 500).collect::<Vec<_>>().join(",");
let t = std::time::Instant::now();
assert_eq!(parse_page_range(&overlapping, 20000).unwrap().len(), 20000);
assert!(t.elapsed() < std::time::Duration::from_secs(5), "{:?}", t.elapsed());
assert_eq!(parse_page_range("3, 1-3", 4).unwrap(), vec![2, 0, 1]);
assert!(parse_page_range("5", 4).is_err());
assert!(parse_page_range("x", 4).is_err());
let bytes = export_pdf(&d, &Cache::new(), &PdfOptions { pages: Some(vec![1, 2]), ..Default::default() }).unwrap();