Merge remote-tracking branch 'upstream/master' into docs-imageshow
5
.github/workflows/test-windows.yml
vendored
|
@ -52,6 +52,11 @@ jobs:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
architecture: ${{ matrix.architecture }}
|
architecture: ${{ matrix.architecture }}
|
||||||
|
|
||||||
|
- name: Set up TCL
|
||||||
|
if: "contains(matrix.python-version, 'pypy')"
|
||||||
|
run: Write-Host "::set-env name=TCL_LIBRARY::$env:pythonLocation\tcl\tcl8.5"
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
- name: Print build system information
|
- name: Print build system information
|
||||||
run: python .github/workflows/system-info.py
|
run: python .github/workflows/system-info.py
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/variation_adobe_older_harfbuzz.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/variation_adobe_older_harfbuzz_axes.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/variation_adobe_older_harfbuzz_name.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
|
@ -1,3 +1,5 @@
|
||||||
|
import io
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image, WebPImagePlugin
|
from PIL import Image, WebPImagePlugin
|
||||||
|
|
||||||
|
@ -54,15 +56,10 @@ class TestFileWebp:
|
||||||
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
||||||
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
|
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
|
||||||
|
|
||||||
def test_write_rgb(self, tmp_path):
|
def _roundtrip(self, tmp_path, mode, epsilon, args={}):
|
||||||
"""
|
|
||||||
Can we write a RGB mode file to webp without error.
|
|
||||||
Does it have the bits we expect?
|
|
||||||
"""
|
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = str(tmp_path / "temp.webp")
|
||||||
|
|
||||||
hopper(self.rgb_mode).save(temp_file)
|
hopper(mode).save(temp_file, **args)
|
||||||
with Image.open(temp_file) as image:
|
with Image.open(temp_file) as image:
|
||||||
assert image.mode == self.rgb_mode
|
assert image.mode == self.rgb_mode
|
||||||
assert image.size == (128, 128)
|
assert image.size == (128, 128)
|
||||||
|
@ -70,6 +67,7 @@ class TestFileWebp:
|
||||||
image.load()
|
image.load()
|
||||||
image.getdata()
|
image.getdata()
|
||||||
|
|
||||||
|
if mode == self.rgb_mode:
|
||||||
# generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm
|
# generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm
|
||||||
assert_image_similar_tofile(
|
assert_image_similar_tofile(
|
||||||
image, "Tests/images/hopper_webp_write.ppm", 12.0
|
image, "Tests/images/hopper_webp_write.ppm", 12.0
|
||||||
|
@ -78,10 +76,29 @@ class TestFileWebp:
|
||||||
# This test asserts that the images are similar. If the average pixel
|
# This test asserts that the images are similar. If the average pixel
|
||||||
# difference between the two images is less than the epsilon value,
|
# difference between the two images is less than the epsilon value,
|
||||||
# then we're going to accept that it's a reasonable lossy version of
|
# then we're going to accept that it's a reasonable lossy version of
|
||||||
# the image. The old lena images for WebP are showing ~16 on
|
# the image.
|
||||||
# Ubuntu, the jpegs are showing ~18.
|
target = hopper(mode)
|
||||||
target = hopper(self.rgb_mode)
|
if mode != self.rgb_mode:
|
||||||
assert_image_similar(image, target, 12.0)
|
target = target.convert(self.rgb_mode)
|
||||||
|
assert_image_similar(image, target, epsilon)
|
||||||
|
|
||||||
|
def test_write_rgb(self, tmp_path):
|
||||||
|
"""
|
||||||
|
Can we write a RGB mode file to webp without error?
|
||||||
|
Does it have the bits we expect?
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._roundtrip(tmp_path, self.rgb_mode, 12.5)
|
||||||
|
|
||||||
|
def test_write_method(self, tmp_path):
|
||||||
|
self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6})
|
||||||
|
|
||||||
|
buffer_no_args = io.BytesIO()
|
||||||
|
hopper().save(buffer_no_args, format="WEBP")
|
||||||
|
|
||||||
|
buffer_method = io.BytesIO()
|
||||||
|
hopper().save(buffer_method, format="WEBP", method=6)
|
||||||
|
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
|
||||||
|
|
||||||
def test_write_unsupported_mode_L(self, tmp_path):
|
def test_write_unsupported_mode_L(self, tmp_path):
|
||||||
"""
|
"""
|
||||||
|
@ -89,18 +106,7 @@ class TestFileWebp:
|
||||||
similar to the original file.
|
similar to the original file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
self._roundtrip(tmp_path, "L", 10.0)
|
||||||
hopper("L").save(temp_file)
|
|
||||||
with Image.open(temp_file) as image:
|
|
||||||
assert image.mode == self.rgb_mode
|
|
||||||
assert image.size == (128, 128)
|
|
||||||
assert image.format == "WEBP"
|
|
||||||
|
|
||||||
image.load()
|
|
||||||
image.getdata()
|
|
||||||
target = hopper("L").convert(self.rgb_mode)
|
|
||||||
|
|
||||||
assert_image_similar(image, target, 10.0)
|
|
||||||
|
|
||||||
def test_write_unsupported_mode_P(self, tmp_path):
|
def test_write_unsupported_mode_P(self, tmp_path):
|
||||||
"""
|
"""
|
||||||
|
@ -108,18 +114,7 @@ class TestFileWebp:
|
||||||
similar to the original file.
|
similar to the original file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
self._roundtrip(tmp_path, "P", 50.0)
|
||||||
hopper("P").save(temp_file)
|
|
||||||
with Image.open(temp_file) as image:
|
|
||||||
assert image.mode == self.rgb_mode
|
|
||||||
assert image.size == (128, 128)
|
|
||||||
assert image.format == "WEBP"
|
|
||||||
|
|
||||||
image.load()
|
|
||||||
image.getdata()
|
|
||||||
target = hopper("P").convert(self.rgb_mode)
|
|
||||||
|
|
||||||
assert_image_similar(image, target, 50.0)
|
|
||||||
|
|
||||||
def test_WebPEncode_with_invalid_args(self):
|
def test_WebPEncode_with_invalid_args(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -13,7 +13,6 @@ from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
assert_image_similar,
|
assert_image_similar,
|
||||||
assert_image_similar_tofile,
|
assert_image_similar_tofile,
|
||||||
is_mingw,
|
|
||||||
is_pypy,
|
is_pypy,
|
||||||
is_win32,
|
is_win32,
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
|
@ -661,7 +660,22 @@ class TestImageFont:
|
||||||
{"name": b"Size", "minimum": 0, "maximum": 300, "default": 0}
|
{"name": b"Size", "minimum": 0, "maximum": 300, "default": 0}
|
||||||
]
|
]
|
||||||
|
|
||||||
@pytest.mark.skipif(is_mingw(), reason="epsilon too high for meaningful test")
|
def _check_text(self, font, path, epsilon):
|
||||||
|
im = Image.new("RGB", (100, 75), "white")
|
||||||
|
d = ImageDraw.Draw(im)
|
||||||
|
d.text((10, 10), "Text", font=font, fill="black")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with Image.open(path) as expected:
|
||||||
|
assert_image_similar(im, expected, epsilon)
|
||||||
|
except AssertionError:
|
||||||
|
if "_adobe" in path:
|
||||||
|
path = path.replace("_adobe", "_adobe_older_harfbuzz")
|
||||||
|
with Image.open(path) as expected:
|
||||||
|
assert_image_similar(im, expected, epsilon)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
def test_variation_set_by_name(self):
|
def test_variation_set_by_name(self):
|
||||||
font = self.get_font()
|
font = self.get_font()
|
||||||
|
|
||||||
|
@ -674,27 +688,18 @@ class TestImageFont:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
font.set_variation_by_name("Bold")
|
font.set_variation_by_name("Bold")
|
||||||
|
|
||||||
def _check_text(font, path, epsilon):
|
|
||||||
im = Image.new("RGB", (100, 75), "white")
|
|
||||||
d = ImageDraw.Draw(im)
|
|
||||||
d.text((10, 10), "Text", font=font, fill="black")
|
|
||||||
|
|
||||||
with Image.open(path) as expected:
|
|
||||||
assert_image_similar(im, expected, epsilon)
|
|
||||||
|
|
||||||
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
|
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
|
||||||
_check_text(font, "Tests/images/variation_adobe.png", 11)
|
self._check_text(font, "Tests/images/variation_adobe.png", 11)
|
||||||
for name in ["Bold", b"Bold"]:
|
for name in ["Bold", b"Bold"]:
|
||||||
font.set_variation_by_name(name)
|
font.set_variation_by_name(name)
|
||||||
_check_text(font, "Tests/images/variation_adobe_name.png", 11)
|
self._check_text(font, "Tests/images/variation_adobe_name.png", 11)
|
||||||
|
|
||||||
font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
|
font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
|
||||||
_check_text(font, "Tests/images/variation_tiny.png", 40)
|
self._check_text(font, "Tests/images/variation_tiny.png", 40)
|
||||||
for name in ["200", b"200"]:
|
for name in ["200", b"200"]:
|
||||||
font.set_variation_by_name(name)
|
font.set_variation_by_name(name)
|
||||||
_check_text(font, "Tests/images/variation_tiny_name.png", 40)
|
self._check_text(font, "Tests/images/variation_tiny_name.png", 40)
|
||||||
|
|
||||||
@pytest.mark.skipif(is_mingw(), reason="epsilon too high for meaningful test")
|
|
||||||
def test_variation_set_by_axes(self):
|
def test_variation_set_by_axes(self):
|
||||||
font = self.get_font()
|
font = self.get_font()
|
||||||
|
|
||||||
|
@ -707,21 +712,13 @@ class TestImageFont:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
font.set_variation_by_axes([500, 50])
|
font.set_variation_by_axes([500, 50])
|
||||||
|
|
||||||
def _check_text(font, path, epsilon):
|
|
||||||
im = Image.new("RGB", (100, 75), "white")
|
|
||||||
d = ImageDraw.Draw(im)
|
|
||||||
d.text((10, 10), "Text", font=font, fill="black")
|
|
||||||
|
|
||||||
with Image.open(path) as expected:
|
|
||||||
assert_image_similar(im, expected, epsilon)
|
|
||||||
|
|
||||||
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
|
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
|
||||||
font.set_variation_by_axes([500, 50])
|
font.set_variation_by_axes([500, 50])
|
||||||
_check_text(font, "Tests/images/variation_adobe_axes.png", 5.1)
|
self._check_text(font, "Tests/images/variation_adobe_axes.png", 5.1)
|
||||||
|
|
||||||
font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
|
font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
|
||||||
font.set_variation_by_axes([100])
|
font.set_variation_by_axes([100])
|
||||||
_check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
|
self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("raqm")
|
@skip_unless_feature("raqm")
|
||||||
|
|
|
@ -17,19 +17,21 @@ def test_register():
|
||||||
ImageShow._viewers.pop()
|
ImageShow._viewers.pop()
|
||||||
|
|
||||||
|
|
||||||
def test_viewer_show():
|
@pytest.mark.parametrize(
|
||||||
|
"order", [-1, 0],
|
||||||
|
)
|
||||||
|
def test_viewer_show(order):
|
||||||
class TestViewer(ImageShow.Viewer):
|
class TestViewer(ImageShow.Viewer):
|
||||||
methodCalled = False
|
|
||||||
|
|
||||||
def show_image(self, image, **options):
|
def show_image(self, image, **options):
|
||||||
self.methodCalled = True
|
self.methodCalled = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
viewer = TestViewer()
|
viewer = TestViewer()
|
||||||
ImageShow.register(viewer, -1)
|
ImageShow.register(viewer, order)
|
||||||
|
|
||||||
for mode in ("1", "I;16", "LA", "RGB", "RGBA"):
|
for mode in ("1", "I;16", "LA", "RGB", "RGBA"):
|
||||||
with hopper() as im:
|
viewer.methodCalled = False
|
||||||
|
with hopper(mode) as im:
|
||||||
assert ImageShow.show(im)
|
assert ImageShow.show(im)
|
||||||
assert viewer.methodCalled
|
assert viewer.methodCalled
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ Many of Pillow's features require external libraries:
|
||||||
* **littlecms** provides color management
|
* **littlecms** provides color management
|
||||||
|
|
||||||
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
|
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
|
||||||
above uses liblcms2. Tested with **1.19** and **2.7-2.9**.
|
above uses liblcms2. Tested with **1.19** and **2.7-2.11**.
|
||||||
|
|
||||||
* **libwebp** provides the WebP format.
|
* **libwebp** provides the WebP format.
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ or the clipboard to a PIL image memory.
|
||||||
|
|
||||||
.. versionadded:: 1.1.3
|
.. versionadded:: 1.1.3
|
||||||
|
|
||||||
.. py:function:: PIL.ImageGrab.grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None)
|
.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None)
|
||||||
|
|
||||||
Take a snapshot of the screen. The pixels inside the bounding box are
|
Take a snapshot of the screen. The pixels inside the bounding box are
|
||||||
returned as an "RGBA" on macOS, or an "RGB" image otherwise.
|
returned as an "RGBA" on macOS, or an "RGB" image otherwise.
|
||||||
|
@ -26,12 +26,15 @@ or the clipboard to a PIL image memory.
|
||||||
|
|
||||||
.. versionadded:: 6.2.0
|
.. versionadded:: 6.2.0
|
||||||
|
|
||||||
:param xdisplay: X11 Display address. Pass ``None`` to grab the default system screen. Pass ``""`` to grab the default X11 screen on Windows or macOS.
|
:param xdisplay:
|
||||||
|
X11 Display address. Pass ``None`` to grab the default system screen. Pass ``""`` to grab the default X11 screen on Windows or macOS.
|
||||||
|
|
||||||
|
You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``.
|
||||||
|
|
||||||
.. versionadded:: 7.1.0
|
.. versionadded:: 7.1.0
|
||||||
:return: An image
|
:return: An image
|
||||||
|
|
||||||
.. py:function:: PIL.ImageGrab.grabclipboard()
|
.. py:function:: grabclipboard()
|
||||||
|
|
||||||
Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported.
|
Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported.
|
||||||
|
|
||||||
|
|
60
docs/reference/features.rst
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
.. py:module:: PIL.features
|
||||||
|
.. py:currentmodule:: PIL.features
|
||||||
|
|
||||||
|
:py:mod:`features` Module
|
||||||
|
==========================
|
||||||
|
|
||||||
|
The :py:mod:`PIL.features` module can be used to detect which Pillow features are available on your system.
|
||||||
|
|
||||||
|
.. autofunction:: PIL.features.pilinfo
|
||||||
|
.. autofunction:: PIL.features.check
|
||||||
|
.. autofunction:: PIL.features.get_supported
|
||||||
|
|
||||||
|
Modules
|
||||||
|
-------
|
||||||
|
|
||||||
|
Support for the following modules can be checked:
|
||||||
|
|
||||||
|
* ``pil``: The Pillow core module, required for all functionality.
|
||||||
|
* ``tkinter``: Tkinter support.
|
||||||
|
* ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`.
|
||||||
|
* ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`.
|
||||||
|
* ``webp``: WebP image support.
|
||||||
|
|
||||||
|
.. autofunction:: PIL.features.check_module
|
||||||
|
.. autofunction:: PIL.features.get_supported_modules
|
||||||
|
|
||||||
|
Codecs
|
||||||
|
------
|
||||||
|
|
||||||
|
These are only checked during Pillow compilation.
|
||||||
|
If the required library was uninstalled from the system, the ``pil`` core module may fail to load instead.
|
||||||
|
|
||||||
|
Support for the following codecs can be checked:
|
||||||
|
|
||||||
|
* ``jpg``: (compile time) Libjpeg support, required for JPEG based image formats.
|
||||||
|
* ``jpg_2000``: (compile time) OpenJPEG support, required for JPEG 2000 image formats.
|
||||||
|
* ``zlib``: (compile time) Zlib support, required for zlib compressed formats, such as PNG.
|
||||||
|
* ``libtiff``: (compile time) LibTIFF support, required for TIFF based image formats.
|
||||||
|
|
||||||
|
.. autofunction:: PIL.features.check_codec
|
||||||
|
.. autofunction:: PIL.features.get_supported_codecs
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
Some of these are only checked during Pillow compilation.
|
||||||
|
If the required library was uninstalled from the system, the relevant module may fail to load instead.
|
||||||
|
|
||||||
|
Support for the following features can be checked:
|
||||||
|
|
||||||
|
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg.
|
||||||
|
* ``transp_webp``: Support for transparency in WebP images.
|
||||||
|
* ``webp_mux``: (compile time) Support for EXIF data in WebP images.
|
||||||
|
* ``webp_anim``: (compile time) Support for animated WebP images.
|
||||||
|
* ``raqm``: Raqm library, required for ``ImageFont.LAYOUT_RAQM`` in :py:func:`PIL.ImageFont.truetype`.
|
||||||
|
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`.
|
||||||
|
* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library.
|
||||||
|
|
||||||
|
.. autofunction:: PIL.features.check_feature
|
||||||
|
.. autofunction:: PIL.features.get_supported_features
|
|
@ -32,6 +32,7 @@ Reference
|
||||||
PSDraw
|
PSDraw
|
||||||
PixelAccess
|
PixelAccess
|
||||||
PyAccess
|
PyAccess
|
||||||
|
features
|
||||||
../PIL
|
../PIL
|
||||||
plugins
|
plugins
|
||||||
internal_design
|
internal_design
|
||||||
|
|
29
docs/releasenotes/7.2.0.rst
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
7.2.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
API Changes
|
||||||
|
===========
|
||||||
|
|
||||||
|
Replaced TiffImagePlugin DEBUG with logging
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
``TiffImagePlugin.DEBUG = True`` has been a way to print various debugging
|
||||||
|
information when interacting with TIFF images. This has now been removed
|
||||||
|
in favour of Python's ``logging`` module, already used in other places in the
|
||||||
|
Pillow source code.
|
||||||
|
|
||||||
|
Corrected default offset when writing EXIF data
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Previously, the default ``offset`` argument for
|
||||||
|
:py:meth:`~PIL.Image.Exif.tobytes` was 0, which did not include the magic
|
||||||
|
header. It is now 8.
|
||||||
|
|
||||||
|
Moved to ImageFileDirectory_v2 in Image.Exif
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Moved from the legacy :py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v1` to
|
||||||
|
:py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v2` in
|
||||||
|
:py:class:`PIL.Image.Exif`. This means that Exif RATIONALs and SIGNED_RATIONALs
|
||||||
|
are now read as :py:class:`PIL.TiffImagePlugin.IFDRational`, instead of as a
|
||||||
|
tuple with a numerator and a denominator.
|
|
@ -6,6 +6,7 @@ Release Notes
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
7.2.0
|
||||||
7.1.2
|
7.1.2
|
||||||
7.1.1
|
7.1.1
|
||||||
7.1.0
|
7.1.0
|
||||||
|
|
|
@ -1051,10 +1051,12 @@ class Image:
|
||||||
of colors.
|
of colors.
|
||||||
|
|
||||||
:param colors: The desired number of colors, <= 256
|
:param colors: The desired number of colors, <= 256
|
||||||
:param method: 0 = median cut
|
:param method: ``Image.MEDIANCUT=0`` (median cut),
|
||||||
1 = maximum coverage
|
``Image.MAXCOVERAGE=1`` (maximum coverage),
|
||||||
2 = fast octree
|
``Image.FASTOCTREE=2`` (fast octree),
|
||||||
3 = libimagequant
|
``Image.LIBIMAGEQUANT=3`` (libimagequant; check support using
|
||||||
|
:py:func:`PIL.features.check_feature`
|
||||||
|
with ``feature="libimagequant"``).
|
||||||
:param kmeans: Integer
|
:param kmeans: Integer
|
||||||
:param palette: Quantize to the palette of given
|
:param palette: Quantize to the palette of given
|
||||||
:py:class:`PIL.Image.Image`.
|
:py:class:`PIL.Image.Image`.
|
||||||
|
|
|
@ -637,6 +637,11 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
|
||||||
encoding of any text provided in subsequent operations.
|
encoding of any text provided in subsequent operations.
|
||||||
:param layout_engine: Which layout engine to use, if available:
|
:param layout_engine: Which layout engine to use, if available:
|
||||||
`ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`.
|
`ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`.
|
||||||
|
|
||||||
|
You can check support for Raqm layout using
|
||||||
|
:py:func:`PIL.features.check_feature` with ``feature="raqm"``.
|
||||||
|
|
||||||
|
.. versionadded:: 4.2.0
|
||||||
:return: A font object.
|
:return: A font object.
|
||||||
:exception OSError: If the file could not be read.
|
:exception OSError: If the file could not be read.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -39,7 +39,7 @@ def register(viewer, order=1):
|
||||||
pass # raised if viewer wasn't a class
|
pass # raised if viewer wasn't a class
|
||||||
if order > 0:
|
if order > 0:
|
||||||
_viewers.append(viewer)
|
_viewers.append(viewer)
|
||||||
elif order < 0:
|
else:
|
||||||
_viewers.insert(0, viewer)
|
_viewers.insert(0, viewer)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,9 @@
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
from cffi import FFI
|
from cffi import FFI
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
defs = """
|
defs = """
|
||||||
struct Pixel_RGBA {
|
struct Pixel_RGBA {
|
||||||
unsigned char r,g,b,a;
|
unsigned char r,g,b,a;
|
||||||
|
@ -38,6 +36,14 @@ struct Pixel_I16 {
|
||||||
"""
|
"""
|
||||||
ffi = FFI()
|
ffi = FFI()
|
||||||
ffi.cdef(defs)
|
ffi.cdef(defs)
|
||||||
|
except ImportError as ex:
|
||||||
|
# Allow error import for doc purposes, but error out when accessing
|
||||||
|
# anything in core.
|
||||||
|
from ._util import deferred_error
|
||||||
|
|
||||||
|
FFI = ffi = deferred_error(ex)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PyAccess:
|
class PyAccess:
|
||||||
|
|
|
@ -314,6 +314,7 @@ def _save(im, fp, filename):
|
||||||
if isinstance(exif, Image.Exif):
|
if isinstance(exif, Image.Exif):
|
||||||
exif = exif.tobytes()
|
exif = exif.tobytes()
|
||||||
xmp = im.encoderinfo.get("xmp", "")
|
xmp = im.encoderinfo.get("xmp", "")
|
||||||
|
method = im.encoderinfo.get("method", 0)
|
||||||
|
|
||||||
if im.mode not in _VALID_WEBP_LEGACY_MODES:
|
if im.mode not in _VALID_WEBP_LEGACY_MODES:
|
||||||
alpha = (
|
alpha = (
|
||||||
|
@ -331,6 +332,7 @@ def _save(im, fp, filename):
|
||||||
float(quality),
|
float(quality),
|
||||||
im.mode,
|
im.mode,
|
||||||
icc_profile,
|
icc_profile,
|
||||||
|
method,
|
||||||
exif,
|
exif,
|
||||||
xmp,
|
xmp,
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,6 +17,13 @@ modules = {
|
||||||
|
|
||||||
|
|
||||||
def check_module(feature):
|
def check_module(feature):
|
||||||
|
"""
|
||||||
|
Checks if a module is available.
|
||||||
|
|
||||||
|
:param feature: The module to check for.
|
||||||
|
:returns: ``True`` if available, ``False`` otherwise.
|
||||||
|
:raises ValueError: If the module is not defined in this version of Pillow.
|
||||||
|
"""
|
||||||
if not (feature in modules):
|
if not (feature in modules):
|
||||||
raise ValueError("Unknown module %s" % feature)
|
raise ValueError("Unknown module %s" % feature)
|
||||||
|
|
||||||
|
@ -30,6 +37,9 @@ def check_module(feature):
|
||||||
|
|
||||||
|
|
||||||
def get_supported_modules():
|
def get_supported_modules():
|
||||||
|
"""
|
||||||
|
:returns: A list of all supported modules.
|
||||||
|
"""
|
||||||
return [f for f in modules if check_module(f)]
|
return [f for f in modules if check_module(f)]
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,6 +47,13 @@ codecs = {"jpg": "jpeg", "jpg_2000": "jpeg2k", "zlib": "zip", "libtiff": "libtif
|
||||||
|
|
||||||
|
|
||||||
def check_codec(feature):
|
def check_codec(feature):
|
||||||
|
"""
|
||||||
|
Checks if a codec is available.
|
||||||
|
|
||||||
|
:param feature: The codec to check for.
|
||||||
|
:returns: ``True`` if available, ``False`` otherwise.
|
||||||
|
:raises ValueError: If the codec is not defined in this version of Pillow.
|
||||||
|
"""
|
||||||
if feature not in codecs:
|
if feature not in codecs:
|
||||||
raise ValueError("Unknown codec %s" % feature)
|
raise ValueError("Unknown codec %s" % feature)
|
||||||
|
|
||||||
|
@ -46,6 +63,9 @@ def check_codec(feature):
|
||||||
|
|
||||||
|
|
||||||
def get_supported_codecs():
|
def get_supported_codecs():
|
||||||
|
"""
|
||||||
|
:returns: A list of all supported codecs.
|
||||||
|
"""
|
||||||
return [f for f in codecs if check_codec(f)]
|
return [f for f in codecs if check_codec(f)]
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,6 +81,13 @@ features = {
|
||||||
|
|
||||||
|
|
||||||
def check_feature(feature):
|
def check_feature(feature):
|
||||||
|
"""
|
||||||
|
Checks if a feature is available.
|
||||||
|
|
||||||
|
:param feature: The feature to check for.
|
||||||
|
:returns: ``True`` if available, ``False`` if unavailable, ``None`` if unknown.
|
||||||
|
:raises ValueError: If the feature is not defined in this version of Pillow.
|
||||||
|
"""
|
||||||
if feature not in features:
|
if feature not in features:
|
||||||
raise ValueError("Unknown feature %s" % feature)
|
raise ValueError("Unknown feature %s" % feature)
|
||||||
|
|
||||||
|
@ -74,10 +101,20 @@ def check_feature(feature):
|
||||||
|
|
||||||
|
|
||||||
def get_supported_features():
|
def get_supported_features():
|
||||||
|
"""
|
||||||
|
:returns: A list of all supported features.
|
||||||
|
"""
|
||||||
return [f for f in features if check_feature(f)]
|
return [f for f in features if check_feature(f)]
|
||||||
|
|
||||||
|
|
||||||
def check(feature):
|
def check(feature):
|
||||||
|
"""
|
||||||
|
:param feature: A module, feature, or codec name.
|
||||||
|
:returns:
|
||||||
|
``True`` if the module, feature, or codec is available,
|
||||||
|
``False`` or ``None`` otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
if feature in modules:
|
if feature in modules:
|
||||||
return check_module(feature)
|
return check_module(feature)
|
||||||
if feature in codecs:
|
if feature in codecs:
|
||||||
|
@ -89,6 +126,10 @@ def check(feature):
|
||||||
|
|
||||||
|
|
||||||
def get_supported():
|
def get_supported():
|
||||||
|
"""
|
||||||
|
:returns: A list of all supported modules, features, and codecs.
|
||||||
|
"""
|
||||||
|
|
||||||
ret = get_supported_modules()
|
ret = get_supported_modules()
|
||||||
ret.extend(get_supported_features())
|
ret.extend(get_supported_features())
|
||||||
ret.extend(get_supported_codecs())
|
ret.extend(get_supported_codecs())
|
||||||
|
@ -96,6 +137,16 @@ def get_supported():
|
||||||
|
|
||||||
|
|
||||||
def pilinfo(out=None, supported_formats=True):
|
def pilinfo(out=None, supported_formats=True):
|
||||||
|
"""
|
||||||
|
Prints information about this installation of Pillow.
|
||||||
|
This function can be called with ``python -m PIL``.
|
||||||
|
|
||||||
|
:param out:
|
||||||
|
The output stream to print to. Defaults to ``sys.stdout`` if ``None``.
|
||||||
|
:param supported_formats:
|
||||||
|
If ``True``, a list of all supported image file formats will be printed.
|
||||||
|
"""
|
||||||
|
|
||||||
if out is None:
|
if out is None:
|
||||||
out = sys.stdout
|
out = sys.stdout
|
||||||
|
|
||||||
|
|
87
src/_webp.c
|
@ -545,6 +545,7 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
|
||||||
int height;
|
int height;
|
||||||
int lossless;
|
int lossless;
|
||||||
float quality_factor;
|
float quality_factor;
|
||||||
|
int method;
|
||||||
uint8_t* rgb;
|
uint8_t* rgb;
|
||||||
uint8_t* icc_bytes;
|
uint8_t* icc_bytes;
|
||||||
uint8_t* exif_bytes;
|
uint8_t* exif_bytes;
|
||||||
|
@ -556,49 +557,75 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
|
||||||
Py_ssize_t exif_size;
|
Py_ssize_t exif_size;
|
||||||
Py_ssize_t xmp_size;
|
Py_ssize_t xmp_size;
|
||||||
size_t ret_size;
|
size_t ret_size;
|
||||||
|
int rgba_mode;
|
||||||
|
int channels;
|
||||||
|
int ok;
|
||||||
ImagingSectionCookie cookie;
|
ImagingSectionCookie cookie;
|
||||||
|
WebPConfig config;
|
||||||
|
WebPMemoryWriter writer;
|
||||||
|
WebPPicture pic;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "y#iiifss#s#s#",
|
if (!PyArg_ParseTuple(args, "y#iiifss#is#s#",
|
||||||
(char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode,
|
(char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode,
|
||||||
&icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) {
|
&icc_bytes, &icc_size, &method, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (strcmp(mode, "RGBA")==0){
|
|
||||||
if (size < width * height * 4){
|
rgba_mode = strcmp(mode, "RGBA") == 0;
|
||||||
|
if (!rgba_mode && strcmp(mode, "RGB") != 0) {
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
|
|
||||||
if (lossless) {
|
channels = rgba_mode ? 4 : 3;
|
||||||
ImagingSectionEnter(&cookie);
|
if (size < width * height * channels) {
|
||||||
ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4 * width, &output);
|
|
||||||
ImagingSectionLeave(&cookie);
|
|
||||||
} else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
ImagingSectionEnter(&cookie);
|
|
||||||
ret_size = WebPEncodeRGBA(rgb, width, height, 4 * width, quality_factor, &output);
|
|
||||||
ImagingSectionLeave(&cookie);
|
|
||||||
}
|
|
||||||
} else if (strcmp(mode, "RGB")==0){
|
|
||||||
if (size < width * height * 3){
|
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
|
|
||||||
if (lossless) {
|
// Setup config for this frame
|
||||||
ImagingSectionEnter(&cookie);
|
if (!WebPConfigInit(&config)) {
|
||||||
ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3 * width, &output);
|
PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!");
|
||||||
ImagingSectionLeave(&cookie);
|
return NULL;
|
||||||
} else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
ImagingSectionEnter(&cookie);
|
|
||||||
ret_size = WebPEncodeRGB(rgb, width, height, 3 * width, quality_factor, &output);
|
|
||||||
ImagingSectionLeave(&cookie);
|
|
||||||
}
|
}
|
||||||
|
config.lossless = lossless;
|
||||||
|
config.quality = quality_factor;
|
||||||
|
config.method = method;
|
||||||
|
|
||||||
|
// Validate the config
|
||||||
|
if (!WebPValidateConfig(&config)) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "invalid configuration");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WebPPictureInit(&pic)) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "could not initialise picture");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
pic.width = width;
|
||||||
|
pic.height = height;
|
||||||
|
pic.use_argb = 1; // Don't convert RGB pixels to YUV
|
||||||
|
|
||||||
|
if (rgba_mode) {
|
||||||
|
WebPPictureImportRGBA(&pic, rgb, channels * width);
|
||||||
} else {
|
} else {
|
||||||
Py_RETURN_NONE;
|
WebPPictureImportRGB(&pic, rgb, channels * width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WebPMemoryWriterInit(&writer);
|
||||||
|
pic.writer = WebPMemoryWrite;
|
||||||
|
pic.custom_ptr = &writer;
|
||||||
|
|
||||||
|
ImagingSectionEnter(&cookie);
|
||||||
|
ok = WebPEncode(&config, &pic);
|
||||||
|
ImagingSectionLeave(&cookie);
|
||||||
|
|
||||||
|
WebPPictureFree(&pic);
|
||||||
|
if (!ok) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "encoding error");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
output = writer.mem;
|
||||||
|
ret_size = writer.size;
|
||||||
|
|
||||||
#ifndef HAVE_WEBPMUX
|
#ifndef HAVE_WEBPMUX
|
||||||
if (ret_size > 0) {
|
if (ret_size > 0) {
|
||||||
PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size);
|
PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size);
|
||||||
|
|
|
@ -105,9 +105,9 @@ header = [
|
||||||
# dependencies, listed in order of compilation
|
# dependencies, listed in order of compilation
|
||||||
deps = {
|
deps = {
|
||||||
"libjpeg": {
|
"libjpeg": {
|
||||||
"url": SF_MIRROR + "/project/libjpeg-turbo/2.0.3/libjpeg-turbo-2.0.3.tar.gz",
|
"url": SF_MIRROR + "/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4.tar.gz",
|
||||||
"filename": "libjpeg-turbo-2.0.3.tar.gz",
|
"filename": "libjpeg-turbo-2.0.4.tar.gz",
|
||||||
"dir": "libjpeg-turbo-2.0.3",
|
"dir": "libjpeg-turbo-2.0.4",
|
||||||
"build": [
|
"build": [
|
||||||
cmd_cmake(
|
cmd_cmake(
|
||||||
[
|
[
|
||||||
|
@ -195,9 +195,9 @@ deps = {
|
||||||
# "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"],
|
# "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"],
|
||||||
},
|
},
|
||||||
"lcms2": {
|
"lcms2": {
|
||||||
"url": SF_MIRROR + "/project/lcms/lcms/2.10/lcms2-2.10.tar.gz",
|
"url": SF_MIRROR + "/project/lcms/lcms/2.11/lcms2-2.11.tar.gz",
|
||||||
"filename": "lcms2-2.10.tar.gz",
|
"filename": "lcms2-2.11.tar.gz",
|
||||||
"dir": "lcms2-2.10",
|
"dir": "lcms2-2.11",
|
||||||
"patch": {
|
"patch": {
|
||||||
r"Projects\VC2017\lcms2_static\lcms2_static.vcxproj": {
|
r"Projects\VC2017\lcms2_static\lcms2_static.vcxproj": {
|
||||||
# default is /MD for x86 and /MT for x64, we need /MD always
|
# default is /MD for x86 and /MT for x64, we need /MD always
|
||||||
|
@ -251,9 +251,9 @@ deps = {
|
||||||
"libs": [r"*.lib"],
|
"libs": [r"*.lib"],
|
||||||
},
|
},
|
||||||
"harfbuzz": {
|
"harfbuzz": {
|
||||||
"url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.4.zip",
|
"url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.7.zip",
|
||||||
"filename": "harfbuzz-2.6.4.zip",
|
"filename": "harfbuzz-2.6.7.zip",
|
||||||
"dir": "harfbuzz-2.6.4",
|
"dir": "harfbuzz-2.6.7",
|
||||||
"build": [
|
"build": [
|
||||||
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
|
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
|
||||||
cmd_nmake(target="clean"),
|
cmd_nmake(target="clean"),
|
||||||
|
|