PDF 頁面位置、頁面標籤和印刷頁碼是不同資料
Page positions, page labels, and printed numbers are different PDF data
PDF 擷取子集即使頁數和文字都正確,頁面標籤仍可能與所選來源頁面不符;檢查時須區分頁面位置、邏輯標籤和頁面上印刷的頁碼。文章以 pypdf 6.10.0 示範唯讀檢查器,核對輸出標籤是否保留所選來源頁面的標籤,並指出重新編號亦是有效政策,須先明確訂定。
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.
Define which “page number” you mean
Three values can differ:
- Position: where a page occurs in the document. A UI commonly starts this count at 1; an array commonly starts at 0.
-
Logical label: metadata a reader can display, such as
i,1, orA-2. - Printed number: text or graphics drawn on the page itself.
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: 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.
Follow the source-to-output mapping
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.
Add the label policy to a small checker
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 returns labels in document order.
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.
A different numbering policy needs a different assertion
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.
來源:DEV Community · dev.to