04/27 Updated inspection PDF export with compress, low images to reduce file sizes
This commit is contained in:
+58
-1
@@ -17,6 +17,12 @@ import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from PIL import Image as PILImage
|
||||
_PIL_AVAILABLE = True
|
||||
except ImportError:
|
||||
_PIL_AVAILABLE = False
|
||||
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.units import inch
|
||||
@@ -94,6 +100,51 @@ def _build_styles():
|
||||
STYLES = _build_styles()
|
||||
|
||||
|
||||
# ── Image compression helper ──────────────────────────────────────────────────
|
||||
|
||||
# Max pixel dimension for any side of an image embedded in the PDF.
|
||||
# Phone cameras produce 12–48 MP images; 800px is plenty for a printed report.
|
||||
_IMG_MAX_PX = 800
|
||||
# JPEG quality for re-encoded images (0–95). 55 is visually acceptable for
|
||||
# a printed report and reduces a typical phone photo by ~90% vs the original.
|
||||
_IMG_JPEG_QUALITY = 55
|
||||
|
||||
|
||||
def _compress_image(src_path: str) -> io.BytesIO | None:
|
||||
"""
|
||||
Load an image from disk, resize it to fit within _IMG_MAX_PX on any side,
|
||||
re-encode as JPEG at _IMG_JPEG_QUALITY, and return a BytesIO buffer.
|
||||
|
||||
Returns None if PIL is unavailable or the image cannot be processed,
|
||||
signalling the caller to fall back to the original file path.
|
||||
"""
|
||||
if not _PIL_AVAILABLE:
|
||||
return None
|
||||
try:
|
||||
img = PILImage.open(src_path)
|
||||
|
||||
# Strip transparency — JPEG does not support alpha channels.
|
||||
# Convert palette / RGBA / LA → RGB before saving.
|
||||
if img.mode in ('RGBA', 'LA', 'P'):
|
||||
background = PILImage.new('RGB', img.size, (255, 255, 255))
|
||||
if img.mode == 'P':
|
||||
img = img.convert('RGBA')
|
||||
background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
|
||||
img = background
|
||||
elif img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
# Resize proportionally so neither dimension exceeds _IMG_MAX_PX.
|
||||
img.thumbnail((_IMG_MAX_PX, _IMG_MAX_PX), PILImage.LANCZOS)
|
||||
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format='JPEG', quality=_IMG_JPEG_QUALITY, optimize=True)
|
||||
buf.seek(0)
|
||||
return buf
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ── Header / footer callbacks ─────────────────────────────────────────────────
|
||||
|
||||
def _on_page(canvas, doc, title, generated_at):
|
||||
@@ -417,7 +468,13 @@ def _form_fields_section(form_fields, form_data, static_folder):
|
||||
# File existence already verified in the skip gate above
|
||||
img_path = os.path.join(static_folder, val)
|
||||
try:
|
||||
val_p = RLImage(img_path,
|
||||
# Attempt to compress the image before embedding.
|
||||
# _compress_image() resizes to _IMG_MAX_PX and re-encodes
|
||||
# as JPEG at _IMG_JPEG_QUALITY — typically 85–95% smaller
|
||||
# than the original phone photo.
|
||||
compressed = _compress_image(img_path)
|
||||
img_src = compressed if compressed is not None else img_path
|
||||
val_p = RLImage(img_src,
|
||||
width=min(cell_w - 12, 1.4 * inch),
|
||||
height=1.0 * inch,
|
||||
kind='proportional')
|
||||
|
||||
Reference in New Issue
Block a user