Merge branch 'main' into exif_lightsource

This commit is contained in:
Andrew Murray 2022-12-22 07:50:35 +11:00
commit 818e967fec
43 changed files with 676 additions and 704 deletions

View File

@ -13,6 +13,10 @@ indent_style = space
trim_trailing_whitespace = true
[*.rst]
# Four-space indentation
indent_size = 4
[*.yml]
# Two-space indentation
indent_size = 2

View File

@ -20,7 +20,7 @@ jobs:
steps:
- name: "Check issues"
uses: actions/stale@v6
uses: actions/stale@v7
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action"

View File

@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-minor-version: [7, 8, 9]
python-minor-version: [8, 9]
timeout-minutes: 40
@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v3
- name: Install Cygwin
uses: cygwin/cygwin-install-action@v2
uses: cygwin/cygwin-install-action@v3
with:
platform: x86_64
packages: >
@ -48,7 +48,7 @@ jobs:
qt5-devel-tools subversion xorg-server-extra zlib-devel
- name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v2
uses: egor-tensin/cleanup-path@v3
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
@ -76,7 +76,7 @@ jobs:
- name: Build
shell: bash.exe -eo pipefail -o igncr "{0}"
run: |
.ci/build.sh
SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh
- name: Test
run: |

View File

@ -19,9 +19,9 @@ jobs:
architecture: ["x86", "x64"]
include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
- python-version: "pypy-3.7"
- python-version: "pypy3.8"
architecture: "x64"
- python-version: "pypy-3.8"
- python-version: "pypy3.9"
architecture: "x64"
timeout-minutes: 30
@ -141,7 +141,7 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_fribidi.cmd"
# trim ~150MB x 9
# trim ~150MB for each job
- name: Optimize build cache
if: steps.build-cache.outputs.cache-hit != 'true'
run: rmdir /S /Q winbuild\build\src

View File

