- Set up main entry point for React application. - Create About, Home, NotFound, Privacy, and Terms pages with SEO support. - Implement API service for file uploads and task management. - Add global styles using Tailwind CSS. - Create utility functions for SEO and text processing. - Configure Vite for development and production builds. - Set up Nginx configuration for serving frontend and backend. - Add scripts for cleanup of expired files and sitemap generation. - Implement deployment script for production environment.
110 lines
3.0 KiB
Python
110 lines
3.0 KiB
Python
"""PDF compression service using Ghostscript."""
|
|
import os
|
|
import subprocess
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PDFCompressionError(Exception):
|
|
"""Custom exception for PDF compression failures."""
|
|
pass
|
|
|
|
|
|
# Ghostscript quality presets
|
|
QUALITY_PRESETS = {
|
|
"low": "/screen", # 72 dpi — smallest file, lowest quality
|
|
"medium": "/ebook", # 150 dpi — good balance (default)
|
|
"high": "/printer", # 300 dpi — high quality, moderate compression
|
|
}
|
|
|
|
|
|
def compress_pdf(
|
|
input_path: str, output_path: str, quality: str = "medium"
|
|
) -> dict:
|
|
"""
|
|
Compress a PDF file using Ghostscript.
|
|
|
|
Args:
|
|
input_path: Path to the input PDF file
|
|
output_path: Path for the compressed output file
|
|
quality: Compression quality — "low", "medium", or "high"
|
|
|
|
Returns:
|
|
dict with original_size, compressed_size, reduction_percent
|
|
|
|
Raises:
|
|
PDFCompressionError: If compression fails
|
|
"""
|
|
if quality not in QUALITY_PRESETS:
|
|
quality = "medium"
|
|
|
|
gs_quality = QUALITY_PRESETS[quality]
|
|
|
|
# Ensure output directory exists
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
|
|
cmd = [
|
|
"gs",
|
|
"-sDEVICE=pdfwrite",
|
|
"-dCompatibilityLevel=1.4",
|
|
f"-dPDFSETTINGS={gs_quality}",
|
|
"-dNOPAUSE",
|
|
"-dQUIET",
|
|
"-dBATCH",
|
|
"-dColorImageResolution=150",
|
|
"-dGrayImageResolution=150",
|
|
"-dMonoImageResolution=150",
|
|
f"-sOutputFile={output_path}",
|
|
input_path,
|
|
]
|
|
|
|
try:
|
|
original_size = os.path.getsize(input_path)
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120,
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
logger.error(f"Ghostscript compression failed: {result.stderr}")
|
|
raise PDFCompressionError(
|
|
f"Compression failed: {result.stderr or 'Unknown error'}"
|
|
)
|
|
|
|
if not os.path.exists(output_path):
|
|
raise PDFCompressionError("Compressed file was not created.")
|
|
|
|
compressed_size = os.path.getsize(output_path)
|
|
|
|
# If compressed file is larger, keep original
|
|
if compressed_size >= original_size:
|
|
import shutil
|
|
shutil.copy2(input_path, output_path)
|
|
compressed_size = original_size
|
|
|
|
reduction = (
|
|
((original_size - compressed_size) / original_size) * 100
|
|
if original_size > 0
|
|
else 0
|
|
)
|
|
|
|
logger.info(
|
|
f"PDF compression: {original_size} → {compressed_size} "
|
|
f"({reduction:.1f}% reduction)"
|
|
)
|
|
|
|
return {
|
|
"original_size": original_size,
|
|
"compressed_size": compressed_size,
|
|
"reduction_percent": round(reduction, 1),
|
|
}
|
|
|
|
except subprocess.TimeoutExpired:
|
|
raise PDFCompressionError("Compression timed out. File may be too large.")
|
|
except FileNotFoundError:
|
|
raise PDFCompressionError("Ghostscript is not installed on the server.")
|