labfreed 0.0.4__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.
- labfreed/PAC_CAT/__init__.py +16 -0
- labfreed/PAC_CAT/category_base.py +51 -0
- labfreed/PAC_CAT/pac_cat.py +159 -0
- labfreed/PAC_CAT/predefined_categories.py +190 -0
- labfreed/PAC_ID/__init__.py +19 -0
- labfreed/PAC_ID/extension.py +48 -0
- labfreed/PAC_ID/id_segment.py +90 -0
- labfreed/PAC_ID/pac_id.py +140 -0
- labfreed/PAC_ID/url_parser.py +154 -0
- labfreed/PAC_ID/url_serializer.py +80 -0
- labfreed/PAC_ID_Resolver/__init__.py +2 -0
- labfreed/PAC_ID_Resolver/cit_v1.py +149 -0
- labfreed/PAC_ID_Resolver/cit_v2.py +303 -0
- labfreed/PAC_ID_Resolver/resolver.py +81 -0
- labfreed/PAC_ID_Resolver/services.py +80 -0
- labfreed/__init__.py +4 -1
- labfreed/labfreed_infrastructure.py +276 -0
- labfreed/qr/__init__.py +1 -0
- labfreed/qr/generate_qr.py +422 -0
- labfreed/trex/__init__.py +16 -0
- labfreed/trex/python_convenience/__init__.py +3 -0
- labfreed/trex/python_convenience/data_table.py +45 -0
- labfreed/trex/python_convenience/pyTREX.py +242 -0
- labfreed/trex/python_convenience/quantity.py +46 -0
- labfreed/trex/table_segment.py +227 -0
- labfreed/trex/trex.py +69 -0
- labfreed/trex/trex_base_models.py +336 -0
- labfreed/trex/value_segments.py +111 -0
- labfreed/{DisplayNameExtension → utilities}/base36.py +29 -13
- labfreed/well_known_extensions/__init__.py +5 -0
- labfreed/well_known_extensions/default_extension_interpreters.py +7 -0
- labfreed/well_known_extensions/display_name_extension.py +40 -0
- labfreed/well_known_extensions/trex_extension.py +31 -0
- labfreed/well_known_keys/gs1/__init__.py +6 -0
- labfreed/well_known_keys/gs1/gs1.py +4 -0
- labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +57 -0
- labfreed/well_known_keys/labfreed/well_known_keys.py +16 -0
- labfreed/well_known_keys/unece/UneceUnits.json +33730 -0
- labfreed/well_known_keys/unece/__init__.py +4 -0
- labfreed/well_known_keys/unece/unece_units.py +68 -0
- labfreed-0.2.0b0.dist-info/METADATA +329 -0
- labfreed-0.2.0b0.dist-info/RECORD +44 -0
- {labfreed-0.0.4.dist-info → labfreed-0.2.0b0.dist-info}/WHEEL +1 -1
- labfreed/DisplayNameExtension/DisplayNameExtension.py +0 -34
- labfreed/PAC_CAT/data_model.py +0 -109
- labfreed/PAC_ID/data_model.py +0 -114
- labfreed/PAC_ID/parse.py +0 -133
- labfreed/PAC_ID/serialize.py +0 -57
- labfreed/TREXExtension/data_model.py +0 -239
- labfreed/TREXExtension/parse.py +0 -46
- labfreed/TREXExtension/uncertainty.py +0 -32
- labfreed/TREXExtension/unit_utilities.py +0 -134
- labfreed-0.0.4.dist-info/METADATA +0 -15
- labfreed-0.0.4.dist-info/RECORD +0 -17
- {labfreed-0.0.4.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
|
+
|
labfreed/qr/__init__.py
ADDED
|
@@ -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
|
+
|