Skip to content

Add optional direct thermal printing to Niimbot printers from the lab… - #1445

Open
jaimelaborda wants to merge 1 commit into
Part-DB:masterfrom
jaimelaborda:feature/niimbot-thermal-printing
Open

Add optional direct thermal printing to Niimbot printers from the lab…#1445
jaimelaborda wants to merge 1 commit into
Part-DB:masterfrom
jaimelaborda:feature/niimbot-thermal-printing

Conversation

@jaimelaborda

Copy link
Copy Markdown

…el generator

Adds a "Print to Niimbot" panel to the label generator dialog that sends the generated label straight to a Niimbot thermal printer (e.g. B1) over Web Bluetooth, without a PDF print dialog or printer driver.

The existing server-side DomPDF label is reused as-is: a new Stimulus controller rasterizes the PDF preview page-by-page with pdf.js at the printer's native resolution, converts it to a 1-bit bitmap and prints it via the niimbluelib library. No changes to the PHP label pipeline are required.

The feature is disabled by default and gated behind the NIIMBOT_ENABLED environment variable, since it is only useful for users who own such a printer.

  • Add @mmote/niimbluelib and pdfjs-dist dependencies
  • New assets/controllers/pages/niimbot_print_controller.js
  • Print options (copies, density, label type, rotation, B/W threshold) in the label dialog, gracefully disabled when Web Bluetooth is unavailable
  • NIIMBOT_ENABLED env flag (off by default), wired through parameters.yaml and a Twig global, documented in .env
  • English translations and documentation

Requires a Chromium-based browser and a secure context (HTTPS or localhost).

…el generator

Adds a "Print to Niimbot" panel to the label generator dialog that sends the
generated label straight to a Niimbot thermal printer (e.g. B1) over Web
Bluetooth, without a PDF print dialog or printer driver.

The existing server-side DomPDF label is reused as-is: a new Stimulus controller
rasterizes the PDF preview page-by-page with pdf.js at the printer's native
resolution, converts it to a 1-bit bitmap and prints it via the niimbluelib
library. No changes to the PHP label pipeline are required.

The feature is disabled by default and gated behind the NIIMBOT_ENABLED
environment variable, since it is only useful for users who own such a printer.

- Add @mmote/niimbluelib and pdfjs-dist dependencies
- New assets/controllers/pages/niimbot_print_controller.js
- Print options (copies, density, label type, rotation, B/W threshold) in the
  label dialog, gracefully disabled when Web Bluetooth is unavailable
- NIIMBOT_ENABLED env flag (off by default), wired through parameters.yaml and a
  Twig global, documented in .env
- English translations and documentation

Requires a Chromium-based browser and a secure context (HTTPS or localhost).
@jbtronics

jbtronics commented Jul 17, 2026

Copy link
Copy Markdown
Member

Thanks for the PR.

I havent looked into details yet, but some remarks:

  • Most configuration should be done via the settings system, so that it can be configured via the webUI (maybe with the ability to override that via env variables). "Env variables only" settings should only be ones that are very security crticial, which I do not see here.
  • The stimulus controller is a candidate to be lazy-loaded, so that it and its dependencies, are not loaded when this feature is disabled. how critical this is dependes on the size of it. in the end it is just a comment in the code.
  • Ideally that system should be somewhat generic. Like having an controller, which handles direct thermo printer control, and the interfacing of the niimbot is just a special case. So that other printers can be added, without much changes. Like some (abstract) base controller and reusebale template fragment for handling the form and conversion to an bitmap, the niimbot is then a specialized implementation of it. This might help with implementing ZPL/EPL printers like in ZPL/EPL Support for direct label printing. #489
  • In principle dompdf also have the ability to directly render into a bitmap, so pdf.js might be avoidable. But i guess the pdf.js solution is fine.

@jaimelaborda

Copy link
Copy Markdown
Author

Thanks for the quick look — all four points make sense. Here's how I'd like to address them, with one question on each of the bigger two.

1. Configuration via the settings system

Agreed, this shouldn't be env-only — it isn't security critical. I'll move it to a #[Settings] class following the PrivacySettings pattern, so it's configurable in the web UI and still overridable via env:

#[Settings(name: "thermal_printing", label: new TM("settings.misc.thermal_printing"))]
#[SettingsIcon("fa-print")]
class ThermalPrintingSettings
{
    use SettingsTrait;

