Release ZipBundler 1.0.0 with guided recompression and complete Windows build

This commit is contained in:
2026-09-17 16:22:18 +02:00
parent ca934fe4b9
commit 66e14f72c3
6 changed files with 851 additions and 155 deletions

View File

@@ -5,10 +5,12 @@ import subprocess
import sys
import tempfile
import threading
import warnings
import zipfile
from dataclasses import dataclass
from contextlib import contextmanager
from dataclasses import dataclass, replace
from pathlib import Path
from tkinter import filedialog, messagebox
from tkinter import Canvas, filedialog, messagebox
import customtkinter as ctk
import fitz
@@ -24,6 +26,8 @@ PDF_OPTIMIZE_PROFILES = (
("piu_leggero", 120, 60),
("minimo", 100, 54),
)
PDF_AGGRESSIVE_PROFILES = ((72, 45), (60, 38), (48, 30), (36, 24), (24, 18))
IMAGE_AGGRESSIVE_PROFILES = ((1000, 50), (800, 40), (600, 30), (400, 24), (200, 18), (100, 12))
IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp", ".webp"}
OFFICE_SUFFIXES = {".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"}
IMAGE_OPTIMIZE_PROFILES = (
@@ -57,6 +61,14 @@ class OptimizedPdf:
profile: str
dpi: int | str | None
quality: int | None
zip_size: int = 0
max_zip_size: int = 0
can_retry: bool = True
next_step: str = ""
class BundleCancelled(Exception):
pass
def mb_to_bytes(size_mb):
@@ -68,6 +80,8 @@ def format_mb(size):
def optimization_label(report):
if report.profile == "originale":
return "originale: nessuna riduzione utile"
if report.kind == "office":
return "conversione PDF"
if report.kind == "image":
@@ -76,7 +90,7 @@ def optimization_label(report):
"qualita_email": "qualita email",
"massima_compressione": "massima compressione",
}
return labels.get(report.profile, report.profile)
return labels.get(report.profile, f"lato massimo {report.dpi} px, qualita {report.quality}")
if report.profile.startswith("ghostscript_"):
return f"Ghostscript {report.dpi}"
if report.dpi is None:
@@ -191,7 +205,7 @@ def optimized_pdf_result(source_file, optimized_path, profile, dpi, quality):
)
def optimize_pdf_with_ghostscript(source_file, output_folder, profile, log=None):
def optimize_pdf_with_ghostscript(source_file, output_folder, profile, log=None, dpi=None, quality=None):
ghostscript = find_ghostscript()
if not ghostscript:
if log:
@@ -204,7 +218,7 @@ def optimize_pdf_with_ghostscript(source_file, output_folder, profile, log=None)
if log:
log(
f"Provo Ghostscript /{profile}: "
f"Provo Ghostscript {str(dpi) + ' DPI' if dpi else '/' + profile}: "
f"{source_file.arcname} ({format_mb(source_file.stat().st_size)})"
)
@@ -220,14 +234,36 @@ def optimize_pdf_with_ghostscript(source_file, output_folder, profile, log=None)
"-dCompressFonts=true",
"-dSubsetFonts=true",
f"-sOutputFile={optimized_path}",
str(source_file.path),
]
if dpi is not None:
for kind in ("Color", "Gray", "Mono"):
command.extend([
f"-dDownsample{kind}Images=true",
f"-d{kind}ImageResolution={dpi}",
f"-d{kind}ImageDownsampleThreshold=1.0",
])
command.extend(["-dPassThroughJPEGImages=false", "-dPassThroughJPXImages=false"])
for kind in ("Color", "Gray"):
command.extend([
f"-d{kind}ImageDownsampleType=/Bicubic",
f"-dAutoFilter{kind}Images=false",
f"-d{kind}ImageFilter=/DCTEncode",
])
qfactor = 1 - (quality or 75) / 100
command.extend([
"-c",
f"<< /ColorImageDict << /QFactor {qfactor:.2f} >> "
f"/GrayImageDict << /QFactor {qfactor:.2f} >> >> setdistillerparams",
"-f",
])
command.append(str(source_file.path))
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=300,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
if completed.returncode != 0 or not optimized_path.exists():
@@ -240,9 +276,9 @@ def optimize_pdf_with_ghostscript(source_file, output_folder, profile, log=None)
return optimized_pdf_result(
source_file,
optimized_path,
profile=f"ghostscript_{profile}",
dpi=profile,
quality=None,
profile=f"dpi_{dpi}" if dpi else f"ghostscript_{profile}",
dpi=dpi if dpi else profile,
quality=quality,
)
@@ -302,7 +338,7 @@ def optimize_pdf(source_file, output_folder, profile, dpi, quality, log=None):
pixmap = source_page.get_pixmap(matrix=matrix, alpha=False)
image_bytes = pixmap.tobytes("jpeg", jpg_quality=quality)
page = optimized_doc.new_page(width=pixmap.width, height=pixmap.height)
page = optimized_doc.new_page(width=source_page.rect.width, height=source_page.rect.height)
page.insert_image(page.rect, stream=image_bytes)
optimized_doc.save(
@@ -425,6 +461,21 @@ def image_target_format(source_path, convert_uncompressed=False):
return None
@contextmanager
def open_image(path, log=None):
# Keep Pillow's hard pixel limit; route its advisory warning to the application log.
with warnings.catch_warnings():
warnings.simplefilter("ignore", Image.DecompressionBombWarning)
image = Image.open(path)
try:
if Image.MAX_IMAGE_PIXELS and image.width * image.height > Image.MAX_IMAGE_PIXELS and log:
log(f"Immagine molto grande ({image.width} x {image.height} pixel): "
"la riduzione puo richiedere piu memoria e tempo.")
yield image
finally:
image.close()
def optimize_image(source_file, output_folder, profile, max_side, quality, log=None, convert_uncompressed=False):
optimized_root = Path(output_folder) / "_immagini_ridotte_email"
target_format = image_target_format(source_file.path, convert_uncompressed=convert_uncompressed)
@@ -445,7 +496,9 @@ def optimize_image(source_file, output_folder, profile, max_side, quality, log=N
f"({format_mb(source_file.stat().st_size)})"
)
with Image.open(source_file.path) as image:
with open_image(source_file.path, log=log) as image:
if image.format == "JPEG":
image.draft(image.mode, (max_side, max_side))
image = ImageOps.exif_transpose(image)
if getattr(image, "is_animated", False) and log:
@@ -465,42 +518,14 @@ def optimize_image(source_file, output_folder, profile, max_side, quality, log=N
def try_optimize_image_for_limit(source_file, output_folder, max_zip_size, log=None, convert_uncompressed=False):
original_size = source_file.stat().st_size
for profile_name, max_side, quality in IMAGE_OPTIMIZE_PROFILES:
optimized = optimize_image(
source_file,
output_folder,
profile_name,
max_side,
quality,
log=log,
attempts = [
lambda p=p, side=side, q=q: optimize_image(
source_file, output_folder, p, side, q, log=log,
convert_uncompressed=convert_uncompressed,
)
if optimized.optimized_size >= original_size:
if log:
log(
"Scarto questa copia: non riduce l'immagine "
f"({format_mb(original_size)} -> {format_mb(optimized.optimized_size)})."
)
optimized.optimized.path.unlink(missing_ok=True)
continue
with tempfile.TemporaryDirectory() as temp_folder:
zip_size = compressed_zip_size([optimized.optimized], temp_folder)
if zip_size <= max_zip_size:
return optimized
if log:
log(
f"La copia immagine e ancora troppo grande: "
f"{format_mb(zip_size)} nello ZIP."
)
optimized.optimized.path.unlink(missing_ok=True)
return None
for p, side, q in IMAGE_OPTIMIZE_PROFILES
]
return select_standard_result(source_file, "image", attempts, max_zip_size, log)
def optimized_office_result(source_file, optimized_path):
@@ -604,7 +629,7 @@ def convert_office_to_pdf(source_file, output_folder, log=None):
return optimized_office_result(source_file, pdf_path)
def try_convert_office_for_limit(source_file, output_folder, max_zip_size, log=None):
def try_convert_office_for_limit(source_file, output_folder, max_zip_size, log=None, confirm_optimization=None):
converted = convert_office_to_pdf(source_file, output_folder, log=log)
if converted is None:
return None
@@ -613,19 +638,55 @@ def try_convert_office_for_limit(source_file, output_folder, max_zip_size, log=N
zip_size = compressed_zip_size([converted.optimized], temp_folder)
if zip_size <= max_zip_size:
return converted
return replace(converted, zip_size=zip_size, max_zip_size=max_zip_size)
if log:
log(f"Il PDF convertito e ancora troppo grande: {format_mb(zip_size)} nello ZIP.")
report, _ = choose_pdf_for_limit(
converted.optimized, output_folder, max_zip_size, log=log,
confirm_optimization=confirm_optimization,
)
return report
converted.optimized.path.unlink(missing_ok=True)
return None
def measured_report(report, max_zip_size):
with tempfile.TemporaryDirectory() as temp_folder:
size = compressed_zip_size([report.optimized], temp_folder)
return replace(report, zip_size=size, max_zip_size=max_zip_size)
def select_standard_result(source_file, kind, attempts, max_zip_size, log=None):
size = source_file.stat().st_size
best = measured_report(
OptimizedPdf(source_file, source_file, size, size, kind, "originale", None, None), max_zip_size,
)
# Profiles write to the same destination. Preserve the best bytes, not just its path.
with tempfile.TemporaryDirectory() as temp_folder:
saved = Path(temp_folder) / "best"
for attempt in attempts:
try:
candidate = attempt()
if candidate is None:
continue
candidate = measured_report(candidate, max_zip_size)
except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as exc:
if log:
log(f"Profilo standard non riuscito: {exc}")
continue
if log:
log(f"{optimization_label(candidate)}: {format_mb(candidate.zip_size)} nello ZIP; "
f"limite {format_mb(max_zip_size)}.")
if candidate.zip_size <= max_zip_size:
return candidate
if candidate.zip_size < best.zip_size:
best = candidate
shutil.copyfile(candidate.optimized.path, saved)
if best.optimized.path != source_file.path:
shutil.copyfile(saved, best.optimized.path)
return best
def try_optimize_pdf_for_limit(source_file, output_folder, max_zip_size, log=None):
original_size = source_file.stat().st_size
skip_raster_profiles = pdf_has_significant_text(source_file.path)
attempts = []
for profile_name, dpi, quality in PDF_OPTIMIZE_PROFILES:
if isinstance(dpi, int) and skip_raster_profiles:
if log:
@@ -635,38 +696,89 @@ def try_optimize_pdf_for_limit(source_file, output_folder, max_zip_size, log=Non
)
break
if log:
log(f"Provo profilo PDF '{profile_name}'.")
if isinstance(dpi, str):
optimized = optimize_pdf_with_ghostscript(source_file, output_folder, dpi, log=log)
if optimized is None:
continue
attempts.append(lambda dpi=dpi: optimize_pdf_with_ghostscript(
source_file, output_folder, dpi, log=log,
))
else:
optimized = optimize_pdf(source_file, output_folder, profile_name, dpi, quality, log=log)
attempts.append(lambda p=profile_name, dpi=dpi, q=quality: optimize_pdf(
source_file, output_folder, p, dpi, q, log=log,
))
return select_standard_result(source_file, "pdf", attempts, max_zip_size, log)
if optimized.optimized_size >= original_size:
if log:
log(
"Scarto questa copia: non riduce il PDF "
f"({format_mb(original_size)} -> {format_mb(optimized.optimized_size)})."
)
optimized.optimized.path.unlink(missing_ok=True)
continue
with tempfile.TemporaryDirectory() as temp_folder:
zip_size = compressed_zip_size([optimized.optimized], temp_folder)
if zip_size <= max_zip_size:
return optimized
if log:
log(
f"La copia ridotta e ancora troppo grande: "
f"{format_mb(zip_size)} nello ZIP."
def pdf_at_dpi(source_file, output_folder, dpi, quality, log=None, raster=False):
if not raster:
try:
report = optimize_pdf_with_ghostscript(
source_file, output_folder, "default", log=log, dpi=dpi, quality=quality,
)
if report is not None:
return report
except (OSError, subprocess.SubprocessError) as exc:
if log:
log(f"Ghostscript a {dpi} DPI non riuscito: {exc}")
if log:
log(f"Ricodifica delle pagine a immagini: {dpi} DPI, qualita JPEG {quality}.")
return optimize_pdf(source_file, output_folder, f"raster_{dpi}", dpi, quality, log=log)
return None
def negotiate_compression(report, attempts, max_zip_size, confirm_optimization, log=None):
if report.zip_size <= max_zip_size:
if log:
log("Profilo standard entro il limite: copia accettata automaticamente.")
return report
if confirm_optimization is None:
raise ValueError("Il file resta fuori limite: avvia la GUI per scegliere una compressione aggiuntiva.")
for index in range(len(attempts) + 1):
next_step = attempts[index][0] if index < len(attempts) else ""
report = replace(report, can_retry=index < len(attempts), next_step=next_step)
answer = confirm_optimization(report)
if answer is True:
return report
if answer is None:
raise BundleCancelled("Elaborazione annullata: nessun risultato rifiutato e stato impacchettato.")
if not report.can_retry:
raise BundleCancelled("Profili esauriti: risultato non accettato, elaborazione annullata.")
try:
candidate = attempts[index][1]()
report = measured_report(candidate, max_zip_size)
if log:
log(f"Compressione aggiuntiva: {optimization_label(report)}, "
f"{format_mb(report.zip_size)} nello ZIP. Richiesta approvazione.")
except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as exc:
# Do not leave a report pointing at an overwritten or partial candidate.
raise ValueError(f"Compressione aggiuntiva non riuscita; nessun pacchetto creato: {exc}") from exc
raise BundleCancelled("Nessuna compressione accettata.")
def choose_pdf_for_limit(source_file, output_folder, max_zip_size, log=None, confirm_optimization=None):
report = try_optimize_pdf_for_limit(source_file, output_folder, max_zip_size, log=log)
attempts = []
for raster in ((False, True) if find_ghostscript() else (True,)):
for dpi, quality in PDF_AGGRESSIVE_PROFILES:
description = (f"Pagine a immagini, {dpi} DPI: il testo non sara selezionabile."
if raster else f"Ghostscript, immagini a {dpi} DPI.")
attempts.append((description, lambda dpi=dpi, q=quality, raster=raster: pdf_at_dpi(
source_file, output_folder, dpi, q, log=log, raster=raster,
)))
report = negotiate_compression(report, attempts, max_zip_size, confirm_optimization, log)
return report, report.zip_size > max_zip_size
def choose_image_for_limit(source_file, output_folder, max_zip_size, log=None,
convert_uncompressed=False, confirm_optimization=None):
report = try_optimize_image_for_limit(
source_file, output_folder, max_zip_size, log=log, convert_uncompressed=convert_uncompressed,
)
attempts = [
(f"Lato massimo {side} pixel, qualita {quality}.",
lambda side=side, q=quality: optimize_image(
source_file, output_folder, f"aggressivo_{side}", side, q, log=log,
convert_uncompressed=convert_uncompressed,
)) for side, quality in IMAGE_AGGRESSIVE_PROFILES
]
return negotiate_compression(report, attempts, max_zip_size, confirm_optimization, log)
def split_into_groups(
@@ -706,20 +818,22 @@ def split_into_groups(
replacement = None
if file_path.path.suffix.lower() == ".pdf":
replacement = try_optimize_pdf_for_limit(
replacement, _ = choose_pdf_for_limit(
file_path,
output_folder,
max_zip_size,
log=log,
confirm_optimization=confirm_optimization,
)
elif file_path.path.suffix.lower() in IMAGE_SUFFIXES:
replacement = try_optimize_image_for_limit(
replacement = choose_image_for_limit(
file_path,
output_folder,
max_zip_size,
log=log,
convert_uncompressed=convert_uncompressed_images,
confirm_optimization=confirm_optimization,
)
elif convert_office_to_pdf_enabled and file_path.path.suffix.lower() in OFFICE_SUFFIXES:
replacement = try_convert_office_for_limit(
@@ -727,17 +841,20 @@ def split_into_groups(
output_folder,
max_zip_size,
log=log,
confirm_optimization=confirm_optimization,
)
if replacement and replacement.kind != "office" and confirm_optimization:
accepted = confirm_optimization(replacement)
if not accepted:
if log:
log(f"Ottimizzazione rifiutata: {file_path.arcname}")
replacement = None
if replacement:
optimized_reports.append(replacement)
if replacement.zip_size > max_zip_size:
if current_group:
groups.append(current_group)
current_group = []
groups.append([replacement.optimized])
if log:
log(f"Pacchetto separato FUORI LIMITE, accettato esplicitamente: "
f"{file_path.arcname}, {format_mb(replacement.zip_size)} nello ZIP.")
continue
replacement_group = current_group + [replacement.optimized]
replacement_group_size = compressed_zip_size(replacement_group, temp_folder)
@@ -762,16 +879,11 @@ def split_into_groups(
f"{format_mb(replacement.optimized_size)})"
)
else:
if current_group:
groups.append(current_group)
current_group = []
if log:
log(
"ATTENZIONE: il file supera il limite anche da solo "
f"e verra inserito in un pacchetto fuori limite: {file_path.arcname} "
f"({format_mb(single_size)} nello ZIP)"
)
groups.append([file_path])
raise ValueError(
f"{file_path.arcname}: {format_mb(single_size)} nello ZIP, oltre il limite. "
"Nessuna conversione disponibile o abilitata per questo file; "
"elaborazione interrotta senza creare pacchetti fuori limite non approvati."
)
if current_group:
groups.append(current_group)
@@ -899,21 +1011,25 @@ def pdf_page_count(pdf_path):
doc.close()
def render_pdf_preview(pdf_path, page_index=0, max_size=(360, 480)):
def render_pdf_preview(pdf_path, page_index=0, max_size=(360, 480), zoom=None):
doc = fitz.open(pdf_path)
try:
page_index = max(0, min(page_index, doc.page_count - 1))
page = doc.load_page(page_index)
pixmap = page.get_pixmap(matrix=fitz.Matrix(1.2, 1.2), alpha=False)
scale = (96 / 72 * zoom) if zoom is not None else min(
max_size[0] / page.rect.width, max_size[1] / page.rect.height,
)
pixmap = page.get_pixmap(matrix=fitz.Matrix(scale, scale), alpha=False)
image = Image.frombytes("RGB", (pixmap.width, pixmap.height), pixmap.samples)
image.thumbnail(max_size, Image.Resampling.LANCZOS)
return image
finally:
doc.close()
def render_image_preview(image_path, max_size=(360, 480)):
with Image.open(image_path) as image:
with open_image(image_path) as image:
if image.format == "JPEG":
image.draft(image.mode, max_size)
image = ImageOps.exif_transpose(image)
if getattr(image, "is_animated", False):
image.seek(0)
@@ -922,9 +1038,9 @@ def render_image_preview(image_path, max_size=(360, 480)):
return image.copy()
def render_preview(report, path, page_index=0):
def render_preview(report, path, page_index=0, zoom=None, max_size=(360, 480)):
if report.kind == "pdf":
return render_pdf_preview(path, page_index)
return render_pdf_preview(path, page_index, max_size=max_size, zoom=zoom)
return render_image_preview(path)
@@ -932,9 +1048,11 @@ class OptimizationDialog(ctk.CTkToplevel):
def __init__(self, parent, report):
super().__init__(parent)
self.report = report
self.result = False
self.result = None
self.page_index = 0
self.page_count = 1
self.zoom = None
self.canvases = []
if report.kind == "pdf":
self.page_count = min(
pdf_page_count(report.original.path),
@@ -949,14 +1067,18 @@ class OptimizationDialog(ctk.CTkToplevel):
self.grid_columnconfigure((0, 1), weight=1)
self.grid_rowconfigure(2, weight=1)
item_label = "PDF" if report.kind == "pdf" else "immagine"
title = (
f"{report.original.arcname.name} supera il limite. "
"Ho creato una copia alleggerita per email."
)
ctk.CTkLabel(self, text=title, wraplength=780, font=ctk.CTkFont(size=16, weight="bold")).grid(
oversized = report.zip_size > report.max_zip_size
status = "FUORI LIMITE" if oversized else "ENTRO IL LIMITE - qualita da approvare"
title = (f"{report.original.arcname.name}\n{status}: ZIP {format_mb(report.zip_size)}; "
f"limite {format_mb(report.max_zip_size)}.\n")
title += (f"Prossimo tentativo: {report.next_step}" if report.can_retry else
"Ultimo profilo disponibile. Accetta questo risultato oppure annulla l'elaborazione.")
self.title_label = ctk.CTkLabel(self, text=title, wraplength=720, font=ctk.CTkFont(size=16, weight="bold"))
self.title_label.grid(
row=0, column=0, columnspan=2, padx=18, pady=(18, 10), sticky="w"
)
self.bind("<Configure>", lambda event: self.title_label.configure(
wraplength=max(300, self.winfo_width() - 50)) if event.widget == self else None)
pager = ctk.CTkFrame(self, fg_color="transparent")
pager.grid(row=1, column=0, columnspan=2, padx=18, pady=(0, 8), sticky="ew")
@@ -973,6 +1095,13 @@ class OptimizationDialog(ctk.CTkToplevel):
if report.kind != "pdf":
self.prev_button.grid_remove()
self.next_button.grid_remove()
else:
self.zoom_menu = ctk.CTkOptionMenu(
pager, values=["Adatta", "50%", "100%", "150%", "200%", "300%", "400%"],
width=100, command=self.set_zoom,
)
self.zoom_menu.set("Adatta")
self.zoom_menu.grid(row=0, column=3, padx=(16, 0))
self.original_image = None
self.optimized_image = None
@@ -992,14 +1121,20 @@ class OptimizationDialog(ctk.CTkToplevel):
buttons = ctk.CTkFrame(self, fg_color="transparent")
buttons.grid(row=3, column=0, columnspan=2, padx=18, pady=(10, 18), sticky="e")
ctk.CTkButton(buttons, text="Rifiuta", width=110, command=self.reject).grid(
ctk.CTkButton(buttons, text="Annulla elaborazione", width=160, command=self.cancel).grid(
row=0, column=0, padx=(0, 10)
)
ctk.CTkButton(buttons, text="Accetta copia ridotta", width=180, command=self.accept).grid(
row=0, column=1
self.more_button = ctk.CTkButton(
buttons, text="Rifiuta e comprimi di piu", width=190, command=self.reject,
state="normal" if report.can_retry else "disabled",
)
self.more_button.grid(row=0, column=1, padx=(0, 10))
ctk.CTkButton(buttons, text="Accetta fuori limite" if oversized else "Accetta risultato",
width=180, command=self.accept).grid(
row=0, column=2
)
self.protocol("WM_DELETE_WINDOW", self.reject)
self.protocol("WM_DELETE_WINDOW", self.cancel)
def _build_preview_column(self, column, label, size_text):
frame = ctk.CTkFrame(self, corner_radius=8)
@@ -1010,23 +1145,48 @@ class OptimizationDialog(ctk.CTkToplevel):
ctk.CTkLabel(frame, text=f"{label} - {size_text}", font=ctk.CTkFont(weight="bold")).grid(
row=0, column=0, padx=12, pady=(12, 8), sticky="w"
)
image_label = ctk.CTkLabel(frame, text="")
image_label.grid(row=1, column=0, padx=12, pady=(0, 12), sticky="n")
canvas = Canvas(frame, background="#333333", highlightthickness=0, width=360, height=420)
canvas.grid(row=1, column=0, padx=(8, 0), sticky="nsew")
vertical = ctk.CTkScrollbar(frame, command=lambda *args: self.scroll_previews("y", *args))
vertical.grid(row=1, column=1, sticky="ns")
horizontal = ctk.CTkScrollbar(
frame, orientation="horizontal", command=lambda *args: self.scroll_previews("x", *args),
)
horizontal.grid(row=2, column=0, sticky="ew", padx=8)
canvas.configure(yscrollcommand=vertical.set, xscrollcommand=horizontal.set)
canvas.bind("<MouseWheel>", self.scroll_wheel)
canvas.bind("<Shift-MouseWheel>", lambda event: self.scroll_wheel(event, "x"))
self.canvases.append(canvas)
if column == 0:
self.original_image_label = image_label
else:
self.optimized_image_label = image_label
def scroll_previews(self, axis, *args):
for canvas in self.canvases:
getattr(canvas, axis + "view")(*args)
def scroll_wheel(self, event, axis="y"):
self.scroll_previews(axis, "scroll", -1 if event.delta > 0 else 1, "units")
return "break"
def set_zoom(self, value):
self.zoom = None if value == "Adatta" else int(value.rstrip("%")) / 100
self.update_previews()
def update_previews(self):
self.update_idletasks()
max_size = (max(100, self.canvases[0].winfo_width() - 8),
max(100, self.canvases[0].winfo_height() - 8))
self.original_image = ImageTk.PhotoImage(
render_preview(self.report, self.report.original.path, self.page_index)
render_preview(self.report, self.report.original.path, self.page_index, self.zoom, max_size)
)
self.optimized_image = ImageTk.PhotoImage(
render_preview(self.report, self.report.optimized.path, self.page_index)
render_preview(self.report, self.report.optimized.path, self.page_index, self.zoom, max_size)
)
self.original_image_label.configure(image=self.original_image)
self.optimized_image_label.configure(image=self.optimized_image)
for canvas, photo in zip(self.canvases, (self.original_image, self.optimized_image)):
canvas.delete("all")
x = max(0, (canvas.winfo_width() - photo.width()) // 2)
canvas.create_image(x, 0, image=photo, anchor="nw")
canvas.configure(scrollregion=(0, 0, max(photo.width(), canvas.winfo_width()), photo.height()))
canvas.xview_moveto(0)
canvas.yview_moveto(0)
if self.report.kind == "pdf":
self.page_label.configure(text=f"Pagina {self.page_index + 1} / {self.page_count}")
else:
@@ -1054,6 +1214,10 @@ class OptimizationDialog(ctk.CTkToplevel):
self.result = False
self.destroy()
def cancel(self):
self.result = None
self.destroy()
class LargeEmailSizeDialog(ctk.CTkToplevel):
def __init__(self, parent, size_value):
@@ -1349,17 +1513,26 @@ class ZipBundlerApp(ctk.CTk):
self.large_size_panel.grid_remove()
def confirm_optimization(self, report):
return self.ask_worker_dialog(lambda: OptimizationDialog(self, report))
def ask_worker_dialog(self, factory):
event = threading.Event()
answer = {"value": False}
def show_dialog():
dialog = OptimizationDialog(self, report)
self.wait_window(dialog)
answer["value"] = dialog.result
event.set()
try:
dialog = factory()
self.wait_window(dialog)
answer["value"] = dialog.result
except Exception as exc:
answer["error"] = exc
finally:
event.set()
self.after(0, show_dialog)
event.wait()
if "error" in answer:
raise answer["error"]
return answer["value"]
def confirm_large_email_size(self, size_value):
@@ -1431,6 +1604,8 @@ class ZipBundlerApp(ctk.CTk):
convert_uncompressed_images=convert_uncompressed,
convert_office_to_pdf_enabled=convert_office,
)
except BundleCancelled as exc:
self.after(0, self.append_log, str(exc))
except Exception as exc:
self.after(0, messagebox.showerror, "Errore", str(exc))
self.after(0, self.append_log, f"ERRORE: {exc}")