"""Plot file processing and HTML rendering.""" import shutil import subprocess from pathlib import Path from typing import Any, Dict try: import fitz # PyMuPDF _PYMUPDF_AVAILABLE = True except ImportError: _PYMUPDF_AVAILABLE = False _IMAGEMAGICK_AVAILABLE = shutil.which("convert") is not None from jinja2 import Template from gallery.utils.metadata import ( resolve_metadata_for_plot, get_metadata_file_path, ) from gallery.utils.stats import ( calculate_directory_stats, format_file_size, ) from gallery.config import GalleryConfig def process_html_file( html_file: Path, web_dir: Path, current_metadata: Dict[str, Any] = None ) -> dict: """ Process HTML plot file, copying it to web directory. Args: html_file: Path to the source HTML file web_dir: Target web directory current_metadata: Current metadata dictionary for the plot Returns: Dictionary containing plot information """ web_html = web_dir / html_file.name if needs_update(html_file, web_html): shutil.copy2(html_file, web_html) # Get source file creation time source_creation_time = int(html_file.stat().st_ctime) # Resolve metadata if provided plot_metadata = {} if current_metadata is not None: plot_metadata = resolve_metadata_for_plot(html_file, current_metadata) return { "name": html_file.stem, "html_href": html_file.name, "is_html": True, "metadata": plot_metadata, "creation_time": source_creation_time } def process_plot_files( config: GalleryConfig, plot_file: Path, web_dir: Path, current_metadata: Dict[str, Any] = None, ) -> dict: """ Process plot files (PDF/PNG or HTML), handling conversion and copying. Args: config: Gallery configuration object plot_file: Path to the source plot file (PDF or HTML) web_dir: Target web directory current_metadata: Current metadata dictionary for the plot Returns: Dictionary containing plot information """ if plot_file.suffix.lower() == '.html': return process_html_file(plot_file, web_dir, current_metadata) # Handle PDF files png_file = plot_file.with_suffix(".png") web_pdf = web_dir / plot_file.name web_png = web_dir / png_file.name if needs_update(plot_file, web_pdf): shutil.copy2(plot_file, web_pdf) if not png_file.exists(): convert_pdf_to_png(plot_file, config=config) if needs_update(png_file, web_png): shutil.copy2(png_file, web_png) source_creation_time = int(plot_file.stat().st_ctime) plot_metadata = {} if current_metadata is not None: plot_metadata = resolve_metadata_for_plot(plot_file, current_metadata) return { "name": plot_file.stem, "pdf_href": plot_file.name, "png_href": png_file.name, "is_html": False, "metadata": plot_metadata, "creation_time": source_creation_time } def render_gallery_page( config: GalleryConfig, template: Template, web_dir: Path, items: list, subdirs: list, relative_path: Path, title: str = None, metadata: dict = None ) -> None: """ Unified template rendering for all gallery pages. Args: config: Gallery configuration object template: Jinja2 template object web_dir: Target web directory items: List of plot items subdirs: List of subdirectory names relative_path: Relative path from gallery root title: Page title (optional) metadata: Metadata dictionary (optional) """ if title is None: title = "Gallery" if relative_path == Path( ".") else f"Gallery: {relative_path}" if metadata is None: metadata = {} # Calculate statistics current_stats = calculate_directory_stats(web_dir) stats = { "file_count": len(items), "folder_count": len(subdirs), "total_size": format_file_size(current_stats["total_size"]), "total_size_bytes": current_stats["total_size"] } # Calculate relative path to assets if relative_path == Path("."): assets_path = "../assets" else: depth = len(relative_path.parts) assets_path = "../" * (depth + 1) + "assets" # For root level, show only directory structure if relative_path == Path("."): items = [] output_html = web_dir / "index.html" with output_html.open("w") as f: paths_dict = { "web_folder": str(config.web_folder), } ui_dict = { "max_recent_plots": 20, "search_debounce_ms": 300, } rendered_html = template.render( title=title, items=items, subdirs=subdirs, relpath=str(relative_path), paths=paths_dict, ui=ui_dict, stats=stats, folder_metadata=metadata, assets_path=assets_path, source_dir=str(web_dir), metadata_file_path=get_metadata_file_path(web_dir) ) f.write(rendered_html) def convert_pdf_to_png(pdf_path: Path, config: GalleryConfig) -> None: """ Convert a PDF file to PNG format. Uses PyMuPDF (fitz) when available; falls back to ImageMagick otherwise. Only converts if the PNG doesn't exist or if the PDF is newer than the PNG (with a 30-second buffer to handle filesystem timing issues). Raises: RuntimeError: If neither PyMuPDF nor ImageMagick is available. subprocess.CalledProcessError: If the ImageMagick fallback fails. """ png_path = pdf_path.with_suffix(".png") if png_path.exists(): pdf_mtime = pdf_path.stat().st_mtime png_mtime = png_path.stat().st_mtime if png_mtime >= (pdf_mtime + 30): return if _PYMUPDF_AVAILABLE: _convert_pdf_pymupdf(pdf_path, png_path, config.png_dpi) elif _IMAGEMAGICK_AVAILABLE: _convert_pdf_imagemagick(pdf_path, png_path, config.png_dpi) else: raise RuntimeError( "No PDF renderer found. Install PyMuPDF (`pip install pymupdf`) " "or ImageMagick (`apt-get install imagemagick`)." ) def _convert_pdf_pymupdf(pdf_path: Path, png_path: Path, dpi: int) -> None: zoom = dpi / 72 # PDF coordinate space is 72 pt/inch doc = fitz.open(str(pdf_path)) try: pix = doc[0].get_pixmap(matrix=fitz.Matrix(zoom, zoom)) pix.save(str(png_path)) finally: doc.close() def _convert_pdf_imagemagick(pdf_path: Path, png_path: Path, dpi: int) -> None: subprocess.run([ "convert", "-density", str(dpi), str(pdf_path), "-quality", "95", str(png_path), ], check=True) def needs_update(source_file: Path, target_file: Path) -> bool: """ Check if target file needs updating based on source modification time. Args: source_file: Path to the source file target_file: Path to the target file Returns: True if target needs update, False otherwise """ if not target_file.exists(): return True source_mtime = source_file.stat().st_mtime target_mtime = target_file.stat().st_mtime return source_mtime > (target_mtime + 30)