Code readingHow-to

Code 39 thermal printer print quality: bars sized in dots

Fix Code 39 thermal printer print quality at the source: build bars from whole print-head dots, 2 dots per narrow bar on 203 dpi, and print unscaled.

LumaSpec Vision10 min回読み取り LinkedInで共有

このページ 9セクション
  1. 01Why the bars print dotted
  2. 02The numbers that set the symbol
  3. 03Draw the symbol in dots, step by step
  4. 04Worked example: ten digits on a 203 dpi head
  5. 05Pick X for your head
  6. 06Prove the print
  7. 07When whole dots still do not read
  8. 08Release checklist
  9. 09Questions

Code 39 labels that leave a thermal printer with dotted, broken bars and only decode after several scanner passes are almost never a symbology fault. The barcode image was rendered at one pixel pitch and the print head places dots at another, so the driver resamples the artwork and bar edges land between dots. This guide fixes that at the source: you generate the symbol directly in print-head dots, one image pixel per dot, with every bar a whole number of dots wide. You will size a 10-digit Code 39 for a 203 dpi head end to end, print it unscaled, and prove it with a reader or a camera. The same arithmetic covers 300 and 600 dpi heads and any roll width.

Why the bars print dotted

A thermal head is a fixed row of heating elements with nothing between them. A 203 dpi head has 203 elements across 25.4 mm — 8 dots per mm, each 0.125 mm wide, either burned or not burned. There is no gray.

When a barcode image drawn for a web page — say 180 px wide, because the page asked for it at 180 — reaches that head, the driver must place each pixel edge somewhere on the dot grid. An edge that falls between dots gets averaged with its neighbour, so a one-pixel bar prints as a single dot with a gap beside it, and the next bar prints differently. That is the dotted, intermittent look, and it is why the reader needs several passes to succeed.

An inkjet hides the same fault because its droplet is larger and spreads into the gap. A thermal dot stays a dot. Resampling, not the Code 39 pattern, is the defect; the cure is to draw on the head's own grid.

The numbers that set the symbol

Four numbers decide whether a thermal Code 39 scans:

  • Narrow element X. 0.25 mm is the practical floor for wand and laser scanners; the symbology was drawn around a nominal 0.33 mm bar. Keep X at or above that floor.
  • Whole dots. The smallest element should be at least 2 head dots; a 1-dot bar can drop out completely on a worn head.
  • Wide-to-narrow ratio. Wide elements are 3 times the narrow one. 2.5:1 is the other standard choice; 3:1 tolerates print spread better.
  • Quiet zone. Ten X on each side, kept clear of any print.

Each Code 39 character is five bars and four spaces — nine elements, three of them wide — so at 3:1 one character spans 6X narrow plus 9X wide = 15X, plus a 1X gap between characters. Bar height comes out of a second rule: at least 15 percent of the symbol length and never under 6.5 mm.

dots_per_mm = dpi / 25.4 X_dots = ceil(X_floor_mm × dots_per_mm) // and never below 2 wide_dots = 3 × X_dots symbol_dots = 15 × X_dots × C + (C − 1) × X_dots image_px = symbol_dots + 2 × 10 × X_dots

C counts characters, including the start and stop asterisks, and X_floor_mm is the scanning floor stated above.

Draw the symbol in dots, step by step

  1. Get the head resolution from the printer specification and convert it: dots per mm = dpi / 25.4. A 203 dpi head gives 8 dots/mm; a 300 dpi head gives 11.8.
  2. Choose the narrow element in whole dots so it reaches the scanning floor: 2 dots at 203 dpi, 3 dots at 300 dpi, 6 dots at 600 dpi.
  3. Set every wide element to 3 × X_dots and every gap to X_dots. Bars, spaces and gaps are now whole dot counts.
  4. Set the bar height in dots to the larger of 15 percent of symbol length and 6.5 mm, rounded up to whole dots.
  5. Render one image pixel per dot — pure black bars on white, no anti-aliasing. The PHP snippet below does exactly that with GD.
  6. Print the PNG at its native size: no width attribute, no CSS width, no fit-to-page, driver set to the head's own resolution. If you print through the printer's command language instead of a page, use its barcode command and give the narrow bar in dots there.

The rendering core in PHP, with every variable in dots:

<?php
// Widths in print-head dots for a 203 dpi head (8 dots/mm).
$dotsPerMm  = 8;        // int: head resolution
$x          = 2;        // int: narrow element, 2 dots
$n          = 3;        // int: wide-to-narrow ratio, wide = 6 dots
$barHeight  = 80;       // int: 10 mm of bars
$code       = '1234567890';        // string: data characters
$patterns   = code39Patterns();    // array<string,string>: nine 'w'/'n' elements per character

