Merge branch 'python-pillow:main' into parametrize

This commit is contained in:
Yay295 2022-09-04 20:49:36 -05:00 committed by GitHub
commit 69de03ba5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 161 additions and 85 deletions

View File

@ -37,8 +37,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
# TODO Remove condition when NumPy supports 3.11 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then

View File

@ -11,6 +11,9 @@ on:
- "**.h" - "**.h"
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
Fuzzing: Fuzzing:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -14,8 +14,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
# TODO Remove condition when NumPy supports 3.11 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -2,6 +2,9 @@ name: Test Cygwin
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest

View File

@ -5,6 +5,30 @@ Changelog (Pillow)
9.3.0 (unreleased) 9.3.0 (unreleased)
------------------ ------------------
- Copy palette when converting from P to PA #6497
[radarhere]
- Allow RGB and RGBA values for PA image putpixel #6504
[radarhere]
- Removed support for tkinter in PyPy before Python 3.6 #6551
[nulano]
- Do not use CCITTFaxDecode filter if libtiff is not available #6518
[radarhere]
- Fallback to not using mmap if buffer is not large enough #6510
[radarhere]
- Fixed writing bytes as ASCII tag #6493
[radarhere]
- Open 1 bit EPS in mode 1 #6499
[radarhere]
- Removed support for tkinter before Python 1.5.2 #6549
[radarhere]
- Allow default ImageDraw font to be set #6484 - Allow default ImageDraw font to be set #6484
[radarhere, hugovk] [radarhere, hugovk]

BIN
Tests/images/1.eps Normal file

Binary file not shown.

BIN
Tests/images/mmap_error.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -39,6 +39,13 @@ def test_invalid_file():
BmpImagePlugin.BmpImageFile(fp) BmpImagePlugin.BmpImageFile(fp)
def test_fallback_if_mmap_errors():
# This image has been truncated,
# so that the buffer is not large enough when using mmap
with Image.open("Tests/images/mmap_error.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp")
def test_save_to_bytes(): def test_save_to_bytes():
output = io.BytesIO() output = io.BytesIO()
im = hopper() im = hopper()

View File

@ -146,6 +146,11 @@ def test_bytesio_object():
assert_image_similar(img, image1_scale1_compare, 5) assert_image_similar(img, image1_scale1_compare, 5)
def test_1_mode():
with Image.open("Tests/images/1.eps") as im:
assert im.mode == "1"
def test_image_mode_not_supported(tmp_path): def test_image_mode_not_supported(tmp_path):
im = hopper("RGBA") im = hopper("RGBA")
tmpfile = str(tmp_path / "temp.eps") tmpfile = str(tmp_path / "temp.eps")

View File

@ -6,7 +6,7 @@ import time
import pytest import pytest
from PIL import Image, PdfParser from PIL import Image, PdfParser, features
from .helper import hopper, mark_if_feature_version from .helper import hopper, mark_if_feature_version
@ -44,7 +44,7 @@ def test_monochrome(tmp_path):
# Act / Assert # Act / Assert
outfile = helper_save_as_pdf(tmp_path, mode) outfile = helper_save_as_pdf(tmp_path, mode)
assert os.path.getsize(outfile) < 5000 assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000)
def test_greyscale(tmp_path): def test_greyscale(tmp_path):

View File

@ -185,6 +185,22 @@ def test_iptc(tmp_path):
im.save(out) im.save(out)
def test_writing_bytes_to_ascii(tmp_path):
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271]
assert tag.type == TiffTags.ASCII
info[271] = b"test"
out = str(tmp_path / "temp.tiff")
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert reloaded.tag_v2[271] == "test"
def test_undefined_zero(tmp_path): def test_undefined_zero(tmp_path):
# Check that the tag has not been changed since this test was created # Check that the tag has not been changed since this test was created
tag = TiffTags.TAGS_V2[45059] tag = TiffTags.TAGS_V2[45059]

View File

