Skills › Scientific Agent Skills › scientific-slides › pdf_to_images.py
pdf_to_images.py
210 lines · skills/scientific-slides/scripts/pdf_to_images.py at 5f2bc03
1#!/usr/bin/env python3
2"""
3PDF to Images Converter for Presentations
5Converts presentation PDFs to images for visual inspection and review.
6Supports multiple output formats and resolutions.
8Uses PyMuPDF (fitz) as the primary conversion method - no external
9dependencies required (no poppler, ghostscript, or ImageMagick needed).
10"""
12import sys
13import argparse
14from pathlib import Path
15from typing import Optional, List
17# Try to import pymupdf (preferred - no external dependencies)
18try:
19 import pymupdf as fitz # Avoid the unrelated PyPI package named fitz
20 HAS_PYMUPDF = True
21except ImportError:
22 HAS_PYMUPDF = False
25class PDFToImagesConverter:
26 """Converts PDF presentations to images."""
28 def __init__(
29 self,
30 pdf_path: str,
31 output_prefix: str,
32 dpi: int = 150,
33 format: str = 'jpg',
34 first_page: Optional[int] = None,
35 last_page: Optional[int] = None
36 ):
37 self.pdf_path = Path(pdf_path)
38 self.output_prefix = output_prefix
39 self.dpi = dpi
40 self.format = format.lower()
41 self.first_page = first_page
42 self.last_page = last_page
44 if dpi <= 0:
45 raise ValueError("dpi must be positive")
46 if first_page is not None and first_page < 1:
47 raise ValueError("first_page must be at least 1")
48 if last_page is not None and last_page < 1:
49 raise ValueError("last_page must be at least 1")
50 if first_page is not None and last_page is not None and first_page > last_page:
51 raise ValueError("first_page must not exceed last_page")
53 # Validate format
54 if self.format not in ['jpg', 'jpeg', 'png']:
55 raise ValueError(f"Unsupported format: {format}. Use jpg or png.")
57 def convert(self) -> List[Path]:
58 """Convert PDF to images using PyMuPDF."""
59 if not self.pdf_path.exists():
60 raise FileNotFoundError(f"PDF not found: {self.pdf_path}")
62 print(f"Converting: {self.pdf_path.name}")
63 print(f"Output prefix: {self.output_prefix}")
64 print(f"DPI: {self.dpi}")
65 print(f"Format: {self.format}")
67 if HAS_PYMUPDF:
68 return self._convert_with_pymupdf()
69 else:
70 raise RuntimeError(
71 "PyMuPDF not installed. Install it with:\n"
72 " uv pip install pymupdf\n\n"
73 "PyMuPDF is a self-contained library - no external dependencies needed."
74 )
76 def _convert_with_pymupdf(self) -> List[Path]:
77 """Convert using PyMuPDF library (no external dependencies)."""
78 print("Using PyMuPDF (no external dependencies required)...")
80 with fitz.open(self.pdf_path) as doc:
81 start_page = self.first_page - 1 if self.first_page is not None else 0
82 end_page = self.last_page if self.last_page is not None else doc.page_count
83 if not 0 <= start_page < end_page <= doc.page_count:
84 raise ValueError(f"Page range must be within 1-{doc.page_count}")
85 output_files = []
86 Path(self.output_prefix).parent.mkdir(parents=True, exist_ok=True)
87 for page_num in range(start_page, end_page):
88 # dpi records resolution metadata as well as setting pixel dimensions.
89 pixmap = doc[page_num].get_pixmap(dpi=self.dpi)
90 output_path = Path(f"{self.output_prefix}-{page_num + 1:03d}.{self.format}")
91 pixmap.save(str(output_path), output="jpeg" if self.format in ("jpg", "jpeg") else "png")
92 output_files.append(output_path)
93 print(f" Created: {output_path.name}")
94 return output_files
97def main():
98 parser = argparse.ArgumentParser(
99 description='Convert presentation PDFs to images',
100 formatter_class=argparse.RawDescriptionHelpFormatter,
101 epilog="""
102Examples:
103 %(prog)s presentation.pdf slides
104 -> Creates slides-001.jpg, slides-002.jpg, ...
106 %(prog)s presentation.pdf output/slide --dpi 300 --format png
107 -> Creates output/slide-001.png, slide-002.png, ... at high resolution
109 %(prog)s presentation.pdf review/s --first 5 --last 10
110 -> Converts only slides 5-10
112Output:
113 Images are named: PREFIX-001.FORMAT, PREFIX-002.FORMAT, etc.
115Resolution:
116 - 150 DPI: Good for screen review (default)
117 - 200 DPI: Higher quality for detailed inspection
118 - 300 DPI: Print quality (larger files)
120Requirements:
121 Install PyMuPDF (no external dependencies needed):
122 uv pip install pymupdf
123 """
124 )
126 parser.add_argument(
127 'pdf_path',
128 help='Path to PDF presentation'
129 )
131 parser.add_argument(
132 'output_prefix',
133 help='Output filename prefix (e.g., "slides" or "output/slide")'
134 )
136 parser.add_argument(
137 '--dpi', '-r',
138 type=int,
139 default=150,
140 help='Resolution in DPI (default: 150)'
141 )
143 parser.add_argument(
144 '--format', '-f',
145 choices=['jpg', 'jpeg', 'png'],
146 default='jpg',
147 help='Output format (default: jpg)'
148 )
150 parser.add_argument(
151 '--first',
152 type=int,
153 help='First page to convert (1-indexed)'
154 )
156 parser.add_argument(
157 '--last',
158 type=int,
159 help='Last page to convert (1-indexed)'
160 )
162 args = parser.parse_args()
164 # Create output directory if needed
165 output_dir = Path(args.output_prefix).parent
166 if output_dir != Path('.'):
167 output_dir.mkdir(parents=True, exist_ok=True)
169 # Convert
170 try:
171 converter = PDFToImagesConverter(
172 pdf_path=args.pdf_path,
173 output_prefix=args.output_prefix,
174 dpi=args.dpi,
175 format=args.format,
176 first_page=args.first,
177 last_page=args.last
178 )
180 output_files = converter.convert()
182 print()
183 print("=" * 60)
184 print(f"[OK] Success! Created {len(output_files)} image(s)")
185 print("=" * 60)
187 if output_files:
188 print(f"\nFirst image: {output_files[0]}")
189 print(f"Last image: {output_files[-1]}")
191 # Calculate total size
192 total_size = sum(f.stat().st_size for f in output_files)
193 size_mb = total_size / (1024 * 1024)
194 print(f"Total size: {size_mb:.2f} MB")
196 print("\nNext steps:")
197 print(" 1. Review images for layout issues")
198 print(" 2. Check for text overflow or element overlap")
199 print(" 3. Verify readability from distance")
200 print(" 4. Document issues with slide numbers")
202 sys.exit(0)
204 except Exception as e:
205 print(f"\n[FAIL] Error: {str(e)}", file=sys.stderr)
206 sys.exit(1)
209if __name__ == '__main__':
210 main()