Add export to latex button (i think)

This commit is contained in:
Kylian Schmidt
2025-07-08 17:04:55 +02:00
parent 8173d7618a
commit 2d98ed0e7a
11 changed files with 1131 additions and 3 deletions
+289
View File
@@ -0,0 +1,289 @@
/* ========================================
EXPORT FUNCTIONALITY STYLES
======================================== */
/* Selection mode styles */
.selection-mode .grid-item {
cursor: pointer;
transition: all 0.2s ease;
}
.selection-mode .grid-item:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* Selection overlay */
.selection-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: none;
justify-content: center;
align-items: center;
border-radius: 8px;
z-index: 5;
}
.selection-mode .selection-overlay {
display: flex;
}
.selection-checkbox {
background: var(--card-background);
border: 2px solid var(--border-color);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.2s ease;
}
.grid-item.selected .selection-checkbox {
background: var(--primary-color, #007bff);
border-color: var(--primary-color, #007bff);
color: white;
}
.checkbox-icon {
line-height: 1;
}
/* Selection counter */
.selection-counter {
position: fixed;
bottom: 100px;
right: 20px;
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 20px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
color: var(--text-color);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 999;
}
/* Export button styles */
.export-btn {
background: #28a745 !important;
}
.export-btn:hover {
background: #218838 !important;
}
.export-btn:disabled {
background: #6c757d !important;
cursor: not-allowed;
}
/* Export messages */
.export-message {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
border-radius: 6px;
font-weight: 500;
z-index: 1001;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s ease;
}
.export-message-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.export-message-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.export-message-warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.export-message-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Selection mode indicator */
.selection-mode::before {
content: "Selection Mode - Click plots to select them";
position: fixed;
top: 0;
left: 0;
right: 0;
background: var(--primary-color, #007bff);
color: white;
text-align: center;
padding: 8px;
font-size: 14px;
font-weight: 500;
z-index: 1000;
}
/* Adjust main content when in selection mode */
.selection-mode {
padding-top: 40px;
}
/* Export instructions overlay */
.export-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1002;
}
.export-instructions {
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 24px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.export-instructions h3 {
margin: 0 0 16px 0;
color: var(--text-color);
}
.export-instructions p {
margin: 0 0 16px 0;
color: var(--text-color);
}
.export-data {
margin: 16px 0;
}
.export-data textarea {
width: 100%;
height: 200px;
font-family: 'Courier New', monospace;
font-size: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px;
background: var(--background-color);
color: var(--text-color);
resize: vertical;
}
.export-commands {
margin: 16px 0;
padding: 12px;
background: var(--header-background);
border-radius: 4px;
border: 1px solid var(--border-color);
}
.export-commands code {
background: var(--background-color);
padding: 4px 8px;
border-radius: 3px;
font-family: 'Courier New', monospace;
border: 1px solid var(--border-color);
display: inline-block;
margin-top: 8px;
}
.export-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 20px;
}
.export-actions button {
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--card-background);
color: var(--text-color);
cursor: pointer;
transition: all 0.2s ease;
}
.export-actions button:hover {
background: var(--header-background);
}
.export-actions button:last-child {
background: var(--primary-color, #007bff);
color: white;
border-color: var(--primary-color, #007bff);
}
.export-actions button:last-child:hover {
background: var(--primary-color-dark, #0056b3);
}
[data-theme="dark"] .selection-checkbox {
background: var(--card-background);
border-color: var(--border-color);
}
[data-theme="dark"] .grid-item.selected .selection-checkbox {
background: var(--primary-color, #0d6efd);
border-color: var(--primary-color, #0d6efd);
}
[data-theme="dark"] .export-message-info {
background: #0c5460;
color: #d1ecf1;
border-color: #086972;
}
[data-theme="dark"] .export-message-success {
background: #155724;
color: #d4edda;
border-color: #1e7e34;
}
[data-theme="dark"] .export-message-warning {
background: #856404;
color: #fff3cd;
border-color: #b58b14;
}
[data-theme="dark"] .export-message-error {
background: #721c24;
color: #f8d7da;
border-color: #a94442;
}
+1
View File
@@ -16,6 +16,7 @@
@import url('./stats.css');
@import url('./comparison.css');
@import url('./metadata.css');
@import url('./export.css');
/* Responsive design */
@import url('./responsive.css');
+320
View File
@@ -0,0 +1,320 @@
/**
* Export Manager for Gallery
* Handles exporting selected plots to merged PDF
*/
export class ExportManager {
constructor() {
this.selectedPlots = new Set();
this.maxPlots = 4;
this.init();
}
init() {
this.createExportButton();
this.bindEvents();
}
/**
* Create the export button in the floating buttons section
*/
createExportButton() {
const floatingButtons = document.querySelector('.floating-buttons');
if (!floatingButtons) return;
const exportBtn = document.createElement('button');
exportBtn.className = 'floating-btn export-btn';
exportBtn.id = 'exportBtn';
exportBtn.title = 'Export Selected Plots (Ctrl+E)';
exportBtn.innerHTML = '📄';
exportBtn.style.display = 'none'; // Hidden by default
exportBtn.onclick = () => this.exportSelectedPlots();
floatingButtons.appendChild(exportBtn);
// Add selection counter
const selectionCounter = document.createElement('div');
selectionCounter.className = 'selection-counter';
selectionCounter.id = 'selectionCounter';
selectionCounter.style.display = 'none';
selectionCounter.innerHTML = '0/4 selected';
floatingButtons.appendChild(selectionCounter);
}
/**
* Bind events for plot selection
*/
bindEvents() {
// Add selection mode toggle
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'e') {
e.preventDefault();
this.toggleSelectionMode();
}
if (e.key === 'Escape') {
this.clearSelection();
}
});
// Add selection handlers to existing plots
this.addSelectionHandlers();
}
/**
* Add selection handlers to all plot items
*/
addSelectionHandlers() {
const plotItems = document.querySelectorAll('.grid-item');
plotItems.forEach(item => this.addSelectionHandler(item));
}
/**
* Add selection handler to a single plot item
*/
addSelectionHandler(item) {
// Create selection overlay
const overlay = document.createElement('div');
overlay.className = 'selection-overlay';
overlay.innerHTML = `
<div class="selection-checkbox">
<span class="checkbox-icon">☐</span>
</div>
`;
item.appendChild(overlay);
// Add click handler for selection
overlay.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.togglePlotSelection(item);
});
}
/**
* Toggle selection mode
*/
toggleSelectionMode() {
const body = document.body;
const isSelectionMode = body.classList.contains('selection-mode');
if (isSelectionMode) {
this.exitSelectionMode();
} else {
this.enterSelectionMode();
}
}
/**
* Enter selection mode
*/
enterSelectionMode() {
document.body.classList.add('selection-mode');
document.getElementById('exportBtn').style.display = 'block';
document.getElementById('selectionCounter').style.display = 'block';
this.updateSelectionCounter();
}
/**
* Exit selection mode
*/
exitSelectionMode() {
document.body.classList.remove('selection-mode');
document.getElementById('exportBtn').style.display = 'none';
document.getElementById('selectionCounter').style.display = 'none';
this.clearSelection();
}
/**
* Toggle plot selection
*/
togglePlotSelection(item) {
const plotName = this.getPlotName(item);
const plotPath = this.getPlotPath(item);
if (this.selectedPlots.has(plotName)) {
this.selectedPlots.delete(plotName);
item.classList.remove('selected');
item.querySelector('.checkbox-icon').textContent = '☐';
} else {
if (this.selectedPlots.size >= this.maxPlots) {
this.showMessage(`Maximum ${this.maxPlots} plots can be selected`, 'warning');
return;
}
this.selectedPlots.add(plotName);
item.classList.add('selected');
item.querySelector('.checkbox-icon').textContent = '☑';
}
this.updateSelectionCounter();
}
/**
* Get plot name from grid item
*/
getPlotName(item) {
const plotName = item.querySelector('.plot-name');
return plotName ? plotName.textContent.trim() : '';
}
/**
* Get plot PDF path from grid item
*/
getPlotPath(item) {
const link = item.querySelector('a[href$=".pdf"]');
return link ? link.href : '';
}
/**
* Update selection counter
*/
updateSelectionCounter() {
const counter = document.getElementById('selectionCounter');
if (counter) {
counter.textContent = `${this.selectedPlots.size}/${this.maxPlots} selected`;
}
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) {
exportBtn.disabled = this.selectedPlots.size === 0;
exportBtn.style.opacity = this.selectedPlots.size === 0 ? '0.5' : '1';
}
}
/**
* Clear all selections
*/
clearSelection() {
this.selectedPlots.clear();
document.querySelectorAll('.grid-item.selected').forEach(item => {
item.classList.remove('selected');
const checkbox = item.querySelector('.checkbox-icon');
if (checkbox) checkbox.textContent = '☐';
});
this.updateSelectionCounter();
}
/**
* Export selected plots to merged PDF
*/
async exportSelectedPlots() {
if (this.selectedPlots.size === 0) {
this.showMessage('No plots selected', 'warning');
return;
}
const plotPaths = Array.from(this.selectedPlots).map(plotName => {
const item = Array.from(document.querySelectorAll('.grid-item'))
.find(item => this.getPlotName(item) === plotName);
return this.getPlotPath(item);
});
this.showMessage('Preparing export...', 'info');
try {
await this.createMergedPDF(plotPaths);
} catch (error) {
this.showMessage('Export failed: ' + error.message, 'error');
}
}
/**
* Create merged PDF using Python script
*/
async createMergedPDF(plotPaths) {
// Convert file:// URLs to actual paths
const actualPaths = plotPaths.map(url => {
if (url.startsWith('file://')) {
return url.substring(7); // Remove 'file://' prefix
}
return url;
});
const payload = {
plots: actualPaths,
layout: this.calculateLayout(actualPaths.length),
output_name: `merged_plots_${new Date().toISOString().split('T')[0]}.pdf`
};
// Save the request to a JSON file that can be picked up by a Python script
const requestData = JSON.stringify(payload, null, 2);
// Since we can't directly call Python from the browser, we'll show instructions
this.showExportInstructions(requestData);
}
/**
* Calculate optimal layout for given number of plots
*/
calculateLayout(numPlots) {
switch (numPlots) {
case 1: return { rows: 1, cols: 1 };
case 2: return { rows: 1, cols: 2 };
case 3: return { rows: 2, cols: 2 }; // 3 plots in 2x2 grid with one empty
case 4: return { rows: 2, cols: 2 };
default: return { rows: 2, cols: 2 };
}
}
/**
* Show export instructions to user
*/
showExportInstructions(requestData) {
const instructions = `
<div class="export-instructions">
<h3>Export Instructions</h3>
<p>To export your selected plots, save the following data to a file called <code>export_request.json</code> and run the export script:</p>
<div class="export-data">
<textarea readonly>${requestData}</textarea>
</div>
<div class="export-commands">
<p><strong>Command to run:</strong></p>
<code>cd /work/kschmidt/web && python export_plots.py export_request.json</code>
</div>
<div class="export-actions">
<button onclick="this.parentElement.parentElement.parentElement.remove()">Close</button>
<button onclick="navigator.clipboard.writeText('${requestData.replace(/'/g, "\\'")}')">Copy JSON</button>
</div>
</div>
`;
const overlay = document.createElement('div');
overlay.className = 'export-overlay';
overlay.innerHTML = instructions;
document.body.appendChild(overlay);
}
/**
* Download the PDF blob
*/
downloadPDF(blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `merged_plots_${new Date().toISOString().split('T')[0]}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Show temporary message to user
*/
showMessage(text, type = 'info') {
// Remove existing message
const existing = document.querySelector('.export-message');
if (existing) existing.remove();
const message = document.createElement('div');
message.className = `export-message export-message-${type}`;
message.textContent = text;
document.body.appendChild(message);
setTimeout(() => {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
}, 3000);
}
}
+3
View File
@@ -9,6 +9,7 @@ import { RecentPlotsManager } from './recent-plots-manager.js';
import { ComparisonManager } from './comparison-manager.js';
import { StatsManager } from './stats-manager.js';
import { KeyboardManager } from './keyboard-manager.js';
import { ExportManager } from './export-manager.js';
import { Utils } from './utils.js';
/**
@@ -28,6 +29,7 @@ export class GalleryApp {
this.recentPlotsManager = new RecentPlotsManager(this.MAX_RECENT_PLOTS);
this.comparisonManager = new ComparisonManager();
this.statsManager = new StatsManager();
this.exportManager = new ExportManager();
this.keyboardManager = new KeyboardManager(this);
this.utils = Utils;
@@ -36,6 +38,7 @@ export class GalleryApp {
window.searchManager = this.searchManager;
window.recentPlotsManager = this.recentPlotsManager;
window.comparisonManager = this.comparisonManager;
window.exportManager = this.exportManager;
window.utils = this.utils;
this.init();
+101
View File
@@ -0,0 +1,101 @@
# PDF Export Functionality
The gallery now includes the ability to export multiple plots to a merged PDF with grid layout.
## Features
- Select up to 4 plots from any gallery page
- Automatic grid layout (1x1, 1x2, 2x2)
- Export to PDF with proper scaling
- Keyboard shortcuts for easy access
## Usage
### Selecting Plots
1. Press **Ctrl+E** to enter selection mode
2. Click on up to 4 plot thumbnails to select them
3. Selected plots will show a checkmark overlay
4. A counter shows how many plots are selected (e.g., "2/4 selected")
### Exporting
1. After selecting plots, click the **📄** export button (appears in floating buttons)
2. The system will show export instructions with:
- JSON data for the export request
- Command to run the export script
3. Copy the JSON data and save it as `export_request.json`
4. Run the export command in your terminal
### Keyboard Shortcuts
- **Ctrl+E**: Toggle selection mode
- **Escape**: Clear all selections and exit selection mode
## Export Methods
The system supports two PDF merging methods:
### Method 1: pdfjam (Recommended)
```bash
# Install on Ubuntu/Debian
sudo apt-get install texlive-extra-utils
# Check if available
python export_plots.py --check-deps
```
### Method 2: Python libraries
```bash
# Install Python dependencies
pip install PyPDF2 reportlab
# Check if available
python export_plots.py --check-deps
```
## Export Script Usage
```bash
# Basic usage
python export_plots.py export_request.json
# Specify output file
python export_plots.py export_request.json --output my_plots.pdf
# Check available dependencies
python export_plots.py --check-deps
```
## JSON Request Format
```json
{
"plots": [
"/path/to/plot1.pdf",
"/path/to/plot2.pdf"
],
"layout": {
"rows": 1,
"cols": 2
},
"output_name": "merged_plots.pdf"
}
```
## Layout Options
- **1 plot**: 1x1 grid
- **2 plots**: 1x2 grid (horizontal)
- **3 plots**: 2x2 grid (one empty slot)
- **4 plots**: 2x2 grid (full)
## Technical Details
The export functionality consists of:
- **Frontend**: JavaScript selection UI and export manager
- **Backend**: Python script for PDF merging
- **CSS**: Styling for selection mode and overlays
The system is designed to work without requiring a web server, using file-based communication between the browser and Python script.
+94
View File
@@ -0,0 +1,94 @@
"""
Simple Flask API for PDF Export
This provides a web API endpoint for the gallery export functionality.
"""
from flask import Flask, request, jsonify, send_file
from pathlib import Path
import json
import tempfile
import os
from orchestration.pdf_export import PDFExporter, check_dependencies
app = Flask(__name__)
exporter = PDFExporter()
@app.route('/api/export-pdf', methods=['POST'])
def export_pdf():
"""Export selected plots to merged PDF"""
try:
data = request.get_json()
if not data or 'plots' not in data:
return jsonify({'error': 'No plots specified'}), 400
plot_urls = data['plots']
layout = data.get('layout', {'rows': 2, 'cols': 2})
# Convert URLs to file paths
plot_paths = []
for url in plot_urls:
# Extract path from file:// URL or relative path
if url.startswith('file://'):
path = url[7:] # Remove 'file://' prefix
elif url.startswith('http'):
return jsonify({'error': 'Remote URLs not supported'}), 400
else:
# Assume relative path from web root
path = url
# Resolve to absolute path
plot_path = Path(path)
if not plot_path.exists():
return jsonify({'error': f'Plot not found: {path}'}), 404
plot_paths.append(str(plot_path))
if not plot_paths:
return jsonify({'error': 'No valid plots found'}), 400
# Create merged PDF
pdf_bytes = exporter.merge_plots(plot_paths, layout)
# Create temporary file to serve
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_file:
tmp_file.write(pdf_bytes)
tmp_path = tmp_file.name
def cleanup_temp_file():
"""Clean up temporary file after sending"""
try:
os.unlink(tmp_path)
except OSError:
pass
return send_file(
tmp_path,
as_attachment=True,
download_name='merged_plots.pdf',
mimetype='application/pdf'
)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/export-status', methods=['GET'])
def export_status():
"""Check export capabilities"""
deps = check_dependencies()
return jsonify({
'available': any(deps.values()),
'dependencies': deps,
'methods': {
'pypdf2': deps['pypdf2'],
'pdfjam': deps['pdfjam']
}
})
if __name__ == '__main__':
# For development only
app.run(debug=True, port=5000)
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Standalone PDF Export Script
This script processes export requests from the gallery and creates merged PDFs.
Usage: python export_plots.py [request_file.json]
"""
import sys
import json
import argparse
from pathlib import Path
from orchestration.pdf_export import PDFExporter, check_dependencies
def main():
parser = argparse.ArgumentParser(description='Export gallery plots to merged PDF')
parser.add_argument('request_file', nargs='?', default='export_request.json',
help='JSON file containing export request')
parser.add_argument('--output', '-o', help='Output PDF file path')
parser.add_argument('--check-deps', action='store_true',
help='Check available dependencies')
args = parser.parse_args()
if args.check_deps:
deps = check_dependencies()
print("Available dependencies:")
for dep, available in deps.items():
status = "" if available else ""
print(f" {status} {dep}")
if not any(deps.values()):
print("\nNo PDF merging tools available!")
print("Install one of the following:")
print(" - PyPDF2 and reportlab: pip install PyPDF2 reportlab")
print(" - pdfjam: apt-get install texlive-extra-utils (on Ubuntu/Debian)")
return
request_file = Path(args.request_file)
if not request_file.exists():
print(f"Error: Request file '{request_file}' not found")
print("Create a JSON file with the following structure:")
print(json.dumps({
"plots": ["/path/to/plot1.pdf", "/path/to/plot2.pdf"],
"layout": {"rows": 1, "cols": 2},
"output_name": "merged_plots.pdf"
}, indent=2))
return 1
try:
with open(request_file, 'r') as f:
request_data = json.load(f)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{request_file}': {e}")
return 1
# Validate request data
if 'plots' not in request_data:
print("Error: Missing 'plots' field in request")
return 1
plot_paths = request_data['plots']
layout = request_data.get('layout', {'rows': 2, 'cols': 2})
output_name = args.output or request_data.get('output_name', 'merged_plots.pdf')
# Validate plot files exist
missing_files = []
for plot_path in plot_paths:
if not Path(plot_path).exists():
missing_files.append(plot_path)
if missing_files:
print("Error: The following plot files were not found:")
for missing in missing_files:
print(f" - {missing}")
return 1
print(f"Exporting {len(plot_paths)} plots to '{output_name}'...")
print(f"Layout: {layout['rows']}x{layout['cols']}")
# Create exporter and merge plots
exporter = PDFExporter()
try:
pdf_bytes = exporter.merge_plots(plot_paths, layout, output_name)
if not args.output and 'output_name' not in request_data:
# Write to file if not already done by exporter
with open(output_name, 'wb') as f:
f.write(pdf_bytes)
print(f"✓ Successfully created '{output_name}'")
print(f" File size: {len(pdf_bytes) / 1024:.1f} KB")
# Clean up request file if export was successful
request_file.unlink(missing_ok=True)
except Exception as e:
print(f"Error during export: {e}")
return 1
return 0
if __name__ == '__main__':
sys.exit(main())
+6 -2
View File
@@ -183,7 +183,7 @@ def build_gallery(source_dir: Path, web_dir: Path,
if relative_path == Path("."):
assets_path = "../assets"
else:
# Count the number of directory levels to go back
# Count directory levels to go back to gallery, then to public_html
depth = len(relative_path.parts)
assets_path = "../" * (depth + 1) + "assets"
@@ -345,6 +345,10 @@ def main() -> None:
"total_size_bytes": current_stats["total_size"]
}
# Calculate relative path to assets for single file
# Single files are at depth 1 (gallery_root/source.name/index.html)
assets_path = "../assets"
output_html = source_web_dir / "index.html"
with output_html.open("w") as f:
f.write(template.render(
@@ -355,7 +359,7 @@ def main() -> None:
ui=CONFIG.ui,
stats=stats,
folder_metadata={},
assets_path="../../assets"
assets_path=assets_path
))
print(f"Generated {output_html}")
+205
View File
@@ -0,0 +1,205 @@
"""
PDF Export Module for Scientific Gallery Generator
This module handles exporting and merging multiple plots into a single PDF
using PyPDF2 and reportlab for layout.
"""
import io
import tempfile
from pathlib import Path
from typing import List, Dict, Any
import subprocess
try:
from PyPDF2 import PdfReader, PdfWriter
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter, A4
from reportlab.lib.utils import ImageReader
HAS_PDF_LIBS = True
except ImportError:
HAS_PDF_LIBS = False
class PDFExporter:
"""Handles exporting multiple plots to a merged PDF"""
def __init__(self):
self.page_size = A4
self.margin = 50
def merge_plots(self, plot_paths: List[str], layout: Dict[str, int],
output_path: str = None) -> bytes:
"""
Merge multiple PDF plots into a single PDF with grid layout.
Args:
plot_paths: List of paths to PDF files
layout: Dictionary with 'rows' and 'cols' keys
output_path: Optional output file path
Returns:
PDF bytes
"""
if not HAS_PDF_LIBS:
return self._merge_with_pdfjam(plot_paths, layout, output_path)
return self._merge_with_pypdf(plot_paths, layout, output_path)
def _merge_with_pypdf(self, plot_paths: List[str], layout: Dict[str, int],
output_path: str = None) -> bytes:
"""Merge PDFs using PyPDF2 and reportlab"""
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
# Create a new PDF with the layout
buffer = io.BytesIO()
c = canvas.Canvas(buffer, pagesize=A4)
page_width, page_height = A4
rows = layout['rows']
cols = layout['cols']
# Calculate dimensions for each plot
plot_width = (page_width - 2 * self.margin) / cols
plot_height = (page_height - 2 * self.margin) / rows
# Place each plot in the grid
for i, plot_path in enumerate(plot_paths):
if i >= rows * cols:
break
row = i // cols
col = i % cols
# Calculate position
x = self.margin + col * plot_width
y = page_height - self.margin - (row + 1) * plot_height
try:
# Read the source PDF
with open(plot_path, 'rb') as f:
reader = PdfReader(f)
if len(reader.pages) > 0:
page = reader.pages[0]
# Convert PDF page to image and place it
# This is a simplified approach - in practice you'd want
# to properly scale and position the PDF content
self._draw_pdf_placeholder(c, x, y, plot_width, plot_height,
Path(plot_path).stem)
except Exception as e:
print(f"Error processing {plot_path}: {e}")
self._draw_error_placeholder(c, x, y, plot_width, plot_height)
c.save()
if output_path:
with open(output_path, 'wb') as f:
f.write(buffer.getvalue())
return buffer.getvalue()
def _merge_with_pdfjam(self, plot_paths: List[str], layout: Dict[str, int],
output_path: str = None) -> bytes:
"""Merge PDFs using pdfjam (requires pdfpages LaTeX package)"""
rows = layout['rows']
cols = layout['cols']
# Create temporary output file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_file:
temp_output = tmp_file.name
try:
# Build pdfjam command
cmd = [
'pdfjam',
'--nup', f'{cols}x{rows}',
'--landscape' if cols > rows else '--no-landscape',
'--frame', 'true',
'--delta', '10pt 10pt',
'--offset', '0pt 0pt',
'--outfile', temp_output
]
# Add input files
cmd.extend(plot_paths[:rows * cols])
# Run pdfjam
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"pdfjam failed: {result.stderr}")
# Read the output file
with open(temp_output, 'rb') as f:
pdf_bytes = f.read()
if output_path:
with open(output_path, 'wb') as f:
f.write(pdf_bytes)
return pdf_bytes
finally:
# Clean up temporary file
Path(temp_output).unlink(missing_ok=True)
def _draw_pdf_placeholder(self, canvas, x: float, y: float, width: float,
height: float, plot_name: str):
"""Draw a placeholder for a PDF plot"""
# Draw border
canvas.setStrokeColorRGB(0.5, 0.5, 0.5)
canvas.setLineWidth(1)
canvas.rect(x, y, width, height)
# Draw plot name
canvas.setFillColorRGB(0, 0, 0)
canvas.setFont("Helvetica", 10)
text_width = canvas.stringWidth(plot_name, "Helvetica", 10)
text_x = x + (width - text_width) / 2
text_y = y + height / 2
canvas.drawString(text_x, text_y, plot_name)
def _draw_error_placeholder(self, canvas, x: float, y: float, width: float,
height: float):
"""Draw an error placeholder"""
# Draw red border
canvas.setStrokeColorRGB(1, 0, 0)
canvas.setLineWidth(2)
canvas.rect(x, y, width, height)
# Draw error text
canvas.setFillColorRGB(1, 0, 0)
canvas.setFont("Helvetica-Bold", 12)
error_text = "Error loading plot"
text_width = canvas.stringWidth(error_text, "Helvetica-Bold", 12)
text_x = x + (width - text_width) / 2
text_y = y + height / 2
canvas.drawString(text_x, text_y, error_text)
def check_dependencies() -> Dict[str, bool]:
"""Check if required dependencies are available"""
deps = {
'pypdf2': HAS_PDF_LIBS,
'pdfjam': False
}
# Check for pdfjam
try:
result = subprocess.run(['pdfjam', '--version'],
capture_output=True, text=True)
deps['pdfjam'] = result.returncode == 0
except FileNotFoundError:
pass
return deps
if __name__ == "__main__":
# Test the exporter
exporter = PDFExporter()
deps = check_dependencies()
print("Available dependencies:", deps)
+4
View File
@@ -104,6 +104,10 @@
<span>Compare plots</span>
<span class="shortcut-key">Ctrl+C</span>
</div>
<div class="shortcut-item">
<span>Export plots</span>
<span class="shortcut-key">Ctrl+E</span>
</div>
<div class="shortcut-item">
<span>Refresh</span>
<span class="shortcut-key">F5</span>
+1 -1
View File
@@ -227,7 +227,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.9"
"version": "3.12.1"
}
},
"nbformat": 4,