$elements = [];                              // array<int>: width of each bar, space and gap in dots
foreach (str_split('*' . $code . '*') as $char) {
    foreach (str_split($patterns[$char]) as $e) {   // five bars, four spaces, three wide
        $elements[] = ($e === 'w') ? $n * $x : $x;
    }
    $elements[] = $x;                        // one narrow gap between characters
}
array_pop($elements);                        // no gap after the stop character
$quiet = 10 * $x;                            // int: 10X quiet zone on each side
$width = 2 * $quiet + array_sum($elements);  // int: image width in pixels

$im    = imagecreatetruecolor($width, $barHeight);  // GdImage: one pixel = one head dot
$white = imagecolorallocate($im, 255, 255, 255);
$black = imagecolorallocate($im, 0, 0, 0);
imagefilledrectangle($im, 0, 0, $width - 1, $barHeight - 1, $white);
$pos = $quiet;                               // int: drawing position in dots
foreach ($elements as $i => $w) {
    if ($i % 2 === 0) {                      // even positions are bars
        imagefilledrectangle($im, $pos, 0, $pos + $w - 1, $barHeight - 1, $black);
    }
    $pos += $w;
}
header('Content-Type: image/png');
imagepng($im);

// Test: 12 characters x 30 dots + 11 gaps x 2 dots + 2 x 20 quiet dots = 422 px
assert($width === 422);
assert(imagesx($im) === 422);

The code39Patterns() table is your own: one nine-element 'w'/'n' string per character. The full Code 39 geometry, including the asterisk patterns, is in Print a readable Code 39 barcode from your own program, and the same one-pixel-per-module discipline behind generate an EAN-13 barcode bitmap with readable digits applies unchanged.

Worked example: ten digits on a 203 dpi head

A ten-digit code carries twelve characters: the digits plus the start and stop asterisks. At X = 2 dots and a 3:1 ratio:

  • One character: 6 narrow × 2 dots + 3 wide × 6 dots = 12 + 18 = 30 dots.
  • Twelve characters: 12 × 30 = 360 dots.
  • Eleven inter-character gaps: 11 × 2 = 22 dots.
  • Symbol: 382 dots ÷ 8 dots/mm = 47.75 mm.
  • Quiet zones: 2 × 10 × 2 = 40 dots = 5.0 mm.
  • Image width: 422 px, printed 52.75 mm wide, wide bars 6 dots = 0.75 mm, ratio 6 / 2 = 3.0.
  • Height: 0.15 × 47.75 = 7.2 mm, so take 10 mm = 80 dots.

On an 80 mm roll with 2 mm edge margins, 76 mm stays printable and the symbol leaves 23.25 mm for human-readable text. If you tried X = 3 dots = 0.375 mm, the 211X symbol-plus-quiet span becomes 211 × 0.375 = 79.1 mm — off the printable width, so 2 dots is the decision on that roll. This is also why a library default that overflows the roll and then gets scaled down fails: compute the dot count first, and it can never surprise you. The same integer-module thinking that sizes a QR code to an exact image with its quiet zone is at work here.

Pick X for your head

Head dots/mm One dot Narrow bar X Wide bar 3X Ten-digit symbol
203 dpi 8.0 0.125 mm 2 dots 6 dots = 0.75 mm 382 dots = 47.75 mm
300 dpi 11.8 0.085 mm 3 dots = 0.254 mm 9 dots = 0.762 mm 573 dots = 48.5 mm
600 dpi 23.6 0.042 mm 6 dots = 0.254 mm 18 dots = 0.762 mm 1146 dots = 48.5 mm

The 300 dpi row carries the trap: 2 dots there is 0.17 mm, which satisfies the two-dot rule but sits under the scanning floor, so use 3 dots. The symbol barely changes width across heads — 47.75 mm to 48.5 mm — because X is pinned to the scanning floor, not to the dot.

Prove the print

Three checks, in order of cost:

  1. First-pass test. Wave ten labels under a handheld reader and require ten reads with no retry. A wired USB handheld such as the LS-HCR-0001 does this quickly, and your station logic should behave like read a code once and stop after the first good read, not a retry loop.
Handheld code readersHandheld industrial barcode reader, 1.0 MP, USB 2.0 (LS-HCR-0001)LS-HCR-0001
Resolution
Up to 3 MP
Shutter
Global
Interface
USB
Connection
Wired
$539.00View part
  1. Measure the bars. Photograph one label and run a run-length count over the bars and spaces: narrow near 2 dots, wide near 0.75 mm, ratio 3.0. The method is in count barcode bars from an image using run lengths. Pale bars at correct widths point at print darkness or a worn head, not geometry.
  2. Camera check on the line. To image a narrow bar with at least 3 pixels across it, one pixel may cover at most a third of X — 0.083 mm on this symbol. Across a 60 mm field of view that needs 60 / 0.083 = 720 pixels — the camera resolution calculator gives this in one step.
Camera resolution for the smallest feature
Units
Pixels needed
–
Sensor size
– MP
Smallest sensor that fits
–
Pixel on the part
– mm
Pixels across the feature, that sensor
–

Pixels needed = field / feature × pixels per feature on each axis (field incl. margin). The lens must resolve the pixels too: Nyquist = 1000 / (2 × pixel µm) lp/mm.

