Merge pull request #4700 from nulano/features-version

This commit is contained in:
Hugo van Kemenade 2020-06-21 21:36:46 +03:00 committed by GitHub
commit 1bc67c9f0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 223 additions and 47 deletions

View File

@ -1,4 +1,5 @@
import io
import re
import pytest
from PIL import features
@ -21,6 +22,27 @@ def test_check():
assert features.check_feature(feature) == features.check(feature)
def test_version():
# Check the correctness of the convenience function
# and the format of version numbers
def test(name, function):
version = features.version(name)
if not features.check(name):
assert version is None
else:
assert function(name) == version
if name != "PIL":
assert version is None or re.search(r"\d+(\.\d+)*$", version)
for module in features.modules:
test(module, features.version_module)
for codec in features.codecs:
test(codec, features.version_codec)
for feature in features.features:
test(feature, features.version_feature)
@skip_unless_feature("webp")
def test_webp_transparency():
assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha()
@ -37,9 +59,22 @@ def test_webp_anim():
assert features.check("webp_anim") == _webp.HAVE_WEBPANIM
@skip_unless_feature("libjpeg_turbo")
def test_libjpeg_turbo_version():
assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo"))
@skip_unless_feature("libimagequant")
def test_libimagequant_version():
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
def test_check_modules():
for feature in features.modules:
assert features.check_module(feature) in [True, False]
def test_check_codecs():
for feature in features.codecs:
assert features.check_codec(feature) in [True, False]
@ -64,6 +99,8 @@ def test_unsupported_codec():
# Act / Assert
with pytest.raises(ValueError):
features.check_codec(codec)
with pytest.raises(ValueError):
features.version_codec(codec)
def test_unsupported_module():
@ -72,6 +109,8 @@ def test_unsupported_module():
# Act / Assert
with pytest.raises(ValueError):
features.check_module(module)
with pytest.raises(ValueError):
features.version_module(module)
def test_pilinfo():

View File

@ -2,14 +2,14 @@ import io
import sys
import pytest
from PIL import IcnsImagePlugin, Image
from PIL import IcnsImagePlugin, Image, features
from .helper import assert_image_equal, assert_image_similar
# sample icon file
TEST_FILE = "Tests/images/pillow.icns"
ENABLE_JPEG2K = hasattr(Image.core, "jp2klib_version")
ENABLE_JPEG2K = features.check_codec("jpg_2000")
def test_sanity():

View File

