Windows - F#と標準機能で選択領域のOCR

commits7 min read読了の目安(約6600字

概要

スクリーンショット撮影アプリ「切り取り&スケッチ[1]」と「Windows.Media.Ocr[2]」を組み合わせて、選択した領域のテキストを読み取るプログラムを F# で作る。

形式としては、領域の選択をグローバルホットキーで開始し、読み取ったテキストをクリップボードに保存する、タスクトレイ常駐型のアプリとしました。

作成環境

対象 バージョン
Windows 20H2 (19042.928)
.NET SDK 5.0.202

実装例

※「切り取り&スケッチ」の設定「クリップボードに自動コピー」をオンにする。

.fsproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
    <RootNamespace>プロジェクト名</RootNamespace>
    <UseWindowsForms>true</UseWindowsForms>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

</Project>

Program.fs
open System

let await (i: Windows.Foundation.IAsyncOperation<'TResult>) =
    i.AsTask() |> Async.AwaitTask


let startScreenclip () =
    let cmd = new System.Diagnostics.ProcessStartInfo "cmd"
    cmd.Arguments <- @"/c start ms-screenclip:"
    cmd.UseShellExecute <- true
    cmd.WindowStyle <- System.Diagnostics.ProcessWindowStyle.Hidden

    let cmdProcess = System.Diagnostics.Process.Start cmd
    cmdProcess.WaitForExit()

    let processArray = System.Diagnostics.Process.GetProcessesByName "ScreenClippingHost"
    processArray.[0].WaitForExit()


let getClipboardBitmap () =
    let bitmapSource = System.Windows.Clipboard.GetImage()

    if bitmapSource <> null then
        let bitmapFrame = System.Windows.Media.Imaging.BitmapFrame.Create bitmapSource

        let encoder = new System.Windows.Media.Imaging.BmpBitmapEncoder()
        encoder.Frames.Add bitmapFrame

        use memoryStream = new System.IO.MemoryStream()
        encoder.Save memoryStream

        let byteArray = memoryStream.ToArray()

        use randomAccessStream = new Windows.Storage.Streams.InMemoryRandomAccessStream()
        use outputStream = randomAccessStream.GetOutputStreamAt(uint64 0)
        use dataWriter = new Windows.Storage.Streams.DataWriter(outputStream)
        dataWriter.WriteBytes byteArray

        async {
            let! _ = await <| dataWriter.StoreAsync()
            let! _ = await <| outputStream.FlushAsync()

            let! decoder = await <| Windows.Graphics.Imaging.BitmapDecoder.CreateAsync randomAccessStream
            let! softwareBitmap = await <| decoder.GetSoftwareBitmapAsync(Windows.Graphics.Imaging.BitmapPixelFormat.Bgra8, Windows.Graphics.Imaging.BitmapAlphaMode.Premultiplied)

            return softwareBitmap
        }
        |> Async.RunSynchronously
    else
        null


let startOCR bitmap =
    let ocrEngine = Windows.Media.Ocr.OcrEngine.TryCreateFromUserProfileLanguages()

    if bitmap <> null then
        async {
            let! ocrResult = await <| ocrEngine.RecognizeAsync bitmap
            return ocrResult.Text
        }
        |> Async.RunSynchronously
    else
        ""


let createIcon () =
    let toolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(Text = "&終了")
    toolStripMenuItem.Click.Add(fun e -> System.Windows.Forms.Application.Exit())

    let contextMenuStrip = new System.Windows.Forms.ContextMenuStrip()
    contextMenuStrip.Items.Add toolStripMenuItem |> ignore

    new System.Windows.Forms.NotifyIcon(
        Icon = new System.Drawing.Icon @".\icon.ico",
        Visible = true,
        Text = "TestApp",
        ContextMenuStrip = contextMenuStrip
    )


[<System.Runtime.InteropServices.DllImport("user32.dll")>]
extern int RegisterHotKey(IntPtr HWnd, int ID, int MOD_KEY, int KEY)

[<System.Runtime.InteropServices.DllImport("user32.dll")>]
extern int UnregisterHotKey(IntPtr HWnd, int ID)


type HotkeyForm () as this =
    inherit System.Windows.Forms.Form()

    let icon = createIcon()

    do
        RegisterHotKey(this.Handle, 0x0000, 0x0001 ||| 0x0008, int System.Windows.Forms.Keys.C) |> ignore

        System.Windows.Forms.Application.ApplicationExit.Add(fun e ->
            UnregisterHotKey(this.Handle, 0x0000) |> ignore
        )

    override this.WndProc m =
        base.WndProc(&m)

        if m.Msg = 0x0312 && int m.WParam = 0x0000 then
            startScreenclip()
            System.Windows.Clipboard.SetText(startOCR <| getClipboardBitmap())

    interface IDisposable with
        member this.Dispose() =
            icon.Dispose()


[<STAThread>]
do
    use form = new HotkeyForm()
    System.Windows.Forms.Application.Run()

解説

切り取り&スケッチ

「切り取り&スケッチ」のクリップボードに選択領域の画像を保存する機能を利用する。

let startScreenclip () =

    // コマンドプロンプト
    let cmd = new System.Diagnostics.ProcessStartInfo "cmd"

    // 「ms-screenclip:」新規切り取りを開始する URI スキーム
    cmd.Arguments <- @"/c start ms-screenclip:"

    cmd.UseShellExecute <- true
    cmd.WindowStyle <- System.Diagnostics.ProcessWindowStyle.Hidden

    // 「切り取り&スケッチ」の起動
    let cmdProcess = System.Diagnostics.Process.Start cmd
    cmdProcess.WaitForExit()

    // 「切り取り&スケッチ」の終了待機
    let processArray = System.Diagnostics.Process.GetProcessesByName "ScreenClippingHost"
    processArray.[0].WaitForExit()

クリップボードから画像を取得する

let getClipboardBitmap () =

    // WPF の機能を利用

    let bitmapSource = System.Windows.Clipboard.GetImage()

    if bitmapSource <> null then

        // 以下「Windows.Media.Ocr」が認識できる SoftwareBitmap に変換する処理

        let bitmapFrame = System.Windows.Media.Imaging.BitmapFrame.Create bitmapSource

        let encoder = new System.Windows.Media.Imaging.BmpBitmapEncoder()
        encoder.Frames.Add bitmapFrame

        use memoryStream = new System.IO.MemoryStream()
        encoder.Save memoryStream

        let byteArray = memoryStream.ToArray()

        use randomAccessStream = new  Windows.Storage.Streams.InMemoryRandomAccessStream()
        use outputStream = randomAccessStream.GetOutputStreamAt(uint64 0)
        use dataWriter = new Windows.Storage.Streams.DataWriter(outputStream)
        dataWriter.WriteBytes byteArray

        async {
            let! _ = await <| dataWriter.StoreAsync()
            let! _ = await <| outputStream.FlushAsync()

            let! decoder = await <| Windows.Graphics.Imaging.BitmapDecoder.CreateAsync randomAccessStream
            let! softwareBitmap = await <| decoder.GetSoftwareBitmapAsync(Windows.Graphics.Imaging.BitmapPixelFormat.Bgra8, Windows.Graphics.Imaging.BitmapAlphaMode.Premultiplied)

            return softwareBitmap
        }
        |> Async.RunSynchronously
    else
        null

Windows.Media.Ocr

https://zenn.dev/shikatan/articles/451245de0f5dd70d28be

グローバルホットキー

https://zenn.dev/shikatan/articles/f6c4c52c134b61

更新履歴

日付 内容
2021/04/26 「切り取り&スケッチ」に関する内容を追加。

end

脚注
  1. 「Windows 10 October 2018 Update」にて標準搭載。 ↩︎

  2. 「Windows 10」にて標準搭載。 ↩︎