Go:通過 io.Writer 將 JPEG 轉(zhuǎn)為 JFIF
大家好,我是程序員幽鬼。
Go 的標準庫可讓你對 JPEG 圖像進行編碼。在 One of these JPEGs is not like the other[1] 一文中,Ben Cox 指出某些硬件不會解碼這些 JPEG 圖像,除非它們被增強為 JFIF 圖像。JFIF 代表“JPEG 文件交換格式”,在概念上是原始 JPEG 格式的次要版本。
硬件缺乏支持有點令人驚訝,因為 JPEG 是一種無處不在的文件格式。他 fork[2] 并 修復[3] 標準 image/jpeg 包以插入必要的 JFIF 字節(jié)。
01 JPEG Wire 格式
就網(wǎng)絡(luò)(或磁盤)上的字節(jié)而言,JPEG 由一系列連接在一起的塊組成。每個塊要么是一個裸標記(兩個字節(jié),以 開頭 0xff)要么是一個標記段(四個或更多字節(jié)是一個兩字節(jié)標記,同樣以 0xff 開頭,一個兩字節(jié)的長度,然后是一個額外的數(shù)據(jù)負載)。以下是 Wikipedia 的Example.jpg[4] 十六進制表示:
- $ wget --quiet https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg
- $ hd Example.jpg | head -n 5
- 00000000 ff d8 ff e0 00 10 4a 46 49 46 00 01 01 01 00 48 |......JFIF.....H|
- 00000010 00 48 00 00 ff e1 00 16 45 78 69 66 00 00 4d 4d |.H......Exif..MM|
- 00000020 00 2a 00 00 00 08 00 00 00 00 00 00 ff fe 00 17 |.*..............|
- 00000030 43 72 65 61 74 65 64 20 77 69 74 68 20 54 68 65 |Created with The|
- 00000040 20 47 49 4d 50 ff db 00 43 00 05 03 04 04 04 03 | GIMP...C.......|
在打開的 80 個字節(jié)標記:
- 一個 ff d8 SOI(圖像的開始)標記。
- 一個 ff e0 APP0 標記段;有效載荷以 “JFIF” 開頭。
- 一個 ff e1 APP1 標記段;有效載荷以 “Exif” 開頭。
- 一個 ff fe 注釋標記段,“Created 等等”。
- 一個 ff db DQT(定義量化表)標記段。
file 命令也認為這是 JFIF(帶 Exif),而不僅僅是 JPEG:
- $ file Example.jpg
- Example.jpg: JPEG image data, JFIF... Exif... baseline...
02 JFIF Wire 格式
JFIF 文件是一個 JPEG 文件,它的第二個塊(在作為第一個塊的 SOI 之后)是一個 APP0 塊,其有效載荷以 “JFIF” 開頭。一個有趣的點是 JFIF 和 EXIF 規(guī)范在技術(shù)上不兼容,因為它們都想占用第二塊(the second chunk):
- JFIF 規(guī)范[5]第 2 頁提到:“JPEG FIF APP0 標記必須緊跟在 SOI 標記之后”。
- EXIF 規(guī)范[6] 第 4.5.4 段提到:“APP1 是緊跟在 SOI 標記之后的”。
在實踐中,似乎 JFIF 'won' 和 EXIF 可以是第三個塊。
03 生成普通的舊 JPEG
這篇博文提供了不需要任何標準庫補丁(或 forks)的 Cox 方法的替代方法。與往常一樣,fork 具有從上游緩慢分叉的長期風險。Go 標準庫的上游補丁受制于“3 個月的新功能,3 個月的穩(wěn)定” 發(fā)布周期[7],并決定額外的 JFIF 塊是強制性的還是可選的(如果可選,API 應(yīng)該是什么,受兼容性限制[8])。
該方案的主要思想是 jpeg.Encode[9] 函數(shù)接受一個 io.Writer 參數(shù),并且很容易包裝 io.Writer 以在正確的位置插入 JFIF 字節(jié)。
首先,讓我們編寫一個簡單的程序來生成一張 1x1 JPEG 圖像。
- package main
- import (
- "image"
- "image/jpeg"
- "os"
- )
- func main() {
- m := image.NewGray(image.Rect(0, 0, 1, 1))
- if err := jpeg.Encode(os.Stdout, m, nil); err != nil {
- os.Stderr.WriteString(err.Error() + "\n")
- os.Exit(1)
- }
- }
運行它會生成一個 JPEG(但不是 JFIF)文件。
- $ go run from-jpeg-to-jfif.go > x
- $ hd x | head -n 5
- 00000000 ff d8 ff db 00 84 00 08 06 06 07 06 05 08 07 07 |................|
- 00000010 07 09 09 08 0a 0c 14 0d 0c 0b 0b 0c 19 12 13 0f |................|
- 00000020 14 1d 1a 1f 1e 1d 1a 1c 1c 20 24 2e 27 20 22 2c |......... $.' ",|
- 00000030 23 1c 1c 28 37 29 2c 30 31 34 34 34 1f 27 39 3d |#..(7),01444.'9=|
- 00000040 38 32 3c 2e 33 34 32 01 09 09 09 0c 0b 0c 18 0d |82<.342.........|
- $ file x
- x: JPEG image data, baseline, precision 8, 1x1, components 1
04 一個 JFIFifying Writer
我們編寫一個 jfifEncode 函數(shù),它可以直接替代 jpeg.Encode 但添加額外的 JFIF 字節(jié),只要第二個標記(緊接在 SOI 之后的那個)不是 APP0。
- package main
- import (
- "errors"
- "image"
- "image/jpeg"
- "io"
- "os"
- )
- func main() {
- m := image.NewGray(image.Rect(0, 0, 1, 1))
- if err := jfifEncode(os.Stdout, m, nil); err != nil {
- os.Stderr.WriteString(err.Error() + "\n")
- os.Exit(1)
- }
- }
- func jfifEncode(w io.Writer, m image.Image, o *jpeg.Options) error {
- return jpeg.Encode(&jfifWriter{w: w}, m, o)
- }
- // jfifWriter wraps an io.Writer to convert the data written to it from a plain
- // JPEG to a JFIF-enhanced JPEG. It implicitly buffers the first three bytes
- // written to it. The fourth byte will tell whether the original JPEG already
- // has the APP0 chunk that JFIF requires.
- type jfifWriter struct {
- // w is the wrapped io.Writer.
- w io.Writer
- // n ranges between 0 and 4 inclusive. It is the number of bytes written to
- // this (which also implements io.Writer), saturating at 4. The first three
- // bytes are expected to be {0xff, 0xd8, 0xff}. The fourth byte indicates
- // whether the second JPEG chunk is an APP0 chunk or something else.
- n int
- }
- func (jw *jfifWriter) Write(p []byte) (int, error) {
- nSkipped := 0
- for jw.n < 3 {
- if len(p) == 0 {
- return nSkipped, nil
- } else if p[0] != jfifChunk[jw.n] {
- return nSkipped, errors.New("jfifWriter: input was not a JPEG")
- }
- nSkipped++
- jw.n++
- p = p[1:]
- }
- if jw.n == 3 {
- if len(p) == 0 {
- return nSkipped, nil
- }
- chunk := jfifChunk
- if p[0] == 0xe0 {
- // The input JPEG already has an APP0 marker. Just write SOI (2
- // bytes) and an 0xff: the three bytes we've previously skipped.
- chunk = chunk[:3]
- }
- if _, err := jw.w.Write(chunk); err != nil {
- return nSkipped, err
- }
- jw.n = 4
- }
- n, err := jw.w.Write(p)
- return n + nSkipped, err
- }
- // jfifChunk is a sequence: an SOI chunk, an APP0/JFIF chunk and finally the
- // 0xff that starts the third chunk.
- var jfifChunk = []byte{
- 0xff, 0xd8, // SOI marker.
- 0xff, 0xe0, // APP0 marker.
- 0x00, 0x10, // Length: 16 byte payload (including these two bytes).
- 0x4a, 0x46, 0x49, 0x46, 0x00, // "JFIF\x00".
- 0x01, 0x01, // Version 1.01.
- 0x00, // No density units.
- 0x00, 0x01, // Horizontal pixel density.
- 0x00, 0x01, // Vertical pixel density.
- 0x00, // Thumbnail width.
- 0x00, // Thumbnail height.
- 0xff, // Start of the third chunk's marker.
- }
現(xiàn)在運行它會生成一個 JFIF 文件,而不僅僅是一個 JPEG 文件。
- $ go run from-jpeg-to-jfif.go > y
- $ hd y | head -n 5
- 00000000 ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01 |......JFIF......|
- 00000010 00 01 00 00 ff db 00 84 00 08 06 06 07 06 05 08 |................|
- 00000020 07 07 07 09 09 08 0a 0c 14 0d 0c 0b 0b 0c 19 12 |................|
- 00000030 13 0f 14 1d 1a 1f 1e 1d 1a 1c 1c 20 24 2e 27 20 |........... $.' |
- 00000040 22 2c 23 1c 1c 28 37 29 2c 30 31 34 34 34 1f 27 |",#..(7),01444.'|
- $ file y
- y: JPEG image data, JFIF... baseline...
05 結(jié)論
這里的細節(jié)是關(guān)于 JPEG 和 JFIF 的,但一般的想法是,如果 encoding 庫(Go 中的一個包)缺少一個功能,你可以不通過更改該庫來修復它(或以其他方式對其進行處理),而是預處理輸入或處理輸出。
原文鏈接:https://nigeltao.github.io/blog/2021/from-jpeg-to-jfif.html
參考資料
[1]One of these JPEGs is not like the other: https://blog.benjojo.co.uk/post/not-all-jpegs-are-the-same
[2]fork: https://github.com/benjojo/app0-image-jpeg
[3]修復: https://github.com/benjojo/app0-image-jpeg/commit/645750c1672807c80c08a57a684a0ada7bf371d9
[4]Example.jpg: https://en.wikipedia.org/wiki/File:Example.jpg
[5]JFIF 規(guī)范: https://www.w3.org/Graphics/JPEG/jfif3.pdf
[6]EXIF 規(guī)范: https://www.exif.org/Exif2-2.PDF
[7]發(fā)布周期: https://github.com/golang/go/wiki/Go-Release-Cycle
[8]兼容性限制: https://golang.org/doc/go1compat
[9]jpeg.Encode: https://pkg.go.dev/image/jpeg#Encode
本文轉(zhuǎn)載自微信公眾號「程序員ug」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系程序員ug公眾號。