Skip to content

Instantly share code, notes, and snippets.

@jonathanarbely
Last active June 21, 2026 16:17
Show Gist options
  • Select an option

  • Save jonathanarbely/1cdf09df40a3e339eec6c3457bb4f66b to your computer and use it in GitHub Desktop.

Select an option

Save jonathanarbely/1cdf09df40a3e339eec6c3457bb4f66b to your computer and use it in GitHub Desktop.
Bluetenshops Label Print Helper - Angel Green Waldshut (waldshut.angelgreen.de) - Windows 10/11, Zebra (PDF via driver)

Label Helper - Angel Green Waldshut

Pre-configured install bundle for the operator computer at Angel Green Waldshut (waldshut.angelgreen.de).

What's configured

Setting Value
$AllowOrigin https://waldshut.angelgreen.de
$PdfPrinterName ZDesigner ZD410-300dpi ZPL (confirmed via Get-Printer 2026-06-18, USB003)
$ZplPrinterName (empty - raw-ZPL path unused)
$Port 9100
Windows version 10 / 11
PDF engine SumatraPDF (preferred) or Adobe Acrobat (fallback)
Label size same product-label size as Jestetten (set server-side in the WP plugin)

Important: Zebra prints the PDF, not raw ZPL

The bluetenshops-product-label-generator produces a PDF (DOMPDF). Waldshut uses a Zebra as its product-label printer, so the PDF is sent to the Zebra through the Zebra's Windows driver via the /print-pdf path (SumatraPDF -print-to "<Zebra>" -silent). That's why the Zebra's name goes in $PdfPrinterName, not $ZplPrinterName. The raw-ZPL /print endpoint is unused here (it's for the separate mini-label flow).

Install (one command)

$b = "$env:TEMP\bs-bootstrap.ps1"
Invoke-WebRequest -Uri 'https://gist.githubusercontent.com/jonathanarbely/1cdf09df40a3e339eec6c3457bb4f66b/raw/bootstrap.ps1' -OutFile $b
Unblock-File $b
powershell -ExecutionPolicy Bypass -File $b

Steps the bootstrap runs: install SumatraPDF (GH mirror first, sumatrapdfreader.org fallback) -> stop any old helper on 9100 -> install helper to %LOCALAPPDATA%\BluetenshopsLabelHelper\ -> URL ACL (one UAC prompt) -> Startup shortcut -> start + print /status.

Expected final line (once the printer name matches):

ok zpl= pdf=ZDesigner ZD410-300dpi ZPL pdfReady=yes pdfEngine=sumatra

Notes

  • Printer name confirmed 2026-06-18: ZDesigner ZD410-300dpi ZPL (Get-Printer, on USB003) - now set in label-print-helper.ps1. SumatraPDF -print-to needs this EXACT string; if the printer is ever renamed or its driver reinstalled, re-confirm with Get-Printer and update $PdfPrinterName.
  • Print quality / scaling. Printing a PDF to a Zebra rasterizes via the Windows driver. The plugin's PDF is sized for the shared product-label dimensions; if labels come out scaled or offset, set the Zebra's label/media size in the driver to match the stock, and confirm "Fit to page" is off.
  • Drucken flow (label-generator v0.10.0, 2026-06-18): the Drucken button opens the per-bag split dialog, then generates + sends each PDF label to this helper via /print-pdf. The old manual "Produktlabel lokal generieren" button and its bluetenshops-hide-local-label-button mu-plugin were removed.
  • See ../README.md for the full protocol reference.
