labfreed 0.0.5__py3-none-any.whl → 0.2.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. labfreed/PAC_CAT/__init__.py +16 -0
  2. labfreed/PAC_CAT/category_base.py +51 -0
  3. labfreed/PAC_CAT/pac_cat.py +159 -0
  4. labfreed/PAC_CAT/predefined_categories.py +190 -0
  5. labfreed/PAC_ID/__init__.py +19 -0
  6. labfreed/PAC_ID/extension.py +48 -0
  7. labfreed/PAC_ID/id_segment.py +90 -0
  8. labfreed/PAC_ID/pac_id.py +140 -0
  9. labfreed/PAC_ID/url_parser.py +154 -0
  10. labfreed/PAC_ID/url_serializer.py +80 -0
  11. labfreed/PAC_ID_Resolver/__init__.py +2 -0
  12. labfreed/PAC_ID_Resolver/cit_v1.py +149 -0
  13. labfreed/PAC_ID_Resolver/cit_v2.py +303 -0
  14. labfreed/PAC_ID_Resolver/resolver.py +81 -0
  15. labfreed/PAC_ID_Resolver/services.py +80 -0
  16. labfreed/__init__.py +4 -1
  17. labfreed/labfreed_infrastructure.py +276 -0
  18. labfreed/qr/__init__.py +1 -0
  19. labfreed/qr/generate_qr.py +422 -0
  20. labfreed/trex/__init__.py +16 -0
  21. labfreed/trex/python_convenience/__init__.py +3 -0
  22. labfreed/trex/python_convenience/data_table.py +45 -0
  23. labfreed/trex/python_convenience/pyTREX.py +242 -0
  24. labfreed/trex/python_convenience/quantity.py +46 -0
  25. labfreed/trex/table_segment.py +227 -0
  26. labfreed/trex/trex.py +69 -0
  27. labfreed/trex/trex_base_models.py +336 -0
  28. labfreed/trex/value_segments.py +111 -0
  29. labfreed/{DisplayNameExtension → utilities}/base36.py +29 -13
  30. labfreed/well_known_extensions/__init__.py +5 -0
  31. labfreed/well_known_extensions/default_extension_interpreters.py +7 -0
  32. labfreed/well_known_extensions/display_name_extension.py +40 -0
  33. labfreed/well_known_extensions/trex_extension.py +31 -0
  34. labfreed/well_known_keys/gs1/__init__.py +6 -0
  35. labfreed/well_known_keys/gs1/gs1.py +4 -0
  36. labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +57 -0
  37. labfreed/{PAC_ID/well_known_segment_keys.py → well_known_keys/labfreed/well_known_keys.py} +1 -1
  38. labfreed/well_known_keys/unece/UneceUnits.json +33730 -0
  39. labfreed/well_known_keys/unece/__init__.py +4 -0
  40. labfreed/well_known_keys/unece/unece_units.py +68 -0
  41. labfreed-0.2.0b0.dist-info/METADATA +329 -0
  42. labfreed-0.2.0b0.dist-info/RECORD +44 -0
  43. {labfreed-0.0.5.dist-info → labfreed-0.2.0b0.dist-info}/WHEEL +1 -1
  44. labfreed/DisplayNameExtension/DisplayNameExtension.py +0 -34
  45. labfreed/PAC_CAT/data_model.py +0 -109
  46. labfreed/PAC_ID/data_model.py +0 -215
  47. labfreed/PAC_ID/parse.py +0 -142
  48. labfreed/PAC_ID/serialize.py +0 -60
  49. labfreed/TREXExtension/data_model.py +0 -239
  50. labfreed/TREXExtension/parse.py +0 -46
  51. labfreed/TREXExtension/uncertainty.py +0 -32
  52. labfreed/TREXExtension/unit_utilities.py +0 -143
  53. labfreed/validation.py +0 -71
  54. labfreed-0.0.5.dist-info/METADATA +0 -34
  55. labfreed-0.0.5.dist-info/RECORD +0 -19
  56. {labfreed-0.0.5.dist-info → labfreed-0.2.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,276 @@
1
+ from enum import Enum, auto
2
+ import logging
3
+ import re
4
+ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
5
+ from typing import Any, List, Set
6
+
7
+ from rich import print
8
+ from rich.table import Table
9
+
10
+ ''' Configure pdoc'''
11
+ __all__ = ["LabFREED_BaseModel", "ValidationMessage", "ValidationMsgLevel", "LabFREED_ValidationError"]
12
+
13
+ class PDOC_Workaround_Base(BaseModel):
14
+ '''@private
15
+ This class only exists to make pdoc work better with Pydantic models. It is set up such, that some things are not showing up in docu
16
+ '''
17
+ model_config = ConfigDict(extra="forbid")
18
+ """@private"""
19
+ def model_post_init(self, context: Any) -> None:
20
+ '''@private'''
21
+ super().model_post_init(context)
22
+
23
+
24
+
25
+
26
+
27
+ class ValidationMsgLevel(Enum):
28
+ '''
29
+ Level of validation messages
30
+ '''
31
+ ERROR = auto()
32
+ '''Model is **invalid**'''
33
+ WARNING = auto()
34
+ '''Model is **valid**, but has issues'''
35
+ RECOMMENDATION = auto()
36
+ '''Model is **valid**, but recommendations apply'''
37
+ INFO = auto()
38
+ '''Model is **valid**. Something of interest was detected, which is not a recommendation.'''
39
+
40
+ class ValidationMessage(PDOC_Workaround_Base):
41
+ '''
42
+ Represents one problem in the model
43
+ '''
44
+ source_id:int
45
+ source:str
46
+ level: ValidationMsgLevel
47
+ msg:str
48
+ highlight:str = "" #this can be used to highlight problematic parts
49
+ highlight_sub_patterns:list[str] = Field(default_factory=list)
50
+
51
+ @field_validator('highlight_sub_patterns', mode='before')
52
+ @classmethod
53
+ def _ensure_list(cls, v):
54
+ if isinstance(v, str):
55
+ return [v]
56
+ return v
57
+
58
+
59
+
60
+ class LabFREED_ValidationError(ValueError):
61
+ '''Error which is raised, when LabFREED validation fails, i.e. when the model contains at least one error.'''
62
+
63
+ def __init__(self, message=None, validation_msgs=None):
64
+ '''@private'''
65
+ super().__init__(message)
66
+ self._validation_msgs = validation_msgs
67
+
68
+ @property
69
+ def validation_msgs(self):
70
+ ''' The validation messages (errors, recommendations, info) present in the invalid model'''
71
+ return self._validation_msgs
72
+
73
+
74
+
75
+
76
+ class LabFREED_BaseModel(PDOC_Workaround_Base):
77
+ """ Extension of Pydantic BaseModel, so that validator can issue warnings.
78
+ The purpose of that is to allow only minimal validation but on top check for stricter recommendations"""
79
+
80
+ _validation_messages: list[ValidationMessage] = PrivateAttr(default_factory=list)
81
+ """Validation messages for this model"""
82
+
83
+ @property
84
+ def is_valid(self) -> bool:
85
+ return len(self.errors()) == 0
86
+
87
+
88
+ def validation_messages(self, nested=True) -> list[ValidationMessage]:
89
+ if nested:
90
+ msgs = self._get_nested_validation_messages()
91
+ else:
92
+ msgs = self._validation_messages
93
+ return msgs
94
+
95
+ def errors(self, nested=True) -> list[ValidationMessage]:
96
+ return _filter_errors(self.validation_messages(nested=nested))
97
+
98
+ def warnings(self, nested=True) -> list[ValidationMessage]:
99
+ return _filter_warnings(self.validation_messages(nested=nested))
100
+
101
+
102
+ def _add_validation_message(self, *, msg: str, level:ValidationMsgLevel, source:str="", highlight_pattern="", highlight_sub=None):
103
+ if not highlight_sub:
104
+ highlight_sub = []
105
+ w = ValidationMessage(msg=msg, source=source, level=level, highlight=highlight_pattern, highlight_sub_patterns=highlight_sub, source_id=id(self))
106
+
107
+ if w not in self._validation_messages:
108
+ self._validation_messages.append(w)
109
+
110
+ # Function to extract warnings from a model and its nested models
111
+ def _get_nested_validation_messages(self, parent_name: str = "", visited: Set[int] = None) -> List['ValidationMessage']:
112
+ """
113
+ Recursively extract warnings from a Pydantic model and its nested fields, including computed fields.
114
+
115
+ :param parent_name: The name of the parent model to track the path.
116
+ :param visited: Set of visited object IDs to prevent infinite loops.
117
+ :return: List of ValidationMessages from this and nested models.
118
+ """
119
+ if visited is None:
120
+ visited = set()
121
+
122
+ model_id = id(self)
123
+ if model_id in visited:
124
+ return []
125
+ visited.add(model_id)
126
+
127
+ warnings_list = [warning for warning in self.validation_messages(nested=False)]
128
+
129
+ # Traverse regular fields
130
+ for field_name, field in self.__fields__.items():
131
+ full_path = f"{parent_name}.{field_name}" if parent_name else field_name
132
+ value = getattr(self, field_name)
133
+
134
+ if isinstance(value, LabFREED_BaseModel):
135
+ warnings_list.extend(value._get_nested_validation_messages(full_path, visited))
136
+ elif isinstance(value, list):
137
+ for index, item in enumerate(value):
138
+ if isinstance(item, LabFREED_BaseModel):
139
+ list_path = f"{full_path}[{index}]"
140
+ warnings_list.extend(item._get_nested_validation_messages(list_path, visited))
141
+
142
+ # Traverse computed fields
143
+ computed_fields = getattr(self, '__pydantic_decorators__', {}).computed_fields or {}
144
+ for field_name in computed_fields:
145
+ full_path = f"{parent_name}.{field_name}" if parent_name else field_name
146
+ try:
147
+ value = getattr(self, field_name)
148
+ except Exception:
149
+ continue # Safely skip computed properties that raise errors
150
+
151
+ if isinstance(value, LabFREED_BaseModel):
152
+ warnings_list.extend(value._get_nested_validation_messages(full_path, visited))
153
+ elif isinstance(value, list):
154
+ for index, item in enumerate(value):
155
+ if isinstance(item, LabFREED_BaseModel):
156
+ list_path = f"{full_path}[{index}]"
157
+ warnings_list.extend(item._get_nested_validation_messages(list_path, visited))
158
+
159
+ return warnings_list
160
+
161
+
162
+
163
+ def _emphasize_in(self, validation_msg, validation_node_str:str, fmt, color='black'):
164
+ if validation_msg.highlight_sub_patterns:
165
+ replacements = validation_msg.highlight_sub_patterns
166
+ else:
167
+ replacements = [validation_msg.highlight]
168
+ # Sort patterns by length descending to avoid subpattern clobbering
169
+ sorted_patterns = sorted(replacements, key=len, reverse=True)
170
+ # Escape the patterns for regex safety
171
+ escaped_patterns = [re.escape(p) for p in sorted_patterns]
172
+ # Create one regex pattern with alternation (longest first)
173
+ pattern = re.compile("|".join(escaped_patterns))
174
+
175
+ out = pattern.sub(lambda m: fmt(m.group(0)), validation_node_str)
176
+ return out
177
+
178
+
179
+ def print_validation_messages(self, target='console'):
180
+ msgs = self._get_nested_validation_messages()
181
+
182
+ table = Table(title="Validation Results", show_header=False, title_justify='left')
183
+
184
+ def col(s):
185
+ return table.add_column(s, vertical='top')
186
+ col("-")
187
+
188
+
189
+ if not msgs:
190
+ table.add_row('All clear!', end_section=True)
191
+ return
192
+
193
+ for m in msgs:
194
+ if m.level == ValidationMsgLevel.ERROR:
195
+ color = 'red'
196
+ else:
197
+ color = 'yellow'
198
+
199
+ match target:
200
+ case 'markdown':
201
+ fmt = lambda s: f'👉{s}👈' # noqa: E731
202
+ case 'console':
203
+ fmt = lambda s: f'[{color} bold]{s}[/{color} bold]' # noqa: E731
204
+ case 'html':
205
+ fmt = lambda s: f'<span class="val_{color}">{s}</span>' # noqa: E731
206
+ case 'html_styled':
207
+ fmt = lambda s: f'<b style="color:{color}>{s}</b>' # noqa: E731
208
+
209
+ serialized = str(self)
210
+ emphazised_highlight = self._emphasize_in(m, serialized, fmt=fmt, color=color)
211
+ emphazised_highlight = emphazised_highlight.replace('👈👉','') # removes two consecutive markers, to make it cleaner
212
+
213
+ txt = f'[bold {color}]{m.level.name} [/bold {color}] in {m.source}'
214
+ txt += '\n' + f'{m.msg}'
215
+ txt += '\n\n' + emphazised_highlight
216
+
217
+ table.add_row( txt)
218
+ table.add_section()
219
+
220
+ logging.info(table)
221
+ print(table)
222
+
223
+
224
+
225
+
226
+
227
+ # def print_validation_messages_(self, str_to_highlight_in=None, target='console'):
228
+ # if not str_to_highlight_in:
229
+ # str_to_highlight_in = str(self)
230
+ # msgs = self.get_nested_validation_messages()
231
+ # print('\n'.join(['\n',
232
+ # '=======================================',
233
+ # 'Validation Results',
234
+ # '---------------------------------------'
235
+ # ]
236
+ # )
237
+ # )
238
+
239
+ # if not msgs:
240
+ # print('All clear!')
241
+ # return
242
+
243
+ # for m in msgs:
244
+ # if m.level.casefold() == "error":
245
+ # color = 'red'
246
+ # else:
247
+ # color = 'yellow'
248
+
249
+ # text = Text.from_markup(f'\n [bold {color}]{m.level} [/bold {color}] in \t {m.source}' )
250
+ # print(text)
251
+ # match target:
252
+ # case 'markdown':
253
+ # formatted_highlight = m.emphazised_highlight.replace('emph', f'🔸').replace('[/', '').replace('[', '').replace(']', '')
254
+ # case 'console':
255
+ # formatted_highlight = m.emphazised_highlight.replace('emph', f'bold {color}')
256
+ # case 'html':
257
+ # formatted_highlight = m.emphazised_highlight.replace('emph', f'b').replace('[', '<').replace(']', '>')
258
+ # fmtd = str_to_highlight_in.replace(m.highlight, formatted_highlight)
259
+ # fmtd = Text.from_markup(fmtd)
260
+ # print(fmtd)
261
+ # print(Text.from_markup(f'{m.problem_msg}'))
262
+
263
+
264
+
265
+
266
+
267
+ def _filter_errors(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
268
+ return [ m for m in val_msg if m.level == ValidationMsgLevel.ERROR ]
269
+
270
+ def _filter_warnings(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
271
+ return [ m for m in val_msg if m.level != ValidationMsgLevel.ERROR ]
272
+
273
+ def _quote_texts(texts:list[str]):
274
+ return ','.join([f"'{t}'" for t in texts])
275
+
276
+
@@ -0,0 +1 @@
1
+ from .generate_qr import save_qr_with_markers # noqa: F401
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env python
2
+ import typer
3
+ from rich import print
4
+ from typing_extensions import Annotated
5
+ import numpy as np
6
+ import segno
7
+ from segno import DataOverflowError, writers
8
+ from typing import List
9
+ from enum import Enum
10
+
11
+
12
+
13
+ class Direction(str, Enum):
14
+ LEFT_TO_RIGHT = "LTR"
15
+ TOP_TO_BOTTOM = "TTB"
16
+ RIGHT_TO_LEFT = "RTL"
17
+
18
+ # 5x5 block of bits that can be used to place decoration next to barcode.
19
+ class VisualMarker:
20
+ size = 5
21
+
22
+ def __init__(self, bits):
23
+ # true means 1 bit, false means 0 bit (eg: 1 means black, 0 means white)
24
+ self.bits = bits
25
+
26
+ marker_dict = {
27
+ 'A': bytearray([
28
+ 0x00, 0x01, 0x01, 0x01, 0x00, # preformatted
29
+ 0x01, 0x00, 0x00, 0x00, 0x01,
30
+ 0x01, 0x01, 0x01, 0x01, 0x01,
31
+ 0x01, 0x00, 0x00, 0x00, 0x01,
32
+ 0x01, 0x00, 0x00, 0x00, 0x01,
33
+ ]),
34
+ 'B': bytearray([
35
+ 0x01, 0x01, 0x01, 0x01, 0x00, # preformatted
36
+ 0x01, 0x00, 0x00, 0x00, 0x01,
37
+ 0x01, 0x01, 0x01, 0x01, 0x00,
38
+ 0x01, 0x00, 0x00, 0x00, 0x01,
39
+ 0x01, 0x01, 0x01, 0x01, 0x00,
40
+ ]),
41
+ 'C': bytearray([
42
+ 0x00, 0x01, 0x01, 0x01, 0x01, # preformatted
43
+ 0x01, 0x00, 0x00, 0x00, 0x00,
44
+ 0x01, 0x00, 0x00, 0x00, 0x00,
45
+ 0x01, 0x00, 0x00, 0x00, 0x00,
46
+ 0x00, 0x01, 0x01, 0x01, 0x01,
47
+ ]),
48
+ 'D': bytearray([
49
+ 0x01, 0x01, 0x01, 0x01, 0x00, # preformatted
50
+ 0x01, 0x00, 0x00, 0x00, 0x01,
51
+ 0x01, 0x00, 0x00, 0x00, 0x01,
52
+ 0x01, 0x00, 0x00, 0x00, 0x01,
53
+ 0x01, 0x01, 0x01, 0x01, 0x00,
54
+ ]),
55
+ 'E': bytearray([
56
+ 0x01, 0x01, 0x01, 0x01, 0x01, # preformatted
57
+ 0x01, 0x00, 0x00, 0x00, 0x00,
58
+ 0x01, 0x01, 0x01, 0x01, 0x01,
59
+ 0x01, 0x00, 0x00, 0x00, 0x00,
60
+ 0x01, 0x01, 0x01, 0x01, 0x01,
61
+ ]),
62
+ 'F': bytearray([
63
+ 0x01, 0x01, 0x01, 0x01, 0x01, # preformatted
64
+ 0x01, 0x00, 0x00, 0x00, 0x00,
65
+ 0x01, 0x01, 0x01, 0x01, 0x01,
66
+ 0x01, 0x00, 0x00, 0x00, 0x00,
67
+ 0x01, 0x00, 0x00, 0x00, 0x00,
68
+ ]),
69
+ 'G': bytearray([
70
+ 0x00, 0x01, 0x01, 0x01, 0x01, # preformatted
71
+ 0x01, 0x00, 0x00, 0x00, 0x00,
72
+ 0x01, 0x00, 0x01, 0x01, 0x00,
73
+ 0x01, 0x00, 0x00, 0x00, 0x01,
74
+ 0x00, 0x01, 0x01, 0x01, 0x01,
75
+ ]),
76
+ 'H': bytearray([
77
+ 0x01, 0x00, 0x00, 0x00, 0x01, # preformatted
78
+ 0x01, 0x00, 0x00, 0x00, 0x01,
79
+ 0x01, 0x01, 0x01, 0x01, 0x01,
80
+ 0x01, 0x00, 0x00, 0x00, 0x01,
81
+ 0x01, 0x00, 0x00, 0x00, 0x01,
82
+ ]),
83
+ 'I': bytearray([
84
+ 0x00, 0x00, 0x01, 0x00, 0x00, # preformatted
85
+ 0x00, 0x00, 0x01, 0x00, 0x00,
86
+ 0x00, 0x00, 0x01, 0x00, 0x00,
87
+ 0x00, 0x00, 0x01, 0x00, 0x00,
88
+ 0x00, 0x00, 0x01, 0x00, 0x00,
89
+ ]),
90
+ 'J': bytearray([
91
+ 0x00, 0x00, 0x01, 0x00, 0x00, # preformatted
92
+ 0x00, 0x00, 0x01, 0x00, 0x00,
93
+ 0x00, 0x00, 0x01, 0x00, 0x00,
94
+ 0x00, 0x00, 0x01, 0x00, 0x00,
95
+ 0x00, 0x01, 0x00, 0x00, 0x00,
96
+ ]),
97
+ 'K': bytearray([
98
+ 0x01, 0x00, 0x00, 0x00, 0x01, # preformatted
99
+ 0x01, 0x00, 0x00, 0x01, 0x00,
100
+ 0x01, 0x01, 0x01, 0x00, 0x00,
101
+ 0x01, 0x00, 0x00, 0x01, 0x00,
102
+ 0x01, 0x00, 0x00, 0x00, 0x01,
103
+ ]),
104
+ 'L': bytearray([
105
+ 0x01, 0x00, 0x00, 0x00, 0x00, # preformatted
106
+ 0x01, 0x00, 0x00, 0x00, 0x00,
107
+ 0x01, 0x00, 0x00, 0x00, 0x00,
108
+ 0x01, 0x00, 0x00, 0x00, 0x00,
109
+ 0x01, 0x01, 0x01, 0x01, 0x01,
110
+ ]),
111
+ 'M': bytearray([
112
+ 0x01, 0x01, 0x00, 0x01, 0x01, # preformatted
113
+ 0x01, 0x00, 0x01, 0x00, 0x01,
114
+ 0x01, 0x00, 0x00, 0x00, 0x01,
115
+ 0x01, 0x00, 0x00, 0x00, 0x01,
116
+ 0x01, 0x00, 0x00, 0x00, 0x01,
117
+ ]),
118
+ 'N': bytearray([
119
+ 0x01, 0x00, 0x00, 0x00, 0x01, # preformatted
120
+ 0x01, 0x01, 0x00, 0x00, 0x01,
121
+ 0x01, 0x00, 0x01, 0x00, 0x01,
122
+ 0x01, 0x00, 0x00, 0x01, 0x01,
123
+ 0x01, 0x00, 0x00, 0x00, 0x01,
124
+ ]),
125
+ 'O': bytearray([
126
+ 0x00, 0x01, 0x01, 0x01, 0x00, # preformatted
127
+ 0x01, 0x00, 0x00, 0x00, 0x01,
128
+ 0x01, 0x00, 0x00, 0x00, 0x01,
129
+ 0x01, 0x00, 0x00, 0x00, 0x01,
130
+ 0x00, 0x01, 0x01, 0x01, 0x00,
131
+ ]),
132
+ 'P': bytearray([
133
+ 0x01, 0x01, 0x01, 0x01, 0x00, # preformatted
134
+ 0x01, 0x00, 0x00, 0x00, 0x01,
135
+ 0x01, 0x01, 0x01, 0x01, 0x00,
136
+ 0x01, 0x00, 0x00, 0x00, 0x00,
137
+ 0x01, 0x00, 0x00, 0x00, 0x00,
138
+ ]),
139
+ 'Q': bytearray([
140
+ 0x00, 0x01, 0x01, 0x01, 0x00, # preformatted
141
+ 0x01, 0x00, 0x00, 0x00, 0x01,
142
+ 0x01, 0x00, 0x01, 0x00, 0x01,
143
+ 0x01, 0x00, 0x00, 0x01, 0x00,
144
+ 0x00, 0x01, 0x01, 0x00, 0x01,
145
+ ]),
146
+ 'R': bytearray([
147
+ 0x01, 0x01, 0x01, 0x01, 0x00, # preformatted
148
+ 0x01, 0x00, 0x00, 0x01, 0x00,
149
+ 0x01, 0x01, 0x01, 0x00, 0x00,
150
+ 0x01, 0x00, 0x00, 0x01, 0x00,
151
+ 0x01, 0x00, 0x00, 0x00, 0x01,
152
+ ]),
153
+ 'S': bytearray([
154
+ 0x00, 0x01, 0x01, 0x01, 0x01, # preformatted
155
+ 0x01, 0x00, 0x00, 0x00, 0x00,
156
+ 0x00, 0x01, 0x01, 0x01, 0x00,
157
+ 0x00, 0x00, 0x00, 0x00, 0x01,
158
+ 0x01, 0x01, 0x01, 0x01, 0x00,
159
+ ]),
160
+ 'T': bytearray([
161
+ 0x01, 0x01, 0x01, 0x01, 0x01, # preformatted
162
+ 0x00, 0x00, 0x01, 0x00, 0x00,
163
+ 0x00, 0x00, 0x01, 0x00, 0x00,
164
+ 0x00, 0x00, 0x01, 0x00, 0x00,
165
+ 0x00, 0x00, 0x01, 0x00, 0x00,
166
+ ]),
167
+ 'U': bytearray([
168
+ 0x01, 0x00, 0x00, 0x00, 0x01, # preformatted
169
+ 0x01, 0x00, 0x00, 0x00, 0x01,
170
+ 0x01, 0x00, 0x00, 0x00, 0x01,
171
+ 0x01, 0x00, 0x00, 0x00, 0x01,
172
+ 0x00, 0x01, 0x01, 0x01, 0x00,
173
+ ]),
174
+ 'V': bytearray([
175
+ 0x01, 0x00, 0x00, 0x00, 0x01, # preformatted
176
+ 0x01, 0x00, 0x00, 0x00, 0x01,
177
+ 0x00, 0x01, 0x00, 0x01, 0x00,
178
+ 0x00, 0x01, 0x01, 0x01, 0x00,
179
+ 0x00, 0x00, 0x01, 0x00, 0x00,
180
+ ]),
181
+ 'W': bytearray([
182
+ 0x01, 0x00, 0x00, 0x00, 0x01, # preformatted
183
+ 0x01, 0x00, 0x01, 0x00, 0x01,
184
+ 0x01, 0x00, 0x01, 0x00, 0x01,
185
+ 0x00, 0x01, 0x01, 0x01, 0x00,
186
+ 0x00, 0x01, 0x00, 0x01, 0x00,
187
+ ]),
188
+ 'X': bytearray([
189
+ 0x01, 0x00, 0x00, 0x00, 0x01, # preformatted
190
+ 0x00, 0x01, 0x00, 0x01, 0x00,
191
+ 0x00, 0x00, 0x01, 0x00, 0x00,
192
+ 0x00, 0x01, 0x00, 0x01, 0x00,
193
+ 0x01, 0x00, 0x00, 0x00, 0x01,
194
+ ]),
195
+ 'Y': bytearray([
196
+ 0x01, 0x00, 0x00, 0x00, 0x01, # preformatted
197
+ 0x01, 0x00, 0x00, 0x00, 0x01,
198
+ 0x00, 0x01, 0x01, 0x01, 0x00,
199
+ 0x00, 0x00, 0x01, 0x00, 0x00,
200
+ 0x00, 0x00, 0x01, 0x00, 0x00,
201
+ ]),
202
+ 'Z': bytearray([
203
+ 0x01, 0x01, 0x01, 0x01, 0x01, # preformatted
204
+ 0x00, 0x00, 0x00, 0x01, 0x00,
205
+ 0x00, 0x00, 0x01, 0x00, 0x00,
206
+ 0x00, 0x01, 0x00, 0x00, 0x00,
207
+ 0x01, 0x01, 0x01, 0x01, 0x01,
208
+ ]),
209
+ '0': bytearray([
210
+ 0x00, 0x00, 0x01, 0x00, 0x00, # preformatted
211
+ 0x00, 0x01, 0x00, 0x01, 0x00,
212
+ 0x00, 0x01, 0x00, 0x01, 0x00,
213
+ 0x00, 0x01, 0x00, 0x01, 0x00,
214
+ 0x00, 0x00, 0x01, 0x00, 0x00,
215
+ ]),
216
+ '1': bytearray([
217
+ 0x00, 0x00, 0x01, 0x00, 0x00, # preformatted
218
+ 0x00, 0x01, 0x01, 0x00, 0x00,
219
+ 0x01, 0x00, 0x01, 0x00, 0x00,
220
+ 0x00, 0x00, 0x01, 0x00, 0x00,
221
+ 0x00, 0x00, 0x01, 0x00, 0x00,
222
+ ]),
223
+ '2': bytearray([
224
+ 0x00, 0x01, 0x01, 0x01, 0x00, # preformatted
225
+ 0x01, 0x00, 0x00, 0x00, 0x01,
226
+ 0x00, 0x00, 0x01, 0x01, 0x00,
227
+ 0x00, 0x01, 0x00, 0x00, 0x00,
228
+ 0x01, 0x01, 0x01, 0x01, 0x01,
229
+ ]),
230
+ '3': bytearray([
231
+ 0x01, 0x01, 0x01, 0x01, 0x00, # preformatted
232
+ 0x00, 0x00, 0x00, 0x00, 0x01,
233
+ 0x01, 0x01, 0x01, 0x01, 0x00,
234
+ 0x00, 0x00, 0x00, 0x00, 0x01,
235
+ 0x01, 0x01, 0x01, 0x01, 0x00,
236
+ ]),
237
+ '4': bytearray([
238
+ 0x01, 0x00, 0x00, 0x00, 0x00, # preformatted
239
+ 0x01, 0x00, 0x00, 0x01, 0x00,
240
+ 0x01, 0x01, 0x01, 0x01, 0x01,
241
+ 0x00, 0x00, 0x00, 0x01, 0x00,
242
+ 0x00, 0x00, 0x00, 0x01, 0x00,
243
+ ]),
244
+ '5': bytearray([
245
+ 0x01, 0x01, 0x01, 0x01, 0x01, # preformatted
246
+ 0x01, 0x00, 0x00, 0x00, 0x00,
247
+ 0x01, 0x01, 0x01, 0x01, 0x00,
248
+ 0x00, 0x00, 0x00, 0x00, 0x01,
249
+ 0x01, 0x01, 0x01, 0x01, 0x00,
250
+ ]),
251
+ '6': bytearray([
252
+ 0x00, 0x01, 0x01, 0x01, 0x00, # preformatted
253
+ 0x01, 0x00, 0x00, 0x00, 0x00,
254
+ 0x01, 0x01, 0x01, 0x01, 0x00,
255
+ 0x01, 0x00, 0x00, 0x00, 0x01,
256
+ 0x00, 0x01, 0x01, 0x01, 0x00,
257
+ ]),
258
+ '7': bytearray([
259
+ 0x01, 0x01, 0x01, 0x01, 0x01, # preformatted
260
+ 0x00, 0x00, 0x00, 0x01, 0x00,
261
+ 0x00, 0x00, 0x01, 0x00, 0x00,
262
+ 0x00, 0x01, 0x00, 0x00, 0x00,
263
+ 0x01, 0x00, 0x00, 0x00, 0x00,
264
+ ]),
265
+ '8': bytearray([
266
+ 0x00, 0x01, 0x01, 0x01, 0x00, # preformatted
267
+ 0x01, 0x00, 0x00, 0x00, 0x01,
268
+ 0x00, 0x01, 0x01, 0x01, 0x00,
269
+ 0x01, 0x00, 0x00, 0x00, 0x01,
270
+ 0x00, 0x01, 0x01, 0x01, 0x00,
271
+ ]),
272
+ '9': bytearray([
273
+ 0x00, 0x01, 0x01, 0x01, 0x00, # preformatted
274
+ 0x01, 0x00, 0x00, 0x00, 0x01,
275
+ 0x00, 0x01, 0x01, 0x01, 0x01,
276
+ 0x00, 0x00, 0x00, 0x00, 0x01,
277
+ 0x00, 0x01, 0x01, 0x01, 0x00,
278
+ ]),
279
+ '.': bytearray([
280
+ 0x00, 0x00, 0x00, 0x00, 0x00, # preformatted
281
+ 0x00, 0x00, 0x00, 0x00, 0x00,
282
+ 0x00, 0x00, 0x01, 0x00, 0x00,
283
+ 0x00, 0x00, 0x00, 0x00, 0x00,
284
+ 0x00, 0x00, 0x00, 0x00, 0x00,
285
+ ]),
286
+ ',': bytearray([
287
+ 0x00, 0x00, 0x00, 0x00, 0x00, # preformatted
288
+ 0x00, 0x00, 0x00, 0x00, 0x00,
289
+ 0x00, 0x00, 0x00, 0x00, 0x00,
290
+ 0x00, 0x00, 0x01, 0x00, 0x00,
291
+ 0x00, 0x00, 0x01, 0x00, 0x00,
292
+ ]),
293
+ ':': bytearray([
294
+ 0x00, 0x00, 0x00, 0x00, 0x00, # preformatted
295
+ 0x00, 0x00, 0x01, 0x00, 0x00,
296
+ 0x00, 0x00, 0x00, 0x00, 0x00,
297
+ 0x00, 0x00, 0x00, 0x00, 0x00,
298
+ 0x00, 0x00, 0x01, 0x00, 0x00,
299
+ ]),
300
+ ';': bytearray([
301
+ 0x00, 0x00, 0x00, 0x00, 0x00, # preformatted
302
+ 0x00, 0x00, 0x01, 0x00, 0x00,
303
+ 0x00, 0x00, 0x00, 0x00, 0x00,
304
+ 0x00, 0x00, 0x01, 0x00, 0x00,
305
+ 0x00, 0x00, 0x01, 0x00, 0x00,
306
+ ]),
307
+ '-': bytearray([
308
+ 0x00, 0x00, 0x00, 0x00, 0x00, # preformatted
309
+ 0x00, 0x00, 0x00, 0x00, 0x00,
310
+ 0x00, 0x01, 0x01, 0x01, 0x00,
311
+ 0x00, 0x00, 0x00, 0x00, 0x00,
312
+ 0x00, 0x00, 0x00, 0x00, 0x00,
313
+ ])
314
+ }
315
+
316
+ def generate_from_text(chars: str, direction:Direction, size_in_modules: int):
317
+ # fill up with "."
318
+ squares_cnt = (size_in_modules + 1) // (VisualMarker.size+1)
319
+ leftover = (size_in_modules + 1) % (VisualMarker.size+1)
320
+ if len(chars) < squares_cnt:
321
+ chars = chars + '.' * (squares_cnt - len(chars))
322
+ result: List[bytearray] = []
323
+ for char in chars[:squares_cnt]:
324
+ if char in VisualMarker.marker_dict:
325
+ marker = VisualMarker.marker_dict[char]
326
+ else:
327
+ marker = VisualMarker.marker_dict['.']
328
+ result.append(marker)
329
+ return VisualMarker.generate_from_squares(result, direction, leftover)
330
+
331
+ def generate_from_squares(marker_squares: List[bytearray], direction:Direction, padding_at_end: int):
332
+ # spacer is a column of zeros if RTL/LTR, or a row if TTB/BTT
333
+ axis = 0 if direction == Direction.TOP_TO_BOTTOM else 1
334
+ spacer_dim = (1, VisualMarker.size) if axis==0 else (VisualMarker.size, 1)
335
+ spacer = np.zeros(spacer_dim, dtype=np.uint8)
336
+ result = np.array([])
337
+ for marker in marker_squares:
338
+ if len(marker) != VisualMarker.size*VisualMarker.size:
339
+ raise ValueError("All markers must be 5x5")
340
+
341
+ np_marker = np.array(marker).reshape(VisualMarker.size, VisualMarker.size)
342
+ if result.size == 0:
343
+ # the first item doesn't need a spacer
344
+ result = np_marker
345
+ else:
346
+ # append in direction specified
347
+ if direction == Direction.RIGHT_TO_LEFT:
348
+ result = np.concatenate((np_marker, spacer, result), axis=1)
349
+ else:
350
+ result = np.concatenate((result, spacer, np_marker), axis=axis)
351
+ if(padding_at_end > 0):
352
+ padding_dim = (padding_at_end, VisualMarker.size) if axis==0 else (VisualMarker.size, padding_at_end)
353
+ padding= np.zeros(padding_dim, dtype=np.uint8)
354
+
355
+ # append in direction specified
356
+ if direction == Direction.RIGHT_TO_LEFT:
357
+ result = np.concatenate((padding, result), axis=1)
358
+ else:
359
+ result = np.concatenate((result, padding), axis=axis)
360
+ return result
361
+
362
+
363
+ app = typer.Typer()
364
+
365
+
366
+ def _generate_qr_with_markers(qr_str, text, title, direction):
367
+ if title:
368
+ #try to use standard size 10. Go bigger if 10 does not fit the data
369
+ try:
370
+ qr = segno.make_qr(qr_str, error="L", version=10)
371
+ except DataOverflowError:
372
+ qr = segno.make_qr(qr_str, error="L")
373
+ else:
374
+ qr = segno.make_qr(qr_str, error="L")
375
+
376
+ if(qr.mode != "alphanumeric"):
377
+ print("[bold yellow]Large QR:[/bold yellow] Provided URL is not alphanumeric!")
378
+ block_count = len(qr.matrix)
379
+ print(f"[bold]Size:[/bold] {block_count}")
380
+ print(f"[bold]Version:[/bold] {qr.version}")
381
+ print(f"[bold]Error Level:[/bold] {qr.error}")
382
+
383
+
384
+ qr_matrix = np.array(qr.matrix)
385
+ visual_marker = VisualMarker.generate_from_text(text.upper(), direction, block_count)
386
+ if title:
387
+ title_marker = VisualMarker.generate_from_text(title.upper(), direction, block_count)
388
+
389
+ append_axis = 1 if direction == Direction.TOP_TO_BOTTOM else 0
390
+ padding_dim = (4, block_count) if append_axis==0 else (block_count, qr.default_border_size)
391
+ padding= np.zeros(padding_dim, dtype=np.uint8)
392
+ if title:
393
+ combined_matrix = np.concatenate((title_marker, padding, qr_matrix, padding, visual_marker), axis=append_axis)
394
+ else:
395
+ combined_matrix = np.concatenate((qr_matrix, padding, visual_marker), axis=append_axis)
396
+
397
+ return combined_matrix
398
+
399
+ def save_qr_with_markers(url, text="PAC", title=None, direction = Direction.LEFT_TO_RIGHT, fmt='png', path='qr'):
400
+ combined_matrix = _generate_qr_with_markers(url, text="PAC", title=None, direction = Direction.LEFT_TO_RIGHT)
401
+ outfile = f'{path}.{fmt}'
402
+ match fmt:
403
+ case 'png':
404
+ writers.write_png(combined_matrix, combined_matrix.shape[::-1], out=outfile, border=9)
405
+ case 'svg':
406
+ writers.write_svg(combined_matrix, combined_matrix.shape[::-1], out=outfile, border=9)
407
+
408
+
409
+
410
+
411
+ def main(url: Annotated[str, typer.Argument(help="The PAC-ID to be rendered as QR.")],
412
+ outfile: Annotated[typer.FileBinaryWrite, typer.Option(help="The file the qr will be written to.")] = "qr.svg",
413
+ text: Annotated[str, typer.Option(help="The text of the PAC decoration.")] = "PAC",
414
+ direction: Annotated[Direction, typer.Option(help="The position/direction of the PAC decoration.")] = Direction.TOP_TO_BOTTOM):
415
+
416
+ save_qr_with_markers(url, text=text, direction=direction, path=outfile)
417
+
418
+
419
+
420
+ if __name__ == "__main__":
421
+ typer.run(main)
422
+