diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-04-09 22:24:50 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-04-09 22:24:50 +0900 |
| commit | d1b4bc44170196a4dcef5254d092fc387e73792e (patch) | |
| tree | a6bbe0e61b73c99a815d730ae7956a5124200066 /vhosts/blog/public/posts/2023-04-01/implementation-of-minimal-png-image-encoder | |
| parent | bba1212ab46ed85c2ed3b646f2362bdbb1f45b63 (diff) | |
| parent | 4f46d262e6967c9c638b40f3b0246d21b7a9b9dc (diff) | |
| download | nsfisis.dev-d1b4bc44170196a4dcef5254d092fc387e73792e.tar.gz nsfisis.dev-d1b4bc44170196a4dcef5254d092fc387e73792e.tar.zst nsfisis.dev-d1b4bc44170196a4dcef5254d092fc387e73792e.zip | |
Merge branch 'nuldoc-djot'
Diffstat (limited to 'vhosts/blog/public/posts/2023-04-01/implementation-of-minimal-png-image-encoder')
| -rw-r--r-- | vhosts/blog/public/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html | 190 |
1 files changed, 65 insertions, 125 deletions
diff --git a/vhosts/blog/public/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html b/vhosts/blog/public/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html index 11831bdf..502a7e3a 100644 --- a/vhosts/blog/public/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html +++ b/vhosts/blog/public/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html @@ -52,51 +52,42 @@ </ol> </section> <section id="section--intro"> - <h2><a href="#section--intro">はじめに</a></h2> + <h2><a href="#section--intro">はじめに</a></h2> <p> - この記事では、PNG 画像として valid な範囲で最大限手抜きしたエンコーダを書く。PNG 画像に対応したビューアであれば読み込めるが、圧縮効率については一切考えない。また、実装には Go 言語を使うが、Go の標準ライブラリにあるさまざまなアルゴリズム (PNG 画像に関係する範囲だと、zlib や CRC32、Adler-32 など) は使わない。 + この記事では、PNG 画像として valid な範囲で最大限手抜きしたエンコーダを書く。 PNG 画像に対応したビューアであれば読み込めるが、圧縮効率については一切考えない。 また、実装には Go 言語を使うが、Go の標準ライブラリにあるさまざまなアルゴリズム (PNG 画像に関係する範囲だと、zlib や CRC32、Adler-32 など) は使わない。 </p> </section> - <section id="section--basic-structure-of-png"> - <h2><a href="#section--basic-structure-of-png">PNG ファイルの基本構造</a></h2> + <h2><a href="#section--basic-structure-of-png">PNG ファイルの基本構造</a></h2> <p> - PNG ファイルの基本構造は次のようになっている。 + PNG ファイルの基本構造は次のようになっている。 </p> - <ol> <li> PNG signature </li> - <li> IHDR chunk </li> - <li> 任意個の chunk </li> - <li> IEND chunk </li> </ol> - <p> - Chunk には画像データを入れる IDAT chunk、パレットデータを入れる PLTE chunk、テキストデータを入れる tEXt chunk などがあるが、今回は最小構成ということで IDAT chunk (と IHDR chunk と IEND chunk) のみを用いる。 + Chunk には画像データを入れる IDAT chunk、パレットデータを入れる PLTE chunk、テキストデータを入れる tEXt chunk などがあるが、 今回は最小構成ということで IDAT chunk (と IHDR chunk と IEND chunk) のみを用いる。 </p> - <p> - 次節で、それぞれの具体的な構造を確認しつつ実装していく。 + 次節で、それぞれの具体的な構造を確認しつつ実装していく。 </p> </section> - <section id="section--implement-png-encoder"> - <h2><a href="#section--implement-png-encoder">PNG のエンコーダを実装する</a></h2> + <h2><a href="#section--implement-png-encoder">PNG のエンコーダを実装する</a></h2> <p> - 以下のソースコードをベースにする。今回 PNG のデコーダは扱わないので、読み込みには Go の標準ライブラリ <code>image/png</code> を用いる。 + 以下のソースコードをベースにする。 今回 PNG のデコーダは扱わないので、読み込みには Go の標準ライブラリ <code>image/png</code> を用いる。 </p> - <div class="codeblock" language="go"> <pre class="shiki github-light" style="background-color:#f5f5f5;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">package</span><span style="color:#6F42C1"> main</span></span> <span class="line"></span> @@ -137,59 +128,46 @@ <span class="line"><span style="color:#6F42C1"> writeChunkIend</span><span style="color:#24292E">(w)</span></span> <span class="line"><span style="color:#24292E">}</span></span></code></pre> </div> - <p> - 以降は、<code>writeSignature</code> や <code>writeChunkIhdr</code> などを実装していく。 + 以降は、<code>writeSignature</code> や <code>writeChunkIhdr</code> などを実装していく。 </p> - <section id="section--implement-png-encoder--png-signature"> - <h3><a href="#section--implement-png-encoder--png-signature">PNG signature</a></h3> + <h3><a href="#section--implement-png-encoder--png-signature">PNG signature</a></h3> <p> - PNG signature は、PNG 画像の先頭に固定で付与されるバイト列で、8 バイトからなる。 + PNG signature は、PNG 画像の先頭に固定で付与されるバイト列で、8 バイトからなる。 </p> - <ol> <li> 0x89 </li> - <li> 0x50 (ASCII コードで「P」) </li> - <li> 0x4E (ASCII コードで「N」) </li> - <li> 0x47 (ASCII コードで「G」) </li> - <li> 0x0D (ASCII コードで CR) </li> - <li> 0x0A (ASCII コードで LF) </li> - <li> 0x1A (ASCII コードで EOF) </li> - <li> 0x0A (ASCII コードで LF) </li> </ol> - <p> - CRLF や LF は、送信中に改行コードの変換が誤っておこなわれていないかどうかを検知するのに使われる。 + CRLF や LF は、送信中に改行コードの変換が誤っておこなわれていないかどうかを検知するのに使われる。 </p> - <p> - <code>writeSignature</code> の実装はこちら: + <code>writeSignature</code> の実装はこちら: </p> - <div class="codeblock" language="go"> <pre class="shiki github-light" style="background-color:#f5f5f5;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">import</span><span style="color:#032F62"> "</span><span style="color:#6F42C1">encoding/binary</span><span style="color:#032F62">"</span></span> <span class="line"></span> @@ -207,40 +185,32 @@ <span class="line"><span style="color:#24292E"> binary.</span><span style="color:#6F42C1">Write</span><span style="color:#24292E">(w, binary.BigEndian, sig)</span></span> <span class="line"><span style="color:#24292E">}</span></span></code></pre> </div> - <p> <code>encoding/binary</code> パッケージの <code>binary.Write</code> を使い、固定の 8 バイトを書き込む。 </p> </section> - <section id="section--implement-png-encoder--structure-of-chunk"> - <h3><a href="#section--implement-png-encoder--structure-of-chunk">Chunk の構造</a></h3> + <h3><a href="#section--implement-png-encoder--structure-of-chunk">Chunk の構造</a></h3> <p> - IHDR chunk に進む前に、chunk 一般の構造を確認する。 + IHDR chunk に進む前に、chunk 一般の構造を確認する。 </p> - <ol> <li> Length: chunk data のバイト長 (符号なし 4 バイト整数) </li> - <li> Chunk type: chunk の種類を示す 4 バイトからなる名前 </li> - <li> Chunk data: 実際のデータ。0 バイトでもよい </li> - <li> CRC: chunk type と chunk data の CRC (符号なし 4 バイト整数) </li> </ol> - <p> - CRC (Cyclic Redundancy Check) は誤り検出符号の一種。Go 言語では <code>hash/crc32</code> パッケージにあるが、今回はこれも自前で実装する。PNG の仕様書に C 言語のサンプルコードが載っている (<a href="https://www.w3.org/TR/png/#D-CRCAppendix" rel="noreferrer" target="_blank">D. Sample CRC implementation</a>) ので、これを Go に移植する。 + CRC (Cyclic Redundancy Check) は誤り検出符号の一種。Go 言語では <code>hash/crc32</code> パッケージにあるが、今回はこれも自前で実装する。PNG の仕様書に C 言語のサンプルコードが載っている ( <a href="https://www.w3.org/TR/png/#D-CRCAppendix" rel="noreferrer" target="_blank">D. Sample CRC implementation</a> ) ので、これを Go に移植する。 </p> - <div class="codeblock" language="go"> <pre class="shiki github-light" style="background-color:#f5f5f5;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">var</span><span style="color:#24292E"> (</span></span> <span class="line"><span style="color:#24292E"> crcTable [</span><span style="color:#005CC5">256</span><span style="color:#24292E">]</span><span style="color:#D73A49">uint32</span></span> @@ -278,11 +248,9 @@ <span class="line"><span style="color:#D73A49"> return</span><span style="color:#6F42C1"> updateCrc</span><span style="color:#24292E">(</span><span style="color:#D73A49">0x</span><span style="color:#005CC5">FFFFFFFF</span><span style="color:#24292E">, buf) </span><span style="color:#D73A49">^</span><span style="color:#D73A49"> 0x</span><span style="color:#005CC5">FFFFFFFF</span></span> <span class="line"><span style="color:#24292E">}</span></span></code></pre> </div> - <p> - できた <code>crc</code> 関数を使って、chunk 一般を書き込む関数も用意しておこう。 + できた <code>crc</code> 関数を使って、chunk 一般を書き込む関数も用意しておこう。 </p> - <div class="codeblock" language="go"> <pre class="shiki github-light" style="background-color:#f5f5f5;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">func</span><span style="color:#6F42C1"> writeChunk</span><span style="color:#24292E">(</span><span style="color:#E36209">w</span><span style="color:#6F42C1"> io</span><span style="color:#24292E">.</span><span style="color:#6F42C1">Writer</span><span style="color:#24292E">, </span><span style="color:#E36209">chunkType</span><span style="color:#D73A49"> string</span><span style="color:#24292E">, </span><span style="color:#E36209">data</span><span style="color:#24292E"> []</span><span style="color:#D73A49">byte</span><span style="color:#24292E">) {</span></span> <span class="line"><span style="color:#24292E"> typeAndData </span><span style="color:#D73A49">:=</span><span style="color:#6F42C1"> make</span><span style="color:#24292E">([]</span><span style="color:#D73A49">byte</span><span style="color:#24292E">, </span><span style="color:#005CC5">0</span><span style="color:#24292E">, </span><span style="color:#6F42C1">len</span><span style="color:#24292E">(chunkType)</span><span style="color:#D73A49">+</span><span style="color:#6F42C1">len</span><span style="color:#24292E">(data))</span></span> @@ -294,91 +262,91 @@ <span class="line"><span style="color:#24292E"> binary.</span><span style="color:#6F42C1">Write</span><span style="color:#24292E">(w, binary.BigEndian, </span><span style="color:#6F42C1">crc</span><span style="color:#24292E">(typeAndData))</span></span> <span class="line"><span style="color:#24292E">}</span></span></code></pre> </div> - <p> - 仕様どおり、<code>chunkType</code> と <code>data</code> から CRC を計算し、<code>data</code> の長さと合わせて書き込んでいる。PNG では基本的に big endian を使うことに注意する。 + 仕様どおり、<code>chunkType</code> と <code>data</code> から CRC を計算し、<code>data</code> の長さと合わせて書き込んでいる。 PNG では基本的に big endian を使うことに注意する。 </p> - <p> - 準備ができたところで、具体的な chunk をエンコードしていく。 + 準備ができたところで、具体的な chunk をエンコードしていく。 </p> </section> - <section id="section--implement-png-encoder--ihdr-chunk"> - <h3><a href="#section--implement-png-encoder--ihdr-chunk">IHDR chunk</a></h3> + <h3><a href="#section--implement-png-encoder--ihdr-chunk">IHDR chunk</a></h3> <p> - IHDR chunk は最初に配置される chunk である。次のようなデータからなる。 + IHDR chunk は最初に配置される chunk である。次のようなデータからなる。 </p> - <ol> <li> 画像の幅 (符号なし 4 バイト整数) </li> - <li> 画像の高さ (符号なし 4 バイト整数) </li> - <li> - ビット深度 (符号なし 1 バイト整数) + <p> + ビット深度 (符号なし 1 バイト整数) + </p> <ul> <li> 1 色に使うビット数。1 ピクセルに 24 bit 使う truecolor 画像では 8 になる </li> </ul> </li> - <li> - 色タイプ (符号なし 1 バイト整数) + <p> + 色タイプ (符号なし 1 バイト整数) + </p> <ul> <li> 0: グレースケール </li> - <li> 2: Truecolor (今回はこれに決め打ち) </li> - <li> 3: パレットのインデックス </li> - <li> 4: グレースケール + アルファ </li> - <li> 6: Truecolor + アルファ </li> </ul> </li> - <li> - 圧縮方式 (符号なし 1 バイト整数) + <p> + 圧縮方式 (符号なし 1 バイト整数) + </p> <ul> - PNG の仕様書に 0 しか定義されていないので 0 で固定 + <li> + PNG の仕様書に 0 しか定義されていないので 0 で固定 + </li> </ul> </li> - <li> - フィルタ方式 (符号なし 1 バイト整数) + <p> + フィルタ方式 (符号なし 1 バイト整数) + </p> <ul> - PNG の仕様書に 0 しか定義されていないので 0 で固定 + <li> + PNG の仕様書に 0 しか定義されていないので 0 で固定 + </li> </ul> </li> - <li> - インターレース方式 (符号なし 1 バイト整数) + <p> + インターレース方式 (符号なし 1 バイト整数) + </p> <ul> - 今回はインターレースしないので 0 + <li> + 今回はインターレースしないので 0 + </li> </ul> </li> </ol> - <p> - 今回ほとんどのデータは決め打ちするので、データに応じて変わるのは width と height だけになる。コードは次のようになる。 + 今回ほとんどのデータは決め打ちするので、データに応じて変わるのは width と height だけになる。コードは次のようになる。 </p> - <div class="codeblock" language="go"> <pre class="shiki github-light" style="background-color:#f5f5f5;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">import</span><span style="color:#032F62"> "</span><span style="color:#6F42C1">bytes</span><span style="color:#032F62">"</span></span> <span class="line"></span> @@ -396,45 +364,36 @@ <span class="line"><span style="color:#24292E">}</span></span></code></pre> </div> </section> - <section id="section--implement-png-encoder--idat-chunk"> - <h3><a href="#section--implement-png-encoder--idat-chunk">IDAT chunk</a></h3> + <h3><a href="#section--implement-png-encoder--idat-chunk">IDAT chunk</a></h3> <p> - IDAT chunk は、実際の画像データが格納された chunk である。IDAT chunk は deflate アルゴリズムにより圧縮され、zlib 形式で格納される。 + IDAT chunk は、実際の画像データが格納された chunk である。IDAT chunk は deflate アルゴリズムにより圧縮され、zlib 形式で格納される。 </p> - <section id="section--implement-png-encoder--idat-chunk--zlib"> - <h4><a href="#section--implement-png-encoder--idat-chunk--zlib">Zlib</a></h4> + <h4><a href="#section--implement-png-encoder--idat-chunk--zlib">Zlib</a></h4> <p> - まずは zlib について確認する。おおよそ次のような構造になっている。 + まずは zlib について確認する。おおよそ次のような構造になっている。 </p> - <ol> <li> 固定で 0x78 (符号なし 1 バイト整数) </li> - <li> 固定で 0x01 (符号なし 1 バイト整数) </li> - <li> データ </li> - <li> データの Adler-32 </li> </ol> - <p> - 最初の 2 バイトにも意味はあるが、PNG では固定で構わない。 + 最初の 2 バイトにも意味はあるが、PNG では固定で構わない。 </p> - <p> - Adler-32 も CRC と同じく誤り検出符号である。こちらも zlib の仕様書に C 言語でサンプルコードが記載されている (<a href="https://www.rfc-editor.org/rfc/rfc1950#section-9" rel="noreferrer" target="_blank">9. Appendix: Sample code</a>) ので、Go に移植する。 + Adler-32 も CRC と同じく誤り検出符号である。こちらも zlib の仕様書に C 言語でサンプルコードが記載されている ( <a href="https://www.rfc-editor.org/rfc/rfc1950#section-9" rel="noreferrer" target="_blank">9. Appendix: Sample code</a> ) ので、Go に移植する。 </p> - <div class="codeblock" language="go"> <pre class="shiki github-light" style="background-color:#f5f5f5;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">const</span><span style="color:#005CC5"> adler32Base</span><span style="color:#D73A49"> =</span><span style="color:#005CC5"> 65521</span></span> <span class="line"></span> @@ -453,37 +412,29 @@ <span class="line"><span style="color:#D73A49"> return</span><span style="color:#6F42C1"> updateAdler32</span><span style="color:#24292E">(</span><span style="color:#005CC5">1</span><span style="color:#24292E">, buf)</span></span> <span class="line"><span style="color:#24292E">}</span></span></code></pre> </div> - <p> - 「データ」の部分には圧縮したデータが入るのだが、真面目に deflate アルゴリズムを実装する必要はない。Zlib には無圧縮のデータブロックを格納することができるので、これを使う。本来は、データの圧縮効率の悪いランダムなデータをそのまま格納するためのものだが、今回は deflate の実装をサボるために使う。 + 「データ」の部分には圧縮したデータが入るのだが、真面目に deflate アルゴリズムを実装する必要はない。Zlib には無圧縮のデータブロックを格納することができるので、これを使う。本来は、データの圧縮効率の悪いランダムなデータをそのまま格納するためのものだが、今回は deflate の実装をサボるために使う。 </p> - <p> - 1 つの無圧縮ブロックには 65535 (2<sup>16</sup> - 1) バイトまで格納できる。それぞれのブロックは次のような構成になっている。 + 1 つの無圧縮ブロックには 65535 (2<sup>16</sup> - 1) バイトまで格納できる。それぞれのブロックは次のような構成になっている。 </p> - <ol> <li> 最終ブロックなら 1、そうでなければ 0 (符号なし 1 バイト整数) </li> - <li> ブロックのバイト長 (符号なし 2 バイト整数) </li> - <li> ブロックのバイト長の 1 の補数、あるいはビット反転 (符号なし 2 バイト整数) </li> - <li> データ (最大 65535 バイト) </li> </ol> - <p> - 実際にこの手抜き zlib を実装したものがこちら: + 実際にこの手抜き zlib を実装したものがこちら: </p> - <div class="codeblock" language="go"> <pre class="shiki github-light" style="background-color:#f5f5f5;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">func</span><span style="color:#6F42C1"> encodeZlib</span><span style="color:#24292E">(</span><span style="color:#E36209">data</span><span style="color:#24292E"> []</span><span style="color:#D73A49">byte</span><span style="color:#24292E">) []</span><span style="color:#D73A49">byte</span><span style="color:#24292E"> {</span></span> <span class="line"><span style="color:#D73A49"> var</span><span style="color:#24292E"> buf </span><span style="color:#6F42C1">bytes</span><span style="color:#24292E">.</span><span style="color:#6F42C1">Buffer</span></span> @@ -511,21 +462,17 @@ <span class="line"><span style="color:#24292E">}</span></span></code></pre> </div> </section> - <section id="section--implement-png-encoder--idat-chunk--image-data"> - <h4><a href="#section--implement-png-encoder--idat-chunk--image-data">画像データ</a></h4> + <h4><a href="#section--implement-png-encoder--idat-chunk--image-data">画像データ</a></h4> <p> - では次に、zlib 形式で格納するデータを用意する。PNG 画像は次のような順にスキャンする。画像の左上のピクセルから同じ行を横にスキャンしていき、一番右まで到達したら次の行の左に向かう。右下のピクセルまで行けば終わり。要は Z 字型に進んでいく。 + では次に、zlib 形式で格納するデータを用意する。PNG 画像は次のような順にスキャンする。 画像の左上のピクセルから同じ行を横にスキャンしていき、一番右まで到達したら次の行の左に向かう。 右下のピクセルまで行けば終わり。要は Z 字型に進んでいく。 </p> - <p> - また、それぞれの行の先頭には、圧縮のためのフィルタタイプを指定する。ただ、今回はその実装を省略するために、常にフィルタ 0 (何も加工しない) を使う。 + また、それぞれの行の先頭には、圧縮のためのフィルタタイプを指定する。 ただ、今回はその実装を省略するために、常にフィルタ 0 (何も加工しない) を使う。 </p> - <p> - 先ほどの <code>encodeZlib</code> も使って実際に実装したものがこちら: + 先ほどの <code>encodeZlib</code> も使って実際に実装したものがこちら: </p> - <div class="codeblock" language="go"> <pre class="shiki github-light" style="background-color:#f5f5f5;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">func</span><span style="color:#6F42C1"> writeChunkIdat</span><span style="color:#24292E">(</span><span style="color:#E36209">w</span><span style="color:#6F42C1"> io</span><span style="color:#24292E">.</span><span style="color:#6F42C1">Writer</span><span style="color:#24292E">, </span><span style="color:#E36209">width</span><span style="color:#24292E">, </span><span style="color:#E36209">height</span><span style="color:#D73A49"> uint32</span><span style="color:#24292E">, </span><span style="color:#E36209">img</span><span style="color:#6F42C1"> image</span><span style="color:#24292E">.</span><span style="color:#6F42C1">Image</span><span style="color:#24292E">) {</span></span> <span class="line"><span style="color:#D73A49"> var</span><span style="color:#24292E"> pixels </span><span style="color:#6F42C1">bytes</span><span style="color:#24292E">.</span><span style="color:#6F42C1">Buffer</span></span> @@ -544,17 +491,14 @@ </div> </section> </section> - <section id="section--implement-png-encoder--iend-chunk"> - <h3><a href="#section--implement-png-encoder--iend-chunk">IEND chunk</a></h3> + <h3><a href="#section--implement-png-encoder--iend-chunk">IEND chunk</a></h3> <p> - 最後に IEND chunk を書き込む。これは PNG 画像の最後に配置される chunk で、PNG のデコーダはこの chunk に出会うとそこでデコードを停止する。 + 最後に IEND chunk を書き込む。これは PNG 画像の最後に配置される chunk で、PNG のデコーダはこの chunk に出会うとそこでデコードを停止する。 </p> - <p> - 特に追加のデータはなく、必要なのは chunk type の <code>IEND</code> くらいなので実装は簡単: + 特に追加のデータはなく、必要なのは chunk type の <code>IEND</code> くらいなので実装は簡単: </p> - <div class="codeblock" language="go"> <pre class="shiki github-light" style="background-color:#f5f5f5;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">func</span><span style="color:#6F42C1"> writeChunkIend</span><span style="color:#24292E">(</span><span style="color:#E36209">w</span><span style="color:#6F42C1"> io</span><span style="color:#24292E">.</span><span style="color:#6F42C1">Writer</span><span style="color:#24292E">) {</span></span> <span class="line"><span style="color:#6F42C1"> writeChunk</span><span style="color:#24292E">(w, </span><span style="color:#032F62">"IEND"</span><span style="color:#24292E">, </span><span style="color:#005CC5">nil</span><span style="color:#24292E">)</span></span> @@ -562,13 +506,11 @@ </div> </section> </section> - <section id="section--outro"> - <h2><a href="#section--outro">おわりに</a></h2> + <h2><a href="#section--outro">おわりに</a></h2> <p> - 最後に全ソースコードを再掲しておく。 + 最後に全ソースコードを再掲しておく。 </p> - <div class="codeblock" language="go"> <pre class="shiki github-light" style="background-color:#f5f5f5;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">package</span><span style="color:#6F42C1"> main</span></span> <span class="line"></span> @@ -746,14 +688,12 @@ <span class="line"><span style="color:#24292E">}</span></span></code></pre> </div> </section> - <section id="section--references"> - <h2><a href="#section--references">参考</a></h2> + <h2><a href="#section--references">参考</a></h2> <ul> <li> <a href="https://www.w3.org/TR/png" rel="noreferrer" target="_blank">Portable Network Graphics (PNG) Specification (Third Edition)</a> </li> - <li> <a href="https://www.rfc-editor.org/rfc/rfc1950" rel="noreferrer" target="_blank">ZLIB Compressed Data Format Specification version 3.3</a> </li> |
