Merge branch 'main' into add-cygwin-to-ci
|
@ -25,8 +25,8 @@ install:
|
|||
- mv c:\pillow-depends-main c:\pillow-depends
|
||||
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
|
||||
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
|
||||
- ..\pillow-depends\gs9550w32.exe /S
|
||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.55.0\bin;%PATH%
|
||||
- ..\pillow-depends\gs9561w32.exe /S
|
||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||
|
@ -43,7 +43,7 @@ build_script:
|
|||
|
||||
test_script:
|
||||
- cd c:\pillow
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
|
||||
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
|
||||
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
|
||||
|
|
4
.github/workflows/cifuzz.yml
vendored
|
@ -31,13 +31,13 @@ jobs:
|
|||
language: python
|
||||
dry-run: false
|
||||
- name: Upload New Crash
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
path: ./out/artifacts
|
||||
- name: Upload Legacy Crash
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: steps.run.outcome == 'success'
|
||||
with:
|
||||
name: crash
|
||||
|
|
4
.github/workflows/lint.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
name: Lint
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: pre-commit cache
|
||||
uses: actions/cache@v2
|
||||
|
@ -21,7 +21,7 @@ jobs:
|
|||
lint-pre-commit-
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
cache: pip
|
||||
|
|
27
.github/workflows/stale.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: Close stale issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "10 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository_owner == 'python-pillow'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: "Check issues"
|
||||
uses: actions/stale@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "Awaiting OP Action"
|
||||
close-issue-message: "Closing this issue as no feedback has been received."
|
||||
days-before-stale: 7
|
||||
days-before-issue-close: 0
|
||||
days-before-pr-close: -1
|
||||
labels-to-remove-when-unstale: "Awaiting OP Action"
|
3
.github/workflows/test-docker.yml
vendored
|
@ -23,7 +23,6 @@ jobs:
|
|||
centos-stream-9-amd64,
|
||||
debian-10-buster-x86,
|
||||
debian-11-bullseye-x86,
|
||||
fedora-34-amd64,
|
||||
fedora-35-amd64,
|
||||
gentoo,
|
||||
ubuntu-18.04-bionic-amd64,
|
||||
|
@ -41,7 +40,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
|
2
.github/workflows/test-mingw.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up shell
|
||||
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
|
||||
|
|
2
.github/workflows/test-valgrind.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
|
14
.github/workflows/test-windows.yml
vendored
|
@ -23,17 +23,17 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Checkout cached dependencies
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: python-pillow/pillow-depends
|
||||
path: winbuild\depends
|
||||
|
||||
# sets env: pythonLocation
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: ${{ matrix.architecture }}
|
||||
|
@ -52,8 +52,8 @@ jobs:
|
|||
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
||||
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
|
||||
|
||||
winbuild\depends\gs9550w32.exe /S
|
||||
echo "C:\Program Files (x86)\gs\gs9.55.0\bin" >> $env:GITHUB_PATH
|
||||
winbuild\depends\gs9561w32.exe /S
|
||||
echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
|
||||
|
||||
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
|
||||
|
||||
|
@ -156,7 +156,7 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
- name: Upload errors
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: errors
|
||||
|
@ -182,7 +182,7 @@ jobs:
|
|||
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
|
||||
shell: cmd
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: "github.event_name != 'pull_request'"
|
||||
with:
|
||||
name: ${{ steps.wheel.outputs.dist }}
|
||||
|
|
8
.github/workflows/test.yml
vendored
|
@ -36,10 +36,10 @@ jobs:
|
|||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
|
@ -84,7 +84,7 @@ jobs:
|
|||
mkdir -p Tests/errors
|
||||
|
||||
- name: Upload errors
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: errors
|
||||
|
@ -93,7 +93,7 @@ jobs:
|
|||
- name: Docs
|
||||
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
|
||||
run: |
|
||||
python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph
|
||||
python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph
|
||||
make doccheck
|
||||
|
||||
- name: After success
|
||||
|
|
2
.github/workflows/tidelift.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Scan
|
||||
uses: tidelift/alignment-action@main
|
||||
env:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: f1d4e742c91dd5179d742b0db9293c4472b765f8 # frozen: 21.12b0
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ["--target-version", "py37"]
|
||||
|
@ -9,35 +9,35 @@ repos:
|
|||
types: []
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: c5e8fa75dda5f764d20f66a215d71c21cfa198e1 # frozen: 5.10.1
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/asottile/yesqa
|
||||
rev: 35cf7dc24fa922927caded7a21b2a8cb04bf8e10 # frozen: v1.3.0
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: yesqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10
|
||||
rev: v1.1.13
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d # frozen: 4.0.1
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0
|
||||
rev: v1.9.0
|
||||
hooks:
|
||||
- id: python-check-blanket-noqa
|
||||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: 8fe62d14e0b4d7d845a7022c5c2c3ae41bdd3f26 # frozen: v4.1.0
|
||||
rev: v4.1.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
|
|
83
CHANGES.rst
|
@ -2,9 +2,90 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
9.1.0 (unreleased)
|
||||
9.2.0 (unreleased)
|
||||
------------------
|
||||
|
||||
- Round lut values where necessary #6188
|
||||
[radarhere]
|
||||
|
||||
- Load before getting size in resize() #6190
|
||||
[radarhere]
|
||||
|
||||
- Load image before performing size calculations in thumbnail() #6186
|
||||
[radarhere]
|
||||
|
||||
- Deprecated PhotoImage.paste() box parameter #6178
|
||||
[radarhere]
|
||||
|
||||
9.1.0 (2022-04-01)
|
||||
------------------
|
||||
|
||||
- Add support for multiple component transformation to JPEG2000 #5500
|
||||
[scaramallion, radarhere, hugovk]
|
||||
|
||||
- Fix loading FriBiDi on Alpine #6165
|
||||
[nulano]
|
||||
|
||||
- Added setting for converting GIF P frames to RGB #6150
|
||||
[radarhere]
|
||||
|
||||
- Allow 1 mode images to be inverted #6034
|
||||
[radarhere]
|
||||
|
||||
- Raise ValueError when trying to save empty JPEG #6159
|
||||
[radarhere]
|
||||
|
||||
- Always save TIFF with contiguous planar configuration #5973
|
||||
[radarhere]
|
||||
|
||||
- Connected discontiguous polygon corners #5980
|
||||
[radarhere]
|
||||
|
||||
- Ensure Tkinter hook is activated for getimage() #6032
|
||||
[radarhere]
|
||||
|
||||
- Use screencapture arguments to crop on macOS #6152
|
||||
[radarhere]
|
||||
|
||||
- Do not mark L mode JPEG as 1 bit in PDF #6151
|
||||
[radarhere]
|
||||
|
||||
- Added support for reading I;16R TIFF images #6132
|
||||
[radarhere]
|
||||
|
||||
- If an error occurs after creating a file, remove the file #6134
|
||||
[radarhere]
|
||||
|
||||
- Fixed calling DisplayViewer or XVViewer without a title #6136
|
||||
[radarhere]
|
||||
|
||||
- Retain RGBA transparency when saving multiple GIF frames #6128
|
||||
[radarhere]
|
||||
|
||||
- Save additional ICO frames with other bit depths if supplied #6122
|
||||
[radarhere]
|
||||
|
||||
- Handle EXIF data truncated to just the header #6124
|
||||
[radarhere]
|
||||
|
||||
- Added support for reading BMP images with RLE8 compression #6102
|
||||
[radarhere]
|
||||
|
||||
- Support Python distributions where _tkinter is compiled in #6006
|
||||
[lukegb]
|
||||
|
||||
- Added support for PPM arbitrary maxval #6119
|
||||
[radarhere]
|
||||
|
||||
- Added BigTIFF reading #6097
|
||||
[radarhere]
|
||||
|
||||
- When converting, clip I;16 to be unsigned, not signed #6112
|
||||
[radarhere]
|
||||
|
||||
- Fixed loading L mode GIF with transparency #6086
|
||||
[radarhere]
|
||||
|
||||
- Improved handling of PPM header #5121
|
||||
[Piolie, radarhere]
|
||||
|
||||
|
|
2
Makefile
|
@ -77,7 +77,7 @@ release-test:
|
|||
-rm dist/*.egg
|
||||
-rmdir dist
|
||||
python3 -m pytest -qq
|
||||
python3 -m check-manifest
|
||||
python3 -m check_manifest
|
||||
python3 -m pyroma .
|
||||
$(MAKE) readme
|
||||
|
||||
|
|
14
RELEASING.md
|
@ -24,13 +24,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
twine check dist/*
|
||||
python3 -m twine check --strict dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||
```bash
|
||||
twine check dist/*
|
||||
twine upload dist/Pillow-5.2.0*
|
||||
python3 -m twine check --strict dist/*
|
||||
python3 -m twine upload dist/Pillow-5.2.0*
|
||||
```
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py`
|
||||
|
@ -61,13 +61,13 @@ Released as needed for security, installation or critical bug fixes.
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
twine check dist/*
|
||||
python3 -m twine check --strict dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||
```bash
|
||||
twine check dist/*
|
||||
twine upload dist/Pillow-5.2.1*
|
||||
python3 -m twine check --strict dist/*
|
||||
python3 -m twine upload dist/Pillow-5.2.1*
|
||||
```
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
|
||||
|
@ -91,7 +91,7 @@ Released as needed privately to individual vendors for critical security-related
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
twine check dist/*
|
||||
python3 -m twine check --strict dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
|
|
BIN
Tests/images/16bit.r.tif
Normal file
BIN
Tests/images/hopper_bigtiff.tif
Normal file
BIN
Tests/images/hopper_rle8.bmp
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
Tests/images/hopper_rle8_row_overflow.bmp
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
Tests/images/imagedraw/discontiguous_corners_polygon.png
Normal file
After Width: | Height: | Size: 486 B |
BIN
Tests/images/no_palette.gif
Normal file
After Width: | Height: | Size: 48 B |
BIN
Tests/images/no_palette_with_background.gif
Normal file
After Width: | Height: | Size: 54 B |
BIN
Tests/images/no_palette_with_transparency.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
|
@ -40,6 +40,7 @@ def test_questionable():
|
|||
"rgb32fakealpha.bmp",
|
||||
"rgb24largepal.bmp",
|
||||
"pal8os2sp.bmp",
|
||||
"pal8rletrns.bmp",
|
||||
"rgb32bf-xbgr.bmp",
|
||||
]
|
||||
for f in get_files("q"):
|
||||
|
|
|
@ -4,7 +4,12 @@ import pytest
|
|||
|
||||
from PIL import BmpImagePlugin, Image
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
assert_image_equal_tofile,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
)
|
||||
|
||||
|
||||
def test_sanity(tmp_path):
|
||||
|
@ -125,6 +130,42 @@ def test_rgba_bitfields():
|
|||
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
|
||||
|
||||
|
||||
def test_rle8():
|
||||
with Image.open("Tests/images/hopper_rle8.bmp") as im:
|
||||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||
|
||||
# This test image has been manually hexedited
|
||||
# to have rows with too much data
|
||||
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
|
||||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||
|
||||
# Signal end of bitmap before the image is finished
|
||||
with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
|
||||
data = fp.read(1063) + b"\x01"
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file_name,length",
|
||||
(
|
||||
# EOF immediately after the header
|
||||
("Tests/images/hopper_rle8.bmp", 1078),
|
||||
# EOF during delta
|
||||
("Tests/images/bmp/q/pal8rletrns.bmp", 3670),
|
||||
# EOF when reading data in absolute mode
|
||||
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
||||
),
|
||||
)
|
||||
def test_rle8_eof(file_name, length):
|
||||
with open(file_name, "rb") as fp:
|
||||
data = fp.read(length)
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_offset():
|
||||
# This image has been hexedited
|
||||
# to exclude the palette size from the pixel data offset
|
||||
|
|
|
@ -196,6 +196,13 @@ def test__accept_false():
|
|||
assert not output
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
with pytest.raises(SyntaxError):
|
||||
DdsImagePlugin.DdsImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_short_header():
|
||||
"""Check a short header"""
|
||||
with open(TEST_FILE_DXT5, "rb") as f:
|
||||
|
|
|
@ -16,6 +16,13 @@ def test_load_dxt1():
|
|||
assert_image_similar(im, target.convert("RGBA"), 15)
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
with pytest.raises(SyntaxError):
|
||||
FtexImagePlugin.FtexImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_constants_deprecation():
|
||||
for enum, prefix in {
|
||||
FtexImagePlugin.Format: "FORMAT_",
|
||||
|
|
|
@ -59,6 +59,51 @@ def test_invalid_file():
|
|||
GifImagePlugin.GifImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_l_mode_transparency():
|
||||
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
|
||||
assert im.mode == "L"
|
||||
assert im.load()[0, 0] == 128
|
||||
assert im.info["transparency"] == 255
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "L"
|
||||
assert im.load()[0, 0] == 128
|
||||
|
||||
|
||||
def test_strategy():
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
expected_zero = im.convert("RGB")
|
||||
|
||||
im.seek(1)
|
||||
expected_one = im.convert("RGB")
|
||||
|
||||
try:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "RGB"
|
||||
assert_image_equal(im, expected_zero)
|
||||
|
||||
GifImagePlugin.LOADING_STRATEGY = (
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||
)
|
||||
# Stay in P mode with only a global palette
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "P"
|
||||
assert_image_equal(im.convert("RGB"), expected_one)
|
||||
|
||||
# Change to RGB mode when a frame has an individual palette
|
||||
with Image.open("Tests/images/iss634.gif") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "RGB"
|
||||
finally:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
|
||||
|
||||
def test_optimize():
|
||||
def test_grayscale(optimize):
|
||||
im = Image.new("L", (1, 1), 0)
|
||||
|
@ -383,18 +428,38 @@ def test_dispose_background_transparency():
|
|||
assert px[35, 30][3] == 0
|
||||
|
||||
|
||||
def test_transparent_dispose():
|
||||
expected_colors = [
|
||||
@pytest.mark.parametrize(
|
||||
"loading_strategy, expected_colors",
|
||||
(
|
||||
(
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
|
||||
(
|
||||
(2, 1, 2),
|
||||
((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
|
||||
((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
|
||||
(
|
||||
(2, 1, 2),
|
||||
(0, 1, 0),
|
||||
(2, 1, 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_transparent_dispose(loading_strategy, expected_colors):
|
||||
GifImagePlugin.LOADING_STRATEGY = loading_strategy
|
||||
try:
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||
for frame in range(3):
|
||||
img.seek(frame)
|
||||
for x in range(3):
|
||||
color = img.getpixel((x, 0))
|
||||
assert color == expected_colors[frame][x]
|
||||
finally:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
|
||||
|
||||
def test_dispose_previous():
|
||||
|
@ -831,6 +896,17 @@ def test_rgb_transparency(tmp_path):
|
|||
assert "transparency" not in reloaded.info
|
||||
|
||||
|
||||
def test_rgba_transparency(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
im = hopper("P")
|
||||
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
reloaded.seek(1)
|
||||
assert_image_equal(hopper("P").convert("RGB"), reloaded)
|
||||
|
||||
|
||||
def test_bbox(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
|
@ -960,6 +1036,11 @@ def test_lzw_bits():
|
|||
def test_extents():
|
||||
with Image.open("Tests/images/test_extents.gif") as im:
|
||||
assert im.size == (100, 100)
|
||||
|
||||
# Check that n_frames does not change the size
|
||||
assert im.n_frames == 2
|
||||
assert im.size == (100, 100)
|
||||
|
||||
im.seek(1)
|
||||
assert im.size == (150, 150)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import io
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -70,6 +71,53 @@ def test_save_to_bytes():
|
|||
)
|
||||
|
||||
|
||||
def test_no_duplicates(tmp_path):
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
temp_file2 = str(tmp_path / "temp2.ico")
|
||||
|
||||
im = hopper()
|
||||
sizes = [(32, 32), (64, 64)]
|
||||
im.save(temp_file, "ico", sizes=sizes)
|
||||
|
||||
sizes.append(sizes[-1])
|
||||
im.save(temp_file2, "ico", sizes=sizes)
|
||||
|
||||
assert os.path.getsize(temp_file) == os.path.getsize(temp_file2)
|
||||
|
||||
|
||||
def test_different_bit_depths(tmp_path):
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
temp_file2 = str(tmp_path / "temp2.ico")
|
||||
|
||||
im = hopper()
|
||||
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
|
||||
|
||||
hopper("1").save(
|
||||
temp_file2,
|
||||
"ico",
|
||||
bitmap_format="bmp",
|
||||
sizes=[(128, 128)],
|
||||
append_images=[im],
|
||||
)
|
||||
|
||||
assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
|
||||
|
||||
# Test that only matching sizes of different bit depths are saved
|
||||
temp_file3 = str(tmp_path / "temp3.ico")
|
||||
temp_file4 = str(tmp_path / "temp4.ico")
|
||||
|
||||
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
|
||||
im.save(
|
||||
temp_file4,
|
||||
"ico",
|
||||
bitmap_format="bmp",
|
||||
sizes=[(128, 128)],
|
||||
append_images=[Image.new("P", (64, 64))],
|
||||
)
|
||||
|
||||
assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
|
||||
def test_save_to_bytes_bmp(mode):
|
||||
output = io.BytesIO()
|
||||
|
|
|
@ -68,6 +68,13 @@ class TestFileJpeg:
|
|||
assert im.format == "JPEG"
|
||||
assert im.get_format_mimetype() == "image/jpeg"
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero(self, size, tmp_path):
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
im = Image.new("RGB", size)
|
||||
with pytest.raises(ValueError):
|
||||
im.save(f)
|
||||
|
||||
def test_app(self):
|
||||
# Test APP/COM reader (@PIL135)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
|
|
|
@ -209,6 +209,49 @@ def test_layers():
|
|||
assert_image_similar(im, test_card, 0.4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name, args, offset, data",
|
||||
(
|
||||
("foo.j2k", {}, 0, b"\xff\x4f"),
|
||||
("foo.jp2", {}, 4, b"jP"),
|
||||
(None, {"no_jp2": True}, 0, b"\xff\x4f"),
|
||||
("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"),
|
||||
("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"),
|
||||
("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"),
|
||||
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
|
||||
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
|
||||
),
|
||||
)
|
||||
def test_no_jp2(name, args, offset, data):
|
||||
out = BytesIO()
|
||||
if name:
|
||||
out.name = name
|
||||
test_card.save(out, "JPEG2000", **args)
|
||||
out.seek(offset)
|
||||
assert out.read(2) == data
|
||||
|
||||
|
||||
def test_mct():
|
||||
# Three component
|
||||
for val in (0, 1):
|
||||
out = BytesIO()
|
||||
test_card.save(out, "JPEG2000", mct=val, no_jp2=True)
|
||||
|
||||
assert out.getvalue()[59] == val
|
||||
with Image.open(out) as im:
|
||||
assert_image_similar(im, test_card, 1.0e-3)
|
||||
|
||||
# Single component should have MCT disabled
|
||||
for val in (0, 1):
|
||||
out = BytesIO()
|
||||
with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
|
||||
jp2.save(out, "JPEG2000", mct=val, no_jp2=True)
|
||||
|
||||
assert out.getvalue()[53] == 0
|
||||
with Image.open(out) as im:
|
||||
assert_image_similar(im, jp2, 1.0e-3)
|
||||
|
||||
|
||||
def test_rgba():
|
||||
# Arrange
|
||||
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
|
||||
|
|
|
@ -4,7 +4,6 @@ import itertools
|
|||
import os
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from ctypes import c_float
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -168,14 +167,11 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
val = original[tag]
|
||||
if tag.endswith("Resolution"):
|
||||
if legacy_api:
|
||||
assert (
|
||||
c_float(val[0][0] / val[0][1]).value
|
||||
== c_float(value[0][0] / value[0][1]).value
|
||||
assert val[0][0] / val[0][1] == (
|
||||
4294967295 / 113653537
|
||||
), f"{tag} didn't roundtrip"
|
||||
else:
|
||||
assert (
|
||||
c_float(val).value == c_float(value).value
|
||||
), f"{tag} didn't roundtrip"
|
||||
assert val == 37.79000115940079, f"{tag} didn't roundtrip"
|
||||
else:
|
||||
assert val == value, f"{tag} didn't roundtrip"
|
||||
|
||||
|
|
|
@ -13,16 +13,53 @@ TEST_FILE = "Tests/images/hopper.ppm"
|
|||
|
||||
def test_sanity():
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.load()
|
||||
assert im.mode == "RGB"
|
||||
assert im.size == (128, 128)
|
||||
assert im.format, "PPM"
|
||||
assert im.format == "PPM"
|
||||
assert im.get_format_mimetype() == "image/x-portable-pixmap"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data, mode, pixels",
|
||||
(
|
||||
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
|
||||
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
|
||||
# P6 with maxval < 255
|
||||
(
|
||||
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11",
|
||||
"RGB",
|
||||
(
|
||||
(0, 15, 30),
|
||||
(120, 135, 150),
|
||||
(225, 240, 255),
|
||||
),
|
||||
),
|
||||
# P6 with maxval > 255
|
||||
# Scale down to 255, since there is no RGB mode with more than 8-bit
|
||||
(
|
||||
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
|
||||
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
|
||||
"RGB",
|
||||
(
|
||||
(0, 1, 2),
|
||||
(127, 128, 129),
|
||||
(254, 255, 255),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_arbitrary_maxval(data, mode, pixels):
|
||||
fp = BytesIO(data)
|
||||
with Image.open(fp) as im:
|
||||
assert im.size == (3, 1)
|
||||
assert im.mode == mode
|
||||
|
||||
px = im.load()
|
||||
assert tuple(px[x, 0] for x in range(3)) == pixels
|
||||
|
||||
|
||||
def test_16bit_pgm():
|
||||
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
||||
im.load()
|
||||
assert im.mode == "I"
|
||||
assert im.size == (20, 100)
|
||||
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
||||
|
@ -32,8 +69,6 @@ def test_16bit_pgm():
|
|||
|
||||
def test_16bit_pgm_write(tmp_path):
|
||||
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
||||
im.load()
|
||||
|
||||
f = str(tmp_path / "temp.pgm")
|
||||
im.save(f, "PPM")
|
||||
|
||||
|
@ -91,19 +126,8 @@ def test_token_too_long(tmp_path):
|
|||
assert str(e.value) == "Token too long in file header: b'01234567890'"
|
||||
|
||||
|
||||
def test_too_many_colors(tmp_path):
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P6\n1 1\n1000\n")
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
with Image.open(path):
|
||||
pass
|
||||
|
||||
assert str(e.value) == "Too many colors for band: 1000"
|
||||
|
||||
|
||||
def test_truncated_file(tmp_path):
|
||||
# Test EOF in header
|
||||
path = str(tmp_path / "temp.pgm")
|
||||
with open(path, "w") as f:
|
||||
f.write("P6")
|
||||
|
@ -114,6 +138,12 @@ def test_truncated_file(tmp_path):
|
|||
|
||||
assert str(e.value) == "Reached EOF while reading header"
|
||||
|
||||
# Test EOF for PyDecoder
|
||||
fp = BytesIO(b"P5 3 1 4")
|
||||
with Image.open(fp) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_neg_ppm():
|
||||
# Storage.c accepted negative values for xsize, ysize. the
|
||||
|
|
|
@ -87,6 +87,10 @@ class TestFileTiff:
|
|||
|
||||
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
|
||||
|
||||
def test_bigtiff(self):
|
||||
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file_name,mode,size,offset",
|
||||
[
|
||||
|
@ -221,6 +225,15 @@ class TestFileTiff:
|
|||
assert b[0] == ord(b"\x01")
|
||||
assert b[1] == ord(b"\xe0")
|
||||
|
||||
def test_16bit_r(self):
|
||||
with Image.open("Tests/images/16bit.r.tif") as im:
|
||||
assert im.getpixel((0, 0)) == 480
|
||||
assert im.mode == "I;16"
|
||||
|
||||
b = im.tobytes()
|
||||
assert b[0] == ord(b"\xe0")
|
||||
assert b[1] == ord(b"\x01")
|
||||
|
||||
def test_16bit_s(self):
|
||||
with Image.open("Tests/images/16bit.s.tif") as im:
|
||||
im.load()
|
||||
|
@ -598,6 +611,17 @@ class TestFileTiff:
|
|||
with Image.open(infile) as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
||||
|
||||
def test_planar_configuration_save(self, tmp_path):
|
||||
infile = "Tests/images/tiff_tiled_planar_raw.tif"
|
||||
with Image.open(infile) as im:
|
||||
assert im._planar_configuration == 2
|
||||
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile)
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert_image_equal_tofile(reloaded, infile)
|
||||
|
||||
def test_palette(self, tmp_path):
|
||||
def roundtrip(mode):
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
|
|
|
@ -8,6 +8,7 @@ import pytest
|
|||
from PIL import Image, WebPImagePlugin, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
assert_image_similar,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
|
@ -105,6 +106,19 @@ class TestFileWebp:
|
|||
hopper().save(buffer_method, format="WEBP", method=6)
|
||||
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
|
||||
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_save_all(self, tmp_path):
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
im = Image.new("RGB", (1, 1))
|
||||
im2 = Image.new("RGB", (1, 1), "#f00")
|
||||
im.save(temp_file, save_all=True, append_images=[im2])
|
||||
|
||||
with Image.open(temp_file) as reloaded:
|
||||
assert_image_equal(im, reloaded)
|
||||
|
||||
reloaded.seek(1)
|
||||
assert_image_similar(im2, reloaded, 1)
|
||||
|
||||
def test_icc_profile(self, tmp_path):
|
||||
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
|
||||
if _webp.HAVE_WEBPANIM:
|
||||
|
@ -171,9 +185,14 @@ class TestFileWebp:
|
|||
Image.open(blob).load()
|
||||
Image.open(blob).load()
|
||||
|
||||
@skip_unless_feature("webp")
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_background_from_gif(self, tmp_path):
|
||||
# Save L mode GIF with background
|
||||
with Image.open("Tests/images/no_palette_with_background.gif") as im:
|
||||
out_webp = str(tmp_path / "temp.webp")
|
||||
im.save(out_webp, save_all=True)
|
||||
|
||||
# Save P mode GIF with background
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
original_value = im.convert("RGB").getpixel((1, 1))
|
||||
|
||||
|
@ -191,7 +210,6 @@ class TestFileWebp:
|
|||
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
|
||||
assert difference < 5
|
||||
|
||||
@skip_unless_feature("webp")
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_duration(self, tmp_path):
|
||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -27,7 +28,6 @@ def test_n_frames():
|
|||
assert im.is_animated
|
||||
|
||||
|
||||
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
|
||||
def test_write_animation_L(tmp_path):
|
||||
"""
|
||||
Convert an animated GIF to animated WebP, then compare the frame count, and first
|
||||
|
@ -46,6 +46,11 @@ def test_write_animation_L(tmp_path):
|
|||
orig.load()
|
||||
im.load()
|
||||
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
||||
|
||||
if is_big_endian():
|
||||
webp = parse_version(features.version_module("webp"))
|
||||
if webp < parse_version("1.2.2"):
|
||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||
orig.seek(orig.n_frames - 1)
|
||||
im.seek(im.n_frames - 1)
|
||||
orig.load()
|
||||
|
@ -53,7 +58,6 @@ def test_write_animation_L(tmp_path):
|
|||
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
||||
|
||||
|
||||
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
|
||||
def test_write_animation_RGB(tmp_path):
|
||||
"""
|
||||
Write an animated WebP from RGB frames, and ensure the frames
|
||||
|
@ -69,6 +73,10 @@ def test_write_animation_RGB(tmp_path):
|
|||
assert_image_equal(im, frame1.convert("RGBA"))
|
||||
|
||||
# Compare second frame to original
|
||||
if is_big_endian():
|
||||
webp = parse_version(features.version_module("webp"))
|
||||
if webp < parse_version("1.2.2"):
|
||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||
im.seek(1)
|
||||
im.load()
|
||||
assert_image_equal(im, frame2.convert("RGBA"))
|
||||
|
|
|
@ -2,7 +2,7 @@ from io import BytesIO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, XbmImagePlugin
|
||||
|
||||
from .helper import hopper
|
||||
|
||||
|
@ -63,6 +63,13 @@ def test_open_filename_with_underscore():
|
|||
assert im.size == (128, 128)
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
with pytest.raises(SyntaxError):
|
||||
XbmImagePlugin.XbmImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_save_wrong_mode(tmp_path):
|
||||
im = hopper()
|
||||
out = str(tmp_path / "temp.xbm")
|
||||
|
|
|
@ -652,6 +652,15 @@ class TestImage:
|
|||
with warnings.catch_warnings():
|
||||
im.save(temp_file)
|
||||
|
||||
def test_no_new_file_on_error(self, tmp_path):
|
||||
temp_file = str(tmp_path / "temp.jpg")
|
||||
|
||||
im = Image.new("RGB", (0, 0))
|
||||
with pytest.raises(ValueError):
|
||||
im.save(temp_file)
|
||||
|
||||
assert not os.path.exists(temp_file)
|
||||
|
||||
def test_load_on_nonexclusive_multiframe(self):
|
||||
with open("Tests/images/frozenpond.mpo", "rb") as fp:
|
||||
|
||||
|
@ -666,6 +675,19 @@ class TestImage:
|
|||
|
||||
assert not fp.closed
|
||||
|
||||
def test_empty_exif(self):
|
||||
with Image.open("Tests/images/exif.png") as im:
|
||||
exif = im.getexif()
|
||||
assert dict(exif) != {}
|
||||
|
||||
# Test that exif data is cleared after another load
|
||||
exif.load(None)
|
||||
assert dict(exif) == {}
|
||||
|
||||
# Test loading just the EXIF header
|
||||
exif.load(b"Exif\x00\x00")
|
||||
assert dict(exif) == {}
|
||||
|
||||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import ctypes
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
@ -154,14 +153,17 @@ class TestImageGetPixel(AccessTest):
|
|||
|
||||
# Check 0
|
||||
im = Image.new(mode, (0, 0), None)
|
||||
with pytest.raises(IndexError):
|
||||
assert im.load() is not None
|
||||
|
||||
error = ValueError if self._need_cffi_access else IndexError
|
||||
with pytest.raises(error):
|
||||
im.putpixel((0, 0), c)
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((0, 0))
|
||||
# Check 0 negative index
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.putpixel((-1, -1), c)
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((-1, -1))
|
||||
|
||||
# check initial color
|
||||
|
@ -176,10 +178,10 @@ class TestImageGetPixel(AccessTest):
|
|||
|
||||
# Check 0
|
||||
im = Image.new(mode, (0, 0), c)
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((0, 0))
|
||||
# Check 0 negative index
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((-1, -1))
|
||||
|
||||
def test_basic(self):
|
||||
|
@ -401,6 +403,8 @@ class TestEmbeddable:
|
|||
"not from shell",
|
||||
)
|
||||
def test_embeddable(self):
|
||||
import ctypes
|
||||
|
||||
with open("embed_pil.c", "w") as fh:
|
||||
fh.write(
|
||||
"""
|
||||
|
|
|
@ -70,6 +70,11 @@ def test_16bit():
|
|||
with Image.open("Tests/images/16bit.cropped.tif") as im:
|
||||
_test_float_conversion(im)
|
||||
|
||||
for color in (65535, 65536):
|
||||
im = Image.new("I", (1, 1), color)
|
||||
im_i16 = im.convert("I;16")
|
||||
assert im_i16.getpixel((0, 0)) == 65535
|
||||
|
||||
|
||||
def test_16bit_workaround():
|
||||
with Image.open("Tests/images/16bit.cropped.tif") as im:
|
||||
|
@ -135,6 +140,10 @@ def test_trns_l(tmp_path):
|
|||
|
||||
f = str(tmp_path / "temp.png")
|
||||
|
||||
im_la = im.convert("LA")
|
||||
assert "transparency" not in im_la.info
|
||||
im_la.save(f)
|
||||
|
||||
im_rgb = im.convert("RGB")
|
||||
assert im_rgb.info["transparency"] == (128, 128, 128) # undone
|
||||
im_rgb.save(f)
|
||||
|
|
|
@ -67,6 +67,16 @@ class TestImagingPaste:
|
|||
],
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def gradient_LA(self):
|
||||
return Image.merge(
|
||||
"LA",
|
||||
[
|
||||
self.gradient_L,
|
||||
self.gradient_L.transpose(Image.Transpose.ROTATE_90),
|
||||
],
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def gradient_RGBA(self):
|
||||
return Image.merge(
|
||||
|
@ -145,6 +155,28 @@ class TestImagingPaste:
|
|||
],
|
||||
)
|
||||
|
||||
def test_image_mask_LA(self):
|
||||
for mode in ("RGBA", "RGB", "L"):
|
||||
im = Image.new(mode, (200, 200), "white")
|
||||
im2 = getattr(self, "gradient_" + mode)
|
||||
|
||||
self.assert_9points_paste(
|
||||
im,
|
||||
im2,
|
||||
self.gradient_LA,
|
||||
[
|
||||
(128, 191, 255, 191),
|
||||
(112, 207, 206, 111),
|
||||
(128, 254, 128, 1),
|
||||
(208, 208, 239, 239),
|
||||
(192, 191, 191, 191),
|
||||
(207, 207, 112, 113),
|
||||
(255, 255, 255, 255),
|
||||
(239, 207, 207, 239),
|
||||
(255, 191, 128, 191),
|
||||
],
|
||||
)
|
||||
|
||||
def test_image_mask_RGBA(self):
|
||||
for mode in ("RGBA", "RGB", "L"):
|
||||
im = Image.new(mode, (200, 200), "white")
|
||||
|
|
|
@ -10,6 +10,7 @@ def test_sanity():
|
|||
im.point(list(range(256)))
|
||||
im.point(list(range(256)) * 3)
|
||||
im.point(lambda x: x)
|
||||
im.point(lambda x: x * 1.2)
|
||||
|
||||
im = im.convert("I")
|
||||
with pytest.raises(ValueError):
|
||||
|
|
|
@ -264,6 +264,13 @@ class TestImageResize:
|
|||
with pytest.raises(ValueError):
|
||||
im.resize((10, 10), "unknown")
|
||||
|
||||
def test_load_first(self):
|
||||
# load() may change the size of the image
|
||||
# Test that resize() is calling it before getting the size
|
||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||
im = im.resize((64, 64))
|
||||
assert im.size == (64, 64)
|
||||
|
||||
def test_default_filter(self):
|
||||
for mode in "L", "RGB", "I", "F":
|
||||
im = hopper(mode)
|
||||
|
|
|
@ -88,6 +88,14 @@ def test_no_resize():
|
|||
assert im.size == (64, 64)
|
||||
|
||||
|
||||
def test_load_first():
|
||||
# load() may change the size of the image
|
||||
# Test that thumbnail() is calling it before performing size calculations
|
||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||
im.thumbnail((64, 64))
|
||||
assert im.size == (64, 10)
|
||||
|
||||
|
||||
# valgrind test is failing with memory allocated in libjpeg
|
||||
@pytest.mark.valgrind_known_error(reason="Known Failing")
|
||||
def test_DCT_scaling_edges():
|
||||
|
@ -130,4 +138,4 @@ def test_reducing_gap_for_DCT_scaling():
|
|||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
|
||||
|
||||
assert_image_equal(ref, im)
|
||||
assert_image_similar(ref, im, 1.4)
|
||||
|
|
|
@ -1440,3 +1440,15 @@ def test_continuous_horizontal_edges_polygon():
|
|||
assert_image_equal_tofile(
|
||||
img, expected, "continuous horizontal edges polygon failed"
|
||||
)
|
||||
|
||||
|
||||
def test_discontiguous_corners_polygon():
|
||||
img, draw = create_base_image_draw((84, 68))
|
||||
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
|
||||
draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
|
||||
draw.polygon(
|
||||
((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),
|
||||
BLACK,
|
||||
)
|
||||
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
|
||||
assert_image_similar_tofile(img, expected, 1)
|
||||
|
|
|
@ -200,6 +200,9 @@ class MockPyEncoder(ImageFile.PyEncoder):
|
|||
def encode(self, buffer):
|
||||
return 1, 1, b""
|
||||
|
||||
def cleanup(self):
|
||||
self.cleanup_called = True
|
||||
|
||||
|
||||
xoff, yoff, xsize, ysize = 10, 20, 100, 100
|
||||
|
||||
|
@ -327,10 +330,12 @@ class TestPyEncoder(CodecsTest):
|
|||
im = MockImageFile(buf)
|
||||
|
||||
fp = BytesIO()
|
||||
self.encoder.cleanup_called = False
|
||||
with pytest.raises(ValueError):
|
||||
ImageFile._save(
|
||||
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
||||
)
|
||||
assert self.encoder.cleanup_called
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImageFile._save(
|
||||
|
|
|
@ -48,10 +48,6 @@ def img_string_normalize(im):
|
|||
return img_to_string(string_to_img(im))
|
||||
|
||||
|
||||
def assert_img_equal(A, B):
|
||||
assert img_to_string(A) == img_to_string(B)
|
||||
|
||||
|
||||
def assert_img_equal_img_string(A, Bstring):
|
||||
assert img_to_string(A) == img_string_normalize(Bstring)
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ def test_sanity():
|
|||
ImageOps.grayscale(hopper("L"))
|
||||
ImageOps.grayscale(hopper("RGB"))
|
||||
|
||||
ImageOps.invert(hopper("1"))
|
||||
ImageOps.invert(hopper("L"))
|
||||
ImageOps.invert(hopper("RGB"))
|
||||
|
||||
|
|
|
@ -75,8 +75,16 @@ def test_photoimage_blank():
|
|||
assert im_tk.width() == 100
|
||||
assert im_tk.height() == 100
|
||||
|
||||
# reloaded = ImageTk.getimage(im_tk)
|
||||
# assert_image_equal(reloaded, im)
|
||||
im = Image.new(mode, (100, 100))
|
||||
reloaded = ImageTk.getimage(im_tk)
|
||||
assert_image_equal(reloaded.convert(mode), im)
|
||||
|
||||
|
||||
def test_box_deprecation():
|
||||
im = hopper()
|
||||
im_tk = ImageTk.PhotoImage(im)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
im_tk.paste(im, (0, 0, 128, 128))
|
||||
|
||||
|
||||
def test_bitmapimage():
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import ctypes
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image, ImageWin
|
||||
|
@ -8,6 +7,7 @@ from .helper import hopper, is_win32
|
|||
# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652
|
||||
|
||||
if is_win32():
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
|
||||
class BITMAPFILEHEADER(ctypes.Structure):
|
||||
|
|
|
@ -444,6 +444,8 @@ class TestLibUnpack:
|
|||
self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0))
|
||||
self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16))
|
||||
self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15))
|
||||
self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16))
|
||||
self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15))
|
||||
self.assert_unpack(
|
||||
"RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)
|
||||
)
|
||||
|
|
|
@ -115,6 +115,6 @@ def test_pdf_repr():
|
|||
assert pdf_repr(True) == b"true"
|
||||
assert pdf_repr(False) == b"false"
|
||||
assert pdf_repr(None) == b"null"
|
||||
assert pdf_repr(b"a)/b\\(c") == br"(a\)/b\\\(c)"
|
||||
assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)"
|
||||
assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]"
|
||||
assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>"
|
||||
|
|
15
docs/conf.py
|
@ -16,8 +16,6 @@
|
|||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
import sphinx_rtd_theme
|
||||
|
||||
import PIL
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
@ -126,13 +124,15 @@ nitpicky = True
|
|||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
html_theme = "furo"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# html_theme_options = {}
|
||||
html_theme_options = {
|
||||
"light_logo": "pillow-logo-dark-text.png",
|
||||
"dark_logo": "pillow-logo.png",
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
@ -146,7 +146,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
|||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
html_logo = "resources/pillow-logo.png"
|
||||
# html_logo = "resources/pillow-logo.png"
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
|
@ -311,10 +311,7 @@ texinfo_documents = [
|
|||
|
||||
|
||||
def setup(app):
|
||||
app.add_js_file("js/script.js")
|
||||
app.add_css_file("css/styles.css")
|
||||
app.add_css_file("css/dark.css")
|
||||
app.add_css_file("css/light.css")
|
||||
|
||||
|
||||
# GitHub repo for sphinx-issues
|
||||
|
|
|
@ -69,7 +69,7 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
|
|||
Constants
|
||||
~~~~~~~~~
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
.. deprecated:: 9.1.0
|
||||
|
||||
A number of constants have been deprecated and will be removed in Pillow 10.0.0
|
||||
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
|
||||
|
@ -142,6 +142,13 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re
|
|||
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
|
||||
:mod:`~PIL.FitsImagePlugin` instead.
|
||||
|
||||
PhotoImage.paste box parameter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -210,7 +210,9 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
format_description = "DirectDraw Surface"
|
||||
|
||||
def _open(self):
|
||||
magic, header_size = struct.unpack("<II", self.fp.read(8))
|
||||
if not _accept(self.fp.read(4)):
|
||||
raise SyntaxError("not a DDS file")
|
||||
(header_size,) = struct.unpack("<I", self.fp.read(4))
|
||||
if header_size != 124:
|
||||
raise OSError(f"Unsupported header size {repr(header_size)}")
|
||||
header_bytes = self.fp.read(header_size - 4)
|
||||
|
@ -251,7 +253,7 @@ class DXT1Decoder(ImageFile.PyDecoder):
|
|||
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
|
||||
except struct.error as e:
|
||||
raise OSError("Truncated DDS file") from e
|
||||
return 0, 0
|
||||
return -1, 0
|
||||
|
||||
|
||||
class DXT5Decoder(ImageFile.PyDecoder):
|
||||
|
@ -262,7 +264,7 @@ class DXT5Decoder(ImageFile.PyDecoder):
|
|||
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
|
||||
except struct.error as e:
|
||||
raise OSError("Truncated DDS file") from e
|
||||
return 0, 0
|
||||
return -1, 0
|
||||
|
||||
|
||||
Image.register_decoder("DXT1", DXT1Decoder)
|
||||
|
|
|
@ -24,8 +24,6 @@ attribute will be ``None``.
|
|||
Fully supported formats
|
||||
-----------------------
|
||||
|
||||
.. contents::
|
||||
|
||||
BLP
|
||||
^^^
|
||||
|
||||
|
@ -44,8 +42,9 @@ BMP
|
|||
^^^
|
||||
|
||||
Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
|
||||
or ``RGB`` data. 16-colour images are read as ``P`` images. Run-length encoding
|
||||
is not supported.
|
||||
or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding
|
||||
is not supported. Support for reading 8-bit run-length encoding was added in Pillow
|
||||
9.1.0.
|
||||
|
||||
The :py:meth:`~PIL.Image.open` method sets the following
|
||||
:py:attr:`~PIL.Image.Image.info` properties:
|
||||
|
@ -106,8 +105,34 @@ writes run-length encoded files in GIF87a by default, unless GIF89a features
|
|||
are used or GIF89a is already in use.
|
||||
|
||||
GIF files are initially read as grayscale (``L``) or palette mode (``P``)
|
||||
images, but seeking to later frames in an image will change the mode to either
|
||||
``RGB`` or ``RGBA``, depending on whether the first frame had transparency.
|
||||
images. Seeking to later frames in a ``P`` image will change the image to
|
||||
``RGB`` (or ``RGBA`` if the first frame had transparency).
|
||||
|
||||
``P`` mode images are changed to ``RGB`` because each frame of a GIF may contain
|
||||
its own individual palette of up to 256 colors. When a new frame is placed onto a
|
||||
previous frame, those colors may combine to exceed the ``P`` mode limit of 256
|
||||
colors. Instead, the image is converted to ``RGB`` handle this.
|
||||
|
||||
If you would prefer the first ``P`` image frame to be ``RGB`` as well, so that
|
||||
every ``P`` frame is converted to ``RGB`` or ``RGBA`` mode, there is a setting
|
||||
available::
|
||||
|
||||
from PIL import GifImagePlugin
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||
|
||||
GIF frames do not always contain individual palettes however. If there is only
|
||||
a global palette, then all of the colors can fit within ``P`` mode. If you would
|
||||
prefer the frames to be kept as ``P`` in that case, there is also a setting
|
||||
available::
|
||||
|
||||
from PIL import GifImagePlugin
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||
|
||||
To restore the default behavior, where ``P`` mode images are only converted to
|
||||
``RGB`` or ``RGBA`` after the first frame::
|
||||
|
||||
from PIL import GifImagePlugin
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
|
||||
The :py:meth:`~PIL.Image.open` method sets the following
|
||||
:py:attr:`~PIL.Image.Image.info` properties:
|
||||
|
@ -364,10 +389,12 @@ The :py:meth:`~PIL.Image.open` method may set the following
|
|||
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||
|
||||
**quality**
|
||||
The image quality, on a scale from 0 (worst) to 95 (best). The default is
|
||||
75. Values above 95 should be avoided; 100 disables portions of the JPEG
|
||||
compression algorithm, and results in large files with hardly any gain in
|
||||
image quality.
|
||||
The image quality, on a scale from 0 (worst) to 95 (best), or the string
|
||||
``keep``. The default is 75. Values above 95 should be avoided; 100 disables
|
||||
portions of the JPEG compression algorithm, and results in large files with
|
||||
hardly any gain in image quality. The value ``keep`` is only valid for JPEG
|
||||
files and will retain the original image quality level, subsampling, and
|
||||
qtables.
|
||||
|
||||
**optimize**
|
||||
If present and true, indicates that the encoder should make an extra pass
|
||||
|
@ -475,9 +502,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
|||
and must be greater than the code-block size.
|
||||
|
||||
**irreversible**
|
||||
If ``True``, use the lossy Irreversible Color Transformation
|
||||
followed by DWT 9-7. Defaults to ``False``, which means to use the
|
||||
Reversible Color Transformation with DWT 5-3.
|
||||
If ``True``, use the lossy discrete waveform transformation DWT 9-7.
|
||||
Defaults to ``False``, which uses the lossless DWT 5-3.
|
||||
|
||||
**mct**
|
||||
If ``1`` then enable multiple component transformation when encoding,
|
||||
otherwise use ``0`` for no component transformation (default). If MCT is
|
||||
enabled and ``irreversible`` is ``True`` then the Irreversible Color
|
||||
Transformation will be applied, otherwise encoding will use the
|
||||
Reversible Color Transformation. MCT works best with a ``mode`` of
|
||||
``RGB`` and is only applicable when the image data has 3 components.
|
||||
|
||||
.. versionadded:: 9.1.0
|
||||
|
||||
**progression**
|
||||
Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``,
|
||||
|
@ -497,6 +533,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
|||
for compliant 4K files, *at least one* of the dimensions must match
|
||||
4096 x 2160.
|
||||
|
||||
**no_jp2**
|
||||
If ``True`` then don't wrap the raw codestream in the JP2 file format when
|
||||
saving, otherwise the extension of the filename will be used to determine
|
||||
the format (default).
|
||||
|
||||
.. versionadded:: 9.1.0
|
||||
|
||||
.. note::
|
||||
|
||||
To enable JPEG 2000 support, you need to build and install the OpenJPEG
|
||||
|
@ -743,7 +786,7 @@ parameter must be set to ``True``. The following parameters can also be set:
|
|||
PPM
|
||||
^^^
|
||||
|
||||
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L`` or
|
||||
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
|
||||
``RGB`` data.
|
||||
|
||||
SGI
|
||||
|
|
|
@ -171,20 +171,37 @@ Rolling an image
|
|||
|
||||
::
|
||||
|
||||
def roll(image, delta):
|
||||
def roll(im, delta):
|
||||
"""Roll an image sideways."""
|
||||
xsize, ysize = image.size
|
||||
xsize, ysize = im.size
|
||||
|
||||
delta = delta % xsize
|
||||
if delta == 0:
|
||||
return image
|
||||
return im
|
||||
|
||||
part1 = image.crop((0, 0, delta, ysize))
|
||||
part2 = image.crop((delta, 0, xsize, ysize))
|
||||
image.paste(part1, (xsize - delta, 0, xsize, ysize))
|
||||
image.paste(part2, (0, 0, xsize - delta, ysize))
|
||||
part1 = im.crop((0, 0, delta, ysize))
|
||||
part2 = im.crop((delta, 0, xsize, ysize))
|
||||
im.paste(part1, (xsize - delta, 0, xsize, ysize))
|
||||
im.paste(part2, (0, 0, xsize - delta, ysize))
|
||||
|
||||
return image
|
||||
return im
|
||||
|
||||
Or if you would like to merge two images into a wider image:
|
||||
|
||||
Merging images
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
def merge(im1, im2):
|
||||
w = im1.size[0] + im2.size[0]
|
||||
h = max(im1.size[1], im2.size[1])
|
||||
im = Image.new("RGBA", (w, h))
|
||||
|
||||
im.paste(im1)
|
||||
im.paste(im2, (im1.size[0], 0))
|
||||
|
||||
return im
|
||||
|
||||
For more advanced tricks, the paste method can also take a transparency mask as
|
||||
an optional argument. In this mask, the value 255 indicates that the pasted
|
||||
|
|
|
@ -123,8 +123,12 @@ The ``tile`` attribute
|
|||
To be able to read the file as well as just identifying it, the ``tile``
|
||||
attribute must also be set. This attribute consists of a list of tile
|
||||
descriptors, where each descriptor specifies how data should be loaded to a
|
||||
given region in the image. In most cases, only a single descriptor is used,
|
||||
covering the full image.
|
||||
given region in the image.
|
||||
|
||||
In most cases, only a single descriptor is used, covering the full image.
|
||||
:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine
|
||||
channels within a single layer, given that the channels are stored separately,
|
||||
one after the other.
|
||||
|
||||
The tile descriptor is a 4-tuple with the following contents::
|
||||
|
||||
|
@ -324,42 +328,42 @@ The fields are used as follows:
|
|||
Whether the first line in the image is the top line on the screen (1), or
|
||||
the bottom line (-1). If omitted, the orientation defaults to 1.
|
||||
|
||||
.. _file-decoders:
|
||||
.. _file-codecs:
|
||||
|
||||
Writing Your Own File Decoder in C
|
||||
==================================
|
||||
Writing Your Own File Codec in C
|
||||
================================
|
||||
|
||||
There are 3 stages in a file decoder's lifetime:
|
||||
There are 3 stages in a file codec's lifetime:
|
||||
|
||||
1. Setup: Pillow looks for a function in the decoder registry, falling
|
||||
back to a function named ``[decodername]_decoder`` on the internal
|
||||
core image object. That function is called with the ``args`` tuple
|
||||
from the ``tile`` setup in the ``_open`` method.
|
||||
1. Setup: Pillow looks for a function in the decoder or encoder registry,
|
||||
falling back to a function named ``[codecname]_decoder`` or
|
||||
``[codecname]_encoder`` on the internal core image object. That function is
|
||||
called with the ``args`` tuple from the ``tile``.
|
||||
|
||||
2. Decoding: The decoder's decode function is repeatedly called with
|
||||
chunks of image data.
|
||||
2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly
|
||||
called with chunks of image data.
|
||||
|
||||
3. Cleanup: If the decoder has registered a cleanup function, it will
|
||||
be called at the end of the decoding process, even if there was an
|
||||
3. Cleanup: If the codec has registered a cleanup function, it will
|
||||
be called at the end of the transformation process, even if there was an
|
||||
exception raised.
|
||||
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
The current conventions are that the decoder setup function is named
|
||||
``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The
|
||||
python binding for it is named ``[decodername]_decoder`` and is setup
|
||||
from within the ``_imaging.c`` file in the codecs section of the
|
||||
function array.
|
||||
The current conventions are that the codec setup function is named
|
||||
``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew``
|
||||
and defined in ``decode.c`` or ``encode.c``. The Python binding for it is
|
||||
named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from
|
||||
within the ``_imaging.c`` file in the codecs section of the function array.
|
||||
|
||||
The setup function needs to call ``PyImaging_DecoderNew`` and at the
|
||||
very least, set the ``decode`` function pointer. The fields of
|
||||
interest in this object are:
|
||||
The setup function needs to call ``PyImaging_DecoderNew`` or
|
||||
``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or
|
||||
``encode`` function pointer. The fields of interest in this object are:
|
||||
|
||||
**decode**
|
||||
Function pointer to the decode function, which has access to
|
||||
``im``, ``state``, and the buffer of data to be added to the image.
|
||||
**decode**/**encode**
|
||||
Function pointer to the decode or encode function, which has access to
|
||||
``im``, ``state``, and the buffer of data to be transformed.
|
||||
|
||||
**cleanup**
|
||||
Function pointer to the cleanup function, has access to ``state``.
|
||||
|
@ -369,36 +373,34 @@ interest in this object are:
|
|||
|
||||
**state**
|
||||
An ImagingCodecStateInstance, will be set by Pillow. The ``context``
|
||||
member is an opaque struct that can be used by the decoder to store
|
||||
member is an opaque struct that can be used by the codec to store
|
||||
any format specific state or options.
|
||||
|
||||
**pulls_fd**
|
||||
**EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1,
|
||||
``state->fd`` will be a pointer to the Python file like object. The
|
||||
decoder may use the functions in ``codec_fd.c`` to read directly
|
||||
from the file like object rather than have the data pushed through a
|
||||
buffer. Note that this implementation may be refactored until this
|
||||
warning is removed.
|
||||
**pulls_fd**/**pushes_fd**
|
||||
If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1,
|
||||
``state->fd`` will be a pointer to the Python file like object. The codec may
|
||||
use the functions in ``codec_fd.c`` to read or write directly with the file
|
||||
like object rather than have the data pushed through a buffer.
|
||||
|
||||
.. versionadded:: 3.3.0
|
||||
|
||||
|
||||
Decoding
|
||||
--------
|
||||
Transforming
|
||||
------------
|
||||
|
||||
The decode function is called with the target (core) image, the
|
||||
decoder state structure, and a buffer of data to be decoded.
|
||||
The decode or encode function is called with the target (core) image, the codec
|
||||
state structure, and a buffer of data to be transformed.
|
||||
|
||||
**Experimental** -- If ``pulls_fd`` is set, then the decode function
|
||||
is called once, with an empty buffer. It is the decoder's
|
||||
responsibility to decode the entire tile in that one call. The rest of
|
||||
this section only applies if ``pulls_fd`` is not set.
|
||||
It is the codec's responsibility to pull as much data as possible out of the
|
||||
buffer and return the number of bytes consumed. The next call to the codec will
|
||||
include the previous unconsumed tail. The codec function will be called
|
||||
multiple times as the data processed.
|
||||
|
||||
It is the decoder's responsibility to pull as much data as possible
|
||||
out of the buffer and return the number of bytes consumed. The next
|
||||
call to the decoder will include the previous unconsumed tail. The
|
||||
decoder function will be called multiple times as the data is read
|
||||
from the file like object.
|
||||
Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or
|
||||
encode function is called once, with an empty buffer. It is the codec's
|
||||
responsibility to transform the entire tile in that one call. Using this will
|
||||
provide a codec with more freedom, but that freedom may mean increased memory
|
||||
usage if the entire tile is held in memory at once by the codec.
|
||||
|
||||
If an error occurs, set ``state->errcode`` and return -1.
|
||||
|
||||
|
@ -407,10 +409,9 @@ Return -1 on success, without setting the errcode.
|
|||
Cleanup
|
||||
-------
|
||||
|
||||
The cleanup function is called after the decoder returns a negative
|
||||
value, or if there is a read error from the file. This function should
|
||||
free any allocated memory and release any resources from external
|
||||
libraries.
|
||||
The cleanup function is called after the codec returns a negative
|
||||
value, or if there is an error. This function should free any allocated
|
||||
memory and release any resources from external libraries.
|
||||
|
||||
.. _file-codecs-py:
|
||||
|
||||
|
@ -425,11 +426,32 @@ They should be registered using :py:meth:`PIL.Image.register_decoder` and
|
|||
the file codecs, there are three stages in the lifetime of a
|
||||
Python-based file codec:
|
||||
|
||||
1. Setup: Pillow looks for the decoder in the registry, then
|
||||
1. Setup: Pillow looks for the codec in the decoder or encoder registry, then
|
||||
instantiates the class.
|
||||
|
||||
2. Transforming: The instance's ``decode`` method is repeatedly called with
|
||||
a buffer of data to be interpreted, or the ``encode`` method is repeatedly
|
||||
called with the size of data to be output.
|
||||
|
||||
3. Cleanup: The instance's ``cleanup`` method is called.
|
||||
Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's
|
||||
``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode``
|
||||
will only be called once. In the decoder, ``self.fd`` can be used to access
|
||||
the file-like object. Using this will provide a codec with more freedom, but
|
||||
that freedom may mean increased memory usage if entire file is held in
|
||||
memory at once by the codec.
|
||||
|
||||
In ``decode``, once the data has been interpreted, ``set_as_raw`` can be
|
||||
used to populate the image.
|
||||
|
||||
3. Cleanup: The instance's ``cleanup`` method is called once the transformation
|
||||
is complete. This can be used to clean up any resources used by the codec.
|
||||
|
||||
If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you
|
||||
probably chose to perform any cleanup tasks at the end of ``decode`` or
|
||||
``encode``.
|
||||
|
||||
For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin
|
||||
<https://github.com/python-pillow/Pillow/blob/main/docs/example/DdsImagePlugin.py>`_.
|
||||
For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and
|
||||
:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin
|
||||
<https://github.com/python-pillow/Pillow/blob/main/src/PIL/BlpImagePlugin.py>`_
|
||||
|
|
|
@ -461,8 +461,6 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| Debian 11 Bullseye | 3.9 | x86 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 34 | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 35 | 3.10 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Gentoo | 3.9 | x86-64 |
|
||||
|
|
|
@ -14,6 +14,16 @@ for a region of an image.
|
|||
statistics. You can also pass in a previously calculated histogram.
|
||||
|
||||
:param image: A PIL image, or a precalculated histogram.
|
||||
|
||||
.. note::
|
||||
|
||||
For a PIL image, calculations rely on the
|
||||
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
|
||||
grouped into 256 bins, even if the image has more than 8 bits per
|
||||
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
|
||||
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
|
||||
of more than 255.
|
||||
|
||||
:param mask: An optional mask.
|
||||
|
||||
.. py:attribute:: extrema
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
The PixelAccess class provides read and write access to
|
||||
:py:class:`PIL.Image` data at a pixel level.
|
||||
|
||||
.. note:: Accessing individual pixels is fairly slow. If you are looping over all of the pixels in an image, there is likely a faster way using other parts of the Pillow API.
|
||||
.. note:: Accessing individual pixels is fairly slow. If you are
|
||||
looping over all of the pixels in an image, there is likely
|
||||
a faster way using other parts of the Pillow API.
|
||||
|
||||
:mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps`
|
||||
have methods for many standard operations. If you wish to perform
|
||||
a custom mapping, check out :py:meth:`~PIL.Image.Image.point`.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
@ -39,7 +45,7 @@ Access using negative indexes is also possible.
|
|||
|
||||
|
||||
:py:class:`PixelAccess` Class
|
||||
-----------------------------------
|
||||
-----------------------------
|
||||
|
||||
.. class:: PixelAccess
|
||||
|
||||
|
|
|
@ -10,6 +10,10 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the
|
|||
looping over all of the pixels in an image, there is likely
|
||||
a faster way using other parts of the Pillow API.
|
||||
|
||||
:mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps`
|
||||
have methods for many standard operations. If you wish to perform
|
||||
a custom mapping, check out :py:meth:`~PIL.Image.Image.point`.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
|
|
|
@ -146,12 +146,24 @@ At present, the information within each block is merely returned as a dictionary
|
|||
"data" entry. This will allow more useful information to be added in the future without
|
||||
breaking backwards compatibility.
|
||||
|
||||
Added rawmode argument to Image.getpalette()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Added mct and no_jp2 options for saving JPEG 2000
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
By default, :py:meth:`~PIL.Image.Image.getpalette` returns RGB data from the palette.
|
||||
A ``rawmode`` argument has been added, to allow the mode to be chosen instead. ``None``
|
||||
can be used to return data in the current mode of the palette.
|
||||
The :py:meth:`PIL.Image.Image.save` method now supports the following options for
|
||||
JPEG 2000:
|
||||
|
||||
**mct**
|
||||
If ``1`` then enable multiple component transformation when encoding,
|
||||
otherwise use ``0`` for no component transformation (default). If MCT is
|
||||
enabled and ``irreversible`` is ``True`` then the Irreversible Color
|
||||
Transformation will be applied, otherwise encoding will use the
|
||||
Reversible Color Transformation. MCT works best with a ``mode`` of
|
||||
``RGB`` and is only applicable when the image data has 3 components.
|
||||
|
||||
**no_jp2**
|
||||
If ``True`` then don't wrap the raw codestream in the JP2 file format when
|
||||
saving, otherwise the extension of the filename will be used to determine
|
||||
the format (default).
|
||||
|
||||
Added PyEncoder
|
||||
^^^^^^^^^^^^^^^
|
||||
|
@ -160,9 +172,35 @@ Added PyEncoder
|
|||
written in Python. See :ref:`Writing Your Own File Codec in Python<file-codecs-py>` for
|
||||
more information.
|
||||
|
||||
GifImagePlugin loading strategy
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This
|
||||
behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as
|
||||
well.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from PIL import GifImagePlugin
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||
|
||||
Or subsequent frames can be kept in ``P`` mode as long as there is only a single
|
||||
palette.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from PIL import GifImagePlugin
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
musllinux wheels
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
Pillow now builds binary wheels for musllinux, suitable for Linux distributions based on the musl C standard library such as Alpine
|
||||
(rather than the glibc library used by manylinux wheels). See :pep:`656`.
|
||||
|
||||
ImageShow temporary files on Unix
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -177,6 +215,11 @@ Image._repr_pretty_
|
|||
identity of the object. This allows Jupyter to describe an image and have that
|
||||
description stay the same on subsequent executions of the same code.
|
||||
|
||||
Added BigTIFF reading
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Support has been added for reading BigTIFF images.
|
||||
|
||||
Added BLP saving
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
@media (prefers-color-scheme: light) {
|
||||
|
||||
.wy-menu-vertical li.toctree-l2.current a,
|
||||
.wy-menu-vertical li.toctree-l3.current a {
|
||||
background-color: #c9c9c9;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
th p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rst-content tr .line-block {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
jQuery(document).ready(function ($) {
|
||||
setTimeout(function () {
|
||||
var sectionID = 'base';
|
||||
var search = function ($section, $sidebarItem) {
|
||||
$section.children('.section, .function, .method').each(function () {
|
||||
if ($(this).hasClass('section')) {
|
||||
sectionID = $(this).attr('id');
|
||||
search($(this), $sidebarItem.parent().find('[href="#'+sectionID+'"]'));
|
||||
} else {
|
||||
var $dt = $(this).children('dt');
|
||||
var id = $dt.attr('id');
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $functionsUL = $sidebarItem.siblings('[data-sectionID='+sectionID+']');
|
||||
if (!$functionsUL.length) {
|
||||
$functionsUL = $('<ul />').attr('data-sectionID', sectionID);
|
||||
$functionsUL.insertAfter($sidebarItem);
|
||||
}
|
||||
|
||||
var $li = $('<li />');
|
||||
var $a = $('<a />').css('font-size', '11.5px');
|
||||
var $upperA = $sidebarItem.parent().children('a');
|
||||
var $upperAParent = $upperA.parent();
|
||||
if ($upperAParent.hasClass('toctree-l2')) {
|
||||
$a.css('padding-left', '4em');
|
||||
} else if ($upperAParent.hasClass('toctree-l3')) {
|
||||
if (!$upperA.find('.toctree-expand').length) {
|
||||
$upperA.prepend($('<span />').addClass('toctree-expand'));
|
||||
}
|
||||
$a.css('padding-left', '5em');
|
||||
} else {
|
||||
$a.css('background-color', '#bdbdbd');
|
||||
$a.css('padding-left', '6.25em');
|
||||
}
|
||||
$a.attr('href', '#'+id);
|
||||
$a.text('- '+$dt.find('code').text());
|
||||
$a.click(function () {
|
||||
setTimeout(function () {
|
||||
$a.css('font-weight', 'bold');
|
||||
}, 0);
|
||||
});
|
||||
$li.append($a);
|
||||
$functionsUL.append($li);
|
||||
}
|
||||
});
|
||||
};
|
||||
search($('[itemprop=articleBody] > .section'), $('.wy-nav-side a[href="#"]'));
|
||||
}, 0);
|
||||
$(window).on('hashchange', function () {
|
||||
$('ul[data-sectionID]').each(function () {
|
||||
$(this).find('a').each(function () {
|
||||
$(this).css('font-weight', 'normal');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
BIN
docs/resources/pillow-logo-dark-text.png
Normal file
After Width: | Height: | Size: 16 KiB |
|
@ -37,12 +37,12 @@ python_requires = >=3.7
|
|||
|
||||
[options.extras_require]
|
||||
docs =
|
||||
furo
|
||||
olefile
|
||||
sphinx>=2.4
|
||||
sphinx-copybutton
|
||||
sphinx-issues>=3.0.1
|
||||
sphinx-removed-in
|
||||
sphinx-rtd-theme>=1.0
|
||||
sphinxext-opengraph
|
||||
tests =
|
||||
check-manifest
|
||||
|
|
2
setup.py
|
@ -167,7 +167,7 @@ def _find_library_dirs_ldconfig():
|
|||
# Assuming GLIBC's ldconfig (with option -p)
|
||||
# Alpine Linux uses musl that can't print cache
|
||||
args = ["/sbin/ldconfig", "-p"]
|
||||
expr = fr".*\({abi_type}.*\) => (.*)"
|
||||
expr = rf".*\({abi_type}.*\) => (.*)"
|
||||
env = dict(os.environ)
|
||||
env["LC_ALL"] = "C"
|
||||
env["LANG"] = "C"
|
||||
|
|
|
@ -306,7 +306,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
self._load()
|
||||
except struct.error as e:
|
||||
raise OSError("Truncated BLP file") from e
|
||||
return 0, 0
|
||||
return -1, 0
|
||||
|
||||
def _read_blp_header(self):
|
||||
self.fd.seek(4)
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
#
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import i32le as i32
|
||||
|
@ -167,6 +169,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")
|
||||
|
||||
# ---------------- Process BMP with Bitfields compression (not palette)
|
||||
decoder_name = "raw"
|
||||
if file_info["compression"] == self.BITFIELDS:
|
||||
SUPPORTED = {
|
||||
32: [
|
||||
|
@ -208,6 +211,8 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
elif file_info["compression"] == self.RAW:
|
||||
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
|
||||
raw_mode, self.mode = "BGRA", "RGBA"
|
||||
elif file_info["compression"] == self.RLE8:
|
||||
decoder_name = "bmp_rle"
|
||||
else:
|
||||
raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
|
||||
|
||||
|
@ -247,7 +252,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
self.info["compression"] = file_info["compression"]
|
||||
self.tile = [
|
||||
(
|
||||
"raw",
|
||||
decoder_name,
|
||||
(0, 0, file_info["width"], file_info["height"]),
|
||||
offset or self.fp.tell(),
|
||||
(
|
||||
|
@ -271,6 +276,57 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
self._bitmap(offset=offset)
|
||||
|
||||
|
||||
class BmpRleDecoder(ImageFile.PyDecoder):
|
||||
_pulls_fd = True
|
||||
|
||||
def decode(self, buffer):
|
||||
data = bytearray()
|
||||
x = 0
|
||||
while len(data) < self.state.xsize * self.state.ysize:
|
||||
pixels = self.fd.read(1)
|
||||
byte = self.fd.read(1)
|
||||
if not pixels or not byte:
|
||||
break
|
||||
num_pixels = pixels[0]
|
||||
if num_pixels:
|
||||
# encoded mode
|
||||
if x + num_pixels > self.state.xsize:
|
||||
# Too much data for row
|
||||
num_pixels = max(0, self.state.xsize - x)
|
||||
data += byte * num_pixels
|
||||
x += num_pixels
|
||||
else:
|
||||
if byte[0] == 0:
|
||||
# end of line
|
||||
while len(data) % self.state.xsize != 0:
|
||||
data += b"\x00"
|
||||
x = 0
|
||||
elif byte[0] == 1:
|
||||
# end of bitmap
|
||||
break
|
||||
elif byte[0] == 2:
|
||||
# delta
|
||||
bytes_read = self.fd.read(2)
|
||||
if len(bytes_read) < 2:
|
||||
break
|
||||
right, up = self.fd.read(2)
|
||||
data += b"\x00" * (right + up * self.state.xsize)
|
||||
x = len(data) % self.state.xsize
|
||||
else:
|
||||
# absolute mode
|
||||
bytes_read = self.fd.read(byte[0])
|
||||
data += bytes_read
|
||||
if len(bytes_read) < byte[0]:
|
||||
break
|
||||
x += byte[0]
|
||||
|
||||
# align to 16-bit word boundary
|
||||
if self.fd.tell() % 2 != 0:
|
||||
self.fd.seek(1, os.SEEK_CUR)
|
||||
self.set_as_raw(bytes(data), ("P", 0, self.args[-1]))
|
||||
return -1, 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Image plugin for the DIB format (BMP alias)
|
||||
# =============================================================================
|
||||
|
@ -372,6 +428,8 @@ Image.register_extension(BmpImageFile.format, ".bmp")
|
|||
|
||||
Image.register_mime(BmpImageFile.format, "image/bmp")
|
||||
|
||||
Image.register_decoder("bmp_rle", BmpRleDecoder)
|
||||
|
||||
Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
|
||||
Image.register_save(DibImageFile.format, _dib_save)
|
||||
|
||||
|
|
|
@ -111,7 +111,9 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
format_description = "DirectDraw Surface"
|
||||
|
||||
def _open(self):
|
||||
magic, header_size = struct.unpack("<II", self.fp.read(8))
|
||||
if not _accept(self.fp.read(4)):
|
||||
raise SyntaxError("not a DDS file")
|
||||
(header_size,) = struct.unpack("<I", self.fp.read(4))
|
||||
if header_size != 124:
|
||||
raise OSError(f"Unsupported header size {repr(header_size)}")
|
||||
header_bytes = self.fp.read(header_size - 4)
|
||||
|
|
|
@ -26,7 +26,11 @@ from ._binary import o8
|
|||
|
||||
|
||||
def _accept(prefix):
|
||||
return len(prefix) >= 6 and i16(prefix, 4) in [0xAF11, 0xAF12]
|
||||
return (
|
||||
len(prefix) >= 6
|
||||
and i16(prefix, 4) in [0xAF11, 0xAF12]
|
||||
and i16(prefix, 14) in [0, 3] # flags
|
||||
)
|
||||
|
||||
|
||||
##
|
||||
|
@ -44,11 +48,7 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
|
||||
# HEAD
|
||||
s = self.fp.read(128)
|
||||
if not (
|
||||
_accept(s)
|
||||
and i16(s, 14) in [0, 3] # flags
|
||||
and s[20:22] == b"\x00\x00" # reserved
|
||||
):
|
||||
if not (_accept(s) and s[20:22] == b"\x00\x00"):
|
||||
raise SyntaxError("not an FLI/FLC file")
|
||||
|
||||
# frames
|
||||
|
|
|
@ -94,7 +94,8 @@ class FtexImageFile(ImageFile.ImageFile):
|
|||
format_description = "Texture File Format (IW2:EOC)"
|
||||
|
||||
def _open(self):
|
||||
struct.unpack("<I", self.fp.read(4)) # magic
|
||||
if not _accept(self.fp.read(4)):
|
||||
raise SyntaxError("not an FTEX file")
|
||||
struct.unpack("<i", self.fp.read(4)) # version
|
||||
self._size = struct.unpack("<2i", self.fp.read(8))
|
||||
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
|
||||
|
|
|
@ -43,9 +43,9 @@ class GbrImageFile(ImageFile.ImageFile):
|
|||
|
||||
def _open(self):
|
||||
header_size = i32(self.fp.read(4))
|
||||
version = i32(self.fp.read(4))
|
||||
if header_size < 20:
|
||||
raise SyntaxError("not a GIMP brush")
|
||||
version = i32(self.fp.read(4))
|
||||
if version not in (1, 2):
|
||||
raise SyntaxError(f"Unsupported GIMP brush version: {version}")
|
||||
|
||||
|
|
|
@ -28,12 +28,25 @@ import itertools
|
|||
import math
|
||||
import os
|
||||
import subprocess
|
||||
from enum import IntEnum
|
||||
|
||||
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import o8
|
||||
from ._binary import o16le as o16
|
||||
|
||||
|
||||
class LoadingStrategy(IntEnum):
|
||||
""".. versionadded:: 9.1.0"""
|
||||
|
||||
RGB_AFTER_FIRST = 0
|
||||
RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
|
||||
RGB_ALWAYS = 2
|
||||
|
||||
|
||||
#: .. versionadded:: 9.1.0
|
||||
LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Identify/read GIF files
|
||||
|
||||
|
@ -61,6 +74,12 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
return self.fp.read(s[0])
|
||||
return None
|
||||
|
||||
def _is_palette_needed(self, p):
|
||||
for i in range(0, len(p), 3):
|
||||
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _open(self):
|
||||
|
||||
# Screen
|
||||
|
@ -79,11 +98,9 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
self.info["background"] = s[11]
|
||||
# check if palette contains colour indices
|
||||
p = self.fp.read(3 << bits)
|
||||
for i in range(0, len(p), 3):
|
||||
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
|
||||
if self._is_palette_needed(p):
|
||||
p = ImagePalette.raw("RGB", p)
|
||||
self.global_palette = self.palette = p
|
||||
break
|
||||
|
||||
self.__fp = self.fp # FIXME: hack
|
||||
self.__rewind = self.fp.tell()
|
||||
|
@ -97,7 +114,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
current = self.tell()
|
||||
try:
|
||||
while True:
|
||||
self.seek(self.tell() + 1)
|
||||
self._seek(self.tell() + 1, False)
|
||||
except EOFError:
|
||||
self._n_frames = self.tell() + 1
|
||||
self.seek(current)
|
||||
|
@ -110,9 +127,11 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
self._is_animated = self._n_frames != 1
|
||||
else:
|
||||
current = self.tell()
|
||||
|
||||
if current:
|
||||
self._is_animated = True
|
||||
else:
|
||||
try:
|
||||
self.seek(1)
|
||||
self._seek(1, False)
|
||||
self._is_animated = True
|
||||
except EOFError:
|
||||
self._is_animated = False
|
||||
|
@ -135,26 +154,22 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
self.seek(last_frame)
|
||||
raise EOFError("no more images in GIF file") from e
|
||||
|
||||
def _seek(self, frame):
|
||||
def _seek(self, frame, update_image=True):
|
||||
|
||||
if frame == 0:
|
||||
# rewind
|
||||
self.__offset = 0
|
||||
self.dispose = None
|
||||
self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1
|
||||
self.__frame = -1
|
||||
self.__fp.seek(self.__rewind)
|
||||
self.disposal_method = 0
|
||||
else:
|
||||
# ensure that the previous frame was loaded
|
||||
if self.tile:
|
||||
if self.tile and update_image:
|
||||
self.load()
|
||||
|
||||
if frame != self.__frame + 1:
|
||||
raise ValueError(f"cannot seek to frame {frame}")
|
||||
self.__frame = frame
|
||||
|
||||
self.tile = []
|
||||
|
||||
self.fp = self.__fp
|
||||
if self.__offset:
|
||||
|
@ -164,27 +179,23 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
pass
|
||||
self.__offset = 0
|
||||
|
||||
if self.__frame == 1:
|
||||
self.pyaccess = None
|
||||
if "transparency" in self.info:
|
||||
self.mode = "RGBA"
|
||||
self.im.putpalettealpha(self.info["transparency"], 0)
|
||||
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
|
||||
s = self.fp.read(1)
|
||||
if not s or s == b";":
|
||||
raise EOFError
|
||||
|
||||
del self.info["transparency"]
|
||||
else:
|
||||
self.mode = "RGB"
|
||||
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
||||
if self.dispose:
|
||||
self.im.paste(self.dispose, self.dispose_extent)
|
||||
self.__frame = frame
|
||||
|
||||
self.tile = []
|
||||
|
||||
palette = None
|
||||
|
||||
info = {}
|
||||
frame_transparency = None
|
||||
interlace = None
|
||||
frame_dispose_extent = None
|
||||
while True:
|
||||
|
||||
if not s:
|
||||
s = self.fp.read(1)
|
||||
if not s or s == b";":
|
||||
break
|
||||
|
@ -223,6 +234,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
else:
|
||||
info["comment"] = block
|
||||
block = self.data()
|
||||
s = None
|
||||
continue
|
||||
elif s[0] == 255:
|
||||
#
|
||||
|
@ -245,16 +257,18 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
# extent
|
||||
x0, y0 = i16(s, 0), i16(s, 2)
|
||||
x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
|
||||
if x1 > self.size[0] or y1 > self.size[1]:
|
||||
if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
|
||||
self._size = max(x1, self.size[0]), max(y1, self.size[1])
|
||||
self.dispose_extent = x0, y0, x1, y1
|
||||
frame_dispose_extent = x0, y0, x1, y1
|
||||
flags = s[8]
|
||||
|
||||
interlace = (flags & 64) != 0
|
||||
|
||||
if flags & 128:
|
||||
bits = (flags & 7) + 1
|
||||
palette = ImagePalette.raw("RGB", self.fp.read(3 << bits))
|
||||
p = self.fp.read(3 << bits)
|
||||
if self._is_palette_needed(p):
|
||||
palette = ImagePalette.raw("RGB", p)
|
||||
|
||||
# image data
|
||||
bits = self.fp.read(1)[0]
|
||||
|
@ -264,16 +278,56 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
else:
|
||||
pass
|
||||
# raise OSError, "illegal GIF tag `%x`" % s[0]
|
||||
s = None
|
||||
|
||||
frame_palette = palette or self.global_palette
|
||||
if interlace is None:
|
||||
# self.__fp = None
|
||||
raise EOFError
|
||||
if not update_image:
|
||||
return
|
||||
|
||||
if self.dispose:
|
||||
self.im.paste(self.dispose, self.dispose_extent)
|
||||
|
||||
self._frame_palette = palette or self.global_palette
|
||||
if frame == 0:
|
||||
if self._frame_palette:
|
||||
self.mode = (
|
||||
"RGB" if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS else "P"
|
||||
)
|
||||
else:
|
||||
self.mode = "L"
|
||||
|
||||
if not palette and self.global_palette:
|
||||
from copy import copy
|
||||
|
||||
palette = copy(self.global_palette)
|
||||
self.palette = palette
|
||||
else:
|
||||
self._frame_transparency = frame_transparency
|
||||
if self.mode == "P":
|
||||
if (
|
||||
LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||
or palette
|
||||
):
|
||||
self.pyaccess = None
|
||||
if "transparency" in self.info:
|
||||
self.im.putpalettealpha(self.info["transparency"], 0)
|
||||
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
|
||||
self.mode = "RGBA"
|
||||
del self.info["transparency"]
|
||||
else:
|
||||
self.mode = "RGB"
|
||||
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
||||
|
||||
def _rgb(color):
|
||||
if frame_palette:
|
||||
color = tuple(frame_palette.palette[color * 3 : color * 3 + 3])
|
||||
if self._frame_palette:
|
||||
color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
|
||||
else:
|
||||
color = (color, color, color)
|
||||
return color
|
||||
|
||||
self.dispose_extent = frame_dispose_extent
|
||||
try:
|
||||
if self.disposal_method < 2:
|
||||
# do not dispose or none specified
|
||||
|
@ -288,17 +342,21 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
Image._decompression_bomb_check(dispose_size)
|
||||
|
||||
# by convention, attempt to use transparency first
|
||||
dispose_mode = "P"
|
||||
color = self.info.get("transparency", frame_transparency)
|
||||
if color is not None:
|
||||
if self.mode in ("RGB", "RGBA"):
|
||||
dispose_mode = "RGBA"
|
||||
color = _rgb(color) + (0,)
|
||||
else:
|
||||
color = self.info.get("background", 0)
|
||||
if self.mode in ("RGB", "RGBA"):
|
||||
dispose_mode = "RGB"
|
||||
color = _rgb(self.info.get("background", 0))
|
||||
color = _rgb(color)
|
||||
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
||||
else:
|
||||
# replace with previous contents
|
||||
if self.im:
|
||||
if self.im is not None:
|
||||
# only dispose the extent in this frame
|
||||
self.dispose = self._crop(self.im, self.dispose_extent)
|
||||
elif frame_transparency is not None:
|
||||
|
@ -306,26 +364,30 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
dispose_size = (x1 - x0, y1 - y0)
|
||||
|
||||
Image._decompression_bomb_check(dispose_size)
|
||||
self.dispose = Image.core.fill(
|
||||
"RGBA", dispose_size, _rgb(frame_transparency) + (0,)
|
||||
)
|
||||
dispose_mode = "P"
|
||||
color = frame_transparency
|
||||
if self.mode in ("RGB", "RGBA"):
|
||||
dispose_mode = "RGBA"
|
||||
color = _rgb(frame_transparency) + (0,)
|
||||
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if interlace is not None:
|
||||
if frame == 0 and frame_transparency is not None:
|
||||
transparency = -1
|
||||
if frame_transparency is not None:
|
||||
if frame == 0:
|
||||
self.info["transparency"] = frame_transparency
|
||||
elif self.mode not in ("RGB", "RGBA"):
|
||||
transparency = frame_transparency
|
||||
self.tile = [
|
||||
(
|
||||
"gif",
|
||||
(x0, y0, x1, y1),
|
||||
self.__offset,
|
||||
(bits, interlace),
|
||||
(bits, interlace, transparency),
|
||||
)
|
||||
]
|
||||
else:
|
||||
# self.__fp = None
|
||||
raise EOFError
|
||||
|
||||
for k in ["duration", "comment", "extension", "loop"]:
|
||||
if k in info:
|
||||
|
@ -333,45 +395,42 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
elif k in self.info:
|
||||
del self.info[k]
|
||||
|
||||
if frame == 0:
|
||||
self.mode = "P" if frame_palette else "L"
|
||||
|
||||
if self.mode == "P" and not palette:
|
||||
from copy import copy
|
||||
|
||||
palette = copy(self.global_palette)
|
||||
self.palette = palette
|
||||
else:
|
||||
self._frame_palette = frame_palette
|
||||
self._frame_transparency = frame_transparency
|
||||
|
||||
def load_prepare(self):
|
||||
temp_mode = "P" if self._frame_palette else "L"
|
||||
self._prev_im = None
|
||||
if self.__frame == 0:
|
||||
if "transparency" in self.info:
|
||||
self.im = Image.core.fill(
|
||||
self.mode, self.size, self.info["transparency"]
|
||||
temp_mode, self.size, self.info["transparency"]
|
||||
)
|
||||
else:
|
||||
elif self.mode in ("RGB", "RGBA"):
|
||||
self._prev_im = self.im
|
||||
if self._frame_palette:
|
||||
self.mode = "P"
|
||||
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
|
||||
self.im.putpalette(*self._frame_palette.getdata())
|
||||
self._frame_palette = None
|
||||
else:
|
||||
self.mode = "L"
|
||||
self.im = None
|
||||
self.mode = temp_mode
|
||||
self._frame_palette = None
|
||||
|
||||
super().load_prepare()
|
||||
|
||||
def load_end(self):
|
||||
if self.__frame == 0:
|
||||
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
|
||||
self.mode = "RGB"
|
||||
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
||||
return
|
||||
if self.mode == "P" and self._prev_im:
|
||||
if self._frame_transparency is not None:
|
||||
self.im.putpalettealpha(self._frame_transparency, 0)
|
||||
frame_im = self.im.convert("RGBA")
|
||||
else:
|
||||
frame_im = self.im.convert("RGB")
|
||||
else:
|
||||
if not self._prev_im:
|
||||
return
|
||||
frame_im = self.im
|
||||
frame_im = self._crop(frame_im, self.dispose_extent)
|
||||
|
||||
self.im = self._prev_im
|
||||
|
@ -401,7 +460,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
RAWMODE = {"1": "L", "L": "L", "P": "P"}
|
||||
|
||||
|
||||
def _normalize_mode(im, initial_call=False):
|
||||
def _normalize_mode(im):
|
||||
"""
|
||||
Takes an image (or frame), returns an image in a mode that is appropriate
|
||||
for saving in a Gif.
|
||||
|
@ -409,31 +468,20 @@ def _normalize_mode(im, initial_call=False):
|
|||
It may return the original image, or it may return an image converted to
|
||||
palette or 'L' mode.
|
||||
|
||||
UNDONE: What is the point of mucking with the initial call palette, for
|
||||
an image that shouldn't have a palette, or it would be a mode 'P' and
|
||||
get returned in the RAWMODE clause.
|
||||
|
||||
:param im: Image object
|
||||
:param initial_call: Default false, set to true for a single frame.
|
||||
:returns: Image object
|
||||
"""
|
||||
if im.mode in RAWMODE:
|
||||
im.load()
|
||||
return im
|
||||
if Image.getmodebase(im.mode) == "RGB":
|
||||
if initial_call:
|
||||
palette_size = 256
|
||||
if im.palette:
|
||||
palette_size = len(im.palette.getdata()[1]) // 3
|
||||
im = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=palette_size)
|
||||
im = im.convert("P", palette=Image.Palette.ADAPTIVE)
|
||||
if im.palette.mode == "RGBA":
|
||||
for rgba in im.palette.colors.keys():
|
||||
if rgba[3] == 0:
|
||||
im.info["transparency"] = im.palette.colors[rgba]
|
||||
break
|
||||
return im
|
||||
else:
|
||||
return im.convert("P")
|
||||
return im.convert("L")
|
||||
|
||||
|
||||
|
@ -491,7 +539,7 @@ def _normalize_palette(im, palette, info):
|
|||
|
||||
|
||||
def _write_single_frame(im, fp, palette):
|
||||
im_out = _normalize_mode(im, True)
|
||||
im_out = _normalize_mode(im)
|
||||
for k, v in im_out.info.items():
|
||||
im.encoderinfo.setdefault(k, v)
|
||||
im_out = _normalize_palette(im_out, palette, im.encoderinfo)
|
||||
|
@ -623,11 +671,14 @@ def get_interlace(im):
|
|||
def _write_local_header(fp, im, offset, flags):
|
||||
transparent_color_exists = False
|
||||
try:
|
||||
if "transparency" in im.encoderinfo:
|
||||
transparency = im.encoderinfo["transparency"]
|
||||
except KeyError:
|
||||
else:
|
||||
transparency = im.info["transparency"]
|
||||
transparency = int(transparency)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
else:
|
||||
transparency = int(transparency)
|
||||
# optimize the block away if transparent color is not used
|
||||
transparent_color_exists = True
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ class GimpPaletteFile:
|
|||
break
|
||||
|
||||
# skip fields and comment lines
|
||||
if re.match(br"\w+:|#", s):
|
||||
if re.match(rb"\w+:|#", s):
|
||||
continue
|
||||
if len(s) > 100:
|
||||
raise SyntaxError("bad palette file")
|
||||
|
|
|
@ -167,7 +167,7 @@ class IcnsFile:
|
|||
self.dct = dct = {}
|
||||
self.fobj = fobj
|
||||
sig, filesize = nextheader(fobj)
|
||||
if sig != MAGIC:
|
||||
if not _accept(sig):
|
||||
raise SyntaxError("not an icns file")
|
||||
i = HEADERSIZE
|
||||
while i < filesize:
|
||||
|
@ -287,7 +287,7 @@ class IcnsImageFile(ImageFile.ImageFile):
|
|||
)
|
||||
|
||||
px = Image.Image.load(self)
|
||||
if self.im and self.im.size == self.size:
|
||||
if self.im is not None and self.im.size == self.size:
|
||||
# Already loaded
|
||||
return px
|
||||
self.load_prepare()
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
|
||||
|
||||
|
||||
import struct
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
from math import ceil, log
|
||||
|
@ -30,6 +29,8 @@ from math import ceil, log
|
|||
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import i32le as i32
|
||||
from ._binary import o8
|
||||
from ._binary import o16le as o16
|
||||
from ._binary import o32le as o32
|
||||
|
||||
#
|
||||
|
@ -40,57 +41,72 @@ _MAGIC = b"\0\0\1\0"
|
|||
|
||||
def _save(im, fp, filename):
|
||||
fp.write(_MAGIC) # (2+2)
|
||||
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
|
||||
sizes = im.encoderinfo.get(
|
||||
"sizes",
|
||||
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
||||
)
|
||||
frames = []
|
||||
provided_ims = [im] + im.encoderinfo.get("append_images", [])
|
||||
width, height = im.size
|
||||
sizes = filter(
|
||||
lambda x: False
|
||||
if (x[0] > width or x[1] > height or x[0] > 256 or x[1] > 256)
|
||||
else True,
|
||||
sizes,
|
||||
)
|
||||
sizes = list(sizes)
|
||||
fp.write(struct.pack("<H", len(sizes))) # idCount(2)
|
||||
offset = fp.tell() + len(sizes) * 16
|
||||
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
|
||||
provided_images = {im.size: im for im in im.encoderinfo.get("append_images", [])}
|
||||
for size in sizes:
|
||||
width, height = size
|
||||
for size in sorted(set(sizes)):
|
||||
if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
|
||||
continue
|
||||
|
||||
for provided_im in provided_ims:
|
||||
if provided_im.size != size:
|
||||
continue
|
||||
frames.append(provided_im)
|
||||
if bmp:
|
||||
bits = BmpImagePlugin.SAVE[provided_im.mode][1]
|
||||
bits_used = [bits]
|
||||
for other_im in provided_ims:
|
||||
if other_im.size != size:
|
||||
continue
|
||||
bits = BmpImagePlugin.SAVE[other_im.mode][1]
|
||||
if bits not in bits_used:
|
||||
# Another image has been supplied for this size
|
||||
# with a different bit depth
|
||||
frames.append(other_im)
|
||||
bits_used.append(bits)
|
||||
break
|
||||
else:
|
||||
# TODO: invent a more convenient method for proportional scalings
|
||||
frame = provided_im.copy()
|
||||
frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
|
||||
frames.append(frame)
|
||||
fp.write(o16(len(frames))) # idCount(2)
|
||||
offset = fp.tell() + len(frames) * 16
|
||||
for frame in frames:
|
||||
width, height = frame.size
|
||||
# 0 means 256
|
||||
fp.write(struct.pack("B", width if width < 256 else 0)) # bWidth(1)
|
||||
fp.write(struct.pack("B", height if height < 256 else 0)) # bHeight(1)
|
||||
fp.write(b"\0") # bColorCount(1)
|
||||
fp.write(o8(width if width < 256 else 0)) # bWidth(1)
|
||||
fp.write(o8(height if height < 256 else 0)) # bHeight(1)
|
||||
|
||||
bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0)
|
||||
fp.write(o8(colors)) # bColorCount(1)
|
||||
fp.write(b"\0") # bReserved(1)
|
||||
fp.write(b"\0\0") # wPlanes(2)
|
||||
|
||||
tmp = provided_images.get(size)
|
||||
if not tmp:
|
||||
# TODO: invent a more convenient method for proportional scalings
|
||||
tmp = im.copy()
|
||||
tmp.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
|
||||
bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32
|
||||
fp.write(struct.pack("<H", bits)) # wBitCount(2)
|
||||
fp.write(o16(bits)) # wBitCount(2)
|
||||
|
||||
image_io = BytesIO()
|
||||
if bmp:
|
||||
tmp.save(image_io, "dib")
|
||||
frame.save(image_io, "dib")
|
||||
|
||||
if bits != 32:
|
||||
and_mask = Image.new("1", tmp.size)
|
||||
and_mask = Image.new("1", size)
|
||||
ImageFile._save(
|
||||
and_mask, image_io, [("raw", (0, 0) + tmp.size, 0, ("1", 0, -1))]
|
||||
and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))]
|
||||
)
|
||||
else:
|
||||
tmp.save(image_io, "png")
|
||||
frame.save(image_io, "png")
|
||||
image_io.seek(0)
|
||||
image_bytes = image_io.read()
|
||||
if bmp:
|
||||
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
|
||||
bytes_len = len(image_bytes)
|
||||
fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4)
|
||||
fp.write(struct.pack("<I", offset)) # dwImageOffset(4)
|
||||
fp.write(o32(bytes_len)) # dwBytesInRes(4)
|
||||
fp.write(o32(offset)) # dwImageOffset(4)
|
||||
current = fp.tell()
|
||||
fp.seek(offset)
|
||||
fp.write(image_bytes)
|
||||
|
@ -304,7 +320,7 @@ class IcoImageFile(ImageFile.ImageFile):
|
|||
self._size = value
|
||||
|
||||
def load(self):
|
||||
if self.im and self.im.size == self.size:
|
||||
if self.im is not None and self.im.size == self.size:
|
||||
# Already loaded
|
||||
return Image.Image.load(self)
|
||||
im = self.ico.getimage(self.size)
|
||||
|
|
|
@ -100,7 +100,7 @@ for i in range(2, 33):
|
|||
# --------------------------------------------------------------------
|
||||
# Read IM directory
|
||||
|
||||
split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
||||
split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
||||
|
||||
|
||||
def number(s):
|
||||
|
|
|
@ -49,7 +49,7 @@ except ImportError:
|
|||
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
||||
# Use __version__ instead.
|
||||
from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
|
||||
from ._binary import i32le
|
||||
from ._binary import i32le, o32be, o32le
|
||||
from ._util import deferred_error, isPath
|
||||
|
||||
|
||||
|
@ -847,7 +847,7 @@ class Image:
|
|||
:returns: An image access object.
|
||||
:rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess`
|
||||
"""
|
||||
if self.im and self.palette and self.palette.dirty:
|
||||
if self.im is not None and self.palette and self.palette.dirty:
|
||||
# realize palette
|
||||
mode, arr = self.palette.getdata()
|
||||
self.im.putpalette(mode, arr)
|
||||
|
@ -864,7 +864,7 @@ class Image:
|
|||
self.palette.mode = palette_mode
|
||||
self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
|
||||
|
||||
if self.im:
|
||||
if self.im is not None:
|
||||
if cffi and USE_CFFI_ACCESS:
|
||||
if self.pyaccess:
|
||||
return self.pyaccess
|
||||
|
@ -975,7 +975,9 @@ class Image:
|
|||
delete_trns = False
|
||||
# transparency handling
|
||||
if has_transparency:
|
||||
if self.mode in ("1", "L", "I", "RGB") and mode == "RGBA":
|
||||
if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or (
|
||||
self.mode == "RGB" and mode == "RGBA"
|
||||
):
|
||||
# Use transparent conversion to promote from transparent
|
||||
# color to an alpha channel.
|
||||
new_im = self._new(
|
||||
|
@ -1416,6 +1418,7 @@ class Image:
|
|||
"".join(self.info["Raw profile type exif"].split("\n")[3:])
|
||||
)
|
||||
elif hasattr(self, "tag_v2"):
|
||||
self._exif.bigtiff = self.tag_v2._bigtiff
|
||||
self._exif.endian = self.tag_v2._endian
|
||||
self._exif.load_from_fp(self.fp, self.tag_v2._offset)
|
||||
if exif_info is not None:
|
||||
|
@ -1492,11 +1495,12 @@ class Image:
|
|||
|
||||
def histogram(self, mask=None, extrema=None):
|
||||
"""
|
||||
Returns a histogram for the image. The histogram is returned as
|
||||
a list of pixel counts, one for each pixel value in the source
|
||||
image. If the image has more than one band, the histograms for
|
||||
all bands are concatenated (for example, the histogram for an
|
||||
"RGB" image contains 768 values).
|
||||
Returns a histogram for the image. The histogram is returned as a
|
||||
list of pixel counts, one for each pixel value in the source
|
||||
image. Counts are grouped into 256 bins for each band, even if
|
||||
the image has more than 8 bits per band. If the image has more
|
||||
than one band, the histograms for all bands are concatenated (for
|
||||
example, the histogram for an "RGB" image contains 768 values).
|
||||
|
||||
A bilevel image (mode "1") is treated as a greyscale ("L") image
|
||||
by this method.
|
||||
|
@ -1564,8 +1568,8 @@ class Image:
|
|||
also use color strings as supported by the ImageColor module.
|
||||
|
||||
If a mask is given, this method updates only the regions
|
||||
indicated by the mask. You can use either "1", "L" or "RGBA"
|
||||
images (in the latter case, the alpha band is used as mask).
|
||||
indicated by the mask. You can use either "1", "L", "LA", "RGBA"
|
||||
or "RGBa" images (if present, the alpha band is used as mask).
|
||||
Where the mask is 255, the given image is copied as is. Where
|
||||
the mask is 0, the current value is preserved. Intermediate
|
||||
values will mix the two images together, including their alpha
|
||||
|
@ -1613,7 +1617,7 @@ class Image:
|
|||
elif isImageType(im):
|
||||
im.load()
|
||||
if self.mode != im.mode:
|
||||
if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"):
|
||||
if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"):
|
||||
# should use an adapter for this!
|
||||
im = im.convert(self.mode)
|
||||
im = im.im
|
||||
|
@ -1716,6 +1720,8 @@ class Image:
|
|||
# FIXME: _imaging returns a confusing error message for this case
|
||||
raise ValueError("point operation not supported for this mode")
|
||||
|
||||
if mode != "F":
|
||||
lut = [round(i) for i in lut]
|
||||
return self._new(self.im.point(lut, mode))
|
||||
|
||||
def putalpha(self, alpha):
|
||||
|
@ -2020,6 +2026,7 @@ class Image:
|
|||
|
||||
size = tuple(size)
|
||||
|
||||
self.load()
|
||||
if box is None:
|
||||
box = (0, 0) + self.size
|
||||
else:
|
||||
|
@ -2282,7 +2289,9 @@ class Image:
|
|||
else:
|
||||
save_handler = SAVE[format.upper()]
|
||||
|
||||
created = False
|
||||
if open_fp:
|
||||
created = not os.path.exists(filename)
|
||||
if params.get("append", False):
|
||||
# Open also for reading ("+"), because TIFF save_all
|
||||
# writer needs to go back and edit the written data.
|
||||
|
@ -2292,8 +2301,15 @@ class Image:
|
|||
|
||||
try:
|
||||
save_handler(self, fp, filename)
|
||||
finally:
|
||||
# do what we can to clean up
|
||||
except Exception:
|
||||
if open_fp:
|
||||
fp.close()
|
||||
if created:
|
||||
try:
|
||||
os.remove(filename)
|
||||
except PermissionError:
|
||||
pass
|
||||
raise
|
||||
if open_fp:
|
||||
fp.close()
|
||||
|
||||
|
@ -2435,6 +2451,7 @@ class Image:
|
|||
:returns: None
|
||||
"""
|
||||
|
||||
self.load()
|
||||
x, y = map(math.floor, size)
|
||||
if x >= self.width and y >= self.height:
|
||||
return
|
||||
|
@ -2781,7 +2798,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args):
|
|||
|
||||
You can also use any pixel decoder supported by PIL. For more
|
||||
information on available decoders, see the section
|
||||
:ref:`Writing Your Own File Decoder <file-decoders>`.
|
||||
:ref:`Writing Your Own File Codec <file-codecs>`.
|
||||
|
||||
Note that this function decodes pixel data only, not entire images.
|
||||
If you have an entire image in a string, wrap it in a
|
||||
|
@ -3134,7 +3151,7 @@ def alpha_composite(im1, im2):
|
|||
def blend(im1, im2, alpha):
|
||||
"""
|
||||
Creates a new image by interpolating between two input images, using
|
||||
a constant alpha.::
|
||||
a constant alpha::
|
||||
|
||||
out = image1 * (1.0 - alpha) + image2 * alpha
|
||||
|
||||
|
@ -3423,6 +3440,7 @@ atexit.register(core.clear_cache)
|
|||
|
||||
class Exif(MutableMapping):
|
||||
endian = None
|
||||
bigtiff = False
|
||||
|
||||
def __init__(self):
|
||||
self._data = {}
|
||||
|
@ -3458,10 +3476,15 @@ class Exif(MutableMapping):
|
|||
return self._fixup_dict(info)
|
||||
|
||||
def _get_head(self):
|
||||
version = b"\x2B" if self.bigtiff else b"\x2A"
|
||||
if self.endian == "<":
|
||||
return b"II\x2A\x00\x08\x00\x00\x00"
|
||||
head = b"II" + version + b"\x00" + o32le(8)
|
||||
else:
|
||||
return b"MM\x00\x2A\x00\x00\x00\x08"
|
||||
head = b"MM\x00" + version + o32be(8)
|
||||
if self.bigtiff:
|
||||
head += o32le(8) if self.endian == "<" else o32be(8)
|
||||
head += b"\x00\x00\x00\x00"
|
||||
return head
|
||||
|
||||
def load(self, data):
|
||||
# Extract EXIF information. This is highly experimental,
|
||||
|
@ -3475,12 +3498,12 @@ class Exif(MutableMapping):
|
|||
self._loaded_exif = data
|
||||
self._data.clear()
|
||||
self._ifds.clear()
|
||||
if data and data.startswith(b"Exif\x00\x00"):
|
||||
data = data[6:]
|
||||
if not data:
|
||||
self._info = None
|
||||
return
|
||||
|
||||
if data.startswith(b"Exif\x00\x00"):
|
||||
data = data[6:]
|
||||
self.fp = io.BytesIO(data)
|
||||
self.head = self.fp.read(8)
|
||||
# process dictionary
|
||||
|
|
|
@ -223,15 +223,15 @@ class ImageFile(Image.Image):
|
|||
)
|
||||
]
|
||||
for decoder_name, extents, offset, args in self.tile:
|
||||
seek(offset)
|
||||
decoder = Image._getdecoder(
|
||||
self.mode, decoder_name, args, self.decoderconfig
|
||||
)
|
||||
try:
|
||||
seek(offset)
|
||||
decoder.setimage(self.im, extents)
|
||||
if decoder.pulls_fd:
|
||||
decoder.setfd(self.fp)
|
||||
status, err_code = decoder.decode(b"")
|
||||
err_code = decoder.decode(b"")[1]
|
||||
else:
|
||||
b = prefix
|
||||
while True:
|
||||
|
@ -499,40 +499,33 @@ def _save(im, fp, tile, bufsize=0):
|
|||
try:
|
||||
fh = fp.fileno()
|
||||
fp.flush()
|
||||
except (AttributeError, io.UnsupportedOperation) as exc:
|
||||
# compress to Python file-compatible object
|
||||
exc = None
|
||||
except (AttributeError, io.UnsupportedOperation) as e:
|
||||
exc = e
|
||||
for e, b, o, a in tile:
|
||||
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
||||
if o > 0:
|
||||
fp.seek(o)
|
||||
e.setimage(im.im, b)
|
||||
if e.pushes_fd:
|
||||
e.setfd(fp)
|
||||
l, s = e.encode_to_pyfd()
|
||||
encoder = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
||||
try:
|
||||
encoder.setimage(im.im, b)
|
||||
if encoder.pushes_fd:
|
||||
encoder.setfd(fp)
|
||||
l, s = encoder.encode_to_pyfd()
|
||||
else:
|
||||
if exc:
|
||||
# compress to Python file-compatible object
|
||||
while True:
|
||||
l, s, d = e.encode(bufsize)
|
||||
l, s, d = encoder.encode(bufsize)
|
||||
fp.write(d)
|
||||
if s:
|
||||
break
|
||||
if s < 0:
|
||||
raise OSError(f"encoder error {s} when writing image file") from exc
|
||||
e.cleanup()
|
||||
else:
|
||||
# slight speedup: compress to real file object
|
||||
for e, b, o, a in tile:
|
||||
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
||||
if o > 0:
|
||||
fp.seek(o)
|
||||
e.setimage(im.im, b)
|
||||
if e.pushes_fd:
|
||||
e.setfd(fp)
|
||||
l, s = e.encode_to_pyfd()
|
||||
else:
|
||||
s = e.encode_to_file(fh, bufsize)
|
||||
s = encoder.encode_to_file(fh, bufsize)
|
||||
if s < 0:
|
||||
raise OSError(f"encoder error {s} when writing image file")
|
||||
e.cleanup()
|
||||
raise OSError(f"encoder error {s} when writing image file") from exc
|
||||
finally:
|
||||
encoder.cleanup()
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
|
@ -671,7 +664,7 @@ class PyDecoder(PyCodec):
|
|||
|
||||
:param buffer: A bytes object with the data to be decoded.
|
||||
:returns: A tuple of ``(bytes consumed, errcode)``.
|
||||
If finished with decoding return 0 for the bytes consumed.
|
||||
If finished with decoding return -1 for the bytes consumed.
|
||||
Err codes are from :data:`.ImageFile.ERRORS`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
@ -725,6 +718,9 @@ class PyEncoder(PyCodec):
|
|||
|
||||
def encode_to_pyfd(self):
|
||||
"""
|
||||
If ``pushes_fd`` is ``True``, then this method will be used,
|
||||
and ``encode()`` will only be called once.
|
||||
|
||||
:returns: A tuple of ``(bytes consumed, errcode)``.
|
||||
Err codes are from :data:`.ImageFile.ERRORS`.
|
||||
"""
|
||||
|
|
|
@ -30,14 +30,18 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
|
|||
if sys.platform == "darwin":
|
||||
fh, filepath = tempfile.mkstemp(".png")
|
||||
os.close(fh)
|
||||
subprocess.call(["screencapture", "-x", filepath])
|
||||
args = ["screencapture"]
|
||||
if bbox:
|
||||
left, top, right, bottom = bbox
|
||||
args += ["-R", f"{left},{right},{right-left},{bottom-top}"]
|
||||
subprocess.call(args + ["-x", filepath])
|
||||
im = Image.open(filepath)
|
||||
im.load()
|
||||
os.unlink(filepath)
|
||||
if bbox:
|
||||
im_cropped = im.crop(bbox)
|
||||
im_resized = im.resize((right - left, bottom - top))
|
||||
im.close()
|
||||
return im_cropped
|
||||
return im_resized
|
||||
return im
|
||||
elif sys.platform == "win32":
|
||||
offset, size, data = Image.core.grabscreen_win32(
|
||||
|
|
|
@ -525,7 +525,7 @@ def invert(image):
|
|||
lut = []
|
||||
for i in range(256):
|
||||
lut.append(255 - i)
|
||||
return _lut(image, lut)
|
||||
return image.point(lut) if image.mode == "1" else _lut(image, lut)
|
||||
|
||||
|
||||
def mirror(image):
|
||||
|
|
|
@ -32,8 +32,6 @@ class ImagePalette:
|
|||
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):
|
||||
|
|
|
@ -270,8 +270,9 @@ class DisplayViewer(UnixViewer):
|
|||
else:
|
||||
raise TypeError("Missing required argument: 'path'")
|
||||
args = ["display"]
|
||||
if "title" in options and options["title"] is not None:
|
||||
args += ["-title", options["title"]]
|
||||
title = options.get("title")
|
||||
if title:
|
||||
args += ["-title", title]
|
||||
args.append(path)
|
||||
|
||||
subprocess.Popen(args)
|
||||
|
@ -368,8 +369,9 @@ class XVViewer(UnixViewer):
|
|||
else:
|
||||
raise TypeError("Missing required argument: 'path'")
|
||||
args = ["xv"]
|
||||
if "title" in options:
|
||||
args += ["-name", options["title"]]
|
||||
title = options.get("title")
|
||||
if title:
|
||||
args += ["-name", title]
|
||||
args.append(path)
|
||||
|
||||
subprocess.Popen(args)
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#
|
||||
|
||||
import tkinter
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
|
||||
from . import Image
|
||||
|
@ -58,6 +59,33 @@ def _get_image_from_kw(kw):
|
|||
return Image.open(source)
|
||||
|
||||
|
||||
def _pyimagingtkcall(command, photo, id):
|
||||
tk = photo.tk
|
||||
try:
|
||||
tk.call(command, photo, id)
|
||||
except tkinter.TclError:
|
||||
# activate Tkinter hook
|
||||
# may raise an error if it cannot attach to Tkinter
|
||||
from . import _imagingtk
|
||||
|
||||
try:
|
||||
if hasattr(tk, "interp"):
|
||||
# Required for PyPy, which always has CFFI installed
|
||||
from cffi import FFI
|
||||
|
||||
ffi = FFI()
|
||||
|
||||
# PyPy is using an FFI CDATA element
|
||||
# (Pdb) self.tk.interp
|
||||
# <cdata 'Tcl_Interp *' 0x3061b50>
|
||||
_imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
|
||||
else:
|
||||
_imagingtk.tkinit(tk.interpaddr(), 1)
|
||||
except AttributeError:
|
||||
_imagingtk.tkinit(id(tk), 0)
|
||||
tk.call(command, photo, id)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# PhotoImage
|
||||
|
||||
|
@ -156,11 +184,15 @@ class PhotoImage:
|
|||
:param im: A PIL image. The size must match the target region. If the
|
||||
mode does not match, the image is converted to the mode of
|
||||
the bitmap image.
|
||||
:param box: A 4-tuple defining the left, upper, right, and lower pixel
|
||||
coordinate. See :ref:`coordinate-system`. If None is given
|
||||
instead of a tuple, all of the image is assumed.
|
||||
"""
|
||||
|
||||
if box is not None:
|
||||
warnings.warn(
|
||||
"The box parameter is deprecated and will be removed in Pillow 10 "
|
||||
"(2023-07-01).",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
# convert to blittable
|
||||
im.load()
|
||||
image = im.im
|
||||
|
@ -170,33 +202,7 @@ class PhotoImage:
|
|||
block = image.new_block(self.__mode, im.size)
|
||||
image.convert2(block, image) # convert directly between buffers
|
||||
|
||||
tk = self.__photo.tk
|
||||
|
||||
try:
|
||||
tk.call("PyImagingPhoto", self.__photo, block.id)
|
||||
except tkinter.TclError:
|
||||
# activate Tkinter hook
|
||||
try:
|
||||
from . import _imagingtk
|
||||
|
||||
try:
|
||||
if hasattr(tk, "interp"):
|
||||
# Required for PyPy, which always has CFFI installed
|
||||
from cffi import FFI
|
||||
|
||||
ffi = FFI()
|
||||
|
||||
# PyPy is using an FFI CDATA element
|
||||
# (Pdb) self.tk.interp
|
||||
# <cdata 'Tcl_Interp *' 0x3061b50>
|
||||
_imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
|
||||
else:
|
||||
_imagingtk.tkinit(tk.interpaddr(), 1)
|
||||
except AttributeError:
|
||||
_imagingtk.tkinit(id(tk), 0)
|
||||
tk.call("PyImagingPhoto", self.__photo, block.id)
|
||||
except (ImportError, AttributeError, tkinter.TclError):
|
||||
raise # configuration problem; cannot attach to Tkinter
|
||||
_pyimagingtkcall("PyImagingPhoto", self.__photo, block.id)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
@ -276,7 +282,7 @@ def getimage(photo):
|
|||
im = Image.new("RGBA", (photo.width(), photo.height()))
|
||||
block = im.im
|
||||
|
||||
photo.tk.call("PyImagingPhotoGet", photo, block.id)
|
||||
_pyimagingtkcall("PyImagingPhotoGet", photo, block.id)
|
||||
|
||||
return im
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ from . import Image, ImageFile
|
|||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
field = re.compile(br"([a-z]*) ([^ \r\n]*)")
|
||||
field = re.compile(rb"([a-z]*) ([^ \r\n]*)")
|
||||
|
||||
|
||||
##
|
||||
|
|
|
@ -290,14 +290,14 @@ def _accept(prefix):
|
|||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if filename.endswith(".j2k"):
|
||||
# Get the keyword arguments
|
||||
info = im.encoderinfo
|
||||
|
||||
if filename.endswith(".j2k") or info.get("no_jp2", False):
|
||||
kind = "j2k"
|
||||
else:
|
||||
kind = "jp2"
|
||||
|
||||
# Get the keyword arguments
|
||||
info = im.encoderinfo
|
||||
|
||||
offset = info.get("offset", None)
|
||||
tile_offset = info.get("tile_offset", None)
|
||||
tile_size = info.get("tile_size", None)
|
||||
|
@ -320,6 +320,7 @@ def _save(im, fp, filename):
|
|||
irreversible = info.get("irreversible", False)
|
||||
progression = info.get("progression", "LRCP")
|
||||
cinema_mode = info.get("cinema_mode", "no")
|
||||
mct = info.get("mct", 0)
|
||||
fd = -1
|
||||
|
||||
if hasattr(fp, "fileno"):
|
||||
|
@ -340,6 +341,7 @@ def _save(im, fp, filename):
|
|||
irreversible,
|
||||
progression,
|
||||
cinema_mode,
|
||||
mct,
|
||||
fd,
|
||||
)
|
||||
|
||||
|
|
|
@ -626,6 +626,8 @@ def get_sampling(im):
|
|||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if im.width == 0 or im.height == 0:
|
||||
raise ValueError("cannot write empty image as JPEG")
|
||||
|
||||
try:
|
||||
rawmode = RAWMODE[im.mode]
|
||||
|
|
|
@ -148,7 +148,7 @@ class MspDecoder(ImageFile.PyDecoder):
|
|||
|
||||
self.set_as_raw(img.getvalue(), ("1", 0, 1))
|
||||
|
||||
return 0, 0
|
||||
return -1, 0
|
||||
|
||||
|
||||
Image.register_decoder("MSP", MspDecoder)
|
||||
|
|
|
@ -127,7 +127,6 @@ def _save(im, fp, filename, save_all=False):
|
|||
filter = "DCTDecode"
|
||||
colorspace = PdfParser.PdfName("DeviceGray")
|
||||
procset = "ImageB" # grayscale
|
||||
bits = 1
|
||||
elif im.mode == "L":
|
||||
filter = "DCTDecode"
|
||||
# params = f"<< /Predictor 15 /Columns {width-2} >>"
|
||||
|
|
|
@ -576,42 +576,42 @@ class PdfParser:
|
|||
self.xref_table[reference.object_id] = (offset, 0)
|
||||
return reference
|
||||
|
||||
delimiter = br"[][()<>{}/%]"
|
||||
delimiter_or_ws = br"[][()<>{}/%\000\011\012\014\015\040]"
|
||||
whitespace = br"[\000\011\012\014\015\040]"
|
||||
whitespace_or_hex = br"[\000\011\012\014\015\0400-9a-fA-F]"
|
||||
delimiter = rb"[][()<>{}/%]"
|
||||
delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]"
|
||||
whitespace = rb"[\000\011\012\014\015\040]"
|
||||
whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]"
|
||||
whitespace_optional = whitespace + b"*"
|
||||
whitespace_mandatory = whitespace + b"+"
|
||||
# No "\012" aka "\n" or "\015" aka "\r":
|
||||
whitespace_optional_no_nl = br"[\000\011\014\040]*"
|
||||
newline_only = br"[\r\n]+"
|
||||
whitespace_optional_no_nl = rb"[\000\011\014\040]*"
|
||||
newline_only = rb"[\r\n]+"
|
||||
newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl
|
||||
re_trailer_end = re.compile(
|
||||
whitespace_mandatory
|
||||
+ br"trailer"
|
||||
+ rb"trailer"
|
||||
+ whitespace_optional
|
||||
+ br"\<\<(.*\>\>)"
|
||||
+ rb"\<\<(.*\>\>)"
|
||||
+ newline
|
||||
+ br"startxref"
|
||||
+ rb"startxref"
|
||||
+ newline
|
||||
+ br"([0-9]+)"
|
||||
+ rb"([0-9]+)"
|
||||
+ newline
|
||||
+ br"%%EOF"
|
||||
+ rb"%%EOF"
|
||||
+ whitespace_optional
|
||||
+ br"$",
|
||||
+ rb"$",
|
||||
re.DOTALL,
|
||||
)
|
||||
re_trailer_prev = re.compile(
|
||||
whitespace_optional
|
||||
+ br"trailer"
|
||||
+ rb"trailer"
|
||||
+ whitespace_optional
|
||||
+ br"\<\<(.*?\>\>)"
|
||||
+ rb"\<\<(.*?\>\>)"
|
||||
+ newline
|
||||
+ br"startxref"
|
||||
+ rb"startxref"
|
||||
+ newline
|
||||
+ br"([0-9]+)"
|
||||
+ rb"([0-9]+)"
|
||||
+ newline
|
||||
+ br"%%EOF"
|
||||
+ rb"%%EOF"
|
||||
+ whitespace_optional,
|
||||
re.DOTALL,
|
||||
)
|
||||
|
@ -655,12 +655,12 @@ class PdfParser:
|
|||
re_whitespace_optional = re.compile(whitespace_optional)
|
||||
re_name = re.compile(
|
||||
whitespace_optional
|
||||
+ br"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?="
|
||||
+ rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?="
|
||||
+ delimiter_or_ws
|
||||
+ br")"
|
||||
+ rb")"
|
||||
)
|
||||
re_dict_start = re.compile(whitespace_optional + br"\<\<")
|
||||
re_dict_end = re.compile(whitespace_optional + br"\>\>" + whitespace_optional)
|
||||
re_dict_start = re.compile(whitespace_optional + rb"\<\<")
|
||||
re_dict_end = re.compile(whitespace_optional + rb"\>\>" + whitespace_optional)
|
||||
|
||||
@classmethod
|
||||
def interpret_trailer(cls, trailer_data):
|
||||
|
@ -689,7 +689,7 @@ class PdfParser:
|
|||
)
|
||||
return trailer
|
||||
|
||||
re_hashes_in_name = re.compile(br"([^#]*)(#([0-9a-fA-F]{2}))?")
|
||||
re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?")
|
||||
|
||||
@classmethod
|
||||
def interpret_name(cls, raw, as_text=False):
|
||||
|
@ -704,53 +704,53 @@ class PdfParser:
|
|||
else:
|
||||
return bytes(name)
|
||||
|
||||
re_null = re.compile(whitespace_optional + br"null(?=" + delimiter_or_ws + br")")
|
||||
re_true = re.compile(whitespace_optional + br"true(?=" + delimiter_or_ws + br")")
|
||||
re_false = re.compile(whitespace_optional + br"false(?=" + delimiter_or_ws + br")")
|
||||
re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")")
|
||||
re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")")
|
||||
re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")")
|
||||
re_int = re.compile(
|
||||
whitespace_optional + br"([-+]?[0-9]+)(?=" + delimiter_or_ws + br")"
|
||||
whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")"
|
||||
)
|
||||
re_real = re.compile(
|
||||
whitespace_optional
|
||||
+ br"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?="
|
||||
+ rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?="
|
||||
+ delimiter_or_ws
|
||||
+ br")"
|
||||
+ rb")"
|
||||
)
|
||||
re_array_start = re.compile(whitespace_optional + br"\[")
|
||||
re_array_end = re.compile(whitespace_optional + br"]")
|
||||
re_array_start = re.compile(whitespace_optional + rb"\[")
|
||||
re_array_end = re.compile(whitespace_optional + rb"]")
|
||||
re_string_hex = re.compile(
|
||||
whitespace_optional + br"\<(" + whitespace_or_hex + br"*)\>"
|
||||
whitespace_optional + rb"\<(" + whitespace_or_hex + rb"*)\>"
|
||||
)
|
||||
re_string_lit = re.compile(whitespace_optional + br"\(")
|
||||
re_string_lit = re.compile(whitespace_optional + rb"\(")
|
||||
re_indirect_reference = re.compile(
|
||||
whitespace_optional
|
||||
+ br"([-+]?[0-9]+)"
|
||||
+ rb"([-+]?[0-9]+)"
|
||||
+ whitespace_mandatory
|
||||
+ br"([-+]?[0-9]+)"
|
||||
+ rb"([-+]?[0-9]+)"
|
||||
+ whitespace_mandatory
|
||||
+ br"R(?="
|
||||
+ rb"R(?="
|
||||
+ delimiter_or_ws
|
||||
+ br")"
|
||||
+ rb")"
|
||||
)
|
||||
re_indirect_def_start = re.compile(
|
||||
whitespace_optional
|
||||
+ br"([-+]?[0-9]+)"
|
||||
+ rb"([-+]?[0-9]+)"
|
||||
+ whitespace_mandatory
|
||||
+ br"([-+]?[0-9]+)"
|
||||
+ rb"([-+]?[0-9]+)"
|
||||
+ whitespace_mandatory
|
||||
+ br"obj(?="
|
||||
+ rb"obj(?="
|
||||
+ delimiter_or_ws
|
||||
+ br")"
|
||||
+ rb")"
|
||||
)
|
||||
re_indirect_def_end = re.compile(
|
||||
whitespace_optional + br"endobj(?=" + delimiter_or_ws + br")"
|
||||
whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")"
|
||||
)
|
||||
re_comment = re.compile(
|
||||
br"(" + whitespace_optional + br"%[^\r\n]*" + newline + br")*"
|
||||
rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*"
|
||||
)
|
||||
re_stream_start = re.compile(whitespace_optional + br"stream\r?\n")
|
||||
re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n")
|
||||
re_stream_end = re.compile(
|
||||
whitespace_optional + br"endstream(?=" + delimiter_or_ws + br")"
|
||||
whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -876,7 +876,7 @@ class PdfParser:
|
|||
raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32]))
|
||||
|
||||
re_lit_str_token = re.compile(
|
||||
br"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))"
|
||||
rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))"
|
||||
)
|
||||
escaped_chars = {
|
||||
b"n": b"\n",
|
||||
|
@ -922,16 +922,16 @@ class PdfParser:
|
|||
offset = m.end()
|
||||
raise PdfFormatError("unfinished literal string")
|
||||
|
||||
re_xref_section_start = re.compile(whitespace_optional + br"xref" + newline)
|
||||
re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline)
|
||||
re_xref_subsection_start = re.compile(
|
||||
whitespace_optional
|
||||
+ br"([0-9]+)"
|
||||
+ rb"([0-9]+)"
|
||||
+ whitespace_mandatory
|
||||
+ br"([0-9]+)"
|
||||
+ rb"([0-9]+)"
|
||||
+ whitespace_optional
|
||||
+ newline_only
|
||||
)
|
||||
re_xref_entry = re.compile(br"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
|
||||
re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
|
||||
|
||||
def read_xref_table(self, xref_section_offset):
|
||||
subsection_found = False
|
||||
|
|
|
@ -48,7 +48,7 @@ from ._binary import o32be as o32
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
is_cid = re.compile(br"\w\w\w\w").match
|
||||
is_cid = re.compile(rb"\w\w\w\w").match
|
||||
|
||||
|
||||
_MAGIC = b"\211PNG\r\n\032\n"
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i16be as i16
|
||||
from ._binary import o8
|
||||
from ._binary import o32le as o32
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
@ -102,6 +105,7 @@ class PpmImageFile(ImageFile.ImageFile):
|
|||
else:
|
||||
self.mode = rawmode = mode
|
||||
|
||||
decoder_name = "raw"
|
||||
for ix in range(3):
|
||||
token = int(self._read_token())
|
||||
if ix == 0: # token is the x size
|
||||
|
@ -112,18 +116,44 @@ class PpmImageFile(ImageFile.ImageFile):
|
|||
break
|
||||
elif ix == 2: # token is maxval
|
||||
maxval = token
|
||||
if maxval > 255:
|
||||
if not mode == "L":
|
||||
raise ValueError(f"Too many colors for band: {token}")
|
||||
if maxval < 2 ** 16:
|
||||
if maxval > 255 and mode == "L":
|
||||
self.mode = "I"
|
||||
|
||||
# If maxval matches a bit depth, use the raw decoder directly
|
||||
if maxval == 65535 and mode == "L":
|
||||
rawmode = "I;16B"
|
||||
else:
|
||||
self.mode = "I"
|
||||
rawmode = "I;32B"
|
||||
elif maxval != 255:
|
||||
decoder_name = "ppm"
|
||||
args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval)
|
||||
|
||||
self._size = xsize, ysize
|
||||
self.tile = [("raw", (0, 0, xsize, ysize), self.fp.tell(), (rawmode, 0, 1))]
|
||||
self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
|
||||
|
||||
|
||||
class PpmDecoder(ImageFile.PyDecoder):
|
||||
_pulls_fd = True
|
||||
|
||||
def decode(self, buffer):
|
||||
data = bytearray()
|
||||
maxval = min(self.args[-1], 65535)
|
||||
in_byte_count = 1 if maxval < 256 else 2
|
||||
out_byte_count = 4 if self.mode == "I" else 1
|
||||
out_max = 65535 if self.mode == "I" else 255
|
||||
bands = Image.getmodebands(self.mode)
|
||||
while len(data) < self.state.xsize * self.state.ysize * bands * out_byte_count:
|
||||
pixels = self.fd.read(in_byte_count * bands)
|
||||
if len(pixels) < in_byte_count * bands:
|
||||
# eof
|
||||
break
|
||||
for b in range(bands):
|
||||
value = (
|
||||
pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count)
|
||||
)
|
||||
value = min(out_max, round(value / maxval * out_max))
|
||||
data += o32(value) if self.mode == "I" else o8(value)
|
||||
rawmode = "I;32" if self.mode == "I" else self.mode
|
||||
self.set_as_raw(bytes(data), (rawmode, 0, 1))
|
||||
return -1, 0
|
||||
|
||||
|
||||
#
|
||||
|
@ -136,26 +166,19 @@ def _save(im, fp, filename):
|
|||
elif im.mode == "L":
|
||||
rawmode, head = "L", b"P5"
|
||||
elif im.mode == "I":
|
||||
if im.getextrema()[1] < 2 ** 16:
|
||||
rawmode, head = "I;16B", b"P5"
|
||||
else:
|
||||
rawmode, head = "I;32B", b"P5"
|
||||
elif im.mode == "RGB":
|
||||
rawmode, head = "RGB", b"P6"
|
||||
elif im.mode == "RGBA":
|
||||
elif im.mode in ("RGB", "RGBA"):
|
||||
rawmode, head = "RGB", b"P6"
|
||||
else:
|
||||
raise OSError(f"cannot write mode {im.mode} as PPM")
|
||||
fp.write(head + ("\n%d %d\n" % im.size).encode("ascii"))
|
||||
fp.write(head + b"\n%d %d\n" % im.size)
|
||||
if head == b"P6":
|
||||
fp.write(b"255\n")
|
||||
if head == b"P5":
|
||||
elif head == b"P5":
|
||||
if rawmode == "L":
|
||||
fp.write(b"255\n")
|
||||
elif rawmode == "I;16B":
|
||||
else:
|
||||
fp.write(b"65535\n")
|
||||
elif rawmode == "I;32B":
|
||||
fp.write(b"2147483648\n")
|
||||
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
|
||||
|
||||
# ALTERNATIVE: save via builtin debug function
|
||||
|
@ -169,6 +192,8 @@ def _save(im, fp, filename):
|
|||
Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
|
||||
Image.register_save(PpmImageFile.format, _save)
|
||||
|
||||
Image.register_decoder("ppm", PpmDecoder)
|
||||
|
||||
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
|
||||
|
||||
Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")
|
||||
|
|
|
@ -155,14 +155,6 @@ class PsdImageFile(ImageFile.ImageFile):
|
|||
# return layer number (0=image, 1..max=layers)
|
||||
return self.frame
|
||||
|
||||
def load_prepare(self):
|
||||
# create image memory if necessary
|
||||
if not self.im or self.im.mode != self.mode or self.im.size != self.size:
|
||||
self.im = Image.core.fill(self.mode, self.size, 0)
|
||||
# create palette (optional)
|
||||
if self.mode == "P":
|
||||
Image.Image.load(self)
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
|
|
|
@ -175,6 +175,7 @@ OPEN_INFO = {
|
|||
(II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
|
||||
(II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"),
|
||||
(MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"),
|
||||
(II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"),
|
||||
(II, 1, (2,), 1, (16,), ()): ("I", "I;16S"),
|
||||
(MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"),
|
||||
(II, 0, (3,), 1, (32,), ()): ("F", "F;32F"),
|
||||
|
@ -260,6 +261,8 @@ PREFIXES = [
|
|||
b"II\x2A\x00", # Valid TIFF header with little-endian byte order
|
||||
b"MM\x2A\x00", # Invalid TIFF header, assume big-endian
|
||||
b"II\x00\x2A", # Invalid TIFF header, assume little-endian
|
||||
b"MM\x00\x2B", # BigTIFF with big-endian byte order
|
||||
b"II\x2B\x00", # BigTIFF with little-endian byte order
|
||||
]
|
||||
|
||||
|
||||
|
@ -493,7 +496,7 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
endianness.
|
||||
:param prefix: Override the endianness of the file.
|
||||
"""
|
||||
if ifh[:4] not in PREFIXES:
|
||||
if not _accept(ifh):
|
||||
raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)")
|
||||
self._prefix = prefix if prefix is not None else ifh[:2]
|
||||
if self._prefix == MM:
|
||||
|
@ -502,11 +505,14 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
self._endian = "<"
|
||||
else:
|
||||
raise SyntaxError("not a TIFF IFD")
|
||||
self._bigtiff = ifh[2] == 43
|
||||
self.group = group
|
||||
self.tagtype = {}
|
||||
""" Dictionary of tag types """
|
||||
self.reset()
|
||||
(self.next,) = self._unpack("L", ifh[4:])
|
||||
(self.next,) = (
|
||||
self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:])
|
||||
)
|
||||
self._legacy_api = False
|
||||
|
||||
prefix = property(lambda self: self._prefix)
|
||||
|
@ -699,6 +705,7 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
(TiffTags.FLOAT, "f", "float"),
|
||||
(TiffTags.DOUBLE, "d", "double"),
|
||||
(TiffTags.IFD, "L", "long"),
|
||||
(TiffTags.LONG8, "Q", "long8"),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
@ -776,8 +783,17 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
self._offset = fp.tell()
|
||||
|
||||
try:
|
||||
for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]):
|
||||
tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12))
|
||||
tag_count = (
|
||||
self._unpack("Q", self._ensure_read(fp, 8))
|
||||
if self._bigtiff
|
||||
else self._unpack("H", self._ensure_read(fp, 2))
|
||||
)[0]
|
||||
for i in range(tag_count):
|
||||
tag, typ, count, data = (
|
||||
self._unpack("HHQ8s", self._ensure_read(fp, 20))
|
||||
if self._bigtiff
|
||||
else self._unpack("HHL4s", self._ensure_read(fp, 12))
|
||||
)
|
||||
|
||||
tagname = TiffTags.lookup(tag, self.group).name
|
||||
typname = TYPES.get(typ, "unknown")
|
||||
|
@ -789,9 +805,9 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
logger.debug(msg + f" - unsupported type {typ}")
|
||||
continue # ignore unsupported type
|
||||
size = count * unit_size
|
||||
if size > 4:
|
||||
if size > (8 if self._bigtiff else 4):
|
||||
here = fp.tell()
|
||||
(offset,) = self._unpack("L", data)
|
||||
(offset,) = self._unpack("Q" if self._bigtiff else "L", data)
|
||||
msg += f" Tag Location: {here} - Data Location: {offset}"
|
||||
fp.seek(offset)
|
||||
data = ImageFile._safe_read(fp, size)
|
||||
|
@ -820,7 +836,11 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
)
|
||||
logger.debug(msg)
|
||||
|
||||
(self.next,) = self._unpack("L", self._ensure_read(fp, 4))
|
||||
(self.next,) = (
|
||||
self._unpack("Q", self._ensure_read(fp, 8))
|
||||
if self._bigtiff
|
||||
else self._unpack("L", self._ensure_read(fp, 4))
|
||||
)
|
||||
except OSError as msg:
|
||||
warnings.warn(str(msg))
|
||||
return
|
||||
|
@ -1042,6 +1062,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
|
||||
# Header
|
||||
ifh = self.fp.read(8)
|
||||
if ifh[2] == 43:
|
||||
ifh += self.fp.read(8)
|
||||
|
||||
self.tag_v2 = ImageFileDirectory_v2(ifh)
|
||||
|
||||
|
@ -1558,7 +1580,7 @@ def _save(im, fp, filename):
|
|||
libtiff = WRITE_LIBTIFF or compression != "raw"
|
||||
|
||||
# required for color libtiff images
|
||||
ifd[PLANAR_CONFIGURATION] = getattr(im, "_planar_configuration", 1)
|
||||
ifd[PLANAR_CONFIGURATION] = 1
|
||||
|
||||
ifd[IMAGEWIDTH] = im.size[0]
|
||||
ifd[IMAGELENGTH] = im.size[1]
|
||||
|
|
|
@ -74,6 +74,7 @@ SIGNED_RATIONAL = 10
|
|||
FLOAT = 11
|
||||
DOUBLE = 12
|
||||
IFD = 13
|
||||
LONG8 = 16
|
||||
|
||||
TAGS_V2 = {
|
||||
254: ("NewSubfileType", LONG, 1),
|
||||
|
|
|
@ -190,9 +190,11 @@ def _save_all(im, fp, filename):
|
|||
palette = im.getpalette()
|
||||
if palette:
|
||||
r, g, b = palette[background * 3 : (background + 1) * 3]
|
||||
background = (r, g, b, 0)
|
||||
background = (r, g, b, 255)
|
||||
else:
|
||||
background = (background, background, background, 255)
|
||||
|
||||
duration = im.encoderinfo.get("duration", im.info.get("duration"))
|
||||
duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
|
||||
loop = im.encoderinfo.get("loop", 0)
|
||||
minimize_size = im.encoderinfo.get("minimize_size", False)
|
||||
kmin = im.encoderinfo.get("kmin", None)
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i16le as word
|
||||
from ._binary import i32le as dword
|
||||
from ._binary import si16le as short
|
||||
from ._binary import si32le as _long
|
||||
|
||||
|
@ -112,7 +111,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
|
|||
if s[22:26] != b"\x01\x00\t\x00":
|
||||
raise SyntaxError("Unsupported WMF file format")
|
||||
|
||||
elif dword(s) == 1 and s[40:44] == b" EMF":
|
||||
elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF":
|
||||
# enhanced metafile
|
||||
|
||||
# get bounding box
|
||||
|
|