【GAS】画像を送ると文字に起こすLINEbot【OCR】

## 概要
OCR機能を備えたサービスなんて山ほどあるのに、何故わざわざ面倒な事をするのか・・・。

・GASならスプレッドシートやカレンダーと連携しやすい。
・LINEならWebhookが使えるから送信後、即レスポンス。
・LINEのメッセージは住所やリンクから勝手にアプリと連携する。

などなど汎用性や利便性が高そうなので、「LINEに画像を送るとGASを経由して文字に起こすBot」が出来たら何かと使えるんじゃないかなと思って作成。

コチラは、コード全文とその簡単な解説をする記事。

何が素晴らしいって、GoogleとLINEのアカウントがあれば全て無料で実装可能！

## 続編あり
Qiita記事：[【GAS】画像を文字に起こすLINEbotに翻訳機能を追加（言語サポート全部乗せ）](https://qiita.com/YasumiYasumi/items/c357b0197e1d7779dd0f)

## LINEデベロッパーズ
LINEのIDがあれば、無料でLINEのオフィシャルアカウントを作れるサービスがある。そちらでLINEBot用のアカウントを作成し、初期設定を行う。

LINEBot導入については、詳しく説明しているサイトが多いので割愛。

オフィシャルサイト
https://developers.line.biz/ja/

メッセージAPIのリファレンス
https://developers.line.biz/ja/reference/messaging-api/

## GoogleDriveAPI
画像をOCRで文字に起こす際に`Drive.Files.insert`と言うメソッドが必要。それを使うためにはGASの「DriveAPI」を有効にしなければならない。

こちらも詳しく説明しているサイトが多いので割愛。

## 下ごしらえ
LINEデベロッパーズで作ったBot用アカウントのアクセストークンを取得。

GAS側でウェブアプリ公開の操作をしてアドレスを作成、LINE側はWebhookを有効にする。

ウェブアプリ公開の手順も割愛<(_ _)>

## 実行結果
お菓子の箱を読み込ませてみたけど、なかなか優秀 ﾟДﾟ)ﾌﾑﾌﾑ
![Screenshot_20201206-234920-01.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/f5266ea6-3356-1afd-c288-4999e2d9a472.jpeg)
![Screenshot_20201206-234938-01.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/7ae116d4-bd37-89cd-a3ea-55ceea2bcf15.jpeg)

オフィシャルアカウントのアイコンは「いらすとや」さんの「虫眼鏡で本を読むお年寄りのイラスト」を使用。
https://www.irasutoya.com/2016/03/blog-post_814.html

## コード全文
<details><summary>この後バラして説明するけど、とりあえずコード全文を見るならコチラ。</summary><div>

