动机
当前的 Decode
函数在 image
包中提供了一种配置底层格式解码器的方法。image
使用的注册系统使得扩展已注册格式处理程序的解码行为变得困难。然而,有许多可以由少量解码选项解决的开放问题。本提案描述了一种扩展现有的格式注册系统的方法,以便将选项传递给各个格式解码器。它引入了一个 DecodeOptions
类型作为扩展点。
有两个广泛考虑的配置领域。本提案解决了这两个领域,但它们是正交的,可以独立评估和实现。这些领域是:
避免无法处理的图像上的大分配
这类问题通常是由图像文件中的错误或恶意头信息引起的。通常这可能是一个非常大的 x 或 y 维度,导致在准备读取一系列像素数据流之前分配大量缓冲区。目前可以通过首先解码图像配置来检查尺寸,然后再进行完整解码来缓解这个问题。但是,这有一个缺点,即要么需要两次读取输入,要么缓冲并重新读取以进行解码。相关问题的建议表明,使用 16k 缓冲区是合适的。以下问题与此用例相关(#5050 是总体问题)
- 5050 - image/gif:解码不受信任(非常大的)图像可能会导致巨大的内存分配
- 10790 - x/image/webp:过度消耗内存
- 12512 - image/png:限制内存 Decode 在解码过程中可以使用多少内存
- 10399 - x/image/bmp:内存不足
对无效输入更加宽容
标准库中的图像解码器严格且对无效输入失败。有一些类型的无效输入可能对于某些用途是可以接受的。以下问题建议采用宽松的解码模式会有所帮助:
- 10447 - image/jpeg:向部分解码或容忍性解码无效图像添加选项?
- 20804 - image/gif:解码 gif 返回
unknown block type: 0x01
错误 - 20899 - image/png:解码位图失败
- 20856 - image/gif:解码 gif 返回
frame bounds larger than image bounds
错误
其他相关选项问题
此外,以下问题建议其他领域可以从解码选项中受益,但在本提案中未进一步考虑:
- 8055 - image:将图像解码/调整大小到现有缓冲区
- 18098 - proposal:为 image/jpeg、image/png 等添加验证函数
- 4341 - image/jpeg:纠正 EXIF 方向?
- 18365 - image/png:不支持设置和检索 PPI/DPI
包更改
主要的可扩展点是一个新类型。其字段将在本节末尾讨论。
type DecodeOptions struct
格式解码器通过一个新的包级函数 RegisterFormatDecoder
进行注册。
func RegisterFormatDecoder(f FormatDecoder)
任何提供图像格式解码的包都需要实现 FormatDecoder 接口:
type FormatDecoder interface {
// Name is the name of the format, like "jpeg" or "png".
Name() string
// Prefix is the magic prefix that identifies the format's encoding. The magic
// string can contain "?" wildcards that each match any one byte.
Prefix() string
// Decode decodes an image using the supplied options.
Decode(r io.Reader, o *DecodeOptions) (Image, string, error)
// DecodeConfig decodes the color model and dimensions of an image using the supplied options.
DecodeConfig(r io.Reader, o *DecodeOptions) (Config, string, error)
}
引入了一个带有基本构造函数函数的新导出 Decoder 类型:
type Decoder struct {
DecodeOptions
}
这些 Decode 方法将从阅读器的类型中嗅探图像,并查找相应的已注册 FormatDecoder。如果有可用的,则调用其相应的 Decode 方法,传入 Reader 和 Decoder 的选项。这要求 Decode 具有访问包级注册的格式解码器的能力。
要配置解码,开发人员将创建一个新的 Decoder,然后在调用 Decode 或 DecodeConfig 之前设置适当的字段。
以下是提议的选项。
MaxHeight 和 MaxWidth
MaxHeight 和 MaxWidth 是 DecodeOptions 字段,用于设置解码图像的最大允许尺寸。这些字段限制了为故障或恶意图像分配大量内存的可能性。
这些选项仅由 Decode 方法使用。当一个解码器尝试解码具有超过 MaxHeight 或 MaxWidth 的尺寸的图像时,应停止处理并返回错误。可以在图像包 ErrDimensionsExceeded
中定义一个新的错误标准,或者让解码器返回其自己的错误。DecodeConfig 应根据图像数据流描述的宽度和高度返回一个包含配置信息的 Config,而不考虑配置选项。
ReturnPartial
ReturnPartial 是 DecodeOptions 字段,指示解码器在遇到错误时可能返回部分解码的图像。
当前的 Decode 行为是在出现错误时不返回任何图像数据。当此选项为 true 时,当解码器在解码过程中遇到与格式相关的错误时,调用者可能会从解码过程中接收到部分图像。
现有的包级 Decode
和 DecodeConfig
函数将被重写,以创建一个解码器并调用它,而不使用任何选项:
func Decode(r io.Reader) (Image, string, error) {
return NewDecoder().Decode(r)
}
func DecodeConfig(r io.Reader) (Config, string, error) {
return NewDecoder().DecodeConfig(r)
}
RegisterFormat 的弃用
RegisterFormat 将被标记为弃用,以便使用 RegisterFormatDecoder。
4条答案
按热度按时间9bfwbjaz1#
/cc @nigeltao
tjvv9vkg2#
整体目标似乎还不错。以下是一些主要和次要的想法:
我建议从
image/*
开始分叉,并在向标准库的 API 更改之前证明概念。例如,API 提案仍然不允许我们在解码渐进式 JPEG 时看到中间阶段,当压缩字节缓慢到来时。有一个新的ReturnPartial
选项被提议,但据我所知,只有在我们看到错误时才会触发,而不会在我们只是被io.Reader
阻塞时触发。我们现在不必解决渐进式 JPEG 的情况,但我犹豫是否要在 Go 1.12 中承诺一个使 Go 1.16 更难解决该情况的 API。一种可能的设计方法是使用更像
text.Transformer
而不是io.Reader
的东西。这就是我在我的 Wuffs 项目中采用的方法。虽然有点难以理解,但这就是 Wuffs
io_buffer_meta
类型的样子。没有阻塞 I/O,协程会将部分解码的图像yield给调用者。这种
Transformer
类似的方法,允许 非 读取字节,也可能导致显著的速度提升。有一些优化技术,例如 C 的 libjpeg 可以这样做,而 Go 的 image/jpeg 不能这样做,因为 C 代码可以乐观地读取太多字节,稍后取消这些它不需要的字节。相比之下,Go 的io.Reader
接口和Decode(r io.Reader) (image.Image, string, error)
函数类型没有地方说“对不起,我从r
读取了太多字节,这里是更改”。但是使用类似于
Transformer
而不是io.Reader
的方法是一个重大的变化,也比 Go 的习惯用法“使用一个io.Reader
并阻塞在 I/O 上,因为 goroutines 是廉价的”更复杂。尝试使用这样的方法应该真的开始在 stdlib 之外进行,但也可以说保持在 stdlib 之外,作为x/image2
、std2/image
、iand/image
或其他什么。打破向后兼容性的约束也可能意味着能够将默认字节顺序从 RGBA 更改为 BGRA,或者以其他方式更适合 BGRA,这将使与 Windows 和 X11 GUI 的原生格式集成更容易,而无需例如每帧都相当昂贵的 swizzling。最后的一个小问题。我认为如果客户端可以使用
Decoder{}
结构体字面量,我们就不需要NewDecoder
函数。mwg9r5ms3#
感谢nigeltao的评论。我会对读者的观点做更多的研究。看起来@dsnet关于#23154的评论是相关的,尤其是他提到的BufferedReader类型。
von4xj4u4#
请将此标记为待定,直到@nigeltao和@iand完成更多研究。一旦准备好进行更多审查,请随时删除标签。