pagerange.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. """
  2. Representation and utils for ranges of PDF file pages.
  3. Copyright (c) 2014, Steve Witham <switham_github@mac-guyver.com>.
  4. All rights reserved. This software is available under a BSD license;
  5. see https://github.com/py-pdf/pypdf/blob/main/LICENSE
  6. """
  7. import re
  8. from typing import Any, Union
  9. from .errors import ParseError
  10. _INT_RE = r"(0|-?[1-9]\d*)" # A decimal int, don't allow "-0".
  11. PAGE_RANGE_RE = f"^({_INT_RE}|({_INT_RE}?(:{_INT_RE}?(:{_INT_RE}?)?)))$"
  12. # groups: 12 34 5 6 7 8
  13. class PageRange:
  14. """
  15. A slice-like representation of a range of page indices.
  16. For example, page numbers, only starting at zero.
  17. The syntax is like what you would put between brackets [ ].
  18. The slice is one of the few Python types that can't be subclassed,
  19. but this class converts to and from slices, and allows similar use.
  20. - PageRange(str) parses a string representing a page range.
  21. - PageRange(slice) directly "imports" a slice.
  22. - to_slice() gives the equivalent slice.
  23. - str() and repr() allow printing.
  24. - indices(n) is like slice.indices(n).
  25. """
  26. def __init__(self, arg: Union[slice, "PageRange", str]) -> None:
  27. """
  28. Initialize with either a slice -- giving the equivalent page range,
  29. or a PageRange object -- making a copy,
  30. or a string like
  31. "int", "[int]:[int]" or "[int]:[int]:[int]",
  32. where the brackets indicate optional ints.
  33. Remember, page indices start with zero.
  34. Page range expression examples:
  35. : all pages. -1 last page.
  36. 22 just the 23rd page. :-1 all but the last page.
  37. 0:3 the first three pages. -2 second-to-last page.
  38. :3 the first three pages. -2: last two pages.
  39. 5: from the sixth page onward. -3:-1 third & second to last.
  40. The third, "stride" or "step" number is also recognized.
  41. ::2 0 2 4 ... to the end. 3:0:-1 3 2 1 but not 0.
  42. 1:10:2 1 3 5 7 9 2::-1 2 1 0.
  43. ::-1 all pages in reverse order.
  44. Note the difference between this notation and arguments to slice():
  45. slice(3) means the first three pages;
  46. PageRange("3") means the range of only the fourth page.
  47. However PageRange(slice(3)) means the first three pages.
  48. """
  49. if isinstance(arg, slice):
  50. self._slice = arg
  51. return
  52. if isinstance(arg, PageRange):
  53. self._slice = arg.to_slice()
  54. return
  55. m = isinstance(arg, str) and re.match(PAGE_RANGE_RE, arg)
  56. if not m:
  57. raise ParseError(arg)
  58. if m.group(2):
  59. # Special case: just an int means a range of one page.
  60. start = int(m.group(2))
  61. stop = start + 1 if start != -1 else None
  62. self._slice = slice(start, stop)
  63. else:
  64. self._slice = slice(*[int(g) if g else None for g in m.group(4, 6, 8)])
  65. @staticmethod
  66. def valid(input: Any) -> bool:
  67. """
  68. True if input is a valid initializer for a PageRange.
  69. Args:
  70. input: A possible PageRange string or a PageRange object.
  71. Returns:
  72. True, if the ``input`` is a valid PageRange.
  73. """
  74. return isinstance(input, (slice, PageRange)) or (
  75. isinstance(input, str) and bool(re.match(PAGE_RANGE_RE, input))
  76. )
  77. def to_slice(self) -> slice:
  78. """Return the slice equivalent of this page range."""
  79. return self._slice
  80. def __str__(self) -> str:
  81. """A string like "1:2:3"."""
  82. s = self._slice
  83. indices: Union[tuple[int, int], tuple[int, int, int]]
  84. if s.step is None:
  85. if s.start is not None and s.stop == s.start + 1:
  86. return str(s.start)
  87. indices = s.start, s.stop
  88. else:
  89. indices = s.start, s.stop, s.step
  90. return ":".join("" if i is None else str(i) for i in indices)
  91. def __repr__(self) -> str:
  92. """A string like "PageRange('1:2:3')"."""
  93. return "PageRange(" + repr(str(self)) + ")"
  94. def indices(self, n: int) -> tuple[int, int, int]:
  95. """
  96. Assuming a sequence of length n, calculate the start and stop indices,
  97. and the stride length of the PageRange.
  98. See help(slice.indices).
  99. Args:
  100. n: the length of the list of pages to choose from.
  101. Returns:
  102. Arguments for range().
  103. """
  104. return self._slice.indices(n)
  105. def __eq__(self, other: object) -> bool:
  106. if not isinstance(other, PageRange):
  107. return False
  108. return self._slice == other._slice
  109. def __hash__(self) -> int:
  110. return hash((self.__class__, (self._slice.start, self._slice.stop, self._slice.step)))
  111. def __add__(self, other: "PageRange") -> "PageRange":
  112. if not isinstance(other, PageRange):
  113. raise TypeError(f"Can't add PageRange and {type(other)}")
  114. if self._slice.step is not None or other._slice.step is not None:
  115. raise ValueError("Can't add PageRange with stride")
  116. a = self._slice.start, self._slice.stop
  117. b = other._slice.start, other._slice.stop
  118. if a[0] > b[0]:
  119. a, b = b, a
  120. # Now a[0] is the smallest
  121. if b[0] > a[1]:
  122. # There is a gap between a and b.
  123. raise ValueError("Can't add PageRanges with gap")
  124. return PageRange(slice(a[0], max(a[1], b[1])))
  125. PAGE_RANGE_ALL = PageRange(":") # The range of all pages.
  126. def parse_filename_page_ranges(
  127. args: list[Union[str, PageRange, None]]
  128. ) -> list[tuple[str, PageRange]]:
  129. """
  130. Given a list of filenames and page ranges, return a list of (filename, page_range) pairs.
  131. Args:
  132. args: A list where the first element is a filename. The other elements are
  133. filenames, page-range expressions, slice objects, or PageRange objects.
  134. A filename not followed by a page range indicates all pages of the file.
  135. Returns:
  136. A list of (filename, page_range) pairs.
  137. """
  138. pairs: list[tuple[str, PageRange]] = []
  139. pdf_filename: Union[str, None] = None
  140. did_page_range = False
  141. for arg in [*args, None]:
  142. if PageRange.valid(arg):
  143. if not pdf_filename:
  144. raise ValueError(
  145. "The first argument must be a filename, not a page range."
  146. )
  147. assert arg is not None
  148. pairs.append((pdf_filename, PageRange(arg)))
  149. did_page_range = True
  150. else:
  151. # New filename or end of list - use the complete previous file?
  152. if pdf_filename and not did_page_range:
  153. pairs.append((pdf_filename, PAGE_RANGE_ALL))
  154. assert not isinstance(arg, PageRange), arg
  155. pdf_filename = arg
  156. did_page_range = False
  157. return pairs
  158. PageRangeSpec = Union[str, PageRange, tuple[int, int], tuple[int, int, int], list[int]]