Go:通过 io.Writer 将 JPEG 转为 JFIF

开发 后端
Go 的标准库可让你对 JPEG 图像进行编码。在 One of these JPEGs is not like the other[1] 一文中,Ben Cox 指出某些硬件不会解码这些 JPEG 图像,除非它们被增强为 JFIF 图像。JFIF 代表“JPEG 文件交换格式”,在概念上是原始 JPEG 格式的次要版本。

 [[439345]]

大家好,我是程序员幽鬼。

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 字节。

01 JPEG Wire 格式

就网络(或磁盘)上的字节而言,JPEG 由一系列连接在一起的块组成。每个块要么是一个裸标记(两个字节,以 开头 0xff)要么是一个标记段(四个或更多字节是一个两字节标记,同样以 0xff 开头,一个两字节的长度,然后是一个额外的数据负载)。以下是 Wikipedia 的Example.jpg[4] 十六进制表示:

  1. $ wget --quiet https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg 
  2. $ hd Example.jpg | head -n 5 
  3. 00000000  ff d8 ff e0 00 10 4a 46  49 46 00 01 01 01 00 48  |......JFIF.....H| 
  4. 00000010  00 48 00 00 ff e1 00 16  45 78 69 66 00 00 4d 4d  |.H......Exif..MM| 
  5. 00000020  00 2a 00 00 00 08 00 00  00 00 00 00 ff fe 00 17  |.*..............| 
  6. 00000030  43 72 65 61 74 65 64 20  77 69 74 68 20 54 68 65  |Created with The| 
  7. 00000040  20 47 49 4d 50 ff db 00  43 00 05 03 04 04 04 03  | GIMP...C.......| 

在打开的 80 个字节标记:

  • 一个 ff d8 SOI(图像的开始)标记。
  • 一个 ff e0 APP0 标记段;有效载荷以 “JFIF” 开头。
  • 一个 ff e1 APP1 标记段;有效载荷以 “Exif” 开头。
  • 一个 ff fe 注释标记段,“Created 等等”。
  • 一个 ff db DQT(定义量化表)标记段。

file 命令也认为这是 JFIF(带 Exif),而不仅仅是 JPEG:

  1. $ file Example.jpg 
  2. Example.jpg: JPEG image data, JFIF... Exif... baseline... 

02 JFIF Wire 格式

JFIF 文件是一个 JPEG 文件,它的第二个块(在作为第一个块的 SOI 之后)是一个 APP0 块,其有效载荷以 “JFIF” 开头。一个有趣的点是 JFIF 和 EXIF 规范在技术上不兼容,因为它们都想占用第二块(the second chunk):

  • JFIF 规范[5]第 2 页提到:“JPEG FIF APP0 标记必须紧跟在 SOI 标记之后”。
  • EXIF 规范[6] 第 4.5.4 段提到:“APP1 是紧跟在 SOI 标记之后的”。

在实践中,似乎 JFIF 'won' 和 EXIF 可以是第三个块。

03 生成普通的旧 JPEG

这篇博文提供了不需要任何标准库补丁(或 forks)的 Cox 方法的替代方法。与往常一样,fork 具有从上游缓慢分叉的长期风险。Go 标准库的上游补丁受制于“3 个月的新功能,3 个月的稳定” 发布周期[7],并决定额外的 JFIF 块是强制性的还是可选的(如果可选,API 应该是什么,受兼容性限制[8])。

该方案的主要思想是 jpeg.Encode[9] 函数接受一个 io.Writer 参数,并且很容易包装 io.Writer 以在正确的位置插入 JFIF 字节。

首先,让我们编写一个简单的程序来生成一张 1x1 JPEG 图像。

  1. package main 
  2.  
  3. import ( 
  4.     "image" 
  5.     "image/jpeg" 
  6.     "os" 
  7.  
  8. func main() { 
  9.     m := image.NewGray(image.Rect(0, 0, 1, 1)) 
  10.     if err := jpeg.Encode(os.Stdout, m, nil); err != nil { 
  11.         os.Stderr.WriteString(err.Error() + "\n"
  12.         os.Exit(1) 
  13.     } 

运行它会生成一个 JPEG(但不是 JFIF)文件。

  1. $ go run from-jpeg-to-jfif.go > x 
  2. $ hd x | head -n 5 
  3. 00000000  ff d8 ff db 00 84 00 08  06 06 07 06 05 08 07 07  |................| 
  4. 00000010  07 09 09 08 0a 0c 14 0d  0c 0b 0b 0c 19 12 13 0f  |................| 
  5. 00000020  14 1d 1a 1f 1e 1d 1a 1c  1c 20 24 2e 27 20 22 2c  |......... $.' ",| 
  6. 00000030  23 1c 1c 28 37 29 2c 30  31 34 34 34 1f 27 39 3d  |#..(7),01444.'9=| 
  7. 00000040  38 32 3c 2e 33 34 32 01  09 09 09 0c 0b 0c 18 0d  |82<.342.........| 
  8. $ file x 
  9. x: JPEG image data, baseline, precision 8, 1x1, components 1 

04 一个 JFIFifying Writer

我们编写一个 jfifEncode 函数,它可以直接替代 jpeg.Encode 但添加额外的 JFIF 字节,只要第二个标记(紧接在 SOI 之后的那个)不是 APP0。

  1. package main 
  2.  
  3. import ( 
  4.     "errors" 
  5.     "image" 
  6.     "image/jpeg" 
  7.     "io" 
  8.     "os" 
  9.  
  10. func main() { 
  11.     m := image.NewGray(image.Rect(0, 0, 1, 1)) 
  12.     if err := jfifEncode(os.Stdout, m, nil); err != nil { 
  13.         os.Stderr.WriteString(err.Error() + "\n"
  14.         os.Exit(1) 
  15.     } 
  16.  
  17. func jfifEncode(w io.Writer, m image.Image, o *jpeg.Options) error { 
  18.     return jpeg.Encode(&jfifWriter{w: w}, m, o) 
  19.  
  20. // jfifWriter wraps an io.Writer to convert the data written to it from a plain 
  21. // JPEG to a JFIF-enhanced JPEG. It implicitly buffers the first three bytes 
  22. // written to it. The fourth byte will tell whether the original JPEG already 
  23. // has the APP0 chunk that JFIF requires. 
  24. type jfifWriter struct { 
  25.     // w is the wrapped io.Writer. 
  26.     w io.Writer 
  27.     // n ranges between 0 and 4 inclusive. It is the number of bytes written to 
  28.     // this (which also implements io.Writer), saturating at 4. The first three 
  29.     // bytes are expected to be {0xff, 0xd8, 0xff}. The fourth byte indicates 
  30.     // whether the second JPEG chunk is an APP0 chunk or something else
  31.     n int 
  32.  
  33. func (jw *jfifWriter) Write(p []byte) (int, error) { 
  34.     nSkipped := 0 
  35.  
  36.     for jw.n < 3 { 
  37.         if len(p) == 0 { 
  38.             return nSkipped, nil 
  39.         } else if p[0] != jfifChunk[jw.n] { 
  40.             return nSkipped, errors.New("jfifWriter: input was not a JPEG"
  41.         } 
  42.         nSkipped++ 
  43.         jw.n++ 
  44.         p = p[1:] 
  45.     } 
  46.  
  47.     if jw.n == 3 { 
  48.         if len(p) == 0 { 
  49.             return nSkipped, nil 
  50.         } 
  51.         chunk := jfifChunk 
  52.         if p[0] == 0xe0 { 
  53.             // The input JPEG already has an APP0 marker. Just write SOI (2 
  54.             // bytes) and an 0xff: the three bytes we've previously skipped. 
  55.             chunk = chunk[:3] 
  56.         } 
  57.         if _, err := jw.w.Write(chunk); err != nil { 
  58.             return nSkipped, err 
  59.         } 
  60.         jw.n = 4 
  61.     } 
  62.  
  63.     n, err := jw.w.Write(p) 
  64.     return n + nSkipped, err 
  65.  
  66. // jfifChunk is a sequence: an SOI chunk, an APP0/JFIF chunk and finally the 
  67. // 0xff that starts the third chunk. 
  68. var jfifChunk = []byte{ 
  69.     0xff, 0xd8, // SOI  marker. 
  70.     0xff, 0xe0, // APP0 marker. 
  71.     0x00, 0x10, // Length: 16 byte payload (including these two bytes). 
  72.     0x4a, 0x46, 0x49, 0x46, 0x00, // "JFIF\x00"
  73.     0x01, 0x01, // Version 1.01. 
  74.     0x00,       // No density units. 
  75.     0x00, 0x01, // Horizontal pixel density. 
  76.     0x00, 0x01, // Vertical   pixel density. 
  77.     0x00, // Thumbnail width. 
  78.     0x00, // Thumbnail height. 
  79.     0xff, // Start of the third chunk's marker. 

现在运行它会生成一个 JFIF 文件,而不仅仅是一个 JPEG 文件。

  1. $ go run from-jpeg-to-jfif.go > y 
  2. $ hd y | head -n 5 
  3. 00000000  ff d8 ff e0 00 10 4a 46  49 46 00 01 01 00 00 01  |......JFIF......| 
  4. 00000010  00 01 00 00 ff db 00 84  00 08 06 06 07 06 05 08  |................| 
  5. 00000020  07 07 07 09 09 08 0a 0c  14 0d 0c 0b 0b 0c 19 12  |................| 
  6. 00000030  13 0f 14 1d 1a 1f 1e 1d  1a 1c 1c 20 24 2e 27 20  |........... $.' | 
  7. 00000040  22 2c 23 1c 1c 28 37 29  2c 30 31 34 34 34 1f 27  |",#..(7),01444.'| 
  8. $ file y 
  9. y: JPEG image data, JFIF... baseline... 

05 结论

这里的细节是关于 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 规范: https://www.w3.org/Graphics/JPEG/jfif3.pdf

[6]EXIF 规范: https://www.exif.org/Exif2-2.PDF

[7]发布周期: 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

本文转载自微信公众号「程序员ug」,可以通过以下二维码关注。转载本文请联系程序员ug公众号。

 

责任编辑:武晓燕 来源: 程序员ug
相关推荐

2024-07-09 08:07:37

Go性能工具

2022-10-20 08:59:18

Go接口类型

2023-11-28 08:52:48

Go日志库

2023-10-07 09:08:32

Golangbufio

2023-11-07 09:02:07

Golangbytes

2024-04-29 08:45:16

Go语言PDF

2020-10-25 06:30:48

Go语言编程语言

2012-05-19 22:17:30

Android

2022-02-15 11:49:08

eBPFGo内存

2015-12-10 10:47:25

微软Windows Liv开源

2020-11-11 17:00:02

PythonOffice文件PDF

2021-08-27 15:30:13

PSPhotoshopPS格式

2023-01-30 15:41:10

Channel控制并发

2022-04-24 15:29:17

微服务go

2021-12-29 07:56:32

Go byte io.Reader

2021-09-17 14:13:28

JavaScript编程字符串

2014-11-14 09:54:08

灾难恢复DRaaS云灾难恢复

2019-01-03 15:40:38

数据库SQL查询JSON

2021-02-01 06:39:42

模块封装库

2022-05-07 08:55:11

Go语言排序算法
点赞
收藏

51CTO技术栈公众号