こんにちはジョージです。かの有名な海賊王は、仲間にしかわからない方法でメッセージを伝えたといいます。
そして、どれだけ情報を秘匿しカスタム App から利用できるかは、我々じぇねんちゅの課題でもあります。
PNG画像への情報埋め込みの例
さて、ここに 1 枚の PNG 画像があります。皆様は何かメッセージを受け取れたでしょうか?
macOS ユーザは、画像を自分の PC に保存してから「プレビュー App」で情報を確認してみましょう。
iPTC タブのキャプション情報に「明日、六本木に集合👍」とメッセージが確認できました。
この PNG 画像を使って仲間と伝文すれば、敵に悟られる事なく無事に目的地で集合できるわけです。
PNGファイル仕様
さっそく PNG のファイル仕様を確認すると、画像データ以外にもさまざまな補助データの埋め込みが可能であることがわかります。
https://ja.wikipedia.org/wiki/Portable_Network_Graphics
ものすごく簡単にいえば PNG は以下のような構造となるわけです。
ファイルヘッダ
IHDR
IDAT(任意の数)
IEND
また、IHDR や IDAT、IEND はチャンクと呼ばれ、その構造は以下のようになっています。
チャンクサイズ(4 バイト)
チャンクタイプ(4 バイト)
チャンクデータ(任意)
CRC(4 バイト)
チャンク の種類がやたら多いです。今回は iPhone から撮影して PNG に変換した画像に残される以下のチャンクタイプのみを取り扱います。
“IHDR”; //イメージヘッダ
“iCCP”; //組み込み ICC プロフィール
“eXIf”; //エグジフ
“iTXt”; //テキストデータ
“pHYs”; //物理的なピクセル寸法
“IDAT”; //イメージデータ
“IEND”; //イメージ終端
CRC(Cyclic Redundancy Check)巡回冗長検査
PNG ではファイルの破損を検出する為に CRC によるチェックを採用しています。ファイルに保存されているチャンクタイプ+データから算出した CRC の値と、ファイルに保存されている CRC の値が違う場合は、データが破損しているとみなされ正常に PNG が表示されません。
https://ja.wikipedia.org/wiki/%E5%B7%A1%E5%9B%9E%E5%86%97%E9%95%B7%E6%A4%9C%E6%9F%BB
チャンクタイプ+チャンクデータから CRC の算出は大変ややこしいのですが、インターネットでは先人達が素晴らしいコードを残してくれています。今回はこちらで紹介されているコードを使って WebViewer から利用してみます。共有や修正は自由のようです、ありがとうございました!
This source code is in the public domain. You may use, share, modify it freely, without any conditions or restrictions.
https://simplycalc.com/crc32-source.php
ただし、こちらのソース crc32_compute_string 関数では引数に、テキスト文字列を想定しているので、カスタム App から使いやすいように 16 進フォーマットのテキストをそのまま扱えるように修正しておきます。
for (i = 0; i < str.length; i = i + 2) {
// crc = crc32_add_byte(table, crc, str.charCodeAt(i));
var ucp = parseInt(str.substr( i, 2 ), 16);
crc = crc32_add_byte(table, crc, ucp);
}
10進数、16進数のカスタム関数はClaris 公式の関数・スクリプトガイドにも載っているので参考にしてみてください。(下記URLのページからPDFを無料でダウンロードすることもできます。)
https://content.claris.com/fmb19_reg-ja?utm_medium=partner
16 進数から 10 進数、10 進数から 16 進数への変換はよく使うので、事前に以下のサイトから使い勝手のよいカスタム関数を登録しておくのもよいと思います。
https://www.briandunning.com/cf/
今回使った カスタム App スクリプトやJavaScript 一覧
main.html
WebViewer に展開して、Web ビューアで JavaScript を実行 スクリプトステップから利用します。
<html>
<head>
<script type='text/javascript' src='crc32.js'></script>
<script type='text/javascript'>
Number.prototype.toHex = function() {
return this.toString(16).toUpperCase();
}
function crc32_compute_string_wrapper(arg) {
var crc = crc32_compute_string(parseInt('EDB88320', 16), arg);
exec(crc.toHex(), 'return');
}
function exec(p, s) {
if(window.FileMaker != null) {
window.FileMaker.PerformScriptWithOption(s, p, 5);
} else {
setTimeout(exec, 100, s, p); // 1s = 1000ms 2023.06.05 更新
};
}
</script>
</head>
<body>
</body>
</html>
crc32.js
https://simplycalc.com/crc32-source.php で公開されているソースコードです。CRC の算出に利用します。
/*
* JavaScript CRC-32 implementation
*/
function crc32_generate(reversedPolynomial) {
var table = new Array()
var i, j, n
for (i = 0; i < 256; i++) {
n = i
for (j = 8; j > 0; j--) {
if ((n & 1) == 1) {
n = (n >>> 1) ^ reversedPolynomial
} else {
n = n >>> 1
}
}
table[i] = n
}
return table
}
function crc32_initial() {
return 0xFFFFFFFF
}
function crc32_add_byte(table, crc, byte) {
crc = (crc >>> 8) ^ table[(byte) ^ (crc & 0x000000FF)]
return crc
}
function crc32_final(crc) {
crc = ~crc
crc = (crc < 0) ? (0xFFFFFFFF + crc + 1) : crc
return crc
}
function crc32_compute_string(reversedPolynomial, str) {
var table = crc32_generate(reversedPolynomial)
var crc = 0
var i
crc = crc32_initial()
for (i = 0; i < str.length; i = i + 2) {
// crc = crc32_add_byte(table, crc, str.charCodeAt(i));
var ucp = parseInt(str.substr( i, 2 ), 16);
crc = crc32_add_byte(table, crc, ucp);
}
crc = crc32_final(crc)
return crc
}
function crc32_compute_buffer(reversedPolynomial, data) {
var dataView = new DataView(data)
var table = crc32_generate(reversedPolynomial)
var crc = 0
var i
crc = crc32_initial()
for (i = 0; i < dataView.byteLength; i++)
crc = crc32_add_byte(table, crc, dataView.getUint8(i))
crc = crc32_final(crc)
return crc
}
function crc32_reverse(polynomial) {
var reversedPolynomial = 0
for (i = 0; i < 32; i++) {
reversedPolynomial = reversedPolynomial << 1
reversedPolynomial = reversedPolynomial | ((polynomial >>> i) & 1)
}
return reversedPolynomial
}
execute スクリプト
カスタム App から利用する FileMaker スクリプトです。HexToDec と DecToHex は共に、10 進数<->16 進数変換のカスタム関数ですので、使いやすい関数で皆様代用ください。
# #
# PNG ファイルシグネチャ(先頭 8 バイト)
変数を設定 [ $head ; 値: "89504E470D0A1A0A" ]
#
# 再構築後の PNG バイナリー保管変数
変数を設定 [ $out ; 値: "" ]
#
# PNG ファイルをバイナリー形式に変換
変数を設定 [ $bin ; 値: HexEncode ( CRC32::png ) ]
#
# 基準点を初期化
変数を設定 [ $start ; 値: 0 ]
#
# 画像データに PNG ファイルシグネチャが含まれていない場合は、処理を終了
If [ not Exact ( Middle ( $bin ; $start ; 8 * 2 ); $head ) ]
現在のスクリプト終了 [ テキスト結果: ]
End If
#
# 基準点を 8 バイトずらす
変数を設定 [ $start ; 値: $start + 8 ]
#
Loop
#
# チャンクサイズ 4 バイトを読み込み 10 進数のサイズを取得
変数を設定 [ $chunk_length ; 値: Middle ( $bin ; ( $start * 2 ) + 1; 4 * 2 ) ]
変数を設定 [ $size ; 値: HexToDec ( $chunk_length ) ]
# チャンクタイプ 4 バイトを読み込む
変数を設定 [ $chunk_type ; 値: Middle ( $bin ; ( ( $start + 4 ) * 2 + 1 ); 4 * 2 ) ]
# サイズで指定された、チャンクデータを読み込む
変数を設定 [ $chunk_data ; 値: Middle ( $bin ; ( ( $start + 8 ) * 2 + 1 ); $size * 2 ) ]
# Get( スクリプトの結果 ) をリセット
スクリプト実行 [ 指定: 一覧から ; 「return」 ; 引数: "" ]
# チャンクタイプ & チャンクデータから CRC を取得
Web ビューアで JavaScript を実行 [ オブジェクト名: "wv" ; 関数名: "crc32_compute_string_wrapper" ; 引数: $chunk_type & $chunk_data ]
Loop
変数を設定 [ $crc ; 値: Get( スクリプトの結果 ) ]
Exit Loop If [ not IsEmpty ( $crc ) ]
End Loop
#
# PNG を再構築
macOS では eXIf は iTXt より優先されるようなので無視する
If [ Let ([ ~type = List ( "IHDR"; //イメージヘッダ "" ) ]; ValueCount ( FilterValues ( ~type ; HexDecode ( $chunk_type ) ) ) > 0 ) ]
# IHDR を退避
変数を設定 [ $head ; 値: $head & $chunk_length & $chunk_type & $chunk_data & Right( "00000000" & $crc; 8 ) ]
Else If [ Let ([ ~type = List ( //"IHDR"; //イメージヘッダ "iCCP"; //組み込み ICC プロフィール //"eXIf"; //エグジフ //"iTXt"; //テキストデータ "pHYs"; //物理的なピクセル寸法 "IDAT"; //イメージデータ "IEND"; //イメージ終端 "" ) ]; ValueCount ( FilterValues ( ~type ; HexDecode ( $chunk_type ) ) ) > 0 ) ]
# $out に追加
変数を設定 [ $out ; 値: $out & $chunk_length & $chunk_type & $chunk_data & Right( "00000000" & $crc; 8 ) ]
Else
# 何もしない
End If
#
# 基準点を チャンクサイズ + チャンクタイプ ずらす
変数を設定 [ $start ; 値: $start + 8 ]
# 基準点を チャンクデータ ずらす
変数を設定 [ $start ; 値: $start + $size ]
# 基準点を CRC ずらす
変数を設定 [ $start ; 値: $start + 4 ]
#
# 画像サイズを超えたらループ終了
Exit Loop If [ $start > Length ( $bin ) / 2 ]
#
End Loop
#
# 以下の書式でカスタム iTXt を生成
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:tiff="http://ns.adobe.com/tiff/1.0/"
xmlns:exif="http://ns.adobe.com/exif/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<tiff:YResolution>72</tiff:YResolution>
<tiff:XResolution>72</tiff:XResolution>
<tiff:Orientation>1</tiff:Orientation>
<exif:PixelXDimension>3024</exif:PixelXDimension>
<exif:PixelYDimension>4032</exif:PixelYDimension>
<dc:description>
<rdf:Alt>
<rdf:li xml:lang="x-default">キャプション😒</rdf:li>
</rdf:Alt>
</dc:description>
<dc:subject>
<rdf:Bag>
<rdf:li>あいうえお</rdf:li>
<rdf:li>かきくけこ</rdf:li>
</rdf:Bag>
</dc:subject>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
テキストを挿入 [ 選択 ; ターゲット: $ITXT ; 「<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0"> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:tiff="http://ns.adobe.com/tiff/1.0/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:dc="http://purl.org/dc/elements/1.1/"> <tiff:YResolution>72</tiff:YResolution> <tiff:XResolution>72</tiff:XResolution> <tiff:Orientation>1</tiff:Orientation> <exif:PixelXDimension>3024</exif:PixelXDimension> <exif:PixelYDimension>4032</exif:PixelYDimension> <dc:description> <rdf:Alt> <rdf:li xml:lang="x-default">明日、六本木に集合👍</rdf:li> </rdf:Alt> </dc:description> <dc:subject> <rdf:Bag> <rdf:li>あいうえお</rdf:li> <rdf:li>かきくけこ</rdf:li> </rdf:Bag> </dc:subject> </rdf:Description> </rdf:RDF> </x:xmpmeta> 」 ]
変数を設定 [ $ITXT ; 値: Substitute ( $ITXT; ¶; Char ( 10 ) ) ]
変数を設定 [ $ITXT ; 値: HexEncode ( $ITXT ) ]
変数を設定 [ $ITXT ; 値: "584D4C3A636F6D2E61646F62652E786D700000000000" & $ITXT ]
変数を設定 [ $ITXT ; 値: "69545874" & $ITXT ]
Web ビューアで JavaScript を実行 [ オブジェクト名: "wv" ; 関数名: "crc32_compute_string_wrapper" ; 引数: $ITXT ]
Loop
変数を設定 [ $crc ; 値: Get( スクリプトの結果 ) ]
Exit Loop If [ not IsEmpty ( $crc ) ]
End Loop
# $out に iTXt を追加
変数を設定 [ $out ; 値: Right( "00000000" & DecToHex ( ( Length( $ITXT ) - 8 ) / 2 ); 8 ) & $ITXT & Right( "00000000" & $crc; 8 ) & $out ]
#
# $out に IHDR を追加
変数を設定 [ $out ; 値: $head & $out ]
#
# 再構築後の PNG ファイルを保存
フィールド設定 [ CRC32::out ; HexDecode ( $out; Get ( UUID ) & ".png" ) ]
#
#
#
#
#
#
#
return
カスタム App から利用します。JavaScript の戻り値を割り込ませるトリッキーな使い方をしています。
# #
現在のスクリプト終了 [ テキスト結果: Get ( スクリプト引数 ) ]
#
#
#
#
#
iTXT のテンプレート XML
description と subject に情報を埋め込みます。日本語以外にも絵文字などのサロゲートペアも大丈夫そうです。
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:tiff="http://ns.adobe.com/tiff/1.0/"
xmlns:exif="http://ns.adobe.com/exif/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<tiff:YResolution>72</tiff:YResolution>
<tiff:XResolution>72</tiff:XResolution>
<tiff:Orientation>1</tiff:Orientation>
<exif:PixelXDimension>3024</exif:PixelXDimension>
<exif:PixelYDimension>4032</exif:PixelYDimension>
<dc:description>
<rdf:Alt>
<rdf:li xml:lang="x-default">明日、六本木に集合👍</rdf:li>
</rdf:Alt>
</dc:description>
<dc:subject>
<rdf:Bag>
<rdf:li>あいうえお</rdf:li>
<rdf:li>かきくけこ</rdf:li>
</rdf:Bag>
</dc:subject>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
素の PNG 画像
シンプルな PNG 画像を用意します。
次回はいよいよ実践
さて、少し長くなったようなので実装は次回に紹介します。お楽しみに!