Skills › Scientific Agent Skills › scientific-slides › slides_to_pdf.py
slides_to_pdf.py
237 lines · skills/scientific-slides/scripts/slides_to_pdf.py at 5f2bc03
1#!/usr/bin/env python3
2"""
3Combine slide images into a single PDF presentation.
5This script takes multiple slide images (PNG, JPG) and combines them
6into a single PDF file, maintaining aspect ratio and quality.
8Usage:
9 # Combine all PNG files in a directory
10 python slides_to_pdf.py slides/*.png -o presentation.pdf
12 # Combine specific files in order
13 python slides_to_pdf.py slide_01.png slide_02.png slide_03.png -o presentation.pdf
15 # From a directory (sorted by filename)
16 python slides_to_pdf.py slides/ -o presentation.pdf
17"""
19import argparse
20import sys
21from pathlib import Path
22from typing import List
24try:
25 from PIL import Image
26except ImportError:
27 print("Error: Pillow library not found. Install with: uv pip install Pillow")
28 sys.exit(1)
31def get_image_files(paths: List[str]) -> List[Path]:
32 """
33 Get list of image files from paths (files or directories).
35 Args:
36 paths: List of file paths or directory paths
38 Returns:
39 Explicit paths in input order; each directory/glob expands in filename order
40 """
41 image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'}
42 image_files = []
43 seen = set()
44 for path_str in paths:
45 path = Path(path_str)
46 if path.is_file():
47 matches = [path]
48 elif path.is_dir():
49 matches = sorted(path.iterdir(), key=lambda p: p.name)
50 elif path.parent.exists():
51 matches = sorted(path.parent.glob(path.name), key=lambda p: p.name)
52 else:
53 matches = []
54 for match in matches:
55 if not match.is_file():
56 continue
57 if match.suffix.lower() not in image_extensions:
58 if path.is_file():
59 print(f"Warning: Skipping non-image file: {match}")
60 continue
61 identity = match.resolve()
62 if identity not in seen:
63 seen.add(identity)
64 image_files.append(match)
66 return image_files
69def combine_images_to_pdf(image_paths: List[Path], output_path: Path,
70 dpi: int = 150, verbose: bool = False) -> bool:
71 """
72 Combine multiple images into a single PDF.
74 Args:
75 image_paths: List of image file paths
76 output_path: Output PDF path
77 dpi: Resolution for the PDF (default: 150)
78 verbose: Print progress information
80 Returns:
81 True if successful, False otherwise
82 """
83 if dpi <= 0:
84 raise ValueError("dpi must be positive")
85 if not image_paths:
86 print("Error: No image files found")
87 return False
89 if verbose:
90 print(f"Combining {len(image_paths)} images into PDF...")
92 # Load all images
93 images = []
94 for i, img_path in enumerate(image_paths):
95 try:
96 with Image.open(img_path) as source:
97 img = source.copy()
98 # Convert to RGB if necessary (PDF doesn't support RGBA)
99 if img.mode in ('RGBA', 'P'):
100 # Create white background
101 background = Image.new('RGB', img.size, (255, 255, 255))
102 if img.mode == 'P':
103 img = img.convert('RGBA')
104 background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
105 img = background
106 elif img.mode != 'RGB':
107 img = img.convert('RGB')
109 images.append(img)
111 if verbose:
112 print(f" [{i+1}/{len(image_paths)}] Loaded: {img_path.name} ({img.size[0]}x{img.size[1]})")
113 except Exception as e:
114 print(f"Error loading {img_path}: {e}")
115 for loaded in images:
116 loaded.close()
117 return False
119 if not images:
120 print("Error: No images could be loaded")
121 return False
123 # Create output directory if needed
124 output_path.parent.mkdir(parents=True, exist_ok=True)
126 # Save as PDF
127 try:
128 # First image
129 first_image = images[0]
131 # Remaining images (if any)
132 remaining_images = images[1:] if len(images) > 1 else []
134 # Save to PDF
135 first_image.save(
136 output_path,
137 "PDF",
138 resolution=dpi,
139 save_all=True,
140 append_images=remaining_images
141 )
143 if verbose:
144 print(f"\n[OK] PDF created: {output_path}")
145 print(f" Total slides: {len(images)}")
146 file_size = output_path.stat().st_size
147 if file_size > 1024 * 1024:
148 print(f" File size: {file_size / (1024 * 1024):.1f} MB")
149 else:
150 print(f" File size: {file_size / 1024:.1f} KB")
152 return True
153 except Exception as e:
154 print(f"Error creating PDF: {e}")
155 return False
156 finally:
157 # Close all images
158 for img in images:
159 img.close()
162def main():
163 """Command-line interface."""
164 parser = argparse.ArgumentParser(
165 description="Combine slide images into a single PDF presentation",
166 formatter_class=argparse.RawDescriptionHelpFormatter,
167 epilog="""
168Examples:
169 # Combine PNG files using glob pattern
170 python slides_to_pdf.py slides/*.png -o presentation.pdf
172 # Combine specific files in order
173 python slides_to_pdf.py title.png intro.png methods.png results.png -o talk.pdf
175 # Combine all images from a directory (sorted by filename)
176 python slides_to_pdf.py slides/ -o presentation.pdf
178 # With custom DPI and verbose output
179 python slides_to_pdf.py slides/*.png -o presentation.pdf --dpi 200 -v
181Supported formats: PNG, JPG, JPEG, GIF, WEBP, BMP
183Tips:
184 - Name your slide images with numbers for correct ordering:
185 01_title.png, 02_intro.png, 03_methods.png, etc.
186 - Use the generate_slide_image.py script to create slides first
187 - Standard presentation aspect ratio is 16:9 (1920x1080 or 1280x720)
188 """
189 )
191 parser.add_argument("images", nargs="+",
192 help="Image files, directories, or glob patterns")
193 parser.add_argument("-o", "--output", required=True,
194 help="Output PDF file path")
195 parser.add_argument("--dpi", type=int, default=150,
196 help="PDF resolution in DPI (default: 150)")
197 parser.add_argument("-v", "--verbose", action="store_true",
198 help="Verbose output")
200 args = parser.parse_args()
201 if args.dpi <= 0:
202 parser.error("--dpi must be positive")
204 # Get image files
205 image_files = get_image_files(args.images)
207 if not image_files:
208 print("Error: No image files found matching the specified paths")
209 print("\nUsage examples:")
210 print(" python slides_to_pdf.py slides/*.png -o presentation.pdf")
211 print(" python slides_to_pdf.py slide1.png slide2.png -o presentation.pdf")
212 sys.exit(1)
214 print(f"Found {len(image_files)} image(s)")
215 if args.verbose:
216 for f in image_files:
217 print(f" - {f}")
219 # Combine into PDF
220 output_path = Path(args.output)
221 success = combine_images_to_pdf(
222 image_files,
223 output_path,
224 dpi=args.dpi,
225 verbose=args.verbose
226 )
228 if success:
229 print(f"\n[OK] PDF created: {output_path}")
230 sys.exit(0)
231 else:
232 print(f"\n[FAIL] Failed to create PDF")
233 sys.exit(1)
236if __name__ == "__main__":
237 main()