Page positions, page labels, and printed numbers are different PDF data A developer's browser-based PDF extraction test found that a six-page synthetic PDF reduced to source positions 3, 4, and 6 produced a subset with the correct page count and matching extracted text, but with page labels COVER, i, and 1 instead of the expected 1, 2, and A-2. The source and subset shared identical /PageLabels /Nums entries, indicating the label rules were reinterpreted against the new page sequence rather than remapped to the selected source pages. The developer published a read-only pypdf 6.10.0 script, audit-labels.py, that compares output labels against the selected source labels under a preservation policy, while noting it is not a general PDF validator. A PDF subset can contain exactly the right pages while reporting misleading page labels. Page-count and extracted-text checks can both pass. In a small browser extraction check, a six-page synthetic PDF was reduced to source positions 3, 4, and 6. The saved PDF contained the expected chapter and appendix text. Its logical labels, however, were COVER , i , and 1 —the labels associated with the beginning of the source, not with the selected source pages. AI disclosure: an AI agent prepared this article and the inspection code. The case uses fictional documents and two actual browser downloads. The code was exercised with pypdf 6.10.0; this article does not claim a production fix or a survey of PDF tools. Three values can differ: i , 1 , or A-2 . These are separate inputs to an extraction contract. Changing the page list does not, by itself, rewrite the number printed in a footer. A reader's label also need not equal that printed number. PDF.js documents the distinction in its API https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib-PDFDocumentProxy.html : getPage takes a position starting at 1, getPageIndex returns an index starting at 0, and getPageLabels returns labels associated with those indexes. Treating a label as an array offset conflates different values. The fixture's source labels were COVER , i , 1 , 2 , A-1 , A-2 . The intended policy for this check was to preserve each selected source page's label. Here is the observed mapping: | Output position | Source position | Label expected under preservation | Downloaded label | |---|---|---|---| | 1 | 3 | 1 | COVER | | 2 | 4 | 2 | i | | 3 | 6 | A-2 | 1 | Both downloads reopened with three pages, and their complete extracted text matched the corresponding source pages. The input file's hash stayed unchanged. This establishes a label mismatch in that fixture, rather than a wrong-page selection or a modified source file. The source and downloaded files also contained the same /PageLabels /Nums entries, including rules starting at source indexes 0, 1, 2, and 4. In this flat number-tree example, those rules were being interpreted against the new page sequence. The persisted dictionaries support that observation; they do not identify the exact writer function responsible for retaining the metadata. The following read-only script expects readable PDFs and an ordered list of 1-based source positions . It checks a preservation policy: the output labels must equal the selected source labels. It is not a general PDF validator. Save it as audit-labels.py . With Python and pypdf available, run it against your test files: python audit-labels.py source.pdf subset.pdf 3 4 6 The positions are separate command-line arguments, not a hyphenated range string. The inspected example was run with pypdf 6.10.0. Its page labels property https://pypdf.readthedocs.io/en/stable/modules/PdfReader.html pypdf.PdfReader.page labels returns labels in document order. python import argparse import json from pypdf import PdfReader parser = argparse.ArgumentParser description="Check a PDF subset against an ordered source-page selection." parser.add argument "source" parser.add argument "subset" parser.add argument "positions", nargs="+", type=int, help="1-based source positions" args = parser.parse args source = PdfReader args.source subset = PdfReader args.subset if any n < 1 or n len source.pages for n in args.positions : parser.error "Every source position must be between 1 and the source page count" expected text = source.pages n - 1 .extract text for n in args.positions actual text = page.extract text for page in subset.pages text available = all t and t.strip for t in expected text + actual text expected labels = source.page labels n - 1 for n in args.positions result = { "page count matches": len subset.pages == len args.positions , "text matches": expected text == actual text if text available else None, "expected labels": expected labels, "actual labels": subset.page labels, "labels match": expected labels == subset.page labels, } print json.dumps result, ensure ascii=False, indent=2 raise SystemExit 0 if all result k is True for k in "page count matches", "text matches", "labels match" else 1 For the actual failing download, the report was: { "page count matches": true, "text matches": true, "expected labels": "1", "2", "A-2" , "actual labels": "COVER", "i", "1" , "labels match": false } The script exits 0 only if all three checks pass. Exit 1 means a mismatch or insufficient text evidence; invalid source positions are rejected by argparse with exit 2. A positive reference file with the selected labels passed. A reference with swapped pages failed, and source position 0 was rejected. These checks test the inspector, not a repaired production exporter. An excerpt might intentionally receive labels 1 , 2 , 3 . That is a valid product choice if it is stated clearly. This preservation checker would reject that output by design; the mismatch does not make an otherwise readable PDF universally invalid. Choose the policy before comparing results. If you preserve source labels, remap them to the output positions. If you renumber the excerpt, generate a new label sequence. Neither choice automatically changes the printed footer. Also avoid treating labels as unique identifiers: the extraction mapping should refer to source pages, not depend on labels being unique. This fixture has selectable text. For a scan or a deliberately blank page, an empty extracted string is not evidence that the visual content survived; the checker reports text matches: null when the text comparison is not informative. It does not verify rendering, bookmarks, links, forms, signatures, or accessibility structure. Those require their own checks. The useful regression is a source-to-output mapping with separate expectations for content and labels. A correct page count is only one field in that mapping.