@ -20,8 +20,8 @@ jobs:
"ubuntu-latest",
]
python-version: [
"pypy-3.8",
"pypy-3.7",
"pypy3.9",
"pypy3.8",
"3.11",
"3.10",
"3.9",

View File

@ -1,18 +1,25 @@
repos:
- repo: https://github.com/psf/black
rev: 22.10.0
rev: 22.12.0
hooks:
- id: black
args: ["--target-version", "py37"]
args: [--target-version=py37]
# Only .py files, until https://github.com/psf/black/issues/402 resolved
files: \.py$
types: []
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
rev: 5.11.1
hooks:
- id: isort
- repo: https://github.com/PyCQA/bandit
rev: 1.7.4
hooks:
- id: bandit
args: [--severity-level=high]
files: ^src/
- repo: https://github.com/asottile/yesqa
rev: v1.4.0
hooks:
@ -25,7 +32,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
@ -37,7 +44,7 @@ repos:
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v4.4.0
hooks:
- id: check-merge-conflict
- id: check-json
@ -48,5 +55,10 @@ repos:
hooks:
- id: sphinx-lint
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 0.5.2
hooks:
- id: tox-ini-fmt
ci:
autoupdate_schedule: monthly

View File

@ -5,6 +5,30 @@ Changelog (Pillow)
9.4.0 (unreleased)
------------------
- Ignore non-opaque WebP background when saving as GIF #6792
[radarhere]
- Only set tile in ImageFile __setstate__ #6793
[radarhere]
- When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767
[radarhere]
- Added IFD enum to ExifTags #6748
[radarhere]
- Fixed bug combining GIF frame durations #6779
[radarhere]
- Support saving JPEG comments #6774
[smason, radarhere]
- Added getxmp() to WebPImagePlugin #6758
[radarhere]
- Added "exact" option when saving WebP #6747
[ashafaei, radarhere]
- Use fractional coordinates when drawing text #6722
[radarhere]

View File

@ -1,7 +1,6 @@
include *.c
include *.h
include *.in
include *.lock
include *.md
include *.py
include *.rst
@ -10,7 +9,6 @@ include *.txt
include *.yaml
include LICENSE
include Makefile
include Pipfile
include tox.ini
graft Tests
graft src

22
Pipfile
View File

@ -1,22 +0,0 @@
[[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"

324
Pipfile.lock generated
View File

@ -1,324 +0,0 @@
{
"_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": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -19,9 +19,7 @@ python3 setup.py build --build-base=/tmp/build install
# Build fuzzers in $OUT.
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
fuzzer_basename=$(basename -s .py $fuzzer)
fuzzer_package=${fuzzer_basename}.pkg
pyinstaller \
compile_python_fuzzer $fuzzer \
--add-binary /usr/local/lib/libjpeg.so.62.3.0:. \
--add-binary /usr/local/lib/libfreetype.so.6:. \
--add-binary /usr/local/lib/liblcms2.so.2:. \
@ -31,17 +29,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
--add-binary /usr/local/lib/libwebp.so.7:. \
--add-binary /usr/local/lib/libwebpdemux.so.2:. \
--add-binary /usr/local/lib/libwebpmux.so.3:. \
--add-binary /usr/local/lib/libxcb.so.1:. \
--distpath $OUT --onefile --name $fuzzer_package $fuzzer
# Create execution wrapper.
echo "#!/bin/sh
# LLVMFuzzerTestOneInput for fuzzer detection.
this_dir=\$(dirname \"\$0\")
LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \
ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \
\$this_dir/$fuzzer_package \$@" > $OUT/$fuzzer_basename
chmod u+x $OUT/$fuzzer_basename
--add-binary /usr/local/lib/libxcb.so.1:.
done
find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@

View File

@ -791,6 +791,22 @@ def test_roundtrip_info_duration(tmp_path):
] == duration_list
def test_roundtrip_info_duration_combined(tmp_path):
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/duplicate_frame.gif") as im:
assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
1000,
1000,
1000,
]
im.save(out, save_all=True)
with Image.open(out) as reloaded:
assert [
frame.info["duration"] for frame in ImageSequence.Iterator(reloaded)
] == [1000, 2000]
def test_identical_frames(tmp_path):
duration_list = [1000, 1500, 2000, 4000]
@ -859,12 +875,21 @@ def test_background(tmp_path):
im.info["background"] = 1
im.save(out)
with Image.open(out) as reread:
assert reread.info["background"] == im.info["background"]
def test_webp_background(tmp_path):
out = str(tmp_path / "temp.gif")
# Test opaque WebP background
if features.check("webp") and features.check("webp_anim"):
with Image.open("Tests/images/hopper.webp") as im:
assert isinstance(im.info["background"], tuple)
assert im.info["background"] == (255, 255, 255, 255)
im.save(out)
# Test non-opaque WebP background
im = Image.new("L", (100, 100), "#000")
im.info["background"] = (0, 0, 0, 0)
im.save(out)

View File

@ -86,6 +86,33 @@ class TestFileJpeg:
assert len(im.applist) == 2
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
assert im.app["COM"] == im.info["comment"]
def test_comment_write(self):
with Image.open(TEST_FILE) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
# Test that existing comment is saved by default
out = BytesIO()
im.save(out, format="JPEG")
with Image.open(out) as reloaded:
assert im.info["comment"] == reloaded.info["comment"]
# Ensure that a blank comment causes any existing comment to be removed
for comment in ("", b"", None):
out = BytesIO()
im.save(out, format="JPEG", comment=comment)
with Image.open(out) as reloaded:
assert "comment" not in reloaded.info
# Test that a comment argument overrides the default comment
for comment in ("Test comment text", b"Text comment text"):
out = BytesIO()
im.save(out, format="JPEG", comment=comment)
with Image.open(out) as reloaded:
if not isinstance(comment, bytes):
comment = comment.encode()
assert reloaded.info["comment"] == comment
def test_cmyk(self):
# Test CMYK handling. Thanks to Tim and Charlie for test data,
@ -415,6 +442,13 @@ class TestFileJpeg:
info = im._getexif()
assert info[305] == "Adobe Photoshop CS Macintosh"
def test_get_child_images(self):
with Image.open("Tests/images/flower.jpg") as im:
ims = im.get_child_images()
assert len(ims) == 1
assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png")
def test_mp(self):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert im._getmp() is None

View File

@ -97,6 +97,35 @@ def test_write_rgba(tmp_path):
assert_image_similar(image, pil_image, 1.0)
def test_keep_rgb_values_when_transparent(tmp_path):
"""
Saving transparent pixels should retain their original RGB values
when using the "exact" parameter.
"""
image = hopper("RGB")
# create a copy of the image
# with the left half transparent
half_transparent_image = image.copy()
new_alpha = Image.new("L", (128, 128), 255)
new_alpha.paste(0, (0, 0, 64, 128))
half_transparent_image.putalpha(new_alpha)
# save with transparent area preserved
temp_file = str(tmp_path / "temp.webp")
half_transparent_image.save(temp_file, exact=True, lossless=True)
with Image.open(temp_file) as reloaded:
assert reloaded.mode == "RGBA"
assert reloaded.format == "WEBP"
# even though it is lossless, if we don't use exact=True
# in libwebp >= 0.5, the transparent area will be filled with black
# (or something more conducive to compression)
assert_image_equal(reloaded.convert("RGB"), image)
def test_write_unsupported_mode_PA(tmp_path):
"""
Saving a palette-based file with transparency to WebP format

View File

@ -11,6 +11,11 @@ pytestmark = [
skip_unless_feature("webp_mux"),
]
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
def test_read_exif_metadata():
@ -110,6 +115,22 @@ def test_read_no_exif():
assert not webp_image._getexif()
def test_getxmp():
with Image.open("Tests/images/flower.webp") as im:
assert "xmp" not in im.info
assert im.getxmp() == {}
with Image.open("Tests/images/flower2.webp") as im:
if ElementTree is None:
with pytest.warns(UserWarning):
assert im.getxmp() == {}
else:
assert (
im.getxmp()["xmpmeta"]["xmptk"]
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
)
@skip_unless_feature("webp_anim")
def test_write_animated_metadata(tmp_path):
iccp_data = b"<iccp_data>"

View File

@ -7,7 +7,14 @@ import warnings
import pytest
from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features
from PIL import (
ExifTags,
Image,
ImageDraw,
ImagePalette,
UnidentifiedImageError,
features,
)
from .helper import (
assert_image_equal,
@ -808,6 +815,18 @@ class TestImage:
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005)
def test_exif_ifd1(self):
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
assert exif.get_ifd(ExifTags.IFD.IFD1) == {
513: 2036,
514: 5448,
259: 6,
296: 2,
282: 180.0,
283: 180.0,
}
def test_exif_ifd(self):
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()

View File

@ -34,7 +34,7 @@ def test_numpy_to_image():
# Check supported 1-bit integer formats
assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE)
assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE)
assert_image(to_image(numpy.bool_, 1, 1), "1", TEST_IMAGE_SIZE)
# Check supported 8-bit integer formats
assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE)
@ -193,7 +193,7 @@ def test_putdata():
"dtype",
(
bool,
numpy.bool8,
numpy.bool_,
numpy.int8,
numpy.int16,
numpy.int32,

View File

@ -15,11 +15,12 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " serve to start a local server for viewing docs"
@echo " livehtml to start a local server for viewing docs and auto-reload on change"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@ -39,42 +40,49 @@ help:
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
.PHONY: clean
clean:
-rm -rf $(BUILDDIR)/*
install-sphinx:
$(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile
$(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph
.PHONY: html
html:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@ -82,6 +90,7 @@ htmlhelp:
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@ -92,6 +101,7 @@ qthelp:
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc"
.PHONY: devhelp
devhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@ -102,12 +112,14 @@ devhelp:
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork"
@echo "# devhelp"
.PHONY: epub
epub:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: latex
latex:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@ -116,6 +128,7 @@ latex:
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@ -123,18 +136,21 @@ latexpdf:
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@ -143,6 +159,7 @@ texinfo:
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@ -150,18 +167,21 @@ info:
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
@ -169,14 +189,17 @@ linkcheck:
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: livehtml
livehtml: html
livereload $(BUILDDIR)/html -p 33233
.PHONY: serve
serve:
cd $(BUILDDIR)/html; $(PYTHON) -m http.server

View File

@ -27,12 +27,13 @@ needs_sphinx = "2.4"
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx_copybutton",
"sphinx_issues",
"sphinx_removed_in",
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
"sphinx_copybutton",
"sphinx_inline_tabs",
"sphinx_issues",
"sphinx_removed_in",
"sphinxext.opengraph",
]

View File

@ -474,6 +474,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
.. versionadded:: 2.5.0
**comment**
A comment about the image.
.. versionadded:: 9.4.0
.. note::
@ -1124,6 +1129,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**method**
Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4.
**exact**
If true, preserve the transparent RGB values. Otherwise, discard
invisible RGB values for better compression. Defaults to false.
Requires libwebp 0.5.0 or later.
**icc_profile**
The ICC Profile to include in the saved file. Only supported if
the system WebP library was built with webpmux support.

View File

@ -23,6 +23,11 @@ Pillow supports these Python versions.
:file: older-versions.csv
:header-rows: 1
.. _Linux Installation:
.. _macOS Installation:
.. _Windows Installation:
.. _FreeBSD Installation:
Basic Installation
------------------
@ -38,34 +43,7 @@ Install Pillow with :command:`pip`::
python3 -m pip install --upgrade Pillow
Windows Installation
^^^^^^^^^^^^^^^^^^^^
We provide Pillow binaries for Windows compiled for the matrix of
supported Pythons in both 32 and 64-bit versions in the wheel format.
These binaries include support for all optional libraries except
libimagequant and libxcb. Raqm support requires
FriBiDi to be installed separately::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_.
macOS Installation
^^^^^^^^^^^^^^^^^^
We provide binaries for macOS for each of the supported Python
versions in the wheel format. These include support for all optional
libraries except libimagequant. Raqm support requires
FriBiDi to be installed separately::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
Linux Installation
^^^^^^^^^^^^^^^^^^
.. tab:: Linux
We provide binaries for Linux for each of the supported Python
versions in the manylinux wheel format. These include support for all
@ -80,8 +58,30 @@ also include Pillow in packages that previously contained PIL e.g.
``python-imaging``. Debian splits it into two packages, ``python3-pil``
and ``python3-pil.imagetk``.
FreeBSD Installation
^^^^^^^^^^^^^^^^^^^^
.. tab:: macOS
We provide binaries for macOS for each of the supported Python
versions in the wheel format. These include support for all optional
libraries except libimagequant. Raqm support requires
FriBiDi to be installed separately::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
.. tab:: Windows
We provide Pillow binaries for Windows compiled for the matrix of
supported Pythons in both 32 and 64-bit versions in the wheel format.
These binaries include support for all optional libraries except
libimagequant and libxcb. Raqm support requires
FriBiDi to be installed separately::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_.
.. tab:: FreeBSD
Pillow can be installed on FreeBSD via the official Ports or Packages systems:
@ -100,13 +100,16 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems:
are tested by the ports team with all supported FreeBSD versions.
.. _Building on Linux:
.. _Building on macOS:
.. _Building on Windows:
.. _Building on Windows using MSYS2/MinGW:
.. _Building on FreeBSD:
.. _Building on Android:
Building From Source
--------------------
Download and extract the `compressed archive from PyPI`_.
.. _compressed archive from PyPI: https://pypi.org/project/Pillow/
.. _external-libraries:
External Libraries
@ -191,164 +194,7 @@ Many of Pillow's features require external libraries:
* **libxcb** provides X11 screengrab support.
Once you have installed the prerequisites, run::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow --no-binary :all:
If the prerequisites are installed in the standard library locations
for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
additional configuration should be required. If they are installed in
a non-standard location, you may need to configure setuptools to use
those locations by editing :file:`setup.py` or
:file:`setup.cfg`, or by adding environment variables on the command
line::
CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
If Pillow has been previously built without the required
prerequisites, it may be necessary to manually clear the pip cache or
build without cache using the ``--no-cache-dir`` option to force a
build with newly installed external libraries.
Build Options
^^^^^^^^^^^^^
* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
multiprocessing to build the extension. Setting ``MAX_CONCURRENCY``
sets the number of CPUs to use, or can disable parallel building by
using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
available, as many as are present.
* Build flags: ``--disable-zlib``, ``--disable-jpeg``,
``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``,
``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``,
``--disable-imagequant``, ``--disable-xcb``.
Disable building the corresponding feature even if the development
libraries are present on the building machine.
* Build flags: ``--enable-zlib``, ``--enable-jpeg``,
``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``,
``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``,
``--enable-imagequant``, ``--enable-xcb``.
Require that the corresponding feature is built. The build will raise
an exception if the libraries are not found. Webpmux (WebP metadata)
relies on WebP support. Tcl and Tk also must be used together.
* Build flags: ``--vendor-raqm --vendor-fribidi``
These flags are used to compile a modified version of libraqm and
a shim that dynamically loads libfribidi at runtime. These are
used to compile the standard Pillow wheels. Compiling libraqm requires
a C99-compliant compiler.
* Build flag: ``--disable-platform-guessing``. Skips all of the
platform dependent guessing of include and library directories for
automated build systems that configure the proper paths in the
environment variables (e.g. Buildroot).
* Build flag: ``--debug``. Adds a debugging flag to the include and
library search process to dump all paths searched for and found to
stdout.
Sample usage::
python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]"
Building on macOS
^^^^^^^^^^^^^^^^^
The Xcode command line tools are required to compile portions of
Pillow. The tools are installed by running ``xcode-select --install``
from the command line. The command line tools are required even if you
have the full Xcode package installed. It may be necessary to run
``sudo xcodebuild -license`` to accept the license prior to using the
tools.
The easiest way to install external libraries is via `Homebrew
<https://brew.sh/>`_. After you install Homebrew, run::
brew install libjpeg libtiff little-cms2 openjpeg webp
To install libraqm on macOS use Homebrew to install its dependencies::
brew install freetype harfbuzz fribidi
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
Now install Pillow with::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow --no-binary :all:
or from within the uncompressed source directory::
python3 -m pip install .
Building on Windows
^^^^^^^^^^^^^^^^^^^
We recommend you use prebuilt wheels from PyPI.
If you wish to compile Pillow manually, you can use the build scripts
in the ``winbuild`` directory used for CI testing and development.
These scripts require Visual Studio 2017 or newer and NASM.
Building on Windows using MSYS2/MinGW
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or
**MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly.
The following instructions target the 64-bit build, for 32-bit
replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``.
Make sure you have Python and GCC installed::
pacman -S \
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-python3 \
mingw-w64-x86_64-python3-pip \
mingw-w64-x86_64-python3-setuptools
Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
pacman -S \
mingw-w64-x86_64-libjpeg-turbo \
mingw-w64-x86_64-zlib \
mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-freetype \
mingw-w64-x86_64-lcms2 \
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libraqm
Now install Pillow with::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow --no-binary :all:
Building on FreeBSD
^^^^^^^^^^^^^^^^^^^
.. Note:: Only FreeBSD 10 and 11 tested
Make sure you have Python's development libraries installed::
sudo pkg install python3
Prerequisites are installed on **FreeBSD 10 or 11** with::
sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
Building on Linux
^^^^^^^^^^^^^^^^^
.. tab:: Linux
If you didn't build Python from source, make sure you have Python's
development libraries installed.
@ -395,8 +241,80 @@ See also the ``Dockerfile``\s in the Test Infrastructure repo
(https://github.com/python-pillow/docker-images) for a known working
install process for other tested distros.
Building on Android
^^^^^^^^^^^^^^^^^^^
.. tab:: macOS
The Xcode command line tools are required to compile portions of
Pillow. The tools are installed by running ``xcode-select --install``
from the command line. The command line tools are required even if you
have the full Xcode package installed. It may be necessary to run
``sudo xcodebuild -license`` to accept the license prior to using the
tools.
The easiest way to install external libraries is via `Homebrew
<https://brew.sh/>`_. After you install Homebrew, run::
brew install libjpeg libtiff little-cms2 openjpeg webp
To install libraqm on macOS use Homebrew to install its dependencies::
brew install freetype harfbuzz fribidi
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
.. tab:: Windows
We recommend you use prebuilt wheels from PyPI.
If you wish to compile Pillow manually, you can use the build scripts
in the ``winbuild`` directory used for CI testing and development.
These scripts require Visual Studio 2017 or newer and NASM.
The scripts also install Pillow from the local copy of the source code, so the
`Installing`_ instructions will not be necessary afterwards.
.. tab:: Windows using MSYS2/MinGW
To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or
**MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly.
The following instructions target the 64-bit build, for 32-bit
replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``.
Make sure you have Python and GCC installed::
pacman -S \
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-python3 \
mingw-w64-x86_64-python3-pip \
mingw-w64-x86_64-python3-setuptools
Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
pacman -S \
mingw-w64-x86_64-libjpeg-turbo \
mingw-w64-x86_64-zlib \
mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-freetype \
mingw-w64-x86_64-lcms2 \
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libraqm
.. tab:: FreeBSD
.. Note:: Only FreeBSD 10 and 11 tested
Make sure you have Python's development libraries installed::
sudo pkg install python3
Prerequisites are installed on **FreeBSD 10 or 11** with::
sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
.. tab:: Android
Basic Android support has been added for compilation within the Termux
environment. The dependencies can be installed by::
@ -406,6 +324,84 @@ environment. The dependencies can be installed by::
This has been tested within the Termux app on ChromeOS, on x86.
Installing
^^^^^^^^^^
Once you have installed the prerequisites, to install Pillow from the source
code on PyPI, run::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow --no-binary :all:
If the prerequisites are installed in the standard library locations
for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
additional configuration should be required. If they are installed in
a non-standard location, you may need to configure setuptools to use
those locations by editing :file:`setup.py` or
:file:`setup.cfg`, or by adding environment variables on the command
line::
CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
If Pillow has been previously built without the required
prerequisites, it may be necessary to manually clear the pip cache or
build without cache using the ``--no-cache-dir`` option to force a
build with newly installed external libraries.
If you would like to install from a local copy of the source code instead, you
can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow``
or download and extract the `compressed archive from PyPI`_.
After navigating to the Pillow directory, run::
python3 -m pip install --upgrade pip
python3 -m pip install .
.. _compressed archive from PyPI: https://pypi.org/project/Pillow/#files
Build Options
"""""""""""""
* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
multiprocessing to build the extension. Setting ``MAX_CONCURRENCY``
sets the number of CPUs to use, or can disable parallel building by
using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
available, as many as are present.
* Build flags: ``--disable-zlib``, ``--disable-jpeg``,
``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``,
``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``,
``--disable-imagequant``, ``--disable-xcb``.
Disable building the corresponding feature even if the development
libraries are present on the building machine.
* Build flags: ``--enable-zlib``, ``--enable-jpeg``,
``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``,
``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``,
``--enable-imagequant``, ``--enable-xcb``.
Require that the corresponding feature is built. The build will raise
an exception if the libraries are not found. Webpmux (WebP metadata)
relies on WebP support. Tcl and Tk also must be used together.
* Build flags: ``--vendor-raqm --vendor-fribidi``
These flags are used to compile a modified version of libraqm and
a shim that dynamically loads libfribidi at runtime. These are
used to compile the standard Pillow wheels. Compiling libraqm requires
a C99-compliant compiler.
* Build flag: ``--disable-platform-guessing``. Skips all of the
platform dependent guessing of include and library directories for
automated build systems that configure the proper paths in the
environment variables (e.g. Buildroot).
* Build flag: ``--debug``. Adds a debugging flag to the include and
library search process to dump all paths searched for and found to
stdout.
Sample usage::
python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]"
Platform Support
----------------
@ -464,7 +460,7 @@ These platforms are built and tested for every change.
| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86, x86-64 |
| +----------------------------+---------------------+
| | 3.7, 3.8, 3.9 (Cygwin) | x86-64 |
| | 3.8, 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+

View File

@ -31,6 +31,13 @@ which provide constants and clear-text names for various well-known EXIF tags.
>>> Interop(4096).name
'RelatedImageFileFormat'
.. py:data:: IFD
>>> from PIL.ExifTags import IFD
>>> IFD.Exif.value
34665
>>> IFD(34665).name
'Exif'
Two of these values are also exposed as dictionaries.

View File

@ -37,6 +37,28 @@ support a ``start`` argument. This tuple of horizontal and vertical offset
will be used internally by :py:meth:`.ImageDraw.text` to more accurately place
text at the ``xy`` coordinates.
Added the ``exact`` encoding option for WebP
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``exact`` encoding option for WebP is now supported. The WebP encoder
removes the hidden RGB values for better compression by default in libwebp 0.5
or later. By setting this option to ``True``, the encoder will keep the hidden
RGB values.
getxmp()
^^^^^^^^
`XMP data <https://en.wikipedia.org/wiki/Extensible_Metadata_Platform>`_ can now be
decoded for WEBP images through ``getxmp()``.
Writing JPEG comments
^^^^^^^^^^^^^^^^^^^^^
When saving a JPEG image, a comment can now be written from
:py:attr:`~PIL.Image.Image.info`, or by using an argument when saving::
im.save(out, comment="Test comment")
Security
========

View File

@ -46,6 +46,7 @@ docs =
olefile
sphinx>=2.4
sphinx-copybutton
sphinx-inline-tabs
sphinx-issues>=3.0.1
sphinx-removed-in
sphinxext-opengraph

View File

@ -373,6 +373,9 @@ class BLP1Decoder(_BLPBaseDecoder):
data = BytesIO(data)
image = JpegImageFile(data)
Image._decompression_bomb_check(image.size)
if image.mode == "CMYK":
decoder_name, extents, offset, args = image.tile[0]
image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
r, g, b = image.convert("RGB").split()
image = Image.merge("RGB", (b, g, r))
self.set_as_raw(image.tobytes())

View File

@ -348,6 +348,14 @@ class Interop(IntEnum):
RleatedImageHeight = 4098
class IFD(IntEnum):
Exif = 34665
GPSInfo = 34853
Makernote = 37500
Interop = 40965
IFD1 = -1
class LightSource(IntEnum):
Unknown = 0
Daylight = 1

View File

@ -618,7 +618,7 @@ def _write_multiple_frames(im, fp, palette):
bbox = delta.getbbox()
if not bbox:
# This frame is identical to the previous frame
if duration:
if encoderinfo.get("duration"):
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
continue
else:
@ -886,20 +886,23 @@ def _get_palette_bytes(im):
def _get_background(im, info_background):
background = 0
if info_background:
background = info_background
if isinstance(background, tuple):
if isinstance(info_background, tuple):
# WebPImagePlugin stores an RGBA value in info["background"]
# So it must be converted to the same format as GifImagePlugin's
# info["background"] - a global color table index
try:
background = im.palette.getcolor(background, im)
background = im.palette.getcolor(info_background, im)
except ValueError as e:
if str(e) == "cannot allocate more than 256 colors":
if str(e) not in (
# If all 256 colors are in use,
# then there is no need for the background color
return 0
else:
"cannot allocate more than 256 colors",
# Ignore non-opaque WebP background
"cannot add non-opaque RGBA color to RGB palette",
):
raise
else:
background = info_background
return background

View File

@ -47,7 +47,14 @@ except ImportError:
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
# Use __version__ instead.
from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
from . import (
ExifTags,
ImageMode,
TiffTags,
UnidentifiedImageError,
__version__,
_plugins,
)
from ._binary import i32le, o32be, o32le
from ._deprecate import deprecate
from ._util import DeferredError, is_path
@ -704,7 +711,6 @@ class Image:
def __setstate__(self, state):
Image.__init__(self)
self.tile = []
info, mode, size, palette, data = state
self.info = info
self.mode = mode
@ -1447,6 +1453,49 @@ class Image:
self._exif._loaded = False
self.getexif()
def get_child_images(self):
child_images = []
exif = self.getexif()
ifds = []
if ExifTags.Base.SubIFDs in exif:
subifd_offsets = exif[ExifTags.Base.SubIFDs]
if subifd_offsets:
if not isinstance(subifd_offsets, tuple):
subifd_offsets = (subifd_offsets,)
for subifd_offset in subifd_offsets:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(513):
ifds.append((ifd1, exif._info.next))
offset = None
for ifd, ifd_offset in ifds:
current_offset = self.fp.tell()
if offset is None:
offset = current_offset
fp = self.fp
thumbnail_offset = ifd.get(513)
if thumbnail_offset is not None:
try:
thumbnail_offset += self._exif_offset
except AttributeError:
pass
self.fp.seek(thumbnail_offset)
data = self.fp.read(ifd.get(514))
fp = io.BytesIO(data)
with open(fp) as im:
if thumbnail_offset is None:
im._frame_pos = [ifd_offset]
im._seek(0)
im.load()
child_images.append(im)
if offset is not None:
self.fp.seek(offset)
return child_images
def getim(self):
"""
Returns a capsule that points to the internal image memory.
@ -3598,14 +3647,16 @@ class Exif(MutableMapping):
merged_dict = dict(self)
# get EXIF extension
if 0x8769 in self:
ifd = self._get_ifd_dict(self[0x8769])
if ExifTags.IFD.Exif in self:
ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif])
if ifd:
merged_dict.update(ifd)
# GPS
if 0x8825 in self:
merged_dict[0x8825] = self._get_ifd_dict(self[0x8825])
if ExifTags.IFD.GPSInfo in self:
merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict(
self[ExifTags.IFD.GPSInfo]
)
return merged_dict
@ -3615,31 +3666,34 @@ class Exif(MutableMapping):
head = self._get_head()
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
for tag, value in self.items():
if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict):
if tag in [
ExifTags.IFD.Exif,
ExifTags.IFD.GPSInfo,
] and not isinstance(value, dict):
value = self.get_ifd(tag)
if (
tag == 0x8769
and 0xA005 in value
and not isinstance(value[0xA005], dict)
tag == ExifTags.IFD.Exif
and ExifTags.IFD.Interop in value
and not isinstance(value[ExifTags.IFD.Interop], dict)
):
value = value.copy()
value[0xA005] = self.get_ifd(0xA005)
value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop)
ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset)
def get_ifd(self, tag):
if tag not in self._ifds:
if tag in [0x8769, 0x8825]:
# exif, gpsinfo
if tag == ExifTags.IFD.IFD1:
if self._info is not None:
self._ifds[tag] = self._get_ifd_dict(self._info.next)
elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
if tag in self:
self._ifds[tag] = self._get_ifd_dict(self[tag])
elif tag in [0xA005, 0x927C]:
# interop, makernote
if 0x8769 not in self._ifds:
self.get_ifd(0x8769)
tag_data = self._ifds[0x8769][tag]
if tag == 0x927C:
# makernote
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif)
tag_data = self._ifds[ExifTags.IFD.Exif][tag]
if tag == ExifTags.IFD.Makernote:
from .TiffImagePlugin import ImageFileDirectory_v2
if tag_data[:8] == b"FUJIFILM":
@ -3715,7 +3769,7 @@ class Exif(MutableMapping):
makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
self._ifds[tag] = makernote
else:
# interop
# Interop
self._ifds[tag] = self._get_ifd_dict(tag_data)
return self._ifds.get(tag, {})

