qltool 0.1.2__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.
- qltool/__init__.py +2 -0
- qltool/main.py +561 -0
- qltool-0.1.2.dist-info/METADATA +61 -0
- qltool-0.1.2.dist-info/RECORD +6 -0
- qltool-0.1.2.dist-info/WHEEL +4 -0
- qltool-0.1.2.dist-info/entry_points.txt +2 -0
qltool/__init__.py
ADDED
qltool/main.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
#---------text
|
|
4
|
+
rm /tmp/tmp_qrcode.png ; uv run src/qltool/main.py "Ahoj
|
|
5
|
+
aaa
|
|
6
|
+
456 8jgqy 789 456 456 " --font-size 55 --font-size 45
|
|
7
|
+
|
|
8
|
+
# ---- create QR
|
|
9
|
+
|
|
10
|
+
uv run src/qltool/main.py "ahoj
|
|
11
|
+
momo
|
|
12
|
+
123 456 789 jjj" -p -q
|
|
13
|
+
|
|
14
|
+
# -------- use any image/qrcode
|
|
15
|
+
uv run src/qltool/main.py "Užaj jetů" --qr /tmp/baba.png
|
|
16
|
+
|
|
17
|
+
uv run src/qltool/main.py "Už je tuná" --qr /tmp/baba.png -p
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
import shlex
|
|
21
|
+
import subprocess as sp
|
|
22
|
+
import glob
|
|
23
|
+
import tempfile
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
import click
|
|
27
|
+
from importlib_resources import files, as_file
|
|
28
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
29
|
+
# uv add Pillow
|
|
30
|
+
import cv2
|
|
31
|
+
# uv add opencv-python
|
|
32
|
+
import numpy as np
|
|
33
|
+
from console import fg, bg
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
import re
|
|
37
|
+
|
|
38
|
+
import qrcode
|
|
39
|
+
|
|
40
|
+
# =====================================================================
|
|
41
|
+
#
|
|
42
|
+
# ----------------------------------------------------------------------
|
|
43
|
+
def is_user_in_lp_group():
|
|
44
|
+
"""
|
|
45
|
+
Return True if 'lp' appears in the output of the `groups` command.
|
|
46
|
+
Uses runme() to call `groups`.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
out = runme("groups", reallyrun=True)
|
|
50
|
+
except Exception:
|
|
51
|
+
return False
|
|
52
|
+
tokens = re.findall(r"\w+", out)
|
|
53
|
+
return "lp" in tokens
|
|
54
|
+
|
|
55
|
+
# --- helpers (kept similar to your original) ---
|
|
56
|
+
# =====================================================================
|
|
57
|
+
#
|
|
58
|
+
# ----------------------------------------------------------------------
|
|
59
|
+
def get_data_path():
|
|
60
|
+
src = files("data").joinpath("ql570")
|
|
61
|
+
with as_file(src) as p:
|
|
62
|
+
return str(p)
|
|
63
|
+
|
|
64
|
+
# =====================================================================
|
|
65
|
+
#
|
|
66
|
+
# ----------------------------------------------------------------------
|
|
67
|
+
def check_lpx():
|
|
68
|
+
prs = glob.glob("/dev/usb/lp*")
|
|
69
|
+
if not prs:
|
|
70
|
+
raise RuntimeError("No LP device found under /dev/usb/lp*")
|
|
71
|
+
return prs[0]
|
|
72
|
+
|
|
73
|
+
# =====================================================================
|
|
74
|
+
#
|
|
75
|
+
# ----------------------------------------------------------------------
|
|
76
|
+
def runme(CMDi, silent=False, reallyrun=False):
|
|
77
|
+
print("D...", fg.orange, CMDi, fg.default)
|
|
78
|
+
CMD = shlex.split(CMDi)
|
|
79
|
+
if not reallyrun:
|
|
80
|
+
return "ok"
|
|
81
|
+
res = sp.check_output(CMD).decode("utf8")
|
|
82
|
+
if not silent:
|
|
83
|
+
print("i... RESULT:", fg.darkslategray, res, fg.default)
|
|
84
|
+
return res
|
|
85
|
+
|
|
86
|
+
# =====================================================================
|
|
87
|
+
#
|
|
88
|
+
# ----------------------------------------------------------------------
|
|
89
|
+
def get_tmp_fname():
|
|
90
|
+
temp_file = tempfile.NamedTemporaryFile(suffix=".png", dir="/tmp", delete=False)
|
|
91
|
+
name = temp_file.name
|
|
92
|
+
temp_file.close()
|
|
93
|
+
return name
|
|
94
|
+
|
|
95
|
+
# =====================================================================
|
|
96
|
+
#
|
|
97
|
+
# ----------------------------------------------------------------------
|
|
98
|
+
# --- image creation ---
|
|
99
|
+
def make_bw_text_image(lines, width=714,
|
|
100
|
+
# font_path="/usr/share/fonts/truetype/freefont/FreeMono.ttf",
|
|
101
|
+
font_path="/usr/share/fonts/truetype/clear-sans/ClearSans-Regular.ttf",
|
|
102
|
+
font_size=None,
|
|
103
|
+
pad=13):
|
|
104
|
+
# lines: list of 1-3 strings
|
|
105
|
+
if not (1 <= len(lines) <= 3):
|
|
106
|
+
raise ValueError("Provide 1 to 3 lines.")
|
|
107
|
+
# choose font
|
|
108
|
+
FSIZ = font_size or 36
|
|
109
|
+
try:
|
|
110
|
+
if font_path:
|
|
111
|
+
font = ImageFont.truetype(font_path, FSIZ)
|
|
112
|
+
else:
|
|
113
|
+
# try a reasonably sized PIL default or fallback
|
|
114
|
+
font = ImageFont.load_default()
|
|
115
|
+
except Exception:
|
|
116
|
+
font = ImageFont.load_default()
|
|
117
|
+
|
|
118
|
+
PAD = int( (FSIZ / 36) * pad) # for 36 pad 12 is OK
|
|
119
|
+
# compute sizes
|
|
120
|
+
# use a big temporary image to measure text
|
|
121
|
+
tmp = Image.new("L", (width, 2000), 255)
|
|
122
|
+
draw = ImageDraw.Draw(tmp)
|
|
123
|
+
#line_heights = []
|
|
124
|
+
#line_widths = []
|
|
125
|
+
line_sizes = []
|
|
126
|
+
for ln in lines:
|
|
127
|
+
bbox = draw.textbbox((0, 0), ln, font=font)
|
|
128
|
+
w = bbox[2] - bbox[0]
|
|
129
|
+
h = bbox[3] - bbox[1]
|
|
130
|
+
line_sizes.append((w, h))
|
|
131
|
+
#w, h = draw.textsize(ln, font=font)
|
|
132
|
+
#line_widths.append(w)
|
|
133
|
+
#line_heights.append(h)
|
|
134
|
+
# compute total height with small spacing
|
|
135
|
+
spacing = int(0.2 * (line_sizes[0][1] if line_sizes else 12)) or 4
|
|
136
|
+
total_h = sum(h for _, h in line_sizes) + spacing * (len(lines) - 1) + PAD * 2
|
|
137
|
+
# create final image
|
|
138
|
+
im = Image.new("L", (width, total_h), 255) # white background
|
|
139
|
+
draw = ImageDraw.Draw(im)
|
|
140
|
+
# center horizontally, stack lines vertically
|
|
141
|
+
y = PAD
|
|
142
|
+
for (w, h), ln in zip(line_sizes, lines):
|
|
143
|
+
x = max((width - w) // 2, 0)
|
|
144
|
+
draw.text((x, y), ln, font=font, fill=0)
|
|
145
|
+
y += h + spacing
|
|
146
|
+
return im.convert("RGB")
|
|
147
|
+
|
|
148
|
+
# =====================================================================
|
|
149
|
+
#
|
|
150
|
+
# ----------------------------------------------------------------------
|
|
151
|
+
def pil_to_cv2(im):
|
|
152
|
+
arr = np.array(im)[:, :, ::-1] # RGB->BGR
|
|
153
|
+
return arr
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# =====================================================================
|
|
157
|
+
#
|
|
158
|
+
# ----------------------------------------------------------------------
|
|
159
|
+
# --- mono conversion with Floyd-Steinberg and gray bias ---
|
|
160
|
+
def convert_to_mono_floyd(img_rgb, gray_percent=50):
|
|
161
|
+
"""
|
|
162
|
+
img_rgb: PIL RGB image
|
|
163
|
+
gray_percent: 0..100, default 50. 50 means no bias. Values>50 bias lighter, <50 bias darker.
|
|
164
|
+
Returns a PIL Image in mode '1' (mono).
|
|
165
|
+
"""
|
|
166
|
+
if not (0 <= gray_percent <= 100):
|
|
167
|
+
raise ValueError("gray_percent must be 0..100")
|
|
168
|
+
# convert to grayscale
|
|
169
|
+
im_l = img_rgb.convert("L")
|
|
170
|
+
# compute offset: 50 => 0, >50 => positive (lighter), <50 => negative (darker)
|
|
171
|
+
offset = int(round((gray_percent - 50) * 255 / 100.0))
|
|
172
|
+
if offset != 0:
|
|
173
|
+
lut = [min(255, max(0, i + offset)) for i in range(256)]
|
|
174
|
+
im_l = im_l.point(lut)
|
|
175
|
+
# convert to 1-bit using Floyd-Steinberg
|
|
176
|
+
mono = im_l.convert("1", dither=Image.FLOYDSTEINBERG)
|
|
177
|
+
if mono.mode != "1":
|
|
178
|
+
# fallback: force mode to '1'
|
|
179
|
+
mono = mono.convert("1")
|
|
180
|
+
return mono
|
|
181
|
+
|
|
182
|
+
# =====================================================================
|
|
183
|
+
#
|
|
184
|
+
# ----------------------------------------------------------------------
|
|
185
|
+
def rotate_mono_90(img, clockwise=True):
|
|
186
|
+
"""
|
|
187
|
+
Rotate a mono (1-bit) image by 90 degrees.
|
|
188
|
+
- img: PIL.Image or path to image file.
|
|
189
|
+
- clockwise: True => rotate 90° clockwise, False => 90° counter-clockwise.
|
|
190
|
+
Returns a PIL.Image in mode '1'.
|
|
191
|
+
"""
|
|
192
|
+
if isinstance(img, str):
|
|
193
|
+
img = Image.open(img)
|
|
194
|
+
if img.mode != "1":
|
|
195
|
+
img = img.convert("1")
|
|
196
|
+
# Use transpose for exact 90° rotation without resampling
|
|
197
|
+
return img.transpose(Image.ROTATE_270 if clockwise else Image.ROTATE_90)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# =====================================================================
|
|
203
|
+
#
|
|
204
|
+
# ----------------------------------------------------------------------
|
|
205
|
+
def create_qr(qr_path, lines, width=5):
|
|
206
|
+
qrtag = ";".join(lines)#f"{lines}"
|
|
207
|
+
qrtag = qrtag.replace(r"\n", ";")
|
|
208
|
+
qrtag = qrtag.replace(" ", " ")
|
|
209
|
+
qrtag = qrtag.replace(" ", " ")
|
|
210
|
+
qrtag = qrtag.replace(" ", " ")
|
|
211
|
+
qrtag = qrtag.replace(" ", " ")
|
|
212
|
+
qrtag = qrtag.replace(" ;", ";")
|
|
213
|
+
print(f"i... QRTAG /{fg.cyan}{qrtag}{fg.default}/")
|
|
214
|
+
# I will join Name: asoidj ; owner: OKjd ; location: asd o; ID: 8998244;
|
|
215
|
+
qr = qrcode.QRCode(
|
|
216
|
+
version=1,
|
|
217
|
+
error_correction=qrcode.constants.ERROR_CORRECT_M,
|
|
218
|
+
box_size=width,
|
|
219
|
+
border=3,
|
|
220
|
+
)
|
|
221
|
+
qr.add_data(qrtag)
|
|
222
|
+
qr.make(fit=True)
|
|
223
|
+
img = qr.make_image(fill_color="darkblue", back_color="white")
|
|
224
|
+
img.save(qr_path)
|
|
225
|
+
# ----------------------- QRDONE -------------
|
|
226
|
+
# #qr = qrcode.make( qrtag, width=width)
|
|
227
|
+
# qrs = qrtag.split(";")
|
|
228
|
+
# # I have nlabel and slabel: what about midlabel
|
|
229
|
+
# nlabel, midlabel, slabel= None, None, None
|
|
230
|
+
# for q in qrs:
|
|
231
|
+
# nam, *val= q.split(":")
|
|
232
|
+
# if len(val) == 0:continue
|
|
233
|
+
# # EXTRACT ID and NAME
|
|
234
|
+
# if nam.strip().find( "id") == 0:
|
|
235
|
+
# nlabel = val[0].strip()
|
|
236
|
+
# if nam.strip().find( "name") == 0:
|
|
237
|
+
# slabel = val[0].strip()
|
|
238
|
+
# if nam.strip().find( "owner") == 0:
|
|
239
|
+
# midlabel = val[0].strip()
|
|
240
|
+
# print(f"i... LABELS SN: /{fg.green}{nlabel}{fg.default}/ x /{fg.yellow}{midlabel}{fg.default}/ x /{fg.green}{slabel}{fg.default}/")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def make_bw_text_with_qr(lines,
|
|
245
|
+
qr_path,
|
|
246
|
+
width=714,
|
|
247
|
+
# font_path="/usr/share/fonts/truetype/freefont/FreeMono.ttf",
|
|
248
|
+
font_path="/usr/share/fonts/truetype/clear-sans/ClearSans-Regular.ttf",
|
|
249
|
+
font_size=None,
|
|
250
|
+
pad=13,
|
|
251
|
+
qr_max_width=350):
|
|
252
|
+
"""
|
|
253
|
+
Compose an RGB image (width x dynamic height) with:
|
|
254
|
+
- QR image placed at left (must be < qr_max_width),
|
|
255
|
+
- text block (1-3 lines) placed on the right of the QR.
|
|
256
|
+
Behavior of text layout follows your make_bw_text_image(), but horizontal
|
|
257
|
+
placement is centered inside the area to the right of the QR. x_offset is
|
|
258
|
+
computed as (qr_image_width + PAD) where PAD ~= scaled pad value.
|
|
259
|
+
Returns a PIL.Image in RGB.
|
|
260
|
+
"""
|
|
261
|
+
if not (1 <= len(lines) <= 3):
|
|
262
|
+
raise ValueError("Provide 1 to 3 lines.")
|
|
263
|
+
if not os.path.exists(qr_path):
|
|
264
|
+
raise FileNotFoundError(qr_path)
|
|
265
|
+
|
|
266
|
+
# load QR and check width
|
|
267
|
+
qr = Image.open(qr_path).convert("RGB")
|
|
268
|
+
if qr.width >= qr_max_width:
|
|
269
|
+
#raise ValueError(f"QR image width {qr.width}px >= limit {qr_max_width}px")
|
|
270
|
+
qr = qr.resize( (qr_max_width, qr_max_width) )
|
|
271
|
+
|
|
272
|
+
# font choice
|
|
273
|
+
FSIZ = font_size or 36
|
|
274
|
+
try:
|
|
275
|
+
font = ImageFont.truetype(font_path, FSIZ) if font_path else ImageFont.load_default()
|
|
276
|
+
except Exception:
|
|
277
|
+
font = ImageFont.load_default()
|
|
278
|
+
|
|
279
|
+
PAD = int((FSIZ / 36) * pad) # scaled pad
|
|
280
|
+
|
|
281
|
+
# Measure text lines using a temp image (same as original)
|
|
282
|
+
tmp = Image.new("L", (width, 2000), 255)
|
|
283
|
+
draw_tmp = ImageDraw.Draw(tmp)
|
|
284
|
+
line_sizes = []
|
|
285
|
+
for ln in lines:
|
|
286
|
+
bbox = draw_tmp.textbbox((0, 0), ln, font=font)
|
|
287
|
+
w = bbox[2] - bbox[0]
|
|
288
|
+
h = bbox[3] - bbox[1]
|
|
289
|
+
line_sizes.append((w, h))
|
|
290
|
+
|
|
291
|
+
spacing = int(0.2 * (line_sizes[0][1] if line_sizes else 12)) or 4
|
|
292
|
+
text_content_h = sum(h for _, h in line_sizes) + spacing * (len(lines) - 1)
|
|
293
|
+
text_total_h = text_content_h + PAD * 2
|
|
294
|
+
qr_total_h = qr.height + PAD * 2
|
|
295
|
+
|
|
296
|
+
# canvas height must fit both QR and text
|
|
297
|
+
canvas_h = max(text_total_h, qr_total_h)
|
|
298
|
+
canvas = Image.new("RGB", (width, canvas_h), "white")
|
|
299
|
+
draw = ImageDraw.Draw(canvas)
|
|
300
|
+
|
|
301
|
+
# place QR at left with left padding PAD, vertically centered
|
|
302
|
+
qr_x = PAD
|
|
303
|
+
qr_y = (canvas_h - qr.height) // 2
|
|
304
|
+
canvas.paste(qr, (qr_x, qr_y))
|
|
305
|
+
|
|
306
|
+
# compute x_offset = qr_image width + PAD (as requested)
|
|
307
|
+
x_offset = qr.width + PAD
|
|
308
|
+
|
|
309
|
+
# available width for text: leave right PAD margin
|
|
310
|
+
available_for_text = width - x_offset - PAD
|
|
311
|
+
if available_for_text < 0:
|
|
312
|
+
available_for_text = 0
|
|
313
|
+
|
|
314
|
+
# vertical start for text: center the text block vertically in canvas
|
|
315
|
+
y = (canvas_h - text_content_h) // 2
|
|
316
|
+
|
|
317
|
+
# draw each line centered inside the available area starting at x_offset
|
|
318
|
+
for (w, h), ln in zip(line_sizes, lines):
|
|
319
|
+
x = x_offset + max((available_for_text - w) // 2, 0)
|
|
320
|
+
draw.text((x, y), ln, font=font, fill=0)
|
|
321
|
+
y += h + spacing
|
|
322
|
+
|
|
323
|
+
return canvas
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# =====================================================================
|
|
328
|
+
#
|
|
329
|
+
# ----------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
# --- CLI ---
|
|
332
|
+
@click.command()
|
|
333
|
+
@click.argument("lines", nargs=-1, required=True)
|
|
334
|
+
@click.option("-p", "--print", "do_print", is_flag=True, default=False, help="Send to printer (calls ql570).")
|
|
335
|
+
@click.option("-d", "--device", "device", default=None, help="Printer device (defaults to first /dev/usb/lp*).")
|
|
336
|
+
@click.option("-f", "--format", "fmt", default=62, help="Format code for ql570 (default 62 at the width of 714).")
|
|
337
|
+
@click.option("--width", default=714, show_default=True, help="Total image width.")
|
|
338
|
+
@click.option("--gray", "gray", default=50, show_default=True, help="Gray bias percent (0..100), default 50.")
|
|
339
|
+
@click.option("--font-size", "font_size", default=72, show_default=True, help="Font size for text.")
|
|
340
|
+
@click.option("--qr", "qr_path", default=None, show_default=True, help="QR image path.")
|
|
341
|
+
@click.option("-q", "qr_create", is_flag=True, default=False, show_default=True, help="do QR")
|
|
342
|
+
def cli(lines, do_print, device, fmt, width, gray, font_size, qr_path, qr_create):
|
|
343
|
+
"""
|
|
344
|
+
Create a black-on-white PNG from 1-3 lines of text.
|
|
345
|
+
By default displays the image with OpenCV. Use -p to print via ql570.
|
|
346
|
+
"""
|
|
347
|
+
#print(lines, type(lines), len(lines))
|
|
348
|
+
if type(lines)is not tuple:
|
|
349
|
+
print("X... the text should be python-tuple for some reason....")
|
|
350
|
+
sys.exit(1)
|
|
351
|
+
|
|
352
|
+
if len(lines) > 3: # "A" "B" "C"
|
|
353
|
+
print("Error: max 3 lines allowed.", file=sys.stderr)
|
|
354
|
+
sys.exit(2)
|
|
355
|
+
|
|
356
|
+
if len(lines) == 1:
|
|
357
|
+
#print("Q", lines[0].find("\n"))
|
|
358
|
+
if lines[0].find("\n") > 0:
|
|
359
|
+
print("Q... contains several lines, I understand...")
|
|
360
|
+
lines = lines[0].split("\n")
|
|
361
|
+
else:
|
|
362
|
+
print("Q... lines ...I do not understand")
|
|
363
|
+
|
|
364
|
+
if len(lines) > 3: # "A" "B" "C"
|
|
365
|
+
print("Error: max 3 lines allowed.", file=sys.stderr)
|
|
366
|
+
sys.exit(2)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ------- decide on qr
|
|
370
|
+
use_qr_version = False
|
|
371
|
+
my_qr_path = None
|
|
372
|
+
if qr_path is not None: # Existing PATH
|
|
373
|
+
if os.path.exists(qr_path):
|
|
374
|
+
use_qr_version = True
|
|
375
|
+
my_qr_path = qr_path
|
|
376
|
+
#create_qr(qr_path, lines)
|
|
377
|
+
|
|
378
|
+
if qr_create:
|
|
379
|
+
my_qr_path = "/tmp/tmp_qrcode.png"
|
|
380
|
+
create_qr(my_qr_path, lines)
|
|
381
|
+
use_qr_version = True
|
|
382
|
+
|
|
383
|
+
# ----------------------------------- create image
|
|
384
|
+
lines = ["\n".join(lines) ] # BACK TO NICE \n
|
|
385
|
+
|
|
386
|
+
im = None
|
|
387
|
+
if use_qr_version:
|
|
388
|
+
im = make_bw_text_with_qr(list(lines), my_qr_path, width=714, font_size=font_size)
|
|
389
|
+
else:
|
|
390
|
+
im = make_bw_text_image(list(lines), width=714, font_size=font_size)
|
|
391
|
+
|
|
392
|
+
mono = convert_to_mono_floyd(im, gray_percent=int(gray))
|
|
393
|
+
mono = rotate_mono_90(mono, clockwise=True)
|
|
394
|
+
|
|
395
|
+
out = get_tmp_fname()
|
|
396
|
+
print(f"D.... {out}")
|
|
397
|
+
mono.save(out, format="PNG")
|
|
398
|
+
if not do_print:
|
|
399
|
+
# display with cv2
|
|
400
|
+
cvim = pil_to_cv2(im)
|
|
401
|
+
cv2.imshow("Preview - press any key to close", cvim)
|
|
402
|
+
cv2.waitKey(0)
|
|
403
|
+
cv2.destroyAllWindows()
|
|
404
|
+
print("i... Saved:", out)
|
|
405
|
+
return
|
|
406
|
+
# printing path
|
|
407
|
+
exe = get_data_path()
|
|
408
|
+
dev = device if device else check_lpx()
|
|
409
|
+
CMD = f"{exe} {dev} {fmt} {out}"
|
|
410
|
+
|
|
411
|
+
if not is_user_in_lp_group():
|
|
412
|
+
print("X... ", fg.red, "you are not in lp group", fg.default)
|
|
413
|
+
sys.exit(1)
|
|
414
|
+
runme(CMD, reallyrun=True)
|
|
415
|
+
print("i... Printed (and saved):", out)
|
|
416
|
+
|
|
417
|
+
# =====================================================================
|
|
418
|
+
#
|
|
419
|
+
# ----------------------------------------------------------------------
|
|
420
|
+
# =====================================================================
|
|
421
|
+
#
|
|
422
|
+
# ----------------------------------------------------------------------
|
|
423
|
+
# =====================================================================
|
|
424
|
+
#
|
|
425
|
+
# ----------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
if __name__ == "__main__":
|
|
428
|
+
cli()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# from importlib_resources import files, as_file
|
|
432
|
+
# import shlex # split correctly CMD
|
|
433
|
+
# import subprocess as sp
|
|
434
|
+
# import glob
|
|
435
|
+
# import tempfile
|
|
436
|
+
# from console import fg, bg
|
|
437
|
+
|
|
438
|
+
# def get_data_path():
|
|
439
|
+
# exe_path1 = None
|
|
440
|
+
|
|
441
|
+
# source = files('data').joinpath('ql570')
|
|
442
|
+
# with as_file(source) as eml:
|
|
443
|
+
# exe_path1 = str(eml)
|
|
444
|
+
# return exe_path1
|
|
445
|
+
|
|
446
|
+
# def check_lpx():
|
|
447
|
+
# prs = glob.glob("/dev/usb/lp*")
|
|
448
|
+
# print(f"D... LP devices : {prs}")
|
|
449
|
+
# return prs[0]
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# def runme(CMDi, silent = False, reallyrun=False):
|
|
453
|
+
# """
|
|
454
|
+
# run with the help of safe shlex
|
|
455
|
+
# """
|
|
456
|
+
# print("D...", "_"*(90-len(CMDi)), fg.yellow, CMDi, fg.default) # Just show on terminal
|
|
457
|
+
# CMD = shlex.split(CMDi)# .split()
|
|
458
|
+
# if not reallyrun:
|
|
459
|
+
# return "ok"
|
|
460
|
+
# res=sp.check_output( CMD ).decode("utf8")
|
|
461
|
+
# if not silent:
|
|
462
|
+
# print("i... RESULT:", res)
|
|
463
|
+
# return res
|
|
464
|
+
|
|
465
|
+
# def get_tmp_fname():
|
|
466
|
+
# """
|
|
467
|
+
# the printer understands to PNG
|
|
468
|
+
# """
|
|
469
|
+
# suffix = '.png'
|
|
470
|
+
# tmp_dir = '/tmp'
|
|
471
|
+
# temp_file = tempfile.NamedTemporaryFile(suffix=suffix, dir=tmp_dir, delete=False)
|
|
472
|
+
# temp_filename = temp_file.name
|
|
473
|
+
# temp_file.close()
|
|
474
|
+
# return temp_filename
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# def main():
|
|
478
|
+
# print("i... STARTING")
|
|
479
|
+
# exe = get_data_path()
|
|
480
|
+
# lpx = check_lpx()
|
|
481
|
+
# myformat = 62
|
|
482
|
+
# image = get_tmp_fname()
|
|
483
|
+
# CMD = f"{exe} {lpx} {myformat} {image}"
|
|
484
|
+
# runme(CMD)
|
|
485
|
+
# print("i... FINISHED")
|
|
486
|
+
|
|
487
|
+
# if __name__ == "__main__":
|
|
488
|
+
# main()
|
|
489
|
+
################################################################################
|
|
490
|
+
# # Compose QR (left) + text block (right)
|
|
491
|
+
# def compose_qr_and_text(qr_path, lines, total_width=714, pad=10, gap=10, font_path=None, font_size=36, qr_max_size=350):
|
|
492
|
+
# if not (1 <= len(lines) <= 3):
|
|
493
|
+
# raise ValueError("Provide 1..3 lines.")
|
|
494
|
+
# if not os.path.exists(qr_path):
|
|
495
|
+
# raise FileNotFoundError(qr_path)
|
|
496
|
+
# # load font
|
|
497
|
+
# try:
|
|
498
|
+
# font = ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
|
|
499
|
+
# except Exception:
|
|
500
|
+
# font = ImageFont.load_default()
|
|
501
|
+
# # measure text lines
|
|
502
|
+
# tmp_h = 4000
|
|
503
|
+
# tmp = Image.new("L", (total_width, tmp_h), 255)
|
|
504
|
+
# draw_tmp = ImageDraw.Draw(tmp)
|
|
505
|
+
# line_sizes = []
|
|
506
|
+
# for ln in lines:
|
|
507
|
+
# bbox = draw_tmp.textbbox((0, 0), ln, font=font)
|
|
508
|
+
# w = bbox[2] - bbox[0]
|
|
509
|
+
# h = bbox[3] - bbox[1]
|
|
510
|
+
# line_sizes.append((w, h))
|
|
511
|
+
# spacing = max(int(0.2 * (line_sizes[0][1] if line_sizes else 12)), 4)
|
|
512
|
+
# text_block_w = max(w for w, _ in line_sizes)
|
|
513
|
+
# text_block_h = sum(h for _, h in line_sizes) + spacing * (len(lines) - 1)
|
|
514
|
+
# # open qr and possibly scale
|
|
515
|
+
# qr = Image.open(qr_path).convert("RGB")
|
|
516
|
+
# # constrain QR to qr_max_size and to available width
|
|
517
|
+
# available_for_qr = total_width - (text_block_w + gap + pad*2)
|
|
518
|
+
# if available_for_qr <= 0:
|
|
519
|
+
# # if no room, allocate half-half
|
|
520
|
+
# available_for_qr = min(qr_max_size, total_width // 2 - pad - gap)
|
|
521
|
+
# qr_target_h = min(qr_max_size, max(text_block_h, qr.height))
|
|
522
|
+
# # scale QR to match text_block_h (if bigger) but not exceed available width/height
|
|
523
|
+
# # compute scale preserving aspect ratio
|
|
524
|
+
# resample = getattr(Image, "Resampling", Image).LANCZOS
|
|
525
|
+
# scale_h = text_block_h
|
|
526
|
+
# # ensure we don't exceed available_for_qr width
|
|
527
|
+
# # try target height = text_block_h, compute width
|
|
528
|
+
# w_ratio = qr.width / qr.height if qr.height else 1
|
|
529
|
+
# qr_w_by_h = int(scale_h * w_ratio)
|
|
530
|
+
# if qr_w_by_h > available_for_qr:
|
|
531
|
+
# # reduce height to fit available width
|
|
532
|
+
# qr_w = available_for_qr
|
|
533
|
+
# qr_h = max(1, int(qr_w / w_ratio))
|
|
534
|
+
# else:
|
|
535
|
+
# qr_w = qr_w_by_h
|
|
536
|
+
# qr_h = max(1, int(scale_h))
|
|
537
|
+
# # also ensure not exceeding qr_max_size
|
|
538
|
+
# if qr_h > qr_max_size:
|
|
539
|
+
# qr_h = qr_max_size
|
|
540
|
+
# qr_w = max(1, int(qr_h * w_ratio))
|
|
541
|
+
# qr = qr.resize((qr_w, qr_h), resample=resample)
|
|
542
|
+
# # recompute text area width to fill remaining space
|
|
543
|
+
# text_area_w = total_width - (pad*2 + gap + qr_w)
|
|
544
|
+
# if text_area_w < 1:
|
|
545
|
+
# text_area_w = max(1, total_width - (pad*2) - qr_w - gap)
|
|
546
|
+
# # create canvas height as max(qr_h, text_block_h) + pad*2
|
|
547
|
+
# canvas_h = max(qr_h, text_block_h) + pad*2
|
|
548
|
+
# canvas = Image.new("RGB", (total_width, canvas_h), "white")
|
|
549
|
+
# # paste QR at left, vertically centered
|
|
550
|
+
# qr_y = pad + (canvas_h - pad*2 - qr_h) // 2
|
|
551
|
+
# canvas.paste(qr, (pad, qr_y))
|
|
552
|
+
# # draw text block on right of QR
|
|
553
|
+
# draw = ImageDraw.Draw(canvas)
|
|
554
|
+
# text_x = pad + qr_w + gap
|
|
555
|
+
# # vertical starting position to center the text block
|
|
556
|
+
# text_y = pad + (canvas_h - pad*2 - text_block_h) // 2
|
|
557
|
+
# for (w, h), ln in zip(line_sizes, lines):
|
|
558
|
+
# x = text_x + max((text_area_w - w) // 2, 0)
|
|
559
|
+
# draw.text((x, text_y), ln, font=font, fill=0)
|
|
560
|
+
# text_y += h + spacing
|
|
561
|
+
# return canvas
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qltool
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: PRINT simple labels on ql700
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: click>=8.2.1
|
|
7
|
+
Requires-Dist: console>=0.9911
|
|
8
|
+
Requires-Dist: importlib-resources>=6.5.2
|
|
9
|
+
Requires-Dist: opencv-python>=4.12.0.88
|
|
10
|
+
Requires-Dist: pillow>=11.3.0
|
|
11
|
+
Requires-Dist: qrcode>=8.2
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# qltool
|
|
15
|
+
|
|
16
|
+
*tool to print some useful stuff with ql700 brother printer*
|
|
17
|
+
|
|
18
|
+
- It is just a wrapper around the great `ql570.c` of Asbjørn Sloth
|
|
19
|
+
Tønnesen, Marc Juul and Luca Zimmermann
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
``` {.bash org-language="sh"}
|
|
24
|
+
pip3 install qltool
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
``` {.bash org-language="sh"}
|
|
30
|
+
#---------text
|
|
31
|
+
rm /tmp/tmp_qrcode.png ; uv run src/qltool/main.py "Ahoj
|
|
32
|
+
aaa
|
|
33
|
+
456 8jgqy 789 456 456 " --font-size 55 --font-size 45
|
|
34
|
+
|
|
35
|
+
# ---- create QR
|
|
36
|
+
|
|
37
|
+
uv run src/qltool/main.py "ahoj
|
|
38
|
+
momo
|
|
39
|
+
123 456 789 jjj" -p -q
|
|
40
|
+
|
|
41
|
+
# -------- use any image/qrcode
|
|
42
|
+
uv run src/qltool/main.py "Užaj jetů" --qr /tmp/baba.png
|
|
43
|
+
|
|
44
|
+
uv run src/qltool/main.py "Už je tuná" --qr /tmp/baba.png -p
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Org-mode inventory - broken now
|
|
48
|
+
|
|
49
|
+
If this function is somewhere in the `.emacs` configfiles ...
|
|
50
|
+
|
|
51
|
+
It selects two fields fromthe TABLE and puts -q option.
|
|
52
|
+
|
|
53
|
+
You point to a line of a table and `c-c q`{.verbatim} or
|
|
54
|
+
`c-c w`{.verbatim} will view/print the code for inventory.
|
|
55
|
+
|
|
56
|
+
whatever id name owner location
|
|
57
|
+
---------- ------------- --------------------------- ------- ----------
|
|
58
|
+
123 D4-12345678 najaka blbost z inventury Pepa Home
|
|
59
|
+
|
|
60
|
+
and this is run
|
|
61
|
+
`./bin_qltool.py "id: D4-12345678; name: najaka blbost z inventury owner:Pepa " -q`
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
qltool/__init__.py,sha256=tPmKiA61zeZ_78r716j0_ZNtoNy3pQfaPX2oKmE1TZQ,52
|
|
2
|
+
qltool/main.py,sha256=AL6RuYBQUDCYARvqr-7nA3lWIUgRYFdl5g1Wf8nfiC8,20065
|
|
3
|
+
qltool-0.1.2.dist-info/METADATA,sha256=v3db_jHi_rYM1HxfnZ3ZVInUtvzYCrdsipcultEBWTQ,1611
|
|
4
|
+
qltool-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
qltool-0.1.2.dist-info/entry_points.txt,sha256=q5ksREBeeGS17aFhE-z2qUWXW5TcoTi5nO938fGiLhE,39
|
|
6
|
+
qltool-0.1.2.dist-info/RECORD,,
|