@ -215,11 +215,14 @@ class TestImageGetPixel(AccessTest):
self.check(mode, 2**15 + 1) self.check(mode, 2**15 + 1)
self.check(mode, 2**16 - 1) self.check(mode, 2**16 - 1)
@pytest.mark.parametrize("mode", ("P", "PA"))
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
def test_p_putpixel_rgb_rgba(self, color): def test_p_putpixel_rgb_rgba(self, mode, color):
im = Image.new("P", (1, 1), 0) im = Image.new(mode, (1, 1))
im.putpixel((0, 0), color) im.putpixel((0, 0), color)
assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
alpha = color[3] if len(color) == 4 and mode == "PA" else 255
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
@pytest.mark.skipif(cffi is None, reason="No CFFI") @pytest.mark.skipif(cffi is None, reason="No CFFI")
@ -340,12 +343,15 @@ class TestCffi(AccessTest):
# pixels can contain garbage if image is released # pixels can contain garbage if image is released
assert px[i, 0] == 0 assert px[i, 0] == 0
def test_p_putpixel_rgb_rgba(self): @pytest.mark.parametrize("mode", ("P", "PA"))
for color in [(255, 0, 0), (255, 0, 0, 255)]: def test_p_putpixel_rgb_rgba(self, mode):
im = Image.new("P", (1, 1), 0) for color in [(255, 0, 0), (255, 0, 0, 127)]:
im = Image.new(mode, (1, 1))
access = PyAccess.new(im, False) access = PyAccess.new(im, False)
access.putpixel((0, 0), color) access.putpixel((0, 0), color)
assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
alpha = color[3] if len(color) == 4 and mode == "PA" else 255
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
class TestImagePutPixelError(AccessTest): class TestImagePutPixelError(AccessTest):

View File

@ -236,6 +236,12 @@ def test_p2pa_alpha():
assert im_a.getpixel((x, y)) == alpha assert im_a.getpixel((x, y)) == alpha
def test_p2pa_palette():
with Image.open("Tests/images/tiny.png") as im:
im_pa = im.convert("PA")
assert im_pa.getpalette() == im.getpalette()
def test_matrix_illegal_conversion(): def test_matrix_illegal_conversion():
# Arrange # Arrange
im = hopper("CMYK") im = hopper("CMYK")

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install libimagequant # install libimagequant
archive=libimagequant-4.0.2 archive=libimagequant-4.0.4
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -837,6 +837,24 @@ Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``,
``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and ``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and
run-length encoded TGAs. run-length encoded TGAs.
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**compression**
If set to "tga_rle", the file will be run-length encoded.
.. versionadded:: 5.3.0
**id_section**
The identification field.
.. versionadded:: 5.3.0
**orientation**
If present and a positive number, the first pixel is for the top left corner,
rather than the bottom left corner.
.. versionadded:: 5.3.0
TIFF TIFF
^^^^ ^^^^

View File

@ -166,7 +166,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.0.2** * Pillow has been tested with libimagequant **2.6-4.0.4**
* Libimagequant is licensed GPLv3, which is more restrictive than * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.

View File

@ -73,7 +73,7 @@ Access using negative indexes is also possible.
Modifies the pixel at x,y. The color is given as a single Modifies the pixel at x,y. The color is given as a single
numerical value for single band images, and a tuple for numerical value for single band images, and a tuple for
multi-band images. In addition to this, RGB and RGBA tuples multi-band images. In addition to this, RGB and RGBA tuples
are accepted for P images. are accepted for P and PA images.
:param xy: The pixel coordinate, given as (x, y). :param xy: The pixel coordinate, given as (x, y).
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode) :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)

View File

@ -288,11 +288,14 @@ class EpsImageFile(ImageFile.ImageFile):
# Encoded bitmapped image. # Encoded bitmapped image.
x, y, bi, mo = s[11:].split(None, 7)[:4] x, y, bi, mo = s[11:].split(None, 7)[:4]
if int(bi) != 8: if int(bi) == 1:
break self.mode = "1"
try: elif int(bi) == 8:
self.mode = self.mode_map[int(mo)] try:
except ValueError: self.mode = self.mode_map[int(mo)]
except ValueError:
break
else:
break break
self._size = int(x), int(y) self._size = int(x), int(y)

View File

