From c3c686a984f7b7fae9c29a9c3f397649fa708563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Luiz=20Bortot=20Monteiro=20do=20Ros=C3=A1rio?= <60971278+PedroBMR@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:46:38 -0300 Subject: [PATCH] Guard Pillow-dependent features --- README.md | 3 +++ compile/__init__.py | 19 +++++++++++++++++-- compile/tspl.py | 20 ++++++++++++++++++-- compile/zpl.py | 20 ++++++++++++++++++-- editor/items/base.py | 8 +++++++- editor/roll_preview.py | 16 +++++++++++++++- editor/widget.py | 24 +++++++++++++++++++++++- imaging/loader.py | 19 +++++++++++++++++-- imaging/raster.py | 19 ++++++++++++++++++- printing/__init__.py | 32 +++++++++++++++++++++++++++++--- quick_print.py | 28 ++++++++++++++++++++++++++-- render/preview.py | 23 +++++++++++++++++++++-- tests/test_compile_backends.py | 9 +++++++++ tests/test_export_labels.py | 11 +++++++++++ tests/test_preview_render.py | 11 +++++++++++ tests/test_printing_new.py | 9 +++++++++ tests/test_raster_cache.py | 8 ++++++++ tests/test_utils.py | 9 +++++++++ utils/__init__.py | 18 +++++++++++++++++- 19 files changed, 286 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 55bf1a3..1d48ba6 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ reimprimir etiquetas e realizar backup automático dos dados. - **Python 3.11 ou superior** - **PyQt5** (até Python 3.12) ou **PySide6** (Python 3.13+) para a interface gráfica - **Pillow** para tratamento de imagens + - Sem Pillow, recursos de pré-visualização, renderização e exportação ficam + indisponíveis; o aplicativo continua executando, mas essas áreas exibem + mensagens orientando a instalar a biblioteca. - **pywin32** (somente no Windows) para envio direto à impressora - Impressora térmica compatível com **TSPL** (ex.: Gainscha GS‑2406T) diff --git a/compile/__init__.py b/compile/__init__.py index f725be9..dc7ed5c 100644 --- a/compile/__init__.py +++ b/compile/__init__.py @@ -25,9 +25,23 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any, Mapping -from PIL import Image, ImageDraw, ImageFont +try: # pragma: no cover - optional dependency guard + from PIL import Image, ImageDraw, ImageFont # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + Image = ImageDraw = ImageFont = None # type: ignore[assignment] + +if TYPE_CHECKING: # pragma: no cover - typing aid + from PIL import Image as PILImage + + +def _require_pillow() -> None: + if Image is None or ImageDraw is None or ImageFont is None: + raise RuntimeError( + "Pillow must be installed to render label graphics. " + "Install the 'Pillow' package to enable this feature." + ) from fonts import font_store from imaging import load_image @@ -92,6 +106,7 @@ def image_from_source(source: Any) -> Image.Image: def render_text_bitmap(text: str, font_spec: Mapping[str, Any]) -> Image.Image: """Render ``text`` into a monochrome bitmap using Pillow.""" + _require_pillow() size = int(font_spec.get("size", 24)) font_path = font_store.resolve_spec_path(font_spec.get("ttf")) try: diff --git a/compile/tspl.py b/compile/tspl.py index 2337628..8e7e2f8 100644 --- a/compile/tspl.py +++ b/compile/tspl.py @@ -2,9 +2,23 @@ from __future__ import annotations -from typing import Any, Iterable, Mapping +from typing import TYPE_CHECKING, Any, Iterable, Mapping -from PIL import Image +try: # pragma: no cover - optional dependency guard + from PIL import Image # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + Image = None # type: ignore[assignment] + +if TYPE_CHECKING: # pragma: no cover - typing only + from PIL import Image as PILImage + + +def _require_pillow() -> None: + if Image is None: + raise RuntimeError( + "Pillow must be installed to render TSPL bitmaps. " + "Install the 'Pillow' package to enable this feature." + ) from imaging.raster import ( fingerprint_image, @@ -232,6 +246,7 @@ def _render_shape( def _load_image( self, source: Any, element: Mapping[str, Any] ) -> tuple[Image.Image, str]: + _require_pillow() image = image_from_source(source) image.load() return image, fingerprint_image(image) @@ -258,6 +273,7 @@ def _bitmap_command( *, digest: str | None = None, ) -> bytes: + _require_pillow() settings = self._resolve_raster_settings(element) width = self.optional_dimension(element, "width", axis="x") height = self.optional_dimension(element, "height", axis="y") diff --git a/compile/zpl.py b/compile/zpl.py index 5fc3ab6..8ac2d72 100644 --- a/compile/zpl.py +++ b/compile/zpl.py @@ -2,9 +2,23 @@ from __future__ import annotations -from typing import Any, Iterable, Mapping +from typing import TYPE_CHECKING, Any, Iterable, Mapping -from PIL import Image +try: # pragma: no cover - optional dependency guard + from PIL import Image # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + Image = None # type: ignore[assignment] + +if TYPE_CHECKING: # pragma: no cover - typing only + from PIL import Image as PILImage + + +def _require_pillow() -> None: + if Image is None: + raise RuntimeError( + "Pillow must be installed to render ZPL bitmaps. " + "Install the 'Pillow' package to enable this feature." + ) from imaging.raster import fingerprint_image, prepare_bitmap_for_tspl from printing_utils import ZPLMediaParams, emit_zpl_footer, emit_zpl_media_setup @@ -197,6 +211,7 @@ def _render_shape( def _load_image( self, source: Any, element: Mapping[str, Any] ) -> tuple[Image.Image, str]: + _require_pillow() image = image_from_source(source) image.load() return image, fingerprint_image(image) @@ -223,6 +238,7 @@ def _graphic_field( *, digest: str | None = None, ) -> bytes: + _require_pillow() settings = self._resolve_raster_settings(element) width = self.optional_dimension(element, "width", axis="x") height = self.optional_dimension(element, "height", axis="y") diff --git a/editor/items/base.py b/editor/items/base.py index 14b031d..c0486a8 100644 --- a/editor/items/base.py +++ b/editor/items/base.py @@ -6,7 +6,10 @@ from dataclasses import dataclass from typing import Any, Iterable, Mapping -from PIL.ImageQt import ImageQt +try: # pragma: no cover - optional dependency guard + from PIL.ImageQt import ImageQt # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + ImageQt = None # type: ignore[assignment] from qt_compat import ( QColor, @@ -121,6 +124,9 @@ def _update_pixmap(self) -> None: self.prepareGeometryChange() self._rect = QRectF(0.0, 0.0, width_px, height_px) self.setTransformOriginPoint(self._rect.center()) + if ImageQt is None: + self._pixmap = QPixmap() + return self._pixmap = QPixmap.fromImage(ImageQt(image)) else: self._pixmap = QPixmap() diff --git a/editor/roll_preview.py b/editor/roll_preview.py index e43a2e7..723429e 100644 --- a/editor/roll_preview.py +++ b/editor/roll_preview.py @@ -4,7 +4,10 @@ from typing import Any, Mapping -from PIL.ImageQt import ImageQt +try: # pragma: no cover - optional dependency guard + from PIL.ImageQt import ImageQt # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + ImageQt = None # type: ignore[assignment] from qt_compat import ( QColor, @@ -91,9 +94,19 @@ def __init__(self, parent: QWidget | None = None) -> None: self._on_speed_changed(self.speed_slider.value()) self._update_controls_enabled(False) + self._pillow_available = ImageQt is not None + if not self._pillow_available: + self.placeholder.setText("Simulação indisponível: instale Pillow.") + self.play_button.setEnabled(False) + self.speed_slider.setEnabled(False) + self.speed_label.setText("-") # ------------------------------------------------------------------ def set_image(self, image, metadata: Mapping[str, Any] | None = None) -> None: + if ImageQt is None: + self._clear() + self.placeholder.setText("Simulação indisponível: instale Pillow.") + return if image is None: self._clear() return @@ -212,6 +225,7 @@ def _calculate_pixels_per_mm( return 1.0 def _update_controls_enabled(self, enabled: bool) -> None: + enabled = enabled and self._pillow_available for widget in (self.play_button, self.speed_slider): widget.setEnabled(enabled) if not enabled: diff --git a/editor/widget.py b/editor/widget.py index 6aa648f..dcf1be3 100644 --- a/editor/widget.py +++ b/editor/widget.py @@ -7,7 +7,10 @@ from datetime import datetime from typing import Any, Mapping -from PIL.ImageQt import ImageQt +try: # pragma: no cover - optional dependency guard + from PIL.ImageQt import ImageQt # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + ImageQt = None # type: ignore[assignment] from qt_compat import ( QBrush, @@ -260,6 +263,13 @@ def __init__(self, parent: QWidget | None = None) -> None: self.metadata_label.setStyleSheet("color: #555; font-size: 11px;") layout.addWidget(self.metadata_label) + self._pillow_available = ImageQt is not None + if not self._pillow_available: + self.placeholder.setText( + "Pré-visualização indisponível: instale Pillow." + ) + self.refresh_button.setEnabled(False) + self.metadata_label.setText("Pré-visualização desativada") self._update_controls_enabled(False) # ------------------------------------------------------------------ @@ -274,6 +284,7 @@ def _build_checkerboard_brush(self) -> QBrush: return QBrush(pixmap) def _update_controls_enabled(self, enabled: bool) -> None: + enabled = enabled and self._pillow_available for widget in ( self.zoom_out_button, self.zoom_in_button, @@ -345,6 +356,17 @@ def _on_zoom_slider_changed(self, value: int) -> None: # ------------------------------------------------------------------ def set_image(self, image, metadata: Mapping[str, Any] | None = None) -> None: + if ImageQt is None: + self._pixmap = None + self._pixmap_item = None + self.scene.clear() + self.placeholder.setText( + "Pré-visualização indisponível: instale Pillow." + ) + self.stacked.setCurrentWidget(self.placeholder) + self.metadata_label.setText("Pré-visualização desativada") + self._update_controls_enabled(False) + return if image is None: self._pixmap = None self._pixmap_item = None diff --git a/imaging/loader.py b/imaging/loader.py index 82e0f8a..6158acc 100644 --- a/imaging/loader.py +++ b/imaging/loader.py @@ -5,9 +5,23 @@ import io import uuid from pathlib import Path -from typing import Any, BinaryIO +from typing import TYPE_CHECKING, Any, BinaryIO -from PIL import Image +try: # pragma: no cover - optional dependency guard + from PIL import Image # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + Image = None # type: ignore[assignment] + +if TYPE_CHECKING: # pragma: no cover - typing only + from PIL import Image as PILImage + + +def _require_pillow() -> None: + if Image is None: + raise RuntimeError( + "Pillow must be installed to load and process images. " + "Install the 'Pillow' package to enable this feature." + ) from utils.fs import canonical_path, file_hash @@ -85,6 +99,7 @@ def _record_asset( def load_image(source: Any) -> Image.Image: """Return a :class:`PIL.Image.Image` from ``source`` recording metadata.""" + _require_pillow() if isinstance(source, Image.Image): return source.copy() diff --git a/imaging/raster.py b/imaging/raster.py index 77d6ac7..9554f39 100644 --- a/imaging/raster.py +++ b/imaging/raster.py @@ -6,14 +6,29 @@ import io from collections.abc import Iterable, MutableMapping from pathlib import Path +from typing import TYPE_CHECKING -from PIL import Image, ImageFilter, ImageOps +try: # pragma: no cover - optional dependency guard + from PIL import Image, ImageFilter, ImageOps # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + Image = ImageFilter = ImageOps = None # type: ignore[assignment] + +if TYPE_CHECKING: # pragma: no cover - typing aid + from PIL import Image as PILImage try: # pragma: no cover - optional Pillow component from PIL import ImageCms # type: ignore except Exception: # pragma: no cover - optional dependency ImageCms = None # type: ignore[assignment] + +def _require_pillow() -> None: + if Image is None or ImageFilter is None or ImageOps is None: + raise RuntimeError( + "Pillow must be installed to rasterise images. " + "Install the 'Pillow' package to enable this feature." + ) + __all__ = [ "clear_raster_cache", "prepare_bitmap_for_tspl", @@ -229,6 +244,7 @@ def prepare_bitmap_for_tspl( accessed. """ + _require_pillow() dots_x = width_dots or _mm_to_dots(width_mm, dpi_x) dots_y = height_dots or _mm_to_dots(height_mm, dpi_y) @@ -399,6 +415,7 @@ def tspl_bitmap_command( def fingerprint_image(image: Image.Image) -> str: """Return a stable digest identifying ``image`` contents.""" + _require_pillow() try: image.load() except Exception: # pragma: no cover - depends on Pillow backend diff --git a/printing/__init__.py b/printing/__init__.py index 3fbb37c..b8ac0b4 100644 --- a/printing/__init__.py +++ b/printing/__init__.py @@ -14,9 +14,15 @@ import unicodedata from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable, Mapping, Protocol +from typing import TYPE_CHECKING, Any, Callable, Mapping, Protocol -from PIL import Image +try: # pragma: no cover - optional dependency guard + from PIL import Image # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + Image = None # type: ignore[assignment] + +if TYPE_CHECKING: # pragma: no cover - typing only + from PIL import Image as PILImage from compile import Calibration, CompilationError, SafeDict, get_backend from db import store as db_store @@ -27,7 +33,7 @@ from utils import recurso_caminho from utils.fs import canonical_path as canonical_file_path -from .queue import PrinterTarget, enqueue_job +from .queue import PrinterTarget, enqueue_job, wait_for_all as _queue_wait_for_all # ---------------------------------------------------------------------------- @@ -214,6 +220,21 @@ }, } + +def _require_pillow() -> None: + if Image is None: + raise RuntimeError( + "Pillow must be installed to render or export labels. " + "Install the 'Pillow' package to enable this feature." + ) + + +def wait_for_all_jobs(timeout: float | None = None) -> bool: + """Aguarda a conclusão de todos os trabalhos na fila.""" + + return _queue_wait_for_all(timeout) + + ALIASES: dict[str, str] = {"padrao": "100x30", "padrão": "100x30"} CALIBRATION_SQUARE_MM = 50.0 @@ -827,6 +848,7 @@ def render_label_images( *, media_settings: Mapping[str, Any] | None = None, ) -> list[Image.Image]: + _require_pillow() working = template if media_settings: working = apply_media_settings(template, media_settings) @@ -840,12 +862,14 @@ def render_label_images( def _write_png(image: Image.Image, destino: Path) -> Path: + _require_pillow() destino.parent.mkdir(parents=True, exist_ok=True) image.convert("L").save(destino, format="PNG") return destino def _write_pdf(images: list[Image.Image], destino: Path) -> Path: + _require_pillow() destino.parent.mkdir(parents=True, exist_ok=True) primary = images[0].convert("RGB") extras = [img.convert("RGB") for img in images[1:]] @@ -854,6 +878,7 @@ def _write_pdf(images: list[Image.Image], destino: Path) -> Path: def _write_svg(image: Image.Image, destino: Path) -> Path: + _require_pillow() destino.parent.mkdir(parents=True, exist_ok=True) buffer = io.BytesIO() image.save(buffer, format="PNG") @@ -881,6 +906,7 @@ def export_labels( media_settings: Mapping[str, Any] | None = None, batch: bool = False, ) -> tuple[list[Path], int]: + _require_pillow() images = render_label_images(template, dados, media_settings=media_settings) if not images: raise ValueError("Nenhuma etiqueta foi gerada para exportação") diff --git a/quick_print.py b/quick_print.py index c7ddef6..61606f5 100644 --- a/quick_print.py +++ b/quick_print.py @@ -5,7 +5,10 @@ import json from typing import Any, Mapping -from PIL.ImageQt import ImageQt +try: # pragma: no cover - optional dependency guard + from PIL.ImageQt import ImageQt # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + ImageQt = None # type: ignore[assignment] from db import store as db_store from log import logger @@ -106,6 +109,13 @@ def __init__(self, parent=None) -> None: self.preview_check = QCheckBox("Mostrar pré-visualização antes de enviar") self.preview_check.setStyleSheet("color:#d0d0d0;") + self._preview_available = ImageQt is not None + if not self._preview_available: + self.preview_check.setChecked(False) + self.preview_check.setEnabled(False) + self.preview_check.setToolTip( + "Pré-visualização indisponível: instale Pillow para habilitar." + ) form_layout = QVBoxLayout() form_layout.setSpacing(10) @@ -270,10 +280,17 @@ def _on_print(self) -> None: QMessageBox.critical(self, "Erro", str(exc)) return - if self.preview_check.isChecked(): + if self.preview_check.isChecked() and self._preview_available: if not self._show_preview(template, variables): self._update_status("Pré-visualização cancelada", "#f6ad55") return + elif self.preview_check.isChecked() and not self._preview_available: + QMessageBox.warning( + self, + "Pré-visualização", + "Instale o pacote Pillow para habilitar a pré-visualização.", + ) + self.preview_check.setChecked(False) media_settings = resolve_media_settings(config, printer_profile=profile) @@ -460,6 +477,13 @@ def _on_reprint_last(self) -> None: self._update_status(message, color) def _show_preview(self, template, variables: Mapping[str, Any]) -> bool: + if ImageQt is None: + QMessageBox.warning( + self, + "Pré-visualização", + "Pré-visualização indisponível: instale Pillow.", + ) + return False context = dict(variables) items = context.pop("items", None) if isinstance(items, list) and items: diff --git a/render/preview.py b/render/preview.py index 346adcd..d83bd08 100644 --- a/render/preview.py +++ b/render/preview.py @@ -17,14 +17,20 @@ from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Iterable, Mapping +from typing import TYPE_CHECKING, Any, Iterable, Mapping try: # pragma: no cover - optional dependency during runtime import qrcode except Exception: # pragma: no cover - optional dependency missing qrcode = None # type: ignore[assignment] -from PIL import Image, ImageDraw, ImageFont +try: # pragma: no cover - optional dependency guard + from PIL import Image, ImageDraw, ImageFont # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + Image = ImageDraw = ImageFont = None # type: ignore[assignment] + +if TYPE_CHECKING: # pragma: no cover - typing aid + from PIL import Image as PILImage try: # pragma: no cover - optional dependency during runtime import barcode # type: ignore @@ -48,6 +54,14 @@ _MM_PER_INCH = 25.4 +def _require_pillow() -> None: + if Image is None or ImageDraw is None or ImageFont is None: + raise RuntimeError( + "Pillow must be installed to render previews. " + "Install the 'Pillow' package to enable this feature." + ) + + def _dots_per_mm(page: Mapping[str, Any]) -> float: dpi = page.get("dpi") if dpi: @@ -394,6 +408,7 @@ def render_element_image( ) -> tuple[Image.Image, int, int] | None: """Render an element to an image without applying rotation.""" + _require_pillow() calibration = calibration or Calibration() dots_per_mm = _dots_per_mm(page) width = _value_from_element( @@ -450,6 +465,7 @@ def render_element( ) -> tuple[Image.Image, tuple[int, int]] | None: """Render a single element and return the image plus placement.""" + _require_pillow() calibration = calibration or Calibration() base = render_element_image( element, @@ -504,6 +520,7 @@ class PreviewRenderer: height: int = field(init=False) def __post_init__(self) -> None: + _require_pillow() self.page = dict(self.page) if isinstance(self.metadata, Mapping): fonts_metadata = self.metadata.get("fonts") @@ -526,6 +543,7 @@ def render( elements: Iterable[Mapping[str, Any]], variables: Mapping[str, Any] | None = None, ) -> Image.Image: + _require_pillow() context = SafeDict(dict(variables or {})) image = Image.new("L", (self.width, self.height), color=255) for element in elements: @@ -553,6 +571,7 @@ def render_template( ) -> Image.Image: """Render ``template`` into a Pillow image.""" + _require_pillow() page = template.get("page", template) elements = template.get("elements", []) metadata = template.get("metadata", {}) diff --git a/tests/test_compile_backends.py b/tests/test_compile_backends.py index f56ced7..87b4dbf 100644 --- a/tests/test_compile_backends.py +++ b/tests/test_compile_backends.py @@ -1,7 +1,16 @@ from __future__ import annotations +import pytest + +pytest.importorskip("PIL") + from PIL import Image +try: + Image.new("1", (1, 1)) +except Exception: + pytest.skip("Pillow não está disponível", allow_module_level=True) + from compile import Calibration from compile.tspl import TSPLBackend from compile.zpl import ZPLBackend diff --git a/tests/test_export_labels.py b/tests/test_export_labels.py index 33f9267..1dd63c5 100644 --- a/tests/test_export_labels.py +++ b/tests/test_export_labels.py @@ -1,3 +1,14 @@ +import pytest + +pytest.importorskip("PIL") + +from PIL import Image + +try: + Image.new("1", (1, 1)) +except Exception: + pytest.skip("Pillow não está disponível", allow_module_level=True) + from model.template import TemplateDocument, TemplateVariable from printing import Template, export_labels diff --git a/tests/test_preview_render.py b/tests/test_preview_render.py index 23db402..eec0192 100644 --- a/tests/test_preview_render.py +++ b/tests/test_preview_render.py @@ -1,5 +1,16 @@ from hashlib import sha256 +import pytest + +pytest.importorskip("PIL") + +from PIL import Image + +try: + Image.new("1", (1, 1)) +except Exception: + pytest.skip("Pillow não está disponível", allow_module_level=True) + from model.template import TemplateDocument, TemplateVariable from printing import compile_template from render.preview import render_template diff --git a/tests/test_printing_new.py b/tests/test_printing_new.py index 4e47e5c..6902d30 100644 --- a/tests/test_printing_new.py +++ b/tests/test_printing_new.py @@ -2,6 +2,15 @@ import pytest +pytest.importorskip("PIL") + +from PIL import Image + +try: + Image.new("1", (1, 1)) +except Exception: + pytest.skip("Pillow não está disponível", allow_module_level=True) + from compile import Calibration from db import store as db_store from model.template import TemplateDocument, TemplateVariable diff --git a/tests/test_raster_cache.py b/tests/test_raster_cache.py index aa66a9d..7af5199 100644 --- a/tests/test_raster_cache.py +++ b/tests/test_raster_cache.py @@ -3,8 +3,16 @@ from pathlib import Path import pytest + +pytest.importorskip("PIL") + from PIL import Image +try: + Image.new("1", (1, 1)) +except Exception: + pytest.skip("Pillow não está disponível", allow_module_level=True) + from imaging.raster import clear_raster_cache, prepare_bitmap_for_tspl diff --git a/tests/test_utils.py b/tests/test_utils.py index c0ab119..5e94b2d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,8 +15,17 @@ import sys from datetime import datetime +import pytest + +pytest.importorskip("PIL") + from PIL import Image +try: + Image.new("1", (1, 1)) +except Exception: + pytest.skip("Pillow não está disponível", allow_module_level=True) + sys.path.append(os.path.dirname(os.path.dirname(__file__))) import utils diff --git a/utils/__init__.py b/utils/__init__.py index e0504cb..69a3e60 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -19,8 +19,15 @@ import sys from datetime import datetime from functools import lru_cache +from typing import TYPE_CHECKING -from PIL import Image +try: # pragma: no cover - optional dependency guard + from PIL import Image # type: ignore +except ImportError: # pragma: no cover - Pillow missing at runtime + Image = None # type: ignore[assignment] + +if TYPE_CHECKING: # pragma: no cover - typing aid + from PIL import Image as PILImage def _base_dir() -> str: @@ -75,6 +82,14 @@ def load_image(*args, **kwargs): return _load_image(*args, **kwargs) +def _require_pillow() -> None: + if Image is None: + raise RuntimeError( + "Pillow deve estar instalado para processar imagens. " + "Instale o pacote 'Pillow' para habilitar esta funcionalidade." + ) + + def normalize_text(texto: str, *, max_len: int) -> tuple[str, str | None]: """Remove espaços extras e caracteres inválidos de ``texto``. @@ -109,6 +124,7 @@ def melhorar_logo( tuple[bytes, int, int]: Dados do bitmap, largura em bytes e altura em pixels. """ + _require_pillow() logo = load_image(path_logo).convert("L") # escala de cinza proporcao = largura_desejada / logo.width nova_altura = int(round(logo.height * proporcao))