"""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