mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-08-12 08:14:45 +03:00
Merge branch 'main' into closed
This commit is contained in:
commit
a2290358d5
12
CHANGES.rst
12
CHANGES.rst
|
@ -5,6 +5,18 @@ Changelog (Pillow)
|
||||||
9.5.0 (unreleased)
|
9.5.0 (unreleased)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
- Close OleFileIO instance when closing or exiting FPX or MIC #7005
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added __int__ to IFDRational for Python >= 3.11 #6998
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added memoryview support to Dib.frombytes() #6988
|
||||||
|
[radarhere, nulano]
|
||||||
|
|
||||||
|
- Close file pointer copy in the libtiff encoder if still open #6986
|
||||||
|
[fcarron, radarhere]
|
||||||
|
|
||||||
- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978
|
- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
HAS_UPLOADER = False
|
HAS_UPLOADER = False
|
||||||
|
|
||||||
if os.environ.get("SHOW_ERRORS", None):
|
if os.environ.get("SHOW_ERRORS"):
|
||||||
# local img.show for errors.
|
# local img.show for errors.
|
||||||
HAS_UPLOADER = True
|
HAS_UPLOADER = True
|
||||||
|
|
||||||
|
@ -271,7 +271,7 @@ def netpbm_available():
|
||||||
|
|
||||||
def magick_command():
|
def magick_command():
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
magickhome = os.environ.get("MAGICK_HOME", "")
|
magickhome = os.environ.get("MAGICK_HOME")
|
||||||
if magickhome:
|
if magickhome:
|
||||||
imagemagick = [os.path.join(magickhome, "convert.exe")]
|
imagemagick = [os.path.join(magickhome, "convert.exe")]
|
||||||
graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"]
|
graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"]
|
||||||
|
|
BIN
Tests/images/hopper.qoi
Normal file
BIN
Tests/images/hopper.qoi
Normal file
Binary file not shown.
BIN
Tests/images/pil123rgba.qoi
Normal file
BIN
Tests/images/pil123rgba.qoi
Normal file
Binary file not shown.
|
@ -56,6 +56,7 @@ def test_handler(tmp_path):
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im):
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
def save(self, im, fp, filename):
|
def save(self, im, fp, filename):
|
||||||
|
|
|
@ -60,6 +60,7 @@ def test_stub_deprecated():
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im):
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
handler = Handler()
|
handler = Handler()
|
||||||
|
|
|
@ -18,6 +18,16 @@ def test_sanity():
|
||||||
assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png")
|
assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png")
|
||||||
|
|
||||||
|
|
||||||
|
def test_close():
|
||||||
|
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
||||||
|
pass
|
||||||
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
im = Image.open("Tests/images/input_bw_one_band.fpx")
|
||||||
|
im.close()
|
||||||
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file():
|
||||||
# Test an invalid OLE file
|
# Test an invalid OLE file
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
|
@ -56,6 +56,7 @@ def test_handler(tmp_path):
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im):
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
def save(self, im, fp, filename):
|
def save(self, im, fp, filename):
|
||||||
|
|
|
@ -57,6 +57,7 @@ def test_handler(tmp_path):
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im):
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
def save(self, im, fp, filename):
|
def save(self, im, fp, filename):
|
||||||
|
|
|
@ -984,6 +984,36 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
) as im:
|
) as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
|
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"file_name, mode, size, tile",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"tiff_wrong_bits_per_sample.tiff",
|
||||||
|
"RGBA",
|
||||||
|
(52, 53),
|
||||||
|
[("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tiff_wrong_bits_per_sample_2.tiff",
|
||||||
|
"RGB",
|
||||||
|
(16, 16),
|
||||||
|
[("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tiff_wrong_bits_per_sample_3.tiff",
|
||||||
|
"RGBA",
|
||||||
|
(512, 256),
|
||||||
|
[("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
|
||||||
|
with Image.open("Tests/images/" + file_name) as im:
|
||||||
|
assert im.mode == mode
|
||||||
|
assert im.size == size
|
||||||
|
assert im.tile == tile
|
||||||
|
im.load()
|
||||||
|
|
||||||
def test_no_rows_per_strip(self):
|
def test_no_rows_per_strip(self):
|
||||||
# This image does not have a RowsPerStrip TIFF tag
|
# This image does not have a RowsPerStrip TIFF tag
|
||||||
infile = "Tests/images/no_rows_per_strip.tif"
|
infile = "Tests/images/no_rows_per_strip.tif"
|
||||||
|
@ -1071,3 +1101,21 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
for _ in range(10000):
|
for _ in range(10000):
|
||||||
im.save(out, compression="jpeg")
|
im.save(out, compression="jpeg")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path, sizes",
|
||||||
|
(
|
||||||
|
("Tests/images/hopper.tif", ()),
|
||||||
|
("Tests/images/child_ifd.tiff", (16, 8)),
|
||||||
|
("Tests/images/child_ifd_jpeg.tiff", (20,)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_get_child_images(self, path, sizes):
|
||||||
|
with Image.open(path) as im:
|
||||||
|
ims = im.get_child_images()
|
||||||
|
|
||||||
|
assert len(ims) == len(sizes)
|
||||||
|
for i, im in enumerate(ims):
|
||||||
|
w = sizes[i]
|
||||||
|
expected = Image.new("RGB", (w, w), "#f00")
|
||||||
|
assert_image_similar(im, expected, 1)
|
||||||
|
|
|
@ -51,6 +51,16 @@ def test_seek():
|
||||||
assert im.tell() == 0
|
assert im.tell() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_close():
|
||||||
|
with Image.open(TEST_FILE) as im:
|
||||||
|
pass
|
||||||
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
im = Image.open(TEST_FILE)
|
||||||
|
im.close()
|
||||||
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file():
|
||||||
# Test an invalid OLE file
|
# Test an invalid OLE file
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, PdfParser, features
|
from PIL import Image, PdfParser, features
|
||||||
|
|
||||||
from .helper import hopper, mark_if_feature_version
|
from .helper import hopper, mark_if_feature_version, skip_unless_feature
|
||||||
|
|
||||||
|
|
||||||
def helper_save_as_pdf(tmp_path, mode, **kwargs):
|
def helper_save_as_pdf(tmp_path, mode, **kwargs):
|
||||||
|
@ -42,6 +42,11 @@ def test_save(tmp_path, mode):
|
||||||
helper_save_as_pdf(tmp_path, mode)
|
helper_save_as_pdf(tmp_path, mode)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless_feature("jpg_2000")
|
||||||
|
def test_save_rgba(tmp_path):
|
||||||
|
helper_save_as_pdf(tmp_path, "RGBA")
|
||||||
|
|
||||||
|
|
||||||
def test_monochrome(tmp_path):
|
def test_monochrome(tmp_path):
|
||||||
# Arrange
|
# Arrange
|
||||||
mode = "1"
|
mode = "1"
|
||||||
|
|
28
Tests/test_file_qoi.py
Normal file
28
Tests/test_file_qoi.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from PIL import Image, QoiImagePlugin
|
||||||
|
|
||||||
|
from .helper import assert_image_equal_tofile, assert_image_similar_tofile
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanity():
|
||||||
|
with Image.open("Tests/images/hopper.qoi") as im:
|
||||||
|
assert im.mode == "RGB"
|
||||||
|
assert im.size == (128, 128)
|
||||||
|
assert im.format == "QOI"
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||||
|
|
||||||
|
with Image.open("Tests/images/pil123rgba.qoi") as im:
|
||||||
|
assert im.mode == "RGBA"
|
||||||
|
assert im.size == (162, 150)
|
||||||
|
assert im.format == "QOI"
|
||||||
|
|
||||||
|
assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_file():
|
||||||
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
QoiImagePlugin.QoiImageFile(invalid_file)
|
|
@ -84,24 +84,6 @@ class TestFileTiff:
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"path, sizes",
|
|
||||||
(
|
|
||||||
("Tests/images/hopper.tif", ()),
|
|
||||||
("Tests/images/child_ifd.tiff", (16, 8)),
|
|
||||||
("Tests/images/child_ifd_jpeg.tiff", (20,)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_get_child_images(self, path, sizes):
|
|
||||||
with Image.open(path) as im:
|
|
||||||
ims = im.get_child_images()
|
|
||||||
|
|
||||||
assert len(ims) == len(sizes)
|
|
||||||
for i, im in enumerate(ims):
|
|
||||||
w = sizes[i]
|
|
||||||
expected = Image.new("RGB", (w, w), "#f00")
|
|
||||||
assert_image_similar(im, expected, 1)
|
|
||||||
|
|
||||||
def test_mac_tiff(self):
|
def test_mac_tiff(self):
|
||||||
# Read RGBa images from macOS [@PIL136]
|
# Read RGBa images from macOS [@PIL136]
|
||||||
|
|
||||||
|
@ -118,36 +100,6 @@ class TestFileTiff:
|
||||||
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"file_name,mode,size,tile",
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"tiff_wrong_bits_per_sample.tiff",
|
|
||||||
"RGBA",
|
|
||||||
(52, 53),
|
|
||||||
[("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"tiff_wrong_bits_per_sample_2.tiff",
|
|
||||||
"RGB",
|
|
||||||
(16, 16),
|
|
||||||
[("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"tiff_wrong_bits_per_sample_3.tiff",
|
|
||||||
"RGBA",
|
|
||||||
(512, 256),
|
|
||||||
[("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
|
|
||||||
with Image.open("Tests/images/" + file_name) as im:
|
|
||||||
assert im.mode == mode
|
|
||||||
assert im.size == size
|
|
||||||
assert im.tile == tile
|
|
||||||
im.load()
|
|
||||||
|
|
||||||
def test_set_legacy_api(self):
|
def test_set_legacy_api(self):
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception) as e:
|
||||||
|
|
|
@ -254,17 +254,6 @@ def test_p2pa_palette():
|
||||||
assert im_pa.getpalette() == im.getpalette()
|
assert im_pa.getpalette() == im.getpalette()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
|
||||||
def test_rgb_lab(mode):
|
|
||||||
im = Image.new(mode, (1, 1))
|
|
||||||
converted_im = im.convert("LAB")
|
|
||||||
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
|
|
||||||
|
|
||||||
im = Image.new("LAB", (1, 1), (255, 0, 0))
|
|
||||||
converted_im = im.convert(mode)
|
|
||||||
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
|
|
||||||
|
|
||||||
|
|
||||||
def test_matrix_illegal_conversion():
|
def test_matrix_illegal_conversion():
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper("CMYK")
|
im = hopper("CMYK")
|
||||||
|
|
|
@ -625,3 +625,14 @@ def test_constants_deprecation():
|
||||||
for name in enum.__members__:
|
for name in enum.__members__:
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
assert getattr(ImageCms, prefix + name) == enum[name]
|
assert getattr(ImageCms, prefix + name) == enum[name]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
||||||
|
def test_rgb_lab(mode):
|
||||||
|
im = Image.new(mode, (1, 1))
|
||||||
|
converted_im = im.convert("LAB")
|
||||||
|
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
|
||||||
|
|
||||||
|
im = Image.new("LAB", (1, 1), (255, 0, 0))
|
||||||
|
converted_im = im.convert(mode)
|
||||||
|
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
|
||||||
|
|
|
@ -55,8 +55,8 @@ def test_show_without_viewers():
|
||||||
viewers = ImageShow._viewers
|
viewers = ImageShow._viewers
|
||||||
ImageShow._viewers = []
|
ImageShow._viewers = []
|
||||||
|
|
||||||
im = hopper()
|
with hopper() as im:
|
||||||
assert not ImageShow.show(im)
|
assert not ImageShow.show(im)
|
||||||
|
|
||||||
ImageShow._viewers = viewers
|
ImageShow._viewers = viewers
|
||||||
|
|
||||||
|
|
|
@ -100,8 +100,11 @@ class TestImageWinDib:
|
||||||
# Act
|
# Act
|
||||||
# Make one the same as the using tobytes()/frombytes()
|
# Make one the same as the using tobytes()/frombytes()
|
||||||
test_buffer = dib1.tobytes()
|
test_buffer = dib1.tobytes()
|
||||||
dib2.frombytes(test_buffer)
|
for datatype in ("bytes", "memoryview"):
|
||||||
|
if datatype == "memoryview":
|
||||||
|
test_buffer = memoryview(test_buffer)
|
||||||
|
dib2.frombytes(test_buffer)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
# Confirm they're the same
|
# Confirm they're the same
|
||||||
assert dib1.tobytes() == dib2.tobytes()
|
assert dib1.tobytes() == dib2.tobytes()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# install libimagequant
|
# install libimagequant
|
||||||
|
|
||||||
archive=libimagequant-4.1.0
|
archive=libimagequant-4.1.1
|
||||||
|
|
||||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||||
|
|
||||||
|
|
|
@ -1457,8 +1457,13 @@ PDF
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4
|
Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4
|
||||||
files, using either JPEG or HEX encoding depending on the image mode (and
|
files. Different encoding methods are used, depending on the image mode.
|
||||||
whether JPEG support is available or not).
|
|
||||||
|
* 1 mode images are saved using TIFF encoding, or JPEG encoding if libtiff support is
|
||||||
|
unavailable
|
||||||
|
* L, RGB and CMYK mode images use JPEG encoding
|
||||||
|
* P mode images use HEX encoding
|
||||||
|
* RGBA mode images use JPEG2000 encoding
|
||||||
|
|
||||||
.. _pdf-saving:
|
.. _pdf-saving:
|
||||||
|
|
||||||
|
@ -1544,6 +1549,13 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
|
||||||
|
|
||||||
.. versionadded:: 5.3.0
|
.. versionadded:: 5.3.0
|
||||||
|
|
||||||
|
QOI
|
||||||
|
^^^
|
||||||
|
|
||||||
|
.. versionadded:: 9.5.0
|
||||||
|
|
||||||
|
Pillow identifies and reads images in Quite OK Image format.
|
||||||
|
|
||||||
XV Thumbnails
|
XV Thumbnails
|
||||||
^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -169,7 +169,7 @@ Many of Pillow's features require external libraries:
|
||||||
|
|
||||||
* **libimagequant** provides improved color quantization
|
* **libimagequant** provides improved color quantization
|
||||||
|
|
||||||
* Pillow has been tested with libimagequant **2.6-4.1**
|
* Pillow has been tested with libimagequant **2.6-4.1.1**
|
||||||
* Libimagequant is licensed GPLv3, which is more restrictive than
|
* Libimagequant is licensed GPLv3, which is more restrictive than
|
||||||
the Pillow license, therefore we will not be distributing binaries
|
the Pillow license, therefore we will not be distributing binaries
|
||||||
with libimagequant support enabled.
|
with libimagequant support enabled.
|
||||||
|
|
|
@ -28,6 +28,11 @@ TODO
|
||||||
API Additions
|
API Additions
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
QOI file format
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Pillow can now read images in Quite OK Image format.
|
||||||
|
|
||||||
Added ``closed`` property to images
|
Added ``closed`` property to images
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -61,7 +66,7 @@ TODO
|
||||||
Other Changes
|
Other Changes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
TODO
|
Added support for saving PDFs in RGBA mode
|
||||||
^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
TODO
|
Using the JPXDecode filter, PDFs can now be saved in RGBA mode.
|
||||||
|
|
8
setup.py
8
setup.py
|
@ -242,7 +242,9 @@ def _find_include_dir(self, dirname, include):
|
||||||
return subdir
|
return subdir
|
||||||
|
|
||||||
|
|
||||||
def _cmd_exists(cmd):
|
def _cmd_exists(cmd: str) -> bool:
|
||||||
|
if "PATH" not in os.environ:
|
||||||
|
return False
|
||||||
return any(
|
return any(
|
||||||
os.access(os.path.join(path, cmd), os.X_OK)
|
os.access(os.path.join(path, cmd), os.X_OK)
|
||||||
for path in os.environ["PATH"].split(os.pathsep)
|
for path in os.environ["PATH"].split(os.pathsep)
|
||||||
|
@ -570,9 +572,7 @@ class pil_build_ext(build_ext):
|
||||||
):
|
):
|
||||||
for dirname in _find_library_dirs_ldconfig():
|
for dirname in _find_library_dirs_ldconfig():
|
||||||
_add_directory(library_dirs, dirname)
|
_add_directory(library_dirs, dirname)
|
||||||
if sys.platform.startswith("linux") and os.environ.get(
|
if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"):
|
||||||
"ANDROID_ROOT", None
|
|
||||||
):
|
|
||||||
# termux support for android.
|
# termux support for android.
|
||||||
# system libraries (zlib) are installed in /system/lib
|
# system libraries (zlib) are installed in /system/lib
|
||||||
# headers are at $PREFIX/include
|
# headers are at $PREFIX/include
|
||||||
|
|
|
@ -235,6 +235,14 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
return ImageFile.ImageFile.load(self)
|
return ImageFile.ImageFile.load(self)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.ole.close()
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.ole.close()
|
||||||
|
super().__exit__()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
|
@ -1012,7 +1012,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
|
||||||
if windir:
|
if windir:
|
||||||
dirs.append(os.path.join(windir, "fonts"))
|
dirs.append(os.path.join(windir, "fonts"))
|
||||||
elif sys.platform in ("linux", "linux2"):
|
elif sys.platform in ("linux", "linux2"):
|
||||||
lindirs = os.environ.get("XDG_DATA_DIRS", "")
|
lindirs = os.environ.get("XDG_DATA_DIRS")
|
||||||
if not lindirs:
|
if not lindirs:
|
||||||
# According to the freedesktop spec, XDG_DATA_DIRS should
|
# According to the freedesktop spec, XDG_DATA_DIRS should
|
||||||
# default to /usr/share
|
# default to /usr/share
|
||||||
|
|
|
@ -89,6 +89,14 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
|
||||||
def tell(self):
|
def tell(self):
|
||||||
return self.frame
|
return self.frame
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.ole.close()
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.ole.close()
|
||||||
|
super().__exit__()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
|
@ -173,6 +173,10 @@ def _save(im, fp, filename, save_all=False):
|
||||||
filter = "DCTDecode"
|
filter = "DCTDecode"
|
||||||
colorspace = PdfParser.PdfName("DeviceRGB")
|
colorspace = PdfParser.PdfName("DeviceRGB")
|
||||||
procset = "ImageC" # color images
|
procset = "ImageC" # color images
|
||||||
|
elif im.mode == "RGBA":
|
||||||
|
filter = "JPXDecode"
|
||||||
|
colorspace = PdfParser.PdfName("DeviceRGB")
|
||||||
|
procset = "ImageC" # color images
|
||||||
elif im.mode == "CMYK":
|
elif im.mode == "CMYK":
|
||||||
filter = "DCTDecode"
|
filter = "DCTDecode"
|
||||||
colorspace = PdfParser.PdfName("DeviceCMYK")
|
colorspace = PdfParser.PdfName("DeviceCMYK")
|
||||||
|
@ -199,6 +203,8 @@ def _save(im, fp, filename, save_all=False):
|
||||||
)
|
)
|
||||||
elif filter == "DCTDecode":
|
elif filter == "DCTDecode":
|
||||||
Image.SAVE["JPEG"](im, op, filename)
|
Image.SAVE["JPEG"](im, op, filename)
|
||||||
|
elif filter == "JPXDecode":
|
||||||
|
Image.SAVE["JPEG2000"](im, op, filename)
|
||||||
elif filter == "FlateDecode":
|
elif filter == "FlateDecode":
|
||||||
ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)])
|
ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)])
|
||||||
elif filter == "RunLengthDecode":
|
elif filter == "RunLengthDecode":
|
||||||
|
|
105
src/PIL/QoiImagePlugin.py
Normal file
105
src/PIL/QoiImagePlugin.py
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
#
|
||||||
|
# The Python Imaging Library.
|
||||||
|
#
|
||||||
|
# QOI support for PIL
|
||||||
|
#
|
||||||
|
# See the README file for information on usage and redistribution.
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from . import Image, ImageFile
|
||||||
|
from ._binary import i32be as i32
|
||||||
|
from ._binary import o8
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix):
|
||||||
|
return prefix[:4] == b"qoif"
|
||||||
|
|
||||||
|
|
||||||
|
class QoiImageFile(ImageFile.ImageFile):
|
||||||
|
format = "QOI"
|
||||||
|
format_description = "Quite OK Image"
|
||||||
|
|
||||||
|
def _open(self):
|
||||||
|
if not _accept(self.fp.read(4)):
|
||||||
|
msg = "not a QOI file"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
self._size = tuple(i32(self.fp.read(4)) for i in range(2))
|
||||||
|
|
||||||
|
channels = self.fp.read(1)[0]
|
||||||
|
self.mode = "RGB" if channels == 3 else "RGBA"
|
||||||
|
|
||||||
|
self.fp.seek(1, os.SEEK_CUR) # colorspace
|
||||||
|
self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)]
|
||||||
|
|
||||||
|
|
||||||
|
class QoiDecoder(ImageFile.PyDecoder):
|
||||||
|
_pulls_fd = True
|
||||||
|
|
||||||
|
def _add_to_previous_pixels(self, value):
|
||||||
|
self._previous_pixel = value
|
||||||
|
|
||||||
|
r, g, b, a = value
|
||||||
|
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
|
||||||
|
self._previously_seen_pixels[hash_value] = value
|
||||||
|
|
||||||
|
def decode(self, buffer):
|
||||||
|
self._previously_seen_pixels = {}
|
||||||
|
self._previous_pixel = None
|
||||||
|
self._add_to_previous_pixels(b"".join(o8(i) for i in (0, 0, 0, 255)))
|
||||||
|
|
||||||
|
data = bytearray()
|
||||||
|
bands = Image.getmodebands(self.mode)
|
||||||
|
while len(data) < self.state.xsize * self.state.ysize * bands:
|
||||||
|
byte = self.fd.read(1)[0]
|
||||||
|
if byte == 0b11111110: # QOI_OP_RGB
|
||||||
|
value = self.fd.read(3) + o8(255)
|
||||||
|
elif byte == 0b11111111: # QOI_OP_RGBA
|
||||||
|
value = self.fd.read(4)
|
||||||
|
else:
|
||||||
|
op = byte >> 6
|
||||||
|
if op == 0: # QOI_OP_INDEX
|
||||||
|
op_index = byte & 0b00111111
|
||||||
|
value = self._previously_seen_pixels.get(op_index, (0, 0, 0, 0))
|
||||||
|
elif op == 1: # QOI_OP_DIFF
|
||||||
|
value = (
|
||||||
|
(self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
|
||||||
|
% 256,
|
||||||
|
(self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2)
|
||||||
|
% 256,
|
||||||
|
(self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256,
|
||||||
|
)
|
||||||
|
value += (self._previous_pixel[3],)
|
||||||
|
elif op == 2: # QOI_OP_LUMA
|
||||||
|
second_byte = self.fd.read(1)[0]
|
||||||
|
diff_green = (byte & 0b00111111) - 32
|
||||||
|
diff_red = ((second_byte & 0b11110000) >> 4) - 8
|
||||||
|
diff_blue = (second_byte & 0b00001111) - 8
|
||||||
|
|
||||||
|
value = tuple(
|
||||||
|
(self._previous_pixel[i] + diff_green + diff) % 256
|
||||||
|
for i, diff in enumerate((diff_red, 0, diff_blue))
|
||||||
|
)
|
||||||
|
value += (self._previous_pixel[3],)
|
||||||
|
elif op == 3: # QOI_OP_RUN
|
||||||
|
run_length = (byte & 0b00111111) + 1
|
||||||
|
value = self._previous_pixel
|
||||||
|
if bands == 3:
|
||||||
|
value = value[:3]
|
||||||
|
data += value * run_length
|
||||||
|
continue
|
||||||
|
value = b"".join(o8(i) for i in value)
|
||||||
|
self._add_to_previous_pixels(value)
|
||||||
|
|
||||||
|
if bands == 3:
|
||||||
|
value = value[:3]
|
||||||
|
data += value
|
||||||
|
self.set_as_raw(bytes(data))
|
||||||
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
|
||||||
|
Image.register_decoder("qoi", QoiDecoder)
|
||||||
|
Image.register_extension(QoiImageFile.format, ".qoi")
|
|
@ -425,6 +425,9 @@ class IFDRational(Rational):
|
||||||
__ceil__ = _delegate("__ceil__")
|
__ceil__ = _delegate("__ceil__")
|
||||||
__floor__ = _delegate("__floor__")
|
__floor__ = _delegate("__floor__")
|
||||||
__round__ = _delegate("__round__")
|
__round__ = _delegate("__round__")
|
||||||
|
# Python >= 3.11
|
||||||
|
if hasattr(Fraction, "__int__"):
|
||||||
|
__int__ = _delegate("__int__")
|
||||||
|
|
||||||
|
|
||||||
class ImageFileDirectory_v2(MutableMapping):
|
class ImageFileDirectory_v2(MutableMapping):
|
||||||
|
|
|
@ -59,6 +59,7 @@ _plugins = [
|
||||||
"PngImagePlugin",
|
"PngImagePlugin",
|
||||||
"PpmImagePlugin",
|
"PpmImagePlugin",
|
||||||
"PsdImagePlugin",
|
"PsdImagePlugin",
|
||||||
|
"QoiImagePlugin",
|
||||||
"SgiImagePlugin",
|
"SgiImagePlugin",
|
||||||
"SpiderImagePlugin",
|
"SpiderImagePlugin",
|
||||||
"SunImagePlugin",
|
"SunImagePlugin",
|
||||||
|
|
14
src/_webp.c
14
src/_webp.c
|
@ -955,6 +955,13 @@ addTransparencyFlagToModule(PyObject *m) {
|
||||||
|
|
||||||
static int
|
static int
|
||||||
setup_module(PyObject *m) {
|
setup_module(PyObject *m) {
|
||||||
|
#ifdef HAVE_WEBPANIM
|
||||||
|
/* Ready object types */
|
||||||
|
if (PyType_Ready(&WebPAnimDecoder_Type) < 0 ||
|
||||||
|
PyType_Ready(&WebPAnimEncoder_Type) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
PyObject *d = PyModule_GetDict(m);
|
PyObject *d = PyModule_GetDict(m);
|
||||||
addMuxFlagToModule(m);
|
addMuxFlagToModule(m);
|
||||||
addAnimFlagToModule(m);
|
addAnimFlagToModule(m);
|
||||||
|
@ -963,13 +970,6 @@ setup_module(PyObject *m) {
|
||||||
PyDict_SetItemString(
|
PyDict_SetItemString(
|
||||||
d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str()));
|
d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str()));
|
||||||
|
|
||||||
#ifdef HAVE_WEBPANIM
|
|
||||||
/* Ready object types */
|
|
||||||
if (PyType_Ready(&WebPAnimDecoder_Type) < 0 ||
|
|
||||||
PyType_Ready(&WebPAnimEncoder_Type) < 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -195,20 +195,21 @@ _releasedc(ImagingDisplayObject *display, PyObject *args) {
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
_frombytes(ImagingDisplayObject *display, PyObject *args) {
|
_frombytes(ImagingDisplayObject *display, PyObject *args) {
|
||||||
char *ptr;
|
Py_buffer buffer;
|
||||||
Py_ssize_t bytes;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) {
|
if (!PyArg_ParseTuple(args, "y*:frombytes", &buffer)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display->dib->ysize * display->dib->linesize != bytes) {
|
if (display->dib->ysize * display->dib->linesize != buffer.len) {
|
||||||
|
PyBuffer_Release(&buffer);
|
||||||
PyErr_SetString(PyExc_ValueError, "wrong size");
|
PyErr_SetString(PyExc_ValueError, "wrong size");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(display->dib->bits, ptr, bytes);
|
memcpy(display->dib->bits, buffer.buf, buffer.len);
|
||||||
|
|
||||||
|
PyBuffer_Release(&buffer);
|
||||||
Py_INCREF(Py_None);
|
Py_INCREF(Py_None);
|
||||||
return Py_None;
|
return Py_None;
|
||||||
}
|
}
|
||||||
|
|
|
@ -487,6 +487,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
|
||||||
goto quick_exit;
|
goto quick_exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strcmp(im->mode, "RGBA") == 0) {
|
||||||
|
image->comps[3].alpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
opj_set_error_handler(codec, j2k_error, context);
|
opj_set_error_handler(codec, j2k_error, context);
|
||||||
opj_set_info_handler(codec, j2k_warn, context);
|
opj_set_info_handler(codec, j2k_warn, context);
|
||||||
opj_set_warning_handler(codec, j2k_warn, context);
|
opj_set_warning_handler(codec, j2k_warn, context);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user