qltool 0.2.4__tar.gz → 0.2.7__tar.gz
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-0.2.4 → qltool-0.2.7}/.bumpversion.cfg +1 -1
- qltool-0.2.7/.pi/settings.json +52 -0
- {qltool-0.2.4 → qltool-0.2.7}/PKG-INFO +6 -2
- {qltool-0.2.4 → qltool-0.2.7}/README.md +4 -0
- {qltool-0.2.4 → qltool-0.2.7}/README.org +5 -0
- qltool-0.2.7/fence.jsonc +175 -0
- {qltool-0.2.4 → qltool-0.2.7}/pyproject.toml +1 -1
- {qltool-0.2.4 → qltool-0.2.7}/src/qltool/client.py +4 -31
- {qltool-0.2.4 → qltool-0.2.7}/src/qltool/main.py +164 -15
- {qltool-0.2.4 → qltool-0.2.7}/.gitignore +0 -0
- {qltool-0.2.4 → qltool-0.2.7}/MANIFEST.in +0 -0
- {qltool-0.2.4 → qltool-0.2.7}/src/qltool/__init__.py +0 -0
- {qltool-0.2.4 → qltool-0.2.7}/src/qltool/data/ql570 +0 -0
- {qltool-0.2.4 → qltool-0.2.7}/src/qltool/data/ql570.c +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"packages": [
|
|
3
|
+
{
|
|
4
|
+
"source": "https://github.com/jaromrax/Crossbar",
|
|
5
|
+
"autoload": false,
|
|
6
|
+
"extensions": [
|
|
7
|
+
"-src/index.ts"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"source": "https://github.com/jaromrax/pi-advisor",
|
|
12
|
+
"autoload": false,
|
|
13
|
+
"extensions": [
|
|
14
|
+
"-dist/index.js"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"source": "git:github.com/jaromrax/pi-pass-secrets",
|
|
19
|
+
"autoload": false,
|
|
20
|
+
"extensions": [
|
|
21
|
+
"-index.ts"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"source": "git:github.com/jaromrax/pi-podman-containers",
|
|
26
|
+
"autoload": false,
|
|
27
|
+
"skills": [
|
|
28
|
+
"-skills/podman-containers/SKILL.md"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"source": "npm:pi-intercom",
|
|
33
|
+
"autoload": false,
|
|
34
|
+
"extensions": [
|
|
35
|
+
"-index.ts"
|
|
36
|
+
],
|
|
37
|
+
"skills": [
|
|
38
|
+
"-skills/pi-intercom/SKILL.md"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"source": "npm:trelly",
|
|
43
|
+
"autoload": false,
|
|
44
|
+
"skills": [
|
|
45
|
+
"-skills/README.md",
|
|
46
|
+
"-skills/trelly/SKILL.md",
|
|
47
|
+
"-skills/trelly-card-display.md",
|
|
48
|
+
"-skills/trelly-mcp/SKILL.md"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: qltool
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: PRINT simple labels on ql700
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: click>=8.2.1
|
|
@@ -77,3 +77,7 @@ You point to a line of a table and `c-c q`{.verbatim} or
|
|
|
77
77
|
|
|
78
78
|
and this is run
|
|
79
79
|
`./bin_qltool.py "id: D4-12345678; name: najaka blbost z inventury owner:Pepa " -q`
|
|
80
|
+
|
|
81
|
+
\## LAST DEVELOPMENTS IN BANNER MODE mosquitto~pub~ -t qltool/print -m
|
|
82
|
+
\'KLADIVA INBUS --banner --font-size 220 -p\' uv run src/qltool/main.py
|
|
83
|
+
\$\'KLADIVA`\nINBUS`{=latex}\' --banner --font-size 220
|
|
@@ -63,3 +63,7 @@ You point to a line of a table and `c-c q`{.verbatim} or
|
|
|
63
63
|
|
|
64
64
|
and this is run
|
|
65
65
|
`./bin_qltool.py "id: D4-12345678; name: najaka blbost z inventury owner:Pepa " -q`
|
|
66
|
+
|
|
67
|
+
\## LAST DEVELOPMENTS IN BANNER MODE mosquitto~pub~ -t qltool/print -m
|
|
68
|
+
\'KLADIVA INBUS --banner --font-size 220 -p\' uv run src/qltool/main.py
|
|
69
|
+
\$\'KLADIVA`\nINBUS`{=latex}\' --banner --font-size 220
|
|
@@ -147,3 +147,8 @@ You point to a line of a table and =c-c q= or =c-c w= will view/print the code f
|
|
|
147
147
|
|
|
148
148
|
and this is run
|
|
149
149
|
~./bin_qltool.py "id: D4-12345678; name: najaka blbost z inventury owner:Pepa " -q~
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
## LAST DEVELOPMENTS IN BANNER MODE
|
|
153
|
+
mosquitto_pub -t qltool/print -m 'KLADIVA INBUS --banner --font-size 220 -p'
|
|
154
|
+
uv run src/qltool/main.py $'KLADIVA\nINBUS' --banner --font-size 220
|
qltool-0.2.7/fence.jsonc
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Starter config generated by `fence config init`; this file extends "code".
|
|
2
|
+
// Rules from "code" are inherited and not shown below.
|
|
3
|
+
// Add your project-specific overrides in this file.
|
|
4
|
+
// Run `fence --list-templates` to see available templates.
|
|
5
|
+
// Configuration reference: https://github.com/fencesandbox/fence/blob/main/docs/configuration.md
|
|
6
|
+
{
|
|
7
|
+
"network": {
|
|
8
|
+
"deniedDomains": ["lupa.cz"],
|
|
9
|
+
"allowedDomains": ["*"],
|
|
10
|
+
"allowLocalOutbound": true,
|
|
11
|
+
"allowLocalOutboundPorts": [3128, 1080],
|
|
12
|
+
"httpProxyPort":3128,
|
|
13
|
+
"socksProxyPort":1080
|
|
14
|
+
},
|
|
15
|
+
"filesystem": {
|
|
16
|
+
"defaultDenyRead": true,
|
|
17
|
+
"allowRead": [
|
|
18
|
+
"~/DATA/pv_live_rec/logs/*",
|
|
19
|
+
".",
|
|
20
|
+
"./**",
|
|
21
|
+
"/usr",
|
|
22
|
+
"/lib",
|
|
23
|
+
"/lib64",
|
|
24
|
+
"/bin",
|
|
25
|
+
"/tmp/**",
|
|
26
|
+
"/var/log/lxini/**",
|
|
27
|
+
"~/.tmux.conf",
|
|
28
|
+
"~/.bashrc",
|
|
29
|
+
"~/.password-store/**",
|
|
30
|
+
"~/.gnupg",
|
|
31
|
+
"~/.gnupg/*",
|
|
32
|
+
"~/.gnupg/**",
|
|
33
|
+
"/run/user/1000/gnupg/**",
|
|
34
|
+
"/run/user/1000/gnupg/**",
|
|
35
|
+
"~/.local/bin/env",
|
|
36
|
+
"~/.npm-global/lib/node_modules/**",
|
|
37
|
+
"~/.npm-global/bin/**",
|
|
38
|
+
"~/44_lxc/**"
|
|
39
|
+
],
|
|
40
|
+
"allowWrite": [
|
|
41
|
+
".",
|
|
42
|
+
// Temp files
|
|
43
|
+
"/tmp",
|
|
44
|
+
|
|
45
|
+
// Local cache, needed by tools like `uv`
|
|
46
|
+
"~/.cache/**",
|
|
47
|
+
|
|
48
|
+
// Claude Code
|
|
49
|
+
"~/.claude*",
|
|
50
|
+
"~/.claude/**",
|
|
51
|
+
|
|
52
|
+
// Amp
|
|
53
|
+
"~/.amp",
|
|
54
|
+
|
|
55
|
+
// Codex
|
|
56
|
+
"~/.codex/**",
|
|
57
|
+
|
|
58
|
+
// Cursor
|
|
59
|
+
"~/.cursor/**",
|
|
60
|
+
|
|
61
|
+
// OpenCode
|
|
62
|
+
"~/.opencode/**",
|
|
63
|
+
"~/.local/state/**",
|
|
64
|
+
|
|
65
|
+
// Gemini CLI
|
|
66
|
+
"~/.gemini/**",
|
|
67
|
+
|
|
68
|
+
// Factory CLI (droid)
|
|
69
|
+
"~/.factory/**",
|
|
70
|
+
|
|
71
|
+
// Pi
|
|
72
|
+
"~/.pi/**",
|
|
73
|
+
|
|
74
|
+
// Copilot
|
|
75
|
+
"~/.copilot/**",
|
|
76
|
+
|
|
77
|
+
// Kiro CLI
|
|
78
|
+
"~/.kiro/**",
|
|
79
|
+
"~/Library/Application Support/kiro-cli/**",
|
|
80
|
+
|
|
81
|
+
// Package manager caches
|
|
82
|
+
"~/.npm/_cacache",
|
|
83
|
+
"~/.npm/_npx",
|
|
84
|
+
"~/.cache",
|
|
85
|
+
"~/.bun/**",
|
|
86
|
+
|
|
87
|
+
// Cargo cache (Rust, used by Codex)
|
|
88
|
+
"~/.cargo/registry/**",
|
|
89
|
+
"~/.cargo/git/**",
|
|
90
|
+
"~/.cargo/.package-cache",
|
|
91
|
+
|
|
92
|
+
// Shell completion cache
|
|
93
|
+
"~/.zcompdump*",
|
|
94
|
+
|
|
95
|
+
// XDG directories for app configs/data
|
|
96
|
+
"~/.local/share/**",
|
|
97
|
+
"~/.config/**",
|
|
98
|
+
"~/.gnupg",
|
|
99
|
+
"~/.gnupg/*",
|
|
100
|
+
"~/.gnupg/**",
|
|
101
|
+
"/run/user/1000/gnupg/**",
|
|
102
|
+
"/run/user/1000/gnupg/**",
|
|
103
|
+
"./**"
|
|
104
|
+
],
|
|
105
|
+
"denyRead": [
|
|
106
|
+
// SSH private keys and config
|
|
107
|
+
"~/.ssh/id_*",
|
|
108
|
+
"~/.ssh/config",
|
|
109
|
+
"~/.ssh/*.pem",
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
// Cloud provider credentials
|
|
113
|
+
"~/.aws/**",
|
|
114
|
+
"~/.config/gcloud/**",
|
|
115
|
+
"~/.kube/**",
|
|
116
|
+
|
|
117
|
+
// Docker config (may contain registry auth)
|
|
118
|
+
"~/.docker/**",
|
|
119
|
+
|
|
120
|
+
// Package manager auth tokens
|
|
121
|
+
"~/.pypirc",
|
|
122
|
+
"~/.netrc",
|
|
123
|
+
"~/.git-credentials",
|
|
124
|
+
"~/.cargo/credentials",
|
|
125
|
+
"~/.cargo/credentials.toml"
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
"command": {
|
|
129
|
+
"useDefaults": true,
|
|
130
|
+
"deny": [
|
|
131
|
+
// Git commands that modify remote state
|
|
132
|
+
"git push",
|
|
133
|
+
"git reset",
|
|
134
|
+
"git clean",
|
|
135
|
+
"git checkout --",
|
|
136
|
+
"git rebase",
|
|
137
|
+
"git merge",
|
|
138
|
+
|
|
139
|
+
// Package publishing commands
|
|
140
|
+
"npm publish",
|
|
141
|
+
"pnpm publish",
|
|
142
|
+
"yarn publish",
|
|
143
|
+
"cargo publish",
|
|
144
|
+
"twine upload",
|
|
145
|
+
"gem push",
|
|
146
|
+
|
|
147
|
+
// Privilege escalation
|
|
148
|
+
"sudo",
|
|
149
|
+
|
|
150
|
+
// GitHub CLI commands that modify remote state
|
|
151
|
+
"gh pr create",
|
|
152
|
+
"gh pr merge",
|
|
153
|
+
"gh pr close",
|
|
154
|
+
"gh pr reopen",
|
|
155
|
+
"gh pr review",
|
|
156
|
+
"gh pr comment",
|
|
157
|
+
"gh release create",
|
|
158
|
+
"gh release delete",
|
|
159
|
+
"gh repo create",
|
|
160
|
+
"gh repo fork",
|
|
161
|
+
"gh repo delete",
|
|
162
|
+
"gh issue create",
|
|
163
|
+
"gh issue close",
|
|
164
|
+
"gh issue comment",
|
|
165
|
+
"gh gist create",
|
|
166
|
+
"gh workflow run",
|
|
167
|
+
"gh api",
|
|
168
|
+
"gh auth login",
|
|
169
|
+
"gh secret set",
|
|
170
|
+
"gh secret delete",
|
|
171
|
+
"gh variable set",
|
|
172
|
+
"gh variable delete"
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -42,37 +42,10 @@ def on_message(client, userdata, msg):
|
|
|
42
42
|
|
|
43
43
|
text = data.decode("utf8")
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
print(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Q = False
|
|
50
|
-
P = False
|
|
51
|
-
lines = []
|
|
52
|
-
for i, obj in enumerate(blocks):
|
|
53
|
-
|
|
54
|
-
print(f"#{i}# {obj}")
|
|
55
|
-
|
|
56
|
-
if obj.find("-") < 0:
|
|
57
|
-
lines.append(obj)
|
|
58
|
-
elif obj == "-q":
|
|
59
|
-
print("create qr")
|
|
60
|
-
Q = True
|
|
61
|
-
lines.append("-q")
|
|
62
|
-
elif obj == "-p":
|
|
63
|
-
print("print")
|
|
64
|
-
lines.append("-p")
|
|
65
|
-
P = True
|
|
66
|
-
elif obj == "--font-size":
|
|
67
|
-
FS = obj[i + 1]
|
|
68
|
-
|
|
69
|
-
if FS is None:
|
|
70
|
-
FS = 72
|
|
71
|
-
|
|
72
|
-
print( f" {lines}, {FS}, create qr ={Q}, print {P}")
|
|
73
|
-
#cli( lines, font_size=FS, qr_create=Q, do_print=P)
|
|
74
|
-
#cli( lines, qr_create=Q, do_print=P)
|
|
75
|
-
res = call_with_runner( lines )
|
|
45
|
+
args = shlex.split(text)
|
|
46
|
+
print(args)
|
|
47
|
+
|
|
48
|
+
res = call_with_runner(args)
|
|
76
49
|
for i in res:
|
|
77
50
|
print(i)
|
|
78
51
|
print("")
|
|
@@ -63,19 +63,88 @@ def is_user_in_lp_group():
|
|
|
63
63
|
#
|
|
64
64
|
# ----------------------------------------------------------------------
|
|
65
65
|
def get_data_path():
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
"""Get path to ql570 binary, works both when installed and run directly."""
|
|
67
|
+
try:
|
|
68
|
+
# Try installed package first
|
|
69
|
+
src = files("qltool").joinpath("data", "ql570")
|
|
70
|
+
with as_file(src) as p:
|
|
71
|
+
return str(p)
|
|
72
|
+
except Exception:
|
|
73
|
+
# Fall back to relative path when running directly
|
|
74
|
+
# __file__ is src/qltool/main.py, data is in src/qltool/data/
|
|
75
|
+
fallback = os.path.join(os.path.dirname(__file__), "data", "ql570")
|
|
76
|
+
if os.path.exists(fallback):
|
|
77
|
+
return fallback
|
|
78
|
+
raise
|
|
70
79
|
|
|
71
80
|
# =====================================================================
|
|
72
81
|
#
|
|
73
82
|
# ----------------------------------------------------------------------
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
def _read(f):
|
|
84
|
+
try:
|
|
85
|
+
with open(f) as fh:
|
|
86
|
+
return fh.read().strip()
|
|
87
|
+
except Exception:
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
def list_lp_candidates():
|
|
91
|
+
"""Find /dev/usb/lp* candidates, enriched with USB VID/PID via sysfs.
|
|
92
|
+
Returns list of dicts: {dev, name, vid, pid, manufacturer, product}.
|
|
93
|
+
Works even when /dev/usb is invisible but /sys/class/usbmisc is not."""
|
|
94
|
+
names = {}
|
|
95
|
+
for p in glob.glob("/sys/class/usbmisc/lp*"):
|
|
96
|
+
names[os.path.basename(p)] = p
|
|
97
|
+
for p in glob.glob("/dev/usb/lp*"):
|
|
98
|
+
names.setdefault(os.path.basename(p), p)
|
|
99
|
+
out = []
|
|
100
|
+
for name in sorted(names):
|
|
101
|
+
sysfs = f"/sys/class/usbmisc/{name}"
|
|
102
|
+
base = f"{sysfs}/device/.."
|
|
103
|
+
out.append({
|
|
104
|
+
"dev": f"/dev/usb/{name}",
|
|
105
|
+
"name": name,
|
|
106
|
+
"vid": _read(f"{base}/idVendor").lower(),
|
|
107
|
+
"pid": _read(f"{base}/idProduct").lower(),
|
|
108
|
+
"manufacturer": _read(f"{base}/manufacturer"),
|
|
109
|
+
"product": _read(f"{base}/product"),
|
|
110
|
+
})
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
def check_lpx(prefer_vid="04f9"):
|
|
114
|
+
"""Auto-detect printer device. Prefers Brother (VID 04f9), else single/first."""
|
|
115
|
+
cands = list_lp_candidates()
|
|
116
|
+
if not cands:
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
"No LP device found (checked /dev/usb/lp* + /sys/class/usbmisc/lp*). "
|
|
119
|
+
"The image was created OK - only the printer device is missing. "
|
|
120
|
+
"Try: ls -l /dev/usb/ ; lsusb -d 04f9: ; "
|
|
121
|
+
"unplug/replug + power on the printer, then "
|
|
122
|
+
"sudo modprobe usblp && ls -l /dev/usb/lp*. "
|
|
123
|
+
"If the node appears under another name, pass it with -d/--device."
|
|
124
|
+
)
|
|
125
|
+
if len(cands) == 1:
|
|
126
|
+
c = cands[0]
|
|
127
|
+
print(f"i... auto-detected printer: {c['dev']} "
|
|
128
|
+
f"({c['manufacturer']} {c['product']} {c['vid']}:{c['pid']})".strip())
|
|
129
|
+
return c["dev"]
|
|
130
|
+
# multiple: prefer Brother VID (covers QL-500/570/700/800/1100...)
|
|
131
|
+
if prefer_vid:
|
|
132
|
+
matches = [c for c in cands if c["vid"] == prefer_vid.lower()]
|
|
133
|
+
if len(matches) == 1:
|
|
134
|
+
c = matches[0]
|
|
135
|
+
print(f"i... auto-detected Brother printer: {c['dev']} "
|
|
136
|
+
f"({c['manufacturer']} {c['product']} {c['vid']}:{c['pid']})".strip())
|
|
137
|
+
return c["dev"]
|
|
138
|
+
if len(matches) > 1:
|
|
139
|
+
lst = ", ".join(m["dev"] for m in matches)
|
|
140
|
+
print(f"W... multiple Brother printers ({lst}), using {matches[0]['dev']} - "
|
|
141
|
+
f"override with -d/--device")
|
|
142
|
+
return matches[0]["dev"]
|
|
143
|
+
lst = ", ".join(f"{c['dev']} [{c['manufacturer']} {c['product']} "
|
|
144
|
+
f"{c['vid']}:{c['pid']}]".strip() for c in cands)
|
|
145
|
+
print(f"W... multiple LP devices ({lst}), using {cands[0]['dev']} - "
|
|
146
|
+
f"override with -d/--device")
|
|
147
|
+
return cands[0]["dev"]
|
|
79
148
|
|
|
80
149
|
# =====================================================================
|
|
81
150
|
#
|
|
@@ -205,6 +274,69 @@ def rotate_mono_90(img, clockwise=True):
|
|
|
205
274
|
return img.transpose(Image.ROTATE_270 if clockwise else Image.ROTATE_90)
|
|
206
275
|
|
|
207
276
|
|
|
277
|
+
# =====================================================================
|
|
278
|
+
#
|
|
279
|
+
# ----------------------------------------------------------------------
|
|
280
|
+
def make_banner_image(lines, paper_width=714, font_size=None, pad=13):
|
|
281
|
+
"""
|
|
282
|
+
Create a banner image with text running along the paper length.
|
|
283
|
+
|
|
284
|
+
Creates a horizontal text image with dimensions (text_length x 714).
|
|
285
|
+
This is NOT rotated - sent to printer as-is so text prints along paper length.
|
|
286
|
+
|
|
287
|
+
- lines: one or more strings stacked vertically
|
|
288
|
+
- paper_width: width of the paper (714px for 62mm) - used as height here
|
|
289
|
+
- font_size: font size (uses CLI font_size or default)
|
|
290
|
+
- pad: padding around text
|
|
291
|
+
|
|
292
|
+
Returns a PIL.Image in RGB mode with dimensions (text_length x 714).
|
|
293
|
+
"""
|
|
294
|
+
FSIZ = font_size or 36
|
|
295
|
+
try:
|
|
296
|
+
font = ImageFont.truetype(FONT_PATH, FSIZ) if FONT_PATH else ImageFont.load_default()
|
|
297
|
+
except Exception:
|
|
298
|
+
font = ImageFont.load_default()
|
|
299
|
+
|
|
300
|
+
PAD = int((FSIZ / 36) * pad)
|
|
301
|
+
|
|
302
|
+
if isinstance(lines, str):
|
|
303
|
+
lines = lines.split("\n")
|
|
304
|
+
lines = [line for line in lines if line != ""]
|
|
305
|
+
if not lines:
|
|
306
|
+
raise ValueError("Provide at least 1 line.")
|
|
307
|
+
|
|
308
|
+
# Measure the text
|
|
309
|
+
tmp = Image.new("L", (10000, 1000), 255)
|
|
310
|
+
draw_tmp = ImageDraw.Draw(tmp)
|
|
311
|
+
line_sizes = []
|
|
312
|
+
for line in lines:
|
|
313
|
+
bbox = draw_tmp.textbbox((0, 0), line, font=font)
|
|
314
|
+
line_sizes.append((bbox[2] - bbox[0], bbox[3] - bbox[1]))
|
|
315
|
+
|
|
316
|
+
text_width = max(w for w, _ in line_sizes)
|
|
317
|
+
spacing = int(0.2 * (line_sizes[0][1] if line_sizes else 12)) or 4
|
|
318
|
+
text_height = sum(h for _, h in line_sizes) + spacing * (len(lines) - 1)
|
|
319
|
+
print(f"D... Banner text measurements: text_width={text_width}, text_height={text_height}, lines={len(lines)}, PAD={PAD}")
|
|
320
|
+
|
|
321
|
+
# Create horizontal text image: width = text length + padding, height = paper width
|
|
322
|
+
# Banners are NOT rotated - they are sent to printer as-is with height=714
|
|
323
|
+
img_width = int(text_width + PAD * 2)
|
|
324
|
+
img_height = int(paper_width)
|
|
325
|
+
print(f"D... Banner image dimensions: {img_width}x{img_height} (NOT rotated, height stays 714)")
|
|
326
|
+
|
|
327
|
+
im = Image.new("RGB", (img_width, img_height), "white")
|
|
328
|
+
draw = ImageDraw.Draw(im)
|
|
329
|
+
|
|
330
|
+
# Center the text block vertically and each line horizontally.
|
|
331
|
+
y = (img_height - text_height) // 2
|
|
332
|
+
for (line_width, line_height), line in zip(line_sizes, lines):
|
|
333
|
+
x = PAD + max((text_width - line_width) // 2, 0)
|
|
334
|
+
draw.text((x, y), line, font=font, fill=0)
|
|
335
|
+
y += line_height + spacing
|
|
336
|
+
|
|
337
|
+
return im
|
|
338
|
+
|
|
339
|
+
|
|
208
340
|
|
|
209
341
|
|
|
210
342
|
# =====================================================================
|
|
@@ -347,7 +479,7 @@ def make_bw_text_with_qr(lines,
|
|
|
347
479
|
@click.version_option(version("qltool"), prog_name="qltool")
|
|
348
480
|
@click.argument("lines", nargs=-1, required=True)
|
|
349
481
|
@click.option("-p", "--print", "do_print", is_flag=True, default=False, help="Send to printer (calls ql570).")
|
|
350
|
-
@click.option("-d", "--device", "device", default=None, help="Printer device (
|
|
482
|
+
@click.option("-d", "--device", "device", default=None, help="Printer device (default: auto-detect Brother /dev/usb/lp*).")
|
|
351
483
|
@click.option("-f", "--format", "fmt", default=62, help="Format code for ql570 (default 62 at the width of 714).")
|
|
352
484
|
@click.option("--width", default=714, show_default=True, help="Total image width.")
|
|
353
485
|
@click.option("--gray", "gray", default=50, show_default=True, help="Gray bias percent (0..100), default 50.")
|
|
@@ -355,17 +487,19 @@ def make_bw_text_with_qr(lines,
|
|
|
355
487
|
@click.option("--qr", "qr_path", default=None, show_default=True, help="QR image path.")
|
|
356
488
|
@click.option("-q", "qr_create", is_flag=True, default=False, show_default=True, help="do QR")
|
|
357
489
|
@click.option("--address", "address", default=None, show_default=True, help="Address/URL to encode in QR code.")
|
|
358
|
-
|
|
490
|
+
@click.option("--banner", "banner", is_flag=True, default=False, show_default=True, help="Create a banner with text along the paper length.")
|
|
491
|
+
def cli(lines, do_print, device, fmt, width, gray, font_size, qr_path, qr_create, address, banner):
|
|
359
492
|
"""
|
|
360
493
|
Create a black-on-white PNG from 1-3 lines of text.
|
|
361
494
|
By default displays the image with OpenCV. Use -p to print via ql570.
|
|
495
|
+
Use --banner to create a long text banner along the paper length.
|
|
362
496
|
"""
|
|
363
497
|
#print(lines, type(lines), len(lines))
|
|
364
498
|
if type(lines)is not tuple:
|
|
365
499
|
print("X... the text should be python-tuple for some reason....")
|
|
366
500
|
sys.exit(1)
|
|
367
501
|
|
|
368
|
-
if len(lines) > 3: # "A" "B" "C"
|
|
502
|
+
if not banner and len(lines) > 3: # "A" "B" "C"
|
|
369
503
|
print("Error: max 3 lines allowed.", file=sys.stderr)
|
|
370
504
|
sys.exit(2)
|
|
371
505
|
|
|
@@ -377,7 +511,7 @@ def cli(lines, do_print, device, fmt, width, gray, font_size, qr_path, qr_create
|
|
|
377
511
|
else:
|
|
378
512
|
print("Q... lines ...I do not understand")
|
|
379
513
|
|
|
380
|
-
if len(lines) > 3: # "A" "B" "C"
|
|
514
|
+
if not banner and len(lines) > 3: # "A" "B" "C"
|
|
381
515
|
print("Error: max 3 lines allowed.", file=sys.stderr)
|
|
382
516
|
sys.exit(2)
|
|
383
517
|
|
|
@@ -400,13 +534,26 @@ def cli(lines, do_print, device, fmt, width, gray, font_size, qr_path, qr_create
|
|
|
400
534
|
lines = ["\n".join(lines) ] # BACK TO NICE \n
|
|
401
535
|
|
|
402
536
|
im = None
|
|
403
|
-
if
|
|
537
|
+
if banner:
|
|
538
|
+
# Banner mode: text runs along paper length, with optional stacked lines.
|
|
539
|
+
banner_lines = lines[0].split("\n")
|
|
540
|
+
im = make_banner_image(banner_lines, paper_width=714, font_size=font_size)
|
|
541
|
+
print(f"D... Banner image size: {im.size}")
|
|
542
|
+
elif use_qr_version:
|
|
404
543
|
im = make_bw_text_with_qr(list(lines), my_qr_path, width=714, font_size=font_size)
|
|
405
544
|
else:
|
|
406
545
|
im = make_bw_text_image(list(lines), width=714, font_size=font_size)
|
|
407
546
|
|
|
408
547
|
mono = convert_to_mono_floyd(im, gray_percent=int(gray))
|
|
409
|
-
|
|
548
|
+
print(f"D... After convert_to_mono_floyd: {mono.size}")
|
|
549
|
+
# Rotate only normal labels, NOT banners
|
|
550
|
+
# Banners are already created with correct orientation: width=variable, height=714
|
|
551
|
+
if not banner:
|
|
552
|
+
mono = rotate_mono_90(mono, clockwise=True)
|
|
553
|
+
print(f"D... After rotation for normal label: {mono.size}")
|
|
554
|
+
else:
|
|
555
|
+
print(f"D... Banner - keeping original orientation")
|
|
556
|
+
print(f"D... Final mono image size: {mono.size}, mode: {mono.mode}")
|
|
410
557
|
|
|
411
558
|
out = get_tmp_fname()
|
|
412
559
|
print(f"D.... {out}")
|
|
@@ -428,6 +575,8 @@ def cli(lines, do_print, device, fmt, width, gray, font_size, qr_path, qr_create
|
|
|
428
575
|
exe = get_data_path()
|
|
429
576
|
dev = device if device else check_lpx()
|
|
430
577
|
CMD = f"{exe} {dev} {fmt} {out}"
|
|
578
|
+
print(f"D... Printer command: {CMD}")
|
|
579
|
+
print(f"D... Image file: {out}, size: {mono.size}")
|
|
431
580
|
|
|
432
581
|
if not is_user_in_lp_group():
|
|
433
582
|
print("X... ", fg.red, "you are not in lp group", fg.default)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|