Skip to content

[Regression 2.1.1 → 2.2.x] pdftopdf no longer emits its %%PDFTOPDF* marker comments, so libppd re-applies page-ranges and silently drops pages #233

Description

@kragol

Summary

cfFilterPDFToPDF() in 2.1.1 tagged its output with % This file was generated by pdftopdf / %%PDFTOPDFNumCopies / %%PDFTOPDFCollate. The PDFio rewrite in 2.2.x emits none of them. libppd's ppdFilterPDFToPS() uses those markers to know that page-level options were already applied upstream; without them it forwards page-ranges to its pstops stage, which applies the range a second time — to a document already reduced to exactly that range. Unless the range starts at page 1, the second selection re-indexes into the already-trimmed document, so the job silently loses pages or comes out empty altogether — and foomatic-rip still exits 0, with CUPS recording job-completed-successfully.

The same missing markers are likely behind #226 and #231 (copy count lost), which are the %%PDFTOPDFNumCopies half of the same contract.

Environment

Arch Linux x86_64 — cups 2.4.19, cups-filters 2.0.1, libcupsfilters 2.2.1, libppd 2.1.1, poppler 26.08.0, ghostscript 10.07.1.

PPD Dell C1765 Foomatic/foo2hbpl2 (foo2zjs):

*cupsFilter:	"application/vnd.cups-postscript 100 foomatic-rip"
*cupsFilter:	"application/vnd.cups-pdf 0 foomatic-rip"

application/vnd.cups-pdf costs 0, so cupsd routes application/postscript → universal → application/vnd.cups-pdf → foomatic-rip. foomatic-rip must then convert PDF back to PostScript for the driver.

Steps to reproduce

Any queue whose PPD is a Foomatic one with a PostScript-input driver, and any 20-page document with numbered pages:

lp -d QUEUE -o page-ranges=3-5   twenty-page.pdf
lp -d QUEUE -o page-ranges=10-15 twenty-page.pdf

Expected: 3 pages, then 6 pages.

Actual: 1 page — the original page 5 — then nothing at all. Both jobs report job-completed-successfully, with nothing logged at the default LogLevel.

Also seen in normal use

The same happens printing from a GUI application with every option left at its default except the page range — here Okular, which sends the whole document as PostScript plus a page-ranges attribute. With ranges far enough into the document to hit the empty case, the job ends job-completed-successfully with job-impressions-completed=0 and total 0 in page_log, having printed nothing.

Extracting the same pages beforehand and printing them with no page-ranges works.


Everything below is what I found investigating it, rather than part of the reproduction.

page-ranges is applied twice

The option is applied at two successive stages of the chain. universal/cfFilterPDFToPDF selects the pages, correctly. foomatic-rip then has to convert that PDF back to PostScript for the driver, and its pstops stage applies the same page-ranges again — to a document that by then contains only those pages.

Until 2.2.x a guard prevented exactly this: pdftopdf marked its output with a comment, and libppd skipped page-level options whenever it saw that marker. The PDFio rewrite stopped emitting the marker, so the guard no longer fires.

The first stage on its own is correct, and its output carries no marker:

cupsfilter -p QUEUE.ppd -m application/vnd.cups-pdf -o page-ranges=3-5 twenty-page.pdf > s1.pdf
pdftotext s1.pdf -                        # Page 3  Page 4  Page 5
head -c 4000 s1.pdf | grep -c PDFTOPDF    # 0   <- marker absent

Passing that correct 3-page selection to foomatic-rip with the same option re-applies it:

export PPD=$PWD/QUEUE.ppd PATH=/usr/lib/cups/filter:$PATH

# what should happen — the range was already applied upstream, so this stage is a no-op
foomatic-rip 1 u t 1 ""                s1.pdf > correct.prn

# what happens in 2.2.x — the range is applied a second time
foomatic-rip 1 u t 1 "page-ranges=3-5" s1.pdf > wrong.prn

