Accessibility APIでブラウザから情報を取得してみる


目次
  1. C++で、IAccessible2のアクセシビリティ情報を取得してみる
    1. Visual Studio Build Tools for C++をインストール
    2. IAccessible2を手元に取ってくる
    3. x64 Native Tools Command Prompt
    4. コードを書く
    5. コンパイルして実行
  2. NVDAのアドオン経由で、アクセシビリティ情報を取得してみる
  3. まとめ

こんにちは、mehm8128です。 普段使っているスクリーンリーダーも、実際にどう動いているかはあんまり知らずに使っています。 ブラウザによって挙動が違うとかスクリーンリーダーによって挙動が違うとか言う前に、ある程度中身を知っておいた方がいいと思い、今回Accessibility APIでブラウザからアクセシビリティ情報を取得してみました。

C++もPythonも全然知らずに雰囲気理解してるので、間違いなどありましたら教えてください。

C++で、IAccessible2のアクセシビリティ情報を取得してみる #

環境構築はChatGPTに聞き、コードはGitHub Copilotくんに書いてもらいました。便利な時代ですね。 あんまり日本語で情報がないような気がするので、手順を追って割と丁寧に説明していきます。 ちなみに、Windows環境を想定しています。

Visual Studio Build Tools for C++をインストール #

C++を使えるようになるやつらしいです。インストールしてください。 インストールするコンポーネントを選択する画面になったら、「C++によるデスクトップ開発」にチェックを入れれば多分いけます。いけなかったらチャッピーに聞いてください。

IAccessible2を手元に取ってくる #

LinuxA11y/IAccessible2: Accessibility services framework for Microsoft Windows environmentsのリポジトリを手元に取ってきます。シェルスクリプトを叩くので、これはWSL側がおすすめです。 手元に取ってきたら、concatidl.shを叩きます。これで諸々のIDLファイルを1つのia2_api_all.idlに合体しています。 WSLではなくてWindows側に取ってきた人は、PowerShellでいい感じのコマンドを叩くか、concatidl.shの中身としてはちょっと置換処理をしてcatで繋げてるだけなので、頑張れば手動でもできます。

このリポジトリはIDLを提供しているので、今回のC++以外からでも使うことができます。Claudeに聞いたところNVDAはPythonでIA2にアクセスしていると言っているのですが、PythonからC++を呼び出す処理がNVDAのソースコードに含まれていたのを前に見たので、あれはなんだったのか気になります。

ちなみにIAccessible2のContributorsの一番最初にいるjcstehさんはW3CのARIA WGのメンバーで、Mozillaのエンジニアです。TPACのときに実物を見ました。前にNVDAのco-lead developerだったとGitHubのプロフィールに記載があります。

x64 Native Tools Command Prompt #

さっきツールをインストールしたので、x64 Native Tools Command Promptというツールが使えるようになっているはずです。これを起動すると、コマンドプロンプトが出てきます。 このコマンドプロンプトでia2_api_all.idlのあるディレクトリに移動し、以下のコマンドを実行します。

midl ia2_api_all.idl

ちなみにLinuxでいうlsdirコマンドで、cdは普通に使えます。

midlコマンドはMicrosoftのIDLを処理してC++のソースコードを生成するコマンドです。叩くと.cファイルとか.hファイルとかが生まれます。

コードを書く #

生成された.hファイルなどを使って、いい感じのコードを書きます。GitHub Copilotくんが一発で出してくれました。 それに対してコメントで説明やドキュメントへのリンクを貼ったものをmehm8128/ia2-get-accessibility-infoに公開しています(コメントの半分くらいはCopilotくんが書いたものです)。

コンパイルして実行 #

僕の場合は以下のコマンドでコンパイルできましたが、書いたコードや環境によって違ってくると思います。

cl /utf-8 main.cpp ia2_api_all_i.c oleacc.lib ole32.lib oleaut32.lib user32.lib

clコマンドは、多分Compile & Linkです。

コンパイルに成功するとmain.exeができるので、実行します。僕のリポジトリにあるコードでは実行してから3秒間猶予があるので、その間にアクセシビリティ情報を取得したいブラウザのページをフォアグラウンドにします。そうしないと、実行したコマンドプロンプトの情報が取得されます。

という一通りの流れをIA2でアクセシビリティ情報を取得するやつ - mehm8128にも書いています。ここに、僕のポートフォリオサイトをFirefoxで開いた際のmain.exeの実行結果を添付しています。

[ia2:30 msaa:リンク(30)] "Referencing HTML elements inside Shadow DOM"

のように、ロールを数値にしたものとロール名、accessible nameを取得できていることが分かります。 get_accNameでaccessible nameを取得していますが、それ以外にも色々なARIA attributes/statesを取得するメソッドが生えていたので、多分それで色々取得できます。

ちなみにWindowsではMSAAが最も古いAPIで、その次にIA2、一番新しいのがUIAらしいのですが、今回はIA2の情報取得を試み、失敗したらMSAAの情報取得を試みるような形になっています。 ここらへんのAccessibility APIの名前は、core-aamhtml-aamの表でも見ると思います。

また今回は詳しく調べていないのですが、COM (Component Object Model)という技術がアプリケーション間通信を可能にしているらしいです。

NVDAのアドオン経由で、アクセシビリティ情報を取得してみる #

次に、NVDAのアドオン経由でアクセシビリティ情報を取得してみます。 公式のガイドはNVDA Add on Development Guide · nvdaaddons/DevGuide Wikiにあります。

環境構築としてはPythonだけ入ってれば動くはずです。

実際にアドオン開発をするときはnvaccess/AddonTemplate: Template and metadata used by NVDA community add-onsというテンプレートがあるのでこれを利用できるのですが、動作確認だけであればもっと楽にする方法があったので、そっちにします。

Getting started: Hands-on examplesのセクションから読むといいです。

最初にNVDAの設定ダイアログで「高度な設定」タブに移動し、「開発者用スクラッチパッドのフォルダーからカスタムコードを読み込む」にチェックを入れます。その後「開発者用スクラッチパッドのフォルダーを開く」をクリックすると、フォルダーが開きます(チェックを入れた後、保存するのを忘れないようにしてください)。 そのフォルダーにglobalPluginsフォルダーやappModulesフォルダーがあると思います。そこにPythonファイルを作り、上記ガイドのExample 1や2で動作確認ができます。 ちなみに、「ツール」>「プラグインの再読み込み」ボタンで更新を反映できます。

ガイドの範囲だけだとビープ音を鳴らすだけなので、ここから具体的にIA2の情報を取得していきます。 簡単に、以下のようなコードをappModules/chrome.pyに書いてChromeでTabキーを押していくと、buttonロールの要素にフォーカスしたタイミングでui.messageに渡した引数が読み上げられます(上下キーで仮想フォーカスを移動したときに毎回実行するイベントは分かりませんでした)。

import appModuleHandler
import ui
import controlTypes

class AppModule(appModuleHandler.AppModule):
  def event_gainFocus(self, obj, nextHandler):
      if obj.role == controlTypes.Role.BUTTON:
          ui.message(obj.name or "名前なし")
          ui.message(str(obj.IA2Attributes))
      nextHandler()

importするモジュールは基本的にNVDAのsourceディレクトリ配下のディレクトリ名をそのまま指定すると、importできるようです。 例えばui.messageならここらへんだし、ビープ音を鳴らしていたtones.beepここらへんだし、今回出てきたcontrolTypes.Roleここらへんです。

イベントハンドラーの引数のobjNVDAObjectという、NVDAのソースコード内の色んなところで参照されている型のオブジェクトが渡ってきています。ここからaccessible nameやロール、IA2Attributesなど色んな情報を取得できます。 controlTypes.RoleはNVDA側で定義しているロールで、どこかのタイミングでAccessibility APIから取得したロールをNVDA側のロールに変換しているのだと思います。

この状態で、例えばGitHubのコードブロックのコピーボタンにフォーカスすると、2つ目のui.messageで以下のような出力になります。

{'display': 'inline-block', 'tag': 'clipboard-copy', 'xml-roles': 'button', 'name-from': 'attribute', 'explicit-name': 'true', 'class': 'ClipboardButton btn js-clipboard-copy m-2 p-0 focus-visible', 'text-align': 'left'}

classはそのままHTML要素のclassがついています。displaytext-alignはCSS、tagはHTML要素のタグ名です(今回はカスタム要素でした)。 xml-rolesはロール、name-fromはおそらくARIAの仕様書authorcontentsprohibitedの分類と似たような感じです。今回はaria-label属性でaccessible nameをつけていたのでattributeになっているのだと思います(つまり上記の分類だとauthor)。

NVDAのアドオンでは、このようにAccessibility APIから情報を取得できます。

これをまとめていたのがNVDA アドオン - mehm8128です。 また、過去にNVDAの内部実装を雑に読んでいた記録もあります(ZennのScrapから移植したものです)。 NVDAと格闘する - mehm8128

まとめ #

いかがでしたか? Webをやっていると普段なかなか触れない部分だと思いますが、AIの力を借りて理解を深められるようになったので、色々活用していきたいです。