编写自己的图像插件

Pillow 使用插件模型,允许您将自己的解码器和编码器添加到库中,而无需对库本身进行任何更改。此类插件通常具有诸如 XxxImagePlugin.py 之类的名称,其中 Xxx 是唯一的格式名称(通常是缩写)。

警告

Pillow >= 2.1.0 不再自动导入 Python 路径中以 ImagePlugin.py 结尾的任何文件。您需要手动导入图像插件。

Pillow 通过两个阶段解码文件

  1. 它按加载顺序循环遍历可用的图像插件,并使用文件的头 16 个字节调用插件的 _accept 函数。如果 _accept 函数返回 true,则调用插件的 _open 方法以设置图像元数据和图像块。 _open 方法不用于解码实际的图像数据。

  2. 当请求图像数据时,将调用 ImageFile.load 方法,该方法为每个块设置解码器,并将数据提供给解码器。

图像插件应包含一个从 PIL.ImageFile.ImageFile 基类派生的格式处理程序。此类应提供一个 _open 方法,该方法读取文件头并至少设置内部 _size_mode 属性,以便填充 modesize。为了能够加载文件,该方法还必须创建一个 tile 描述符列表,其中包含解码器名称、块的范围以及任何特定于解码器的數據。格式处理程序类必须通过调用 Image 模块显式注册。

注意

出于性能原因,_open 方法应快速拒绝没有适当内容的文件。

示例

以下插件支持一个简单的格式,该格式具有 128 字节的头部,包含单词“SPAM”,后跟宽度、高度和像素大小(以位为单位)。头部字段由空格分隔。图像数据紧随头部之后,可以是双电平、灰度或 24 位真彩色。

SpamImagePlugin.py:

from PIL import Image, ImageFile


def _accept(prefix: bytes) -> bool:
    return prefix[:4] == b"SPAM"


class SpamImageFile(ImageFile.ImageFile):

    format = "SPAM"
    format_description = "Spam raster image"

    def _open(self) -> None:

        header = self.fp.read(128).split()

        # size in pixels (width, height)
        self._size = int(header[1]), int(header[2])

        # mode setting
        bits = int(header[3])
        if bits == 1:
            self._mode = "1"
        elif bits == 8:
            self._mode = "L"
        elif bits == 24:
            self._mode = "RGB"
        else:
            msg = "unknown number of bits"
            raise SyntaxError(msg)

        # data descriptor
        self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))]


Image.register_open(SpamImageFile.format, SpamImageFile, _accept)

Image.register_extensions(
    SpamImageFile.format,
    [
        ".spam",
        ".spa",  # DOS version
    ],
)

格式处理程序必须始终设置内部 _size_mode 属性,以便填充 sizemode。如果没有设置这些属性,则无法打开文件。为了简化插件,调用代码将诸如 SyntaxErrorKeyErrorIndexErrorEOFErrorstruct.error 之类的异常视为无法识别文件。

请注意,图像插件必须使用 PIL.Image.register_open() 显式注册。虽然不是必需的,但注册此格式使用的任何扩展也是一个好主意。

导入插件后,就可以使用它了

from PIL import Image
import SpamImagePlugin

with Image.open("hopper.spam") as im:
    pass

tile 属性

为了能够读取文件以及仅识别文件,还必须设置 tile 属性。此属性包含一个块描述符列表,其中每个描述符指定如何将数据加载到图像中的给定区域。

在大多数情况下,只使用一个描述符,涵盖整个图像。 PsdImagePlugin.PsdImageFile 使用多个块将单个层内的通道组合在一起,因为通道是单独存储的,一个接一个。

块描述符是一个 4 元组,包含以下内容

(decoder, region, offset, parameters)

这些字段的用途如下

解码器

指定要使用的解码器。此处使用的 raw 解码器支持各种像素格式的未压缩数据。有关此解码器的更多信息,请参阅下面的说明。

可以在 _imaging.c 中函数数组的 codecs 部分中看到 C 解码器的列表。Python 解码器在相关的插件中注册。

区域

一个 4 元组,指定在图像中存储数据的位置。

偏移量

从文件开头到图像数据的字节偏移量。

参数

解码器的参数。此字段的内容取决于块描述符元组中第一个字段指定的解码器。如果解码器不需要任何参数,则对该字段使用 None

请注意,tile 属性包含一个块描述符列表,而不仅仅是一个描述符。

解码器

raw 解码器

raw 解码器用于从图像文件读取未压缩的数据。它可以与大多数未压缩文件格式一起使用,例如 PPM、BMP、未压缩 TIFF 以及许多其他格式。要在 PIL.Image.frombytes() 函数中使用 raw 解码器,请使用以下语法

