Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/pagerange.py: 31%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Representation and utils for ranges of PDF file pages.
4Copyright (c) 2014, Steve Witham <switham_github@mac-guyver.com>.
5All rights reserved. This software is available under a BSD license;
6see https://github.com/py-pdf/pypdf/blob/main/LICENSE
7"""
9import re
10from typing import Any, Optional, Union
12from .errors import ParseError
14_INT_RE = r"(0|-?[1-9]\d*)" # A decimal int, don't allow "-0".
15PAGE_RANGE_RE = f"^({_INT_RE}|({_INT_RE}?(:{_INT_RE}?(:{_INT_RE}?)?)))$"
16# groups: 12 34 5 6 7 8
19class PageRange:
20 """
21 A slice-like representation of a range of page indices.
23 For example, page numbers, only starting at zero.
25 The syntax is like what you would put between brackets [ ].
26 The slice is one of the few Python types that can't be subclassed,
27 but this class converts to and from slices, and allows similar use.
29 - PageRange(str) parses a string representing a page range.
30 - PageRange(slice) directly "imports" a slice.
31 - to_slice() gives the equivalent slice.
32 - str() and repr() allow printing.
33 - indices(n) is like slice.indices(n).
34 """
36 def __init__(self, arg: Union[slice, "PageRange", str]) -> None:
37 """
38 Initialize with either a slice -- giving the equivalent page range,
39 or a PageRange object -- making a copy,
40 or a string like
41 "int", "[int]:[int]" or "[int]:[int]:[int]",
42 where the brackets indicate optional ints.
43 Remember, page indices start with zero.
44 Page range expression examples:
46 : all pages. -1 last page.
47 22 just the 23rd page. :-1 all but the last page.
48 0:3 the first three pages. -2 second-to-last page.
49 :3 the first three pages. -2: last two pages.
50 5: from the sixth page onward. -3:-1 third & second to last.
51 The third, "stride" or "step" number is also recognized.
52 ::2 0 2 4 ... to the end. 3:0:-1 3 2 1 but not 0.
53 1:10:2 1 3 5 7 9 2::-1 2 1 0.
54 ::-1 all pages in reverse order.
55 Note the difference between this notation and arguments to slice():
56 slice(3) means the first three pages;
57 PageRange("3") means the range of only the fourth page.
58 However PageRange(slice(3)) means the first three pages.
59 """
60 if isinstance(arg, slice):
61 self._slice = arg
62 return
64 if isinstance(arg, PageRange):
65 self._slice = arg.to_slice()
66 return
68 m = isinstance(arg, str) and re.match(PAGE_RANGE_RE, arg)
69 if not m:
70 raise ParseError(arg)
71 if m.group(2):
72 # Special case: just an int means a range of one page.
73 start = int(m.group(2))
74 stop = start + 1 if start != -1 else None
75 self._slice = slice(start, stop)
76 else:
77 bounds = [int(g) if g else None for g in m.group(4, 6, 8)]
78 step = bounds[2]
79 if step == 0:
80 # A zero stride cannot select anything; slice() accepts it here
81 # but raises as soon as the range is applied.
82 raise ParseError(arg)
83 self._slice = slice(*bounds)
85 @staticmethod
86 def valid(input: Any) -> bool:
87 """
88 True if input is a valid initializer for a PageRange.
90 Args:
91 input: A possible PageRange string or a PageRange object.
93 Returns:
94 True, if the ``input`` is a valid PageRange.
96 """
97 if isinstance(input, (slice, PageRange)):
98 return True
99 if not isinstance(input, str):
100 return False
101 try:
102 PageRange(input)
103 except ParseError:
104 return False
105 return True
107 def to_slice(self) -> slice:
108 """Return the slice equivalent of this page range."""
109 return self._slice
111 def __str__(self) -> str:
112 """A string like "1:2:3"."""
113 s = self._slice
114 indices: Union[tuple[int, int], tuple[int, int, int]]
115 if s.step is None:
116 if s.start is not None and s.stop == s.start + 1:
117 return str(s.start)
119 indices = s.start, s.stop
120 else:
121 indices = s.start, s.stop, s.step
122 return ":".join("" if i is None else str(i) for i in indices)
124 def __repr__(self) -> str:
125 """A string like "PageRange('1:2:3')"."""
126 return "PageRange(" + repr(str(self)) + ")"
128 def indices(self, n: int) -> tuple[int, int, int]:
129 """
130 Assuming a sequence of length n, calculate the start and stop indices,
131 and the stride length of the PageRange.
133 See help(slice.indices).
135 Args:
136 n: the length of the list of pages to choose from.
138 Returns:
139 Arguments for range().
141 """
142 return self._slice.indices(n)
144 def __eq__(self, other: object) -> bool:
145 if not isinstance(other, PageRange):
146 return False
147 return self._slice == other._slice
149 def __hash__(self) -> int:
150 return hash((self.__class__, (self._slice.start, self._slice.stop, self._slice.step)))
152 def __add__(self, other: "PageRange") -> "PageRange":
153 if not isinstance(other, PageRange):
154 raise TypeError(f"Can't add PageRange and {type(other)}")
155 if self._slice.step is not None or other._slice.step is not None:
156 raise ValueError("Can't add PageRange with stride")
157 a = self._slice.start, self._slice.stop
158 b = other._slice.start, other._slice.stop
160 # None start means "beginning" (-inf for ordering); None stop means "end" (+inf).
161 def _start_key(v: Optional[int]) -> float:
162 return float("-inf") if v is None else v
164 def _stop_key(v: Optional[int]) -> float:
165 return float("inf") if v is None else v
167 if _start_key(a[0]) > _start_key(b[0]):
168 a, b = b, a
170 # Now `a` has the smaller (or equal) start.
171 if _start_key(b[0]) > _stop_key(a[1]):
172 # There is a gap between a and b.
173 raise ValueError("Can't add PageRanges with gap")
174 stop = b[1] if _stop_key(b[1]) > _stop_key(a[1]) else a[1]
175 return PageRange(slice(a[0], stop))
178PAGE_RANGE_ALL = PageRange(":") # The range of all pages.
181def parse_filename_page_ranges(
182 args: list[Union[str, PageRange, None]]
183) -> list[tuple[str, PageRange]]:
184 """
185 Given a list of filenames and page ranges, return a list of (filename, page_range) pairs.
187 Args:
188 args: A list where the first element is a filename. The other elements are
189 filenames, page-range expressions, slice objects, or PageRange objects.
190 A filename not followed by a page range indicates all pages of the file.
192 Returns:
193 A list of (filename, page_range) pairs.
195 """
196 pairs: list[tuple[str, PageRange]] = []
197 pdf_filename: Union[str, None] = None
198 did_page_range = False
199 for arg in [*args, None]:
200 if PageRange.valid(arg):
201 if not pdf_filename:
202 raise ValueError(
203 "The first argument must be a filename, not a page range."
204 )
206 assert arg is not None
207 pairs.append((pdf_filename, PageRange(arg)))
208 did_page_range = True
209 else:
210 # New filename or end of list - use the complete previous file?
211 if pdf_filename and not did_page_range:
212 pairs.append((pdf_filename, PAGE_RANGE_ALL))
214 assert not isinstance(arg, PageRange), arg
215 pdf_filename = arg
216 did_page_range = False
217 return pairs
220PageRangeSpec = Union[str, PageRange, tuple[int, int], tuple[int, int, int], list[int]]