diff --git a/.ci/install.sh b/.ci/install.sh index 9372d0c51..4917b3a7c 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -27,6 +27,7 @@ python3 -m pip install coverage python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov +python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install test-image-results # TODO Remove condition when numpy supports 3.10 diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml new file mode 100644 index 000000000..9fe8f774f --- /dev/null +++ b/.github/workflows/cifuzz.yml @@ -0,0 +1,47 @@ +name: CIFuzz +on: + push: + paths: + - "**.c" + - "**.h" + pull_request: + paths: + - "**.c" + - "**.h" + +jobs: + Fuzzing: + runs-on: ubuntu-latest + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'pillow' + language: python + dry-run: false + - name: Run Fuzzers + id: run + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'pillow' + fuzz-seconds: 600 + language: python + dry-run: false + - name: Upload New Crash + uses: actions/upload-artifact@v2 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts + - name: Upload Legacy Crash + uses: actions/upload-artifact@v2 + if: steps.run.outcome == 'success' + with: + name: crash + path: ./out/crash* + - name: Fail on legacy crash + if: success() + run: | + [ ! -e out/crash-* ] + echo No legacy crash detected diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index afcb9a5a7..f45824445 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -9,6 +9,7 @@ python3 -m pip install coverage python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov +python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install test-image-results diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml new file mode 100644 index 000000000..7b8474d0f --- /dev/null +++ b/.github/workflows/test-valgrind.yml @@ -0,0 +1,52 @@ +name: Test Valgrind + +# like the docker tests, but running valgrind only on *.c/*.h changes. + +on: + push: + paths: + - "**.c" + - "**.h" + pull_request: + paths: + - "**.c" + - "**.h" + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + docker: [ + ubuntu-20.04-focal-amd64-valgrind, + ] + dockerTag: [master] + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v2 + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Docker pull + run: | + docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + + - name: Build and Run Valgrind + run: | + # The Pillow user in the docker container is UID 1000 + sudo chown -R 1000 $GITHUB_WORKSPACE + docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + sudo chown -R runner $GITHUB_WORKSPACE + + success: + needs: build + runs-on: ubuntu-latest + name: Valgrind Test Successful + steps: + - name: Success + run: echo Valgrind Test Successful diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f3bb85f32..d53daec13 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -57,8 +57,8 @@ jobs: - name: Print build system information run: python .github/workflows/system-info.py - - name: python -m pip install wheel pytest pytest-cov - run: python -m pip install wheel pytest pytest-cov + - name: python -m pip install wheel pytest pytest-cov pytest-timeout + run: python -m pip install wheel pytest pytest-cov pytest-timeout # TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: - name: Upgrade setuptools @@ -110,7 +110,7 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libwebp.cmd" - # for FreeType CBDT font support + # for FreeType CBDT/SBIX font support - name: Build dependencies / libpng if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" @@ -174,7 +174,7 @@ jobs: if: failure() run: | mkdir -p Tests/errors - shell: pwsh + shell: bash - name: Upload errors uses: actions/upload-artifact@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4064a0589..e52fefc69 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,7 +92,6 @@ jobs: if: failure() run: | mkdir -p Tests/errors - shell: pwsh - name: Upload errors uses: actions/upload-artifact@v2 diff --git a/CHANGES.rst b/CHANGES.rst index b4d6b5348..dbe06304d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,36 @@ Changelog (Pillow) 8.2.0 (unreleased) ------------------ +- Save ICC profile from TIFF encoderinfo #5321 + [radarhere] + +- Moved RGB fix inside ImageQt class #5268 + [radarhere] + +- Allow alpha_composite destination to be negative #5313 + [radarhere] + +- Ensure file is closed if it is opened by ImageQt.ImageQt #5260 + [radarhere] + +- Added ImageDraw rounded_rectangle method #5208 + [radarhere] + +- Added IPythonViewer #5289 + [radarhere, Kipkurui-mutai] + +- Only draw each rectangle outline pixel once #5183 + [radarhere] + +- Use mmap instead of built-in Win32 mapper #5224 + [radarhere, cgohlke] + +- Handle PCX images with an odd stride #5214 + [radarhere] + +- Only read different sizes for "Large Thumbnail" MPO frames #5168 + [radarhere] + - Added PyQt6 support #5258 [radarhere] @@ -20,13 +50,37 @@ Changelog (Pillow) - Support for ignoring tests when running valgrind #5150 [wiredfool, radarhere, hugovk] -- PyModule_AddObject fix for Python 3.10 #5194 - [radarhere] - - OSS-Fuzz support #5189 [wiredfool, radarhere] -8.1.0 (2020-01-02) +8.1.2 (2021-03-06) +------------------ + +- Fix Memory DOS in BLP (CVE-2021-27921), ICNS (CVE-2021-27922) and ICO (CVE-2021-27923) Image Plugins + [wiredfool] + +8.1.1 (2021-03-01) +------------------ + +- Use more specific regex chars to prevent ReDoS. CVE-2021-25292 + [hugovk] + +- Fix OOB Read in TiffDecode.c, and check the tile validity before reading. CVE-2021-25291 + [wiredfool] + +- Fix negative size read in TiffDecode.c. CVE-2021-25290 + [wiredfool] + +- Fix OOB read in SgiRleDecode.c. CVE-2021-25293 + [wiredfool] + +- Incorrect error code checking in TiffDecode.c. CVE-2021-25289 + [wiredfool] + +- PyModule_AddObject fix for Python 3.10 #5194 + [radarhere] + +8.1.0 (2021-01-02) ------------------ - Fix TIFF OOB Write error. CVE-2020-35654 #5175 diff --git a/Tests/fonts/DejaVuSans-24-1-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-1-stripped.ttf similarity index 100% rename from Tests/fonts/DejaVuSans-24-1-stripped.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans-24-1-stripped.ttf diff --git a/Tests/fonts/DejaVuSans-24-2-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-2-stripped.ttf similarity index 100% rename from Tests/fonts/DejaVuSans-24-2-stripped.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans-24-2-stripped.ttf diff --git a/Tests/fonts/DejaVuSans-24-4-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-4-stripped.ttf similarity index 100% rename from Tests/fonts/DejaVuSans-24-4-stripped.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans-24-4-stripped.ttf diff --git a/Tests/fonts/DejaVuSans-24-8-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf similarity index 100% rename from Tests/fonts/DejaVuSans-24-8-stripped.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf diff --git a/Tests/fonts/DejaVuSans.ttf b/Tests/fonts/DejaVuSans/DejaVuSans.ttf similarity index 100% rename from Tests/fonts/DejaVuSans.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans.ttf diff --git a/Tests/fonts/DejaVuSans/LICENSE.txt b/Tests/fonts/DejaVuSans/LICENSE.txt new file mode 100644 index 000000000..30516578f --- /dev/null +++ b/Tests/fonts/DejaVuSans/LICENSE.txt @@ -0,0 +1,40 @@ +DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range. + +DejaVu Fonts — License +Fonts are © Bitstream (see below). DejaVu changes are in public domain. Explanation of copyright is on Gnome page on Bitstream Vera fonts. Glyphs imported from Arev fonts are © Tavmjung Bah (see below) + +Bitstream Vera Fonts Copyright +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". + +This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. + +The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. + +Arev Fonts Copyright +Original text + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the modifications to the Bitstream Vera Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr. \ No newline at end of file diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index 06eaa9a4e..104ff677c 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -15,8 +15,11 @@ FreeMono.ttf is licensed under GPLv3, with the GPL font exception. OpenSansCondensed-LightItalic.tt, from https://fonts.google.com/specimen/Open+Sans, under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) -DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range. +chromacheck-sbix.woff, from https://github.com/RoelN/ChromaCheck, under The MIT License (MIT), Copyright (c) 2018 Roel Nieskens, https://pixelambacht.nl Copyright (c) 2018 Google LLC +KhmerOSBattambang-Regular.ttf is licensed under LGPL-2.1 or later. + +FreeMono.ttf is licensed under GPLv3. 10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base diff --git a/Tests/fonts/chromacheck-sbix.woff b/Tests/fonts/chromacheck-sbix.woff new file mode 100644 index 000000000..518d4b7ea Binary files /dev/null and b/Tests/fonts/chromacheck-sbix.woff differ diff --git a/Tests/images/chromacheck-sbix.png b/Tests/images/chromacheck-sbix.png new file mode 100644 index 000000000..b906ef133 Binary files /dev/null and b/Tests/images/chromacheck-sbix.png differ diff --git a/Tests/images/chromacheck-sbix_mask.png b/Tests/images/chromacheck-sbix_mask.png new file mode 100644 index 000000000..4b68ff91b Binary files /dev/null and b/Tests/images/chromacheck-sbix_mask.png differ diff --git a/Tests/images/crash-0c7e0e8e11ce787078f00b5b0ca409a167f070e0.tif b/Tests/images/crash-0c7e0e8e11ce787078f00b5b0ca409a167f070e0.tif new file mode 100644 index 000000000..5275075e9 Binary files /dev/null and b/Tests/images/crash-0c7e0e8e11ce787078f00b5b0ca409a167f070e0.tif differ diff --git a/Tests/images/crash-0e16d3bfb83be87356d026d66919deaefca44dac.tif b/Tests/images/crash-0e16d3bfb83be87356d026d66919deaefca44dac.tif new file mode 100644 index 000000000..f59aab21a Binary files /dev/null and b/Tests/images/crash-0e16d3bfb83be87356d026d66919deaefca44dac.tif differ diff --git a/Tests/images/crash-1152ec2d1a1a71395b6f2ce6721c38924d025bf3.tif b/Tests/images/crash-1152ec2d1a1a71395b6f2ce6721c38924d025bf3.tif new file mode 100644 index 000000000..c8d6e2aad Binary files /dev/null and b/Tests/images/crash-1152ec2d1a1a71395b6f2ce6721c38924d025bf3.tif differ diff --git a/Tests/images/crash-1185209cf7655b5aed8ae5e77784dfdd18ab59e9.tif b/Tests/images/crash-1185209cf7655b5aed8ae5e77784dfdd18ab59e9.tif new file mode 100644 index 000000000..ecf7db38f Binary files /dev/null and b/Tests/images/crash-1185209cf7655b5aed8ae5e77784dfdd18ab59e9.tif differ diff --git a/Tests/images/crash-338516dbd2f0e83caddb8ce256c22db3bd6dc40f.tif b/Tests/images/crash-338516dbd2f0e83caddb8ce256c22db3bd6dc40f.tif new file mode 100644 index 000000000..344d62b27 Binary files /dev/null and b/Tests/images/crash-338516dbd2f0e83caddb8ce256c22db3bd6dc40f.tif differ diff --git a/Tests/images/crash-465703f71a0f0094873a3e0e82c9f798161171b8.sgi b/Tests/images/crash-465703f71a0f0094873a3e0e82c9f798161171b8.sgi new file mode 100644 index 000000000..81ae11823 Binary files /dev/null and b/Tests/images/crash-465703f71a0f0094873a3e0e82c9f798161171b8.sgi differ diff --git a/Tests/images/crash-4f085cc12ece8cde18758d42608bed6a2a2cfb1c.tif b/Tests/images/crash-4f085cc12ece8cde18758d42608bed6a2a2cfb1c.tif new file mode 100644 index 000000000..18197c15f Binary files /dev/null and b/Tests/images/crash-4f085cc12ece8cde18758d42608bed6a2a2cfb1c.tif differ diff --git a/Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif b/Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif new file mode 100644 index 000000000..b89203f75 Binary files /dev/null and b/Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif differ diff --git a/Tests/images/crash-64834657ee604b8797bf99eac6a194c124a9a8ba.sgi b/Tests/images/crash-64834657ee604b8797bf99eac6a194c124a9a8ba.sgi new file mode 100644 index 000000000..f31d810e4 Binary files /dev/null and b/Tests/images/crash-64834657ee604b8797bf99eac6a194c124a9a8ba.sgi differ diff --git a/Tests/images/crash-754d9c7ec485ffb76a90eeaab191ef69a2a3a3cd.sgi b/Tests/images/crash-754d9c7ec485ffb76a90eeaab191ef69a2a3a3cd.sgi new file mode 100644 index 000000000..8e093bdfd Binary files /dev/null and b/Tests/images/crash-754d9c7ec485ffb76a90eeaab191ef69a2a3a3cd.sgi differ diff --git a/Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif b/Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif new file mode 100644 index 000000000..34e4f6014 Binary files /dev/null and b/Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif differ diff --git a/Tests/images/crash-abcf1c97b8fe42a6c68f1fb0b978530c98d57ced.sgi b/Tests/images/crash-abcf1c97b8fe42a6c68f1fb0b978530c98d57ced.sgi new file mode 100644 index 000000000..790cb3744 Binary files /dev/null and b/Tests/images/crash-abcf1c97b8fe42a6c68f1fb0b978530c98d57ced.sgi differ diff --git a/Tests/images/crash-b82e64d4f3f76d7465b6af535283029eda211259.sgi b/Tests/images/crash-b82e64d4f3f76d7465b6af535283029eda211259.sgi new file mode 100644 index 000000000..8b7d87765 Binary files /dev/null and b/Tests/images/crash-b82e64d4f3f76d7465b6af535283029eda211259.sgi differ diff --git a/Tests/images/crash-c1b2595b8b0b92cc5f38b6635e98e3a119ade807.sgi b/Tests/images/crash-c1b2595b8b0b92cc5f38b6635e98e3a119ade807.sgi new file mode 100644 index 000000000..e9d2ca1a6 Binary files /dev/null and b/Tests/images/crash-c1b2595b8b0b92cc5f38b6635e98e3a119ade807.sgi differ diff --git a/Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi b/Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi new file mode 100644 index 000000000..b02aacea9 Binary files /dev/null and b/Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi differ diff --git a/Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif b/Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif new file mode 100644 index 000000000..c6774d459 Binary files /dev/null and b/Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif differ diff --git a/Tests/images/ignore_frame_size.mpo b/Tests/images/ignore_frame_size.mpo new file mode 100644 index 000000000..c4d60707a Binary files /dev/null and b/Tests/images/ignore_frame_size.mpo differ diff --git a/Tests/images/imagedraw_rectangle_translucent_outline.png b/Tests/images/imagedraw_rectangle_translucent_outline.png new file mode 100644 index 000000000..845648762 Binary files /dev/null and b/Tests/images/imagedraw_rectangle_translucent_outline.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle.png b/Tests/images/imagedraw_rounded_rectangle.png new file mode 100644 index 000000000..2e815f4ad Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_both.png b/Tests/images/imagedraw_rounded_rectangle_both.png new file mode 100644 index 000000000..24f600e39 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_both.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_x.png b/Tests/images/imagedraw_rounded_rectangle_x.png new file mode 100644 index 000000000..4bf5211a3 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_x.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_y.png b/Tests/images/imagedraw_rounded_rectangle_y.png new file mode 100644 index 000000000..9b391b95e Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_y.png differ diff --git a/Tests/images/odd_stride.pcx b/Tests/images/odd_stride.pcx new file mode 100644 index 000000000..ee0c2eeca Binary files /dev/null and b/Tests/images/odd_stride.pcx differ diff --git a/Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns b/Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns new file mode 100644 index 000000000..0521f5cf1 Binary files /dev/null and b/Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns differ diff --git a/Tests/images/sugarshack_frame_size.mpo b/Tests/images/sugarshack_frame_size.mpo index 81d58e64b..009280a79 100644 Binary files a/Tests/images/sugarshack_frame_size.mpo and b/Tests/images/sugarshack_frame_size.mpo differ diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index fef2329bc..30ec3dc72 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -140,3 +140,11 @@ def test_not_an_icns_file(): with io.BytesIO(b"invalid\n") as fp: with pytest.raises(SyntaxError): IcnsImagePlugin.IcnsFile(fp) + + +def test_icns_decompression_bomb(): + with Image.open( + "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns" + ) as im: + with pytest.raises(Image.DecompressionBombError): + im.load() diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index c5756649e..9de096458 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -89,6 +89,20 @@ def test_frame_size(): assert im.size == (680, 480) +def test_ignore_frame_size(): + # Ignore the different size of the second frame + # since this is not a "Large Thumbnail" image + with Image.open("Tests/images/ignore_frame_size.mpo") as im: + assert im.size == (64, 64) + + im.seek(1) + assert ( + im.mpinfo[0xB002][1]["Attribute"]["MPType"] + == "Multi-Frame Image: (Disparity)" + ) + assert im.size == (64, 64) + + def test_parallax(): # Nintendo with Image.open("Tests/images/sugarshack.mpo") as im: @@ -132,7 +146,7 @@ def test_mp_attribute(): with Image.open(test_file) as im: mpinfo = im._getmp() frameNumber = 0 - for mpentry in mpinfo[45058]: + for mpentry in mpinfo[0xB002]: mpattr = mpentry["Attribute"] if frameNumber: assert not mpattr["RepresentativeImageFlag"] diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 670c03b95..61e33a57b 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -44,6 +44,14 @@ def test_odd(tmp_path): _roundtrip(tmp_path, hopper(mode).resize((511, 511))) +def test_odd_read(): + # Reading an image with an odd stride, making it malformed + with Image.open("Tests/images/odd_stride.pcx") as im: + im.load() + + assert im.size == (371, 150) + + def test_pil184(): # Check reading of files where xmin/xmax is not zero. diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 57bc7f015..52ea3b6d2 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -517,6 +517,8 @@ class TestFilePng: def test_discard_icc_profile(self): with Image.open("Tests/images/icc_profile.png") as im: + assert "icc_profile" in im.info + im = roundtrip(im, icc_profile=None) assert "icc_profile" not in im.info diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index f09117ca7..ba7f9a084 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -568,6 +568,28 @@ class TestFileTiff: with Image.open(tmpfile) as reloaded: assert b"Dummy value" == reloaded.info["icc_profile"] + def test_save_icc_profile(self, tmp_path): + im = hopper() + assert "icc_profile" not in im.info + + outfile = str(tmp_path / "temp.tif") + icc_profile = b"Dummy value" + im.save(outfile, icc_profile=icc_profile) + + with Image.open(outfile) as reloaded: + assert reloaded.info["icc_profile"] == icc_profile + + def test_discard_icc_profile(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + + with Image.open("Tests/images/icc_profile.png") as im: + assert "icc_profile" in im.info + + im.save(outfile, icc_profile=None) + + with Image.open(outfile) as reloaded: + assert "icc_profile" not in reloaded.info + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") diff --git a/Tests/test_image.py b/Tests/test_image.py index e1c14d0d8..b326ca0f8 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -344,6 +344,12 @@ class TestImage: assert_image_equal(offset.crop((64, 64, 127, 127)), target.crop((0, 0, 63, 63))) assert offset.size == (128, 128) + # with negative offset + offset = src.copy() + offset.alpha_composite(over, (-64, -64)) + assert_image_equal(offset.crop((0, 0, 63, 63)), target.crop((64, 64, 127, 127))) + assert offset.size == (128, 128) + # offset and crop box = src.copy() box.alpha_composite(over, (64, 64), (0, 0, 32, 32)) @@ -367,8 +373,6 @@ class TestImage: source.alpha_composite(over, 0) with pytest.raises(ValueError): source.alpha_composite(over, (0, 0), 0) - with pytest.raises(ValueError): - source.alpha_composite(over, (0, -1)) with pytest.raises(ValueError): source.alpha_composite(over, (0, 0), (0, -1)) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 9c95ed255..55a4b03e2 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -692,6 +692,72 @@ def test_rectangle_I16(): assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") +def test_rectangle_translucent_outline(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.rectangle(BBOX1, fill="black", outline=(0, 255, 0, 127), width=5) + + # Assert + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rectangle_translucent_outline.png" + ) + + +@pytest.mark.parametrize( + "xy", + [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], +) +def test_rounded_rectangle(xy): + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(xy, 30, fill="red", outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png") + + +def test_rounded_rectangle_zero_radius(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(BBOX1, 0, fill="blue", outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png") + + +@pytest.mark.parametrize( + "xy, suffix", + [ + ((20, 10, 80, 90), "x"), + ((10, 20, 90, 80), "y"), + ((20, 20, 80, 80), "both"), + ], +) +def test_rounded_rectangle_translucent(xy, suffix): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.rounded_rectangle( + xy, 30, fill=(255, 0, 0, 127), outline=(0, 255, 0, 127), width=5 + ) + + # Assert + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_" + suffix + ".png" + ) + + def test_floodfill(): red = ImageColor.getrgb("red") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 5d611a27f..dc88cb31d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -51,7 +51,7 @@ class TestImageFont: ttf_copy = ttf.font_variant(size=FONT_SIZE + 1) assert ttf_copy.size == FONT_SIZE + 1 - second_font_path = "Tests/fonts/DejaVuSans.ttf" + second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" ttf_copy = ttf.font_variant(font=second_font_path) assert ttf_copy.path == second_font_path @@ -153,8 +153,8 @@ class TestImageFont: ("text", "L", "FreeMono.ttf", 15, 36, 36), ("text", "1", "FreeMono.ttf", 15, 36, 36), # issue 4177 - ("rrr", "L", "DejaVuSans.ttf", 18, 21, 22.21875), - ("rrr", "1", "DejaVuSans.ttf", 18, 24, 22.21875), + ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875), + ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875), # test 'l' not including extra margin # using exact value 2047 / 64 for raqm, checked with debugger ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), @@ -835,7 +835,7 @@ class TestImageFont: layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE] target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" font = ImageFont.truetype( - f"Tests/fonts/DejaVuSans-24-{bpp}-stripped.ttf", + f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf", 24, layout_engine=self.LAYOUT_ENGINE, ) @@ -869,12 +869,12 @@ class TestImageFont: im = Image.new("RGB", (150, 150), "white") d = ImageDraw.Draw(im) - d.text((10, 10), "\U0001f469", embedded_color=True, font=font) + d.text((10, 10), "\U0001f469", font=font, embedded_color=True) assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) - except IOError as e: + except IOError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or unsupported") + pytest.skip("freetype compiled without libpng or CBDT support") @skip_unless_feature_version("freetype2", "2.5.0") def test_cbdt_mask(self): @@ -893,9 +893,47 @@ class TestImageFont: assert_image_similar_tofile( im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 ) - except IOError as e: + except IOError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or unsupported") + pytest.skip("freetype compiled without libpng or CBDT support") + + @skip_unless_feature_version("freetype2", "2.5.1") + def test_sbix(self): + try: + font = ImageFont.truetype( + "Tests/fonts/chromacheck-sbix.woff", + size=300, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + + d.text((50, 50), "\uE901", font=font, embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) + except IOError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or SBIX support") + + @skip_unless_feature_version("freetype2", "2.5.1") + def test_sbix_mask(self): + try: + font = ImageFont.truetype( + "Tests/fonts/chromacheck-sbix.woff", + size=300, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + + d.text((50, 50), "\uE901", (100, 0, 0), font=font) + + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) + except IOError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or SBIX support") @skip_unless_feature_version("freetype2", "2.10.0") def test_colr(self): @@ -908,7 +946,7 @@ class TestImageFont: im = Image.new("RGB", (300, 75), "white") d = ImageDraw.Draw(im) - d.text((15, 5), "Bungee", embedded_color=True, font=font) + d.text((15, 5), "Bungee", font=font, embedded_color=True) assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) @@ -940,7 +978,9 @@ def test_render_mono_size(): im = Image.new("P", (100, 30), "white") draw = ImageDraw.Draw(im) ttf = ImageFont.truetype( - "Tests/fonts/DejaVuSans.ttf", 18, layout_engine=ImageFont.LAYOUT_BASIC + "Tests/fonts/DejaVuSans/DejaVuSans.ttf", + 18, + layout_engine=ImageFont.LAYOUT_BASIC, ) draw.text((10, 10), "r" * 10, "black", ttf) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 655b7662a..f2a914ff7 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -10,7 +10,7 @@ from .helper import ( ) FONT_SIZE = 20 -FONT_PATH = "Tests/fonts/DejaVuSans.ttf" +FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" pytestmark = skip_unless_feature("raqm") diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 404849cb9..53b1fef7c 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -4,11 +4,14 @@ from PIL import ImageQt from .helper import hopper +pytestmark = pytest.mark.skipif( + not ImageQt.qt_is_installed, reason="Qt bindings are not installed" +) + if ImageQt.qt_is_installed: from PIL.ImageQt import qRgba -@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") def test_rgb(): # from https://doc.qt.io/archives/qt-4.8/qcolor.html # typedef QRgb @@ -38,7 +41,13 @@ def test_rgb(): checkrgb(0, 0, 255) -@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") def test_image(): for mode in ("1", "RGB", "RGBA", "L", "P"): ImageQt.ImageQt(hopper(mode)) + + +def test_closed_file(): + with pytest.warns(None) as record: + ImageQt.ImageQt("Tests/images/hopper.gif") + + assert not record diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 78e80f521..5981e22c0 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -62,4 +62,20 @@ def test_viewer(): def test_viewers(): for viewer in ImageShow._viewers: - viewer.get_command("test.jpg") + try: + viewer.get_command("test.jpg") + except NotImplementedError: + pass + + +def test_ipythonviewer(): + pytest.importorskip("IPython", reason="IPython not installed") + for viewer in ImageShow._viewers: + if isinstance(viewer, ImageShow.IPythonViewer): + test_viewer = viewer + break + else: + assert False + + im = hopper() + assert test_viewer.show(im) == 1 diff --git a/Tests/test_map.py b/Tests/test_map.py index 2b65fb3f9..752c5f268 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -4,10 +4,6 @@ import pytest from PIL import Image -from .helper import is_win32 - -pytestmark = pytest.mark.skipif(is_win32(), reason="Win32 does not call map_buffer") - def test_overflow(): # There is the potential to overflow comparisons in map.c @@ -27,6 +23,13 @@ def test_overflow(): Image.MAX_IMAGE_PIXELS = max_pixels +def test_tobytes(): + # Previously raised an access violation on Windows + with Image.open("Tests/images/l2rgb_read.bmp") as im: + with pytest.raises((ValueError, MemoryError, OSError)): + im.tobytes() + + @pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") def test_ysize(): numpy = pytest.importorskip("numpy", reason="NumPy not installed") diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 550d02eea..def7adf3f 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -31,7 +31,7 @@ def test_numpy_to_image(): return i # Check supported 1-bit integer formats - assert_image(to_image(numpy.bool, 1, 1), "1", TEST_IMAGE_SIZE) + assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE) assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE) # Check supported 8-bit integer formats @@ -65,7 +65,7 @@ def test_numpy_to_image(): to_image(numpy.int64) # Check floating-point formats - assert_image(to_image(numpy.float), "F", TEST_IMAGE_SIZE) + assert_image(to_image(float), "F", TEST_IMAGE_SIZE) with pytest.raises(TypeError): to_image(numpy.float16) assert_image(to_image(numpy.float32), "F", TEST_IMAGE_SIZE) @@ -191,7 +191,7 @@ def test_putdata(): def test_roundtrip_eye(): for dtype in ( - numpy.bool, + bool, numpy.bool8, numpy.int8, numpy.int16, @@ -199,7 +199,7 @@ def test_roundtrip_eye(): numpy.uint8, numpy.uint16, numpy.uint32, - numpy.float, + float, numpy.float32, numpy.float64, ): @@ -218,7 +218,7 @@ def test_zero_size(): def test_bool(): # https://github.com/python-pillow/Pillow/issues/2044 - a = numpy.zeros((10, 2), dtype=numpy.bool) + a = numpy.zeros((10, 2), dtype=bool) a[0][0] = True im2 = Image.fromarray(a) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index a3d5620d3..dec790c50 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -2,18 +2,26 @@ import pytest from PIL import ImageQt -from .helper import assert_image_equal, hopper +from .helper import assert_image_equal, assert_image_equal_tofile, hopper if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap if ImageQt.qt_version == "6": + from PyQt6.QtCore import QPoint + from PyQt6.QtGui import QImage, QPainter, QRegion from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget elif ImageQt.qt_version == "side6": + from PySide6.QtCore import QPoint + from PySide6.QtGui import QImage, QPainter, QRegion from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget elif ImageQt.qt_version == "5": + from PyQt5.QtCore import QPoint + from PyQt5.QtGui import QImage, QPainter, QRegion from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget elif ImageQt.qt_version == "side2": + from PySide2.QtCore import QPoint + from PySide2.QtGui import QImage, QPainter, QRegion from PySide2.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget class Example(QWidget): @@ -49,7 +57,8 @@ def test_sanity(tmp_path): for mode in ("1", "RGB", "RGBA", "L", "P"): # to QPixmap - data = ImageQt.toqpixmap(hopper(mode)) + im = hopper(mode) + data = ImageQt.toqpixmap(im) assert isinstance(data, QPixmap) assert not data.isNull() @@ -58,6 +67,20 @@ def test_sanity(tmp_path): tempfile = str(tmp_path / f"temp_{mode}.png") data.save(tempfile) + # Render the image + qimage = ImageQt.ImageQt(im) + data = QPixmap.fromImage(qimage) + qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage + qimage = QImage(128, 128, qt_format.Format_ARGB32) + painter = QPainter(qimage) + image_label = QLabel() + image_label.setPixmap(data) + image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) + painter.end() + rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") + qimage.save(rendered_tempfile) + assert_image_equal_tofile(im.convert("RGBA"), rendered_tempfile) + # from QPixmap roundtrip(hopper(mode)) diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index ac304aab4..d4ddc12f9 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -11,6 +11,13 @@ from PIL import Image "Tests/images/sgi_crash.bin", "Tests/images/crash-6b7f2244da6d0ae297ee0754a424213444e92778.sgi", "Tests/images/ossfuzz-5730089102868480.sgi", + "Tests/images/crash-754d9c7ec485ffb76a90eeaab191ef69a2a3a3cd.sgi", + "Tests/images/crash-465703f71a0f0094873a3e0e82c9f798161171b8.sgi", + "Tests/images/crash-64834657ee604b8797bf99eac6a194c124a9a8ba.sgi", + "Tests/images/crash-abcf1c97b8fe42a6c68f1fb0b978530c98d57ced.sgi", + "Tests/images/crash-b82e64d4f3f76d7465b6af535283029eda211259.sgi", + "Tests/images/crash-c1b2595b8b0b92cc5f38b6635e98e3a119ade807.sgi", + "Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi", ], ) def test_crashes(test_file): diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index d0de4b305..ae4d0f100 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -24,6 +24,15 @@ from .helper import on_ci "Tests/images/crash_1.tif", "Tests/images/crash_2.tif", "Tests/images/crash-2020-10-test.tif", + "Tests/images/crash-0c7e0e8e11ce787078f00b5b0ca409a167f070e0.tif", + "Tests/images/crash-0e16d3bfb83be87356d026d66919deaefca44dac.tif", + "Tests/images/crash-1152ec2d1a1a71395b6f2ce6721c38924d025bf3.tif", + "Tests/images/crash-1185209cf7655b5aed8ae5e77784dfdd18ab59e9.tif", + "Tests/images/crash-338516dbd2f0e83caddb8ce256c22db3bd6dc40f.tif", + "Tests/images/crash-4f085cc12ece8cde18758d42608bed6a2a2cfb1c.tif", + "Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif", + "Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif", + "Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif", ], ) @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index e204ea9ad..376d8ef9b 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-2.14.0 +archive=libimagequant-2.14.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/docs/conf.py b/docs/conf.py index 4fb9d1f8f..123e93c9b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -312,3 +312,7 @@ def setup(app): app.add_js_file("js/script.js") app.add_css_file("css/dark.css") app.add_css_file("css/light.css") + + +# GitHub repo for sphinx-issues +issues_github_path = "python-pillow/Pillow" diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 35c4177aa..0ccd3b1a4 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -901,6 +901,9 @@ using the general tags available through tiffinfo. **copyright** Strings +**icc_profile** + The ICC Profile to include in the saved file. + **resolution_unit** An integer. 1 for no unit, 2 for inches and 3 for centimeters. diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index 03b4ca601..9b670dba8 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -269,7 +269,7 @@ decoder that can be used to read various packed formats into a floating point image memory. To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use -the following syntax:: +the following syntax: .. code-block:: python diff --git a/docs/installation.rst b/docs/installation.rst index 4610d87d8..ec39c9fa8 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -177,7 +177,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-2.14** + * Pillow has been tested with libimagequant **2.6-2.14.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index f0a368479..c4e8f37a3 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -22,8 +22,8 @@ Windows). .. code-block:: python from PIL import Image - im = Image.open("hopper.jpg") - im.rotate(45).show() + with Image.open("hopper.jpg") as im: + im.rotate(45).show() Create thumbnails ^^^^^^^^^^^^^^^^^ @@ -40,9 +40,9 @@ current directory preserving aspect ratios with 128x128 max resolution. for infile in glob.glob("*.jpg"): file, ext = os.path.splitext(infile) - im = Image.open(infile) - im.thumbnail(size) - im.save(file + ".thumbnail", "JPEG") + with Image.open(infile) as im: + im.thumbnail(size) + im.save(file + ".thumbnail", "JPEG") Functions --------- @@ -145,22 +145,21 @@ This crops the input image with the provided coordinates: from PIL import Image - im = Image.open("hopper.jpg") + with Image.open("hopper.jpg") as im: - # The crop method from the Image module takes four coordinates as input. - # The right can also be represented as (left+width) - # and lower can be represented as (upper+height). - (left, upper, right, lower) = (20, 20, 100, 100) + # The crop method from the Image module takes four coordinates as input. + # The right can also be represented as (left+width) + # and lower can be represented as (upper+height). + (left, upper, right, lower) = (20, 20, 100, 100) - # Here the image "im" is cropped and assigned to new variable im_crop - im_crop = im.crop((left, upper, right, lower)) + # Here the image "im" is cropped and assigned to new variable im_crop + im_crop = im.crop((left, upper, right, lower)) .. automethod:: PIL.Image.Image.draft .. automethod:: PIL.Image.Image.effect_spread .. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter -.. automethod:: PIL.Image.Image.frombytes This blurs the input image using a filter from the ``ImageFilter`` module: @@ -168,11 +167,12 @@ This blurs the input image using a filter from the ``ImageFilter`` module: from PIL import Image, ImageFilter - im = Image.open("hopper.jpg") + with Image.open("hopper.jpg") as im: - # Blur the input image using the filter ImageFilter.BLUR - im_blurred = im.filter(filter=ImageFilter.BLUR) + # Blur the input image using the filter ImageFilter.BLUR + im_blurred = im.filter(filter=ImageFilter.BLUR) +.. automethod:: PIL.Image.Image.frombytes .. automethod:: PIL.Image.Image.getbands This helps to get the bands of the input image: @@ -181,8 +181,8 @@ This helps to get the bands of the input image: from PIL import Image - im = Image.open("hopper.jpg") - print(im.getbands()) # Returns ('R', 'G', 'B') + with Image.open("hopper.jpg") as im: + print(im.getbands()) # Returns ('R', 'G', 'B') .. automethod:: PIL.Image.Image.getbbox @@ -192,9 +192,9 @@ This helps to get the bounding box coordinates of the input image: from PIL import Image - im = Image.open("hopper.jpg") - print(im.getbbox()) - # Returns four coordinates in the format (left, upper, right, lower) + with Image.open("hopper.jpg") as im: + print(im.getbbox()) + # Returns four coordinates in the format (left, upper, right, lower) .. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.getcolors @@ -222,11 +222,11 @@ This resizes the given image from ``(width, height)`` to ``(width/2, height/2)`` from PIL import Image - im = Image.open("hopper.jpg") + with Image.open("hopper.jpg") as im: - # Provide the target width and height of the image - (width, height) = (im.width // 2, im.height // 2) - im_resized = im.resize((width, height)) + # Provide the target width and height of the image + (width, height) = (im.width // 2, im.height // 2) + im_resized = im.resize((width, height)) .. automethod:: PIL.Image.Image.rotate @@ -236,12 +236,12 @@ This rotates the input image by ``theta`` degrees counter clockwise: from PIL import Image - im = Image.open("hopper.jpg") + with Image.open("hopper.jpg") as im: - # Rotate the image by 60 degrees counter clockwise - theta = 60 - # Angle is in degrees counter clockwise - im_rotated = im.rotate(angle=theta) + # Rotate the image by 60 degrees counter clockwise + theta = 60 + # Angle is in degrees counter clockwise + im_rotated = im.rotate(angle=theta) .. automethod:: PIL.Image.Image.save .. automethod:: PIL.Image.Image.seek @@ -260,12 +260,12 @@ This flips the input image by using the :data:`FLIP_LEFT_RIGHT` method. from PIL import Image - im = Image.open("hopper.jpg") + with Image.open("hopper.jpg") as im: - # Flip the image from left to right - im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT) - # To flip the image from top to bottom, - # use the method "Image.FLIP_TOP_BOTTOM" + # Flip the image from left to right + im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT) + # To flip the image from top to bottom, + # use the method "Image.FLIP_TOP_BOTTOM" .. automethod:: PIL.Image.Image.verify diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 57d1c2dda..37fb5f726 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -81,24 +81,24 @@ Example: Draw Partial Opacity Text from PIL import Image, ImageDraw, ImageFont # get an image - base = Image.open("Pillow/Tests/images/hopper.png").convert("RGBA") + with Image.open("Pillow/Tests/images/hopper.png").convert("RGBA") as base: - # make a blank image for the text, initialized to transparent text color - txt = Image.new("RGBA", base.size, (255,255,255,0)) + # make a blank image for the text, initialized to transparent text color + txt = Image.new("RGBA", base.size, (255,255,255,0)) - # get a font - fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) - # get a drawing context - d = ImageDraw.Draw(txt) + # get a font + fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) + # get a drawing context + d = ImageDraw.Draw(txt) - # draw text, half opacity - d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128)) - # draw text, full opacity - d.text((10,60), "World", font=fnt, fill=(255,255,255,255)) + # draw text, half opacity + d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128)) + # draw text, full opacity + d.text((10,60), "World", font=fnt, fill=(255,255,255,255)) - out = Image.alpha_composite(base, txt) + out = Image.alpha_composite(base, txt) - out.show() + out.show() Example: Draw Multiline Text ---------------------------- @@ -285,6 +285,20 @@ Methods .. versionadded:: 5.3.0 +.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1) + + Draws a rounded rectangle. + + :param xy: Two points to define the bounding box. Sequence of either + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The second point + is just outside the drawn rectangle. + :param radius: Radius of the corners. + :param outline: Color to use for the outline. + :param fill: Color to use for the fill. + :param width: The line width, in pixels. + + .. versionadded:: 8.2.0 + .. py:method:: ImageDraw.shape(shape, fill=None, outline=None) .. warning:: This method is experimental. @@ -352,7 +366,7 @@ Methods .. versionadded:: 6.2.0 - :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). .. versionadded:: 8.0.0 @@ -413,7 +427,7 @@ Methods .. versionadded:: 6.2.0 - :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). .. versionadded:: 8.0.0 @@ -577,7 +591,7 @@ Methods correct substitutions as appropriate, if available. It should be a `BCP 47 language code`_. Requires libraqm. - :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). .. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) @@ -626,7 +640,7 @@ Methods It should be a `BCP 47 language code`_. Requires libraqm. :param stroke_width: The width of the text stroke. - :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). .. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) @@ -669,7 +683,7 @@ Methods It should be a `BCP 47 language code`_. Requires libraqm. :param stroke_width: The width of the text stroke. - :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). .. py:method:: getdraw(im=None, hints=None) diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 821f60cf5..63f88fddd 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -15,11 +15,11 @@ Example: Using the :py:mod:`~PIL.ImageMath` module from PIL import Image, ImageMath - im1 = Image.open("image1.jpg") - im2 = Image.open("image2.jpg") + with Image.open("image1.jpg") as im1: + with Image.open("image2.jpg") as im2: - out = ImageMath.eval("convert(min(a, b), 'L')", a=im1, b=im2) - out.save("result.png") + out = ImageMath.eval("convert(min(a, b), 'L')", a=im1, b=im2) + out.save("result.png") .. py:function:: eval(expression, environment) diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index a30a6caed..f1fbd90ce 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -9,6 +9,7 @@ All default viewers convert the image to be shown to PNG format. .. autofunction:: PIL.ImageShow.show +.. autoclass:: IPythonViewer .. autoclass:: WindowsViewer .. autoclass:: MacViewer diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index 893acc699..527b9d7bc 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -63,8 +63,8 @@ Take your test image, and make a really simple harness. :: from PIL import Image - im = Image.open(path) - im.load() + with Image.open(path) as im: + im.load() - Run this through valgrind, but note that python triggers some issues on its own, so you're looking for items within the Pillow hierarchy diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index fd3627e3c..0024a537d 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -74,7 +74,7 @@ Security This release includes security fixes. -* :cve:`CVE-2020-10177` Fix multiple OOB reads in FLI decoding +* :cve:`CVE-2020-10177` Fix multiple out-of-bounds reads in FLI decoding * :cve:`CVE-2020-10378` Fix bounds overflow in PCX decoding * :cve:`CVE-2020-10379` Fix two buffer overflows in TIFF decoding * :cve:`CVE-2020-10994` Fix bounds overflow in JPEG 2000 decoding diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 1bef62e00..28dc8324d 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -115,8 +115,9 @@ now support fonts with embedded color data. To render text with embedded color data, use the parameter ``embedded_color=True``. Support for CBDT fonts requires FreeType 2.5 compiled with libpng. +Support for SBIX fonts requires FreeType 2.5.1 compiled with libpng. Support for COLR fonts requires FreeType 2.10. -SBIX and SVG fonts are not yet supported. +SVG fonts are not yet supported. ImageDraw.textlength ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst index 5c4ee3773..8ed1d9d85 100644 --- a/docs/releasenotes/8.1.0.rst +++ b/docs/releasenotes/8.1.0.rst @@ -18,7 +18,7 @@ vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). Makefile ^^^^^^^^ -The 'install-venv' target has been deprecated. +The ``install-venv`` target has been deprecated. API Additions ============= @@ -46,17 +46,18 @@ The PCX image decoder used the reported image stride to calculate the row buffer rather than calculating it from the image size. This issue dates back to the PIL fork. Thanks to Google's `OSS-Fuzz`_ project for finding this. -* :cve:`CVE-2020-35654` Fix TIFF OOB Write error +* :cve:`CVE-2020-35654` Fix TIFF out-of-bounds write error -OOB Write in TiffDecode.c when reading corrupt YCbCr files in some LibTIFF versions -(4.1.0/Ubuntu 20.04, but not 4.0.9/Ubuntu 18.04). In some cases LibTIFF's -interpretation of the file is different when reading in RGBA mode, leading to an Out of -bounds write in TiffDecode.c. This potentially affects Pillow versions from 6.0.0 to -8.0.1, depending on the version of LibTIFF. This was reported through `Tidelift`_. +Out-of-bounds write in ``TiffDecode.c`` when reading corrupt YCbCr files in some +LibTIFF versions (4.1.0/Ubuntu 20.04, but not 4.0.9/Ubuntu 18.04). In some cases +LibTIFF's interpretation of the file is different when reading in RGBA mode, leading to +an out-of-bounds write in ``TiffDecode.c``. This potentially affects Pillow versions +from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through +`Tidelift`_. * :cve:`CVE-2020-35655` Fix for SGI Decode buffer overrun -4 byte read overflow in SGIRleDecode.c, where the code was not correctly checking the +4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly checking the offsets and length tables. Independently reported through `Tidelift`_ and Google's `OSS-Fuzz`_. This vulnerability covers Pillow versions 4.3.0->8.0.1. @@ -78,7 +79,7 @@ Other Changes Makefile ^^^^^^^^ -The 'co' target has been removed. +The ``co`` target has been removed. PyPy wheels ^^^^^^^^^^^ diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst new file mode 100644 index 000000000..4081c49ca --- /dev/null +++ b/docs/releasenotes/8.1.1.rst @@ -0,0 +1,27 @@ +8.1.1 +----- + +Security +======== + +:cve:`CVE-2021-25289`: The previous fix for :cve:`CVE-2020-35654` was insufficient +due to incorrect error checking in ``TiffDecode.c``. + +:cve:`CVE-2021-25290`: In ``TiffDecode.c``, there is a negative-offset ``memcpy`` +with an invalid size. + +:cve:`CVE-2021-25291`: In ``TiffDecode.c``, invalid tile boundaries could lead to +an out-of-bounds read in ``TIFFReadRGBATile``. + +:cve:`CVE-2021-25292`: The PDF parser has a catastrophic backtracking regex +that could be used as a DOS attack. + +:cve:`CVE-2021-25293`: There is an out-of-bounds read in ``SgiRleDecode.c``, +since Pillow 4.3.0. + + +Other Changes +============= + +A crash with the feature flags for libimagequant, libjpeg-turbo, WebP and XCB on +unreleased Python 3.10 has been fixed (:issue:`5193`). diff --git a/docs/releasenotes/8.1.2.rst b/docs/releasenotes/8.1.2.rst new file mode 100644 index 000000000..50d132f33 --- /dev/null +++ b/docs/releasenotes/8.1.2.rst @@ -0,0 +1,12 @@ +8.1.2 +----- + +Security +======== + +There is an exhaustion of memory DOS in the BLP (:cve:`CVE-2021-27921`), +ICNS (:cve:`CVE-2021-27922`) and ICO (:cve:`CVE-2021-27923`) container formats +where Pillow did not properly check the reported size of the contained image. +These images could cause arbitrarily large memory allocations. This was reported +by Jiayi Lin, Luke Shaffer, Xinran Xie, and Akshay Ajayan of +`Arizona State University `_. diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index 28d39ca46..95b17ab31 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -13,18 +13,51 @@ when Tk/Tcl 8.5 will be the minimum supported. API Changes =========== -TODO -^^^^ +Image.alpha_composite: dest +^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +When calling :py:meth:`~PIL.Image.Image.alpha_composite`, the ``dest`` argument now +accepts negative co-ordinates, like the upper left corner of the ``box`` argument of +:py:meth:`~PIL.Image.Image.paste` can be negative. Naturally, this has effect of +cropping the overlaid image. + +ImageDraw.rounded_rectangle +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as +:py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` +argument. ``radius`` is limited to half of the width or the height, so that users can +create a circle, but not any other ellipse. + +.. code-block:: python + + from PIL import Image, ImageDraw + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + draw.rounded_rectangle(xy=(10, 20, 190, 180), radius=30, fill="red") API Additions ============= -TODO -^^^^ +ImageShow.IPythonViewer +^^^^^^^^^^^^^^^^^^^^^^^ -TODO +If IPython is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will be +registered. It displays images on all IPython frontends. This will be helpful +to users of Google Colab, allowing ``im.show()`` to display images. + +It is lower in priority than the other default :py:class:`PIL.ImageShow.Viewer` +instances, so it will only be used by ``im.show()`` or :py:func:`.ImageShow.show()` +if none of the other viewers are available. This means that the behaviour of +:py:class:`PIL.ImageShow` will stay the same for most Pillow users. + +Saving TIFF with ICC profile +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As is already possible for JPEG, PNG and WebP, the ICC profile for TIFF files can now +be specified through a keyword argument:: + + im.save("out.tif", icc_profile=...) Security ======== diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 0930768e7..117738675 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -15,6 +15,8 @@ expected to be backported to earlier versions. :maxdepth: 2 8.2.0 + 8.1.2 + 8.1.1 8.1.0 8.0.1 8.0.0 diff --git a/requirements.txt b/requirements.txt index 1ed1356f9..4b534ae53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ packaging pyroma pytest pytest-cov +pytest-timeout sphinx>=2.4 sphinx-issues sphinx-removed-in diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index d5d7c0e05..88aae80eb 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -353,6 +353,7 @@ class BLP1Decoder(_BLPBaseDecoder): data = jpeg_header + data data = BytesIO(data) image = JpegImageFile(data) + Image._decompression_bomb_check(image.size) self.tile = image.tile # :/ self.fd = image.fp self.mode = image.mode diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 2a63d75cb..ca6a0adad 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -105,6 +105,7 @@ def read_png_or_jpeg2000(fobj, start_length, size): if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": fobj.seek(start) im = PngImagePlugin.PngImageFile(fobj) + Image._decompression_bomb_check(im.size) return {"RGBA": im} elif ( sig[:4] == b"\xff\x4f\xff\x51" @@ -121,6 +122,7 @@ def read_png_or_jpeg2000(fobj, start_length, size): jp2kstream = fobj.read(length) f = io.BytesIO(jp2kstream) im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) + Image._decompression_bomb_check(im.size) if im.mode != "RGBA": im = im.convert("RGBA") return {"RGBA": im} diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index e1bfa7a59..5634bf8e9 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -178,6 +178,7 @@ class IcoFile: if data[:8] == PngImagePlugin._MAGIC: # png frame im = PngImagePlugin.PngImageFile(self.buf) + Image._decompression_bomb_check(im.size) else: # XOR + AND mask bmp frame im = BmpImagePlugin.DibImageFile(self.buf) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 31eab54a4..545fdc019 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1544,8 +1544,6 @@ class Image: raise ValueError("Destination must be a 2-tuple") if min(source) < 0: raise ValueError("Source must be non-negative") - if min(dest) < 0: - raise ValueError("Destination must be non-negative") if len(source) == 2: source = source + im.size diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index b823be9a2..8988e4233 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -257,6 +257,96 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_rectangle(xy, ink, 0, width) + def rounded_rectangle(self, xy, radius=0, fill=None, outline=None, width=1): + """Draw a rounded rectangle.""" + if isinstance(xy[0], (list, tuple)): + (x0, y0), (x1, y1) = xy + else: + x0, y0, x1, y1 = xy + + d = radius * 2 + + full_x = d >= x1 - x0 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) + + if d == 0: + # If the corners have no curve, that is a rectangle + return self.rectangle(xy, fill, outline, width) + + ink, fill = self._getink(outline, fill) + + def draw_corners(pieslice): + if full_x: + # Draw top and bottom halves + parts = ( + ((x0, y0, x0 + d, y0 + d), 180, 360), + ((x0, y1 - d, x0 + d, y1), 0, 180), + ) + elif full_y: + # Draw left and right halves + parts = ( + ((x0, y0, x0 + d, y0 + d), 90, 270), + ((x1 - d, y0, x1, y0 + d), 270, 90), + ) + else: + # Draw four separate corners + parts = ( + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ((x0, y0, x0 + d, y0 + d), 180, 270), + ) + for part in parts: + if pieslice: + self.draw.draw_pieslice(*(part + (fill, 1))) + else: + self.draw.draw_arc(*(part + (ink, width))) + + if fill is not None: + draw_corners(True) + + if full_x: + self.draw.draw_rectangle( + (x0, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 + ) + else: + self.draw.draw_rectangle( + (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y1), fill, 1 + ) + if not full_x and not full_y: + self.draw.draw_rectangle( + (x0, y0 + d / 2 + 1, x0 + d / 2, y1 - d / 2 - 1), fill, 1 + ) + self.draw.draw_rectangle( + (x1 - d / 2, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 + ) + if ink is not None and ink != fill and width != 0: + draw_corners(False) + + if not full_x: + self.draw.draw_rectangle( + (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y0 + width - 1), ink, 1 + ) + self.draw.draw_rectangle( + (x0 + d / 2 + 1, y1 - width + 1, x1 - d / 2 - 1, y1), ink, 1 + ) + if not full_y: + self.draw.draw_rectangle( + (x0, y0 + d / 2 + 1, x0 + width - 1, y1 - d / 2 - 1), ink, 1 + ) + self.draw.draw_rectangle( + (x1 - width + 1, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), ink, 1 + ) + def _multiline_check(self, text): """Draw text.""" split_character = "\n" if isinstance(text, str) else b"\n" diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f2a55cb54..f58de95bd 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -192,24 +192,14 @@ class ImageFile(Image.Image): and args[0] in Image._MAPMODES ): try: - if hasattr(Image.core, "map"): - # use built-in mapper WIN32 only - self.map = Image.core.map(self.filename) - self.map.seek(offset) - self.im = self.map.readimage( - self.mode, self.size, args[1], args[2] - ) - else: - # use mmap, if possible - import mmap + # use mmap, if possible + import mmap - with open(self.filename) as fp: - self.map = mmap.mmap( - fp.fileno(), 0, access=mmap.ACCESS_READ - ) - self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, offset, args - ) + with open(self.filename) as fp: + self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) + self.im = Image.core.map_buffer( + self.map, self.size, decoder_name, offset, args + ) readonly = 1 # After trashing self.im, # we might need to reload the palette data. diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 74ca3166c..32630f2ca 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -128,6 +128,7 @@ def align8to32(bytes, width, mode): def _toqclass_helper(im): data = None colortable = None + exclusive_fp = False # handle filename, if given instead of image name if hasattr(im, "toUtf8"): @@ -135,6 +136,7 @@ def _toqclass_helper(im): im = str(im.toUtf8(), "utf-8") if isPath(im): im = Image.open(im) + exclusive_fp = True qt_format = QImage.Format if qt_version == "6" else QImage if im.mode == "1": @@ -151,16 +153,24 @@ def _toqclass_helper(im): for i in range(0, len(palette), 3): colortable.append(rgb(*palette[i : i + 3])) elif im.mode == "RGB": - data = im.tobytes("raw", "BGRX") + # Populate the 4th channel with 255 + im = im.convert("RGBA") + + data = im.tobytes("raw", "BGRA") format = qt_format.Format_RGB32 elif im.mode == "RGBA": data = im.tobytes("raw", "BGRA") format = qt_format.Format_ARGB32 else: + if exclusive_fp: + im.close() raise ValueError(f"unsupported image mode {repr(im.mode)}") - __data = data or align8to32(im.tobytes(), im.size[0], im.mode) - return {"data": __data, "im": im, "format": format, "colortable": colortable} + size = im.size + __data = data or align8to32(im.tobytes(), size[0], im.mode) + if exclusive_fp: + im.close() + return {"data": __data, "size": size, "format": format, "colortable": colortable} if qt_is_installed: @@ -182,8 +192,8 @@ if qt_is_installed: self.__data = im_data["data"] super().__init__( self.__data, - im_data["im"].size[0], - im_data["im"].size[1], + im_data["size"][0], + im_data["size"][1], im_data["format"], ) if im_data["colortable"]: @@ -197,11 +207,7 @@ def toqimage(im): def toqpixmap(im): # # This doesn't work. For now using a dumb approach. # im_data = _toqclass_helper(im) - # result = QPixmap(im_data['im'].size[0], im_data['im'].size[1]) - # result.loadFromData(im_data['data']) - # Fix some strange bug that causes - if im.mode == "RGB": - im = im.convert("RGBA") - + # result = QPixmap(im_data["size"][0], im_data["size"][1]) + # result.loadFromData(im_data["data"]) qimage = toqimage(im) return QPixmap.fromImage(qimage) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 1ada8252c..3368865a4 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -69,7 +69,6 @@ class Viewer: Converts the given image to the target format and displays it. """ - # save temporary image to disk if not ( image.mode in ("1", "RGBA") or (self.format == "PNG" and image.mode in ("I;16", "LA")) @@ -226,6 +225,23 @@ if sys.platform not in ("win32", "darwin"): # unixoids if shutil.which("xv"): register(XVViewer) + +class IPythonViewer(Viewer): + """The viewer for IPython frontends.""" + + def show_image(self, image, **options): + ipython_display(image) + return 1 + + +try: + from IPython.display import display as ipython_display +except ImportError: + pass +else: + register(IPythonViewer) + + if __name__ == "__main__": if len(sys.argv) < 2: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 8b49d10e5..7244aa2a9 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -82,9 +82,11 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): n = i16(self.fp.read(2)) - 2 self.info["exif"] = ImageFile._safe_read(self.fp, n) - exif = self.getexif().get_ifd(0x8769) - if 40962 in exif and 40963 in exif: - self._size = (exif[40962], exif[40963]) + mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"] + if mptype.startswith("Large Thumbnail"): + exif = self.getexif().get_ifd(0x8769) + if 40962 in exif and 40963 in exif: + self._size = (exif[40962], exif[40963]) elif "exif" in self.info: del self.info["exif"] diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3874e5436..d2e166bdd 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -66,13 +66,13 @@ class PcxImageFile(ImageFile.ImageFile): version = s[1] bits = s[3] planes = s[65] - ignored_stride = i16(s, 66) + provided_stride = i16(s, 66) logger.debug( "PCX version %s, bits %s, planes %s, stride %s", version, bits, planes, - ignored_stride, + provided_stride, ) self.info["dpi"] = i16(s, 12), i16(s, 14) @@ -110,10 +110,15 @@ class PcxImageFile(ImageFile.ImageFile): self.mode = mode self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] - # don't trust the passed in stride. Calculate for ourselves. + # Don't trust the passed in stride. + # Calculate the approximate position for ourselves. # CVE-2020-35653 stride = (self._size[0] * bits + 7) // 8 - stride += stride % 2 + + # While the specification states that this must be even, + # not all images follow this + if provided_stride != stride: + stride += stride % 2 bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 975905f96..86d78a95c 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -580,8 +580,9 @@ class PdfParser: whitespace_or_hex = br"[\000\011\012\014\015\0400-9a-fA-F]" whitespace_optional = whitespace + b"*" whitespace_mandatory = whitespace + b"+" + whitespace_optional_no_nl = br"[\000\011\014\015\040]*" # no "\012" aka "\n" newline_only = br"[\r\n]+" - newline = whitespace_optional + newline_only + whitespace_optional + newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl re_trailer_end = re.compile( whitespace_mandatory + br"trailer" diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0b70ce382..98c70d7c4 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1481,8 +1481,9 @@ def _save(im, fp, filename): # preserve ICC profile (should also work when saving other formats # which support profiles as TIFF) -- 2008-06-06 Florian Hoech - if "icc_profile" in im.info: - ifd[ICCPROFILE] = im.info["icc_profile"] + icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) + if icc: + ifd[ICCPROFILE] = icc for key, name in [ (IMAGEDESCRIPTION, "description"), diff --git a/src/_imaging.c b/src/_imaging.c index 01dd22486..a5b12d325 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3973,8 +3973,6 @@ PyPath_Create(ImagingObject *self, PyObject *args); extern PyObject * PyOutline_Create(ImagingObject *self, PyObject *args); -extern PyObject * -PyImaging_Mapper(PyObject *self, PyObject *args); extern PyObject * PyImaging_MapBuffer(PyObject *self, PyObject *args); @@ -4030,9 +4028,6 @@ static PyMethodDef functions[] = { /* Memory mapping */ #ifdef WITH_MAPPING -#ifdef _WIN32 - {"map", (PyCFunction)PyImaging_Mapper, 1}, -#endif {"map_buffer", (PyCFunction)PyImaging_MapBuffer, 1}, #endif diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 8471ffb17..b6f63b7e8 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -724,8 +724,8 @@ ImagingDrawRectangle( for (i = 0; i < width; i++) { draw->hline(im, x0, y0 + i, x1, ink); draw->hline(im, x0, y1 - i, x1, ink); - draw->line(im, x1 - i, y0, x1 - i, y1, ink); - draw->line(im, x0 + i, y1, x0 + i, y0, ink); + draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink); + draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink); } } diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 5829cf37f..2e6b5daf0 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -385,7 +385,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { float *pq; if (len > 0) { - if ((unsigned)len > + if ((size_t)len > sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) { len = sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0]); } diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index c19231e02..4eef44ba5 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -25,12 +25,58 @@ read4B(UINT32 *dest, UINT8 *buf) { *dest = (UINT32)((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]); } +/* + SgiRleDecoding is done in a single channel row oriented set of RLE chunks. + + * The file is arranged as + - SGI Header + - Rle Offset Table + - Rle Length Table + - Scanline Data + + * Each RLE atom is c->bpc bytes wide (1 or 2) + + * Each RLE Chunk is [specifier atom] [ 1 or n data atoms ] + + * Copy Atoms are a byte with the high bit set, and the low 7 are + the number of bytes to copy from the source to the + destination. e.g. + + CBBBBBBBB or 0CHLHLHLHLHLHL (B=byte, H/L = Hi low bytes) + + * Run atoms do not have the high bit set, and the low 7 bits are + the number of copies of the next atom to copy to the + destination. e.g.: + + RB -> BBBBB or RHL -> HLHLHLHLHL + + The upshot of this is, there is no way to determine the required + length of the input buffer from reloffset and rlelength without + going through the data at that scan line. + + Furthermore, there's no requirement that individual scan lines + pointed to from the rleoffset table are in any sort of order or + used only once, or even disjoint. There's also no requirement that + all of the data in the scan line area of the image file be used + + */ static int -expandrow(UINT8 *dest, UINT8 *src, int n, int z, int xsize) { +expandrow(UINT8 *dest, UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { + /* + * n here is the number of rlechunks + * z is the number of channels, for calculating the interleave + * offset to go to RGBA style pixels + * xsize is the row width + * end_of_buffer is the address of the end of the input buffer + */ + UINT8 pixel, count; int x = 0; for (; n > 0; n--) { + if (src > end_of_buffer) { + return -1; + } pixel = *src++; if (n == 1 && pixel != 0) { return n; @@ -44,12 +90,18 @@ expandrow(UINT8 *dest, UINT8 *src, int n, int z, int xsize) { } x += count; if (pixel & RLE_COPY_FLAG) { + if (src + count > end_of_buffer) { + return -1; + } while (count--) { *dest = *src++; dest += z; } } else { + if (src > end_of_buffer) { + return -1; + } pixel = *src++; while (count--) { *dest = pixel; @@ -61,12 +113,14 @@ expandrow(UINT8 *dest, UINT8 *src, int n, int z, int xsize) { } static int -expandrow2(UINT8 *dest, const UINT8 *src, int n, int z, int xsize) { +expandrow2(UINT8 *dest, const UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { UINT8 pixel, count; - int x = 0; for (; n > 0; n--) { + if (src + 1 > end_of_buffer) { + return -1; + } pixel = src[1]; src += 2; if (n == 1 && pixel != 0) { @@ -81,12 +135,18 @@ expandrow2(UINT8 *dest, const UINT8 *src, int n, int z, int xsize) { } x += count; if (pixel & RLE_COPY_FLAG) { + if (src + 2 * count > end_of_buffer) { + return -1; + } while (count--) { memcpy(dest, src, 2); src += 2; dest += z * 2; } } else { + if (src + 2 > end_of_buffer) { + return -1; + } while (count--) { memcpy(dest, src, 2); dest += z * 2; @@ -132,7 +192,11 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t return -1; } _imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET); - _imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize); + if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } + /* decoder initialization */ state->count = 0; @@ -166,20 +230,20 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t read4B(&c->lengthtab[c->tabindex], &ptr[c->bufindex]); } - state->count += c->tablen * sizeof(UINT32) * 2; - /* read compressed rows */ for (c->rowno = 0; c->rowno < im->ysize; c->rowno++, state->y += state->ystep) { for (c->channo = 0; c->channo < im->bands; c->channo++) { c->rleoffset = c->starttab[c->rowno + c->channo * im->ysize]; c->rlelength = c->lengthtab[c->rowno + c->channo * im->ysize]; - c->rleoffset -= SGI_HEADER_SIZE; - if (c->rleoffset + c->rlelength > c->bufsize) { + // Check for underflow of rleoffset-SGI_HEADER_SIZE + if (c->rleoffset < SGI_HEADER_SIZE) { state->errcode = IMAGING_CODEC_OVERRUN; goto sgi_finish_decode; } + c->rleoffset -= SGI_HEADER_SIZE; + /* row decompression */ if (c->bpc == 1) { status = expandrow( @@ -187,14 +251,16 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t &ptr[c->rleoffset], c->rlelength, im->bands, - im->xsize); + im->xsize, + &ptr[c->bufsize-1]); } else { status = expandrow2( &state->buffer[c->channo * 2], &ptr[c->rleoffset], c->rlelength, im->bands, - im->xsize); + im->xsize, + &ptr[c->bufsize-1]); } if (status == -1) { state->errcode = IMAGING_CODEC_OVERRUN; @@ -203,15 +269,12 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t goto sgi_finish_decode; } - state->count += c->rlelength; } /* store decompressed data in image */ state->shuffle((UINT8 *)im->image[state->y], state->buffer, im->xsize); } - c->bufsize++; - sgi_finish_decode:; free(c->starttab); @@ -221,5 +284,5 @@ sgi_finish_decode:; state->errcode = err; return -1; } - return state->count - c->bufsize; + return 0; } diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 7f14b5a34..746994da3 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -55,6 +55,10 @@ _tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) { TRACE(("_tiffReadProc: %d \n", (int)size)); dump_state(state); + if (state->loc > state->eof) { + TIFFError("_tiffReadProc", "Invalid Read at loc %llu, eof: %llu", state->loc, state->eof); + return 0; + } to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc); TRACE(("to_read: %d\n", (int)to_read)); @@ -282,8 +286,7 @@ _decodeStripYCbCr(Imaging im, ImagingCodecState state, TIFF *tiff) { img.row_offset = state->y; rows_to_read = min(rows_per_strip, img.height - state->y); - if (TIFFRGBAImageGet(&img, (UINT32 *)state->buffer, img.width, rows_to_read) == - -1) { + if (!TIFFRGBAImageGet(&img, (UINT32 *)state->buffer, img.width, rows_to_read)) { TRACE(("Decode Error, y: %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; goto decodeycbcr_err; @@ -559,6 +562,15 @@ ImagingLibTiffDecode( for (y = state->yoff; y < state->ysize; y += tile_length) { for (x = state->xoff; x < state->xsize; x += tile_width) { + /* Sanity Check. Apparently in some cases, the TiffReadRGBA* functions + have a different view of the size of the tiff than we're getting from + other functions. So, we need to check here. + */ + if (!TIFFCheckTile(tiff, x, y, 0, 0)) { + TRACE(("Check Tile Error, Tile at %dx%d\n", x, y)); + state->errcode = IMAGING_CODEC_BROKEN; + goto decode_err; + } if (isYCbCr) { /* To avoid dealing with YCbCr subsampling, let libtiff handle it */ if (!TIFFReadRGBATile(tiff, x, y, (UINT32 *)state->buffer)) { diff --git a/src/map.c b/src/map.c index 2636a684b..c298bd148 100644 --- a/src/map.c +++ b/src/map.c @@ -28,269 +28,9 @@ PyImaging_CheckBuffer(PyObject *buffer); extern int PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); -/* -------------------------------------------------------------------- */ -/* Standard mapper */ - -typedef struct { - PyObject_HEAD char *base; - int size; - int offset; -#ifdef _WIN32 - HANDLE hFile; - HANDLE hMap; -#endif -} ImagingMapperObject; - -static PyTypeObject ImagingMapperType; - -ImagingMapperObject * -PyImaging_MapperNew(const char *filename, int readonly) { - ImagingMapperObject *mapper; - - if (PyType_Ready(&ImagingMapperType) < 0) { - return NULL; - } - - mapper = PyObject_New(ImagingMapperObject, &ImagingMapperType); - if (mapper == NULL) { - return NULL; - } - - mapper->base = NULL; - mapper->size = mapper->offset = 0; - -#ifdef _WIN32 - mapper->hFile = (HANDLE)-1; - mapper->hMap = (HANDLE)-1; - - /* FIXME: currently supports readonly mappings only */ - mapper->hFile = CreateFile( - filename, - GENERIC_READ, - FILE_SHARE_READ, - NULL, - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, - NULL); - if (mapper->hFile == (HANDLE)-1) { - PyErr_SetString(PyExc_OSError, "cannot open file"); - Py_DECREF(mapper); - return NULL; - } - - mapper->hMap = CreateFileMapping(mapper->hFile, NULL, PAGE_READONLY, 0, 0, NULL); - if (mapper->hMap == (HANDLE)-1) { - CloseHandle(mapper->hFile); - PyErr_SetString(PyExc_OSError, "cannot map file"); - Py_DECREF(mapper); - return NULL; - } - - mapper->base = (char *)MapViewOfFile(mapper->hMap, FILE_MAP_READ, 0, 0, 0); - - mapper->size = GetFileSize(mapper->hFile, 0); -#endif - - return mapper; -} - -static void -mapping_dealloc(ImagingMapperObject *mapper) { -#ifdef _WIN32 - if (mapper->base != 0) { - UnmapViewOfFile(mapper->base); - } - if (mapper->hMap != (HANDLE)-1) { - CloseHandle(mapper->hMap); - } - if (mapper->hFile != (HANDLE)-1) { - CloseHandle(mapper->hFile); - } - mapper->base = 0; - mapper->hMap = mapper->hFile = (HANDLE)-1; -#endif - PyObject_Del(mapper); -} - -/* -------------------------------------------------------------------- */ -/* standard file operations */ - -static PyObject * -mapping_read(ImagingMapperObject *mapper, PyObject *args) { - PyObject *buf; - - int size = -1; - if (!PyArg_ParseTuple(args, "|i", &size)) { - return NULL; - } - - /* check size */ - if (size < 0 || mapper->offset + size > mapper->size) { - size = mapper->size - mapper->offset; - } - if (size < 0) { - size = 0; - } - - buf = PyBytes_FromStringAndSize(NULL, size); - if (!buf) { - return NULL; - } - - if (size > 0) { - memcpy(PyBytes_AsString(buf), mapper->base + mapper->offset, size); - mapper->offset += size; - } - - return buf; -} - -static PyObject * -mapping_seek(ImagingMapperObject *mapper, PyObject *args) { - int offset; - int whence = 0; - if (!PyArg_ParseTuple(args, "i|i", &offset, &whence)) { - return NULL; - } - - switch (whence) { - case 0: /* SEEK_SET */ - mapper->offset = offset; - break; - case 1: /* SEEK_CUR */ - mapper->offset += offset; - break; - case 2: /* SEEK_END */ - mapper->offset = mapper->size + offset; - break; - default: - /* FIXME: raise ValueError? */ - break; - } - - Py_INCREF(Py_None); - return Py_None; -} - -/* -------------------------------------------------------------------- */ -/* map entire image */ - extern PyObject * PyImagingNew(Imaging im); -static void -ImagingDestroyMap(Imaging im) { - return; /* nothing to do! */ -} - -static PyObject * -mapping_readimage(ImagingMapperObject *mapper, PyObject *args) { - int y, size; - Imaging im; - - char *mode; - int xsize; - int ysize; - int stride; - int orientation; - if (!PyArg_ParseTuple( - args, "s(ii)ii", &mode, &xsize, &ysize, &stride, &orientation)) { - return NULL; - } - - if (stride <= 0) { - /* FIXME: maybe we should call ImagingNewPrologue instead */ - if (!strcmp(mode, "L") || !strcmp(mode, "P")) { - stride = xsize; - } else if (!strcmp(mode, "I;16") || !strcmp(mode, "I;16B")) { - stride = xsize * 2; - } else { - stride = xsize * 4; - } - } - - size = ysize * stride; - - if (mapper->offset + size > mapper->size) { - PyErr_SetString(PyExc_OSError, "image file truncated"); - return NULL; - } - - im = ImagingNewPrologue(mode, xsize, ysize); - if (!im) { - return NULL; - } - - /* setup file pointers */ - if (orientation > 0) { - for (y = 0; y < ysize; y++) { - im->image[y] = mapper->base + mapper->offset + y * stride; - } - } else { - for (y = 0; y < ysize; y++) { - im->image[ysize - y - 1] = mapper->base + mapper->offset + y * stride; - } - } - - im->destroy = ImagingDestroyMap; - - mapper->offset += size; - - return PyImagingNew(im); -} - -static struct PyMethodDef methods[] = { - /* standard file interface */ - {"read", (PyCFunction)mapping_read, 1}, - {"seek", (PyCFunction)mapping_seek, 1}, - /* extensions */ - {"readimage", (PyCFunction)mapping_readimage, 1}, - {NULL, NULL} /* sentinel */ -}; - -static PyTypeObject ImagingMapperType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingMapper", /*tp_name*/ - sizeof(ImagingMapperObject), /*tp_size*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)mapping_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ -}; - -PyObject * -PyImaging_Mapper(PyObject *self, PyObject *args) { - char *filename; - if (!PyArg_ParseTuple(args, "s", &filename)) { - return NULL; - } - - return (PyObject *)PyImaging_MapperNew(filename, 1); -} - /* -------------------------------------------------------------------- */ /* Buffer mapper */ diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2f7c858bc..7b561aa4e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -255,10 +255,10 @@ deps = { "libs": [r"bin\*.lib"], }, "libimagequant": { - # e5d454b: Merge tag '2.12.6' into msvc - "url": "https://github.com/ImageOptim/libimagequant/archive/e5d454bc7f5eb63ee50c84a83a7fa5ac94f68ec4.zip", # noqa: E501 - "filename": "libimagequant-e5d454bc7f5eb63ee50c84a83a7fa5ac94f68ec4.zip", - "dir": "libimagequant-e5d454bc7f5eb63ee50c84a83a7fa5ac94f68ec4", + # Merge master into msvc (matches 2.14.1 except for version bump) + "url": "https://github.com/ImageOptim/libimagequant/archive/16adaded22d1f90db5c9154a06d00a8b672ca09a.zip", # noqa: E501 + "filename": "libimagequant-16adaded22d1f90db5c9154a06d00a8b672ca09a.zip", + "dir": "libimagequant-16adaded22d1f90db5c9154a06d00a8b672ca09a", "patch": { "CMakeLists.txt": { "add_library": "add_compile_options(-openmp-)\r\nadd_library",