diff --git a/staplelib/commands.py b/staplelib/commands.py index d37bfa7..6082d28 100644 --- a/staplelib/commands.py +++ b/staplelib/commands.py @@ -296,7 +296,8 @@ def pdf_page_enumeration(pdf): if len(pagelabels_nums_list) > 0 and i >= pagelabels_nums_list[0]: pagelabels_nums_list.pop(0) # discard index pnle = pagelabels_nums_list.pop(0) - style = pnle.get('/S', '/D') + # An omitted /S means the label consists only of its prefix. + style = pnle.get('/S') prefix = pnle.get('/P', '') next_pageno = pnle.get('/St', 1) pageno_str = '' @@ -310,7 +311,7 @@ def pdf_page_enumeration(pdf): pageno_str = int_to_roman(next_pageno) elif style == '/r': pageno_str = int_to_roman(next_pageno).lower() - else: + elif style is not None: raise CommandError("Malformded PDF: unkown page numbering style " + style) labels.append(prefix + pageno_str) next_pageno += 1 diff --git a/tests/test_oss_regression_62.py b/tests/test_oss_regression_62.py new file mode 100644 index 0000000..a1dd2bc --- /dev/null +++ b/tests/test_oss_regression_62.py @@ -0,0 +1,81 @@ +"""Regression coverage for prefix-only PDF page labels (issue #62).""" + +import pytest +from PyPDF2 import PdfFileWriter +from PyPDF2.generic import ( + ArrayObject, + DictionaryObject, + NameObject, + NumberObject, + createStringObject, +) + +from staplelib import main + + +@pytest.mark.parametrize( + "ranges, expected", + [ + ( + [(0, {"/P": "1"}), (1, {"/P": "2"}), (2, {"/P": "3"})], + ["1", "2", "3"], + ), + ( + [(0, {"/P": "i"}), (1, {"/P": "ii"}), + (2, {"/P": "1"}), (3, {"/P": "2"})], + ["i", "ii", "1", "2"], + ), + ([(0, {"/P": "Cover", "/St": 12})], ["Cover", "Cover", "Cover"]), + ([(0, {})], ["", ""]), + ([(0, {"/S": "/D", "/P": "A-", "/St": 9})], ["A-9", "A-10"]), + ( + [(0, {"/S": "/D", "/St": 7}), + (1, {"/S": "/A", "/St": 27}), + (2, {"/S": "/a", "/St": 2}), + (3, {"/S": "/R", "/St": 4}), + (4, {"/S": "/r", "/St": 6})], + ["7", "AA", "b", "IV", "vi"], + ), + (None, ["1", "2", "3"]), + ], + ids=[ + "numeric-prefixes", + "roman-and-numeric-prefixes", + "prefix-only-range", + "empty-prefix", + "decimal-prefix-and-start", + "explicit-numbering-styles", + "no-page-labels", + ], +) +def test_list_logical_page_labels(tmp_path, capsys, ranges, expected): + writer = PdfFileWriter() + for _ in expected: + writer.addBlankPage(72, 72) + + if ranges is not None: + nums = ArrayObject() + for index, values in ranges: + label = DictionaryObject() + for key, value in values.items(): + if key == "/S": + label[NameObject(key)] = NameObject(value) + elif key == "/St": + label[NameObject(key)] = NumberObject(value) + else: + label[NameObject(key)] = createStringObject(value) + nums.extend([NumberObject(index), label]) + writer._root_object[NameObject("/PageLabels")] = DictionaryObject( + {NameObject("/Nums"): nums} + ) + + path = tmp_path / "labels.pdf" + with path.open("wb") as stream: + writer.write(stream) + + main(["list-logical", str(path)]) + + assert capsys.readouterr().out == "".join( + "{}\t{}\n".format(label, index) + for index, label in enumerate(expected, 1) + )