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 | ||||
|  |  | |||