Rust で Windows の OCR を使ってみる

# 動機

Windows 10 以降で OCR できるということを知ったので Rust で OcrEngine を使ってみました。

# 環境

Windows 10 21H1
rustc 1.64.0 (a55dd71d5 2022-09-19)
[windows](https://crates.io/crates/windows) crate 0.42.0

# 実装

### [OcrEngine](https://learn.microsoft.com/ja-jp/uwp/api/windows.media.ocr.ocrengine?view=winrt-22621)

ドキュメントに書いてある通り、OCR するには`OcrEngine`クラスの`RecognizeAsync`関数を呼び出すだけです。簡単！

```rust
let engine = OcrEngine::TryCreateFromUserProfileLanguages()?;
engine.RecognizeAsync(&bmp)?;
```

### [SoftwareBitmap](https://learn.microsoft.com/ja-jp/uwp/api/windows.graphics.imaging.softwarebitmap?view=winrt-22621)

`RecognizeAsync`関数には`SoftwareBitmap`を渡します。`SoftwareBitmap`は圧縮されていないビットマップを表すということなので、どうにかしてビットマップデータを書き込んでやればよいようです。

```rust
let bmp = SoftwareBitmap::Create(BitmapPixelFormat::Rgba8, width, height)?;
// この後、bmp にデータを書き込む処理
```

### [image](https://crates.io/crates/image) crate

いろいろな画像形式に対応したいので image crate を使います。

```toml
[dependencies.image]
version = "0.24"
default-features = false
features = [
    "jpeg",
    "png",
    "bmp",
    "tiff"
]
```
```rust
let img = image::open("path to image file")?;
let width = img.width() as i32;
let height = img.height() as i32;
let rgb = img.to_rgb8().to_vec(); // RGB 形式のビットマップ
```

### [BitmapBuffer](https://learn.microsoft.com/ja-jp/uwp/api/windows.graphics.imaging.bitmapbuffer?view=winrt-22621)

`SoftwareBitmap`の`LockBuffer`関数を呼んで、`BitmapBuffer`を取得します。`BitmapBuffer`で直接ビットマップデータにアクセスできます。`BitmapBuffer`へのデータの書き込みが終わったらスコープを抜けるか、明示的に`drop`を呼ぶなりしてロックを解除する必要があります。

```rust
{
    let bmp_buf = bmp.LockBuffer(BitmapBufferAccessMode::Write)?;
    // ここでビットマップデータの書き込み処理
}
// スコープを抜けてロック解除
```

### [IMemoryBufferByteAccess](https://learn.microsoft.com/en-us/previous-versions//mt297505(v=vs.85))

`BitmapBuffer`への参照を作って、その参照を`IMemoryBufferByteAccess`にキャストします。`IMemoryBufferByteAccess`の`GetBuffer`関数で`BitmapBuffer`への生ポインタを取得できます。

```rust
let array: IMemoryBufferByteAccess = bmp_buf.CreateReference()?.cast()?;

let mut data = ptr::null_mut();
let mut capacity = 0;
unsafe {
    array.GetBuffer(&mut data, &mut capacity)?;
}
// SoftwareBitmap は RGBA 型式なので 幅 × 高さ × 4bytes。
assert_eq!((width * height * 4).abs(), capacity as i32);
```

### 書き込み

バッファへの生ポインタが取得できたので、あとはそこにデータを書き込むだけです。生ポインタをそのまま使うのは厄介なので Rust の世界のスライスに変換して書き込みます。

```rust
let slice = unsafe { slice::from_raw_parts_mut(data, capacity as usize) };
slice.chunks_mut(4).enumerate().for_each(|(i, c)| {
    c[0] = rgb[3 * i];
    c[1] = rgb[3 * i + 1];
    c[2] = rgb[3 * i + 2];
    c[3] = 255;
});
```

### 出力

`SoftwareBitmap`が準備できたので`RecognizeAsync`関数に渡します。`RecognizeAsync`関数は`IAsyncOperation`を返します。`IAsyncOperation`は`Future`を実装しているので、`await`で待ちましょう。そして結果を一行ずつ表示します。

```rust
#[tokio::main]
async fn main() -> Result<()> {
    // omitted
    engine
        .RecognizeAsync(&bmp)?
        .await?
        .Lines()?
        .First()?
        .filter_map(|line| line.Text().ok())
        .for_each(|text| println!("{text}"));
    Ok(())
}
```

### コード

<details><summary>コピペ用サンプルコード</summary>

```toml:Cargo.toml
[package]
name = "ocr"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0"
tokio = { version = "1.21", features = ["rt-multi-thread", "macros"] }

[dependencies.image]
version = "0.24"
default-features = false
features = [
    "jpeg",
    "png",
    "bmp",
    "tiff"
]

[dependencies.windows]
version = "0.42"
features = [
    "Graphics_Imaging",
    "Foundation",
    "Foundation_Collections",
    "Media_Ocr",
    "Win32_System_WinRT"
]
```
```rust:main.rs
use anyhow::Result;
use std::{ptr, slice};
use windows::{
    core::*,
    Graphics::Imaging::{BitmapBufferAccessMode, BitmapPixelFormat, SoftwareBitmap},
    Media::Ocr::*,
    Win32::System::WinRT::*,
};

#[tokio::main]
async fn main() -> Result<()> {
    let img = image::open("path to image file")?;
    let width = img.width() as i32;
    let height = img.height() as i32;
    let rgb = img.to_rgb8().to_vec();

    let bmp = SoftwareBitmap::Create(BitmapPixelFormat::Rgba8, width, height)?;
    {
        let bmp_buf = bmp.LockBuffer(BitmapBufferAccessMode::Write)?;
        let array: IMemoryBufferByteAccess = bmp_buf.CreateReference()?.cast()?;

        let mut data = ptr::null_mut();
        let mut capacity = 0;
        unsafe {
            array.GetBuffer(&mut data, &mut capacity)?;
        }
        assert_eq!((width * height * 4).abs(), capacity as i32);

        let slice = unsafe { slice::from_raw_parts_mut(data, capacity as usize) };
        slice.chunks_mut(4).enumerate().for_each(|(i, c)| {
            c[0] = rgb[3 * i];
            c[1] = rgb[3 * i + 1];
            c[2] = rgb[3 * i + 2];
            c[3] = 255;
        });
    }

    let engine = OcrEngine::TryCreateFromUserProfileLanguages()?;
    engine
        .RecognizeAsync(&bmp)?
        .await?
        .Lines()?
        .First()?
        .filter_map(|line| line.Text().ok())
        .for_each(|text| println!("{text}"));
    Ok(())
}
```
</details>

# 結果

試しに Rust のトップページをキャプチャして OCR にかけてみました。

### [英文](https://www.rust-lang.org/)

```text
lnstall
第 st
Learn
Playground
Tools
Governance
Community
Blog
English (en-US)
A language empoweringeveryone
to build reliable and efficientsoftware.
Why Rust?
performance
Rustis blazinglyfastand memory-
efficient:with no runtime orgarbage
collector, itcan power performance-
criticalservices, run on embedded
devices, and easily i ntegrate with other
languages ・
Reliability
Rust's rich type system and ownership
modelguarantee memory-safetyand
thread-safety—enablingyou tO eliminate
manyclassesofbugsatcompile-time.
GET STARTED
Version 1.64.0
productivity
Rusthasgreatdocumentation,afriendly
com piler with useful erro r messages, a nd
top-notch tooling—an integrated
package managerand build t00 し smart
multi-editorsupportwith auto-
completion and type inspections, an
auto-formatter, and more.
```

### [和文](https://www.rust-lang.org/ja/)

```text
イ ン ス - ト = ル
第 st
学 ぶ
Playground
ッ = ル
カ ( 丿 こ ) ィ . ス
コ
ー ユ
一 丁 ィ
プ ロ グ
日 本 語 (ja)
効 率 的 で 信 頼 で き る ソ フ ト ウ ェ ア を
誰 も が つ く れ る 言 語
な ぜ Rust か ?
パ フ ォ - マ ン ス
Rust は 非 常 に 高 速 で メ モ リ 効 率 が 高 く ラ ン
タ イ ム や カ べ - ジ コ レ ク タ が な い た め 、 バ
フ ォ - マ ン ス 重 視 の サ - ビ ス を 実 装 で き ま
す し 、 組 込 み 機 器 上 で 実 行 し た り 他 の 言 語
と の 調 和 も 簡 単 に で き ま す 。
信 頼 性
バ イ ル 時 に 排 除 す る こ と が 可 能 で す 。
さ れ ま す 。 さ ら に 様 々 な 種 類 の バ グ を コ ン
よ り メ モ リ 安 全 羅 と ス レ ッ ド 安 全 羅 が 保 証
Rust の 豊 か な 型 ラ ス テ ム と 所 有 権 モ デ 丿 レ に
は じ め る
ノ ( - ジ ョ ン 1.64.0
が 数 多 く 揃 っ て い ま す 。
自 動 フ ォ - マ ッ タ と い っ た 一 流 の ツ - ル 群
応 す る ス マ - ト な 自 動 補 完 と 型 検 機 能 、
ジ ャ と ビ ル ド ッ - ル 、 多 数 の エ デ イ タ に 対
イ ラ 、 お よ び 統 合 さ れ た バ ッ ケ - ジ マ ネ -
ラ - メ ッ セ - ジ を 備 え た 使 い や す い コ ン バ
Rust に は 優 れ た ド キ ュ メ ン ト 、 有 用 な 工
生 産 性
```

# まとめ

Windows の OcrEngine を Rust から使ってみました。精度はあまりよくないように思います。

### おまけ

PDF を OCR したいのなら [Ghostscript](https://www.ghostscript.com/) を使って PDF をビットマップ画像に変換すればよいです。下記のコードで動くと思います。OCR したいページ番号は`-sPageList=`のオプションで指定してください。

```rust
let output = Command::new("./gswin64c.exe")
    .arg("-dSAFER")
    .arg("-dBATCH")
    .arg("-dNOPAUSE")
    .arg("-dNOPAGEPROMPT")
    .arg("-dNOPROMPT")
    .arg("-dQUIET")
    .arg("-r300")
    .arg("-sPageList=1")
    .arg("-sDEVICE=bmp16m")
    .arg("-dGraphicsAlphaBits=4")
    .arg("-dTextAlphaBits=4") 
    .arg("-sstdout=%stderr")
    .arg("-sOutputFile=-")
    .arg("path to pdf file")
    .output()?;
let img = image::load_from_memory_with_format(&output.stdout, ImageFormat::Bmp)?;
```

---

記事を書いてから気づきましたが、@osanshouo さんが同じような記事を2年前に投稿していました。自分も「いいね」しておったのに、まったく覚えてないとは…

https://qiita.com/osanshouo/items/bedf56143005a11f5e3a