Merge branch 'master' into tiff_exif
|
@ -22,6 +22,7 @@ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
|||
cmake imagemagick libharfbuzz-dev libfribidi-dev
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade wheel
|
||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
||||
python3 -m pip install coverage
|
||||
python3 -m pip install defusedxml
|
||||
|
@ -31,12 +32,10 @@ python3 -m pip install -U pytest-cov
|
|||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
python3 -m pip install test-image-results
|
||||
# TODO Remove condition when numpy supports 3.10
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
|
||||
python3 -m pip install numpy
|
||||
|
||||
# PyQt5 doesn't support PyPy3
|
||||
# Wheel doesn't yet support 3.10
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* && $GHA_PYTHON_VERSION != "3.10-dev" ]]; then
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
# arm64, ppc64le, s390x CPUs:
|
||||
# "ERROR: Could not find a version that satisfies the requirement pyqt5"
|
||||
sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools
|
||||
|
|
3
.github/workflows/macos-install.sh
vendored
|
@ -15,8 +15,7 @@ python3 -m pip install pyroma
|
|||
python3 -m pip install test-image-results
|
||||
|
||||
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
|
||||
# TODO Remove condition when numpy supports 3.10
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
|
||||
python3 -m pip install numpy
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
2
.github/workflows/test.yml
vendored
|
@ -72,8 +72,6 @@ jobs:
|
|||
if: startsWith(matrix.os, 'macOS')
|
||||
run: |
|
||||
.github/workflows/macos-install.sh
|
||||
env:
|
||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
|
1
.gitignore
vendored
|
@ -83,6 +83,7 @@ docs/_build/
|
|||
Tests/images/README.md
|
||||
Tests/images/crash_1.tif
|
||||
Tests/images/crash_2.tif
|
||||
Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif
|
||||
Tests/images/string_dimension.tiff
|
||||
Tests/images/jpeg2000
|
||||
Tests/images/msp
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: e66be67b9b6811913470f70c28b4d50f94d05b22 # frozen: 20.8b1
|
||||
rev: e3000ace2fd1fcb1c181bb7a8285f1f976bcbdc7 # frozen: 21.7b0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ["--target-version", "py36"]
|
||||
|
@ -9,35 +9,38 @@ repos:
|
|||
types: []
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 377d260ffa6f746693f97b46d95025afc4bd8275 # frozen: 5.4.2
|
||||
rev: fd5ba70665a37ec301a1f714ed09336048b3be63 # frozen: 5.9.3
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/asottile/yesqa
|
||||
rev: 7a009f3ee493c796827ee334f9058b110a0e0db8 # frozen: v1.2.1
|
||||
rev: 644ede78511c02fc6f8e03e014cc1ddcfbf1e1f5 # frozen: v1.2.3
|
||||
hooks:
|
||||
- id: yesqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: f30f4974a08a6b2f6a1eeaf30a4d501cf909163a # frozen: v1.1.9
|
||||
rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
||||
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 05f6544aef321e2fee03a1277ce2eef8880fb927 # frozen: 3.8.3
|
||||
rev: dcd740bc0ebaf2b3d43e59a0060d157c97de13f3 # frozen: 3.9.2
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: eae6397e4c259ed3d057511f6dd5330b92867e62 # frozen: v1.6.0
|
||||
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0
|
||||
hooks:
|
||||
- id: python-check-blanket-noqa
|
||||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: e1668fe86af3810fbca72b8653fe478e66a0afdc # frozen: v3.2.0
|
||||
rev: 38b88246ccc552bffaaf54259d064beeee434539 # frozen: v4.0.1
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: quarterly
|
||||
|
|
108
CHANGES.rst
|
@ -2,6 +2,114 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
8.4.0 (unreleased)
|
||||
------------------
|
||||
|
||||
- Copy Python palette to new image in quantize() #5696
|
||||
[radarhere]
|
||||
|
||||
- Read ICO AND mask from end #5667
|
||||
[radarhere]
|
||||
|
||||
- Actually check the framesize in FliDecode.c #5659
|
||||
[wiredfool]
|
||||
|
||||
- Determine JPEG2000 mode purely from ihdr header box #5654
|
||||
[radarhere]
|
||||
|
||||
- Fixed using info dictionary when writing multiple APNG frames #5611
|
||||
[radarhere]
|
||||
|
||||
- Allow saving 1 and L mode TIFF with PhotometricInterpretation 0 #5655
|
||||
[radarhere]
|
||||
|
||||
- For GIF save_all with palette, do not include palette with each frame #5603
|
||||
[radarhere]
|
||||
|
||||
- Keep transparency when converting from P to LA or PA #5606
|
||||
[radarhere]
|
||||
|
||||
- Copy palette to new image in transform() #5647
|
||||
[radarhere]
|
||||
|
||||
- Added "transparency" argument to EpsImagePlugin load() #5620
|
||||
[radarhere]
|
||||
|
||||
- Corrected pathlib.Path detection when saving #5633
|
||||
[radarhere]
|
||||
|
||||
- Added WalImageFile class #5618
|
||||
[radarhere]
|
||||
|
||||
- Consider I;16 pixel size when drawing text #5598
|
||||
[radarhere]
|
||||
|
||||
- If default conversion from P is RGB with transparency, convert to RGBA #5594
|
||||
[radarhere]
|
||||
|
||||
- Speed up rotating square images by 90 or 270 degrees #5646
|
||||
[radarhere]
|
||||
|
||||
- Add support for reading DPI information from JPEG2000 images
|
||||
[rogermb, radarhere]
|
||||
|
||||
- Catch TypeError from corrupted DPI value in EXIF #5639
|
||||
[homm, radarhere]
|
||||
|
||||
- Do not close file pointer when saving SGI images #5645
|
||||
[farizrahman4u, radarhere]
|
||||
|
||||
- Deprecate ImagePalette size parameter #5641
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Prefer command line tools SDK on macOS #5624
|
||||
[radarhere]
|
||||
|
||||
- Added tags when saving YCbCr TIFF #5597
|
||||
[radarhere]
|
||||
|
||||
- PSD layer count may be negative #5613
|
||||
[radarhere]
|
||||
|
||||
- Fixed ImageOps expand with tuple border on P image #5615
|
||||
[radarhere]
|
||||
|
||||
- Fixed error saving APNG with duplicate frames and different duration times #5609
|
||||
[thak1411, radarhere]
|
||||
|
||||
8.3.2 (2021-09-02)
|
||||
------------------
|
||||
|
||||
- CVE-2021-23437 Raise ValueError if color specifier is too long
|
||||
[hugovk, radarhere]
|
||||
|
||||
- Fix 6-byte OOB read in FliDecode
|
||||
[wiredfool]
|
||||
|
||||
- Add support for Python 3.10 #5569, #5570
|
||||
[hugovk, radarhere]
|
||||
|
||||
- Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression #5588
|
||||
[kmilos, radarhere]
|
||||
|
||||
- Updates for ``ImagePalette`` channel order #5599
|
||||
[radarhere]
|
||||
|
||||
- Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library #5651
|
||||
[nulano]
|
||||
|
||||
8.3.1 (2021-07-06)
|
||||
------------------
|
||||
|
||||
- Catch OSError when checking if fp is sys.stdout #5585
|
||||
[radarhere]
|
||||
|
||||
- Handle removing orientation from alternate types of EXIF data #5584
|
||||
[radarhere]
|
||||
|
||||
- Make Image.__array__ take optional dtype argument #5572
|
||||
[t-vi, radarhere]
|
||||
|
||||
8.3.0 (2021-07-01)
|
||||
------------------
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ def _write_png(tmp_path, xdim, ydim):
|
|||
|
||||
|
||||
def test_large(tmp_path):
|
||||
""" succeeded prepatch"""
|
||||
"""succeeded prepatch"""
|
||||
_write_png(tmp_path, XDIM, YDIM)
|
||||
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ def _write_png(tmp_path, xdim, ydim):
|
|||
|
||||
|
||||
def test_large(tmp_path):
|
||||
""" succeeded prepatch"""
|
||||
"""succeeded prepatch"""
|
||||
_write_png(tmp_path, XDIM, YDIM)
|
||||
|
||||
|
||||
|
|
BIN
Tests/images/balloon_eciRGBv2_aware.jp2
Normal file
BIN
Tests/images/broken_exif_dpi.jpg
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
Tests/images/crash-5762152299364352.fli
Normal file
BIN
Tests/images/exif_imagemagick_orientation.png
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
Tests/images/expected_to_read.jp2
Normal file
BIN
Tests/images/hopper_mask.ico
Normal file
After Width: | Height: | Size: 262 B |
BIN
Tests/images/hopper_mask.png
Normal file
After Width: | Height: | Size: 208 B |
BIN
Tests/images/hopper_wal.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
Tests/images/invalid_header_length.jp2
Normal file
BIN
Tests/images/negative_layer_count.psd
Normal file
BIN
Tests/images/not_enough_data.jp2
Normal file
BIN
Tests/images/palette_negative.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
Tests/images/palette_sepia.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Tests/images/palette_wedge.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
Tests/images/reqd_showpage_transparency.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
Tests/images/zero_dpi.jp2
Normal file
|
@ -433,12 +433,20 @@ def test_apng_save_duration_loop(tmp_path):
|
|||
|
||||
# test removal of duplicated frames
|
||||
frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255))
|
||||
frame.save(test_file, save_all=True, append_images=[frame], duration=[500, 250])
|
||||
frame.save(
|
||||
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.load()
|
||||
assert im.n_frames == 1
|
||||
assert im.info.get("duration") == 750
|
||||
|
||||
# test info duration
|
||||
frame.info["duration"] = 750
|
||||
frame.save(test_file, save_all=True)
|
||||
with Image.open(test_file) as im:
|
||||
assert im.info.get("duration") == 750
|
||||
|
||||
|
||||
def test_apng_save_disposal(tmp_path):
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
@ -529,6 +537,17 @@ def test_apng_save_disposal(tmp_path):
|
|||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
# test info disposal
|
||||
red.info["disposal"] = PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND
|
||||
red.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))],
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(1)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
||||
|
||||
def test_apng_save_disposal_previous(tmp_path):
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
@ -609,3 +628,10 @@ def test_apng_save_blend(tmp_path):
|
|||
im.seek(2)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
# test info blend
|
||||
red.info["blend"] = PngImagePlugin.APNG_BLEND_OP_OVER
|
||||
red.save(test_file, save_all=True, append_images=[green, transparent])
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(2)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
|
|
|
@ -197,7 +197,7 @@ def test__accept_false():
|
|||
|
||||
|
||||
def test_short_header():
|
||||
""" Check a short header"""
|
||||
"""Check a short header"""
|
||||
with open(TEST_FILE_DXT5, "rb") as f:
|
||||
img_file = f.read()
|
||||
|
||||
|
@ -210,7 +210,7 @@ def test_short_header():
|
|||
|
||||
|
||||
def test_short_file():
|
||||
""" Check that the appropriate error is thrown for a short file"""
|
||||
"""Check that the appropriate error is thrown for a short file"""
|
||||
|
||||
with open(TEST_FILE_DXT5, "rb") as f:
|
||||
img_file = f.read()
|
||||
|
@ -224,7 +224,7 @@ def test_short_file():
|
|||
|
||||
|
||||
def test_dxt5_colorblock_alpha_issue_4142():
|
||||
""" Check that colorblocks are decoded correctly in DXT5"""
|
||||
"""Check that colorblocks are decoded correctly in DXT5"""
|
||||
|
||||
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
|
||||
px = im.getpixel((0, 0))
|
||||
|
|
|
@ -96,6 +96,17 @@ def test_showpage():
|
|||
assert_image_similar(plot_image, target, 6)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
def test_transparency():
|
||||
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
|
||||
plot_image.load(transparency=True)
|
||||
assert plot_image.mode == "RGBA"
|
||||
|
||||
with Image.open("Tests/images/reqd_showpage_transparency.png") as target:
|
||||
# fonts could be slightly different
|
||||
assert_image_similar(plot_image, target, 6)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
def test_file_object(tmp_path):
|
||||
# issue 479
|
||||
|
|
|
@ -138,3 +138,16 @@ def test_timeouts(test_file):
|
|||
with Image.open(f) as im:
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
[
|
||||
"Tests/images/crash-5762152299364352.fli",
|
||||
],
|
||||
)
|
||||
def test_crash(test_file):
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
|
|
@ -821,6 +821,29 @@ def test_palette_save_P(tmp_path):
|
|||
assert_image_equal(reloaded, im)
|
||||
|
||||
|
||||
def test_palette_save_all_P(tmp_path):
|
||||
frames = []
|
||||
colors = ((255, 0, 0), (0, 255, 0))
|
||||
for color in colors:
|
||||
frame = Image.new("P", (100, 100))
|
||||
frame.putpalette(color)
|
||||
frames.append(frame)
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
frames[0].save(
|
||||
out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]
|
||||
)
|
||||
|
||||
with Image.open(out) as im:
|
||||
# Assert that the frames are correct, and each frame has the same palette
|
||||
assert_image_equal(im.convert("RGB"), frames[0].convert("RGB"))
|
||||
assert im.palette.palette == im.global_palette.palette
|
||||
|
||||
im.seek(1)
|
||||
assert_image_equal(im.convert("RGB"), frames[1].convert("RGB"))
|
||||
assert im.palette.palette == im.global_palette.palette
|
||||
|
||||
|
||||
def test_palette_save_ImagePalette(tmp_path):
|
||||
# Pass in a different palette, as an ImagePalette.ImagePalette
|
||||
# effectively the same as test_palette_save_P
|
||||
|
@ -833,7 +856,7 @@ def test_palette_save_ImagePalette(tmp_path):
|
|||
|
||||
with Image.open(out) as reloaded:
|
||||
im.putpalette(palette)
|
||||
assert_image_equal(reloaded, im)
|
||||
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
|
||||
|
||||
|
||||
def test_save_I(tmp_path):
|
||||
|
|
|
@ -18,6 +18,11 @@ def test_sanity():
|
|||
assert im.get_format_mimetype() == "image/x-icon"
|
||||
|
||||
|
||||
def test_mask():
|
||||
with Image.open("Tests/images/hopper_mask.ico") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper_mask.png")
|
||||
|
||||
|
||||
def test_black_and_white():
|
||||
with Image.open("Tests/images/black_and_white.ico") as im:
|
||||
assert im.mode == "RGBA"
|
||||
|
|
|
@ -630,7 +630,7 @@ class TestFileJpeg:
|
|||
reloaded.save(f, quality="keep", optimize=True)
|
||||
|
||||
def test_bad_mpo_header(self):
|
||||
""" Treat unknown MPO as JPEG """
|
||||
"""Treat unknown MPO as JPEG"""
|
||||
# Arrange
|
||||
|
||||
# Act
|
||||
|
@ -718,6 +718,15 @@ class TestFileJpeg:
|
|||
# This should return the default, and not raise a ZeroDivisionError
|
||||
assert im.info.get("dpi") == (72, 72)
|
||||
|
||||
def test_dpi_exif_string(self):
|
||||
# Arrange
|
||||
# 0x011A tag in this exif contains string '300300\x02'
|
||||
with Image.open("Tests/images/broken_exif_dpi.jpg") as im:
|
||||
|
||||
# Act / Assert
|
||||
# This should return the default
|
||||
assert im.info.get("dpi") == (72, 72)
|
||||
|
||||
def test_no_dpi_in_exif(self):
|
||||
# Arrange
|
||||
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
|
||||
|
|
|
@ -4,7 +4,7 @@ from io import BytesIO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFile, Jpeg2KImagePlugin, features
|
||||
from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -151,6 +151,38 @@ def test_reduce():
|
|||
assert im.size == (40, 30)
|
||||
|
||||
|
||||
def test_load_dpi():
|
||||
with Image.open("Tests/images/test-card-lossless.jp2") as im:
|
||||
assert im.info["dpi"] == (71.9836, 71.9836)
|
||||
|
||||
with Image.open("Tests/images/zero_dpi.jp2") as im:
|
||||
assert "dpi" not in im.info
|
||||
|
||||
|
||||
def test_restricted_icc_profile():
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
# JPEG2000 image with a restricted ICC profile and a known colorspace
|
||||
with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im:
|
||||
assert im.mode == "RGB"
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
|
||||
def test_header_errors():
|
||||
for path in (
|
||||
"Tests/images/invalid_header_length.jp2",
|
||||
"Tests/images/not_enough_data.jp2",
|
||||
):
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open(path):
|
||||
pass
|
||||
|
||||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/expected_to_read.jp2"):
|
||||
pass
|
||||
|
||||
|
||||
def test_layers_type(tmp_path):
|
||||
outfile = str(tmp_path / "temp_layers.jp2")
|
||||
for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
|
||||
|
|
|
@ -97,13 +97,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
self._assert_noerr(tmp_path, im)
|
||||
|
||||
def test_g4_eq_png(self):
|
||||
""" Checking that we're actually getting the data that we expect"""
|
||||
"""Checking that we're actually getting the data that we expect"""
|
||||
with Image.open("Tests/images/hopper_bw_500.png") as png:
|
||||
assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif")
|
||||
|
||||
# see https://github.com/python-pillow/Pillow/issues/279
|
||||
def test_g4_fillorder_eq_png(self):
|
||||
""" Checking that we're actually getting the data that we expect"""
|
||||
"""Checking that we're actually getting the data that we expect"""
|
||||
with Image.open("Tests/images/g4-fillorder-test.tif") as g4:
|
||||
assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png")
|
||||
|
||||
|
@ -137,7 +137,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
||||
|
||||
def test_write_metadata(self, tmp_path):
|
||||
""" Test metadata writing through libtiff """
|
||||
"""Test metadata writing through libtiff"""
|
||||
for legacy_api in [False, True]:
|
||||
f = str(tmp_path / "temp.tiff")
|
||||
with Image.open("Tests/images/hopper_g4.tif") as img:
|
||||
|
@ -670,6 +670,15 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test_save_ycbcr(self, tmp_path):
|
||||
im = hopper("YCbCr")
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile, compression="jpeg")
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert reloaded.tag_v2[530] == (1, 1)
|
||||
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
||||
|
||||
def test_crashing_metadata(self, tmp_path):
|
||||
# issue 1597
|
||||
with Image.open("Tests/images/rdf.tif") as im:
|
||||
|
@ -968,10 +977,11 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert str(e.value) == "-9"
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test_save_multistrip(self, tmp_path):
|
||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
||||
def test_save_multistrip(self, compression, tmp_path):
|
||||
im = hopper("RGB").resize((256, 256))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
im.save(out, compression="tiff_adobe_deflate")
|
||||
im.save(out, compression=compression)
|
||||
|
||||
with Image.open(out) as im:
|
||||
# Assert that there are multiple strips
|
||||
|
|
|
@ -57,9 +57,10 @@ def test_n_frames():
|
|||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open(test_file) as im:
|
||||
assert im.n_frames == 2
|
||||
assert im.is_animated
|
||||
for path in [test_file, "Tests/images/negative_layer_count.psd"]:
|
||||
with Image.open(path) as im:
|
||||
assert im.n_frames == 2
|
||||
assert im.is_animated
|
||||
|
||||
|
||||
def test_eoferror():
|
||||
|
|
|
@ -73,6 +73,13 @@ def test_write(tmp_path):
|
|||
img.save(out, format="sgi")
|
||||
assert_image_equal_tofile(img, out)
|
||||
|
||||
out = str(tmp_path / "fp.sgi")
|
||||
with open(out, "wb") as fp:
|
||||
img.save(fp)
|
||||
assert_image_equal_tofile(img, out)
|
||||
|
||||
assert not fp.closed
|
||||
|
||||
for mode in ("L", "RGB", "RGBA"):
|
||||
roundtrip(hopper(mode))
|
||||
|
||||
|
|
|
@ -463,6 +463,15 @@ class TestFileTiff:
|
|||
im.seek(1)
|
||||
assert im.getexif()[273] == (1408, 1907)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L"))
|
||||
def test_photometric(self, mode, tmp_path):
|
||||
filename = str(tmp_path / "temp.tif")
|
||||
im = hopper(mode)
|
||||
im.save(filename, tiffinfo={262: 0})
|
||||
with Image.open(filename) as reloaded:
|
||||
assert reloaded.tag_v2[262] == 0
|
||||
assert_image_equal(im, reloaded)
|
||||
|
||||
def test_seek(self):
|
||||
filename = "Tests/images/pil136.tiff"
|
||||
with Image.open(filename) as im:
|
||||
|
@ -705,6 +714,8 @@ class TestFileTiff:
|
|||
# Ignore this UserWarning which triggers for four tags:
|
||||
# "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..."
|
||||
@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data")
|
||||
# Ignore this UserWarning:
|
||||
@pytest.mark.filterwarnings("ignore:Truncated File Read")
|
||||
@pytest.mark.skipif(
|
||||
not os.path.exists("Tests/images/string_dimension.tiff"),
|
||||
reason="Extra image files not installed",
|
||||
|
|
|
@ -122,7 +122,7 @@ def test_read_metadata():
|
|||
|
||||
|
||||
def test_write_metadata(tmp_path):
|
||||
""" Test metadata writing through the python code """
|
||||
"""Test metadata writing through the python code"""
|
||||
with Image.open("Tests/images/hopper.tif") as img:
|
||||
f = str(tmp_path / "temp.tiff")
|
||||
img.save(f, tiffinfo=img.tag)
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
from PIL import WalImageFile
|
||||
|
||||
from .helper import assert_image_equal_tofile
|
||||
|
||||
|
||||
def test_open():
|
||||
# Arrange
|
||||
TEST_FILE = "Tests/images/hopper.wal"
|
||||
|
||||
# Act
|
||||
im = WalImageFile.open(TEST_FILE)
|
||||
with WalImageFile.open(TEST_FILE) as im:
|
||||
|
||||
# Assert
|
||||
assert im.format == "WAL"
|
||||
assert im.format_description == "Quake2 Texture"
|
||||
assert im.mode == "P"
|
||||
assert im.size == (128, 128)
|
||||
# Assert
|
||||
assert im.format == "WAL"
|
||||
assert im.format_description == "Quake2 Texture"
|
||||
assert im.mode == "P"
|
||||
assert im.size == (128, 128)
|
||||
|
||||
assert isinstance(im, WalImageFile.WalImageFile)
|
||||
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
|
||||
|
|
|
@ -104,6 +104,13 @@ class TestFileWebp:
|
|||
hopper().save(buffer_method, format="WEBP", method=6)
|
||||
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
|
||||
|
||||
def test_icc_profile(self, tmp_path):
|
||||
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
|
||||
if _webp.HAVE_WEBPANIM:
|
||||
self._roundtrip(
|
||||
tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True}
|
||||
)
|
||||
|
||||
def test_write_unsupported_mode_L(self, tmp_path):
|
||||
"""
|
||||
Saving a black-and-white file to WebP format should work, and be
|
||||
|
|
|
@ -149,10 +149,11 @@ class TestImage:
|
|||
assert im.mode == "RGB"
|
||||
assert im.size == (128, 128)
|
||||
|
||||
temp_file = str(tmp_path / "temp.jpg")
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
im.save(Path(temp_file))
|
||||
for ext in (".jpg", ".jp2"):
|
||||
temp_file = str(tmp_path / ("temp." + ext))
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
im.save(Path(temp_file))
|
||||
|
||||
def test_fp_name(self, tmp_path):
|
||||
temp_file = str(tmp_path / "temp.jpg")
|
||||
|
|
|
@ -14,6 +14,10 @@ def test_toarray():
|
|||
ai = numpy.array(im.convert(mode))
|
||||
return ai.shape, ai.dtype.str, ai.nbytes
|
||||
|
||||
def test_with_dtype(dtype):
|
||||
ai = numpy.array(im, dtype=dtype)
|
||||
assert ai.dtype == dtype
|
||||
|
||||
# assert test("1") == ((100, 128), '|b1', 1600))
|
||||
assert test("L") == ((100, 128), "|u1", 12800)
|
||||
|
||||
|
@ -27,6 +31,9 @@ def test_toarray():
|
|||
assert test("RGBA") == ((100, 128, 4), "|u1", 51200)
|
||||
assert test("RGBX") == ((100, 128, 4), "|u1", 51200)
|
||||
|
||||
test_with_dtype(numpy.float64)
|
||||
test_with_dtype(numpy.uint8)
|
||||
|
||||
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
|
||||
with pytest.raises(OSError):
|
||||
numpy.array(im_truncated)
|
||||
|
@ -34,7 +41,7 @@ def test_toarray():
|
|||
|
||||
def test_fromarray():
|
||||
class Wrapper:
|
||||
""" Class with API matching Image.fromarray """
|
||||
"""Class with API matching Image.fromarray"""
|
||||
|
||||
def __init__(self, img, arr_params):
|
||||
self.img = img
|
||||
|
|
|
@ -42,10 +42,14 @@ def test_default():
|
|||
|
||||
im = hopper("P")
|
||||
assert_image(im, "P", im.size)
|
||||
im = im.convert()
|
||||
assert_image(im, "RGB", im.size)
|
||||
im = im.convert()
|
||||
assert_image(im, "RGB", im.size)
|
||||
converted_im = im.convert()
|
||||
assert_image(converted_im, "RGB", im.size)
|
||||
converted_im = im.convert()
|
||||
assert_image(converted_im, "RGB", im.size)
|
||||
|
||||
im.info["transparency"] = 0
|
||||
converted_im = im.convert()
|
||||
assert_image(converted_im, "RGBA", im.size)
|
||||
|
||||
|
||||
# ref https://github.com/python-pillow/Pillow/issues/274
|
||||
|
@ -100,18 +104,22 @@ def test_trns_p(tmp_path):
|
|||
# ref https://github.com/python-pillow/Pillow/issues/664
|
||||
|
||||
|
||||
def test_trns_p_rgba():
|
||||
@pytest.mark.parametrize("mode", ("LA", "PA", "RGBA"))
|
||||
def test_trns_p_transparency(mode):
|
||||
# Arrange
|
||||
im = hopper("P")
|
||||
im.info["transparency"] = 128
|
||||
|
||||
# Act
|
||||
im_rgba = im.convert("RGBA")
|
||||
converted_im = im.convert(mode)
|
||||
|
||||
# Assert
|
||||
assert "transparency" not in im_rgba.info
|
||||
# https://github.com/python-pillow/Pillow/issues/2702
|
||||
assert im_rgba.palette is None
|
||||
assert "transparency" not in converted_im.info
|
||||
if mode == "PA":
|
||||
assert converted_im.palette is not None
|
||||
else:
|
||||
# https://github.com/python-pillow/Pillow/issues/2702
|
||||
assert converted_im.palette is None
|
||||
|
||||
|
||||
def test_trns_l(tmp_path):
|
||||
|
|
|
@ -32,7 +32,7 @@ def test_16bit_lut():
|
|||
|
||||
|
||||
def test_f_lut():
|
||||
""" Tests for floating point lut of 8bit gray image """
|
||||
"""Tests for floating point lut of 8bit gray image"""
|
||||
im = hopper("L")
|
||||
lut = [0.5 * float(x) for x in range(256)]
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import pytest
|
|||
|
||||
from PIL import Image, ImagePalette
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
|
||||
def test_putpalette():
|
||||
|
@ -36,9 +36,15 @@ def test_putpalette():
|
|||
def test_imagepalette():
|
||||
im = hopper("P")
|
||||
im.putpalette(ImagePalette.negative())
|
||||
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png")
|
||||
|
||||
im.putpalette(ImagePalette.random())
|
||||
|
||||
im.putpalette(ImagePalette.sepia())
|
||||
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_sepia.png")
|
||||
|
||||
im.putpalette(ImagePalette.wedge())
|
||||
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png")
|
||||
|
||||
|
||||
def test_putpalette_with_alpha_values():
|
||||
|
|
|
@ -63,6 +63,7 @@ def test_quantize_no_dither():
|
|||
|
||||
converted = image.quantize(dither=0, palette=palette)
|
||||
assert_image(converted, "P", converted.size)
|
||||
assert converted.palette.palette == palette.palette.palette
|
||||
|
||||
|
||||
def test_quantize_dither_diff():
|
||||
|
|
|
@ -33,6 +33,9 @@ def test_angle():
|
|||
with Image.open("Tests/images/test-card.png") as im:
|
||||
rotate(im, im.mode, angle)
|
||||
|
||||
im = hopper()
|
||||
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
|
||||
|
||||
|
||||
def test_zero():
|
||||
for angle in (0, 45, 90, 180, 270):
|
||||
|
|
|
@ -32,6 +32,11 @@ class TestImageTransform:
|
|||
new_im = im.transform((100, 100), transform)
|
||||
assert new_im.info["comment"] == comment
|
||||
|
||||
def test_palette(self):
|
||||
with Image.open("Tests/images/hopper.gif") as im:
|
||||
transformed = im.transform(im.size, Image.AFFINE, [1, 0, 0, 0, 1, 0])
|
||||
assert im.palette.palette == transformed.palette.palette
|
||||
|
||||
def test_extent(self):
|
||||
im = hopper("RGB")
|
||||
(w, h) = im.size
|
||||
|
|
|
@ -191,3 +191,12 @@ def test_rounding_errors():
|
|||
assert (255, 255) == ImageColor.getcolor("white", "LA")
|
||||
assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")
|
||||
Image.new("LA", (1, 1), "white")
|
||||
|
||||
|
||||
def test_color_too_long():
|
||||
# Arrange
|
||||
color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)"
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(ValueError):
|
||||
ImageColor.getrgb(color_too_long)
|
||||
|
|
|
@ -134,6 +134,17 @@ class TestImageFont:
|
|||
target = "Tests/images/transparent_background_text_L.png"
|
||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||
|
||||
def test_I16(self):
|
||||
im = Image.new(mode="I;16", size=(300, 100))
|
||||
draw = ImageDraw.Draw(im)
|
||||
ttf = self.get_font()
|
||||
|
||||
txt = "Hello World!"
|
||||
draw.text((10, 10), txt, font=ttf)
|
||||
|
||||
target = "Tests/images/transparent_background_text_L.png"
|
||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||
|
||||
def test_textsize_equal(self):
|
||||
im = Image.new(mode="RGB", size=(300, 100))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
|
|
@ -156,23 +156,31 @@ def test_scale():
|
|||
assert newimg.size == (25, 25)
|
||||
|
||||
|
||||
def test_expand_palette():
|
||||
im = Image.open("Tests/images/p_16.tga")
|
||||
im_expanded = ImageOps.expand(im, 10, (255, 0, 0))
|
||||
@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
|
||||
def test_expand_palette(border):
|
||||
with Image.open("Tests/images/p_16.tga") as im:
|
||||
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
|
||||
|
||||
px = im_expanded.convert("RGB").load()
|
||||
for b in range(10):
|
||||
if isinstance(border, int):
|
||||
left = top = right = bottom = border
|
||||
else:
|
||||
left, top, right, bottom = border
|
||||
px = im_expanded.convert("RGB").load()
|
||||
for x in range(im_expanded.width):
|
||||
assert px[x, b] == (255, 0, 0)
|
||||
assert px[x, im_expanded.height - 1 - b] == (255, 0, 0)
|
||||
for b in range(top):
|
||||
assert px[x, b] == (255, 0, 0)
|
||||
for b in range(bottom):
|
||||
assert px[x, im_expanded.height - 1 - b] == (255, 0, 0)
|
||||
for y in range(im_expanded.height):
|
||||
assert px[b, x] == (255, 0, 0)
|
||||
assert px[b, im_expanded.width - 1 - b] == (255, 0, 0)
|
||||
for b in range(left):
|
||||
assert px[b, y] == (255, 0, 0)
|
||||
for b in range(right):
|
||||
assert px[im_expanded.width - 1 - b, y] == (255, 0, 0)
|
||||
|
||||
im_cropped = im_expanded.crop(
|
||||
(10, 10, im_expanded.width - 10, im_expanded.height - 10)
|
||||
)
|
||||
assert_image_equal(im_cropped, im)
|
||||
im_cropped = im_expanded.crop(
|
||||
(left, top, im_expanded.width - right, im_expanded.height - bottom)
|
||||
)
|
||||
assert_image_equal(im_cropped, im)
|
||||
|
||||
|
||||
def test_colorize_2color():
|
||||
|
@ -335,6 +343,28 @@ def test_exif_transpose():
|
|||
) as orientation_im:
|
||||
check(orientation_im)
|
||||
|
||||
# Orientation from "XML:com.adobe.xmp" info key
|
||||
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
|
||||
assert im.getexif()[0x0112] == 3
|
||||
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
# 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 im:
|
||||
assert im.getexif()[0x0112] == 3
|
||||
|
||||
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 0x0112 not in transposed_im.getexif()
|
||||
|
||||
|
||||
def test_autocontrast_cutoff():
|
||||
# Test the cutoff argument of autocontrast
|
||||
|
|
|
@ -10,15 +10,16 @@ def test_sanity():
|
|||
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
|
||||
assert len(palette.colors) == 256
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with pytest.raises(ValueError):
|
||||
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
|
||||
|
||||
|
||||
def test_reload():
|
||||
im = Image.open("Tests/images/hopper.gif")
|
||||
original = im.copy()
|
||||
im.palette.dirty = 1
|
||||
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
|
||||
with Image.open("Tests/images/hopper.gif") as im:
|
||||
original = im.copy()
|
||||
im.palette.dirty = 1
|
||||
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
|
||||
|
||||
|
||||
def test_getcolor():
|
||||
|
|
|
@ -24,11 +24,17 @@ def test_overflow():
|
|||
|
||||
|
||||
def test_tobytes():
|
||||
# Note that this image triggers the decompression bomb warning:
|
||||
max_pixels = Image.MAX_IMAGE_PIXELS
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
|
||||
# Previously raised an access violation on Windows
|
||||
with Image.open("Tests/images/l2rgb_read.bmp") as im:
|
||||
with pytest.raises((ValueError, MemoryError, OSError)):
|
||||
im.tobytes()
|
||||
|
||||
Image.MAX_IMAGE_PIXELS = max_pixels
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system")
|
||||
def test_ysize():
|
||||
|
|
|
@ -40,6 +40,7 @@ from .helper import on_ci
|
|||
)
|
||||
@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data")
|
||||
@pytest.mark.filterwarnings("ignore:Metadata warning")
|
||||
@pytest.mark.filterwarnings("ignore:Truncated File Read")
|
||||
def test_tiff_crashes(test_file):
|
||||
try:
|
||||
with Image.open(test_file) as im:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install webp
|
||||
|
||||
archive=libwebp-1.2.0
|
||||
archive=libwebp-1.2.1
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -92,6 +92,17 @@ dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no l
|
|||
performs any operations on the data given to it, has been deprecated and will be
|
||||
removed in Pillow 10.0.0 (2023-01-02).
|
||||
|
||||
ImagePalette size parameter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 8.4.0
|
||||
|
||||
The ``size`` parameter will be removed in Pillow 10.0.0 (2023-01-02).
|
||||
|
||||
Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by
|
||||
default, and the size parameter could be used to override that. Pillow 8.3.0 removed
|
||||
the default required length, also removing the need for the size parameter.
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ than leaving them in the original color space. The EPS driver can write images
|
|||
in ``L``, ``RGB`` and ``CMYK`` modes.
|
||||
|
||||
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
|
||||
method with the following parameter to affect how Ghostscript renders the EPS
|
||||
method with the following parameters to affect how Ghostscript renders the EPS
|
||||
|
||||
**scale**
|
||||
Affects the scale of the resultant rasterized image. If the EPS suggests
|
||||
|
@ -79,6 +79,11 @@ method with the following parameter to affect how Ghostscript renders the EPS
|
|||
im.load(scale=2)
|
||||
im.size #(200,200)
|
||||
|
||||
**transparency**
|
||||
If true, generates an RGBA image with a transparent background, instead of
|
||||
the default behaviour of an RGB image with a white background.
|
||||
|
||||
|
||||
GIF
|
||||
^^^
|
||||
|
||||
|
@ -839,7 +844,7 @@ Reading Multi-frame TIFF Images
|
|||
The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and
|
||||
:py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers
|
||||
within the image file. You can combine these methods to seek to the next frame
|
||||
(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.num_frames - 1``,
|
||||
(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.n_frames - 1``,
|
||||
and can be accessed in any order.
|
||||
|
||||
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the
|
||||
|
|
|
@ -18,9 +18,9 @@ Pillow supports these Python versions.
|
|||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 |
|
||||
+======================+=====+=====+=====+=====+=====+=====+=====+=====+
|
||||
| Pillow >= 8.3 | Yes | Yes | Yes | Yes | Yes | | | |
|
||||
| Pillow >= 8.3.2 | Yes | Yes | Yes | Yes | Yes | | | |
|
||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
| Pillow 8.0 - 8.2 | | Yes | Yes | Yes | Yes | | | |
|
||||
| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | |
|
||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | |
|
||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
|
@ -111,7 +111,7 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems:
|
|||
|
||||
**Packages**::
|
||||
|
||||
pkg install py36-pillow
|
||||
pkg install py38-pillow
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -476,7 +476,7 @@ These platforms are built and tested for every change.
|
|||
| +---------------------------+---------------------+
|
||||
| | PyPy3 | x86 |
|
||||
| +---------------------------+---------------------+
|
||||
| | 3.8/MinGW | x86, x86-64 |
|
||||
| | 3.9/MinGW | x86, x86-64 |
|
||||
+----------------------------------+---------------------------+---------------------+
|
||||
|
||||
|
||||
|
@ -494,11 +494,11 @@ These platforms have been reported to work at the versions mentioned.
|
|||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+===========================+==================+==============+
|
||||
| macOS 11.0 Big Sur | 3.7, 3.8, 3.9 | 8.2.0 |arm |
|
||||
| macOS 11.0 Big Sur | 3.7, 3.8, 3.9 | 8.3.1 |arm |
|
||||
| +---------------------------+------------------+--------------+
|
||||
| | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |x86-64 |
|
||||
| | 3.6, 3.7, 3.8, 3.9 | 8.3.1 |x86-64 |
|
||||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.0.1 |x86-64 |
|
||||
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.1 |x86-64 |
|
||||
| +---------------------------+------------------+ |
|
||||
| | 3.5 | 7.2.0 | |
|
||||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
|
|
|
@ -9,10 +9,6 @@ represent the color palette of palette mapped images.
|
|||
|
||||
.. note::
|
||||
|
||||
This module was never well-documented. It hasn't changed since 2001,
|
||||
though, so it's probably safe for you to read the source code and puzzle
|
||||
out the internals if you need to.
|
||||
|
||||
The :py:class:`~PIL.ImagePalette.ImagePalette` class has several methods,
|
||||
but they are all marked as "experimental." Read that as you will. The
|
||||
``[source]`` link is there for a reason.
|
||||
|
|
|
@ -339,7 +339,7 @@ Take your test image, and make a really simple harness.
|
|||
(vpy38-dbg) ubuntu@primary:~/Home/tests$ gdb python
|
||||
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
|
||||
Copyright (C) 2020 Free Software Foundation, Inc.
|
||||
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
|
||||
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
|
||||
This is free software: you are free to change and redistribute it.
|
||||
There is NO WARRANTY, to the extent permitted by law.
|
||||
Type "show copying" and "show warranty" for details.
|
||||
|
@ -348,7 +348,7 @@ Take your test image, and make a really simple harness.
|
|||
For bug reporting instructions, please see:
|
||||
<http://www.gnu.org/software/gdb/bugs/>.
|
||||
Find the GDB manual and other documentation resources online at:
|
||||
<http://www.gnu.org/software/gdb/documentation/>.
|
||||
<https://www.gnu.org/software/gdb/documentation/>.
|
||||
|
||||
For help, type "help".
|
||||
Type "apropos word" to search for commands related to "word"...
|
||||
|
|
|
@ -14,7 +14,7 @@ Png text chunk size limits
|
|||
To prevent potential denial of service attacks using compressed text
|
||||
chunks, there are now limits to the decompressed size of text chunks
|
||||
decoded from PNG images. If the limits are exceeded when opening a PNG
|
||||
image a ``ValueError`` will be raised.
|
||||
image a :py:exc:`ValueError` will be raised.
|
||||
|
||||
Individual text chunks are limited to
|
||||
:py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by
|
||||
|
|
40
docs/releasenotes/8.3.1.rst
Normal file
|
@ -0,0 +1,40 @@
|
|||
8.3.1
|
||||
-----
|
||||
|
||||
Fixed regression converting to NumPy arrays
|
||||
===========================================
|
||||
|
||||
This fixes a regression introduced in 8.3.0 when converting an image to a NumPy array
|
||||
with a ``dtype`` argument.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from PIL import Image
|
||||
>>> import numpy
|
||||
>>> im = Image.new("RGB", (100, 100))
|
||||
>>> numpy.array(im, dtype=numpy.float64)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
TypeError: __array__() takes 1 positional argument but 2 were given
|
||||
>>>
|
||||
|
||||
Catch OSError when checking if destination is sys.stdout
|
||||
========================================================
|
||||
|
||||
In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was
|
||||
updated. This lead to an OSError being raised if the environment restricted access.
|
||||
|
||||
The OSError is now silently caught.
|
||||
|
||||
Fixed removing orientation in ImageOps.exif_transpose
|
||||
=====================================================
|
||||
|
||||
In 8.3.0, :py:meth:`~PIL.ImageOps.exif_transpose` was changed to ensure that the
|
||||
original image EXIF data was not modified, and the orientation was only removed from
|
||||
the modified copy.
|
||||
|
||||
However, for certain images the orientation was already missing from the modified
|
||||
image, leading to a KeyError.
|
||||
|
||||
This error has been resolved, and the copying of metadata to the modified image
|
||||
improved.
|
41
docs/releasenotes/8.3.2.rst
Normal file
|
@ -0,0 +1,41 @@
|
|||
8.3.2
|
||||
-----
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
* :cve:`CVE-2021-23437`: Avoid a potential ReDoS (regular expression denial of service)
|
||||
in :py:class:`~PIL.ImageColor`'s :py:meth:`~PIL.ImageColor.getrgb` by raising
|
||||
:py:exc:`ValueError` if the color specifier is too long. Present since Pillow 5.2.0.
|
||||
|
||||
* Fix 6-byte out-of-bounds (OOB) read. The previous bounds check in ``FliDecode.c``
|
||||
incorrectly calculated the required read buffer size when copying a chunk, potentially
|
||||
reading six extra bytes off the end of the allocated buffer from the heap. Present
|
||||
since Pillow 7.1.0. This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs.
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
Python 3.10 wheels
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Pillow now includes binary wheels for Python 3.10.
|
||||
|
||||
The Python 3.10 release candidate was released on 2021-08-03 with the final release due
|
||||
2021-10-04 (:pep:`619`). The CPython core team strongly encourages maintainers of
|
||||
third-party Python projects to prepare for 3.10 compatibility. And as there are `no ABI
|
||||
changes`_ planned we are releasing wheels to help others prepare for 3.10, and ensure
|
||||
Pillow can be used immediately on release day of 3.10.0 final.
|
||||
|
||||
Fixed regressions
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
* Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression (:pr:`5588`).
|
||||
|
||||
* Updates for :py:class:`~PIL.ImagePalette` channel order (:pr:`5599`).
|
||||
|
||||
* Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library (:pr:`5651`).
|
||||
|
||||
.. _OSS-Fuzz: https://github.com/google/oss-fuzz
|
||||
.. _CIFuzz: https://google.github.io/oss-fuzz/getting-started/continuous-integration/
|
||||
.. _no ABI changes: https://www.python.org/downloads/release/python-3100rc1/
|
61
docs/releasenotes/8.4.0.rst
Normal file
|
@ -0,0 +1,61 @@
|
|||
8.4.0
|
||||
-----
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
Deprecations
|
||||
^^^^^^^^^^^^
|
||||
|
||||
ImagePalette size parameter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``size`` parameter will be removed in Pillow 10.0.0 (2023-01-02).
|
||||
|
||||
Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by
|
||||
default, and the size parameter could be used to override that. Pillow 8.3.0 removed
|
||||
the default required length, also removing the need for the size parameter.
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
Added "transparency" argument for loading EPS images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This new argument switches the Ghostscript device from "ppmraw" to "pngalpha",
|
||||
generating an RGBA image with a transparent background instead of an RGB image with a
|
||||
white background.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with Image.open("sample.eps") as im:
|
||||
im.load(transparency=True)
|
||||
|
||||
Added WalImageFile class
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:py:func:`PIL.WalImageFile.open()` previously returned a generic
|
||||
:py:class:`PIL.Image.Image` instance. It now returns a dedicated
|
||||
:py:class:`PIL.WalImageFile.WalImageFile` class.
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
Speed improvement when rotating square images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Starting with Pillow 3.3.0, the speed of rotating images by 90 or 270 degrees was
|
||||
improved by quickly returning :py:meth:`~PIL.Image.Image.transpose` instead, if the
|
||||
rotate operation allowed for expansion and did not specify a center or post-rotate
|
||||
translation.
|
||||
|
||||
Since the ``expand`` flag makes no difference for square images though, Pillow now
|
||||
uses this faster method for square images without the ``expand`` flag as well.
|
|
@ -14,6 +14,9 @@ expected to be backported to earlier versions.
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
8.4.0
|
||||
8.3.2
|
||||
8.3.1
|
||||
8.3.0
|
||||
8.2.0
|
||||
8.1.2
|
||||
|
|
18
setup.py
|
@ -533,14 +533,16 @@ class pil_build_ext(build_ext):
|
|||
_add_directory(include_dirs, "/usr/X11/include")
|
||||
|
||||
# SDK install path
|
||||
try:
|
||||
sdk_path = (
|
||||
subprocess.check_output(["xcrun", "--show-sdk-path"])
|
||||
.strip()
|
||||
.decode("latin1")
|
||||
)
|
||||
except Exception:
|
||||
sdk_path = None
|
||||
sdk_path = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
|
||||
if not os.path.exists(sdk_path):
|
||||
try:
|
||||
sdk_path = (
|
||||
subprocess.check_output(["xcrun", "--show-sdk-path"])
|
||||
.strip()
|
||||
.decode("latin1")
|
||||
)
|
||||
except Exception:
|
||||
sdk_path = None
|
||||
if sdk_path:
|
||||
_add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib"))
|
||||
_add_directory(include_dirs, os.path.join(sdk_path, "usr", "include"))
|
||||
|
|
|
@ -58,7 +58,7 @@ def _dib_accept(prefix):
|
|||
# Image plugin for the Windows BMP format.
|
||||
# =============================================================================
|
||||
class BmpImageFile(ImageFile.ImageFile):
|
||||
""" Image plugin for the Windows Bitmap format (BMP) """
|
||||
"""Image plugin for the Windows Bitmap format (BMP)"""
|
||||
|
||||
# ------------------------------------------------------------- Description
|
||||
format_description = "Windows Bitmap"
|
||||
|
@ -70,7 +70,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
vars()[k] = v
|
||||
|
||||
def _bitmap(self, header=0, offset=0):
|
||||
""" Read relevant info about the BMP """
|
||||
"""Read relevant info about the BMP"""
|
||||
read, seek = self.fp.read, self.fp.seek
|
||||
if header:
|
||||
seek(header)
|
||||
|
@ -257,7 +257,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
]
|
||||
|
||||
def _open(self):
|
||||
""" Open file, check magic number and read header """
|
||||
"""Open file, check magic number and read header"""
|
||||
# read 14 bytes: magic number, filesize, reserved, header final offset
|
||||
head_data = self.fp.read(14)
|
||||
# choke if the file does not have the required magic bytes
|
||||
|
|
|
@ -61,7 +61,7 @@ def has_ghostscript():
|
|||
return False
|
||||
|
||||
|
||||
def Ghostscript(tile, size, fp, scale=1):
|
||||
def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
||||
"""Render an image using Ghostscript"""
|
||||
|
||||
# Unpack decoder tile
|
||||
|
@ -108,6 +108,8 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
lengthfile -= len(s)
|
||||
f.write(s)
|
||||
|
||||
device = "pngalpha" if transparency else "ppmraw"
|
||||
|
||||
# Build Ghostscript command
|
||||
command = [
|
||||
"gs",
|
||||
|
@ -117,7 +119,7 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
"-dBATCH", # exit after processing
|
||||
"-dNOPAUSE", # don't pause between pages
|
||||
"-dSAFER", # safe mode
|
||||
"-sDEVICE=ppmraw", # ppm driver
|
||||
f"-sDEVICE={device}",
|
||||
f"-sOutputFile={outfile}", # output file
|
||||
# adjust for image origin
|
||||
"-c",
|
||||
|
@ -325,11 +327,11 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
|
||||
return (length, offset)
|
||||
|
||||
def load(self, scale=1):
|
||||
def load(self, scale=1, transparency=False):
|
||||
# Load EPS via Ghostscript
|
||||
if not self.tile:
|
||||
return
|
||||
self.im = Ghostscript(self.tile, self.size, self.fp, scale)
|
||||
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
|
||||
self.mode = self.im.mode
|
||||
self._size = self.im.size
|
||||
self.tile = []
|
||||
|
|
|
@ -396,15 +396,7 @@ def _normalize_palette(im, palette, info):
|
|||
if isinstance(palette, (bytes, bytearray, list)):
|
||||
source_palette = bytearray(palette[:768])
|
||||
if isinstance(palette, ImagePalette.ImagePalette):
|
||||
source_palette = bytearray(
|
||||
itertools.chain.from_iterable(
|
||||
zip(
|
||||
palette.palette[:256],
|
||||
palette.palette[256:512],
|
||||
palette.palette[512:768],
|
||||
)
|
||||
)
|
||||
)
|
||||
source_palette = bytearray(palette.palette)
|
||||
|
||||
if im.mode == "P":
|
||||
if not source_palette:
|
||||
|
@ -414,9 +406,26 @@ def _normalize_palette(im, palette, info):
|
|||
source_palette = bytearray(i // 3 for i in range(768))
|
||||
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
|
||||
|
||||
used_palette_colors = _get_optimize(im, info)
|
||||
if used_palette_colors is not None:
|
||||
return im.remap_palette(used_palette_colors, source_palette)
|
||||
if palette:
|
||||
used_palette_colors = []
|
||||
for i in range(0, len(source_palette), 3):
|
||||
source_color = tuple(source_palette[i : i + 3])
|
||||
try:
|
||||
index = im.palette.colors[source_color]
|
||||
except KeyError:
|
||||
index = None
|
||||
used_palette_colors.append(index)
|
||||
for i, index in enumerate(used_palette_colors):
|
||||
if index is None:
|
||||
for j in range(len(used_palette_colors)):
|
||||
if j not in used_palette_colors:
|
||||
used_palette_colors[i] = j
|
||||
break
|
||||
im = im.remap_palette(used_palette_colors)
|
||||
else:
|
||||
used_palette_colors = _get_optimize(im, info)
|
||||
if used_palette_colors is not None:
|
||||
return im.remap_palette(used_palette_colors, source_palette)
|
||||
|
||||
im.palette.palette = source_palette
|
||||
return im
|
||||
|
@ -507,7 +516,8 @@ def _write_multiple_frames(im, fp, palette):
|
|||
offset = (0, 0)
|
||||
else:
|
||||
# compress difference
|
||||
frame_data["encoderinfo"]["include_color_table"] = True
|
||||
if not palette:
|
||||
frame_data["encoderinfo"]["include_color_table"] = True
|
||||
|
||||
im_frame = im_frame.crop(frame_data["bbox"])
|
||||
offset = frame_data["bbox"][:2]
|
||||
|
@ -787,7 +797,7 @@ def _get_global_header(im, info):
|
|||
"""Return a list of strings representing a GIF header"""
|
||||
|
||||
# Header Block
|
||||
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
# https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
|
||||
version = b"87a"
|
||||
for extensionKey in ["transparency", "duration", "loop", "comment"]:
|
||||
|
|
|
@ -235,8 +235,8 @@ class IcoFile:
|
|||
# the total mask data is
|
||||
# padded row size * height / bits per char
|
||||
|
||||
and_mask_offset = o + int(im.size[0] * im.size[1] * (bpp / 8.0))
|
||||
total_bytes = int((w * im.size[1]) / 8)
|
||||
and_mask_offset = header["offset"] + header["size"] - total_bytes
|
||||
|
||||
self.buf.seek(and_mask_offset)
|
||||
mask_data = self.buf.read(total_bytes)
|
||||
|
|
|
@ -681,7 +681,7 @@ class Image:
|
|||
raise ValueError("Could not save to PNG for display") from e
|
||||
return b.getvalue()
|
||||
|
||||
def __array__(self):
|
||||
def __array__(self, dtype=None):
|
||||
# numpy array interface support
|
||||
import numpy as np
|
||||
|
||||
|
@ -700,7 +700,7 @@ class Image:
|
|||
class ArrayData:
|
||||
__array_interface__ = new
|
||||
|
||||
return np.array(ArrayData())
|
||||
return np.array(ArrayData(), dtype)
|
||||
|
||||
def __getstate__(self):
|
||||
return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()]
|
||||
|
@ -914,16 +914,18 @@ class Image:
|
|||
|
||||
self.load()
|
||||
|
||||
has_transparency = self.info.get("transparency") is not None
|
||||
if not mode and self.mode == "P":
|
||||
# determine default mode
|
||||
if self.palette:
|
||||
mode = self.palette.mode
|
||||
else:
|
||||
mode = "RGB"
|
||||
if mode == "RGB" and has_transparency:
|
||||
mode = "RGBA"
|
||||
if not mode or (mode == self.mode and not matrix):
|
||||
return self.copy()
|
||||
|
||||
has_transparency = self.info.get("transparency") is not None
|
||||
if matrix:
|
||||
# matrix conversion
|
||||
if mode not in ("L", "RGB"):
|
||||
|
@ -1005,7 +1007,7 @@ class Image:
|
|||
trns_im = trns_im.convert("RGB")
|
||||
trns = trns_im.getpixel((0, 0))
|
||||
|
||||
elif self.mode == "P" and mode == "RGBA":
|
||||
elif self.mode == "P" and mode in ("LA", "PA", "RGBA"):
|
||||
t = self.info["transparency"]
|
||||
delete_trns = True
|
||||
|
||||
|
@ -1128,7 +1130,9 @@ class Image:
|
|||
"only RGB or L mode images can be quantized to a palette"
|
||||
)
|
||||
im = self.im.convert("P", dither, palette.im)
|
||||
return self._new(im)
|
||||
new_im = self._new(im)
|
||||
new_im.palette = palette.palette.copy()
|
||||
return new_im
|
||||
|
||||
im = self._new(self.im.quantize(colors, method, kmeans))
|
||||
|
||||
|
@ -1751,14 +1755,19 @@ class Image:
|
|||
Attaches a palette to this image. The image must be a "P", "PA", "L"
|
||||
or "LA" image.
|
||||
|
||||
The palette sequence must contain at most 768 integer values, or 1024
|
||||
integer values if alpha is included. Each group of values represents
|
||||
the red, green, blue (and alpha if included) values for the
|
||||
corresponding pixel index. Instead of an integer sequence, you can use
|
||||
an 8-bit string.
|
||||
The palette sequence must contain at most 256 colors, made up of one
|
||||
integer value for each channel in the raw mode.
|
||||
For example, if the raw mode is "RGB", then it can contain at most 768
|
||||
values, made up of red, green and blue values for the corresponding pixel
|
||||
index in the 256 colors.
|
||||
If the raw mode is "RGBA", then it can contain at most 1024 values,
|
||||
containing red, green, blue and alpha values.
|
||||
|
||||
Alternatively, an 8-bit string may be used instead of an integer sequence.
|
||||
|
||||
:param data: A palette sequence (either a list or a string).
|
||||
:param rawmode: The raw mode of the palette.
|
||||
:param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a
|
||||
mode that can be transformed to "RGB" (e.g. "R", "BGR;15", "RGBA;L").
|
||||
"""
|
||||
from . import ImagePalette
|
||||
|
||||
|
@ -1832,18 +1841,16 @@ class Image:
|
|||
if source_palette is None:
|
||||
if self.mode == "P":
|
||||
self.load()
|
||||
real_source_palette = self.im.getpalette("RGB")[:768]
|
||||
source_palette = self.im.getpalette("RGB")[:768]
|
||||
else: # L-mode
|
||||
real_source_palette = bytearray(i // 3 for i in range(768))
|
||||
else:
|
||||
real_source_palette = source_palette
|
||||
source_palette = bytearray(i // 3 for i in range(768))
|
||||
|
||||
palette_bytes = b""
|
||||
new_positions = [0] * 256
|
||||
|
||||
# pick only the used colors from the palette
|
||||
for i, oldPosition in enumerate(dest_map):
|
||||
palette_bytes += real_source_palette[oldPosition * 3 : oldPosition * 3 + 3]
|
||||
palette_bytes += source_palette[oldPosition * 3 : oldPosition * 3 + 3]
|
||||
new_positions[oldPosition] = i
|
||||
|
||||
# replace the palette color id of all pixel with the new id
|
||||
|
@ -2076,10 +2083,8 @@ class Image:
|
|||
return self.copy()
|
||||
if angle == 180:
|
||||
return self.transpose(ROTATE_180)
|
||||
if angle == 90 and expand:
|
||||
return self.transpose(ROTATE_90)
|
||||
if angle == 270 and expand:
|
||||
return self.transpose(ROTATE_270)
|
||||
if angle in (90, 270) and (expand or self.width == self.height):
|
||||
return self.transpose(ROTATE_90 if angle == 90 else ROTATE_270)
|
||||
|
||||
# Calculate the affine matrix. Note that this is the reverse
|
||||
# transformation (from destination image to source) because we
|
||||
|
@ -2182,12 +2187,12 @@ class Image:
|
|||
|
||||
filename = ""
|
||||
open_fp = False
|
||||
if isPath(fp):
|
||||
filename = fp
|
||||
open_fp = True
|
||||
elif isinstance(fp, Path):
|
||||
if isinstance(fp, Path):
|
||||
filename = str(fp)
|
||||
open_fp = True
|
||||
elif isPath(fp):
|
||||
filename = fp
|
||||
open_fp = True
|
||||
elif fp == sys.stdout:
|
||||
try:
|
||||
fp = sys.stdout.buffer
|
||||
|
@ -2481,6 +2486,8 @@ class Image:
|
|||
raise ValueError("missing method data")
|
||||
|
||||
im = new(self.mode, size, fillcolor)
|
||||
if self.mode == "P" and self.palette:
|
||||
im.palette = self.palette.copy()
|
||||
im.info = self.info.copy()
|
||||
if method == MESH:
|
||||
# list of quads
|
||||
|
|
|
@ -37,7 +37,7 @@ pyCMS
|
|||
http://www.cazabon.com
|
||||
|
||||
pyCMS home page: http://www.cazabon.com/pyCMS
|
||||
littleCMS home page: http://www.littlecms.com
|
||||
littleCMS home page: https://www.littlecms.com
|
||||
(littleCMS is Copyright (C) 1998-2001 Marti Maria)
|
||||
|
||||
Originally released under LGPL. Graciously donated to PIL in
|
||||
|
|
|
@ -32,6 +32,8 @@ def getrgb(color):
|
|||
:param color: A color string
|
||||
:return: ``(red, green, blue[, alpha])``
|
||||
"""
|
||||
if len(color) > 100:
|
||||
raise ValueError("color specifier is too long")
|
||||
color = color.lower()
|
||||
|
||||
rgb = colormap.get(color, None)
|
||||
|
|
|
@ -493,7 +493,11 @@ def _save(im, fp, tile, bufsize=0):
|
|||
# But, it would need at least the image size in most cases. RawEncode is
|
||||
# a tricky case.
|
||||
bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c
|
||||
if fp == sys.stdout or (hasattr(sys.stdout, "buffer") and fp == sys.stdout.buffer):
|
||||
try:
|
||||
stdout = fp == sys.stdout or fp == sys.stdout.buffer
|
||||
except (OSError, AttributeError):
|
||||
stdout = False
|
||||
if stdout:
|
||||
fp.flush()
|
||||
return
|
||||
try:
|
||||
|
|
|
@ -19,8 +19,9 @@
|
|||
|
||||
import functools
|
||||
import operator
|
||||
import re
|
||||
|
||||
from . import Image, ImageDraw
|
||||
from . import Image
|
||||
|
||||
#
|
||||
# helpers
|
||||
|
@ -394,15 +395,16 @@ def expand(image, border=0, fill=0):
|
|||
height = top + image.size[1] + bottom
|
||||
color = _color(fill, image.mode)
|
||||
if image.mode == "P" and image.palette:
|
||||
out = Image.new(image.mode, (width, height))
|
||||
out.putpalette(image.palette)
|
||||
out.paste(image, (left, top))
|
||||
|
||||
draw = ImageDraw.Draw(out)
|
||||
draw.rectangle((0, 0, width - 1, height - 1), outline=color, width=border)
|
||||
image.load()
|
||||
palette = image.palette.copy()
|
||||
if isinstance(color, tuple):
|
||||
color = palette.getcolor(color)
|
||||
else:
|
||||
out = Image.new(image.mode, (width, height), color)
|
||||
out.paste(image, (left, top))
|
||||
palette = None
|
||||
out = Image.new(image.mode, (width, height), color)
|
||||
if palette:
|
||||
out.putpalette(palette.palette)
|
||||
out.paste(image, (left, top))
|
||||
return out
|
||||
|
||||
|
||||
|
@ -588,7 +590,19 @@ def exif_transpose(image):
|
|||
if method is not None:
|
||||
transposed_image = image.transpose(method)
|
||||
transposed_exif = transposed_image.getexif()
|
||||
del transposed_exif[0x0112]
|
||||
transposed_image.info["exif"] = transposed_exif.tobytes()
|
||||
if 0x0112 in transposed_exif:
|
||||
del transposed_exif[0x0112]
|
||||
if "exif" in transposed_image.info:
|
||||
transposed_image.info["exif"] = transposed_exif.tobytes()
|
||||
elif "Raw profile type exif" in transposed_image.info:
|
||||
transposed_image.info[
|
||||
"Raw profile type exif"
|
||||
] = transposed_exif.tobytes().hex()
|
||||
elif "XML:com.adobe.xmp" in transposed_image.info:
|
||||
transposed_image.info["XML:com.adobe.xmp"] = re.sub(
|
||||
r'tiff:Orientation="([0-9])"',
|
||||
"",
|
||||
transposed_image.info["XML:com.adobe.xmp"],
|
||||
)
|
||||
return transposed_image
|
||||
return image.copy()
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#
|
||||
|
||||
import array
|
||||
import warnings
|
||||
|
||||
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
||||
|
||||
|
@ -25,15 +26,14 @@ class ImagePalette:
|
|||
"""
|
||||
Color palette for palette mapped images
|
||||
|
||||
:param mode: The mode to use for the Palette. See:
|
||||
:param mode: The mode to use for the palette. See:
|
||||
:ref:`concept-modes`. Defaults to "RGB"
|
||||
:param palette: An optional palette. If given, it must be a bytearray,
|
||||
an array or a list of ints between 0-255 and of length ``size``
|
||||
times the number of colors in ``mode``. The list must be aligned
|
||||
by channel (All R values must be contiguous in the list before G
|
||||
and B values.) Defaults to 0 through 255 per channel.
|
||||
:param size: An optional palette size. If given, it cannot be equal to
|
||||
or greater than 256. Defaults to 0.
|
||||
an array or a list of ints between 0-255. The list must consist of
|
||||
all channels for one color followed by the next color (e.g. RGBRGBRGB).
|
||||
Defaults to an empty palette.
|
||||
:param size: An optional palette size. If given, an error is raised
|
||||
if ``palette`` is not of equal length.
|
||||
"""
|
||||
|
||||
def __init__(self, mode="RGB", palette=None, size=0):
|
||||
|
@ -41,8 +41,14 @@ class ImagePalette:
|
|||
self.rawmode = None # if set, palette contains raw data
|
||||
self.palette = palette or bytearray()
|
||||
self.dirty = None
|
||||
if size != 0 and size != len(self.palette):
|
||||
raise ValueError("wrong palette size")
|
||||
if size != 0:
|
||||
warnings.warn(
|
||||
"The size parameter is deprecated and will be removed in Pillow 10 "
|
||||
"(2023-01-02).",
|
||||
DeprecationWarning,
|
||||
)
|
||||
if size != len(self.palette):
|
||||
raise ValueError("wrong palette size")
|
||||
|
||||
@property
|
||||
def palette(self):
|
||||
|
@ -205,9 +211,9 @@ def make_gamma_lut(exp):
|
|||
|
||||
|
||||
def negative(mode="RGB"):
|
||||
palette = list(range(256))
|
||||
palette = list(range(256 * len(mode)))
|
||||
palette.reverse()
|
||||
return ImagePalette(mode, palette * len(mode))
|
||||
return ImagePalette(mode, [i // len(mode) for i in palette])
|
||||
|
||||
|
||||
def random(mode="RGB"):
|
||||
|
@ -220,15 +226,13 @@ def random(mode="RGB"):
|
|||
|
||||
|
||||
def sepia(white="#fff0c0"):
|
||||
r, g, b = ImageColor.getrgb(white)
|
||||
r = make_linear_lut(0, r)
|
||||
g = make_linear_lut(0, g)
|
||||
b = make_linear_lut(0, b)
|
||||
return ImagePalette("RGB", r + g + b)
|
||||
bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
|
||||
return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
|
||||
|
||||
|
||||
def wedge(mode="RGB"):
|
||||
return ImagePalette(mode, list(range(256)) * len(mode))
|
||||
palette = list(range(256 * len(mode)))
|
||||
return ImagePalette(mode, [i // len(mode) for i in palette])
|
||||
|
||||
|
||||
def load(filename):
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#
|
||||
# History:
|
||||
# 2014-03-12 ajh Created
|
||||
# 2021-06-30 rogermb Extract dpi information from the 'resc' header box
|
||||
#
|
||||
# Copyright (c) 2014 Coriolis Systems Limited
|
||||
# Copyright (c) 2014 Alastair Houghton
|
||||
|
@ -19,6 +20,79 @@ import struct
|
|||
from . import Image, ImageFile
|
||||
|
||||
|
||||
class BoxReader:
|
||||
"""
|
||||
A small helper class to read fields stored in JPEG2000 header boxes
|
||||
and to easily step into and read sub-boxes.
|
||||
"""
|
||||
|
||||
def __init__(self, fp, length=-1):
|
||||
self.fp = fp
|
||||
self.has_length = length >= 0
|
||||
self.length = length
|
||||
self.remaining_in_box = -1
|
||||
|
||||
def _can_read(self, num_bytes):
|
||||
if self.has_length and self.fp.tell() + num_bytes > self.length:
|
||||
# Outside box: ensure we don't read past the known file length
|
||||
return False
|
||||
if self.remaining_in_box >= 0:
|
||||
# Inside box contents: ensure read does not go past box boundaries
|
||||
return num_bytes <= self.remaining_in_box
|
||||
else:
|
||||
return True # No length known, just read
|
||||
|
||||
def _read_bytes(self, num_bytes):
|
||||
if not self._can_read(num_bytes):
|
||||
raise SyntaxError("Not enough data in header")
|
||||
|
||||
data = self.fp.read(num_bytes)
|
||||
if len(data) < num_bytes:
|
||||
raise OSError(
|
||||
f"Expected to read {num_bytes} bytes but only got {len(data)}."
|
||||
)
|
||||
|
||||
if self.remaining_in_box > 0:
|
||||
self.remaining_in_box -= num_bytes
|
||||
return data
|
||||
|
||||
def read_fields(self, field_format):
|
||||
size = struct.calcsize(field_format)
|
||||
data = self._read_bytes(size)
|
||||
return struct.unpack(field_format, data)
|
||||
|
||||
def read_boxes(self):
|
||||
size = self.remaining_in_box
|
||||
data = self._read_bytes(size)
|
||||
return BoxReader(io.BytesIO(data), size)
|
||||
|
||||
def has_next_box(self):
|
||||
if self.has_length:
|
||||
return self.fp.tell() + self.remaining_in_box < self.length
|
||||
else:
|
||||
return True
|
||||
|
||||
def next_box_type(self):
|
||||
# Skip the rest of the box if it has not been read
|
||||
if self.remaining_in_box > 0:
|
||||
self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
|
||||
self.remaining_in_box = -1
|
||||
|
||||
# Read the length and type of the next box
|
||||
lbox, tbox = self.read_fields(">I4s")
|
||||
if lbox == 1:
|
||||
lbox = self.read_fields(">Q")[0]
|
||||
hlen = 16
|
||||
else:
|
||||
hlen = 8
|
||||
|
||||
if lbox < hlen or not self._can_read(lbox - hlen):
|
||||
raise SyntaxError("Invalid header length")
|
||||
|
||||
self.remaining_in_box = lbox - hlen
|
||||
return tbox
|
||||
|
||||
|
||||
def _parse_codestream(fp):
|
||||
"""Parse the JPEG 2000 codestream to extract the size and component
|
||||
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
|
||||
|
@ -53,101 +127,71 @@ def _parse_codestream(fp):
|
|||
return (size, mode)
|
||||
|
||||
|
||||
def _res_to_dpi(num, denom, exp):
|
||||
"""Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
|
||||
calculated as (num / denom) * 10^exp and stored in dots per meter,
|
||||
to floating-point dots per inch."""
|
||||
if denom != 0:
|
||||
return (254 * num * (10 ** exp)) / (10000 * denom)
|
||||
|
||||
|
||||
def _parse_jp2_header(fp):
|
||||
"""Parse the JP2 header box to extract size, component count and
|
||||
color space information, returning a (size, mode, mimetype) tuple."""
|
||||
"""Parse the JP2 header box to extract size, component count,
|
||||
color space information, and optionally DPI information,
|
||||
returning a (size, mode, mimetype, dpi) tuple."""
|
||||
|
||||
# Find the JP2 header box
|
||||
reader = BoxReader(fp)
|
||||
header = None
|
||||
mimetype = None
|
||||
while True:
|
||||
lbox, tbox = struct.unpack(">I4s", fp.read(8))
|
||||
if lbox == 1:
|
||||
lbox = struct.unpack(">Q", fp.read(8))[0]
|
||||
hlen = 16
|
||||
else:
|
||||
hlen = 8
|
||||
|
||||
if lbox < hlen:
|
||||
raise SyntaxError("Invalid JP2 header length")
|
||||
while reader.has_next_box():
|
||||
tbox = reader.next_box_type()
|
||||
|
||||
if tbox == b"jp2h":
|
||||
header = fp.read(lbox - hlen)
|
||||
header = reader.read_boxes()
|
||||
break
|
||||
elif tbox == b"ftyp":
|
||||
if fp.read(4) == b"jpx ":
|
||||
if reader.read_fields(">4s")[0] == b"jpx ":
|
||||
mimetype = "image/jpx"
|
||||
fp.seek(lbox - hlen - 4, os.SEEK_CUR)
|
||||
else:
|
||||
fp.seek(lbox - hlen, os.SEEK_CUR)
|
||||
|
||||
if header is None:
|
||||
raise SyntaxError("could not find JP2 header")
|
||||
|
||||
size = None
|
||||
mode = None
|
||||
bpc = None
|
||||
nc = None
|
||||
dpi = None # 2-tuple of DPI info, or None
|
||||
|
||||
hio = io.BytesIO(header)
|
||||
while True:
|
||||
lbox, tbox = struct.unpack(">I4s", hio.read(8))
|
||||
if lbox == 1:
|
||||
lbox = struct.unpack(">Q", hio.read(8))[0]
|
||||
hlen = 16
|
||||
else:
|
||||
hlen = 8
|
||||
|
||||
content = hio.read(lbox - hlen)
|
||||
while header.has_next_box():
|
||||
tbox = header.next_box_type()
|
||||
|
||||
if tbox == b"ihdr":
|
||||
height, width, nc, bpc, c, unkc, ipr = struct.unpack(">IIHBBBB", content)
|
||||
height, width, nc, bpc = header.read_fields(">IIHB")
|
||||
size = (width, height)
|
||||
if unkc:
|
||||
if nc == 1 and (bpc & 0x7F) > 8:
|
||||
mode = "I;16"
|
||||
elif nc == 1:
|
||||
mode = "L"
|
||||
elif nc == 2:
|
||||
mode = "LA"
|
||||
elif nc == 3:
|
||||
mode = "RGB"
|
||||
elif nc == 4:
|
||||
mode = "RGBA"
|
||||
break
|
||||
elif tbox == b"colr":
|
||||
meth, prec, approx = struct.unpack_from(">BBB", content)
|
||||
if meth == 1:
|
||||
cs = struct.unpack_from(">I", content, 3)[0]
|
||||
if cs == 16: # sRGB
|
||||
if nc == 1 and (bpc & 0x7F) > 8:
|
||||
mode = "I;16"
|
||||
elif nc == 1:
|
||||
mode = "L"
|
||||
elif nc == 3:
|
||||
mode = "RGB"
|
||||
elif nc == 4:
|
||||
mode = "RGBA"
|
||||
break
|
||||
elif cs == 17: # grayscale
|
||||
if nc == 1 and (bpc & 0x7F) > 8:
|
||||
mode = "I;16"
|
||||
elif nc == 1:
|
||||
mode = "L"
|
||||
elif nc == 2:
|
||||
mode = "LA"
|
||||
break
|
||||
elif cs == 18: # sYCC
|
||||
if nc == 3:
|
||||
mode = "RGB"
|
||||
elif nc == 4:
|
||||
mode = "RGBA"
|
||||
if nc == 1 and (bpc & 0x7F) > 8:
|
||||
mode = "I;16"
|
||||
elif nc == 1:
|
||||
mode = "L"
|
||||
elif nc == 2:
|
||||
mode = "LA"
|
||||
elif nc == 3:
|
||||
mode = "RGB"
|
||||
elif nc == 4:
|
||||
mode = "RGBA"
|
||||
elif tbox == b"res ":
|
||||
res = header.read_boxes()
|
||||
while res.has_next_box():
|
||||
tres = res.next_box_type()
|
||||
if tres == b"resc":
|
||||
vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
|
||||
hres = _res_to_dpi(hrcn, hrcd, hrce)
|
||||
vres = _res_to_dpi(vrcn, vrcd, vrce)
|
||||
if hres is not None and vres is not None:
|
||||
dpi = (hres, vres)
|
||||
break
|
||||
|
||||
if size is None or mode is None:
|
||||
raise SyntaxError("Malformed jp2 header")
|
||||
raise SyntaxError("Malformed JP2 header")
|
||||
|
||||
return (size, mode, mimetype)
|
||||
return (size, mode, mimetype, dpi)
|
||||
|
||||
|
||||
##
|
||||
|
@ -169,7 +213,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
|
||||
self.codec = "jp2"
|
||||
header = _parse_jp2_header(self.fp)
|
||||
self._size, self.mode, self.custom_mimetype = header
|
||||
self._size, self.mode, self.custom_mimetype, dpi = header
|
||||
if dpi is not None:
|
||||
self.info["dpi"] = dpi
|
||||
else:
|
||||
raise SyntaxError("not a JPEG 2000 file")
|
||||
|
||||
|
|
|
@ -168,11 +168,11 @@ def APP(self, marker):
|
|||
# 1 dpcm = 2.54 dpi
|
||||
dpi *= 2.54
|
||||
self.info["dpi"] = dpi, dpi
|
||||
except (KeyError, SyntaxError, ValueError, ZeroDivisionError):
|
||||
except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError):
|
||||
# SyntaxError for invalid/unreadable EXIF
|
||||
# KeyError for dpi not included
|
||||
# ZeroDivisionError for invalid dpi rational value
|
||||
# ValueError for dpi being an invalid float
|
||||
# ValueError or TypeError for dpi being an invalid float
|
||||
self.info["dpi"] = 72, 72
|
||||
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
# Figure 205. Windows Paint Version 1: "DanM" Format
|
||||
# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03
|
||||
#
|
||||
# See also: http://www.fileformat.info/format/mspaint/egff.htm
|
||||
# See also: https://www.fileformat.info/format/mspaint/egff.htm
|
||||
|
||||
import io
|
||||
import struct
|
||||
|
@ -73,7 +73,7 @@ class MspImageFile(ImageFile.ImageFile):
|
|||
|
||||
class MspDecoder(ImageFile.PyDecoder):
|
||||
# The algo for the MSP decoder is from
|
||||
# http://www.fileformat.info/format/mspaint/egff.htm
|
||||
# https://www.fileformat.info/format/mspaint/egff.htm
|
||||
# cc-by-attribution -- That page references is taken from the
|
||||
# Encyclopedia of Graphics File Formats and is licensed by
|
||||
# O'Reilly under the Creative Common/Attribution license
|
||||
|
|
|
@ -1061,8 +1061,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
|
|||
default_image = im.encoderinfo.get("default_image", im.info.get("default_image"))
|
||||
duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
|
||||
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
|
||||
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
|
||||
blend = im.encoderinfo.get("blend", im.info.get("blend"))
|
||||
disposal = im.encoderinfo.get(
|
||||
"disposal", im.info.get("disposal", APNG_DISPOSE_OP_NONE)
|
||||
)
|
||||
blend = im.encoderinfo.get("blend", im.info.get("blend", APNG_BLEND_OP_SOURCE))
|
||||
|
||||
if default_image:
|
||||
chain = itertools.chain(im.encoderinfo.get("append_images", []))
|
||||
|
@ -1117,12 +1119,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
|
|||
and prev_disposal == encoderinfo.get("disposal")
|
||||
and prev_blend == encoderinfo.get("blend")
|
||||
):
|
||||
duration = encoderinfo.get("duration", 0)
|
||||
if duration:
|
||||
if "duration" in previous["encoderinfo"]:
|
||||
previous["encoderinfo"]["duration"] += duration
|
||||
else:
|
||||
previous["encoderinfo"]["duration"] = duration
|
||||
if isinstance(duration, (list, tuple)):
|
||||
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
|
||||
continue
|
||||
else:
|
||||
bbox = None
|
||||
|
@ -1149,9 +1147,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
|
|||
bbox = frame_data["bbox"]
|
||||
im_frame = im_frame.crop(bbox)
|
||||
size = im_frame.size
|
||||
duration = int(round(frame_data["encoderinfo"].get("duration", 0)))
|
||||
disposal = frame_data["encoderinfo"].get("disposal", APNG_DISPOSE_OP_NONE)
|
||||
blend = frame_data["encoderinfo"].get("blend", APNG_BLEND_OP_SOURCE)
|
||||
encoderinfo = frame_data["encoderinfo"]
|
||||
frame_duration = int(round(encoderinfo.get("duration", duration)))
|
||||
frame_disposal = encoderinfo.get("disposal", disposal)
|
||||
frame_blend = encoderinfo.get("blend", blend)
|
||||
# frame control
|
||||
chunk(
|
||||
fp,
|
||||
|
@ -1161,10 +1160,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
|
|||
o32(size[1]), # height
|
||||
o32(bbox[0]), # x_offset
|
||||
o32(bbox[1]), # y_offset
|
||||
o16(duration), # delay_numerator
|
||||
o16(frame_duration), # delay_numerator
|
||||
o16(1000), # delay_denominator
|
||||
o8(disposal), # dispose_op
|
||||
o8(blend), # blend_op
|
||||
o8(frame_disposal), # dispose_op
|
||||
o8(frame_blend), # blend_op
|
||||
)
|
||||
seq_num += 1
|
||||
# frame data
|
||||
|
|
|
@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette
|
|||
from ._binary import i8
|
||||
from ._binary import i16be as i16
|
||||
from ._binary import i32be as i32
|
||||
from ._binary import si16be as si16
|
||||
|
||||
MODES = {
|
||||
# (photoshop mode, bits) -> (pil mode, required channels)
|
||||
|
@ -179,7 +180,7 @@ def _layerinfo(fp, ct_bytes):
|
|||
def read(size):
|
||||
return ImageFile._safe_read(fp, size)
|
||||
|
||||
ct = i16(read(2))
|
||||
ct = si16(read(2))
|
||||
|
||||
# sanity check
|
||||
if ct_bytes < (abs(ct) * 20):
|
||||
|
|
|
@ -128,7 +128,7 @@ class PyAccess:
|
|||
|
||||
|
||||
class _PyAccess32_2(PyAccess):
|
||||
""" PA, LA, stored in first and last bytes of a 32 bit word """
|
||||
"""PA, LA, stored in first and last bytes of a 32 bit word"""
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
||||
|
@ -145,7 +145,7 @@ class _PyAccess32_2(PyAccess):
|
|||
|
||||
|
||||
class _PyAccess32_3(PyAccess):
|
||||
""" RGB and friends, stored in the first three bytes of a 32 bit word """
|
||||
"""RGB and friends, stored in the first three bytes of a 32 bit word"""
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
||||
|
@ -164,7 +164,7 @@ class _PyAccess32_3(PyAccess):
|
|||
|
||||
|
||||
class _PyAccess32_4(PyAccess):
|
||||
""" RGBA etc, all 4 bytes of a 32 bit word """
|
||||
"""RGBA etc, all 4 bytes of a 32 bit word"""
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
||||
|
@ -183,7 +183,7 @@ class _PyAccess32_4(PyAccess):
|
|||
|
||||
|
||||
class _PyAccess8(PyAccess):
|
||||
""" 1, L, P, 8 bit images stored as uint8 """
|
||||
"""1, L, P, 8 bit images stored as uint8"""
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = self.image8
|
||||
|
@ -201,7 +201,7 @@ class _PyAccess8(PyAccess):
|
|||
|
||||
|
||||
class _PyAccessI16_N(PyAccess):
|
||||
""" I;16 access, native bitendian without conversion """
|
||||
"""I;16 access, native bitendian without conversion"""
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("unsigned short **", self.image)
|
||||
|
@ -219,7 +219,7 @@ class _PyAccessI16_N(PyAccess):
|
|||
|
||||
|
||||
class _PyAccessI16_L(PyAccess):
|
||||
""" I;16L access, with conversion """
|
||||
"""I;16L access, with conversion"""
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
|
||||
|
@ -240,7 +240,7 @@ class _PyAccessI16_L(PyAccess):
|
|||
|
||||
|
||||
class _PyAccessI16_B(PyAccess):
|
||||
""" I;16B access, with conversion """
|
||||
"""I;16B access, with conversion"""
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
|
||||
|
@ -261,7 +261,7 @@ class _PyAccessI16_B(PyAccess):
|
|||
|
||||
|
||||
class _PyAccessI32_N(PyAccess):
|
||||
""" Signed Int32 access, native endian """
|
||||
"""Signed Int32 access, native endian"""
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = self.image32
|
||||
|
@ -274,7 +274,7 @@ class _PyAccessI32_N(PyAccess):
|
|||
|
||||
|
||||
class _PyAccessI32_Swap(PyAccess):
|
||||
""" I;32L/B access, with byteswapping conversion """
|
||||
"""I;32L/B access, with byteswapping conversion"""
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = self.image32
|
||||
|
@ -293,7 +293,7 @@ class _PyAccessI32_Swap(PyAccess):
|
|||
|
||||
|
||||
class _PyAccessF(PyAccess):
|
||||
""" 32 bit float access """
|
||||
"""32 bit float access"""
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("float **", self.image32)
|
||||
|
|
|
@ -193,7 +193,8 @@ def _save(im, fp, filename):
|
|||
for channel in im.split():
|
||||
fp.write(channel.tobytes("raw", rawmode, 0, orientation))
|
||||
|
||||
fp.close()
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
|
||||
class SGI16Decoder(ImageFile.PyDecoder):
|
||||
|
|
|
@ -48,7 +48,7 @@ from collections.abc import MutableMapping
|
|||
from fractions import Fraction
|
||||
from numbers import Number, Rational
|
||||
|
||||
from . import Image, ImageFile, ImagePalette, TiffTags
|
||||
from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
|
||||
from ._binary import o8
|
||||
from .TiffTags import TYPES
|
||||
|
||||
|
@ -93,6 +93,7 @@ SUBIFD = 330
|
|||
EXTRASAMPLES = 338
|
||||
SAMPLEFORMAT = 339
|
||||
JPEGTABLES = 347
|
||||
YCBCRSUBSAMPLING = 530
|
||||
REFERENCEBLACKWHITE = 532
|
||||
COPYRIGHT = 33432
|
||||
IPTC_NAA_CHUNK = 33723 # newsphoto properties
|
||||
|
@ -1497,7 +1498,9 @@ def _save(im, fp, filename):
|
|||
|
||||
ifd = ImageFileDirectory_v2(prefix=prefix)
|
||||
|
||||
compression = im.encoderinfo.get("compression", im.info.get("compression"))
|
||||
encoderinfo = im.encoderinfo
|
||||
encoderconfig = im.encoderconfig
|
||||
compression = encoderinfo.get("compression", im.info.get("compression"))
|
||||
if compression is None:
|
||||
compression = "raw"
|
||||
elif compression == "tiff_jpeg":
|
||||
|
@ -1515,10 +1518,10 @@ def _save(im, fp, filename):
|
|||
ifd[IMAGELENGTH] = im.size[1]
|
||||
|
||||
# write any arbitrary tags passed in as an ImageFileDirectory
|
||||
if "tiffinfo" in im.encoderinfo:
|
||||
info = im.encoderinfo["tiffinfo"]
|
||||
elif "exif" in im.encoderinfo:
|
||||
info = im.encoderinfo["exif"]
|
||||
if "tiffinfo" in encoderinfo:
|
||||
info = encoderinfo["tiffinfo"]
|
||||
elif "exif" in encoderinfo:
|
||||
info = encoderinfo["exif"]
|
||||
if isinstance(info, bytes):
|
||||
exif = Image.Exif()
|
||||
exif.load(info)
|
||||
|
@ -1556,7 +1559,7 @@ def _save(im, fp, filename):
|
|||
|
||||
# preserve ICC profile (should also work when saving other formats
|
||||
# which support profiles as TIFF) -- 2008-06-06 Florian Hoech
|
||||
icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
|
||||
icc = encoderinfo.get("icc_profile", im.info.get("icc_profile"))
|
||||
if icc:
|
||||
ifd[ICCPROFILE] = icc
|
||||
|
||||
|
@ -1572,10 +1575,10 @@ def _save(im, fp, filename):
|
|||
(ARTIST, "artist"),
|
||||
(COPYRIGHT, "copyright"),
|
||||
]:
|
||||
if name in im.encoderinfo:
|
||||
ifd[key] = im.encoderinfo[name]
|
||||
if name in encoderinfo:
|
||||
ifd[key] = encoderinfo[name]
|
||||
|
||||
dpi = im.encoderinfo.get("dpi")
|
||||
dpi = encoderinfo.get("dpi")
|
||||
if dpi:
|
||||
ifd[RESOLUTION_UNIT] = 2
|
||||
ifd[X_RESOLUTION] = dpi[0]
|
||||
|
@ -1590,7 +1593,18 @@ def _save(im, fp, filename):
|
|||
if format != 1:
|
||||
ifd[SAMPLEFORMAT] = format
|
||||
|
||||
ifd[PHOTOMETRIC_INTERPRETATION] = photo
|
||||
if PHOTOMETRIC_INTERPRETATION not in ifd:
|
||||
ifd[PHOTOMETRIC_INTERPRETATION] = photo
|
||||
elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:
|
||||
if im.mode == "1":
|
||||
inverted_im = im.copy()
|
||||
px = inverted_im.load()
|
||||
for y in range(inverted_im.height):
|
||||
for x in range(inverted_im.width):
|
||||
px[x, y] = 0 if px[x, y] == 255 else 255
|
||||
im = inverted_im
|
||||
else:
|
||||
im = ImageOps.invert(im)
|
||||
|
||||
if im.mode in ["P", "PA"]:
|
||||
lut = im.im.getpalette("RGB", "RGB;L")
|
||||
|
@ -1600,6 +1614,9 @@ def _save(im, fp, filename):
|
|||
# aim for 64 KB strips when using libtiff writer
|
||||
if libtiff:
|
||||
rows_per_strip = min((2 ** 16 + stride - 1) // 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]
|
||||
strip_byte_counts = stride * rows_per_strip
|
||||
|
@ -1616,9 +1633,16 @@ def _save(im, fp, filename):
|
|||
# no compression by default:
|
||||
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
|
||||
|
||||
if im.mode == "YCbCr":
|
||||
for tag, value in {
|
||||
YCBCRSUBSAMPLING: (1, 1),
|
||||
REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
|
||||
}.items():
|
||||
ifd.setdefault(tag, value)
|
||||
|
||||
if libtiff:
|
||||
if "quality" in im.encoderinfo:
|
||||
quality = im.encoderinfo["quality"]
|
||||
if "quality" in encoderinfo:
|
||||
quality = encoderinfo["quality"]
|
||||
if not isinstance(quality, int) or quality < 0 or quality > 100:
|
||||
raise ValueError("Invalid quality setting")
|
||||
if compression != "jpeg":
|
||||
|
@ -1707,7 +1731,7 @@ def _save(im, fp, filename):
|
|||
tags = list(atts.items())
|
||||
tags.sort()
|
||||
a = (rawmode, compression, _fp, filename, tags, types)
|
||||
e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig)
|
||||
e = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
|
||||
e.setimage(im.im, (0, 0) + im.size)
|
||||
while True:
|
||||
# undone, change to self.decodermaxblock:
|
||||
|
@ -1727,7 +1751,7 @@ def _save(im, fp, filename):
|
|||
)
|
||||
|
||||
# -- helper for multi-page save --
|
||||
if "_debug_multipage" in im.encoderinfo:
|
||||
if "_debug_multipage" in encoderinfo:
|
||||
# just to access o32 and o16 (using correct byte order)
|
||||
im._debug_multipage = ifd
|
||||
|
||||
|
|
|
@ -23,12 +23,44 @@ and has been tested with a few sample files found using google.
|
|||
To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead.
|
||||
"""
|
||||
|
||||
import builtins
|
||||
|
||||
from . import Image
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i32le as i32
|
||||
|
||||
|
||||
class WalImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "WAL"
|
||||
format_description = "Quake2 Texture"
|
||||
|
||||
def _open(self):
|
||||
self.mode = "P"
|
||||
|
||||
# read header fields
|
||||
header = self.fp.read(32 + 24 + 32 + 12)
|
||||
self._size = i32(header, 32), i32(header, 36)
|
||||
Image._decompression_bomb_check(self.size)
|
||||
|
||||
# load pixel data
|
||||
offset = i32(header, 40)
|
||||
self.fp.seek(offset)
|
||||
|
||||
# strings are null-terminated
|
||||
self.info["name"] = header[:32].split(b"\0", 1)[0]
|
||||
next_name = header[56 : 56 + 32].split(b"\0", 1)[0]
|
||||
if next_name:
|
||||
self.info["next_name"] = next_name
|
||||
|
||||
def load(self):
|
||||
if self.im:
|
||||
# Already loaded
|
||||
return
|
||||
|
||||
self.im = Image.core.new(self.mode, self.size)
|
||||
self.frombytes(self.fp.read(self.size[0] * self.size[1]))
|
||||
self.putpalette(quake2palette)
|
||||
Image.Image.load(self)
|
||||
|
||||
|
||||
def open(filename):
|
||||
"""
|
||||
Load texture from a Quake2 WAL texture file.
|
||||
|
@ -39,38 +71,7 @@ def open(filename):
|
|||
:param filename: WAL file name, or an opened file handle.
|
||||
:returns: An image instance.
|
||||
"""
|
||||
# FIXME: modify to return a WalImageFile instance instead of
|
||||
# plain Image object ?
|
||||
|
||||
def imopen(fp):
|
||||
# read header fields
|
||||
header = fp.read(32 + 24 + 32 + 12)
|
||||
size = i32(header, 32), i32(header, 36)
|
||||
offset = i32(header, 40)
|
||||
|
||||
# load pixel data
|
||||
fp.seek(offset)
|
||||
|
||||
Image._decompression_bomb_check(size)
|
||||
im = Image.frombytes("P", size, fp.read(size[0] * size[1]))
|
||||
im.putpalette(quake2palette)
|
||||
|
||||
im.format = "WAL"
|
||||
im.format_description = "Quake2 Texture"
|
||||
|
||||
# strings are null-terminated
|
||||
im.info["name"] = header[:32].split(b"\0", 1)[0]
|
||||
next_name = header[56 : 56 + 32].split(b"\0", 1)[0]
|
||||
if next_name:
|
||||
im.info["next_name"] = next_name
|
||||
|
||||
return im
|
||||
|
||||
if hasattr(filename, "read"):
|
||||
return imopen(filename)
|
||||
else:
|
||||
with builtins.open(filename, "rb") as fp:
|
||||
return imopen(fp)
|
||||
return WalImageFile(filename)
|
||||
|
||||
|
||||
quake2palette = (
|
||||
|
|
|
@ -202,7 +202,7 @@ def _save_all(im, fp, filename):
|
|||
lossless = im.encoderinfo.get("lossless", False)
|
||||
quality = im.encoderinfo.get("quality", 80)
|
||||
method = im.encoderinfo.get("method", 0)
|
||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||
icc_profile = im.encoderinfo.get("icc_profile") or ""
|
||||
exif = im.encoderinfo.get("exif", "")
|
||||
if isinstance(exif, Image.Exif):
|
||||
exif = exif.tobytes()
|
||||
|
@ -309,7 +309,7 @@ def _save_all(im, fp, filename):
|
|||
def _save(im, fp, filename):
|
||||
lossless = im.encoderinfo.get("lossless", False)
|
||||
quality = im.encoderinfo.get("quality", 80)
|
||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||
icc_profile = im.encoderinfo.get("icc_profile") or ""
|
||||
exif = im.encoderinfo.get("exif", "")
|
||||
if isinstance(exif, Image.Exif):
|
||||
exif = exif.tobytes()
|
||||
|
|
|
@ -47,6 +47,16 @@ def si16le(c, o=0):
|
|||
return unpack_from("<h", c, o)[0]
|
||||
|
||||
|
||||
def si16be(c, o=0):
|
||||
"""
|
||||
Converts a 2-bytes (16 bits) string to a signed integer, big endian.
|
||||
|
||||
:param c: string containing bytes to convert
|
||||
:param o: offset of bytes to convert in string
|
||||
"""
|
||||
return unpack_from(">h", c, o)[0]
|
||||
|
||||
|
||||
def i32le(c, o=0):
|
||||
"""
|
||||
Converts a 4-bytes (32 bits) string to an unsigned integer.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* Small excerpts from the Tcl / Tk 8.6 headers
|
||||
*
|
||||
* License terms copied from:
|
||||
* http://www.tcl.tk/software/tcltk/license.html
|
||||
* https://www.tcl.tk/software/tcltk/license.html
|
||||
* as of 20 May 2016.
|
||||
*
|
||||
* Copyright (c) 1987-1994 The Regents of the University of California.
|
||||
|
|
|
@ -808,6 +808,12 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
|
|||
av + stride * 2);
|
||||
free(av);
|
||||
}
|
||||
} else if (key_int == TIFFTAG_YCBCRSUBSAMPLING) {
|
||||
status = ImagingLibTiffSetField(
|
||||
&encoder->state,
|
||||
(ttag_t)key_int,
|
||||
(UINT16)PyLong_AsLong(PyTuple_GetItem(value, 0)),
|
||||
(UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1)));
|
||||
} else if (type == TIFF_SHORT) {
|
||||
UINT16 *av;
|
||||
/* malloc check ok, calloc checks for overflow */
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
#include "Imaging.h"
|
||||
|
||||
/* use Tests/make_hash.py to calculate these values */
|
||||
/* use make_hash.py from the pillow-scripts repository to calculate these values */
|
||||
#define ACCESS_TABLE_SIZE 27
|
||||
#define ACCESS_TABLE_HASH 3078
|
||||
|
||||
|
|
|
@ -46,7 +46,8 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
|
|||
ptr = buf;
|
||||
|
||||
framesize = I32(ptr);
|
||||
if (framesize < I32(ptr)) {
|
||||
// there can be one pad byte in the framesize
|
||||
if (bytes + (bytes % 2) < framesize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -223,8 +224,15 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
|
|||
break;
|
||||
case 16:
|
||||
/* COPY chunk */
|
||||
if (state->xsize > bytes / state->ysize) {
|
||||
if (INT32_MAX / state->xsize < state->ysize) {
|
||||
/* Integer overflow, bail */
|
||||
state->errcode = IMAGING_CODEC_OVERRUN;
|
||||
return -1;
|
||||
}
|
||||
/* Note, have to check Data + size, not just ptr + size) */
|
||||
if (data + (state->xsize * state->ysize) > ptr + bytes) {
|
||||
/* not enough data for frame */
|
||||
/* UNDONE Unclear that we're actually going to leave the buffer at the right place. */
|
||||
return ptr - buf; /* bytes consumed */
|
||||
}
|
||||
for (y = 0; y < state->ysize; y++) {
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
/* This is to work around a bug in GCC prior 4.9 in 64 bit mode.
|
||||
GCC generates code with partial dependency which is 3 times slower.
|
||||
See: http://stackoverflow.com/a/26588074/253146 */
|
||||
See: https://stackoverflow.com/a/26588074/253146 */
|
||||
#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \
|
||||
!defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900)
|
||||
static float __attribute__((always_inline)) inline _i2f(int v) {
|
||||
|
|
|
@ -417,9 +417,16 @@ fill_mask_L(
|
|||
if (imOut->image8) {
|
||||
for (y = 0; y < ysize; y++) {
|
||||
UINT8 *out = imOut->image8[y + dy] + dx;
|
||||
if (strncmp(imOut->mode, "I;16", 4) == 0) {
|
||||
out += dx;
|
||||
}
|
||||
UINT8 *mask = imMask->image8[y + sy] + sx;
|
||||
for (x = 0; x < xsize; x++) {
|
||||
*out = BLEND(*mask, *out, ink[0], tmp1);
|
||||
if (strncmp(imOut->mode, "I;16", 4) == 0) {
|
||||
out++;
|
||||
*out = BLEND(*mask, *out, ink[0], tmp1);
|
||||
}
|
||||
out++, mask++;
|
||||
}
|
||||
}
|
||||
|
|
4
src/thirdparty/fribidi-shim/fribidi.c
vendored
|
@ -12,7 +12,7 @@
|
|||
|
||||
|
||||
/* FriBiDi>=1.0.0 adds bracket_types param, ignore and call legacy function */
|
||||
FriBidiLevel fribidi_get_par_embedding_levels_ex_compat(
|
||||
static FriBidiLevel fribidi_get_par_embedding_levels_ex_compat(
|
||||
const FriBidiCharType *bidi_types,
|
||||
const FriBidiBracketType *bracket_types,
|
||||
const FriBidiStrIndex len,
|
||||
|
@ -24,7 +24,7 @@ FriBidiLevel fribidi_get_par_embedding_levels_ex_compat(
|
|||
}
|
||||
|
||||
/* FriBiDi>=1.0.0 gets bracket types here, ignore */
|
||||
void fribidi_get_bracket_types_compat(
|
||||
static void fribidi_get_bracket_types_compat(
|
||||
const FriBidiChar *str,
|
||||
const FriBidiStrIndex len,
|
||||
const FriBidiCharType *types,
|
||||
|
|
4
src/thirdparty/fribidi-shim/fribidi.h
vendored
|
@ -63,8 +63,12 @@ typedef uint32_t FriBidiParType;
|
|||
/* functions */
|
||||
|
||||
#ifdef FRIBIDI_SHIM_IMPLEMENTATION
|
||||
#ifdef _MSC_VER
|
||||
#define FRIBIDI_ENTRY
|
||||
#else
|
||||
#define FRIBIDI_ENTRY __attribute__((visibility ("hidden")))
|
||||
#endif
|
||||
#else
|
||||
#define FRIBIDI_ENTRY extern
|
||||
#endif
|
||||
|
||||
|
|
2
src/thirdparty/raqm/raqm.c
vendored
|
@ -491,7 +491,7 @@ raqm_set_text_utf8 (raqm_t *rq,
|
|||
*
|
||||
* The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph
|
||||
* direction based on the first character with strong bidi type (see [rule
|
||||
* P2](http://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm),
|
||||
* P2](https://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm),
|
||||
* which can be good enough for many cases but has problems when a mainly
|
||||
* right-to-left paragraph starts with a left-to-right character and vice versa
|
||||
* as the detected paragraph direction will be the wrong one, or when text does
|
||||
|
|
|
@ -105,9 +105,9 @@ header = [
|
|||
# dependencies, listed in order of compilation
|
||||
deps = {
|
||||
"libjpeg": {
|
||||
"url": SF_MIRROR + "/project/libjpeg-turbo/2.1.0/libjpeg-turbo-2.1.0.tar.gz",
|
||||
"filename": "libjpeg-turbo-2.1.0.tar.gz",
|
||||
"dir": "libjpeg-turbo-2.1.0",
|
||||
"url": SF_MIRROR + "/project/libjpeg-turbo/2.1.1/libjpeg-turbo-2.1.1.tar.gz",
|
||||
"filename": "libjpeg-turbo-2.1.1.tar.gz",
|
||||
"dir": "libjpeg-turbo-2.1.1",
|
||||
"build": [
|
||||
cmd_cmake(
|
||||
[
|
||||
|
@ -154,9 +154,9 @@ deps = {
|
|||
# "bins": [r"libtiff\*.dll"],
|
||||
},
|
||||
"libwebp": {
|
||||
"url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.0.tar.gz",
|
||||
"filename": "libwebp-1.2.0.tar.gz",
|
||||
"dir": "libwebp-1.2.0",
|
||||
"url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.1.tar.gz",
|
||||
"filename": "libwebp-1.2.1.tar.gz",
|
||||
"dir": "libwebp-1.2.1",
|
||||
"build": [
|
||||
cmd_rmdir(r"output\release-static"), # clean
|
||||
cmd_nmake(
|
||||
|
@ -277,9 +277,9 @@ deps = {
|
|||
"libs": [r"*.lib"],
|
||||
},
|
||||
"harfbuzz": {
|
||||
"url": "https://github.com/harfbuzz/harfbuzz/archive/2.8.1.zip",
|
||||
"filename": "harfbuzz-2.8.1.zip",
|
||||
"dir": "harfbuzz-2.8.1",
|
||||
"url": "https://github.com/harfbuzz/harfbuzz/archive/2.9.0.zip",
|
||||
"filename": "harfbuzz-2.9.0.zip",
|
||||
"dir": "harfbuzz-2.9.0",
|
||||
"build": [
|
||||
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
|
||||
cmd_nmake(target="clean"),
|
||||
|
|