mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-07-28 17:10:02 +03:00
Merge branch 'main' into qoi_write
This commit is contained in:
commit
2f5137fdce
2
.github/workflows/wheels-dependencies.sh
vendored
2
.github/workflows/wheels-dependencies.sh
vendored
|
@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main
|
||||||
FREETYPE_VERSION=2.13.3
|
FREETYPE_VERSION=2.13.3
|
||||||
HARFBUZZ_VERSION=11.2.1
|
HARFBUZZ_VERSION=11.2.1
|
||||||
LIBPNG_VERSION=1.6.48
|
LIBPNG_VERSION=1.6.48
|
||||||
JPEGTURBO_VERSION=3.1.0
|
JPEGTURBO_VERSION=3.1.1
|
||||||
OPENJPEG_VERSION=2.5.3
|
OPENJPEG_VERSION=2.5.3
|
||||||
XZ_VERSION=5.8.1
|
XZ_VERSION=5.8.1
|
||||||
TIFF_VERSION=4.7.0
|
TIFF_VERSION=4.7.0
|
||||||
|
|
Binary file not shown.
BIN
Tests/images/op_index.qoi
Normal file
BIN
Tests/images/op_index.qoi
Normal file
Binary file not shown.
|
@ -288,12 +288,13 @@ def test_non_integer_token(tmp_path: Path) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_header_token_too_long(tmp_path: Path) -> None:
|
@pytest.mark.parametrize("data", (b"P3\x0cAAAAAAAAAA\xee", b"P6\n 01234567890"))
|
||||||
|
def test_header_token_too_long(tmp_path: Path, data: bytes) -> None:
|
||||||
path = tmp_path / "temp.ppm"
|
path = tmp_path / "temp.ppm"
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(b"P6\n 01234567890")
|
f.write(data)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Token too long in file header: 01234567890"):
|
with pytest.raises(ValueError, match="Token too long in file header: "):
|
||||||
with Image.open(path):
|
with Image.open(path):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,12 @@ def test_invalid_file() -> None:
|
||||||
QoiImagePlugin.QoiImageFile(invalid_file)
|
QoiImagePlugin.QoiImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_op_index() -> None:
|
||||||
|
# QOI_OP_INDEX as the first chunk
|
||||||
|
with Image.open("Tests/images/op_index.qoi") as im:
|
||||||
|
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_save(tmp_path: Path) -> None:
|
def test_save(tmp_path: Path) -> None:
|
||||||
f = tmp_path / "temp.qoi"
|
f = tmp_path / "temp.qoi"
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ from PIL import (
|
||||||
ImageFile,
|
ImageFile,
|
||||||
JpegImagePlugin,
|
JpegImagePlugin,
|
||||||
TiffImagePlugin,
|
TiffImagePlugin,
|
||||||
|
TiffTags,
|
||||||
UnidentifiedImageError,
|
UnidentifiedImageError,
|
||||||
)
|
)
|
||||||
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
|
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
|
||||||
|
@ -900,6 +901,29 @@ class TestFileTiff:
|
||||||
assert description[0]["format"] == "image/tiff"
|
assert description[0]["format"] == "image/tiff"
|
||||||
assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
|
assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
|
||||||
|
|
||||||
|
def test_getxmp_undefined(self, tmp_path: Path) -> None:
|
||||||
|
tmpfile = tmp_path / "temp.tif"
|
||||||
|
im = Image.new("L", (1, 1))
|
||||||
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
ifd.tagtype[700] = TiffTags.UNDEFINED
|
||||||
|
with Image.open("Tests/images/lab.tif") as im_xmp:
|
||||||
|
ifd[700] = im_xmp.info["xmp"]
|
||||||
|
im.save(tmpfile, tiffinfo=ifd)
|
||||||
|
|
||||||
|
with Image.open(tmpfile) as im_reloaded:
|
||||||
|
if ElementTree is None:
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match="XMP data cannot be read without defusedxml dependency",
|
||||||
|
):
|
||||||
|
assert im_reloaded.getxmp() == {}
|
||||||
|
else:
|
||||||
|
assert "xmp" in im_reloaded.info
|
||||||
|
xmp = im_reloaded.getxmp()
|
||||||
|
|
||||||
|
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||||
|
assert description[0]["format"] == "image/tiff"
|
||||||
|
|
||||||
def test_get_photoshop_blocks(self) -> None:
|
def test_get_photoshop_blocks(self) -> None:
|
||||||
with Image.open("Tests/images/lab.tif") as im:
|
with Image.open("Tests/images/lab.tif") as im:
|
||||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
|
@ -974,6 +974,11 @@ class TestImage:
|
||||||
assert tag not in exif.get_ifd(0x8769)
|
assert tag not in exif.get_ifd(0x8769)
|
||||||
assert exif.get_ifd(0xA005)
|
assert exif.get_ifd(0xA005)
|
||||||
|
|
||||||
|
def test_exif_from_xmp_bytes(self) -> None:
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
im.info["xmp"] = b'\xff tiff:Orientation="2"'
|
||||||
|
assert im.getexif()[274] == 2
|
||||||
|
|
||||||
def test_empty_xmp(self) -> None:
|
def test_empty_xmp(self) -> None:
|
||||||
with Image.open("Tests/images/hopper.gif") as im:
|
with Image.open("Tests/images/hopper.gif") as im:
|
||||||
if ElementTree is None:
|
if ElementTree is None:
|
||||||
|
|
|
@ -783,9 +783,10 @@ def test_rectangle_I16(bbox: Coords) -> None:
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
draw.rectangle(bbox, outline=0xFFFF)
|
draw.rectangle(bbox, outline=0xCDEF)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
|
assert im.getpixel((X0, Y0)) == 0xCDEF
|
||||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff")
|
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -52,3 +52,17 @@ def test_tiff_crashes(test_file: str) -> None:
|
||||||
pytest.skip("test image not found")
|
pytest.skip("test image not found")
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_tiff_mmap() -> None:
|
||||||
|
try:
|
||||||
|
with Image.open("Tests/images/crash_mmap.tif") as im:
|
||||||
|
im.seek(1)
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
im.seek(0)
|
||||||
|
im.load()
|
||||||
|
except FileNotFoundError:
|
||||||
|
if on_ci():
|
||||||
|
raise
|
||||||
|
pytest.skip("test image not found")
|
||||||
|
|
|
@ -18,6 +18,9 @@ OpenType fonts (as well as other font formats supported by the FreeType
|
||||||
library). For earlier versions, TrueType support is only available as part of
|
library). For earlier versions, TrueType support is only available as part of
|
||||||
the imToolkit package.
|
the imToolkit package.
|
||||||
|
|
||||||
|
When measuring text sizes, this module will not break at newline characters. For
|
||||||
|
multiline text, see the :py:mod:`~PIL.ImageDraw` module.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
To protect against potential DOS attacks when using arbitrary strings as
|
To protect against potential DOS attacks when using arbitrary strings as
|
||||||
text input, Pillow will raise a :py:exc:`ValueError` if the number of characters
|
text input, Pillow will raise a :py:exc:`ValueError` if the number of characters
|
||||||
|
|
|
@ -1542,10 +1542,11 @@ class Image:
|
||||||
# XMP tags
|
# XMP tags
|
||||||
if ExifTags.Base.Orientation not in self._exif:
|
if ExifTags.Base.Orientation not in self._exif:
|
||||||
xmp_tags = self.info.get("XML:com.adobe.xmp")
|
xmp_tags = self.info.get("XML:com.adobe.xmp")
|
||||||
|
pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])'
|
||||||
if not xmp_tags and (xmp_tags := self.info.get("xmp")):
|
if not xmp_tags and (xmp_tags := self.info.get("xmp")):
|
||||||
xmp_tags = xmp_tags.decode("utf-8")
|
pattern = rb'tiff:Orientation(="|>)([0-9])'
|
||||||
if xmp_tags:
|
if xmp_tags:
|
||||||
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
|
match = re.search(pattern, xmp_tags)
|
||||||
if match:
|
if match:
|
||||||
self._exif[ExifTags.Base.Orientation] = int(match[2])
|
self._exif[ExifTags.Base.Orientation] = int(match[2])
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ class PcxImageFile(ImageFile.ImageFile):
|
||||||
# header
|
# header
|
||||||
assert self.fp is not None
|
assert self.fp is not None
|
||||||
|
|
||||||
s = self.fp.read(128)
|
s = self.fp.read(68)
|
||||||
if not _accept(s):
|
if not _accept(s):
|
||||||
msg = "not a PCX file"
|
msg = "not a PCX file"
|
||||||
raise SyntaxError(msg)
|
raise SyntaxError(msg)
|
||||||
|
@ -66,6 +66,8 @@ class PcxImageFile(ImageFile.ImageFile):
|
||||||
raise SyntaxError(msg)
|
raise SyntaxError(msg)
|
||||||
logger.debug("BBox: %s %s %s %s", *bbox)
|
logger.debug("BBox: %s %s %s %s", *bbox)
|
||||||
|
|
||||||
|
offset = self.fp.tell() + 60
|
||||||
|
|
||||||
# format
|
# format
|
||||||
version = s[1]
|
version = s[1]
|
||||||
bits = s[3]
|
bits = s[3]
|
||||||
|
@ -102,7 +104,6 @@ class PcxImageFile(ImageFile.ImageFile):
|
||||||
break
|
break
|
||||||
if mode == "P":
|
if mode == "P":
|
||||||
self.palette = ImagePalette.raw("RGB", s[1:])
|
self.palette = ImagePalette.raw("RGB", s[1:])
|
||||||
self.fp.seek(128)
|
|
||||||
|
|
||||||
elif version == 5 and bits == 8 and planes == 3:
|
elif version == 5 and bits == 8 and planes == 3:
|
||||||
mode = "RGB"
|
mode = "RGB"
|
||||||
|
@ -128,9 +129,7 @@ class PcxImageFile(ImageFile.ImageFile):
|
||||||
bbox = (0, 0) + self.size
|
bbox = (0, 0) + self.size
|
||||||
logger.debug("size: %sx%s", *self.size)
|
logger.debug("size: %sx%s", *self.size)
|
||||||
|
|
||||||
self.tile = [
|
self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))]
|
||||||
ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
|
@ -94,8 +94,8 @@ class PpmImageFile(ImageFile.ImageFile):
|
||||||
msg = "Reached EOF while reading header"
|
msg = "Reached EOF while reading header"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
elif len(token) > 10:
|
elif len(token) > 10:
|
||||||
msg = f"Token too long in file header: {token.decode()}"
|
msg_too_long = b"Token too long in file header: %s" % token
|
||||||
raise ValueError(msg)
|
raise ValueError(msg_too_long)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def _open(self) -> None:
|
def _open(self) -> None:
|
||||||
|
|
|
@ -54,7 +54,7 @@ class QoiDecoder(ImageFile.PyDecoder):
|
||||||
assert self.fd is not None
|
assert self.fd is not None
|
||||||
|
|
||||||
self._previously_seen_pixels = {}
|
self._previously_seen_pixels = {}
|
||||||
self._add_to_previous_pixels(bytearray((0, 0, 0, 255)))
|
self._previous_pixel = bytearray((0, 0, 0, 255))
|
||||||
|
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
bands = Image.getmodebands(self.mode)
|
bands = Image.getmodebands(self.mode)
|
||||||
|
|
|
@ -1217,9 +1217,10 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
return
|
return
|
||||||
self._seek(frame)
|
self._seek(frame)
|
||||||
if self._im is not None and (
|
if self._im is not None and (
|
||||||
self.im.size != self._tile_size or self.im.mode != self.mode
|
self.im.size != self._tile_size
|
||||||
|
or self.im.mode != self.mode
|
||||||
|
or self.readonly
|
||||||
):
|
):
|
||||||
# The core image will no longer be used
|
|
||||||
self._im = None
|
self._im = None
|
||||||
|
|
||||||
def _seek(self, frame: int) -> None:
|
def _seek(self, frame: int) -> None:
|
||||||
|
@ -1259,7 +1260,10 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
self.fp.seek(self._frame_pos[frame])
|
self.fp.seek(self._frame_pos[frame])
|
||||||
self.tag_v2.load(self.fp)
|
self.tag_v2.load(self.fp)
|
||||||
if XMP in self.tag_v2:
|
if XMP in self.tag_v2:
|
||||||
self.info["xmp"] = self.tag_v2[XMP]
|
xmp = self.tag_v2[XMP]
|
||||||
|
if isinstance(xmp, tuple) and len(xmp) == 1:
|
||||||
|
xmp = xmp[0]
|
||||||
|
self.info["xmp"] = xmp
|
||||||
elif "xmp" in self.info:
|
elif "xmp" in self.info:
|
||||||
del self.info["xmp"]
|
del self.info["xmp"]
|
||||||
self._reload_exif()
|
self._reload_exif()
|
||||||
|
|
|
@ -104,8 +104,6 @@ point32rgba(Imaging im, int x, int y, int ink) {
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
|
hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
|
||||||
int pixelwidth;
|
|
||||||
|
|
||||||
if (y0 >= 0 && y0 < im->ysize) {
|
if (y0 >= 0 && y0 < im->ysize) {
|
||||||
if (x0 < 0) {
|
if (x0 < 0) {
|
||||||
x0 = 0;
|
x0 = 0;
|
||||||
|
@ -118,20 +116,30 @@ hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
|
||||||
x1 = im->xsize - 1;
|
x1 = im->xsize - 1;
|
||||||
}
|
}
|
||||||
if (x0 <= x1) {
|
if (x0 <= x1) {
|
||||||
pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1;
|
int bigendian = -1;
|
||||||
if (mask == NULL) {
|
if (strncmp(im->mode, "I;16", 4) == 0) {
|
||||||
memset(
|
bigendian =
|
||||||
im->image8[y0] + x0 * pixelwidth,
|
(
|
||||||
(UINT8)ink,
|
#ifdef WORDS_BIGENDIAN
|
||||||
(x1 - x0 + 1) * pixelwidth
|
strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16L") == 0
|
||||||
);
|
#else
|
||||||
|
strcmp(im->mode, "I;16B") == 0
|
||||||
|
#endif
|
||||||
|
)
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
if (mask == NULL && bigendian == -1) {
|
||||||
|
memset(im->image8[y0] + x0, (UINT8)ink, (x1 - x0 + 1));
|
||||||
} else {
|
} else {
|
||||||
UINT8 *p = im->image8[y0];
|
UINT8 *p = im->image8[y0];
|
||||||
while (x0 <= x1) {
|
while (x0 <= x1) {
|
||||||
if (mask->image8[y0][x0]) {
|
if (mask == NULL || mask->image8[y0][x0]) {
|
||||||
p[x0 * pixelwidth] = ink;
|
if (bigendian == -1) {
|
||||||
if (pixelwidth == 2) {
|
p[x0] = ink;
|
||||||
p[x0 * pixelwidth + 1] = ink;
|
} else {
|
||||||
|
p[x0 * 2 + (bigendian ? 1 : 0)] = ink;
|
||||||
|
p[x0 * 2 + (bigendian ? 0 : 1)] = ink >> 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
x0++;
|
x0++;
|
||||||
|
|
|
@ -137,6 +137,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
im->read_only = view.readonly;
|
||||||
im->destroy = mapping_destroy_buffer;
|
im->destroy = mapping_destroy_buffer;
|
||||||
|
|
||||||
Py_INCREF(target);
|
Py_INCREF(target);
|
||||||
|
|
|
@ -114,7 +114,7 @@ V = {
|
||||||
"FREETYPE": "2.13.3",
|
"FREETYPE": "2.13.3",
|
||||||
"FRIBIDI": "1.0.16",
|
"FRIBIDI": "1.0.16",
|
||||||
"HARFBUZZ": "11.2.1",
|
"HARFBUZZ": "11.2.1",
|
||||||
"JPEGTURBO": "3.1.0",
|
"JPEGTURBO": "3.1.1",
|
||||||
"LCMS2": "2.17",
|
"LCMS2": "2.17",
|
||||||
"LIBAVIF": "1.3.0",
|
"LIBAVIF": "1.3.0",
|
||||||
"LIBIMAGEQUANT": "4.3.4",
|
"LIBIMAGEQUANT": "4.3.4",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user