# Bluetenshops Label Helper - single-step bootstrap for Angel Green Waldshut.
#
# Does everything in one go (safe to re-run):
# 1. Installs SumatraPDF silently if not already present (no winget needed)
# 2. Stops any old helper instance
# 3. Downloads the latest label-print-helper.ps1 from the gist
# 4. Grants the localhost URL ACL (one UAC prompt, first run only)
# 5. Creates the hidden-window Startup shortcut
# 6. Starts the helper and prints /status
#
# Invocation (from a normal PowerShell prompt):
# $b = "$env:TEMP\bs-bootstrap.ps1"
# Invoke-WebRequest -Uri 'https://gist.githubusercontent.com/jonathanarbely/1cdf09df40a3e339eec6c3457bb4f66b/raw/bootstrap.ps1' -OutFile $b
# Unblock-File $b
# powershell -ExecutionPolicy Bypass -File $b
$ErrorActionPreference = 'Stop'
$Port = 9100
$WorkDir = Join-Path $env:LOCALAPPDATA 'BluetenshopsLabelHelper'
$HelperPath = Join-Path $WorkDir 'label-print-helper.ps1'
$HelperUrl = 'https://gist.githubusercontent.com/jonathanarbely/1cdf09df40a3e339eec6c3457bb4f66b/raw/label-print-helper.ps1'
# SumatraPDF installer. Primary: our public GH mirror (Chocolatey-bundled installer,
# bytewise-identical to upstream). Fallback: sumatrapdfreader.org direct. The mirror
# exists because sumatrapdfreader.org had an extended 502 outage during a deploy.
$SumatraUrls = @(
'https://github.com/jonathanarbely/label-helper-deps/releases/download/v1.0/SumatraPDF-3.6.1-64-install_x64.exe',
'https://www.sumatrapdfreader.org/dl/rel/3.5.2/SumatraPDF-3.5.2-64-install.exe'
)
Write-Host "===== Bluetenshops Label Helper bootstrap ====="
Write-Host ""
# 1) Ensure SumatraPDF is installed -----------------------------------------------------
$sumatraCandidates = @(
(Join-Path $env:LOCALAPPDATA 'SumatraPDF\SumatraPDF.exe'),
(Join-Path $env:LOCALAPPDATA 'Programs\SumatraPDF\SumatraPDF.exe'),
(Join-Path $env:ProgramFiles 'SumatraPDF\SumatraPDF.exe'),
(Join-Path ${env:ProgramFiles(x86)} 'SumatraPDF\SumatraPDF.exe')
)
$haveSumatra = $sumatraCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $haveSumatra) {
$g = Get-Command 'SumatraPDF.exe' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if ($g) { $haveSumatra = $g.Source }
}
if (-not $haveSumatra) {
Write-Host "[1/6] SumatraPDF not found - downloading installer..."
$installer = Join-Path $env:TEMP 'SumatraPDF-install.exe'
$downloaded = $false
foreach ($url in $SumatraUrls) {
Write-Host " trying $url"
try {
Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing -TimeoutSec 60
if ((Get-Item $installer).Length -gt 1000000) {
$downloaded = $true
Write-Host " downloaded $((Get-Item $installer).Length) bytes"
break
}
} catch {
Write-Warning " $url failed: $_"
}
}
if (-not $downloaded) {
Write-Warning "All SumatraPDF download URLs failed. Install SumatraPDF manually from https://www.sumatrapdfreader.org/ and re-run this bootstrap."
throw "SumatraPDF download failed"
}
Unblock-File $installer
# /S runs silent install. The Chocolatey-bundled installer is the official upstream
# binary and accepts /S the same way. If silent install needs elevation it cannot get,
# the installer returns non-zero - retry without /S so the user gets the GUI.
$p = Start-Process -FilePath $installer -ArgumentList '/S' -Wait -PassThru -NoNewWindow
if ($p.ExitCode -ne 0) {
Write-Warning "Silent install returned exit $($p.ExitCode); retrying without /S (per-user install)..."
Start-Process -FilePath $installer -Wait -NoNewWindow
}
Remove-Item $installer -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
$haveSumatra = $sumatraCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $haveSumatra) {
Write-Warning "SumatraPDF installation could not be verified. The helper will still install - please install SumatraPDF manually from https://www.sumatrapdfreader.org/ and then re-run this bootstrap."
} else {
Write-Host " installed to $haveSumatra"
}
} else {
Write-Host "[1/6] SumatraPDF already present at $haveSumatra"
}
# 2) Stop any running helper ------------------------------------------------------------
Write-Host "[2/6] Stopping any old helper on port $Port..."
$existing = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty OwningProcess -Unique
foreach ($procId in $existing) { Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue }
Start-Sleep -Seconds 1
# 3) Install helper script --------------------------------------------------------------
Write-Host "[3/6] Installing helper to $WorkDir"
New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null
Invoke-WebRequest -Uri $HelperUrl -OutFile $HelperPath
Unblock-File $HelperPath
# 4) URL ACL (elevation required, first run only) ---------------------------------------
$urlAclCheck = (netsh http show urlacl url="http://localhost:$Port/") -join "`n"
if ($urlAclCheck -notmatch [regex]::Escape("$env:USERDOMAIN\$env:USERNAME")) {
Write-Host "[4/6] Granting URL ACL for http://localhost:$Port/ (one UAC prompt)..."
$aclScript = @"
netsh http add urlacl url=http://localhost:$Port/ user='$env:USERDOMAIN\$env:USERNAME'
netsh http add urlacl url=http://127.0.0.1:$Port/ user='$env:USERDOMAIN\$env:USERNAME'
"@
$aclTmp = Join-Path $env:TEMP 'bs-helper-acl.ps1'
$aclScript | Out-File -FilePath $aclTmp -Encoding utf8
Start-Process powershell -Verb RunAs -ArgumentList "-ExecutionPolicy Bypass -File `"$aclTmp`"" -Wait
Remove-Item $aclTmp -Force -ErrorAction SilentlyContinue
} else {
Write-Host "[4/6] URL ACL already granted; skipping."
}
# 5) Startup shortcut -------------------------------------------------------------------
$startup = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
New-Item -ItemType Directory -Path $startup -Force | Out-Null # some stripped Win11 user profiles lack this folder
$lnk = Join-Path $startup 'Mini-Label Helper.lnk'
$WshShell = New-Object -ComObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut($lnk)
$Shortcut.TargetPath = 'powershell.exe'
$Shortcut.Arguments = "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$HelperPath`""
$Shortcut.WorkingDirectory = $WorkDir
$Shortcut.Description = 'Bluetenshops label print helper'
$Shortcut.Save()
Write-Host "[5/6] Startup shortcut: $lnk"
# 6) Start helper and verify ------------------------------------------------------------
Start-Process powershell.exe -WindowStyle Hidden -ArgumentList '-ExecutionPolicy','Bypass','-File',$HelperPath
Start-Sleep -Seconds 3
try {
$r = Invoke-WebRequest -Uri "http://localhost:$Port/status" -UseBasicParsing -TimeoutSec 5
Write-Host ""
Write-Host "[6/6] ===== SUCCESS ====="
Write-Host $r.Content
Write-Host ""
Write-Host "Helper will auto-start on next login. Try printing from the shop admin now."
} catch {
Write-Warning "[6/6] Helper did not respond on /status. Check the deployed copy at $HelperPath and confirm the printer name matches Get-Printer."
throw
}

Handoff: debug the Zebra label printer at Angel Green Waldshut

For: a local Claude Code instance running ON the Waldshut operator PC (Windows, C:\Users\shop). From: Mac CC (owns the bluetenshops repo, the gist, and the canonical helper). Date: 2026-06-18 (updated with live findings). Mission: get product labels physically printing on the ZD410 at Waldshut, then confirm the "Drucken" button flow.

You are running hands-on on the machine with the printer attached, with a person standing by at the printer to load media and check printouts. Mac CC cannot see the printer; you can. Diagnose locally, find the exact fix, report it back. Do not push to GitHub — see "Ownership split" at the end.


CURRENT STATUS — read this first (2026-06-18)

The current blocker is HARDWARE/MEDIA, not the helper. Symptom reported: the Zebra prints nothing even on a manual Windows print — that path doesn't use our helper at all, so the problem is the printer/driver/media layer.

Confirmed facts:

  • Exact printer (Get-Printer): ZDesigner ZD410-300dpi ZPL — a Zebra ZD410, 300 dpi, ZDesigner ZPL driver, on USB003. Direct-thermal (no ribbon).
  • Loaded media (from the roll label): LW11356 = DYMO LabelWriter 11356 format, 41 × 89 mm, die-cut, direct-thermal, 300/roll. This is DYMO-format media in a Zebra — the ZD410 must be calibrated to these die-cut gaps or it will feed blank / not print. This is the #1 suspect for "prints nothing."
  • Helper config is already correct and deployed to source: $PdfPrinterName = 'ZDesigner ZD410-300dpi ZPL' is live in the repo (bluetenshops commit aa78265) and the gist 1cdf09df40a3e339eec6c3457bb4f66b. The copy on this machine may still hold the old Zebra ZD410 guess — once the printer physically prints, re-run the bootstrap one-liner (below) so the deployed helper picks up the exact name, then restart it.
  • Label-generator is now v0.10.0 (PR #72): the Drucken button itself opens the split dialog, generates one PDF per bag, and POSTs each to this helper's /print-pdf. (The old manual "Produktlabel lokal generieren" button + its hide mu-plugin are gone.)

Do hardware first (sections below), THEN the helper re-run + Drucken test. Don't debug the helper while the printer can't print at all.

Bootstrap re-run (after the printer prints physically):

$b = "$env:TEMP\bs-bootstrap.ps1"
Invoke-WebRequest -Uri 'https://gist.githubusercontent.com/jonathanarbely/1cdf09df40a3e339eec6c3457bb4f66b/raw/bootstrap.ps1' -OutFile $b
Unblock-File $b
powershell -ExecutionPolicy Bypass -File $b

UPDATE 2 (2026-06-18) — ZPL path chosen; current blocker = "accepted but nothing prints"

Decision: product label → Zebra via native ZPL image (^GFA), driver-independent (approved). Don't pursue the PDF→driver path. Reference converter: scripts/waldshut/pdf-to-zpl.py (rasterize PDF→1-bit 300dpi→^GFA, rotate 89×41 landscape into the 41×89 portrait web). Production will do the same in the plugin via php-imagick.

Current symptom: posting our test ZPL to the helper /print?printer=ZDesigner%20ZD410-300dpi%20ZPL returns printed (winspool accepted the RAW bytes) but nothing physically comes out. Note: earlier the GDI/PDF path DID feed labels (blank/mis-sized), so the device works and feeds.

Your job (local CC on DBK15): bisect "accepted-but-no-output." In order:

  1. Minimal ZPL smoke test (tiny, rules out graphic size/RAM):
    $mini = "^XA^LH0,0^PW488^FO30,40^A0N,60,60^FDZD410 TEST^FS^FO30,130^A0N,34,34^FD41x89 raw ZPL ok^FS^XZ"
    Invoke-RestMethod -Method Post -Uri 'http://localhost:9100/print?printer=ZDesigner%20ZD410-300dpi%20ZPL' -Body $mini -ContentType 'text/plain'
    • Prints → raw-ZPL pass-through + device OK → the big ^GFA is too large for the ZD410 at 300dpi → tell Mac CC to drop to 203dpi / optimize (~DG/compressed ^GFA).
    • Nothing → continue.
  2. Queue / device state: Get-PrintJob -PrinterName 'ZDesigner ZD410-300dpi ZPL' (stuck/errored?), printer not "offline"/paused, USB003 present, physical status LED (solid green vs red/amber). Clear stuck jobs.
  3. Printer self-test (power off, hold FEED, power on ~2s, release) → does the device print its own config label? If blank → SmartCal for the DYMO die-cut media (FEED+PAUSE ~2s, or ZDesigner driver → Tools → Calibrate) so it can find label boundaries.
  4. RAW pass-through question (likely culprit): the ZDesigner driver rendered the earlier PDF via GDI, but may not pass our RAW ZPL straight through. If minimal ZPL won't print but the self-test does, add a "Generic / Text Only" printer on USB003 and send ZPL to that (raw), or enable ZPL pass-through in the ZDesigner driver. Report which works — Mac CC will point $ZplPrinterName / the ?printer= at the working queue.

Report back: which step changed behavior + a photo of any printout. Mac CC then tunes the converter (dpi/rotation/threshold/darkness) and builds the plugin ZPL mode. Guardrails (ASCII, $pdfEngineLabel, don't push to GitHub) unchanged.


How the print path works

WP admin (waldshut.angelgreen.de) "Drucken"
  -> browser fetches the product-label PDF (same-origin, cookies auth)
  -> POST http://localhost:9100/print-pdf  (raw PDF bytes)
  -> helper writes a temp .pdf, then runs:
       SumatraPDF.exe -print-to "<PdfPrinterName>" -silent "<temp.pdf>"
  -> Zebra prints via its Windows driver

Key fact: the bluetenshops-product-label-generator plugin outputs a PDF, not ZPL. So the Zebra is driven as a normal Windows printer receiving a rasterized PDF — NOT via raw ZPL. That's why the Zebra's name lives in $PdfPrinterName, and the raw-ZPL /print endpoint is unused here.

Where everything is (this machine)

  • Helper script (the live one): %LOCALAPPDATA%\BluetenshopsLabelHelper\label-print-helper.ps1
  • Auto-start shortcut: %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\Mini-Label Helper.lnk
  • SumatraPDF: %LOCALAPPDATA%\SumatraPDF\SumatraPDF.exe
  • Listener: http://localhost:9100 (endpoints /status, /print, /print-pdf)
  • Public source of truth for this shop (read-only for you): gist 1cdf09df40a3e339eec6c3457bb4f66b
    • https://gist.githubusercontent.com/jonathanarbely/1cdf09df40a3e339eec6c3457bb4f66b/raw/label-print-helper.ps1

Current config (top of the helper — already correct in source)

$Port            = 9100
$ZplPrinterName  = ''                            # raw-ZPL path unused (product label is a PDF)
$PdfPrinterName  = 'ZDesigner ZD410-300dpi ZPL'  # CONFIRMED via Get-Printer (USB003)
$AllowOrigin     = 'https://waldshut.angelgreen.de'

Name is confirmed and correct in repo+gist. SumatraPDF -print-to needs this EXACT string. The on-machine copy may still say Zebra ZD410 until the bootstrap is re-run.


Suspected root causes for "prints NOTHING", most-likely first

The printer doesn't print even on a manual Windows print, so start at the hardware/media layer — the helper is downstream of all of this.

  1. DYMO die-cut media not calibrated on the Zebra (TOP SUSPECT). Loaded roll is DYMO LW11356 (41×89 mm die-cut). The ZD410's gap sensor must learn this label pitch via SmartCal, or it can't find label boundaries and feeds blank / won't print. Fix: calibrate (below).
  2. Printer not "ready" / offline / paused / stuck queue. Status LED not solid green; or Windows shows the queue offline / a stuck job. Manual print "does nothing" classically = job sitting in an offline queue.
  3. Darkness too low / wrong side. Direct-thermal prints on the heat-sensitive side only; if labels are in upside-down, or darkness is near 0, output is blank. (DYMO stock + Zebra default darkness can be too light.)
  4. Media physically misfeeding. DYMO roll core/width vs the ZD410 media guides — labels not tracking under the sensor.
  5. Wrong/!= ZDesigner driver mode. The ZPL driver rasterizes a PDF fine, but verify it's the standard ZDesigner driver and not mis-set.
  6. Size mismatch (won't cause "nothing", but fix before declaring done): the product-label PDF was historically 89×36 mm (Jestetten "Large Address"); loaded media is 41×89 mm. Once it prints, check the label size in the driver matches the stock and the plugin's page size, and disable "fit to page".

Helper-layer causes (only relevant AFTER the printer prints physically): stale helper on port 9100 (/status missing pdfEngine=), or pdfEngine=none (SumatraPDF not found). Fix by re-running the bootstrap + relogin.


Diagnostic recipe — bisect hardware vs Windows FIRST

Step A — printer self-test (bypasses Windows entirely). Person at the printer: Power the ZD410 off. Hold FEED, power on, keep holding ~2s until it starts printing, release.

  • Config label prints -> head + media physically OK -> the issue is calibration/darkness/Windows (Steps B–D).
  • Blank feeds / nothing -> media side / wrong labels / needs calibration / hardware (Step B then re-test).

Step B — SmartCal media calibration (teaches the ZD410 the DYMO die-cut gap):

  • Button method (ZD410): with the printer ready, press and hold FEED + PAUSE together for ~2 seconds until it calibrates (feeds 1–2 labels). (If this ZD410 has only a FEED button, use the driver method.) Confirm against the ZD410 quick-start if buttons differ.
  • Driver method (preferred, you can do this on the PC): Zebra Setup Utilities → Open Printer Tools → Action → Calibrate Media, or the ZDesigner driver Printing Preferences → Tools → Calibrate. If Zebra Setup Utilities isn't installed, the driver Preferences route works.
  • After calibration, press FEED once: it should advance exactly one label and stop at the gap. If it feeds multiple/continuous, calibration didn't take — recheck media seating under the sensor.

Step C — Windows print path (only once the self-test/FEED behaves):

Get-Printer | Select-Object Name, DriverName, PortName, PrinterStatus
Get-PrintJob -PrinterName 'ZDesigner ZD410-300dpi ZPL'   # any stuck jobs?

Clear stuck jobs; ensure the queue isn't "Use Printer Offline"/paused. Then Print Test Page (driver Properties → General). If the test page prints, raise Darkness and set label size = 41×89 mm in Printing Preferences if it's faint/scaled.

Step D — our path (only after C prints a real label):

# direct through SumatraPDF, exact name, bypassing WordPress:
& "$env:LOCALAPPDATA\SumatraPDF\SumatraPDF.exe" -print-to "ZDesigner ZD410-300dpi ZPL" -silent "C:\path\to\any.pdf"
# then re-run the bootstrap (top of this doc) so the deployed helper has the confirmed name, restart it, check:
(Invoke-WebRequest http://localhost:9100/status -UseBasicParsing).Content   # expect ...pdfReady=yes pdfEngine=sumatra
# finally: trigger Drucken in the shop admin and confirm a real label.

If /print-pdf itself errors, capture the real cause by watching the helper's window output, or temporarily run the helper in a visible window:

Get-NetTCPConnection -LocalPort 9100 -State Listen | %{ Stop-Process -Id $_.OwningProcess -Force }
powershell -ExecutionPolicy Bypass -File "$env:LOCALAPPDATA\BluetenshopsLabelHelper\label-print-helper.ps1"
# (leave this window open; trigger a Drucken from the shop; read the [hh:mm:ss] PDF lines / errors)

Applying a fix (local, to test)

Edit the LIVE copy and restart it:

notepad "$env:LOCALAPPDATA\BluetenshopsLabelHelper\label-print-helper.ps1"   # e.g. correct $PdfPrinterName
# then restart:
Get-NetTCPConnection -LocalPort 9100 -State Listen | %{ Stop-Process -Id $_.OwningProcess -Force }
Start-Process powershell -WindowStyle Hidden -ArgumentList '-ExecutionPolicy','Bypass','-File',"$env:LOCALAPPDATA\BluetenshopsLabelHelper\label-print-helper.ps1"
Start-Sleep 2; (Invoke-WebRequest http://localhost:9100/status -UseBasicParsing).Content

Guardrails (do NOT skip)

  • Keep the .ps1 pure ASCII. PowerShell 5.1 on German Windows reads a no-BOM .ps1 as cp1252; an em-dash or umlaut in the source becomes mojibake and breaks parsing. Use - not , ue not ü, etc.
  • Do not reintroduce the $pdfEngine casing bug. PS 5.1 variable names are case-insensitive: $pdfEngine and $PdfEngine are the SAME variable. The /status label local is deliberately named $pdfEngineLabel so it doesn't clobber the $PdfEngine engine hashtable. Leave it.
  • Do not push to the bluetenshops repo or edit the gist. That's Mac CC's job (keeps repo, gist, and the per-shop memory in sync, and lets other shops benefit). You don't need GitHub access for any of this.

Ownership split / report-back

  • You (local CC): everything on this machine — Get-Printer, driver settings, calibration, SumatraPDF print tests, editing the LOCAL helper to find the working config, confirming a clean label prints from the actual Drucken button.
  • Mac CC: lands your validated fix into bluetenshops/scripts/waldshut/label-print-helper.ps1 + the gist 1cdf09df40a3e339eec6c3457bb4f66b + the memory runbook, so it persists across reinstalls.

When done, report back (to Jonathan, who relays): (1) the exact root cause, (2) the exact config/code change that fixed it (e.g. the real $PdfPrinterName, or the driver setting), (3) /status before & after, (4) whether it was our software or the Zebra driver/media. Mac CC then makes it permanent.

# Installs the Bluetenshops Mini-Label print helper on a Windows pharmacy
# computer with a USB-attached Zebra ZD410 (or compatible ZPL printer).
#
# What this does:
# 1. Copies label-print-helper.ps1 alongside this script into a stable
# location under %LOCALAPPDATA%\BluetenshopsLabelHelper.
# 2. Grants this user the URL ACL needed to bind localhost:9100 without
# admin (one-time, runs elevated).
# 3. Creates a hidden-window Startup shortcut so it auto-launches on login.
# 4. Starts the helper now and pings /status to confirm.
#
# Usage:
# 1. Edit label-print-helper.ps1 and change $AllowOrigin / $PrinterName
# if the shop or printer name differs from the default.
# 2. Right-click this file -> "Run with PowerShell" (or run from a normal
# PowerShell prompt - it self-elevates only the URL ACL step).
#
# Re-running is safe; existing files and shortcuts are overwritten.
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$Source = Join-Path $ScriptDir 'label-print-helper.ps1'
if (-not (Test-Path $Source)) {
throw "label-print-helper.ps1 not found next to this installer at $ScriptDir"
}
$InstallDir = Join-Path $env:LOCALAPPDATA 'BluetenshopsLabelHelper'
$Target = Join-Path $InstallDir 'label-print-helper.ps1'
Write-Host "Installing helper to $InstallDir"
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
Copy-Item -Path $Source -Destination $Target -Force
# 1) URL ACL (admin required; run once)
$acl = (netsh http show urlacl url=http://localhost:9100/) -join "`n"
if ($acl -notmatch [regex]::Escape("$env:USERDOMAIN\$env:USERNAME")) {
Write-Host "Granting URL ACL for http://localhost:9100/ (UAC prompt)..."
$aclScript = @"
netsh http add urlacl url=http://localhost:9100/ user='$env:USERDOMAIN\$env:USERNAME'
netsh http add urlacl url=http://127.0.0.1:9100/ user='$env:USERDOMAIN\$env:USERNAME'
"@
$tmp = Join-Path $env:TEMP 'bs-helper-acl.ps1'
$aclScript | Out-File -FilePath $tmp -Encoding utf8
Start-Process powershell -Verb RunAs -ArgumentList "-ExecutionPolicy Bypass -File `"$tmp`"" -Wait
} else {
Write-Host "URL ACL already granted; skipping."
}
# 2) Startup shortcut
$startup = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
New-Item -ItemType Directory -Path $startup -Force | Out-Null # some stripped Win11 user profiles lack this folder
$lnk = Join-Path $startup 'Mini-Label Helper.lnk'
$WshShell = New-Object -ComObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut($lnk)
$Shortcut.TargetPath = 'powershell.exe'
$Shortcut.Arguments = "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$Target`""
$Shortcut.WorkingDirectory = $InstallDir
$Shortcut.Description = 'Listens on localhost:9100, sends ZPL to local Zebra printer'
$Shortcut.Save()
Write-Host "Startup shortcut: $lnk"
# 3) Stop any old instance and start the new one
$existing = Get-NetTCPConnection -LocalPort 9100 -State Listen -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty OwningProcess -Unique
foreach ($p in $existing) { Stop-Process -Id $p -Force -ErrorAction SilentlyContinue }
Start-Sleep -Seconds 1
Start-Process powershell.exe -ArgumentList "-WindowStyle","Hidden","-ExecutionPolicy","Bypass","-File",$Target | Out-Null
Start-Sleep -Seconds 4
# 4) Verify
try {
$r = Invoke-WebRequest -Uri 'http://localhost:9100/status' -UseBasicParsing -TimeoutSec 5
Write-Host ""
Write-Host "SUCCESS: $($r.Content)"
Write-Host "Helper will auto-start on next login."
} catch {
Write-Warning "Helper did not respond on /status. Check the printer name in label-print-helper.ps1 matches an installed Windows printer (Get-Printer)."
throw
}
# Bluetenshops Label Print Helper - Angel Green Waldshut
#
# Pre-configured for:
# Shop: https://waldshut.angelgreen.de
# Printer: Zebra label printer. The product-label-generator outputs a PDF, so the
# Zebra receives it via the /print-pdf path (SumatraPDF -> Windows driver),
# NOT raw ZPL. Same product-label size as Jestetten.
# Windows: 10/11
#
# Tiny HTTP listener on localhost that supports:
# POST /print-pdf - body = PDF -> silent print to the Zebra via its Windows driver (product labels)
# POST /print - body = ZPL -> raw-ZPL path, UNUSED here (product label is a PDF)
# GET /status - health check
#
# Run:
# powershell -ExecutionPolicy Bypass -File label-print-helper.ps1
#
# Auto-start on login is handled by install-helper.ps1 (Startup shortcut).
$ErrorActionPreference = 'Stop'
$Port = 9100
$ZplPrinterName = '' # raw-ZPL /print path unused (product label is a PDF)
$PdfPrinterName = 'ZDesigner ZD410-300dpi ZPL' # confirmed via Get-Printer 2026-06-18 (Zebra ZD410 300dpi, USB003)
$AllowOrigin = 'https://waldshut.angelgreen.de'
# Locate a silent-print PDF engine. SumatraPDF (free, https://www.sumatrapdfreader.org/)
# is preferred - lightweight, native silent print. Adobe Acrobat / Reader is a fallback.
# This is called at startup AND on each /print-pdf request, so installing Sumatra
# while the helper is already running just works on the next print attempt.
function Find-PdfEngine {
$sumatraCandidates = @(
(Join-Path $env:LOCALAPPDATA 'SumatraPDF\SumatraPDF.exe'),
(Join-Path $env:LOCALAPPDATA 'Programs\SumatraPDF\SumatraPDF.exe'),
(Join-Path $env:LOCALAPPDATA 'Programs\SumatraPDF\SumatraPDF-3.exe'),
(Join-Path $env:ProgramFiles 'SumatraPDF\SumatraPDF.exe'),
(Join-Path ${env:ProgramFiles(x86)} 'SumatraPDF\SumatraPDF.exe')
)
foreach ($p in $sumatraCandidates) {
if ($p -and (Test-Path $p)) { return @{ Path = $p; Type = 'sumatra' } }
}
# PATH fallback - covers any non-standard install location
$cmd = Get-Command 'SumatraPDF.exe' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if ($cmd) { return @{ Path = $cmd.Source; Type = 'sumatra' } }
$acrobatCandidates = @(
'C:\Program Files\Adobe\Acrobat DC\Acrobat\Acrobat.exe',
'C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe',
'C:\Program Files\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe'
)
foreach ($p in $acrobatCandidates) {
if (Test-Path $p) { return @{ Path = $p; Type = 'acrobat' } }
}
return $null
}
$PdfEngine = Find-PdfEngine
$AcrobatExe = if ($PdfEngine) { $PdfEngine.Path } else { $null } # legacy alias
# --- Raw printer P/Invoke (winspool.drv) ---------------------------------
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class RawPrinter {
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public class DOCINFO {
[MarshalAs(UnmanagedType.LPWStr)] public string pDocName;
[MarshalAs(UnmanagedType.LPWStr)] public string pOutputFile;
[MarshalAs(UnmanagedType.LPWStr)] public string pDataType;
}
[DllImport("winspool.Drv", EntryPoint="OpenPrinterW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool OpenPrinter(string p, out IntPtr h, IntPtr pd);
[DllImport("winspool.Drv", EntryPoint="ClosePrinter", SetLastError=true, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool ClosePrinter(IntPtr h);
[DllImport("winspool.Drv", EntryPoint="StartDocPrinterW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool StartDocPrinter(IntPtr h, Int32 lvl, [In, MarshalAs(UnmanagedType.LPStruct)] DOCINFO di);
[DllImport("winspool.Drv", EntryPoint="EndDocPrinter", SetLastError=true, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool EndDocPrinter(IntPtr h);
[DllImport("winspool.Drv", EntryPoint="StartPagePrinter", SetLastError=true, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool StartPagePrinter(IntPtr h);
[DllImport("winspool.Drv", EntryPoint="EndPagePrinter", SetLastError=true, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool EndPagePrinter(IntPtr h);
[DllImport("winspool.Drv", EntryPoint="WritePrinter", SetLastError=true, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool WritePrinter(IntPtr h, IntPtr buf, Int32 cnt, out Int32 written);
public static bool Send(string printer, byte[] bytes) {
IntPtr h;
var di = new DOCINFO { pDocName = "Mini Label", pDataType = "RAW" };
if (!OpenPrinter(printer, out h, IntPtr.Zero)) return false;
try {
if (!StartDocPrinter(h, 1, di)) return false;
try {
if (!StartPagePrinter(h)) return false;
IntPtr buf = Marshal.AllocCoTaskMem(bytes.Length);
try {
Marshal.Copy(bytes, 0, buf, bytes.Length);
Int32 written;
if (!WritePrinter(h, buf, bytes.Length, out written)) return false;
} finally { Marshal.FreeCoTaskMem(buf); }
EndPagePrinter(h);
} finally { EndDocPrinter(h); }
} finally { ClosePrinter(h); }
return true;
}
}
"@
# --- HTTP listener -------------------------------------------------------
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://localhost:$Port/")
$listener.Prefixes.Add("http://127.0.0.1:$Port/")
$listener.Start()
Write-Host "Label helper listening on http://localhost:$Port"
Write-Host " ZPL printer: $ZplPrinterName"
Write-Host " PDF printer: $PdfPrinterName (via $(if ($PdfEngine) { "$($PdfEngine.Type): $(Split-Path -Leaf $PdfEngine.Path)" } else { 'NO PDF ENGINE FOUND - install SumatraPDF or Acrobat Reader' }))"
function Set-Cors($resp) {
$resp.Headers.Add('Access-Control-Allow-Origin', $AllowOrigin)
$resp.Headers.Add('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
$resp.Headers.Add('Access-Control-Allow-Headers', 'Content-Type')
$resp.Headers.Add('Vary', 'Origin')
}
function Send-Text($resp, $code, $text) {
$resp.StatusCode = $code
$resp.ContentType = 'text/plain; charset=utf-8'
$bytes = [Text.Encoding]::UTF8.GetBytes($text)
$resp.ContentLength64 = $bytes.Length
$resp.OutputStream.Write($bytes, 0, $bytes.Length)
$resp.OutputStream.Close()
}
while ($listener.IsListening) {
try {
$ctx = $listener.GetContext()
$req = $ctx.Request
$resp = $ctx.Response
Set-Cors $resp
# CORS preflight
if ($req.HttpMethod -eq 'OPTIONS') {
$resp.StatusCode = 204
$resp.Close()
continue
}
# Health check - used by the dialog to detect the helper
if ($req.Url.AbsolutePath -eq '/status' -and $req.HttpMethod -eq 'GET') {
if (-not $PdfEngine) { $PdfEngine = Find-PdfEngine } # late-installed Sumatra/Acrobat picked up here
$pdfReady = if ($PdfEngine) { 'yes' } else { 'no' }
# Local must NOT case-collide with the script-scope $PdfEngine hashtable;
# PowerShell 5.1 treats $pdfEngine and $PdfEngine as the SAME variable, and
# the earlier `$pdfEngine = ...Type` would clobber $PdfEngine with a string,
# making the very next /print-pdf call hit Start-Process -FilePath $null.
# See bluetenshops/scripts/README.md "PS 5.1 case-insensitivity trap".
$pdfEngineLabel = if ($PdfEngine) { $PdfEngine.Type } else { 'none' }
Send-Text $resp 200 "ok zpl=$ZplPrinterName pdf=$PdfPrinterName pdfReady=$pdfReady pdfEngine=$pdfEngineLabel"
continue
}
# Raw-ZPL print endpoint - unused at Waldshut (product label is a PDF, printed via /print-pdf)
if ($req.Url.AbsolutePath -eq '/print' -and $req.HttpMethod -eq 'POST') {
$reader = New-Object IO.StreamReader $req.InputStream, ([Text.Encoding]::UTF8)
$zpl = $reader.ReadToEnd()
$reader.Close()
if (-not $zpl) { Send-Text $resp 400 'empty body'; continue }
# Override printer with ?printer=... if provided (e.g. ZD421 alongside ZD410)
$targetZplPrinter = $req.QueryString['printer']
if (-not $targetZplPrinter) { $targetZplPrinter = $ZplPrinterName }
if (-not $targetZplPrinter) { Send-Text $resp 500 'no zpl printer configured'; continue }
$bytes = [Text.Encoding]::ASCII.GetBytes($zpl)
$ok = [RawPrinter]::Send($targetZplPrinter, $bytes)
if ($ok) {
Write-Host ("[{0}] ZPL printed {1} bytes -> {2}" -f (Get-Date -Format HH:mm:ss), $bytes.Length, $targetZplPrinter)
Send-Text $resp 200 'printed'
} else {
Write-Host ("[{0}] ZPL FAILED - printer offline?" -f (Get-Date -Format HH:mm:ss))
Send-Text $resp 500 'print failed'
}
continue
}
# PDF print endpoint (Dymo)
if ($req.Url.AbsolutePath -eq '/print-pdf' -and $req.HttpMethod -eq 'POST') {
if (-not $PdfEngine) { $PdfEngine = Find-PdfEngine } # rescan in case Sumatra/Acrobat got installed after the helper started
if (-not $PdfEngine) { Send-Text $resp 500 'no pdf engine installed (install SumatraPDF or Acrobat Reader)'; continue }
# Override printer with ?printer=... if provided
$targetPrinter = $req.QueryString['printer']
if (-not $targetPrinter) { $targetPrinter = $PdfPrinterName }
# Read PDF bytes from request body to a temp file
$tmpPdf = Join-Path $env:TEMP ("bs-label-" + [Guid]::NewGuid().ToString() + ".pdf")
$fs = [IO.File]::Create($tmpPdf)
$req.InputStream.CopyTo($fs)
$fs.Close()
$size = (Get-Item $tmpPdf).Length
if ($size -lt 100) {
Remove-Item $tmpPdf -Force -ErrorAction SilentlyContinue
Send-Text $resp 400 'pdf too small'
continue
}
try {
if ($PdfEngine.Type -eq 'sumatra') {
# SumatraPDF: -print-to "Printer" -silent "file.pdf"
$argString = "-print-to `"$targetPrinter`" -silent `"$tmpPdf`""
} else {
# Acrobat: /n = new instance, /t = silent print to named printer, then close
$argString = "/n /t `"$tmpPdf`" `"$targetPrinter`""
}
$proc = Start-Process -FilePath $PdfEngine.Path `
-ArgumentList $argString `
-PassThru -WindowStyle Hidden
# Engine should exit on its own; give it up to 30 s
if (-not $proc.WaitForExit(30000)) {
$proc.Kill()
Write-Host ("[{0}] PDF TIMEOUT killed {1}" -f (Get-Date -Format HH:mm:ss), $PdfEngine.Type)
Send-Text $resp 500 "$($PdfEngine.Type) timeout"
continue
}
Write-Host ("[{0}] PDF printed {1} bytes -> {2} (via {3})" -f (Get-Date -Format HH:mm:ss), $size, $targetPrinter, $PdfEngine.Type)
Send-Text $resp 200 'printed'
} finally {
Remove-Item $tmpPdf -Force -ErrorAction SilentlyContinue
}
continue
}
Send-Text $resp 404 'not found'
} catch {
Write-Host "Error: $_"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment