OCR を使って、紙の書類をデータ化して kintone に移行しよう

目次

はじめに

社内のシステムとして kintone を導入したり、kintone を利用してペーパーレス化を行いたいときなどに、社内に蓄積された紙の書類をデータにするのは一苦労です。

今回の記事は JavaScript ライブラリの Tesseract.js を用いて、紙の書類の電子化を楽にするカスタマイズを紹介します。

完成イメージ

今回のカスタマイズを適用することで、レコード作成画面、レコード編集画面上で OCR で画像ファイルを解析し、読み取ったテキストを文字列(複数行)フィールドに転記できます。
また、解析した画像ファイルも、kintone の添付ファイルフィールドに保存されます。

画像データを解析する流れ

  1. 「ファイルを選択」ボタンで、解析したい画像ファイルを読み込みます。
  2. 「解析する」ボタンを押すと画像解析が始まります。

  3. 解析された結果が表示されます。

  4. ← ボタンを押すことで内容をまるごと転記できます。

    あるいは、マウスドラッグで必要な箇所のみ選択して転記できます。
    解析に使ったファイルは、そのまま kintone に保存されます。

実際の操作の流れは、下記 GIF 画像を確認してください。

デモ環境

デモ環境で実際に動作を確認できます。
https://dev-demo.cybozu.com/k/340/ (External link)

ログイン情報は cybozu developer network デモ環境 で確認してください。

アプリの準備

「はじめから作成」でアプリを新規作成し、文字列(複数行)フィールド、ボタンや読み取ったテキストを配置するスペースフィールドと、添付ファイルフィールドを次のように設定しましょう。

フィールドの種類 フィールドコード 備考
スペース recognize_space ファイルを読み取る Input 要素や解析のための Button 要素などを設置します。
文字列(複数行) text kintone にデータを保持するための文字列フィールドです。
添付ファイル file 解析したファイルを kintone に格納するためのファイルフィールドです。

サンプルコード

ocr.js

