🖼️

PNGファイルにテキストを格納したり読み取ったりする

2024/09/27に公開

挨拶

こんにちは、T4D4です。今回はインターン先でPNGファイルにテキストを格納したり、テキストを読み取ったりする方法を調べることになり勉強がてらに簡単なWebアプリを作成したのでその事について書きます。

そもそもPNGファイルとは?

PNGファイル(Portable Network Graphics)は、画像ファイルの一種で、とくにWebページでよく使われるファイル形式です。
PNGファイルはまず先頭にPNGであることを示す8バイトの識別子があります。その後にチャンクと呼ばれるデータのまとまりが連なっています。チャンクにはいくつか種類があり、PNGを構成するのに必要な必須チャンクとオプションの補助チャンクが存在します。必須チャンクには、画像のサイズや圧縮方式が含まれるIHDRチャンク、画像データ本体が含まれるIDATチャンク等があります。補助チャンクには透過情報が含まれるtRNSチャンクや、タイムスタンプ情報を含むtIMEチャンク等があります。
肝心のテキストを格納できるチャンクはtEXtチャンク、zTXtチャンク、iTXtチャンクという3つのチャンクがあります。詳細はこちら↓
https://qiita.com/mikecat_mixc/items/0fb6a2a8e80263421253#テキスト情報
3つのチャンクの内、iTXtチャンクがtEXtチャンクのinternational版でUTF-8のテキストを格納可能なのでiTXtチャンクにテキストを格納していこうと思います。

どうやるか?

とりあえずnpmjs.comで「iTXt chunk」と検索したら2件ヒットし、両方確認したところ片方はタグにiTXtと入ってはいるものの、りどみには実装予定とだけ書かれており最終更新が1年前となっていました。なので実質他に選択肢がないのでpng-chunk-itxtというライブラリを使用することにしました。
npmのページによると、単独ではなく、png-chunks-extractpng-chunks-encodeという2つのライブラリと組み合わせて使うみたいです。
りどみを読み進めて行くと、シンプルな使い方がエンコードのやり方とデコードのやり方と1つづつ載っています。これらをコピペして少し弄ればすぐに動きそうですね。

他の技術選定

Webフレームワークには、今回はLogic blocks({#if: }みたいな書き方ができる機能)とかが好きなのでSveltekit(Svelte版のNext.jsみたいなヤツ)をチョイスしました。
ビルドツール(で良いのかな?)はvite、linterとformatterはbiome、IaasにはVercelを使いました。

Webアプリについての紹介

作成したWebアプリはこちらになります。
https://png-meta-rw.vercel.app/
使い方はHow to useの文におおよそ書いてあります。

コード解説

GitHubのリポジトリはこちらです:https://github.com/T4D4-IU/png-meta-rw
フロントエンドはpng-metadata-rw/src/routes/+page.svelte、バックエンドはpng-metadata-rw/src/routes/+page.server.tsにそれぞれすべて書かれています。

フロントエンドのフォームはSveltekit公式のドキュメントのこちらのページを参考にして書きました。
https://kit.svelte.jp/docs/form-actions#anatomy-of-an-action
あとはフォームの上にHow to use、下にデータが返ってきた時に表示させる部分を{#if: }を乱用して適当に書きました。

バックエンドも同様に公式のドキュメントを読んで書きました。大まかな流れとしてはフォームの送信を受け取った後、受け取ったデータを確認し、書き込みにも読み取りにも必要な変数を宣言し、テキスト入力の有無で分岐しています。後はテキスト入力がある場合はPNGのiTXtチャンクにテキストの書き込み、テキスト入力がない場合はiTXtチャンクからテキストの読み取りをしてそれぞれフロントにreturnを返しています。凄く雑な実装をしているのですでに書き込まれている場合を想定してませんでした

つまった部分

PNGファイルの扱いに結構苦労しました。

具体的には、最初はサンプルコードではconst buffer = fs.readFileSync("test.png");となっていたので、ローカルに保存してある画像を読み込まないといけない?と思い、フロントから受け取った画像をNode.jsのfsモジュールを使いsrc/assets配下に保存し、パスを保存しconst buffer = fs.readFileSync(${filepath});でbufferに格納するみたいなコトをしていました。ローカルでdevサーバーでライブラリを使えるか確かめるには十分でしたが、デプロイしようとするとVercelでは500番、CloudflarePagesではNode.jsのビルトイン機能が使えない為ビルド時エラーがそれぞれ出た為他の方法を考える必要が出ました。

次の策として画像の保存先をローカルからCloudflareR2に変更してみました。選定理由は低価格かつ、インターン先で使用しているS3と互換性のあるAPIを提供しているからです。
画像の保存は問題無くできました。しかし、フロントから画像のダウンロードができるようにしようとすると、ググるとどうやらR2バケットのサブドメインの公開が必要そうで、ドメインの取得が必要そうでした。また、CloudflarePagesにサイトをデプロイしようとすると、Node.jsのビルトイン機能が使えないコトについてはすでに書きましたが、fsなどの+page.server.tsで使用していたモノを置き換えてもそれら以外にiTXtチャンクへの操作に使用するライブラリの依存関係にも含まれていた為ビルド時エラーが以前として残っていました。流石にエッジコンピューティング対応のライブラリを自前で実装する技術力も暇もないのでCloudflareを使うのを諦めました。

最終的に、そもそもローカルなりDBなりに保存しなくても良いのでは?という結論に辿り着きました。😅
フロントから受け取ったファイルをローカルにもDBにも保存せず直接Bufferオブジェクトに変換し格納し、加工後のファイルも保存せずBufferオブジェクトに変換し、さらにtoString("base64)でエンコードしフロントに送信。そしてフロントでdata:image/png;base64でデコードするだけでした(紆余曲折した割にあっさりとした解決でした)

おわりに

PNGファイルにテキストを格納したり、格納されたテキストを読み取るnpmのパッケージの紹介と、それを利用して開発した簡単なサイトを紹介しました。
Verifiable Credentialsみたいなのを組み合わせる等、埋め込むテキストによってはおもしろい使い方が出来そうな気がします。
バグ報告や内容の指摘などがあれば記事コメントやTwitterのDMに気軽に連絡ください

参考資料

https://zenn.dev/knowledgework/articles/read-png-file
https://qiita.com/mikecat_mixc/items/0fb6a2a8e80263421253

Discussion