5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebAssembly で libtiff を!

Last updated at Posted at 2020-06-08

概要

※ tiff2pdf.js / tiff2pdf.wasm の DEMO を試作しました。 github

tiff なんてもうほぼ絶滅種の画像フォーマットで今や PDF が当たり前の時代なんですが、図面の電子化であえて使うケースがあります。

複数ページの格納に対応、複数の解像度・カラーモノクロを混在可能、ラスタフォーマットという… 画像フォーマットとして見ると優れた形式です。

※ 優れた形式と書きましたが、互換性の問題は存在します。例を挙げますと…

  • 2 種類の JPEG フォーマット (COMPRESSION_OJPEGCOMPRESSION_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 です:

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

jswasm が生成されました。jswasm を呼び出すためのグルーコードとなっているようで、両方とも必須の様子です。

$ 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 の外からファイルを充填できるようです。知りませんでした。

参考記事です:

wasm の外から wasm の中のファイルの読み書きができるという事は…
普通に C/C++ で書かれた tiff2png のコマンドラインツールを探してきて、
エントリポイントを見繕いすれば…
移植性の高い方法で wasm 化を実現できるのではと感じました。

tiff2pdf の WebAssembly 版を WEB で使えるようにする

tiff2pdf を WEB ブラウザーから再利用できるにしたいです。純粋なコマンドラインアプリとして利用する事を目指したいです。

ビルド

そこで、ビルドオプションを修正してみます。修正前:

libtiff/tools/CMakeLists.txt
add_executable(tiff2pdf tiff2pdf.c)
target_link_libraries(tiff2pdf tiff port)

修正後:

libtiff/tools/CMakeLists.txt
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()

修正の詳細:

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; },
  });

quitmain() が正常終了 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 です。
  • オブジェクト URLblob: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 の世界へ容易に招き入れる技術だとわたしは解釈しました。

割と適当な図ですが…

image.png

WebAssembly の他の例はというと ASP.NET Core Blazor が出てきていて、わたしは「DOM を操作する WebAssembly」と解釈しています。

WebAssembly と DOM/JavaScript との Interoperability という話になると思いますが、この辺りの体系化・標準化が進めば WebAssembly を取り巻く世界も広がるのではと推測しています。

libtiff の tools/tiff2pdf が unsupported へ移行

どうも libtiff v4.6 より tools/tiff2pdf 等が unsupported へ移行する雰囲気です。

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

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?