画像に写った文字を、コピペできるテキストに起こしたい——領収書、名刺、スクショ、資料の写真。いわゆる OCR ですが、「専用アプリを入れるほどでもない」「画像をどこかにアップロードするのはちょっと不安」で止まりがちです。
この記事では、Tesseract.js を使って、画像をアップロードせずブラウザの中だけで OCR する方法をまとめます。特に、外部CDNを読めない(CSPが厳しい)環境で ライブラリ一式を自前ホストするときの手順と、そこで一度ハマった落とし穴を残しておきます。
ゴール:画像を選ぶだけで、日本語・英語の文字をブラウザ内で読み取ってテキスト化できるようになること。
Tesseract.js とは
Tesseract.js は、OSSのOCRエンジン Tesseract を WebAssembly に移植したものです。ポイントは次の3つ。
- ブラウザ完結:画像をサーバーに送らず、端末の中だけで認識できる。
-
多言語:日本語(
jpn)・英語(eng)をはじめ100以上の言語データがある。 - 重い:エンジン本体(wasm)と言語データを読み込むので、初回は数MB〜十数MBのダウンロードが走る。
「軽くはないけど、プライバシー的に安心・サーバー不要」というのが最大の魅力です。
基本:createWorker で読み取る
一番シンプルな形はこれだけです。CDNをそのまま使える環境なら、これで動きます。
// createWorker(言語, oem, options)
const worker = await Tesseract.createWorker("jpn+eng");
const { data } = await worker.recognize(file); // file は File / Blob / <img> など
console.log(data.text); // 読み取れたテキスト
await worker.terminate(); // 使い終わったら必ず破棄
"jpn+eng" のように + でつなぐと、日本語と英語を同時に認識できます。日本語の資料に英数字が混ざるケースは多いので、この指定が便利です。
外部CDNを読めない環境で「自前ホスト」する
ここからが本題です。セキュリティ要件で CSP(Content-Security-Policy) を絞っていると、https://cdn.jsdelivr.net などから勝手にスクリプトやwasmを取ってくる構成が使えません。この場合、Tesseract.js の関連ファイルをすべて自分のサイトに置いて、パスを明示的に渡します。
必要なファイルはざっくり4種類です。
-
tesseract.min.js… 本体(<script>で読み込む) -
worker.min.js… Web Worker 用スクリプト -
tesseract-core*.wasm.jsと.wasm… OCRエンジン本体(複数バリアントあり) -
lang/eng.traineddata.gzlang/jpn.traineddata.gz… 言語データ
配置したら、createWorker の第3引数でパスを渡します。
const worker = await Tesseract.createWorker("jpn+eng", 1, {
workerPath: "assets/vendor/tesseract/worker.min.js",
corePath: "assets/vendor/tesseract/", // ← ディレクトリを渡す
langPath: "assets/vendor/tesseract/lang/", // 言語データの置き場所
gzip: true, // .gz のまま置いているので true
});
corePath はファイル名まで指定せず、ディレクトリを渡すのがコツです。理由は次のとおり。
落とし穴:oem=1(LSTM)は -lstm コアを要求する
createWorker の第2引数 1 は OCRエンジンモード(oem) で、1 は精度の高い LSTM(ニューラルネット) を意味します。実務ではだいたいこれを使います。
ここで一度ハマりました。エンジン本体(core)には、環境に応じた複数のバリアントがあります。
-
tesseract-core.wasm.js(通常) -
tesseract-core-simd.wasm.js(SIMD対応で高速) tesseract-core-lstm.wasm.jstesseract-core-simd-lstm.wasm.js
oem=1 を指定すると、Tesseract.js は実行時に -lstm 付きのコア(例:tesseract-core-simd-lstm.wasm.js)を要求します。 ところが通常版とSIMD版だけを置いていたため、こんなエラーで止まりました。
NetworkError: ... tesseract-core-simd-lstm.wasm.js failed to load
つまり corePath にディレクトリを渡していても、中に -lstm 系のファイルが無いと読み込めません。解決策はシンプルで、4バリアントすべて(.wasm.js と .wasm の両方)を置くこと。どれが選ばれるかは実行環境(SIMD対応かどうか)に依存するので、全部揃えておくのが安全です。
ポイント:oem=1 で
*-lstm.wasm.js failed to loadが出たら、-lstmと-simd-lstmのコアを追加し忘れていないか確認する。
進捗を表示する(初回は待たせるので大事)
初回はエンジンと言語データのダウンロードで数秒〜十数秒かかります。無言だと「固まった?」と誤解されるので、logger で状態を拾ってUIに出します。
const worker = await Tesseract.createWorker("jpn+eng", 1, {
workerPath: "assets/vendor/tesseract/worker.min.js",
corePath: "assets/vendor/tesseract/",
langPath: "assets/vendor/tesseract/lang/",
gzip: true,
logger: (m) => {
// m.status 例: "loading language traineddata", "recognizing text"
// m.progress は 0〜1
if (m.status) {
const pct = typeof m.progress === "number"
? " " + Math.round(m.progress * 100) + "%"
: "";
console.log(m.status + pct);
}
},
});
m.status は英語で来るので、"recognizing text" → 「文字を認識中…」のように辞書で日本語化すると親切です。m.progress はプログレスバーにそのまま使えます。
認識精度を上げるコツ
Tesseract は前処理でだいぶ結果が変わります。実際に効いたものだけ挙げます。
- 解像度を確保する。 小さすぎる画像は苦手。文字の高さがある程度大きくなるよう、必要なら拡大してから渡す。
-
言語を絞る。 英数字だけと分かっているなら
"eng"単独の方が速く・正確。逆に混在なら"jpn+eng"。 - コントラストと傾き。 くっきりした画像・水平な文字が有利。斜めの写真は補正すると精度が上がる。
-
終わったら terminate。 ワーカーは重いので、使い終わりに
worker.terminate()でメモリを解放する。
手元では、"Hello Morphy 2026" の英字画像が約3秒、日本語の短い画像が約1.7秒で、どちらも正確に読み取れました(初回のダウンロード時間を除く)。
つまずきやすいところ
- 初回が重い。 wasm+言語データで数MB〜。2回目以降はブラウザキャッシュが効くが、初回のダウンロード表示は必須。
-
-lstmコアの入れ忘れ(前述)。oem=1 なら-lstm系まで揃える。 -
gzipの指定ミス。.gzのまま置くならgzip: true、展開して置くならfalse。ここがずれると言語データを読めない。 - 手書きは苦手。 Tesseract は基本、活字向け。手書きメモの精度は期待しすぎない。
-
CSPの設定。 自前ホストなら
script-src 'self' 'wasm-unsafe-eval' blob:とworker-src 'self' blob:あたりが要る。wasm実行に'wasm-unsafe-eval'、ワーカー生成にblob:が必要になりやすい。
まとめ
- Tesseract.js を使えば、画像OCRをブラウザ内だけで完結できる(アップロード不要)
- 基本は
createWorker("jpn+eng")→recognize(file)→terminate()の3ステップ - CDNを読めない環境では関連ファイルを自前ホストし、
workerPath/corePath/langPathを明示する -
oem=1(LSTM)は
-lstm系のコアを要求するので、コアは4バリアント全部置くのが安全 - 初回は重いので
loggerで進捗表示を必ず入れる - 精度は解像度・言語指定・前処理で素直に上がる
「画像の文字を、安全に・サッとテキスト化したい」に、Tesseract.js × ブラウザ完結はよく効きます。参考になれば嬉しいです。