diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 051cfc483..d94c7d537 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,7 +60,7 @@ jobs: pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: CFLAGS="-coverage" python3 setup.py build_ext install + run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . - name: Test Pillow run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9bbc426e2..414c7e94e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false matrix: os: [ - "macOS-latest", + "macos-latest", "ubuntu-latest", ] python-version: [ @@ -29,7 +29,7 @@ jobs: # Include new variables for Codecov - os: ubuntu-latest codecov-flag: GHA_Ubuntu - - os: macOS-latest + - os: macos-latest codecov-flag: GHA_macOS runs-on: ${{ matrix.os }} diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml new file mode 100644 index 000000000..c2b8b3bda --- /dev/null +++ b/.github/workflows/tidelift.yml @@ -0,0 +1,26 @@ +name: Tidelift Align +on: + schedule: + - cron: "30 2 * * *" # daily at 02:30 UTC + push: + paths: + - ".github/workflows/tidelift.yml" + pull_request: + paths: + - ".github/workflows/tidelift.yml" + workflow_dispatch: + +jobs: + build: + if: github.repository_owner == 'python-pillow' + name: Run Tidelift to ensure approved open source packages are in use + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Scan + uses: tidelift/alignment-action@main + env: + TIDELIFT_API_KEY: ${{ secrets.TIDELIFT_API_KEY }} + TIDELIFT_ORGANIZATION: team/aclark4life + TIDELIFT_PROJECT: pillow diff --git a/CHANGES.rst b/CHANGES.rst index 3cf83a7a9..e0f347d58 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.0.0 (unreleased) ------------------ +- Added ImageShow support for xdg-open #5897 + [m-shinder, radarhere] + +- Support 16-bit grayscale ImageQt conversion #5856 + [cmbruns, radarhere] + - Convert subsequent GIF frames to RGB or RGBA #5857 [radarhere] diff --git a/MANIFEST.in b/MANIFEST.in index e9aaa8318..26f9401f2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include *.c include *.h include *.in +include *.lock include *.md include *.py include *.rst @@ -9,6 +10,7 @@ include *.txt include *.yaml include LICENSE include Makefile +include Pipfile include tox.ini graft Tests graft src diff --git a/Makefile b/Makefile index 546b91838..0dac63d39 100644 --- a/Makefile +++ b/Makefile @@ -50,16 +50,16 @@ help: .PHONY: inplace inplace: clean - python3 setup.py develop build_ext --inplace + python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" . .PHONY: install install: - python3 setup.py install + python3 -m pip install . python3 selftest.py .PHONY: install-coverage install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 setup.py build_ext install + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" . python3 selftest.py .PHONY: debug @@ -68,7 +68,7 @@ debug: # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python3 setup.py build_ext install > /dev/null + CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null .PHONY: install-req install-req: @@ -83,10 +83,10 @@ install-venv: .PHONY: release-test release-test: $(MAKE) install-req - python3 setup.py develop + python3 -m pip install -e . python3 selftest.py python3 -m pytest Tests - python3 setup.py install + python3 -m pip install . -rm dist/*.egg -rmdir dist python3 -m pytest -qq diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..1e611a63c --- /dev/null +++ b/Pipfile @@ -0,0 +1,22 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +black = "*" +check-manifest = "*" +coverage = "*" +defusedxml = "*" +packaging = "*" +markdown2 = "*" +olefile = "*" +pyroma = "*" +pytest = "*" +pytest-cov = "*" +pytest-timeout = "*" + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..600b19050 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,324 @@ +{ + "_meta": { + "hash": { + "sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "black": { + "hashes": [ + "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3", + "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f" + ], + "index": "pypi", + "version": "==21.12b0" + }, + "build": { + "hashes": [ + "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f", + "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "certifi": { + "hashes": [ + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + ], + "version": "==2021.10.8" + }, + "charset-normalizer": { + "hashes": [ + "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", + "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" + ], + "markers": "python_version >= '3'", + "version": "==2.0.9" + }, + "check-manifest": { + "hashes": [ + "sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95", + "sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce" + ], + "index": "pypi", + "version": "==0.47" + }, + "click": { + "hashes": [ + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.3" + }, + "coverage": { + "hashes": [ + "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", + "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", + "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", + "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", + "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", + "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", + "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", + "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", + "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", + "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", + "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", + "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", + "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", + "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", + "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", + "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", + "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", + "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", + "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", + "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", + "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", + "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", + "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", + "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", + "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", + "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", + "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", + "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", + "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", + "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", + "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", + "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", + "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", + "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", + "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", + "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", + "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", + "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", + "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", + "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", + "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", + "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", + "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", + "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", + "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", + "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", + "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" + ], + "index": "pypi", + "version": "==6.2" + }, + "defusedxml": { + "hashes": [ + "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" + ], + "index": "pypi", + "version": "==0.7.1" + }, + "docutils": { + "hashes": [ + "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", + "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.18.1" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3'", + "version": "==3.3" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "markdown2": { + "hashes": [ + "sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0", + "sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e" + ], + "index": "pypi", + "version": "==2.4.2" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "olefile": { + "hashes": [ + "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" + ], + "index": "pypi", + "version": "==0.46" + }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "index": "pypi", + "version": "==21.3" + }, + "pathspec": { + "hashes": [ + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + ], + "version": "==0.9.0" + }, + "pep517": { + "hashes": [ + "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0", + "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161" + ], + "version": "==0.12.0" + }, + "platformdirs": { + "hashes": [ + "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", + "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" + ], + "markers": "python_version >= '3.6'", + "version": "==2.4.0" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "py": { + "hashes": [ + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" + }, + "pygments": { + "hashes": [ + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" + ], + "markers": "python_version >= '3.5'", + "version": "==2.10.0" + }, + "pyparsing": { + "hashes": [ + "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", + "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.6" + }, + "pyroma": { + "hashes": [ + "sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65", + "sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a" + ], + "index": "pypi", + "version": "==3.2" + }, + "pytest": { + "hashes": [ + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" + ], + "index": "pypi", + "version": "==6.2.5" + }, + "pytest-cov": { + "hashes": [ + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" + ], + "index": "pypi", + "version": "==3.0.0" + }, + "pytest-timeout": { + "hashes": [ + "sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112", + "sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717" + ], + "index": "pypi", + "version": "==2.0.2" + }, + "requests": { + "hashes": [ + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.26.0" + }, + "setuptools": { + "hashes": [ + "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c", + "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b" + ], + "markers": "python_version >= '3.7'", + "version": "==60.0.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, + "tomli": { + "hashes": [ + "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", + "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "urllib3": { + "hashes": [ + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.7" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 205eada76..782b81f33 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ As of 2019, Pillow development is Code coverage + Tidelift Align diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 436a417d1..c26fc93bd 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -41,7 +41,7 @@ def test_sanity(): def test_default(): im = hopper("P") - assert_image(im, "P", im.size) + assert im.mode == "P" converted_im = im.convert() assert_image(converted_im, "RGB", im.size) converted_im = im.convert() diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index bd9db362c..27f4640e0 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -2,18 +2,18 @@ import pytest from PIL import Image -from .helper import assert_image, assert_image_similar, hopper, is_ppc64le +from .helper import assert_image_similar, hopper, is_ppc64le def test_sanity(): image = hopper() converted = image.quantize() - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 10) image = hopper() converted = image.quantize(palette=hopper("P")) - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 60) @@ -27,7 +27,7 @@ def test_libimagequant_quantize(): pytest.skip("libimagequant support not available") else: raise - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 15) assert len(converted.getcolors()) == 100 @@ -35,7 +35,7 @@ def test_libimagequant_quantize(): def test_octree_quantize(): image = hopper() converted = image.quantize(100, Image.FASTOCTREE) - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 20) assert len(converted.getcolors()) == 100 @@ -52,7 +52,7 @@ def test_quantize(): with Image.open("Tests/images/caption_6_33_22.png") as image: image = image.convert("RGB") converted = image.quantize() - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 1) @@ -62,7 +62,7 @@ def test_quantize_no_dither(): palette = palette.convert("P") converted = image.quantize(dither=0, palette=palette) - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert converted.palette.palette == palette.palette.palette diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index c36285451..fa2291582 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -6,7 +6,7 @@ import pytest from PIL import Image, ImageGrab -from .helper import assert_image, assert_image_equal_tofile, skip_unless_feature +from .helper import assert_image_equal_tofile, skip_unless_feature class TestImageGrab: @@ -14,25 +14,20 @@ class TestImageGrab: sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" ) def test_grab(self): - for im in [ - ImageGrab.grab(), - ImageGrab.grab(include_layered_windows=True), - ImageGrab.grab(all_screens=True), - ]: - assert_image(im, im.mode, im.size) + ImageGrab.grab() + ImageGrab.grab(include_layered_windows=True) + ImageGrab.grab(all_screens=True) im = ImageGrab.grab(bbox=(10, 20, 50, 80)) - assert_image(im, im.mode, (40, 60)) + assert im.size == (40, 60) @skip_unless_feature("xcb") def test_grab_x11(self): try: if sys.platform not in ("win32", "darwin"): - im = ImageGrab.grab() - assert_image(im, im.mode, im.size) + ImageGrab.grab() - im2 = ImageGrab.grab(xdisplay="") - assert_image(im2, im2.mode, im2.size) + ImageGrab.grab(xdisplay="") except OSError as e: pytest.skip(str(e)) @@ -71,8 +66,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only" return - im = ImageGrab.grabclipboard() - assert_image(im, im.mode, im.size) + ImageGrab.grabclipboard() @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_file(self): diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 53b1fef7c..589cb5a21 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -2,7 +2,7 @@ import pytest from PIL import ImageQt -from .helper import hopper +from .helper import assert_image_similar, hopper pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" @@ -42,8 +42,17 @@ def test_rgb(): def test_image(): - for mode in ("1", "RGB", "RGBA", "L", "P"): - ImageQt.ImageQt(hopper(mode)) + modes = ["1", "RGB", "RGBA", "L", "P"] + qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage + if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + modes.append("I;16") + + for mode in modes: + im = hopper(mode) + roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im)) + if mode not in ("RGB", "RGBA"): + im = im.convert("RGB") + assert_image_similar(roundtripped_im, im, 1) def test_closed_file(): diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 727eb7327..aa9efe192 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -497,6 +497,43 @@ Reading from a tar archive fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg") im = Image.open(fp) + +Batch processing +^^^^^^^^^^^^^^^^ + +Operations can be applied to multiple image files. For example, all PNG images +in the current directory can be saved as JPEGs at reduced quality. + +:: + + import glob + from PIL import Image + + + def compress_image(source_path, dest_path): + with Image.open(source_path) as img: + if img.mode != "RGB": + img = img.convert("RGB") + img.save(dest_path, "JPEG", optimize=True, quality=80) + + + paths = glob.glob("*.png") + for path in paths: + compress_image(path, path[:-4] + ".jpg") + +Since images can also be opened from a ``Path`` from the ``pathlib`` module, +the example could be modified to use ``pathlib`` instead of the ``glob`` +module. + +:: + + from pathlib import Path + + paths = Path(".").glob("*.png") + for path in paths: + compress_image(path, path.stem + ".jpg") + + Controlling the decoder ----------------------- diff --git a/docs/index.rst b/docs/index.rst index 3fbc8d0e6..0e16259f3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -49,6 +49,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_: + + Fredrik was an early Python contributor (e.g. Elementtree and the 're' + module) and his enthusiasm for the language and community were inspiring + for all who encountered him or his work. He spent countless hours on + comp.lang.python answering questions from newbies and advanced users alike. + + He also co-founded an early Python startup, Secret Labs AB, which among + other software released an IDE named PythonWorks. Fredrik also created the + Python Imaging Library (PIL) which is still THE way to interact with images + in Python, now most often through its Pillow fork. His effbot.org site was + a valuable resource for generations of Python users, especially its Tkinter + documentation. + +Thank you, Fredrik. + Backwards Incompatible Changes ============================== @@ -44,14 +67,6 @@ ImageFile.raise_ioerror has been removed. Use ``ImageFile.raise_oserror`` instead. -Deprecations -============ - -TODO -^^^^ - -TODO - API Changes =========== @@ -60,11 +75,19 @@ Added line width parameter to ImageDraw polygon An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``. -TODO API Additions ============= +ImageShow.XDGViewer +^^^^^^^^^^^^^^^^^^^ + +If ``xdg-open`` is present on Linux, this new :py:class:`PIL.ImageShow.Viewer` subclass +will be registered. It displays images using the application selected by the system. + +It is higher in priority than the other default :py:class:`PIL.ImageShow.Viewer` +instances, so it will be preferred by ``im.show()`` or :py:func:`.ImageShow.show()`. + Added support for "title" argument to DisplayViewer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index e142f1f27..db8fa0fa9 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -108,7 +108,7 @@ def align8to32(bytes, width, mode): converts each scanline of data from 8 bit to 32 bit aligned """ - bits_per_pixel = {"1": 1, "L": 8, "P": 8}[mode] + bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode] # calculate bytes per line and the extra padding if needed bits_per_line = bits_per_pixel * width @@ -167,6 +167,10 @@ def _toqclass_helper(im): elif im.mode == "RGBA": data = im.tobytes("raw", "BGRA") format = qt_format.Format_ARGB32 + elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + im = im.point(lambda i: i * 256) + + format = qt_format.Format_Grayscale16 else: if exclusive_fp: im.close() diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index cd0737c36..2135293e5 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -186,6 +186,16 @@ class UnixViewer(Viewer): return 1 +class XDGViewer(UnixViewer): + """ + The freedesktop.org ``xdg-open`` command. + """ + + def get_command_ex(self, file, **options): + command = executable = "xdg-open" + return command, executable + + class DisplayViewer(UnixViewer): """ The ImageMagick ``display`` command. @@ -233,6 +243,8 @@ class XVViewer(UnixViewer): if sys.platform not in ("win32", "darwin"): # unixoids + if shutil.which("xdg-open"): + register(XDGViewer) if shutil.which("display"): register(DisplayViewer) if shutil.which("gm"): diff --git a/src/_webp.c b/src/_webp.c index 90719b466..bccfb3d89 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -486,7 +486,7 @@ static struct PyMethodDef _anim_encoder_methods[] = { {NULL, NULL} /* sentinel */ }; -// WebPAnimDecoder type definition +// WebPAnimEncoder type definition static PyTypeObject WebPAnimEncoder_Type = { PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimEncoder", /*tp_name */ sizeof(WebPAnimEncoderObject), /*tp_size */ diff --git a/tox.ini b/tox.ini index 96d84f933..bdedc2bd5 100644 --- a/tox.ini +++ b/tox.ini @@ -11,8 +11,8 @@ minversion = 1.9 [testenv] commands = - {envpython} setup.py clean - {envpython} setup.py build_ext --inplace + make clean + {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} deps = diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c3a825c37..b1c95aa74 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -278,9 +278,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/3.1.2.zip", - "filename": "harfbuzz-3.1.2.zip", - "dir": "harfbuzz-3.1.2", + "url": "https://github.com/harfbuzz/harfbuzz/archive/3.2.0.zip", + "filename": "harfbuzz-3.2.0.zip", + "dir": "harfbuzz-3.2.0", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"),