mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-13 02:36:17 +03:00
Merge branch 'python-pillow-main'
This commit is contained in:
commit
c3cc621c67
2
.github/workflows/test-valgrind.yml
vendored
2
.github/workflows/test-valgrind.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
docker: [
|
docker: [
|
||||||
ubuntu-20.04-focal-amd64-valgrind,
|
ubuntu-22.04-jammy-amd64-valgrind,
|
||||||
]
|
]
|
||||||
dockerTag: [main]
|
dockerTag: [main]
|
||||||
|
|
||||||
|
|
|
@ -19,13 +19,13 @@ repos:
|
||||||
- id: yesqa
|
- id: yesqa
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||||
rev: v1.2.0
|
rev: v1.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: remove-tabs
|
- id: remove-tabs
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 4.0.1
|
rev: 5.0.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||||
|
|
15
CHANGES.rst
15
CHANGES.rst
|
@ -5,6 +5,21 @@ Changelog (Pillow)
|
||||||
9.3.0 (unreleased)
|
9.3.0 (unreleased)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
- Parse orientation from XMP tag contents #6463
|
||||||
|
[bigcat88, radarhere]
|
||||||
|
|
||||||
|
- Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457
|
||||||
|
[REDxEYE, radarhere]
|
||||||
|
|
||||||
|
- Do not clear GIF tile when checking number of frames #6455
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support saving multiple MPO frames #6444
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not double quote Pillow version for setuptools >= 60 #6450
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
- Added ABGR BMP mask mode #6436
|
- Added ABGR BMP mask mode #6436
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
|
|
@ -96,8 +96,8 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
## Binary Distributions
|
## Binary Distributions
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
* [ ] Contact `@cgohlke` for Windows binaries via release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174.
|
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
|
||||||
* [ ] Download and extract tarball from `@cgohlke` and copy into `dist/`
|
and copy into `dist/`
|
||||||
|
|
||||||
### Mac and Linux
|
### Mac and Linux
|
||||||
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
|
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
|
||||||
|
|
BIN
Tests/images/ati1.dds
Normal file
BIN
Tests/images/ati1.dds
Normal file
Binary file not shown.
BIN
Tests/images/ati1.png
Normal file
BIN
Tests/images/ati1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 969 B |
BIN
Tests/images/ati2.dds
Normal file
BIN
Tests/images/ati2.dds
Normal file
Binary file not shown.
BIN
Tests/images/comment_after_only_frame.gif
Normal file
BIN
Tests/images/comment_after_only_frame.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 B |
BIN
Tests/images/xmp_tags_orientation_exiftool.png
Normal file
BIN
Tests/images/xmp_tags_orientation_exiftool.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
|
@ -10,6 +10,8 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||||
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
|
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
|
||||||
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
|
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
|
||||||
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
|
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
|
||||||
|
TEST_FILE_ATI1 = "Tests/images/ati1.dds"
|
||||||
|
TEST_FILE_ATI2 = "Tests/images/ati2.dds"
|
||||||
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
|
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
|
||||||
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
|
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
|
||||||
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
|
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
|
||||||
|
@ -64,6 +66,32 @@ def test_sanity_dxt5():
|
||||||
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
|
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanity_ati1():
|
||||||
|
"""Check ATI1 images can be opened"""
|
||||||
|
|
||||||
|
with Image.open(TEST_FILE_ATI1) as im:
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
assert im.format == "DDS"
|
||||||
|
assert im.mode == "L"
|
||||||
|
assert im.size == (64, 64)
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanity_ati2():
|
||||||
|
"""Check ATI2 images can be opened"""
|
||||||
|
|
||||||
|
with Image.open(TEST_FILE_ATI2) as im:
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
assert im.format == "DDS"
|
||||||
|
assert im.mode == "RGB"
|
||||||
|
assert im.size == (256, 256)
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, TEST_FILE_DX10_BC5_UNORM.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("image_path", "expected_path"),
|
("image_path", "expected_path"),
|
||||||
(
|
(
|
||||||
|
|
|
@ -399,6 +399,11 @@ def test_no_change():
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
assert_image_equal(im, expected)
|
assert_image_equal(im, expected)
|
||||||
|
|
||||||
|
with Image.open("Tests/images/comment_after_only_frame.gif") as im:
|
||||||
|
expected = Image.new("P", (1, 1))
|
||||||
|
assert not im.is_animated
|
||||||
|
assert_image_equal(im, expected)
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror():
|
def test_eoferror():
|
||||||
with Image.open(TEST_GIF) as im:
|
with Image.open(TEST_GIF) as im:
|
||||||
|
|
|
@ -5,15 +5,19 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import assert_image_similar, is_pypy, skip_unless_feature
|
from .helper import (
|
||||||
|
assert_image_equal,
|
||||||
|
assert_image_similar,
|
||||||
|
is_pypy,
|
||||||
|
skip_unless_feature,
|
||||||
|
)
|
||||||
|
|
||||||
test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
|
test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
|
||||||
|
|
||||||
pytestmark = skip_unless_feature("jpg")
|
pytestmark = skip_unless_feature("jpg")
|
||||||
|
|
||||||
|
|
||||||
def frame_roundtrip(im, **options):
|
def roundtrip(im, **options):
|
||||||
# Note that for now, there is no MPO saving functionality
|
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
im.save(out, "MPO", **options)
|
im.save(out, "MPO", **options)
|
||||||
test_bytes = out.tell()
|
test_bytes = out.tell()
|
||||||
|
@ -237,13 +241,38 @@ def test_image_grab():
|
||||||
|
|
||||||
|
|
||||||
def test_save():
|
def test_save():
|
||||||
# Note that only individual frames can be saved at present
|
|
||||||
for test_file in test_files:
|
for test_file in test_files:
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
assert im.tell() == 0
|
assert im.tell() == 0
|
||||||
jpg0 = frame_roundtrip(im)
|
jpg0 = roundtrip(im)
|
||||||
assert_image_similar(im, jpg0, 30)
|
assert_image_similar(im, jpg0, 30)
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert im.tell() == 1
|
assert im.tell() == 1
|
||||||
jpg1 = frame_roundtrip(im)
|
jpg1 = roundtrip(im)
|
||||||
assert_image_similar(im, jpg1, 30)
|
assert_image_similar(im, jpg1, 30)
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_all():
|
||||||
|
for test_file in test_files:
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
im_reloaded = roundtrip(im, save_all=True)
|
||||||
|
|
||||||
|
im.seek(0)
|
||||||
|
assert_image_similar(im, im_reloaded, 30)
|
||||||
|
|
||||||
|
im.seek(1)
|
||||||
|
im_reloaded.seek(1)
|
||||||
|
assert_image_similar(im, im_reloaded, 30)
|
||||||
|
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
im2 = Image.new("RGB", (1, 1), "#f00")
|
||||||
|
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
|
||||||
|
|
||||||
|
assert_image_equal(im, im_reloaded)
|
||||||
|
|
||||||
|
im_reloaded.seek(1)
|
||||||
|
assert_image_similar(im2, im_reloaded, 1)
|
||||||
|
|
||||||
|
# Test that a single frame image will not be saved as an MPO
|
||||||
|
jpg = roundtrip(im, save_all=True)
|
||||||
|
assert "mp" not in jpg.info
|
||||||
|
|
|
@ -345,11 +345,15 @@ def test_exif_transpose():
|
||||||
check(orientation_im)
|
check(orientation_im)
|
||||||
|
|
||||||
# Orientation from "XML:com.adobe.xmp" info key
|
# Orientation from "XML:com.adobe.xmp" info key
|
||||||
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
|
for suffix in ("", "_exiftool"):
|
||||||
assert im.getexif()[0x0112] == 3
|
with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im:
|
||||||
|
assert im.getexif()[0x0112] == 3
|
||||||
|
|
||||||
transposed_im = ImageOps.exif_transpose(im)
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
assert 0x0112 not in transposed_im.getexif()
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
|
transposed_im._reload_exif()
|
||||||
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
# Orientation from "Raw profile type exif" info key
|
# Orientation from "Raw profile type exif" info key
|
||||||
# This test image has been manually hexedited from exif_imagemagick.png
|
# This test image has been manually hexedited from exif_imagemagick.png
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# install libimagequant
|
# install libimagequant
|
||||||
|
|
||||||
archive=libimagequant-4.0.0
|
archive=libimagequant-4.0.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
|
||||||
|
|
||||||
|
|
|
@ -1209,6 +1209,17 @@ image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL
|
||||||
methods may be used to read other pictures from the file. The pictures are
|
methods may be used to read other pictures from the file. The pictures are
|
||||||
zero-indexed and random access is supported.
|
zero-indexed and random access is supported.
|
||||||
|
|
||||||
|
When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default
|
||||||
|
only the first frame of a multiframe image will be saved. If the ``save_all``
|
||||||
|
argument is present and true, then all frames will be saved, and the following
|
||||||
|
option will also be available.
|
||||||
|
|
||||||
|
**append_images**
|
||||||
|
A list of images to append as additional pictures. Each of the
|
||||||
|
images in the list can be single or multiframe images.
|
||||||
|
|
||||||
|
.. versionadded:: 9.3.0
|
||||||
|
|
||||||
PCD
|
PCD
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
|
|
|
@ -15,35 +15,13 @@ Python Support
|
||||||
|
|
||||||
Pillow supports these Python versions.
|
Pillow supports these Python versions.
|
||||||
|
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
.. csv-table:: Newer versions
|
||||||
| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 |
|
:file: newer-versions.csv
|
||||||
+======================+=====+=====+=====+=====+=====+=====+=====+=====+
|
:header-rows: 1
|
||||||
| Pillow >= 9.0 | Yes | Yes | Yes | Yes | | | | |
|
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
| Pillow 8.3.2 - 8.4 | Yes | Yes | Yes | Yes | Yes | | | |
|
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | |
|
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | |
|
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
| Pillow 6.2.1 - 6.2.2 | | | Yes | Yes | Yes | Yes | | Yes |
|
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
| Pillow 6.0 - 6.2.0 | | | | Yes | Yes | Yes | | Yes |
|
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
| Pillow 5.2 - 5.4 | | | | Yes | Yes | Yes | Yes | Yes |
|
|
||||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
|
|
||||||
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
.. csv-table:: Older versions
|
||||||
| Python | 3.6 | 3.5 | 3.4 | 3.3 | 3.2 | 2.7 | 2.6 | 2.5 | 2.4 |
|
:file: older-versions.csv
|
||||||
+==================+=====+=====+=====+=====+=====+=====+=====+=====+=====+
|
:header-rows: 1
|
||||||
| Pillow 5.0 - 5.1 | Yes | Yes | Yes | | | Yes | | | |
|
|
||||||
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
| Pillow 4 | Yes | Yes | Yes | Yes | | Yes | | | |
|
|
||||||
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
| Pillow 2 - 3 | | Yes | Yes | Yes | Yes | Yes | Yes | | |
|
|
||||||
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
| Pillow < 2 | | | | | | Yes | Yes | Yes | Yes |
|
|
||||||
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|
|
||||||
|
|
||||||
Basic Installation
|
Basic Installation
|
||||||
------------------
|
------------------
|
||||||
|
@ -188,7 +166,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.0**
|
* Pillow has been tested with libimagequant **2.6-4.0.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.
|
||||||
|
|
6
docs/newer-versions.csv
Normal file
6
docs/newer-versions.csv
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5
|
||||||
|
Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,,
|
||||||
|
Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,,
|
||||||
|
Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes,
|
||||||
|
Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes,
|
||||||
|
Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes
|
|
8
docs/older-versions.csv
Normal file
8
docs/older-versions.csv
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
Python,3.8,3.7,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4
|
||||||
|
Pillow 6.2.1 - 6.2.2,Yes,Yes,Yes,Yes,,,,Yes,,,
|
||||||
|
Pillow 6.0 - 6.2.0,,Yes,Yes,Yes,,,,Yes,,,
|
||||||
|
Pillow 5.2 - 5.4,,Yes,Yes,Yes,Yes,,,Yes,,,
|
||||||
|
Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,,
|
||||||
|
Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,,
|
||||||
|
Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,,
|
||||||
|
Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes
|
|
59
docs/releasenotes/9.3.0.rst
Normal file
59
docs/releasenotes/9.3.0.rst
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
9.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
Backwards Incompatible Changes
|
||||||
|
==============================
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
Deprecations
|
||||||
|
============
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
API Changes
|
||||||
|
===========
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
API Additions
|
||||||
|
=============
|
||||||
|
|
||||||
|
Saving multiple MPO frames
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Multiple MPO frames can now be saved. Using the ``save_all`` argument, all of
|
||||||
|
an image's frames will be saved to file::
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
im = Image.open("frozenpond.mpo")
|
||||||
|
im.save(out, save_all=True)
|
||||||
|
|
||||||
|
Additional images can also be appended when saving, by combining the
|
||||||
|
``save_all`` argument with the ``append_images`` argument::
|
||||||
|
|
||||||
|
im.save(out, save_all=True, append_images=[im1, im2, ...])
|
||||||
|
|
||||||
|
|
||||||
|
Security
|
||||||
|
========
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
=============
|
||||||
|
|
||||||
|
Added DDS ATI1 and ATI2 reading
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Support has been added to read the ATI1 and ATI2 formats of DDS images.
|
|
@ -14,6 +14,7 @@ expected to be backported to earlier versions.
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
9.3.0
|
||||||
9.2.0
|
9.2.0
|
||||||
9.1.1
|
9.1.1
|
||||||
9.1.0
|
9.1.0
|
||||||
|
|
|
@ -158,6 +158,14 @@ class DdsImageFile(ImageFile.ImageFile):
|
||||||
elif fourcc == b"DXT5":
|
elif fourcc == b"DXT5":
|
||||||
self.pixel_format = "DXT5"
|
self.pixel_format = "DXT5"
|
||||||
n = 3
|
n = 3
|
||||||
|
elif fourcc == b"ATI1":
|
||||||
|
self.pixel_format = "BC4"
|
||||||
|
n = 4
|
||||||
|
self.mode = "L"
|
||||||
|
elif fourcc == b"ATI2":
|
||||||
|
self.pixel_format = "BC5"
|
||||||
|
n = 5
|
||||||
|
self.mode = "RGB"
|
||||||
elif fourcc == b"BC5S":
|
elif fourcc == b"BC5S":
|
||||||
self.pixel_format = "BC5S"
|
self.pixel_format = "BC5S"
|
||||||
n = 5
|
n = 5
|
||||||
|
|
|
@ -185,8 +185,6 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
if not s or s == b";":
|
if not s or s == b";":
|
||||||
raise EOFError
|
raise EOFError
|
||||||
|
|
||||||
self.tile = []
|
|
||||||
|
|
||||||
palette = None
|
palette = None
|
||||||
|
|
||||||
info = {}
|
info = {}
|
||||||
|
@ -295,6 +293,8 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
if not update_image:
|
if not update_image:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.tile = []
|
||||||
|
|
||||||
if self.dispose:
|
if self.dispose:
|
||||||
self.im.paste(self.dispose, self.dispose_extent)
|
self.im.paste(self.dispose, self.dispose_extent)
|
||||||
|
|
||||||
|
|
|
@ -1404,9 +1404,9 @@ class Image:
|
||||||
if 0x0112 not in self._exif:
|
if 0x0112 not in self._exif:
|
||||||
xmp_tags = self.info.get("XML:com.adobe.xmp")
|
xmp_tags = self.info.get("XML:com.adobe.xmp")
|
||||||
if xmp_tags:
|
if xmp_tags:
|
||||||
match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags)
|
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
|
||||||
if match:
|
if match:
|
||||||
self._exif[0x0112] = int(match[1])
|
self._exif[0x0112] = int(match[2])
|
||||||
|
|
||||||
return self._exif
|
return self._exif
|
||||||
|
|
||||||
|
|
|
@ -499,9 +499,14 @@ def _save(im, fp, tile, bufsize=0):
|
||||||
try:
|
try:
|
||||||
fh = fp.fileno()
|
fh = fp.fileno()
|
||||||
fp.flush()
|
fp.flush()
|
||||||
exc = None
|
_encode_tile(im, fp, tile, bufsize, fh)
|
||||||
except (AttributeError, io.UnsupportedOperation) as e:
|
except (AttributeError, io.UnsupportedOperation) as exc:
|
||||||
exc = e
|
_encode_tile(im, fp, tile, bufsize, None, exc)
|
||||||
|
if hasattr(fp, "flush"):
|
||||||
|
fp.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_tile(im, fp, tile, bufsize, fh, exc=None):
|
||||||
for e, b, o, a in tile:
|
for e, b, o, a in tile:
|
||||||
if o > 0:
|
if o > 0:
|
||||||
fp.seek(o)
|
fp.seek(o)
|
||||||
|
@ -526,8 +531,6 @@ def _save(im, fp, tile, bufsize=0):
|
||||||
raise OSError(f"encoder error {s} when writing image file") from exc
|
raise OSError(f"encoder error {s} when writing image file") from exc
|
||||||
finally:
|
finally:
|
||||||
encoder.cleanup()
|
encoder.cleanup()
|
||||||
if hasattr(fp, "flush"):
|
|
||||||
fp.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_read(fp, size):
|
def _safe_read(fp, size):
|
||||||
|
|
|
@ -601,10 +601,12 @@ def exif_transpose(image):
|
||||||
"Raw profile type exif"
|
"Raw profile type exif"
|
||||||
] = transposed_exif.tobytes().hex()
|
] = transposed_exif.tobytes().hex()
|
||||||
elif "XML:com.adobe.xmp" in transposed_image.info:
|
elif "XML:com.adobe.xmp" in transposed_image.info:
|
||||||
transposed_image.info["XML:com.adobe.xmp"] = re.sub(
|
for pattern in (
|
||||||
r'tiff:Orientation="([0-9])"',
|
r'tiff:Orientation="([0-9])"',
|
||||||
"",
|
r"<tiff:Orientation>([0-9])</tiff:Orientation>",
|
||||||
transposed_image.info["XML:com.adobe.xmp"],
|
):
|
||||||
)
|
transposed_image.info["XML:com.adobe.xmp"] = re.sub(
|
||||||
|
pattern, "", transposed_image.info["XML:com.adobe.xmp"]
|
||||||
|
)
|
||||||
return transposed_image
|
return transposed_image
|
||||||
return image.copy()
|
return image.copy()
|
||||||
|
|
|
@ -711,7 +711,7 @@ def _save(im, fp, filename):
|
||||||
qtables = getattr(im, "quantization", None)
|
qtables = getattr(im, "quantization", None)
|
||||||
qtables = validate_qtables(qtables)
|
qtables = validate_qtables(qtables)
|
||||||
|
|
||||||
extra = b""
|
extra = info.get("extra", b"")
|
||||||
|
|
||||||
icc_profile = info.get("icc_profile")
|
icc_profile = info.get("icc_profile")
|
||||||
if icc_profile:
|
if icc_profile:
|
||||||
|
|
|
@ -18,16 +18,66 @@
|
||||||
# See the README file for information on usage and redistribution.
|
# See the README file for information on usage and redistribution.
|
||||||
#
|
#
|
||||||
|
|
||||||
from . import Image, ImageFile, JpegImagePlugin
|
import itertools
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
|
from ._binary import o32le
|
||||||
|
|
||||||
# def _accept(prefix):
|
# def _accept(prefix):
|
||||||
# return JpegImagePlugin._accept(prefix)
|
# return JpegImagePlugin._accept(prefix)
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename):
|
def _save(im, fp, filename):
|
||||||
# Note that we can only save the current frame at present
|
JpegImagePlugin._save(im, fp, filename)
|
||||||
return JpegImagePlugin._save(im, fp, filename)
|
|
||||||
|
|
||||||
|
def _save_all(im, fp, filename):
|
||||||
|
append_images = im.encoderinfo.get("append_images", [])
|
||||||
|
if not append_images:
|
||||||
|
try:
|
||||||
|
animated = im.is_animated
|
||||||
|
except AttributeError:
|
||||||
|
animated = False
|
||||||
|
if not animated:
|
||||||
|
_save(im, fp, filename)
|
||||||
|
return
|
||||||
|
|
||||||
|
offsets = []
|
||||||
|
for imSequence in itertools.chain([im], append_images):
|
||||||
|
for im_frame in ImageSequence.Iterator(imSequence):
|
||||||
|
if not offsets:
|
||||||
|
# APP2 marker
|
||||||
|
im.encoderinfo["extra"] = (
|
||||||
|
b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70
|
||||||
|
)
|
||||||
|
JpegImagePlugin._save(im_frame, fp, filename)
|
||||||
|
offsets.append(fp.tell())
|
||||||
|
else:
|
||||||
|
im_frame.save(fp, "JPEG")
|
||||||
|
offsets.append(fp.tell() - offsets[-1])
|
||||||
|
|
||||||
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
ifd[0xB001] = len(offsets)
|
||||||
|
|
||||||
|
mpentries = b""
|
||||||
|
data_offset = 0
|
||||||
|
for i, size in enumerate(offsets):
|
||||||
|
if i == 0:
|
||||||
|
mptype = 0x030000 # Baseline MP Primary Image
|
||||||
|
else:
|
||||||
|
mptype = 0x000000 # Undefined
|
||||||
|
mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0)
|
||||||
|
if i == 0:
|
||||||
|
data_offset -= 28
|
||||||
|
data_offset += size
|
||||||
|
ifd[0xB002] = mpentries
|
||||||
|
|
||||||
|
fp.seek(28)
|
||||||
|
fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8))
|
||||||
|
fp.seek(0, os.SEEK_END)
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -124,6 +174,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
|
||||||
# Image.register_open(MpoImageFile.format,
|
# Image.register_open(MpoImageFile.format,
|
||||||
# JpegImagePlugin.jpeg_factory, _accept)
|
# JpegImagePlugin.jpeg_factory, _accept)
|
||||||
Image.register_save(MpoImageFile.format, _save)
|
Image.register_save(MpoImageFile.format, _save)
|
||||||
|
Image.register_save_all(MpoImageFile.format, _save_all)
|
||||||
|
|
||||||
Image.register_extension(MpoImageFile.format, ".mpo")
|
Image.register_extension(MpoImageFile.format, ".mpo")
|
||||||
|
|
||||||
|
|
|
@ -281,9 +281,9 @@ deps = {
|
||||||
"libs": [r"imagequant.lib"],
|
"libs": [r"imagequant.lib"],
|
||||||
},
|
},
|
||||||
"harfbuzz": {
|
"harfbuzz": {
|
||||||
"url": "https://github.com/harfbuzz/harfbuzz/archive/4.4.1.zip",
|
"url": "https://github.com/harfbuzz/harfbuzz/archive/5.1.0.zip",
|
||||||
"filename": "harfbuzz-4.4.1.zip",
|
"filename": "harfbuzz-5.1.0.zip",
|
||||||
"dir": "harfbuzz-4.4.1",
|
"dir": "harfbuzz-5.1.0",
|
||||||
"build": [
|
"build": [
|
||||||
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
|
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
|
||||||
cmd_nmake(target="clean"),
|
cmd_nmake(target="clean"),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user