Skip to content

Commit

Permalink
Merge branch 'main' into winbuild-update
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed Oct 14, 2022
2 parents 7242a8a + 7ff0592 commit 147c52f
Show file tree
Hide file tree
Showing 24 changed files with 198 additions and 58 deletions.
24 changes: 24 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ Changelog (Pillow)
9.3.0 (unreleased)
------------------

- Don't reassign crc on ChunkStream close #6627
[wiredfool, radarhere]

- Raise a warning if NumPy failed to raise an error during conversion #6594
[radarhere]

- Show all frames in ImageShow #6611
[radarhere]

- Allow FLI palette chunk to not be first #6626
[radarhere]

- If first GIF frame has transparency for RGB_ALWAYS loading strategy, use RGBA mode #6592
[radarhere]

- Round box position to integer when pasting embedded color #6517
[radarhere, nulano]

- Removed EXIF prefix when saving WebP #6582
[radarhere]

- Pad IM palette to 768 bytes when saving #6579
[radarhere]

- Added DDS BC6 reading #6449
[ShadelessFox, REDxEYE, radarhere]

Expand Down
Binary file added Tests/images/bw_gradient.imt
Binary file not shown.
Binary file added Tests/images/hopper_palette_chunk_second.fli
Binary file not shown.
Binary file added Tests/images/text_float_coord.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/text_float_coord_1_alt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion Tests/test_file_fli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from PIL import FliImagePlugin, Image

from .helper import assert_image_equal_tofile, is_pypy
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy

# created as an export of a palette image from Gimp2.6
# save as...-> hopper.fli, default options.
Expand Down Expand Up @@ -79,6 +79,12 @@ def test_invalid_file():
FliImagePlugin.FliImageFile(invalid_file)


def test_palette_chunk_second():
with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im:
with Image.open(static_test_file) as expected:
assert_image_equal(im.convert("RGB"), expected.convert("RGB"))


def test_n_frames():
with Image.open(static_test_file) as im:
assert im.n_frames == 1
Expand Down
17 changes: 12 additions & 5 deletions Tests/test_file_gif.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,24 @@ def test_l_mode_transparency():


def test_strategy():
with Image.open("Tests/images/iss634.gif") as im:
expected_rgb_always = im.convert("RGB")

with Image.open("Tests/images/chi.gif") as im:
expected_zero = im.convert("RGB")
expected_rgb_always_rgba = im.convert("RGBA")

im.seek(1)
expected_one = im.convert("RGB")
expected_different = im.convert("RGB")

try:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
with Image.open("Tests/images/chi.gif") as im:
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "RGB"
assert_image_equal(im, expected_zero)
assert_image_equal(im, expected_rgb_always)

with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "RGBA"
assert_image_equal(im, expected_rgb_always_rgba)

GifImagePlugin.LOADING_STRATEGY = (
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
Expand All @@ -105,7 +112,7 @@ def test_strategy():

im.seek(1)
assert im.mode == "P"
assert_image_equal(im.convert("RGB"), expected_one)
assert_image_equal(im.convert("RGB"), expected_different)

# Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im:
Expand Down
12 changes: 12 additions & 0 deletions Tests/test_file_im.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ def test_roundtrip(mode, tmp_path):
assert_image_equal_tofile(im, out)


def test_small_palette(tmp_path):
im = Image.new("P", (1, 1))
colors = [0, 1, 2]
im.putpalette(colors)

out = str(tmp_path / "temp.im")
im.save(out)

with Image.open(out) as reloaded:
assert reloaded.getpalette() == colors + [0] * 765


def test_save_unsupported_mode(tmp_path):
out = str(tmp_path / "temp.im")
im = hopper("HSV")
Expand Down
19 changes: 19 additions & 0 deletions Tests/test_file_imt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import io

import pytest

from PIL import Image, ImtImagePlugin

from .helper import assert_image_equal_tofile


def test_sanity():
with Image.open("Tests/images/bw_gradient.imt") as im:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")


@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n"))
def test_invalid_file(data):
with io.BytesIO(data) as fp:
with pytest.raises(SyntaxError):
ImtImagePlugin.ImtImageFile(fp)
4 changes: 1 addition & 3 deletions Tests/test_file_webp_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ def test_write_exif_metadata():
test_buffer.seek(0)
with Image.open(test_buffer) as webp_image:
webp_exif = webp_image.info.get("exif", None)
assert webp_exif
if webp_exif:
assert webp_exif == expected_exif, "WebP EXIF didn't match"
assert webp_exif == expected_exif[6:], "WebP EXIF didn't match"


def test_read_icc_profile():
Expand Down
7 changes: 5 additions & 2 deletions Tests/test_image_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ def test_with_dtype(dtype):
test_with_dtype(numpy.float64)
test_with_dtype(numpy.uint8)

if parse_version(numpy.__version__) >= parse_version("1.23"):
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
if parse_version(numpy.__version__) >= parse_version("1.23"):
with pytest.raises(OSError):
numpy.array(im_truncated)
else:
with pytest.warns(UserWarning):
numpy.array(im_truncated)


def test_fromarray():
Expand Down
25 changes: 24 additions & 1 deletion Tests/test_imagefont.py
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,30 @@ def test_standard_embedded_color(layout_engine):
d = ImageDraw.Draw(im)
d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True)

assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2)
assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1)


@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA"))
def test_float_coord(layout_engine, fontmode):
txt = "Hello World!"
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)

im = Image.new("RGB", (300, 64), "white")
d = ImageDraw.Draw(im)
if fontmode == "1":
d.fontmode = "1"

embedded_color = fontmode == "RGBA"
d.text((9.5, 9.5), txt, font=ttf, fill="#fa6", embedded_color=embedded_color)
try:
assert_image_similar_tofile(im, "Tests/images/text_float_coord.png", 3.9)
except AssertionError:
if fontmode == "1" and layout_engine == ImageFont.Layout.BASIC:
assert_image_similar_tofile(
im, "Tests/images/text_float_coord_1_alt.png", 1
)
else:
raise


def test_cbdt(layout_engine):
Expand Down
6 changes: 2 additions & 4 deletions Tests/test_imagemath.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
def pixel(im):
if hasattr(im, "im"):
return f"{im.mode} {repr(im.getpixel((0, 0)))}"
else:
if isinstance(im, int):
return int(im) # hack to deal with booleans
print(im)
elif isinstance(im, int):
return int(im) # hack to deal with booleans


A = Image.new("L", (1, 1), 1)
Expand Down
5 changes: 4 additions & 1 deletion docs/handbook/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ Pillow also provides limited support for a few additional modes, including:
* ``BGR;24`` (24-bit reversed true colour)
* ``BGR;32`` (32-bit reversed true colour)

However, Pillow doesn’t support user-defined modes; if you need to handle band
Apart from these additional modes, Pillow doesn't yet support multichannel
images with a depth of more than 8 bits per channel.

Pillow also doesn’t support user-defined modes; if you need to handle band
combinations that are not listed above, use a sequence of Image objects.