View File

@ -137,6 +137,10 @@ class ImageFile(Image.Image):
if self.format is not None:
return Image.MIME.get(self.format.upper())
def __setstate__(self, state):
self.tile = []
super().__setstate__(state)
def verify(self):
"""Check file integrity"""

View File

@ -125,7 +125,7 @@ class Viewer:
path = options.pop("file")
else:
raise TypeError("Missing required argument: 'path'")
os.system(self.get_command(path, **options))
os.system(self.get_command(path, **options)) # nosec
return 1

View File

@ -45,6 +45,7 @@ from . import Image, ImageFile, TiffImagePlugin
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
from ._binary import o16be as o16
from ._deprecate import deprecate
from .JpegPresets import presets
@ -89,6 +90,7 @@ def APP(self, marker):
if "exif" not in self.info:
# extract EXIF information (incomplete)
self.info["exif"] = s # FIXME: value will change
self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change
@ -724,7 +726,7 @@ def _save(im, fp, filename):
icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:]
i = 1
for marker in markers:
size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker))
size = o16(2 + ICC_OVERHEAD_LEN + len(marker))
extra += (
b"\xFF\xE2"
+ size
@ -735,6 +737,8 @@ def _save(im, fp, filename):
)
i += 1
comment = info.get("comment", im.info.get("comment"))
# "progressive" is the official name, but older documentation
# says "progression"
# FIXME: issue a warning if the wrong form is used (post-1.1.7)
@ -757,6 +761,7 @@ def _save(im, fp, filename):
dpi[1],
subsampling,
qtables,
comment,
extra,
exif,
)

