Skip to content

Commit cbeb91f

Browse files
authored
Merge pull request #25 from maresb/clean-up-barcode
Clean up barcodes
2 parents 4eb5452 + 93741f5 commit cbeb91f

File tree

3 files changed

+142
-111
lines changed

3 files changed

+142
-111
lines changed

src/labelle/lib/barcode_to_image.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# === LICENSE STATEMENT ===
2+
# Copyright (c) 2011 Sebastian J. Bronner <[email protected]>
3+
#
4+
# Copying and distribution of this file, with or without modification, are
5+
# permitted in any medium without royalty provided the copyright notice and
6+
# this notice are preserved.
7+
# === END LICENSE STATEMENT ===
8+
9+
from typing import List, Tuple, Union
10+
11+
from PIL import Image, ImageDraw
12+
13+
from labelle.lib.barcode_writer import BinaryString
14+
15+
16+
def _mm2px(mm: float, dpi: float = 25.4) -> float:
17+
return (mm * dpi) / 25.4
18+
19+
20+
def _list_of_runs(line: BinaryString) -> List[int]:
21+
# Pack line to list give better gfx result, otherwise in can
22+
# result in aliasing gaps
23+
# '11010111' -> [2, -1, 1, -1, 3]
24+
c = 1
25+
mlist = []
26+
for i in range(0, len(line)):
27+
if i + 1 < len(line) and line[i] == line[i + 1]:
28+
c += 1
29+
else:
30+
if line[i] == "1":
31+
mlist.append(c)
32+
else:
33+
mlist.append(-c)
34+
c = 1
35+
return mlist
36+
37+
38+
def _calculate_size(
39+
*,
40+
modules_per_line: int,
41+
quiet_zone: float,
42+
module_width: float,
43+
module_height: float,
44+
vertical_margin: float,
45+
dpi: float = 25.4,
46+
) -> Tuple[int, int]:
47+
width = 2 * quiet_zone + modules_per_line * module_width
48+
height = vertical_margin * 2 + module_height
49+
return int(_mm2px(width, dpi)), int(_mm2px(height, dpi))
50+
51+
52+
def convert_binary_string_to_barcode_image(
53+
line: BinaryString, quiet_zone: float, module_height: float
54+
) -> Image.Image:
55+
"""Render a barcode string into an image.
56+
57+
line: A string of 0s and 1s representing the barcode.
58+
"""
59+
module_width = 2
60+
vertical_margin = 8
61+
dpi = 25.4
62+
63+
width, height = _calculate_size(
64+
modules_per_line=len(line),
65+
dpi=dpi,
66+
quiet_zone=quiet_zone,
67+
module_width=module_width,
68+
module_height=module_height,
69+
vertical_margin=vertical_margin,
70+
)
71+
image = Image.new("1", (width, height), 0)
72+
draw = ImageDraw.Draw(image)
73+
74+
ypos = vertical_margin
75+
mlist = _list_of_runs(line)
76+
# Left quiet zone is x startposition
77+
xpos = quiet_zone
78+
for mod in mlist:
79+
if mod < 1:
80+
color = 0
81+
else:
82+
color = 1
83+
# remove painting for background colored tiles?
84+
_paint_module(
85+
xpos=xpos,
86+
ypos=ypos,
87+
width=module_width * abs(mod),
88+
color=color,
89+
dpi=dpi,
90+
module_height=module_height,
91+
draw=draw,
92+
)
93+
xpos += module_width * abs(mod)
94+
return image
95+
96+
97+
def _paint_module(
98+
*,
99+
xpos: float,
100+
ypos: float,
101+
width: float,
102+
color: Union[int, str],
103+
dpi: float,
104+
module_height: float,
105+
draw: ImageDraw.ImageDraw,
106+
) -> None:
107+
size = (
108+
(_mm2px(xpos, dpi), _mm2px(ypos, dpi)),
109+
(
110+
_mm2px(xpos + width, dpi),
111+
_mm2px(ypos + module_height, dpi),
112+
),
113+
)
114+
draw.rectangle(size, outline=color, fill=color)

src/labelle/lib/barcode_writer.py

Lines changed: 17 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -6,106 +6,29 @@
66
# this notice are preserved.
77
# === END LICENSE STATEMENT ===
88

9-
from typing import Optional
9+
from typing import List, NamedTuple, NewType
1010

1111
from barcode.writer import BaseWriter
12-
from PIL import Image, ImageDraw
1312

13+
BinaryString = NewType("BinaryString", str)
14+
"""A string that's been validated to contain only '0's and '1's."""
1415

15-
def mm2px(mm, dpi=25.4):
16-
return (mm * dpi) / 25.4
1716

17+
def _validate_string_as_binary(s: str) -> BinaryString:
18+
if not all(c in ("0", "1") for c in s):
19+
raise ValueError("Barcode can only contain 0 and 1")
20+
return BinaryString(s)
1821

19-
class BarcodeImageWriter(BaseWriter):
20-
_draw: Optional[ImageDraw.ImageDraw]
2122

22-
def __init__(self):
23-
super().__init__(self._init, self._paint_module, None, self._finish)
24-
self.format = "PNG"
25-
self.dpi = 25.4
26-
self._image = None
27-
self._draw = None
28-
self.vertical_margin = 0
23+
class BarcodeResult(NamedTuple):
24+
line: BinaryString
25+
quiet_zone: float
2926

