見出し画像

【VBA】OneNote操作(画像からテキスト抽出)

VBAでOneNoteを操作するコードサンプルです。
OneNoteのOCR(文字認識)機能を使って画像データからテキスト抽出する例と、OneNote上に画像データをアップロードする例を紹介します。
先に言ってきますが、OCR機能の精度(文字認識精度)はOneNoteに依存します。
※OneNoteは、Microsoft Office365に付いてくるメモアプリです。

OCR機能とは

画像データ(手書きや印刷文字の画像データ)内の文字を判別し、テキストを取得する機能です。光学式文字認識(Optical Character Reader)の略。
OneNoteでは、このOCR機能が標準搭載されています。
OneNoteに貼付けられた画像データは瞬時にスキャンされて、OCRテキスト情報としてOneNote内部に保存されます。通常は画像を右クリック>画像からテキストをコピー、で確認可能。

画像を右クリックしたところ
(この例だと、「SEVEN&i SEVENCAFÉ」とテキスト抽出できる)

このOCR機能(文字認識)を何かに活用できないかなーと思い、VBAで操作するサンプルを作りました。

OCRテキストを取得

OneNoteのOCRテキストを取得するコードがこちら。

Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwmilliseconds As LongPtr)

'参照設定
'Microsoft OneNote 15.0 Object Library
'Microsoft OneNote 15.0 Extended Type Library

Private Sub OneNote_OCR()
 'OneNote起動
 Dim exec As Object
 Set exec = CreateObject("Wscript.shell").exec("onenote.exe")
 Sleep 1000
 Dim app As OneNote.Application
 Set app = New OneNote.Application
 Sleep 3000
 
 'OneNote全体のXMLを取得
 Dim XML As String
 Dim Doc As Object 'MSXML2.DOMDocument60
 app.GetHierarchy vbNullString, hsPages, XML
 Set Doc = CreateObject("MSXML2.DOMDocument.6.0")
 Doc.LoadXML XML 'XML文字列をパース
 Doc.SetProperty "SelectionLanguage", "XPath"
 Doc.SetProperty "SelectionNamespaces", "xmlns:one='http://schemas.microsoft.com/office/onenote/2013/onenote'" '「one」の名前空間の指定
 
 'ページタイトルから対象ページを特定
 Dim node As Object 'MSXML2.IXMLDOMNode
 Dim PageName As String
 Dim pageID As String
 For Each node In Doc.SelectNodes("//one:Page")
  PageName = node.Attributes.getNamedItem("name").Text
  If PageName = "テスト" Then
   pageID = node.Attributes.getNamedItem("ID").Text 'pageIDをゲット
  End If
 Next
 Set Doc = Nothing
 If pageID = "" Then
  Debug.Print "対象ページが見つかりませんでした"
  Exit Sub
 End If
 
 'ページ内の全コンテンツを取得
 Dim pageXML As String
 app.GetPageContent pageID, pageXML
 
 'OCRテキストを抽出
 Dim nStart As Long
 Dim nEnd As Long
 Dim temp As String
 Dim Cnt As Integer
 Dim OCRs() As String
 ReDim OCRs(0)
 Do
  nStart = InStr(nStart + 1, pageXML, "<one:OCRText>")
  If nStart = 0 Then
   Exit Do
  Else
   nEnd = InStr(nEnd + 1, pageXML, "</one:OCRText>")
   temp = Mid(pageXML, nStart + 22, nEnd - nStart - 25)
   Cnt = Cnt + 1
   ReDim Preserve OCRs(Cnt)
   OCRs(Cnt) = temp
  End If
 Loop
 If UBound(OCRs) = 0 Then
  Debug.Print "OCRデータが見つかりませんでした"
  Exit Sub
 End If
 
 'OCRテキストを加工
 Dim arr() As String
 Dim i As Long
 Dim j As Long
 For i = 1 To UBound(OCRs) '画像ごとに処理
  Debug.Print "画像 " & i & "個目"
  arr = Split(OCRs(i), vbCrLf)
  For j = 1 To UBound(arr) '改行毎に処理
   Debug.Print j & ": " & arr(j)
  Next
 Next
 
 'OneNote終了
 exec.Terminate
 Set app = Nothing
 Set exec = Nothing
 