View File

@ -22,7 +22,14 @@ import itertools
import os
import struct
from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin
from . import (
ExifTags,
Image,
ImageFile,
ImageSequence,
JpegImagePlugin,
TiffImagePlugin,
)
from ._binary import i16be as i16
from ._binary import o32le
@ -137,7 +144,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"]
if mptype.startswith("Large Thumbnail"):
exif = self.getexif().get_ifd(0x8769)
exif = self.getexif().get_ifd(ExifTags.IFD.Exif)
if 40962 in exif and 40963 in exif:
self._size = (exif[40962], exif[40963])
elif "exif" in self.info:

View File

@ -208,7 +208,9 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
tokens = b"".join(block.split())
for token in tokens:
if token not in (48, 49):
raise ValueError(f"Invalid token for this mode: {bytes([token])}")
raise ValueError(
b"Invalid token for this mode: %s" % bytes([token])
)
data = (data + tokens)[:total_bytes]
invert = bytes.maketrans(b"01", b"\xFF\x00")
return data.translate(invert)
@ -242,13 +244,13 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
half_token = tokens.pop() # save half token for later
if len(half_token) > max_len: # prevent buildup of half_token
raise ValueError(
f"Token too long found in data: {half_token[:max_len + 1]}"
b"Token too long found in data: %s" % half_token[: max_len + 1]
)
for token in tokens:
if len(token) > max_len:
raise ValueError(
f"Token too long found in data: {token[:max_len + 1]}"
b"Token too long found in data: %s" % token[: max_len + 1]
)
value = int(token)
if value > maxval:

