labelle 1.0.0__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.
- labelle/__init__.py +4 -0
- labelle/_version.py +16 -0
- labelle/barcode_writer.py +108 -0
- labelle/command_line.py +265 -0
- labelle/constants.py +97 -0
- labelle/detect.py +214 -0
- labelle/dymo_print_engines.py +360 -0
- labelle/font_config.py +46 -0
- labelle/gui.py +179 -0
- labelle/labeler.py +223 -0
- labelle/metadata.py +10 -0
- labelle/q_dymo_label_widgets.py +424 -0
- labelle/q_dymo_labels_list.py +150 -0
- labelle/resources/fonts/Carlito-Bold.ttf +0 -0
- labelle/resources/fonts/Carlito-BoldItalic.ttf +0 -0
- labelle/resources/fonts/Carlito-Italic.ttf +0 -0
- labelle/resources/fonts/Carlito-Regular.ttf +0 -0
- labelle/resources/fonts/LICENSE +94 -0
- labelle/resources/fonts/__init__.py +0 -0
- labelle/resources/icons/__init__.py +0 -0
- labelle/resources/icons/barcode_icon.png +0 -0
- labelle/resources/icons/barcode_text_icon.png +0 -0
- labelle/resources/icons/img_icon.png +0 -0
- labelle/resources/icons/logo_small.png +0 -0
- labelle/resources/icons/qr_icon.png +0 -0
- labelle/resources/icons/txt_icon.png +0 -0
- labelle/unicode_blocks.py +42 -0
- labelle/utils.py +37 -0
- labelle-1.0.0.dist-info/METADATA +243 -0
- labelle-1.0.0.dist-info/RECORD +33 -0
- labelle-1.0.0.dist-info/WHEEL +4 -0
- labelle-1.0.0.dist-info/entry_points.txt +3 -0
- labelle-1.0.0.dist-info/licenses/LICENSE +201 -0
labelle/__init__.py
ADDED
labelle/_version.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '1.0.0'
|
|
16
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# === LICENSE STATEMENT ===
|
|
2
|
+
# Copyright (c) 2011 Sebastian J. Bronner <waschtl@sbronner.com>
|
|
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 Optional
|
|
10
|
+
|
|
11
|
+
from barcode.writer import BaseWriter
|
|
12
|
+
from PIL import Image, ImageDraw
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def mm2px(mm, dpi=25.4):
|
|
16
|
+
return (mm * dpi) / 25.4
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BarcodeImageWriter(BaseWriter):
|
|
20
|
+
_draw: Optional[ImageDraw.ImageDraw]
|
|
21
|
+
|
|
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
|
|
29
|
+
|
|
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
|
+
|
|
35
|
+
def render(self, code):
|
|
36
|
+
"""Renders the barcode to whatever the inheriting writer provides,
|
|
37
|
+
using the registered callbacks.
|
|
38
|
+
|
|
39
|
+
:parameters:
|
|
40
|
+
code : List
|
|
41
|
+
List of strings matching the writer spec
|
|
42
|
+
(only contain 0 or 1).
|
|
43
|
+
"""
|
|
44
|
+
if self._callbacks["initialize"] is not None:
|
|
45
|
+
self._callbacks["initialize"](code)
|
|
46
|
+
ypos = self.vertical_margin
|
|
47
|
+
for cc, line in enumerate(code):
|
|
48
|
+
"""
|
|
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
|
+
"""
|
|
53
|
+
line += " "
|
|
54
|
+
c = 1
|
|
55
|
+
mlist = []
|
|
56
|
+
for i in range(0, len(line) - 1):
|
|
57
|
+
if line[i] == line[i + 1]:
|
|
58
|
+
c += 1
|
|
59
|
+
else:
|
|
60
|
+
if line[i] == "1":
|
|
61
|
+
mlist.append(c)
|
|
62
|
+
else:
|
|
63
|
+
mlist.append(-c)
|
|
64
|
+
c = 1
|
|
65
|
+
# Left quiet zone is x startposition
|
|
66
|
+
xpos = self.quiet_zone
|
|
67
|
+
for mod in mlist:
|
|
68
|
+
if mod < 1:
|
|
69
|
+
color = self.background
|
|
70
|
+
else:
|
|
71
|
+
color = self.foreground
|
|
72
|
+
# remove painting for background colored tiles?
|
|
73
|
+
self._callbacks["paint_module"](
|
|
74
|
+
xpos, ypos, self.module_width * abs(mod), color
|
|
75
|
+
)
|
|
76
|
+
xpos += self.module_width * abs(mod)
|
|
77
|
+
# Add right quiet zone to every line, except last line,
|
|
78
|
+
# quiet zone already provided with background,
|
|
79
|
+
# should it be removed complety?
|
|
80
|
+
if (cc + 1) != len(code):
|
|
81
|
+
self._callbacks["paint_module"](
|
|
82
|
+
xpos, ypos, self.quiet_zone, self.background
|
|
83
|
+
)
|
|
84
|
+
ypos += self.module_height
|
|
85
|
+
return self._callbacks["finish"]()
|
|
86
|
+
|
|
87
|
+
def _init(self, code):
|
|
88
|
+
size = self.calculate_size(len(code[0]), len(code), self.dpi)
|
|
89
|
+
self._image = Image.new("1", size, self.background)
|
|
90
|
+
self._draw = ImageDraw.Draw(self._image)
|
|
91
|
+
|
|
92
|
+
def _paint_module(self, xpos, ypos, width, color):
|
|
93
|
+
size = [
|
|
94
|
+
(mm2px(xpos, self.dpi), mm2px(ypos, self.dpi)),
|
|
95
|
+
(
|
|
96
|
+
mm2px(xpos + width, self.dpi),
|
|
97
|
+
mm2px(ypos + self.module_height, self.dpi),
|
|
98
|
+
),
|
|
99
|
+
]
|
|
100
|
+
self._draw.rectangle(size, outline=color, fill=color)
|
|
101
|
+
|
|
102
|
+
def _finish(self):
|
|
103
|
+
return self._image
|
|
104
|
+
|
|
105
|
+
def save(self, filename, output):
|
|
106
|
+
filename = f"{filename}.{self.format.lower()}"
|
|
107
|
+
output.save(filename, self.format.upper())
|
|
108
|
+
return filename
|
labelle/command_line.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# === LICENSE STATEMENT ===
|
|
2
|
+
# Copyright (c) 2011 Sebastian J. Bronner <waschtl@sbronner.com>
|
|
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
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from PIL import Image, ImageOps
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .constants import (
|
|
16
|
+
AVAILABLE_BARCODES,
|
|
17
|
+
DEFAULT_MARGIN_PX,
|
|
18
|
+
PIXELS_PER_MM,
|
|
19
|
+
USE_QR,
|
|
20
|
+
e_qrcode,
|
|
21
|
+
)
|
|
22
|
+
from .dymo_print_engines import DymoRenderEngine, print_label
|
|
23
|
+
from .font_config import font_filename
|
|
24
|
+
from .metadata import our_metadata
|
|
25
|
+
from .unicode_blocks import image_to_unicode
|
|
26
|
+
from .utils import die
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_args():
|
|
30
|
+
# check for any text specified on the command line
|
|
31
|
+
parser = argparse.ArgumentParser(description=our_metadata["Summary"])
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"text",
|
|
37
|
+
nargs="+",
|
|
38
|
+
help="Text Parameter, each parameter gives a new line",
|
|
39
|
+
type=str,
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"-f",
|
|
43
|
+
action="count",
|
|
44
|
+
help="Draw frame around the text, more arguments for thicker frame",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"-s",
|
|
48
|
+
choices=["r", "b", "i", "n"],
|
|
49
|
+
default="r",
|
|
50
|
+
help="Set fonts style (regular,bold,italic,narrow)",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"-a",
|
|
54
|
+
choices=[
|
|
55
|
+
"left",
|
|
56
|
+
"center",
|
|
57
|
+
"right",
|
|
58
|
+
],
|
|
59
|
+
default="left",
|
|
60
|
+
help="Align multiline text (left,center,right)",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--test-pattern",
|
|
64
|
+
type=int,
|
|
65
|
+
default=0,
|
|
66
|
+
help="Prints test pattern of a desired dot width",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
length_options = parser.add_argument_group("Length options")
|
|
70
|
+
|
|
71
|
+
length_options.add_argument(
|
|
72
|
+
"-l",
|
|
73
|
+
"--min-length",
|
|
74
|
+
type=int,
|
|
75
|
+
default=0,
|
|
76
|
+
help="Specify minimum label length in mm",
|
|
77
|
+
)
|
|
78
|
+
length_options.add_argument(
|
|
79
|
+
"--max-length",
|
|
80
|
+
type=int,
|
|
81
|
+
default=None,
|
|
82
|
+
help="Specify maximum label length in mm, error if the label won't fit",
|
|
83
|
+
)
|
|
84
|
+
length_options.add_argument(
|
|
85
|
+
"--fixed-length",
|
|
86
|
+
type=int,
|
|
87
|
+
default=None,
|
|
88
|
+
help="Specify fixed label length in mm, error if the label won't fit",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
length_options.add_argument(
|
|
92
|
+
"-j",
|
|
93
|
+
choices=[
|
|
94
|
+
"left",
|
|
95
|
+
"center",
|
|
96
|
+
"right",
|
|
97
|
+
],
|
|
98
|
+
default="center",
|
|
99
|
+
help=(
|
|
100
|
+
"Justify content of label if label content is less than the "
|
|
101
|
+
"minimum or fixed length (left, center, right)"
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument("-u", nargs="?", help='Set user font, overrides "-s" parameter')
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"-n",
|
|
107
|
+
"--preview",
|
|
108
|
+
action="store_true",
|
|
109
|
+
help="Unicode preview of label, do not send to printer",
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--preview-inverted",
|
|
113
|
+
action="store_true",
|
|
114
|
+
help="Unicode preview of label, colors inverted, do not send to printer",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--imagemagick",
|
|
118
|
+
action="store_true",
|
|
119
|
+
help="Preview label with Imagemagick, do not send to printer",
|
|
120
|
+
)
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"-qr", action="store_true", help="Printing the first text parameter as QR-code"
|
|
123
|
+
)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"-c",
|
|
126
|
+
"--barcode",
|
|
127
|
+
choices=AVAILABLE_BARCODES,
|
|
128
|
+
default=False,
|
|
129
|
+
help="Printing the first text parameter as barcode",
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--barcode-text",
|
|
133
|
+
choices=AVAILABLE_BARCODES,
|
|
134
|
+
default=False,
|
|
135
|
+
help="Printing the first text parameter as barcode and text under it",
|
|
136
|
+
)
|
|
137
|
+
parser.add_argument("-p", "--picture", help="Print the specified picture")
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"-m",
|
|
140
|
+
type=int,
|
|
141
|
+
default=DEFAULT_MARGIN_PX,
|
|
142
|
+
help=f"Margin in px (default is {DEFAULT_MARGIN_PX})",
|
|
143
|
+
)
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--scale", type=int, default=90, help="Scaling font factor, [0,10] [%%]"
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"-t",
|
|
149
|
+
type=int,
|
|
150
|
+
choices=[6, 9, 12, 19],
|
|
151
|
+
default=12,
|
|
152
|
+
help="Tape size: 6,9,12,19 mm, default=12mm",
|
|
153
|
+
)
|
|
154
|
+
return parser.parse_args()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def mm_to_payload_px(mm, margin):
|
|
158
|
+
"""Convert a length in mm to a number of pixels of payload
|
|
159
|
+
|
|
160
|
+
The print resolution is 7 pixels/mm, and margin is subtracted
|
|
161
|
+
from each side."""
|
|
162
|
+
return (mm * PIXELS_PER_MM) - margin * 2
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def main():
|
|
166
|
+
args = parse_args()
|
|
167
|
+
render_engine = DymoRenderEngine(args.t)
|
|
168
|
+
|
|
169
|
+
# read config file
|
|
170
|
+
FONT_FILENAME = font_filename(args.s)
|
|
171
|
+
|
|
172
|
+
labeltext = args.text
|
|
173
|
+
|
|
174
|
+
if args.u is not None:
|
|
175
|
+
if os.path.isfile(args.u):
|
|
176
|
+
FONT_FILENAME = args.u
|
|
177
|
+
else:
|
|
178
|
+
die("Error: file '%s' not found." % args.u)
|
|
179
|
+
|
|
180
|
+
# check if barcode, qrcode or text should be printed, use frames only on text
|
|
181
|
+
if args.qr and not USE_QR:
|
|
182
|
+
die("Error: %s" % e_qrcode)
|
|
183
|
+
|
|
184
|
+
if args.barcode and args.qr:
|
|
185
|
+
die("Error: can not print both QR and Barcode on the same label (yet)")
|
|
186
|
+
|
|
187
|
+
if args.fixed_length is not None and (
|
|
188
|
+
args.min_length != 0 or args.max_length is not None
|
|
189
|
+
):
|
|
190
|
+
die("Error: can't specify min/max and fixed length at the same time")
|
|
191
|
+
|
|
192
|
+
if args.max_length is not None and args.max_length < args.min_length:
|
|
193
|
+
die("Error: maximum length is less than minimum length")
|
|
194
|
+
|
|
195
|
+
bitmaps = []
|
|
196
|
+
|
|
197
|
+
if args.test_pattern:
|
|
198
|
+
bitmaps.append(render_engine.render_test(args.test_pattern))
|
|
199
|
+
|
|
200
|
+
if args.qr:
|
|
201
|
+
bitmaps.append(render_engine.render_qr(labeltext.pop(0)))
|
|
202
|
+
|
|
203
|
+
elif args.barcode:
|
|
204
|
+
bitmaps.append(render_engine.render_barcode(labeltext.pop(0), args.barcode))
|
|
205
|
+
|
|
206
|
+
elif args.barcode_text:
|
|
207
|
+
bitmaps.append(
|
|
208
|
+
render_engine.render_barcode_with_text(
|
|
209
|
+
labeltext.pop(0), args.barcode_text, FONT_FILENAME, args.f
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if labeltext:
|
|
214
|
+
bitmaps.append(
|
|
215
|
+
render_engine.render_text(
|
|
216
|
+
text_lines=labeltext,
|
|
217
|
+
font_file_name=FONT_FILENAME,
|
|
218
|
+
frame_width_px=args.f,
|
|
219
|
+
font_size_ratio=int(args.scale) / 100.0,
|
|
220
|
+
align=args.a,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if args.picture:
|
|
225
|
+
bitmaps.append(render_engine.render_picture(args.picture))
|
|
226
|
+
|
|
227
|
+
margin = args.m
|
|
228
|
+
justify = args.j
|
|
229
|
+
|
|
230
|
+
if args.fixed_length is not None:
|
|
231
|
+
min_label_mm_len = args.fixed_length
|
|
232
|
+
max_label_mm_len = args.fixed_length
|
|
233
|
+
else:
|
|
234
|
+
min_label_mm_len = args.min_length
|
|
235
|
+
max_label_mm_len = args.max_length
|
|
236
|
+
|
|
237
|
+
min_payload_len_px = max(0, mm_to_payload_px(min_label_mm_len, margin))
|
|
238
|
+
max_payload_len_px = (
|
|
239
|
+
mm_to_payload_px(max_label_mm_len, margin)
|
|
240
|
+
if max_label_mm_len is not None
|
|
241
|
+
else None
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
label_bitmap = render_engine.merge_render(
|
|
245
|
+
bitmaps=bitmaps,
|
|
246
|
+
min_payload_len_px=min_payload_len_px,
|
|
247
|
+
max_payload_len_px=max_payload_len_px,
|
|
248
|
+
justify=justify,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# print or show the label
|
|
252
|
+
if args.preview or args.preview_inverted or args.imagemagick:
|
|
253
|
+
print("Demo mode: showing label..")
|
|
254
|
+
# fix size, adding print borders
|
|
255
|
+
label_image = Image.new(
|
|
256
|
+
"L", (margin + label_bitmap.width + margin, label_bitmap.height)
|
|
257
|
+
)
|
|
258
|
+
label_image.paste(label_bitmap, (margin, 0))
|
|
259
|
+
if args.preview or args.preview_inverted:
|
|
260
|
+
label_rotated = label_bitmap.transpose(Image.ROTATE_270)
|
|
261
|
+
print(image_to_unicode(label_rotated, invert=args.preview_inverted))
|
|
262
|
+
if args.imagemagick:
|
|
263
|
+
ImageOps.invert(label_image).show()
|
|
264
|
+
else:
|
|
265
|
+
print_label(label_bitmap, margin_px=args.m, tape_size_mm=args.t)
|
labelle/constants.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# === LICENSE STATEMENT ===
|
|
2
|
+
# Copyright (c) 2011 Sebastian J. Bronner <waschtl@sbronner.com>
|
|
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
|
+
# On systems with access to sysfs under /sys, this script will use the three
|
|
10
|
+
# variables DEV_CLASS, DEV_VENDOR, and DEV_PRODUCT to find the device file
|
|
11
|
+
# under /dev automatically. This behavior can be overridden by setting the
|
|
12
|
+
# variable DEV_NODE to the device file path. This is intended for cases, where
|
|
13
|
+
# either sysfs is unavailable or unusable by this script for some reason.
|
|
14
|
+
# Please beware that DEV_NODE must be set to None when not used, else you will
|
|
15
|
+
# be bitten by the NameError exception.
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import labelle.resources.fonts
|
|
20
|
+
import labelle.resources.icons
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from pyqrcode import QRCode
|
|
24
|
+
|
|
25
|
+
USE_QR = True
|
|
26
|
+
e_qrcode = None
|
|
27
|
+
except ImportError as error:
|
|
28
|
+
e_qrcode = error
|
|
29
|
+
USE_QR = False
|
|
30
|
+
QRCode = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
UNCONFIRMED_MESSAGE = (
|
|
34
|
+
"WARNING: This device is not confirmed to work with this software. Please "
|
|
35
|
+
"report your experiences in https://github.com/computerlyrik/dymoprint/issues/44"
|
|
36
|
+
)
|
|
37
|
+
SUPPORTED_PRODUCTS = {
|
|
38
|
+
0x0011: "DYMO LabelMANAGER PC",
|
|
39
|
+
0x0015: "LabelPoint 350",
|
|
40
|
+
0x1001: "LabelManager PnP (no mode switch)",
|
|
41
|
+
0x1002: "LabelManager PnP (mode switch)",
|
|
42
|
+
0x1003: f"LabelManager 420P (no mode switch) {UNCONFIRMED_MESSAGE}",
|
|
43
|
+
0x1004: f"LabelManager 420P (mode switch) {UNCONFIRMED_MESSAGE}",
|
|
44
|
+
0x1005: "LabelManager 280 (no mode switch)",
|
|
45
|
+
0x1006: "LabelManager 280 (no mode switch)",
|
|
46
|
+
0x1007: f"LabelManager Wireless PnP (no mode switch) {UNCONFIRMED_MESSAGE}",
|
|
47
|
+
0x1008: f"LabelManager Wireless PnP (mode switch) {UNCONFIRMED_MESSAGE}",
|
|
48
|
+
0x1009: f"MobileLabeler {UNCONFIRMED_MESSAGE}",
|
|
49
|
+
}
|
|
50
|
+
DEV_VENDOR = 0x0922
|
|
51
|
+
|
|
52
|
+
PRINTER_INTERFACE_CLASS = 0x07
|
|
53
|
+
HID_INTERFACE_CLASS = 0x03
|
|
54
|
+
|
|
55
|
+
# Escape character preceeding all commands
|
|
56
|
+
ESC = 0x1B
|
|
57
|
+
|
|
58
|
+
# Synchronization character preceding uncompressed print data
|
|
59
|
+
SYN = 0x16
|
|
60
|
+
|
|
61
|
+
FONT_SIZERATIO = 7 / 8
|
|
62
|
+
|
|
63
|
+
DEFAULT_FONT_STYLE = "regular"
|
|
64
|
+
|
|
65
|
+
DEFAULT_MARGIN_PX = 56
|
|
66
|
+
|
|
67
|
+
FLAG_TO_STYLE = {
|
|
68
|
+
"r": "regular",
|
|
69
|
+
"b": "bold",
|
|
70
|
+
"i": "italic",
|
|
71
|
+
"n": "narrow",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
DPI = 180
|
|
75
|
+
MM_PER_INCH = 25.4
|
|
76
|
+
PIXELS_PER_MM = DPI / MM_PER_INCH
|
|
77
|
+
|
|
78
|
+
DEFAULT_FONT_DIR = Path(labelle.resources.fonts.__file__).parent
|
|
79
|
+
ICON_DIR = Path(labelle.resources.icons.__file__).parent
|
|
80
|
+
|
|
81
|
+
AVAILABLE_BARCODES = [
|
|
82
|
+
"code39",
|
|
83
|
+
"code128",
|
|
84
|
+
"ean",
|
|
85
|
+
"ean13",
|
|
86
|
+
"ean8",
|
|
87
|
+
"gs1",
|
|
88
|
+
"gtin",
|
|
89
|
+
"isbn",
|
|
90
|
+
"isbn10",
|
|
91
|
+
"isbn13",
|
|
92
|
+
"issn",
|
|
93
|
+
"jan",
|
|
94
|
+
"pzn",
|
|
95
|
+
"upc",
|
|
96
|
+
"upca",
|
|
97
|
+
]
|
labelle/detect.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from typing import NamedTuple, NoReturn
|
|
3
|
+
|
|
4
|
+
import usb
|
|
5
|
+
|
|
6
|
+
from labelle.utils import die
|
|
7
|
+
|
|
8
|
+
from .constants import (
|
|
9
|
+
DEV_VENDOR,
|
|
10
|
+
HID_INTERFACE_CLASS,
|
|
11
|
+
PRINTER_INTERFACE_CLASS,
|
|
12
|
+
SUPPORTED_PRODUCTS,
|
|
13
|
+
UNCONFIRMED_MESSAGE,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
GITHUB_LINK = "<https://github.com/computerlyrik/dymoprint/pull/56>"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DetectedDevice(NamedTuple):
|
|
20
|
+
id: int
|
|
21
|
+
"""See labelle.constants.SUPPORTED_PRODUCTS for a list of known IDs."""
|
|
22
|
+
dev: usb.core.Device
|
|
23
|
+
intf: usb.core.Interface
|
|
24
|
+
devout: usb.core.Endpoint
|
|
25
|
+
devin: usb.core.Endpoint
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def device_info(dev: usb.core.Device) -> str:
|
|
29
|
+
try:
|
|
30
|
+
dev.manufacturer
|
|
31
|
+
except ValueError:
|
|
32
|
+
instruct_on_access_denied(dev)
|
|
33
|
+
res = ""
|
|
34
|
+
res += f"{repr(dev)}\n"
|
|
35
|
+
res += f" manufacturer: {dev.manufacturer}\n"
|
|
36
|
+
res += f" product: {dev.product}\n"
|
|
37
|
+
res += f" serial: {dev.serial_number}\n"
|
|
38
|
+
configs = dev.configurations()
|
|
39
|
+
if configs:
|
|
40
|
+
res += " configurations:\n"
|
|
41
|
+
for cfg in configs:
|
|
42
|
+
res += f" - {repr(cfg)}\n"
|
|
43
|
+
intfs = cfg.interfaces()
|
|
44
|
+
if intfs:
|
|
45
|
+
res += " interfaces:\n"
|
|
46
|
+
for intf in intfs:
|
|
47
|
+
res += f" - {repr(intf)}\n"
|
|
48
|
+
return res
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def detect_device() -> DetectedDevice:
|
|
52
|
+
dymo_devs = list(usb.core.find(idVendor=DEV_VENDOR, find_all=True))
|
|
53
|
+
if len(dymo_devs) == 0:
|
|
54
|
+
print(f"No Dymo devices found (expected vendor {hex(DEV_VENDOR)})")
|
|
55
|
+
for dev in usb.core.find(find_all=True):
|
|
56
|
+
print(
|
|
57
|
+
f"- Vendor ID: {hex(dev.idVendor):6} "
|
|
58
|
+
f"Product ID: {hex(dev.idProduct)}"
|
|
59
|
+
)
|
|
60
|
+
die("Unable to open device.")
|
|
61
|
+
if len(dymo_devs) > 1:
|
|
62
|
+
print("Found multiple Dymo devices:")
|
|
63
|
+
for dev in dymo_devs:
|
|
64
|
+
print(device_info(dev))
|
|
65
|
+
print("Using first device.")
|
|
66
|
+
dev = dymo_devs[0]
|
|
67
|
+
else:
|
|
68
|
+
dev = dymo_devs[0]
|
|
69
|
+
print(f"Found one Dymo device: {device_info(dev)}")
|
|
70
|
+
dev = dymo_devs[0]
|
|
71
|
+
if dev.idProduct in SUPPORTED_PRODUCTS:
|
|
72
|
+
print(f"Recognized device as {SUPPORTED_PRODUCTS[dev.idProduct]}")
|
|
73
|
+
else:
|
|
74
|
+
print(f"Unrecognized device: {hex(dev.idProduct)}. {UNCONFIRMED_MESSAGE}")
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
dev.get_active_configuration()
|
|
78
|
+
print("Active device configuration already found.")
|
|
79
|
+
except usb.core.USBError:
|
|
80
|
+
try:
|
|
81
|
+
dev.set_configuration()
|
|
82
|
+
print("Device configuration set.")
|
|
83
|
+
except usb.core.USBError as e:
|
|
84
|
+
if e.errno == 13:
|
|
85
|
+
raise RuntimeError("Access denied")
|
|
86
|
+
if e.errno == 16:
|
|
87
|
+
print("Device is busy, but this is okay.")
|
|
88
|
+
else:
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
intf = usb.util.find_descriptor(
|
|
92
|
+
dev.get_active_configuration(), bInterfaceClass=PRINTER_INTERFACE_CLASS
|
|
93
|
+
)
|
|
94
|
+
if intf is not None:
|
|
95
|
+
print(f"Opened printer interface: {repr(intf)}")
|
|
96
|
+
else:
|
|
97
|
+
intf = usb.util.find_descriptor(
|
|
98
|
+
dev.get_active_configuration(), bInterfaceClass=HID_INTERFACE_CLASS
|
|
99
|
+
)
|
|
100
|
+
if intf is not None:
|
|
101
|
+
print(f"Opened HID interface: {repr(intf)}")
|
|
102
|
+
else:
|
|
103
|
+
die("Could not open a valid interface.")
|
|
104
|
+
assert isinstance(intf, usb.core.Interface)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
if dev.is_kernel_driver_active(intf.bInterfaceNumber):
|
|
108
|
+
print(f"Detaching kernel driver from interface {intf.bInterfaceNumber}")
|
|
109
|
+
dev.detach_kernel_driver(intf.bInterfaceNumber)
|
|
110
|
+
except NotImplementedError:
|
|
111
|
+
print(f"Kernel driver detaching not necessary on " f"{platform.system()}.")
|
|
112
|
+
devout = usb.util.find_descriptor(
|
|
113
|
+
intf,
|
|
114
|
+
custom_match=(
|
|
115
|
+
lambda e: usb.util.endpoint_direction(e.bEndpointAddress)
|
|
116
|
+
== usb.util.ENDPOINT_OUT
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
devin = usb.util.find_descriptor(
|
|
120
|
+
intf,
|
|
121
|
+
custom_match=(
|
|
122
|
+
lambda e: usb.util.endpoint_direction(e.bEndpointAddress)
|
|
123
|
+
== usb.util.ENDPOINT_IN
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if not devout or not devin:
|
|
128
|
+
die("The device endpoints not be found.")
|
|
129
|
+
return DetectedDevice(
|
|
130
|
+
id=dev.idProduct, dev=dev, intf=intf, devout=devout, devin=devin
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def instruct_on_access_denied(dev: usb.core.Device) -> NoReturn:
|
|
135
|
+
system = platform.system()
|
|
136
|
+
if system == "Linux":
|
|
137
|
+
instruct_on_access_denied_linux(dev)
|
|
138
|
+
elif system == "Windows":
|
|
139
|
+
raise RuntimeError(
|
|
140
|
+
"Couldn't access the device. Please make sure that the "
|
|
141
|
+
"device driver is set to WinUSB. This can be accomplished "
|
|
142
|
+
"with Zadig <https://zadig.akeo.ie/>."
|
|
143
|
+
)
|
|
144
|
+
elif system == "Darwin":
|
|
145
|
+
raise RuntimeError(
|
|
146
|
+
f"Could not access {dev}. Thanks for bravely trying this on a Mac. You "
|
|
147
|
+
f"are in uncharted territory. It would be appreciated if you share the "
|
|
148
|
+
f"results of your experimentation at {GITHUB_LINK}."
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
raise RuntimeError(f"Unknown platform {system}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def instruct_on_access_denied_linux(dev: usb.core.Device) -> NoReturn:
|
|
155
|
+
# try:
|
|
156
|
+
# os_release = platform.freedesktop_os_release()
|
|
157
|
+
# except OSError:
|
|
158
|
+
# os_release = {}
|
|
159
|
+
# dists_with_empties = [os_release.get("ID", "")] + os_release.get(
|
|
160
|
+
# "ID_LIKE", ""
|
|
161
|
+
# ).split(" ")
|
|
162
|
+
# dists = [dist for dist in dists_with_empties if dist]
|
|
163
|
+
# if "arch" in dists:
|
|
164
|
+
# restart_udev_command = "sudo udevadm control --reload"
|
|
165
|
+
# elif "ubuntu" in dists or "debian" in dists:
|
|
166
|
+
# restart_udev_command = "sudo systemctl restart udev.service"
|
|
167
|
+
# # detect whether we are in arch linux or ubuntu linux
|
|
168
|
+
# if Path("/etc/arch-release").exists():
|
|
169
|
+
# restart_udev_command = "sudo udevadm control --reload"
|
|
170
|
+
# elif Path("/etc/lsb-release").exists():
|
|
171
|
+
# restart_udev_command = "sudo systemctl restart udev.service"
|
|
172
|
+
# else:
|
|
173
|
+
# restart_udev_command = None
|
|
174
|
+
|
|
175
|
+
lines = []
|
|
176
|
+
lines.append(
|
|
177
|
+
"You do not have sufficient access to the "
|
|
178
|
+
"device. You probably want to add the a udev rule in "
|
|
179
|
+
"/etc/udev/rules.d with the following command:"
|
|
180
|
+
)
|
|
181
|
+
lines.append("")
|
|
182
|
+
udev_rule = ", ".join(
|
|
183
|
+
[
|
|
184
|
+
'ACTION=="add"',
|
|
185
|
+
'SUBSYSTEMS=="usb"',
|
|
186
|
+
f'ATTRS{{idVendor}}=="{dev.idVendor:04x}"',
|
|
187
|
+
f'ATTRS{{idProduct}}=="{dev.idProduct:04x}"',
|
|
188
|
+
'MODE="0666"',
|
|
189
|
+
]
|
|
190
|
+
)
|
|
191
|
+
lines.append(
|
|
192
|
+
f" echo '{udev_rule}' "
|
|
193
|
+
f"| sudo tee /etc/udev/rules.d/91-dymo-{dev.idProduct:x}.rules"
|
|
194
|
+
)
|
|
195
|
+
lines.append("")
|
|
196
|
+
lines.append("Next refresh udev with:")
|
|
197
|
+
lines.append("")
|
|
198
|
+
lines.append(" sudo udevadm control --reload-rules")
|
|
199
|
+
lines.append(' sudo udevadm trigger --attr-match=idVendor="0922"')
|
|
200
|
+
lines.append("")
|
|
201
|
+
lines.append(
|
|
202
|
+
"Finally, turn your device off and back "
|
|
203
|
+
"on again to activate the new permissions."
|
|
204
|
+
)
|
|
205
|
+
lines.append("")
|
|
206
|
+
lines.append(
|
|
207
|
+
f"If this still does not resolve the problem, you might need to reboot. "
|
|
208
|
+
f"In case rebooting is necessary, please report this at {GITHUB_LINK}. "
|
|
209
|
+
f"We are still trying to figure out a simple procedure which works "
|
|
210
|
+
f"for everyone. In case you still cannot connect, "
|
|
211
|
+
f"or if you have any information or ideas, please post them at "
|
|
212
|
+
f"that link."
|
|
213
|
+
)
|
|
214
|
+
raise RuntimeError("\n\n" + "\n".join(lines) + "\n")
|