#!/usr/bin/env python3 """ Figure Export Utilities for Publication-Ready Scientific Figures This module provides utilities to export matplotlib figures in publication-ready formats with appropriate settings for various journals. """ import matplotlib.pyplot as plt from pathlib import Path from typing import List, Optional, Union def save_publication_figure( fig: plt.Figure, filename: Union[str, Path], formats: List[str] = ['pdf', 'png'], dpi: int = 300, transparent: bool = False, bbox_inches: str = 'tight', pad_inches: float = 0.1, facecolor: str = 'white', **kwargs ) -> List[Path]: """ Save a matplotlib figure in multiple formats with publication-quality settings. Parameters ---------- fig : matplotlib.figure.Figure The figure to save filename : str or Path Base filename (without extension) formats : list of str, default ['pdf', 'png'] List of file formats to save. Options: 'pdf', 'png', 'eps', 'svg', 'tiff' dpi : int, default 300 Resolution for raster formats (png, tiff). 300 DPI is minimum for most journals transparent : bool, default False If True, save with transparent background bbox_inches : str, default 'tight' Bounding box specification. 'tight' removes excess whitespace pad_inches : float, default 0.1 Padding around the figure when bbox_inches='tight' facecolor : str, default 'white' Background color (ignored if transparent=True) **kwargs Additional keyword arguments passed to fig.savefig() Returns ------- list of Path List of paths to saved files Examples -------- >>> fig, ax = plt.subplots() >>> ax.plot([1, 2, 3], [1, 4, 9]) >>> save_publication_figure(fig, 'my_plot', formats=['pdf', 'png'], dpi=600) ['my_plot.pdf', 'my_plot.png'] """ filename = Path(filename) base_name = filename.stem output_dir = filename.parent if filename.parent.exists() else Path.cwd() saved_files = [] for fmt in formats: output_file = output_dir / f"{base_name}.{fmt}" # Set format-specific parameters save_kwargs = { 'dpi': dpi, 'bbox_inches': bbox_inches, 'pad_inches': pad_inches, 'facecolor': facecolor if not transparent else 'none', 'edgecolor': 'none', 'transparent': transparent, 'format': fmt, } # Update with user-provided kwargs save_kwargs.update(kwargs) # Adjust DPI for vector formats (DPI less relevant) if fmt in ['pdf', 'eps', 'svg']: save_kwargs['dpi'] = min(dpi, 300) # Lower DPI for embedded rasters in vector try: fig.savefig(output_file, **save_kwargs) saved_files.append(output_file) print(f"✓ Saved: {output_file}") except Exception as e: print(f"✗ Failed to save {output_file}: {e}") return saved_files def save_for_journal( fig: plt.Figure, filename: Union[str, Path], journal: str, figure_type: str = 'combination' ) -> List[Path]: """ Save figure with journal-specific requirements. Parameters ---------- fig : matplotlib.figure.Figure The figure to save filename : str or Path Base filename (without extension) journal : str Journal name. Options: 'nature', 'science', 'cell', 'plos', 'acs', 'ieee' figure_type : str, default 'combination' Type of figure. Options: 'line_art', 'photo', 'combination' Returns ------- list of Path List of paths to saved files Examples -------- >>> fig, ax = plt.subplots() >>> ax.plot([1, 2, 3], [1, 4, 9]) >>> save_for_journal(fig, 'figure1', journal='nature', figure_type='line_art') """ journal = journal.lower() # Define journal-specific requirements journal_specs = { 'nature': { 'line_art': {'formats': ['pdf', 'eps'], 'dpi': 1000}, 'photo': {'formats': ['tiff'], 'dpi': 300}, 'combination': {'formats': ['pdf'], 'dpi': 600}, }, 'science': { 'line_art': {'formats': ['eps', 'pdf'], 'dpi': 1000}, 'photo': {'formats': ['tiff'], 'dpi': 300}, 'combination': {'formats': ['eps'], 'dpi': 600}, }, 'cell': { 'line_art': {'formats': ['pdf', 'eps'], 'dpi': 1000}, 'photo': {'formats': ['tiff'], 'dpi': 300}, 'combination': {'formats': ['pdf'], 'dpi': 600}, }, 'plos': { 'line_art': {'formats': ['pdf', 'eps'], 'dpi': 600}, 'photo': {'formats': ['tiff', 'png'], 'dpi': 300}, 'combination': {'formats': ['tiff'], 'dpi': 300}, }, 'acs': { 'line_art': {'formats': ['tiff', 'pdf'], 'dpi': 600}, 'photo': {'formats': ['tiff'], 'dpi': 300}, 'combination': {'formats': ['tiff'], 'dpi': 600}, }, 'ieee': { 'line_art': {'formats': ['pdf', 'eps'], 'dpi': 600}, 'photo': {'formats': ['tiff'], 'dpi': 300}, 'combination': {'formats': ['pdf'], 'dpi': 300}, }, } if journal not in journal_specs: available = ', '.join(journal_specs.keys()) raise ValueError(f"Journal '{journal}' not recognized. Available: {available}") if figure_type not in journal_specs[journal]: available = ', '.join(journal_specs[journal].keys()) raise ValueError(f"Figure type '{figure_type}' not valid. Available: {available}") specs = journal_specs[journal][figure_type] print(f"Saving for {journal.upper()} ({figure_type}):") print(f" Formats: {', '.join(specs['formats'])}") print(f" DPI: {specs['dpi']}") return save_publication_figure( fig=fig, filename=filename, formats=specs['formats'], dpi=specs['dpi'] ) def check_figure_size(fig: plt.Figure, journal: str = 'nature') -> dict: """ Check if figure dimensions are appropriate for journal requirements. Parameters ---------- fig : matplotlib.figure.Figure The figure to check journal : str, default 'nature' Journal name Returns ------- dict Dictionary with figure dimensions and compliance status Examples -------- >>> fig = plt.figure(figsize=(3.5, 3)) >>> info = check_figure_size(fig, journal='nature') >>> print(info) """ journal = journal.lower() # Get figure dimensions in inches width_inches, height_inches = fig.get_size_inches() width_mm = width_inches * 25.4 height_mm = height_inches * 25.4 # Journal specifications (widths in mm) specs = { 'nature': {'single': 89, 'double': 183, 'max_height': 247}, 'science': {'single': 55, 'double': 175, 'max_height': 233}, 'cell': {'single': 85, 'double': 178, 'max_height': 230}, 'plos': {'single': 83, 'double': 173, 'max_height': 233}, 'acs': {'single': 82.5, 'double': 178, 'max_height': 247}, } if journal not in specs: journal_spec = specs['nature'] print(f"Warning: Journal '{journal}' not found, using Nature specifications") else: journal_spec = specs[journal] # Determine column type column_type = None width_ok = False tolerance = 5 # mm tolerance if abs(width_mm - journal_spec['single']) < tolerance: column_type = 'single' width_ok = True elif abs(width_mm - journal_spec['double']) < tolerance: column_type = 'double' width_ok = True height_ok = height_mm <= journal_spec['max_height'] result = { 'width_inches': width_inches, 'height_inches': height_inches, 'width_mm': width_mm, 'height_mm': height_mm, 'journal': journal, 'column_type': column_type, 'width_ok': width_ok, 'height_ok': height_ok, 'compliant': width_ok and height_ok, 'recommendations': { 'single_column_mm': journal_spec['single'], 'double_column_mm': journal_spec['double'], 'max_height_mm': journal_spec['max_height'], } } # Print report print(f"\n{'='*60}") print(f"Figure Size Check for {journal.upper()}") print(f"{'='*60}") print(f"Current size: {width_mm:.1f} × {height_mm:.1f} mm") print(f" ({width_inches:.2f} × {height_inches:.2f} inches)") print(f"\n{journal.upper()} specifications:") print(f" Single column: {journal_spec['single']} mm") print(f" Double column: {journal_spec['double']} mm") print(f" Max height: {journal_spec['max_height']} mm") print(f"\nCompliance:") print(f" Width: {'✓ OK' if width_ok else '✗ Non-standard'} ({column_type or 'custom'})") print(f" Height: {'✓ OK' if height_ok else '✗ Too tall'}") print(f" Overall: {'✓ COMPLIANT' if result['compliant'] else '✗ NEEDS ADJUSTMENT'}") print(f"{'='*60}\n") return result def verify_font_embedding(pdf_path: Union[str, Path]) -> bool: """ Check if fonts are embedded in a PDF file. Note: This requires PyPDF2 or a similar library to be installed. Parameters ---------- pdf_path : str or Path Path to PDF file Returns ------- bool True if fonts are embedded, False otherwise """ try: from PyPDF2 import PdfReader except ImportError: print("Warning: PyPDF2 not installed. Cannot verify font embedding.") print("Install with: pip install PyPDF2") return None pdf_path = Path(pdf_path) try: reader = PdfReader(pdf_path) # This is a simplified check; full verification is complex print(f"PDF has {len(reader.pages)} page(s)") print("Note: Full font embedding verification requires detailed PDF inspection.") return True except Exception as e: print(f"Error reading PDF: {e}") return False if __name__ == "__main__": # Example usage import numpy as np # Create example figure fig, ax = plt.subplots(figsize=(3.5, 2.5)) x = np.linspace(0, 10, 100) ax.plot(x, np.sin(x), label='sin(x)') ax.plot(x, np.cos(x), label='cos(x)') ax.set_xlabel('x') ax.set_ylabel('y') ax.legend() ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) # Check size check_figure_size(fig, journal='nature') # Save in multiple formats print("\nSaving figure...") save_publication_figure(fig, 'example_figure', formats=['pdf', 'png'], dpi=300) # Save with journal-specific requirements print("\nSaving for Nature...") save_for_journal(fig, 'example_figure_nature', journal='nature', figure_type='line_art') plt.close(fig)