Find cameras with enough pixels Open the full calculator

A 1.2 MP sensor puts roughly 1280 pixels across that field, about 5 pixels per narrow bar — comfortable margin over the 3-pixel floor. For a fixed check station, the LS-CRD-0047 is a compact reader with a 1.2 MP sensor, a USB 2.0 interface and a 60-120 mm reading distance.

Fixed code readersCompact fixed industrial barcode reader, 1.2 MP, USB 2.0, 60-120 mm reading distance (LS-CRD-0047)LS-CRD-0047
Resolution
Up to 3 MP
Shutter
Global
Interface
USB
$559.00View part

On a moving web of labels, the LS-CRD-0028 reads up to 60 codes/s with a 2560 x 2048 sensor and an 8 mm lens.

Fixed code readers2560 x 2048 fixed code reader, 30 fps, 60 codes/s, 8 mm lens (LS-CRD-0028)LS-CRD-0028
Interface
Fast Ethernet
Resolution
5 MP
Sensor format
1/1.7"
Shutter
Global
$920.00View part

When the label needs a formal grade — contrast, defects, decodability — the workflow in verify the string and grade the print applies to thermal Code 39 exactly as it does to inkjet date codes.

When whole dots still do not read

  • The page scaled the image. A width attribute, CSS width or fit-to-page resamples the artwork and reintroduces dotted bars.
  • The driver is in photo mode. Photograph and halftone settings dither solid black bars; print the label in text or line-art mode at the head's own resolution.
  • Bars are pale but correctly sized. Raise the burn level (the darkness setting) or fit a new head; run lengths will show correct widths with weak contrast.
  • The label is glossy laminate. Specular glare defeats the reader; a matte label stock or a slight read angle restores contrast.
  • Print sits in the quiet zone. Ten X each side, clear of text, borders and perforations.

Release checklist

  • Head dpi known, and dots/mm = dpi / 25.4 computed.
  • X is whole dots and reaches the scanning floor: 2, 3 or 6 dots at 203, 300 or 600 dpi.
  • Wide bars are 3 × X dots; every bar, space and gap is an integer dot count.
  • Quiet zone is 10X per side, expressed in dots.
  • Bar height is the larger of 15 percent of symbol length and 6.5 mm.
  • Image width equals the dot count, with no scaling anywhere in the chain.
  • Ten labels yield ten first-pass reads.
  • A run-length spot check reads narrow bars at 2 dots, wide bars at 0.75 mm, ratio 3.0.

When a station rather than a hand has to read these labels, start from fixed code readers or the code reader finder; handheld spot checks draw from handheld code readers. To assemble the reading station end to end, the system configurator walks camera, lens, light and controller; if a label format needs a second opinion, write to us through contact.

質問

Why did my bars print dotted even at one pixel per bar?

A one-pixel bar is not one head dot. The driver resamples the image onto the head's 8 dots per mm, and an averaged edge prints as a single dot with a gap beside it. Inkjet hides this because its droplet spreads; a thermal dot stays discrete. Draw the symbol one image pixel per head dot with the narrow bar at least 2 dots and the artifact disappears.

What is the smallest Code 39 a scanner reads reliably?

A narrow bar of 2 dots on a 203 dpi head is the practical floor for wand and laser scanners; the symbology was designed around a 0.33 mm bar. On a 203 dpi head that means 2 dots, on 300 dpi 3 dots, and never fewer than 2 dots anywhere, because a single-dot bar can drop out on a worn head. A larger X also lengthens the working distance.

How wide is a ten-digit Code 39 printed this way?

Twelve characters (ten digits plus start and stop asterisks) at a 3:1 ratio span 191 narrow-element widths: 47.75 mm of bars and gaps plus 5 mm of quiet zone at a 2-dot X, so 52.75 mm total. The rule is 15X per character plus 1X per gap, so you can predict fit on any roll before you print.

Is a 300 dpi printer better for barcodes?

Not automatically. A 300 dpi dot is 0.085 mm, so the usual two-dot minimum gives a 0.17 mm bar — under the scanning floor set above. You need 3 dots, or 0.254 mm. More dpi buys smaller labels, not better reads; a 203 dpi head with whole-dot bars reads first pass.

Should I use a 2.5:1 or a 3:1 wide-to-narrow ratio?

Use 3:1. With a 2-dot narrow bar, 2.5:1 still gives whole dots (5-dot wide bars) and saves 3 dots per character — about 4.5 mm on a ten-digit code — but 3:1 tolerates dot spread and worn heads better. Take 2.5:1 only when the roll is too tight for the 3:1 width.

How do I prove the print is good without a formal verifier?

Pass a handheld reader over ten labels and require ten first-pass reads, then photograph one label and measure bar and space run lengths: narrow near 2 dots, wide near 0.75 mm, ratio 3.0, quiet zones clear. Pale bars with correct widths point at burn level or a worn head rather than geometry.