加入收藏 | 设为首页 | 会员中心 | 我要投稿 东莞站长网 (https://www.0769zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 编程要点 > 语言 > 正文

Go 通过 io.Writer 将 JPEG 转为 JFIF

发布时间:2021-12-15 15:47:36 所属栏目:语言 来源:互联网
导读:Go 的标准库可让你对 JPEG 图像进行编码。在 One of these JPEGs is not like the other[1] 一文中,Ben Cox 指出某些硬件不会解码这些 JPEG 图像,除非它们被增强为 JFIF 图像。JFIF 代表JPEG 文件交换格式,在概念上是原始 JPEG 格式的次要版本。 硬件缺乏
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] 十六进制表示:
 
$ 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 个字节标记:
 
一个 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 规范在技术上不兼容,因为它们都想占用第二块(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 图像。
 
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 函数,它可以直接替代 jpeg.Encode 但添加额外的 JFIF 字节,只要第二个标记(紧接在 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, // "JFIFx00".
    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.
}
现在运行它会生成一个 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... 

(编辑:东莞站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    热点阅读