Skip to content

Commit

Permalink
Merge branch 'main' into context_manager
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed Jan 3, 2025
2 parents 35f3299 + 9ae8cb8 commit 3c3a2e6
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 87 deletions.
18 changes: 11 additions & 7 deletions .github/workflows/wheels-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,16 @@ fi
ARCHIVE_SDIR=pillow-depends-main

# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.1.0
LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then
GIFLIB_VERSION=5.2.2
else
GIFLIB_VERSION=5.2.1
fi
ZLIB_NG_VERSION=2.2.2
GIFLIB_VERSION=5.2.2
ZLIB_NG_VERSION=2.2.3
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
Expand Down Expand Up @@ -139,6 +135,14 @@ function build {
CFLAGS="$CFLAGS -O3 -DNDEBUG"
if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
# For giflib 5.2.2
elif [ -n "$IS_ALPINE" ]; then
apk add imagemagick
else
if [[ "$MB_ML_VER" == "_2_28" ]]; then
yum install -y epel-release
fi
yum install -y ImageMagick
fi
build_libwebp
CFLAGS=$ORIGINAL_CFLAGS
Expand Down
10 changes: 9 additions & 1 deletion Tests/test_file_blp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from PIL import Image
from PIL import BlpImagePlugin, Image

from .helper import (
assert_image_equal,
Expand All @@ -19,6 +19,7 @@ def test_load_blp1() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")

with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
assert im.mode == "RGBA"
im.load()


Expand All @@ -37,6 +38,13 @@ def test_load_blp2_dxt1a() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")


def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"

with pytest.raises(BlpImagePlugin.BLPFormatError):
BlpImagePlugin.BlpImageFile(invalid_file)


def test_save(tmp_path: Path) -> None:
f = str(tmp_path / "temp.blp")

Expand Down
1 change: 0 additions & 1 deletion Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,6 @@ def test_empty_exif_gps(self) -> None:
assert exif.get_ifd(0x8825) == {}

transposed = ImageOps.exif_transpose(im)
assert transposed is not None
exif = transposed.getexif()
assert exif.get_ifd(0x8825) == {}

Expand Down
1 change: 0 additions & 1 deletion Tests/test_file_libtiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -1125,7 +1125,6 @@ def test_exif_transpose(self) -> None:
for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None

assert_image_similar(base_im, transposed_im, 0.7)

Expand Down
12 changes: 3 additions & 9 deletions Tests/test_imageops.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,6 @@ def check(orientation_im: Image.Image) -> None:
else:
original_exif = im.info["exif"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert_image_similar(base_im, transposed_im, 17)
if orientation_im is base_im:
assert "exif" not in im.info
Expand All @@ -417,7 +416,6 @@ def check(orientation_im: Image.Image) -> None:

# Repeat the operation to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im)
assert transposed_im2 is not None
assert_image_equal(transposed_im2, transposed_im)

check(base_im)
Expand All @@ -434,7 +432,6 @@ def check(orientation_im: Image.Image) -> None:
assert im.getexif()[0x0112] == 3

transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()

transposed_im._reload_exif()
Expand All @@ -443,18 +440,16 @@ def check(orientation_im: Image.Image) -> None:
# Orientation from "Raw profile type exif" info key
# This test image has been manually hexedited from exif_imagemagick.png
# to have a different orientation
with Image.open("Tests/images/exif_imagemagick_orientation.png") as img:
assert img.getexif()[0x0112] == 3
with Image.open("Tests/images/exif_imagemagick_orientation.png") as im:
assert im.getexif()[0x0112] == 3

transposed_im = ImageOps.exif_transpose(img)
assert transposed_im is not None
transposed_im = ImageOps.exif_transpose(im)
assert 0x0112 not in transposed_im.getexif()

# Orientation set directly on Image.Exif
im = hopper()
im.getexif()[0x0112] = 3
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()


Expand All @@ -465,7 +460,6 @@ def test_exif_transpose_xml_without_xmp() -> None:

del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()


Expand Down
2 changes: 1 addition & 1 deletion docs/installation/platform-support.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm |
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm |
| +----------------------------+------------------+ |
| | 3.8 | 10.4.0 | |
+----------------------------------+----------------------------+------------------+--------------+
Expand Down
136 changes: 72 additions & 64 deletions src/PIL/BlpImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,29 +260,44 @@ class BlpImageFile(ImageFile.ImageFile):
def _open(self) -> None:
assert self.fp is not None
self.magic = self.fp.read(4)
if not _accept(self.magic):
msg = f"Bad BLP magic {repr(self.magic)}"
raise BLPFormatError(msg)

self.fp.seek(5, os.SEEK_CUR)
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
compression = struct.unpack("<i", self.fp.read(4))[0]
if self.magic == b"BLP1":
alpha = struct.unpack("<I", self.fp.read(4))[0] != 0
else:
encoding = struct.unpack("<b", self.fp.read(1))[0]
alpha = struct.unpack("<b", self.fp.read(1))[0] != 0
alpha_encoding = struct.unpack("<b", self.fp.read(1))[0]
self.fp.seek(1, os.SEEK_CUR) # mips

self.fp.seek(2, os.SEEK_CUR)
self._size = struct.unpack("<II", self.fp.read(8))

if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode()
args: tuple[int, int, bool] | tuple[int, int, bool, int]
if self.magic == b"BLP1":
encoding = struct.unpack("<i", self.fp.read(4))[0]
self.fp.seek(4, os.SEEK_CUR) # subtype

args = (compression, encoding, alpha)
offset = 28
else:
msg = f"Bad BLP magic {repr(self.magic)}"
raise BLPFormatError(msg)
args = (compression, encoding, alpha, alpha_encoding)
offset = 20

self._mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, self.mode)]
decoder = self.magic.decode()

self._mode = "RGBA" if alpha else "RGB"
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]


class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True

def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
try:
self._read_blp_header()
self._read_header()
self._load()
except struct.error as e:
msg = "Truncated BLP file"
Expand All @@ -293,25 +308,9 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int
def _load(self) -> None:
pass

def _read_blp_header(self) -> None:
assert self.fd is not None
self.fd.seek(4)
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))

(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
self.fd.seek(1, os.SEEK_CUR) # mips

self.size = struct.unpack("<II", self._safe_read(8))

if isinstance(self, BLP1Decoder):
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
self.fd.seek(4, os.SEEK_CUR) # subtype

self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _read_header(self) -> None:
self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._lengths = struct.unpack("<16I", self._safe_read(16 * 4))

def _safe_read(self, length: int) -> bytes:
assert self.fd is not None
Expand All @@ -327,37 +326,41 @@ def _read_palette(self) -> list[tuple[int, int, int, int]]:
ret.append((b, g, r, a))
return ret

def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
def _read_bgra(
self, palette: list[tuple[int, int, int, int]], alpha: bool
) -> bytearray:
data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
_data = BytesIO(self._safe_read(self._lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
d: tuple[int, ...] = (r, g, b)
if self._blp_alpha_depth:
if alpha:
d += (a,)
data.extend(d)
return data


class BLP1Decoder(_BLPBaseDecoder):
def _load(self) -> None:
if self._blp_compression == Format.JPEG:
self._compression, self._encoding, alpha = self.args

if self._compression == Format.JPEG:
self._decode_jpeg_stream()

elif self._blp_compression == 1:
if self._blp_encoding in (4, 5):
elif self._compression == 1:
if self._encoding in (4, 5):
palette = self._read_palette()
data = self._read_bgra(palette)
data = self._read_bgra(palette, alpha)
self.set_as_raw(data)
else:
msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}"
msg = f"Unsupported BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg)
else:
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
msg = f"Unsupported BLP compression {repr(self._encoding)}"
raise BLPFormatError(msg)

def _decode_jpeg_stream(self) -> None:
Expand All @@ -366,8 +369,8 @@ def _decode_jpeg_stream(self) -> None:
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size)
assert self.fd is not None
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._blp_lengths[0])
self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._lengths[0])
data = jpeg_header + data
image = JpegImageFile(BytesIO(data))
Image._decompression_bomb_check(image.size)
Expand All @@ -384,47 +387,47 @@ def _decode_jpeg_stream(self) -> None:

class BLP2Decoder(_BLPBaseDecoder):
def _load(self) -> None:
self._compression, self._encoding, alpha, self._alpha_encoding = self.args

palette = self._read_palette()

assert self.fd is not None
self.fd.seek(self._blp_offsets[0])
self.fd.seek(self._offsets[0])

if self._blp_compression == 1:
if self._compression == 1:
# Uncompressed or DirectX compression

if self._blp_encoding == Encoding.UNCOMPRESSED:
data = self._read_bgra(palette)
if self._encoding == Encoding.UNCOMPRESSED:
data = self._read_bgra(palette, alpha)

elif self._blp_encoding == Encoding.DXT:
elif self._encoding == Encoding.DXT:
data = bytearray()
if self._blp_alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.size[0] + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt1(
self._safe_read(linesize), alpha=bool(self._blp_alpha_depth)
):
if self._alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.state.xsize + 3) // 4 * 8
for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt1(self._safe_read(linesize), alpha):
data += d

elif self._blp_alpha_encoding == AlphaEncoding.DXT3:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
elif self._alpha_encoding == AlphaEncoding.DXT3:
linesize = (self.state.xsize + 3) // 4 * 16
for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt3(self._safe_read(linesize)):
data += d

elif self._blp_alpha_encoding == AlphaEncoding.DXT5:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
elif self._alpha_encoding == AlphaEncoding.DXT5:
linesize = (self.state.xsize + 3) // 4 * 16
for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt5(self._safe_read(linesize)):
data += d
else:
msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
raise BLPFormatError(msg)
else:
msg = f"Unknown BLP encoding {repr(self._blp_encoding)}"
msg = f"Unknown BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg)

else:
msg = f"Unknown BLP compression {repr(self._blp_compression)}"
msg = f"Unknown BLP compression {repr(self._compression)}"
raise BLPFormatError(msg)

self.set_as_raw(data)
Expand Down Expand Up @@ -473,10 +476,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:

assert im.palette is not None
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0))
fp.write(struct.pack("<b", 0)) # alpha encoding
fp.write(struct.pack("<b", 0)) # mips

alpha_depth = 1 if im.palette.mode == "RGBA" else 0
if magic == b"BLP1":
fp.write(struct.pack("<L", alpha_depth))
else:
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", alpha_depth))
fp.write(struct.pack("<b", 0)) # alpha encoding
fp.write(struct.pack("<b", 0)) # mips
fp.write(struct.pack("<II", *im.size))
if magic == b"BLP1":
fp.write(struct.pack("<i", 5))
Expand Down
Loading

0 comments on commit 3c3a2e6

Please sign in to comment.