本サンプルのプログラムです。JavaScript ファイルとして保存してください。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/*
 * tessaract sample program
 * Copyright (c) 2023 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

(() => {
  'use strict';

  let fileInput;
  let fileKey;

  kintone.events.on(['app.record.create.show', 'app.record.edit.show'], (event) => {
    kintone.app.record.setFieldShown('file', false);

    const copyButton = document.createElement('button');
    copyButton.innerText = '←';
    copyButton.onclick = () => {
      event.record.text.value = resultText.innerText;
      kintone.app.record.set(event);
    };

    fileInput = document.createElement('input');
    fileInput.type = 'file';
    fileInput.accept = 'image/*';

    const resultText = document.createElement('div');

    const recognizeButton = document.createElement('button');
    recognizeButton.innerText = '解析する';
    recognizeButton.onclick = () => {

      if (!fileInput.files.length > 0) {
        alert('読み取りたい文字の画像ファイルを選択してください');
        return;
      }

      const reader = new FileReader();

      reader.onload = async () => {
        const worker = await Tesseract.createWorker({
          logger: m => {
            console.log(m);
            resultText.innerText = `${m.status} : ${Math.round(m.progress * 100)}%`;
          },
        });

        await worker.loadLanguage('jpn');
        await worker.initialize('jpn');
        await worker.setParameters({
          preserve_interword_spaces: '1',
        });

        const {data: {text}} = await worker.recognize(reader.result);
        resultText.innerText = text;
        await worker.terminate();
      };

      reader.readAsDataURL(fileInput.files[0]);
    };

    const recognizeSpace = kintone.app.record.getSpaceElement('recognize_space');
    recognizeSpace.style.width = '600px';
    recognizeSpace.appendChild(copyButton);
    recognizeSpace.appendChild(fileInput);
    recognizeSpace.appendChild(recognizeButton);
    recognizeSpace.appendChild(resultText);

    return event;
  });


  kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], async (event) => {
    if (fileInput.files.length === 0) {
      fileKey = undefined;
      return event;
    }

    const formData = new FormData();
    formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());
    formData.append('file', fileInput.files[0]);

    const headers = {
      'X-Requested-With': 'XMLHttpRequest',
    };
    const resp = await fetch('/k/v1/file.json', {
      method: 'POST',
      headers,
      body: formData,
    });
    const respData = await resp.json();
    fileKey = respData.fileKey;

    return event;
  });

  kintone.events.on(['app.record.create.submit.success', 'app.record.edit.submit.success'], async (event) => {
    if (fileKey == null) return event;
    const recordId = event.recordId;
    const params = {
      app: kintone.app.getId(),
      id: recordId,
      record: {
        file: {
          value: [{fileKey: fileKey}]
        }
      }
    };
    await kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', params);

    return event;
  });
})();

サンプルコードの解説

ボタン等の各種要素の設置

レコード追加画面を表示した後のイベント レコード編集画面を表示した後のイベント で、下記画像のように、スペースフィールド(recognize _space)の中に 4 つの要素を用意します。

ファイルインプット

ファイルを読み込む要素を作ります。画像ファイルだけを選択できるよう、MIME タイプで絞り込んでいます。
また、今回は他のイベントでもこの変数を利用するため、レコード追加画面表示・レコード編集画面表示イベントの外のスコープに、変数の宣言をしています。

1
2
3
fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
解析ボタン

画像ファイルを読み込ませたあと、解析ボタンを押すことで画像解析をさせるためのものです。
onclick イベントに、解析をするためのコードを定義します。
tessaract の API それぞれの詳細は ドキュメント (External link) を確認してください。(英語)
進捗や結果などをテキスト表示用の要素(resultText)に innerText で挿入しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const recognizeButton = document.createElement('button');
recognizeButton.innerText = '解析する';
recognizeButton.onclick = () => {

  if (!fileInput.files.length > 0) {
    alert('読み取りたい文字の画像ファイルを選択してください');
    return;
  }

  const reader = new FileReader();

  reader.onload = async () => {
    const worker = await Tesseract.createWorker({
      logger: m => {
        console.log(m);
        resultText.innerText = `${m.status} : ${Math.round(m.progress * 100)}%`;
      },
    });

    await worker.loadLanguage('jpn');
    await worker.initialize('jpn');
    await worker.setParameters({
      preserve_interword_spaces: '1',
    });

    const {data: {text}} = await worker.recognize(reader.result);
    resultText.innerText = text;
    await worker.terminate();
  };

  reader.readAsDataURL(fileInput.files[0]);
};
テキスト表示

画像解析状況の進捗、画像解析した結果を表示するためのものです。今回は Div 要素を利用します。

1
const resultText = document.createElement('div');
コピーボタン

解析したテキストを文字列フィールドにコピーするためのものです。
onclick イベントで、kintone.app.record.set() 関数を用いて文字列フィールドに解析結果をコピーしています。

1
2
3
4
5
6
const copyButton = document.createElement('button');
copyButton.innerText = '←';
copyButton.onclick = () => {
  event.record.text.value = resultText.innerText;
  kintone.app.record.set(event);
};

上記用意したものを、次のようにスペースフィールドにそれぞれ追加します。

1
2
3
4
5
6
const recognizeSpace = kintone.app.record.getSpaceElement('recognize_space');
recognizeSpace.style.width = '600px';
recognizeSpace.appendChild(copyButton);
recognizeSpace.appendChild(fileInput);
recognizeSpace.appendChild(recognizeButton);
recognizeSpace.appendChild(resultText);

ファイルアップロード処理

レコード保存前のイベントで、解析に使われた画像ファイルのアップロードを行います。
ファイルアップロードの方法詳細については「 ファイルをアップロードする」記事を確認してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], async (event) => {
  if (fileInput.files.length === 0) {
    fileKey = undefined;
    return event;
  }

  const formData = new FormData();
  formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());
  formData.append('file', fileInput.files[0]);

  const headers = {
    'X-Requested-With': 'XMLHttpRequest',
  };
  const resp = await fetch('/k/v1/file.json', {
    method: 'POST',
    headers,
    body: formData,
  });
  const respData = await resp.json();
  fileKey = respData.fileKey;

  return event;
});

アップロードしたファイルとレコードの関連付け

アップロードしたファイルとレコードの関連付けは、REST API 経由でしか行えないため レコード追加画面で保存に成功した後のイベント で行います。
アップロードしたファイルとレコードの関連付けについての詳細は「 ファイルアップロードで必須となる3つの手順」記事を確認してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
kintone.events.on(['app.record.create.submit.success', 'app.record.edit.submit.success'], async (event) => {
  if (fileKey == null) return event;
  const recordId = event.recordId;
  const params = {
    app: kintone.app.getId(),
    id: recordId,
    record: {
      file: {
        value: [{fileKey: fileKey}]
      }
    }
  };
  await kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', params);

  return event;
});

動作の確認

ライブラリファイルの準備

今回は OCR のために、Tesseract.js v4.0.2 を利用します。
CDN が次に公開されているため、下記 JavaScript ファイルをリンクとして設定します。

https://unpkg.com/tesseract.js@4.0.2/dist/tesseract.min.js

カスタマイズの適用

さきほど用意したサンプルコードと OCR のためのライブラリを kintone 環境に適用します。適用方法はヘルプ記事「 JavaScriptやCSSでアプリをカスタマイズする (External link) 」を参照してください。

サンプルコードを正常に動作させるために、ライブラリファイルを先に読み込む必要があります。

画像を解析する

こちらからサンプルの画像ファイルをダウンロードし、カスタマイズしたアプリ上で読み込ませ、動作を確認してください。

おわりに

今回の記事は、Tesseract.js を利用して画像から既存の資産を kintone に移しやすくするカスタマイズを紹介しました。

今回は画像のみで PDF には対応していませんが、PDF も一度画像に変換する処理などをすることにより PDF にも対応できます。興味のある方はぜひ確認してみてください。

information

この Tips は、2023 年 5 月版 kintone で動作を確認しています。