Merge branch 'python-pillow:main' into main

This commit is contained in:
Sascha Ronnie Daoudia 2024-01-02 20:45:08 +01:00 committed by GitHub
commit e7d4fa6486
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 567 additions and 194 deletions

View File

@ -2,15 +2,14 @@
[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma:
pragma: no cover
# Don't complain if non-runnable code isn't run:
exclude_also =
# Don't complain if non-runnable code isn't run
if 0:
if __name__ == .__main__.:
# Don't complain about debug code
if DEBUG:
# Don't complain about compatibility code for missing optional dependencies
except ImportError
[run]
omit =

18
.github/problem-matchers/gcc.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
"__comment": "Based on vscode-cpptools' Extension/package.json gcc rule",
"problemMatcher": [
{
"owner": "gcc-problem-matcher",
"pattern": [
{
"regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

View File

@ -86,6 +86,10 @@ jobs:
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'"
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build
run: |
.ci/build.sh

View File

@ -143,6 +143,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow

View File

@ -1,12 +1,12 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.7
rev: v0.1.9
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.12.0
rev: 23.12.1
hooks:
- id: black

View File

@ -2,9 +2,36 @@
Changelog (Pillow)
==================
10.2.0 (unreleased)
10.2.0 (2024-01-02)
-------------------
- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553
[bgilbert, radarhere]
- Trim glyph size in ImageFont.getmask() #7669, #7672
[radarhere, nulano]
- Deprecate IptcImagePlugin helpers #7664
[nulano, hugovk, radarhere]
- Allow uncompressed TIFF images to be saved in chunks #7650
[radarhere]
- Concatenate multiple JPEG EXIF markers #7496
[radarhere]
- Changed IPTC tile tuple to match other plugins #7661
[radarhere]
- Do not assign new fp attribute when exiting context manager #7566
[radarhere]
- Support arbitrary masks for uncompressed RGB DDS images #7589
[radarhere, akx]
- Support setting ROWSPERSTRIP tag #7654
[radarhere]
- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662
[radarhere]

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors.
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
Like PIL, Pillow is licensed under the open source HPND License:

View File

@ -87,7 +87,7 @@ Released as needed privately to individual vendors for critical security-related
and copy into `dist`. Check and upload them e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.0*
python3 -m twine upload dist/pillow-5.2.0*
```
## Publicize Release

BIN
Tests/images/bgr15.dds Normal file

Binary file not shown.

BIN
Tests/images/bgr15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

View File

@ -32,6 +32,7 @@ TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SR
TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
@ -249,6 +250,7 @@ def test_dx10_r8g8b8a8_unorm_srgb():
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15),
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
],
)
@ -341,16 +343,9 @@ def test_palette():
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
@pytest.mark.parametrize(
"test_file",
(
"Tests/images/unsupported_bitcount_rgb.dds",
"Tests/images/unsupported_bitcount_luminance.dds",
),
)
def test_unsupported_bitcount(test_file):
def test_unsupported_bitcount():
with pytest.raises(OSError):
with Image.open(test_file):
with Image.open("Tests/images/unsupported_bitcount.dds"):
pass

View File

@ -6,11 +6,23 @@ import pytest
from PIL import Image, IptcImagePlugin
from .helper import hopper
from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/iptc.jpg"
def test_open():
expected = Image.new("L", (1, 1))
f = BytesIO(
b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
)
with Image.open(f) as im:
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
assert_image_equal(im, expected)
def test_getiptcinfo_jpg_none():
# Arrange
with hopper() as im:
@ -78,24 +90,28 @@ def test_i():
c = b"a"
# Act
ret = IptcImagePlugin.i(c)
with pytest.warns(DeprecationWarning):
ret = IptcImagePlugin.i(c)
# Assert
assert ret == 97
def test_dump():
def test_dump(monkeypatch):
# Arrange
c = b"abc"
# Temporarily redirect stdout
old_stdout = sys.stdout
sys.stdout = mystdout = StringIO()
mystdout = StringIO()
monkeypatch.setattr(sys, "stdout", mystdout)
# Act
IptcImagePlugin.dump(c)
# Reset stdout
sys.stdout = old_stdout
with pytest.warns(DeprecationWarning):
IptcImagePlugin.dump(c)
# Assert
assert mystdout.getvalue() == "61 62 63 \n"
def test_pad_deprecation():
with pytest.warns(DeprecationWarning):
assert IptcImagePlugin.PAD == b"\0\0\0\0"

View File

@ -142,6 +142,19 @@ class TestFileJpeg:
)
assert k > 0.9
def test_rgb(self):
def getchannels(im):
return tuple(v[0] for v in im.layer)
im = hopper()
im_ycbcr = self.roundtrip(im)
assert getchannels(im_ycbcr) == (1, 2, 3)
assert_image_similar(im, im_ycbcr, 17)
im_rgb = self.roundtrip(im, keep_rgb=True)
assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B"))
assert_image_similar(im, im_rgb, 12)
@pytest.mark.parametrize(
"test_image_path",
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
@ -423,25 +436,28 @@ class TestFileJpeg:
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
# experimental API
im = self.roundtrip(hopper(), subsampling=-1) # default
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
im = self.roundtrip(hopper(), subsampling=0) # 4:4:4
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
im = self.roundtrip(hopper(), subsampling=1) # 4:2:2
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
im = self.roundtrip(hopper(), subsampling=2) # 4:2:0
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
im = self.roundtrip(hopper(), subsampling=3) # default (undefined)
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
for subsampling in (-1, 3): # (default, invalid)
im = self.roundtrip(hopper(), subsampling=subsampling)
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
for subsampling in (0, "4:4:4"):
im = self.roundtrip(hopper(), subsampling=subsampling)
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
for subsampling in (1, "4:2:2"):
im = self.roundtrip(hopper(), subsampling=subsampling)
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
for subsampling in (2, "4:2:0", "4:1:1"):
im = self.roundtrip(hopper(), subsampling=subsampling)
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
im = self.roundtrip(hopper(), subsampling="4:4:4")
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
im = self.roundtrip(hopper(), subsampling="4:2:2")
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
im = self.roundtrip(hopper(), subsampling="4:2:0")
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
im = self.roundtrip(hopper(), subsampling="4:1:1")
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
# RGB colorspace
for subsampling in (-1, 0, "4:4:4"):
# "4:4:4" doesn't really make sense for RGB, but the conversion
# to an integer happens at a higher level
im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
for subsampling in (1, "4:2:2", 2, "4:2:0", 3):
with pytest.raises(OSError):
self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
with pytest.raises(TypeError):
self.roundtrip(hopper(), subsampling="1:1:1")
@ -840,6 +856,10 @@ class TestFileJpeg:
# Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09"
def test_multiple_exif(self):
with Image.open("Tests/images/multiple_exif.jpg") as im:
assert im.info["exif"] == b"Exif\x00\x00firstsecond"
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)

View File

@ -484,13 +484,13 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif()
exif[256] = 100
exif[264] = 100
im.save(outfile, exif=exif)
with Image.open(outfile) as im:
exif = im.getexif()
assert exif[256] == 100
assert exif[264] == 100
def test_reload_exif_after_seek(self):
with Image.open("Tests/images/multipage.tiff") as im:
@ -612,6 +612,14 @@ class TestFileTiff:
assert_image_equal_tofile(im, tmpfile)
def test_rowsperstrip(self, tmp_path):
outfile = str(tmp_path / "temp.tif")
im = hopper()
im.save(outfile, tiffinfo={278: 256})
with Image.open(outfile) as im:
assert im.tag_v2[278] == 256
def test_strip_raw(self):
infile = "Tests/images/tiff_strip_raw.tif"
with Image.open(infile) as im:
@ -773,6 +781,27 @@ class TestFileTiff:
4001,
]
def test_tiff_chunks(self, tmp_path):
tmpfile = str(tmp_path / "temp.tif")
im = hopper()
with open(tmpfile, "wb") as fp:
for y in range(0, 128, 32):
chunk = im.crop((0, y, 128, y + 32))
if y == 0:
chunk.save(
fp,
"TIFF",
tiffinfo={
TiffImagePlugin.IMAGEWIDTH: 128,
TiffImagePlugin.IMAGELENGTH: 128,
},
)
else:
fp.write(chunk.tobytes())
assert_image_equal_tofile(im, tmpfile)
def test_close_on_load_exclusive(self, tmp_path):
# similar to test_fd_leak, but runs on unixlike os
tmpfile = str(tmp_path / "temp.tif")

View File

@ -123,6 +123,7 @@ def test_write_metadata(tmp_path):
"""Test metadata writing through the python code"""
with Image.open("Tests/images/hopper.tif") as img:
f = str(tmp_path / "temp.tiff")
del img.tag[278]
img.save(f, tiffinfo=img.tag)
original = img.tag_v2.named()
@ -159,9 +160,11 @@ def test_change_stripbytecounts_tag_type(tmp_path):
out = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper.tif") as im:
info = im.tag_v2
del info[278]
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
im = im.resize((500, 500))
info[TiffImagePlugin.IMAGEWIDTH] = im.width
# STRIPBYTECOUNTS can be a SHORT or a LONG
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT

View File

@ -1016,6 +1016,11 @@ class TestImage:
except OSError as e:
assert str(e) == "buffer overrun when reading image file"
def test_exit_fp(self):
with Image.new("L", (1, 1)) as im:
pass
assert not hasattr(im, "fp")
def test_close_graceful(self, caplog):
with Image.open("Tests/images/hopper.jpg") as im:
copy = im.copy()

View File

@ -1,14 +1,22 @@
from __future__ import annotations
import struct
import pytest
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont, features
from PIL import Image, ImageDraw, ImageFont, features, _util
from .helper import assert_image_equal_tofile
pytestmark = pytest.mark.skipif(
features.check_module("freetype2"),
reason="PILfont superseded if FreeType is supported",
)
original_core = ImageFont.core
def setup_module():
if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError)
def teardown_module():
ImageFont.core = original_core
def test_default_font():
@ -44,3 +52,25 @@ def test_textbbox():
default_font = ImageFont.load_default()
assert d.textlength("test", font=default_font) == 24
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
def test_decompression_bomb():
glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256)
fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256)
font = ImageFont.ImageFont()
font._load_pilfont_data(fp, Image.new("L", (256, 256)))
with pytest.raises(Image.DecompressionBombError):
font.getmask("A" * 1_000_000)
@pytest.mark.timeout(4)
def test_oom():
glyph = struct.pack(
">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767
)
fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256)
font = ImageFont.ImageFont()
font._load_pilfont_data(fp, Image.new("L", (1, 1)))
font.getmask("A" * 1_000_000)

View File

@ -57,15 +57,6 @@ def test_str_to_img():
assert_image_equal_tofile(A, "Tests/images/morph_a.png")
def create_lut():
for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"):
lb = ImageMorph.LutBuilder(op_name=op)
lut = lb.build_lut()
with open(f"Tests/images/{op}.lut", "wb") as f:
f.write(lut)
# create_lut()
@pytest.mark.parametrize(
"op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
)

View File

@ -66,7 +66,7 @@ def test_deferred_error():
# Arrange
# Act
thing = _util.DeferredError(ValueError("Some error text"))
thing = _util.DeferredError.new(ValueError("Some error text"))
# Assert
with pytest.raises(ValueError):

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors
Like PIL, Pillow is licensed under the open source PIL
Software License:

View File

@ -69,6 +69,14 @@ can be found here.
:undoc-members:
:show-inheritance:
:mod:`~PIL.ImageMode` Module
----------------------------
.. automodule:: PIL.ImageMode
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL.ImageTransform` Module
---------------------------------

View File

@ -54,7 +54,7 @@ master_doc = "index"
# General information about the project.
project = "Pillow (PIL Fork)"
copyright = (
"1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors"
"1995-2011 Fredrik Lundh, 2010-2024 Jeffrey A. Clark (Alex) and contributors"
)
author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors"

View File

@ -44,6 +44,17 @@ ImageFile.raise_oserror
error codes returned by a codec's ``decode()`` method, which ImageFile already does
automatically.
IptcImageFile helper functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 10.2.0
The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow
12.0.0 (2025-10-15). These are undocumented helper functions intended
for internal use, so there is no replacement. They can each be replaced
by a single line of code using builtin functions in Python.
Removed features
----------------

View File

@ -487,6 +487,16 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**exif**
If present, the image will be stored with the provided raw EXIF data.
**keep_rgb**
By default, libjpeg converts images with an RGB color space to YCbCr.
If this option is present and true, those images will be stored as RGB
instead.
When this option is enabled, attempting to chroma-subsample RGB images
with the ``subsampling`` option will raise an :py:exc:`OSError`.
.. versionadded:: 10.2.0
**subsampling**
If present, sets the subsampling for the encoder.

View File

@ -542,7 +542,7 @@ Reading from URL
from PIL import Image
from urllib.request import urlopen
url = "https://python-pillow.org/images/pillow-logo.png"
url = "https://python-pillow.org/assets/images/pillow-logo.png"
img = Image.open(urlopen(url))

View File

@ -25,6 +25,19 @@ Internal Modules
:undoc-members:
:show-inheritance:
:mod:`~PIL._typing` Module
--------------------------
.. module:: PIL._typing
Provides a convenient way to import type hints that are not available
on some Python versions.
.. py:data:: TypeGuard
:value: typing.TypeGuard
See :py:obj:`typing.TypeGuard`.
:mod:`~PIL._util` Module
------------------------

View File

@ -1,14 +1,6 @@
10.2.0
------
Backwards Incompatible Changes
==============================
TODO
^^^^
TODO
Deprecations
============
@ -20,10 +12,14 @@ ImageFile.raise_oserror
error codes returned by a codec's ``decode()`` method, which ImageFile already does
automatically.
TODO
^^^^
IptcImageFile helper functions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow
12.0.0 (2025-10-15). These are undocumented helper functions intended
for internal use, so there is no replacement. They can each be replaced
by a single line of code using builtin functions in Python.
API Changes
===========
@ -46,6 +42,14 @@ Added DdsImagePlugin enums
:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT`
enums have been added to :py:class:`PIL.DdsImagePlugin`.
JPEG RGB color space
^^^^^^^^^^^^^^^^^^^^
When saving JPEG files, ``keep_rgb`` can now be set to ``True``. This will store RGB
images in the RGB color space instead of being converted to YCbCr automatically by
libjpeg. When this option is enabled, attempting to chroma-subsample RGB images with
the ``subsampling`` option will raise an :py:exc:`OSError`.
JPEG restart marker interval
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -73,6 +77,16 @@ Pillow will now raise a :py:exc:`ValueError` if the number of characters passed
This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It
can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
A decompression bomb check has also been added to
:py:meth:`PIL.ImageFont.ImageFont.getmask`.
ImageFont.getmask: Trim glyph size
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To protect against potential DOS attacks when using PIL fonts,
:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that
they do not extend beyond the bitmap image.
ImageMath.eval: Restricted environment keys
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -92,16 +106,56 @@ Support has been added to read the BC4U format of DDS images.
Support has also been added to read DX10 BC1 and BC4, whether UNORM or
TYPELESS.
Support arbitrary masks for uncompressed RGB DDS images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
All masks are now supported when reading DDS images with uncompressed RGB data,
allowing for bit counts other than 24 and 32.
Saving TIFF tag RowsPerStrip
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When saving TIFF images, the TIFF tag RowsPerStrip can now be one of the tags set by
the user, rather than always being calculated by Pillow.
Optimized ImageColor.getrgb and getcolor
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The color calculations of :py:attr:`~PIL.ImageColor.getrgb` and
:py:attr:`~PIL.ImageColor.getcolor` are now cached using
:py:func:`functools.lru_cache`. Cached calls of ``getrgb`` are 3.1 - 91.4 times
as fast and ``getcolor`` are 5.1 - 19.6 times as fast.
Optimized ImageMode.getmode
^^^^^^^^^^^^^^^^^^^^^^^^^^^
The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using
:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 1.2 times as
fast.
Optimized ImageStat.Stat count and extrema
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and
:py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the
histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3x as fast
on average and ``st.extrema`` is 12x as fast on average.
histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3 times as fast on
average and ``st.extrema`` is 12 times as fast on average.
Encoder errors now report error detail as string
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:py:exc:`OSError` exceptions from image encoders now include a textual description of
the error instead of a numeric error code.
Type hints
^^^^^^^^^^
Work has begun to add type annotations to Pillow, including:
* :py:mod:`~PIL.ContainerIO`
* :py:mod:`~PIL.FontFile`, :py:mod:`~PIL.BdfFontFile` and :py:mod:`~PIL.PcfFontFile`
* :py:mod:`~PIL.ImageChops`
* :py:mod:`~PIL.ImageMode`
* :py:mod:`~PIL.ImageSequence`
* :py:mod:`~PIL.ImageTransform`
* :py:mod:`~PIL.TarIO`

View File

@ -65,6 +65,9 @@ tests = [
"pytest-cov",
"pytest-timeout",
]
typing = [
'typing-extensions; python_version < "3.10"',
]
xmp = [
"defusedxml",
]
@ -143,9 +146,6 @@ exclude = [
'^src/PIL/DdsImagePlugin.py$',
'^src/PIL/FpxImagePlugin.py$',
'^src/PIL/Image.py$',
'^src/PIL/ImageCms.py$',
'^src/PIL/ImageFile.py$',
'^src/PIL/ImageFont.py$',
'^src/PIL/ImageMath.py$',
'^src/PIL/ImageMorph.py$',
'^src/PIL/ImageQt.py$',

View File

@ -18,6 +18,7 @@ from enum import IntEnum, IntFlag
from . import Image, ImageFile, ImagePalette
from ._binary import i32le as i32
from ._binary import o8
from ._binary import o32le as o32
# Magic ("DDS ")
@ -341,6 +342,7 @@ class DdsImageFile(ImageFile.ImageFile):
flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height)
extents = (0, 0) + self.size
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved
@ -351,22 +353,16 @@ class DdsImageFile(ImageFile.ImageFile):
rawmode = None
if pfflags & DDPF.RGB:
# Texture contains uncompressed RGB data
masks = struct.unpack("<4I", header.read(16))
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
if bitcount == 24:
self._mode = "RGB"
rawmode = masks[0x000000FF] + masks[0x0000FF00] + masks[0x00FF0000]
elif bitcount == 32 and pfflags & DDPF.ALPHAPIXELS:
if pfflags & DDPF.ALPHAPIXELS:
self._mode = "RGBA"
rawmode = (
masks[0x000000FF]
+ masks[0x0000FF00]
+ masks[0x00FF0000]
+ masks[0xFF000000]
)
mask_count = 4
else:
msg = f"Unsupported bitcount {bitcount} for {pfflags}"
raise OSError(msg)
self._mode = "RGB"
mask_count = 3
masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
self.tile = [("dds_rgb", extents, 0, (bitcount, masks))]
return
elif pfflags & DDPF.LUMINANCE:
if bitcount == 8:
self._mode = "L"
@ -464,7 +460,6 @@ class DdsImageFile(ImageFile.ImageFile):
msg = f"Unknown pixel format flags {pfflags}"
raise NotImplementedError(msg)
extents = (0, 0) + self.size
if n:
self.tile = [
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
@ -476,6 +471,39 @@ class DdsImageFile(ImageFile.ImageFile):
pass
class DdsRgbDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
bitcount, masks = self.args
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
# Calculate how many zeros each mask is padded with
mask_offsets = []
# And the maximum value of each channel without the padding
mask_totals = []
for mask in masks:
offset = 0
if mask != 0:
while mask >> (offset + 1) << (offset + 1) == mask:
offset += 1
mask_offsets.append(offset)
mask_totals.append(mask >> offset)
data = bytearray()
bytecount = bitcount // 8
while len(data) < self.state.xsize * self.state.ysize * len(masks):
value = int.from_bytes(self.fd.read(bytecount), "little")
for i, mask in enumerate(masks):
masked_value = value & mask
# Remove the zero padding, and scale it to 8 bits
data += o8(
int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
)
self.set_as_raw(bytes(data))
return -1, 0
def _save(im, fp, filename):
if im.mode not in ("RGB", "RGBA", "L", "LA"):
msg = f"cannot write mode {im.mode} as DDS"
@ -533,5 +561,6 @@ def _accept(prefix):
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
Image.register_decoder("dds_rgb", DdsRgbDecoder)
Image.register_save(DdsImageFile.format, _save)
Image.register_extension(DdsImageFile.format, ".dds")

View File

@ -92,7 +92,7 @@ try:
raise ImportError(msg)
except ImportError as v:
core = DeferredError(ImportError("The _imaging C module is not installed."))
core = DeferredError.new(ImportError("The _imaging C module is not installed."))
# Explanations for ways that we know we might have an import error
if str(v).startswith("Module use of python"):
# The _imaging C module is present, but not compiled for
@ -530,15 +530,19 @@ class Image:
def __enter__(self):
return self
def _close_fp(self):
if getattr(self, "_fp", False):
if self._fp != self.fp:
self._fp.close()
self._fp = DeferredError(ValueError("Operation on closed image"))
if self.fp:
self.fp.close()
def __exit__(self, *args):
if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False):
if getattr(self, "_fp", False):
if self._fp != self.fp:
self._fp.close()
self._fp = DeferredError(ValueError("Operation on closed image"))
if self.fp:
self.fp.close()
self.fp = None
if hasattr(self, "fp"):
if getattr(self, "_exclusive_fp", False):
self._close_fp()
self.fp = None
def close(self):
"""
@ -554,12 +558,7 @@ class Image:
"""
if hasattr(self, "fp"):
try:
if getattr(self, "_fp", False):
if self._fp != self.fp:
self._fp.close()
self._fp = DeferredError(ValueError("Operation on closed image"))
if self.fp:
self.fp.close()
self._close_fp()
self.fp = None
except Exception as msg:
logger.debug("Error closing: %s", msg)
@ -2644,7 +2643,7 @@ class Image:
resample=Resampling.NEAREST,
fill=1,
fillcolor=None,
):
) -> Image:
"""
Transforms this image. This method creates a new image with the
given size, and the same mode as the original, and copies data
@ -3191,7 +3190,7 @@ def _decompression_bomb_check(size):
)
def open(fp, mode="r", formats=None):
def open(fp, mode="r", formats=None) -> Image:
"""
Opens and identifies the given image file.
@ -3416,7 +3415,7 @@ def merge(mode, bands):
# Plugin registry
def register_open(id, factory, accept=None):
def register_open(id, factory, accept=None) -> None:
"""
Register an image file plugin. This function should not be used
in application code.
@ -3470,7 +3469,7 @@ def register_save_all(id, driver):
SAVE_ALL[id.upper()] = driver
def register_extension(id, extension):
def register_extension(id, extension) -> None:
"""
Registers an image extension. This function should not be
used in application code.

View File

@ -28,7 +28,7 @@ except ImportError as ex:
# anything in core.
from ._util import DeferredError
_imagingcms = DeferredError(ex)
_imagingcms = DeferredError.new(ex)
DESCRIPTION = """
pyCMS

View File

@ -32,7 +32,7 @@ import io
import itertools
import struct
import sys
from typing import NamedTuple
from typing import Any, NamedTuple
from . import Image
from ._deprecate import deprecate
@ -94,7 +94,7 @@ class _Tile(NamedTuple):
encoder_name: str
extents: tuple[int, int, int, int]
offset: int
args: tuple | str | None
args: tuple[Any, ...] | str | None
#

View File

@ -396,7 +396,7 @@ class Color3DLUT(MultibandFilter):
if hasattr(table, "shape"):
try:
import numpy
except ImportError: # pragma: no cover
except ImportError:
pass
if numpy and isinstance(table, numpy.ndarray):

View File

@ -34,7 +34,7 @@ import warnings
from enum import IntEnum
from io import BytesIO
from pathlib import Path
from typing import IO
from typing import BinaryIO
from . import Image
from ._util import is_directory, is_path
@ -53,7 +53,7 @@ try:
except ImportError as ex:
from ._util import DeferredError
core = DeferredError(ex)
core = DeferredError.new(ex)
def _string_length_check(text):
@ -150,6 +150,7 @@ class ImageFont:
:py:mod:`PIL.Image.core` interface module.
"""
_string_length_check(text)
Image._decompression_bomb_check(self.font.getsize(text))
return self.font.getmask(text, mode)
def getbbox(self, text, *args, **kwargs):
@ -192,7 +193,7 @@ class FreeTypeFont:
def __init__(
self,
font: bytes | str | Path | IO | None = None,
font: bytes | str | Path | BinaryIO | None = None,
size: float = 10,
index: int = 0,
encoding: str = "",

View File

@ -14,17 +14,26 @@
#
from __future__ import annotations
from typing import Sequence
from . import Image
class Transform(Image.ImageTransformHandler):
def __init__(self, data):
method: Image.Transform
def __init__(self, data: Sequence[int]) -> None:
self.data = data
def getdata(self):
def getdata(self) -> tuple[int, Sequence[int]]:
return self.method, self.data
def transform(self, size, image, **options):
def transform(
self,
size: tuple[int, int],
image: Image.Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]],
) -> Image.Image:
# can be overridden
method, data = self.getdata()
return image.transform(size, method, data, **options)

View File

@ -16,31 +16,46 @@
#
from __future__ import annotations
import os
import tempfile
from io import BytesIO
from typing import Sequence
from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._deprecate import deprecate
COMPRESSION = {1: "raw", 5: "jpeg"}
PAD = b"\0\0\0\0"
def __getattr__(name: str) -> bytes:
if name == "PAD":
deprecate("IptcImagePlugin.PAD", 12)
return b"\0\0\0\0"
msg = f"module '{__name__}' has no attribute '{name}'"
raise AttributeError(msg)
#
# Helpers
def _i(c: bytes) -> int:
return i32((b"\0\0\0\0" + c)[-4:])
def _i8(c: int | bytes) -> int:
return c if isinstance(c, int) else c[0]
def i(c):
return i32((PAD + c)[-4:])
def i(c: bytes) -> int:
""".. deprecated:: 10.2.0"""
deprecate("IptcImagePlugin.i", 12)
return _i(c)
def dump(c):
def dump(c: Sequence[int | bytes]) -> None:
""".. deprecated:: 10.2.0"""
deprecate("IptcImagePlugin.dump", 12)
for i in c:
print("%02x" % _i8(i), end=" ")
print()
@ -55,10 +70,10 @@ class IptcImageFile(ImageFile.ImageFile):
format = "IPTC"
format_description = "IPTC/NAA"
def getint(self, key):
return i(self.info[key])
def getint(self, key: tuple[int, int]) -> int:
return _i(self.info[key])
def field(self):
def field(self) -> tuple[tuple[int, int] | None, int]:
#
# get a IPTC field header
s = self.fp.read(5)
@ -80,13 +95,13 @@ class IptcImageFile(ImageFile.ImageFile):
elif size == 128:
size = 0
elif size > 128:
size = i(self.fp.read(size - 128))
size = _i(self.fp.read(size - 128))
else:
size = i16(s, 3)
return tag, size
def _open(self):
def _open(self) -> None:
# load descriptive fields
while True:
offset = self.fp.tell()
@ -131,27 +146,22 @@ class IptcImageFile(ImageFile.ImageFile):
# tile
if tag == (8, 10):
self.tile = [
("iptc", (compression, offset), (0, 0, self.size[0], self.size[1]))
]
self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
def load(self):
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
return ImageFile.ImageFile.load(self)
type, tile, box = self.tile[0]
encoding, offset = tile
offset, compression = self.tile[0][2:]
self.fp.seek(offset)
# Copy image data to temporary file
o_fd, outfile = tempfile.mkstemp(text=False)
o = os.fdopen(o_fd)
if encoding == "raw":
o = BytesIO()
if compression == "raw":
# To simplify access to the extracted file,
# prepend a PPM header
o.write("P5\n%d %d\n255\n" % self.size)
o.write(b"P5\n%d %d\n255\n" % self.size)
while True:
type, size = self.field()
if type != (8, 10):
@ -162,17 +172,10 @@ class IptcImageFile(ImageFile.ImageFile):
break
o.write(s)
size -= len(s)
o.close()
try:
with Image.open(outfile) as _im:
_im.load()
self.im = _im.im
finally:
try:
os.unlink(outfile)
except OSError:
pass
with Image.open(o) as _im:
_im.load()
self.im = _im.im
Image.register_open(IptcImageFile.format, IptcImageFile)
@ -188,8 +191,6 @@ def getiptcinfo(im):
:returns: A dictionary containing IPTC information, or None if
no IPTC information block was found.
"""
import io
from . import JpegImagePlugin, TiffImagePlugin
data = None
@ -224,7 +225,7 @@ def getiptcinfo(im):
# parse the IPTC information chunk
im.info = {}
im.fp = io.BytesIO(data)
im.fp = BytesIO(data)
try:
im._open()

View File

@ -87,10 +87,12 @@ def APP(self, marker):
self.info["dpi"] = jfif_density
self.info["jfif_unit"] = jfif_unit
self.info["jfif_density"] = jfif_density
elif marker == 0xFFE1 and s[:5] == b"Exif\0":
if "exif" not in self.info:
# extract EXIF information (incomplete)
self.info["exif"] = s # FIXME: value will change
elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
# extract EXIF information
if "exif" in self.info:
self.info["exif"] += s[6:]
else:
self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
@ -783,6 +785,7 @@ def _save(im, fp, filename):
progressive,
info.get("smooth", 0),
optimize,
info.get("keep_rgb", False),
info.get("streamtype", 0),
dpi[0],
dpi[1],

View File

@ -43,7 +43,7 @@ except ImportError as ex:
# anything in core.
from ._util import DeferredError
FFI = ffi = DeferredError(ex)
FFI = ffi = DeferredError.new(ex)
logger = logging.getLogger(__name__)

View File

@ -1704,25 +1704,27 @@ def _save(im, fp, filename):
colormap += [0] * (256 - colors)
ifd[COLORMAP] = colormap
# data orientation
stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
# aim for given strip size (64 KB by default) when using libtiff writer
if libtiff:
im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
# JPEG encoder expects multiple of 8 rows
if compression == "jpeg":
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
else:
rows_per_strip = im.size[1]
if rows_per_strip == 0:
rows_per_strip = 1
strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip
strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip
ifd[ROWSPERSTRIP] = rows_per_strip
w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH]
stride = len(bits) * ((w * bits[0] + 7) // 8)
if ROWSPERSTRIP not in ifd:
# aim for given strip size (64 KB by default) when using libtiff writer
if libtiff:
im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h)
# JPEG encoder expects multiple of 8 rows
if compression == "jpeg":
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h)
else:
rows_per_strip = h
if rows_per_strip == 0:
rows_per_strip = 1
ifd[ROWSPERSTRIP] = rows_per_strip
strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP]
strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP]
if strip_byte_counts >= 2**16:
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),
stride * h - strip_byte_counts * (strips_per_image - 1),
)
ifd[STRIPOFFSETS] = tuple(
range(0, strip_byte_counts * strips_per_image, strip_byte_counts)

5
src/PIL/_imagingcms.pyi Normal file
View File

@ -0,0 +1,5 @@
from __future__ import annotations
from typing import Any
def __getattr__(name: str) -> Any: ...

5
src/PIL/_imagingft.pyi Normal file
View File

@ -0,0 +1,5 @@
from __future__ import annotations
from typing import Any
def __getattr__(name: str) -> Any: ...

18
src/PIL/_typing.py Normal file
View File

@ -0,0 +1,18 @@
from __future__ import annotations
import sys
if sys.version_info >= (3, 10):
from typing import TypeGuard
else:
try:
from typing_extensions import TypeGuard
except ImportError:
from typing import Any
class TypeGuard: # type: ignore[no-redef]
def __class_getitem__(cls, item: Any) -> type[bool]:
return bool
__all__ = ["TypeGuard"]

View File

@ -2,20 +2,31 @@ from __future__ import annotations
import os
from pathlib import Path
from typing import Any, NoReturn
from ._typing import TypeGuard
def is_path(f):
def is_path(f: Any) -> TypeGuard[bytes | str | Path]:
return isinstance(f, (bytes, str, Path))
def is_directory(f):
def is_directory(f: Any) -> TypeGuard[bytes | str | Path]:
"""Checks if an object is a string, and that it points to a directory."""
return is_path(f) and os.path.isdir(f)
class DeferredError:
def __init__(self, ex):
def __init__(self, ex: BaseException):
self.ex = ex
def __getattr__(self, elt):
def __getattr__(self, elt: str) -> NoReturn:
raise self.ex
@staticmethod
def new(ex: BaseException) -> Any:
"""
Creates an object that raises the wrapped exception ``ex`` when used,
and casts it to :py:obj:`~typing.Any` type.
"""
return DeferredError(ex)

View File

@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
__version__ = "10.2.0.dev0"
__version__ = "10.3.0.dev0"

View File

@ -2649,6 +2649,26 @@ _font_new(PyObject *self_, PyObject *args) {
self->glyphs[i].sy0 = S16(B16(glyphdata, 14));
self->glyphs[i].sx1 = S16(B16(glyphdata, 16));
self->glyphs[i].sy1 = S16(B16(glyphdata, 18));
// Do not allow glyphs to extend beyond bitmap image
// Helps prevent DOS by stopping cropped images being larger than the original
if (self->glyphs[i].sx0 < 0) {
self->glyphs[i].dx0 -= self->glyphs[i].sx0;
self->glyphs[i].sx0 = 0;
}
if (self->glyphs[i].sy0 < 0) {
self->glyphs[i].dy0 -= self->glyphs[i].sy0;
self->glyphs[i].sy0 = 0;
}
if (self->glyphs[i].sx1 > self->bitmap->xsize) {
self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize;
self->glyphs[i].sx1 = self->bitmap->xsize;
}
if (self->glyphs[i].sy1 > self->bitmap->ysize) {
self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize;
self->glyphs[i].sy1 = self->bitmap->ysize;
}
if (self->glyphs[i].dy0 < y0) {
y0 = self->glyphs[i].dy0;
}
@ -2721,7 +2741,7 @@ _font_text_asBytes(PyObject *encoded_string, unsigned char **text) {
static PyObject *
_font_getmask(ImagingFontObject *self, PyObject *args) {
Imaging im;
Imaging bitmap;
Imaging bitmap = NULL;
int x, b;
int i = 0;
int status;
@ -2730,7 +2750,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
PyObject *encoded_string;
unsigned char *text;
char *mode = "";
char *mode;
if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) {
return NULL;
@ -2753,10 +2773,13 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
b = self->baseline;
for (x = 0; text[i]; i++) {
glyph = &self->glyphs[text[i]];
bitmap =
ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
if (!bitmap) {
goto failed;
if (i == 0 || text[i] != text[i - 1]) {
ImagingDelete(bitmap);
bitmap =
ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
if (!bitmap) {
goto failed;
}
}
status = ImagingPaste(
im,
@ -2766,17 +2789,18 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
glyph->dy0 + b,
glyph->dx1 + x,
glyph->dy1 + b);
ImagingDelete(bitmap);
if (status < 0) {
goto failed;
}
x = x + glyph->dx;
b = b + glyph->dy;
}
ImagingDelete(bitmap);
free(text);
return PyImagingNew(im);
failed:
ImagingDelete(bitmap);
free(text);
ImagingDelete(im);
Py_RETURN_NONE;

View File

@ -1042,6 +1042,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
Py_ssize_t progressive = 0;
Py_ssize_t smooth = 0;
Py_ssize_t optimize = 0;
int keep_rgb = 0;
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
Py_ssize_t xdpi = 0, ydpi = 0;
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
@ -1059,13 +1060,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
"ss|nnnnnnnnnnOz#y#y#",
"ss|nnnnpnnnnnnOz#y#y#",
&mode,
&rawmode,
&quality,
&progressive,
&smooth,
&optimize,
&keep_rgb,
&streamtype,
&xdpi,
&ydpi,
@ -1150,6 +1152,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8);
((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb;
((JPEGENCODERSTATE *)encoder->state.context)->quality = quality;
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;

View File

@ -74,6 +74,9 @@ typedef struct {
/* Optimize Huffman tables (slow) */
int optimize;
/* Disable automatic conversion of RGB images to YCbCr if nonzero */
int keep_rgb;
/* Stream type (0=full, 1=tables only, 2=image only) */
int streamtype;

View File

@ -137,6 +137,30 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
/* Compressor configuration */
jpeg_set_defaults(&context->cinfo);
/* Prevent RGB -> YCbCr conversion */
if (context->keep_rgb) {
switch (context->cinfo.in_color_space) {
case JCS_RGB:
#ifdef JCS_EXTENSIONS
case JCS_EXT_RGBX:
#endif
switch (context->subsampling) {
case -1: /* Default */
case 0: /* No subsampling */
break;
default:
/* Would subsample the green and blue
channels, which doesn't make sense */
state->errcode = IMAGING_CODEC_CONFIG;
return -1;
}
jpeg_set_colorspace(&context->cinfo, JCS_RGB);
break;
default:
break;
}
}
/* Use custom quantization tables */
if (context->qtables) {
int i;

View File

@ -35,5 +35,7 @@ skip_install = true
deps =
mypy==1.7.1
numpy
extras =
typing
commands =
mypy src {posargs}