|
6 | 6 | # this notice are preserved.
|
7 | 7 | # === END LICENSE STATEMENT ===
|
8 | 8 |
|
9 |
| -from typing import Optional |
| 9 | +from typing import List, NamedTuple, NewType |
10 | 10 |
|
11 | 11 | from barcode.writer import BaseWriter
|
12 |
| -from PIL import Image, ImageDraw |
13 | 12 |
|
| 13 | +BinaryString = NewType("BinaryString", str) |
| 14 | +"""A string that's been validated to contain only '0's and '1's.""" |
14 | 15 |
|
15 |
| -def mm2px(mm, dpi=25.4): |
16 |
| - return (mm * dpi) / 25.4 |
17 | 16 |
|
| 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) |
18 | 21 |
|
19 |
| -class BarcodeImageWriter(BaseWriter): |
20 |
| - _draw: Optional[ImageDraw.ImageDraw] |
21 | 22 |
|
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 |
29 | 26 |
|
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)) |
34 | 27 |
|
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) |
0 commit comments