Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand All @@ -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.

<details>
<summary><b>Two queues: crisp labels + a "photo" (Gathering) queue</b></summary>

Expand All @@ -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.
</details>

<details>
Expand Down
23 changes: 23 additions & 0 deletions ppd/tspl-label.ppd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
126 changes: 105 additions & 21 deletions src/rastertotspl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <g> mm / BLINE <g> mm / GAP 0 / GAP 0 / (omitted)
* GapLength (tenths of mm, 30 = 3 mm; a bare 1..9 or "2.5" is read as mm)
* -> <g>, 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.
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
54 changes: 45 additions & 9 deletions tests/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"