30-
def calculate_size(self, modules_per_line, number_of_lines, dpi=25.4):
31-
width = 2 * self.quiet_zone + modules_per_line * self.module_width
32-
height = self.vertical_margin * 2 + self.module_height * number_of_lines
33-
return int(mm2px(width, dpi)), int(mm2px(height, dpi))
3427

35-
def render(self, code):
36-
"""Render the barcode.
37-
38-
Uses whichever inheriting writer is provided via the registered callbacks.
39-
40-
:parameters:
41-
code : List
42-
List of strings matching the writer spec
43-
(only contain 0 or 1).
44-
"""
45-
if self._callbacks["initialize"] is not None:
46-
self._callbacks["initialize"](code)
47-
ypos = self.vertical_margin
48-
for cc, line in enumerate(code):
49-
# Pack line to list give better gfx result, otherwise in can
50-
# result in aliasing gaps
51-
# '11010111' -> [2, -1, 1, -1, 3]
52-
line += " "
53-
c = 1
54-
mlist = []
55-
for i in range(0, len(line) - 1):
56-
if line[i] == line[i + 1]:
57-
c += 1
58-
else:
59-
if line[i] == "1":
60-
mlist.append(c)
61-
else:
62-
mlist.append(-c)
63-
c = 1
64-
# Left quiet zone is x startposition
65-
xpos = self.quiet_zone
66-
for mod in mlist:
67-
if mod < 1:
68-
color = self.background
69-
else:
70-
color = self.foreground
71-
# remove painting for background colored tiles?
72-
self._callbacks["paint_module"](
73-
xpos, ypos, self.module_width * abs(mod), color
74-
)
75-
xpos += self.module_width * abs(mod)
76-
# Add right quiet zone to every line, except last line,
77-
# quiet zone already provided with background,
78-
# should it be removed complety?
79-
if (cc + 1) != len(code):
80-
self._callbacks["paint_module"](
81-
xpos, ypos, self.quiet_zone, self.background
82-
)
83-
ypos += self.module_height
84-
return self._callbacks["finish"]()
85-
86-
def _init(self, code):
87-
size = self.calculate_size(len(code[0]), len(code), self.dpi)
88-
self._image = Image.new("1", size, self.background)
89-
self._draw = ImageDraw.Draw(self._image)
90-
91-
def _paint_module(self, xpos, ypos, width, color):
92-
size = (
93-
(mm2px(xpos, self.dpi), mm2px(ypos, self.dpi)),
94-
(
95-
mm2px(xpos + width, self.dpi),
96-
mm2px(ypos + self.module_height, self.dpi),
97-
),
98-
)
99-
assert self._draw is not None
100-
self._draw.rectangle(size, outline=color, fill=color)
101-
102-
def _finish(self):
103-
# although Image mode set to "1", draw function writes white as 255
104-
assert self._image is not None
105-
self._image = self._image.point(lambda x: 1 if x > 0 else 0, mode="1")
106-
return self._image
107-
108-
def save(self, filename, output):
109-
filename = f"{filename}.{self.format.lower()}"
110-
output.save(filename, self.format.upper())
111-
return filename
28+
class SimpleBarcodeWriter(BaseWriter):
29+
def render(self, code: List[str]) -> BarcodeResult:
30+
"""Extract the barcode string from the code and render it into an image."""
31+
if len(code) != 1:
32+
raise ValueError("Barcode expected to have only one line")
33+
line = _validate_string_as_binary(code[0])
34+
return BarcodeResult(line=line, quiet_zone=self.quiet_zone)

src/labelle/lib/render_engines/barcode.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
import barcode as barcode_module
44
from PIL import Image
55

6-
from labelle.lib.barcode_writer import BarcodeImageWriter
6+
from labelle.lib.barcode_to_image import convert_binary_string_to_barcode_image
7+
from labelle.lib.barcode_writer import SimpleBarcodeWriter
78
from labelle.lib.constants import DEFAULT_BARCODE_TYPE, BarcodeType
89
from labelle.lib.render_engines.render_context import RenderContext
9-
from labelle.lib.render_engines.render_engine import (
10-
RenderEngine,
11-
RenderEngineException,
12-
)
10+
from labelle.lib.render_engines.render_engine import RenderEngine, RenderEngineException
1311

1412
if DEFAULT_BARCODE_TYPE != BarcodeType.CODE128:
1513
# Ensure that we fail fast if the default barcode type is adjusted
@@ -44,19 +42,15 @@ def render(self, context: RenderContext) -> Image.Image:
4442
# in the GUI before the user entered a barcode.
4543
self.content = " "
4644
try:
47-
code = barcode_module.get(
48-
self.barcode_type, self.content, writer=BarcodeImageWriter()
49-
)
50-
bitmap = code.render(
51-
{
52-
"font_size": 0,
53-
"vertical_margin": 8,
54-
"module_height": context.height_px - 16,
55-
"module_width": 2,
56-
"background": "black",
57-
"foreground": "white",
58-
}
45+
code_obj = barcode_module.get(
46+
self.barcode_type, self.content, writer=SimpleBarcodeWriter()
5947
)
48+
result = code_obj.render()
6049
except BaseException as e:
6150
raise BarcodeRenderError from e
51+
bitmap = convert_binary_string_to_barcode_image(
52+
line=result.line,
53+
quiet_zone=result.quiet_zone,
54+
module_height=context.height_px - 16,
55+
)
6256
return bitmap

0 commit comments

Comments
 (0)