You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode`
Expand Down
19 changes: 14 additions & 5 deletions src/PIL/FliImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# See the README file for information on usage and redistribution.
#

import os

from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
Expand Down Expand Up @@ -80,11 +81,19 @@ def _open(self):

if i16(s, 4) == 0xF1FA:
# look for palette chunk
s = self.fp.read(6)
if i16(s, 4) == 11:
self._palette(palette, 2)
elif i16(s, 4) == 4:
self._palette(palette, 0)
number_of_subchunks = i16(s, 6)
chunk_size = None
for _ in range(number_of_subchunks):
if chunk_size is not None:
self.fp.seek(chunk_size - 6, os.SEEK_CUR)
s = self.fp.read(6)
chunk_type = i16(s, 4)
if chunk_type in (4, 11):
self._palette(palette, 2 if chunk_type == 11 else 0)
break
chunk_size = i32(s)
if not chunk_size:
break

palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette]
self.palette = ImagePalette.raw("RGB", b"".join(palette))
Expand Down
24 changes: 15 additions & 9 deletions src/PIL/GifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,13 @@ def _seek(self, frame, update_image=True):
self.im.paste(self.dispose, self.dispose_extent)

self._frame_palette = palette or self.global_palette
self._frame_transparency = frame_transparency
if frame == 0:
if self._frame_palette:
self.mode = (
"RGB" if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS else "P"
)
if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
self.mode = "RGBA" if frame_transparency is not None else "RGB"
else:
self.mode = "P"
else:
self.mode = "L"

Expand All @@ -313,7 +315,6 @@ def _seek(self, frame, update_image=True):
palette = copy(self.global_palette)
self.palette = palette
else:
self._frame_transparency = frame_transparency
if self.mode == "P":
if (
LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
Expand Down Expand Up @@ -386,7 +387,8 @@ def _rgb(color):
transparency = -1
if frame_transparency is not None:
if frame == 0:
self.info["transparency"] = frame_transparency
if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:
self.info["transparency"] = frame_transparency
elif self.mode not in ("RGB", "RGBA"):
transparency = frame_transparency
self.tile = [
Expand All @@ -410,9 +412,9 @@ def load_prepare(self):
temp_mode = "P" if self._frame_palette else "L"
self._prev_im = None
if self.__frame == 0:
if "transparency" in self.info:
if self._frame_transparency is not None:
self.im = Image.core.fill(
temp_mode, self.size, self.info["transparency"]
temp_mode, self.size, self._frame_transparency
)
elif self.mode in ("RGB", "RGBA"):
self._prev_im = self.im
Expand All @@ -429,8 +431,12 @@ def load_prepare(self):
def load_end(self):
if self.__frame == 0:
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
self.mode = "RGB"
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
if self._frame_transparency is not None:
self.im.putpalettealpha(self._frame_transparency, 0)
self.mode = "RGBA"
else:
self.mode = "RGB"
self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
return
if self.mode == "P" and self._prev_im:
if self._frame_transparency is not None:
Expand Down
8 changes: 7 additions & 1 deletion src/PIL/ImImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,13 @@ def _save(im, fp, filename):
fp.write(b"Lut: 1\r\n")
fp.write(b"\000" * (511 - fp.tell()) + b"\032")
if im.mode in ["P", "PA"]:
fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes
im_palette = im.im.getpalette("RGB", "RGB;L")
colors = len(im_palette) // 3
palette = b""
for i in range(3):
palette += im_palette[colors * i : colors * (i + 1)]
palette += b"\x00" * (256 - colors)
fp.write(palette) # 768 bytes
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))])


Expand Down
24 changes: 18 additions & 6 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,12 +679,24 @@ def __array_interface__(self):
new["shape"] = shape
new["typestr"] = typestr
new["version"] = 3
if self.mode == "1":
# Binary images need to be extended from bits to bytes
# See: https://github.com/python-pillow/Pillow/issues/350
new["data"] = self.tobytes("raw", "L")
else:
new["data"] = self.tobytes()
try:
if self.mode == "1":
# Binary images need to be extended from bits to bytes
# See: https://github.com/python-pillow/Pillow/issues/350
new["data"] = self.tobytes("raw", "L")
else:
new["data"] = self.tobytes()
except Exception as e:
if not isinstance(e, (MemoryError, RecursionError)):
try:
import numpy
from packaging.version import parse as parse_version
except ImportError:
pass
else:
if parse_version(numpy.__version__) < parse_version("1.23"):
warnings.warn(e)
raise
return new

def __getstate__(self):
Expand Down
4 changes: 2 additions & 2 deletions src/PIL/ImageDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,8 @@ def draw_text(ink, stroke_width=0, stroke_offset=None):
# extract mask and set text alpha
color, mask = mask, mask.getband(3)
color.fillband(3, (ink >> 24) & 0xFF)
coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1]
self.im.paste(color, coord + coord2, mask)
x, y = (int(c) for c in coord)
self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
else:
self.draw.draw_bitmap(coord, mask, ink)

Expand Down
6 changes: 3 additions & 3 deletions src/PIL/ImageShow.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class WindowsViewer(Viewer):
"""The default viewer on Windows is the default system application for PNG files."""

format = "PNG"
options = {"compress_level": 1}
options = {"compress_level": 1, "save_all": True}

def get_command(self, file, **options):
return (
Expand All @@ -154,7 +154,7 @@ class MacViewer(Viewer):
"""The default viewer on macOS using ``Preview.app``."""

format = "PNG"
options = {"compress_level": 1}
options = {"compress_level": 1, "save_all": True}

def get_command(self, file, **options):
# on darwin open returns immediately resulting in the temp
Expand Down Expand Up @@ -197,7 +197,7 @@ def show_file(self, path=None, **options):

class UnixViewer(Viewer):
format = "PNG"
options = {"compress_level": 1}
options = {"compress_level": 1, "save_all": True}

def get_command(self, file, **options):
command = self.get_command_ex(file, **options)[0]
Expand Down
Loading

0 comments on commit 147c52f

Please sign in to comment.