Pillow 中的文件处理

在将文件打开为图像时,Pillow 需要一个文件名、os.PathLike 对象或文件类对象。Pillow 使用文件名或 Path 打开文件,因此在本篇文章的其余部分中,它们都将被视为文件类对象。

以下是等效的

from PIL import Image
import io
import pathlib

with Image.open("test.jpg") as im:
    ...

with Image.open(pathlib.Path("test.jpg")) as im2:
    ...

with open("test.jpg", "rb") as f:
    im3 = Image.open(f)
    ...

with open("test.jpg", "rb") as f:
    im4 = Image.open(io.BytesIO(f.read()))
    ...

如果将文件名或路径类对象传递给 Pillow,则 Pillow 打开的结果文件对象也可以在 Image.Image.load() 方法被调用后由 Pillow 关闭,前提是关联的图像没有多个帧。

Pillow 通常无法关闭并重新打开文件,因此对该文件的任何访问都需要在关闭之前进行。

图像生命周期

  • Image.open() 文件名和 Path 对象被打开为文件。元数据从打开的文件中读取。该文件保持打开状态以供进一步使用。

  • Image.Image.load() 当需要图像的像素数据时,会调用 load()。当前帧被读取到内存中。现在,图像可以独立于底层图像文件使用。

    任何基于另一个图像实例创建新图像实例的 Pillow 方法都会在内部对原始图像调用 load(),然后读取数据。新的图像实例将不会与原始图像文件关联。

    如果文件名或 Path 对象被传递给 Image.open(),则文件对象是由 Pillow 打开的,并被认为是 Pillow 独占使用的。因此,如果图像是一个单帧图像,则在读取帧后,该文件将在此方法中关闭。如果图像是一个多帧图像(例如,多页 TIFF 和动画 GIF),则图像文件将保持打开状态,以便 Image.Image.seek() 可以加载相应的帧。

  • Image.Image.close() 关闭文件并销毁核心图像对象。

    Pillow 上下文管理器也会关闭文件,但不会销毁核心图像对象。例如:

    with Image.open("test.jpg") as img:
        img.load()
    assert img.fp is None
    img.save("test.png")
    

单帧图像的生命周期相对简单。该文件必须保持打开状态,直到 load()close() 函数被调用或上下文管理器退出。

多帧图像更复杂。load() 方法不是一个终止方法,因此它不应该关闭底层文件。通常,Pillow 不知道在调用者明确关闭图像之前,是否会存在任何对附加数据的请求。

复杂性

  • TiffImagePlugin 有一些代码可以将底层文件描述符传递到 libtiff(如果在实际文件上工作)。由于 libtiff 在内部关闭文件描述符,因此在将其传递到 libtiff 之前,它会被复制。

  • 文件关闭后,需要文件访问的操作将失败

    with open("test.jpg", "rb") as f:
        im5 = Image.open(f)
    im5.load()  # FAILS, closed file
    
    with Image.open("test.jpg") as im6:
        pass
    im6.load()  # FAILS, closed file
    

建议的文件处理

  • Image.Image.load() 应该关闭图像文件,除非存在多个帧。

  • Image.Image.seek() 不应该关闭图像文件。

  • 库的用户应该使用上下文管理器或对任何使用文件名或 Path 对象打开的图像调用 Image.Image.close(),以确保底层文件被关闭。