Format plugins and decoder internals

Pillow · Expert · 15 min

Knowing the plugin architecture explains lazy loading, partial-data errors and how to support a custom format.

Each file format is a plugin module such as PngImagePlugin or JpegImagePlugin, containing an ImageFile.ImageFile subclass. Plugins register themselves with Image.register_open(format, factory, accept), register_save and register_extension(s).

When you call Image.open, Pillow reads the first bytes and asks each registered plugin's accept function whether it recognises the magic number; the first match runs its _open() method, which parses only the header and fills in mode, size and `tile`. The tile list describes how to decode: for each region, a decoder name ("zip", "jpeg", "raw"...), the box, the file offset and decoder arguments.

load() then walks im.tile, looks up the C decoder in the _imaging extension, feeds it file data in chunks, and fills the internal image memory (im.im, an ImagingCore object). This split is what makes opening lazy and lets draft() change decoding parameters beforehand.

For streaming data, ImageFile.Parser accepts bytes incrementally with feed() and produces an image from close(), useful when downloading.

A minimal custom format plugin only needs format, format_description, an _open that validates the header and sets _size, _mode and tile (often using the built-in raw decoder), plus the register calls. features.pilinfo() prints which plugins and codecs your build includes.

Example

import io
from PIL import Image, ImageFile

class TinyImageFile(ImageFile.ImageFile):
    format, format_description = "TINY", "Toy grayscale format"

    def _open(self):
        header = self.fp.read(8)               # b"TINY" + width + height
        if header[:4] != b"TINY":
            raise SyntaxError("not a TINY file")
        self._mode = "L"
        self._size = (header[4] | header[5] << 8, header[6] | header[7] << 8)
        self.tile = [("raw", (0, 0) + self.size, 8, ("L", 0, 1))]   # decoder, box, offset, args

Image.register_open("TINY", TinyImageFile, lambda prefix: prefix[:4] == b"TINY")

data = b"TINY" + bytes([3, 0, 2, 0]) + bytes([0, 50, 100, 150, 200, 250])
im = Image.open(io.BytesIO(data))
print(im.format, im.size, im.mode)              # TINY (3, 2) L
print(im.getpixel((2, 1)))                      # 250  (decoded on demand)

p = ImageFile.Parser()
for i in range(0, len(data), 4):
    p.feed(data[i:i + 4])                       # incremental bytes
print(p.close().size)                           # (3, 2)

Remember

  • Plugins register an ImageFile subclass plus an accept() magic-number check
  • _open parses the header and sets mode, size and the tile list
  • load() runs C decoders over the tiles into internal memory
  • ImageFile.Parser decodes data fed incrementally

Watch out for

  • Relying on private internals such as im.im in application code; they change between versions.

Check yourself

  1. What does a plugin's `_open()` method typically do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pillow.readthedocs.io/en/stable/handbook/writing-your-own-image-plugin.html