Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_page_labels.py291 linesDownload Raw Back to pypdf
1"""2Page labels are shown by PDF viewers as "the page number".3 4A page has a numeric index, starting at 0. Additionally, the page5has a label. In the most simple case:6 7    label = index + 18 9However, the title page and the table of contents might have Roman numerals as10page labels. This makes things more complicated.11 12Example 113---------14 15>>> reader.root_object["/PageLabels"]["/Nums"]16[0, IndirectObject(18, 0, 139929798197504),17 8, IndirectObject(19, 0, 139929798197504)]18>>> reader.get_object(reader.root_object["/PageLabels"]["/Nums"][1])19{'/S': '/r'}20>>> reader.get_object(reader.root_object["/PageLabels"]["/Nums"][3])21{'/S': '/D'}22 23Example 224---------25The following is a document with pages labeled26i, ii, iii, iv, 1, 2, 3, A-8, A-9, ...27 281 0 obj29    << /Type /Catalog30       /PageLabels << /Nums [31                        0 << /S /r >>32                        4 << /S /D >>33                        7 << /S /D34                             /P ( A- )35                             /St 836                        >>37                        % A number tree containing38                        % three page label dictionaries39                        ]40                   >>41    ...42    >>43endobj44 45 46§12.4.2 PDF Specification 1.7 and 2.047=====================================48 49Entries in a page label dictionary50----------------------------------51The /S key:52D       Decimal Arabic numerals53R       Uppercase Roman numerals54r       Lowercase Roman numerals55A       Uppercase letters (A to Z for the first 26 pages,56                           AA to ZZ for the next 26, and so on)57a       Lowercase letters (a to z for the first 26 pages,58                           aa to zz for the next 26, and so on)59"""60 61from collections.abc import Callable, Iterator62from typing import Optional, cast63 64from ._protocols import PdfCommonDocProtocol65from ._utils import logger_warning66from .generic import (67    ArrayObject,68    DictionaryObject,69    NullObject,70    NumberObject,71    is_null_or_none,72)73 74 75def number2uppercase_roman_numeral(num: int) -> str:76    roman = [77        (1000, "M"),78        (900, "CM"),79        (500, "D"),80        (400, "CD"),81        (100, "C"),82        (90, "XC"),83        (50, "L"),84        (40, "XL"),85        (10, "X"),86        (9, "IX"),87        (5, "V"),88        (4, "IV"),89        (1, "I"),90    ]91 92    def roman_num(num: int) -> Iterator[str]:93        for decimal, roman_repr in roman:94            x, _ = divmod(num, decimal)95            yield roman_repr * x96            num -= decimal * x97            if num <= 0:98                break99 100    return "".join(list(roman_num(num)))101 102 103def number2lowercase_roman_numeral(number: int) -> str:104    return number2uppercase_roman_numeral(number).lower()105 106 107def number2uppercase_letter(number: int) -> str:108    if number <= 0:109        raise ValueError("Expecting a positive number")110    alphabet = [chr(i) for i in range(ord("A"), ord("Z") + 1)]111    rep = ""112    while number > 0:113        remainder = number % 26114        if remainder == 0:115            remainder = 26116        rep = alphabet[remainder - 1] + rep117        # update118        number -= remainder119        number = number // 26120    return rep121 122 123def number2lowercase_letter(number: int) -> str:124    return number2uppercase_letter(number).lower()125 126 127def get_label_from_nums(dictionary_object: DictionaryObject, index: int) -> str:128    # [Nums] shall be an array of the form129    #   [ key_1 value_1 key_2 value_2 ... key_n value_n ]130    # where each key_i is an integer and the corresponding131    # value_i shall be the object associated with that key.132    # The keys shall be sorted in numerical order,133    # analogously to the arrangement of keys in a name tree134    # as described in 7.9.6, "Name Trees."135    nums = cast(ArrayObject, dictionary_object["/Nums"])136    i = 0137    value = None138    start_index = 0139    while i < len(nums):140        start_index = nums[i]141        value = nums[i + 1].get_object()142        if i + 2 == len(nums):143            break144        if nums[i + 2] > index:145            break146        i += 2147    m: dict[Optional[str], Callable[[int], str]] = {148        None: lambda _: "",149        "/D": str,150        "/R": number2uppercase_roman_numeral,151        "/r": number2lowercase_roman_numeral,152        "/A": number2uppercase_letter,153        "/a": number2lowercase_letter,154    }155    # if /Nums array is not following the specification or if /Nums is empty156    if not isinstance(value, dict):157        return str(index + 1)  # Fallback158    start = value.get("/St", 1)159    prefix = cast(str, value.get("/P", ""))160    mapping_function = m[value.get("/S")]161    return prefix + mapping_function(index - start_index + start)162 163 164def index2label(reader: PdfCommonDocProtocol, index: int) -> str:165    """166    See 7.9.7 "Number Trees".167 168    Args:169        reader: The PdfReader170        index: The index of the page171 172    Returns:173        The label of the page, e.g. "iv" or "4".174 175    """176    root = cast(DictionaryObject, reader.root_object)177    if "/PageLabels" not in root:178        return str(index + 1)  # Fallback179    number_tree = cast(DictionaryObject, root["/PageLabels"].get_object())180    if "/Nums" in number_tree:181        return get_label_from_nums(number_tree, index)182    if "/Kids" in number_tree and not isinstance(number_tree["/Kids"], NullObject):183        # number_tree = {'/Kids': [IndirectObject(7333, 0, 140132998195856), ...]}184        # Limit maximum depth.185        level = 0186        while level < 100:187            kids = cast(list[DictionaryObject], number_tree["/Kids"])188            for kid in kids:189                # kid = {'/Limits': [0, 63], '/Nums': [0, {'/P': 'C1'}, ...]}190                limits = cast(list[int], kid["/Limits"])191                if limits[0] <= index <= limits[1]:192                    if not is_null_or_none(kid.get("/Kids", None)):193                        # Recursive definition.194                        level += 1195                        if level == 100:  # pragma: no cover196                            raise NotImplementedError(197                                "Too deep nesting is not supported."198                            )199                        number_tree = kid200                        # Exit the inner `for` loop and continue at the next level with the201                        # next iteration of the `while` loop.202                        break203                    return get_label_from_nums(kid, index)204            else:205                # When there are no kids, make sure to exit the `while` loop directly206                # and continue with the fallback.207                break208 209    logger_warning(f"Could not reliably determine page label for {index}.", __name__)210    return str(index + 1)  # Fallback if neither /Nums nor /Kids is in the number_tree211 212 213def nums_insert(214    key: NumberObject,215    value: DictionaryObject,216    nums: ArrayObject,217) -> None:218    """219    Insert a key, value pair in a Nums array.220 221    See 7.9.7 "Number Trees".222 223    Args:224        key: number key of the entry225        value: value of the entry226        nums: Nums array to modify227 228    """229    if len(nums) % 2 != 0:230        raise ValueError("A nums like array must have an even number of elements")231 232    i = len(nums)233    while i != 0 and key <= nums[i - 2]:234        i = i - 2235 236    if i < len(nums) and key == nums[i]:237        nums[i + 1] = value238    else:239        nums.insert(i, key)240        nums.insert(i + 1, value)241 242 243def nums_clear_range(244    key: NumberObject,245    page_index_to: int,246    nums: ArrayObject,247) -> None:248    """249    Remove all entries in a number tree in a range after an entry.250 251    See 7.9.7 "Number Trees".252 253    Args:254        key: number key of the entry before the range255        page_index_to: The page index of the upper limit of the range256        nums: Nums array to modify257 258    """259    if len(nums) % 2 != 0:260        raise ValueError("A nums like array must have an even number of elements")261    if page_index_to < key:262        raise ValueError("page_index_to must be greater or equal than key")263 264    i = nums.index(key) + 2265    while i < len(nums) and nums[i] <= page_index_to:266        nums.pop(i)267        nums.pop(i)268 269 270def nums_next(271    key: NumberObject,272    nums: ArrayObject,273) -> tuple[Optional[NumberObject], Optional[DictionaryObject]]:274    """275    Return the (key, value) pair of the entry after the given one.276 277    See 7.9.7 "Number Trees".278 279    Args:280        key: number key of the entry281        nums: Nums array282 283    """284    if len(nums) % 2 != 0:285        raise ValueError("A nums like array must have an even number of elements")286 287    i = nums.index(key) + 2288    if i < len(nums):289        return (nums[i], nums[i + 1])290    return (None, None)291 
codekingpro/portable-devtools · Team Ai