Files
pdfcraft/crates/print/src/range.rs
T
mtlcl410andClaude Opus 5.5 4825b67d15 M10: odd and even pages are page numbers, as Acrobat counts them (#3984) (#4034)
* M10: odd and even pages are page numbers, as Acrobat counts them (#3984)

Even pages only with the range 4–11 printed pages 5, 7, 9 and 11: `narrow` took odd and
even places in the chosen pages, and its comment said Acrobat does the same. Acrobat
counts page numbers: "if you set the range to 2, 7–10 and choose Even Pages Only, only
pages 2, 8, and 10 print" (Adobe Acrobat DC Help, last updated 11/28/2019, Options in the
Print dialog box, p. 515; the same example in Using Adobe Acrobat 9 Pro, p. 413). Odd and
even now keep the pages whose number is odd or even, for a typed range, picked thumbnails
and the current page alike, in the order given, and Reverse turns that round. The header,
footer and watermark ranges (marks_ui) and Extract's odd/even filter in #3729 already
count page numbers.

The new test pins Acrobat's example and the report's ranges. select_listed's tests and
cut_stack_printing_through_tools counted places; the latter now asks for even pages of
2–10, which by page number are the 2, 4, 6, 8 and 10 it printed before, so its expected
output is unchanged and would fail with places (3, 5, 7, 9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M10: say that odd and even are page numbers in doc_print, and what page labels still lack (#3984)

Review on #4034: the doc_print description says odd/even count page numbers, and the parity note says a document with page labels still counts page numbers there, which the Acrobat help consulted doesn't settle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M10: cite the cut-stack print test at its path under tests/it (#3984)

main moved the automation integration tests into tests/it, so the
evidence path added for odd/even printing no longer existed and
`cargo xtask parity` failed after the rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:02 -04:00

118 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Pages to Print: all, a range ("1-3, 6, 9-", page labels allowed), odd or even pages, reverse.
use crate::PrintError;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Subset {
#[default]
All,
Odd,
Even,
}
/// Resolve one range token: a page label, or a number (1-based).
fn page_of(tok: &str, count: usize, labels: &[String]) -> Result<usize, PrintError> {
let t = tok.trim();
// Page labels win over numbers (Acrobat's "use logical page numbers").
if let Some(i) = labels.iter().position(|l| l == t) {
return Ok(i);
}
match t.parse::<usize>() {
Ok(n) if n >= 1 && n <= count => Ok(n - 1),
Ok(n) => Err(PrintError::OutOfRange(n, count)),
Err(_) => Err(PrintError::NotAPage(t.to_string())),
}
}
/// Split a range whose endpoint is a logical page label containing a hyphen, such
/// as "A-1-A-3" or "A-2-". The plain range parser must not mistake the first
/// hyphen inside the label for the delimiter between endpoints.
fn hyphenated_label_range(part: &str, count: usize, labels: &[String]) -> Option<(usize, usize)> {
for (split, _) in part.match_indices('-') {
let (Some(first), Some(last)) = (part.get(..split), part.get(split + 1..)) else { continue };
let (first, last) = (first.trim(), last.trim());
// Avoid changing the meaning of ordinary numeric and open-ended ranges.
if !labels.iter().any(|l| l.contains('-') && (l == first || l == last)) {
continue;
}
let from = if first.is_empty() { Some(0) } else { page_of(first, count, labels).ok() };
let to = if last.is_empty() { count.checked_sub(1) } else { page_of(last, count, labels).ok() };
if let (Some(from), Some(to)) = (from, to) {
return Some((from, to));
}
}
None
}
/// The pages to print (0-based, in print order). `range` is `None` for all pages; it may list
/// numbers and labels with `-` ranges (open-ended allowed: `5-`, `-3`). The subset keeps the odd or
/// even pages of those chosen, by page number (see [`narrow`]).
pub fn select_pages(count: usize, range: Option<&str>, labels: &[String], subset: Subset, reverse: bool) -> Result<Vec<usize>, PrintError> {
let mut pages = Vec::new();
match range.map(str::trim).filter(|r| !r.is_empty()) {
None => pages.extend(0..count),
Some(r) => {
for part in r.split([',', ';']).map(str::trim).filter(|p| !p.is_empty()) {
// A complete label (e.g. "A-1") still wins over interpreting it as a range.
// A range of such labels (e.g. "A-1-A-3") needs a later '-' split.
if !labels.iter().any(|l| l == part)
&& let Some((from, to)) = hyphenated_label_range(part, count, labels)
{
if from <= to {
pages.extend(from..=to);
} else {
pages.extend((to..=from).rev());
}
continue;
}
match part.split_once('-') {
// A label may itself contain a dash ("A-1"): try the whole token first.
Some(_) if labels.iter().any(|l| l == part) => pages.push(page_of(part, count, labels)?),
Some((a, b)) => {
let from = if a.trim().is_empty() { 0 } else { page_of(a, count, labels)? };
let to = if b.trim().is_empty() { count.saturating_sub(1) } else { page_of(b, count, labels)? };
if from <= to {
pages.extend(from..=to);
} else {
pages.extend((to..=from).rev());
}
}
None => pages.push(page_of(part, count, labels)?),
}
}
}
}
narrow(pages, subset, reverse)
}
/// The pages to print from an explicit list (0-based, e.g. the thumbnails selected in the
/// interface), in the order given. Unlike a typed range the entries are page positions, never
/// labels. The subset and `reverse` apply as in [`select_pages`].
pub fn select_listed(count: usize, pages: &[usize], subset: Subset, reverse: bool) -> Result<Vec<usize>, PrintError> {
if let Some(p) = pages.iter().find(|p| **p >= count) {
return Err(PrintError::OutOfRange(p.saturating_add(1), count));
}
narrow(pages.to_vec(), subset, reverse)
}
/// The odd or even pages of those chosen, by page number (the first page of the document is 1,
/// odd; `pages` are 0-based), then the print order. Acrobat counts the same way (#3984): "if you
/// set the range to 2, 7–10 and choose Even Pages Only, only pages 2, 8, and 10 print" (Adobe
/// Acrobat DC Help, last updated 11/28/2019, Options in the Print dialog box, p. 515; the same
/// example in Using Adobe Acrobat 9 Pro, p. 413). Counting places in the range instead printed
/// 7 and 9 there, and a range starting on an even page started its "even" pages on an odd one.
fn narrow(pages: Vec<usize>, subset: Subset, reverse: bool) -> Result<Vec<usize>, PrintError> {
let mut out: Vec<usize> = match subset {
Subset::All => pages,
Subset::Odd => pages.into_iter().filter(|p| p % 2 == 0).collect(),
Subset::Even => pages.into_iter().filter(|p| p % 2 == 1).collect(),
};
if reverse {
out.reverse();
}
if out.is_empty() {
return Err(PrintError::NoPages);
}
Ok(out)
}