Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
19 changes: 17 additions & 2 deletions compile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
20 changes: 18 additions & 2 deletions compile/tspl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down
20 changes: 18 additions & 2 deletions compile/zpl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down
8 changes: 7 additions & 1 deletion editor/items/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
16 changes: 15 additions & 1 deletion editor/roll_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
24 changes: 23 additions & 1 deletion editor/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

# ------------------------------------------------------------------
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions imaging/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()

Expand Down
19 changes: 18 additions & 1 deletion imaging/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading