画面上のバーコードは正しく見えていました。印刷も綺麗に出ました。ところが倉庫では別の商品として読み取られ、出荷が済むまで誰も気づきませんでした。

原因は、重みを逆方向に当てて計算したチェックデジット(チェックディジットとも表記します)でした。シンボルとしての構造は正しいので、どのレンダラーも受け付けますし、プレビューもすべて正常に見えます。スキャナーは意図を検証しません。バーが示すとおりに復号し、その数字を返すだけです。

バーコードの不具合は、ハードウェアが絡むまで表に出ません。この記事では、バーコード作成において読み取りの成否を実際に左右する要素を扱います。データに合うシンボロジーの選び方、チェックデジットの計算、そして「規格上は正しいのに読めないラベル」を生む数少ない印刷・レイアウト上のミスです。

バーコードジェネレーターを開く →

シンボロジーの選び方

小売商品では、選択の余地はありません。採番体系がシンボロジーを決めますし、番号そのものが「どれを使うことになっているか」を教えてくれます。

シンボロジーデータ桁数使われる場所
EAN-13数字12 桁 + チェックデジット世界共通の小売。ISBN-13、ISSN、JAN も同じエンコード
EAN-8数字7 桁 + チェックデジットEAN-13 が入らない小型パッケージ
UPC-A数字11 桁 + チェックデジット北米の小売
Code 128ASCII 0-127可変物流、倉庫管理、社内資産タグ
Code 390-9 A-Z スペース - . $ / + %可変工業、自動車、防衛系のレガシーシステム
ITF-14数字13 桁 + チェックデジット出荷用カートン、パレット単位の取引単位
Codabar0-9 - $ : / . +可変図書館、血液バンク、宅配ラベル

GTIN-13 なら EAN-13 しかありません。GTIN-12 なら UPC-A です。小売のレジを通るものにラベルを貼るなら、そこでやっているのはシンボロジーの選定ではなく、割り当てられた番号のエンコードであり、シンボロジーは番号に付いてきます。

日本の JAN コードもこの例に含まれます。JAN は EAN-13 と同一の規格で、エンコード方式も同じです。GS1 が日本に割り当てた国コードは 45 と 49 で、13 桁の JAN コードはこのどちらかで始まります。ライブラリの設定に「JAN」という項目がなくても問題はありません。EAN-13 を選べばそれが JAN コードです。

社内で完結する用途なら、ここは本当に選べます。そして多くの場合 Code 128 が正解です。ASCII の全範囲をエンコードでき、3 つのサブセットを自動で切り替えて幅を詰め、現代の物流システムが前提にしているのもこれです。Code 39 が古い設備で生き残っているのは、印刷も復号も簡単だからですが、同じ幅に入る文字数は少なく、文字セットも 43 種類で止まります。Code 39 を選ぶのは受け入れ側のシステムが要求するときで、ドロップダウンの先頭にあるからではありません。

チェックデジットと、向きが効く理由

EAN-13、EAN-8、UPC-A、ITF-14 は同じ計算式を使います。データ部の右端の桁から左へ進みながら、3 と 1 の重みを交互に掛けます。掛けた結果を合計し、次の 10 の倍数までの差を取ります。

check = (10 - (weighted sum mod 10)) mod 10

590123412345 で追ってみます。

digit:   5  9  0  1  2  3  4  1  2  3  4  5
weight:  1  3  1  3  1  3  1  3  1  3  1  3
product: 5 27  0  3  2  9  4  3  2  9  4 15   → sum 83
check:   (10 - 83 mod 10) mod 10 = 7

完成した EAN-13 は 5901234123457 です。

落とし穴は基点です。EAN-13 の解説はたいてい、この規則を桁位置で述べます——「左から数えて奇数位置は重み 1、偶数位置は重み 3」——そして EAN-13 に限ってはそれで正しく動きます。データ部が 12 桁なので、2 つの言い方が一致するからです。4 つのうちデータ長が偶数なのは EAN-13 だけです。UPC-A は 11 桁、EAN-8 は 7 桁、ITF-14 は 13 桁。位置ベースの規則をそのまま持ち込むと、重みが間違った桁に乗ります。

これは稀なエッジケースではありません。これらの桁数のランダムなデータで比べると、位置ベースの規則が正解と食い違う割合はおよそ 80% です。そして危険なのは残りの 20% のほうで、手で選んだテストベクタが 1 本だけだと、通ってしまう確率が十分にあります。

データシンボロジー右端基点位置ベースの規則
9638507EAN-844
1234567EAN-808
03600029145UPC-A28
1540141253226ITF-1442

9638507 は EAN-8 のドキュメントに最もよく出てくるベクタで、2 つの実装は同じ答えを返します。教科書の例を軸に組んだテストスイートは、このバグをそのまま出荷します。

// データ部の右端を基点にする。EAN-13、EAN-8、UPC-A、ITF-14 の
// いずれでも正しく動く。
function checkDigit(data) {
  let sum = 0;
  for (let i = data.length - 1, weight = 3; i >= 0; i--, weight = 4 - weight) {
    sum += Number(data[i]) * weight;
  }
  return (10 - (sum % 10)) % 10;
}

checkDigit('590123412345');  // 7   EAN-13
checkDigit('1234567');       // 0   EAN-8
checkDigit('03600029145');   // 2   UPC-A

データ長が奇数のペイロードを最低 1 本はテストしてください。123456703600029145 はどちらも、右端基点の実装と位置ベースの実装を切り分けます。よくある教科書のベクタでは切り分けられません。

UPC-A は名前が短いだけの EAN-13

UPC-A のシンボルは、先頭にゼロを 1 つ足した EAN-13 です。0360002914520036000291452 と同じバーになりますし、ゼロを足してもチェックデジットは変わりません。

変わるのはスキャナーの報告のしかたで、その違いは端末ごとではなくプラットフォームごとに出ます。macOS と iOS では、システムのデコーダーが UPC-A を 13 桁の ean_13 として返します。Apple の Technical Note TN2325 にも、復号された UPC-A は先頭にゼロが付いた EAN-13 オブジェクトとして出力されると書かれています。Chromium の BarcodeDetector もその挙動を引き継ぎ、macOS で getSupportedFormats() を呼ぶと ean_13upc_e は含まれるのに upc_a は一切返ってきません。Android では同じ API の実装が ML Kit で、そこでは UPC-A が独立したフォーマットとして扱われ、12 桁で返ります。

同じコードで同じラベルを読んでも、片方のプラットフォームでは 13 桁、もう片方では 12 桁になります。商品を 12 桁の UPC で保存していれば macOS では検索が全部外れますし、ゼロ埋めして保存すれば今度は Android 側が外れます。境界で正規化してください。内部表現を 1 つに決め、すべてのスキャン結果を照合前にその表現へ変換します。受け取った桁数をそのまま信用してはいけません。

同じ関係が、ときおり要求される UPC-E の説明にもなります。UPC-E は別の採番体系ではなく、フルサイズのシンボルが載らない小型パッケージ向けに UPC-A をゼロ抑制して印刷したものです。多くのスキャナーは 12 桁の UPC-A へ展開し直す設定を持っていて、Zebra のハードウェアなら Convert UPC-E to UPC-A がそのパラメータです。ただし設定であって保証ではないので、思い込まずに確認してください。製品コードを求められたら、UPC-A を生成します。

Code 128 が幅を詰められる仕組み

Code 128 には 3 つの文字サブセットがあります。A は制御文字と英大文字、B は小文字を含む印字可能な ASCII、そして C は数字を2 桁ずつ——1 シンボルあたり 1 桁ではなく 2 桁——エンコードします。

この 2 桁まとめが、Code 128 が長い数字列に強い理由であり、シンボル長が入力に比例しない理由でもあります。20 桁のシリアル番号はサブセット C なら 10 シンボル。同じ 20 桁がサブセット B では 20 シンボルです。C に切り替えないエンコーダーは、必要な幅のおよそ 2 倍のシンボルを吐きます。

切り替えには 1 シンボルかかるので、ある閾値を超えて初めて元が取れます。ISO/IEC 15417 Annex E の最小化ルールをならすと、おおよそこうなります。データの先頭または末尾なら 4 桁以上——開始シンボルやペイロードの終端がコストを吸収してくれます。途中なら 6 桁以上——サブセット C に入るための切り替えシンボルと、出るためのシンボルを 1 つずつ使う必要があります。これを下回れば、今のサブセットに留まるほうが安く済みます。実装がどこまで突き詰めるかは様々で、JsBarcode は固定の閾値を使い、zint は閾値を使わず最小コストのエンコードを探索します。そのため規格に適合した 2 つのエンコーダーが、同じ入力に対して異なるシンボル数を出すことがあります。復号結果はどちらも同じ文字列です。

開始シンボル以降のすべてが、モジュロ 103(mod-103)のチェックシンボルに寄与します。

checksum = (start value + Σ(symbol value × position)) mod 103

位置は最初のデータシンボルを 1 として数え、開始シンボルは自身の値を 1 回だけ加算します。サブセットの切り替えも通常のシンボルで、他と同じように数えます。チェックシンボルはバーの中に埋め込まれ、人間可読文字(HRI)には一切現れません。Code 128 のラベルがエンコードした内容をそのまま表示しながら、目に見えないシンボルを 1 つ余分に抱えているのはこのためです。

GS1-128 が必要になる場面

出荷ラベルは複数のフィールドを載せることがよくあります。GTIN、ロット番号、有効期限、シリアル番号。これらを 1 つの Code 128 シンボルに詰め込むための取り決めが GS1-128 です。別のシンボロジーではなく、Code 128 にデータ構造を足したものです。

各フィールドの先頭にはアプリケーション識別子(AI)が付きます。2〜4 桁の接頭辞で、後ろに何が来て何桁なのかを宣言します。(01) は GTIN-14、(17)YYMMDD 形式の有効期限、(10) はロットコードです。

(01)09501101020917(17)260801(10)ABC123

丸括弧は人間が読むための表記で、エンコードされることはありません。バーの中では FNC1 が構造を示します。1 回目は最初のシンボルとして置き、データが GS1 形式であることを宣言します。2 回目は、末尾ではない可変長フィールドを終端するために置きます。(01)(17) のような固定長のアプリケーション識別子は識別子から桁数が決まるので終端は不要ですが、(10) のような可変長のものには必要です。

ここで汎用の Code 128 エンコーダーと GS1-128 エンコーダーは道が分かれます。汎用エンコーダーに上の文字列を渡すと、丸括弧をそのまま 1 文字としてエンコードし、FNC1 は入りません。シンボルはスキャンでき、括弧付きの文字列が返り、GS1 を理解するシステムはそれを拒否します。ZeroTool のジェネレーターは汎用の Code 128 エンコーダーで、入力を字面どおりに扱います。アプリケーション識別子の構文を解釈しないツールとしては、これが正直な挙動です。本物の GS1-128 が必要なら、アプリケーション識別子を理解するエンコーダーを使ってください。zint --barcode=16 --data="[01]09501101020917[17]260801[10]ABC123" は FNC1 の配置を処理してくれますし、識別子の一覧は GS1 自身の Digital Link と構文に関する資料にまとまっています。

データが本当に構造化されていて、読み取り側のハードウェアが二次元コードに対応しているなら、QR コードや GS1 DataMatrix を検討してください。フィールドを 4 つ抱えるまで伸びた一次元シンボルは、たいていデータがそのフォーマットに収まらなくなった合図です。

コードレビューをすり抜ける失敗パターン

正しくエンコードするのは簡単なほうの半分です。ここから挙げるのは、テストは全部通るのに荷役の現場で落ちるものです。

クワイエットゾーンが切り落とされる。 一次元シンボルはどれも左右に余白を必要とし、スキャナーはそこを手がかりにシンボルの開始位置を見つけます。必要な幅はシンボロジーごとにモジュール単位で規定されていて、左右対称とは限りません。EAN-13 は左に 11 モジュール、右に 7 モジュール。EAN-8 は両側に 7 モジュール。UPC-A は 9 モジュール、Code 128 は最低 10 モジュールです。デザイナーは余白を見つけてレイアウトを詰めますし、スクリプトは画像をバウンディングボックスに合わせてトリミングします。バーは無傷のまま、シンボルは読めなくなります。クワイエットゾーンを含めて書き出し、内容に合わせたトリミングは絶対にしないでください。

誰かがブランドカラーに合わせる。 復号はバーと背景のコントラストに依存します。ベージュのラベルに中間グレーのバー、黒地に濃紺、白抜きの反転シンボル——白地に黒の校正刷りを完璧に読めたハードウェアが、これらでは読めなくなります。赤は特殊なケースです。多くのレーザースキャナーは赤色光源を使うため、白地の赤いバーは丸ごと消えてしまうことがあります。明るい背景に濃いバー、を守ってください。

最も細いバーがプリンターの再現限界より細い。 ベクター上では成立するモジュール幅も、203 dpi のサーマルプリンターではにじんだ灰色の線になります。感熱式やインクジェットはインクがわずかに広がるため、バーが太り、スペースが狭まります。本番と同じサイズ・同じ用紙でテストラベルを印刷し、実際に使うハードウェアでスキャンしてください。スマホのカメラは固定式レーザーよりはるかに寛容なので、代わりにはなりません。

ペイロードがスキャナーの読み取り幅を超える。 可変長のシンボロジーには明確な上限がないので、300 文字の文字列でもエンコードできてしまいます。出来上がるのは構造上は正しく、物理的に読めないシンボルです。ハンディスキャナーは読み取り窓に収まった分しか復号できないからです。Code 128 はおよそ 48 文字まで、Code 39 と Codabar はおよそ 43 文字までに抑えてください。この 2 つは密度が低く、使える幅を先に使い切ります。長いペイロードは二次元シンボルの領分です。

ITF-14 からベアラバーが消える。 Interleaved 2 of 5 は部分読み取りに弱く、シンボルの一部だけを横切るように走査すると、短く誤った数字が復号できてしまいます。ITF-14 の上下にある太い横線はそれを防ぐためにあり、装飾ではなく規格の一部です。ラベルの面積を稼ぐために削ると、カートンが別の GTIN として読まれます。

Code 39 のチェックキャラクタで両端の認識がずれる。 Code 39 のモジュロ 43(mod-43)チェックキャラクタは任意です。想定していないスキャナーは、それをデータの最後の 1 文字として報告します。必須としているシステムは、付いていないシンボルを拒否します。有効にする前に受け入れ側がどちらを求めるかを確認してください。またこの文字は人間可読文字にも現れます。

印刷前の検証

最も強い検証はラウンドトリップです。ペイロードをエンコードし、ラスタライズし、別実装のデコーダーで読み戻します。入れたものと戻ってきたものが一字一句同じでなければ、見た目がどうであれそのシンボルは間違っています。

最近の Chromium 系ブラウザーは、デコーダーを標準で公開しています。

// 描画済みのバーコードを、ブラウザー内蔵のスキャナーで復号する。
const detector = new BarcodeDetector();
const results = await detector.detect(canvas);
console.log(results.map(r => `${r.format}:${r.rawValue}`));
// → ["ean_13:5901234123457"]

形だけの確認で終わらせないための細部が 2 つあります。細いバーがサンプリングで潰れないよう、表示サイズの数倍でラスタライズすること。そして復号結果を、チェックデジットを含めたペイロードと比較すること。データ部だけを比較すると、いちばん重要なバグがちょうど隠れます。

バッチ処理ではブラウザーの外で検証します。zint が描画し、zbarimg が復号します。

# EAN-13 のラベルをまとめて生成し、それぞれが元の番号に
# 復号されるか検証する。
while read -r gtin; do
  zint -b EAN13 --data="$gtin" --output="labels/$gtin.png" --height=60
  decoded=$(zbarimg --quiet --raw "labels/$gtin.png")
  [ "$decoded" = "$gtin" ] || echo "MISMATCH: $gtin -> $decoded"
done < gtins.txt

ツールについて 2 点。zint では数値 ID ではなくシンボロジーの名前を使ってください。-b EAN13 は安定していますが、古い数値の 13 は、新しいリリースが後方互換のためだけに残しているレガシーな EANX タイプを指します。そして zbarimg --raw は復号値だけを出力します。上の文字列比較が安全に成り立つのはそのおかげで、人間向けのブロックを出力するデコーダーだと、比較の前にパースが必要になります。

Python でも、ジェネレーターを守っているのと同じチェックデジットのロジックで、データの取り込みを守るべきです。

def check_digit(data: str) -> int:
    """GS1 モジュロ 10。データ部の右端を基点にする。"""
    total = 0
    for i, char in enumerate(reversed(data)):
        total += int(char) * (3 if i % 2 == 0 else 1)
    return (10 - total % 10) % 10


def is_valid_gtin(code: str) -> bool:
    # code[:-1] ではなく文字列全体を検査する。そうしないと末尾の
    # 非数字が検査をすり抜け、False を返す代わりに int() が例外を投げる。
    return code.isdigit() and int(code[-1]) == check_digit(code[:-1])


assert is_valid_gtin('5901234123457')
assert not is_valid_gtin('5901234123456')
assert not is_valid_gtin('590123412345X')

取り込み時に GTIN を検証すれば、転記ミスがまだ安いうちに捕まります。スプレッドシートの 1 桁の誤りを直すのは 1 分ですが、同じ 1 桁が 1 万個のカートンに印刷されたときのコストは桁が違います。

ZeroTool のジェネレーターが違うところ

オンラインのバーコード作成サービスの多くは、入力をサーバーへ送って画像を生成します。公開済みの商品 GTIN なら実害はありません。未発売の SKU、社内の資産タグ、シリアル番号となると、技術的な理由もなくデータが自分のマシンから出ていくことになります。エンコード処理はルックアップテーブルと少しの算術で、ブラウザーの中で完結すべきものです。

ZeroTool のバーコードジェネレーターは、すべてクライアントサイドでエンコードします。データ桁を入力すればチェックデジットを付与し、完成したコードを貼り付けた場合は代わりに検証して、食い違っていれば正しい値を教えます。バーの色は意図的に変更できません。コントラストの低い配色はハードウェアで読めないラベルの最大の原因なので、選択肢を消すことで失敗も消しています。可変長のペイロードがハンディスキャナーの一度の読み取り幅を超えたときは、黙って使えないシンボルを出す代わりに、ステータス行がそれを伝えます。

書き出しは SVG と PNG です。印刷や、あとでサイズを変えるものには SVG を選んでください。どの倍率でもバーの端が正確なままで、スキャナーが気にするのはまさにそこです。PNG は画面表示の 4 倍でラスタライズされるので、細いモジュールも潰れません。

単発のラベルではなくパイプラインを組むなら、ライブラリを使ってください。コマンドラインなら zint、Python なら python-barcode、Node なら JsBarcode。このジェネレーターは、依存関係を 1 つ増やすコストのほうがラベルの価値を上回る場面のためのものです。

参考リンク

ZeroTool の関連ツール:

規格と一次資料: