テキストをPNGに

檸檬

黒白2色のQRコードをフルカラー(1677万7216色)にしたら、1677万÷2で800万倍以上のデータを格納できるという笑い話がありました。完全に1677万色を区別できたとしても、そんなに大量のデータを格納できるわけはなく、log216777216 = 24 倍にしかなりません。そもそもフルカラー画像は1ピクセルあたりRGB 8ビットずつ、24ビットのデータ量しかありません。224 = 16777216 だから1677万色が表せるのです。

それはそうと、フルカラー画像なら1ピクセルあたり3バイトのデータを格納できるので、これを使えば小さな画像にちょっとした小説を入れることができそうです。

この発想が小飼弾さんの UTF-PNG (aka Unicolor) なのですが、UTF-PNGは1ピクセルにUTF-8の1文字を入れたのに対して、私は単純にUTF-8テキストをバイト列に直して3バイトずつを1ピクセルにすることにしました。

テキストをPNGにして保存する関数と、PNGファイルを読んでテキストにする関数です(ChatGPTに手伝ってもらいました):

import numpy as np
from PIL import Image

def text2png(text, png_path):
    byte_string = text.encode("utf-8")
    area = (len(byte_string) + 2) // 3
    width = min(int(np.sqrt(area)), 1024)
    height = (area + width - 1) // width
    byte_string += b"\x00" * (width * height * 3 - len(byte_string))
    image_array = np.frombuffer(byte_string, dtype=np.uint8).reshape((height, width, 3))
    image = Image.fromarray(image_array, "RGB")
    image.save(png_path)

def png2text(png_path):
    image = Image.open(png_path)
    byte_string = np.array(image).flatten().tobytes()
    return byte_string.decode("utf-8").rstrip("\x00")

梶井基次郎の檸檬という小説のPNG画像が冒頭の図です。これは次のようにして作りました:

text2png(r"""檸檬
梶井基次郎

 えたいの知れない不吉な塊が(以下略)
""", "lemon.png")

「檸檬」(16499バイト)が10255バイトの lemon.png になりました。

テキストは普通に "..." で与えてもいいのですが、r"""...""" とすると途中に改行やバックスラッシュで始まる特殊文字が入っていてもうまくいきます。

元に戻すには、この画像をダウンロードして、次のようにします:

print(png2text("lemon.png"))

ちなみに、1ピクセル3バイトと書きましたが、アルファチャンネル(透明度)を追加すれば4バイトになります。また、既存画像のピクセルの末位ビットをいじっても画像の見え方はあまり変化しませんので、アルファチャンネルを含めた既存画像に秘密の情報を混ぜ込むこともできそうです(ステガノグラフィ)。ChatGPTに手伝ってもらえば、いろいろ楽しいプログラミングができそうです。