PDFがCMYKで出力されているのを無課金で調べる

 私事で印刷業者にちょっとしたものを印刷してもらう用事ができたのですが、その入稿データとしてCMYKで出力されたPDFを要求されました。CMYKはシアン、マゼンタ、イエロー、黒の4色を混ぜ合わせてカラー原稿を表現する方式で、印刷業界で標準的に使われている方式です。いっぽう、PCでは一般的にRGB(レッド、グリーン、ブルー)の3色を混ぜ合わせて色を表現するのが一般的で、多くの画像編集ツールやデジタルカメラなどではこちらのRGBを使った形式でファイルを出力することが多いです。そのため、CMYKを取り扱うにはCMYK出力をサポートしたソフトウェアが必要になります。

 といっても、CMYKでPDFを出力すること自体はInkscape等のフリーソフトウェアでも可能です。とはいえ、特にCMYKでの出力は初めてということもあって、そこで出力したものが本当に正しくCMYKで出力されているのか、入稿前に確認しようと考えました。

 軽く調べたところ、Acrobatの有償版(Acrobat Pro)で利用できる「印刷プレビュー」機能を利用すれば、使用されている色やRGBデータの有無を確認できるようです。ただ、単にデータを確認するだけに安くない金額を払うのにはとても抵抗感があります。また、他にも印刷向けにPDFの情報をチェックできるソフトウェアが存在するようですが、見つけたものはいずれも有償のものでした。

 ということで、連休で時間があるというのもあってPDFフォーマットを分析して自力でPDFでCMYKが使用されているかを調べる方法を調べることにしました。

参考資料

 PDFのフォーマット仕様は標準規格として公開されており、その仕様書はAdobeのオープンソースサイト(https://opensource.adobe.com/dc-acrobat-sdk-docs/acrobatsdk/)で公開されています。ただし英語で分量も多いため、これをいきなり読むのはハードでしょう。

 日本語の情報としては、下記が有用でした。

PDFがCMYKで出力されているかを調べるために必要な情報の要約

 以上の資料を読んだうえで、後述するPDFを取り扱うライブラリを使っていくつかのPDFを確認したところ、以下のような理解になりました。

  • PDFはオブジェクトと呼ばれるデータ要素の組み合わせで構成されている
  • PDFには必ずルートオブジェクトがあり、ページ内のコンテンツはルートオブジェクトからたどれる/Pagesオブジェクトに入っている
    • ルートオブジェクトにはそのほか画面表示のためのプロパティやメタデータなどが含まれていることがあるが、これらはPDFが含む色には関係ない
      • PDF作成ツールによってはメタデータに使用している色空間の情報が入っていることもあるようだ
  • PDFには「Indirect Object」という別のオブジェクトを参照する仕組みがあり、各オブジェクトの子オブジェクトとして実際のオブジェクトを指定する代わりにIndirect Objectを使って別のオブジェクトを参照させることもできる
  • /Pagesオブジェクトはページを表現する/Pageオブジェクトを子オブジェクトとして保持する/Kidsオブジェクトを含んでいる
  • /Pagesオブジェクトや/Pageオブジェクトには各ページで使われているコンテンツや各種設定を含むリソースやページサイズ、印刷サイズといった情報が含まれている
  • PDFは1ファイル中にさまざまな色空間を混在させることができる
    • 色空間としてはグレイスケール、RGB、CMYKを取り扱える。さらにそれぞれカラーマネージメント(デバイスや出力装置に応じて適切に補正を行うことで色の再現性を高める仕組み)あり/なしの両方をサポートする
  • ページ内に描画するコンテンツは各/Pageオブジェクト内の/Contentsオブジェクトに格納されている
  • PDFはテキストベースで記述されているが、/Contentsオブジェクトにはテキストを圧縮したバイナリデータが含まれていることがある。その場合、/Contentsオブジェクト内のデータにアクセスするには指定された方式でバイナリを展開する必要がある
  • /Contentsオブジェクトには「<オペランド(パラメータ)> <オペレータ(命令)>」という形式で描画命令が格納されている。用意されている命令には直線や図形、文字などを描画するものや領域を塗るといったものに加えて、色や描画パラメータを変更するものや、描画するオブジェクトをグルーピング(レイヤー分け)するものなどが含まれている
    • 色や塗り潰しパラメータはグレイスケール/RGB/CMYKで指定できるほか、あらかじめ定義しておいた色(インデックスカラーもしくは特色)やパターンを選択することもできる
    • 色や塗り潰し、描画のパラメータは直接記述することもできるし、/Pagesや/Pageオブジェクト内の/Resourcesオブジェクトに格納しておいてそれを参照することもできる
      • あらかじめ定義しておいた色やパターンは/ColorSpaceというキー、また描画パラメータについては/ExtGStateというキーにひも付けられて格納されている

 まとめと言いながら長々と書いてしまいましたが、まず重要なポイントとしてはPDF内では複数の色空間を同時に扱えるという点です。そのため、「CMYKで出力されている」かどうかは、「グレイスケールもしくはRGBが使われていない」で判断することになりそうです。

 また、インデックスカラーや特色、塗りつぶしパターンは/Resourceオブジェクトに格納されていることもありますが、グレイスケール/RGB/CMYKの色指定は/Contentsオブジェクト内に記述された命令で行われています。そのため、グレイスケール/RGB/CMYKのどれが使われているかは/Contentsオブジェクト内で使われている色指定命令をすべて調べる必要があるようです。

pypdfでPDFの内部データにアクセスする

 PDFではIndirect Objectを使った参照が頻繁に使われるほか、バイナリデータを保持する/Contentsオブジェクトなども存在するため、内部構造を解析するだけでも大変です。そのため、今回はPython向けのPDFライブラリであるpypdfを利用して内部データにアクセスすることにしました。

 pypdfでは、次のようにpypdf.PdfReaderクラスを使用することでファイルオブジェクトなどからPDFデータを読みだすことができます。

with open(fn, "rb") as fp:
    r = pypdf.PdfReader(fp)

 ルートオブジェクトは、pypdf.PdfReaderオブジェクトのroot_objectプロパティに格納されています。たとえば次のようなコードでルートオブジェクトに格納されているオブジェクトとそのクラスを確認できます。

print("ROOT OBJECTS:")
for k in r.root_object:
    print(k, r.root_object[k].__class__)

 たとえば、とあるPDFのルートオブジェクトを確認した結果、次のような出力になりました。

ROOT OBJECTS:
/Type <class 'pypdf.generic._base.NameObject'>
/Pages <class 'pypdf.generic._data_structures.DictionaryObject'>
/ViewerPreferences <class 'pypdf.generic._data_structures.DictionaryObject'>
/Lang <class 'pypdf.generic._base.TextStringObject'>
/OCProperties <class 'pypdf.generic._data_structures.DictionaryObject'>
/Metadata <class 'pypdf.generic._data_structures.DecodedStreamObject'>

 pypdfではPDFで使われる各オブジェクトの形式に応じたクラスが定義されており、PDF内のオブジェクトは自動的に対応するpypdfのオブジェクトに変換されて木構造が構築されます。

ページ内のコンテンツを確認する

 前述のように、PDFの各ページはルートオブジェクトから参照される/Pagesオブジェクトに格納されています。このオブジェクトの情報は、次のようなコードで確認できます。

print("PAGES:")
print(p.root_object["/Pages"])

 実行結果は下記のようになりました。これは、PDFのページ数が1で、また/Pagesオブジェクトにはその実態となるオブジェクトの代わりにそのオブジェクトへの参照(IndirectObject)が含まれていることを意味します。

{'/Type': '/Pages', '/Count': 1, '/Kids': [IndirectObject(7, 0, 1219641424528)]}

 pypdfでは、get_object()メソッドでそのIndirect Objectが参照するオブジェクトの実態を取得できます。つまり、この例の場合、PDF内に含まれる唯一のページに対応するオブジェクトは次のようにして取得できます。

page = pages["/Kids"][0].get_object()

 /Pageオブジェクトには、/Contentsオブジェクトとしてそのページ内に描画されるデータが含まれています。また、get_data()メソッドでオブジェクトからその生バイナリデータを取得できます。

raw_content = page["/Contents"].get_data()

 PDFの描画データ(描画命令)はテキスト形式で表現できますが、/Contentsオブジェクトに含まれているデータはその生データ(バイナリデータ)なので、テキストとして扱いたい場合はデコードする必要があります。たとえばデコードしたデータを出力するには次のようにします。

print(raw_content.decode("ascii"))

 次の例は、このコードで実際に抜き出した描画データの一部です。

/OC/oc2 BDC
0.721569 0.678431 0.670588 0.882353 k
5.58236 48.749 m
8.19161 48.749 l
8.57899 48.749 8.86249 48.6587 9.04211 48.4782 c
9.22174 48.2979 9.31155 48.0342 9.31155 47.6872 c
9.31155 45.7844 l
9.31155 45.5798 9.27192 45.4135 9.19267 45.2857 c
9.11355 45.1579 8.99586 45.0742 8.83961 45.0346 c
8.99586 44.9937 9.11355 44.9182 9.19267 44.8081 c
9.27192 44.698 9.31155 44.5414 9.31155 44.3384 c
9.31155 42.2962 l
9.31155 41.9492 9.22174 41.6854 9.04211 41.5049 c
8.86249 41.3245 8.57899 41.2344 8.19161 41.2344 c
5.58236 41.2344 l
h

 最初の「/OC/oc2 BDC」はこれから描画するコンテンツがどのレイヤーに所属しているかを示すものです。「/oc2」は/Pageオブジェクトの/Resources内で定義されているプロパティで、このプロパティにはレイヤー名やそのレイヤーに関する情報が含まれています。

 次に「0.721569 0.678431 0.670588 0.882353 k」という命令が続きます。この「k」は次のような形でCMYK形式で色を指定する命令です。

<c> <m> <y> <k> k

つまり、これに続く命令はCMYKで指定された色で描画を行うことになります。

なお、CMYK形式の場合は「k」もしくは「K」ですが、グレイスケールの場合は「g」もしくは「G」、RGBの場合は「rg」もしくは「RG」で色を指定します。大文字の場合ストロークの色を、小文字の場合はストローク以外の塗りつぶし等の色を指定することになります。

 色指定にはそのほか「sc」「SC」や「scn」「SCN」という命令もありますが、これらは色空間を指定する「cs」「CS」命令と組み合わせて使うもので、csもしくはCS命令で指定した色空間に応じてパラメータを指定することになります。

描画に使用されている色空間を判断する

 さて、本記事のテーマである色空間の判別ですが、まとめると以下のようになります。

  • 描画データ中に「g」や「G」命令がある場合:グレイスケールが使われている
  • 描画データ中に「rg」や「RG」命令がある場合:RGBが使われている
  • 描画データ中に「k」や「K」命令がある場合:CMYKが使われている
  • 描画データ中に「sc」や「SC」、「scn」、「SCN」命令がある場合:その時点で最後に実行された「cs」「CS」で指定されている色空間が使われている

 この中で厄介なのが「sc」や「SC」、「scn」、「SCN」命令のパターンで、この場合まずその前に実行されている「cs」や「CS」命令を探し、そこで指定されている色空間がグレイスケール/RGB/CMYKのどれなのかを調べる必要があります。「cs」や「CS」命令ではパターンや特色を指定することもでき、その場合そのパターンや特色がどの色空間で指定されているかも調べる必要があります。

 これを踏まえて問題のPDFのデータを確認したところ、描画中には「k」と「K」命令しか含まれていませんでした。つまり、このPDFは無事CMYKで出力されていた、と言うことになります。

 ちなみに確認は上記で挙げたコード例のようなコードでページのコンテンツをテキストファイルに出力し、それをテキストエディタで開いて「g」や「rg」、「k」、「sc」という文字列を検索して目視で確認するという原始的な方法で行いました。今回は1ページだけのPDFファイルで、かつラスター画像を含まないものだったのでこれで十分でしたが、ページ数が増えた場合はプログラムを書いてちゃんと分析しないと大変そうです。

Windows環境でSANE経由でScanSnapを使う

 書籍や漫画単行本をスキャンして電子化する作業(いわゆる自炊)のためにScanSnap iX1300というドキュメントスキャナを購入したのですが、画質面ではイマイチのようです。そこで、SANEというオープンソースのスキャナライブラリを使ってこの問題への対処を図ってみました。

ScanSnapの画質問題とSANE

 書籍・単行本の電子化自体は以前から少しずつ進めており、これまではEPSONのDS-310というドキュメントスキャナを使用していました。DS-310は比較的安価でコンパクトなのにスキャンがそれなりに速く、画質も問題ないレベルだったために満足していたのですが、久しぶりに使ったところスキャンした画像の一部がグレー一色になってしまうという問題が発生するようになりました。状況から察するにセンサーの故障のようで、この機種ではよくある問題のようです。ただ、すでにこの製品は終売しており、保証期間も過ぎています。そのためScanSnap iX1300を購入したのですが、使ってみたところすぐに以下のような不満点が浮かんできました。

  • 画像や写真をスキャンすると目立つレベルでノイズが出る
  • 生成された画像のファイルサイズが妙に大きい
  • 専用のスキャンソフトウェアは常時バックグラウンドで起動しっぱなしの状態になり、終了もできない

 いくつか試してみたところ、どうもScanSnapは全体的にシャープネスとコントラストが強い画質にチューニングされているようです。これは書類などの文字がメインの印刷物をスキャンするのには適していますが、写真や画像、イラストなどのスキャンにはあまり向いていません。この問題はScanSnap iX1300だけでなく他の機種でも発生するようで、ネットで検索するといくつかScanSnapの画質について言及するページが見つかりました。

 ただ、専用スキャンソフトウェアの構成ファイルを観察してみると、どうも画像の圧縮は本体側ではなくこの専用ソフトウェア側で実行されている雰囲気があります。しかし、TWAINやWIAなどの汎用インターフェイスに対応していればWindowsの標準スキャン機能やサードパーティの画像処理ソフトウェアでスキャンできるのですが、残念ながらScanSnapはこれらのインターフェイスを提供していません。

 ところが、なんとオープンソースのスキャナライブラリであるSANEはUSB接続に限定されるもののScanSnapシリーズにも対応しており、動作検証もされているようです。ということで、このSANEを使ったスキャンを試してみることにしました。

MacでSANEを使う

 SANEは主としてLinuxなどのUNIX系OS向けに開発されています。そのため、LinuxやmacOSでは容易に導入が可能です。DebianなどのLinuxディストリビューションでは標準でパッケージが提供されていますし、macOSでもHomebrewというパッケージマネージャ経由でインストールが可能です。

$ brew install sane-backends

 もちろん、自前でソースコードからビルドすることもできます(自分はソースコードからビルドを行いましたが、特に詰まることもなくビルドができました)。

 SANEはスキャナにアクセスする汎用APIを提供するという位置付けのソフトウェアで、一応「scanimage」というコマンドラインから操作するスキャンソフトウェアも付属しています。SANEのAPIに対応したGUIのスキャンソフトウェアもありますが、今回はひとまずこのscanimageでスキャンを試してみました。

 次の例は、グレースケール、300dpi、読み込みサイズ115×175mm、JPEG形式でスキャンを行い、「test.jpeg」というファイル名でスキャン結果を保存するものです。

$ scanimage -d fujitsu --source="ADF Duplex" --mode=Gray --resolution=300 --page-width=115 --page-height=175 --format=jpeg -o test.jpeg

 この場合は片面のみのスキャンになりますが、「-b」オプションを使えば両面で連続スキャンを行うことができます。

$ scanimage -d fujitsu --source="ADF Duplex" --mode=Gray --resolution=300 --page-width=115 --page-height=175 --format=jpeg -b

 なお、macOSの場合ではSANEを使用する場合でも純正ドライバをインストールしておく必要があるようです。

 下記の画像はScanSnap公式ソフトウェアでスキャンしたものとSANE経由でスキャンしたものを比較したものですが、このようにScanSnap公式ソフトウェアの場合は文字の周りに歪んだような汚れが目立つのに対し、SANEでスキャンしたものは若干ぼやけた感じにはなっているものの、目立つ汚れは少ない傾向になり、またファイルサイズもJPEG形式の場合でおおむね3分の2~半分程度になりました。

画質比較:公式は文字の周りのノイズが目立つ

WindowsでSANEを使う

 SANEは現在公式にはWindowsをサポートしていないようですが、試したところMSYS2というUNIX互換の開発環境を使えばビルドが可能でした。MSYS2経由でバイナリパッケージをインストールすることもできるのですが、試したところ動作に怪しいところがあったため、以下では自前でビルドしてインストールしています。一部のスキャナ向けコードはそのままではビルドできないようですが、ScanSnap用のドライバに関しては問題なく利用できました。

MSYS2環境の構築

 MSYS2は、Windows環境でいわゆるUNIX系OS向けのソフトウェアを利用・ビルドするためのプラットフォームです。動作環境はWindows 10以降(64ビット専用)です。MSYS2のWebサイトでインストーラが公開されているので、こちらをダウンロードして指示に従ってインストールします。

MSYS2の環境について

 MSYS2では、使用するコンパイラやCランタイムが異なる複数の環境が用意されており、それぞれの環境のシェルを起動するショートカットが用意されています。

 環境について詳しくはEnvironmentsドキュメントで解説されていますが、今回はコンパイラとしてGCC、CランタイムとしてUCRTを使用する「ucrt64」環境を使用しています。

 ちなみにUCRTはWindows 10以降で利用できる互換性の高いランタイムで、詳しくはMicrosoftの公式ドキュメントで説明されています。

 MSYS2で提供されているソフトウェアやライブラリの一部は環境ごとに異なるパッケージで提供されているため、必要に応じて適切なパッケージをインストールする必要があります。たとえばucrt64環境向けのパッケージは「mingw-w64-ucrt-x86_64-」というプレフィックスが付いています。

必要なパッケージのインストール

 MSYS2環境では多数のコンパイル済みパッケージが提供されており、「pacman」というパッケージマネージャを使ってそれらをダウンロード/インストールできます。提供されているパッケージを探すには、MSYS2 Packagesが便利です。

 パッケージをインストールするには、「pacman -S <パッケージ名>」コマンドを使用します。下記のパッケージをインストールすることで、一般的なソフトウェアのビルド環境を構築できます。

make
mingw-w64-ucrt-x86_64-autotools
mingw-w64-ucrt-x86_64-gcc
autoconf-archive
patch

 SANEのビルドにはこれに加えて、下記のパッケージが必要です。

mingw-w64-ucrt-x86_64-python
git
mingw-w64-ucrt-x86_64-libusb
mingw-w64-ucrt-x86_64-libjpeg-turbo
mingw-w64-ucrt-x86_64-libpng
mingw-w64-ucrt-x86_64-libtiff

 Python以外はオプショナルですが、libusbがないとUSB接続のスキャナが使えず、またlibjpeg-turboやlibpng、libftiffはそれぞれJPEG(とPDF)、PNG、TIFF形式での出力に必要なのであったほうが良いでしょう。

ソースコードの入手

 SANEのソースコードはGitLabのsane-projectリポジトリから入手できます。ここで「コード」ボタンをクリックするとclone用のURLやダウンロードリンクが表示されます。今回はHTTPS用のURLを使ってクローンし、記事執筆時点での最新バージョンに対応するタグである「1.3.1」を「msys2」というブランチ名でチェックアウトしています。

$ cd <適当なディレクトリ>
$ git clone https://gitlab.com/sane-project/backends.git
$ cd backends
$ git checkout -b 1.3.1 msys2

パッチの適用

 GitHub状にあるMSYS2のリポジトリでは、MSYS2で提供されているパッケージをMINGWでビルドするためのファイルが公開されており、ここでSANE用のパッチも公開されています。前述のようにこれだけでは正常には動作しないのですが、ビルドの際に問題となる多くの部分がこのパッチで修正されているため、まずはこちらを適用します。このパッチをダウンロードし、ソースコードが含まれるトップディレクトリで次のように実行します。

$ patch -p1 < ../001-fix-build-on-mingw.patch

 また、これに加えていくつか修正が必要と思われる部分を見つけたので、そちらも修正しました。sane-1.3.1-msys2.patchというパッチにまとめたので、こちらも同様に適用できます。

$ patch -p1 < ../sane-1.3.1-msys2.patch

ビルド

 ビルドは一般的なUNIX系OS向けソフトウェアと同じような手順でビルド可能です。まず、配布ソースコードに含まれている「autogen.sh」を実行してconfigureファイルを生成します。

$ ./autogen.sh

 続いてconfigureを実行するのですが、ここで使用するバックエンド(各スキャナハードウェア固有の処理が実装されたもの。「device」や「driver」とも呼ばれる)を選択します。そのままではWindows環境でビルドできないものも含まれているので、実際に使いたいものだけを選択しておくことをおすすめします。また、テスト・デバッグ用の「test」というバックエンドも用意されているので、こちらもビルドしておくと検証に役立ちます。

 さらに、指定するオプションによっても正常にビルドできない可能性があります。今回はMSYS2のパッケージビルドスクリプトを参考に下記の設定で実行しました。

./configure BACKENDS="fujitsu test" PRELOADABLE_BACKENDS="fujitsu test" CFLAGS="-Wno-implicit-function-declaration -Wno-int-conversion -Wno-incompatible-pointer-types" --disable-locking --disable-rpath --disable-shared

 正常に処理が完了したら、makeコマンドでビルドを行います。

$ make

 ビルドが完了したら、「make install」コマンドでインストールが行えます。また、ソースコードに手を入れながらビルドして試行錯誤する場合などは、インストールを行わずに「./frontend/scanimage.exe」を直接実行することも可能です。ただし、その場合事前に利用するバックエンドに対応する設定ファイルを設定ディレクトリ(ucrt環境の場合は/ucrt64/etc/sane.d)に配置しておく必要があります。設定ファイルのひな形は「backend」ディレクトリ内に、「<バックエンド名>.conf」というファイルで用意されているので、これを適宜コピーしておきます。

$ mkdir /ucrt64/etc/sane.d
$ cp backend/test.conf /ucrt64/etc/sane.d/
$ cp backend/fujitsu.conf /ucrt64/etc/sane.d/

テスト

 スキャナが接続されていない状態でも、「test」バックエンドを使ってSANEやscanimageが正常に動作するかを検証できます。たとえば次のように実行すると、「test.tiff」というファイル名で、マス目上のテスト画像が生成されるはずです。

$ ./frontend/scanimage.exe -d test --test-picture=Grid -o test.tiff

デバッグ

scanimageの実行時に「SANE_DEBUG_<バックエンド名>」環境変数を設定することでテスト用メッセージを表示できます。値にはデバッグレベル(数値)を指定します。

$ SANE_DEBUG_TEST=1 ./frontend/scanimage.exe -d test --test-picture=Grid -o test.tiff
[21:48:21.196960] [sanei_debug] Setting debug level of test to 1.
[21:48:21.198227] [test] sane_init: SANE test backend version 1.0.28 from sane-backends 1.3.1.1-685c1-dirty

デバッグレベルとして指定する数値を大きくすると、より多くのログが表示されます。

$ SANE_DEBUG_TEST=2 ./frontend/scanimage.exe -d test --test-picture=Grid -o test.tiff
[21:49:42.818044] [sanei_debug] Setting debug level of test to 2.
...

汎用USBドライバの設定

 Windows環境におけるSANEの制約として、対象のスキャナに汎用ドライバ(libusb)がひも付けられている必要があります。これは、Zadigというツールを使って実行できます。ただし、一度対象のScanSnapデバイスにlibusbドライバをひも付けると、そのデバイスは(libusbドライバを削除するまでは)ScanSnap公式ソフトウェアからは使用できなくなります点に注意してください(この場合でも無線接続なら公式ソフトウェア経由でもスキャンできそうな気もするのですが、自分は前述のソフトウェア常駐問題があったためすっぱりとアンインストールしてしまったため試せていません)。

 Zadigをダウンロードして起動したら、「Options」メニューの「List All Devices」を選択します。

Zadigの「Options」メニューから「List All Devices」を選択する ]

 するとドロップダウンリストで接続されているUSBデバイスを選択できるようになるので、ここで対象のScanSnapを選択し、続いて使用するドライバとして「libusb-win32」もしくは「libusbK」を選択します。一応微妙に機能に違いがあるようなのですが、SANEから使う限りは特に違いは感じられませんでした。最後に「Replace Driver」をクリックするとドライバがインストールされ、指定したデバイスへのひも付けが行われます。

対象のScanSnapを選択し、続いて使用するドライバを選択する ]

scanimage.exeでスキャンを実行する

 SANEとドライバの両方の設定が完了すると、macOSの場合と同様にscanimage.exeでスキャンが実行できます。

 次の例は、グレースケール、300dpi、読み込みサイズ115×175mm、JPEG形式でスキャンを行い、「test.jpeg」というファイル名でスキャン結果を保存するものです。

$ scanimage.exe -d fujitsu --source="ADF Duplex" --mode=Gray --resolution=300 --page-width=115 --page-height=175 --format=jpeg -o test.jpeg

 この場合は片面のみのスキャンになりますが、「-b」オプションを使えば両面で連続スキャンを行うことができます。

$ scanimage.exe -d fujitsu --source="ADF Duplex" --mode=Gray --resolution=300 --page-width=115 --page-height=175 --format=jpeg -b

 Windows環境ではコードの修正(実質的にデバッグ)が必要だったため若干手間がかかりましたが、ひとまずこれで問題なく実用ができそうです。

Vue.jsっぽい感じでWebComponentを実装できるフレームワークを作った

 毎年連休は1人ハッカソン的なものをひっそりとやっています。テーマは行き当たりばったり、車輪の再発明も気にしない、とりあえず期間内で動くものを作る、無理をしない、あたりがルールです。今年はEvernoteの値上げが話題になっていたのでEvernote的なものでも作ろうかと思っていたのですが、その途中で使っていた自前WebフレームワークがうまいことWebComponent対応できてしまったので最終的にこちらを作りこむことに。見事な本末転倒です。

WebComponentの話

 WebComponentについてはMDNの解説記事がとても詳しいですが、簡単に言えばJavaScriptで独自のHTMLタグを実装できる仕組みです。昨今広く使われているReactやVue.jsといったフレームワークも雑に言えば独自タグのようなコンポーネントを組み上げてアプリケーションを実装していくものですが、WebComponentがこれらと異なるのは、Webブラウザがネイティブで備えている仕組みを使っている点です。そのため、適切に実装さえ行えば再利用性が高く、作成したコンポーネントは(基本的には)どのようなフレームワークとも組み合わせて利用できるというのが利点です。

 いっぽうで、ReactやVue.jsのように1つのファイルにHTMLとCSSとコードをまとめて記述できる単一ファイルコンポーネントのような仕組みは提供されていないため、その点ではやや実装しにくい、理解しにくい、というハードルもあります。

 また、ReactやVue.jsの別の特徴として、仮想DOMという、画面上に表示されているものとは異なる形式でメモリ上にドキュメントを保持し、それを利用してドキュメントが変更された際に高速にコンテンツ描画を行う、というものがあります。WebComponentは標準ではこういった仕組みを備えていないため、実装によってはパフォーマンス面で問題が発生する可能性もあります。

仮想DOMを使わずにコンポーネントを実装する

 ReactやVue.jsは便利でとても使いやすいのですが、開発環境を構築する際にはたくさんの依存ライブラリをインストールする必要がありますし、リリース向けにビルドされるコードも依存ライブラリの関係で大きくなりがちです。そのため、たとえばブログやメディアサイトといった、単純にコンテンツを表示するだけの部分が多いサイトの一部のみで利用したいといった場合(たとえばログインフォームを含むドロップダウンメニューを作りたい、等)、やや使いにくい印象があります。いっぽうで、昨今のWebブラウザが備えるDOM APIは非常に強力で、かつてのjQueryのような別途ライブラリ等を導入しなくとも、簡単に複雑な操作も実装できるようになっています。

 ということで、以前jumon-jsという、HTML要素に属性としてマークアップを行うことで、それらの要素にアクセスできるオブジェクトを作成できるようにする仕組みを実装しました。たとえば次のように、<input>タグにjumon-bind="width"のような感じでプロパティ名を指定したHTMLを用意します。

<form>
  <input value="50" jumon-bind="width" jumon-type="Number">
  <input value="100" jumon-bind="height" jumon-type="Number">
</form>

 これに対し、次のようにJumonクラスのオブジェクトを作成すると、そのオブジェクトのwidthプロパティ経由でHTML要素の値を取得したり設定できるようになる、というものです。

import { Jumon } from './jumon.js';
const parameter = new Jumon();
// read text box's values
console.log(parameter.width);
console.log(parameter.height);

// update text box's value
parameter.width = 100;
parameter.height = 200;

 これ自体はJavaScriptのObject.defineProperty()などの機能を使って動的にgetter/setterを追加する、みたな感じで実装した簡単なものなのですが、この仕組みにちょっと手を入れるだけでWebComponentの実装に応用できることに気付いて実装したものが今回紹介するjumon-componentになります。

実装例とその説明

 jumon-componentは、テンプレートとなるHTMLおよびCSS、動作を定義するクラスの3つを用意してmakeComponent()メソッドを実行するだけでWebComponentを実装できます。現状ではHTMLとCSSはJavaScriptファイル内にテンプレートリテラルとして埋め込んでいますが(簡単なヒストリ機能を備える電卓を実装したサンプルコード)、ちょっと頑張ってWebpackのプラグインを作れば簡単に単一ファイルコンポーネントのような形式で実装することも可能でしょう。

サンプルとして実装した電卓アプリ

 テンプレートでは、各HTMLタグにjumon-で始まる属性を指定することで、WebComponentで指定したクラスのプロパティとのひも付けを行います。たとえば次のようにjumon-text="operation"を指定すると、WebComponentオブジェクトの.operationプロパティの値が自動的にそのタグのtextContentになります。

<div class="indicator">
  <span class="operation" jumon-text="operation"></span>
  <input class="value" jumon-bind="currentValue" jumon-type="Number" value="0">
</div>

 jumon-bindも似たような動作をしますが、プロパティがtextContentではなくvalueにひも付けられる点が異なります。こちらはフォーム要素などでの利用を想定しています。

 イベントハンドラはjumon-listener属性で指定できます。値は{イベント名}:{メソッド名}で指定します。たとえばclick:setOperation('+')を指定した場合、この要素がクリックされた際にWebComponentオブジェクトのsetOperation()というメソッドが'+'という引数で実行されます。

<div class="action-buttons">
  <button jumon-listener="click:setOperation('+')">+</button>
  <button jumon-listener="click:setOperation('-')">-</button>
  <button jumon-listener="click:setOperation('*')">*</button>
  <button jumon-listener="click:setOperation('/')">/</button>
  <button jumon-listener="click:run()">=</button>
  <button jumon-listener="click:allClear">AC</button>
</div>

 Array型のプロパティに対してテンプレートを紐づけるjumon-foreach属性もあります。ここではWebComponentオブジェクトのhistoriesプロパティに<div class="history-item" jumon-text="history"></div>というテンプレートを紐づけており、このhistoriesプロパティに対するpush()pop()、添え字アクセスによる代入等で動的に要素の挿入・削除が可能です。

<div class="history-items">
  <template jumon-foreach="history in histories">
    <div class="history-item" jumon-text="history"></div>
  </template>
</div>

 WebComponentに紐づけるクラスについては、HTMLElementのサブクラスである必要はありますが、それ以外は任意に実装が可能です。ただしconnectedCallbackなどに代表されるWebComponentのライフサイクルコールバックについてはjumon-component側で隠蔽しているため、これらが必要な場合は代わりにconnecteddisconnectedadoptedattributeChangedといったメソッドを定義します。もちろんこれらをまったく定義しなくとも利用可能です(下記は実際に作成したクラスの抜粋)。

class TinyCalculator extends HTMLElement {
  constructor() {
    super();
    this._isEntered = false;
    this._lastValue = 0;
    this._stack = [];
  }
  setNumber(num) {
    if (this._isEntered) {
      this.currentValue = num;
      this._isEntered = false;
    } else {
      this.currentValue = this.currentValue * 10 + num;
    }
  }
  setOperation(operation) {
    if (!this.operation) {
      this._stack.push(this.currentValue);
    }
    if (!this._isEntered) {
      this._isEntered = true;
      this._lastValue = this.currentValue;
      //this._stack.push(this.currentValue);
      if (this.operation) {
        this.run(true);
      }
    }
    this.operation = operation;
  }

 クラス、HTML(文字列)、CSS(文字列)の3点を用意したら、これらを引数としてmakeComponent()メソッドを実行すると、WebComponentが作成されます。makeComponent()メソッドの第一引数に与えた文字列が作成されたWebComponentのタグ名となります。

makeComponent("tiny-calculator", template, {
  baseClass: TinyCalculator,
  styleSheet: css,
});

 作成したコンポーネントは一般的なHTMLタグと同様の形で利用できます。

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Tiny Calculator Demo</title>
  <script type="module" src="./tiny-calculator.js"></script>
</head>
<body>
  <tiny-calculator></tiny-calculator>
</body>
</html>

今後の展望

 このように、ひとまず最低限動く仕組みを実装はしていますが、実用しようとするともう少し機能拡充は必要でしょう。現在不足しているものとして、jumon-foreachで紐づけたarrayオブジェクトの操作としてpushpopunshiftshiftspliceしかサポートしていない点と、WebComponentタグの属性サポートを現状一切考慮していない点があります。特に属性サポートはコンポーネントを組み合わせてアプリケーションを実装する際には必須と言えるもののため、ぜひ実装したいところではあります。

USB/NFCセキュリティトークンによる二要素認証の導入

 アカウント乗っ取りへの対策として、昨今では多くのサービスが二要素認証を導入しています。二要素認証の方法としてはメールやSMSを利用するものが多いですが、それらが受信できない場合は認証ができなくなってしまうという問題があります。ということで、USB/NFCセキュリティトークンを使った二要素認証を導入してみました。

 メールやSMSを利用する二要素認証では、たとえばメールアカウントがブロックされたり、SMSを受信する端末が手元に無い場合などに認証ができません。自分はメールをGmailとYahoo!メールに依存しているため、万が一これらのアカウントがロックされてしまうとメールを利用した二要素認証を利用するサービスすべてで認証が行えなくなってしまいます。また、SMSに関しても、スマートフォンのバッテリー残量がなくなったり、出先でスマートフォンを携帯していないため受信できない、自宅でスマートフォンを鞄に入れっぱなしにしていてすぐに出てこない、といったケースがそれなりにあります。USB/NFCセキュリティトークンを利用することで、これらの問題を回避できるのではないかと期待しての導入でした。

Google Titanセキュリティキーの導入

 まず導入したのが、「Google Titanセキュリティキー」です。こちらについてはPC Watchの記事(Google「Titan セキュリティキー」ってどう使うの?NFCによるスマホ利用にも対応)が詳しいのですが、USB Type-CポートとNFCインターフェイスの両方に対応しており、スマートフォンでもPCでも利用できる、というのがポイントです。価格は4,500円。Googleストアで購入後、2日後に到着しました。

 届いたTitanセキュリティキーはUSBメモリサイズのデバイスですが、しっかりした化粧箱に入っていました。過剰包装な感じもありますが、ちゃんと説明書が入っているのは親切ではあります。

届いたTitanセキュリティキー

パッケージの中身

 Googleアカウントとの紐づけはPC Watchの記事に書いてある通りで、自分のメインブラウザはFirefox、OSはWindows 11ですが、特に問題なく設定が完了し、二要素認証で利用できるようになりました。

TitanセキュリティキーはGoogle以外では利用できない?

 続いて、Google以外でのサービスでも試してみました。Webサービスにおけるセキュリティキー利用に関しては、FIDO2という共通規格があり、FIDO2をサポートしているサービスではセキュリティトークンの利用が可能な はずです。FIDO2のサポートをうたう代表的なサービスとしては、GoogleアカウントのほかMicrosoftアカウント、Facebook、Twitter、ヤフーなどがあります。

 ということで、まずはネットで利用事例があるMicrosoftアカウントへのTitanセキュリティキーの紐づけを試してみました(参考記事:ASCII.jp:Windows 10+FIDO2デバイスでパスワード入力なしにMicrosoftアカウントにログインケータイWatch:Microsoft アカウントで使えるようになったFIDO2セキュリティキーを試してみた)。しかし、記事どおりの手順を試してみても、「このセキュリティキーは使用できません」というエラーが出てセットアップが進みません。

セットアップの続行エラー

 これはどういうことなんだと調べた結果、Titanセキュリティキーがサポートしているのは「FIDO U2F」という規格で、FIDO2とは異なる規格なのだそうです。そのため、TitanセキュリティキーではFIDO2が必要なサービスを利用できないということが分かりました。U2Fに対応しているサービスとしてはGitHubやFacebook、Dropboxなどがあるとのことですが、MicrosoftアカウントはU2Fには対応していないため利用できないようです。

FIDO2対応セキュリティキーの導入

 前述のように、自分はヤフーメールとGmailをメールによる二要素認証の送信先として利用しています。GmailはTitanセキュリティキーによる二要素認証に対応していますが、ヤフーメールもどうにかして二要素認証を利用したいところです。ということで、FIDO2対応のセキュリティキーも購入することにしました。FIDO2対応セキュリティキーとしては、Yubicoというメーカーの製品と、飛天ジャパンというメーカーの製品が比較的入手しやすいようです。今回はYubicoのUSB Type-A接続およびNFCに対応するセキュリティキーを購入してみました。入手しやすいといっても、家電量販店等ではほぼ扱われておらず、個人が買うならほぼAmazon.co.jp一択のようです。価格は5,500円でした。

 こちらも注文してから2日ほどで手元に届きました。こちらは厚さ2~3mmほどのデバイスなので、パッケージは簡素です。取扱説明書はありませんが、まあ問題ないでしょう。

Yubicoのセキュリティキー

PINの設定

 FIDO2対応のセキュリティキーでは、まずデバイスにPIN(暗証番号)を登録する必要があります。これは、セキュリティキーをPCに接続した状態でWindowsの「設定」-「アカウント」-「サインインオプション」画面内にある「セキュリティキー」-「管理」から行えます。

「サインインオプション」設定画面

 セキュリティキーを接続した状態で指示に従って操作すると、「セキュリティキー暗証番号(PIN)」と表示されたダイアログが表示されるので、ここで「追加」ボタンをクリックして、PINを登録します。登録できるPINの長さは4~63文字だそうです。

「セキュリティキー暗証番号(PIN)」設定画面

 これでここで登録したPINを使って認証が行えるようになります。

Microsoftアカウントにセキュリティキーを紐づける

 続いてMicrosoftアカウントにセキュリティキーの紐づけを行ってみました。手順に関しては前述のケータイWatchやASCII.jpの記事に記載してある通りです。なお、ケータイWatchの記事では言語設定を英語に変更するという手順が記載されていましたが、自分の環境では日本語環境のままで問題なく設定が可能でした。また、セキュリティキーを紐づけると、Microsoftアカウントのログイン時に、パスワードではなくセキュリティキーのPINの入力とセキュリティキーへのタッチが求められるようになります。

 なお、Microsoftアカウントではログイン時の「サインインオプション」に「セキュリティキー」という選択肢があります。これを利用すると、登録メールアドレスの入力なしにセキュリティキーだけでログインができるように見えるのですが、自分の環境ではこちらはエラーとなり利用できませんでした。

ほかのサービスでセキュリティキーを紐づける

 続いて、FacebookやTwitterでもセキュリティキーの紐づけを行ってみましたが、こちらも問題なく紐づけができました。ただ、ヤフーに関しては設定ページを開いても、「 現在アクセスしている環境では登録ができません」と表示され設定が行えませんでした。プレスリリース:ヤフー、パソコンでも「FIDO2」規格に準じた指紋・顔認証を利用したログインに対応に書いてある環境のはずなのですが、ここでは「指紋・顔認証」と書いてあるので、セキュリティトークンによる認証には非対応のようです。

 ということで、結局ヤフーのアカウントをセキュリティトークンベースの二要素認証に対応させることはできませんでした。自分が実現したいのは、「物理トークンさえあれば 任意のデバイスから二要素認証を使ってログインできる」ことなので、ヤフーさんには頑張ってもらいたいところです。

Windows 11でデスクトップ右上に時計を表示する

 自分は長らくタスクバーを画面上側に表示する設定でWindowsを使っていたのですが、なんとWindows 11ではタスクバーの位置変更機能が削除されてしまい、画面下部以外にタスクバーを表示することはサポート外となりました。さらに、サポート外とはいえレジストリエディタで直接設定を書き換えることでタスクバーの位置を変えることはできたのですが、ついにこの抜け道もWindows 11の最近の大型アップデートでふさがれてしまったようです。

 正直なところ、タスクバーを画面下に表示させることに関してはそこまでこだわりはないのですが(そもそもmacOSは画面下にDockがあるし)、困るのは時計です。画面右上に視線を移して日付時刻を確認する、というのが体に染みついてますし、自分はmacOSとWindowsを併用しているため、macOSでは右上、Windowsでは右下、とOS環境に応じて自らの動作をカスタマイズできる気がしません。

 ということで、Windows環境でも画面右上に時計を表示させる方法を模索してみました。結論としては、「ElevenClock」というツールを使って実現できました。

ElevenClockについて

 ElevenClockについては窓の杜の記事が詳しいのですが、Windows 11のタスクバーの時計表示をカスタマイズするツールです。マルチディスプレイ環境で2つめ以降のディスプレイ上に時計を表示するのが主なユースケースのようですが、設定を変えることでタスクバー上の時計はそのまま表示させておき、それに加えて画面右上に追加で時計を表示することが可能です。

 なお、ElevenClockはPython+GUI Toolkitライブラリで実装されており、GPLv3ライセンスでソースコードも公開されているため、頑張ればより柔軟な改造も可能ではあります。また、インストーラはMicrosoft Storeでも配布されています。

Microsoft StoreのElevenClock検索結果

画面右上に時計を表示させるためのElevenClock設定

 ElevenClockではフォントの種類やサイズ、日付時刻フォーマットのカスタマイズが可能です。これらを変更して好みの表示に設定した後、時計を右クリックしてコンテキストメニューから「モニターツール」-「この時計を画面上に移動」を選択します。これで時計が画面右上に表示されるようになります。

時計を画面上部に表示させる設定はElevenClockの設定とは別の場所にある

 さらに、ElevenClockの設定で「修正やその他の試験機能」内にある「既定のWindowsの時計を隠す機能を無効にする」にチェックを入れることで、タスクバー上の時計はそのまま表示しておくことができます。

 時計を画面右上に表示させることができたら、もうタスクバーにはほとんど用はないので、タスクバーの設定で「タスクバーを自動的に隠す」にチェックを入れ、通常時は非表示にしておきました。これで問題なくWindows 11のアップデートができそうです。

完成したデスクトップ