pdf-section-binding 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdf_section_binding/__init__.py +15 -0
- pdf_section_binding/cli.py +380 -0
- pdf_section_binding/core.py +252 -0
- pdf_section_binding/version.py +8 -0
- pdf_section_binding-1.0.0.dist-info/METADATA +324 -0
- pdf_section_binding-1.0.0.dist-info/RECORD +10 -0
- pdf_section_binding-1.0.0.dist-info/WHEEL +5 -0
- pdf_section_binding-1.0.0.dist-info/entry_points.txt +3 -0
- pdf_section_binding-1.0.0.dist-info/licenses/LICENSE +21 -0
- pdf_section_binding-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
"""PDF Section Binding - A CLI tool for reordering PDF pages for bookbinding."""
|
2
|
+
|
3
|
+
from .version import __version__, __author__, __email__, __description__
|
4
|
+
from .core import calculate_signature_order, SectionBindingProcessor
|
5
|
+
from .cli import main
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"__version__",
|
9
|
+
"__author__",
|
10
|
+
"__email__",
|
11
|
+
"__description__",
|
12
|
+
"calculate_signature_order",
|
13
|
+
"SectionBindingProcessor",
|
14
|
+
"main",
|
15
|
+
]
|
@@ -0,0 +1,380 @@
|
|
1
|
+
"""Enhanced CLI interface for PDF section binding."""
|
2
|
+
|
3
|
+
import argparse
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
import traceback
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Optional
|
9
|
+
|
10
|
+
from .core import SectionBindingProcessor, print_binding_instructions
|
11
|
+
from .version import __version__, __description__
|
12
|
+
|
13
|
+
|
14
|
+
# ANSI color codes for better CLI experience
|
15
|
+
class Colors:
|
16
|
+
"""ANSI color codes for terminal output."""
|
17
|
+
|
18
|
+
RESET = "\033[0m"
|
19
|
+
BOLD = "\033[1m"
|
20
|
+
DIM = "\033[2m"
|
21
|
+
RED = "\033[91m"
|
22
|
+
GREEN = "\033[92m"
|
23
|
+
YELLOW = "\033[93m"
|
24
|
+
BLUE = "\033[94m"
|
25
|
+
MAGENTA = "\033[95m"
|
26
|
+
CYAN = "\033[96m"
|
27
|
+
WHITE = "\033[97m"
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def colorize(cls, text: str, color: str) -> str:
|
31
|
+
"""Colorize text if terminal supports it."""
|
32
|
+
if os.getenv("NO_COLOR") or not sys.stdout.isatty():
|
33
|
+
return text
|
34
|
+
return f"{color}{text}{cls.RESET}"
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
def success(cls, text: str) -> str:
|
38
|
+
"""Green success text."""
|
39
|
+
return cls.colorize(text, cls.GREEN)
|
40
|
+
|
41
|
+
@classmethod
|
42
|
+
def error(cls, text: str) -> str:
|
43
|
+
"""Red error text."""
|
44
|
+
return cls.colorize(text, cls.RED)
|
45
|
+
|
46
|
+
@classmethod
|
47
|
+
def warning(cls, text: str) -> str:
|
48
|
+
"""Yellow warning text."""
|
49
|
+
return cls.colorize(text, cls.YELLOW)
|
50
|
+
|
51
|
+
@classmethod
|
52
|
+
def info(cls, text: str) -> str:
|
53
|
+
"""Blue info text."""
|
54
|
+
return cls.colorize(text, cls.BLUE)
|
55
|
+
|
56
|
+
@classmethod
|
57
|
+
def highlight(cls, text: str) -> str:
|
58
|
+
"""Cyan highlighted text."""
|
59
|
+
return cls.colorize(text, cls.CYAN)
|
60
|
+
|
61
|
+
|
62
|
+
def format_file_size(size_bytes: int) -> str:
|
63
|
+
"""Format file size in human readable format."""
|
64
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
65
|
+
if size_bytes < 1024.0:
|
66
|
+
return f"{size_bytes:.1f} {unit}"
|
67
|
+
size_bytes /= 1024.0
|
68
|
+
return f"{size_bytes:.1f} TB"
|
69
|
+
|
70
|
+
|
71
|
+
def suggest_signature_sizes(current: int) -> str:
|
72
|
+
"""Suggest alternative signature sizes."""
|
73
|
+
if current % 4 != 0:
|
74
|
+
suggested_lower = (current // 4) * 4
|
75
|
+
suggested_higher = ((current // 4) + 1) * 4
|
76
|
+
return f"Try: {suggested_lower} or {suggested_higher}"
|
77
|
+
|
78
|
+
common_sizes = [4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64]
|
79
|
+
nearby = [s for s in common_sizes if abs(s - current) <= 8 and s != current]
|
80
|
+
if nearby:
|
81
|
+
return f"Common alternatives: {', '.join(map(str, nearby[:3]))}"
|
82
|
+
return "Common sizes: 4, 8, 16, 32, 40"
|
83
|
+
|
84
|
+
|
85
|
+
def validate_pdf_file(filepath: str) -> Optional[str]:
|
86
|
+
"""Validate PDF file and return error message if invalid."""
|
87
|
+
if not os.path.exists(filepath):
|
88
|
+
return f"File not found: {filepath}"
|
89
|
+
|
90
|
+
if not os.path.isfile(filepath):
|
91
|
+
return f"Path is not a file: {filepath}"
|
92
|
+
|
93
|
+
if not os.access(filepath, os.R_OK):
|
94
|
+
return f"File is not readable: {filepath}"
|
95
|
+
|
96
|
+
# Check file size
|
97
|
+
size = os.path.getsize(filepath)
|
98
|
+
if size == 0:
|
99
|
+
return f"File is empty: {filepath}"
|
100
|
+
|
101
|
+
if size > 500 * 1024 * 1024: # 500MB
|
102
|
+
return f"File is very large ({format_file_size(size)}). Processing may be slow."
|
103
|
+
|
104
|
+
# Basic PDF validation
|
105
|
+
try:
|
106
|
+
with open(filepath, "rb") as f:
|
107
|
+
header = f.read(4)
|
108
|
+
if header != b"%PDF":
|
109
|
+
return f"File does not appear to be a valid PDF: {filepath}"
|
110
|
+
except (OSError, IOError) as e:
|
111
|
+
return f"Error reading file: {e}"
|
112
|
+
|
113
|
+
return None
|
114
|
+
|
115
|
+
|
116
|
+
def create_parser() -> argparse.ArgumentParser:
|
117
|
+
"""Create and configure the argument parser."""
|
118
|
+
parser = argparse.ArgumentParser(
|
119
|
+
prog="pdf-section-binding",
|
120
|
+
description=__description__,
|
121
|
+
epilog="""
|
122
|
+
Examples:
|
123
|
+
%(prog)s book.pdf # Basic usage (8-page signatures)
|
124
|
+
%(prog)s book.pdf -s 40 # 10 papers per signature
|
125
|
+
%(prog)s book.pdf -o output.pdf # Specify output file
|
126
|
+
%(prog)s book.pdf --dry-run # Preview without creating file
|
127
|
+
%(prog)s book.pdf -v # Verbose output
|
128
|
+
""",
|
129
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
130
|
+
)
|
131
|
+
|
132
|
+
# Positional arguments
|
133
|
+
parser.add_argument("input", help="Input PDF file path")
|
134
|
+
|
135
|
+
# Optional arguments
|
136
|
+
parser.add_argument(
|
137
|
+
"-o",
|
138
|
+
"--output",
|
139
|
+
help="Output PDF file path (default: input_name_section_bound.pdf)",
|
140
|
+
)
|
141
|
+
|
142
|
+
parser.add_argument(
|
143
|
+
"-s",
|
144
|
+
"--signature-size",
|
145
|
+
type=int,
|
146
|
+
default=8,
|
147
|
+
metavar="PAGES",
|
148
|
+
help="Pages per signature (must be multiple of 4, default: 8). "
|
149
|
+
"Common: 4, 8, 16, 32, 40. Formula: papers = pages ÷ 4",
|
150
|
+
)
|
151
|
+
|
152
|
+
# Behavior flags
|
153
|
+
parser.add_argument(
|
154
|
+
"--dry-run",
|
155
|
+
action="store_true",
|
156
|
+
help="Show what would be done without creating output file",
|
157
|
+
)
|
158
|
+
|
159
|
+
parser.add_argument(
|
160
|
+
"-v", "--verbose", action="store_true", help="Enable verbose output"
|
161
|
+
)
|
162
|
+
|
163
|
+
parser.add_argument(
|
164
|
+
"-q", "--quiet", action="store_true", help="Suppress all output except errors"
|
165
|
+
)
|
166
|
+
|
167
|
+
parser.add_argument(
|
168
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
169
|
+
)
|
170
|
+
|
171
|
+
# Advanced options
|
172
|
+
parser.add_argument(
|
173
|
+
"--force", action="store_true", help="Overwrite output file if it exists"
|
174
|
+
)
|
175
|
+
|
176
|
+
return parser
|
177
|
+
|
178
|
+
|
179
|
+
def validate_arguments(args: argparse.Namespace) -> None:
|
180
|
+
"""Validate command line arguments.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
args: Parsed command line arguments
|
184
|
+
|
185
|
+
Raises:
|
186
|
+
SystemExit: If validation fails
|
187
|
+
"""
|
188
|
+
# Enhanced PDF file validation
|
189
|
+
pdf_error = validate_pdf_file(args.input)
|
190
|
+
if pdf_error:
|
191
|
+
if "very large" in pdf_error:
|
192
|
+
print(Colors.warning(f"Warning: {pdf_error}"), file=sys.stderr)
|
193
|
+
else:
|
194
|
+
print(Colors.error(f"Error: {pdf_error}"), file=sys.stderr)
|
195
|
+
sys.exit(1)
|
196
|
+
|
197
|
+
# Enhanced signature size validation
|
198
|
+
if args.signature_size % 4 != 0:
|
199
|
+
suggestions = suggest_signature_sizes(args.signature_size)
|
200
|
+
print(
|
201
|
+
Colors.error(
|
202
|
+
"Error: Signature size must be a multiple of 4 (each paper = 4 pages)."
|
203
|
+
),
|
204
|
+
file=sys.stderr,
|
205
|
+
)
|
206
|
+
print(
|
207
|
+
Colors.info(f"You specified {args.signature_size}. {suggestions}"),
|
208
|
+
file=sys.stderr,
|
209
|
+
)
|
210
|
+
sys.exit(1)
|
211
|
+
|
212
|
+
if args.signature_size < 4:
|
213
|
+
print(
|
214
|
+
Colors.error("Error: Signature size must be at least 4 pages (1 paper)."),
|
215
|
+
file=sys.stderr,
|
216
|
+
)
|
217
|
+
sys.exit(1)
|
218
|
+
|
219
|
+
if args.signature_size > 128:
|
220
|
+
print(
|
221
|
+
Colors.error(
|
222
|
+
f"Error: Signature size too large ({args.signature_size}). "
|
223
|
+
"Maximum recommended: 128 pages (32 papers)."
|
224
|
+
),
|
225
|
+
file=sys.stderr,
|
226
|
+
)
|
227
|
+
print(
|
228
|
+
Colors.info(
|
229
|
+
"Large signature sizes make folding difficult and may not bind well."
|
230
|
+
),
|
231
|
+
file=sys.stderr,
|
232
|
+
)
|
233
|
+
sys.exit(1)
|
234
|
+
|
235
|
+
# Generate output filename if not provided
|
236
|
+
if not args.output:
|
237
|
+
input_path = Path(args.input)
|
238
|
+
args.output = str(input_path.with_stem(f"{input_path.stem}_section_bound"))
|
239
|
+
|
240
|
+
# Validate output path
|
241
|
+
if not args.dry_run:
|
242
|
+
output_path = Path(args.output)
|
243
|
+
|
244
|
+
# Check if output file exists and --force not specified
|
245
|
+
if output_path.exists() and not args.force:
|
246
|
+
print(
|
247
|
+
f"Error: Output file '{args.output}' already exists. "
|
248
|
+
"Use --force to overwrite.",
|
249
|
+
file=sys.stderr,
|
250
|
+
)
|
251
|
+
sys.exit(1)
|
252
|
+
|
253
|
+
# Check if output directory is writable
|
254
|
+
output_dir = output_path.parent
|
255
|
+
if not output_dir.exists():
|
256
|
+
try:
|
257
|
+
output_dir.mkdir(parents=True)
|
258
|
+
except PermissionError:
|
259
|
+
print(
|
260
|
+
f"Error: Cannot create output directory '{output_dir}'.",
|
261
|
+
file=sys.stderr,
|
262
|
+
)
|
263
|
+
sys.exit(1)
|
264
|
+
|
265
|
+
if not os.access(output_dir, os.W_OK):
|
266
|
+
print(
|
267
|
+
f"Error: No write permission for output directory '{output_dir}'.",
|
268
|
+
file=sys.stderr,
|
269
|
+
)
|
270
|
+
sys.exit(1)
|
271
|
+
|
272
|
+
# Validate conflicting options
|
273
|
+
if args.quiet and args.verbose:
|
274
|
+
print("Error: Cannot use both --quiet and --verbose options.", file=sys.stderr)
|
275
|
+
sys.exit(1)
|
276
|
+
|
277
|
+
|
278
|
+
def main() -> int:
|
279
|
+
"""Main CLI entry point.
|
280
|
+
|
281
|
+
Returns:
|
282
|
+
Exit code (0 for success, non-zero for error)
|
283
|
+
"""
|
284
|
+
try:
|
285
|
+
# Parse arguments
|
286
|
+
parser = create_parser()
|
287
|
+
args = parser.parse_args()
|
288
|
+
|
289
|
+
# Validate arguments
|
290
|
+
validate_arguments(args)
|
291
|
+
|
292
|
+
# Configure output verbosity
|
293
|
+
verbose = args.verbose and not args.quiet
|
294
|
+
quiet = args.quiet
|
295
|
+
|
296
|
+
if not quiet:
|
297
|
+
print(Colors.highlight(f"PDF Section Binding Tool v{__version__}"))
|
298
|
+
print("=" * 50)
|
299
|
+
|
300
|
+
# Create processor
|
301
|
+
processor = SectionBindingProcessor(verbose=verbose)
|
302
|
+
|
303
|
+
# Process the PDF
|
304
|
+
result = processor.process_pdf(
|
305
|
+
input_path=args.input,
|
306
|
+
output_path=args.output,
|
307
|
+
signature_size=args.signature_size,
|
308
|
+
dry_run=args.dry_run,
|
309
|
+
)
|
310
|
+
|
311
|
+
# Print results and instructions
|
312
|
+
if not quiet:
|
313
|
+
if args.dry_run:
|
314
|
+
print(f"\n{Colors.info('🔍 DRY RUN ANALYSIS:')}")
|
315
|
+
print(
|
316
|
+
f"Would process "
|
317
|
+
f"{Colors.highlight(str(result['total_pages']))} pages"
|
318
|
+
)
|
319
|
+
print(
|
320
|
+
f"Would create "
|
321
|
+
f"{Colors.highlight(str(result['total_signatures']))} "
|
322
|
+
"signatures"
|
323
|
+
)
|
324
|
+
print(
|
325
|
+
f"Would need "
|
326
|
+
f"{Colors.highlight(str(result['total_papers']))} "
|
327
|
+
"papers total"
|
328
|
+
)
|
329
|
+
print(f"Would save to: {Colors.highlight(result['output_path'])}")
|
330
|
+
else:
|
331
|
+
print(
|
332
|
+
f"\n{Colors.success('✅ Successfully created:')} "
|
333
|
+
f"{Colors.highlight(result['output_path'])}"
|
334
|
+
)
|
335
|
+
print(
|
336
|
+
f"{Colors.info('📊 Processed')} "
|
337
|
+
f"{Colors.highlight(str(result['total_pages']))} pages into "
|
338
|
+
f"{Colors.highlight(str(result['output_pages']))} "
|
339
|
+
"reordered pages"
|
340
|
+
)
|
341
|
+
|
342
|
+
# Print binding instructions
|
343
|
+
print_binding_instructions(result, quiet=quiet)
|
344
|
+
|
345
|
+
return 0
|
346
|
+
|
347
|
+
except KeyboardInterrupt:
|
348
|
+
if not (locals().get("args") and args.quiet):
|
349
|
+
print(
|
350
|
+
f"\n{Colors.error('❌ Operation cancelled by user.')}", file=sys.stderr
|
351
|
+
)
|
352
|
+
return 130 # Standard exit code for SIGINT
|
353
|
+
|
354
|
+
except FileNotFoundError as e:
|
355
|
+
print(Colors.error(f"❌ File error: {e}"), file=sys.stderr)
|
356
|
+
return 2
|
357
|
+
|
358
|
+
except PermissionError as e:
|
359
|
+
print(Colors.error(f"❌ Permission error: {e}"), file=sys.stderr)
|
360
|
+
return 13
|
361
|
+
|
362
|
+
except ValueError as e:
|
363
|
+
print(Colors.error(f"❌ Invalid input: {e}"), file=sys.stderr)
|
364
|
+
return 22
|
365
|
+
|
366
|
+
except RuntimeError as e:
|
367
|
+
print(Colors.error(f"❌ Runtime error: {e}"), file=sys.stderr)
|
368
|
+
if locals().get("args") and args.verbose:
|
369
|
+
traceback.print_exc()
|
370
|
+
return 1
|
371
|
+
|
372
|
+
except (OSError, IOError) as e:
|
373
|
+
print(Colors.error(f"❌ System error: {e}"), file=sys.stderr)
|
374
|
+
if locals().get("args") and args.verbose:
|
375
|
+
traceback.print_exc()
|
376
|
+
return 1
|
377
|
+
|
378
|
+
|
379
|
+
if __name__ == "__main__":
|
380
|
+
sys.exit(main())
|
@@ -0,0 +1,252 @@
|
|
1
|
+
"""Core functionality for PDF section binding."""
|
2
|
+
|
3
|
+
from typing import List
|
4
|
+
from PyPDF2 import PdfReader, PdfWriter
|
5
|
+
|
6
|
+
|
7
|
+
def create_progress_bar(current: int, total: int, width: int = 50) -> str:
|
8
|
+
"""Create a simple progress bar string."""
|
9
|
+
if total == 0:
|
10
|
+
return "[" + "=" * width + "]"
|
11
|
+
|
12
|
+
progress = current / total
|
13
|
+
filled = int(width * progress)
|
14
|
+
progress_bar = "=" * filled + "-" * (width - filled)
|
15
|
+
percentage = int(progress * 100)
|
16
|
+
return f"[{progress_bar}] {percentage:3d}% ({current}/{total})"
|
17
|
+
|
18
|
+
|
19
|
+
def show_progress(current: int, total: int, prefix: str = "Progress") -> None:
|
20
|
+
"""Show progress bar for terminal output."""
|
21
|
+
if total < 50: # Don't show progress for small files
|
22
|
+
return
|
23
|
+
|
24
|
+
if current % max(1, total // 20) == 0 or current == total: # Update every 5%
|
25
|
+
progress_bar = create_progress_bar(current, total)
|
26
|
+
print(f"\r{prefix}: {progress_bar}", end="", flush=True)
|
27
|
+
if current == total:
|
28
|
+
print() # New line when complete
|
29
|
+
|
30
|
+
|
31
|
+
def calculate_signature_order(total_pages: int, signature_size: int) -> List[int]:
|
32
|
+
"""
|
33
|
+
Calculate the page order for section binding.
|
34
|
+
|
35
|
+
For section binding, pages are arranged so that when printed double-sided
|
36
|
+
and folded, they appear in correct reading order.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
total_pages: Total number of pages in the PDF
|
40
|
+
signature_size: Number of pages per signature (must be multiple of 4)
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
Ordered list of page numbers for section binding
|
44
|
+
|
45
|
+
Raises:
|
46
|
+
ValueError: If signature_size is not a multiple of 4 or is invalid
|
47
|
+
"""
|
48
|
+
if signature_size % 4 != 0:
|
49
|
+
raise ValueError(
|
50
|
+
f"Signature size must be a multiple of 4, got {signature_size}"
|
51
|
+
)
|
52
|
+
|
53
|
+
if signature_size < 4:
|
54
|
+
raise ValueError(f"Signature size must be at least 4, got {signature_size}")
|
55
|
+
|
56
|
+
# Pad pages to make them divisible by signature size
|
57
|
+
padded_pages = (
|
58
|
+
(total_pages + signature_size - 1) // signature_size
|
59
|
+
) * signature_size
|
60
|
+
|
61
|
+
# Create list of pages (1-indexed)
|
62
|
+
pages = list(range(1, total_pages + 1))
|
63
|
+
|
64
|
+
# Add blank pages if needed
|
65
|
+
while len(pages) < padded_pages:
|
66
|
+
pages.append(None) # None represents blank pages
|
67
|
+
|
68
|
+
# Calculate signature order
|
69
|
+
reordered_pages = []
|
70
|
+
|
71
|
+
# Process each signature
|
72
|
+
for signature_start in range(0, padded_pages, signature_size):
|
73
|
+
# Calculate the correct order for this signature
|
74
|
+
signature_order = []
|
75
|
+
|
76
|
+
for i in range(signature_size // 2):
|
77
|
+
left_page = signature_start + i
|
78
|
+
right_page = signature_start + signature_size - 1 - i
|
79
|
+
|
80
|
+
# Add the pages for this sheet
|
81
|
+
if right_page < len(pages):
|
82
|
+
signature_order.append(pages[right_page])
|
83
|
+
if left_page < len(pages):
|
84
|
+
signature_order.append(pages[left_page])
|
85
|
+
|
86
|
+
reordered_pages.extend(signature_order)
|
87
|
+
|
88
|
+
return [p for p in reordered_pages if p is not None]
|
89
|
+
|
90
|
+
|
91
|
+
class SectionBindingProcessor:
|
92
|
+
"""Main processor for PDF section binding operations."""
|
93
|
+
|
94
|
+
def __init__(self, verbose: bool = False):
|
95
|
+
"""Initialize processor.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
verbose: Whether to output verbose information
|
99
|
+
"""
|
100
|
+
self.verbose = verbose
|
101
|
+
|
102
|
+
def log(self, message: str) -> None:
|
103
|
+
"""Log a message if verbose mode is enabled."""
|
104
|
+
if self.verbose:
|
105
|
+
print(f"[INFO] {message}")
|
106
|
+
|
107
|
+
def process_pdf(
|
108
|
+
self,
|
109
|
+
input_path: str,
|
110
|
+
output_path: str,
|
111
|
+
signature_size: int = 8,
|
112
|
+
dry_run: bool = False,
|
113
|
+
) -> dict:
|
114
|
+
"""
|
115
|
+
Process a PDF for section binding.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
input_path: Path to input PDF
|
119
|
+
output_path: Path to output PDF
|
120
|
+
signature_size: Pages per signature (default: 8)
|
121
|
+
dry_run: If True, don't create output file, just analyze
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
Dictionary with processing information
|
125
|
+
|
126
|
+
Raises:
|
127
|
+
FileNotFoundError: If input file doesn't exist
|
128
|
+
PermissionError: If unable to read input or write output
|
129
|
+
ValueError: If signature size is invalid
|
130
|
+
"""
|
131
|
+
try:
|
132
|
+
# Read the input PDF
|
133
|
+
self.log(f"Reading PDF: {input_path}")
|
134
|
+
reader = PdfReader(input_path)
|
135
|
+
total_pages = len(reader.pages)
|
136
|
+
|
137
|
+
self.log(f"Total pages: {total_pages}")
|
138
|
+
self.log(f"Signature size: {signature_size}")
|
139
|
+
|
140
|
+
# Calculate page order
|
141
|
+
page_order = calculate_signature_order(total_pages, signature_size)
|
142
|
+
|
143
|
+
# Calculate statistics
|
144
|
+
papers_per_signature = signature_size // 4
|
145
|
+
total_signatures = (total_pages + signature_size - 1) // signature_size
|
146
|
+
total_papers = total_signatures * papers_per_signature
|
147
|
+
|
148
|
+
result = {
|
149
|
+
"input_path": input_path,
|
150
|
+
"output_path": output_path,
|
151
|
+
"total_pages": total_pages,
|
152
|
+
"signature_size": signature_size,
|
153
|
+
"papers_per_signature": papers_per_signature,
|
154
|
+
"total_signatures": total_signatures,
|
155
|
+
"total_papers": total_papers,
|
156
|
+
"page_order": page_order,
|
157
|
+
"dry_run": dry_run,
|
158
|
+
}
|
159
|
+
|
160
|
+
if dry_run:
|
161
|
+
self.log("Dry run mode - not creating output file")
|
162
|
+
return result
|
163
|
+
|
164
|
+
# Create output PDF
|
165
|
+
self.log("Creating reordered PDF...")
|
166
|
+
writer = PdfWriter()
|
167
|
+
|
168
|
+
# Add pages in the calculated order
|
169
|
+
for i, page_num in enumerate(page_order):
|
170
|
+
# Show progress for large documents
|
171
|
+
if not dry_run and self.verbose and total_pages > 50:
|
172
|
+
show_progress(i + 1, len(page_order), "Creating PDF")
|
173
|
+
elif not dry_run and i % 100 == 0 and total_pages > 200:
|
174
|
+
# Show minimal progress for very large files even without verbose
|
175
|
+
self.log(f"Processing page {i+1}/{len(page_order)}...")
|
176
|
+
|
177
|
+
if page_num and page_num <= total_pages:
|
178
|
+
writer.add_page(reader.pages[page_num - 1]) # Convert to 0-indexed
|
179
|
+
|
180
|
+
# Write the output PDF
|
181
|
+
self.log(f"Writing output: {output_path}")
|
182
|
+
with open(output_path, "wb") as output_file:
|
183
|
+
writer.write(output_file)
|
184
|
+
|
185
|
+
result["output_pages"] = len(writer.pages)
|
186
|
+
self.log(
|
187
|
+
f"Successfully created {output_path} with {len(writer.pages)} pages"
|
188
|
+
)
|
189
|
+
|
190
|
+
return result
|
191
|
+
|
192
|
+
except FileNotFoundError as e:
|
193
|
+
raise FileNotFoundError(f"Input file not found: {input_path}") from e
|
194
|
+
except PermissionError as e:
|
195
|
+
raise PermissionError(f"Permission error: {e}") from e
|
196
|
+
except Exception as e:
|
197
|
+
raise RuntimeError(f"Unexpected error processing PDF: {e}") from e
|
198
|
+
|
199
|
+
|
200
|
+
def print_binding_instructions(result: dict, quiet: bool = False) -> None:
|
201
|
+
"""Print binding instructions based on processing result."""
|
202
|
+
if quiet:
|
203
|
+
return
|
204
|
+
|
205
|
+
# Import Colors here to avoid circular imports
|
206
|
+
try:
|
207
|
+
from .cli import Colors
|
208
|
+
except ImportError:
|
209
|
+
# Fallback if Colors not available
|
210
|
+
class Colors:
|
211
|
+
"""Fallback color class when CLI colors are not available."""
|
212
|
+
|
213
|
+
@staticmethod
|
214
|
+
def highlight(text):
|
215
|
+
"""Return text without highlighting."""
|
216
|
+
return text
|
217
|
+
|
218
|
+
@staticmethod
|
219
|
+
def info(text):
|
220
|
+
"""Return text without info formatting."""
|
221
|
+
return text
|
222
|
+
|
223
|
+
@staticmethod
|
224
|
+
def success(text):
|
225
|
+
"""Return text without success formatting."""
|
226
|
+
return text
|
227
|
+
|
228
|
+
print("\n" + "=" * 60)
|
229
|
+
print(Colors.highlight("SECTION BINDING INSTRUCTIONS"))
|
230
|
+
print("=" * 60)
|
231
|
+
print(f"📄 Input: {Colors.info(result['input_path'])}")
|
232
|
+
print(f"📋 Total pages: {Colors.highlight(str(result['total_pages']))}")
|
233
|
+
print(
|
234
|
+
f"📐 Signature size: {Colors.highlight(str(result['signature_size']))}" " pages"
|
235
|
+
)
|
236
|
+
print(
|
237
|
+
f"📎 Papers per signature: "
|
238
|
+
f"{Colors.highlight(str(result['papers_per_signature']))}"
|
239
|
+
)
|
240
|
+
print(f"📚 Total signatures: {Colors.highlight(str(result['total_signatures']))}")
|
241
|
+
print(f"🗞️ Total papers needed: {Colors.highlight(str(result['total_papers']))}")
|
242
|
+
|
243
|
+
if not result["dry_run"]:
|
244
|
+
print(f"💾 Output: {Colors.success(result['output_path'])}")
|
245
|
+
|
246
|
+
print(f"\n{Colors.info('📋 PRINTING & BINDING STEPS:')}")
|
247
|
+
print("1. Print the output PDF double-sided (flip on long edge)")
|
248
|
+
print(f"2. Each {result['papers_per_signature']} papers forms one signature")
|
249
|
+
print("3. Fold each signature in half along the center")
|
250
|
+
print("4. Stack all signatures in order")
|
251
|
+
print("5. Bind along the folded edge (staple, glue, or spiral bind)")
|
252
|
+
print("=" * 60)
|
@@ -0,0 +1,324 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: pdf-section-binding
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: A CLI tool for reordering PDF pages for section binding (bookbinding)
|
5
|
+
Home-page: https://github.com/sravankumarrekandar/pdf-section-binding
|
6
|
+
Author: Sravan Kumar Rekandar
|
7
|
+
Author-email: Sravan Kumar Rekandar <sravankumarrekandar@example.com>
|
8
|
+
License: MIT
|
9
|
+
Project-URL: Homepage, https://github.com/yourusername/pdf-section-binding
|
10
|
+
Project-URL: Bug Reports, https://github.com/yourusername/pdf-section-binding/issues
|
11
|
+
Project-URL: Source, https://github.com/yourusername/pdf-section-binding
|
12
|
+
Project-URL: Documentation, https://github.com/yourusername/pdf-section-binding#readme
|
13
|
+
Keywords: pdf,bookbinding,section-binding,printing,cli,page-reordering,signatures,folding
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
16
|
+
Classifier: Intended Audience :: Developers
|
17
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
18
|
+
Classifier: Topic :: Printing
|
19
|
+
Classifier: Topic :: Utilities
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
21
|
+
Classifier: Programming Language :: Python :: 3.7
|
22
|
+
Classifier: Programming Language :: Python :: 3.8
|
23
|
+
Classifier: Programming Language :: Python :: 3.9
|
24
|
+
Classifier: Programming Language :: Python :: 3.10
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
27
|
+
Classifier: Operating System :: OS Independent
|
28
|
+
Classifier: Environment :: Console
|
29
|
+
Requires-Python: >=3.7
|
30
|
+
Description-Content-Type: text/markdown
|
31
|
+
License-File: LICENSE
|
32
|
+
Requires-Dist: PyPDF2>=3.0.0
|
33
|
+
Provides-Extra: dev
|
34
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
35
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
36
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
37
|
+
Requires-Dist: flake8>=5.0.0; extra == "dev"
|
38
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
39
|
+
Provides-Extra: test
|
40
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
41
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
42
|
+
Dynamic: author
|
43
|
+
Dynamic: home-page
|
44
|
+
Dynamic: license-file
|
45
|
+
Dynamic: requires-python
|
46
|
+
|
47
|
+
# PDF Section Binding Tool
|
48
|
+
|
49
|
+
This tool reorders PDF pages for section binding (bookbinding), where pages are arranged in signatures (folded sheets) so that when printed and folded, they appear in the correct reading order.
|
50
|
+
|
51
|
+
## Installation
|
52
|
+
|
53
|
+
### From PyPI (Recommended)
|
54
|
+
|
55
|
+
```bash
|
56
|
+
pip install pdf-section-binding
|
57
|
+
```
|
58
|
+
|
59
|
+
### From Source
|
60
|
+
|
61
|
+
1. Clone the repository:
|
62
|
+
```bash
|
63
|
+
git clone https://github.com/yourusername/pdf-section-binding.git
|
64
|
+
cd pdf-section-binding
|
65
|
+
```
|
66
|
+
|
67
|
+
2. Create and activate virtual environment:
|
68
|
+
```bash
|
69
|
+
python3 -m venv venv
|
70
|
+
source venv/bin/activate # On macOS/Linux
|
71
|
+
# or
|
72
|
+
venv\Scripts\activate # On Windows
|
73
|
+
```
|
74
|
+
|
75
|
+
3. Install in development mode:
|
76
|
+
```bash
|
77
|
+
pip install -e .
|
78
|
+
```
|
79
|
+
|
80
|
+
## Usage
|
81
|
+
|
82
|
+
After installation, use the `pdf-section-binding` command (or the shorter `section-binding` alias):
|
83
|
+
|
84
|
+
```bash
|
85
|
+
pdf-section-binding input.pdf [options]
|
86
|
+
```
|
87
|
+
|
88
|
+
### Command Line Options
|
89
|
+
|
90
|
+
- `-o, --output`: Output PDF file path (default: `input_name_section_bound.pdf`)
|
91
|
+
- `-s, --signature-size`: Pages per signature - must be multiple of 4 (default: 8)
|
92
|
+
- `--dry-run`: Preview analysis without creating output file
|
93
|
+
- `-v, --verbose`: Enable verbose output with progress tracking
|
94
|
+
- `-q, --quiet`: Suppress all output except errors
|
95
|
+
- `--force`: Overwrite output file if it exists
|
96
|
+
- `--version`: Show version information
|
97
|
+
- `-h, --help`: Show help message
|
98
|
+
|
99
|
+
### Examples
|
100
|
+
|
101
|
+
```bash
|
102
|
+
# Basic usage with 8-page signatures (2 papers per signature)
|
103
|
+
pdf-section-binding book.pdf
|
104
|
+
|
105
|
+
# Specify output file and 4-page signatures (1 paper per signature)
|
106
|
+
pdf-section-binding book.pdf -o output.pdf -s 4
|
107
|
+
|
108
|
+
# Use 16-page signatures (4 papers per signature)
|
109
|
+
pdf-section-binding book.pdf -s 16
|
110
|
+
|
111
|
+
# Use 40-page signatures (10 papers per signature)
|
112
|
+
pdf-section-binding book.pdf -s 40
|
113
|
+
|
114
|
+
# Preview without creating file (dry run)
|
115
|
+
pdf-section-binding book.pdf --dry-run
|
116
|
+
|
117
|
+
# Verbose output with progress tracking
|
118
|
+
pdf-section-binding book.pdf -v
|
119
|
+
|
120
|
+
# Quiet mode (minimal output)
|
121
|
+
pdf-section-binding book.pdf -q
|
122
|
+
|
123
|
+
# Force overwrite existing output
|
124
|
+
pdf-section-binding book.pdf --force
|
125
|
+
```
|
126
|
+
|
127
|
+
## Features
|
128
|
+
|
129
|
+
✨ **Enhanced CLI Tool**
|
130
|
+
- 🎨 **Colorized output** for better user experience
|
131
|
+
- 📊 **Progress tracking** for large documents
|
132
|
+
- 🔍 **Dry run mode** to preview without creating files
|
133
|
+
- ⚡ **Fast processing** with optimized algorithms
|
134
|
+
- 🛡️ **Robust error handling** with helpful suggestions
|
135
|
+
- 📏 **Flexible signature sizes** (any multiple of 4)
|
136
|
+
|
137
|
+
🔧 **Advanced Options**
|
138
|
+
- Verbose and quiet modes
|
139
|
+
- Force overwrite protection
|
140
|
+
- Input validation with helpful error messages
|
141
|
+
- Multiple command aliases (`pdf-section-binding` or `section-binding`)
|
142
|
+
|
143
|
+
📦 **Library Usage**
|
144
|
+
- Use as a Python library in your own projects
|
145
|
+
- Clean API with `SectionBindingProcessor` class
|
146
|
+
- Comprehensive test suite included
|
147
|
+
|
148
|
+
## CLI Features in Detail
|
149
|
+
|
150
|
+
### Smart Error Messages
|
151
|
+
The tool provides helpful suggestions when you make mistakes:
|
152
|
+
|
153
|
+
```bash
|
154
|
+
$ pdf-section-binding book.pdf -s 15
|
155
|
+
Error: Signature size must be a multiple of 4 (each paper = 4 pages).
|
156
|
+
You specified 15. Try: 12 or 16
|
157
|
+
```
|
158
|
+
|
159
|
+
### Progress Tracking
|
160
|
+
For large documents, see progress in real-time:
|
161
|
+
|
162
|
+
```bash
|
163
|
+
$ pdf-section-binding large-book.pdf -v
|
164
|
+
PDF Section Binding Tool v1.0.0
|
165
|
+
==================================================
|
166
|
+
[INFO] Reading PDF: large-book.pdf
|
167
|
+
[INFO] Total pages: 1500
|
168
|
+
[INFO] Signature size: 8
|
169
|
+
[INFO] Creating reordered PDF...
|
170
|
+
Creating PDF: [████████████████████████████████████████████████] 100% (1500/1500)
|
171
|
+
[INFO] Writing output: large-book_section_bound.pdf
|
172
|
+
✅ Successfully created: large-book_section_bound.pdf
|
173
|
+
```
|
174
|
+
|
175
|
+
### Dry Run Analysis
|
176
|
+
Preview the binding setup without creating files:
|
177
|
+
|
178
|
+
```bash
|
179
|
+
$ pdf-section-binding book.pdf --dry-run
|
180
|
+
🔍 DRY RUN ANALYSIS:
|
181
|
+
Would process 701 pages
|
182
|
+
Would create 88 signatures
|
183
|
+
Would need 176 papers total
|
184
|
+
Would save to: book_section_bound.pdf
|
185
|
+
```
|
186
|
+
|
187
|
+
Section binding involves:
|
188
|
+
|
189
|
+
1. **Signature Creation**: Pages are grouped into signatures (folded sheets)
|
190
|
+
2. **Page Reordering**: Pages are rearranged so that when printed double-sided and folded, they appear in correct reading order
|
191
|
+
3. **Printing**: Print the reordered PDF double-sided
|
192
|
+
4. **Folding**: Fold each signature in half
|
193
|
+
5. **Binding**: Stack signatures and bind along the fold
|
194
|
+
|
195
|
+
## Signature Sizes
|
196
|
+
|
197
|
+
- **4 pages**: Each signature uses 1 paper (2 pages front, 2 pages back)
|
198
|
+
- **8 pages**: Each signature uses 2 papers (4 pages front, 4 pages back)
|
199
|
+
- **16 pages**: Each signature uses 4 papers (8 pages front, 8 pages back)
|
200
|
+
- **32 pages**: Each signature uses 8 papers (16 pages front, 16 pages back)
|
201
|
+
- **40 pages**: Each signature uses 10 papers (20 pages front, 20 pages back)
|
202
|
+
- **Custom sizes**: Any multiple of 4 pages (e.g., 12=3 papers, 20=5 papers, 24=6 papers)
|
203
|
+
|
204
|
+
## Paper Calculation
|
205
|
+
|
206
|
+
**Formula**: `Papers per signature = Signature size ÷ 4`
|
207
|
+
|
208
|
+
Examples:
|
209
|
+
- 10 papers = 40 pages per signature
|
210
|
+
- 6 papers = 24 pages per signature
|
211
|
+
- 3 papers = 12 pages per signature
|
212
|
+
|
213
|
+
## Example Output
|
214
|
+
|
215
|
+
For a 4-page signature with pages 1, 2, 3, 4:
|
216
|
+
- The reordered sequence would be: [4, 1, 3, 2]
|
217
|
+
- When printed double-sided and folded, you get pages in order: 1, 2, 3, 4
|
218
|
+
|
219
|
+
## Generated Files
|
220
|
+
|
221
|
+
This tool was used to process the book in `data/book.pdf`:
|
222
|
+
|
223
|
+
- `data/book_section_bound.pdf` - Default 8-page signatures (2 papers each)
|
224
|
+
- `data/book_40page_signature.pdf` - 40-page signatures (10 papers each)
|
225
|
+
|
226
|
+
## Publishing to PyPI
|
227
|
+
|
228
|
+
This package is ready for PyPI publication! Here's how to publish it:
|
229
|
+
|
230
|
+
### Prerequisites
|
231
|
+
|
232
|
+
1. Create accounts on [PyPI](https://pypi.org/) and [TestPyPI](https://test.pypi.org/)
|
233
|
+
2. Install build tools:
|
234
|
+
```bash
|
235
|
+
pip install build twine
|
236
|
+
```
|
237
|
+
|
238
|
+
### Build and Upload
|
239
|
+
|
240
|
+
1. **Build the package:**
|
241
|
+
```bash
|
242
|
+
python -m build
|
243
|
+
```
|
244
|
+
|
245
|
+
2. **Test on TestPyPI first:**
|
246
|
+
```bash
|
247
|
+
twine upload --repository testpypi dist/*
|
248
|
+
```
|
249
|
+
|
250
|
+
3. **Upload to PyPI:**
|
251
|
+
```bash
|
252
|
+
twine upload dist/*
|
253
|
+
```
|
254
|
+
|
255
|
+
### Package Structure
|
256
|
+
|
257
|
+
The package follows modern Python packaging standards:
|
258
|
+
|
259
|
+
```
|
260
|
+
pdf-section-binding/
|
261
|
+
├── src/pdf_section_binding/ # Source code
|
262
|
+
│ ├── __init__.py
|
263
|
+
│ ├── cli.py # Enhanced CLI interface
|
264
|
+
│ ├── core.py # Core processing logic
|
265
|
+
│ └── version.py # Version and metadata
|
266
|
+
├── tests/ # Comprehensive test suite
|
267
|
+
├── pyproject.toml # Modern packaging config
|
268
|
+
├── setup.py # Fallback setup config
|
269
|
+
├── LICENSE # MIT license
|
270
|
+
├── README.md # This file
|
271
|
+
└── requirements.txt # Dependencies
|
272
|
+
|
273
|
+
```
|
274
|
+
|
275
|
+
### Development Setup
|
276
|
+
|
277
|
+
For development work:
|
278
|
+
|
279
|
+
```bash
|
280
|
+
# Clone and setup
|
281
|
+
git clone <your-repo-url>
|
282
|
+
cd pdf-section-binding
|
283
|
+
|
284
|
+
# Create virtual environment
|
285
|
+
python -m venv venv
|
286
|
+
source venv/bin/activate # or venv\Scripts\activate on Windows
|
287
|
+
|
288
|
+
# Install in development mode
|
289
|
+
pip install -e .[dev]
|
290
|
+
|
291
|
+
# Run tests
|
292
|
+
pytest
|
293
|
+
|
294
|
+
# Run with coverage
|
295
|
+
pytest --cov=pdf_section_binding
|
296
|
+
|
297
|
+
# Format code
|
298
|
+
black src/ tests/
|
299
|
+
|
300
|
+
# Lint code
|
301
|
+
pylint src/ tests/
|
302
|
+
|
303
|
+
# Type checking
|
304
|
+
mypy src/
|
305
|
+
```
|
306
|
+
|
307
|
+
## Requirements
|
308
|
+
|
309
|
+
- Python 3.7+
|
310
|
+
- PyPDF2 library
|
311
|
+
|
312
|
+
## Binding Instructions
|
313
|
+
|
314
|
+
After running the script:
|
315
|
+
|
316
|
+
1. Print the output PDF double-sided (flip on long edge)
|
317
|
+
2. Cut or separate the printed sheets by signature
|
318
|
+
3. Fold each signature in half along the center
|
319
|
+
4. Stack all signatures in order
|
320
|
+
5. Bind along the folded edge using:
|
321
|
+
- Saddle stitching (stapling)
|
322
|
+
- Perfect binding (glue)
|
323
|
+
- Spiral binding
|
324
|
+
- Or other binding methods
|
@@ -0,0 +1,10 @@
|
|
1
|
+
pdf_section_binding/__init__.py,sha256=7EucSTcj9lj8j-rpWZDMP_24A657gA2znv97ZzHbW5Q,414
|
2
|
+
pdf_section_binding/cli.py,sha256=u4JVDy6-_FBe1Ie4DZ12ruiaDCMT0pAZvJY9S8r5vuo,11604
|
3
|
+
pdf_section_binding/core.py,sha256=hsq8pyy7Hw9c1NSTfnX9Zp6GhIOxHjVBMX8-V-PTb6g,8958
|
4
|
+
pdf_section_binding/version.py,sha256=gkFzipRY3Y91FTMo3jJta1QwMOwWITZKmNsiuVuarYo,255
|
5
|
+
pdf_section_binding-1.0.0.dist-info/licenses/LICENSE,sha256=OphKV48tcMv6ep-7j-8T6nycykPT0g8ZlMJ9zbGvdPs,1066
|
6
|
+
pdf_section_binding-1.0.0.dist-info/METADATA,sha256=6Q12htDOTV4I2G6pIA86gqTOuzVnmglXHkKO9GsDyCc,9542
|
7
|
+
pdf_section_binding-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
+
pdf_section_binding-1.0.0.dist-info/entry_points.txt,sha256=LHMx24JfaML_v-WnFW4--Q522gcOo_cEoG378hJGwlQ,116
|
9
|
+
pdf_section_binding-1.0.0.dist-info/top_level.txt,sha256=YpD4FK2NE0ODPhxz2VkN8LT1rpmqSxdcOwHIpF2Qcdk,20
|
10
|
+
pdf_section_binding-1.0.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Your Name
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
pdf_section_binding
|