pstops receives %%Pages: 3 in both runs, so it is re-selecting from the already-trimmed document. Substituting cat for the driver in the PPD makes that output countable: without the option, three pages (%%Page: 1 1, 2 2, 3 3); with it, a single page (%%Page: 3 1) whose content is Page 5.

Root cause

libppd/ppd/pdftops.c:578 strips page-management options from the pstops option list only when pdftopdfapplied is set:

if (pdftopdfapplied)
{
  for (j = 0, exclude = pstops_exclude_page_management[j]; exclude;
       j++, exclude = pstops_exclude_page_management[j])
    if (!strcasecmp(option->name, exclude))
      break;
  if (exclude)
    continue;
}

pstops_exclude_page_management[] already lists page-ranges, input-page-ranges, page-set, number-up, Collate, OutputOrder, … under a comment stating the intent exactly: "we have applied these options already to the PDF input … by use of the pdftopdf filter before this filter."

pdftopdfapplied is set only by parse_pdftopdf_comment() (pdftops.c:143), which scans the head of the PDF for %%PDFTOPDFNumCopies, %%PDFTOPDFCollate, or % This file was generated by pdftopdf.

libcupsfilters 2.1.1 wrote those comments at cupsfilters/pdftopdf/pdftopdf.cxx:964 (output.push_back("% This file was generated by pdftopdf")proc->set_comments(output)). In 2.2.x/master, cupsfilters/pdftopdf.c has no marker emission at all — grepping it for PDFTOPDF returns nothing — and its output begins %PDF-1.7 straight into objects.

So pdftopdfapplied stays 0 and the guard never fires.

Confirmed by running the universal filter over the same input against each library version. 2.1.1 emits all three markers, 2.2.1 emits none:

% This file was generated by pdftopdf
%%PDFTOPDFNumCopies : 1
%%PDFTOPDFCollate : false

Feeding each of those results to the same foomatic-rip isolates the effect — only the presence of the markers differs:

input produced by output bytes, no option output bytes, page-ranges=3-5
2.1.1 30744 30744 byte-identical — guard strips the option
2.2.1 30728 24476 range applied a second time

Scope

  • Seven stock PPDs, four drivers (foo2hbpl2, foo2zjs, foo2qpdl, foo2lava), and both the Ghostscript and the Poppler-pdftops renderer branch of ppdFilterPDFToPS().
  • Also reproduced on a second, independently installed Arch machine, on a default printer configuration with no manual settings at all. No local CUPS customisation on either machine — no MIME overrides in /etc/cups, no /etc/foomatic/filter.conf, cupsd.conf unmodified from the packaged default.
  • Any range not starting at page 1, because the second pass re-indexes into the already-trimmed document. page-ranges=3-5 on a 20-page file prints a single page — the original page 5 — with no error of any kind; ranges reaching further into the document lose everything and print nothing.
  • Silent — the job reports success and nothing appears in error_log at default LogLevel; at debug level the only trace is No pages left, outputting empty file. That part is intended handling for a genuine zero-page job (Graceful/Consistent handling of zero-page jobs cups-filters#117); the defect is that a valid selection becomes a zero-page job.
  • number-up, page-set, Collate and OutputOrder sit behind the same guard, so the same double application likely reaches them too.

Not the number-up interaction of OpenPrinting/cups-filters#690

That issue shows the same symptom and the same No pages left line, explained there as page-ranges counting impressions when number-up > 1 (apple/cups#4838). That does not apply here: number-up = 1 and copies = 1 throughout, and the input to the failing stage already contains exactly the requested pages, so the second application is redundant regardless of impression semantics.


Diagnosed and drafted with Claude (Claude Code), then reviewed, corrected and cut down by me before filing. Every command, number and code reference above was actually run or checked on the affected systems; hypotheses that did not survive testing were removed rather than left in as speculation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions