from __future__ import annotations import os import sys from pathlib import Path from tkpptx import ( BackendUnavailableError, ExportError, ExportOptions, UnsupportedOperationError, resolve_page_range, ) PP_SAVE_AS_PDF = 32 class ComBackend: name = "com" def is_available(self) -> bool: if os.name != "nt": return False try: import comtypes.client # noqa: F401 except ImportError: return False return True def _create_application(self): if not self.is_available(): raise BackendUnavailableError( "PowerPoint COM requires Windows, Microsoft PowerPoint, " "and the 'comtypes' package." ) import comtypes.client try: app = comtypes.client.CreateObject("PowerPoint.Application") app.Visible = 1 return app except Exception as exc: raise BackendUnavailableError( "Microsoft PowerPoint could not be started through COM." ) from exc def export_pdf( self, input_path: Path, output_path: Path, options: ExportOptions ) -> Path: app = self._create_application() presentation = None try: presentation = app.Presentations.Open( str(input_path), WithWindow=False ) presentation.SaveAs(str(output_path), PP_SAVE_AS_PDF) except Exception as exc: raise ExportError( f"PowerPoint COM PDF export failed: {exc}" ) from exc finally: if presentation is not None: try: presentation.Close() except Exception: pass try: app.Quit() except Exception: pass if not output_path.exists(): raise ExportError( f"PowerPoint reported success, but no PDF was created: " f"{output_path}" ) return output_path def export_images( self, input_path: Path, output_dir: Path, options: ExportOptions ) -> list[Path]: app = self._create_application() presentation = None outputs: list[Path] = [] try: presentation = app.Presentations.Open( str(input_path), WithWindow=False ) slide_count = int(presentation.Slides.Count) start = options.start end = options.end if options.end is not None else slide_count if start < 1 or end < start or end > slide_count: raise ValueError( f"Invalid slide range {start}-{end}; " f"presentation has {slide_count} slides." ) width = int(options.width) if options.height is not None: height = int(options.height) else: page_width = float(presentation.PageSetup.SlideWidth) page_height = float(presentation.PageSetup.SlideHeight) height = max(1, round(width * page_height / page_width)) for page in range(start, end + 1): target = output_dir / f"slide_{page:03d}.png" if target.exists() and options.overwrite: target.unlink() slide = presentation.Slides.Item(page) slide.Export(str(target), "PNG", width, height) if not target.exists(): raise ExportError( f"PowerPoint did not create slide image: {target}" ) outputs.append(target) except Exception as exc: if isinstance(exc, (ValueError, ExportError)): raise raise ExportError( f"PowerPoint COM image export failed: {exc}" ) from exc finally: if presentation is not None: try: presentation.Close() except Exception: pass try: app.Quit() except Exception: pass return outputs def export_html( self, input_path: Path, output_path: Path, options: ExportOptions ) -> Path: raise UnsupportedOperationError( "The COM backend does not provide HTML export." ) def get_backend() -> ComBackend: return ComBackend()