Source code for pystrich.ean13.renderer

"""Rendering code for EAN-13 barcode"""

from __future__ import annotations

from functools import reduce
from typing import TYPE_CHECKING, TypedDict

from PIL import Image, ImageDraw

from pystrich._vector_text import make_text_label
from pystrich.bar_renderer import Bar1DRenderer
from pystrich.fonts import get_font
from pystrich.marks import BarLayout, iter_bar_marks

if TYPE_CHECKING:
    from PIL.Image import Image as PILImage

# GS1 specifies an asymmetric quiet zone for EAN-13: 11 modules on the left
# and 7 on the right.
QUIET_LEFT_MODULES = 11
QUIET_RIGHT_MODULES = 7

# Long-standing pyStrich proportions: guards extend to 90% of the image
# height, data bars to 80%. The remaining 10% accommodates the digit
# baseline. Roughly approximates the GS1 nominal of guards extending 5
# modules below the data baseline.
GUARD_HEIGHT_FRACTION = 0.9
DATA_HEIGHT_FRACTION = 0.8

font_sizes = {1: 8, 2: 14, 3: 18, 4: 24}


[docs] class EAN13RenderOptions(TypedDict, total=False): """Optional render-time tweaks for EAN-13 barcodes. The label layout in EAN-13 is fixed by the standard, so this is a small set of cosmetic toggles. All keys are optional; omitted keys fall back to library defaults. .. versionadded:: 0.11 """ height: int """Total image height in pixels (= user units for SVG/EPS at default DPI). Defaults to half the symbol's pixel width. .. versionadded:: 0.12""" first_digit_y_offset: float """How far above the other text the first digit sits, as a fraction of image height. Defaults to ``0.1`` (the long-standing pyStrich look, where the leading number-system digit sits slightly higher than the two main digit groups). Set to ``0`` for a level baseline across all three groups."""
class EAN13Renderer(Bar1DRenderer): """Rendering class - given the code and corresponding bar encodings and guard bars, it will add edge zones and render to an image""" options: EAN13RenderOptions code: str left_bars: str right_bars: str guards: tuple[str, str, str] def __init__( self, code: str, left_bars: str, right_bars: str, guards: tuple[str, str, str], options: EAN13RenderOptions | None = None, ) -> None: super().__init__(options) self.code = code self.left_bars = left_bars self.right_bars = right_bars self.guards = guards @property def width(self) -> int: """Backwards-compatible alias for :attr:`image_width`. .. deprecated:: 0.12 Use :attr:`image_width` for parity with the other 1D renderers. """ return self.image_width @property def height(self) -> int: """Backwards-compatible alias for :attr:`image_height`. .. deprecated:: 0.12 Use :attr:`image_height` for parity with the other 1D renderers. """ return self.image_height def _bar_layout(self, bar_width: int) -> BarLayout: """Pixel-precise layout shared by PNG, SVG and EPS rendering.""" def sum_len(total: int, item: str) -> int: return total + len(item) num_bars = (7 * 12) + reduce(sum_len, self.guards, 0) left_quiet = bar_width * QUIET_LEFT_MODULES right_quiet = bar_width * QUIET_RIGHT_MODULES image_width = left_quiet + right_quiet + (num_bars * bar_width) image_height = self.options.get("height") or image_width // 2 symbol_top = (left_quiet + right_quiet) // 4 guard_pixel_height = int(image_height * GUARD_HEIGHT_FRACTION) - symbol_top data_pixel_height = int(image_height * DATA_HEIGHT_FRACTION) - symbol_top segments: list[tuple[str, int]] = [ (self.guards[0], guard_pixel_height), (self.left_bars, data_pixel_height), (self.guards[1], guard_pixel_height), (self.right_bars, data_pixel_height), (self.guards[2], guard_pixel_height), ] heights = [h if c == "1" else 0 for bars, h in segments for c in bars] # Space below tallest bars: where the digit baseline goes in PNG; # left blank in SVG/EPS so total canvas height matches. quiet_bottom = image_height - symbol_top - guard_pixel_height font_size = font_sizes.get(bar_width, 24) text_y = image_height * 0.8 first_digit_y = text_y - image_height * self.options.get("first_digit_y_offset", 0.1) labels = ( make_text_label(self.code[0], 1 * bar_width, first_digit_y, font_size), make_text_label(self.code[1:7], left_quiet + 7 * bar_width, text_y, font_size), make_text_label(self.code[7:], left_quiet + 54 * bar_width, text_y, font_size), ) return BarLayout( heights=heights, bar_width=bar_width, quiet_left=left_quiet, quiet_right=right_quiet, quiet_top=symbol_top, quiet_bottom=quiet_bottom, labels=labels, ) def get_pilimage(self, bar_width: int) -> PILImage: layout = self._bar_layout(bar_width) self.image_width = ( layout.quiet_left + len(layout.heights) * layout.bar_width + layout.quiet_right ) self.image_height = layout.quiet_top + max(layout.heights) + layout.quiet_bottom img = Image.new("L", (self.image_width, self.image_height), 255) draw = ImageDraw.Draw(img) for mark in iter_bar_marks( layout.heights, layout.bar_width, quiet_left=layout.quiet_left, quiet_top=layout.quiet_top, ): draw.rectangle( (mark.x, mark.y, mark.x + mark.width - 1, mark.y + mark.height - 1), fill=0, ) for label in layout.labels: font = get_font("courR", label.font_size) draw.text((label.x, int(label.y)), label.text, font=font) return img