Source code for scitex_writer.figures

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Timestamp: 2026-01-27
# File: src/scitex_writer/figures.py

"""Figure management functions.

Usage::

    import scitex_writer as sw

    # List figures in project
    result = sw.figures.list("./my-paper")

    # Add a figure
    sw.figures.add("./my-paper", "fig01", "./plot.png", "Results plot")

    # Convert figure format
    sw.figures.convert("input.pdf", "output.png")
"""

import shutil as _shutil
from pathlib import Path as _Path
from typing import List as _List
from typing import Literal as _Literal
from typing import Optional as _Optional
from typing import Union as _Union

from ._mcp.handlers import convert_figure as _convert_figure
from ._mcp.handlers import list_figures as _list_figures
from ._mcp.handlers import pdf_to_images as _pdf_to_images
from ._mcp.utils import resolve_project_path as _resolve_project_path

try:
    from scitex_dev.decorators import supports_return_as as _supports_return_as
except ImportError:

    def _supports_return_as(fn):
        return fn


[docs] @_supports_return_as def list( project_dir: str, extensions: _Optional[_List[str]] = None, ) -> dict: """List all figures in a writer project. Args: project_dir: Path to scitex-writer project. extensions: File extensions to include (default: common image formats). Returns: Dict with figures list and count. """ return _list_figures(project_dir, extensions)
[docs] @_supports_return_as def add( project_dir: str, name: str, image_path: str, caption: str, label: _Optional[str] = None, doc_type: _Literal["manuscript", "supplementary"] = "manuscript", ) -> dict: """Add a figure (copy image + create caption) to the project. Args: project_dir: Path to scitex-writer project. name: Figure name (without extension). image_path: Path to source image file. caption: Figure caption text. label: LaTeX label (default: fig:<name>). doc_type: Target document type. Returns: Dict with image_path, caption_path, label. """ try: project_path = _resolve_project_path(project_dir) doc_dirs = { "manuscript": project_path / "01_manuscript", "supplementary": project_path / "02_supplementary", } doc_dir = doc_dirs.get(doc_type) if not doc_dir: return {"success": False, "error": f"Invalid doc_type: {doc_type}"} fig_dir = doc_dir / "contents" / "figures" / "caption_and_media" fig_dir.mkdir(parents=True, exist_ok=True) src_image = _Path(image_path) if not src_image.exists(): return {"success": False, "error": f"Image not found: {image_path}"} dest_image = fig_dir / f"{name}{src_image.suffix}" _shutil.copy2(src_image, dest_image) if label is None: label = f"fig:{name.replace(' ', '_')}" caption_content = f"\\caption{{{caption}}}\n\\label{{{label}}}\n" caption_path = fig_dir / f"{name}.tex" caption_path.write_text(caption_content, encoding="utf-8") return { "success": True, "image_path": str(dest_image), "caption_path": str(caption_path), "label": label, } except Exception as e: return {"success": False, "error": str(e)}
def _find_all_figure_files(fig_dir: _Path, name: str) -> list: """Find all files associated with a figure name across all subdirectories.""" files = [] extensions = [ ".tex", ".png", ".jpg", ".jpeg", ".pdf", ".tif", ".tiff", ".eps", ".svg", ".mmd", ] # Base directory for ext in extensions: p = fig_dir / f"{name}{ext}" if p.exists() or p.is_symlink(): files.append(p) # Subdirectories: jpg_for_compilation, mermaid_originals for subdir_name in ["jpg_for_compilation", "mermaid_originals"]: subdir = fig_dir / subdir_name if subdir.is_dir(): for ext in extensions: p = subdir / f"{name}{ext}" if p.exists() or p.is_symlink(): files.append(p) return files
[docs] @_supports_return_as def remove( project_dir: str, name: str, doc_type: _Literal["manuscript", "supplementary"] = "manuscript", ) -> dict: """Remove a figure (image + caption) from the project. Removes all associated files including those in subdirectories (jpg_for_compilation/, mermaid_originals/). Args: project_dir: Path to scitex-writer project. name: Figure name (without extension). doc_type: Document type. Returns: Dict with removed file paths. """ try: project_path = _resolve_project_path(project_dir) doc_dirs = { "manuscript": project_path / "01_manuscript", "supplementary": project_path / "02_supplementary", } doc_dir = doc_dirs.get(doc_type) if not doc_dir: return {"success": False, "error": f"Invalid doc_type: {doc_type}"} fig_dir = doc_dir / "contents" / "figures" / "caption_and_media" all_files = _find_all_figure_files(fig_dir, name) removed = [] for p in all_files: p.unlink() removed.append(str(p)) if not removed: return {"success": False, "error": f"Figure not found: {name}"} return {"success": True, "removed": removed} except Exception as e: return {"success": False, "error": str(e)}
[docs] @_supports_return_as def archive( project_dir: str, name: str, doc_type: _Literal["manuscript", "supplementary"] = "manuscript", ) -> dict: """Move a figure to legacy/ instead of deleting. Moves all associated files (tex, images, mermaid sources) from caption_and_media/ to legacy/, preserving them for potential reuse. Args: project_dir: Path to scitex-writer project. name: Figure name (without extension). doc_type: Document type. Returns: Dict with archived file paths. """ try: project_path = _resolve_project_path(project_dir) doc_dirs = { "manuscript": project_path / "01_manuscript", "supplementary": project_path / "02_supplementary", } doc_dir = doc_dirs.get(doc_type) if not doc_dir: return {"success": False, "error": f"Invalid doc_type: {doc_type}"} fig_dir = doc_dir / "contents" / "figures" / "caption_and_media" legacy_dir = doc_dir / "contents" / "figures" / "legacy" legacy_dir.mkdir(parents=True, exist_ok=True) all_files = _find_all_figure_files(fig_dir, name) archived = [] for p in all_files: dest = legacy_dir / p.name _shutil.move(str(p), str(dest)) archived.append({"from": str(p), "to": str(dest)}) if not archived: return {"success": False, "error": f"Figure not found: {name}"} return {"success": True, "archived": archived} except Exception as e: return {"success": False, "error": str(e)}
[docs] @_supports_return_as def convert( input_path: str, output_path: str, dpi: int = 300, quality: int = 95, ) -> dict: """Convert figure between formats (e.g., PDF to PNG). Args: input_path: Source image path. output_path: Destination image path. dpi: Resolution for PDF conversion. quality: JPEG quality (1-100). Returns: Dict with input_path and output_path. """ return _convert_figure(input_path, output_path, dpi, quality)
[docs] @_supports_return_as def pdf_to_images( pdf_path: str, output_dir: _Optional[str] = None, pages: _Optional[_Union[int, _List[int]]] = None, dpi: int = 600, format: _Literal["png", "jpg"] = "png", ) -> dict: """Render PDF pages as images. Args: pdf_path: Path to PDF file. output_dir: Output directory (default: temp dir). pages: Page numbers to render (0-indexed). dpi: Resolution. format: Output format. Returns: Dict with images list and output_dir. """ return _pdf_to_images(pdf_path, output_dir, pages, dpi, format)
__all__ = ["list", "add", "remove", "archive", "convert", "pdf_to_images"] # EOF