End Sub

OneNoteはXML形式で記述されているので、アプリケーション>ノートブック>セクショングループ>セクション>ページ>ページコンテンツと階層を辿っていけば、目的の画像にたどり着きます。
まずはgetHierarchyメソッドを使って、OneNoteの階層構造を取得します。
第一引数にnullを渡すとルート以下全てのノード(全ノートブック、セクション)が取得されます。第二引数は取得する範囲の下限値を定数で指定。第三引数がOutパラメーターで、ここに処理結果(XML出力)が格納されます。
次に、XMLドキュメントの中から、<one:Page>タグを1つづつ検索していきます。タグ内のname属性値から対象ページを特定し、pageIDを取得。
pageIDが取得できたら、GetPageContentメソッドを使って、ページ内の全てのコンテンツを同じくXML形式で取得します。
欲しい情報であるOCRテキストは、<one:OCRText>タグ内のinnerTextを参照すれば手に入ります。1つの画像につき1つの<one:PCRText>タグがあるので、画像の数だけinnerTextを取得すればOK。

ちなみに余談ですが、ページ内のコンテンツを操作したい場合は、こんな感じでXMLドキュメントからコンテンツを特定(Nodeや属性の特定)すればコンテンツ操作が可能です。

'貼り付けられた画像を左に寄せる
Set pageDoc = CreateObject("MSXML2.DOMDocument.6.0")
pageDoc.LoadXML (pageXML)
pageDoc.SetProperty "SelectionLanguage", "XPath"
pageDoc.SetProperty "SelectionNamespaces", "xmlns:one='http://schemas.microsoft.com/office/onenote/2013/onenote'"

Dim paneNode As Object 'MSXML2.IXMLDOMNode
Set pageNode = pageDoc.SelectSingleNode("one:Page//one:Position ")
Set PositionX = pageNode.Attributes.getNamedItem("x")
PositionX.Value = "72.0"

もしくは、

Dim PositionX As Object 'MSXML2.IXMLDOMAttribute
Set PositionX = pageDoc.SelectSingleNode("one:Page//one:Position/@x ")
PositionX.Value = "72.0"

のように書いてもOK。
後者の方は、階層ごとにダブルスラッシュで区切ってNodeを指定し、シングルスラッシュと@でタグ内属性を指定しています。
自分の書きやすい方で良いと思います。


画像をアップロード

続いてOneNoteにアップロードするサンプルです。

'待機用PAI
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwmilliseconds As LongPtr)
'Windows GDI+用API(画像サイズ取得用)
Private Declare PtrSafe Function GdiplusStartup Lib "gdiplus" (ByRef token As LongPtr, ByRef inputBuf As GdiplusStartupInput, ByVal outputBuf As Long) As Long
Private Declare PtrSafe Sub GdiplusShutdown Lib "gdiplus" (ByVal token As LongPtr)
Private Declare PtrSafe Function GdipLoadImageFromFile Lib "gdiplus" (ByVal FileName As LongPtr, ByRef image As LongPtr) As Long
Private Declare PtrSafe Function GdipDisposeImage Lib "gdiplus" (ByVal image As LongPtr) As Long
Private Declare PtrSafe Function GdipGetImageWidth Lib "gdiplus" (ByVal image As LongPtr, ByRef width As Long) As Long
Private Declare PtrSafe Function GdipGetImageHeight Lib "gdiplus" (ByVal image As LongPtr, ByRef height As Long) As Long
Private Type GdiplusStartupInput
 GdiplusVersion As Long
 DebugEventCallback As LongPtr
 SuppressBackgroundThread As Long
 SuppressExternalCodecs As Long
End Type

'参照設定
'Microsoft OneNote 15.0 Object Library
'Microsoft OneNote 15.0 Extended Type Library

