mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-08-18 19:24:47 +03:00
Accept any path-like object on Image.{open,save}
and truetype
, not just pathlib.Path
Also tries to fix some bugs where PIL will crash in case of binary paths, by using `os.fsdecode`.
This commit is contained in:
parent
f518149a1b
commit
ebf82e41c9
|
@ -1,5 +1,6 @@
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -21,6 +22,22 @@ from .helper import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MyStrPathLike:
|
||||||
|
def __init__(self, path_str):
|
||||||
|
self.path_str = path_str
|
||||||
|
|
||||||
|
def __fspath__(self):
|
||||||
|
return self.path_str
|
||||||
|
|
||||||
|
|
||||||
|
class MyBytesPathLike:
|
||||||
|
def __init__(self, path_str):
|
||||||
|
self.path_bytes = os.fsencode(path_str)
|
||||||
|
|
||||||
|
def __fspath__(self):
|
||||||
|
return self.path_bytes
|
||||||
|
|
||||||
|
|
||||||
class TestImage:
|
class TestImage:
|
||||||
def test_image_modes_success(self):
|
def test_image_modes_success(self):
|
||||||
for mode in [
|
for mode in [
|
||||||
|
@ -138,22 +155,30 @@ class TestImage:
|
||||||
with Image.open(io.StringIO()):
|
with Image.open(io.StringIO()):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_pathlib(self, tmp_path):
|
@pytest.mark.parametrize(
|
||||||
from PIL.Image import Path
|
"PathCls",
|
||||||
|
(
|
||||||
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
|
pathlib.Path,
|
||||||
|
MyStrPathLike, # Returns `str` on `os.fspath`
|
||||||
|
MyBytesPathLike, # Returns `bytes` on `os.fspath`
|
||||||
|
str,
|
||||||
|
os.fsencode, # Converts path to `bytes`
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_path_like(self, tmp_path, PathCls):
|
||||||
|
with Image.open(PathCls("Tests/images/multipage-mmap.tiff")) as im:
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
assert im.size == (10, 10)
|
assert im.size == (10, 10)
|
||||||
|
|
||||||
with Image.open(Path("Tests/images/hopper.jpg")) as im:
|
with Image.open(PathCls("Tests/images/hopper.jpg")) as im:
|
||||||
assert im.mode == "RGB"
|
assert im.mode == "RGB"
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
|
|
||||||
for ext in (".jpg", ".jp2"):
|
for ext in (".jpg", ".jp2"):
|
||||||
temp_file = str(tmp_path / ("temp." + ext))
|
temp_file = str((tmp_path / "temp").with_suffix(ext))
|
||||||
if os.path.exists(temp_file):
|
if os.path.exists(temp_file):
|
||||||
os.remove(temp_file)
|
os.remove(temp_file)
|
||||||
im.save(Path(temp_file))
|
im.save(PathCls(temp_file))
|
||||||
|
|
||||||
def test_fp_name(self, tmp_path):
|
def test_fp_name(self, tmp_path):
|
||||||
temp_file = str(tmp_path / "temp.jpg")
|
temp_file = str(tmp_path / "temp.jpg")
|
||||||
|
|
|
@ -27,6 +27,21 @@ def test_path_obj_is_path():
|
||||||
assert it_is
|
assert it_is
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_like_is_path():
|
||||||
|
# Arrange
|
||||||
|
class PathThingy:
|
||||||
|
def __fspath__(self):
|
||||||
|
return "UNIMPORTANT" # pragma: no cover
|
||||||
|
|
||||||
|
test_path = PathThingy()
|
||||||
|
|
||||||
|
# Act
|
||||||
|
it_is = _util.isPath(test_path)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert it_is
|
||||||
|
|
||||||
|
|
||||||
def test_is_not_path(tmp_path):
|
def test_is_not_path(tmp_path):
|
||||||
# Arrange
|
# Arrange
|
||||||
with (tmp_path / "temp.ext").open("w") as fp:
|
with (tmp_path / "temp.ext").open("w") as fp:
|
||||||
|
@ -39,6 +54,22 @@ def test_is_not_path(tmp_path):
|
||||||
assert not it_is_not
|
assert not it_is_not
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_like_file_is_not_path(tmp_path):
|
||||||
|
# Arrange
|
||||||
|
import io
|
||||||
|
|
||||||
|
class PathAndFileThingy(io.TextIOWrapper):
|
||||||
|
def __fspath__(self):
|
||||||
|
return "UNIMPORTANT" # pragma: no cover
|
||||||
|
|
||||||
|
with PathAndFileThingy(open(tmp_path / "temp.ext", "wb")) as test_obj:
|
||||||
|
# Act
|
||||||
|
it_is_not = _util.isPath(test_obj)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert not it_is_not
|
||||||
|
|
||||||
|
|
||||||
def test_is_directory():
|
def test_is_directory():
|
||||||
# Arrange
|
# Arrange
|
||||||
directory = "Tests"
|
directory = "Tests"
|
||||||
|
|
|
@ -2155,9 +2155,9 @@ class Image:
|
||||||
|
|
||||||
def save(self, fp, format=None, **params):
|
def save(self, fp, format=None, **params):
|
||||||
"""
|
"""
|
||||||
Saves this image under the given filename. If no format is
|
Saves this image under the given filepath. If no format is
|
||||||
specified, the format to use is determined from the filename
|
specified, the format to use is determined from the filepath's
|
||||||
extension, if possible.
|
file extension, if possible.
|
||||||
|
|
||||||
Keyword options can be used to provide additional instructions
|
Keyword options can be used to provide additional instructions
|
||||||
to the writer. If a writer doesn't recognise an option, it is
|
to the writer. If a writer doesn't recognise an option, it is
|
||||||
|
@ -2165,12 +2165,12 @@ class Image:
|
||||||
:doc:`image format documentation
|
:doc:`image format documentation
|
||||||
<../handbook/image-file-formats>` for each writer.
|
<../handbook/image-file-formats>` for each writer.
|
||||||
|
|
||||||
You can use a file object instead of a filename. In this case,
|
You can use a file object instead of a filepath. In this case,
|
||||||
you must always specify the format. The file object must
|
you must always specify the format. The file object must
|
||||||
implement the ``seek``, ``tell``, and ``write``
|
implement the ``seek``, ``tell``, and ``write``
|
||||||
methods, and be opened in binary mode.
|
methods, and be opened in binary mode.
|
||||||
|
|
||||||
:param fp: A filename (string), pathlib.Path object or file object.
|
:param fp: A filepath (str or bytes), path-like object or file object.
|
||||||
:param format: Optional format override. If omitted, the
|
:param format: Optional format override. If omitted, the
|
||||||
format to use is determined from the filename extension.
|
format to use is determined from the filename extension.
|
||||||
If a file object was used instead of a filename, this
|
If a file object was used instead of a filename, this
|
||||||
|
@ -2209,7 +2209,7 @@ class Image:
|
||||||
|
|
||||||
preinit()
|
preinit()
|
||||||
|
|
||||||
ext = os.path.splitext(filename)[1].lower()
|
ext = os.fsdecode(os.path.splitext(filename)[1].lower())
|
||||||
|
|
||||||
if not format:
|
if not format:
|
||||||
if ext not in EXTENSION:
|
if ext not in EXTENSION:
|
||||||
|
@ -2930,7 +2930,7 @@ def open(fp, mode="r", formats=None):
|
||||||
:py:meth:`~PIL.Image.Image.load` method). See
|
:py:meth:`~PIL.Image.Image.load` method). See
|
||||||
:py:func:`~PIL.Image.new`. See :ref:`file-handling`.
|
:py:func:`~PIL.Image.new`. See :ref:`file-handling`.
|
||||||
|
|
||||||
:param fp: A filename (string), pathlib.Path object or a file object.
|
:param fp: A filepath (str or bytes), path-like object or a file object.
|
||||||
The file object must implement ``file.read``,
|
The file object must implement ``file.read``,
|
||||||
``file.seek``, and ``file.tell`` methods,
|
``file.seek``, and ``file.tell`` methods,
|
||||||
and be opened in binary mode.
|
and be opened in binary mode.
|
||||||
|
|
|
@ -199,6 +199,7 @@ class FreeTypeFont:
|
||||||
|
|
||||||
if isPath(font):
|
if isPath(font):
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
|
font = os.fspath(font)
|
||||||
font_bytes_path = font if isinstance(font, bytes) else font.encode()
|
font_bytes_path = font if isinstance(font, bytes) else font.encode()
|
||||||
try:
|
try:
|
||||||
font_bytes_path.decode("ascii")
|
font_bytes_path.decode("ascii")
|
||||||
|
@ -796,10 +797,10 @@ def load(filename):
|
||||||
|
|
||||||
|
|
||||||
def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
|
def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
|
||||||
"""
|
r"""
|
||||||
Load a TrueType or OpenType font from a file or file-like object,
|
Load a TrueType or OpenType font from a filepath or file-like object,
|
||||||
and create a font object.
|
and create a font object.
|
||||||
This function loads a font object from the given file or file-like
|
This function loads a font object from the given filepath or file-like
|
||||||
object, and creates a font object for a font of the given size.
|
object, and creates a font object for a font of the given size.
|
||||||
|
|
||||||
Pillow uses FreeType to open font files. If you are opening many fonts
|
Pillow uses FreeType to open font files. If you are opening many fonts
|
||||||
|
@ -809,12 +810,12 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
|
||||||
|
|
||||||
This function requires the _imagingft service.
|
This function requires the _imagingft service.
|
||||||
|
|
||||||
:param font: A filename or file-like object containing a TrueType font.
|
:param font: A filepath or file-like object containing a TrueType font.
|
||||||
If the file is not found in this filename, the loader may also
|
If the file is not found at this filepath, the loader may also
|
||||||
search in other directories, such as the :file:`fonts/`
|
search in other directories, such as the :file:`%WINDIR%\fonts\`
|
||||||
directory on Windows or :file:`/Library/Fonts/`,
|
directory on Windows or :file:`/Library/Fonts/`,
|
||||||
:file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on
|
:file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/`
|
||||||
macOS.
|
on macOS.
|
||||||
|
|
||||||
:param size: The requested size, in points.
|
:param size: The requested size, in points.
|
||||||
:param index: Which font face to load (default is first available face).
|
:param index: Which font face to load (default is first available face).
|
||||||
|
@ -856,7 +857,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
|
||||||
except OSError:
|
except OSError:
|
||||||
if not isPath(font):
|
if not isPath(font):
|
||||||
raise
|
raise
|
||||||
ttf_filename = os.path.basename(font)
|
ttf_filename = os.fsdecode(os.path.basename(font))
|
||||||
|
|
||||||
dirs = []
|
dirs = []
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
|
|
|
@ -317,7 +317,7 @@ def _accept(prefix):
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename):
|
def _save(im, fp, filename):
|
||||||
if filename.endswith(".j2k"):
|
if os.fsdecode(os.path.splitext(filename)[1]) == ".j2k":
|
||||||
kind = "j2k"
|
kind = "j2k"
|
||||||
else:
|
else:
|
||||||
kind = "jp2"
|
kind = "jp2"
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
|
# Checks whether the given object is a string or path-like object that
|
||||||
|
# isn't also a file-like object
|
||||||
def isPath(f):
|
def isPath(f):
|
||||||
return isinstance(f, (bytes, str, Path))
|
return isinstance(f, (bytes, str, os.PathLike)) and not hasattr(f, "read")
|
||||||
|
|
||||||
|
|
||||||
# Checks if an object is a string, and that it points to a directory.
|
# Checks if an object is a string, and that it points to a directory.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user