Files
ETPlot/gallery/utils/processing.py
T
2026-05-15 09:07:48 +02:00

262 lines
7.2 KiB
Python

"""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)