image = Image.frombytes(
    mode, size, data, "raw",
    raw_mode, stride, orientation
    )

在块描述符中使用时,参数字段应类似于

(raw_mode, stride, orientation)

这些字段的用途如下

raw_mode

文件中使用的像素布局,用于将数据正确转换为 PIL 的内部布局。有关可用格式的摘要,请参阅下表。

跨度

图像中两条相邻行之间的字节距离。如果为 0,则假定图像为打包的(行之间没有填充)。如果省略,则跨度默认为 0。

方向

图像中的第一行是屏幕上的顶行 (1) 还是底行 (-1)。如果省略,则方向默认为 1。

**原始模式** 字段用于确定如何解压缩数据以匹配 PIL 的内部像素布局。PIL 支持大量原始模式;有关完整列表,请参阅 Unpack.c 模块中的表格。下表描述了一些常用的 **原始模式**

模式

描述

1

1 位双色调,以最左侧像素在最高位存储。
有效位为 0 表示黑色,1 表示白色。

1;I

1 位反转双色调,以最左侧像素在最高位存储。
有效位为 0 表示白色,1 表示黑色。

1;R

1 位反转双色调,以最左侧像素在最低位存储。
有效位为 0 表示黑色,1 表示白色。

L

8 位灰度。0 表示黑色,255 表示白色。

L;I

8 位反转灰度。0 表示白色,255 表示黑色。

P

8 位调色板映射图像。

RGB

24 位真彩色,存储为 (红色、绿色、蓝色)。

BGR

24 位真彩色,存储为 (蓝色、绿色、红色)。

RGBX

24 位真彩色,存储为 (红色、绿色、蓝色、填充)。填充
像素可能会有所不同。

RGB;L

24 位真彩色,行交错 (首先是所有红色像素,然后是
所有绿色像素,最后是所有蓝色像素)。

请注意,对于最常见的情况,原始模式与模式相同。

Python Imaging Library 支持许多其他解码器,包括 JPEG、PNG 和 PackBits。有关详细信息,请参阅 decode.c 源文件,以及库提供的标准插件实现。

解码浮点数据

PIL 提供了一些特殊机制,允许您将各种格式加载到 F (浮点) 图像内存中。

您可以使用 raw 解码器读取数据以任何标准机器数据类型打包的图像,使用以下原始模式之一

模式

描述

F

32 位原生浮点。

F;8

8 位无符号整数。

F;8S

8 位有符号整数。

F;16

16 位小端无符号整数。

F;16S

16 位小端有符号整数。

F;16B

16 位大端无符号整数。

F;16BS

16 位大端有符号整数。

F;16N

16 位原生无符号整数。

F;16NS

16 位原生有符号整数。

F;32

32 位小端无符号整数。

F;32S

32 位小端有符号整数。

F;32B

32 位大端无符号整数。

F;32BS

32 位大端有符号整数。

F;32N

32 位原生无符号整数。

F;32NS

32 位原生有符号整数。

F;32F

32 位小端浮点。

F;32BF

32 位大端浮点。

F;32NF

32 位原生浮点。

F;64F

64 位小端浮点。

F;64BF

64 位大端浮点。

F;64NF

64 位原生浮点。

位解码器

如果原始解码器无法处理您的格式,PIL 还提供一个特殊的“位”解码器,可用于将各种打包格式读取到浮点图像内存中。

要将位解码器与 PIL.Image.frombytes() 函数一起使用,请使用以下语法

image = Image.frombytes(
    mode, size, data, "bit",
    bits, pad, fill, sign, orientation
    )

在块描述符中使用时,参数字段应类似于

(bits, pad, fill, sign, orientation)

这些字段的用途如下

每个像素的位数 (2-32)。无默认值。

填充

行之间的填充,以位为单位。如果无填充,则为 0;如果行填充为完整字节,则为 8。如果省略,则填充值默认为 8。

填充

控制如何将数据添加到解码器位缓冲区以及从解码器位缓冲区存储数据。

填充=0

将字节添加到解码器缓冲区的 LSB 结尾;从 MSB 结尾存储像素。

填充=1

将字节添加到解码器缓冲区的 MSB 结尾;从 MSB 结尾存储像素。

填充=2

将字节添加到解码器缓冲区的 LSB 结尾;从 LSB 结尾存储像素。

填充=3

将字节添加到解码器缓冲区的 MSB 结尾;从 LSB 结尾存储像素。

如果省略,则填充顺序默认为 0。

符号

如果非零,则位域为符号扩展。如果为零或省略,则位域为无符号。

方向

图像中的第一行是屏幕上的顶行 (1) 还是底行 (-1)。如果省略,则方向默认为 1。

用 C 编写自己的文件编解码器

文件编解码器的一生包含 3 个阶段

  1. 设置:Pillow 在解码器或编码器注册表中查找函数,然后回退到内部核心图像对象上名为 [codecname]_decoder[codecname]_encoder 的函数。该函数将使用 args 元组从 tile 中调用。

  2. 转换:编解码器的 decodeencode 函数将使用图像数据的块重复调用。

  3. 清理:如果编解码器注册了清理函数,则在转换过程结束时会调用它,即使引发了异常。

设置

当前约定是,编解码器设置函数名为 PyImaging_[codecname]DecoderNewPyImaging_[codecname]EncoderNew,并在 decode.cencode.c 中定义。它的 Python 绑定名为 [codecname]_decoder[codecname]_encoder,并从 _imaging.c 文件的编解码器部分的函数数组中设置。

设置函数需要调用 PyImaging_DecoderNewPyImaging_EncoderNew,至少需要设置 decodeencode 函数指针。此对象中值得注意的字段是

**解码** / **编码**

指向解码或编码函数的函数指针,该函数可以访问 imstate 和要转换的数据缓冲区。

清理

指向清理函数的函数指针,可以访问 state

im

目标图像,将由 Pillow 设置。

状态

ImagingCodecStateInstance,将由 Pillow 设置。 context 成员是一个不透明的结构,编解码器可以使用它来存储任何格式特定的状态或选项。

**pulls_fd** / **pushes_fd**

如果解码器已将 pulls_fd 设置为 1 或编码器已将 pushes_fd 设置为 1,则 state->fd 将指向 Python 文件类对象。编解码器可以使用 codec_fd.c 中的函数直接使用文件类对象进行读写,而不是将数据推送到缓冲区。

在 3.3.0 版本中添加。

转换

使用目标 (核心) 图像、编解码器状态结构和要转换的数据缓冲区调用解码或编码函数。

编解码器有责任从缓冲区中尽可能多地提取数据并返回消耗的字节数。对编解码器的下一次调用将包括前一个未消耗的尾部。编解码器函数将被多次调用,因为数据被处理。

或者,如果设置了 pulls_fdpushes_fd,则解码或编码函数将只被调用一次,并使用一个空缓冲区。编解码器有责任在该调用中转换整个图块。使用此方法将为编解码器提供更多自由度,但这种自由度可能意味着如果编解码器一次将整个图块保存在内存中,则内存使用量会增加。

如果出现错误,请设置 state->errcode 并返回 -1。

成功时返回 -1,不设置 errcode。

清理

在编解码器返回负值或出现错误后,将调用清理函数。此函数应释放任何已分配的内存并释放来自外部库的任何资源。

用 Python 编写自己的文件编解码器

Python 文件解码器和编码器应分别从 PIL.ImageFile.PyDecoderPIL.ImageFile.PyEncoder 派生,并且至少应该覆盖解码或编码方法。它们应该使用 PIL.Image.register_decoder()PIL.Image.register_encoder() 进行注册。与基于 C 的文件编解码器的实现一样,基于 Python 的文件编解码器的一生包含三个阶段

  1. 设置:Pillow 在解码器或编码器注册表中查找编解码器,然后实例化该类。

  2. 转换:实例的 decode 方法将使用要解释的数据缓冲区重复调用,或者 encode 方法将使用要输出的数据大小重复调用。

    或者,如果解码器的 _pulls_fd 属性(或编码器的 _pushes_fd 属性)设置为 True,则 decodeencode 只会被调用一次。在解码器中,self.fd 可用于访问文件类对象。使用此方法将为编解码器提供更多自由度,但这种自由度可能意味着如果编解码器一次将整个文件保存在内存中,则内存使用量会增加。

    decode 中,一旦解释完数据,就可以使用 set_as_raw 来填充图像。

  3. 清理: 一旦转换完成,实例的 cleanup 方法会被调用。 这可以用来清理编解码器使用的任何资源。

    但是,如果你将 _pulls_fd_pushes_fd 设置为 True,那么你可能选择在 decodeencode 结束时执行任何清理任务。

关于 PIL.ImageFile.PyDecoder 的示例,请参阅 DdsImagePlugin。 对于同时使用 PIL.ImageFile.PyDecoderPIL.ImageFile.PyEncoder 的插件,请参阅 BlpImagePlugin