Docs: screenshots and full label designer guide; icon category tabs
- README: overview, installation, designer walkthrough with light/dark screenshots and sample output, library usage, D101 pairing note - Emoji picker tabs use emoji icons so the sidebar fits at 1280px; file actions and theme toggle move to the bottom bar
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
+50
-33
@@ -51,16 +51,17 @@ LABEL_PRESETS = [
|
|||||||
("Custom", None, None),
|
("Custom", None, None),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# (tab label, emoji-data category, tab icon)
|
||||||
EMOJI_CATEGORIES = [
|
EMOJI_CATEGORIES = [
|
||||||
("Smileys", "Smileys & Emotion"),
|
("Smileys", "Smileys & Emotion", "😀"),
|
||||||
("People", "People & Body"),
|
("People", "People & Body", "👍"),
|
||||||
("Animals", "Animals & Nature"),
|
("Animals", "Animals & Nature", "🐶"),
|
||||||
("Food", "Food & Drink"),
|
("Food", "Food & Drink", "🍎"),
|
||||||
("Travel", "Travel & Places"),
|
("Travel", "Travel & Places", "🚗"),
|
||||||
("Activities", "Activities"),
|
("Activities", "Activities", "⚽"),
|
||||||
("Objects", "Objects"),
|
("Objects", "Objects", "💡"),
|
||||||
("Symbols", "Symbols"),
|
("Symbols", "Symbols", "❤️"),
|
||||||
("Flags", "Flags"),
|
("Flags", "Flags", "🏁"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -92,8 +93,8 @@ EMOJI_RE = re.compile(f"(?:{_EMOJI_BASE}{_VS}{_SKIN}(?:{_ZWJ}{_EMOJI_BASE}{_VS})
|
|||||||
|
|
||||||
def load_emoji_db():
|
def load_emoji_db():
|
||||||
"""Return {category label: [(char, name), ...]} ordered like the emoji keyboard."""
|
"""Return {category label: [(char, name), ...]} ordered like the emoji keyboard."""
|
||||||
by_cat = {label: [] for label, _ in EMOJI_CATEGORIES}
|
by_cat = {label: [] for label, _, _ in EMOJI_CATEGORIES}
|
||||||
cat_label = {full: label for label, full in EMOJI_CATEGORIES}
|
cat_label = {full: label for label, full, _ in EMOJI_CATEGORIES}
|
||||||
for e in sorted(emoji_data_python.emoji_data, key=lambda e: e.sort_order):
|
for e in sorted(emoji_data_python.emoji_data, key=lambda e: e.sort_order):
|
||||||
label = cat_label.get(e.category)
|
label = cat_label.get(e.category)
|
||||||
if label and not e.obsoleted_by:
|
if label and not e.obsoleted_by:
|
||||||
@@ -282,7 +283,7 @@ class LabelApp(ttk.Window):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
title="Niimbot Label Designer",
|
title="Niimbot Label Designer",
|
||||||
themename=THEMES.get(self.theme_mode, "flatly"),
|
themename=THEMES.get(self.theme_mode, "flatly"),
|
||||||
minsize=(1100, 680),
|
minsize=(1200, 720),
|
||||||
)
|
)
|
||||||
self.option_add("*Font", UI_FONT)
|
self.option_add("*Font", UI_FONT)
|
||||||
self.design = Design.from_dict(self.settings.get("design", {}))
|
self.design = Design.from_dict(self.settings.get("design", {}))
|
||||||
@@ -393,19 +394,29 @@ class LabelApp(ttk.Window):
|
|||||||
self.hover_name = ttk.Label(box, text=" ", bootstyle="secondary")
|
self.hover_name = ttk.Label(box, text=" ", bootstyle="secondary")
|
||||||
self.hover_name.pack(fill="x", pady=(4, 2))
|
self.hover_name.pack(fill="x", pady=(4, 2))
|
||||||
|
|
||||||
self.nb = ttk.Notebook(box, width=420, height=260)
|
self.nb = ttk.Notebook(box, width=400, height=300)
|
||||||
self.nb.pack(fill="both", expand=True)
|
self.nb.pack(fill="both", expand=True)
|
||||||
self.emoji_index: dict[tk.Text, list[tuple[str, str]]] = {}
|
self.emoji_index: dict[tk.Text, list[tuple[str, str]]] = {}
|
||||||
for label, _ in EMOJI_CATEGORIES:
|
self._tab_icons = []
|
||||||
self._make_emoji_tab(label, self.emoji_db[label])
|
for label, _, icon in EMOJI_CATEGORIES:
|
||||||
self.search_tab = self._make_emoji_tab("Search", [])
|
photo = ImageTk.PhotoImage(emoji_thumbnail(icon, 22))
|
||||||
|
self._tab_icons.append(photo)
|
||||||
|
self._make_emoji_tab(label, self.emoji_db[label], image=photo)
|
||||||
|
self.search_tab = self._make_emoji_tab("🔍", [])
|
||||||
self.nb.hide(self.search_tab.master)
|
self.nb.hide(self.search_tab.master)
|
||||||
|
self.nb.bind("<<NotebookTabChanged>>", self._tab_changed)
|
||||||
|
|
||||||
def _make_emoji_tab(self, label, entries) -> tk.Text:
|
def _make_emoji_tab(self, label, entries, image=None) -> tk.Text:
|
||||||
frame = ttk.Frame(self.nb)
|
frame = ttk.Frame(self.nb)
|
||||||
self.nb.add(frame, text=label)
|
frame.category = label
|
||||||
|
if image is not None:
|
||||||
|
self.nb.add(frame, image=image, padding=(6, 2))
|
||||||
|
else:
|
||||||
|
self.nb.add(frame, text=label)
|
||||||
txt = tk.Text(
|
txt = tk.Text(
|
||||||
frame,
|
frame,
|
||||||
|
width=1, # let the notebook decide the width
|
||||||
|
height=1,
|
||||||
wrap="char",
|
wrap="char",
|
||||||
cursor="hand2",
|
cursor="hand2",
|
||||||
spacing1=2,
|
spacing1=2,
|
||||||
@@ -444,6 +455,10 @@ class LabelApp(ttk.Window):
|
|||||||
return self.emoji_index[txt][int(tag[1:])]
|
return self.emoji_index[txt][int(tag[1:])]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _tab_changed(self, _event=None):
|
||||||
|
frame = self.nametowidget(self.nb.select())
|
||||||
|
self.hover_name.configure(text=getattr(frame, "category", " "))
|
||||||
|
|
||||||
def _picker_hover(self, event):
|
def _picker_hover(self, event):
|
||||||
entry = self._picker_entry(event)
|
entry = self._picker_entry(event)
|
||||||
self.hover_name.configure(text=entry[1] if entry else " ")
|
self.hover_name.configure(text=entry[1] if entry else " ")
|
||||||
@@ -466,7 +481,7 @@ class LabelApp(ttk.Window):
|
|||||||
if q in n.lower()
|
if q in n.lower()
|
||||||
][:300]
|
][:300]
|
||||||
self._fill_emoji_tab(self.search_tab, hits)
|
self._fill_emoji_tab(self.search_tab, hits)
|
||||||
self.nb.add(self.search_tab.master, text=f"Search ({len(hits)})")
|
self.nb.add(self.search_tab.master, text=f"🔍 {len(hits)}")
|
||||||
self.nb.select(self.search_tab.master)
|
self.nb.select(self.search_tab.master)
|
||||||
|
|
||||||
def _build_printer_box(self, parent):
|
def _build_printer_box(self, parent):
|
||||||
@@ -519,18 +534,6 @@ class LabelApp(ttk.Window):
|
|||||||
btn("Center H", lambda: self.center("h"))
|
btn("Center H", lambda: self.center("h"))
|
||||||
btn("Center V", lambda: self.center("v"))
|
btn("Center V", lambda: self.center("v"))
|
||||||
btn("Fit", self.fit_selected)
|
btn("Fit", self.fit_selected)
|
||||||
sep()
|
|
||||||
btn("New", self.new_design, "link")
|
|
||||||
btn("Open…", self.open_design, "link")
|
|
||||||
btn("Save…", self.save_design, "link")
|
|
||||||
self.theme_btn = ttk.Button(
|
|
||||||
bar,
|
|
||||||
text="☾",
|
|
||||||
width=3,
|
|
||||||
bootstyle="secondary-link",
|
|
||||||
command=self.toggle_theme,
|
|
||||||
)
|
|
||||||
self.theme_btn.pack(side="right")
|
|
||||||
|
|
||||||
def _build_canvas(self, parent):
|
def _build_canvas(self, parent):
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
@@ -567,7 +570,7 @@ class LabelApp(ttk.Window):
|
|||||||
self.prop_text = tk.Text(
|
self.prop_text = tk.Text(
|
||||||
box,
|
box,
|
||||||
height=3,
|
height=3,
|
||||||
width=40,
|
width=20,
|
||||||
font=("DejaVu Sans", 12),
|
font=("DejaVu Sans", 12),
|
||||||
relief="flat",
|
relief="flat",
|
||||||
padx=6,
|
padx=6,
|
||||||
@@ -618,8 +621,22 @@ class LabelApp(ttk.Window):
|
|||||||
ttk.Button(
|
ttk.Button(
|
||||||
bar, text="Save PNG…", command=self.save_png, bootstyle="secondary-outline"
|
bar, text="Save PNG…", command=self.save_png, bootstyle="secondary-outline"
|
||||||
).pack(side="left", padx=(8, 0), ipady=4)
|
).pack(side="left", padx=(8, 0), ipady=4)
|
||||||
|
self.theme_btn = ttk.Button(
|
||||||
|
bar,
|
||||||
|
text="☾",
|
||||||
|
width=3,
|
||||||
|
bootstyle="secondary-link",
|
||||||
|
command=self.toggle_theme,
|
||||||
|
)
|
||||||
|
self.theme_btn.pack(side="right")
|
||||||
|
for text, cmd in (
|
||||||
|
("Save…", self.save_design),
|
||||||
|
("Open…", self.open_design),
|
||||||
|
("New", self.new_design),
|
||||||
|
):
|
||||||
|
ttk.Button(bar, text=text, command=cmd, bootstyle="link").pack(side="right")
|
||||||
ttk.Label(bar, textvariable=self.status, bootstyle="secondary").pack(
|
ttk.Label(bar, textvariable=self.status, bootstyle="secondary").pack(
|
||||||
side="right"
|
side="right", padx=(0, 12)
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- theme
|
# ---- theme
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
# `niimprint` — Niimbot Printer Client
|
# `niimprint` — Niimbot Printer Client + Label Designer
|
||||||
|
|
||||||
**Fork changelog & differences from original version:**
|
Print to Niimbot label printers (B1, B18, B21, D11, D101, D110) over Bluetooth or USB, from a CLI or a desktop label designer with full emoji support.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Fork of [AndBondStyle/niimprint](https://github.com/AndBondStyle/niimprint). Changes in this fork:
|
||||||
|
|
||||||
|
- **Label designer app** (`label_app.py`): drag-and-drop text and emoji, rotation, live 1-bit preview, light/dark themes
|
||||||
|
- Printer model table (`niimprint/models.py`) with per-model width / density / protocol quirks; adds **D101**
|
||||||
|
- Ported upstream PRs [#28](https://github.com/AndBondStyle/niimprint/pull/28) and [#12](https://github.com/AndBondStyle/niimprint/pull/12): D-series `ALLOW_PRINT_CLEAR`/`SET_QUANTITY` (fixes tall labels), print-status polling instead of a fixed sleep
|
||||||
|
- `PrinterError` raised on timeouts / rejected commands instead of returning `None`
|
||||||
|
- Tested on a D101 (firmware 20.51) over Bluetooth
|
||||||
|
|
||||||
|
Upstream's own changelog vs. the original project:
|
||||||
|
|
||||||
- Tested on Niimbot B1, B18, B21, D11, D110 and Python 3.11
|
- Tested on Niimbot B1, B18, B21, D11, D110 and Python 3.11
|
||||||
- Added transport abstraction: switch between bluetooth and USB (serial)
|
- Added transport abstraction: switch between bluetooth and USB (serial)
|
||||||
@@ -8,11 +20,24 @@
|
|||||||
- Switched to [click](https://click.palletsprojects.com/) CLI library instead of argparse
|
- Switched to [click](https://click.palletsprojects.com/) CLI library instead of argparse
|
||||||
- Integrated [pyproject.toml](https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/) and [poetry](https://python-poetry.org)
|
- Integrated [pyproject.toml](https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/) and [poetry](https://python-poetry.org)
|
||||||
- Integrated [pre-commit](https://pre-commit.com/) and [ruff](https://docs.astral.sh/ruff/), re-formatted all files
|
- Integrated [pre-commit](https://pre-commit.com/) and [ruff](https://docs.astral.sh/ruff/), re-formatted all files
|
||||||
- Miscellaneous refactoring / file renaming / etc.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Recommended method is to use [poetry](https://python-poetry.org) and install with `poetry install`. However `requirements.txt` is also provided for convenience. Project is tested on Python 3.11, but should work on other versions.
|
```
|
||||||
|
git clone https://git.gracious.one/gracious_admin/NIIMBOT-D101.git
|
||||||
|
cd NIIMBOT-D101
|
||||||
|
python -m venv --system-site-packages .venv # system Python gives you tkinter
|
||||||
|
.venv/bin/pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with [poetry](https://python-poetry.org): `poetry install`. Python 3.10+ (uses `match`); tested on 3.13.
|
||||||
|
|
||||||
|
The label designer needs two fonts, both standard on most distros:
|
||||||
|
|
||||||
|
- DejaVu Sans — `/usr/share/fonts/TTF/DejaVuSans*.ttf` (Arch) or `/usr/share/fonts/truetype/dejavu/` (Debian)
|
||||||
|
- Noto Color Emoji — `/usr/share/fonts/noto/NotoColorEmoji.ttf`
|
||||||
|
|
||||||
|
Edit `TEXT_FONTS` / `EMOJI_FONT` at the top of `label_app.py` if yours live elsewhere.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -58,6 +83,8 @@ It seems like B21 and B1 (and maybe other models?) have two bluetooth adresses.
|
|||||||
|
|
||||||
To identify which address is the correct one, run `bluetoothctl info` on the address you want to check. The incorrect one might list `UUID: Generic Access Profile` and `UUID: Generic Attribute Profile`, while the correct one will list `UUID: Serial Port`.
|
To identify which address is the correct one, run `bluetoothctl info` on the address you want to check. The incorrect one might list `UUID: Generic Access Profile` and `UUID: Generic Attribute Profile`, while the correct one will list `UUID: Serial Port`.
|
||||||
|
|
||||||
|
The D101 does the same: it advertises as `D101-<serial>` on two addresses (e.g. `94:11:02:…` and `11:02:94:…`); use the one whose `bluetoothctl info` lists `UUID: Serial Port`, then `pair` and `trust` it once.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
**B21, USB connection, 30x15 mm (240x120 px) label**
|
**B21, USB connection, 30x15 mm (240x120 px) label**
|
||||||
@@ -76,25 +103,52 @@ python niimprint -c bluetooth -a "E2:E1:08:03:09:87" -r 90 -i examples/B21_80x50
|
|||||||
|
|
||||||
[]()
|
[]()
|
||||||
|
|
||||||
|
## Label designer app
|
||||||
|
|
||||||
|
```
|
||||||
|
.venv/bin/python label_app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
| Light | Dark |
|
||||||
|
|---|---|
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
The 40 × 12 mm label above, exactly as sent to the printer (320 × 96 px, 1-bit):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Designing
|
||||||
|
|
||||||
|
- **Label size** — pick a preset (12 × 40, 12 × 30, 12 × 22, 15 × 30 mm) or *Custom* and type width / length in mm. 8 px per mm.
|
||||||
|
- **+ Text** adds a text block. Type in the *Selected item* box; multi-line is fine, and emoji typed inline are rendered too.
|
||||||
|
- **Emoji** — click any emoji in the picker to add it as its own item. The tabs are 😀 Smileys, 👍 People, 🐶 Animals, 🍎 Food, 🚗 Travel, ⚽ Activities, 💡 Objects, ❤️ Symbols, 🏁 Flags (~1,900 in total); the *Search* box filters by name; hovering shows the name.
|
||||||
|
- **Move** — drag on the canvas, arrow keys nudge 1 px (Shift = 10 px), or type X / Y. **Center H / V** snap to the middle.
|
||||||
|
- **Rotate** — ⟲ / ⟳ 90° buttons, or any angle in the *Angle* spinbox (15° steps).
|
||||||
|
- **Size / Bold / Align** — per item. **Fit** sizes the selected item to fill the label.
|
||||||
|
- **Duplicate / Delete** (or the Delete key).
|
||||||
|
- The canvas shows the dithered black-and-white image the printer will actually produce, at 2–4× zoom depending on label size. Emoji are darkened (gamma 3) so pale colours don't dither away on thermal paper.
|
||||||
|
|
||||||
|
### Printing & files
|
||||||
|
|
||||||
|
- **Print** — connects lazily and prints in the background; *Copies* repeats the label. Errors show in a dialog.
|
||||||
|
- **Save PNG…** exports the 1-bit image; **Save… / Open…** store the design as JSON; **New** clears the label.
|
||||||
|
- Printer model, connection, address, density, theme, and the last design are remembered in `~/.config/niimprint-label/settings.json`.
|
||||||
|
- ☾ / ☀ toggles light and dark themes ([ttkbootstrap](https://ttkbootstrap.readthedocs.io) *flatly* / *darkly*).
|
||||||
|
|
||||||
|
### Library use
|
||||||
|
|
||||||
|
```python
|
||||||
|
from PIL import Image
|
||||||
|
from niimprint import BluetoothTransport, PrinterClient, prepare_print
|
||||||
|
|
||||||
|
image = Image.open("label.png") # landscape, 8 px/mm
|
||||||
|
image, density = prepare_print("d101", image, 3, rotate=90)
|
||||||
|
printer = PrinterClient(BluetoothTransport("94:11:02:66:16:4D"))
|
||||||
|
printer.print_image(image, density=density, model="d101")
|
||||||
|
```
|
||||||
|
|
||||||
|
`prepare_print` clamps density to the model's maximum, rotates, and raises `ValueError` if the image is wider than the print head. `print_image` raises `PrinterError` if the printer stops responding or rejects a command.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
[MIT](https://choosealicense.com/licenses/mit/). Originally developed by [kjy00302](https://github.com/kjy00302), forked & enhanced by [AndBondStyle](https://github.com/AndBondStyle)
|
[MIT](https://choosealicense.com/licenses/mit/). Originally developed by [kjy00302](https://github.com/kjy00302), forked & enhanced by [AndBondStyle](https://github.com/AndBondStyle)
|
||||||
|
|
||||||
## Label designer app
|
|
||||||
|
|
||||||
`label_app.py` is a Tkinter app for designing and printing labels with text and emoji:
|
|
||||||
|
|
||||||
```
|
|
||||||
python label_app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
- Pick a label size preset (or custom mm)
|
|
||||||
- **+ Text** adds a text block; click any emoji in the picker (all ~1,900, searchable by name) to add it
|
|
||||||
- Click an item to select it, drag to move, arrow keys nudge (Shift = 10 px), Delete removes
|
|
||||||
- Selected item panel: edit text, size, angle (any degrees, or the ⟲/⟳ 90° buttons), X/Y, bold, alignment
|
|
||||||
- **Fit** resizes the item to fill the label; **Center H/V** aligns it
|
|
||||||
- The canvas shows the dithered 1-bit image exactly as the printer will produce it
|
|
||||||
- **Save…/Open…** store designs as JSON; the last design and printer settings are restored on launch
|
|
||||||
- ☾/☀ in the toolbar switches between light and dark themes ([ttkbootstrap](https://ttkbootstrap.readthedocs.io))
|
|
||||||
|
|
||||||
Fonts: DejaVu Sans (`/usr/share/fonts/TTF/`) for text and Noto Color Emoji (`/usr/share/fonts/noto/`) for emoji; edit `TEXT_FONTS` / `EMOJI_FONT` at the top of the file for other distros. Emoji are rendered by Pillow with the same font that prints, so the picker is WYSIWYG.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user