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.
@@ -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,8 @@
1
+ """Version information for pdf-section-binding."""
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "Sravan Kumar Rekandar"
5
+ __email__ = "sravankumarrekandar@example.com"
6
+ __description__ = (
7
+ "A CLI tool for reordering PDF pages for section binding (bookbinding)"
8
+ )
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pdf-section-binding = pdf_section_binding.cli:main
3
+ section-binding = pdf_section_binding.cli:main
@@ -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