    #[SettingsParameter(
        label: new TM("settings.misc.thermal_printing.enabled"),
        description: new TM("settings.misc.thermal_printing.enabled.help"),
        envVar: 'bool:THERMAL_PRINTING_ENABLED', envVarMode: EnvVarMode::OVERWRITE)]
    public bool $enabled = false;
}

That lets me drop the partdb.label.niimbot_enabled parameter and the Twig global I added, and the template gate becomes settings_instance('thermal_printing').enabled. Default stays false.

Question: where would you prefer it to live — a new section embedded in MiscSettings, or somewhere else? I went with Misc since it sits alongside things like the KiCad/EDA settings.

2. Lazy loading

Agreed, and I think it's worth doing even though it's "just a comment", because right now the controller is bundled for everyone regardless of the flag (the require.context in stimulus_bootstrap.js picks it up either way). In an isolated production webpack build, the controller + niimbluelib + pdf.js came to ~546 KiB minified, plus a separate ~1.13 MiB pdf.js worker chunk — not something disabled installs should pay for.

I'll add /* stimulusFetch: 'lazy' */, and combined with the driver split below the printer library is only fetched when someone actually prints.

3. Making it generic

Agreed, and I'd like to get your opinion on the shape before I refactor. What I have in mind:

assets/js/thermal_printing/
    raster.js                     # PDF -> canvas -> rotate -> threshold (shared, pure functions)
    drivers/abstract_driver.js    # isSupported() / connect() / capabilities / printBitmaps() / disconnect()
    drivers/niimbot_driver.js     # wraps niimbluelib
assets/controllers/pages/thermal_print_controller.js    # one generic controller
templates/label_system/_thermal_print_panel.html.twig   # reusable fragment

The controller stays printer-agnostic: it picks a driver by id from a Stimulus value and import()s it dynamically, so only the selected driver's dependencies are downloaded. The driver reports its capabilities (dpi, printhead width, density range, supported label types) and the form fragment renders itself from those, so adding a printer means adding one module — no controller or template changes.

This should fit ZPL/EPL (#489) reasonably well, since ^GFA takes a 1-bit raster — exactly what the shared rasterizer already produces, so only the transport and the command envelope differ.

One thing worth flagging: networked ZPL printers (raw TCP/9100) can't be reached from the browser at all, so that path would need a server-side sibling of the driver interface. Direct-attached ZPL over Web Serial/USB would fit the browser-side interface fine. My inclination is to define the JS driver interface now and not add a PHP-side driver registry until someone actually implements the network path — but if you'd rather I design both seams up front, I'm happy to.

Question: does the above match what you had in mind, or would you prefer a literal abstract base Stimulus controller that niimbot_controller extends? The driver-module approach gives better code splitting (one data-controller attribute, printer chosen by value), but inheritance is closer to how the rest of the controllers are organised, so I'm happy either way.

4. dompdf rendering to a bitmap directly

I looked into this, and I don't think it works for Part-DB labels specifically — dompdf's GD backend can't render SVG. Part-DB embeds every barcode/QR as an SVG data URI (BarcodeHelper::barcodeAsSVG(), used in label_page_qr.html.twig / label_page_1d.html.twig), and the two adapters diverge there:

  • Adapter\CPDF::image() has an explicit case "svg": $this->_pdf->addSvgFromFile(...)
  • Adapter\GD::image() has no SVG case — it builds "imagecreatefrom$img_type", so an SVG ends up calling imagecreatefromsvg() and throws Function imagecreatefromsvg() not found

So a GD-rendered label would fail on essentially any label with a barcode, which is most of them. Unless I'm missing another route (Imagick on the CPDF output?), I'd stick with pdf.js.

There are two smaller arguments for rasterizing client-side anyway: the printer is attached to the client, and the correct pixel dimensions depend on capabilities that are only known after connecting (e.g. the B1 reports 203 dpi / 384 px printhead), so doing it in the browser keeps that loop local. And with lazy loading the pdf.js cost is only paid by people who actually print.

That said — if networked ZPL ever lands, server-side rasterization becomes necessary for it, so I'd expect that to appear as a sibling path rather than a replacement.


I'll hold off on the refactor until you've weighed in on 3 (and the placement question in 1), then push everything in one go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants