diff --git a/.appveyor.yml b/.appveyor.yml index 7337aed58..8ef91212d 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -24,8 +24,8 @@ install: - mv c:\pillow-depends-master c:\pillow-depends - xcopy /s c:\pillow-depends\test_images\* c:\pillow\tests\images - 7z x ..\pillow-depends\nasm-2.14.02-win64.zip -oc:\ -- ..\pillow-depends\gs9531w32.exe /S -- path c:\nasm-2.14.02;C:\Program Files (x86)\gs\gs9.53.1\bin;%PATH% +- ..\pillow-depends\gs9533w32.exe /S +- path c:\nasm-2.14.02;C:\Program Files (x86)\gs\gs9.53.3\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ diff --git a/.ci/install.sh b/.ci/install.sh index 5baaded48..afb88eb17 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -21,7 +21,7 @@ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ cmake imagemagick libharfbuzz-dev libfribidi-dev -if [[ $TRAVIS_CPU_ARCH == "s390x" ]]; then sudo chown $USER ~/.cache/pip/wheels ; fi +if [[ $TRAVIS_CPU_ARCH == "s390x" || $TRAVIS_CPU_ARCH == "ppc64le" ]]; then sudo chown $USER ~/.cache/pip/wheels ; fi pip install --upgrade pip PYTHONOPTIMIZE=0 pip install cffi @@ -33,8 +33,10 @@ pip install pyroma pip install test-image-results pip install numpy -# TODO Remove when 3.9-dev includes setuptools 49.3.2+: -if [ "$GHA_PYTHON_VERSION" == "3.9-dev" ]; then pip install -U "setuptools>=49.3.2" ; fi +# TODO Remove when 3.8 / 3.9 / PyPy3 includes setuptools 49.3.2+: +if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then pip install -U "setuptools>=49.3.2" ; fi +if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then pip install -U "setuptools>=49.3.2" ; fi +if [ "$TRAVIS_PYTHON_VERSION" == "pypy3.6-7.3.1" ]; then pip install -U "setuptools>=49.3.2" ; fi if [[ $TRAVIS_PYTHON_VERSION == 3.* ]]; then # arm64, ppc64le, s390x CPUs: diff --git a/.github/mergify.yml b/.github/mergify.yml new file mode 100644 index 000000000..3683bc483 --- /dev/null +++ b/.github/mergify.yml @@ -0,0 +1,14 @@ +pull_request_rules: + - name: Automatic merge + conditions: + - "#approved-reviews-by>=1" + - label=automerge + - status-success=Lint + - status-success=Test Successful + - status-success=Docker Test Successful + - status-success=Windows Test Successful + - status-success=continuous-integration/appveyor/pr + - status-success=continuous-integration/travis-ci/pr + actions: + merge: + method: merge diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index a0418c3ba..16f532edf 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -15,8 +15,9 @@ pip install test-image-results echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg pip install numpy -# TODO Remove when 3.9-dev includes setuptools 49.3.2+: -if [ "$GHA_PYTHON_VERSION" == "3.9-dev" ]; then pip install -U "setuptools>=49.3.2" ; fi +# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: +if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then pip install -U "setuptools>=49.3.2" ; fi +if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then pip install -U "setuptools>=49.3.2" ; fi # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 9025dec1c..322244b12 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -61,3 +61,11 @@ jobs: with: flags: GHA_Docker name: ${{ matrix.docker }} + + success: + needs: build + runs-on: ubuntu-latest + name: Docker Test Successful + steps: + - name: Success + run: echo Docker Test Successful diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 2371bf68c..d384a7d29 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9-dev", "pypy3"] + python-version: ["3.6", "3.7", "3.8", "3.9", "pypy3"] architecture: ["x86", "x64"] include: - architecture: "x86" @@ -54,7 +54,7 @@ jobs: - name: Set up TCL if: "contains(matrix.python-version, 'pypy')" - run: Write-Host "::set-env name=TCL_LIBRARY::$env:pythonLocation\tcl\tcl8.5" + run: echo "TCL_LIBRARY=$env:pythonLocation\tcl\tcl8.5" >> $env:GITHUB_ENV shell: pwsh - name: Print build system information @@ -63,18 +63,18 @@ jobs: - name: pip install wheel pytest pytest-cov run: python -m pip install wheel pytest pytest-cov - # TODO Remove when 3.9-dev includes setuptools 49.3.2+: + # TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: - name: Upgrade setuptools - if: "contains(matrix.python-version, '3.9-dev')" + if: "contains(matrix.python-version, '3.8') || contains(matrix.python-version, '3.9')" run: python -m pip install -U "setuptools>=49.3.2" - name: Install dependencies run: | 7z x winbuild\depends\nasm-2.14.02-win64.zip "-o$env:RUNNER_WORKSPACE\" - Write-Host "::add-path::$env:RUNNER_WORKSPACE\nasm-2.14.02" + echo "$env:RUNNER_WORKSPACE\nasm-2.14.02" >> $env:GITHUB_PATH - winbuild\depends\gs9531w32.exe /S - Write-Host "::add-path::C:\Program Files (x86)\gs\gs9.53.1\bin" + winbuild\depends\gs9533w32.exe /S + echo "C:\Program Files (x86)\gs\gs9.53.3\bin" >> $env:GITHUB_PATH xcopy /s winbuild\depends\test_images\* Tests\images\ shell: pwsh @@ -105,6 +105,10 @@ jobs: - name: Build dependencies / WebP if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libwebp.cmd" + # for FreeType CBDT font support + - name: Build dependencies / libpng + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libpng.cmd" - name: Build dependencies / FreeType if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_freetype.cmd" @@ -224,7 +228,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up shell - run: echo ::add-path::C:\msys64\usr\bin\ + run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH shell: pwsh - name: Install Dependencies @@ -267,3 +271,11 @@ jobs: bash <(curl -s https://codecov.io/bash) -F GHA_Windows env: CODECOV_NAME: ${{ matrix.name }} + + success: + needs: [build, msys] + runs-on: ubuntu-latest + name: Windows Test Successful + steps: + - name: Success + run: echo Windows Test Successful diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dbe0eb822..6cb099640 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: ] python-version: [ "pypy3", - "3.9-dev", + "3.9", "3.8", "3.7", "3.6", @@ -107,3 +107,11 @@ jobs: run: bash <(curl -s https://codecov.io/bash) -F ${{ matrix.codecov-flag }} env: CODECOV_NAME: ${{ matrix.os }} Python ${{ matrix.python-version }} + + success: + needs: build + runs-on: ubuntu-latest + name: Test Successful + steps: + - name: Success + run: echo Test Successful diff --git a/.travis.yml b/.travis.yml index 1056df1a5..b02f054d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ matrix: - python: "3.8" arch: s390x - - python: "pypy3" + - python: "pypy3.6-7.3.1" name: "PyPy3 Xenial" - python: "3.9-dev" name: "3.9-dev Xenial" diff --git a/CHANGES.rst b/CHANGES.rst index db6ce70d9..0c12d35b5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,45 @@ Changelog (Pillow) 8.0.0 (unreleased) ------------------ +- Add getlength and getbbox functions for TrueType fonts #4959 + [nulano, radarhere, hugovk] + +- Drop support for PyPy3 < 7.2.0 #4964 + [nulano] + +- Allow tuples with one item to give single color value in getink #4927 + [radarhere, nulano] + +- Add support for CBDT and COLR fonts #4955 + [nulano, hugovk] + +- Removed OSError in favour of DecompressionBombError for BMP #4966 + [radarhere] + +- Implemented another ellipse drawing algorithm #4523 + [xtsm, radarhere] + +- Removed unused JpegImagePlugin._fixup_dict function #4957 + [radarhere] + +- Added reading and writing of private PNG chunks #4292 + [radarhere] + +- Implement anchor for TrueType fonts #4930 + [nulano, hugovk] + +- Fixed bug in Exif __delitem__ #4942 + [radarhere] + +- Fix crash in ImageTk.PhotoImage on MinGW 64-bit #4946 + [nulano] + +- Moved CVE images to pillow-depends #4929 + [radarhere] + +- Refactor font_getsize and font_render #4910 + [nulano] + - Fixed loading profile with non-ASCII path on Windows #4914 [radarhere] @@ -4008,7 +4047,7 @@ Pre-fork This section may not be fully complete. For changes since this file was last updated, see the repository revision history: - https://bitbucket.org/effbot/pil-2009-raclette/commits/all + http://svn.effbot.org/public/pil/ (1.1.7 final) diff --git a/Makefile b/Makefile index cbee5923b..4fd031b69 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ install: .PHONY: install-coverage install-coverage: - CFLAGS="-coverage" python3 setup.py build_ext install + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 setup.py build_ext install python3 selftest.py .PHONY: debug diff --git a/README.md b/README.md index 6ca6cbf83..0e6fa1157 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,14 @@ As of 2019, Pillow development is +## Overview + +The Python Imaging Library adds image processing capabilities to your Python interpreter. + +This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities. + +The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. + ## More Information - [Documentation](https://pillow.readthedocs.io/) diff --git a/Tests/check_tiff_crashes.py b/Tests/check_tiff_crashes.py deleted file mode 100644 index f4eb04375..000000000 --- a/Tests/check_tiff_crashes.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python - -# Reproductions/tests for crashes/read errors in TiffDecode.c - -# When run in python, all of these images should fail for -# one reason or another, either as a buffer overrun, -# unrecognized datastream, or truncated image file. -# There shouldn't be any segfaults. -# -# if run like -# `valgrind --tool=memcheck python check_tiff_crashes.py 2>&1 | grep TiffDecode.c` -# the output should be empty. There may be python issues -# in the valgrind especially if run in a debug python -# version. - - -from PIL import Image - -repro_read_strip = ( - "images/crash_1.tif", - "images/crash_2.tif", -) - -for path in repro_read_strip: - with Image.open(path) as im: - try: - im.load() - except Exception as msg: - print(msg) diff --git a/Tests/fonts/BungeeColor-Regular_colr_Windows.ttf b/Tests/fonts/BungeeColor-Regular_colr_Windows.ttf new file mode 100644 index 000000000..d8eabb3b6 Binary files /dev/null and b/Tests/fonts/BungeeColor-Regular_colr_Windows.ttf differ diff --git a/Tests/fonts/DejaVuSans-24-1-stripped.ttf b/Tests/fonts/DejaVuSans-24-1-stripped.ttf new file mode 100644 index 000000000..8eaf1ee08 Binary files /dev/null and b/Tests/fonts/DejaVuSans-24-1-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans-24-2-stripped.ttf b/Tests/fonts/DejaVuSans-24-2-stripped.ttf new file mode 100644 index 000000000..233667251 Binary files /dev/null and b/Tests/fonts/DejaVuSans-24-2-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans-24-4-stripped.ttf b/Tests/fonts/DejaVuSans-24-4-stripped.ttf new file mode 100644 index 000000000..9accc9ebc Binary files /dev/null and b/Tests/fonts/DejaVuSans-24-4-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans-24-8-stripped.ttf b/Tests/fonts/DejaVuSans-24-8-stripped.ttf new file mode 100644 index 000000000..0f9344267 Binary files /dev/null and b/Tests/fonts/DejaVuSans-24-8-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans-bitmap.ttf b/Tests/fonts/DejaVuSans-bitmap.ttf deleted file mode 100644 index 702cce37d..000000000 Binary files a/Tests/fonts/DejaVuSans-bitmap.ttf and /dev/null differ diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index b9488cb94..db3af7c34 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -1,13 +1,20 @@ NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts +NotoSans-Regular.ttf, from https://www.google.com/get/noto/ NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ +NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa ter-x20b.pcf, from http://terminus-font.sourceforge.net/ +BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. +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. + 10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base diff --git a/Tests/fonts/NotoColorEmoji.ttf b/Tests/fonts/NotoColorEmoji.ttf new file mode 100644 index 000000000..ef7b72575 Binary files /dev/null and b/Tests/fonts/NotoColorEmoji.ttf differ diff --git a/Tests/fonts/NotoSans-Regular.ttf b/Tests/fonts/NotoSans-Regular.ttf new file mode 100644 index 000000000..a1b8994ed Binary files /dev/null and b/Tests/fonts/NotoSans-Regular.ttf differ diff --git a/Tests/fonts/OpenSansCondensed-LightItalic.ttf b/Tests/fonts/OpenSansCondensed-LightItalic.ttf new file mode 100644 index 000000000..b4ee4951f Binary files /dev/null and b/Tests/fonts/OpenSansCondensed-LightItalic.ttf differ diff --git a/Tests/helper.py b/Tests/helper.py index c8cbb80e1..3da2571f2 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -11,6 +11,7 @@ import tempfile from io import BytesIO import pytest +from packaging.version import parse as parse_version from PIL import Image, ImageMath, features @@ -162,6 +163,16 @@ def skip_unless_feature(feature): return pytest.mark.skipif(not features.check(feature), reason=reason) +def skip_unless_feature_version(feature, version_required, reason=None): + if not features.check(feature): + return pytest.mark.skip(f"{feature} not available") + if reason is None: + reason = f"{feature} is older than {version_required}" + version_required = parse_version(version_required) + version_available = parse_version(features.version(feature)) + return pytest.mark.skipif(version_available < version_required, reason=reason) + + @pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") class PillowLeakTestCase: # requires unix/macOS diff --git a/Tests/images/bitmap_font_1_basic.png b/Tests/images/bitmap_font_1_basic.png new file mode 100644 index 000000000..01a05606c Binary files /dev/null and b/Tests/images/bitmap_font_1_basic.png differ diff --git a/Tests/images/bitmap_font_1_raqm.png b/Tests/images/bitmap_font_1_raqm.png new file mode 100644 index 000000000..560efb685 Binary files /dev/null and b/Tests/images/bitmap_font_1_raqm.png differ diff --git a/Tests/images/bitmap_font_2_basic.png b/Tests/images/bitmap_font_2_basic.png new file mode 100644 index 000000000..44d137dd6 Binary files /dev/null and b/Tests/images/bitmap_font_2_basic.png differ diff --git a/Tests/images/bitmap_font_2_raqm.png b/Tests/images/bitmap_font_2_raqm.png new file mode 100644 index 000000000..7a40bd6c2 Binary files /dev/null and b/Tests/images/bitmap_font_2_raqm.png differ diff --git a/Tests/images/bitmap_font_4_basic.png b/Tests/images/bitmap_font_4_basic.png new file mode 100644 index 000000000..e79d86aa8 Binary files /dev/null and b/Tests/images/bitmap_font_4_basic.png differ diff --git a/Tests/images/bitmap_font_4_raqm.png b/Tests/images/bitmap_font_4_raqm.png new file mode 100644 index 000000000..d98a3bc3e Binary files /dev/null and b/Tests/images/bitmap_font_4_raqm.png differ diff --git a/Tests/images/bitmap_font_8_basic.png b/Tests/images/bitmap_font_8_basic.png new file mode 100644 index 000000000..15a7c9809 Binary files /dev/null and b/Tests/images/bitmap_font_8_basic.png differ diff --git a/Tests/images/bitmap_font_8_raqm.png b/Tests/images/bitmap_font_8_raqm.png new file mode 100644 index 000000000..1ad088c93 Binary files /dev/null and b/Tests/images/bitmap_font_8_raqm.png differ diff --git a/Tests/images/cbdt_notocoloremoji.png b/Tests/images/cbdt_notocoloremoji.png new file mode 100644 index 000000000..1da12fba1 Binary files /dev/null and b/Tests/images/cbdt_notocoloremoji.png differ diff --git a/Tests/images/cbdt_notocoloremoji_mask.png b/Tests/images/cbdt_notocoloremoji_mask.png new file mode 100644 index 000000000..6d036a0b6 Binary files /dev/null and b/Tests/images/cbdt_notocoloremoji_mask.png differ diff --git a/Tests/images/colr_bungee.png b/Tests/images/colr_bungee.png new file mode 100644 index 000000000..b10a60be0 Binary files /dev/null and b/Tests/images/colr_bungee.png differ diff --git a/Tests/images/colr_bungee_mask.png b/Tests/images/colr_bungee_mask.png new file mode 100644 index 000000000..f13e17677 Binary files /dev/null and b/Tests/images/colr_bungee_mask.png differ diff --git a/Tests/images/crash_1.tif b/Tests/images/crash_1.tif deleted file mode 100644 index 230d4439a..000000000 Binary files a/Tests/images/crash_1.tif and /dev/null differ diff --git a/Tests/images/crash_2.tif b/Tests/images/crash_2.tif deleted file mode 100644 index 26c00d0ff..000000000 Binary files a/Tests/images/crash_2.tif and /dev/null differ diff --git a/Tests/images/imagedraw_arc.png b/Tests/images/imagedraw_arc.png index b09774389..967e214d9 100644 Binary files a/Tests/images/imagedraw_arc.png and b/Tests/images/imagedraw_arc.png differ diff --git a/Tests/images/imagedraw_arc_end_le_start.png b/Tests/images/imagedraw_arc_end_le_start.png index aee48e1c6..191cc0b3a 100644 Binary files a/Tests/images/imagedraw_arc_end_le_start.png and b/Tests/images/imagedraw_arc_end_le_start.png differ diff --git a/Tests/images/imagedraw_arc_high.png b/Tests/images/imagedraw_arc_high.png new file mode 100644 index 000000000..e3fb66cd0 Binary files /dev/null and b/Tests/images/imagedraw_arc_high.png differ diff --git a/Tests/images/imagedraw_arc_no_loops.png b/Tests/images/imagedraw_arc_no_loops.png index e45ad57a5..03bbd4b43 100644 Binary files a/Tests/images/imagedraw_arc_no_loops.png and b/Tests/images/imagedraw_arc_no_loops.png differ diff --git a/Tests/images/imagedraw_arc_width.png b/Tests/images/imagedraw_arc_width.png index ff3f1f0b2..70dae7d5f 100644 Binary files a/Tests/images/imagedraw_arc_width.png and b/Tests/images/imagedraw_arc_width.png differ diff --git a/Tests/images/imagedraw_arc_width_fill.png b/Tests/images/imagedraw_arc_width_fill.png index 9572a6059..6c135ab76 100644 Binary files a/Tests/images/imagedraw_arc_width_fill.png and b/Tests/images/imagedraw_arc_width_fill.png differ diff --git a/Tests/images/imagedraw_arc_width_non_whole_angle.png b/Tests/images/imagedraw_arc_width_non_whole_angle.png index 1fb9a3c86..f54eb1c29 100644 Binary files a/Tests/images/imagedraw_arc_width_non_whole_angle.png and b/Tests/images/imagedraw_arc_width_non_whole_angle.png differ diff --git a/Tests/images/imagedraw_arc_width_pieslice.png b/Tests/images/imagedraw_arc_width_pieslice.png index 950d95dd6..e1aa95e88 100644 Binary files a/Tests/images/imagedraw_arc_width_pieslice.png and b/Tests/images/imagedraw_arc_width_pieslice.png differ diff --git a/Tests/images/imagedraw_chord_L.png b/Tests/images/imagedraw_chord_L.png index a5a0078d0..6c89da9ea 100644 Binary files a/Tests/images/imagedraw_chord_L.png and b/Tests/images/imagedraw_chord_L.png differ diff --git a/Tests/images/imagedraw_chord_RGB.png b/Tests/images/imagedraw_chord_RGB.png index af6fc7660..d4592cda1 100644 Binary files a/Tests/images/imagedraw_chord_RGB.png and b/Tests/images/imagedraw_chord_RGB.png differ diff --git a/Tests/images/imagedraw_chord_too_fat.png b/Tests/images/imagedraw_chord_too_fat.png new file mode 100644 index 000000000..2021202fe Binary files /dev/null and b/Tests/images/imagedraw_chord_too_fat.png differ diff --git a/Tests/images/imagedraw_chord_width.png b/Tests/images/imagedraw_chord_width.png index 33a59b487..04d3dadf8 100644 Binary files a/Tests/images/imagedraw_chord_width.png and b/Tests/images/imagedraw_chord_width.png differ diff --git a/Tests/images/imagedraw_chord_width_fill.png b/Tests/images/imagedraw_chord_width_fill.png index 809c3ea1c..77475d266 100644 Binary files a/Tests/images/imagedraw_chord_width_fill.png and b/Tests/images/imagedraw_chord_width_fill.png differ diff --git a/Tests/images/imagedraw_chord_zero_width.png b/Tests/images/imagedraw_chord_zero_width.png index c1c0058d7..c8ebe3917 100644 Binary files a/Tests/images/imagedraw_chord_zero_width.png and b/Tests/images/imagedraw_chord_zero_width.png differ diff --git a/Tests/images/imagedraw_ellipse_L.png b/Tests/images/imagedraw_ellipse_L.png index e47e6e441..d5959cc08 100644 Binary files a/Tests/images/imagedraw_ellipse_L.png and b/Tests/images/imagedraw_ellipse_L.png differ diff --git a/Tests/images/imagedraw_ellipse_RGB.png b/Tests/images/imagedraw_ellipse_RGB.png index b52b12802..1d310ce11 100644 Binary files a/Tests/images/imagedraw_ellipse_RGB.png and b/Tests/images/imagedraw_ellipse_RGB.png differ diff --git a/Tests/images/imagedraw_ellipse_edge.png b/Tests/images/imagedraw_ellipse_edge.png index 25a95a601..a2235115a 100644 Binary files a/Tests/images/imagedraw_ellipse_edge.png and b/Tests/images/imagedraw_ellipse_edge.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes.png b/Tests/images/imagedraw_ellipse_various_sizes.png new file mode 100644 index 000000000..11a1be6fa Binary files /dev/null and b/Tests/images/imagedraw_ellipse_various_sizes.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes_filled.png b/Tests/images/imagedraw_ellipse_various_sizes_filled.png new file mode 100644 index 000000000..d71e175b8 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_various_sizes_filled.png differ diff --git a/Tests/images/imagedraw_ellipse_width.png b/Tests/images/imagedraw_ellipse_width.png index ec0ca6731..54cfc291c 100644 Binary files a/Tests/images/imagedraw_ellipse_width.png and b/Tests/images/imagedraw_ellipse_width.png differ diff --git a/Tests/images/imagedraw_ellipse_width_fill.png b/Tests/images/imagedraw_ellipse_width_fill.png index 9b7be6029..4a1edc379 100644 Binary files a/Tests/images/imagedraw_ellipse_width_fill.png and b/Tests/images/imagedraw_ellipse_width_fill.png differ diff --git a/Tests/images/imagedraw_ellipse_width_large.png b/Tests/images/imagedraw_ellipse_width_large.png index 9d3c3326b..e95186019 100644 Binary files a/Tests/images/imagedraw_ellipse_width_large.png and b/Tests/images/imagedraw_ellipse_width_large.png differ diff --git a/Tests/images/imagedraw_ellipse_zero_width.png b/Tests/images/imagedraw_ellipse_zero_width.png index f14a279f2..6661b7d99 100644 Binary files a/Tests/images/imagedraw_ellipse_zero_width.png and b/Tests/images/imagedraw_ellipse_zero_width.png differ diff --git a/Tests/images/imagedraw_outline_chord_RGB.png b/Tests/images/imagedraw_outline_chord_RGB.png index 9e9cb5af0..3c71312c7 100644 Binary files a/Tests/images/imagedraw_outline_chord_RGB.png and b/Tests/images/imagedraw_outline_chord_RGB.png differ diff --git a/Tests/images/imagedraw_pieslice.png b/Tests/images/imagedraw_pieslice.png index 2f8c09191..41c786e77 100644 Binary files a/Tests/images/imagedraw_pieslice.png and b/Tests/images/imagedraw_pieslice.png differ diff --git a/Tests/images/imagedraw_pieslice_wide.png b/Tests/images/imagedraw_pieslice_wide.png new file mode 100644 index 000000000..446874788 Binary files /dev/null and b/Tests/images/imagedraw_pieslice_wide.png differ diff --git a/Tests/images/imagedraw_pieslice_width.png b/Tests/images/imagedraw_pieslice_width.png index 3bd69222c..422d92f3b 100644 Binary files a/Tests/images/imagedraw_pieslice_width.png and b/Tests/images/imagedraw_pieslice_width.png differ diff --git a/Tests/images/imagedraw_pieslice_width_fill.png b/Tests/images/imagedraw_pieslice_width_fill.png index c5a34e0f3..bee6aac3b 100644 Binary files a/Tests/images/imagedraw_pieslice_width_fill.png and b/Tests/images/imagedraw_pieslice_width_fill.png differ diff --git a/Tests/images/imagedraw_pieslice_zero_width.png b/Tests/images/imagedraw_pieslice_zero_width.png index 4ca051583..d6ceb0b5a 100644 Binary files a/Tests/images/imagedraw_pieslice_zero_width.png and b/Tests/images/imagedraw_pieslice_zero_width.png differ diff --git a/Tests/images/standard_embedded.png b/Tests/images/standard_embedded.png new file mode 100644 index 000000000..890532531 Binary files /dev/null and b/Tests/images/standard_embedded.png differ diff --git a/Tests/images/test_anchor_multiline_lm_center.png b/Tests/images/test_anchor_multiline_lm_center.png new file mode 100644 index 000000000..6fff287e4 Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_center.png differ diff --git a/Tests/images/test_anchor_multiline_lm_left.png b/Tests/images/test_anchor_multiline_lm_left.png new file mode 100644 index 000000000..b76a81b81 Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_left.png differ diff --git a/Tests/images/test_anchor_multiline_lm_right.png b/Tests/images/test_anchor_multiline_lm_right.png new file mode 100644 index 000000000..c12a8d63e Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_right.png differ diff --git a/Tests/images/test_anchor_multiline_ma_center.png b/Tests/images/test_anchor_multiline_ma_center.png new file mode 100644 index 000000000..4f35d781f Binary files /dev/null and b/Tests/images/test_anchor_multiline_ma_center.png differ diff --git a/Tests/images/test_anchor_multiline_md_center.png b/Tests/images/test_anchor_multiline_md_center.png new file mode 100644 index 000000000..8290d045c Binary files /dev/null and b/Tests/images/test_anchor_multiline_md_center.png differ diff --git a/Tests/images/test_anchor_multiline_mm_center.png b/Tests/images/test_anchor_multiline_mm_center.png new file mode 100644 index 000000000..773cf2a4a Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_center.png differ diff --git a/Tests/images/test_anchor_multiline_mm_left.png b/Tests/images/test_anchor_multiline_mm_left.png new file mode 100644 index 000000000..87d56636a Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_left.png differ diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png new file mode 100644 index 000000000..cf002b12c Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_right.png differ diff --git a/Tests/images/test_anchor_multiline_rm_center.png b/Tests/images/test_anchor_multiline_rm_center.png new file mode 100644 index 000000000..98073144b Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_center.png differ diff --git a/Tests/images/test_anchor_multiline_rm_left.png b/Tests/images/test_anchor_multiline_rm_left.png new file mode 100644 index 000000000..838fd7858 Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_left.png differ diff --git a/Tests/images/test_anchor_multiline_rm_right.png b/Tests/images/test_anchor_multiline_rm_right.png new file mode 100644 index 000000000..290f58417 Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_right.png differ diff --git a/Tests/images/test_anchor_quick_ls.png b/Tests/images/test_anchor_quick_ls.png new file mode 100644 index 000000000..524c417c3 Binary files /dev/null and b/Tests/images/test_anchor_quick_ls.png differ diff --git a/Tests/images/test_anchor_quick_ma.png b/Tests/images/test_anchor_quick_ma.png new file mode 100644 index 000000000..cfff27f7d Binary files /dev/null and b/Tests/images/test_anchor_quick_ma.png differ diff --git a/Tests/images/test_anchor_quick_mb.png b/Tests/images/test_anchor_quick_mb.png new file mode 100644 index 000000000..ff11f478e Binary files /dev/null and b/Tests/images/test_anchor_quick_mb.png differ diff --git a/Tests/images/test_anchor_quick_md.png b/Tests/images/test_anchor_quick_md.png new file mode 100644 index 000000000..5cbccb170 Binary files /dev/null and b/Tests/images/test_anchor_quick_md.png differ diff --git a/Tests/images/test_anchor_quick_mm.png b/Tests/images/test_anchor_quick_mm.png new file mode 100644 index 000000000..500294c3b Binary files /dev/null and b/Tests/images/test_anchor_quick_mm.png differ diff --git a/Tests/images/test_anchor_quick_ms.png b/Tests/images/test_anchor_quick_ms.png new file mode 100644 index 000000000..b1012463e Binary files /dev/null and b/Tests/images/test_anchor_quick_ms.png differ diff --git a/Tests/images/test_anchor_quick_mt.png b/Tests/images/test_anchor_quick_mt.png new file mode 100644 index 000000000..19423e51a Binary files /dev/null and b/Tests/images/test_anchor_quick_mt.png differ diff --git a/Tests/images/test_anchor_quick_rs.png b/Tests/images/test_anchor_quick_rs.png new file mode 100644 index 000000000..20a5e6c6e Binary files /dev/null and b/Tests/images/test_anchor_quick_rs.png differ diff --git a/Tests/images/test_anchor_ttb_f_lt.png b/Tests/images/test_anchor_ttb_f_lt.png new file mode 100644 index 000000000..5f70a65c4 Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_lt.png differ diff --git a/Tests/images/test_anchor_ttb_f_mm.png b/Tests/images/test_anchor_ttb_f_mm.png new file mode 100644 index 000000000..e7be557d2 Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_mm.png differ diff --git a/Tests/images/test_anchor_ttb_f_rb.png b/Tests/images/test_anchor_ttb_f_rb.png new file mode 100644 index 000000000..b78e2f954 Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_rb.png differ diff --git a/Tests/images/test_anchor_ttb_f_sm.png b/Tests/images/test_anchor_ttb_f_sm.png new file mode 100644 index 000000000..f6dc7c70f Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_sm.png differ diff --git a/Tests/images/test_arabictext_features.png b/Tests/images/test_arabictext_features.png index 9bfa5a931..a03845ace 100644 Binary files a/Tests/images/test_arabictext_features.png and b/Tests/images/test_arabictext_features.png differ diff --git a/Tests/images/test_combine_caron.png b/Tests/images/test_combine_caron.png new file mode 100644 index 000000000..1097f4be5 Binary files /dev/null and b/Tests/images/test_combine_caron.png differ diff --git a/Tests/images/test_combine_caron_below.png b/Tests/images/test_combine_caron_below.png new file mode 100644 index 000000000..6e7d88a92 Binary files /dev/null and b/Tests/images/test_combine_caron_below.png differ diff --git a/Tests/images/test_combine_caron_below_lb.png b/Tests/images/test_combine_caron_below_lb.png new file mode 100644 index 000000000..f59e722b2 Binary files /dev/null and b/Tests/images/test_combine_caron_below_lb.png differ diff --git a/Tests/images/test_combine_caron_below_ld.png b/Tests/images/test_combine_caron_below_ld.png new file mode 100644 index 000000000..540ab7d42 Binary files /dev/null and b/Tests/images/test_combine_caron_below_ld.png differ diff --git a/Tests/images/test_combine_caron_below_ls.png b/Tests/images/test_combine_caron_below_ls.png new file mode 100644 index 000000000..1109b4ee6 Binary files /dev/null and b/Tests/images/test_combine_caron_below_ls.png differ diff --git a/Tests/images/test_combine_caron_below_ttb.png b/Tests/images/test_combine_caron_below_ttb.png new file mode 100644 index 000000000..5c7576de0 Binary files /dev/null and b/Tests/images/test_combine_caron_below_ttb.png differ diff --git a/Tests/images/test_combine_caron_below_ttb_lb.png b/Tests/images/test_combine_caron_below_ttb_lb.png new file mode 100644 index 000000000..bacd6a141 Binary files /dev/null and b/Tests/images/test_combine_caron_below_ttb_lb.png differ diff --git a/Tests/images/test_combine_caron_la.png b/Tests/images/test_combine_caron_la.png new file mode 100644 index 000000000..1097f4be5 Binary files /dev/null and b/Tests/images/test_combine_caron_la.png differ diff --git a/Tests/images/test_combine_caron_ls.png b/Tests/images/test_combine_caron_ls.png new file mode 100644 index 000000000..1a721873c Binary files /dev/null and b/Tests/images/test_combine_caron_ls.png differ diff --git a/Tests/images/test_combine_caron_lt.png b/Tests/images/test_combine_caron_lt.png new file mode 100644 index 000000000..91e50d45f Binary files /dev/null and b/Tests/images/test_combine_caron_lt.png differ diff --git a/Tests/images/test_combine_caron_ttb.png b/Tests/images/test_combine_caron_ttb.png new file mode 100644 index 000000000..a94be2f0a Binary files /dev/null and b/Tests/images/test_combine_caron_ttb.png differ diff --git a/Tests/images/test_combine_caron_ttb_lt.png b/Tests/images/test_combine_caron_ttb_lt.png new file mode 100644 index 000000000..a94be2f0a Binary files /dev/null and b/Tests/images/test_combine_caron_ttb_lt.png differ diff --git a/Tests/images/test_combine_double_breve_below.png b/Tests/images/test_combine_double_breve_below.png new file mode 100644 index 000000000..30252107f Binary files /dev/null and b/Tests/images/test_combine_double_breve_below.png differ diff --git a/Tests/images/test_combine_double_breve_below_ma.png b/Tests/images/test_combine_double_breve_below_ma.png new file mode 100644 index 000000000..aea09538f Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ma.png differ diff --git a/Tests/images/test_combine_double_breve_below_ra.png b/Tests/images/test_combine_double_breve_below_ra.png new file mode 100644 index 000000000..febd3ab67 Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ra.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb.png b/Tests/images/test_combine_double_breve_below_ttb.png new file mode 100644 index 000000000..8e42c0d16 Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb_mt.png b/Tests/images/test_combine_double_breve_below_ttb_mt.png new file mode 100644 index 000000000..9a755e8f8 Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_mt.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb_rt.png b/Tests/images/test_combine_double_breve_below_ttb_rt.png new file mode 100644 index 000000000..b954a5415 Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_rt.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb_st.png b/Tests/images/test_combine_double_breve_below_ttb_st.png new file mode 100644 index 000000000..b6b08145e Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_st.png differ diff --git a/Tests/images/test_combine_multiline_lm_center.png b/Tests/images/test_combine_multiline_lm_center.png new file mode 100644 index 000000000..7b1e9c4e4 Binary files /dev/null and b/Tests/images/test_combine_multiline_lm_center.png differ diff --git a/Tests/images/test_combine_multiline_lm_left.png b/Tests/images/test_combine_multiline_lm_left.png new file mode 100644 index 000000000..a26996c2d Binary files /dev/null and b/Tests/images/test_combine_multiline_lm_left.png differ diff --git a/Tests/images/test_combine_multiline_lm_right.png b/Tests/images/test_combine_multiline_lm_right.png new file mode 100644 index 000000000..7caf5cb74 Binary files /dev/null and b/Tests/images/test_combine_multiline_lm_right.png differ diff --git a/Tests/images/test_combine_multiline_mm_center.png b/Tests/images/test_combine_multiline_mm_center.png new file mode 100644 index 000000000..a859e9570 Binary files /dev/null and b/Tests/images/test_combine_multiline_mm_center.png differ diff --git a/Tests/images/test_combine_multiline_mm_left.png b/Tests/images/test_combine_multiline_mm_left.png new file mode 100644 index 000000000..aadb5191f Binary files /dev/null and b/Tests/images/test_combine_multiline_mm_left.png differ diff --git a/Tests/images/test_combine_multiline_mm_right.png b/Tests/images/test_combine_multiline_mm_right.png new file mode 100644 index 000000000..8238d4ec8 Binary files /dev/null and b/Tests/images/test_combine_multiline_mm_right.png differ diff --git a/Tests/images/test_combine_multiline_rm_center.png b/Tests/images/test_combine_multiline_rm_center.png new file mode 100644 index 000000000..7568dd63a Binary files /dev/null and b/Tests/images/test_combine_multiline_rm_center.png differ diff --git a/Tests/images/test_combine_multiline_rm_left.png b/Tests/images/test_combine_multiline_rm_left.png new file mode 100644 index 000000000..b8c3b5b14 Binary files /dev/null and b/Tests/images/test_combine_multiline_rm_left.png differ diff --git a/Tests/images/test_combine_multiline_rm_right.png b/Tests/images/test_combine_multiline_rm_right.png new file mode 100644 index 000000000..14c478a72 Binary files /dev/null and b/Tests/images/test_combine_multiline_rm_right.png differ diff --git a/Tests/images/test_combine_overline.png b/Tests/images/test_combine_overline.png new file mode 100644 index 000000000..dc5e86361 Binary files /dev/null and b/Tests/images/test_combine_overline.png differ diff --git a/Tests/images/test_combine_overline_la.png b/Tests/images/test_combine_overline_la.png new file mode 100644 index 000000000..dc5e86361 Binary files /dev/null and b/Tests/images/test_combine_overline_la.png differ diff --git a/Tests/images/test_combine_overline_ra.png b/Tests/images/test_combine_overline_ra.png new file mode 100644 index 000000000..cbb2d472d Binary files /dev/null and b/Tests/images/test_combine_overline_ra.png differ diff --git a/Tests/images/test_combine_overline_ttb.png b/Tests/images/test_combine_overline_ttb.png new file mode 100644 index 000000000..f74538d9c Binary files /dev/null and b/Tests/images/test_combine_overline_ttb.png differ diff --git a/Tests/images/test_combine_overline_ttb_mt.png b/Tests/images/test_combine_overline_ttb_mt.png new file mode 100644 index 000000000..e915543d6 Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_mt.png differ diff --git a/Tests/images/test_combine_overline_ttb_rt.png b/Tests/images/test_combine_overline_ttb_rt.png new file mode 100644 index 000000000..186d6ee84 Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_rt.png differ diff --git a/Tests/images/test_combine_overline_ttb_st.png b/Tests/images/test_combine_overline_ttb_st.png new file mode 100644 index 000000000..e915543d6 Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_st.png differ diff --git a/Tests/images/test_complex_unicode_text.png b/Tests/images/test_complex_unicode_text.png index f1a6f7ec6..61174d75f 100644 Binary files a/Tests/images/test_complex_unicode_text.png and b/Tests/images/test_complex_unicode_text.png differ diff --git a/Tests/images/test_complex_unicode_text2.png b/Tests/images/test_complex_unicode_text2.png index 543b174c0..0526233c0 100644 Binary files a/Tests/images/test_complex_unicode_text2.png and b/Tests/images/test_complex_unicode_text2.png differ diff --git a/Tests/images/test_direction_ltr.png b/Tests/images/test_direction_ltr.png index 42239334d..b30fcd5d8 100644 Binary files a/Tests/images/test_direction_ltr.png and b/Tests/images/test_direction_ltr.png differ diff --git a/Tests/images/test_direction_rtl.png b/Tests/images/test_direction_rtl.png index 966b67d6b..282eed883 100644 Binary files a/Tests/images/test_direction_rtl.png and b/Tests/images/test_direction_rtl.png differ diff --git a/Tests/images/test_direction_ttb.png b/Tests/images/test_direction_ttb.png index 825f3213e..52dbf5723 100644 Binary files a/Tests/images/test_direction_ttb.png and b/Tests/images/test_direction_ttb.png differ diff --git a/Tests/images/test_direction_ttb_stroke.png b/Tests/images/test_direction_ttb_stroke.png index 3fa844e9a..4b689c38e 100644 Binary files a/Tests/images/test_direction_ttb_stroke.png and b/Tests/images/test_direction_ttb_stroke.png differ diff --git a/Tests/images/test_kerning_features.png b/Tests/images/test_kerning_features.png index ca895735c..78bcd951b 100644 Binary files a/Tests/images/test_kerning_features.png and b/Tests/images/test_kerning_features.png differ diff --git a/Tests/images/test_language.png b/Tests/images/test_language.png index 8daf007b0..c77215318 100644 Binary files a/Tests/images/test_language.png and b/Tests/images/test_language.png differ diff --git a/Tests/images/test_ligature_features.png b/Tests/images/test_ligature_features.png index 664e9929d..89ea648bf 100644 Binary files a/Tests/images/test_ligature_features.png and b/Tests/images/test_ligature_features.png differ diff --git a/Tests/images/test_text.png b/Tests/images/test_text.png index c156399cd..5888e52e5 100644 Binary files a/Tests/images/test_text.png and b/Tests/images/test_text.png differ diff --git a/Tests/images/test_x_max_and_y_offset.png b/Tests/images/test_x_max_and_y_offset.png index f8bec3e95..21401813f 100644 Binary files a/Tests/images/test_x_max_and_y_offset.png and b/Tests/images/test_x_max_and_y_offset.png differ diff --git a/Tests/images/test_y_offset.png b/Tests/images/test_y_offset.png index 2d57890cb..bda2490df 100644 Binary files a/Tests/images/test_y_offset.png and b/Tests/images/test_y_offset.png differ diff --git a/Tests/images/variation_adobe.png b/Tests/images/variation_adobe.png index 3ac326f51..e9cfafb48 100644 Binary files a/Tests/images/variation_adobe.png and b/Tests/images/variation_adobe.png differ diff --git a/Tests/images/variation_adobe_axes.png b/Tests/images/variation_adobe_axes.png index 93cad98ff..ad3a3a960 100644 Binary files a/Tests/images/variation_adobe_axes.png and b/Tests/images/variation_adobe_axes.png differ diff --git a/Tests/images/variation_adobe_name.png b/Tests/images/variation_adobe_name.png index acb0df63c..11ceaf6e6 100644 Binary files a/Tests/images/variation_adobe_name.png and b/Tests/images/variation_adobe_name.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz.png b/Tests/images/variation_adobe_older_harfbuzz.png index 71b879bc5..5abc907ca 100644 Binary files a/Tests/images/variation_adobe_older_harfbuzz.png and b/Tests/images/variation_adobe_older_harfbuzz.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz_axes.png b/Tests/images/variation_adobe_older_harfbuzz_axes.png index 9376c1d7b..b39d460f9 100644 Binary files a/Tests/images/variation_adobe_older_harfbuzz_axes.png and b/Tests/images/variation_adobe_older_harfbuzz_axes.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz_name.png b/Tests/images/variation_adobe_older_harfbuzz_name.png index 9e5fe70e5..2adb517a7 100644 Binary files a/Tests/images/variation_adobe_older_harfbuzz_name.png and b/Tests/images/variation_adobe_older_harfbuzz_name.png differ diff --git a/Tests/images/variation_tiny_axes.png b/Tests/images/variation_tiny_axes.png index d06ac7a60..8cb6d1f62 100644 Binary files a/Tests/images/variation_tiny_axes.png and b/Tests/images/variation_tiny_axes.png differ diff --git a/Tests/images/variation_tiny_name.png b/Tests/images/variation_tiny_name.png index a0c6ffe3f..69f1550db 100644 Binary files a/Tests/images/variation_tiny_name.png and b/Tests/images/variation_tiny_name.png differ diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 5799fc0ed..132269933 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -60,6 +60,10 @@ class TestDecompressionBomb: with pytest.raises(Image.DecompressionBombError): Image.open("Tests/images/decompression_bomb.gif") + def test_exception_bmp(self): + with pytest.raises(Image.DecompressionBombError): + Image.open("Tests/images/bmp/b/reallybig.bmp") + class TestDecompressionCrop: @classmethod diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index f580f4ae7..80305c7b4 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -564,6 +564,28 @@ class TestFilePng: chunks = PngImagePlugin.getchunks(im) assert len(chunks) == 3 + def test_read_private_chunks(self): + im = Image.open("Tests/images/exif.png") + assert im.private_chunks == [(b"orNT", b"\x01")] + + def test_roundtrip_private_chunk(self): + # Check private chunk roundtripping + + with Image.open(TEST_PNG_FILE) as im: + info = PngImagePlugin.PngInfo() + info.add(b"prIV", b"VALUE") + info.add(b"atEC", b"VALUE2") + info.add(b"prIV", b"VALUE3", True) + + im = roundtrip(im, pnginfo=info) + assert im.private_chunks == [(b"prIV", b"VALUE"), (b"atEC", b"VALUE2")] + im.load() + assert im.private_chunks == [ + (b"prIV", b"VALUE"), + (b"atEC", b"VALUE2"), + (b"prIV", b"VALUE3", True), + ] + def test_textual_chunks_after_idat(self): with Image.open("Tests/images/hopper.png") as im: assert "comment" in im.text.keys() diff --git a/Tests/test_image.py b/Tests/test_image.py index 89894c9a7..84c098cc8 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -498,6 +498,12 @@ class TestImage: with pytest.raises(ValueError): Image.core.fill("RGB", (2, -2), (0, 0, 0)) + def test_one_item_tuple(self): + for mode in ("I", "F", "L"): + im = Image.new(mode, (100, 100), (5,)) + px = im.load() + assert px[0, 0] == 5 + def test_linear_gradient_wrong_mode(self): # Arrange wrong_mode = "RGB" @@ -650,12 +656,14 @@ class TestImage: with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian exif = im.getexif() assert 258 not in exif + assert 274 in exif assert 40960 in exif assert exif[40963] == 450 assert exif[11] == "gThumb 3.0.1" out = str(tmp_path / "temp.jpg") exif[258] = 8 + del exif[274] del exif[40960] exif[40963] = 455 exif[11] = "Pillow test" @@ -663,6 +671,7 @@ class TestImage: with Image.open(out) as reloaded: reloaded_exif = reloaded.getexif() assert reloaded_exif[258] == 8 + assert 274 not in reloaded_exif assert 40960 not in reloaded_exif assert reloaded_exif[40963] == 455 assert reloaded_exif[11] == "Pillow test" diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 9afeb77f7..e86dc8530 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -330,21 +330,22 @@ class TestCffi(AccessTest): class TestImagePutPixelError(AccessTest): IMAGE_MODES1 = ["L", "LA", "RGB", "RGBA"] IMAGE_MODES2 = ["I", "I;16", "BGR;15"] - INVALID_TYPES1 = ["foo", 1.0, None] - INVALID_TYPES2 = [*INVALID_TYPES1, (10,)] + INVALID_TYPES = ["foo", 1.0, None] @pytest.mark.parametrize("mode", IMAGE_MODES1) def test_putpixel_type_error1(self, mode): im = hopper(mode) - for v in self.INVALID_TYPES1: + for v in self.INVALID_TYPES: with pytest.raises(TypeError, match="color must be int or tuple"): im.putpixel((0, 0), v) @pytest.mark.parametrize("mode", IMAGE_MODES2) def test_putpixel_type_error2(self, mode): im = hopper(mode) - for v in self.INVALID_TYPES2: - with pytest.raises(TypeError, match="color must be int"): + for v in self.INVALID_TYPES: + with pytest.raises( + TypeError, match="color must be int or single-element tuple" + ): im.putpixel((0, 0), v) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 7d042cb9f..a19fbf239 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -311,8 +311,8 @@ def test_subtract(): # Assert assert new.getbbox() == (25, 50, 76, 76) - assert new.getpixel((50, 50)) == GREEN - assert new.getpixel((50, 51)) == BLACK + assert new.getpixel((50, 51)) == GREEN + assert new.getpixel((50, 52)) == BLACK def test_subtract_scale_offset(): @@ -350,8 +350,8 @@ def test_subtract_modulo(): # Assert assert new.getbbox() == (25, 50, 76, 76) - assert new.getpixel((50, 50)) == GREEN - assert new.getpixel((50, 51)) == BLACK + assert new.getpixel((50, 51)) == GREEN + assert new.getpixel((50, 52)) == BLACK def test_subtract_modulo_no_clip(): diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 72f732d78..9fab41746 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -48,7 +48,7 @@ def test_sanity(): assert list(map(type, v)) == [str, str, str, str] # internal version number - assert re.search(r"\d+\.\d+$", features.version_module("littlecms2")) + assert re.search(r"\d+\.\d+(\.\d+)?$", features.version_module("littlecms2")) skip_missing() i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 0f9f20eb1..a87c1f2be 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -164,6 +164,19 @@ def test_arc_width_non_whole_angle(): assert_image_similar_tofile(im, expected, 1) +def test_arc_high(): + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc([10, 10, 89, 189], 20, 330, width=20, fill="white") + draw.arc([110, 10, 189, 189], 20, 150, width=20, fill="white") + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_arc_high.png")) + + def test_bitmap(): # Arrange im = Image.new("RGB", (W, H)) @@ -194,13 +207,11 @@ def helper_chord(mode, bbox, start, end): def test_chord1(): for mode in ["RGB", "L"]: helper_chord(mode, BBOX1, 0, 180) - helper_chord(mode, BBOX1, 0.5, 180.4) def test_chord2(): for mode in ["RGB", "L"]: helper_chord(mode, BBOX2, 0, 180) - helper_chord(mode, BBOX2, 0.5, 180.4) def test_chord_width(): @@ -240,6 +251,18 @@ def test_chord_zero_width(): assert_image_equal(im, expected) +def test_chord_too_fat(): + # Arrange + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + + # Act + draw.chord([-150, -150, 99, 99], 15, 60, width=10, fill="white", outline="red") + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_chord_too_fat.png")) + + def helper_ellipse(mode, bbox): # Arrange im = Image.new(mode, (W, H)) @@ -282,15 +305,18 @@ def test_ellipse_edge(): draw = ImageDraw.Draw(im) # Act - draw.ellipse(((0, 0), (W - 1, H)), fill="white") + draw.ellipse(((0, 0), (W - 1, H - 1)), fill="white") # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) def test_ellipse_symmetric(): - for bbox in [(25, 25, 76, 76), (25, 25, 75, 75)]: - im = Image.new("RGB", (101, 101)) + for width, bbox in ( + (100, (24, 24, 75, 75)), + (101, (25, 25, 75, 75)), + ): + im = Image.new("RGB", (width, 100)) draw = ImageDraw.Draw(im) draw.ellipse(bbox, fill="green", outline="blue") assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT)) @@ -345,6 +371,43 @@ def test_ellipse_zero_width(): assert_image_equal(im, expected) +def ellipse_various_sizes_helper(filled): + ellipse_sizes = range(32) + image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 + im = Image.new("RGB", (image_size, image_size)) + draw = ImageDraw.Draw(im) + + x = 1 + for w in ellipse_sizes: + y = 1 + for h in ellipse_sizes: + border = [x, y, x + w - 1, y + h - 1] + if filled: + draw.ellipse(border, fill="white") + else: + draw.ellipse(border, outline="white") + y += h + 1 + x += w + 1 + + return im + + +def test_ellipse_various_sizes(): + im = ellipse_various_sizes_helper(False) + + with Image.open("Tests/images/imagedraw_ellipse_various_sizes.png") as expected: + assert_image_equal(im, expected) + + +def test_ellipse_various_sizes_filled(): + im = ellipse_various_sizes_helper(True) + + with Image.open( + "Tests/images/imagedraw_ellipse_various_sizes_filled.png" + ) as expected: + assert_image_equal(im, expected) + + def helper_line(points): # Arrange im = Image.new("RGB", (W, H)) @@ -420,13 +483,13 @@ def helper_pieslice(bbox, start, end): def test_pieslice1(): - helper_pieslice(BBOX1, -90, 45) - helper_pieslice(BBOX1, -90.5, 45.4) + helper_pieslice(BBOX1, -92, 46) + helper_pieslice(BBOX1, -92.2, 46.2) def test_pieslice2(): - helper_pieslice(BBOX2, -90, 45) - helper_pieslice(BBOX2, -90.5, 45.4) + helper_pieslice(BBOX2, -92, 46) + helper_pieslice(BBOX2, -92.2, 46.2) def test_pieslice_width(): @@ -467,6 +530,18 @@ def test_pieslice_zero_width(): assert_image_equal(im, expected) +def test_pieslice_wide(): + # Arrange + im = Image.new("RGB", (200, 100)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice([0, 0, 199, 99], 190, 170, width=10, fill="white", outline="red") + + # Assert + assert_image_equal(im, Image.open("Tests/images/imagedraw_pieslice_wide.png")) + + def helper_point(points): # Arrange im = Image.new("RGB", (W, H)) @@ -1020,7 +1095,7 @@ def test_stroke(): font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) # Act - draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill) + draw.text((12, 12), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill) # Assert assert_image_similar_tofile( @@ -1036,7 +1111,7 @@ def test_stroke_descender(): font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) # Act - draw.text((10, 0), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") + draw.text((12, 2), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) @@ -1051,7 +1126,7 @@ def test_stroke_multiline(): # Act draw.multiline_text( - (10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0" + (12, 12), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0" ) # Assert diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 0d9f16ce8..b78dfe85b 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -79,7 +79,7 @@ def test_ellipse_edge(): brush = ImageDraw2.Brush("white") # Act - draw.ellipse(((0, 0), (W - 1, H)), brush) + draw.ellipse(((0, 0), (W - 1, H - 1)), brush) # Assert assert_image_similar(im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 62b6230ae..7824b7f53 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -15,9 +15,9 @@ from .helper import ( assert_image_equal_tofile, assert_image_similar, assert_image_similar_tofile, - is_pypy, is_win32, skip_unless_feature, + skip_unless_feature_version, ) FONT_PATH = "Tests/fonts/FreeMono.ttf" @@ -35,9 +35,30 @@ class TestImageFont: # Freetype has different metrics depending on the version. # (and, other things, but first things first) METRICS = { - (">=2.3", "<2.4"): {"multiline": 30, "textsize": 12, "getters": (13, 16)}, - (">=2.7",): {"multiline": 6.2, "textsize": 2.5, "getters": (12, 16)}, - "Default": {"multiline": 0.5, "textsize": 0.5, "getters": (12, 16)}, + (">=2.3", "<2.4"): { + "multiline": 30, + "textsize": 12, + "getters": (13, 16), + "mask": (107, 13), + "multiline-anchor": 6, + "getlength": (36, 27, 27, 33), + }, + (">=2.7",): { + "multiline": 6.2, + "textsize": 2.5, + "getters": (12, 16), + "mask": (108, 13), + "multiline-anchor": 4, + "getlength": (36, 21, 24, 33), + }, + "Default": { + "multiline": 0.5, + "textsize": 0.5, + "getters": (12, 16), + "mask": (108, 13), + "multiline-anchor": 4, + "getlength": (36, 24, 24, 33), + }, } @classmethod @@ -180,6 +201,37 @@ class TestImageFont: # Epsilon ~.5 fails with FreeType 2.7 assert_image_similar(im, target_img, self.metrics["textsize"]) + @pytest.mark.parametrize( + "text, mode, font, size, length_basic_index, length_raqm", + ( + # basic test + ("text", "L", "FreeMono.ttf", 15, 0, 36), + ("text", "1", "FreeMono.ttf", 15, 0, 36), + # issue 4177 + ("rrr", "L", "DejaVuSans.ttf", 18, 1, 22.21875), + ("rrr", "1", "DejaVuSans.ttf", 18, 2, 22.21875), + # test 'l' not including extra margin + # using exact value 2047 / 64 for raqm, checked with debugger + ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 3, 31.984375), + ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 3, 31.984375), + ), + ) + def test_getlength(self, text, mode, font, size, length_basic_index, length_raqm): + f = ImageFont.truetype( + "Tests/fonts/" + font, size, layout_engine=self.LAYOUT_ENGINE + ) + + im = Image.new(mode, (1, 1), 0) + d = ImageDraw.Draw(im) + + if self.LAYOUT_ENGINE == ImageFont.LAYOUT_BASIC: + length = d.textlength(text, f) + assert length == self.metrics["getlength"][length_basic_index] + else: + # disable kerning, kerning metrics changed + length = d.textlength(text, f, features=["-kern"]) + assert length == length_raqm + def test_render_multiline(self): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -343,7 +395,7 @@ class TestImageFont: mask = transposed_font.getmask(text) # Assert - assert mask.size == (13, 108) + assert mask.size == self.metrics["mask"][::-1] def test_unrotated_transposed_font_get_mask(self): # Arrange @@ -356,7 +408,7 @@ class TestImageFont: mask = transposed_font.getmask(text) # Assert - assert mask.size == (108, 13) + assert mask.size == self.metrics["mask"] def test_free_type_font_get_name(self): # Arrange @@ -400,7 +452,7 @@ class TestImageFont: mask = font.getmask(text) # Assert - assert mask.size == (108, 13) + assert mask.size == self.metrics["mask"] def test_load_path_not_found(self): # Arrange @@ -456,7 +508,6 @@ class TestImageFont: with pytest.raises(UnicodeEncodeError): font.getsize("’") - @pytest.mark.xfail(is_pypy(), reason="failing on PyPy with Raqm") def test_unicode_extended(self): # issue #3777 text = "A\u278A\U0001F12B" @@ -471,7 +522,8 @@ class TestImageFont: d = ImageDraw.Draw(img) d.text((10, 10), text, font=ttf) - assert_image_similar_tofile(img, target, self.metrics["multiline"]) + # fails with 14.7 + assert_image_similar_tofile(img, target, 6.2) def _test_fake_loading_font(self, monkeypatch, path_to_fake, fontname): # Make a copy of FreeTypeFont so we can patch the original @@ -728,24 +780,238 @@ class TestImageFont: font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) font.set_variation_by_axes([500, 50]) - self._check_text(font, "Tests/images/variation_adobe_axes.png", 5.1) + self._check_text(font, "Tests/images/variation_adobe_axes.png", 11.05) font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) font.set_variation_by_axes([100]) self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) + @pytest.mark.parametrize( + "anchor, left, left_old, top", + ( + # test horizontal anchors + ("ls", 0, 0, -36), + ("ms", -64, -65, -36), + ("rs", -128, -129, -36), + # test vertical anchors + ("ma", -64, -65, 16), + ("mt", -64, -65, 0), + ("mm", -64, -65, -17), + ("mb", -64, -65, -44), + ("md", -64, -65, -51), + ), + ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), + ) + def test_anchor(self, anchor, left, left_old, top): + name, text = "quick", "Quick" + path = f"Tests/images/test_anchor_{name}_{anchor}.png" + + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.4"): + width, height = (129, 44) + left = left_old + elif self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM: + width, height = (129, 44) + else: + width, height = (128, 44) + + bbox_expected = (left, top, left + width, top + height) + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE + ) + + im = Image.new("RGB", (200, 200), "white") + d = ImageDraw.Draw(im) + d.line(((0, 100), (200, 100)), "gray") + d.line(((100, 0), (100, 200)), "gray") + d.text((100, 100), text, fill="black", anchor=anchor, font=f) + + assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected + + with Image.open(path) as expected: + assert_image_similar(im, expected, 7) + + @pytest.mark.parametrize( + "anchor, align", + ( + # test horizontal anchors + ("lm", "left"), + ("lm", "center"), + ("lm", "right"), + ("mm", "left"), + ("mm", "center"), + ("mm", "right"), + ("rm", "left"), + ("rm", "center"), + ("rm", "right"), + # test vertical anchors + ("ma", "center"), + # ("mm", "center"), # duplicate + ("md", "center"), + ), + ) + def test_anchor_multiline(self, anchor, align): + target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" + text = "a\nlong\ntext sample" + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE + ) + + # test render + im = Image.new("RGB", (600, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (600, 200)), "gray") + d.line(((300, 0), (300, 400)), "gray") + d.multiline_text( + (300, 200), text, fill="black", anchor=anchor, font=f, align=align + ) + + with Image.open(target) as expected: + assert_image_similar(im, expected, self.metrics["multiline-anchor"]) + + def test_anchor_invalid(self): + font = self.get_font() + im = Image.new("RGB", (100, 100), "white") + d = ImageDraw.Draw(im) + d.font = font + + for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: + pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) + pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) + pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) + pytest.raises( + ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor) + ) + pytest.raises( + ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), + ) + for anchor in ["lt", "lb"]: + pytest.raises( + ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), + ) + + @skip_unless_feature("freetype2") + @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) + def test_bitmap_font(self, bpp): + text = "Bitmap Font" + 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", + 24, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font) + + assert_image_equal_tofile(im, target) + + def test_standard_embedded_color(self): + txt = "Hello World!" + ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE) + ttf.getsize(txt) + + im = Image.new("RGB", (300, 64), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) + + with Image.open("Tests/images/standard_embedded.png") as expected: + assert_image_similar(im, expected, max(self.metrics["multiline"], 3)) + + @skip_unless_feature_version("freetype2", "2.5.0") + def test_cbdt(self): + try: + font = ImageFont.truetype( + "Tests/fonts/NotoColorEmoji.ttf", + size=109, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (150, 150), "white") + d = ImageDraw.Draw(im) + + d.text((10, 10), "\U0001f469", embedded_color=True, font=font) + + with Image.open("Tests/images/cbdt_notocoloremoji.png") as expected: + assert_image_similar(im, expected, self.metrics["multiline"]) + except IOError as e: + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or unsupported") + + @skip_unless_feature_version("freetype2", "2.5.0") + def test_cbdt_mask(self): + try: + font = ImageFont.truetype( + "Tests/fonts/NotoColorEmoji.ttf", + size=109, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (150, 150), "white") + d = ImageDraw.Draw(im) + + d.text((10, 10), "\U0001f469", "black", font=font) + + with Image.open("Tests/images/cbdt_notocoloremoji_mask.png") as expected: + assert_image_similar(im, expected, self.metrics["multiline"]) + except IOError as e: + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or unsupported") + + @skip_unless_feature_version("freetype2", "2.10.0") + def test_colr(self): + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", embedded_color=True, font=font) + + with Image.open("Tests/images/colr_bungee.png") as expected: + assert_image_similar(im, expected, 21) + + @skip_unless_feature_version("freetype2", "2.10.0") + def test_colr_mask(self): + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", "black", font=font) + + with Image.open("Tests/images/colr_bungee_mask.png") as expected: + assert_image_similar(im, expected, 22) + @skip_unless_feature("raqm") class TestImageFont_RaqmLayout(TestImageFont): LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM +@skip_unless_feature_version("freetype2", "2.4", "Different metrics") def test_render_mono_size(): # issue 4177 - if parse_version(ImageFont.core.freetype2_version) < parse_version("2.4"): - pytest.skip("Different metrics") - im = Image.new("P", (100, 30), "white") draw = ImageDraw.Draw(im) ttf = ImageFont.truetype( diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py deleted file mode 100644 index 0ba682885..000000000 --- a/Tests/test_imagefont_bitmap.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest - -from PIL import Image, ImageDraw, ImageFont - -from .helper import assert_image_similar - -image_font_installed = True -try: - ImageFont.core.getfont -except ImportError: - image_font_installed = False - - -@pytest.mark.skipif(not image_font_installed, reason="Image font not installed") -def test_similar(): - text = "EmbeddedBitmap" - font_outline = ImageFont.truetype(font="Tests/fonts/DejaVuSans.ttf", size=24) - font_bitmap = ImageFont.truetype(font="Tests/fonts/DejaVuSans-bitmap.ttf", size=24) - size_outline = font_outline.getsize(text) - size_bitmap = font_bitmap.getsize(text) - size_final = ( - max(size_outline[0], size_bitmap[0]), - max(size_outline[1], size_bitmap[1]), - ) - im_bitmap = Image.new("RGB", size_final, (255, 255, 255)) - im_outline = im_bitmap.copy() - draw_bitmap = ImageDraw.Draw(im_bitmap) - draw_outline = ImageDraw.Draw(im_outline) - - # Metrics are different on the bitmap and TTF fonts, - # more so on some platforms and versions of FreeType than others. - # Mac has a 1px difference, Linux doesn't. - draw_bitmap.text( - (0, size_final[1] - size_bitmap[1]), text, fill=(0, 0, 0), font=font_bitmap - ) - draw_outline.text( - (0, size_final[1] - size_outline[1]), - text, - fill=(0, 0, 0), - font=font_outline, - ) - assert_image_similar(im_bitmap, im_outline, 20) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index edd6fab57..82e2b4ebc 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,8 +1,13 @@ import pytest +from packaging.version import parse as parse_version -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw, ImageFont, features -from .helper import assert_image_similar, skip_unless_feature +from .helper import ( + assert_image_similar, + skip_unless_feature, + skip_unless_feature_version, +) FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans.ttf" @@ -113,7 +118,7 @@ def test_text_direction_ttb(): target = "Tests/images/test_direction_ttb.png" with Image.open(target) as target_img: - assert_image_similar(im, target_img, 1.15) + assert_image_similar(im, target_img, 2.8) def test_text_direction_ttb_stroke(): @@ -123,7 +128,7 @@ def test_text_direction_ttb_stroke(): draw = ImageDraw.Draw(im) try: draw.text( - (25, 25), + (27, 27), "あい", font=ttf, fill=500, @@ -137,7 +142,7 @@ def test_text_direction_ttb_stroke(): target = "Tests/images/test_direction_ttb_stroke.png" with Image.open(target) as target_img: - assert_image_similar(im, target_img, 12.4) + assert_image_similar(im, target_img, 19.4) def test_ligature_features(): @@ -206,3 +211,221 @@ def test_language(): target = "Tests/images/test_language.png" with Image.open(target) as target_img: assert_image_similar(im, target_img, 0.5) + + +@pytest.mark.parametrize("mode", ("L", "1")) +@pytest.mark.parametrize( + "text, direction, expected", + ( + ("سلطنة عمان Oman", None, 173.703125), + ("سلطنة عمان Oman", "ltr", 173.703125), + ("Oman سلطنة عمان", "rtl", 173.703125), + ("English عربي", "rtl", 123.796875), + ("test", "ttb", 80.0), + ), + ids=("None", "ltr", "rtl2", "rtl", "ttb"), +) +def test_getlength(mode, text, direction, expected): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode, (1, 1), 0) + d = ImageDraw.Draw(im) + + try: + assert d.textlength(text, ttf, direction) == expected + except ValueError as ex: + if ( + direction == "ttb" + and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" + ): + pytest.skip("libraqm 0.7 or greater not available") + + +@pytest.mark.parametrize("mode", ("L", "1")) +@pytest.mark.parametrize("direction", ("ltr", "ttb")) +@pytest.mark.parametrize( + "text", + ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), + ids=("caron-above", "caron-below", "double-breve", "overline"), +) +def test_getlength_combine(mode, direction, text): + if text == "i\u0305i" and direction == "ttb": + pytest.skip("fails with this font") + + ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + + try: + target = ttf.getlength("ii", mode, direction) + actual = ttf.getlength(text, mode, direction) + + assert actual == target + except ValueError as ex: + if ( + direction == "ttb" + and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" + ): + pytest.skip("libraqm 0.7 or greater not available") + + +# FreeType 2.5.1 README: Miscellaneous Changes: +# Improved computation of emulated vertical metrics for TrueType fonts. +@skip_unless_feature_version( + "freetype2", "2.5.1", "FreeType <2.5.1 has incompatible ttb metrics" +) +@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) +def test_anchor_ttb(anchor): + text = "f" + path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) + + im = Image.new("RGB", (200, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (200, 200)), "gray") + d.line(((100, 0), (100, 400)), "gray") + try: + d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + with Image.open(path) as expected: + assert_image_similar(im, expected, 1) # fails at 5 + + +combine_tests = ( + # extends above (e.g. issue #4553) + ("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08), + ("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08), + ("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08), + ("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08), + ("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3), + ("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3), + # extends below + ("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02), + ("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02), + ("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02), + ("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02), + ("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03), + ("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03), + # extends to the right (e.g. issue #3745) + ("double_breve_below", "a\u035Ci", None, None, 0.02), + ("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02), + ("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02), + ("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02), + ("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02), + ("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02), + ("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02), + # extends to the left (fail=0.064) + ("overline", "i\u0305", None, None, 0.02), + ("overline_la", "i\u0305", "la", None, 0.02), + ("overline_ra", "i\u0305", "ra", None, 0.02), + ("overline_ttb", "i\u0305", None, "ttb", 0.02), + ("overline_ttb_rt", "i\u0305", "rt", "ttb", 0.02), + ("overline_ttb_mt", "i\u0305", "mt", "ttb", 0.02), + ("overline_ttb_st", "i\u0305", "st", "ttb", 0.02), +) + + +# this tests various combining characters for anchor alignment and clipping +@pytest.mark.parametrize( + "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] +) +def test_combine(name, text, dir, anchor, epsilon): + if ( + parse_version(features.version_module("freetype2")) < parse_version("2.5.1") + and dir == "ttb" + ): + # FreeType 2.5.1 README: Miscellaneous Changes: + # Improved computation of emulated vertical metrics for TrueType fonts. + pytest.skip("FreeType <2.5.1 has incompatible ttb metrics") + + path = f"Tests/images/test_combine_{name}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (400, 200)), "gray") + d.line(((200, 0), (200, 400)), "gray") + try: + d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + with Image.open(path) as expected: + assert_image_similar(im, expected, epsilon) + + +@pytest.mark.parametrize( + "anchor, align", + ( + ("lm", "left"), # pass with getsize + ("lm", "center"), # fail at 2.12 + ("lm", "right"), # fail at 2.57 + ("mm", "left"), # fail at 2.12 + ("mm", "center"), # pass with getsize + ("mm", "right"), # fail at 2.12 + ("rm", "left"), # fail at 2.57 + ("rm", "center"), # fail at 2.12 + ("rm", "right"), # pass with getsize + ), +) +def test_combine_multiline(anchor, align): + # test that multiline text uses getlength, not getsize or getbbox + + path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (400, 200)), "gray") + d.line(((200, 0), (200, 400)), "gray") + bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align) + d.rectangle(bbox, outline="red") + d.multiline_text((200, 200), text, fill="black", anchor=anchor, font=f, align=align) + + with Image.open(path) as expected: + assert_image_similar(im, expected, 0.015) + + +def test_anchor_invalid_ttb(): + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new("RGB", (100, 100), "white") + d = ImageDraw.Draw(im) + d.font = font + + for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]: + pytest.raises( + ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb") + ) + pytest.raises( + ValueError, lambda: font.getbbox("hello", anchor=anchor, direction="ttb") + ) + pytest.raises( + ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb") + ) + pytest.raises( + ValueError, + lambda: d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb"), + ) + pytest.raises( + ValueError, + lambda: d.multiline_text( + (0, 0), "foo\nbar", anchor=anchor, direction="ttb" + ), + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox( + (0, 0), "foo\nbar", anchor=anchor, direction="ttb" + ), + ) + # ttb multiline text does not support anchors at all + pytest.raises( + ValueError, + lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"), + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb"), + ) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py new file mode 100644 index 000000000..9c293e014 --- /dev/null +++ b/Tests/test_tiff_crashes.py @@ -0,0 +1,36 @@ +# Reproductions/tests for crashes/read errors in TiffDecode.c + +# When run in Python, all of these images should fail for +# one reason or another, either as a buffer overrun, +# unrecognized datastream, or truncated image file. +# There shouldn't be any segfaults. +# +# if run like +# `valgrind --tool=memcheck pytest test_tiff_crashes.py 2>&1 | grep TiffDecode.c` +# the output should be empty. There may be Python issues +# in the valgrind especially if run in a debug Python +# version. + +import pytest + +from PIL import Image + +from .helper import on_ci + + +@pytest.mark.parametrize( + "test_file", ["Tests/images/crash_1.tif", "Tests/images/crash_2.tif"] +) +@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") +@pytest.mark.filterwarnings("ignore:Metadata warning") +def test_tiff_crashes(test_file): + try: + with Image.open(test_file) as im: + im.load() + except FileNotFoundError: + if not on_ci(): + pytest.skip("test image not found") + return + raise + except OSError: + pass diff --git a/docs/example/anchors.png b/docs/example/anchors.png new file mode 100644 index 000000000..40476b092 Binary files /dev/null and b/docs/example/anchors.png differ diff --git a/docs/example/anchors.py b/docs/example/anchors.py new file mode 100644 index 000000000..3447de4f7 --- /dev/null +++ b/docs/example/anchors.py @@ -0,0 +1,28 @@ +from PIL import Image, ImageDraw, ImageFont + +font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 16) + + +def test(anchor): + im = Image.new("RGBA", (200, 100), "white") + d = ImageDraw.Draw(im) + d.line(((100, 0), (100, 100)), "gray") + d.line(((0, 50), (200, 50)), "gray") + d.text((100, 50), f"{anchor} example", "black", font, anchor) + return im + + +if __name__ == "__main__": + im = Image.new("RGBA", (600, 300), "white") + d = ImageDraw.Draw(im) + for y, row in enumerate( + (("ma", "mt", "mm"), ("ms", "mb", "md"), ("ls", "ms", "rs")) + ): + for x, anchor in enumerate(row): + im.paste(test(anchor), (x * 200, y * 100)) + if x != 0: + d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3) + if y != 0: + d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3) + im.save("docs/example/anchors.png") + im.show() diff --git a/docs/handbook/appendices.rst b/docs/handbook/appendices.rst index e6c415cc6..6afaef071 100644 --- a/docs/handbook/appendices.rst +++ b/docs/handbook/appendices.rst @@ -7,4 +7,5 @@ Appendices :maxdepth: 2 image-file-formats + text-anchors writing-your-own-file-decoder diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index f54b7acf7..5ed72ac1a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -535,7 +535,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: A tuple of two numbers corresponding to the desired dpi in each direction. **pnginfo** - A :py:class:`PIL.PngImagePlugin.PngInfo` instance containing text tags. + A :py:class:`PIL.PngImagePlugin.PngInfo` instance containing chunks. **compress_level** ZLIB compression level, a number between 0 and 9: 1 gives best speed, diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst new file mode 100644 index 000000000..0aecd3483 --- /dev/null +++ b/docs/handbook/text-anchors.rst @@ -0,0 +1,140 @@ + +.. _text-anchors: + +Text anchors +============ + +The ``anchor`` parameter determines the alignment of drawn text relative to the ``xy`` parameter. +The default alignment is top left, specifically ``la`` (left-ascender) for horizontal text +and ``lt`` (left-top) for vertical text. + +This parameter is only supported by OpenType/TrueType fonts. +Other fonts may ignore the parameter and use the default (top left) alignment. + +Specifying an anchor +^^^^^^^^^^^^^^^^^^^^ + +An anchor is specified with a two-character string. The first character is the +horizontal alignment, the second character is the vertical alignment. +For example, the default value of ``la`` for horizontal text means left-ascender +aligned text. + +When drawing text with :py:meth:`PIL.ImageDraw.ImageDraw.text` with a specific anchor, +the text will be placed such that the specified anchor point is at the ``xy`` coordinates. + +For example, in the following image, the text is ``ms`` (middle-baseline) aligned, with +``xy`` at the intersection of the two lines: + +.. image:: ../../Tests/images/test_anchor_quick_ms.png + :alt: ms (middle-baseline) aligned text. + :align: left + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + im = Image.new("RGB", (200, 200), "white") + d = ImageDraw.Draw(im) + d.line(((0, 100), (200, 100)), "gray") + d.line(((100, 0), (100, 200)), "gray") + d.text((100, 100), "Quick", fill="black", anchor="ms", font=font) + +.. container:: clearer + + | + +.. only: comment + The container above prevents the image alignment from affecting the following text. + +Quick reference +^^^^^^^^^^^^^^^ + +.. image:: ../resources/anchor_horizontal.svg + :alt: Horizontal text + :align: center + +.. image:: ../resources/anchor_vertical.svg + :alt: Vertical text + :align: center + +Horizontal anchor alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``l`` --- left + Anchor is to the left of the text. + + For *horizontal* text this is the origin of the first glyph, as shown in the `FreeType tutorial`_. + +``m`` --- middle + Anchor is horizontally centered with the text. + + For *vertical* text it is recommended to use ``s`` (baseline) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``r`` --- right + Anchor is to the right of the text. + + For *horizontal* text this is the advanced origin of the last glyph, as shown in the `FreeType tutorial`_. + +``s`` --- baseline *(vertical text only)* + Anchor is at the baseline (middle) of the text. The exact alignment depends on the font. + + For *vertical* text this is the recommended alignment, + as it does not change based on the specific glyphs of the given text + (see image for vertical text above). + +Vertical anchor alignment +^^^^^^^^^^^^^^^^^^^^^^^^^ + +``a`` --- ascender / top *(horizontal text only)* + Anchor is at the ascender line (top) of the first line of text, as defined by the font. + + See `Font metrics on Wikipedia`_ for more information. + +``t`` --- top *(single-line text only)* + Anchor is at the top of the text. + + For *vertical* text this is the origin of the first glyph, as shown in the `FreeType tutorial`_. + + For *horizontal* text it is recommended to use ``a`` (ascender) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``m`` --- middle + Anchor is vertically centered with the text. + + For *horizontal* text this is the midpoint of the first ascender line and the last descender line. + +``s`` --- baseline *(horizontal text only)* + Anchor is at the baseline (bottom) of the first line of text, only descenders extend below the anchor. + + See `Font metrics on Wikipedia`_ for more information. + +``b`` --- bottom *(single-line text only)* + Anchor is at the bottom of the text. + + For *vertical* text this is the advanced origin of the last glyph, as shown in the `FreeType tutorial`_. + + For *horizontal* text it is recommended to use ``d`` (descender) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``d`` --- descender / bottom *(horizontal text only)* + Anchor is at the descender line (bottom) of the last line of text, as defined by the font. + + See `Font metrics on Wikipedia`_ for more information. + +Examples +^^^^^^^^ + +The following image shows several examples of anchors for horizontal text. +In each section the ``xy`` parameter was set to the center shown by the intersection +of the two lines. + +.. comment: Image generated with ../example/anchors.py + +.. image:: ../example/anchors.png + :alt: Text anchor examples + :align: center + +.. _Font metrics on Wikipedia: https://en.wikipedia.org/wiki/Typeface#Font_metrics +.. _FreeType tutorial: https://freetype.org/freetype2/docs/tutorial/step2.html diff --git a/docs/index.rst b/docs/index.rst index 4f16ba960..fb59a6c00 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,6 +57,15 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more + + + + Pillow horizontal text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Pillow horizontal text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (d) descender + (s) baseline + (a) ascender + (m) middle + (t) top + (b) bottom + (l) left + (r) right + (m) middle + + + Horizontal text + + diff --git a/docs/resources/anchor_vertical.svg b/docs/resources/anchor_vertical.svg new file mode 100644 index 000000000..95da30ffd --- /dev/null +++ b/docs/resources/anchor_vertical.svg @@ -0,0 +1,841 @@ + + + + + Pillow vertical text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Pillow vertical text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (l)left + (s) baseline + (r)right + (t) top + (m) middle + (b) bottom + (m)middle + (l)left + (s) baseline + (r)right + (t) top + (m) middle + (b) bottom + (m)middle + + + Verticaltext + + diff --git a/setup.py b/setup.py index 4e07d0604..8b6fdab81 100755 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ TIFF_ROOT = None ZLIB_ROOT = None -if sys.platform == "win32" and sys.version_info >= (3, 9): +if sys.platform == "win32" and sys.version_info >= (3, 10): warnings.warn( f"Pillow {PILLOW_VERSION} does not support Python " f"{sys.version_info.major}.{sys.version_info.minor} and does not provide " @@ -869,6 +869,7 @@ try: "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 711e030e1..1bcbe93b2 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -162,10 +162,6 @@ class BmpImageFile(ImageFile.ImageFile): else (1 << file_info["bits"]) ) - # ------------------------------- Check abnormal values for DOS attacks - if file_info["width"] * file_info["height"] > 2 ** 31: - raise OSError("Unsupported BMP Size: (%dx%d)" % self.size) - # ---------------------- Check bit depth for unusual unsupported values self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) if self.mode is None: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a18a231d3..f9a5d82fd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3285,7 +3285,7 @@ class Exif(MutableMapping): return value def _fixup_dict(self, src_dict): - # Helper function for _getexif() + # Helper function # returns a dict with any single item tuples/lists as individual values return {k: self._fixup(v) for k, v in src_dict.items()} @@ -3466,7 +3466,8 @@ class Exif(MutableMapping): def __delitem__(self, tag): if self._info is not None and tag in self._info: del self._info[tag] - del self._data[tag] + else: + del self._data[tag] def __iter__(self): keys = set(self._data) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 47d970aef..b823be9a2 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -118,7 +118,7 @@ class ImageDraw: fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None, width=0): + def arc(self, xy, start, end, fill=None, width=1): """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: @@ -282,6 +282,7 @@ class ImageDraw: language=None, stroke_width=0, stroke_fill=None, + embedded_color=False, *args, **kwargs, ): @@ -299,8 +300,12 @@ class ImageDraw: language, stroke_width, stroke_fill, + embedded_color, ) + if embedded_color and self.mode not in ("RGB", "RGBA"): + raise ValueError("Embedded color supported only in RGB and RGBA modes") + if font is None: font = self.getfont() @@ -311,15 +316,20 @@ class ImageDraw: return ink def draw_text(ink, stroke_width=0, stroke_offset=None): + mode = self.fontmode + if stroke_width == 0 and embedded_color: + mode = "RGBA" coord = xy try: mask, offset = font.getmask2( text, - self.fontmode, + mode, direction=direction, features=features, language=language, stroke_width=stroke_width, + anchor=anchor, + ink=ink, *args, **kwargs, ) @@ -328,11 +338,13 @@ class ImageDraw: try: mask = font.getmask( text, - self.fontmode, + mode, direction, features, language, stroke_width, + anchor, + ink, *args, **kwargs, ) @@ -340,7 +352,15 @@ class ImageDraw: mask = font.getmask(text) if stroke_offset: coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] - self.draw.draw_bitmap(coord, mask, ink) + if mode == "RGBA": + # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A + # extract mask and set text alpha + color, mask = mask, mask.getband(3) + color.fillband(3, (ink >> 24) & 0xFF) + coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1] + self.im.paste(color, coord + coord2, mask) + else: + self.draw.draw_bitmap(coord, mask, ink) ink = getink(fill) if ink is not None: @@ -353,7 +373,7 @@ class ImageDraw: draw_text(stroke_ink, stroke_width) # Draw normal text - draw_text(ink, 0, (stroke_width, stroke_width)) + draw_text(ink, 0) else: # Only draw normal text draw_text(ink) @@ -372,7 +392,18 @@ class ImageDraw: language=None, stroke_width=0, stroke_fill=None, + embedded_color=False, ): + if direction == "ttb": + raise ValueError("ttb direction is unsupported for multiline text") + + if anchor is None: + anchor = "la" + elif len(anchor) != 2: + raise ValueError("anchor must be a 2 character string") + elif anchor[1] in "tb": + raise ValueError("anchor not supported for multiline text") + widths = [] max_width = 0 lines = self._multiline_split(text) @@ -380,26 +411,38 @@ class ImageDraw: self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing ) for line in lines: - line_width, line_height = self.textsize( - line, - font, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, + line_width = self.textlength( + line, font, direction=direction, features=features, language=language ) widths.append(line_width) max_width = max(max_width, line_width) - left, top = xy + + top = xy[1] + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # first align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + + # then align by align parameter if align == "left": - pass # left = x + pass elif align == "center": - left += (max_width - widths[idx]) / 2.0 + left += width_difference / 2.0 elif align == "right": - left += max_width - widths[idx] + left += width_difference else: raise ValueError('align must be "left", "center" or "right"') + self.text( (left, top), line, @@ -411,9 +454,9 @@ class ImageDraw: language=language, stroke_width=stroke_width, stroke_fill=stroke_fill, + embedded_color=embedded_color, ) top += line_spacing - left = xy[0] def textsize( self, @@ -457,6 +500,172 @@ class ImageDraw: max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing + def textlength( + self, + text, + font=None, + direction=None, + features=None, + language=None, + embedded_color=False, + ): + """Get the length of a given string, in pixels with 1/64 precision.""" + if self._multiline_check(text): + raise ValueError("can't measure length of multiline text") + if embedded_color and self.mode not in ("RGB", "RGBA"): + raise ValueError("Embedded color supported only in RGB and RGBA modes") + + if font is None: + font = self.getfont() + mode = "RGBA" if embedded_color else self.fontmode + try: + return font.getlength(text, mode, direction, features, language) + except AttributeError: + size = self.textsize( + text, font, direction=direction, features=features, language=language + ) + if direction == "ttb": + return size[1] + return size[0] + + def textbbox( + self, + xy, + text, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + embedded_color=False, + ): + """Get the bounding box of a given string, in pixels.""" + if embedded_color and self.mode not in ("RGB", "RGBA"): + raise ValueError("Embedded color supported only in RGB and RGBA modes") + + if self._multiline_check(text): + return self.multiline_textbbox( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + ) + + if font is None: + font = self.getfont() + mode = "RGBA" if embedded_color else self.fontmode + bbox = font.getbbox( + text, mode, direction, features, language, stroke_width, anchor + ) + return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] + + def multiline_textbbox( + self, + xy, + text, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + embedded_color=False, + ): + if direction == "ttb": + raise ValueError("ttb direction is unsupported for multiline text") + + if anchor is None: + anchor = "la" + elif len(anchor) != 2: + raise ValueError("anchor must be a 2 character string") + elif anchor[1] in "tb": + raise ValueError("anchor not supported for multiline text") + + widths = [] + max_width = 0 + lines = self._multiline_split(text) + line_spacing = ( + self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing + ) + for line in lines: + line_width = self.textlength( + line, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + widths.append(line_width) + max_width = max(max_width, line_width) + + top = xy[1] + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + + bbox = None + + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # first align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + + # then align by align parameter + if align == "left": + pass + elif align == "center": + left += width_difference / 2.0 + elif align == "right": + left += width_difference + else: + raise ValueError('align must be "left", "center" or "right"') + + bbox_line = self.textbbox( + (left, top), + line, + font, + anchor, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + embedded_color=embedded_color, + ) + if bbox is None: + bbox = bbox_line + else: + bbox = ( + min(bbox[0], bbox_line[0]), + min(bbox[1], bbox_line[1]), + max(bbox[2], bbox_line[2]), + max(bbox[3], bbox_line[3]), + ) + + top += line_spacing + + if bbox is None: + return xy[0], xy[1], xy[0], xy[1] + return bbox + def Draw(im, mode=None): """ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 63f819151..89d180ce8 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -215,6 +215,147 @@ class FreeTypeFont: """ return self.font.ascent, self.font.descent + def getlength(self, text, mode="", direction=None, features=None, language=None): + """ + Returns length (in pixels with 1/64 precision) of given text when rendered + in font with provided direction, features, and language. + + This is the amount by which following text should be offset. + Text bounding box may extend past the length in some fonts, + e.g. when using italics or accents. + + The result is returned as a float; it is a whole number if using basic layout. + + Note that the sum of two lengths may not equal the length of a concatenated + string due to kerning. If you need to adjust for kerning, include the following + character and subtract its length. + + For example, instead of + + .. code-block:: python + + hello = font.getlength("Hello") + world = font.getlength("World") + hello_world = hello + world # not adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # may fail + + use + + .. code-block:: python + + hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning + world = font.getlength("World") + hello_world = hello + world # adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # True + + or disable kerning with (requires libraqm) + + .. code-block:: python + + hello = draw.textlength("Hello", font, features=["-kern"]) + world = draw.textlength("World", font, features=["-kern"]) + hello_world = hello + world # kerning is disabled, no need to adjust + assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) + + .. versionadded:: 8.0.0 + + :param text: Text to measure. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + :return: Width for horizontal, height for vertical text. + """ + return self.font.getlength(text, mode, direction, features, language) / 64 + + def getbbox( + self, + text, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + anchor=None, + ): + """ + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + + Use :py:meth:`getlength()` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + ` + Requires libraqm. + + :param stroke_width: The width of the text stroke. + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + :return: ``(left, top, right, bottom)`` bounding box + """ + size, offset = self.font.getsize( + text, mode, direction, features, language, anchor + ) + left, top = offset[0] - stroke_width, offset[1] - stroke_width + width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width + return left, top, left + width, top + height + def getsize( self, text, direction=None, features=None, language=None, stroke_width=0 ): @@ -222,6 +363,15 @@ class FreeTypeFont: Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language. + Use :py:meth:`getlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. + + .. note:: For historical reasons this function measures text height from + the ascender line instead of the top, see :ref:`text-anchors`. + If you wish to measure text height from the top, it is recommended + to use the bottom value of :meth:`getbbox` with ``anchor='lt'`` instead. + :param text: Text to measure. :param direction: Direction of the text. It can be 'rtl' (right to @@ -259,9 +409,11 @@ class FreeTypeFont: :return: (width, height) """ - size, offset = self.font.getsize(text, False, direction, features, language) + # vertical offset is added for historical reasons + # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 + size, offset = self.font.getsize(text, "L", direction, features, language) return ( - size[0] + stroke_width * 2 + offset[0], + size[0] + stroke_width * 2, size[1] + stroke_width * 2 + offset[1], ) @@ -345,12 +497,15 @@ class FreeTypeFont: features=None, language=None, stroke_width=0, + anchor=None, + ink=0, ): """ Create a bitmap for the text. If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. Otherwise, it should have mode ``1``. + maximum value of 255. If the font has embedded color data, the bitmap + should have mode ``RGBA``. Otherwise, it should have mode ``1``. :param text: Text to render. :param mode: Used by some graphics drivers to indicate what mode the @@ -393,6 +548,16 @@ class FreeTypeFont: .. versionadded:: 6.2.0 + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. """ @@ -403,6 +568,8 @@ class FreeTypeFont: features=features, language=language, stroke_width=stroke_width, + anchor=anchor, + ink=ink, )[0] def getmask2( @@ -414,6 +581,8 @@ class FreeTypeFont: features=None, language=None, stroke_width=0, + anchor=None, + ink=0, *args, **kwargs, ): @@ -421,7 +590,8 @@ class FreeTypeFont: Create a bitmap for the text. If the font uses antialiasing, the bitmap should have mode ``L`` and use a - maximum value of 255. Otherwise, it should have mode ``1``. + maximum value of 255. If the font has embedded color data, the bitmap + should have mode ``RGBA``. Otherwise, it should have mode ``1``. :param text: Text to render. :param mode: Used by some graphics drivers to indicate what mode the @@ -464,17 +634,28 @@ class FreeTypeFont: .. versionadded:: 6.2.0 + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + :return: A tuple of an internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ size, offset = self.font.getsize( - text, mode == "1", direction, features, language + text, mode, direction, features, language, anchor ) size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 - im = fill("L", size, 0) + offset = offset[0] - stroke_width, offset[1] - stroke_width + im = fill("RGBA" if mode == "RGBA" else "L", size, 0) self.font.render( - text, im.id, mode == "1", direction, features, language, stroke_width + text, im.id, mode, direction, features, language, stroke_width, ink ) return im, offset diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6145e5da7..8841bbf6c 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -475,13 +475,6 @@ class JpegImageFile(ImageFile.ImageFile): return _getmp(self) -def _fixup_dict(src_dict): - # Helper function for _getexif() - # returns a dict with any single item tuples/lists as individual values - exif = Image.Exif() - return exif._fixup_dict(src_dict) - - def _getexif(self): if "exif" not in self.info: return None diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index bcc6b1c98..6af41af6e 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -266,15 +266,20 @@ class PngInfo: def __init__(self): self.chunks = [] - def add(self, cid, data): + def add(self, cid, data, after_idat=False): """Appends an arbitrary chunk. Use with caution. :param cid: a byte string, 4 bytes long. :param data: a byte string of the encoded data + :param after_idat: for use with private chunks. Whether the chunk + should be written after IDAT """ - self.chunks.append((cid, data)) + chunk = [cid, data] + if after_idat: + chunk.append(True) + self.chunks.append(tuple(chunk)) def add_itxt(self, key, value, lang="", tkey="", zip=False): """Appends an iTXt chunk. @@ -676,6 +681,7 @@ class PngImageFile(ImageFile.ImageFile): # # Parse headers up to the first IDAT or fDAT chunk + self.private_chunks = [] self.png = PngStream(self.fp) while True: @@ -692,6 +698,8 @@ class PngImageFile(ImageFile.ImageFile): except AttributeError: logger.debug("%r %s %s (unknown)", cid, pos, length) s = ImageFile._safe_read(self.fp, length) + if cid[1:2].islower(): + self.private_chunks.append((cid, s)) self.png.crc(cid, s) @@ -923,7 +931,9 @@ class PngImageFile(ImageFile.ImageFile): ImageFile._safe_read(self.fp, length) except AttributeError: logger.debug("%r %s %s (unknown)", cid, pos, length) - ImageFile._safe_read(self.fp, length) + s = ImageFile._safe_read(self.fp, length) + if cid[1:2].islower(): + self.private_chunks.append((cid, s, True)) self._text = self.png.im_text if not self.is_animated: self.png.close() @@ -1248,12 +1258,18 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): info = im.encoderinfo.get("pnginfo") if info: chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] - for cid, data in info.chunks: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] if cid in chunks: chunks.remove(cid) chunk(fp, cid, data) elif cid in chunks_multiple_allowed: chunk(fp, cid, data) + elif cid[1:2].islower(): + # Private chunk + after_idat = info_chunk[2:3] + if not after_idat: + chunk(fp, cid, data) if im.mode == "P": palette_byte_number = (2 ** bits) * 3 @@ -1303,7 +1319,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): if info: chunks = [b"bKGD", b"hIST"] - for cid, data in info.chunks: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] if cid in chunks: chunks.remove(cid) chunk(fp, cid, data) @@ -1321,6 +1338,15 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): else: ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + if info: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] + if cid[1:2].islower(): + # Private chunk + after_idat = info_chunk[2:3] + if after_idat: + chunk(fp, cid, data) + chunk(fp, b"IEND", b"") if hasattr(fp, "flush"): diff --git a/src/PIL/features.py b/src/PIL/features.py index fa86b9cb0..c2df9c525 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -224,6 +224,8 @@ def pilinfo(out=None, supported_formats=True): If ``True``, a list of all supported image file formats will be printed. """ + from pkg_resources import parse_version + if out is None: out = sys.stdout @@ -269,7 +271,10 @@ def pilinfo(out=None, supported_formats=True): else: v = version(name) if v is not None: - t = "compiled for" if name in ("pil", "jpg") else "loaded" + version_static = name in ("pil", "jpg") + if name == "littlecms2": + version_static = parse_version(v) < parse_version("2.7.0") + t = "compiled for" if version_static else "loaded" print("---", feature, "support ok,", t, v, file=out) else: print("---", feature, "support ok", file=out) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index a0d9d38eb..bf74d2ba8 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -63,7 +63,7 @@ ImagingFind(const char* name) Py_ssize_t id; /* FIXME: use CObject instead? */ -#if defined(_MSC_VER) && defined(_WIN64) +#if defined(_WIN64) id = _atoi64(name); #else id = atol(name); diff --git a/src/_imaging.c b/src/_imaging.c index b490763a2..3635d2afa 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -518,6 +518,9 @@ getink(PyObject* color, Imaging im, char* ink) be cast to either UINT8 or INT32 */ int rIsInt = 0; + if (PyTuple_Check(color) && PyTuple_Size(color) == 1) { + color = PyTuple_GetItem(color, 0); + } if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || im->type == IMAGING_TYPE_SPECIAL) { @@ -533,7 +536,7 @@ getink(PyObject* color, Imaging im, char* ink) return NULL; } } else { - PyErr_SetString(PyExc_TypeError, "color must be int"); + PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); return NULL; } } @@ -2836,8 +2839,7 @@ _draw_arc(ImagingDrawObject* self, PyObject* args) int ink; int width = 0; float start, end; - int op = 0; - if (!PyArg_ParseTuple(args, "Offi|ii", &data, &start, &end, &ink, &width)) { + if (!PyArg_ParseTuple(args, "Offi|i", &data, &start, &end, &ink, &width)) { return NULL; } @@ -2854,7 +2856,7 @@ _draw_arc(ImagingDrawObject* self, PyObject* args) n = ImagingDrawArc(self->image->image, (int) xy[0], (int) xy[1], (int) xy[2], (int) xy[3], - start, end, &ink, width, op + start, end, &ink, width, self->blend ); free(xy); diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 6114b6018..2e5c0d572 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1512,8 +1512,16 @@ setup_module(PyObject* m) { d = PyModule_GetDict(m); +#if LCMS_VERSION < 2070 + vn = LCMS_VERSION; +#else vn = cmsGetEncodedCMMversion(); - v = PyUnicode_FromFormat("%d.%d", vn / 100, vn % 100); +#endif + if (vn % 10) { + v = PyUnicode_FromFormat("%d.%d.%d", vn / 1000, (vn / 10) % 100, vn % 10); + } else { + v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); + } PyDict_SetItemString(d, "littlecms_version", v); return 0; diff --git a/src/_imagingft.c b/src/_imagingft.c index 4883f2bf8..62db561ea 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -25,9 +25,13 @@ #include #include FT_FREETYPE_H #include FT_GLYPH_H +#include FT_BITMAP_H #include FT_STROKER_H #include FT_MULTIPLE_MASTERS_H #include FT_SFNT_NAMES_H +#ifdef FT_COLOR_H +#include FT_COLOR_H +#endif #define KEEP_PY_UNICODE @@ -131,8 +135,8 @@ typedef struct { static p_raqm_func p_raqm; -/* round a 26.6 pixel coordinate to the nearest larger integer */ -#define PIXEL(x) ((((x)+63) & -64)>>6) +/* round a 26.6 pixel coordinate to the nearest integer */ +#define PIXEL(x) ((((x)+32) & -64)>>6) static PyObject* geterror(int code) @@ -350,7 +354,7 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out) static size_t text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *features, - const char* lang, GlyphInfo **glyph_info, int mask) + const char* lang, GlyphInfo **glyph_info, int mask, int color) { size_t i = 0, count = 0, start = 0; raqm_t *rq; @@ -364,27 +368,6 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * goto failed; } -#if (defined(PYPY_VERSION_NUM) && (PYPY_VERSION_NUM < 0x07020000)) - if (PyUnicode_Check(string)) { - Py_UNICODE *text = PyUnicode_AS_UNICODE(string); - Py_ssize_t size = PyUnicode_GET_SIZE(string); - if (! size) { - /* return 0 and clean up, no glyphs==no size, - and raqm fails with empty strings */ - goto failed; - } - if (!(*p_raqm.set_text)(rq, (const uint32_t *)(text), size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); - goto failed; - } - if (lang) { - if (!(*p_raqm.set_language)(rq, lang, start, size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); - goto failed; - } - } - } -#else if (PyUnicode_Check(string)) { Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); Py_ssize_t size = PyUnicode_GET_LENGTH(string); @@ -406,7 +389,6 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject * } } } -#endif else { PyErr_SetString(PyExc_TypeError, "expected string"); goto failed; @@ -529,7 +511,7 @@ failed: static size_t text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObject *features, - const char* lang, GlyphInfo **glyph_info, int mask) + const char* lang, GlyphInfo **glyph_info, int mask, int color) { int error, load_flags; FT_ULong ch; @@ -561,10 +543,15 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje return 0; } - load_flags = FT_LOAD_NO_BITMAP; + load_flags = FT_LOAD_DEFAULT; if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } +#ifdef FT_LOAD_COLOR + if (color) { + load_flags |= FT_LOAD_COLOR; + } +#endif for (i = 0; font_getchar(string, i, &ch); i++) { (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); @@ -585,7 +572,8 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje } (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; - (*glyph_info)[i].y_advance = glyph->metrics.vertAdvance; + // y_advance is only used in ttb, which is not supported by basic layout + (*glyph_info)[i].y_advance = 0; last_index = (*glyph_info)[i].index; (*glyph_info)[i].cluster = ch; } @@ -594,131 +582,181 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje static size_t text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *features, - const char* lang, GlyphInfo **glyph_info, int mask) + const char* lang, GlyphInfo **glyph_info, int mask, int color) { size_t count; if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) { - count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask); + count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask, color); } else { - count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask); + count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask, color); } return count; } static PyObject* -font_getsize(FontObject* self, PyObject* args) +font_getlength(FontObject* self, PyObject* args) { - int x_position, x_max, x_min, y_max, y_min; - FT_Face face; - int xoffset, yoffset; - int horizontal_dir; - int mask = 0; - int load_flags; + int length; /* length along primary axis, in 26.6 precision */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; const char *dir = NULL; const char *lang = NULL; - size_t i, count; - GlyphInfo *glyph_info = NULL; PyObject *features = Py_None; + PyObject *string; /* calculate size and bearing for a given string */ - PyObject* string; - if (!PyArg_ParseTuple(args, "O|izOz:getsize", &string, &mask, &dir, &features, &lang)) { + if (!PyArg_ParseTuple(args, "O|zzOz:getlength", &string, &mode, &dir, &features, &lang)) { return NULL; } - count = text_layout(string, self, dir, features, lang, &glyph_info, mask); + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { return NULL; } - face = NULL; - xoffset = yoffset = 0; - x_position = x_max = x_min = y_max = y_min = 0; + length = 0; + for (i = 0; i < count; i++) { + if (horizontal_dir) { + length += glyph_info[i].x_advance; + } else { + length -= glyph_info[i].y_advance; + } + } + + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + + return PyLong_FromLong(length); +} + +static PyObject* +font_getsize(FontObject* self, PyObject* args) +{ + int position; /* pen position along primary axis, in 26.6 precision */ + int advanced; /* pen position along primary axis, in pixels */ + int px, py; /* position of current glyph, in pixels */ + int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ + int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ + int load_flags; /* FreeType load_flags parameter */ + int error; + FT_Face face; + FT_Glyph glyph; + FT_BBox bbox; /* glyph bounding box */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + const char *anchor = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple(args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + return NULL; + } horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + if (anchor == NULL) { + anchor = horizontal_dir ? "la" : "lt"; + } + if (strlen(anchor) != 2) { + goto bad_anchor; + } + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } +#ifdef FT_LOAD_COLOR + if (color) { + load_flags |= FT_LOAD_COLOR; + } +#endif + + /* + * text bounds are given by: + * - bounding boxes of individual glyphs + * - pen line, i.e. 0 to `advanced` along primary axis + * this means point (0, 0) is part of the text bounding box + */ + face = NULL; + position = x_min = x_max = y_min = y_max = 0; for (i = 0; i < count; i++) { - int index, error, offset, x_advanced; - FT_BBox bbox; - FT_Glyph glyph; face = self->face; - index = glyph_info[i].index; - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 - * Yifu Yu, 2014-10-15 - */ - load_flags = FT_LOAD_NO_BITMAP; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; + + if (horizontal_dir) { + px = PIXEL(position + glyph_info[i].x_offset); + py = PIXEL(glyph_info[i].y_offset); + + position += glyph_info[i].x_advance; + advanced = PIXEL(position); + if (advanced > x_max) { + x_max = advanced; + } + } else { + px = PIXEL(glyph_info[i].x_offset); + py = PIXEL(position + glyph_info[i].y_offset); + + position += glyph_info[i].y_advance; + advanced = PIXEL(position); + if (advanced < y_min) { + y_min = advanced; + } } - error = FT_Load_Glyph(face, index, load_flags); + + error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { return geterror(error); } - if (i == 0) { - if (horizontal_dir) { - if (face->glyph->metrics.horiBearingX < 0) { - xoffset = face->glyph->metrics.horiBearingX; - x_position -= xoffset; - } - } else { - if (face->glyph->metrics.vertBearingY < 0) { - yoffset = face->glyph->metrics.vertBearingY; - y_max -= yoffset; - } - } + error = FT_Get_Glyph(face->glyph, &glyph); + if (error) { + return geterror(error); } - FT_Get_Glyph(face->glyph, &glyph); - FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); - if (horizontal_dir) { - x_position += glyph_info[i].x_advance; - - x_advanced = x_position; - offset = glyph_info[i].x_advance - - face->glyph->metrics.width - - face->glyph->metrics.horiBearingX; - if (offset < 0) { - x_advanced -= offset; - } - if (x_advanced > x_max) { - x_max = x_advanced; - } - - bbox.yMax += glyph_info[i].y_offset; - bbox.yMin += glyph_info[i].y_offset; - if (bbox.yMax > y_max) { - y_max = bbox.yMax; - } - if (bbox.yMin < y_min) { - y_min = bbox.yMin; - } - - // find max distance of baseline from top - if (face->glyph->metrics.horiBearingY > yoffset) { - yoffset = face->glyph->metrics.horiBearingY; - } - } else { - y_max -= glyph_info[i].y_advance; - - if (i == count - 1) { - // trim end gap from final glyph - int offset; - offset = -glyph_info[i].y_advance - - face->glyph->metrics.height - - face->glyph->metrics.vertBearingY; - if (offset < 0) { - y_max -= offset; - } - } - - if (bbox.xMax > x_max) { - x_max = bbox.xMax; - } - if (i == 0 || bbox.xMin < x_min) { - x_min = bbox.xMin; - } + FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); + bbox.xMax += px; + if (bbox.xMax > x_max) { + x_max = bbox.xMax; + } + bbox.xMin += px; + if (bbox.xMin < x_min) { + x_min = bbox.xMin; + } + bbox.yMax += py; + if (bbox.yMax > y_max) { + y_max = bbox.yMax; + } + bbox.yMin += py; + if (bbox.yMin < y_min) { + y_min = bbox.yMin; } FT_Done_Glyph(glyph); @@ -729,73 +767,156 @@ font_getsize(FontObject* self, PyObject* args) glyph_info = NULL; } + x_anchor = y_anchor = 0; if (face) { if (horizontal_dir) { - // left bearing - if (xoffset < 0) { - x_max -= xoffset; - } else { - xoffset = 0; + switch (anchor[0]) { + case 'l': // left + x_anchor = 0; + break; + case 'm': // middle (left + right) / 2 + x_anchor = PIXEL(position / 2); + break; + case 'r': // right + x_anchor = PIXEL(position); + break; + case 's': // vertical baseline + default: + goto bad_anchor; + } + switch (anchor[1]) { + case 'a': // ascender + y_anchor = PIXEL(self->face->size->metrics.ascender); + break; + case 't': // top + y_anchor = y_max; + break; + case 'm': // middle (ascender + descender) / 2 + y_anchor = PIXEL((self->face->size->metrics.ascender + self->face->size->metrics.descender) / 2); + break; + case 's': // horizontal baseline + y_anchor = 0; + break; + case 'b': // bottom + y_anchor = y_min; + break; + case 'd': // descender + y_anchor = PIXEL(self->face->size->metrics.descender); + break; + default: + goto bad_anchor; } - - /* difference between the font ascender and the distance of - * the baseline from the top */ - yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); } else { - // top bearing - if (yoffset < 0) { - y_max -= yoffset; - } else { - yoffset = 0; + switch (anchor[0]) { + case 'l': // left + x_anchor = x_min; + break; + case 'm': // middle (left + right) / 2 + x_anchor = (x_min + x_max) / 2; + break; + case 'r': // right + x_anchor = x_max; + break; + case 's': // vertical baseline + x_anchor = 0; + break; + default: + goto bad_anchor; + } + switch (anchor[1]) { + case 't': // top + y_anchor = 0; + break; + case 'm': // middle (top + bottom) / 2 + y_anchor = PIXEL(position / 2); + break; + case 'b': // bottom + y_anchor = PIXEL(position); + break; + case 'a': // ascender + case 's': // horizontal baseline + case 'd': // descender + default: + goto bad_anchor; } } } return Py_BuildValue( "(ii)(ii)", - PIXEL(x_max - x_min), PIXEL(y_max - y_min), - PIXEL(xoffset), yoffset - ); + (x_max - x_min), (y_max - y_min), + (-x_anchor + x_min), -(-y_anchor + y_max) + ); + +bad_anchor: + PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); + return NULL; } static PyObject* font_render(FontObject* self, PyObject* args) { - int x; - unsigned int y; - Imaging im; - int index, error, ascender, horizontal_dir; - int load_flags; - unsigned char *source; + int x, y; /* pen position, in 26.6 precision */ + int px, py; /* position of current glyph, in pixels */ + int x_min, y_max; /* text offset in 26.6 precision */ + int load_flags; /* FreeType load_flags parameter */ + int error; FT_Glyph glyph; FT_GlyphSlot glyph_slot; FT_Bitmap bitmap; + FT_Bitmap bitmap_converted; /* initialized lazily, for non-8bpp fonts */ FT_BitmapGlyph bitmap_glyph; - int stroke_width = 0; FT_Stroker stroker = NULL; - FT_Int left; - /* render string into given buffer (the buffer *must* have - the right size, or this will crash) */ - PyObject* string; + int bitmap_converted_ready = 0; /* has bitmap_converted been initialized */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int xx, yy; /* pixel offset of current glyph bitmap */ + int x0, x1; /* horizontal bounds of glyph bitmap to copy */ + unsigned int bitmap_y; /* glyph bitmap y index */ + unsigned char *source; /* glyph bitmap source buffer */ + unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ + Imaging im; Py_ssize_t id; - int mask = 0; - int temp; - int xx, x0, x1; - int yy; - unsigned int bitmap_y; + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + int stroke_width = 0; + PY_LONG_LONG foreground_ink_long = 0; + unsigned int foreground_ink; + const char *mode = NULL; const char *dir = NULL; const char *lang = NULL; - size_t i, count; - GlyphInfo *glyph_info; - PyObject *features = NULL; + PyObject *features = Py_None; + PyObject* string; - if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang, - &stroke_width)) { + /* render string into given buffer (the buffer *must* have + the right size, or this will crash) */ + + if (!PyArg_ParseTuple(args, "On|zzOziL:render", &string, &id, &mode, &dir, &features, &lang, + &stroke_width, &foreground_ink_long)) { return NULL; } - glyph_info = NULL; - count = text_layout(string, self, dir, features, lang, &glyph_info, mask); + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + foreground_ink = foreground_ink_long; + +#ifdef FT_COLOR_H + if (color) { + FT_Color foreground_color; + FT_Byte* ink = (FT_Byte*)&foreground_ink; + foreground_color.red = ink[0]; + foreground_color.green = ink[1]; + foreground_color.blue = ink[2]; + foreground_color.alpha = (FT_Byte) 255; /* ink alpha is handled in ImageDraw.text */ + FT_Palette_Set_Foreground_Color(self->face, foreground_color); + } +#endif + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { return NULL; } @@ -813,16 +934,26 @@ font_render(FontObject* self, PyObject* args) } im = (Imaging) id; - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ - load_flags = FT_LOAD_NO_BITMAP; + load_flags = FT_LOAD_DEFAULT; if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } +#ifdef FT_LOAD_COLOR + if (color) { + load_flags |= FT_LOAD_COLOR; + } +#endif - ascender = 0; + /* + * calculate x_min and y_max + * must match font_getsize or there may be clipping! + */ + x = y = x_min = y_max = 0; for (i = 0; i < count; i++) { - index = glyph_info[i].index; - error = FT_Load_Glyph(self->face, index, load_flags | FT_LOAD_RENDER); + px = PIXEL(x + glyph_info[i].x_offset); + py = PIXEL(y + glyph_info[i].y_offset); + + error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); if (error) { return geterror(error); } @@ -830,22 +961,30 @@ font_render(FontObject* self, PyObject* args) glyph_slot = self->face->glyph; bitmap = glyph_slot->bitmap; - temp = bitmap.rows - glyph_slot->bitmap_top; - temp -= PIXEL(glyph_info[i].y_offset); - if (temp > ascender) { - ascender = temp; + if (glyph_slot->bitmap_top + py > y_max) { + y_max = glyph_slot->bitmap_top + py; } + if (glyph_slot->bitmap_left + px < x_min) { + x_min = glyph_slot->bitmap_left + px; + } + + x += glyph_info[i].x_advance; + y += glyph_info[i].y_advance; } + /* set pen position to text origin */ + x = (-x_min + stroke_width) << 6; + y = (-y_max + (-stroke_width)) << 6; + if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; } - x = y = 0; - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; for (i = 0; i < count; i++) { - index = glyph_info[i].index; - error = FT_Load_Glyph(self->face, index, load_flags); + px = PIXEL(x + glyph_info[i].x_offset); + py = PIXEL(y + glyph_info[i].y_offset); + + error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); if (error) { return geterror(error); } @@ -867,25 +1006,64 @@ font_render(FontObject* self, PyObject* args) bitmap_glyph = (FT_BitmapGlyph)glyph; bitmap = bitmap_glyph->bitmap; - left = bitmap_glyph->left; + xx = px + bitmap_glyph->left; + yy = -(py + bitmap_glyph->top); } else { bitmap = glyph_slot->bitmap; - left = glyph_slot->bitmap_left; + xx = px + glyph_slot->bitmap_left; + yy = -(py + glyph_slot->bitmap_top); } - if (horizontal_dir) { - if (i == 0 && glyph_slot->metrics.horiBearingX < 0) { - x = -glyph_slot->metrics.horiBearingX; - } - xx = PIXEL(x) + left; - xx += PIXEL(glyph_info[i].x_offset) + stroke_width; - } else { - if (glyph_slot->metrics.vertBearingX < 0) { - x = -glyph_slot->metrics.vertBearingX; - } - xx = im->xsize / 2 - bitmap.width / 2; + /* convert non-8bpp bitmaps */ + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + convert_scale = 255; + break; + case FT_PIXEL_MODE_GRAY2: + convert_scale = 255 / 3; + break; + case FT_PIXEL_MODE_GRAY4: + convert_scale = 255 / 15; + break; + default: + convert_scale = 1; + } + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + case FT_PIXEL_MODE_GRAY2: + case FT_PIXEL_MODE_GRAY4: + if (!bitmap_converted_ready) { + +#if FREETYPE_MAJOR > 2 ||\ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 6) + FT_Bitmap_Init(&bitmap_converted); +#else + FT_Bitmap_New(&bitmap_converted); +#endif + bitmap_converted_ready = 1; + } + error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1); + if (error) { + geterror(error); + goto glyph_error; + } + bitmap = bitmap_converted; + /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ + case FT_PIXEL_MODE_GRAY: + break; +#ifdef FT_LOAD_COLOR + case FT_PIXEL_MODE_BGRA: + if (color) { + break; + } + /* we didn't ask for color, fall through to default */ +#endif + default: + PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + goto glyph_error; } + /* clip glyph bitmap width to target image bounds */ x0 = 0; x1 = bitmap.width; if (xx < 0) { @@ -896,51 +1074,85 @@ font_render(FontObject* self, PyObject* args) } source = (unsigned char*) bitmap.buffer; - for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++) { - if (horizontal_dir) { - yy = bitmap_y + im->ysize - (PIXEL(glyph_slot->metrics.horiBearingY) + ascender); - yy -= PIXEL(glyph_info[i].y_offset) + stroke_width * 2; - } else { - yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + ascender; - yy += PIXEL(glyph_info[i].y_offset); - } + for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) { + /* clip glyph bitmap height to target image bounds */ if (yy >= 0 && yy < im->ysize) { - // blend this glyph into the buffer - unsigned char *target = im->image8[yy] + xx; - if (mask) { - // use monochrome mask (on palette images, etc) - int j, k, m = 128; - for (j = k = 0; j < x1; j++) { - if (j >= x0 && (source[k] & m)) { - target[j] = 255; + /* blend this glyph into the buffer */ + int k; + unsigned char v; + unsigned char* target; + if (color) { + /* target[RGB] returns the color, target[A] returns the mask */ + /* target bands get split again in ImageDraw.text */ + target = im->image[yy] + xx * 4; + } else { + target = im->image8[yy] + xx; + } +#ifdef FT_LOAD_COLOR + if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { + /* paste color glyph */ + for (k = x0; k < x1; k++) { + if (target[k * 4 + 3] < source[k * 4 + 3]) { + /* unpremultiply BGRa to RGBA */ + target[k * 4 + 0] = CLIP8((255 * (int)source[k * 4 + 2]) / source[k * 4 + 3]); + target[k * 4 + 1] = CLIP8((255 * (int)source[k * 4 + 1]) / source[k * 4 + 3]); + target[k * 4 + 2] = CLIP8((255 * (int)source[k * 4 + 0]) / source[k * 4 + 3]); + target[k * 4 + 3] = source[k * 4 + 3]; } - if (!(m >>= 1)) { - m = 128; - k++; + } + } else +#endif + if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + if (color) { + unsigned char* ink = (unsigned char*)&foreground_ink; + for (k = x0; k < x1; k++) { + v = source[k] * convert_scale; + if (target[k * 4 + 3] < v) { + target[k * 4 + 0] = ink[0]; + target[k * 4 + 1] = ink[1]; + target[k * 4 + 2] = ink[2]; + target[k * 4 + 3] = v; + } + } + } else { + for (k = x0; k < x1; k++) { + v = source[k] * convert_scale; + if (target[k] < v) { + target[k] = v; + } } } } else { - // use antialiased rendering - int k; - for (k = x0; k < x1; k++) { - if (target[k] < source[k]) { - target[k] = source[k]; - } - } + PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + goto glyph_error; } } source += bitmap.pitch; } x += glyph_info[i].x_advance; - y -= glyph_info[i].y_advance; + y += glyph_info[i].y_advance; if (stroker != NULL) { FT_Done_Glyph(glyph); } } + if (bitmap_converted_ready) { + FT_Bitmap_Done(library, &bitmap_converted); + } FT_Stroker_Done(stroker); PyMem_Del(glyph_info); Py_RETURN_NONE; + +glyph_error: + if (stroker != NULL) { + FT_Done_Glyph(glyph); + } + if (bitmap_converted_ready) { + FT_Bitmap_Done(library, &bitmap_converted); + } + FT_Stroker_Done(stroker); + PyMem_Del(glyph_info); + return NULL; } #if FREETYPE_MAJOR > 2 ||\ @@ -1122,6 +1334,7 @@ font_dealloc(FontObject* self) static PyMethodDef font_methods[] = { {"render", (PyCFunction) font_render, METH_VARARGS}, {"getsize", (PyCFunction) font_getsize, METH_VARARGS}, + {"getlength", (PyCFunction) font_getlength, METH_VARARGS}, #if FREETYPE_MAJOR > 2 ||\ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) ||\ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 08f6c3843..a2b2b10f3 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -35,6 +35,7 @@ #include "Imaging.h" #include +#include #define CEIL(v) (int) ceil(v) #define FLOOR(v) ((v) >= 0.0 ? (int) (v) : (int) floor(v)) @@ -818,222 +819,751 @@ ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void* ink, /* -------------------------------------------------------------------- */ /* standard shapes */ -#define ARC 0 -#define CHORD 1 -#define PIESLICE 2 +// Imagine 2D plane and ellipse with center in (0, 0) and semi-major axes a and b. +// Then quarter_* stuff approximates its top right quarter (x, y >= 0) with integer +// points from set {(2x+x0, 2y+y0) | x,y in Z} where x0, y0 are from {0, 1} and +// are such that point (a, b) is in the set. -static void -ellipsePoint(int cx, int cy, int w, int h, - float i, int *x, int *y) -{ - float i_cos, i_sin; - float x_f, y_f; - double modf_int; - i_cos = cos(i*M_PI/180); - i_sin = sin(i*M_PI/180); - x_f = (i_cos * w/2) + cx; - y_f = (i_sin * h/2) + cy; - if (modf(x_f, &modf_int) == 0.5) { - *x = i_cos > 0 ? FLOOR(x_f) : CEIL(x_f); +typedef struct { + int32_t a, b, cx, cy, ex, ey; + int64_t a2, b2, a2b2; + int8_t finished; +} quarter_state; + +void quarter_init(quarter_state* s, int32_t a, int32_t b) { + if (a < 0 || b < 0) { + s->finished = 1; } else { - *x = FLOOR(x_f + 0.5); - } - if (modf(y_f, &modf_int) == 0.5) { - *y = i_sin > 0 ? FLOOR(y_f) : CEIL(y_f); - } else { - *y = FLOOR(y_f + 0.5); + s->a = a; + s->b = b; + s->cx = a; + s->cy = b % 2; + s->ex = a % 2; + s->ey = b; + s->a2 = a * a; + s->b2 = b * b; + s->a2b2 = s->a2 * s->b2; + s->finished = 0; } } -static int -ellipse(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink_, int fill, - int width, int mode, int op) -{ - float i; - int inner; - int n; - int maxEdgeCount; - int w, h; - int x, y; - int cx, cy; - int lx = 0, ly = 0; - int sx = 0, sy = 0; - int lx_inner = 0, ly_inner = 0; - int sx_inner = 0, sy_inner = 0; - DRAW* draw; - INT32 ink; - Edge* e; +// deviation of the point from ellipse curve, basically a substitution +// of the point into the ellipse equation +int64_t quarter_delta(quarter_state* s, int64_t x, int64_t y) { + return llabs(s->a2 * y * y + s->b2 * x * x - s->a2b2); +} - DRAWINIT(); - - while (end < start) { - end += 360; +int8_t quarter_next(quarter_state* s, int32_t* ret_x, int32_t* ret_y) { + if (s->finished) { + return -1; } - - if (end - start > 360) { - // no need to go in loops - end = start + 361; - } - - w = x1 - x0; - h = y1 - y0; - if (w <= 0 || h <= 0) { - return 0; - } - - cx = (x0 + x1) / 2; - cy = (y0 + y1) / 2; - - if (!fill && width <= 1) { - for (i = start; i < end+1; i++) { - if (i > end) { - i = end; - } - ellipsePoint(cx, cy, w, h, i, &x, &y); - if (i != start) { - draw->line(im, lx, ly, x, y, ink); - } else { - sx = x, sy = y; - } - lx = x, ly = y; - } - - if (i != start) { - if (mode == PIESLICE) { - if (x != cx || y != cy) { - draw->line(im, x, y, cx, cy, ink); - draw->line(im, cx, cy, sx, sy, ink); - } - } else if (mode == CHORD) { - if (x != sx || y != sy) { - draw->line(im, x, y, sx, sy, ink); - } - } - } + *ret_x = s->cx; + *ret_y = s->cy; + if (s->cx == s->ex && s->cy == s->ey) { + s->finished = 1; } else { - inner = (mode == ARC || !fill) ? 1 : 0; - - // Build edge list - // malloc check UNDONE, FLOAT? - maxEdgeCount = ceil(end - start); - if (inner) { - maxEdgeCount *= 2; - } - maxEdgeCount += 3; - e = calloc(maxEdgeCount, sizeof(Edge)); - if (!e) { - ImagingError_MemoryError(); - return -1; - } - - // Outer circle - n = 0; - for (i = start; i < end+1; i++) { - if (i > end) { - i = end; + // Bresenham's algorithm, possible optimization: only consider 2 of 3 + // next points depending on current slope + int32_t nx = s->cx; + int32_t ny = s->cy + 2; + int64_t ndelta = quarter_delta(s, nx, ny); + if (nx > 1) { + int64_t newdelta = quarter_delta(s, s->cx - 2, s->cy + 2); + if (ndelta > newdelta) { + nx = s->cx - 2; + ny = s->cy + 2; + ndelta = newdelta; } - ellipsePoint(cx, cy, w, h, i, &x, &y); - if (i == start) { - sx = x, sy = y; - } else { - add_edge(&e[n++], lx, ly, x, y); - } - lx = x, ly = y; - } - if (n == 0) { - return 0; - } - - if (inner) { - // Inner circle - x0 += width - 1; - y0 += width - 1; - x1 -= width - 1; - y1 -= width - 1; - - w = x1 - x0; - h = y1 - y0; - if (w <= 0 || h <= 0) { - // ARC with no gap in the middle is a PIESLICE - mode = PIESLICE; - inner = 0; - } else { - for (i = start; i < end+1; i++) { - if (i > end) { - i = end; - } - ellipsePoint(cx, cy, w, h, i, &x, &y); - if (i == start) { - sx_inner = x, sy_inner = y; - } else { - add_edge(&e[n++], lx_inner, ly_inner, x, y); - } - lx_inner = x, ly_inner = y; - } + newdelta = quarter_delta(s, s->cx - 2, s->cy); + if (ndelta > newdelta) { + nx = s->cx - 2; + ny = s->cy; } } - - if (end - start < 360) { - // Close polygon - if (mode == PIESLICE) { - if (x != cx || y != cy) { - add_edge(&e[n++], sx, sy, cx, cy); - add_edge(&e[n++], cx, cy, lx, ly); - if (inner) { - ImagingDrawWideLine(im, sx, sy, cx, cy, &ink, width, op); - ImagingDrawWideLine(im, cx, cy, lx, ly, &ink, width, op); - } - } - } else if (mode == CHORD) { - add_edge(&e[n++], sx, sy, lx, ly); - if (inner) { - add_edge(&e[n++], sx_inner, sy_inner, lx_inner, ly_inner); - } - } else if (mode == ARC) { - add_edge(&e[n++], sx, sy, sx_inner, sy_inner); - add_edge(&e[n++], lx, ly, lx_inner, ly_inner); - } - } - - draw->polygon(im, n, e, ink, 0); - - free(e); + s->cx = nx; + s->cy = ny; } - return 0; } -int -ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink, int width, int op) -{ - return ellipse(im, x0, y0, x1, y1, start, end, ink, 0, width, ARC, op); +// quarter_* stuff can "draw" a quarter of an ellipse with thickness 1, great. +// Now we use ellipse_* stuff to join all four quarters of two different sized +// ellipses and receive horizontal segments of a complete ellipse with +// specified thickness. +// +// Still using integer grid with step 2 at this point (like in quarter_*) +// to ease angle clipping in future. + +typedef struct { + quarter_state st_o, st_i; + int32_t py, pl, pr; + int32_t cy[4], cl[4], cr[4]; + int8_t bufcnt; + int8_t finished; + int8_t leftmost; +} ellipse_state; + +void ellipse_init(ellipse_state* s, int32_t a, int32_t b, int32_t w) { + s->bufcnt = 0; + s->leftmost = a % 2; + quarter_init(&s->st_o, a, b); + if (w < 1 || quarter_next(&s->st_o, &s->pr, &s->py) == -1) { + s->finished = 1; + } else { + s->finished = 0; + quarter_init(&s->st_i, a - 2 * (w - 1), b - 2 * (w - 1)); + s->pl = s->leftmost; + } } -int -ImagingDrawChord(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink, int fill, - int width, int op) +int8_t ellipse_next(ellipse_state* s, int32_t* ret_x0, int32_t* ret_y, int32_t* ret_x1) { + if (s->bufcnt == 0) { + if (s->finished) { + return -1; + } + int32_t y = s->py; + int32_t l = s->pl; + int32_t r = s->pr; + int32_t cx = 0, cy = 0; + int8_t next_ret; + while ((next_ret = quarter_next(&s->st_o, &cx, &cy)) != -1 && cy <= y) { + } + if (next_ret == -1) { + s->finished = 1; + } else { + s->pr = cx; + s->py = cy; + } + while ((next_ret = quarter_next(&s->st_i, &cx, &cy)) != -1 && cy <= y) { + l = cx; + } + s->pl = next_ret == -1 ? s->leftmost : cx; + + if ((l > 0 || l < r) && y > 0) { + s->cl[s->bufcnt] = l == 0 ? 2 : l; + s->cy[s->bufcnt] = y; + s->cr[s->bufcnt] = r; + ++s->bufcnt; + } + if (y > 0) { + s->cl[s->bufcnt] = -r; + s->cy[s->bufcnt] = y; + s->cr[s->bufcnt] = -l; + ++s->bufcnt; + } + if (l > 0 || l < r) { + s->cl[s->bufcnt] = l == 0 ? 2 : l; + s->cy[s->bufcnt] = -y; + s->cr[s->bufcnt] = r; + ++s->bufcnt; + } + s->cl[s->bufcnt] = -r; + s->cy[s->bufcnt] = -y; + s->cr[s->bufcnt] = -l; + ++s->bufcnt; + } + --s->bufcnt; + *ret_x0 = s->cl[s->bufcnt]; + *ret_y = s->cy[s->bufcnt]; + *ret_x1 = s->cr[s->bufcnt]; + return 0; +} + +// Clipping tree consists of half-plane clipping nodes and combining nodes. +// We can throw a horizontal segment in such a tree and collect an ordered set +// of resulting disjoint clipped segments organized into a sorted linked list +// of their end points. +typedef enum { + CT_AND, // intersection + CT_OR, // union + CT_CLIP // half-plane clipping +} clip_type; + +typedef struct clip_node { + clip_type type; + double a, b, c; // half-plane coeffs, only used in clipping nodes + struct clip_node* l; // child pointers, are only non-NULL in combining nodes + struct clip_node* r; +} clip_node; + +// Linked list for the ends of the clipped horizontal segments. +// Since the segment is always horizontal, we don't need to store Y coordinate. +typedef struct event_list { + int32_t x; + int8_t type; // used internally, 1 for the left end (smaller X), -1 for the + // right end; pointless in output since the output segments + // are disjoint, therefore the types would always come in pairs + // and interchange (1 -1 1 -1 ...) + struct event_list* next; +} event_list; + +// Mirrors all the clipping nodes of the tree relative to the y = x line. +void clip_tree_transpose(clip_node* root) { + if (root != NULL) { + if (root->type == CT_CLIP) { + double t = root->a; + root->a = root->b; + root->b = t; + } + clip_tree_transpose(root->l); + clip_tree_transpose(root->r); + } +} + +// Outputs a sequence of open-close events (types -1 and 1) for +// non-intersecting segments sorted by X coordinate. +// Combining nodes (AND, OR) may also accept sequences for intersecting +// segments, i.e. something like correct bracket sequences. +int clip_tree_do_clip(clip_node* root, int32_t x0, int32_t y, int32_t x1, event_list** ret) { + if (root == NULL) { + event_list* start = malloc(sizeof(event_list)); + if (!start) { + ImagingError_MemoryError(); + return -1; + } + event_list* end = malloc(sizeof(event_list)); + if (!end) { + free(start); + ImagingError_MemoryError(); + return -1; + } + start->x = x0; + start->type = 1; + start->next = end; + end->x = x1; + end->type = -1; + end->next = NULL; + *ret = start; + return 0; + } + if (root->type == CT_CLIP) { + double eps = 1e-9; + double A = root->a; + double B = root->b; + double C = root->c; + if (fabs(A) < eps) { + if (B * y + C < -eps) { + x0 = 1; + x1 = 0; + } + } else { + // X of intersection + double ix = - (B * y + C) / A; + if (A * x0 + B * y + C < eps) { + x0 = lround(fmax(x0, ix)); + } + if (A * x1 + B * y + C < eps) { + x1 = lround(fmin(x1, ix)); + } + } + if (x0 <= x1) { + event_list* start = malloc(sizeof(event_list)); + if (!start) { + ImagingError_MemoryError(); + return -1; + } + event_list* end = malloc(sizeof(event_list)); + if (!end) { + free(start); + ImagingError_MemoryError(); + return -1; + } + start->x = x0; + start->type = 1; + start->next = end; + end->x = x1; + end->type = -1; + end->next = NULL; + *ret = start; + } else { + *ret = NULL; + } + return 0; + } + if (root->type == CT_OR || root->type == CT_AND) { + event_list* l1; + event_list* l2; + if (clip_tree_do_clip(root->l, x0, y, x1, &l1) < 0) { + return -1; + } + if (clip_tree_do_clip(root->r, x0, y, x1, &l2) < 0) { + while (l1) { + l2 = l1->next; + free(l1); + l1 = l2; + } + return -1; + } + *ret = NULL; + event_list* tail = NULL; + int32_t k1 = 0; + int32_t k2 = 0; + while (l1 != NULL || l2 != NULL) { + event_list* t; + if (l2 == NULL || (l1 != NULL && (l1->x < l2->x || (l1->x == l2->x && l1->type > l2->type)))) { + t = l1; + k1 += t->type; + assert(k1 >= 0); + l1 = l1->next; + } else { + t = l2; + k2 += t->type; + assert(k2 >= 0); + l2 = l2->next; + } + t->next = NULL; + if ((root->type == CT_OR && ( + (t->type == 1 && (tail == NULL || tail->type == -1)) || + (t->type == -1 && k1 == 0 && k2 == 0) + )) || + (root->type == CT_AND && ( + (t->type == 1 && (tail == NULL || tail->type == -1) && k1 > 0 && k2 > 0) || + (t->type == -1 && tail != NULL && tail->type == 1 && (k1 == 0 || k2 == 0)) + ))) { + if (tail == NULL) { + *ret = t; + } else { + tail->next = t; + } + tail = t; + } else { + free(t); + } + } + return 0; + } + *ret = NULL; + return 0; +} + +// One more layer of processing on top of the regular ellipse. +// Uses the clipping tree. +// Used for producing ellipse derivatives such as arc, chord, pie, etc. +typedef struct { + ellipse_state st; + clip_node* root; + clip_node nodes[7]; + int32_t node_count; + event_list* head; + int32_t y; +} clip_ellipse_state; + +typedef void (*clip_ellipse_init)(clip_ellipse_state*, int32_t, int32_t, int32_t, float, float); + +void debug_clip_tree(clip_node* root, int space) { + if (root == NULL) { + return; + } + if (root->type == CT_CLIP) { + int t = space; + while (t--) { + fputc(' ', stderr); + } + fprintf(stderr, "clip %+fx%+fy%+f > 0\n", root->a, root->b, root->c); + } else { + debug_clip_tree(root->l, space + 2); + int t = space; + while (t--) { + fputc(' ', stderr); + } + fprintf(stderr, "%s\n", root->type == CT_AND ? "and" : "or"); + debug_clip_tree(root->r, space + 2); + } + if (space == 0) { + fputc('\n', stderr); + } +} + +// Resulting angles will satisfy 0 <= al < 360, al <= ar <= al + 360 +void normalize_angles(float* al, float* ar) { + if (*ar - *al >= 360) { + *al = 0; + *ar = 360; + } else { + *al = fmod(*al < 0 ? 360 - (fmod(-*al, 360)) : *al, 360); + *ar = *al + fmod(*ar < *al ? 360 - fmod(*al - *ar, 360) : *ar - *al, 360); + } +} + +// An arc with caps orthogonal to the ellipse curve. +void arc_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { + if (a < b) { + // transpose the coordinate system + arc_init(s, b, a, w, 90 - ar, 90 - al); + ellipse_init(&s->st, a, b, w); + clip_tree_transpose(s->root); + } else { + // a >= b, based on "wide" ellipse + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + normalize_angles(&al, &ar); + + // building clipping tree, a lot of different cases + if (ar == al + 360) { + s->root = NULL; + } else { + clip_node* lc = s->nodes + s->node_count++; + clip_node* rc = s->nodes + s->node_count++; + lc->l = lc->r = rc->l = rc->r = NULL; + lc->type = rc->type = CT_CLIP; + lc->a = -a * sin(al * M_PI / 180.0); + lc->b = b * cos(al * M_PI / 180.0); + lc->c = (a * a - b * b) * sin(al * M_PI / 90.0) / 2.0; + rc->a = a * sin(ar * M_PI / 180.0); + rc->b = -b * cos(ar * M_PI / 180.0); + rc->c = (b * b - a * a) * sin(ar * M_PI / 90.0) / 2.0; + if (fmod(al, 180) == 0 || fmod(ar, 180) == 0) { + s->root = s->nodes + s->node_count++; + s->root->l = lc; + s->root->r = rc; + s->root->type = ar - al < 180 ? CT_AND : CT_OR; + } else if (((int)(al / 180) + (int)(ar / 180)) % 2 == 1) { + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->l->l = s->nodes + s->node_count++; + s->root->l->r = lc; + s->root->r = s->nodes + s->node_count++; + s->root->r->l = s->nodes + s->node_count++; + s->root->r->r = rc; + s->root->type = CT_OR; + s->root->l->type = CT_AND; + s->root->r->type = CT_AND; + s->root->l->l->type = CT_CLIP; + s->root->r->l->type = CT_CLIP; + s->root->l->l->l = s->root->l->l->r = NULL; + s->root->r->l->l = s->root->r->l->r = NULL; + s->root->l->l->a = s->root->l->l->c = 0; + s->root->r->l->a = s->root->r->l->c = 0; + s->root->l->l->b = (int)(al / 180) % 2 == 0 ? 1 : -1; + s->root->r->l->b = (int)(ar / 180) % 2 == 0 ? 1 : -1; + } else { + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->r = s->nodes + s->node_count++; + s->root->type = s->root->l->type = ar - al < 180 ? CT_AND : CT_OR; + s->root->l->l = lc; + s->root->l->r = rc; + s->root->r->type = CT_CLIP; + s->root->r->l = s->root->r->r = NULL; + s->root->r->a = s->root->r->c = 0; + s->root->r->b = ar < 180 || ar > 540 ? 1 : -1; + } + } + } +} + +// A chord line. +void chord_line_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, a + b + 1); + + s->head = NULL; + s->node_count = 0; + + // line equation for chord + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->r = s->nodes + s->node_count++; + s->root->type = CT_AND; + s->root->l->type = s->root->r->type = CT_CLIP; + s->root->l->l = s->root->l->r = s->root->r->l = s->root->r->r = NULL; + s->root->l->a = yr - yl; + s->root->l->b = xl - xr; + s->root->l->c = -(s->root->l->a * xl + s->root->l->b * yl); + s->root->r->a = -s->root->l->a; + s->root->r->b = -s->root->l->b; + s->root->r->c = 2 * w * sqrt(pow(s->root->l->a, 2.0) + pow(s->root->l->b, 2.0)) - s->root->l->c; +} + +// Pie side. +void pie_side_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float _) { + ellipse_init(&s->st, a, b, a + b + 1); + + s->head = NULL; + s->node_count = 0; + + double xl = a * cos(al * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0); + double a1 = -yl; + double b1 = xl; + double c1 = w * sqrt(a1 * a1 + b1 * b1); + + s->root = s->nodes + s->node_count++; + s->root->type = CT_AND; + s->root->l = s->nodes + s->node_count++; + s->root->l->type = CT_AND; + + clip_node* cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = a1; + cnode->b = b1; + cnode->c = c1; + s->root->l->l = cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = -a1; + cnode->b = -b1; + cnode->c = c1; + s->root->l->r = cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = b1; + cnode->b = -a1; + cnode->c = 0; + s->root->r = cnode; +} + +// A chord. +void chord_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + + // line equation for chord + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + s->root = s->nodes + s->node_count++; + s->root->l = s->root->r = NULL; + s->root->type = CT_CLIP; + s->root->a = yr - yl; + s->root->b = xl - xr; + s->root->c = -(s->root->a * xl + s->root->b * yl); +} + +// A pie. Can also be used to draw an arc with ugly sharp caps. +void pie_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + + // line equations for pie sides + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + + clip_node* lc = s->nodes + s->node_count++; + clip_node* rc = s->nodes + s->node_count++; + lc->l = lc->r = rc->l = rc->r = NULL; + lc->type = rc->type = CT_CLIP; + lc->a = -yl; + lc->b = xl; + lc->c = 0; + rc->a = yr; + rc->b = -xr; + rc->c = 0; + + s->root = s->nodes + s->node_count++; + s->root->l = lc; + s->root->r = rc; + s->root->type = ar - al < 180 ? CT_AND : CT_OR; +} + +void clip_ellipse_free(clip_ellipse_state* s) { + while (s->head != NULL) { + event_list* t = s->head; + s->head = s->head->next; + free(t); + } +} + +int8_t clip_ellipse_next(clip_ellipse_state* s, int32_t* ret_x0, int32_t* ret_y, int32_t* ret_x1) { + int32_t x0, y, x1; + while (s->head == NULL && ellipse_next(&s->st, &x0, &y, &x1) >= 0) { + if (clip_tree_do_clip(s->root, x0, y, x1, &s->head) < 0) { + return -2; + } + s->y = y; + } + if (s->head != NULL) { + *ret_y = s->y; + event_list* t = s->head; + s->head = s->head->next; + *ret_x0 = t->x; + free(t); + t = s->head; + assert(t != NULL); + s->head = s->head->next; + *ret_x1 = t->x; + free(t); + return 0; + } + return -1; +} + +static int +ellipseNew(Imaging im, int x0, int y0, int x1, int y1, + const void* ink_, int fill, + int width, int op) { - return ellipse(im, x0, y0, x1, y1, start, end, ink, fill, width, CHORD, op); + DRAW* draw; + INT32 ink; + DRAWINIT(); + + int a = x1 - x0; + int b = y1 - y0; + if (a < 0 || b < 0) { + return 0; + } + if (fill) { + width = a + b; + } + + ellipse_state st; + ellipse_init(&st, a, b, width); + int32_t X0, Y, X1; + while (ellipse_next(&st, &X0, &Y, &X1) != -1) { + draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + } + return 0; +} + +static int +clipEllipseNew(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, + const void* ink_, int width, int op, clip_ellipse_init init) +{ + DRAW* draw; + INT32 ink; + DRAWINIT(); + + int a = x1 - x0; + int b = y1 - y0; + if (a < 0 || b < 0) { + return 0; + } + + clip_ellipse_state st; + init(&st, a, b, width, start, end); + // debug_clip_tree(st.root, 0); + int32_t X0, Y, X1; + int next_code; + while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) { + draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + } + clip_ellipse_free(&st); + return next_code == -1 ? 0 : -1; +} +static int +arcNew(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, + const void* ink_, int width, int op) +{ + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, arc_init); +} + +static int +chordNew(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, + const void* ink_, int width, int op) +{ + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_init); +} + +static int +chordLineNew(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, + const void* ink_, int width, int op) +{ + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_line_init); +} + +static int +pieNew(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, + const void* ink_, int width, int op) +{ + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, pie_init); +} + +static int +pieSideNew(Imaging im, int x0, int y0, int x1, int y1, + float start, + const void* ink_, int width, int op) +{ + return clipEllipseNew(im, x0, y0, x1, y1, start, 0, ink_, width, op, pie_side_init); } int ImagingDrawEllipse(Imaging im, int x0, int y0, int x1, int y1, const void* ink, int fill, int width, int op) { - return ellipse(im, x0, y0, x1, y1, 0, 360, ink, fill, width, CHORD, op); + return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); } +int +ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, const void* ink, int width, int op) +{ + normalize_angles(&start, &end); + if (start + 360 == end) { + return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, 0, width, op); + } + if (start == end) { + return 0; + } + return arcNew(im, x0, y0, x1, y1, start, end, ink, width, op); +} + + +int +ImagingDrawChord(Imaging im, int x0, int y0, int x1, int y1, + float start, float end, const void* ink, int fill, + int width, int op) +{ + normalize_angles(&start, &end); + if (start + 360 == end) { + return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, fill, width, op); + } + if (start == end) { + return 0; + } + if (fill) { + return chordNew(im, x0, y0, x1, y1, start, end, ink, x1 - x0 + y1 - y0 + 1, op); + } else { + if (chordLineNew(im, x0, y0, x1, y1, start, end, ink, width, op)) { + return -1; + } + return chordNew(im, x0, y0, x1, y1, start, end, ink, width, op); + } +} + + int ImagingDrawPieslice(Imaging im, int x0, int y0, int x1, int y1, float start, float end, const void* ink, int fill, int width, int op) { - return ellipse(im, x0, y0, x1, y1, start, end, ink, fill, width, PIESLICE, op); + normalize_angles(&start, &end); + if (start + 360 == end) { + return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); + } + if (start == end) { + return 0; + } + if (fill) { + return pieNew(im, x0, y0, x1, y1, start, end, ink, x1 + y1 - x0 - y0, op); + } else { + if (pieSideNew(im, x0, y0, x1, y1, start, ink, width, op)) { + return -1; + } + if (pieSideNew(im, x0, y0, x1, y1, end, ink, width, op)) { + return -1; + } + int xc = lround((x0 + x1 - width) / 2.0), yc = lround((y0 + y1 - width) / 2.0); + ellipseNew(im, xc, yc, xc + width - 1, yc + width - 1, ink, 1, 0, op); + return pieNew(im, x0, y0, x1, y1, start, end, ink, width, op); + } } + /* -------------------------------------------------------------------- */ /* experimental level 2 ("arrow") graphics stuff. this implements diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index d7a0a5b9f..b08e607a7 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -742,8 +742,8 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) swapped), bail. */ if (tile_info.x0 >= tile_info.x1 || tile_info.y0 >= tile_info.y1 - || tile_info.x0 < image->x0 - || tile_info.y0 < image->y0 + || tile_info.x0 < (OPJ_INT32)image->x0 + || tile_info.y0 < (OPJ_INT32)image->y0 || tile_info.x1 - image->x0 > im->xsize || tile_info.y1 - image->y0 > im->ysize) { state->errcode = IMAGING_CODEC_BROKEN; diff --git a/src/path.c b/src/path.c index 0a6d744d5..8eba81aa4 100644 --- a/src/path.c +++ b/src/path.c @@ -56,7 +56,7 @@ alloc_array(Py_ssize_t count) PyErr_NoMemory(); return NULL; } - if (count > (SIZE_MAX / (2 * sizeof(double))) - 1 ) { + if ((unsigned long long)count > (SIZE_MAX / (2 * sizeof(double))) - 1 ) { PyErr_NoMemory(); return NULL; } diff --git a/tox.ini b/tox.ini index 22ca36daf..81c6e19cf 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ [tox] envlist = lint - py{36,37,38,py3} + py{36,37,38,39,py3} minversion = 1.9 [testenv] diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 6a3aeaf64..352c3f76b 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -169,17 +169,41 @@ deps = { ], "libs": [r"output\release-static\{architecture}\lib\*.lib"], }, + "libpng": { + "url": SF_MIRROR + "/project/libpng/libpng16/1.6.37/lpng1637.zip", + "filename": "lpng1637.zip", + "dir": "lpng1637", + "build": [ + # lint: do not inline + cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")), + cmd_nmake(target="clean"), + cmd_nmake(), + cmd_copy("libpng16_static.lib", "libpng16.lib"), + ], + "headers": [r"png*.h"], + "libs": [r"libpng16.lib"], + }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.2.tar.gz", # noqa: E501 - "filename": "freetype-2.10.2.tar.gz", - "dir": "freetype-2.10.2", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.3.tar.gz", # noqa: E501 + "filename": "freetype-2.10.3.tar.gz", + "dir": "freetype-2.10.3", "patch": { r"builds\windows\vc2010\freetype.vcxproj": { # freetype setting is /MD for .dll and /MT for .lib, we need /MD "MultiThreaded": "MultiThreadedDLL", # noqa: E501 # freetype doesn't specify SDK version, MSBuild may guess incorrectly '': '\n $(WindowsSDKVersion)', # noqa: E501 - } + }, + r"builds\windows\vc2010\freetype.user.props": { + "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ", # noqa: E501 + "": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501 + "": "{lib_dir}", # noqa: E501 + "": "zlib.lib;libpng16.lib", # noqa: E501 + }, + r"src/autofit/afshaper.c": { + # link against harfbuzz.lib once it becomes available + "#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ": '#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ\n#pragma comment(lib, "harfbuzz.lib")', # noqa: E501 + }, }, "build": [ cmd_rmdir("objs"), @@ -488,7 +512,7 @@ if __name__ == "__main__": elif arg == "--no-imagequant": disabled += ["libimagequant"] elif arg == "--no-raqm": - disabled += ["harfbuzz", "fribidi", "libraqm"] + disabled += ["fribidi", "libraqm"] elif arg.startswith("--depends="): depends_dir = arg[10:] elif arg.startswith("--python="): @@ -562,6 +586,9 @@ if __name__ == "__main__": "header": sum([header, msvs["header"], ["@echo on"]], []), } + for k, v in deps.items(): + prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) + print() write_script(".gitignore", ["*"])