Monalog

3135人のHaskell好きが訪れました

OGP画像をHaskellで作ろう:その2

2026/03/10

GIFでOGP画像を作ろう。PNGも検討したがGIFの方が簡単そうだ。簡単そうだというのは調べずに言っている。

若い人であればWebの画像といえばPNGやWebPだと言うが老人の僕はGIFだ。そしてGIFといえば特許騒動が懐かしい。GIFにはファイルサイズを小さくするために圧縮アルゴリズムLZW(Lempel-Ziv-Welch)が採用されている。インターネット黎明期、GIFは広く使われていたが、突如LZWの特許を持っているUnisys社がGIF画像編集ソフトウェアにライセンス料を課すると発表した騒動だ。これに反発してPNGが作られたのも懐かしい。なお現在は特許の有効期限は切れている。

このLZWは、GIFをライブラリを使わずにバイナリから組み立てるのにも障壁になる。ようは実装が面倒くさい。そこで特許問題時に発明された「無圧縮GIF」という抜け道を使う。これはGIFを使いたいがLZWを使わないことで特許を迂回する方法だ。これであればドット絵の見た目そのままにバイナリデータを作れる。無圧縮GIFの作り方はここでは説明しない、検索すると出てくるので興味がある人は調べて実装してみてほしい。

GIFバイナリのフォーマットはLZWを無視すれば結構シンプル。構造さえ分かればバイナリエディタでドット絵が作れな気分になる。

# 2x2 のGIF画像バイナリ例
Header & LSD:
47 49 46 38 39 61  (GIF89a)
02 00 02 00        (Width: 2, Height: 2)
80                 (GCT Flag=1, Color Res=1, Sort=0, Size=0 -> 2 colors)
00 00              (BG Color Index=0, Aspect Ratio=0)

Global Color Table (GCT):
00 00 00           (Color 0: Black)
FF FF FF           (Color 1: White)

Image Descriptor:
2C                 (Separator ',')
00 00 00 00        (Left: 0, Top: 0)
02 00 02 00        (Width: 2, Height: 2)
00                 (Local Color Table Flag=0)

Image Data:
07                 (LZW Minimum Code Size = 7)
06                 (サブブロックのバイト数: 6バイト)
80                 (Clear Code: 128)
00                 (Pixel 1: Black)
01                 (Pixel 2: White)
01                 (Pixel 3: White)
00                 (Pixel 4: Black)
81                 (End of Information Code: 129)
00                 (ブロック終端)

Trailer:
3B                 (Trailer ';')

画像サイズやカラーテーブルを定義したら、あとは1ドット = 1byteでデータを置いていくだけ。もしデータが大きくなってきたらデータ分割が必要になる。126byteごとにクリアコードの 0x80 をはさまないといけないのと、255byteごとに 0xFF をいれる感じ。

Haskellでバイナリを出力

前置きが長くなったがHaskellでGIFを出力してみる。

といっても難しいことはなにもない。単にサーバでレスポンスヘッダ Content-Type: image/gif を出力したら ByteString 型のバイナリデータを返せばそれでブラウザはGIFとして表示してくれる。

おそらく https://monalog.nanabit.dev/ogp/sample このURLにアクセスすると画像が表示されるはずだ。

今回はバイナリデータの組み立て方は紹介しないが次回、データの拡大計算処理とからめて記事をかけたらと思っている。

関数名の衝突

take を使ったところエラーが出た。Ambiguous occurrence take ということで同名がimportされてて、どの take か分からないよとのこと。僕は import Data.List as LData.ByteString as B と丁寧にエイリアスを付けていたので「なんでやねん!」という気持ちを抱えながら Prelude.take とか書いて誤魔化していた。

でも改めて調べてみたら import Data.List as L ではだめで qualified を付けて import Data.List qualified as L としないと意味がないことが分かった。qualified がないとエイリアスなしでも全部importされてしまう。つまり as L の意味はない。

戻る