Notes › Tech Notes › The decoder only echoes the raw text: EAN-13 check digit, UPC-A leading zero, CODE128 mod 103

The decoder only echoes the raw text: EAN-13 check digit, UPC-A leading zero, CODE128 mod 103

The decode page is far narrower than its copy

I carried a home-printed EAN-13 bitmap to the barcode decoder page. What appeared was not the decoded payload but one small line saying this browser does not support barcode detection.

The template is 195 lines and loads 0 external scripts, so no second engine stands behind that chain. Line 101's 'BarcodeDetector' in window is the only gate, and it measured false in my headless Chrome, which makes the whole decoding block on lines 102-122 unreachable and leaves only the unsupported panel on lines 172-175.

To rule out a bad image, I drew a legal EAN-13 with the on-site barcode generator (5901234123457, measured 95x40 px) and fed it into the same input. Output: 0 characters. That image has a valid check digit, a scanner gun reads it, and the browser side still gives back nothing.

Scanning does not exist either: getUserMedia, video and capture each appear 0 times in the template. Line 15 holds one static upload box, type="file".

The check digit is computed from 12 digits

The 13th position of an EAN-13 is a mod 10 weighted sum: line 188 of the generator multiplies even-indexed digits by 1 and odd-indexed digits by 3, line 190 takes (10 - sum % 10) % 10. Measured: '590123412345' sum=83, check digit 7; '999999999999' sum=216, check digit 4.

590123412345 sum 83, digit 7 400638133393 sum 89, digit 1 690123456789 sum 128, digit 2 999999999999 sum 216, digit 4 Weights: even index x1, odd index x3, check digit = (10 - sum % 10) % 10 13 digits in, no recompute: 5901234123456 ships as drawn, right digit 7

Fig. 1: how the check digit comes out of 12 digits, plus one bad code that shipped anyway

Feed 13 digits and not one of them is recomputed

The confusion behind "it clearly computed a check digit but the code does not match" usually starts on the generation side. Generator lines 184-194 compute the check digit only inside the length === 12 branch, and a full 13 digits falls straight into the empty branch. Measured: 6 of 6 groups with a wrong check digit shipped as drawn, '5901234123456' should end in 7, '4006381333930' should end in 1, '9999999999999' should end in 4, and no error is reported for any of them. Past 13 digits, generator lines 192-193 simply truncate, and because truncation happens after padding, an over-long input never triggers a recompute either. That a picture came out only proves the digits got drawn; it does not prove the string is self-consistent.

UPC-A needs one leading 0 to be equivalent

The two symbologies sit one position apart in weight direction, and the conversion is a single move: pad one 0 in front of 11-digit UPC-A data, then run encodeEAN13. Measured: 10 of 10 groups agree with the check digit UPC-A computes on its own, and both '04210000526' returning 4 and '01234567890' returning 5 land on this rule.

The trap is 12-digit UPC-A that already carries its last digit. Feed it in as-is as 12-digit EAN-13 data and the last digit is recomputed into a different one, so '012345678905' becomes '0123456789050'. The same digit string is legal under one reading and illegal under the other, and that cross-over is most likely what the earlier "does not match" really was.

CODE128 runs on mod 103

Switching to mod 103 is where the earlier rule stops applying: lines 116-117 take start code 104, which doubles as the initial checksum value, line 123 accumulates with the positional weight (i+1), and line 126 closes with checksum % 103. Measured: 'HELLO123' sum=1141, check digit 8; 'abc' sum=502, check digit 90.

The pattern table itself is broken as well: the patterns array measures 184 entries while CODE128 needs only 107; across 0..102 there are just 79 distinct patterns and 24 groups are shared by two code values; stop code 106 sits on the very same 11-module pattern as value 7, while the standard stop symbol is 13 modules. The result is same image, different text - the bit strings of 'CS' and 'DS' match position by position and both run 55 modules, and a sweep of all 1296 two-character inputs returns only 1292 distinct patterns. On that family the loop from generator to decoder never closes.

CODE39 has no check digit and swallows characters

Generator lines 229-257 never sum and never take a modulus. They add a '*' delimiter at each end and stop there. Generator line 230 calls toUpperCase first, so 'abc' and 'ABC' produce identical bit strings. Characters outside the table are quieter still: line 249 drops them outright, so encodeCode39('A_B') and encodeCode39('AB') both come out at 51 modules, and 'UU工具箱' survives as only the UU inside those 51 modules.

EAN-13 95 modules, mod 10 check CODE128 121 modules, mod 103 sum CODE39 129 modules, no check digit EAN-13 is always 95 modules, whatever the content, and holds no letters CODE128 and CODE39 rows are measured modules for 'HELLO123'

Fig. 2: the symbologies side by side - mod 10, mod 103, and no check at all

Cards render in full, copy takes only the first one

The forEach on line 133 renders every result: passing 4 codes produced 4 cards and margin-top:10px showed up 3 times. But line 159 writes only codes[0].rawValue into dataset.result, so the second, third and fourth codes are visible and impossible to copy. The not-recognized branch on line 165 and the not-supported branch on line 174 both leave dataset.result alone, so after the panel swaps over to not-supported the old value is still sitting there, and copyResult on lines 177-182 delivers it to the clipboard as if it were fresh.

Every rule laid out in one table

SymbologyCheck ruleMeasured result
EAN-13mod 10 weightedsum=83, digit 7; 13 digits in, no recompute
UPC-Apad one leading 0, then EAN-13'01234567890' gives 5; feeding 12 digits recomputes the last one
CODE128mod 103, weight (i+1)'HELLO123' sum=1141, digit 8; the table holds only 79 patterns
CODE39no check digit'A_B' and 'AB' both land on 51 modules

The boundary is clear. All a barcode can prove by itself is that this digit string agrees with itself. In the template the words checksum, verify, check digit, product and authenticity each appear 0 times, and line 134 takes rawValue as it comes, with no recomputation, so it has no way to judge whether the product behind the code is genuine.

Settle the symbology, the digit count and whether a check digit belongs there first - the barcode type choice article covers that layer. Once the rule is fixed, render one picture with the barcode generator. Back on the decoder page, what you are checking is whether the string on screen and the string in the clipboard are the same thing.

Three questions people get backwards

Q: It generated 13 digits with no error, so the check digit must be right?

No. Generator lines 184-194 only compute inside the length === 12 branch, and 13 digits fall straight through. To test it yourself, drop the last digit, rerun the 12 remaining ones and compare.

Q: Just add a 0 in front of UPC-A and it is fine?

Padding a 0 only holds for 11-digit data. A 12-digit code that already carries its last digit cannot be reused as 12-digit EAN-13 data, because that last digit gets recomputed into another one.

Q: The panel says unsupported, yet copy still hands me a code?

That is a value from the previous run - neither failure exit clears dataset.result. When the failure panel is showing, the card content is not trustworthy, and the string in your clipboard is even less so.

📎 Related articles

Merged line counts never add up: a missing trailing newline, a swallowed BOM and an unreliable order 2026-10-26 Excel pasted into JSON: dates, long numbers and precision are already wrong the moment you paste 2026-10-26 QR code decoding tested: the error-correction level cannot beat an occlusion, 2 pixels per module is the real line 2026-10-26 Character frequency or word frequency: one Chinese sentence, a 23-row board and an empty table 2026-10-26 CSS minify left the calc() spaces alone, but the beautifier split .btn:hover in two 2026-10-26