View File

@ -1153,39 +1153,6 @@ class TiffImageFile(ImageFile.ImageFile):
"""Return the current frame number"""
return self.__frame
def get_child_images(self):
if SUBIFD not in self.tag_v2:
return []
child_images = []
exif = self.getexif()
offset = None
for im_offset in self.tag_v2[SUBIFD]:
# reset buffered io handle in case fp
# was passed to libtiff, invalidating the buffer
current_offset = self._fp.tell()
if offset is None:
offset = current_offset
fp = self._fp
ifd = exif._get_ifd_dict(im_offset)
jpegInterchangeFormat = ifd.get(513)
if jpegInterchangeFormat is not None:
fp.seek(jpegInterchangeFormat)
jpeg_data = fp.read(ifd.get(514))
fp = io.BytesIO(jpeg_data)
with Image.open(fp) as im:
if jpegInterchangeFormat is None:
im._frame_pos = [im_offset]
im._seek(0)
im.load()
child_images.append(im)
if offset is not None:
self._fp.seek(offset)
return child_images
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.

View File

@ -98,6 +98,15 @@ class WebPImageFile(ImageFile.ImageFile):
return None
return self.getexif()._get_merged_dict()
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
:returns: XMP tags in a dictionary.
"""
return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {}
def seek(self, frame):
if not self._seek_check(frame):
return
@ -318,6 +327,7 @@ def _save(im, fp, filename):
exif = exif[6:]
xmp = im.encoderinfo.get("xmp", "")
method = im.encoderinfo.get("method", 4)
exact = 1 if im.encoderinfo.get("exact") else 0
if im.mode not in _VALID_WEBP_LEGACY_MODES:
alpha = (
@ -336,6 +346,7 @@ def _save(im, fp, filename):
im.mode,
icc_profile,
method,
exact,
exif,
xmp,
)

View File

@ -178,12 +178,11 @@ _anim_encoder_new(PyObject *self, PyObject *args) {
return NULL;
}
PyObject *
void
_anim_encoder_dealloc(PyObject *self) {
WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self;
WebPPictureFree(&(encp->frame));
WebPAnimEncoderDelete(encp->enc);
Py_RETURN_NONE;
}
PyObject *
@ -400,12 +399,11 @@ _anim_decoder_new(PyObject *self, PyObject *args) {
return NULL;
}
PyObject *
void
_anim_decoder_dealloc(PyObject *self) {
WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self;
WebPDataClear(&(decp->data));
WebPAnimDecoderDelete(decp->dec);
Py_RETURN_NONE;
}
PyObject *
@ -576,6 +574,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
int lossless;
float quality_factor;
int method;
int exact;
uint8_t *rgb;
uint8_t *icc_bytes;
uint8_t *exif_bytes;
@ -597,7 +596,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
"y#iiifss#is#s#",
"y#iiifss#iis#s#",
(char **)&rgb,
&size,
&width,
@ -608,6 +607,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
&icc_bytes,
&icc_size,
&method,
&exact,
&exif_bytes,
&exif_size,
&xmp_bytes,
@ -633,6 +633,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
config.lossless = lossless;
config.quality = quality_factor;
config.method = method;
#if WEBP_ENCODER_ABI_VERSION >= 0x0209
// the "exact" flag is only available in libwebp 0.5.0 and later
config.exact = exact;
#endif
// Validate the config
if (!WebPValidateConfig(&config)) {

View File

@ -1048,6 +1048,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
PyObject *qtables = NULL;
unsigned int *qarrays = NULL;
int qtablesLen = 0;
char *comment = NULL;
Py_ssize_t comment_size;
char *extra = NULL;
Py_ssize_t extra_size;
char *rawExif = NULL;
@ -1055,7 +1057,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
"ss|nnnnnnnnOy#y#",
"ss|nnnnnnnnOz#y#y#",
&mode,
&rawmode,
&quality,
@ -1067,6 +1069,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
&ydpi,
&subsampling,
&qtables,
&comment,
&comment_size,
&extra,
&extra_size,
&rawExif,
@ -1090,13 +1094,28 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
return NULL;
}
// Freed in JpegEncode, Case 5
// Freed in JpegEncode, Case 6
qarrays = get_qtables_arrays(qtables, &qtablesLen);
if (comment && comment_size > 0) {
/* malloc check ok, length is from python parsearg */
char *p = malloc(comment_size); // Freed in JpegEncode, Case 6
if (!p) {
return ImagingError_MemoryError();
}
memcpy(p, comment, comment_size);
comment = p;
} else {
comment = NULL;
}
if (extra && extra_size > 0) {
/* malloc check ok, length is from python parsearg */
char *p = malloc(extra_size); // Freed in JpegEncode, Case 5
char *p = malloc(extra_size); // Freed in JpegEncode, Case 6
if (!p) {
if (comment) {
free(comment);
}
return ImagingError_MemoryError();
}
memcpy(p, extra, extra_size);
@ -1107,8 +1126,11 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (rawExif && rawExifLen > 0) {
/* malloc check ok, length is from python parsearg */
char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5
char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6
if (!pp) {
if (comment) {
free(comment);
}
if (extra) {
free(extra);
}
@ -1134,6 +1156,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype;
((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi;
((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi;
((JPEGENCODERSTATE *)encoder->state.context)->comment = comment;
((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size;
((JPEGENCODERSTATE *)encoder->state.context)->extra = extra;
((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size;
((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif;

View File

@ -92,6 +92,10 @@ typedef struct {
/* in factors of DCTSIZE2 */
int qtablesLen;
/* Comment */
char *comment;
size_t comment_size;
/* Extra data (to be injected after header) */
char *extra;
int extra_size;

View File

@ -277,6 +277,13 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
case 4:
if (context->comment) {
jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size);
}
state->state++;
case 5:
if (1024 > context->destination.pub.free_in_buffer) {
break;
}
@ -301,7 +308,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
state->state++;
/* fall through */
case 5:
case 6:
/* Finish compression */
if (context->destination.pub.free_in_buffer < 100) {
@ -310,6 +317,10 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
jpeg_finish_compress(&context->cinfo);
/* Clean up */
if (context->comment) {
free(context->comment);
context->comment = NULL;
}
if (context->extra) {
free(context->extra);
context->extra = NULL;

25
tox.ini
View File

@ -1,15 +1,13 @@
# Tox (https://tox.readthedocs.io/en/latest/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it,
# "python3 -m pip install tox" and then run "tox" from this directory.
[tox]
envlist =
lint
py{37,38,39,310,311,py3}
py{py3, 311, 310, 39, 38, 37}
minversion = 1.9
[testenv]
deps =
cffi
numpy
extras =
tests
commands =
@ -17,16 +15,15 @@ commands =
{envpython} -m pip install --global-option="build_ext" --global-option="--inplace" .
{envpython} selftest.py
{envpython} -m pytest -W always {posargs}
deps =
cffi
numpy
allowlist_externals = make
[testenv:lint]
passenv =
PRE_COMMIT_COLOR
skip_install = true
deps =
check-manifest
pre-commit
commands =
pre-commit run --all-files --show-diff-on-failure
check-manifest
deps =
pre-commit
check-manifest
skip_install = true
passenv = PRE_COMMIT_COLOR

View File

@ -152,9 +152,9 @@ deps = {
"libs": [r"*.lib"],
},
"xz": {
"url": SF_PROJECTS + "/lzmautils/files/xz-5.2.8.tar.gz/download",
"filename": "xz-5.2.8.tar.gz",
"dir": "xz-5.2.8",
"url": SF_PROJECTS + "/lzmautils/files/xz-5.4.0.tar.gz/download",
"filename": "xz-5.4.0.tar.gz",
"dir": "xz-5.4.0",
"license": "COPYING",
"patch": {
r"src\liblzma\api\lzma.h": {
@ -228,9 +228,9 @@ deps = {
# "bins": [r"libtiff\*.dll"],
},
"libpng": {
"url": SF_PROJECTS + "/libpng/files/libpng16/1.6.38/lpng1638.zip/download",
"filename": "lpng1638.zip",
"dir": "lpng1638",
"url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download",
"filename": "lpng1639.zip",
"dir": "lpng1639",
"license": "LICENSE",
"build": [
# lint: do not inline
@ -355,9 +355,9 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/5.3.1.zip",
"filename": "harfbuzz-5.3.1.zip",
"dir": "harfbuzz-5.3.1",
"url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip",
"filename": "harfbuzz-6.0.0.zip",
"dir": "harfbuzz-6.0.0",
"license": "COPYING",
"build": [
cmd_set("CXXFLAGS", "-d2FH4-"),