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,則在呼叫 Image.Image.load() 方法後,Pillow 開啟的結果檔案物件也可能由 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(),以確保底層檔案已關閉。