@ -1839,7 +1839,7 @@ class Image:
Modifies the pixel at the given position. The color is given as Modifies the pixel at the given position. The color is given as
a single numerical value for single-band images, and a tuple for a single numerical value for single-band images, and a tuple for
multi-band images. In addition to this, RGB and RGBA tuples are multi-band images. In addition to this, RGB and RGBA tuples are
accepted for P images. accepted for P and PA images.
Note that this method is relatively slow. For more extensive changes, Note that this method is relatively slow. For more extensive changes,
use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw`
@ -1864,12 +1864,17 @@ class Image:
return self.pyaccess.putpixel(xy, value) return self.pyaccess.putpixel(xy, value)
if ( if (
self.mode == "P" self.mode in ("P", "PA")
and isinstance(value, (list, tuple)) and isinstance(value, (list, tuple))
and len(value) in [3, 4] and len(value) in [3, 4]
): ):
# RGB or RGBA value for a P image # RGB or RGBA value for a P or PA image
if self.mode == "PA":
alpha = value[3] if len(value) == 4 else 255
value = value[:3]
value = self.palette.getcolor(value, self) value = self.palette.getcolor(value, self)
if self.mode == "PA":
value = (value, alpha)
return self.im.putpixel(xy, value) return self.im.putpixel(xy, value)
def remap_palette(self, dest_map, source_palette=None): def remap_palette(self, dest_map, source_palette=None):

View File

@ -192,6 +192,9 @@ class ImageFile(Image.Image):
with open(self.filename) as fp: with open(self.filename) as fp:
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
if offset + self.size[1] * args[1] > self.map.size():
# buffer is not large enough
raise OSError
self.im = Image.core.map_buffer( self.im = Image.core.map_buffer(
self.map, self.size, decoder_name, offset, args self.map, self.size, decoder_name, offset, args
) )

View File

@ -68,21 +68,7 @@ def _pyimagingtkcall(command, photo, id):
# may raise an error if it cannot attach to Tkinter # may raise an error if it cannot attach to Tkinter
from . import _imagingtk from . import _imagingtk
try: _imagingtk.tkinit(tk.interpaddr())
if hasattr(tk, "interp"):
# Required for PyPy, which always has CFFI installed
from cffi import FFI
ffi = FFI()
# PyPy is using an FFI CDATA element
# (Pdb) self.tk.interp
# <cdata 'Tcl_Interp *' 0x3061b50>
_imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
else:
_imagingtk.tkinit(tk.interpaddr(), 1)
except AttributeError:
_imagingtk.tkinit(id(tk), 0)
tk.call(command, photo, id) tk.call(command, photo, id)

View File

@ -25,7 +25,7 @@ import math
import os import os
import time import time
from . import Image, ImageFile, ImageSequence, PdfParser, __version__ from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -130,20 +130,23 @@ def _save(im, fp, filename, save_all=False):
width, height = im.size width, height = im.size
if im.mode == "1": if im.mode == "1":
filter = "CCITTFaxDecode" if features.check("libtiff"):
bits = 1 filter = "CCITTFaxDecode"
params = PdfParser.PdfArray( bits = 1
[ params = PdfParser.PdfArray(
PdfParser.PdfDict( [
{ PdfParser.PdfDict(
"K": -1, {
"BlackIs1": True, "K": -1,
"Columns": width, "BlackIs1": True,
"Rows": height, "Columns": width,
} "Rows": height,
) }
] )
) ]
)
else:
filter = "DCTDecode"
colorspace = PdfParser.PdfName("DeviceGray") colorspace = PdfParser.PdfName("DeviceGray")
procset = "ImageB" # grayscale procset = "ImageB" # grayscale
elif im.mode == "L": elif im.mode == "L":

View File

@ -58,7 +58,7 @@ class PyAccess:
# Keep pointer to im object to prevent dereferencing. # Keep pointer to im object to prevent dereferencing.
self._im = img.im self._im = img.im
if self._im.mode == "P": if self._im.mode in ("P", "PA"):
self._palette = img.palette self._palette = img.palette
# Debugging is polluting test traces, only useful here # Debugging is polluting test traces, only useful here
@ -89,12 +89,17 @@ class PyAccess:
(x, y) = self.check_xy((x, y)) (x, y) = self.check_xy((x, y))
if ( if (
self._im.mode == "P" self._im.mode in ("P", "PA")
and isinstance(color, (list, tuple)) and isinstance(color, (list, tuple))
and len(color) in [3, 4] and len(color) in [3, 4]
): ):
# RGB or RGBA value for a P image # RGB or RGBA value for a P or PA image
if self._im.mode == "PA":
alpha = color[3] if len(color) == 4 else 255
color = color[:3]
color = self._palette.getcolor(color, self._img) color = self._palette.getcolor(color, self._img)
if self._im.mode == "PA":
color = (color, alpha)
return self.set_pixel(x, y, color) return self.set_pixel(x, y, color)

View File

@ -727,7 +727,9 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_writer(2) @_register_writer(2)
def write_string(self, value): def write_string(self, value):
# remerge of https://github.com/python-pillow/Pillow/pull/1416 # remerge of https://github.com/python-pillow/Pillow/pull/1416
return b"" + value.encode("ascii", "replace") + b"\0" if not isinstance(value, bytes):
value = value.encode("ascii", "replace")
return value + b"\0"
@_register_loader(5, 8) @_register_loader(5, 8)
def load_rational(self, data, legacy_api=True): def load_rational(self, data, legacy_api=True):
@ -1153,7 +1155,7 @@ class TiffImageFile(ImageFile.ImageFile):
:returns: XMP tags in a dictionary. :returns: XMP tags in a dictionary.
""" """
return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {} return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}
def get_photoshop_blocks(self): def get_photoshop_blocks(self):
""" """
@ -1328,7 +1330,7 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug(f"- photometric_interpretation: {photo}") logger.debug(f"- photometric_interpretation: {photo}")
logger.debug(f"- planar_configuration: {self._planar_configuration}") logger.debug(f"- planar_configuration: {self._planar_configuration}")
logger.debug(f"- fill_order: {fillorder}") logger.debug(f"- fill_order: {fillorder}")
logger.debug(f"- YCbCr subsampling: {self.tag.get(530)}") logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}")
# size # size
xsize = int(self.tag_v2.get(IMAGEWIDTH)) xsize = int(self.tag_v2.get(IMAGEWIDTH))
@ -1469,8 +1471,8 @@ class TiffImageFile(ImageFile.ImageFile):
else: else:
# tiled image # tiled image
offsets = self.tag_v2[TILEOFFSETS] offsets = self.tag_v2[TILEOFFSETS]
w = self.tag_v2.get(322) w = self.tag_v2.get(TILEWIDTH)
h = self.tag_v2.get(323) h = self.tag_v2.get(TILELENGTH)
for offset in offsets: for offset in offsets:
if x + w > xsize: if x + w > xsize:

View File

@ -23,33 +23,16 @@ TkImaging_Init(Tcl_Interp *interp);
extern int extern int
load_tkinter_funcs(void); load_tkinter_funcs(void);
/* copied from _tkinter.c (this isn't as bad as it may seem: for new
versions, we use _tkinter's interpaddr hook instead, and all older
versions use this structure layout) */
typedef struct {
PyObject_HEAD Tcl_Interp *interp;
} TkappObject;
static PyObject * static PyObject *
_tkinit(PyObject *self, PyObject *args) { _tkinit(PyObject *self, PyObject *args) {
Tcl_Interp *interp; Tcl_Interp *interp;
PyObject *arg; PyObject *arg;
int is_interp; if (!PyArg_ParseTuple(args, "O", &arg)) {
if (!PyArg_ParseTuple(args, "Oi", &arg, &is_interp)) {
return NULL; return NULL;
} }
if (is_interp) { interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg);
interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg);
} else {
TkappObject *app;
/* Do it the hard way. This will break if the TkappObject
layout changes */
app = (TkappObject *)PyLong_AsVoidPtr(arg);
interp = app->interp;
}
/* This will bomb if interp is invalid... */ /* This will bomb if interp is invalid... */
TkImaging_Init(interp); TkImaging_Init(interp);

View File

@ -1243,7 +1243,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) {
if (!imOut) { if (!imOut) {
return NULL; return NULL;
} }
if (strcmp(mode, "P") == 0) { if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) {
ImagingPaletteDelete(imOut->palette); ImagingPaletteDelete(imOut->palette);
imOut->palette = ImagingPaletteDuplicate(imIn->palette); imOut->palette = ImagingPaletteDuplicate(imIn->palette);
} }

View File

@ -916,7 +916,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
dump_state(clientstate); dump_state(clientstate);
if (state->state == 0) { if (state->state == 0) {
TRACE(("Encoding line bt line")); TRACE(("Encoding line by line"));
while (state->y < state->ysize) { while (state->y < state->ysize) {
state->shuffle( state->shuffle(
state->buffer, state->buffer,

View File

@ -11,8 +11,8 @@ For more extensive info, see the [Windows build instructions](build.rst).
* Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires Microsoft Visual Studio 2017 or newer with C++ component.
* Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires NASM for libjpeg-turbo, a required dependency when using this script.
* Requires CMake 3.12 or newer (available as Visual Studio component). * Requires CMake 3.12 or newer (available as Visual Studio component).
* Tested on Windows Server 2016 with Visual Studio 2017 Community (AppVeyor). * Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor).
* Tested on Windows Server 2019 with Visual Studio 2019 Enterprise (GitHub Actions). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
The following is a simplified version of the script used on AppVeyor: The following is a simplified version of the script used on AppVeyor:
``` ```