Private Sub OneNote_OCR()
 'OneNote起動
 Dim exec As Object
 Set exec = CreateObject("Wscript.shell").exec("onenote.exe")
 Sleep 1000
 Dim app As OneNote.Application
 Set app = New OneNote.Application
 Sleep 3000
 
 'OneNote全体のXMLを取得
 Dim XML As String
 Dim Doc As Object 'MSXML2.DOMDocument60
 app.GetHierarchy vbNullString, hsPages, XML
 Set Doc = CreateObject("MSXML2.DOMDocument.6.0")
 Doc.LoadXML XML 'XML文字列をパース
 Doc.SetProperty "SelectionLanguage", "XPath"
 Doc.SetProperty "SelectionNamespaces", "xmlns:one='http://schemas.microsoft.com/office/onenote/2013/onenote'" '「one」の名前空間の指定
 
 'ページタイトルから対象ページを特定
 Dim node As Object 'MSXML2.IXMLDOMNode
 Dim PageName As String
 Dim pageID As String
 For Each node In Doc.SelectNodes("//one:Page")
  PageName = node.Attributes.getNamedItem("name").text
  If PageName = "画像解析テスト" Then
   pageID = node.Attributes.getNamedItem("ID").text 'pageIDをゲット
  End If
 Next
 Set Doc = Nothing
 If pageID = "" Then
  Debug.Print "対象ページが見つかりませんでした"
  Exit Sub
 End If
 
 'ページ内の全コンテンツを取得
 Dim pageXML As String
 app.GetPageContent pageID, pageXML
 
 '対象ページのXML記述を取得
 Dim pageDoc As Object 'MSXML2.DOMDocument60
 Set pageDoc = CreateObject("MSXML2.DOMDocument.6.0")
 pageDoc.LoadXML pageXML
 pageDoc.SetProperty "SelectionLanguage", "XPath"
 pageDoc.SetProperty "SelectionNamespaces", "xmlns:one='http://schemas.microsoft.com/office/onenote/2013/onenote'" '「one」の名前空間の指定
 '画像データを指定
 Dim FilePath As String
 FilePath = "C:\Users\****\Downloads\****.PNG"
 '<one:Page>の子階層に、<one:Image>タグを新規作成
 Dim pageNode As Object 'MSXML2.IXMLDOMNode
 Dim newElm As Object 'MSXML2.IXMLDOMElement
 Dim imgNode As Object 'MSXML2.IXMLDOMNode
 Dim newAtt As Object 'MSXML2.IXMLDOMAttribute
 Set pageNode = pageDoc.SelectSingleNode("one:Page ")
 Set newElm = pageDoc.createElement("one:Image")
 Set imgNode = pageNode.appendChild(newElm)
 Set newAtt = pageDoc.createAttribute("format")
 newAtt.Value = "png"
 imgNode.Attributes.setNamedItem newAtt
 '<one:Image>の子階層に、<one:Position>タグを新規作成
 Dim newNode As Object 'MSXML2.IXMLDOMNode
 Set newElm = pageDoc.createElement("one:Position")
 Set newNode = imgNode.appendChild(newElm)
 Set newAtt = pageDoc.createAttribute("x")
 newAtt.Value = "72.0"
 newNode.Attributes.setNamedItem newAtt
 Set newAtt = pageDoc.createAttribute("y")
 newAtt.Value = "50.0"
 newNode.Attributes.setNamedItem newAtt
 Set newAtt = pageDoc.createAttribute("z")
 newAtt.Value = "1"
 newNode.Attributes.setNamedItem newAtt
 '<one:Image>の子階層に、<one:Size>タグを新規作成
 Set newElm = pageDoc.createElement("one:Size")
 Set newNode = imgNode.appendChild(newElm)
 Set newAtt = pageDoc.createAttribute("width")
 newAtt.Value = "400.0"
 newNode.Attributes.setNamedItem newAtt
 Set newAtt = pageDoc.createAttribute("height")
 newAtt.Value = 400 * getAspect(FilePath)
 newNode.Attributes.setNamedItem newAtt
 '<one:Image>の子階層に、<one:Date>タグを新規作成
 Set newElm = pageDoc.createElement("one:Data")
 Set newNode = imgNode.appendChild(newElm)
 newNode.text = EncodeBase64(FilePath)
 
 'OneNoteページを更新
 app.UpdatePageContent pageDoc.XML
 
 'OneNote終了
 exec.Terminate
 Set app = Nothing
 Set exec = Nothing
End Sub


'画像データをBase-64形式に変換する
Private Function EncodeBase64(ByVal FilePath As String) As String
 Dim elm As Object
 Dim temp As String
 On Error Resume Next
 Set elm = CreateObject("MSXML2.DOMDocument").createElement("base64")
 With CreateObject("ADODB.Stream")
  .Type = 1 'adTypeBinary
  .Open
  .LoadFromFile FilePath
  elm.DataType = "bin.base64"
  elm.nodeTypedValue = .Read(-1) '.Read(adReadAll)
  temp = elm.text
  .Close
 End With
 On Error GoTo 0
 EncodeBase64 = temp
End Function

'画像ファイルの縦横サイズを取得し、縦横比を返す
Private Function getAspect(ByVal FilePath As String) As Long
 Dim uGdiStartupInput As GdiplusStartupInput
 Dim nGdiToken As LongPtr
 Dim hImage As LongPtr
 Dim width As Long
 Dim height As Long
 uGdiStartupInput.GdiplusVersion = 1 'GDI+のバージョン指定
 If GdiplusStartup(nGdiToken, uGdiStartupInput, 0&) = 0 Then 'GDI+を初期化
  If GdipLoadImageFromFile(ByVal StrPtr(FilePath), hImage) = 0 Then '画像読み込み
   GdipGetImageWidth hImage, width '幅取得
   GdipGetImageHeight hImage, height '高さ取得
  End If
 End If
 GdipDisposeImage hImage 'メモリ開放
 GdiplusShutdown nGdiToken 'リソースをクリーンアップ
 getAspect = height / width
End Function

OneNoteのページを更新するには、UpdatePageContentメソッドを使います。引数として、ページに対して行う変更を含むXML文字列を渡します。
つまり画像をアップロードしたいときは、
 ①現在のOneNoteページのXML記述を取得
 ②画像データを文字列(Base64形式)にエンコード
 ③上記①と②をドッキング
 (①のXML記述に対し人為的にタグとデータ本体(②)を加筆)
 ④ドッキングしたXML記述を、UpdataPageContentメソッドで上書き
すれば良いということ。

XML記述で考えるなら、以下の選択部を意図的に追記してやれば良い。
画像データを扱うために必要なタグや属性を用意し、画像データをBase-64形式に変換して記述します。レファレンスによると、バイナリデータ(画像など)はBase-64形式で扱われているとあったので、エンコードしてから記述します。

今回追加したいXML記述部(選択部)

これで画像がアップロードできます。

なおサンプルコード内に出てくるWin API関数は、画像データの縦横サイズ比を取得したいがために使用したものです。OneNote操作においてはWin APIを使用しません。
※サブルーチンgetAspect()ファンクション部分でWin API使用。

備忘

OneNoteアプリケーションは事前バインディングしてください。
 参照設定
・Microsoft OneNote 15.0 Object Library
・Microsoft OneNote 15.0 Extended Type Library

なおXML記述の編集時は「Microsoft XML v6.0」を使っていますが、開発時はこれも参照設定しておくと入力補完が使えるので捗ります。

OneNoteのOCR機能がどういう内部処理をしているのか詳しく知りませんが、画像が小さい場合は誤認識がチラホラ発生します。
OCR機能に特化したアプリ等と比べると、やっぱり読み取り精度は見劣りするかなーという印象。
ただしOffice365のライセンスさえあれば誰でも無料で手軽に使えるのが魅力です。誤認識が気になる場合は、抽出後のテキストを何らかのスキーム(AIとかパターンとか)で補正してやる必要があるかもしれません。

最後に、話が変わりますがOCR関連でいうと、Excelのパワークエリを使って画像からテキスト抽出する機能も中々使い勝手が良かったので、書き留めておきます。あとはPowerAutomateのAI Builderを使ってOCR+AI補正したテキスト抽出も中々高精度でした。
どちらもライセンスに依って機能が制限されている点が残念ですが、使える環境にある場合は選択肢の一つに挙がってくるでしょう。

OCRのような便利機能がどんどん普及して、OneNote以外の色々なアプリにも標準装備される時代が早く来てほしいです…。

この記事が気に入ったらサポートをしてみませんか?