```javascript
var TOKEN = "xxxxxxxxxxxxxxxxxxxx";
var replyURL = "https://api.line.me/v2/bot/message/reply";

//LINEに投稿があったら実行
function doPost(event) {

  //投稿を取得 返信用TOKEN,メッセージタイプ,メッセージIDを抽出
  var json = JSON.parse(event.postData.contents);
  var replyToken = json.events[0].replyToken;
  var type = json.events[0].message.type;
  var messageId = json.events[0].message.id;
  
  //投稿が画像でない時の処理
  if (type !== "image"){
  
    replyBot(replyToken);
    return;
  
  }

  //投稿が画像の時 画像のURLを生成し画像取得
  var imageURL = "https://api-data.line.me/v2/bot/message/" + messageId + "/content";
  var image = getImage(imageURL);
  
  //取得した画像をドライブに保存したい場合使用
  //var folder = DriveApp.getFolderById("ドライブのフォルダID");
  //folder.createFile(image);
  
  //取得した画像を文字に起こす
  var ocrText = getText(image);
  
  //起こした文字をLINEで返信する
  replyText(ocrText,replyToken);
  
}

//投稿が画像でないときのメッセージ
function replyBot(replyToken) {
  
  //メッセージ作成
  var botMessage = "画像を送ってね";
  var payload = JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "text",
        "text": botMessage
      }]
  });
  
  //メッセージ送信
  UrlFetchApp.fetch(replyURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;

}

//投稿から画像を取得
function getImage(imageURL) {

  //取得と同時にBlobしておく
  var image = UrlFetchApp.fetch(imageURL, {
       "headers": {
         "Content-Type": "application/json; charset=UTF-8",
         "Authorization": "Bearer " + TOKEN,
       },
       "method": "get"
  }).getBlob();
  
  return image;
  
}

//画像を文字に起こす
function getText(image) {
  
  //画像のタイトルとタイプを取得
  var title = image.getName();
  var mimeType = image.getContentType();
  
  //ドキュメントを作成し画像を挿入（DriveAPIの設定が必要）
  var resource = {title: title, mimeType: mimeType};
  var fileId = Drive.Files.insert(resource, image, {ocr: true}).id;
  
  //ドキュメントからテキストを取得したらドキュメントを削除
  var document = DocumentApp.openById(fileId);
  var ocrText = document.getBody().getText().replace("\n", "");
  Drive.Files.remove(fileId);
  
  return ocrText;

}

//起こした文字をLINEで返信
function replyText(ocrText,replyToken) {
  
  //メッセージ作成
  var payload = JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "text",
        "text": ocrText
      }]
  });
  
  //メッセージ送信
  UrlFetchApp.fetch(replyURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;

}
```

</div></details>

## コード①
LINEに投稿があった場合に実行するコード

```javascript
var TOKEN = "××××××××××××××××××××××";
var replyURL = "https://api.line.me/v2/bot/message/reply";

//LINEに投稿があったら実行
function doPost(event) {

  //投稿を取得 返信用TOKEN,メッセージタイプ,メッセージIDを抽出
  var json = JSON.parse(event.postData.contents);
  var replyToken = json.events[0].replyToken;
  var type = json.events[0].message.type;
  var messageId = json.events[0].message.id;
  
  //投稿が画像でない時の処理
  if (type !== "image"){
  
    replyBot(replyToken);
    return;
  
  }

  //投稿が画像の時 画像のURLを生成し画像取得
  var imageURL = "https://api-data.line.me/v2/bot/message/" + messageId + "/content";
  var image = getImage(imageURL);
  
  //取得した画像をドライブに保存したい場合使用
  //var folder = DriveApp.getFolderById("ドライブのフォルダID");
  //folder.createFile(image);
  
  //取得した画像を文字に起こす
  var ocrText = getText(image);
  
  //起こした文字をLINEで返信する
  replyText(ocrText,replyToken);
  
}
```

LINEデベロッパーズで設定するアクセストークンと、返信用のHTTPリクエストURLをグローバルに定義。Webhookを有効にすることで、LINEに投稿があれば`doPost`が実行される。

投稿がJSON形式で取得されるので、そこから返信用の`replyToken`、`メッセージのタイプ`、`メッセージID`を読み込んでおく。

メッセージタイプは、テキスト、スタンプ、画像、動画、音声、位置情報など様々。

今回は画像から文字を起こすので、投稿が画像でなければ「画像をちょうだい」ってメッセージを返すようにする。

で、画像だった場合にはその画像を使いたいので、LINEデベロッパーズAPIを使って取得する。メッセージから画像や動画、音声などのコンテンツを受け取る場合のURLは`https://api-data.line.me/v2/bot/message/{messageId}/content/`となるので、メッセージのIDを取得してURLを作成、画像を取得する関数へ渡す。

画像を取得したら、それを文字に起こすための関数へ渡し、文字が返って来たらそれをLINEのメッセージで送る関数へ渡すという流れになっている。

※取得した画像をドライブへ保存させたい場合のコードも記述してみたけど、とりあえず使わないのでコメントアウトさせている。

## コード②
投稿が画像でなかった場合。

