From 2b8eae7ae9e0dfea0d2952c66991afeb2e30a078 Mon Sep 17 00:00:00 2001 From: owlot <95369289+owlot@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:45:08 +0200 Subject: [PATCH 1/3] Add GapLength CUPS option for the TSPL gap/black-mark distance The 3mm gap sent by GAP/BLINE was hardcoded, but many small die-cut labels use a shorter physical gap (as low as ~1.5mm). A mismatch makes the firmware overshoot hunting for the next boundary, wasting 1-2 labels before it resyncs. GapLength (tenths of mm) is now a PPD/CUPS option, defaulting to 30 (3.0mm) to preserve existing behavior. --- README.md | 7 +++++++ ppd/tspl-label.ppd | 17 +++++++++++++++++ src/rastertotspl.c | 23 +++++++++++++++++------ tests/smoke.sh | 13 +++++++++++-- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2dd6fc9..9049ad1 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ The Pi renders, so **clients never install a driver** — they just add the shar | **Darkness** | `0`–`15` (default 8) | `DENSITY` | | **Print Speed** | `1`–`6` in/sec (default 4) · Printer default (sends nothing) | `SPEED` | | **Media tracking** | **Die-cut (gap)** · Black-mark · Continuous · Printer setting | `GAP` / `BLINE` | +| **Gap/black-mark length** | `1.5`–`5.0` mm (default 3.0mm) | the `` in `GAP`/`BLINE` | | **Resolution** | `203` / `300` dpi | — | Loaded **black-mark or continuous stock** instead of die-cut labels? Set it per queue — @@ -193,6 +194,12 @@ TSPL firmwares skip or garble labels when `SIZE` disagrees with the stock. Note sends no boundary command at all — and GAP/BLINE **persist in printer memory**, so such a queue inherits whatever the last job set (e.g. `GAP 0` from a Continuous queue sharing the printer). +The **3.0mm gap default is only a guess** — many small die-cut labels use a shorter gap (as low as +~1.5mm). A mismatch here doesn't stop printing outright; it makes the firmware overshoot hunting for +the next gap, wasting 1-2 labels before it resyncs. Measure your stock's actual gap and set it with +`-o GapLength=` (e.g. `-o GapLength=20` for 2.0mm), or bake it into the queue like the +other options below. +
Two queues: crisp labels + a "photo" (Gathering) queue diff --git a/ppd/tspl-label.ppd b/ppd/tspl-label.ppd index 96dc05e..d1d1677 100644 --- a/ppd/tspl-label.ppd +++ b/ppd/tspl-label.ppd @@ -111,6 +111,23 @@ *MediaTracking PrinterDefault/Use printer setting: "" *CloseUI: *MediaTracking +*OpenUI *GapLength/Gap/Black-mark length (mm): PickOne +*OrderDependency: 46 AnySetup *GapLength +*% Physical length of the gap (die-cut) or black mark on the label stock, sent +*% as the second SIZE-boundary argument (GAP mm / BLINE mm). Wrong +*% value = firmware overshoots hunting for the next boundary and wastes +*% labels. Measure your stock; 3mm is a common default but many small +*% die-cut labels use ~1.5-2mm. Ignored when Media tracking is Continuous +*% or Use printer setting. +*DefaultGapLength: 30 +*GapLength 15/1.5 mm: "" +*GapLength 20/2.0 mm: "" +*GapLength 25/2.5 mm: "" +*GapLength 30/3.0 mm (default): "" +*GapLength 40/4.0 mm: "" +*GapLength 50/5.0 mm: "" +*CloseUI: *GapLength + *OpenUI *Darkness/Darkness: PickOne *OrderDependency: 50 AnySetup *Darkness *DefaultDarkness: 8 diff --git a/src/rastertotspl.c b/src/rastertotspl.c index b044978..c6b9ec9 100644 --- a/src/rastertotspl.c +++ b/src/rastertotspl.c @@ -24,7 +24,8 @@ * Darkness (0..15) -> DENSITY * PrintSpeed (10..60 = ips x10) -> SPEED (in/sec); 0 = omit (printer default) * MediaTracking Gap / BlackMark / Continuous / PrinterDefault - * -> GAP 3 mm / BLINE 3 mm / GAP 0 / (omitted) + * -> GAP mm / BLINE mm / GAP 0 / (omitted) + * GapLength (tenths of mm, e.g. 30 = 3.0mm) -> the above; default 3.0mm * Horizontal,Vertical (dots) -> REFERENCE * PrintMode 0 None / 2 Diffusion / 3 Gathering / 4 ErrorDiffusion / 5 Default * -> halftone used to flatten 8bpp grey into 1bpp dots. @@ -110,21 +111,31 @@ int main(int argc, char *argv[]) int href = opt_int(ppd, num_options, options, "Horizontal", 0); int vref = opt_int(ppd, num_options, options, "Vertical", 0); + /* GapLength: physical gap/black-mark length in tenths of mm (30 = 3.0mm). + * The stock 3mm assumption is wrong often enough on small die-cut labels + * (seen as low as ~1.5-2mm) that a mismatch makes the firmware overshoot + * hunting for the next boundary, wasting 1-2 labels before it resyncs. */ + int gap_tenths = opt_int(ppd, num_options, options, "GapLength", 30); + if (gap_tenths < 0) gap_tenths = 0; + double gap_mm = gap_tenths / 10.0; + /* MediaTracking -> the boundary command. GAP/BLINE select the sensor; * sending GAP to continuous or black-mark stock makes the firmware hunt * for a gap that never comes (feeds a label + margin, then errors), so an * unrecognized value must not silently fall through without a warning. - * Resolved to a static string here because ppdClose frees the choice. */ - const char *track = "GAP 3 mm,0 mm\r\n"; /* Gap (die-cut) */ + * Resolved to a fixed buffer here because ppdClose frees the choice. */ + char track[32] = ""; + snprintf(track, sizeof track, "GAP %.1f mm,0 mm\r\n", gap_mm); /* Gap (die-cut) */ { const char *v = cupsGetOption("MediaTracking", num_options, options); ppd_choice_t *c; if (!v && ppd && (c = ppdFindMarkedChoice(ppd, "MediaTracking")) != NULL) v = c->choice; if (v && *v) { - if (!strcasecmp(v, "BlackMark")) track = "BLINE 3 mm,0 mm\r\n"; - else if (!strcasecmp(v, "Continuous")) track = "GAP 0 mm,0 mm\r\n"; - else if (!strcasecmp(v, "PrinterDefault")) track = ""; /* stored setting */ + if (!strcasecmp(v, "BlackMark")) + snprintf(track, sizeof track, "BLINE %.1f mm,0 mm\r\n", gap_mm); + else if (!strcasecmp(v, "Continuous")) strcpy(track, "GAP 0 mm,0 mm\r\n"); + else if (!strcasecmp(v, "PrinterDefault")) track[0] = '\0'; /* stored setting */ else if (strcasecmp(v, "Gap")) fprintf(stderr, "WARNING: unknown MediaTracking '%s' — assuming " "Gap (die-cut); use Gap, BlackMark, Continuous or " diff --git a/tests/smoke.sh b/tests/smoke.sh index 789d8d4..ed74405 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -26,7 +26,7 @@ od -An -v -tx1 < "$OUT" | tr -d ' \n' > "$OUT.hex" # --- the TSPL header, line by line (12x8 px @300dpi -> 1x1 mm; note the # spec-required space before "mm") --- -for cmd in 'SIZE 1 mm,1 mm' 'GAP 3 mm,0 mm' 'DENSITY 8' 'SPEED 4' \ +for cmd in 'SIZE 1 mm,1 mm' 'GAP 3.0 mm,0 mm' 'DENSITY 8' 'SPEED 4' \ 'DIRECTION 0,0' 'REFERENCE 0,0' 'CLS'; do grep -q "^$cmd" "$OUT.txt" || fail "missing TSPL command: $cmd" done @@ -53,7 +53,7 @@ grep -q '^PRINT 1,3$' "$OUT.txt" && fail "argv[4] copies leaked into PRINT" # --- option handling: BlackMark -> BLINE (no GAP), PrintSpeed=0 -> no SPEED --- src/rastertotspl 1 tester smoke 1 'MediaTracking=BlackMark PrintSpeed=0' \ < "$OUT.ras" 2>/dev/null | tr -d '\r' > "$OUT.txt" -grep -q '^BLINE 3 mm,0 mm' "$OUT.txt" || fail "BlackMark should emit BLINE" +grep -q '^BLINE 3.0 mm,0 mm' "$OUT.txt" || fail "BlackMark should emit BLINE" grep -q '^GAP' "$OUT.txt" && fail "BlackMark must not also emit GAP" grep -q '^SPEED' "$OUT.txt" && fail "PrintSpeed=0 must omit SPEED" @@ -63,4 +63,13 @@ src/rastertotspl 1 tester smoke 1 'MediaTracking=Continuous PrintSpeed=9' \ grep -q '^GAP 0 mm,0 mm' "$OUT.txt" || fail "Continuous should emit GAP 0" grep -q '^SPEED 6$' "$OUT.txt" || fail "PrintSpeed=9 should clamp to SPEED 6" +# --- GapLength (tenths of mm): custom value on Gap and BlackMark tracking --- +src/rastertotspl 1 tester smoke 1 'GapLength=20' \ + < "$OUT.ras" 2>/dev/null | tr -d '\r' > "$OUT.txt" +grep -q '^GAP 2.0 mm,0 mm' "$OUT.txt" || fail "GapLength=20 should emit GAP 2.0 mm" + +src/rastertotspl 1 tester smoke 1 'MediaTracking=BlackMark GapLength=15' \ + < "$OUT.ras" 2>/dev/null | tr -d '\r' > "$OUT.txt" +grep -q '^BLINE 1.5 mm,0 mm' "$OUT.txt" || fail "GapLength=15 should emit BLINE 1.5 mm" + echo "smoke test OK" From c06c94daf7d7b82d1451ec68f153d31afa67a4ae Mon Sep 17 00:00:00 2001 From: owlot <95369289+owlot@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:51:33 +0200 Subject: [PATCH 2/3] Continuous mode: feed the full label+gap pitch, not just the label On Continuous stock there's no sensor to stop the feed at the real boundary, so the printer trusts SIZE's height as the whole feed pitch. Sending only the label's own height underfeeds by the physical gap, landing further into the next label on every subsequent print. GapLength is now added to SIZE's height when MediaTracking=Continuous (it still isn't sent as a GAP/BLINE command there, since Continuous means no sensing at all). Found while chasing a specific printer (2e3c:5757, a very short 12mm/2mm-gap label) whose gap sensor couldn't reliably track that pitch even after calibration, forcing a switch to Continuous mode - where this underfeed then showed up as consistently landing 2-3mm short of the next label. The underlying math bug is generic to any TSPL printer run in Continuous mode, but I'm not sure how common that setup is among this driver's users - flagging that uncertainty rather than assuming it's broadly needed. --- README.md | 7 +++++++ src/rastertotspl.c | 22 +++++++++++++++++++--- tests/smoke.sh | 11 ++++++++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9049ad1..a952fcb 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,13 @@ the next gap, wasting 1-2 labels before it resyncs. Measure your stock's actual `-o GapLength=` (e.g. `-o GapLength=20` for 2.0mm), or bake it into the queue like the other options below. +On **Continuous** stock there's no sensor to stop the feed at the real label boundary, so the printer +just trusts `SIZE`'s height as the full feed pitch. Sending only the label's own height undershoots by +the physical gap between labels — each print lands a bit further into the next label. To compensate, +`GapLength` is added to `SIZE`'s height on Continuous queues (it isn't sent as a `GAP`/`BLINE` command +there, since Continuous means no sensing at all); set it to your stock's actual inter-label spacing — +`0` if labels butt up against each other with no gap at all. +
Two queues: crisp labels + a "photo" (Gathering) queue diff --git a/src/rastertotspl.c b/src/rastertotspl.c index c6b9ec9..d931b66 100644 --- a/src/rastertotspl.c +++ b/src/rastertotspl.c @@ -25,7 +25,12 @@ * PrintSpeed (10..60 = ips x10) -> SPEED (in/sec); 0 = omit (printer default) * MediaTracking Gap / BlackMark / Continuous / PrinterDefault * -> GAP mm / BLINE mm / GAP 0 / (omitted) - * GapLength (tenths of mm, e.g. 30 = 3.0mm) -> the above; default 3.0mm + * GapLength (tenths of mm, e.g. 30 = 3.0mm) -> the above; default 3.0mm. + * On Continuous stock (no gap sensor to stop + * the feed on its own) GapLength is instead + * added to SIZE's height, so the printer + * feeds the full label+gap pitch per label + * instead of landing mid-label next print. * Horizontal,Vertical (dots) -> REFERENCE * PrintMode 0 None / 2 Diffusion / 3 Gathering / 4 ErrorDiffusion / 5 Default * -> halftone used to flatten 8bpp grey into 1bpp dots. @@ -125,6 +130,7 @@ int main(int argc, char *argv[]) * unrecognized value must not silently fall through without a warning. * Resolved to a fixed buffer here because ppdClose frees the choice. */ char track[32] = ""; + int continuous = 0; snprintf(track, sizeof track, "GAP %.1f mm,0 mm\r\n", gap_mm); /* Gap (die-cut) */ { const char *v = cupsGetOption("MediaTracking", num_options, options); @@ -134,7 +140,10 @@ int main(int argc, char *argv[]) if (v && *v) { if (!strcasecmp(v, "BlackMark")) snprintf(track, sizeof track, "BLINE %.1f mm,0 mm\r\n", gap_mm); - else if (!strcasecmp(v, "Continuous")) strcpy(track, "GAP 0 mm,0 mm\r\n"); + else if (!strcasecmp(v, "Continuous")) { + strcpy(track, "GAP 0 mm,0 mm\r\n"); + continuous = 1; + } else if (!strcasecmp(v, "PrinterDefault")) track[0] = '\0'; /* stored setting */ else if (strcasecmp(v, "Gap")) fprintf(stderr, "WARNING: unknown MediaTracking '%s' — assuming " @@ -268,7 +277,14 @@ int main(int argc, char *argv[]) * CRLF is the cheapest way back to command mode (pdf2tspl et al). */ int wmm = (int)lround((double)W * 25.4 / resx); int hmm = (int)lround((double)H * 25.4 / resy); - printf("\r\nSIZE %d mm,%d mm\r\n", wmm, hmm); + /* Continuous stock has no gap sensor, so the printer can't stop on its + * own at the next label — it trusts SIZE's height as the full feed + * pitch. Sending just the label height (hmm) undershoots by however + * big the physical inter-label gap really is, landing mid-label on + * the next print. Gap/BlackMark modes don't need this: the sensor + * finds the real boundary regardless of what SIZE says. */ + int size_hmm = continuous ? hmm + (int)lround(gap_mm) : hmm; + printf("\r\nSIZE %d mm,%d mm\r\n", wmm, size_hmm); fputs(track, stdout); printf("DENSITY %d\r\n", darkness); if (speed_ips >= 1) diff --git a/tests/smoke.sh b/tests/smoke.sh index ed74405..0b2ac61 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -57,11 +57,20 @@ grep -q '^BLINE 3.0 mm,0 mm' "$OUT.txt" || fail "BlackMark should emit BLINE" grep -q '^GAP' "$OUT.txt" && fail "BlackMark must not also emit GAP" grep -q '^SPEED' "$OUT.txt" && fail "PrintSpeed=0 must omit SPEED" -# --- continuous media -> GAP 0; out-of-range speed clamps to 6 ips --- +# --- continuous media -> GAP 0; out-of-range speed clamps to 6 ips; SIZE +# height gets the default 3mm GapLength added since there's no sensor to +# stop the feed at the real (unknown-to-the-printer) label boundary --- src/rastertotspl 1 tester smoke 1 'MediaTracking=Continuous PrintSpeed=9' \ < "$OUT.ras" 2>/dev/null | tr -d '\r' > "$OUT.txt" grep -q '^GAP 0 mm,0 mm' "$OUT.txt" || fail "Continuous should emit GAP 0" grep -q '^SPEED 6$' "$OUT.txt" || fail "PrintSpeed=9 should clamp to SPEED 6" +grep -q '^SIZE 1 mm,4 mm$' "$OUT.txt" || fail "Continuous should add default 3mm GapLength to SIZE height" + +# --- continuous + custom GapLength=20 (2.0mm) -> SIZE height is label + 2mm, +# not label + the 3mm default --- +src/rastertotspl 1 tester smoke 1 'MediaTracking=Continuous GapLength=20' \ + < "$OUT.ras" 2>/dev/null | tr -d '\r' > "$OUT.txt" +grep -q '^SIZE 1 mm,3 mm$' "$OUT.txt" || fail "Continuous should add custom GapLength to SIZE height" # --- GapLength (tenths of mm): custom value on Gap and BlackMark tracking --- src/rastertotspl 1 tester smoke 1 'GapLength=20' \ From 4878f6003c0dd995e414ac74f3bb1f6aeead292b Mon Sep 17 00:00:00 2001 From: Sergei Romanov Date: Mon, 7 Sep 2026 12:50:04 +1000 Subject: [PATCH 3/3] GapLength: integer whole-mm, bare-mm input, range guards; FixedPitch instead of padding Continuous Whole-millimetre gaps go out as "GAP 3 mm,0 mm" again, so the default stream is byte-identical to 1.3.4; fractions ("GAP 2.5 mm") are built from integer tenths, which is in the TSC spec and locale-proof. A bare 1..9 or a value with a decimal point is read as millimetres, like PrintSpeed's bare ips. Values under 1 mm on a sensor mode would go out as GAP 0, which the firmware takes as "continuous" and remembers, so they now warn and fall back to 3 mm; over 25.4 mm (the spec maximum) clamps. Continuous no longer pads SIZE: a continuous roll has no gap, and the page height already is the feed length, so the +3 mm default would have changed every existing Continuous queue after upgrade. The blind-feed case (die-cut stock whose gap the sensor cannot hold) is now its own MediaTracking choice, FixedPitch: GAP 0 plus SIZE = label + GapLength, summed in tenths and rounded once so nothing accumulates label after label. PPD text now matches the code (first GAP argument, not second; used on FixedPitch, not ignored), the INFO log shows the SIZE height, tracking and gap actually sent, and the smoke test covers the guards, the bare mm forms, PrinterDefault, and FixedPitch. --- README.md | 39 +++++++----- ppd/tspl-label.ppd | 28 +++++---- src/rastertotspl.c | 153 +++++++++++++++++++++++++++++++-------------- tests/smoke.sh | 68 ++++++++++++-------- 4 files changed, 189 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index a952fcb..4cb6129 100644 --- a/README.md +++ b/README.md @@ -182,8 +182,8 @@ The Pi renders, so **clients never install a driver** — they just add the shar | **Print Mode** (halftone) | **Default** (threshold — crisp text/barcodes) · **Gathering** (dither — greys/photos) · None · Diffusion · Error Diffusion | — (rendered into the bitmap) | | **Darkness** | `0`–`15` (default 8) | `DENSITY` | | **Print Speed** | `1`–`6` in/sec (default 4) · Printer default (sends nothing) | `SPEED` | -| **Media tracking** | **Die-cut (gap)** · Black-mark · Continuous · Printer setting | `GAP` / `BLINE` | -| **Gap/black-mark length** | `1.5`–`5.0` mm (default 3.0mm) | the `` in `GAP`/`BLINE` | +| **Media tracking** | **Die-cut (gap)** · Black-mark · Continuous · Fixed pitch (die-cut, sensor off) · Printer setting | `GAP` / `BLINE` | +| **Gap / black-mark length** | `1.5`–`5` mm (default 3 mm) | the length in `GAP`/`BLINE`; on Fixed pitch, added to `SIZE` | | **Resolution** | `203` / `300` dpi | — | Loaded **black-mark or continuous stock** instead of die-cut labels? Set it per queue — @@ -194,18 +194,24 @@ TSPL firmwares skip or garble labels when `SIZE` disagrees with the stock. Note sends no boundary command at all — and GAP/BLINE **persist in printer memory**, so such a queue inherits whatever the last job set (e.g. `GAP 0` from a Continuous queue sharing the printer). -The **3.0mm gap default is only a guess** — many small die-cut labels use a shorter gap (as low as -~1.5mm). A mismatch here doesn't stop printing outright; it makes the firmware overshoot hunting for -the next gap, wasting 1-2 labels before it resyncs. Measure your stock's actual gap and set it with -`-o GapLength=` (e.g. `-o GapLength=20` for 2.0mm), or bake it into the queue like the -other options below. - -On **Continuous** stock there's no sensor to stop the feed at the real label boundary, so the printer -just trusts `SIZE`'s height as the full feed pitch. Sending only the label's own height undershoots by -the physical gap between labels — each print lands a bit further into the next label. To compensate, -`GapLength` is added to `SIZE`'s height on Continuous queues (it isn't sent as a `GAP`/`BLINE` command -there, since Continuous means no sensing at all); set it to your stock's actual inter-label spacing — -`0` if labels butt up against each other with no gap at all. +**Gap length.** The `3 mm` in `GAP 3 mm,0 mm` is the TSC factory default and what every other TSPL +driver sends, but small die-cut labels are often cut with a **2 mm** gap, the minimum TSC rates its +sensors for. If labels lose register, measure the gap and set it: `-o GapLength=20` (tenths of a +millimetre; a bare `-o GapLength=2` or `-o GapLength=2.5` is read as millimetres). Values under 1 mm +are refused on the sensor modes: TSPL takes `GAP 0` as "continuous", and the printer remembers it. + +**Fixed pitch.** Some clone sensors cannot hold a very short pitch (a 12 mm label with a 2 mm gap, say): +the first label prints, the next feed faults, whatever `GAP` says. `-o MediaTracking=FixedPitch` +takes the sensor out of the loop: it sends `GAP 0` like Continuous but puts the full **label + gap** +pitch in `SIZE` (from `GapLength`), so the printer feeds blind and stays in register. Plain +**Continuous** is untouched: on a continuous roll the page height *is* the feed length. + +Two things come with running blind. The **last label stops short of the tear bar** — positioning to +tear-off needs the gap sensor, so it sits half out until the next job pushes it through; that is the +printer, not a lost job. And the pitch is only as good as your measurement: `GapLength` has to match +the real gap or the image walks a little further along the roll with every label. Measure it, print a +few, and check the last one still lands where the first did. Set the queue back to **Die-cut (gap)** +and the printer re-syncs on the next job.
Two queues: crisp labels + a "photo" (Gathering) queue @@ -225,7 +231,10 @@ sudo lpadmin -p HZD950-Photo -E -v tspl://auto -P /usr/share/ppd/tspl/tspl-label Baked into the queue default, this works even for driverless clients (AirPrint/IPP-Everywhere) that can't show the option menus — they just pick the right queue. Values: **PrintMode** `5`=Default `3`=Gathering `0`=None `2`=Diffusion `4`=ErrorDiffusion · **Darkness** `0`–`15` · **PrintSpeed** = in/sec ×10 -(`0` = leave it to the printer) · **MediaTracking** `Gap`/`BlackMark`/`Continuous`/`PrinterDefault`. +(`0` = leave it to the printer) · **MediaTracking** `Gap`/`BlackMark`/`Continuous`/`FixedPitch`/`PrinterDefault` +· **GapLength** = mm ×10 (`20` = 2 mm). `lpadmin -o GapLength=` only keeps values the PPD lists +(`15 20 25 30 40 50`); for any other value use `-o GapLength-default=17`, which CUPS then applies to +every job on the queue.
diff --git a/ppd/tspl-label.ppd b/ppd/tspl-label.ppd index d1d1677..ede2d3d 100644 --- a/ppd/tspl-label.ppd +++ b/ppd/tspl-label.ppd @@ -104,28 +104,34 @@ *% BLINE (black-mark stock), GAP 0 (continuous roll), or send neither and use *% the printer's stored setting. Sending GAP to continuous/black-mark stock *% makes the firmware hunt for a gap that never comes and error out. +*% FixedPitch is for die-cut stock whose gap the sensor cannot hold (short +*% labels, 2 mm gaps): GAP 0 like Continuous, but SIZE carries the full +*% label + GapLength pitch so the blind feed stays in register. *DefaultMediaTracking: Gap *MediaTracking Gap/Die-cut labels (gap sensor): "" *MediaTracking BlackMark/Black-mark stock: "" *MediaTracking Continuous/Continuous roll: "" +*MediaTracking FixedPitch/Die-cut labels, sensor off (feed label + gap): "" *MediaTracking PrinterDefault/Use printer setting: "" *CloseUI: *MediaTracking -*OpenUI *GapLength/Gap/Black-mark length (mm): PickOne +*OpenUI *GapLength/Gap or black-mark length: PickOne *OrderDependency: 46 AnySetup *GapLength -*% Physical length of the gap (die-cut) or black mark on the label stock, sent -*% as the second SIZE-boundary argument (GAP mm / BLINE mm). Wrong -*% value = firmware overshoots hunting for the next boundary and wastes -*% labels. Measure your stock; 3mm is a common default but many small -*% die-cut labels use ~1.5-2mm. Ignored when Media tracking is Continuous -*% or Use printer setting. +*% Physical length of the gap between die-cut labels, or the height of the +*% black mark, in tenths of a millimetre (30 = 3 mm: the TSC factory default +*% and what every TSPL driver sends). Goes out as the first argument of +*% GAP / BLINE; the second (sensor offset) is always 0. Many small die-cut +*% labels are cut with a 2 mm gap, TSC's stated sensor minimum; measure yours +*% if labels lose register. On FixedPitch queues the value is added to SIZE +*% instead, so the printer feeds label + gap per label with the sensor off. +*% Ignored for Continuous and Use printer setting. *DefaultGapLength: 30 *GapLength 15/1.5 mm: "" -*GapLength 20/2.0 mm: "" +*GapLength 20/2 mm: "" *GapLength 25/2.5 mm: "" -*GapLength 30/3.0 mm (default): "" -*GapLength 40/4.0 mm: "" -*GapLength 50/5.0 mm: "" +*GapLength 30/3 mm (default): "" +*GapLength 40/4 mm: "" +*GapLength 50/5 mm: "" *CloseUI: *GapLength *OpenUI *Darkness/Darkness: PickOne diff --git a/src/rastertotspl.c b/src/rastertotspl.c index d931b66..57e3e6f 100644 --- a/src/rastertotspl.c +++ b/src/rastertotspl.c @@ -23,14 +23,13 @@ * CUPS options honoured (same as the vendor PPD, plus MediaTracking): * Darkness (0..15) -> DENSITY * PrintSpeed (10..60 = ips x10) -> SPEED (in/sec); 0 = omit (printer default) - * MediaTracking Gap / BlackMark / Continuous / PrinterDefault - * -> GAP mm / BLINE mm / GAP 0 / (omitted) - * GapLength (tenths of mm, e.g. 30 = 3.0mm) -> the above; default 3.0mm. - * On Continuous stock (no gap sensor to stop - * the feed on its own) GapLength is instead - * added to SIZE's height, so the printer - * feeds the full label+gap pitch per label - * instead of landing mid-label next print. + * MediaTracking Gap / BlackMark / Continuous / FixedPitch / PrinterDefault + * -> GAP mm / BLINE mm / GAP 0 / GAP 0 / (omitted) + * GapLength (tenths of mm, 30 = 3 mm; a bare 1..9 or "2.5" is read as mm) + * -> , the gap / black-mark length; default 3 mm. + * FixedPitch: nothing but SIZE stops the feed, so GapLength is + * added to SIZE's height and the printer feeds label + gap per + * label (die-cut stock whose gap the sensor cannot hold). * Horizontal,Vertical (dots) -> REFERENCE * PrintMode 0 None / 2 Diffusion / 3 Gathering / 4 ErrorDiffusion / 5 Default * -> halftone used to flatten 8bpp grey into 1bpp dots. @@ -81,6 +80,29 @@ static const int CLUSTER8[8][8] = { * the PPD's marked default (which reflects the queue default set via * `lpadmin -p QUEUE -o Name=Value`); otherwise the built-in fallback. This is * what lets two queues sharing this filter have different defaults. */ +/* Raw option text: the job option first, else the queue PPD's marked choice + * (valid only until ppdClose). */ +static const char *opt_str(ppd_file_t *ppd, int num_options, cups_option_t *options, + const char *kw) +{ + const char *v = cupsGetOption(kw, num_options, options); + if (v) return v; + ppd_choice_t *c; + if (ppd && (c = ppdFindMarkedChoice(ppd, kw)) != NULL) return c->choice; + return NULL; +} + +/* Tenths of mm -> "3" or "2.5". Whole millimetres keep the integer form the + * stream has always used (the default bytes stay identical); fractions are in + * the TSC spec ("GAP 7.62 mm,2.54 mm" is a manual example) and are built from + * ints so no locale can turn the point into a comma. */ +static const char *fmt_mm(char *buf, size_t n, int tenths) +{ + if (tenths % 10) snprintf(buf, n, "%d.%d", tenths / 10, tenths % 10); + else snprintf(buf, n, "%d", tenths / 10); + return buf; +} + static int opt_int(ppd_file_t *ppd, int num_options, cups_option_t *options, const char *kw, int dflt) { @@ -116,39 +138,61 @@ int main(int argc, char *argv[]) int href = opt_int(ppd, num_options, options, "Horizontal", 0); int vref = opt_int(ppd, num_options, options, "Vertical", 0); - /* GapLength: physical gap/black-mark length in tenths of mm (30 = 3.0mm). - * The stock 3mm assumption is wrong often enough on small die-cut labels - * (seen as low as ~1.5-2mm) that a mismatch makes the firmware overshoot - * hunting for the next boundary, wasting 1-2 labels before it resyncs. */ - int gap_tenths = opt_int(ppd, num_options, options, "GapLength", 30); - if (gap_tenths < 0) gap_tenths = 0; - double gap_mm = gap_tenths / 10.0; - - /* MediaTracking -> the boundary command. GAP/BLINE select the sensor; - * sending GAP to continuous or black-mark stock makes the firmware hunt - * for a gap that never comes (feeds a label + margin, then errors), so an - * unrecognized value must not silently fall through without a warning. - * Resolved to a fixed buffer here because ppdClose frees the choice. */ - char track[32] = ""; - int continuous = 0; - snprintf(track, sizeof track, "GAP %.1f mm,0 mm\r\n", gap_mm); /* Gap (die-cut) */ + /* MediaTracking -> how the printer finds the next label. GAP/BLINE select + * the sensor; sending GAP to continuous or black-mark stock makes the + * firmware hunt for a gap that never comes (feeds a label + margin, then + * errors), so an unrecognized value must not silently fall through + * without a warning. FixedPitch is the escape hatch for die-cut stock + * whose gap the sensor cannot hold (short labels, gaps at the 2 mm sensor + * floor): GAP 0 like Continuous, but SIZE carries the full label + gap + * pitch so the blind feed stays in register. + * Resolved to an enum here because ppdClose frees the choice. */ + enum { TRK_GAP, TRK_BLINE, TRK_CONTINUOUS, TRK_FIXEDPITCH, TRK_PRINTER } track = TRK_GAP; { - const char *v = cupsGetOption("MediaTracking", num_options, options); - ppd_choice_t *c; - if (!v && ppd && (c = ppdFindMarkedChoice(ppd, "MediaTracking")) != NULL) - v = c->choice; + const char *v = opt_str(ppd, num_options, options, "MediaTracking"); if (v && *v) { - if (!strcasecmp(v, "BlackMark")) - snprintf(track, sizeof track, "BLINE %.1f mm,0 mm\r\n", gap_mm); - else if (!strcasecmp(v, "Continuous")) { - strcpy(track, "GAP 0 mm,0 mm\r\n"); - continuous = 1; - } - else if (!strcasecmp(v, "PrinterDefault")) track[0] = '\0'; /* stored setting */ + if (!strcasecmp(v, "BlackMark")) track = TRK_BLINE; + else if (!strcasecmp(v, "Continuous")) track = TRK_CONTINUOUS; + else if (!strcasecmp(v, "FixedPitch")) track = TRK_FIXEDPITCH; + else if (!strcasecmp(v, "PrinterDefault")) track = TRK_PRINTER; /* stored setting */ else if (strcasecmp(v, "Gap")) fprintf(stderr, "WARNING: unknown MediaTracking '%s' — assuming " - "Gap (die-cut); use Gap, BlackMark, Continuous or " - "PrinterDefault\n", v); + "Gap (die-cut); use Gap, BlackMark, Continuous, " + "FixedPitch or PrinterDefault\n", v); + } + } + + /* GapLength: the gap / black-mark length in tenths of mm (30 = 3 mm). + * 3 mm is the TSC factory default and what every other TSPL driver sends; + * small die-cut labels are often cut with 2 mm, TSC's sensor floor. As + * with PrintSpeed, a bare 1..9 (or anything with a decimal point) is read + * as whole mm so a hand-typed -o GapLength=2 does the intuitive thing. + * Bounds: the spec caps GAP/BLINE at 25.4 mm, and under 1 mm on a sensor + * mode would go out as GAP 0 -- which the firmware takes as "continuous", + * switches the sensor off, and remembers across jobs. */ + int gap_tenths = 30; + { + const char *v = opt_str(ppd, num_options, options, "GapLength"); + if (v && *v) { + char *end; + double n = strtod(v, &end); + if (end == v || n < 0) { + fprintf(stderr, "WARNING: GapLength '%s' is not a length — using 3 mm\n", v); + } else { + if (strchr(v, '.') || n < 10) n *= 10; /* millimetres -> tenths */ + gap_tenths = (int)lround(n); + if (gap_tenths > 254) { + fprintf(stderr, "WARNING: GapLength %s is over the TSPL maximum of " + "25.4 mm — clamped\n", v); + gap_tenths = 254; + } + if (gap_tenths < 10 && (track == TRK_GAP || track == TRK_BLINE)) { + fprintf(stderr, "WARNING: GapLength %s is under 1 mm, which would switch " + "the %s sensor off (GAP 0 = continuous) — using 3 mm\n", + v, track == TRK_GAP ? "gap" : "black-mark"); + gap_tenths = 30; + } + } } } if (ppd) ppdClose(ppd); @@ -277,15 +321,24 @@ int main(int argc, char *argv[]) * CRLF is the cheapest way back to command mode (pdf2tspl et al). */ int wmm = (int)lround((double)W * 25.4 / resx); int hmm = (int)lround((double)H * 25.4 / resy); - /* Continuous stock has no gap sensor, so the printer can't stop on its - * own at the next label — it trusts SIZE's height as the full feed - * pitch. Sending just the label height (hmm) undershoots by however - * big the physical inter-label gap really is, landing mid-label on - * the next print. Gap/BlackMark modes don't need this: the sensor - * finds the real boundary regardless of what SIZE says. */ - int size_hmm = continuous ? hmm + (int)lround(gap_mm) : hmm; - printf("\r\nSIZE %d mm,%d mm\r\n", wmm, size_hmm); - fputs(track, stdout); + char sizeh[16], gapstr[16]; + if (track == TRK_FIXEDPITCH) { + /* Nothing but SIZE stops the feed, so it must be the true pitch: + * label height plus the physical gap, summed in tenths and rounded + * once, because on a blind feed every rounding error accumulates + * label after label. A decimal SIZE is spec (and what the TSC and + * Munbyn vendor filters emit). */ + fmt_mm(sizeh, sizeof sizeh, (int)lround((double)H * 254.0 / resy) + gap_tenths); + } else + snprintf(sizeh, sizeof sizeh, "%d", hmm); + printf("\r\nSIZE %d mm,%s mm\r\n", wmm, sizeh); + switch (track) { + case TRK_GAP: printf("GAP %s mm,0 mm\r\n", fmt_mm(gapstr, sizeof gapstr, gap_tenths)); break; + case TRK_BLINE: printf("BLINE %s mm,0 mm\r\n", fmt_mm(gapstr, sizeof gapstr, gap_tenths)); break; + case TRK_CONTINUOUS: + case TRK_FIXEDPITCH: fputs("GAP 0 mm,0 mm\r\n", stdout); break; + case TRK_PRINTER: break; /* stored setting */ + } printf("DENSITY %d\r\n", darkness); if (speed_ips >= 1) printf("SPEED %d\r\n", speed_ips); @@ -297,8 +350,12 @@ int main(int argc, char *argv[]) fflush(stdout); free(bm); - fprintf(stderr, "INFO: TSPL page %d: %ux%u dots (%dx%dmm) mode=%d density=%d speed=%d copies=%u\n", - page, W, H, wmm, hmm, printmode, darkness, speed_ips, copies); + static const char *const trkname[] = + { "Gap", "BlackMark", "Continuous", "FixedPitch", "PrinterDefault" }; + fprintf(stderr, "INFO: TSPL page %d: %ux%u dots (SIZE %dx%smm) tracking=%s gap=%smm " + "mode=%d density=%d speed=%d copies=%u\n", + page, W, H, wmm, sizeh, trkname[track], fmt_mm(gapstr, sizeof gapstr, gap_tenths), + printmode, darkness, speed_ips, copies); } cupsRasterClose(ras); diff --git a/tests/smoke.sh b/tests/smoke.sh index 0b2ac61..87c72e4 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -16,7 +16,7 @@ fail() { echo "FAIL: $*" 1>&2; exit 1; } make -s src/rastertotspl tests/mkras OUT="${TMPDIR:-/tmp}/tspl-smoke.$$" -trap 'rm -f "$OUT" "$OUT.ras" "$OUT.txt" "$OUT.hex"' EXIT +trap 'rm -f "$OUT" "$OUT.ras" "$OUT.txt" "$OUT.hex" "$OUT.err"' EXIT tests/mkras > "$OUT.ras" src/rastertotspl 1 tester smoke 3 '' < "$OUT.ras" > "$OUT" 2>/dev/null @@ -26,7 +26,7 @@ od -An -v -tx1 < "$OUT" | tr -d ' \n' > "$OUT.hex" # --- the TSPL header, line by line (12x8 px @300dpi -> 1x1 mm; note the # spec-required space before "mm") --- -for cmd in 'SIZE 1 mm,1 mm' 'GAP 3.0 mm,0 mm' 'DENSITY 8' 'SPEED 4' \ +for cmd in 'SIZE 1 mm,1 mm' 'GAP 3 mm,0 mm' 'DENSITY 8' 'SPEED 4' \ 'DIRECTION 0,0' 'REFERENCE 0,0' 'CLS'; do grep -q "^$cmd" "$OUT.txt" || fail "missing TSPL command: $cmd" done @@ -51,34 +51,52 @@ grep -q '^PRINT 1,4$' "$OUT.txt" || fail "page 2 should print 4 device copies" grep -q '^PRINT 1,3$' "$OUT.txt" && fail "argv[4] copies leaked into PRINT" # --- option handling: BlackMark -> BLINE (no GAP), PrintSpeed=0 -> no SPEED --- -src/rastertotspl 1 tester smoke 1 'MediaTracking=BlackMark PrintSpeed=0' \ - < "$OUT.ras" 2>/dev/null | tr -d '\r' > "$OUT.txt" -grep -q '^BLINE 3.0 mm,0 mm' "$OUT.txt" || fail "BlackMark should emit BLINE" +opt() { src/rastertotspl 1 tester smoke 1 "$1" < "$OUT.ras" 2>"$OUT.err" | tr -d '\r' > "$OUT.txt"; } +opt 'MediaTracking=BlackMark PrintSpeed=0' +grep -qx 'BLINE 3 mm,0 mm' "$OUT.txt" || fail "BlackMark should emit BLINE 3 mm" grep -q '^GAP' "$OUT.txt" && fail "BlackMark must not also emit GAP" grep -q '^SPEED' "$OUT.txt" && fail "PrintSpeed=0 must omit SPEED" -# --- continuous media -> GAP 0; out-of-range speed clamps to 6 ips; SIZE -# height gets the default 3mm GapLength added since there's no sensor to -# stop the feed at the real (unknown-to-the-printer) label boundary --- -src/rastertotspl 1 tester smoke 1 'MediaTracking=Continuous PrintSpeed=9' \ - < "$OUT.ras" 2>/dev/null | tr -d '\r' > "$OUT.txt" -grep -q '^GAP 0 mm,0 mm' "$OUT.txt" || fail "Continuous should emit GAP 0" -grep -q '^SPEED 6$' "$OUT.txt" || fail "PrintSpeed=9 should clamp to SPEED 6" -grep -q '^SIZE 1 mm,4 mm$' "$OUT.txt" || fail "Continuous should add default 3mm GapLength to SIZE height" +# --- continuous roll -> GAP 0 and SIZE stays the page height (nothing to add: +# the page IS the feed length); out-of-range speed clamps to 6 ips --- +opt 'MediaTracking=Continuous PrintSpeed=9' +grep -qx 'GAP 0 mm,0 mm' "$OUT.txt" || fail "Continuous should emit GAP 0" +grep -qx 'SIZE 1 mm,1 mm' "$OUT.txt" || fail "Continuous must not pad SIZE" +grep -qx 'SPEED 6' "$OUT.txt" || fail "PrintSpeed=9 should clamp to SPEED 6" -# --- continuous + custom GapLength=20 (2.0mm) -> SIZE height is label + 2mm, -# not label + the 3mm default --- -src/rastertotspl 1 tester smoke 1 'MediaTracking=Continuous GapLength=20' \ - < "$OUT.ras" 2>/dev/null | tr -d '\r' > "$OUT.txt" -grep -q '^SIZE 1 mm,3 mm$' "$OUT.txt" || fail "Continuous should add custom GapLength to SIZE height" +# --- GapLength (tenths of mm) on the sensor modes: whole millimetres keep the +# integer form, fractions are spec ("GAP 7.62 mm,2.54 mm" is a manual +# example); a bare 1..9 or a decimal is read as mm, like PrintSpeed's ips --- +opt 'GapLength=20'; grep -qx 'GAP 2 mm,0 mm' "$OUT.txt" || fail "GapLength=20 -> GAP 2 mm" +opt 'GapLength=2'; grep -qx 'GAP 2 mm,0 mm' "$OUT.txt" || fail "bare GapLength=2 -> GAP 2 mm" +opt 'GapLength=2.5'; grep -qx 'GAP 2.5 mm,0 mm' "$OUT.txt" || fail "GapLength=2.5 -> GAP 2.5 mm" +opt 'MediaTracking=BlackMark GapLength=15' +grep -qx 'BLINE 1.5 mm,0 mm' "$OUT.txt" || fail "GapLength=15 -> BLINE 1.5 mm" +opt 'MediaTracking=PrinterDefault GapLength=20' +grep -qE '^(GAP|BLINE)' "$OUT.txt" && fail "PrinterDefault must send no boundary command" -# --- GapLength (tenths of mm): custom value on Gap and BlackMark tracking --- -src/rastertotspl 1 tester smoke 1 'GapLength=20' \ - < "$OUT.ras" 2>/dev/null | tr -d '\r' > "$OUT.txt" -grep -q '^GAP 2.0 mm,0 mm' "$OUT.txt" || fail "GapLength=20 should emit GAP 2.0 mm" +# --- GapLength guards: under 1 mm or unparsable on a sensor mode would go out +# as GAP 0 = continuous, switching the sensor off and persisting in the +# printer -> warn and fall back to 3 mm; the spec caps GAP at 25.4 mm --- +for bad in 'GapLength=0' 'GapLength=abc' 'GapLength=-5' 'MediaTracking=BlackMark GapLength=0'; do + opt "$bad" + grep -qE '^(GAP|BLINE) 3 mm,0 mm$' "$OUT.txt" || fail "$bad should fall back to 3 mm" + grep -q '^WARNING' "$OUT.err" || fail "$bad should warn" +done +opt 'GapLength=999' +grep -qx 'GAP 25.4 mm,0 mm' "$OUT.txt" || fail "GapLength=999 should clamp to 25.4 mm" +grep -q '^WARNING' "$OUT.err" || fail "GapLength=999 should warn" -src/rastertotspl 1 tester smoke 1 'MediaTracking=BlackMark GapLength=15' \ - < "$OUT.ras" 2>/dev/null | tr -d '\r' > "$OUT.txt" -grep -q '^BLINE 1.5 mm,0 mm' "$OUT.txt" || fail "GapLength=15 should emit BLINE 1.5 mm" +# --- FixedPitch: GAP 0 like Continuous, but SIZE is label + gap summed in +# tenths and rounded once (8 dots @300 dpi = 0.68 mm -> 0.7; + 3 mm = 3.7; +# + 2.5 mm = 3.2; + 0 = 0.7, and 0 is valid here) --- +opt 'MediaTracking=FixedPitch' +grep -qx 'GAP 0 mm,0 mm' "$OUT.txt" || fail "FixedPitch should emit GAP 0" +grep -qx 'SIZE 1 mm,3.7 mm' "$OUT.txt" || fail "FixedPitch should add the 3 mm default gap to SIZE" +opt 'MediaTracking=FixedPitch GapLength=25' +grep -qx 'SIZE 1 mm,3.2 mm' "$OUT.txt" || fail "FixedPitch GapLength=25 -> SIZE 3.2 mm" +opt 'MediaTracking=FixedPitch GapLength=0' +grep -qx 'SIZE 1 mm,0.7 mm' "$OUT.txt" || fail "FixedPitch GapLength=0 -> bare label height" +grep -q '^WARNING' "$OUT.err" && fail "GapLength=0 is valid on FixedPitch" echo "smoke test OK"