@ -3,7 +3,7 @@ import re
from io import BytesIO
import pytest
from PIL import ExifTags, Image, ImageFile, JpegImagePlugin
from PIL import ExifTags, Image, ImageFile, JpegImagePlugin, features
from .helper import (
assert_image,
@ -41,7 +41,7 @@ class TestFileJpeg:
def test_sanity(self):
# internal version number
assert re.search(r"\d+\.\d+$", Image.core.jpeglib_version)
assert re.search(r"\d+\.\d+$", features.version_codec("jpg"))
with Image.open(TEST_FILE) as im:
im.load()

View File

@ -2,7 +2,7 @@ import re
from io import BytesIO
import pytest
from PIL import Image, ImageFile, Jpeg2KImagePlugin
from PIL import Image, ImageFile, Jpeg2KImagePlugin, features
from .helper import (
assert_image_equal,
@ -35,7 +35,7 @@ def roundtrip(im, **options):
def test_sanity():
# Internal version number
assert re.search(r"\d+\.\d+\.\d+$", Image.core.jp2klib_version)
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000"))
with Image.open("Tests/images/test-card-lossless.jp2") as im:
px = im.load()

View File

@ -3,11 +3,12 @@ import io
import itertools
import logging
import os
import re
from collections import namedtuple
from ctypes import c_float
import pytest
from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags
from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features
from .helper import (
assert_image_equal,
@ -47,6 +48,9 @@ class LibTiffTestCase:
class TestFileLibTiff(LibTiffTestCase):
def test_version(self):
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff"))
def test_g4_tiff(self, tmp_path):
"""Test the ordinary file path load path"""

View File

@ -3,7 +3,7 @@ import zlib
from io import BytesIO
import pytest
from PIL import Image, ImageFile, PngImagePlugin
from PIL import Image, ImageFile, PngImagePlugin, features
from .helper import (
PillowLeakTestCase,
@ -73,7 +73,7 @@ class TestFilePng:
def test_sanity(self, tmp_path):
# internal version number
assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", Image.core.zlib_version)
assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib"))
test_file = str(tmp_path / "temp.png")

View File

@ -1,7 +1,8 @@
import io
import re
import pytest
from PIL import Image, WebPImagePlugin
from PIL import Image, WebPImagePlugin, features
from .helper import (
assert_image_similar,
@ -38,6 +39,7 @@ class TestFileWebp:
def test_version(self):
_webp.WebPDecoderVersion()
_webp.WebPDecoderBuggyAlpha()
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp"))
def test_read_rgb(self):
"""

View File

@ -4,7 +4,7 @@ import re
from io import BytesIO
import pytest
from PIL import Image, ImageMode
from PIL import Image, ImageMode, features
from .helper import assert_image, assert_image_equal, assert_image_similar, hopper
@ -46,7 +46,7 @@ def test_sanity():
assert list(map(type, v)) == [str, str, str, str]
# internal version number
assert re.search(r"\d+\.\d+$", ImageCms.core.littlecms_version)
assert re.search(r"\d+\.\d+$", features.version_module("littlecms2"))
skip_missing()
i = ImageCms.profileToProfile(hopper(), SRGB, SRGB)

View File

@ -7,7 +7,7 @@ import sys
from io import BytesIO
import pytest
from PIL import Image, ImageDraw, ImageFont
from PIL import Image, ImageDraw, ImageFont, features
from .helper import (
assert_image_equal,
@ -41,7 +41,7 @@ class TestImageFont:
@classmethod
def setup_class(self):
freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
freetype = distutils.version.StrictVersion(features.version_module("freetype2"))
self.metrics = self.METRICS["Default"]
for conditions, metrics in self.METRICS.items():
@ -68,7 +68,7 @@ class TestImageFont:
)
def test_sanity(self):
assert re.search(r"\d+\.\d+\.\d+$", ImageFont.core.freetype2_version)
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2"))
def test_font_properties(self):
ttf = self.get_font()
@ -620,7 +620,7 @@ class TestImageFont:
def test_variation_get(self):
font = self.get_font()
freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
freetype = distutils.version.StrictVersion(features.version_module("freetype2"))
if freetype < "2.9.1":
with pytest.raises(NotImplementedError):
font.get_variation_names()
@ -692,7 +692,7 @@ class TestImageFont:
def test_variation_set_by_name(self):
font = self.get_font()
freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
freetype = distutils.version.StrictVersion(features.version_module("freetype2"))
if freetype < "2.9.1":
with pytest.raises(NotImplementedError):
font.set_variation_by_name("Bold")
@ -716,7 +716,7 @@ class TestImageFont:
def test_variation_set_by_axes(self):
font = self.get_font()
freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
freetype = distutils.version.StrictVersion(features.version_module("freetype2"))
if freetype < "2.9.1":
with pytest.raises(NotImplementedError):
font.set_variation_by_axes([100])

View File

@ -8,6 +8,7 @@ The :py:mod:`PIL.features` module can be used to detect which Pillow features ar
.. autofunction:: PIL.features.pilinfo
.. autofunction:: PIL.features.check
.. autofunction:: PIL.features.version
.. autofunction:: PIL.features.get_supported
Modules
@ -16,28 +17,31 @@ Modules
Support for the following modules can be checked:
* ``pil``: The Pillow core module, required for all functionality.
* ``tkinter``: Tkinter support.
* ``tkinter``: Tkinter support. Version number not available.
* ``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.version_module
.. autofunction:: PIL.features.get_supported_modules
Codecs
------
These are only checked during Pillow compilation.
Support for these is only checked during Pillow compilation.
If the required library was uninstalled from the system, the ``pil`` core module may fail to load instead.
Except for ``jpg``, the version number is checked at run-time.
Support for the following codecs can be checked:
* ``jpg``: (compile time) Libjpeg support, required for JPEG based image formats.
* ``jpg``: (compile time) Libjpeg support, required for JPEG based image formats. Only compile time version number is available.
* ``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.version_codec
.. autofunction:: PIL.features.get_supported_codecs
Features
@ -45,16 +49,18 @@ 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.
Feature version numbers are available only where stated.
Support for the following features can be checked:
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg.
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available.
* ``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`.
* ``raqm``: Raqm library, required for ``ImageFont.LAYOUT_RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library.
.. autofunction:: PIL.features.check_feature
.. autofunction:: PIL.features.version_feature
.. autofunction:: PIL.features.get_supported_features

View File

@ -23,10 +23,10 @@ import subprocess
import sys
import tempfile
from PIL import Image, ImageFile, PngImagePlugin
from PIL import Image, ImageFile, PngImagePlugin, features
from PIL._binary import i8
enable_jpeg2k = hasattr(Image.core, "jp2klib_version")
enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k:
from PIL import Jpeg2KImagePlugin

View File

@ -8,11 +8,11 @@ import PIL
from . import Image
modules = {
"pil": "PIL._imaging",
"tkinter": "PIL._tkinter_finder",
"freetype2": "PIL._imagingft",
"littlecms2": "PIL._imagingcms",
"webp": "PIL._webp",
"pil": ("PIL._imaging", "PILLOW_VERSION"),
"tkinter": ("PIL._tkinter_finder", None),
"freetype2": ("PIL._imagingft", "freetype2_version"),
"littlecms2": ("PIL._imagingcms", "littlecms_version"),
"webp": ("PIL._webp", "webpdecoder_version"),
}
@ -27,7 +27,7 @@ def check_module(feature):
if not (feature in modules):
raise ValueError("Unknown module %s" % feature)
module = modules[feature]
module, ver = modules[feature]
try:
__import__(module)
@ -36,6 +36,24 @@ def check_module(feature):
return False
def version_module(feature):
"""
:param feature: The module to check for.
:returns:
The loaded version number as a string, or ``None`` if unknown or not available.
:raises ValueError: If the module is not defined in this version of Pillow.
"""
if not check_module(feature):
return None
module, ver = modules[feature]
if ver is None:
return None
return getattr(__import__(module, fromlist=[ver]), ver)
def get_supported_modules():
"""
:returns: A list of all supported modules.
@ -43,7 +61,12 @@ def get_supported_modules():
return [f for f in modules if check_module(f)]
codecs = {"jpg": "jpeg", "jpg_2000": "jpeg2k", "zlib": "zip", "libtiff": "libtiff"}
codecs = {
"jpg": ("jpeg", "jpeglib"),
"jpg_2000": ("jpeg2k", "jp2klib"),
"zlib": ("zip", "zlib"),
"libtiff": ("libtiff", "libtiff"),
}
def check_codec(feature):
@ -57,11 +80,32 @@ def check_codec(feature):
if feature not in codecs:
raise ValueError("Unknown codec %s" % feature)
codec = codecs[feature]
codec, lib = codecs[feature]
return codec + "_encoder" in dir(Image.core)
def version_codec(feature):
"""
:param feature: The codec to check for.
:returns:
The version number as a string, or ``None`` if not available.
Checked at compile time for ``jpg``, run-time otherwise.
:raises ValueError: If the codec is not defined in this version of Pillow.
"""
if not check_codec(feature):
return None
codec, lib = codecs[feature]
version = getattr(Image.core, lib + "_version")
if feature == "libtiff":
return version.split("\n")[0].split("Version ")[1]
return version
def get_supported_codecs():
"""
:returns: A list of all supported codecs.
@ -70,13 +114,13 @@ def get_supported_codecs():
features = {
"webp_anim": ("PIL._webp", "HAVE_WEBPANIM"),
"webp_mux": ("PIL._webp", "HAVE_WEBPMUX"),
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"),
"raqm": ("PIL._imagingft", "HAVE_RAQM"),
"libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO"),
"libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT"),
"xcb": ("PIL._imaging", "HAVE_XCB"),
"webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None),
"webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None),
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None),
"raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"),
"libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"),
"libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"),
"xcb": ("PIL._imaging", "HAVE_XCB", None),
}
@ -91,7 +135,7 @@ def check_feature(feature):
if feature not in features:
raise ValueError("Unknown feature %s" % feature)
module, flag = features[feature]
module, flag, ver = features[feature]
try:
imported_module = __import__(module, fromlist=["PIL"])
@ -100,6 +144,23 @@ def check_feature(feature):
return None
def version_feature(feature):
"""
:param feature: The feature to check for.
:returns: The version number as a string, or ``None`` if not available.
:raises ValueError: If the feature is not defined in this version of Pillow.
"""
if not check_feature(feature):
return None
module, flag, ver = features[feature]
if ver is None:
return None
return getattr(__import__(module, fromlist=[ver]), ver)
def get_supported_features():
"""
:returns: A list of all supported features.
@ -109,9 +170,9 @@ def get_supported_features():
def check(feature):
"""
:param feature: A module, feature, or codec name.
:param feature: A module, codec, or feature name.
:returns:
``True`` if the module, feature, or codec is available,
``True`` if the module, codec, or feature is available,
``False`` or ``None`` otherwise.
"""
@ -125,6 +186,22 @@ def check(feature):
return False
def version(feature):
"""
:param feature:
The module, codec, or feature to check for.
:returns:
The version number as a string, or ``None`` if unknown or not available.
"""
if feature in modules:
return version_module(feature)
if feature in codecs:
return version_codec(feature)
if feature in features:
return version_feature(feature)
return None
def get_supported():
"""
:returns: A list of all supported modules, features, and codecs.
@ -187,7 +264,15 @@ def pilinfo(out=None, supported_formats=True):
("xcb", "XCB (X protocol)"),
]:
if check(name):
print("---", feature, "support ok", file=out)
if name == "jpg" and check_feature("libjpeg_turbo"):
v = "libjpeg-turbo " + version_feature("libjpeg_turbo")
else:
v = version(name)
if v is not None:
t = "compiled for" if name in ("pil", "jpg") else "loaded"
print("---", feature, "support ok,", t, v, file=out)
else:
print("---", feature, "support ok", file=out)
else:
print("***", feature, "support not installed", file=out)
print("-" * 68, file=out)

View File

@ -4168,12 +4168,21 @@ setup_module(PyObject* m) {
#ifdef LIBJPEG_TURBO_VERSION
PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", Py_True);
#define tostr1(a) #a
#define tostr(a) tostr1(a)
PyDict_SetItemString(d, "libjpeg_turbo_version", PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)));
#undef tostr
#undef tostr1
#else
PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", Py_False);
#endif
#ifdef HAVE_LIBIMAGEQUANT
PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", Py_True);
{
extern const char* ImagingImageQuantVersion(void);
PyDict_SetItemString(d, "imagequant_version", PyUnicode_FromString(ImagingImageQuantVersion()));
}
#else
PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", Py_False);
#endif

View File

@ -1608,6 +1608,7 @@ static int
setup_module(PyObject* m) {
PyObject *d;
PyObject *v;
int vn;
d = PyModule_GetDict(m);
@ -1622,7 +1623,8 @@ setup_module(PyObject* m) {
d = PyModule_GetDict(m);
v = PyUnicode_FromFormat("%d.%d", LCMS_VERSION / 100, LCMS_VERSION % 100);
vn = cmsGetEncodedCMMversion();
v = PyUnicode_FromFormat("%d.%d", vn / 100, vn % 100);
PyDict_SetItemString(d, "littlecms_version", v);
return 0;

View File

@ -81,6 +81,7 @@ typedef struct {
static PyTypeObject Font_Type;
typedef const char* (*t_raqm_version_string) (void);
typedef bool (*t_raqm_version_atleast)(unsigned int major,
unsigned int minor,
unsigned int micro);
@ -112,6 +113,7 @@ typedef void (*t_raqm_destroy) (raqm_t *rq);
typedef struct {
void* raqm;
int version;
t_raqm_version_string version_string;
t_raqm_version_atleast version_atleast;
t_raqm_create create;
t_raqm_set_text set_text;
@ -173,6 +175,7 @@ setraqm(void)
}
#ifndef _WIN32
p_raqm.version_string = (t_raqm_version_atleast)dlsym(p_raqm.raqm, "raqm_version_string");
p_raqm.version_atleast = (t_raqm_version_atleast)dlsym(p_raqm.raqm, "raqm_version_atleast");
p_raqm.create = (t_raqm_create)dlsym(p_raqm.raqm, "raqm_create");
p_raqm.set_text = (t_raqm_set_text)dlsym(p_raqm.raqm, "raqm_set_text");
@ -206,6 +209,7 @@ setraqm(void)
return 2;
}
#else
p_raqm.version_string = (t_raqm_version_atleast)GetProcAddress(p_raqm.raqm, "raqm_version_string");
p_raqm.version_atleast = (t_raqm_version_atleast)GetProcAddress(p_raqm.raqm, "raqm_version_atleast");
p_raqm.create = (t_raqm_create)GetProcAddress(p_raqm.raqm, "raqm_create");
p_raqm.set_text = (t_raqm_set_text)GetProcAddress(p_raqm.raqm, "raqm_set_text");
@ -1257,6 +1261,9 @@ setup_module(PyObject* m) {
setraqm();
v = PyBool_FromLong(!!p_raqm.raqm);
PyDict_SetItemString(d, "HAVE_RAQM", v);
if (p_raqm.version_string) {
PyDict_SetItemString(d, "raqm_version", PyUnicode_FromString(p_raqm.version_string()));
}
return 0;
}

View File

@ -821,6 +821,16 @@ PyObject* WebPDecoderVersion_wrapper() {
return Py_BuildValue("i", WebPGetDecoderVersion());
}
// Version as string
const char*
WebPDecoderVersion_str(void)
{
static char version[20];
int version_number = WebPGetDecoderVersion();
sprintf(version, "%d.%d.%d", version_number >> 16, (version_number >> 8) % 0x100, version_number % 0x100);
return version;
}
/*
* The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well.
* Files that are valid with 0.3 are reported as being invalid.
@ -872,10 +882,13 @@ void addTransparencyFlagToModule(PyObject* m) {
}
static int setup_module(PyObject* m) {
PyObject* d = PyModule_GetDict(m);
addMuxFlagToModule(m);
addAnimFlagToModule(m);
addTransparencyFlagToModule(m);
PyDict_SetItemString(d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str()));
#ifdef HAVE_WEBPANIM
/* Ready object types */
if (PyType_Ready(&WebPAnimDecoder_Type) < 0 ||

View File

@ -113,4 +113,13 @@ err:
return result;
}
const char*
ImagingImageQuantVersion(void)
{
static char version[20];
int number = liq_version();
sprintf(version, "%d.%d.%d", number / 10000, (number / 100) % 100, number % 100);
return version;
}
#endif

View File

@ -373,7 +373,7 @@ ImagingZipEncodeCleanup(ImagingCodecState state) {
const char*
ImagingZipVersion(void)
{
return ZLIB_VERSION;
return zlibVersion();
}
#endif