ROTATE_COMMAND = '/Rotate'
+_TOK_RANGE_START = 'start'
+_TOK_RANGE_END = 'end'
+
+
+class PageRange:
+ 'Section of multi-page document.'
+ start = 0
+ end: int
+
+ def __init__(
+ self,
+ code: Optional[str],
+ end_max: int = -1
+ ) -> None:
+ if code:
+ prefix = 'page range string'
+ if '-' not in code:
+ raise MsgFail(f'{prefix} lacks "-": {code}')
+ tokens = code.split('-')
+ if len(tokens) > 2:
+ raise MsgFail(f'{prefix} has too many "-": {code}')
+ for i, token in enumerate(tokens):
+ if token == '':
+ continue
+ if i == 0 and token == _TOK_RANGE_START:
+ continue
+ if i == 1 and token == _TOK_RANGE_END:
+ continue
+ try:
+ int(token)
+ except ValueError as e:
+ raise MsgFail(
+ f'{prefix} carries value neither integer, nor '
+ f'"{_TOK_RANGE_START}", nor "{_TOK_RANGE_END}": {code}'
+ ) from e
+ if int(token) < 1:
+ raise MsgFail(f'{prefix} carries page number <1: {code}')
+ str_start, str_end = code.split('-')
+ self.end = end_max
+ if str_end not in {'', _TOK_RANGE_END}:
+ self.end = int(str_end)
+ if self.end > end_max >= 0:
+ raise MsgFail(
+ f'{prefix} exceeds maximum {end_max}: {code}')
+ if str_start not in {'', _TOK_RANGE_START}:
+ self.start = int(str_start) - 1
+
+ def ranging(
+ self
+ ) -> range:
+ 'Turn into range().'
+ assert self.end >= 0
+ return range(self.start, self.end)
+
+
class Page:
'Fuses PdfPage, PageCrop, and PdfTransformation.'
help='output PDF file')
parser.add_argument('-p',
'--page_range',
+ type=PageRange,
action='append',
- help='page range, e.g. "2-9" or "3-end" or "start-14"')
+ help=(f'page range, e.g. "2-9" or "3-{_TOK_RANGE_END}"'
+ f'or "{_TOK_RANGE_START}-14"'))
parser.add_argument('-c',
'--crops',
action='append',
) -> None:
'Check command args against general syntax expectations.'
- def validate_page_range(
- pr_string: str
- ) -> None:
- prefix = 'page range string'
- if '-' not in pr_string:
- raise MsgFail(f'{prefix} lacks "-": {pr_string}')
- tokens = pr_string.split('-')
- if len(tokens) > 2:
- raise MsgFail(f'{prefix} has too many "-": {pr_string}')
- for i, token in enumerate(tokens):
- if token == '':
- continue
- if i == 0 and token == 'start':
- continue
- if i == 1 and token == 'end':
- continue
- try:
- int(token)
- except ValueError as e:
- raise MsgFail(f'{prefix} carries value neither integer, '
- f'nor "start", nor "end": {pr_string}') from e
- if int(token) < 1:
- raise MsgFail(f'{prefix} carries page number <1: {pr_string}')
- start = -1
- end = -1
- try:
- start = int(tokens[0])
- end = int(tokens[1])
- except ValueError:
- pass
- if start > end > 0:
- raise MsgFail(
- f'{prefix} has higher start than end value: {pr_string}')
-
try:
if args.page_range:
arg_char = 'p'
- for p_string in args.page_range:
- validate_page_range(p_string)
if len(args.page_range) > len(args.input_file):
raise MsgFail(
'more --page_range arguments than --input_file arguments')
raise MsgFail(
f'cropping string has multiple ":": {c_string}')
page_range, crops_str = split_crops_string(c_string)
+ PageRange(page_range)
crops = crops_str.split(',')
- if page_range:
- validate_page_range(page_range)
if len(crops) != 4:
raise MsgFail('cropping does not contain exactly three '
f'",": {c_string}')
if r < 1:
raise MsgFail('value must not be <1: {r}')
except MsgFail as e:
- raise ArgFail(arg_char, e.msg)
+ raise ArgFail(arg_char, e.msg) from e
try:
float(args.print_margin)
except ValueError as e:
return page_range, crops
-def parse_page_range(
- range_string: Optional[str],
- idx_after: int
- ) -> tuple[int, int]:
- 'Based on actual pages size read range_string into range limit indices.'
- idx_start = 0
- if range_string:
- start, end = range_string.split('-')
- if not (len(start) == 0 or start == 'start'):
- idx_start = int(start) - 1
- if not (len(end) == 0 or end == 'end'):
- idx_after = int(end)
- return idx_start, idx_after
-
-
class Book:
'Collection of pages.'
def __init__(
self,
input_filenames: list[str],
- args_page_range: list[str],
+ args_page_range: list[PageRange],
do_analyze: bool
) -> None:
self.pages: list[Page] = []
to_close.close()
raise ArgFail('i',
f'cannot interpret {path} as PDF file') from e
- range_string = None
- if args_page_range and len(args_page_range) > i:
- range_string = args_page_range[i]
- for old_page_num in range(*parse_page_range(range_string,
- len(reader.pages))):
+ for old_page_num in (
+ args_page_range[i] if len(args_page_range) > i
+ else PageRange(None, len(reader.pages))
+ ).ranging():
new_page_num += 1
page = (Page.new_blank() if old_page_num >= len(reader.pages)
else Page(reader.pages[old_page_num]))
len_ = len(self)
if args_crops:
for c_string in args_crops:
- if (page_range := split_crops_string(c_string)[0])\
- and parse_page_range(page_range, len_)[1] > len_:
- raise ArgFail('c',
- 'page range goes beyond number of pages '
- f'we\'re building: {page_range}')
+ try:
+ PageRange(split_crops_string(c_string)[0], len_ - 1)
+ except MsgFail as e:
+ raise ArgFail('c', e.msg) from e
if args_rotate_page:
for r in [r for r in args_rotate_page if r > len_]:
raise ArgFail('r',
'Calculate individual PageCrops from inputs.'
if args_crops:
for c_string in args_crops:
- page_range, crops = split_crops_string(c_string)
- idx_start, idx_after = parse_page_range(page_range, len(self))
+ range_code, crops = split_crops_string(c_string)
+ page_range = PageRange(range_code, len(self))
prefix = '-c, -t' if args_symmetry else '-c'
suffix = ((' (but alternating left and right crop '
'between even and odd pages)') if args_symmetry
else '')
page_crop = PageCrop(*(float(crop)
for crop in crops.split(',')))
- print(f'{prefix}: to pages {idx_start + 1}:{idx_after} '
- f'applying crop: {page_crop.format_in_cm}{suffix}')
- for idx in range(idx_start, idx_after):
+ print(f'{prefix}: to pages {page_range.start + 1}:'
+ f'{page_range.end} applying crop: '
+ f'{page_crop.format_in_cm}{suffix}')
+ for idx in page_range.ranging():
self.pages[idx].crop.add(page_crop,
args_symmetry and idx % 2)
elif args_keep_mediabox: