概要
※ tiff2pdf.js / tiff2pdf.wasm の DEMO を試作しました。 github
tiff なんてもうほぼ絶滅種の画像フォーマットで今や PDF が当たり前の時代なんですが、図面の電子化であえて使うケースがあります。
複数ページの格納に対応、複数の解像度・カラーモノクロを混在可能、ラスタフォーマットという… 画像フォーマットとして見ると優れた形式です。
※ 優れた形式と書きましたが、互換性の問題は存在します。例を挙げますと…
- 2 種類の JPEG フォーマット (
COMPRESSION_OJPEG
とCOMPRESSION_JPEG
) があり、アプリによっては対応していないものがあります。 - PDF の注釈機能に相当する独自タグが存在します。こちらもアプリによっては未対応 (表示できない) で、TIFF を編集&保存するとこの情報を継承できずに損失する場合があります。
- 一部の Windows のペイントで TIFF ファイルを保存すると 32-bpp カラー TIFF になります。FreeImage は 32-bpp カラー TIFF の読み込みに対応していないようで、エラーになります。
HTML5 の時代にこの tiff を表示するには…
- サーバーサイドにて、ページ単位で jpg や png 形式に変換、または全ページを PDF に変換してからダウンロードして表示
- tiff.js, UTIF.js などの JavaScript を用い、web ブラウザーの対処能力で画像変換し、表示
という方法がパッと考えられます。今回は JavaScript を使う方法に焦点を当てていきます。
A4 300 dpi の画像をページめくりするスピードでデコードしていきたいのですが、2480 × 3508 のような大きな画像です。JavaScript では画像の圧縮解除に数秒の時間が掛かる感じでした。tiff の処理以外にも <canvas>
を中継するなど追加のコーディングが必要になりますが… 周辺コードの CPU コストはそこまで高くない感じでした。
ですので、画像デコード処理がガチで重いものと推測します。
WebAssembly
そこで期待の wasm です:
- C/C++からWebAssemblyにコンパイルする - WebAssembly | MDN
- Compiling an Existing C Module to WebAssembly - WebAssembly | MDN
GNU ツールチェインのような使用感
実はもう「GNU ツールチェイン」で見られるような、ビルドのためのエコシステムが WebAssembly でも出来上がっているようなのです。
これを知ってビックリ&コーフンしました!
例はつぎの zlib にて:
zlib
zlib を CMake を使い、つぎのようにセットアップして emmake make install
すると、~/wasm-root
以下にインストールまで実施されます。
$ emcmake cmake -G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX:PATH=~/wasm-root \
..
※ -DCMAKE_BUILD_TYPE=Release
を指定すると JavaScript のグルーコードは最適化 (minify) されます。
~/wasm-root/lib/
の様子です
$ ls ~/wasm-root/lib/ -lh
合計 3.1M
-rw-r--r-- 1 ku ku 422K 6月 3 19:18 libjpeg.a
drwxrwxr-x 2 ku ku 4.0K 6月 8 13:06 libpng
lrwxrwxrwx 1 ku ku 10 6月 8 13:06 libpng.a -> libpng16.a
lrwxrwxrwx 1 ku ku 10 6月 8 13:06 libpng.so -> libpng16.a
-rw-r--r-- 1 ku ku 597K 6月 8 13:05 libpng16.a
-rw-r--r-- 1 ku ku 1.2M 6月 3 19:20 libtiff.a
-rw-r--r-- 1 ku ku 28K 6月 3 19:20 libtiffxx.a
-rw-r--r-- 1 ku ku 564K 6月 3 19:17 libturbojpeg.a
-rw-r--r-- 1 ku ku 242K 6月 3 18:58 libz.a
drwxrwxr-x 2 ku ku 4.0K 6月 8 13:06 pkgconfig
libz.a
の中身は何でしょうか。まずはファイルリストを:
$ ar t ~/wasm-root/lib/libz.a
adler32.o
compress.o
crc32.o
deflate.o
gzclose.o
gzlib.o
gzread.o
gzwrite.o
inflate.o
infback.o
inftrees.o
inffast.o
trees.o
uncompr.o
zutil.o
zutil.o を例にとってみてみましょう。file
を使います。Man page of FILE: ファイルの中身からファイル形式を推定できる便利なツールです。
$ ar p ~/wasm-root/lib/libz.a zutil.o | file -
/dev/stdin: WebAssembly (wasm) binary module version 0x1 (MVP)
参考がてら hexdump で覗いてみます:
$ ar p ~/wasm-root/lib/libz.a zutil.o | hexdump -C | head -4
00000000 00 61 73 6d 01 00 00 00 01 9a 80 80 80 00 05 60 |.asm...........`|
00000010 00 01 7f 60 01 7f 01 7f 60 03 7f 7f 7f 01 7f 60 |...`....`......`|
00000020 02 7f 7f 00 60 01 7f 00 02 e9 80 80 80 00 05 03 |....`...........|
00000030 65 6e 76 0f 5f 5f 6c 69 6e 65 61 72 5f 6d 65 6d |env.__linear_mem|
wabt の wasm-objdump で可視化を試みました: WebAssembly/wabt: The WebAssembly Binary Toolkit
$ ./wasm-objdump -x /tmp/zutil.o
zutil.o: file format wasm 0x1
Section Details:
Type[5]:
- type[0] () -> i32
- type[1] (i32) -> i32
- type[2] (i32, i32, i32) -> i32
- type[3] (i32, i32) -> nil
- type[4] (i32) -> nil
Import[5]:
- memory[0] pages: initial=1 <- env.__linear_memory
- table[0] type=funcref initial=0 <- env.__indirect_function_table
- global[0] i32 mutable=1 <- env.__stack_pointer
- func[0] sig=1 <env.malloc> <- env.malloc
- func[1] sig=4 <env.free> <- env.free
Function[5]:
- func[2] sig=0 <zlibVersion>
- func[3] sig=0 <zlibCompileFlags>
- func[4] sig=1 <zError>
- func[5] sig=2 <zcalloc>
- func[6] sig=3 <zcfree>
...
zlibVersion
function とかエクスポートされていることが確認できます。
という事で… emsdk の力により zlib が他の wasm プログラムのビルドで使える状態にしてくれたことがわかりました。
libpng
$ emcmake cmake -G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX:PATH=~/wasm-root \
-DZLIB_LIBRARY:PATH=~/wasm-root/lib/libz.a \
-DZLIB_INCLUDE_DIR:PATH=~/wasm-root/include \
..
libjpeg
libjpeg (jpegsrc.v9d.tar.gz) は libtiff で使うのでビルド:
$ emconfigure ./configure --prefix=`realpath ~/wasm-root/` --disable-shared
libtiff
ビルド。いちいち LIBRARY/INCLUDE を指定しているのがダサいですが、パッケージを自動充填させる方法が判らなかったので、時間の関係で PATH を指定しました。
$ emcmake cmake -G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX:PATH=~/wasm-root \
-DZLIB_LIBRARY:PATH=~/wasm-root/lib/libz.a \
-DZLIB_INCLUDE_DIR=~/wasm-root/include/ \
-DJPEG_LIBRARY:PATH=~/wasm-root/lib/libjpeg.a \
-DJPEG_INCLUDE_DIR:PATH=~/wasm-root/include \
..
make install
の結果、libtiff.a は ~/wasm-root/lib/libtiff.a
に存在することを先に確認しましたので、省略。
libtiff/tools
js
と wasm
が生成されました。js
は wasm
を呼び出すためのグルーコードとなっているようで、両方とも必須の様子です。
$ ls -lh
合計 32M
drwxrwxr-x 22 ku ku 4.0K 6月 3 19:20 CMakeFiles
-rw-rw-r-- 1 ku ku 263 6月 3 19:01 CTestTestfile.cmake
-rw-rw-r-- 1 ku ku 32K 6月 3 19:01 Makefile
-rw-rw-r-- 1 ku ku 18K 6月 3 19:01 cmake_install.cmake
-rw-rw-r-- 1 ku ku 237K 6月 3 19:20 fax2ps.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 fax2ps.wasm
-rw-rw-r-- 1 ku ku 229K 6月 3 19:20 fax2tiff.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 fax2tiff.wasm
-rw-rw-r-- 1 ku ku 225K 6月 3 19:20 pal2rgb.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 pal2rgb.wasm
-rw-rw-r-- 1 ku ku 229K 6月 3 19:20 ppm2tiff.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 ppm2tiff.wasm
-rw-rw-r-- 1 ku ku 225K 6月 3 19:20 raw2tiff.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 raw2tiff.wasm
-rw-rw-r-- 1 ku ku 226K 6月 3 19:20 rgb2ycbcr.js
-rw-rw-r-- 1 ku ku 1.5M 6月 3 19:20 rgb2ycbcr.wasm
-rw-rw-r-- 1 ku ku 225K 6月 3 19:20 thumbnail.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 thumbnail.wasm
-rw-rw-r-- 1 ku ku 225K 6月 3 19:20 tiff2bw.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 tiff2bw.wasm
-rw-rw-r-- 1 ku ku 234K 6月 3 19:20 tiff2pdf.js
-rw-rw-r-- 1 ku ku 1.7M 6月 3 19:20 tiff2pdf.wasm
-rw-rw-r-- 1 ku ku 236K 6月 3 19:20 tiff2ps.js
-rw-rw-r-- 1 ku ku 1.5M 6月 3 19:20 tiff2ps.wasm
-rw-rw-r-- 1 ku ku 226K 6月 3 19:20 tiff2rgba.js
-rw-rw-r-- 1 ku ku 1.5M 6月 3 19:20 tiff2rgba.wasm
-rw-rw-r-- 1 ku ku 225K 6月 3 19:20 tiffcmp.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 tiffcmp.wasm
-rw-rw-r-- 1 ku ku 225K 6月 3 19:20 tiffcp.js
-rw-rw-r-- 1 ku ku 1.5M 6月 3 19:20 tiffcp.wasm
-rw-rw-r-- 1 ku ku 229K 6月 3 19:20 tiffcrop.js
-rw-rw-r-- 1 ku ku 1.6M 6月 3 19:20 tiffcrop.wasm
-rw-rw-r-- 1 ku ku 225K 6月 3 19:20 tiffdither.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 tiffdither.wasm
-rw-rw-r-- 1 ku ku 213K 6月 3 19:20 tiffdump.js
-rw-rw-r-- 1 ku ku 54K 6月 3 19:20 tiffdump.wasm
-rw-rw-r-- 1 ku ku 225K 6月 3 19:20 tiffinfo.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 tiffinfo.wasm
-rw-rw-r-- 1 ku ku 225K 6月 3 19:20 tiffmedian.js
-rw-rw-r-- 1 ku ku 1.5M 6月 3 19:20 tiffmedian.wasm
-rw-rw-r-- 1 ku ku 229K 6月 3 19:20 tiffset.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 tiffset.wasm
-rw-rw-r-- 1 ku ku 225K 6月 3 19:20 tiffsplit.js
-rw-rw-r-- 1 ku ku 1.4M 6月 3 19:20 tiffsplit.wasm
node で実行できたのがまたビックリです。
$ node tiffinfo.js
LIBTIFF, Version 4.1.0
Copyright (c) 1988-1996 Sam Leffler
Copyright (c) 1991-1996 Silicon Graphics, Inc.
usage: tiffinfo [options] input...
where options are:
-D read data
-i ignore read errors
-c display data for grey/color response curve or colormap
-d display raw/decoded image data
-f lsb2msb force lsb-to-msb FillOrder for input
-f msb2lsb force msb-to-lsb FillOrder for input
-j show JPEG tables
-o offset set initial directory offset
-r read/display raw image data instead of decoded data
-s display strip offsets and byte counts
-w display raw data in words rather than bytes
-z enable strip chopping
-# set initial directory (first directory is # 0)
program exited (with status: -1), but EXIT_RUNTIME is not set, so halting execution but not exiting the runtime or preventing further async execution (build with EXIT_RUNTIME=1, if you want a true shutdown)
実際に使用しようとすると、No such file or directory.
のエラーになりました。
$ node tiffinfo.js ~/libtiff/test/images/testfax4.tiff
TIFFOpen: /home/ku/libtiff/test/images/testfax4.tiff: No such file or directory.
多分、wasm の世界のファイルシステムが Linux のものと異なるのでしょう…
fopen はどうなる…?
TIFF の読み込みをしたいので、TIFFClientOpen を使いメモリから TIFF をロードするのかなと考えていました。
先述の Compiling an Existing C Module to WebAssembly - WebAssembly | MDN の記事を見ていると、どうもそんな書き方だったので。
下記の File System API を使う事で、wasm の外からファイルを充填できるようです。知りませんでした。
参考記事です:
- emscriptenのファイルIO :右京web
- azakai's blog: HOWTO: Port a C/C++ Library to JavaScript (xml.js)
- File System API — Emscripten 1.39.17 documentation
wasm の外から wasm の中のファイルの読み書きができるという事は…
普通に C/C++ で書かれた tiff2png のコマンドラインツールを探してきて、
エントリポイントを見繕いすれば…
移植性の高い方法で wasm 化を実現できるのではと感じました。
tiff2pdf の WebAssembly 版を WEB で使えるようにする
tiff2pdf を WEB ブラウザーから再利用できるにしたいです。純粋なコマンドラインアプリとして利用する事を目指したいです。
ビルド
そこで、ビルドオプションを修正してみます。修正前:
add_executable(tiff2pdf tiff2pdf.c)
target_link_libraries(tiff2pdf tiff port)
修正後:
add_executable(tiff2pdf tiff2pdf.c)
target_link_libraries(tiff2pdf tiff port)
if(EMSCRIPTEN)
set_target_properties(tiff2pdf
PROPERTIES LINK_FLAGS "\
-s EXPORTED_RUNTIME_METHODS=FS,callMain \
-s INITIAL_MEMORY=64MB \
-s MAXIMUM_MEMORY=640MB \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s EXPORT_NAME=\"create_tiff2pdf\" \
-s WASM=1 \
-s SINGLE_FILE=0 \
-s FORCE_FILESYSTEM=1 \
"
)
endif()
修正の詳細:
-
EXPORTED_RUNTIME_METHODS=FS,callMain
- Emscripten API functions へのアクセスを許します。
- 詳細は: Emscripten Compiler Settings — Emscripten 3.1.61-git (dev) documentation
-
INITIAL_MEMORY=64MB
は、- ドキュメントが見当たらないため推測になりますが、初期のヒープ領域のサイズのようです。
- 詳細は: Release Notes — Emscripten 2.0.16
ocumentation
-
MAXIMUM_MEMORY=640MB
は、- ドキュメントが見当たらないため推測になりますが、ヒープ領域を自動拡張する場合の上限値のようです。
- 詳細は: Release Notes — Emscripten 2.0.16
ocumentation -
ALLOW_MEMORY_GROWTH
を一緒に指定しないとMAXIMUM_MEMORY
は機能しないそうです。 - 詳細は: Set INITIAL_MEMORY and MAXIMUM_MEMORY must -s ALLOW_MEMORY_GROWTH=1 ? · Issue #14667 · emscripten-core/emscripten
-
ALLOW_MEMORY_GROWTH
は、- ドキュメントが見当たらないため推測になりますが、ヒープ領域の自動拡張を許可するオプションのようです。
-
MODULARIZE=1
- Factory パターンを適用します。
Module()
読み出しによって Module object への Promise を生成します。 - 詳細は: WebIDL Binder — Emscripten 2.0.16 documentation
- Factory パターンを適用します。
-
EXPORT_NAME=\"create_tiff2pdf\"
- 詳細は: WebIDL Binder — Emscripten 2.0.16 documentation
- モジュールインスタンス
Module
の名称を変更します。 -
MODULARIZE=1
と併せる事でModule()
ではなくcreate_tiff2pdf()
で Module object の Promise を生成できるようにします。
-
WASM=1
- WebAssembly でビルドします。
- 詳細は: Building to WebAssembly — Emscripten 2.0.16 documentation
-
SINGLE_FILE=0
-
.js
と.wasm
を分けてビルドします。 -
SINGLE_FILE=1
について-
.js
ファイルに BASE64 化された WebAssembly のバイナリを封入したい場合に使います。 - これを使用したいケースについてですが…
cmake
とmake install
をすると、実は.wasm
のインストールが忘れられる問題があるようで、この問題を回避するために使うことが出来ます。 -
~/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake
を確認するとset(CMAKE_EXECUTABLE_SUFFIX ".js")
という記述があります。恐らく asm.js での利用を前提とした設計なのでしょうか。
-
- 詳細は: Building Projects — Emscripten 2.0.16 documentation
-
-
FORCE_FILESYSTEM=1
- これを指定しないと
FS.writeFile
がエラーで機能しない場合があったため、指定しています。 - 詳細は: File System API — Emscripten 2.0.16 documentation
- これを指定しないと
HTML
HTML は、いつもの通りです。
<script src="./js/tiff2pdf.js"></script>
JavaScript
Module object の詳細は: Module object — Emscripten 2.0.16 documentation
また、実際に生成されたグルーコードを眺めながらキーワードを Google 検索すると理解が捗るかもしれません。
Module object を返す Promise を生成。 {noInitialRun: true}
とする事で main()
を実行しないようにします。
tiff2pdf = await create_tiff2pdf({noInitialRun: true})
tiff2pdf 内の File System "/tmp/input.tif"
にファイルを書き込みます。
tiff2pdf.FS.writeFile("/tmp/input.tif", new Uint8Array([0x49,0x49,0x2A,0x00,xxx]))
※ FS
の詳細は: File System API — Emscripten 2.0.16 documentation
main()
を呼び出します。callMain
の引数の配列には argv[1]
相当から指定します。 argv[0] = "./this.program"
は自動的に挿入されます。
tiff2pdf.callMain(["-o", "/tmp/output.pdf", "/tmp/input.tif"])
main()
の戻り値の判定をしたい場合は Module object 生成の段階で細工します。
// Assume 0, quit isn't called on successful exit from main().
let quitOnStatus = 0;
const tiff2pdf = await create_tiff2pdf({
noInitialRun: true,
quit: function (status, ex) { quitOnStatus = status; },
});
※ quit
は main()
が正常終了 return 0;
した場合にはコールされないようです。というのも main()
の正常終了は Runtime が正常な状態で待機している事に起因するようです。
つぎに "/tmp/output.pdf"
の内容物を Uint8Array で取得します。
pdfByteArray = tiff2pdf.FS.readFile("/tmp/output.pdf")
pdfByteArray
を URL に変換して <object id="pdfOutput"></object>
へ設定する方法を考えます。
-
DATA URL は
"data:application/pdf;base64,JVBERi0x...Cg=="
のような URL です。 -
オブジェクト URL は
blob:https://hiraokahypertools.github.io/796b34fd-b61f-41da-87e8-d898fb21de8e"
のような URL です。
DATA URL を使用する案:
const pdfBlob = new Blob([pdfByteArray], { type: "application/pdf" });
const fileReader = new FileReader();
fileReader.readAsDataURL(pdfBlob);
$(fileReader).on("load", function () {
$("#pdfOutput").prop("data", fileReader.result);
});
オブジェクト URL を使用する案:
const pdfBlob = new Blob([pdfByteArray], {type: "application/pdf"})
const url = URL.createObjectURL(pdfBlob);
$("#pdfOutput").prop("data", url);
WebAssembly の価値と、付加価値について
WebAssembly の価値は使用方法を問わず 不変 のものですが…
WebAssembly の周りを囲んでいる glue code によって、開発者へ大きな生産性 (付加価値) を提供しているのかなと感じました。
※ まあでも glue code は generator によって生成されるのだから… generator の価値になるのかもしれません。
WebAssembly by Emscripten は、C/C++ で書かれた CLI (Command line interface) アプリ (及び C/C++ ライブラリ) を Web の世界へ容易に招き入れる技術だとわたしは解釈しました。
割と適当な図ですが…
WebAssembly の他の例はというと ASP.NET Core Blazor が出てきていて、わたしは「DOM を操作する WebAssembly」と解釈しています。
WebAssembly と DOM/JavaScript との Interoperability という話になると思いますが、この辺りの体系化・標準化が進めば WebAssembly を取り巻く世界も広がるのではと推測しています。
libtiff の tools/tiff2pdf が unsupported へ移行
どうも libtiff v4.6 より tools/tiff2pdf 等が unsupported へ移行する雰囲気です。
- 2023.04.06 19:07, "Re: [Tiff] Remove TIFFCROP from LibTiff", by Even Rouault
- Prepare LibTIFF Tools Removal (#580) · Issues · libtiff / libtiff · GitLab
- Move most TIFF tools to archive and keep some as unsupported (see #580). (!520) · Merge requests · libtiff / libtiff · GitLab
tiff2pdf に対して提出していたマージリクエストがクローズされました。
Closing due to tiff2pdf being unmaintained
その理由については ML で述べられているように、この 2 行に集約されているのではないかと思います。
I have been trying to fix the constant CVE issues at tiffcrop for several years.
Today I can say "fixing is not possible".
「数年間 tiffcrop に対して絶えず報告される CVE の問題を修正しようとしてきました。」
「今日、私は『修正は不可能だ』と言いたい。」
tiff2pdf ではなく tiffcrop について言及されていますが,
tiff2pdf についても同様の「メンテ不能」問題が存在していると思います。
また、
The vast majority of recent libtiff related CVEs in recent years are not in libtiff itself, but in its utilities.
Personally I don't care about libtiff utilities (perhaps except tiffinfo and tiffdump for debugging purposes), just the lib.
- 「近年の libtiff 関連の CVE は libtiff 本体のものというよりかは、libtiff のユーティリティを対象としたものが大多数である」
- 「libtiff ユーティリティの修正にはあまり興味がない」
という心境も伺うことができます。
CVE についてですが、2023 年だけでも 23 件挙げられており、
その対応を 2 名のコアメンバーだけで処理するのは大変なようにも思えます。
tiff2pdf の代替手段については ImageMagick 等の利用が挙げられます。
今後新しいプロジェクトで tiff2pdf にべったり依存するのは避けた方が良いでしょう。
libtiff の tools/tiff2pdf が unsupported から復帰
v4.7.0 より tools/tiff2pdf などがメンテナンス状態へ復帰されるようです。
.. note::
At libtiff v4.6.0, the source code for most TIFF tools (except tiffinfo,
tiffdump, tiffcp and tiffset) has been moved to archive/ directory
and was not built.
tiff2ps and tiff2pdf source code has been moved in a unsupported category,
no longer built by default, but were still part of the the source
distribution.
With libtiff v4.7.0 those tools were restored.
このような変化が齎されたのは、精力的なメンテナによる貢献のお陰だと思います。感謝いたします。
2024.03.12 19:27, "[Tiff] working through wontfix-unmaintained bugs", by Lee Howard
2024.04.19 08:34, "[Tiff] Call for discussion: RFC 2: Restoring needed libtiff tools", by Sulau