diff --git a/README.md b/README.md index 2dd6fc9..4cb6129 100644 --- a/README.md +++ b/README.md @@ -182,7 +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` | +| **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 — @@ -193,6 +194,25 @@ 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). +**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 @@ -211,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 96dc05e..ede2d3d 100644 --- a/ppd/tspl-label.ppd +++ b/ppd/tspl-label.ppd @@ -104,13 +104,36 @@ *% 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 or black-mark length: PickOne +*OrderDependency: 46 AnySetup *GapLength +*% 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 mm: "" +*GapLength 25/2.5 mm: "" +*GapLength 30/3 mm (default): "" +*GapLength 40/4 mm: "" +*GapLength 50/5 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..57e3e6f 100644 --- a/src/rastertotspl.c +++ b/src/rastertotspl.c @@ -23,8 +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 3 mm / BLINE 3 mm / GAP 0 / (omitted) + * 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. @@ -75,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) { @@ -110,25 +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); - /* 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) */ + /* 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")) 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")) 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); @@ -257,8 +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); - printf("\r\nSIZE %d mm,%d mm\r\n", wmm, 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); @@ -270,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 789d8d4..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 @@ -51,16 +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 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 --- -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" +# --- 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" + +# --- 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 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" + +# --- 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"