From 1d1602e0c5bc52a2fa6bb52bb8456a01b7dd84c3 Mon Sep 17 00:00:00 2001 From: Andrew Baumann <0xabu@users.noreply.github.com> Date: Tue, 1 Feb 2022 01:08:05 -0800 Subject: [PATCH] Added feature: page labels (#680) * port page label code from pdfannots * add tests and clean up * more cleanup; harden against non-conforming input * one more test * update CHANGELOG * cleanup & respond to review feedback (incomplete) * Refactor implementation of get_page_labels() into a NumberTree and PageLabels class. * PageLabels *is* a NumberTree and should always behave like one. This justifies inheriting its data and behavior. And it simplifies the code a bit more. * fix type errors and cleanup slightly * fix mypy errors (including tweaking code to avoid problematic dynamic types) * hoist dict_value from NumberTree (where it may not be a dict) to PageLabels (where it must be) * avoid repeated warnings by calling _parse() recursively, and checking sortedness only at the end Co-authored-by: Pieter Marsman --- CHANGELOG.md | 1 + pdfminer/data_structures.py | 53 ++++++++++++++++++++ pdfminer/pdfdocument.py | 85 ++++++++++++++++++++++++++++++++- pdfminer/pdfpage.py | 20 ++++++-- pdfminer/utils.py | 45 +++++++++++++++++ samples/contrib/pagelabels.pdf | Bin 0 -> 39917 bytes tests/test_pdfdocument.py | 24 +++++++++- tests/test_pdfpage.py | 18 +++++++ tests/test_utils.py | 34 ++++++++++++- 9 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 pdfminer/data_structures.py create mode 100644 samples/contrib/pagelabels.pdf create mode 100644 tests/test_pdfpage.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 21483e3..649b4ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Export type annotations from pypi package per PEP561 ([#679](https://github.com/pdfminer/pdfminer.six/pull/679)) - Support for identity cmap's ([#626](https://github.com/pdfminer/pdfminer.six/pull/626)) +- Add support for PDF page labels ([#680](https://github.com/pdfminer/pdfminer.six/pull/680)) ### Fixed - Hande decompression error due to CRC checksum error ([#637](https://github.com/pdfminer/pdfminer.six/pull/637)) diff --git a/pdfminer/data_structures.py b/pdfminer/data_structures.py new file mode 100644 index 0000000..5239a38 --- /dev/null +++ b/pdfminer/data_structures.py @@ -0,0 +1,53 @@ +import functools +from typing import Any, Dict, Iterable, List, Optional, Tuple + +from pdfminer import settings +from pdfminer.pdfparser import PDFSyntaxError +from pdfminer.pdftypes import list_value, int_value, dict_value +from pdfminer.utils import choplist + + +class NumberTree: + """A PDF number tree. + + See Section 3.8.6 of the PDF Reference. + """ + def __init__(self, obj: Any): + self._obj = dict_value(obj) + self.nums: Optional[Iterable[Any]] = None + self.kids: Optional[Iterable[Any]] = None + self.limits: Optional[Iterable[Any]] = None + + if 'Nums' in self._obj: + self.nums = list_value(self._obj['Nums']) + if 'Kids' in self._obj: + self.kids = list_value(self._obj['Kids']) + if 'Limits' in self._obj: + self.limits = list_value(self._obj['Limits']) + + def _parse(self) -> List[Tuple[int, Any]]: + l = [] + if self.nums: # Leaf node + for k, v in choplist(2, self.nums): + l.append((int_value(k), v)) + + if self.kids: # Root or intermediate node + for child_ref in self.kids: + l += NumberTree(child_ref)._parse() + + return l + + values: List[Tuple[int, Any]] # workaround decorators unsupported by mypy + + @property # type: ignore [no-redef,misc] + @functools.lru_cache + def values(self) -> List[Tuple[int, Any]]: + values = self._parse() + + if settings.STRICT: + if not all(a[0] <= b[0] for a, b in zip(values, values[1:])): + raise PDFSyntaxError('Number tree elements are out of order') + else: + values.sort(key=lambda t: t[0]) + + return values diff --git a/pdfminer/pdfdocument.py b/pdfminer/pdfdocument.py index ee61937..1968569 100644 --- a/pdfminer/pdfdocument.py +++ b/pdfminer/pdfdocument.py @@ -1,3 +1,4 @@ +import itertools import logging import re import struct @@ -10,12 +11,14 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from . import settings from .arcfour import Arcfour +from .data_structures import NumberTree from .pdfparser import PDFSyntaxError, PDFParser, PDFStreamParser from .pdftypes import DecipherCallable, PDFException, PDFTypeError, PDFStream, \ PDFObjectNotFound, decipher_all, int_value, str_value, list_value, \ uint_value, dict_value, stream_value from .psparser import PSEOF, literal_name, LIT, KWD -from .utils import choplist, nunpack, decode_text +from .utils import choplist, decode_text, nunpack, format_int_roman, \ + format_int_alpha log = logging.getLogger(__name__) @@ -36,6 +39,10 @@ class PDFNoOutlines(PDFException): pass +class PDFNoPageLabels(PDFException): + pass + + class PDFDestinationNotFound(PDFException): pass @@ -890,6 +897,24 @@ class PDFDocument: return return search(self.catalog['Outlines'], 0) + def get_page_labels(self) -> Iterator[str]: + """ + Generate page label strings for the PDF document. + + If the document includes page labels, generates strings, one per page. + If not, raises PDFNoPageLabels. + + The resulting iteration is unbounded. + """ + assert self.catalog is not None + + try: + page_labels = PageLabels(self.catalog['PageLabels']) + except (PDFTypeError, KeyError): + raise PDFNoPageLabels + + return page_labels.labels + def lookup_name( self, cat: str, @@ -989,3 +1014,61 @@ class PDFDocument: pos = int_value(trailer['Prev']) self.read_xref_from(parser, pos, xrefs) return + + +class PageLabels(NumberTree): + """PageLabels from the document catalog. + + See Section 8.3.1 in the PDF Reference. + """ + + @property + def labels(self) -> Iterator[str]: + ranges = self.values + + # The tree must begin with page index 0 + if len(ranges) == 0 or ranges[0][0] != 0: + if settings.STRICT: + raise PDFSyntaxError('PageLabels is missing page index 0') + else: + # Try to cope, by assuming empty labels for the initial pages + ranges.insert(0, (0, {})) + + for (next, (start, label_dict_unchecked)) in enumerate(ranges, 1): + label_dict = dict_value(label_dict_unchecked) + style = label_dict.get('S') + prefix = decode_text(str_value(label_dict.get('P', b''))) + first_value = int_value(label_dict.get('St', 1)) + + if next == len(ranges): + # This is the last specified range. It continues until the end + # of the document. + values: Iterable[int] = itertools.count(first_value) + else: + end, _ = ranges[next] + range_length = end - start + values = range(first_value, first_value + range_length) + + for value in values: + label = self._format_page_label(value, style) + yield prefix + label + + @staticmethod + def _format_page_label(value: int, style: Any) -> str: + """Format page label value in a specific style""" + if style is None: + label = '' + elif style is LIT('D'): # Decimal arabic numerals + label = str(value) + elif style is LIT('R'): # Uppercase roman numerals + label = format_int_roman(value).upper() + elif style is LIT('r'): # Lowercase roman numerals + label = format_int_roman(value) + elif style is LIT('A'): # Uppercase letters A-Z, AA-ZZ... + label = format_int_alpha(value).upper() + elif style is LIT('a'): # Lowercase letters a-z, aa-zz... + label = format_int_alpha(value) + else: + log.warning('Unknown page label style: %r', style) + label = '' + return label diff --git a/pdfminer/pdfpage.py b/pdfminer/pdfpage.py index c822083..39195a3 100644 --- a/pdfminer/pdfpage.py +++ b/pdfminer/pdfpage.py @@ -1,9 +1,11 @@ +import itertools import logging from typing import BinaryIO, Container, Dict, Iterator, List, Optional, Tuple from pdfminer.utils import Rect from . import settings -from .pdfdocument import PDFDocument, PDFTextExtractionNotAllowed +from .pdfdocument import PDFDocument, PDFTextExtractionNotAllowed, \ + PDFNoPageLabels from .pdfparser import PDFParser from .pdftypes import PDFObjectNotFound from .pdftypes import dict_value @@ -38,23 +40,27 @@ class PDFPage: rotate: the page rotation (in degree). annots: the page annotations. beads: a chain that represents natural reading order. + label: the page's label (typically, the logical page number). """ def __init__( self, doc: PDFDocument, pageid: object, - attrs: object + attrs: object, + label: Optional[str] ) -> None: """Initialize a page object. doc: a PDFDocument object. pageid: any Python object that can uniquely identify the page. attrs: a dictionary of page attributes. + label: page label string. """ self.doc = doc self.pageid = pageid self.attrs = dict_value(attrs) + self.label = label self.lastmod = resolve1(self.attrs.get('LastModified')) self.resources: Dict[object, object] = \ resolve1(self.attrs.get('Resources', dict())) @@ -109,11 +115,17 @@ class PDFPage: elif tree_type is LITERAL_PAGE: log.info('Page: %r', tree) yield (objid, tree) + + try: + page_labels: Iterator[Optional[str]] = document.get_page_labels() + except PDFNoPageLabels: + page_labels = itertools.repeat(None) + pages = False if 'Pages' in document.catalog: objects = search(document.catalog['Pages'], document.catalog) for (objid, tree) in objects: - yield cls(document, objid, tree) + yield cls(document, objid, tree, next(page_labels)) pages = True if not pages: # fallback when /Pages is missing. @@ -123,7 +135,7 @@ class PDFPage: obj = document.getobj(objid) if isinstance(obj, dict) \ and obj.get('Type') is LITERAL_PAGE: - yield cls(document, objid, obj) + yield cls(document, objid, obj, next(page_labels)) except PDFObjectNotFound: pass return diff --git a/pdfminer/utils.py b/pdfminer/utils.py index 01c5901..77d5f9b 100644 --- a/pdfminer/utils.py +++ b/pdfminer/utils.py @@ -3,6 +3,7 @@ Miscellaneous Routines. """ import io import pathlib +import string import struct from html import escape from typing import (Any, BinaryIO, Callable, Dict, Generic, Iterable, Iterator, @@ -527,3 +528,47 @@ class Plane(Generic[LTComponentT]): or y1 <= obj.y0: continue yield obj + + +ROMAN_ONES = ['i', 'x', 'c', 'm'] +ROMAN_FIVES = ['v', 'l', 'd'] + + +def format_int_roman(value: int) -> str: + """Format a number as lowercase Roman numerals.""" + + assert 0 < value < 4000 + result: List[str] = [] + index = 0 + + while value != 0: + value, remainder = divmod(value, 10) + if remainder == 9: + result.insert(0, ROMAN_ONES[index]) + result.insert(1, ROMAN_ONES[index + 1]) + elif remainder == 4: + result.insert(0, ROMAN_ONES[index]) + result.insert(1, ROMAN_FIVES[index]) + else: + over_five = remainder >= 5 + if over_five: + result.insert(0, ROMAN_FIVES[index]) + remainder -= 5 + result.insert(1 if over_five else 0, ROMAN_ONES[index] * remainder) + index += 1 + + return ''.join(result) + + +def format_int_alpha(value: int) -> str: + """Format a number as lowercase letters a-z, aa-zz, etc.""" + + assert value > 0 + result: List[str] = [] + + while value != 0: + value, remainder = divmod(value - 1, len(string.ascii_lowercase)) + result.append(string.ascii_lowercase[remainder]) + + result.reverse() + return ''.join(result) diff --git a/samples/contrib/pagelabels.pdf b/samples/contrib/pagelabels.pdf new file mode 100644 index 0000000000000000000000000000000000000000..44e2d6bf12d5187373daad48d18c369a640e0aa1 GIT binary patch literal 39917 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%0DBfE57Mee3IeeWv#lMhc_An7UOy6sfcorZVdrAl0 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 F{1-6j0fPVl literal 0 HcmV?d00001 diff --git a/tests/test_pdfdocument.py b/tests/test_pdfdocument.py index 3947b1b..d90abc0 100644 --- a/tests/test_pdfdocument.py +++ b/tests/test_pdfdocument.py @@ -1,9 +1,11 @@ +import itertools + from nose.tools import assert_equal, raises from helpers import absolute_sample_path -from pdfminer.pdfdocument import PDFDocument +from pdfminer.pdfdocument import PDFDocument, PDFNoPageLabels from pdfminer.pdfparser import PDFParser -from pdfminer.pdftypes import PDFObjectNotFound +from pdfminer.pdftypes import PDFObjectNotFound, dict_value, int_value class TestPdfDocument(object): @@ -25,3 +27,21 @@ class TestPdfDocument(object): doc = PDFDocument(parser) assert_equal(doc.info, [{'Producer': b'European Patent Office'}]) + + def test_page_labels(self): + path = absolute_sample_path('contrib/pagelabels.pdf') + with open(path, 'rb') as fp: + parser = PDFParser(fp) + doc = PDFDocument(parser) + total_pages = int_value(dict_value(doc.catalog['Pages'])['Count']) + assert_equal( + list(itertools.islice(doc.get_page_labels(), total_pages)), + ['iii', 'iv', '1', '2', '1']) + + @raises(PDFNoPageLabels) + def test_no_page_labels(self): + path = absolute_sample_path('simple1.pdf') + with open(path, 'rb') as fp: + parser = PDFParser(fp) + doc = PDFDocument(parser) + doc.get_page_labels() diff --git a/tests/test_pdfpage.py b/tests/test_pdfpage.py new file mode 100644 index 0000000..06574c3 --- /dev/null +++ b/tests/test_pdfpage.py @@ -0,0 +1,18 @@ +from nose.tools import assert_equal + +from helpers import absolute_sample_path +from pdfminer.pdfdocument import PDFDocument +from pdfminer.pdfparser import PDFParser +from pdfminer.pdfpage import PDFPage + + +class TestPdfPage(object): + def test_page_labels(self): + path = absolute_sample_path('contrib/pagelabels.pdf') + expected_labels = ['iii', 'iv', '1', '2', '1'] + + with open(path, 'rb') as fp: + parser = PDFParser(fp) + doc = PDFDocument(parser) + for (i, page) in enumerate(PDFPage.create_pages(doc)): + assert_equal(page.label, expected_labels[i]) diff --git a/tests/test_utils.py b/tests/test_utils.py index dca99a6..6c32181 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,8 @@ import pathlib from helpers import absolute_sample_path from pdfminer.layout import LTComponent -from pdfminer.utils import open_filename, Plane, shorten_str +from pdfminer.utils import (format_int_alpha, format_int_roman, open_filename, + Plane, shorten_str) class TestOpenFilename: @@ -76,3 +77,34 @@ class TestFunctions(object): def test_shorten_to_really_short(self): assert_equal('Hello', shorten_str('Hello World', 5)) + + def test_format_int_alpha(self): + assert_equal('a', format_int_alpha(1)) + assert_equal('b', format_int_alpha(2)) + assert_equal('z', format_int_alpha(26)) + assert_equal('aa', format_int_alpha(27)) + assert_equal('ab', format_int_alpha(28)) + assert_equal('az', format_int_alpha(26*2)) + assert_equal('ba', format_int_alpha(26*2 + 1)) + assert_equal('zz', format_int_alpha(26*27)) + assert_equal('aaa', format_int_alpha(26*27 + 1)) + + def test_format_int_roman(self): + assert_equal('i', format_int_roman(1)) + assert_equal('ii', format_int_roman(2)) + assert_equal('iii', format_int_roman(3)) + assert_equal('iv', format_int_roman(4)) + assert_equal('v', format_int_roman(5)) + assert_equal('vi', format_int_roman(6)) + assert_equal('vii', format_int_roman(7)) + assert_equal('viii', format_int_roman(8)) + assert_equal('ix', format_int_roman(9)) + assert_equal('x', format_int_roman(10)) + assert_equal('xi', format_int_roman(11)) + assert_equal('xx', format_int_roman(20)) + assert_equal('xl', format_int_roman(40)) + assert_equal('xlv', format_int_roman(45)) + assert_equal('l', format_int_roman(50)) + assert_equal('xc', format_int_roman(90)) + assert_equal('xci', format_int_roman(91)) + assert_equal('c', format_int_roman(100))