```javascript
//投稿が画像でないときのメッセージ
function replyBot(replyToken) {
  
  //メッセージ作成
  var botMessage = "画像を送ってね";
  var payload = JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "text",
        "text": botMessage
      }]
  });
  
  //メッセージ送信
  UrlFetchApp.fetch(replyURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;

}
```

`payload`の`texe`にテキストを入れればメッセージを送信できる。

この場合は、投稿が画像じゃなかったので「画像を送ってね」と返信している。

## コード③
投稿から画像を取得する。

```javascript
//投稿から画像を取得
function getImage(imageURL) {

  //取得と同時にBlobしておく
  var image = UrlFetchApp.fetch(imageURL, {
       "headers": {
         "Content-Type": "application/json; charset=UTF-8",
         "Authorization": "Bearer " + TOKEN,
       },
       "method": "get"
  }).getBlob();
  
  return image;
  
}
```

事前にAPI用に作成したURLへ画像を取りに行くだけ。ついでに`getBlob()`しておく。

## コード④
ココが肝！取得した画像をドキュメントにして、OCRで文字に起こす。

```javascript
//画像を文字に起こす
function getText(image) {
  
  //画像のタイトルとタイプを取得
  var title = image.getName();
  var mimeType = image.getContentType();
  
  //ドキュメントを作成し画像を挿入（DriveAPIの設定が必要）
  var resource = {title: title, mimeType: mimeType};
  var fileId = Drive.Files.insert(resource, image, {ocr: true}).id;
  
  //ドキュメントからテキストを取得したらドキュメントを削除
  var document = DocumentApp.openById(fileId);
  var ocrText = document.getBody().getText().replace("\n", "");
  Drive.Files.remove(fileId);
  
  return ocrText;

}
```

ここで画像をドキュメントに渡してOCRで文字に起こすんだけど、冒頭でも記述したように、`Drive.Files.insert`メソッドでドキュメントを作成するには、DriveAPIの設定が必要。

最初に改行が入ってしまうので`replace`している。

最後に、テキスト取得後のドキュメントファイルを削除^^)b

## コード⑤
画像から起こした文字をLINEで返信

```javascript
//起こした文字をLINEで返信
function replyText(ocrText,replyToken) {
  
  //メッセージ作成
  var payload = JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "text",
        "text": ocrText
      }]
  });
  
  //メッセージ送信
  UrlFetchApp.fetch(replyURL, {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + TOKEN,
      },
      "method": "post",
      "payload": payload
  });
  return;

}
```

コード②と理屈は同じ。取得したテキストをLINEに返信している。

## その他
ベースがGASなので、スプレッドシートはもちろんメールやカレンダーとも紐付けやすいし、翻訳を絡めるのも簡単^^)b

メッセージIDをスプレッドシートに残しておいて、LINEからの操作で呼び出す事なんかも出来るかも？

## 補足記事
今回使用したLINEデベロッパーズAPIについて、ちょっとだけ補足した記事。
[【GAS】LINEbotで画像やテキストを受け取ったりメッセージを送信したりする](https://qiita.com/YasumiYasumi/items/a2a08909a86c8582e69f)

## 後日談
ある日、急にOCRが機能しなくなった。
画像以外を送った時のリアクションは活きてるからBotは動作してるみたい。

コードを変えたわけじゃないのになんでだろうと色々調べてたら、Lineのリファレンスのコンテンツのリクエストの部分が

```shell
curl -v -X GET https://api-data.line.me/v2/bot/message/{messageId}/content \
-H 'Authorization: Bearer {channel access token}'
```
ってなってた。

自作コード内のリクエストは
`https://api-data.line.me/v2/bot/message/{messageId}/content/`

まさかな・・・と思いながら末尾のスラッシュを取ってみたところ動き始めたとさ。
こんなことあるのね^^;

と言うわけで、本文中のコードも末尾スラッシュ無しに修正。2022/3/19

### おしまい