Merge branch 'main' into convert_mode

This commit is contained in:
Andrew Murray 2025-03-31 08:32:11 +11:00
commit 5ae44e4438
161 changed files with 2222 additions and 895 deletions

View File

@ -20,7 +20,7 @@ fi
set -e
if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev

View File

@ -1 +1 @@
cibuildwheel==2.22.0
cibuildwheel==2.23.2

View File

@ -1,4 +1,4 @@
mypy==1.14.1
mypy==1.15.0
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython

View File

@ -16,6 +16,6 @@
}
],
"schedule": [
"on the 3rd day of the month"
"* * 3 * *"
]
}

View File

@ -35,6 +35,10 @@ jobs:
matrix:
os: ["ubuntu-latest"]
docker: [
# Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-24.04-noble-ppc64le,
ubuntu-24.04-noble-s390x,
# Then run the remainder
alpine,
amazon-2-amd64,
amazon-2023-amd64,
@ -52,13 +56,9 @@ jobs:
dockerTag: [main]
include:
- docker: "ubuntu-24.04-noble-ppc64le"
os: "ubuntu-22.04"
qemu-arch: "ppc64le"
dockerTag: main
- docker: "ubuntu-24.04-noble-s390x"
os: "ubuntu-22.04"
qemu-arch: "s390x"
dockerTag: main
- docker: "ubuntu-24.04-noble-arm64v8"
os: "ubuntu-24.04-arm"
dockerTag: main
@ -75,8 +75,9 @@ jobs:
- name: Set up QEMU
if: "matrix.qemu-arch"
run: |
docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }}
uses: docker/setup-qemu-action@v3
with:
platforms: ${{ matrix.qemu-arch }}
- name: Docker pull
run: |

View File

@ -60,6 +60,7 @@ jobs:
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-ghostscript \
mingw-w64-x86_64-lcms2 \
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libjpeg-turbo \
mingw-w64-x86_64-libraqm \
mingw-w64-x86_64-libtiff \

View File

@ -94,8 +94,8 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.4.0 --no-progress
echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH
choco install ghostscript --version=10.5.0 --no-progress
echo "C:\Program Files\gs\gs10.05.0\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images

View File

@ -70,7 +70,7 @@ jobs:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: Quansight-Labs/setup-python@v5
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true

View File

@ -38,13 +38,17 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.2.0
LIBPNG_VERSION=1.6.46
HARFBUZZ_VERSION=11.0.0
LIBPNG_VERSION=1.6.47
JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.4
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
if [[ $MB_ML_VER == 2014 ]]; then
XZ_VERSION=5.6.4
else
XZ_VERSION=5.8.0
fi
TIFF_VERSION=4.7.0
LCMS2_VERSION=2.17
ZLIB_NG_VERSION=2.2.4
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8

View File

@ -63,7 +63,7 @@ jobs:
- name: "macOS 10.15 x86_64"
os: macos-13
cibw_arch: x86_64
build: "pp310*"
build: "pp3*"
macosx_deployment_target: "10.15"
- name: "macOS arm64"
os: macos-latest

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.4
rev: v0.9.9
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
@ -11,7 +11,7 @@ repos:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.8.2
rev: 1.8.3
hooks:
- id: bandit
args: [--severity-level=high]
@ -50,14 +50,14 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.31.1
rev: 0.31.2
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.3.0
rev: v1.4.1
hooks:
- id: zizmor
@ -67,7 +67,7 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.5.0
rev: v2.5.1
hooks:
- id: pyproject-fmt

View File

@ -9,6 +9,6 @@ from PIL import Image
def test_j2k_overflow(tmp_path: Path) -> None:
im = Image.new("RGBA", (1024, 131584))
target = str(tmp_path / "temp.jpc")
target = tmp_path / "temp.jpc"
with pytest.raises(OSError):
im.save(target)

View File

@ -32,7 +32,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy
def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
f = str(tmp_path / "temp.png")
f = tmp_path / "temp.png"
im = Image.new("L", (xdim, ydim), 0)
im.save(f)

View File

@ -28,7 +28,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy
def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
dtype = np.uint8
a = np.zeros((xdim, ydim), dtype=dtype)
f = str(tmp_path / "temp.png")
f = tmp_path / "temp.png"
im = Image.fromarray(a, "L")
im.save(f)

View File

@ -13,6 +13,7 @@ import tempfile
from collections.abc import Sequence
from functools import lru_cache
from io import BytesIO
from pathlib import Path
from typing import Any, Callable
import pytest
@ -95,7 +96,10 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -
def assert_image_equal_tofile(
a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
a: Image.Image,
filename: str | Path,
msg: str | None = None,
mode: str | None = None,
) -> None:
with Image.open(filename) as img:
if mode:
@ -136,7 +140,7 @@ def assert_image_similar(
def assert_image_similar_tofile(
a: Image.Image,
filename: str,
filename: str | Path,
epsilon: float,
msg: str | None = None,
) -> None:

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

View File

@ -0,0 +1,260 @@
GIMP Palette
Name: fullpalette
Columns: 4
#
0 0 0 Index 0
1 1 1 Index 1
2 2 2 Index 2
3 3 3 Index 3
4 4 4 Index 4
5 5 5 Index 5
6 6 6 Index 6
7 7 7 Index 7
8 8 8 Index 8
9 9 9 Index 9
10 10 10 Index 10
11 11 11 Index 11
12 12 12 Index 12
13 13 13 Index 13
14 14 14 Index 14
15 15 15 Index 15
16 16 16 Index 16
17 17 17 Index 17
18 18 18 Index 18
19 19 19 Index 19
20 20 20 Index 20
21 21 21 Index 21
22 22 22 Index 22
23 23 23 Index 23
24 24 24 Index 24
25 25 25 Index 25
26 26 26 Index 26
27 27 27 Index 27
28 28 28 Index 28
29 29 29 Index 29
30 30 30 Index 30
31 31 31 Index 31
32 32 32 Index 32
33 33 33 Index 33
34 34 34 Index 34
35 35 35 Index 35
36 36 36 Index 36
37 37 37 Index 37
38 38 38 Index 38
39 39 39 Index 39
40 40 40 Index 40
41 41 41 Index 41
42 42 42 Index 42
43 43 43 Index 43
44 44 44 Index 44
45 45 45 Index 45
46 46 46 Index 46
47 47 47 Index 47
48 48 48 Index 48
49 49 49 Index 49
50 50 50 Index 50
51 51 51 Index 51
52 52 52 Index 52
53 53 53 Index 53
54 54 54 Index 54
55 55 55 Index 55
56 56 56 Index 56
57 57 57 Index 57
58 58 58 Index 58
59 59 59 Index 59
60 60 60 Index 60
61 61 61 Index 61
62 62 62 Index 62
63 63 63 Index 63
64 64 64 Index 64
65 65 65 Index 65
66 66 66 Index 66
67 67 67 Index 67
68 68 68 Index 68
69 69 69 Index 69
70 70 70 Index 70
71 71 71 Index 71
72 72 72 Index 72
73 73 73 Index 73
74 74 74 Index 74
75 75 75 Index 75
76 76 76 Index 76
77 77 77 Index 77
78 78 78 Index 78
79 79 79 Index 79
80 80 80 Index 80
81 81 81 Index 81
82 82 82 Index 82
83 83 83 Index 83
84 84 84 Index 84
85 85 85 Index 85
86 86 86 Index 86
87 87 87 Index 87
88 88 88 Index 88
89 89 89 Index 89
90 90 90 Index 90
91 91 91 Index 91
92 92 92 Index 92
93 93 93 Index 93
94 94 94 Index 94
95 95 95 Index 95
96 96 96 Index 96
97 97 97 Index 97
98 98 98 Index 98
99 99 99 Index 99
100 100 100 Index 100
101 101 101 Index 101
102 102 102 Index 102
103 103 103 Index 103
104 104 104 Index 104
105 105 105 Index 105
106 106 106 Index 106
107 107 107 Index 107
108 108 108 Index 108
109 109 109 Index 109
110 110 110 Index 110
111 111 111 Index 111
112 112 112 Index 112
113 113 113 Index 113
114 114 114 Index 114
115 115 115 Index 115
116 116 116 Index 116
117 117 117 Index 117
118 118 118 Index 118
119 119 119 Index 119
120 120 120 Index 120
121 121 121 Index 121
122 122 122 Index 122
123 123 123 Index 123
124 124 124 Index 124
125 125 125 Index 125
126 126 126 Index 126
127 127 127 Index 127
128 128 128 Index 128
129 129 129 Index 129
130 130 130 Index 130
131 131 131 Index 131
132 132 132 Index 132
133 133 133 Index 133
134 134 134 Index 134
135 135 135 Index 135
136 136 136 Index 136
137 137 137 Index 137
138 138 138 Index 138
139 139 139 Index 139
140 140 140 Index 140
141 141 141 Index 141
142 142 142 Index 142
143 143 143 Index 143
144 144 144 Index 144
145 145 145 Index 145
146 146 146 Index 146
147 147 147 Index 147
148 148 148 Index 148
149 149 149 Index 149
150 150 150 Index 150
151 151 151 Index 151
152 152 152 Index 152
153 153 153 Index 153
154 154 154 Index 154
155 155 155 Index 155
156 156 156 Index 156
157 157 157 Index 157
158 158 158 Index 158
159 159 159 Index 159
160 160 160 Index 160
161 161 161 Index 161
162 162 162 Index 162
163 163 163 Index 163
164 164 164 Index 164
165 165 165 Index 165
166 166 166 Index 166
167 167 167 Index 167
168 168 168 Index 168
169 169 169 Index 169
170 170 170 Index 170
171 171 171 Index 171
172 172 172 Index 172
173 173 173 Index 173
174 174 174 Index 174
175 175 175 Index 175
176 176 176 Index 176
177 177 177 Index 177
178 178 178 Index 178
179 179 179 Index 179
180 180 180 Index 180
181 181 181 Index 181
182 182 182 Index 182
183 183 183 Index 183
184 184 184 Index 184
185 185 185 Index 185
186 186 186 Index 186
187 187 187 Index 187
188 188 188 Index 188
189 189 189 Index 189
190 190 190 Index 190
191 191 191 Index 191
192 192 192 Index 192
193 193 193 Index 193
194 194 194 Index 194
195 195 195 Index 195
196 196 196 Index 196
197 197 197 Index 197
198 198 198 Index 198
199 199 199 Index 199
200 200 200 Index 200
201 201 201 Index 201
202 202 202 Index 202
203 203 203 Index 203
204 204 204 Index 204
205 205 205 Index 205
206 206 206 Index 206
207 207 207 Index 207
208 208 208 Index 208
209 209 209 Index 209
210 210 210 Index 210
211 211 211 Index 211
212 212 212 Index 212
213 213 213 Index 213
214 214 214 Index 214
215 215 215 Index 215
216 216 216 Index 216
217 217 217 Index 217
218 218 218 Index 218
219 219 219 Index 219
220 220 220 Index 220
221 221 221 Index 221
222 222 222 Index 222
223 223 223 Index 223
224 224 224 Index 224
225 225 225 Index 225
226 226 226 Index 226
227 227 227 Index 227
228 228 228 Index 228
229 229 229 Index 229
230 230 230 Index 230
231 231 231 Index 231
232 232 232 Index 232
233 233 233 Index 233
234 234 234 Index 234
235 235 235 Index 235
236 236 236 Index 236
237 237 237 Index 237
238 238 238 Index 238
239 239 239 Index 239
240 240 240 Index 240
241 241 241 Index 241
242 242 242 Index 242
243 243 243 Index 243
244 244 244 Index 244
245 245 245 Index 245
246 246 246 Index 246
247 247 247 Index 247
248 248 248 Index 248
249 249 249 Index 249
250 250 250 Index 250
251 251 251 Index 251
252 252 252 Index 252
253 253 253 Index 253
254 254 254 Index 254
255 255 255 Index 255

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 533 B

View File

@ -12,6 +12,7 @@ from PIL import Image, ImageSequence, PngImagePlugin
# (referenced from https://wiki.mozilla.org/APNG_Specification)
def test_apng_basic() -> None:
with Image.open("Tests/images/apng/single_frame.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
assert im.n_frames == 1
assert im.get_format_mimetype() == "image/apng"
@ -20,6 +21,7 @@ def test_apng_basic() -> None:
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/single_frame_default.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.is_animated
assert im.n_frames == 2
assert im.get_format_mimetype() == "image/apng"
@ -34,8 +36,11 @@ def test_apng_basic() -> None:
with pytest.raises(EOFError):
im.seek(2)
# test rewind support
im.seek(0)
with pytest.raises(ValueError, match="cannot seek to frame 2"):
im._seek(2)
# test rewind support
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
assert im.getpixel((64, 32)) == (255, 0, 0, 255)
im.seek(1)
@ -49,6 +54,7 @@ def test_apng_basic() -> None:
)
def test_apng_fdat(filename: str) -> None:
with Image.open(filename) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -56,31 +62,37 @@ def test_apng_fdat(filename: str) -> None:
def test_apng_dispose() -> None:
with Image.open("Tests/images/apng/dispose_op_none.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/dispose_op_background.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
with Image.open("Tests/images/apng/dispose_op_background_final.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/dispose_op_previous.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
@ -88,21 +100,25 @@ def test_apng_dispose() -> None:
def test_apng_dispose_region() -> None:
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
with Image.open("Tests/images/apng/dispose_op_background_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 255, 255)
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -129,6 +145,7 @@ def test_apng_dispose_op_previous_frame() -> None:
# ],
# )
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
@ -142,26 +159,31 @@ def test_apng_dispose_op_background_p_mode() -> None:
def test_apng_blend() -> None:
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 2)
assert im.getpixel((64, 32)) == (0, 255, 0, 2)
with Image.open("Tests/images/apng/blend_op_over.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 97)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -175,6 +197,7 @@ def test_apng_blend_transparency() -> None:
def test_apng_chunk_order() -> None:
with Image.open("Tests/images/apng/fctl_actl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -230,24 +253,28 @@ def test_apng_num_plays() -> None:
def test_apng_mode() -> None:
with Image.open("Tests/images/apng/mode_16bit.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "RGBA"
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 128, 191)
assert im.getpixel((64, 32)) == (0, 0, 128, 191)
with Image.open("Tests/images/apng/mode_grayscale.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "L"
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == 128
assert im.getpixel((64, 32)) == 255
with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "LA"
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (128, 191)
assert im.getpixel((64, 32)) == (128, 191)
with Image.open("Tests/images/apng/mode_palette.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGB")
@ -255,6 +282,7 @@ def test_apng_mode() -> None:
assert im.getpixel((64, 32)) == (0, 255, 0)
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGBA")
@ -262,6 +290,7 @@ def test_apng_mode() -> None:
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGBA")
@ -271,25 +300,31 @@ def test_apng_mode() -> None:
def test_apng_chunk_errors() -> None:
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
im.load()
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
with Image.open("Tests/images/apng/chunk_no_fctl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1)
with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1)
with Image.open("Tests/images/apng/chunk_no_fdat.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1)
@ -297,26 +332,31 @@ def test_apng_chunk_errors() -> None:
def test_apng_syntax_errors() -> None:
with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
with pytest.raises(OSError):
im.load()
with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
im.load()
# we can handle this case gracefully
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
with pytest.raises(OSError):
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
im.load()
with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
im.load()
@ -336,16 +376,18 @@ def test_apng_syntax_errors() -> None:
def test_apng_sequence_errors(test_file: str) -> None:
with pytest.raises(SyntaxError):
with Image.open(f"Tests/images/apng/{test_file}") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
im.load()
def test_apng_save(tmp_path: Path) -> None:
with Image.open("Tests/images/apng/single_frame.png") as im:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im.save(test_file, save_all=True)
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.load()
assert not im.is_animated
assert im.n_frames == 1
@ -361,6 +403,7 @@ def test_apng_save(tmp_path: Path) -> None:
)
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.load()
assert im.is_animated
assert im.n_frames == 2
@ -372,7 +415,7 @@ def test_apng_save(tmp_path: Path) -> None:
def test_apng_save_alpha(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127))
@ -390,7 +433,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
# frames with image data spanning multiple fdAT chunks (in this case
# both the default image and first animation frame will span multiple
# data chunks)
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
with Image.open("Tests/images/old-style-jpeg-compression.png") as im:
frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))]
im.save(
@ -400,12 +443,13 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
append_images=frames,
)
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
im.load()
def test_apng_save_duration_loop(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
with Image.open("Tests/images/apng/delay.png") as im:
frames = []
durations = []
@ -442,6 +486,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
)
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 1
assert "duration" not in im.info
@ -453,6 +498,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
duration=[500, 100, 150],
)
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 2
assert im.info["duration"] == 600
@ -463,12 +509,13 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
frame.info["duration"] = 300
frame.save(test_file, save_all=True, append_images=[frame, different_frame])
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 2
assert im.info["duration"] == 600
def test_apng_save_disposal(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255))
@ -569,7 +616,7 @@ def test_apng_save_disposal(tmp_path: Path) -> None:
def test_apng_save_disposal_previous(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
size = (128, 64)
blue = Image.new("RGBA", size, (0, 0, 255, 255))
red = Image.new("RGBA", size, (255, 0, 0, 255))
@ -591,7 +638,7 @@ def test_apng_save_disposal_previous(tmp_path: Path) -> None:
def test_apng_save_blend(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255))
@ -659,7 +706,7 @@ def test_apng_save_blend(tmp_path: Path) -> None:
def test_apng_save_size(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im = Image.new("L", (100, 100))
im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))])
@ -683,7 +730,7 @@ def test_seek_after_close() -> None:
def test_different_modes_in_later_frames(
mode: str, default_image: bool, duplicate: bool, tmp_path: Path
) -> None:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im = Image.new("L", (1, 1))
im.save(
@ -697,7 +744,7 @@ def test_different_modes_in_later_frames(
def test_different_durations(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
with Image.open("Tests/images/apng/different_durations.png") as im:
for _ in range(3):

View File

@ -46,7 +46,7 @@ def test_invalid_file() -> None:
def test_save(tmp_path: Path) -> None:
f = str(tmp_path / "temp.blp")
f = tmp_path / "temp.blp"
for version in ("BLP1", "BLP2"):
im = hopper("P")
@ -56,7 +56,7 @@ def test_save(tmp_path: Path) -> None:
assert_image_equal(im.convert("RGB"), reloaded)
with Image.open("Tests/images/transparent.png") as im:
f = str(tmp_path / "temp.blp")
f = tmp_path / "temp.blp"
im.convert("P").save(f, blp_version=version)
with Image.open(f) as reloaded:

View File

@ -15,25 +15,19 @@ from .helper import (
)
def test_sanity(tmp_path: Path) -> None:
def roundtrip(im: Image.Image) -> None:
outfile = str(tmp_path / "temp.bmp")
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
def test_sanity(mode: str, tmp_path: Path) -> None:
outfile = tmp_path / "temp.bmp"
im.save(outfile, "BMP")
im = hopper(mode)
im.save(outfile, "BMP")
with Image.open(outfile) as reloaded:
reloaded.load()
assert im.mode == reloaded.mode
assert im.size == reloaded.size
assert reloaded.format == "BMP"
assert reloaded.get_format_mimetype() == "image/bmp"
roundtrip(hopper())
roundtrip(hopper("1"))
roundtrip(hopper("L"))
roundtrip(hopper("P"))
roundtrip(hopper("RGB"))
with Image.open(outfile) as reloaded:
reloaded.load()
assert im.mode == reloaded.mode
assert im.size == reloaded.size
assert reloaded.format == "BMP"
assert reloaded.get_format_mimetype() == "image/bmp"
def test_invalid_file() -> None:
@ -66,7 +60,7 @@ def test_small_palette(tmp_path: Path) -> None:
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
im.putpalette(colors)
out = str(tmp_path / "temp.bmp")
out = tmp_path / "temp.bmp"
im.save(out)
with Image.open(out) as reloaded:
@ -74,7 +68,7 @@ def test_small_palette(tmp_path: Path) -> None:
def test_save_too_large(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.bmp")
outfile = tmp_path / "temp.bmp"
with Image.new("RGB", (1, 1)) as im:
im._size = (37838, 37838)
with pytest.raises(ValueError):
@ -96,7 +90,7 @@ def test_dpi() -> None:
def test_save_bmp_with_dpi(tmp_path: Path) -> None:
# Test for #1301
# Arrange
outfile = str(tmp_path / "temp.jpg")
outfile = tmp_path / "temp.jpg"
with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["dpi"] == (95.98654816726399, 95.98654816726399)
@ -112,7 +106,7 @@ def test_save_bmp_with_dpi(tmp_path: Path) -> None:
def test_save_float_dpi(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.bmp")
outfile = tmp_path / "temp.bmp"
with Image.open("Tests/images/hopper.bmp") as im:
im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
with Image.open(outfile) as reloaded:
@ -152,7 +146,7 @@ def test_dib_header_size(header_size: int, path: str) -> None:
def test_save_dib(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.dib")
outfile = tmp_path / "temp.dib"
with Image.open("Tests/images/clipboard.dib") as im:
im.save(outfile)
@ -230,3 +224,13 @@ def test_offset() -> None:
# to exclude the palette size from the pixel data offset
with Image.open("Tests/images/pal8_offset.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp")
def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/bmp/g/rgb32.bmp") as im:
assert im.info["compression"] == BmpImagePlugin.BmpImageFile.COMPRESSIONS["RAW"]
assert im.mode == "RGB"
monkeypatch.setattr(BmpImagePlugin, "USE_RAW_ALPHA", True)
with Image.open("Tests/images/bmp/g/rgb32.bmp") as im:
assert im.mode == "RGBA"

View File

@ -43,7 +43,7 @@ def test_load() -> None:
def test_save(tmp_path: Path) -> None:
# Arrange
im = hopper()
tmpfile = str(tmp_path / "temp.bufr")
tmpfile = tmp_path / "temp.bufr"
# Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError):
@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None:
im.load()
assert handler.is_loaded()
temp_file = str(tmp_path / "temp.bufr")
temp_file = tmp_path / "temp.bufr"
im.save(temp_file)
assert handler.saved

View File

@ -69,12 +69,14 @@ def test_tell() -> None:
def test_n_frames() -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, DcxImagePlugin.DcxImageFile)
assert im.n_frames == 1
assert not im.is_animated
def test_eoferror() -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, DcxImagePlugin.DcxImageFile)
n_frames = im.n_frames
# Test seeking past the last frame

View File

@ -9,7 +9,13 @@ import pytest
from PIL import DdsImagePlugin, Image
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar,
assert_image_similar_tofile,
hopper,
)
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
@ -109,6 +115,32 @@ def test_sanity_ati1_bc4u(image_path: str) -> None:
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
def test_dx10_bc2(tmp_path: Path) -> None:
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT3) as im:
im.save(out, pixel_format="BC2")
with Image.open(out) as reloaded:
assert reloaded.format == "DDS"
assert reloaded.mode == "RGBA"
assert reloaded.size == (256, 256)
assert_image_similar(im, reloaded, 3.81)
def test_dx10_bc3(tmp_path: Path) -> None:
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT5) as im:
im.save(out, pixel_format="BC3")
with Image.open(out) as reloaded:
assert reloaded.format == "DDS"
assert reloaded.mode == "RGBA"
assert reloaded.size == (256, 256)
assert_image_similar(im, reloaded, 3.69)
@pytest.mark.parametrize(
"image_path",
(
@ -368,9 +400,9 @@ def test_not_implemented(test_file: str) -> None:
def test_save_unsupported_mode(tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds")
out = tmp_path / "temp.dds"
im = hopper("HSV")
with pytest.raises(OSError):
with pytest.raises(OSError, match="cannot write mode HSV as DDS"):
im.save(out)
@ -384,10 +416,98 @@ def test_save_unsupported_mode(tmp_path: Path) -> None:
],
)
def test_save(mode: str, test_file: str, tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds")
out = tmp_path / "temp.dds"
with Image.open(test_file) as im:
assert im.mode == mode
im.save(out)
with Image.open(out) as reloaded:
assert_image_equal(im, reloaded)
assert_image_equal_tofile(im, out)
def test_save_unsupported_pixel_format(tmp_path: Path) -> None:
out = tmp_path / "temp.dds"
im = hopper()
with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"):
im.save(out, pixel_format="UNKNOWN")
def test_save_dxt1(tmp_path: Path) -> None:
# RGB
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT1) as im:
im.convert("RGB").save(out, pixel_format="DXT1")
assert_image_similar_tofile(im, out, 1.84)
# RGBA
im_alpha = im.copy()
im_alpha.putpixel((0, 0), (0, 0, 0, 0))
im_alpha.save(out, pixel_format="DXT1")
with Image.open(out) as reloaded:
assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0)
# L
im_l = im.convert("L")
im_l.save(out, pixel_format="DXT1")
assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07)
# LA
im_alpha.convert("LA").save(out, pixel_format="DXT1")
with Image.open(out) as reloaded:
assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0)
def test_save_dxt3(tmp_path: Path) -> None:
# RGB
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT3) as im:
im_rgb = im.convert("RGB")
im_rgb.save(out, pixel_format="DXT3")
assert_image_similar_tofile(im_rgb.convert("RGBA"), out, 1.26)
# RGBA
im.save(out, pixel_format="DXT3")
assert_image_similar_tofile(im, out, 3.81)
# L
im_l = im.convert("L")
im_l.save(out, pixel_format="DXT3")
assert_image_similar_tofile(im_l.convert("RGBA"), out, 5.89)
# LA
im_la = im.convert("LA")
im_la.save(out, pixel_format="DXT3")
assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.44)
def test_save_dxt5(tmp_path: Path) -> None:
# RGB
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT1) as im:
im.convert("RGB").save(out, pixel_format="DXT5")
assert_image_similar_tofile(im, out, 1.84)
# RGBA
with Image.open(TEST_FILE_DXT5) as im_rgba:
im_rgba.save(out, pixel_format="DXT5")
assert_image_similar_tofile(im_rgba, out, 3.69)
# L
im_l = im.convert("L")
im_l.save(out, pixel_format="DXT5")
assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07)
# LA
im_la = im_rgba.convert("LA")
im_la.save(out, pixel_format="DXT5")
assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.32)
def test_save_dx10_bc5(tmp_path: Path) -> None:
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im:
im.save(out, pixel_format="BC5")
assert_image_similar_tofile(im, out, 9.56)
im = hopper("L")
with pytest.raises(OSError, match="only RGB mode can be written as BC5"):
im.save(out, pixel_format="BC5")

View File

@ -86,6 +86,8 @@ simple_eps_file_with_long_binary_data = (
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
expected_size = tuple(s * scale for s in size)
with Image.open(filename) as image:
assert isinstance(image, EpsImagePlugin.EpsImageFile)
image.load(scale=scale)
assert image.mode == "RGB"
assert image.size == expected_size
@ -227,6 +229,8 @@ def test_showpage() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_transparency() -> None:
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
assert isinstance(plot_image, EpsImagePlugin.EpsImageFile)
plot_image.load(transparency=True)
assert plot_image.mode == "RGBA"
@ -239,7 +243,7 @@ def test_transparency() -> None:
def test_file_object(tmp_path: Path) -> None:
# issue 479
with Image.open(FILE1) as image1:
with open(str(tmp_path / "temp.eps"), "wb") as fh:
with open(tmp_path / "temp.eps", "wb") as fh:
image1.save(fh, "EPS")
@ -274,7 +278,7 @@ def test_1(filename: str) -> None:
def test_image_mode_not_supported(tmp_path: Path) -> None:
im = hopper("RGBA")
tmpfile = str(tmp_path / "temp.eps")
tmpfile = tmp_path / "temp.eps"
with pytest.raises(ValueError):
im.save(tmpfile)
@ -308,6 +312,7 @@ def test_render_scale2() -> None:
# Zero bounding box
with Image.open(FILE1) as image1_scale2:
assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile)
image1_scale2.load(scale=2)
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
image1_scale2_compare = image1_scale2_compare.convert("RGB")
@ -316,6 +321,7 @@ def test_render_scale2() -> None:
# Non-zero bounding box
with Image.open(FILE2) as image2_scale2:
assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile)
image2_scale2.load(scale=2)
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
image2_scale2_compare = image2_scale2_compare.convert("RGB")

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import io
import warnings
import pytest
@ -21,6 +22,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
def test_sanity() -> None:
with Image.open(static_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
im.load()
assert im.mode == "P"
assert im.size == (128, 128)
@ -28,6 +31,8 @@ def test_sanity() -> None:
assert not im.is_animated
with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.mode == "P"
assert im.size == (320, 200)
assert im.format == "FLI"
@ -111,16 +116,19 @@ def test_palette_chunk_second() -> None:
def test_n_frames() -> None:
with Image.open(static_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.n_frames == 1
assert not im.is_animated
with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.n_frames == 384
assert im.is_animated
def test_eoferror() -> None:
with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
n_frames = im.n_frames
# Test seeking past the last frame
@ -132,6 +140,15 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)
def test_missing_frame_size() -> None:
with open(animated_test_file, "rb") as fp:
data = fp.read()
data = data[:6188]
with Image.open(io.BytesIO(data)) as im:
with pytest.raises(EOFError, match="missing frame size"):
im.seek(1)
def test_seek_tell() -> None:
with Image.open(animated_test_file) as im:
layer_number = im.tell()
@ -156,10 +173,14 @@ def test_seek_tell() -> None:
def test_seek() -> None:
with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
im.seek(50)
assert_image_equal_tofile(im, "Tests/images/a_fli.png")
with pytest.raises(ValueError, match="cannot seek to frame 52"):
im._seek(52)
@pytest.mark.parametrize(
"test_file",

View File

@ -22,10 +22,11 @@ def test_sanity() -> None:
def test_close() -> None:
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
pass
assert isinstance(im, FpxImagePlugin.FpxImageFile)
assert im.ole.fp.closed
im = Image.open("Tests/images/input_bw_one_band.fpx")
assert isinstance(im, FpxImagePlugin.FpxImageFile)
im.close()
assert im.ole.fp.closed

View File

@ -1,5 +1,8 @@
from __future__ import annotations
import io
import struct
import pytest
from PIL import FtexImagePlugin, Image
@ -23,3 +26,15 @@ def test_invalid_file() -> None:
with pytest.raises(SyntaxError):
FtexImagePlugin.FtexImageFile(invalid_file)
def test_invalid_texture() -> None:
with open("Tests/images/ftex_dxt1.ftc", "rb") as fp:
data = fp.read()
# Change texture compression format
data = data[:24] + struct.pack("<i", 2) + data[28:]
with pytest.raises(ValueError, match="Invalid texture compression format: 2"):
with Image.open(io.BytesIO(data)):
pass

View File

@ -16,7 +16,7 @@ def test_load() -> None:
with Image.open("Tests/images/gbr.gbr") as im:
px = im.load()
assert px is not None
assert im.load()[0, 0] == (0, 0, 0, 0)
assert px[0, 0] == (0, 0, 0, 0)
# Test again now that it has already been loaded once
px = im.load()

View File

@ -4,6 +4,8 @@ import pytest
from PIL import GdImageFile, UnidentifiedImageError
from .helper import assert_image_similar_tofile
TEST_GD_FILE = "Tests/images/hopper.gd"
@ -11,6 +13,7 @@ def test_sanity() -> None:
with GdImageFile.open(TEST_GD_FILE) as im:
assert im.size == (128, 128)
assert im.format == "GD"
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14)
def test_bad_mode() -> None:

View File

@ -228,7 +228,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None:
def test_full_palette_second_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("P", (1, 256))
full_palette_im = Image.new("P", (1, 256))
@ -249,7 +249,7 @@ def test_full_palette_second_frame(tmp_path: Path) -> None:
def test_roundtrip(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = hopper()
im.save(out)
with Image.open(out) as reread:
@ -258,7 +258,7 @@ def test_roundtrip(tmp_path: Path) -> None:
def test_roundtrip2(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/403
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
with Image.open(TEST_GIF) as im:
im2 = im.copy()
im2.save(out)
@ -268,7 +268,7 @@ def test_roundtrip2(tmp_path: Path) -> None:
def test_roundtrip_save_all(tmp_path: Path) -> None:
# Single frame image
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = hopper()
im.save(out, save_all=True)
with Image.open(out) as reread:
@ -276,7 +276,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
# Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im.save(out, save_all=True)
with Image.open(out) as reread:
@ -284,7 +284,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
def test_roundtrip_save_all_1(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1)
im.save(out, save_all=True, append_images=[im2])
@ -329,7 +329,7 @@ def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im:
info = im.info.copy()
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im.save(out, save_all=True)
with Image.open(out) as reread:
for header in important_headers:
@ -345,7 +345,7 @@ def test_palette_handling(tmp_path: Path) -> None:
im = im.resize((100, 100), Image.Resampling.LANCZOS)
im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
f = str(tmp_path / "temp.gif")
f = tmp_path / "temp.gif"
im2.save(f, optimize=True)
with Image.open(f) as reloaded:
@ -356,7 +356,7 @@ def test_palette_434(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/434
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out)
@ -402,6 +402,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
def test_seek() -> None:
with Image.open("Tests/images/dispose_none.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
frame_count = 0
try:
while True:
@ -410,6 +411,10 @@ def test_seek() -> None:
except EOFError:
assert frame_count == 5
img.seek(0)
with pytest.raises(ValueError, match="cannot seek to frame 2"):
img._seek(2)
def test_seek_info() -> None:
with Image.open("Tests/images/iss634.gif") as im:
@ -442,10 +447,12 @@ def test_seek_rewind() -> None:
def test_n_frames(path: str, n_frames: int) -> None:
# Test is_animated before n_frames
with Image.open(path) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.is_animated == (n_frames != 1)
# Test is_animated after n_frames
with Image.open(path) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
@ -455,6 +462,7 @@ def test_no_change() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(1)
expected = im.copy()
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == 5
assert_image_equal(im, expected)
@ -462,17 +470,20 @@ def test_no_change() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(3)
expected = im.copy()
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.is_animated
assert_image_equal(im, expected)
with Image.open("Tests/images/comment_after_only_frame.gif") as im:
expected = Image.new("P", (1, 1))
assert isinstance(im, GifImagePlugin.GifImageFile)
assert not im.is_animated
assert_image_equal(im, expected)
def test_eoferror() -> None:
with Image.open(TEST_GIF) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
n_frames = im.n_frames
# Test seeking past the last frame
@ -491,6 +502,7 @@ def test_first_frame_transparency() -> None:
def test_dispose_none() -> None:
with Image.open("Tests/images/dispose_none.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try:
while True:
img.seek(img.tell() + 1)
@ -514,6 +526,7 @@ def test_dispose_none_load_end() -> None:
def test_dispose_background() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try:
while True:
img.seek(img.tell() + 1)
@ -567,6 +580,7 @@ def test_transparent_dispose(
def test_dispose_previous() -> None:
with Image.open("Tests/images/dispose_prev.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try:
while True:
img.seek(img.tell() + 1)
@ -595,15 +609,16 @@ def test_previous_frame_loaded() -> None:
def test_save_dispose(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#111"),
Image.new("L", (100, 100), "#222"),
]
for method in range(0, 4):
for method in range(4):
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
with Image.open(out) as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
for _ in range(2):
img.seek(img.tell() + 1)
assert img.disposal_method == method
@ -617,13 +632,14 @@ def test_save_dispose(tmp_path: Path) -> None:
)
with Image.open(out) as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
for i in range(2):
img.seek(img.tell() + 1)
assert img.disposal_method == i + 1
def test_dispose2_palette(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
# Four colors: white, gray, black, red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
@ -657,7 +673,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
def test_dispose2_diff(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
# 4 frames: red/blue, red/red, blue/blue, red/blue
circles = [
@ -699,7 +715,7 @@ def test_dispose2_diff(tmp_path: Path) -> None:
def test_dispose2_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im_list = []
@ -725,7 +741,7 @@ def test_dispose2_background(tmp_path: Path) -> None:
def test_dispose2_background_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im_list = [Image.new("RGBA", (1, 20))]
@ -739,11 +755,12 @@ def test_dispose2_background_frame(tmp_path: Path) -> None:
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
with Image.open(out) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == 3
def test_dispose2_previous_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("P", (100, 100))
im.info["transparency"] = 0
@ -762,7 +779,7 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None:
def test_dispose2_without_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("P", (100, 100))
@ -777,7 +794,7 @@ def test_dispose2_without_transparency(tmp_path: Path) -> None:
def test_transparency_in_second_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0
@ -807,7 +824,7 @@ def test_no_transparency_in_second_frame() -> None:
def test_remapped_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("P", (1, 2))
im2 = im.copy()
@ -825,7 +842,7 @@ def test_remapped_transparency(tmp_path: Path) -> None:
def test_duration(tmp_path: Path) -> None:
duration = 1000
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
# Check that the argument has priority over the info settings
@ -839,7 +856,7 @@ def test_duration(tmp_path: Path) -> None:
def test_multiple_duration(tmp_path: Path) -> None:
duration_list = [1000, 2000, 3000]
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#111"),
@ -874,7 +891,7 @@ def test_multiple_duration(tmp_path: Path) -> None:
def test_roundtrip_info_duration(tmp_path: Path) -> None:
duration_list = [100, 500, 500]
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
with Image.open("Tests/images/transparent_dispose.gif") as im:
assert [
frame.info["duration"] for frame in ImageSequence.Iterator(im)
@ -889,7 +906,7 @@ def test_roundtrip_info_duration(tmp_path: Path) -> None:
def test_roundtrip_info_duration_combined(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = 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,
@ -907,7 +924,7 @@ def test_roundtrip_info_duration_combined(tmp_path: Path) -> None:
def test_identical_frames(tmp_path: Path) -> None:
duration_list = [1000, 1500, 2000, 4000]
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
@ -920,6 +937,8 @@ def test_identical_frames(tmp_path: Path) -> None:
out, save_all=True, append_images=im_list[1:], duration=duration_list
)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
# Assert that the first three frames were combined
assert reread.n_frames == 2
@ -940,7 +959,7 @@ def test_identical_frames(tmp_path: Path) -> None:
def test_identical_frames_to_single_frame(
duration: int | list[int], tmp_path: Path
) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
@ -949,6 +968,8 @@ def test_identical_frames_to_single_frame(
im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
# Assert that all frames were combined
assert reread.n_frames == 1
@ -957,7 +978,7 @@ def test_identical_frames_to_single_frame(
def test_loop_none(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
im.save(out, loop=None)
with Image.open(out) as reread:
@ -967,7 +988,7 @@ def test_loop_none(tmp_path: Path) -> None:
def test_number_of_loops(tmp_path: Path) -> None:
number_of_loops = 2
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
im.save(out, loop=number_of_loops)
with Image.open(out) as reread:
@ -983,7 +1004,7 @@ def test_number_of_loops(tmp_path: Path) -> None:
def test_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
im.info["background"] = 1
im.save(out)
@ -992,7 +1013,7 @@ def test_background(tmp_path: Path) -> None:
def test_webp_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
# Test opaque WebP background
if features.check("webp"):
@ -1010,7 +1031,7 @@ def test_comment(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0"
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
im.info["comment"] = b"Test comment text"
im.save(out)
@ -1027,7 +1048,7 @@ def test_comment(tmp_path: Path) -> None:
def test_comment_over_255(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
comment = b"Test comment text"
while len(comment) < 256:
@ -1053,7 +1074,7 @@ def test_read_multiple_comment_blocks() -> None:
def test_empty_string_comment(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
with Image.open("Tests/images/chi.gif") as im:
assert "comment" in im.info
@ -1087,7 +1108,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
assert "comment" not in im.info
# Test that a saved image keeps the comment
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
with Image.open("Tests/images/dispose_prev.gif") as im:
im.save(out, save_all=True, comment="Test")
@ -1097,7 +1118,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
def test_version(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
def assert_version_after_save(im: Image.Image, version: bytes) -> None:
im.save(out)
@ -1127,7 +1148,7 @@ def test_version(tmp_path: Path) -> None:
def test_append_images(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
# Test appending single frame images
im = Image.new("RGB", (100, 100), "#f00")
@ -1135,6 +1156,14 @@ def test_append_images(tmp_path: Path) -> None:
im.copy().save(out, save_all=True, append_images=ims)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 3
# Test append_images without save_all
im.copy().save(out, append_images=ims)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 3
# Tests appending using a generator
@ -1144,6 +1173,7 @@ def test_append_images(tmp_path: Path) -> None:
im.save(out, save_all=True, append_images=im_generator(ims))
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 3
# Tests appending single and multiple frame images
@ -1152,11 +1182,12 @@ def test_append_images(tmp_path: Path) -> None:
im.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 10
def test_append_different_size_image(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("RGB", (100, 100))
bigger_im = Image.new("RGB", (200, 200), "#f00")
@ -1183,7 +1214,7 @@ def test_transparent_optimize(tmp_path: Path) -> None:
im.frombytes(data)
im.putpalette(palette)
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im.save(out, transparency=im.getpixel((252, 0)))
with Image.open(out) as reloaded:
@ -1191,7 +1222,7 @@ def test_transparent_optimize(tmp_path: Path) -> None:
def test_removed_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("RGB", (256, 1))
for x in range(256):
@ -1206,7 +1237,7 @@ def test_removed_transparency(tmp_path: Path) -> None:
def test_rgb_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
# Single frame
im = Image.new("RGB", (1, 1))
@ -1228,7 +1259,7 @@ def test_rgb_transparency(tmp_path: Path) -> None:
def test_rgba_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = hopper("P")
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
@ -1238,25 +1269,26 @@ def test_rgba_transparency(tmp_path: Path) -> None:
assert_image_equal(hopper("P").convert("RGB"), reloaded)
def test_background_outside_palettte(tmp_path: Path) -> None:
def test_background_outside_palettte() -> None:
with Image.open("Tests/images/background_outside_palette.gif") as im:
im.seek(1)
assert im.info["background"] == 255
def test_bbox(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("RGB", (100, 100), "#fff")
ims = [Image.new("RGB", (100, 100), "#000")]
im.save(out, save_all=True, append_images=ims)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 2
def test_bbox_alpha(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im = Image.new("RGBA", (1, 2), (255, 0, 0, 255))
im.putpixel((0, 1), (255, 0, 0, 0))
@ -1264,6 +1296,7 @@ def test_bbox_alpha(tmp_path: Path) -> None:
im.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 2
@ -1275,7 +1308,7 @@ def test_palette_save_L(tmp_path: Path) -> None:
palette = im.getpalette()
assert palette is not None
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im_l.save(out, palette=bytes(palette))
with Image.open(out) as reloaded:
@ -1286,7 +1319,7 @@ def test_palette_save_P(tmp_path: Path) -> None:
im = Image.new("P", (1, 2))
im.putpixel((0, 1), 1)
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
with Image.open(out) as reloaded:
@ -1302,7 +1335,7 @@ def test_palette_save_duplicate_entries(tmp_path: Path) -> None:
im.putpalette((0, 0, 0, 0, 0, 0))
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1])
with Image.open(out) as reloaded:
@ -1317,7 +1350,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
frame.putpalette(color)
frames.append(frame)
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
frames[0].save(
out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]
)
@ -1340,7 +1373,7 @@ def test_palette_save_ImagePalette(tmp_path: Path) -> None:
im = hopper("P")
palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3)
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im.save(out, palette=palette)
with Image.open(out) as reloaded:
@ -1353,7 +1386,7 @@ def test_save_I(tmp_path: Path) -> None:
im = hopper("I")
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im.save(out)
with Image.open(out) as reloaded:
@ -1427,6 +1460,7 @@ def test_extents(
) -> None:
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
with Image.open("Tests/images/" + test_file) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.size == (100, 100)
# Check that n_frames does not change the size
@ -1449,7 +1483,7 @@ def test_missing_background() -> None:
def test_saving_rgba(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
with Image.open("Tests/images/transparent.png") as im:
im.save(out)
@ -1460,7 +1494,7 @@ def test_saving_rgba(tmp_path: Path) -> None:
@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False}))
def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
out = str(tmp_path / "temp.gif")
out = tmp_path / "temp.gif"
im1 = Image.new("P", (100, 100))
d = ImageDraw.Draw(im1)
@ -1474,4 +1508,5 @@ def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
im1.save(out, save_all=True, append_images=[im2], **params)
with Image.open(out) as reloaded:
assert isinstance(reloaded, GifImagePlugin.GifImageFile)
assert reloaded.n_frames == 2

View File

@ -1,5 +1,7 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL.GimpPaletteFile import GimpPaletteFile
@ -14,17 +16,20 @@ def test_sanity() -> None:
GimpPaletteFile(fp)
with open("Tests/images/bad_palette_file.gpl", "rb") as fp:
with pytest.raises(SyntaxError):
with pytest.raises(SyntaxError, match="bad palette file"):
GimpPaletteFile(fp)
with open("Tests/images/bad_palette_entry.gpl", "rb") as fp:
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="bad palette entry"):
GimpPaletteFile(fp)
def test_get_palette() -> None:
@pytest.mark.parametrize(
"filename, size", (("custom_gimp_palette.gpl", 8), ("full_gimp_palette.gpl", 256))
)
def test_get_palette(filename: str, size: int) -> None:
# Arrange
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
with open("Tests/images/" + filename, "rb") as fp:
palette_file = GimpPaletteFile(fp)
# Act
@ -32,3 +37,36 @@ def test_get_palette() -> None:
# Assert
assert mode == "RGB"
assert len(palette) / 3 == size
def test_frombytes() -> None:
# Test that __init__ stops reading after 260 lines
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
custom_data = fp.read()
custom_data += b"#\n" * 300 + b" 0 0 0 Index 12"
b = BytesIO(custom_data)
palette = GimpPaletteFile(b)
assert len(palette.palette) / 3 == 8
# Test that __init__ only reads 256 entries
with open("Tests/images/full_gimp_palette.gpl", "rb") as fp:
full_data = fp.read()
data = full_data.replace(b"#\n", b"") + b" 0 0 0 Index 256"
b = BytesIO(data)
palette = GimpPaletteFile(b)
assert len(palette.palette) / 3 == 256
# Test that frombytes() can read beyond that
palette = GimpPaletteFile.frombytes(data)
assert len(palette.palette) / 3 == 257
# Test that __init__ raises an error if a comment is too long
data = full_data[:-1] + b"a" * 100
b = BytesIO(data)
with pytest.raises(SyntaxError, match="bad palette file"):
palette = GimpPaletteFile(b)
# Test that frombytes() can read the data regardless
palette = GimpPaletteFile.frombytes(data)
assert len(palette.palette) / 3 == 256

View File

@ -43,7 +43,7 @@ def test_load() -> None:
def test_save(tmp_path: Path) -> None:
# Arrange
im = hopper()
tmpfile = str(tmp_path / "temp.grib")
tmpfile = tmp_path / "temp.grib"
# Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError):
@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None:
im.load()
assert handler.is_loaded()
temp_file = str(tmp_path / "temp.grib")
temp_file = tmp_path / "temp.grib"
im.save(temp_file)
assert handler.saved

View File

@ -43,7 +43,7 @@ def test_save() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
dummy_fp = BytesIO()
dummy_filename = "dummy.filename"
dummy_filename = "dummy.h5"
# Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError):
@ -81,7 +81,7 @@ def test_handler(tmp_path: Path) -> None:
im.load()
assert handler.is_loaded()
temp_file = str(tmp_path / "temp.h5")
temp_file = tmp_path / "temp.h5"
im.save(temp_file)
assert handler.saved

View File

@ -43,7 +43,7 @@ def test_load() -> None:
def test_save(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.icns")
temp_file = tmp_path / "temp.icns"
with Image.open(TEST_FILE) as im:
im.save(temp_file)
@ -60,7 +60,7 @@ def test_save(tmp_path: Path) -> None:
def test_save_append_images(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.icns")
temp_file = tmp_path / "temp.icns"
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128))
with Image.open(TEST_FILE) as im:
@ -69,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None:
assert_image_similar_tofile(im, temp_file, 1)
with Image.open(temp_file) as reread:
assert isinstance(reread, IcnsImagePlugin.IcnsImageFile)
reread.size = (16, 16)
reread.load(2)
assert_image_equal(reread, provided_im)
@ -90,6 +91,7 @@ def test_sizes() -> None:
# Check that we can load all of the sizes, and that the final pixel
# dimensions are as expected
with Image.open(TEST_FILE) as im:
assert isinstance(im, IcnsImagePlugin.IcnsImageFile)
for w, h, r in im.info["sizes"]:
wr = w * r
hr = h * r
@ -118,6 +120,7 @@ def test_older_icon() -> None:
wr = w * r
hr = h * r
with Image.open("Tests/images/pillow2.icns") as im2:
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
im2.size = (w, h)
im2.load(r)
assert im2.mode == "RGBA"
@ -135,6 +138,7 @@ def test_jp2_icon() -> None:
wr = w * r
hr = h * r
with Image.open("Tests/images/pillow3.icns") as im2:
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
im2.size = (w, h)
im2.load(r)
assert im2.mode == "RGBA"

View File

@ -41,7 +41,7 @@ def test_black_and_white() -> None:
def test_palette(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico")
temp_file = tmp_path / "temp.ico"
im = Image.new("P", (16, 16))
im.save(temp_file)
@ -77,6 +77,7 @@ def test_save_to_bytes() -> None:
# The other one
output.seek(0)
with Image.open(output) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.size = (32, 32)
assert im.mode == reloaded.mode
@ -88,12 +89,13 @@ def test_save_to_bytes() -> None:
def test_getpixel(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico")
temp_file = tmp_path / "temp.ico"
im = hopper()
im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)])
with Image.open(temp_file) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.load()
reloaded.size = (32, 32)
@ -101,8 +103,8 @@ def test_getpixel(tmp_path: Path) -> None:
def test_no_duplicates(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
temp_file = tmp_path / "temp.ico"
temp_file2 = tmp_path / "temp2.ico"
im = hopper()
sizes = [(32, 32), (64, 64)]
@ -115,8 +117,8 @@ def test_no_duplicates(tmp_path: Path) -> None:
def test_different_bit_depths(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
temp_file = tmp_path / "temp.ico"
temp_file2 = tmp_path / "temp2.ico"
im = hopper()
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
@ -132,8 +134,8 @@ def test_different_bit_depths(tmp_path: Path) -> None:
assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
# Test that only matching sizes of different bit depths are saved
temp_file3 = str(tmp_path / "temp3.ico")
temp_file4 = str(tmp_path / "temp4.ico")
temp_file3 = tmp_path / "temp3.ico"
temp_file4 = tmp_path / "temp4.ico"
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
im.save(
@ -167,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
# The other one
output.seek(0)
with Image.open(output) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.size = (32, 32)
assert "RGBA" == reloaded.mode
@ -178,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
def test_incorrect_size() -> None:
with Image.open(TEST_ICO_FILE) as im:
assert isinstance(im, IcoImagePlugin.IcoImageFile)
with pytest.raises(ValueError):
im.size = (1, 1)
@ -186,7 +190,7 @@ def test_save_256x256(tmp_path: Path) -> None:
"""Issue #2264 https://github.com/python-pillow/Pillow/issues/2264"""
# Arrange
with Image.open("Tests/images/hopper_256x256.ico") as im:
outfile = str(tmp_path / "temp_saved_hopper_256x256.ico")
outfile = tmp_path / "temp_saved_hopper_256x256.ico"
# Act
im.save(outfile)
@ -202,7 +206,7 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None:
"""
# Arrange
with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48
outfile = str(tmp_path / "temp_saved_python.ico")
outfile = tmp_path / "temp_saved_python.ico"
# Act
im.save(outfile)
@ -215,10 +219,11 @@ def test_save_append_images(tmp_path: Path) -> None:
# append_images should be used for scaled down versions of the image
im = hopper("RGBA")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0))
outfile = str(tmp_path / "temp_saved_multi_icon.ico")
outfile = tmp_path / "temp_saved_multi_icon.ico"
im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im])
with Image.open(outfile) as reread:
assert isinstance(reread, IcoImagePlugin.IcoImageFile)
assert_image_equal(reread, hopper("RGBA"))
reread.size = (32, 32)
@ -235,7 +240,7 @@ def test_unexpected_size() -> None:
def test_draw_reloaded(tmp_path: Path) -> None:
with Image.open(TEST_ICO_FILE) as im:
outfile = str(tmp_path / "temp_saved_hopper_draw.ico")
outfile = tmp_path / "temp_saved_hopper_draw.ico"
draw = ImageDraw.Draw(im)
draw.line((0, 0) + im.size, "#f00")

View File

@ -23,7 +23,7 @@ def test_sanity() -> None:
def test_name_limit(tmp_path: Path) -> None:
out = str(tmp_path / ("name_limit_test" * 7 + ".im"))
out = tmp_path / ("name_limit_test" * 7 + ".im")
with Image.open(TEST_IM) as im:
im.save(out)
assert filecmp.cmp(out, "Tests/images/hopper_long_name.im")
@ -68,12 +68,14 @@ def test_tell() -> None:
def test_n_frames() -> None:
with Image.open(TEST_IM) as im:
assert isinstance(im, ImImagePlugin.ImImageFile)
assert im.n_frames == 1
assert not im.is_animated
def test_eoferror() -> None:
with Image.open(TEST_IM) as im:
assert isinstance(im, ImImagePlugin.ImImageFile)
n_frames = im.n_frames
# Test seeking past the last frame
@ -87,7 +89,7 @@ def test_eoferror() -> None:
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
def test_roundtrip(mode: str, tmp_path: Path) -> None:
out = str(tmp_path / "temp.im")
out = tmp_path / "temp.im"
im = hopper(mode)
im.save(out)
assert_image_equal_tofile(im, out)
@ -98,7 +100,7 @@ def test_small_palette(tmp_path: Path) -> None:
colors = [0, 1, 2]
im.putpalette(colors)
out = str(tmp_path / "temp.im")
out = tmp_path / "temp.im"
im.save(out)
with Image.open(out) as reloaded:
@ -106,7 +108,7 @@ def test_small_palette(tmp_path: Path) -> None:
def test_save_unsupported_mode(tmp_path: Path) -> None:
out = str(tmp_path / "temp.im")
out = tmp_path / "temp.im"
im = hopper("HSV")
with pytest.raises(ValueError):
im.save(out)

View File

@ -83,7 +83,7 @@ class TestFileJpeg:
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
im = Image.new("RGB", size)
with pytest.raises(ValueError):
im.save(f)
@ -91,6 +91,7 @@ class TestFileJpeg:
def test_app(self) -> None:
# Test APP/COM reader (@PIL135)
with Image.open(TEST_FILE) as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00")
assert im.applist[1] == (
"COM",
@ -194,7 +195,7 @@ class TestFileJpeg:
icc_profile = im1.info["icc_profile"]
assert len(icc_profile) == 3144
# Roundtrip via physical file.
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
im1.save(f, icc_profile=icc_profile)
with Image.open(f) as im2:
assert im2.info.get("icc_profile") == icc_profile
@ -238,7 +239,7 @@ class TestFileJpeg:
# Sometimes the meta data on the icc_profile block is bigger than
# Image.MAXBLOCK or the image size.
with Image.open("Tests/images/icc_profile_big.jpg") as im:
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
icc_profile = im.info["icc_profile"]
# Should not raise OSError for image with icc larger than image size.
im.save(
@ -250,11 +251,11 @@ class TestFileJpeg:
)
with Image.open("Tests/images/flower2.jpg") as im:
f = str(tmp_path / "temp2.jpg")
f = tmp_path / "temp2.jpg"
im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955)
with Image.open("Tests/images/flower2.jpg") as im:
f = str(tmp_path / "temp3.jpg")
f = tmp_path / "temp3.jpg"
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
def test_optimize(self) -> None:
@ -268,7 +269,7 @@ class TestFileJpeg:
def test_optimize_large_buffer(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/148
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
# this requires ~ 1.5x Image.MAXBLOCK
im = Image.new("RGB", (4096, 4096), 0xFF3333)
im.save(f, format="JPEG", optimize=True)
@ -288,13 +289,13 @@ class TestFileJpeg:
assert im1_bytes >= im3_bytes
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
# this requires ~ 1.5x Image.MAXBLOCK
im = Image.new("RGB", (4096, 4096), 0xFF3333)
im.save(f, format="JPEG", progressive=True)
def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
im = self.gen_random_image((255, 255))
# this requires more bytes than pixels in the image
im.save(f, format="JPEG", progressive=True, quality=100)
@ -307,7 +308,7 @@ class TestFileJpeg:
def test_large_exif(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/148
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
im = hopper()
im.save(f, "JPEG", quality=90, exif=b"1" * 65533)
@ -316,6 +317,8 @@ class TestFileJpeg:
def test_exif_typeerror(self) -> None:
with Image.open("Tests/images/exif_typeerror.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
# Should not raise a TypeError
im._getexif()
@ -335,7 +338,7 @@ class TestFileJpeg:
assert exif[gps_index] == expected_exif_gps
# Writing
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
exif = Image.Exif()
exif[gps_index] = expected_exif_gps
hopper().save(f, exif=exif)
@ -500,20 +503,21 @@ class TestFileJpeg:
def test_mp(self) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert im._getmp() is None
def test_quality_keep(self, tmp_path: Path) -> None:
# RGB
with Image.open("Tests/images/hopper.jpg") as im:
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
im.save(f, quality="keep")
# Grayscale
with Image.open("Tests/images/hopper_gray.jpg") as im:
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
im.save(f, quality="keep")
# CMYK
with Image.open("Tests/images/pil_sample_cmyk.jpg") as im:
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
im.save(f, quality="keep")
def test_junk_jpeg_header(self) -> None:
@ -558,12 +562,14 @@ class TestFileJpeg:
with Image.open(test_file) as im:
im.save(b, "JPEG", qtables=[[n] * 64] * n)
with Image.open(b) as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert len(im.quantization) == n
reloaded = self.roundtrip(im, qtables="keep")
assert im.quantization == reloaded.quantization
assert max(reloaded.quantization[0]) <= 255
with Image.open("Tests/images/hopper.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
qtables = im.quantization
reloaded = self.roundtrip(im, qtables=qtables, subsampling=0)
assert im.quantization == reloaded.quantization
@ -663,6 +669,7 @@ class TestFileJpeg:
def test_load_16bit_qtables(self) -> None:
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert len(im.quantization) == 2
assert len(im.quantization[0]) == 64
assert max(im.quantization[0]) > 255
@ -705,6 +712,7 @@ class TestFileJpeg:
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
def test_load_djpeg(self) -> None:
with Image.open(TEST_FILE) as img:
assert isinstance(img, JpegImagePlugin.JpegImageFile)
img.load_djpeg()
assert_image_similar_tofile(img, TEST_FILE, 5)
@ -726,7 +734,7 @@ class TestFileJpeg:
def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None:
im = self.gen_random_image((512, 512))
f = str(tmp_path / "temp.jpeg")
f = tmp_path / "temp.jpeg"
im.save(f, quality=100, optimize=True)
with Image.open(f) as reloaded:
@ -773,7 +781,7 @@ class TestFileJpeg:
def test_save_tiff_with_dpi(self, tmp_path: Path) -> None:
# Arrange
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/hopper.tif") as im:
# Act
im.save(outfile, "JPEG", dpi=im.info["dpi"])
@ -784,7 +792,7 @@ class TestFileJpeg:
assert im.info["dpi"] == reloaded.info["dpi"]
def test_save_dpi_rounding(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.jpg")
outfile = tmp_path / "temp.jpg"
with Image.open("Tests/images/hopper.jpg") as im:
im.save(outfile, dpi=(72.2, 72.2))
@ -870,7 +878,7 @@ class TestFileJpeg:
exif = im.getexif()
assert exif[282] == 180
out = str(tmp_path / "out.jpg")
out = tmp_path / "out.jpg"
with warnings.catch_warnings():
warnings.simplefilter("error")
@ -920,6 +928,7 @@ class TestFileJpeg:
def test_photoshop_malformed_and_multiple(self) -> None:
with Image.open("Tests/images/app13-multiple.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert "photoshop" in im.info
assert 24 == len(im.info["photoshop"])
apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"]
@ -1016,7 +1025,7 @@ class TestFileJpeg:
assert im.getxmp() == {"xmpmeta": None}
def test_save_xmp(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
f = tmp_path / "temp.jpg"
im = hopper()
im.save(f, xmp=b"XMP test")
with Image.open(f) as reloaded:
@ -1095,6 +1104,7 @@ class TestFileJpeg:
def test_deprecation(self) -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
with pytest.warns(DeprecationWarning):
assert im.huffman_ac == {}
with pytest.warns(DeprecationWarning):
@ -1105,7 +1115,7 @@ class TestFileJpeg:
@skip_unless_feature("jpg")
class TestFileCloseW32:
def test_fd_leak(self, tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.jpg")
tmpfile = tmp_path / "temp.jpg"
with Image.open("Tests/images/hopper.jpg") as im:
im.save(tmpfile)

View File

@ -99,7 +99,7 @@ def test_bytesio(card: ImageFile.ImageFile) -> None:
def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im:
im.load()
outfile = str(tmp_path / "temp_test-card.png")
outfile = tmp_path / "temp_test-card.png"
im.save(outfile)
assert_image_similar(im, card, 1.0e-3)
@ -213,7 +213,7 @@ def test_header_errors() -> None:
def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp_layers.jp2")
outfile = tmp_path / "temp_layers.jp2"
for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
card.save(outfile, quality_layers=quality_layers)
@ -228,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None:
out.seek(0)
with Image.open(out) as im:
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
im.layers = 1
im.load()
assert_image_similar(im, card, 13)
out.seek(0)
with Image.open(out) as im:
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
im.layers = 3
im.load()
assert_image_similar(im, card, 0.4)
@ -289,7 +291,7 @@ def test_mct(card: ImageFile.ImageFile) -> None:
def test_sgnd(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.jp2")
outfile = tmp_path / "temp.jp2"
im = Image.new("L", (1, 1))
im.save(outfile)
@ -313,6 +315,18 @@ def test_rgba(ext: str) -> None:
assert im.mode == "RGBA"
def test_grayscale_four_channels() -> None:
with open("Tests/images/rgb_trns_ycbc.jp2", "rb") as fp:
data = fp.read()
# Change color space to OPJ_CLRSPC_GRAY
data = data[:76] + b"\x11" + data[77:]
with Image.open(BytesIO(data)) as im:
im.load()
assert im.mode == "RGBA"
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)

View File

@ -36,10 +36,11 @@ class LibTiffTestCase:
im.load()
im.getdata()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im._compression == "group4"
# can we write it back out, in a different form.
out = str(tmp_path / "temp.png")
out = tmp_path / "temp.png"
im.save(out)
out_bytes = io.BytesIO()
@ -123,7 +124,7 @@ class TestFileLibTiff(LibTiffTestCase):
"""Checking to see that the saved image is the same as what we wrote"""
test_file = "Tests/images/hopper_g4_500.tif"
with Image.open(test_file) as orig:
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
rot = orig.transpose(Image.Transpose.ROTATE_90)
assert rot.size == (500, 500)
rot.save(out)
@ -151,8 +152,9 @@ class TestFileLibTiff(LibTiffTestCase):
@pytest.mark.parametrize("legacy_api", (False, True))
def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None:
"""Test metadata writing through libtiff"""
f = str(tmp_path / "temp.tiff")
f = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper_g4.tif") as img:
assert isinstance(img, TiffImagePlugin.TiffImageFile)
img.save(f, tiffinfo=img.tag)
if legacy_api:
@ -170,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase):
]
with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
if legacy_api:
reloaded = loaded.tag.named()
else:
@ -212,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Exclude ones that have special meaning
# that we're already testing them
with Image.open("Tests/images/hopper_g4.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
for tag in im.tag_v2:
try:
del core_items[tag]
@ -247,7 +251,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Extra samples really doesn't make sense in this application.
del new_ifd[338]
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, tiffinfo=new_ifd)
@ -313,10 +317,11 @@ class TestFileLibTiff(LibTiffTestCase):
) -> None:
im = hopper()
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
im.save(out, tiffinfo=tiffinfo)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
for tag, value in tiffinfo.items():
reloaded_value = reloaded.tag_v2[tag]
if (
@ -347,14 +352,16 @@ class TestFileLibTiff(LibTiffTestCase):
)
def test_osubfiletype(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/g4_orientation_6.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[OSUBFILETYPE] = 1
im.save(outfile)
def test_subifd(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/g4_orientation_6.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[SUBIFD] = 10000
# Should not segfault
@ -365,17 +372,18 @@ class TestFileLibTiff(LibTiffTestCase):
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
if 700 in reloaded.tag_v2:
assert reloaded.tag_v2[700] == b"xmlpacket tag"
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
# issue #1765
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, dpi=(72, 72))
with Image.open(out) as reloaded:
@ -383,7 +391,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g3_compression(self, tmp_path: Path) -> None:
with Image.open("Tests/images/hopper_g4_500.tif") as i:
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
i.save(out, compression="group3")
with Image.open(out) as reread:
@ -400,7 +408,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert b[0] == ord(b"\xe0")
assert b[1] == ord(b"\x01")
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
# out = "temp.le.tif"
im.save(out)
with Image.open(out) as reread:
@ -420,7 +428,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert b[0] == ord(b"\x01")
assert b[1] == ord(b"\xe0")
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
im.save(out)
with Image.open(out) as reread:
assert reread.info["compression"] == im.info["compression"]
@ -430,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase):
"""Tests String data in info directory"""
test_file = "Tests/images/hopper_g4_500.tif"
with Image.open(test_file) as orig:
out = str(tmp_path / "temp.tif")
assert isinstance(orig, TiffImagePlugin.TiffImageFile)
out = tmp_path / "temp.tif"
orig.tag[269] = "temp.tif"
orig.save(out)
with Image.open(out) as reread:
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
assert "temp.tif" == reread.tag_v2[269]
assert "temp.tif" == reread.tag[269][0]
@ -457,7 +468,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_blur(self, tmp_path: Path) -> None:
# test case from irc, how to do blur on b/w image
# and save to compressed tif.
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
with Image.open("Tests/images/pport_g4.tif") as im:
im = im.convert("L")
@ -470,7 +481,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Test various tiff compressions and assert similar image content but reduced
# file sizes.
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
im.save(out)
size_raw = os.path.getsize(out)
@ -494,7 +505,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_tiff_jpeg_compression(self, tmp_path: Path) -> None:
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
im.save(out, compression="tiff_jpeg")
with Image.open(out) as reloaded:
@ -502,7 +513,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_tiff_deflate_compression(self, tmp_path: Path) -> None:
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
im.save(out, compression="tiff_deflate")
with Image.open(out) as reloaded:
@ -510,7 +521,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_quality(self, tmp_path: Path) -> None:
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
with pytest.raises(ValueError):
im.save(out, compression="tiff_lzw", quality=50)
@ -525,7 +536,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_cmyk_save(self, tmp_path: Path) -> None:
im = hopper("CMYK")
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
im.save(out, compression="tiff_adobe_deflate")
assert_image_equal_tofile(im, out)
@ -534,19 +545,20 @@ class TestFileLibTiff(LibTiffTestCase):
def test_palette_save(
self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out)
with Image.open(out) as reloaded:
# colormap/palette tag
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert len(reloaded.tag_v2[320]) == 768
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None:
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
with pytest.raises(OSError):
im.save(out, compression=compression)
@ -572,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/multipage.tiff") as im:
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.seek(0)
assert im.size == (10, 10)
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
@ -591,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase):
# issue #862
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
frames = im.n_frames
assert frames == 3
for _ in range(frames):
@ -610,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/hopper.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert not im.tag.next
im.load()
assert not im.tag.next
@ -686,25 +701,29 @@ class TestFileLibTiff(LibTiffTestCase):
def test_save_ycbcr(self, tmp_path: Path) -> None:
im = hopper("YCbCr")
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
im.save(outfile, compression="jpeg")
with Image.open(outfile) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
def test_exif_ifd(self) -> None:
out = io.BytesIO()
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[34665] == 125456
im.save(out, "TIFF")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert 34665 not in reloaded.tag_v2
im.save(out, "TIFF", tiffinfo={34665: 125456})
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456
@ -713,7 +732,7 @@ class TestFileLibTiff(LibTiffTestCase):
) -> None:
# issue 1597
with Image.open("Tests/images/rdf.tif") as im:
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
# this shouldn't crash
@ -724,7 +743,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Test TIFF with tag 297 (Page Number) having value of 0 0.
# The first number is the current page number.
# The second is the total number of pages, zero means not available.
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
# Created by printing a page in Chrome to PDF, then:
# /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif
# -dNOPAUSE /tmp/test.pdf -c quit
@ -736,7 +755,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_fd_duplication(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/1651
tmpfile = str(tmp_path / "temp.tif")
tmpfile = tmp_path / "temp.tif"
with open(tmpfile, "wb") as f:
with open("Tests/images/g4-multi.tiff", "rb") as src:
f.write(src.read())
@ -779,13 +798,14 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_profile = img.info["icc_profile"]
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
img.save(out, icc_profile=icc_profile)
with Image.open(out) as reloaded:
assert icc_profile == reloaded.info["icc_profile"]
def test_multipage_compression(self) -> None:
with Image.open("Tests/images/compression.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.seek(0)
assert im._compression == "tiff_ccitt"
assert im.size == (10, 10)
@ -802,7 +822,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None:
# Arrange
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
# Created with ImageMagick: convert hopper.jpg hopper_jpg.tif
# Contains JPEGTables (347) tag
@ -864,7 +884,7 @@ class TestFileLibTiff(LibTiffTestCase):
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
im = Image.new("F", (1, 1))
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out)
@ -1008,7 +1028,7 @@ class TestFileLibTiff(LibTiffTestCase):
@pytest.mark.parametrize("compression", (None, "jpeg"))
def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None:
im = hopper()
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
tags = {
TiffImagePlugin.TILEWIDTH: 256,
@ -1026,6 +1046,17 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/old-style-jpeg-compression.tif") as im:
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
def test_old_style_jpeg_orientation(self) -> None:
with open("Tests/images/old-style-jpeg-compression.tif", "rb") as fp:
data = fp.read()
# Set EXIF Orientation to 2
data = data[:102] + b"\x02" + data[103:]
with Image.open(io.BytesIO(data)) as im:
im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
def test_open_missing_samplesperpixel(self) -> None:
with Image.open(
"Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif"
@ -1079,6 +1110,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 274 in im.tag_v2
im.load()
@ -1140,16 +1172,14 @@ class TestFileLibTiff(LibTiffTestCase):
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
with pytest.raises(OSError) as e:
im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "decoder error -9"
with pytest.raises(OSError, match="decoder error -9"):
im.load()
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
im.save(out, compression=compression)
with Image.open(out) as im:
@ -1162,7 +1192,7 @@ class TestFileLibTiff(LibTiffTestCase):
self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
if not argument:
monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18)
@ -1178,13 +1208,13 @@ class TestFileLibTiff(LibTiffTestCase):
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
def test_save_zero(self, compression: str | None, tmp_path: Path) -> None:
im = Image.new("RGB", (0, 0))
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
with pytest.raises(SystemError):
im.save(out, compression=compression)
def test_save_many_compressed(self, tmp_path: Path) -> None:
im = hopper()
out = str(tmp_path / "temp.tif")
out = tmp_path / "temp.tif"
for _ in range(10000):
im.save(out, compression="jpeg")

View File

@ -30,11 +30,13 @@ def test_sanity() -> None:
def test_n_frames() -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, MicImagePlugin.MicImageFile)
assert im.n_frames == 1
def test_is_animated() -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, MicImagePlugin.MicImageFile)
assert not im.is_animated
@ -55,10 +57,11 @@ def test_seek() -> None:
def test_close() -> None:
with Image.open(TEST_FILE) as im:
pass
assert isinstance(im, MicImagePlugin.MicImageFile)
assert im.ole.fp.closed
im = Image.open(TEST_FILE)
assert isinstance(im, MicImagePlugin.MicImageFile)
im.close()
assert im.ole.fp.closed

View File

@ -6,7 +6,7 @@ from typing import Any
import pytest
from PIL import Image, ImageFile, MpoImagePlugin
from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin
from .helper import (
assert_image_equal,
@ -29,12 +29,17 @@ def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
@pytest.mark.parametrize("test_file", test_files)
def test_sanity(test_file: str) -> None:
with Image.open(test_file) as im:
def check(im: ImageFile.ImageFile) -> None:
im.load()
assert im.mode == "RGB"
assert im.size == (640, 480)
assert im.format == "MPO"
with Image.open(test_file) as im:
check(im)
with MpoImagePlugin.MpoImageFile(test_file) as im:
check(im)
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
@ -75,10 +80,11 @@ def test_context_manager() -> None:
def test_app(test_file: str) -> None:
# Test APP/COM reader (@PIL135)
with Image.open(test_file) as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
assert im.applist[0][0] == "APP1"
assert im.applist[1][0] == "APP2"
assert (
im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
assert im.applist[1][1].startswith(
b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
)
assert len(im.applist) == 2
@ -215,12 +221,14 @@ def test_seek(test_file: str) -> None:
def test_n_frames() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
assert im.n_frames == 2
assert im.is_animated
def test_eoferror() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
n_frames = im.n_frames
# Test seeking past the last frame
@ -234,6 +242,8 @@ def test_eoferror() -> None:
def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im)

View File

@ -15,7 +15,7 @@ YA_EXTRA_DIR = "Tests/images/msp"
def test_sanity(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.msp")
test_file = tmp_path / "temp.msp"
hopper("1").save(test_file)
@ -84,7 +84,7 @@ def test_msp_v2() -> None:
def test_cannot_save_wrong_mode(tmp_path: Path) -> None:
# Arrange
im = hopper()
filename = str(tmp_path / "temp.msp")
filename = tmp_path / "temp.msp"
# Act/Assert
with pytest.raises(OSError):

View File

@ -14,7 +14,7 @@ from .helper import assert_image_equal, hopper, magick_command
def helper_save_as_palm(tmp_path: Path, mode: str) -> None:
# Arrange
im = hopper(mode)
outfile = str(tmp_path / ("temp_" + mode + ".palm"))
outfile = tmp_path / ("temp_" + mode + ".palm")
# Act
im.save(outfile)
@ -25,7 +25,7 @@ def helper_save_as_palm(tmp_path: Path, mode: str) -> None:
def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image:
outfile = str(tmp_path / "temp.png")
outfile = tmp_path / "temp.png"
rc = subprocess.call(
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
)
@ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None:
im.save(outfile)
converted = open_with_magick(magick, tmp_path, outfile)
if mode == "P":
assert converted.mode == "P"
im = im.convert("RGB")
converted = converted.convert("RGB")
assert_image_equal(converted, im)
@ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None:
roundtrip(tmp_path, mode)
@pytest.mark.xfail(reason="Palm P image is wrong")
def test_p_mode(tmp_path: Path) -> None:
# Arrange
mode = "P"

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import io
from pathlib import Path
import pytest
@ -10,7 +11,7 @@ from .helper import assert_image_equal, hopper
def _roundtrip(tmp_path: Path, im: Image.Image) -> None:
f = str(tmp_path / "temp.pcx")
f = tmp_path / "temp.pcx"
im.save(f)
with Image.open(f) as im2:
assert im2.mode == im.mode
@ -30,12 +31,34 @@ def test_sanity(tmp_path: Path) -> None:
_roundtrip(tmp_path, im)
# Test an unsupported mode
f = str(tmp_path / "temp.pcx")
f = tmp_path / "temp.pcx"
im = hopper("RGBA")
with pytest.raises(ValueError):
im.save(f)
def test_bad_image_size() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read()
data = data[:4] + b"\xff\xff" + data[6:]
b = io.BytesIO(data)
with pytest.raises(SyntaxError, match="bad PCX image size"):
with PcxImagePlugin.PcxImageFile(b):
pass
def test_unknown_mode() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read()
data = data[:3] + b"\xff" + data[4:]
b = io.BytesIO(data)
with pytest.raises(OSError, match="unknown PCX mode"):
with Image.open(b):
pass
def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"

View File

@ -55,7 +55,7 @@ def test_save_alpha(tmp_path: Path, mode: str) -> None:
def test_p_alpha(tmp_path: Path) -> None:
# Arrange
outfile = str(tmp_path / "temp.pdf")
outfile = tmp_path / "temp.pdf"
with Image.open("Tests/images/pil123p.png") as im:
assert im.mode == "P"
assert isinstance(im.info["transparency"], bytes)
@ -80,7 +80,7 @@ def test_monochrome(tmp_path: Path) -> None:
def test_unsupported_mode(tmp_path: Path) -> None:
im = hopper("PA")
outfile = str(tmp_path / "temp_PA.pdf")
outfile = tmp_path / "temp_PA.pdf"
with pytest.raises(ValueError):
im.save(outfile)
@ -89,7 +89,7 @@ def test_unsupported_mode(tmp_path: Path) -> None:
def test_resolution(tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
outfile = tmp_path / "temp.pdf"
im.save(outfile, resolution=150)
with open(outfile, "rb") as fp:
@ -117,7 +117,7 @@ def test_resolution(tmp_path: Path) -> None:
def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
outfile = tmp_path / "temp.pdf"
im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp:
@ -144,7 +144,7 @@ def test_save_all(tmp_path: Path) -> None:
# Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
outfile = str(tmp_path / "temp.pdf")
outfile = tmp_path / "temp.pdf"
im.save(outfile, save_all=True)
assert os.path.isfile(outfile)
@ -177,7 +177,7 @@ def test_save_all(tmp_path: Path) -> None:
def test_multiframe_normal_save(tmp_path: Path) -> None:
# Test saving a multiframe image without save_all
with Image.open("Tests/images/dispose_bgnd.gif") as im:
outfile = str(tmp_path / "temp.pdf")
outfile = tmp_path / "temp.pdf"
im.save(outfile)
assert os.path.isfile(outfile)

View File

@ -68,7 +68,7 @@ def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile:
@skip_unless_feature("zlib")
class TestFilePng:
def get_chunks(self, filename: str) -> list[bytes]:
def get_chunks(self, filename: Path) -> list[bytes]:
chunks = []
with open(filename, "rb") as fp:
fp.read(8)
@ -89,7 +89,7 @@ class TestFilePng:
assert version is not None
assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version)
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
hopper("RGB").save(test_file)
@ -258,7 +258,7 @@ class TestFilePng:
# each palette entry
assert len(im.info["transparency"]) == 256
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im.save(test_file)
# check if saved image contains same transparency
@ -279,7 +279,7 @@ class TestFilePng:
assert im.info["transparency"] == 164
assert im.getpixel((31, 31)) == 164
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im.save(test_file)
# check if saved image contains same transparency
@ -302,7 +302,7 @@ class TestFilePng:
assert im.getcolors() == [(100, (0, 0, 0, 0))]
im = im.convert("P")
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im.save(test_file)
# check if saved image contains same transparency
@ -323,7 +323,7 @@ class TestFilePng:
im_rgba = im.convert("RGBA")
assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im.save(test_file)
with Image.open(test_file) as test_im:
@ -337,7 +337,7 @@ class TestFilePng:
def test_save_rgb_single_transparency(self, tmp_path: Path) -> None:
in_file = "Tests/images/caption_6_33_22.png"
with Image.open(in_file) as im:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im.save(test_file)
def test_load_verify(self) -> None:
@ -496,7 +496,7 @@ class TestFilePng:
im = hopper("P")
im.info["transparency"] = 0
f = str(tmp_path / "temp.png")
f = tmp_path / "temp.png"
im.save(f)
with Image.open(f) as im2:
@ -557,7 +557,7 @@ class TestFilePng:
def test_chunk_order(self, tmp_path: Path) -> None:
with Image.open("Tests/images/icc_profile.png") as im:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im.convert("P").save(test_file, dpi=(100, 100))
chunks = self.get_chunks(test_file)
@ -584,6 +584,7 @@ class TestFilePng:
def test_read_private_chunks(self) -> None:
with Image.open("Tests/images/exif.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.private_chunks == [(b"orNT", b"\x01")]
def test_roundtrip_private_chunk(self) -> None:
@ -606,6 +607,7 @@ class TestFilePng:
def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/hopper.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert "comment" in im.text
for k, v in {
"date:create": "2014-09-04T09:37:08+03:00",
@ -615,15 +617,19 @@ class TestFilePng:
# Raises a SyntaxError in load_end
with Image.open("Tests/images/broken_data_stream.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(OSError):
assert isinstance(im.text, dict)
# Raises an EOFError in load_end
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
# Raises a UnicodeDecodeError in load_end
with Image.open("Tests/images/truncated_image.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
# The file is truncated
with pytest.raises(OSError):
im.text
@ -669,7 +675,7 @@ class TestFilePng:
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
im = hopper("P")
out = str(tmp_path / "temp.png")
out = tmp_path / "temp.png"
im.save(out, bits=4, save_all=save_all)
with Image.open(out) as reloaded:
@ -679,8 +685,8 @@ class TestFilePng:
im = Image.new("P", (1, 1))
im.putpalette((1, 1, 1))
out = str(tmp_path / "temp.png")
im.save(str(tmp_path / "temp.png"))
out = tmp_path / "temp.png"
im.save(out)
with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 3
@ -729,11 +735,12 @@ class TestFilePng:
def test_exif_save(self, tmp_path: Path) -> None:
# Test exif is not saved from info
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
with Image.open("Tests/images/exif.png") as im:
im.save(test_file)
with Image.open(test_file) as reloaded:
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
assert reloaded._getexif() is None
# Test passing in exif
@ -749,7 +756,7 @@ class TestFilePng:
)
def test_exif_from_jpg(self, tmp_path: Path) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded:
@ -758,7 +765,7 @@ class TestFilePng:
def test_exif_argument(self, tmp_path: Path) -> None:
with Image.open(TEST_PNG_FILE) as im:
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
im.save(test_file, exif=b"exifstring")
with Image.open(test_file) as reloaded:

View File

@ -94,7 +94,7 @@ def test_16bit_pgm() -> None:
def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im:
filename = str(tmp_path / "temp.pgm")
filename = tmp_path / "temp.pgm"
im.save(filename, "PPM")
assert_image_equal_tofile(im, filename)
@ -106,7 +106,7 @@ def test_pnm(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.pnm") as im:
assert_image_similar(im, hopper(), 0.0001)
filename = str(tmp_path / "temp.pnm")
filename = tmp_path / "temp.pnm"
im.save(filename)
assert_image_equal_tofile(im, filename)
@ -117,7 +117,7 @@ def test_pfm(tmp_path: Path) -> None:
assert im.info["scale"] == 1.0
assert_image_equal(im, hopper("F"))
filename = str(tmp_path / "tmp.pfm")
filename = tmp_path / "tmp.pfm"
im.save(filename)
assert_image_equal_tofile(im, filename)
@ -128,7 +128,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None:
assert im.info["scale"] == 2.5
assert_image_equal(im, hopper("F"))
filename = str(tmp_path / "tmp.pfm")
filename = tmp_path / "tmp.pfm"
im.save(filename)
assert_image_equal_tofile(im, filename)
@ -194,8 +194,8 @@ def test_16bit_plain_pgm() -> None:
def test_plain_data_with_comment(
tmp_path: Path, header: bytes, data: bytes, comment_count: int
) -> None:
path1 = str(tmp_path / "temp1.ppm")
path2 = str(tmp_path / "temp2.ppm")
path1 = tmp_path / "temp1.ppm"
path2 = tmp_path / "temp2.ppm"
comment = b"# comment" * comment_count
with open(path1, "wb") as f1, open(path2, "wb") as f2:
f1.write(header + b"\n\n" + data)
@ -207,7 +207,7 @@ def test_plain_data_with_comment(
@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n"))
def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:
path = str(tmp_path / "temp.ppm")
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(data)
@ -218,7 +218,7 @@ def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:
@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A"))
def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:
path = str(tmp_path / "temp.ppm")
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(data)
@ -235,7 +235,7 @@ def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:
),
)
def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
path = str(tmp_path / "temp.ppm")
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(data)
@ -245,7 +245,7 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
def test_plain_ppm_value_negative(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n-1")
@ -255,7 +255,7 @@ def test_plain_ppm_value_negative(tmp_path: Path) -> None:
def test_plain_ppm_value_too_large(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n256")
@ -270,7 +270,7 @@ def test_magic() -> None:
def test_header_with_comments(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n")
@ -279,7 +279,7 @@ def test_header_with_comments(tmp_path: Path) -> None:
def test_non_integer_token(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P6\nTEST")
@ -289,29 +289,25 @@ def test_non_integer_token(tmp_path: Path) -> None:
def test_header_token_too_long(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P6\n 01234567890")
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Token too long in file header: 01234567890"):
with Image.open(path):
pass
assert str(e.value) == "Token too long in file header: 01234567890"
def test_truncated_file(tmp_path: Path) -> None:
# Test EOF in header
path = str(tmp_path / "temp.pgm")
path = tmp_path / "temp.pgm"
with open(path, "wb") as f:
f.write(b"P6")
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Reached EOF while reading header"):
with Image.open(path):
pass
assert str(e.value) == "Reached EOF while reading header"
# Test EOF for PyDecoder
fp = BytesIO(b"P5 3 1 4")
with Image.open(fp) as im:
@ -320,7 +316,7 @@ def test_truncated_file(tmp_path: Path) -> None:
def test_not_enough_image_data(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P2 1 2 255 255")
@ -331,16 +327,16 @@ def test_not_enough_image_data(tmp_path: Path) -> None:
@pytest.mark.parametrize("maxval", (b"0", b"65536"))
def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P6\n3 1 " + maxval)
with pytest.raises(ValueError) as e:
with pytest.raises(
ValueError, match="maxval must be greater than 0 and less than 65536"
):
with Image.open(path):
pass
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
def test_neg_ppm() -> None:
# Storage.c accepted negative values for xsize, ysize. the
@ -354,7 +350,7 @@ def test_neg_ppm() -> None:
def test_mimetypes(tmp_path: Path) -> None:
path = str(tmp_path / "temp.pgm")
path = tmp_path / "temp.pgm"
with open(path, "wb") as f:
f.write(b"P4\n128 128\n255")

View File

@ -59,17 +59,21 @@ def test_invalid_file() -> None:
def test_n_frames() -> None:
with Image.open("Tests/images/hopper_merged.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
assert im.n_frames == 1
assert not im.is_animated
for path in [test_file, "Tests/images/negative_layer_count.psd"]:
with Image.open(path) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
assert im.n_frames == 2
assert im.is_animated
def test_eoferror() -> None:
with Image.open(test_file) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
# PSD seek index starts at 1 rather than 0
n_frames = im.n_frames + 1
@ -119,11 +123,13 @@ def test_rgba() -> None:
def test_negative_top_left_layer() -> None:
with Image.open("Tests/images/negative_top_left_layer.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
assert im.layers[0][2] == (-50, -50, 50, 50)
def test_layer_skip() -> None:
with Image.open("Tests/images/five_channels.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
assert im.n_frames == 1
@ -175,5 +181,6 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None:
def test_layer_crashes(test_file: str) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
with pytest.raises(SyntaxError):
im.layers

View File

@ -71,31 +71,33 @@ def test_invalid_file() -> None:
SgiImagePlugin.SgiImageFile(invalid_file)
def test_write(tmp_path: Path) -> None:
def roundtrip(img: Image.Image) -> None:
out = str(tmp_path / "temp.sgi")
img.save(out, format="sgi")
def roundtrip(img: Image.Image, tmp_path: Path) -> None:
out = tmp_path / "temp.sgi"
img.save(out, format="sgi")
assert_image_equal_tofile(img, out)
out = tmp_path / "fp.sgi"
with open(out, "wb") as fp:
img.save(fp)
assert_image_equal_tofile(img, out)
out = str(tmp_path / "fp.sgi")
with open(out, "wb") as fp:
img.save(fp)
assert_image_equal_tofile(img, out)
assert not fp.closed
assert not fp.closed
for mode in ("L", "RGB", "RGBA"):
roundtrip(hopper(mode))
@pytest.mark.parametrize("mode", ("L", "RGB", "RGBA"))
def test_write(mode: str, tmp_path: Path) -> None:
roundtrip(hopper(mode), tmp_path)
# Test 1 dimension for an L mode image
roundtrip(Image.new("L", (10, 1)))
def test_write_L_mode_1_dimension(tmp_path: Path) -> None:
roundtrip(Image.new("L", (10, 1)), tmp_path)
def test_write16(tmp_path: Path) -> None:
test_file = "Tests/images/hopper16.rgb"
with Image.open(test_file) as im:
out = str(tmp_path / "temp.sgi")
out = tmp_path / "temp.sgi"
im.save(out, format="sgi", bpc=2)
assert_image_equal_tofile(im, out)
@ -103,7 +105,7 @@ def test_write16(tmp_path: Path) -> None:
def test_unsupported_mode(tmp_path: Path) -> None:
im = hopper("LA")
out = str(tmp_path / "temp.sgi")
out = tmp_path / "temp.sgi"
with pytest.raises(ValueError):
im.save(out, format="sgi")

View File

@ -51,7 +51,7 @@ def test_context_manager() -> None:
def test_save(tmp_path: Path) -> None:
# Arrange
temp = str(tmp_path / "temp.spider")
temp = tmp_path / "temp.spider"
im = hopper()
# Act
@ -96,6 +96,7 @@ def test_tell() -> None:
def test_n_frames() -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, SpiderImagePlugin.SpiderImageFile)
assert im.n_frames == 1
assert not im.is_animated

View File

@ -1,10 +1,11 @@
from __future__ import annotations
import io
import os
import pytest
from PIL import Image, SunImagePlugin
from PIL import Image, SunImagePlugin, _binary
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -33,6 +34,60 @@ def test_im1() -> None:
assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png")
def _sun_header(
depth: int = 0, file_type: int = 0, palette_length: int = 0
) -> io.BytesIO:
return io.BytesIO(
_binary.o32be(0x59A66A95)
+ b"\x00" * 8
+ _binary.o32be(depth)
+ b"\x00" * 4
+ _binary.o32be(file_type)
+ b"\x00" * 4
+ _binary.o32be(palette_length)
)
def test_unsupported_mode_bit_depth() -> None:
with pytest.raises(SyntaxError, match="Unsupported Mode/Bit Depth"):
with SunImagePlugin.SunImageFile(_sun_header()):
pass
def test_unsupported_color_palette_length() -> None:
with pytest.raises(SyntaxError, match="Unsupported Color Palette Length"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1025)):
pass
def test_unsupported_palette_type() -> None:
with pytest.raises(SyntaxError, match="Unsupported Palette Type"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1)):
pass
def test_unsupported_file_type() -> None:
with pytest.raises(SyntaxError, match="Unsupported Sun Raster file type"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, file_type=6)):
pass
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_rgbx() -> None:
with open(os.path.join(EXTRA_DIR, "32bpp.ras"), "rb") as fp:
data = fp.read()
# Set file type to 3
data = data[:20] + _binary.o32be(3) + data[24:]
with Image.open(io.BytesIO(data)) as im:
r, g, b = im.split()
im = Image.merge("RGB", (b, g, r))
assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png"))
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)

View File

@ -1,8 +1,6 @@
from __future__ import annotations
import os
from glob import glob
from itertools import product
from pathlib import Path
import pytest
@ -15,16 +13,29 @@ _TGA_DIR = os.path.join("Tests", "images", "tga")
_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common")
_MODES = ("L", "LA", "P", "RGB", "RGBA")
_ORIGINS = ("tl", "bl")
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
@pytest.mark.parametrize("mode", _MODES)
def test_sanity(mode: str, tmp_path: Path) -> None:
@pytest.mark.parametrize(
"size_mode",
(
("1x1", "L"),
("200x32", "L"),
("200x32", "LA"),
("200x32", "P"),
("200x32", "RGB"),
("200x32", "RGBA"),
),
)
@pytest.mark.parametrize("origin", _ORIGINS)
@pytest.mark.parametrize("rle", (True, False))
def test_sanity(
size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path
) -> None:
def roundtrip(original_im: Image.Image) -> None:
out = str(tmp_path / "temp.tga")
out = tmp_path / "temp.tga"
original_im.save(out, rle=rle)
with Image.open(out) as saved_im:
@ -36,36 +47,29 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
assert_image_equal(saved_im, original_im)
png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png"))
size, mode = size_mode
png_path = os.path.join(_TGA_DIR_COMMON, size + "_" + mode.lower() + ".png")
with Image.open(png_path) as reference_im:
assert reference_im.mode == mode
for png_path in png_paths:
with Image.open(png_path) as reference_im:
assert reference_im.mode == mode
path_no_ext = os.path.splitext(png_path)[0]
tga_path = "{}_{}_{}.tga".format(path_no_ext, origin, "rle" if rle else "raw")
path_no_ext = os.path.splitext(png_path)[0]
for origin, rle in product(_ORIGINS, (True, False)):
tga_path = "{}_{}_{}.tga".format(
path_no_ext, origin, "rle" if rle else "raw"
)
with Image.open(tga_path) as original_im:
assert original_im.format == "TGA"
assert original_im.get_format_mimetype() == "image/x-tga"
if rle:
assert original_im.info["compression"] == "tga_rle"
assert original_im.info["orientation"] == _ORIGIN_TO_ORIENTATION[origin]
if mode == "P":
assert original_im.getpalette() == reference_im.getpalette()
with Image.open(tga_path) as original_im:
assert original_im.format == "TGA"
assert original_im.get_format_mimetype() == "image/x-tga"
if rle:
assert original_im.info["compression"] == "tga_rle"
assert (
original_im.info["orientation"]
== _ORIGIN_TO_ORIENTATION[origin]
)
if mode == "P":
assert original_im.getpalette() == reference_im.getpalette()
assert_image_equal(original_im, reference_im)
assert_image_equal(original_im, reference_im)
roundtrip(original_im)
roundtrip(original_im)
def test_palette_depth_8(tmp_path: Path) -> None:
def test_palette_depth_8() -> None:
with pytest.raises(UnidentifiedImageError):
Image.open("Tests/images/p_8.tga")
@ -76,7 +80,7 @@ def test_palette_depth_16(tmp_path: Path) -> None:
assert im.palette.mode == "RGBA"
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
out = str(tmp_path / "temp.png")
out = tmp_path / "temp.png"
im.save(out)
with Image.open(out) as reloaded:
assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
@ -122,7 +126,7 @@ def test_cross_scan_line() -> None:
def test_save(tmp_path: Path) -> None:
test_file = "Tests/images/tga_id_field.tga"
with Image.open(test_file) as im:
out = str(tmp_path / "temp.tga")
out = tmp_path / "temp.tga"
# Save
im.save(out)
@ -141,7 +145,7 @@ def test_small_palette(tmp_path: Path) -> None:
colors = [0, 0, 0]
im.putpalette(colors)
out = str(tmp_path / "temp.tga")
out = tmp_path / "temp.tga"
im.save(out)
with Image.open(out) as reloaded:
@ -155,7 +159,7 @@ def test_missing_palette() -> None:
def test_save_wrong_mode(tmp_path: Path) -> None:
im = hopper("PA")
out = str(tmp_path / "temp.tga")
out = tmp_path / "temp.tga"
with pytest.raises(OSError):
im.save(out)
@ -172,7 +176,7 @@ def test_save_mapdepth() -> None:
def test_save_id_section(tmp_path: Path) -> None:
test_file = "Tests/images/rgb32rle.tga"
with Image.open(test_file) as im:
out = str(tmp_path / "temp.tga")
out = tmp_path / "temp.tga"
# Check there is no id section
im.save(out)
@ -202,7 +206,7 @@ def test_save_id_section(tmp_path: Path) -> None:
def test_save_orientation(tmp_path: Path) -> None:
test_file = "Tests/images/rgb32rle.tga"
out = str(tmp_path / "temp.tga")
out = tmp_path / "temp.tga"
with Image.open(test_file) as im:
assert im.info["orientation"] == -1
@ -229,7 +233,7 @@ def test_save_rle(tmp_path: Path) -> None:
with Image.open(test_file) as im:
assert im.info["compression"] == "tga_rle"
out = str(tmp_path / "temp.tga")
out = tmp_path / "temp.tga"
# Save
im.save(out)
@ -266,7 +270,7 @@ def test_save_l_transparency(tmp_path: Path) -> None:
assert im.mode == "LA"
assert im.getchannel("A").getcolors()[0][0] == num_transparent
out = str(tmp_path / "temp.tga")
out = tmp_path / "temp.tga"
im.save(out)
with Image.open(out) as test_im:

View File

@ -9,7 +9,13 @@ from types import ModuleType
import pytest
from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError
from PIL import (
Image,
ImageFile,
JpegImagePlugin,
TiffImagePlugin,
UnidentifiedImageError,
)
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
from .helper import (
@ -31,7 +37,7 @@ except ImportError:
class TestFileTiff:
def test_sanity(self, tmp_path: Path) -> None:
filename = str(tmp_path / "temp.tif")
filename = tmp_path / "temp.tif"
hopper("RGB").save(filename)
@ -112,20 +118,23 @@ class TestFileTiff:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_bigtiff_save(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
im = hopper()
im.save(outfile, big_tiff=True)
with Image.open(outfile) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2._bigtiff is True
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
with Image.open(outfile) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2._bigtiff is True
def test_seek_too_large(self) -> None:
@ -134,13 +143,14 @@ class TestFileTiff:
def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="Not allowing setting of legacy api"):
ifd.legacy_api = False
assert str(e.value) == "Not allowing setting of legacy api"
def test_xyres_tiff(self) -> None:
filename = "Tests/images/pil168.tif"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# legacy api
assert isinstance(im.tag[X_RESOLUTION][0], tuple)
assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
@ -154,6 +164,8 @@ class TestFileTiff:
def test_xyres_fallback_tiff(self) -> None:
filename = "Tests/images/compression.tif"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# v2 api
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
@ -168,6 +180,8 @@ class TestFileTiff:
def test_int_resolution(self) -> None:
filename = "Tests/images/pil168.tif"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# Try to read a file where X,Y_RESOLUTION are ints
im.tag_v2[X_RESOLUTION] = 71
im.tag_v2[Y_RESOLUTION] = 71
@ -182,11 +196,12 @@ class TestFileTiff:
with Image.open(
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
assert im.info["dpi"] == (dpi, dpi)
def test_save_float_dpi(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/hopper.tif") as im:
dpi = (72.2, 72.2)
im.save(outfile, dpi=dpi)
@ -199,6 +214,7 @@ class TestFileTiff:
with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
im.save(b, format="tiff", resolution=123.45)
with Image.open(b) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[X_RESOLUTION] == 123.45
assert im.tag_v2[Y_RESOLUTION] == 123.45
@ -214,19 +230,21 @@ class TestFileTiff:
TiffImagePlugin.PREFIXES.pop()
def test_bad_exif(self) -> None:
with Image.open("Tests/images/hopper_bad_exif.jpg") as i:
with Image.open("Tests/images/hopper_bad_exif.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
# Should not raise struct.error.
with pytest.warns(UserWarning):
i._getexif()
im._getexif()
def test_save_rgba(self, tmp_path: Path) -> None:
im = hopper("RGBA")
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
im.save(outfile)
def test_save_unsupported_mode(self, tmp_path: Path) -> None:
im = hopper("HSV")
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
with pytest.raises(OSError):
im.save(outfile)
@ -308,11 +326,13 @@ class TestFileTiff:
)
def test_n_frames(self, path: str, n_frames: int) -> None:
with Image.open(path) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
def test_eoferror(self) -> None:
with Image.open("Tests/images/multipage-lastframe.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
n_frames = im.n_frames
# Test seeking past the last frame
@ -356,19 +376,24 @@ class TestFileTiff:
def test_frame_order(self) -> None:
# A frame can't progress to itself after reading
with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 1
# A frame can't progress to a frame that has already been read
with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 2
# Frames don't have to be in sequence
with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 3
def test___str__(self) -> None:
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# Act
ret = str(im.ifd)
@ -379,6 +404,8 @@ class TestFileTiff:
# Arrange
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# v2 interface
v2_tags = {
256: 55,
@ -418,6 +445,7 @@ class TestFileTiff:
def test__delitem__(self) -> None:
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
len_before = len(dict(im.ifd))
del im.ifd[256]
len_after = len(dict(im.ifd))
@ -450,6 +478,7 @@ class TestFileTiff:
def test_ifd_tag_type(self) -> None:
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 0x8825 in im.tag_v2
def test_exif(self, tmp_path: Path) -> None:
@ -486,14 +515,14 @@ class TestFileTiff:
assert gps[0] == b"\x03\x02\x00\x00"
assert gps[18] == "WGS-84"
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif()
check_exif(exif)
im.save(outfile, exif=exif)
outfile2 = str(tmp_path / "temp2.tif")
outfile2 = tmp_path / "temp2.tif"
with Image.open(outfile) as im:
exif = im.getexif()
check_exif(exif)
@ -505,7 +534,7 @@ class TestFileTiff:
check_exif(exif)
def test_modify_exif(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif()
exif[264] = 100
@ -534,10 +563,11 @@ class TestFileTiff:
@pytest.mark.parametrize("mode", ("1", "L"))
def test_photometric(self, mode: str, tmp_path: Path) -> None:
filename = str(tmp_path / "temp.tif")
filename = tmp_path / "temp.tif"
im = hopper(mode)
im.save(filename, tiffinfo={262: 0})
with Image.open(filename) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[262] == 0
assert_image_equal(im, reloaded)
@ -613,9 +643,11 @@ class TestFileTiff:
def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
filename = str(tmp_path / "temp.tif")
filename = tmp_path / "temp.tif"
hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72
assert im.tag[Y_RESOLUTION][0][0] == 36
@ -631,14 +663,14 @@ class TestFileTiff:
with Image.open(infile) as im:
assert im.getpixel((0, 0)) == pixel_value
tmpfile = str(tmp_path / "temp.tif")
tmpfile = tmp_path / "temp.tif"
im.save(tmpfile)
assert_image_equal_tofile(im, tmpfile)
def test_iptc(self, tmp_path: Path) -> None:
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/hopper.tif") as im:
im.load()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
@ -653,7 +685,7 @@ class TestFileTiff:
assert 33723 not in im.tag_v2
def test_rowsperstrip(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
im = hopper()
im.save(outfile, tiffinfo={278: 256})
@ -661,6 +693,18 @@ class TestFileTiff:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[278] == 256
im = hopper()
im2 = Image.new("L", (128, 128))
im2.encoderinfo = {"tiffinfo": {278: 256}}
im.save(outfile, save_all=True, append_images=[im2])
with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[278] == 128
im.seek(1)
assert im.tag_v2[278] == 256
def test_strip_raw(self) -> None:
infile = "Tests/images/tiff_strip_raw.tif"
with Image.open(infile) as im:
@ -690,9 +734,10 @@ class TestFileTiff:
def test_planar_configuration_save(self, tmp_path: Path) -> None:
infile = "Tests/images/tiff_tiled_planar_raw.tif"
with Image.open(infile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im._planar_configuration == 2
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
im.save(outfile)
with Image.open(outfile) as reloaded:
@ -707,7 +752,7 @@ class TestFileTiff:
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
im = hopper(mode)
im.save(outfile)
@ -722,6 +767,7 @@ class TestFileTiff:
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 3
# Test appending images
@ -732,6 +778,7 @@ class TestFileTiff:
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
assert reread.n_frames == 3
# Test appending using a generator
@ -743,6 +790,7 @@ class TestFileTiff:
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
assert reread.n_frames == 3
def test_fixoffsets(self) -> None:
@ -801,7 +849,7 @@ class TestFileTiff:
im.info["icc_profile"] = "Dummy value"
# Try save-load round trip to make sure both handle icc_profile.
tmpfile = str(tmp_path / "temp.tif")
tmpfile = tmp_path / "temp.tif"
im.save(tmpfile, "TIFF", compression="raw")
with Image.open(tmpfile) as reloaded:
assert b"Dummy value" == reloaded.info["icc_profile"]
@ -810,7 +858,7 @@ class TestFileTiff:
im = hopper()
assert "icc_profile" not in im.info
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
icc_profile = b"Dummy value"
im.save(outfile, icc_profile=icc_profile)
@ -821,11 +869,11 @@ class TestFileTiff:
with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["compression"] == 0
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
im.save(outfile)
def test_discard_icc_profile(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/icc_profile.png") as im:
assert "icc_profile" in im.info
@ -853,6 +901,7 @@ class TestFileTiff:
def test_get_photoshop_blocks(self) -> None:
with Image.open("Tests/images/lab.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert list(im.get_photoshop_blocks().keys()) == [
1061,
1002,
@ -878,7 +927,7 @@ class TestFileTiff:
]
def test_tiff_chunks(self, tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.tif")
tmpfile = tmp_path / "temp.tif"
im = hopper()
with open(tmpfile, "wb") as fp:
@ -900,7 +949,7 @@ class TestFileTiff:
def test_close_on_load_exclusive(self, tmp_path: Path) -> None:
# similar to test_fd_leak, but runs on unixlike os
tmpfile = str(tmp_path / "temp.tif")
tmpfile = tmp_path / "temp.tif"
with Image.open("Tests/images/uint16_1_4660.tif") as im:
im.save(tmpfile)
@ -912,7 +961,7 @@ class TestFileTiff:
assert fp.closed
def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.tif")
tmpfile = tmp_path / "temp.tif"
with Image.open("Tests/images/uint16_1_4660.tif") as im:
im.save(tmpfile)
@ -963,7 +1012,7 @@ class TestFileTiff:
@pytest.mark.skipif(not is_win32(), reason="Windows only")
class TestFileTiffW32:
def test_fd_leak(self, tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.tif")
tmpfile = tmp_path / "temp.tif"
# this is an mmaped file.
with Image.open("Tests/images/uint16_1_4660.tif") as im:

View File

@ -56,11 +56,12 @@ def test_rt_metadata(tmp_path: Path) -> None:
info[ImageDescription] = text_data
f = str(tmp_path / "temp.tif")
f = tmp_path / "temp.tif"
img.save(f, tiffinfo=info)
with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
@ -80,12 +81,14 @@ def test_rt_metadata(tmp_path: Path) -> None:
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
img.save(f, tiffinfo=info)
with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
def test_read_metadata() -> None:
with Image.open("Tests/images/hopper_g4.tif") as img:
assert isinstance(img, TiffImagePlugin.TiffImageFile)
assert {
"YResolution": IFDRational(4294967295, 113653537),
"PlanarConfiguration": 1,
@ -128,13 +131,15 @@ def test_read_metadata() -> None:
def test_write_metadata(tmp_path: Path) -> None:
"""Test metadata writing through the python code"""
with Image.open("Tests/images/hopper.tif") as img:
f = str(tmp_path / "temp.tiff")
assert isinstance(img, TiffImagePlugin.TiffImageFile)
f = tmp_path / "temp.tiff"
del img.tag[278]
img.save(f, tiffinfo=img.tag)
original = img.tag_v2.named()
with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
reloaded = loaded.tag_v2.named()
ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"]
@ -163,8 +168,9 @@ def test_write_metadata(tmp_path: Path) -> None:
def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
info = im.tag_v2
del info[278]
@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
@ -210,7 +217,7 @@ def test_no_duplicate_50741_tag() -> None:
def test_iptc(tmp_path: Path) -> None:
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper.Lab.tif") as im:
im.save(out)
@ -227,10 +234,11 @@ def test_writing_other_types_to_ascii(
info[271] = value
im = hopper()
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[271] == expected
@ -244,10 +252,11 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path)
info[700] = value
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[700] == b"\x01"
@ -263,10 +272,11 @@ def test_writing_other_types_to_undefined(
info[33723] = value
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[33723] == b"1"
@ -296,7 +306,7 @@ def test_empty_metadata() -> None:
def test_iccprofile(tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/1462
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
im.save(out)
@ -311,19 +321,20 @@ def test_iccprofile_binary() -> None:
# but probably won't be able to save it.
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2.tagtype[34675] == 1
assert im.info["icc_profile"]
def test_iccprofile_save_png(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
outfile = str(tmp_path / "temp.png")
outfile = tmp_path / "temp.png"
im.save(outfile)
def test_iccprofile_binary_save_png(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
outfile = str(tmp_path / "temp.png")
outfile = tmp_path / "temp.png"
im.save(outfile)
@ -332,10 +343,11 @@ def test_exif_div_zero(tmp_path: Path) -> None:
info = TiffImagePlugin.ImageFileDirectory_v2()
info[41988] = TiffImagePlugin.IFDRational(0, 0)
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert 0 == reloaded.tag_v2[41988].numerator
assert 0 == reloaded.tag_v2[41988].denominator
@ -351,10 +363,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert max_long == reloaded.tag_v2[41493].numerator
assert 1 == reloaded.tag_v2[41493].denominator
@ -363,10 +376,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert max_long == reloaded.tag_v2[41493].numerator
assert 1 == reloaded.tag_v2[41493].denominator
@ -381,10 +395,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert numerator == reloaded.tag_v2[37380].numerator
assert denominator == reloaded.tag_v2[37380].denominator
@ -393,10 +408,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert numerator == reloaded.tag_v2[37380].numerator
assert denominator == reloaded.tag_v2[37380].denominator
@ -406,10 +422,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
assert -1 == reloaded.tag_v2[37380].denominator
@ -420,10 +437,11 @@ def test_ifd_signed_long(tmp_path: Path) -> None:
info[37000] = -60000
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[37000] == -60000
@ -444,11 +462,13 @@ def test_empty_values() -> None:
def test_photoshop_info(tmp_path: Path) -> None:
with Image.open("Tests/images/issue_2278.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[34377]) == 70
assert isinstance(im.tag_v2[34377], bytes)
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
im.save(out)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert len(reloaded.tag_v2[34377]) == 70
assert isinstance(reloaded.tag_v2[34377], bytes)
@ -480,7 +500,7 @@ def test_tag_group_data() -> None:
def test_empty_subifd(tmp_path: Path) -> None:
out = str(tmp_path / "temp.jpg")
out = tmp_path / "temp.jpg"
im = hopper()
exif = im.getexif()

View File

@ -160,9 +160,8 @@ class TestFileWebp:
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
im = Image.new("RGB", (15000, 15000))
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="encoding error 6"):
im.save(tmp_path / "temp.webp", method=0)
assert str(e.value) == "encoding error 6"
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
@ -237,7 +236,7 @@ class TestFileWebp:
with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1))
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3))
assert difference < 5
def test_duration(self, tmp_path: Path) -> None:

View File

@ -42,7 +42,7 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
Does it have the bits we expect?
"""
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
# temp_file = "temp.webp"
pil_image = hopper("RGBA")
@ -71,7 +71,7 @@ def test_write_rgba(tmp_path: Path) -> None:
Does it have the bits we expect?
"""
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20))
pil_image.save(temp_file)
@ -104,7 +104,7 @@ def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None:
half_transparent_image.putalpha(new_alpha)
# save with transparent area preserved
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
half_transparent_image.save(temp_file, exact=True, lossless=True)
with Image.open(temp_file) as reloaded:
@ -123,7 +123,7 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
should work, and be similar to the original file.
"""
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
file_path = "Tests/images/transparent.gif"
with Image.open(file_path) as im:
im.save(temp_file)
@ -142,10 +142,10 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
def test_alpha_quality(tmp_path: Path) -> None:
with Image.open("Tests/images/transparent.png") as im:
out = str(tmp_path / "temp.webp")
out = tmp_path / "temp.webp"
im.save(out)
out_quality = str(tmp_path / "quality.webp")
out_quality = tmp_path / "quality.webp"
im.save(out_quality, alpha_quality=50)
with Image.open(out) as reloaded:
with Image.open(out_quality) as reloaded_quality:

View File

@ -6,7 +6,7 @@ from pathlib import Path
import pytest
from packaging.version import parse as parse_version
from PIL import Image, features
from PIL import GifImagePlugin, Image, WebPImagePlugin, features
from .helper import (
assert_image_equal,
@ -22,10 +22,12 @@ def test_n_frames() -> None:
"""Ensure that WebP format sets n_frames and is_animated attributes correctly."""
with Image.open("Tests/images/hopper.webp") as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 1
assert not im.is_animated
with Image.open("Tests/images/iss634.webp") as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 42
assert im.is_animated
@ -37,11 +39,13 @@ def test_write_animation_L(tmp_path: Path) -> None:
"""
with Image.open("Tests/images/iss634.gif") as orig:
assert isinstance(orig, GifImagePlugin.GifImageFile)
assert orig.n_frames > 1
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
orig.save(temp_file, save_all=True)
with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == orig.n_frames
# Compare first and last frames to the original animated GIF
@ -67,8 +71,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
are visually similar to the originals.
"""
def check(temp_file: str) -> None:
def check(temp_file: Path) -> None:
with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 2
# Compare first frame to original
@ -87,7 +92,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
with Image.open("Tests/images/anim_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2:
temp_file1 = str(tmp_path / "temp.webp")
temp_file1 = tmp_path / "temp.webp"
frame1.copy().save(
temp_file1, save_all=True, append_images=[frame2], lossless=True
)
@ -99,7 +104,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
) -> Generator[Image.Image, None, None]:
yield from ims
temp_file2 = str(tmp_path / "temp_generator.webp")
temp_file2 = tmp_path / "temp_generator.webp"
frame1.copy().save(
temp_file2,
save_all=True,
@ -116,7 +121,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
"""
durations = [0, 10, 20, 30, 40]
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
with Image.open("Tests/images/anim_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2:
frame1.save(
@ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
)
with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 5
assert im.is_animated
@ -141,7 +147,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
def test_float_duration(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
with Image.open("Tests/images/iss634.apng") as im:
assert im.info["duration"] == 70.0
@ -159,7 +165,7 @@ def test_seeking(tmp_path: Path) -> None:
"""
dur = 33
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
with Image.open("Tests/images/anim_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2:
frame1.save(
@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None:
)
with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 5
assert im.is_animated
@ -196,10 +203,10 @@ def test_alpha_quality(tmp_path: Path) -> None:
with Image.open("Tests/images/transparent.png") as im:
first_frame = Image.new("L", im.size)
out = str(tmp_path / "temp.webp")
out = tmp_path / "temp.webp"
first_frame.save(out, save_all=True, append_images=[im])
out_quality = str(tmp_path / "quality.webp")
out_quality = tmp_path / "quality.webp"
first_frame.save(
out_quality, save_all=True, append_images=[im], alpha_quality=50
)

View File

@ -13,7 +13,7 @@ RGB_MODE = "RGB"
def test_write_lossless_rgb(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
hopper(RGB_MODE).save(temp_file, lossless=True)

View File

@ -6,7 +6,7 @@ from types import ModuleType
import pytest
from PIL import Image
from PIL import Image, WebPImagePlugin
from .helper import mark_if_feature_version, skip_unless_feature
@ -40,7 +40,7 @@ def test_read_exif_metadata() -> None:
def test_read_exif_metadata_without_prefix() -> None:
with Image.open("Tests/images/flower2.webp") as im:
# Assert prefix is not present
assert im.info["exif"][:6] != b"Exif\x00\x00"
assert not im.info["exif"].startswith(b"Exif\x00\x00")
exif = im.getexif()
assert exif[305] == "Adobe Photoshop CS6 (Macintosh)"
@ -110,6 +110,7 @@ def test_read_no_exif() -> None:
test_buffer.seek(0)
with Image.open(test_buffer) as webp_image:
assert isinstance(webp_image, WebPImagePlugin.WebPImageFile)
assert not webp_image._getexif()
@ -146,7 +147,7 @@ def test_write_animated_metadata(tmp_path: Path) -> None:
exif_data = b"<exif_data>"
xmp_data = b"<xmp_data>"
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
with Image.open("Tests/images/anim_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2:
frame1.save(

View File

@ -8,7 +8,7 @@ import pytest
from PIL import Image, ImageFile, WmfImagePlugin
from .helper import assert_image_similar_tofile, hopper
from .helper import assert_image_equal_tofile, assert_image_similar_tofile, hopper
def test_load_raw() -> None:
@ -44,6 +44,15 @@ def test_load_zero_inch() -> None:
pass
def test_render() -> None:
with open("Tests/images/drawing.emf", "rb") as fp:
data = fp.read()
b = BytesIO(data[:808] + b"\x00" + data[809:])
with Image.open(b) as im:
if hasattr(Image.core, "drawwmf"):
assert_image_equal_tofile(im, "Tests/images/drawing.emf")
def test_register_handler(tmp_path: Path) -> None:
class TestHandler(ImageFile.StubHandler):
methodCalled = False
@ -59,7 +68,7 @@ def test_register_handler(tmp_path: Path) -> None:
WmfImagePlugin.register_handler(handler)
im = hopper()
tmpfile = str(tmp_path / "temp.wmf")
tmpfile = tmp_path / "temp.wmf"
im.save(tmpfile)
assert handler.methodCalled
@ -80,6 +89,7 @@ def test_load_float_dpi() -> None:
def test_load_set_dpi() -> None:
with Image.open("Tests/images/drawing.wmf") as im:
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
assert im.size == (82, 82)
if hasattr(Image.core, "drawwmf"):
@ -88,11 +98,27 @@ def test_load_set_dpi() -> None:
assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1)
with Image.open("Tests/images/drawing.emf") as im:
assert im.size == (1625, 1625)
if not hasattr(Image.core, "drawwmf"):
return
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
im.load(im.info["dpi"])
assert im.size == (1625, 1625)
with Image.open("Tests/images/drawing.emf") as im:
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
im.load((72, 144))
assert im.size == (82, 164)
assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref_72_144.png")
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
def test_save(ext: str, tmp_path: Path) -> None:
im = hopper()
tmpfile = str(tmp_path / ("temp" + ext))
tmpfile = tmp_path / ("temp" + ext)
with pytest.raises(OSError):
im.save(tmpfile)

View File

@ -73,7 +73,7 @@ def test_invalid_file() -> None:
def test_save_wrong_mode(tmp_path: Path) -> None:
im = hopper()
out = str(tmp_path / "temp.xbm")
out = tmp_path / "temp.xbm"
with pytest.raises(OSError):
im.save(out)
@ -81,7 +81,7 @@ def test_save_wrong_mode(tmp_path: Path) -> None:
def test_hotspot(tmp_path: Path) -> None:
im = hopper("1")
out = str(tmp_path / "temp.xbm")
out = tmp_path / "temp.xbm"
hotspot = (0, 7)
im.save(out, hotspot=hotspot)

View File

@ -30,6 +30,7 @@ def test_invalid_file() -> None:
def test_load_read() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
assert isinstance(im, XpmImagePlugin.XpmImageFile)
dummy_bytes = 1
# Act

View File

@ -1,5 +1,7 @@
from __future__ import annotations
import io
import pytest
from PIL import BdfFontFile, FontFile
@ -8,13 +10,20 @@ filename = "Tests/images/courB08.bdf"
def test_sanity() -> None:
with open(filename, "rb") as test_file:
font = BdfFontFile.BdfFontFile(test_file)
with open(filename, "rb") as fp:
font = BdfFontFile.BdfFontFile(fp)
assert isinstance(font, FontFile.FontFile)
assert len([_f for _f in font.glyph if _f]) == 190
def test_zero_width_chars() -> None:
with open(filename, "rb") as fp:
data = fp.read()
data = data[:2650] + b"\x00\x00" + data[2652:]
BdfFontFile.BdfFontFile(io.BytesIO(data))
def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):

View File

@ -4,7 +4,20 @@ from pathlib import Path
import pytest
from PIL import FontFile
from PIL import FontFile, Image
def test_compile() -> None:
font = FontFile.FontFile()
font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0)))
font.compile()
assert font.ysize == 1
font.ysize = 2
font.compile()
# Assert that compiling again did not change anything
assert font.ysize == 2
def test_save(tmp_path: Path) -> None:

View File

@ -66,21 +66,20 @@ class TestImage:
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode: str) -> None:
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="unrecognized image mode"):
Image.new(mode, (1, 1))
assert str(e.value) == "unrecognized image mode"
def test_exception_inheritance(self) -> None:
assert issubclass(UnidentifiedImageError, OSError)
def test_sanity(self) -> None:
im = Image.new("L", (100, 100))
assert repr(im)[:45] == "<PIL.Image.Image image mode=L size=100x100 at"
assert repr(im).startswith("<PIL.Image.Image image mode=L size=100x100 at")
assert im.mode == "L"
assert im.size == (100, 100)
im = Image.new("RGB", (100, 100))
assert repr(im)[:45] == "<PIL.Image.Image image mode=RGB size=100x100 "
assert repr(im).startswith("<PIL.Image.Image image mode=RGB size=100x100 ")
assert im.mode == "RGB"
assert im.size == (100, 100)
@ -177,6 +176,13 @@ class TestImage:
with Image.open(io.StringIO()): # type: ignore[arg-type]
pass
def test_string(self, tmp_path: Path) -> None:
out = str(tmp_path / "temp.png")
im = hopper()
im.save(out)
with Image.open(out) as reloaded:
assert_image_equal(im, reloaded)
def test_pathlib(self, tmp_path: Path) -> None:
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
assert im.mode == "P"
@ -189,14 +195,13 @@ class TestImage:
for ext in (".jpg", ".jp2"):
if ext == ".jp2" and not features.check_codec("jpg_2000"):
pytest.skip("jpg_2000 not available")
temp_file = str(tmp_path / ("temp." + ext))
im.save(Path(temp_file))
im.save(tmp_path / ("temp." + ext))
def test_fp_name(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.jpg")
temp_file = tmp_path / "temp.jpg"
class FP(io.BytesIO):
name: str
name: Path
if sys.version_info >= (3, 12):
from collections.abc import Buffer
@ -226,10 +231,10 @@ class TestImage:
assert_image_similar(im, reloaded, 20)
def test_unknown_extension(self, tmp_path: Path) -> None:
im = hopper()
temp_file = str(tmp_path / "temp.unknown")
with pytest.raises(ValueError):
im.save(temp_file)
temp_file = tmp_path / "temp.unknown"
with hopper() as im:
with pytest.raises(ValueError):
im.save(temp_file)
def test_internals(self) -> None:
im = Image.new("L", (100, 100))
@ -247,7 +252,7 @@ class TestImage:
reason="Test requires opening an mmaped file for writing",
)
def test_readonly_save(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.bmp")
temp_file = tmp_path / "temp.bmp"
shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file)
with Image.open(temp_file) as im:
@ -801,7 +806,7 @@ class TestImage:
# https://github.com/python-pillow/Pillow/issues/835
# Arrange
test_file = "Tests/images/hopper.png"
temp_file = str(tmp_path / "temp.jpg")
temp_file = tmp_path / "temp.jpg"
# Act/Assert
with Image.open(test_file) as im:
@ -811,7 +816,7 @@ class TestImage:
im.save(temp_file)
def test_no_new_file_on_error(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.jpg")
temp_file = tmp_path / "temp.jpg"
im = Image.new("RGB", (0, 0))
with pytest.raises(ValueError):
@ -878,7 +883,7 @@ class TestImage:
assert exif[296] == 2
assert exif[11] == "gThumb 3.0.1"
out = str(tmp_path / "temp.jpg")
out = tmp_path / "temp.jpg"
exif[258] = 8
del exif[274]
del exif[282]
@ -900,7 +905,7 @@ class TestImage:
assert exif[274] == 1
assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)"
out = str(tmp_path / "temp.jpg")
out = tmp_path / "temp.jpg"
exif[258] = 8
del exif[306]
exif[274] = 455
@ -919,7 +924,7 @@ class TestImage:
exif = im.getexif()
assert exif == {}
out = str(tmp_path / "temp.webp")
out = tmp_path / "temp.webp"
exif[258] = 8
exif[40963] = 455
exif[305] = "Pillow test"
@ -941,7 +946,7 @@ class TestImage:
exif = im.getexif()
assert exif == {274: 1}
out = str(tmp_path / "temp.png")
out = tmp_path / "temp.png"
exif[258] = 8
del exif[274]
exif[40963] = 455

View File

@ -118,7 +118,7 @@ def test_trns_p(tmp_path: Path) -> None:
im = hopper("P")
im.info["transparency"] = 0
f = str(tmp_path / "temp.png")
f = tmp_path / "temp.png"
im_l = im.convert("L")
assert im_l.info["transparency"] == 0
@ -154,7 +154,7 @@ def test_trns_l(tmp_path: Path) -> None:
im = hopper("L")
im.info["transparency"] = 128
f = str(tmp_path / "temp.png")
f = tmp_path / "temp.png"
im_la = im.convert("LA")
assert "transparency" not in im_la.info
@ -177,7 +177,7 @@ def test_trns_RGB(tmp_path: Path) -> None:
im = hopper("RGB")
im.info["transparency"] = im.getpixel((0, 0))
f = str(tmp_path / "temp.png")
f = tmp_path / "temp.png"
im_l = im.convert("L")
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone

View File

@ -171,7 +171,7 @@ class TestImagingCoreResize:
# platforms. So if a future Pillow change requires that the test file
# be updated, that is okay.
im = hopper().resize((64, 64))
temp_file = str(tmp_path / "temp.gif")
temp_file = tmp_path / "temp.gif"
im.save(temp_file)
with Image.open(temp_file) as reloaded:

View File

@ -45,9 +45,9 @@ def test_split_merge(mode: str) -> None:
def test_split_open(tmp_path: Path) -> None:
if features.check("zlib"):
test_file = str(tmp_path / "temp.png")
test_file = tmp_path / "temp.png"
else:
test_file = str(tmp_path / "temp.pcx")
test_file = tmp_path / "temp.pcx"
def split_open(mode: str) -> int:
hopper(mode).save(test_file)

View File

@ -39,6 +39,8 @@ BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X
POINTS = (
((10, 10), (20, 40), (30, 30)),
[(10, 10), (20, 40), (30, 30)],
([10, 10], [20, 40], [30, 30]),
[[10, 10], [20, 40], [30, 30]],
(10, 10, 20, 40, 30, 30),
[10, 10, 20, 40, 30, 30],
)
@ -46,6 +48,8 @@ POINTS = (
KITE_POINTS = (
((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)),
[(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)],
([10, 50], [70, 10], [90, 50], [70, 90], [10, 50]),
[[10, 50], [70, 10], [90, 50], [70, 90], [10, 50]],
)
@ -448,7 +452,6 @@ def test_shape1() -> None:
x3, y3 = 95, 5
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -470,7 +473,6 @@ def test_shape2() -> None:
x3, y3 = 5, 95
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -489,7 +491,6 @@ def test_transform() -> None:
draw = ImageDraw.Draw(im)
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.line(0, 0)
s.transform((0, 0, 0, 0, 0, 0))
@ -1047,8 +1048,8 @@ def create_base_image_draw(
background2: tuple[int, int, int] = GRAY,
) -> tuple[Image.Image, ImageDraw.ImageDraw]:
img = Image.new(mode, size, background1)
for x in range(0, size[0]):
for y in range(0, size[1]):
for x in range(size[0]):
for y in range(size[1]):
if (x + y) % 2 == 0:
img.putpixel((x, y), background2)
return img, ImageDraw.Draw(img)
@ -1526,7 +1527,6 @@ def test_same_color_outline(bbox: Coords) -> None:
x2, y2 = 95, 50
x3, y3 = 95, 5
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -1630,7 +1630,7 @@ def test_compute_regular_polygon_vertices(
0,
ValueError,
"bounding_circle should contain 2D coordinates "
"and a radius (e.g. (x, y, r) or ((x, y), r) )",
r"and a radius \(e.g. \(x, y, r\) or \(\(x, y\), r\) \)",
),
(
3,
@ -1644,7 +1644,7 @@ def test_compute_regular_polygon_vertices(
((50, 50, 50), 25),
0,
ValueError,
"bounding_circle centre should contain 2D coordinates (e.g. (x, y))",
r"bounding_circle centre should contain 2D coordinates \(e.g. \(x, y\)\)",
),
(
3,
@ -1669,9 +1669,8 @@ def test_compute_regular_polygon_vertices_input_error_handling(
expected_error: type[Exception],
error_message: str,
) -> None:
with pytest.raises(expected_error) as e:
with pytest.raises(expected_error, match=error_message):
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
assert str(e.value) == error_message
def test_continuous_horizontal_edges_polygon() -> None:
@ -1705,7 +1704,7 @@ def test_discontiguous_corners_polygon() -> None:
BLACK,
)
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
assert_image_similar_tofile(img, expected, 1)
assert_image_equal_tofile(img, expected)
def test_polygon2() -> None:

View File

@ -131,6 +131,26 @@ class TestImageFile:
assert_image_equal(im1, im2)
def test_tile_size(self) -> None:
with open("Tests/images/hopper.tif", "rb") as im_fp:
data = im_fp.read()
reads = []
class FP(BytesIO):
def read(self, size: int | None = None) -> bytes:
reads.append(size)
return super().read(size)
fp = FP(data)
with Image.open(fp) as im:
assert len(im.tile) == 7
im.load()
# Despite multiple tiles, assert only one tile caused a read of maxblock size
assert reads.count(im.decodermaxblock) == 1
def test_raise_oserror(self) -> None:
with pytest.warns(DeprecationWarning):
with pytest.raises(OSError):
@ -176,9 +196,8 @@ class TestImageFile:
b"0" * ImageFile.SAFEBLOCK
) # only SAFEBLOCK bytes, so that the header is truncated
)
with pytest.raises(OSError) as e:
with pytest.raises(OSError, match="Truncated File Read"):
BmpImagePlugin.BmpImageFile(b)
assert str(e.value) == "Truncated File Read"
@skip_unless_feature("zlib")
def test_truncated_with_errors(self) -> None:

View File

@ -124,7 +124,7 @@ def test_render_equal(layout_engine: ImageFont.Layout) -> None:
def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None:
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
tempfile = tmp_path / ("temp_" + chr(128) + ".ttf")
try:
shutil.copy(FONT_PATH, tempfile)
except UnicodeEncodeError:

View File

@ -80,15 +80,12 @@ def test_lut(op: str) -> None:
def test_no_operator_loaded() -> None:
im = Image.new("L", (1, 1))
mop = ImageMorph.MorphOp()
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="No operator loaded"):
mop.apply(im)
assert str(e.value) == "No operator loaded"
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="No operator loaded"):
mop.match(im)
assert str(e.value) == "No operator loaded"
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="No operator loaded"):
mop.save_lut("")
assert str(e.value) == "No operator loaded"
# Test the named patterns
@ -238,15 +235,12 @@ def test_incorrect_mode() -> None:
im = hopper("RGB")
mop = ImageMorph.MorphOp(op_name="erosion8")
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Image mode must be L"):
mop.apply(im)
assert str(e.value) == "Image mode must be L"
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Image mode must be L"):
mop.match(im)
assert str(e.value) == "Image mode must be L"
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Image mode must be L"):
mop.get_on_pixels(im)
assert str(e.value) == "Image mode must be L"
def test_add_patterns() -> None:
@ -279,9 +273,10 @@ def test_pattern_syntax_error() -> None:
lb.add_patterns(new_patterns)
# Act / Assert
with pytest.raises(Exception) as e:
with pytest.raises(
Exception, match='Syntax error in pattern "a pattern with a syntax error"'
):
lb.build_lut()
assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"'
def test_load_invalid_mrl() -> None:
@ -290,9 +285,8 @@ def test_load_invalid_mrl() -> None:
mop = ImageMorph.MorphOp()
# Act / Assert
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="Wrong size operator file!"):
mop.load_lut(invalid_mrl)
assert str(e.value) == "Wrong size operator file!"
def test_roundtrip_mrl(tmp_path: Path) -> None:

View File

@ -448,6 +448,15 @@ def test_exif_transpose() -> None:
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_with_xmp_tuple() -> None:
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
assert im.getexif()[0x0112] == 3
im.info["xmp"] = (b"test",)
transposed_im = ImageOps.exif_transpose(im)
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_xml_without_xmp() -> None:
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
assert im.getexif()[0x0112] == 3

View File

@ -112,7 +112,7 @@ def test_make_linear_lut() -> None:
assert isinstance(lut, list)
assert len(lut) == 256
# Check values
for i in range(0, len(lut)):
for i in range(len(lut)):
assert lut[i] == i

View File

@ -68,25 +68,10 @@ def test_path_constructors(
assert list(p) == [(0.0, 1.0)]
@pytest.mark.parametrize(
"coords",
(
("a", "b"),
([0, 1],),
[[0, 1]],
([0.0, 1.0],),
[[0.0, 1.0]],
),
)
def test_invalid_path_constructors(
coords: tuple[str, str] | Sequence[Sequence[int]],
) -> None:
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
# Assert
assert str(e.value) == "incorrect coordinate type"
def test_invalid_path_constructors() -> None:
# Arrange / Act
with pytest.raises(ValueError, match="incorrect coordinate type"):
ImagePath.Path(("a", "b"))
@pytest.mark.parametrize(
@ -99,13 +84,9 @@ def test_invalid_path_constructors(
),
)
def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None:
# Act
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="wrong number of coordinates"):
ImagePath.Path(coords)
# Assert
assert str(e.value) == "wrong number of coordinates"
@pytest.mark.parametrize(
"coords, expected",

View File

@ -4,13 +4,13 @@ from pathlib import Path
import pytest
from PIL import Image, ImageSequence, TiffImagePlugin
from PIL import Image, ImageSequence, PsdImagePlugin, TiffImagePlugin
from .helper import assert_image_equal, hopper, skip_unless_feature
def test_sanity(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.im")
test_file = tmp_path / "temp.im"
im = hopper("RGB")
im.save(test_file)
@ -31,8 +31,9 @@ def test_sanity(tmp_path: Path) -> None:
def test_iterator() -> None:
with Image.open("Tests/images/multipage.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
i = ImageSequence.Iterator(im)
for index in range(0, im.n_frames):
for index in range(im.n_frames):
assert i[index] == next(i)
with pytest.raises(IndexError):
i[index + 1]
@ -42,6 +43,7 @@ def test_iterator() -> None:
def test_iterator_min_frame() -> None:
with Image.open("Tests/images/hopper.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
i = ImageSequence.Iterator(im)
for index in range(1, im.n_frames):
assert i[index] == next(i)

View File

@ -88,7 +88,7 @@ if is_win32():
def test_pointer(tmp_path: Path) -> None:
im = hopper()
(width, height) = im.size
opath = str(tmp_path / "temp.png")
opath = tmp_path / "temp.png"
imdib = ImageWin.Dib(im)
hdr = BITMAPINFOHEADER()

View File

@ -44,7 +44,7 @@ def test_basic(tmp_path: Path, mode: str) -> None:
im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(im_out) # transform
filename = str(tmp_path / "temp.im")
filename = tmp_path / "temp.im"
im_in.save(filename)
with Image.open(filename) as im_out:

View File

@ -18,7 +18,7 @@ def helper_pickle_file(
) -> None:
# Arrange
with Image.open(test_file) as im:
filename = str(tmp_path / "temp.pkl")
filename = tmp_path / "temp.pkl"
if mode:
im = im.convert(mode)
@ -65,7 +65,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
("Tests/images/itxt_chunks.png", None),
],
)
@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1))
@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_image(
tmp_path: Path, test_file: str, test_mode: str | None, protocol: int
) -> None:
@ -87,12 +87,12 @@ def test_pickle_jpeg() -> None:
def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
# Arrange
filename = str(tmp_path / "temp.pkl")
filename = tmp_path / "temp.pkl"
with Image.open("Tests/images/hopper.jpg") as im:
im = im.convert("PA")
# Act / Assert
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
im._mode = "LA"
with open(filename, "wb") as f:
pickle.dump(im, f, protocol)
@ -133,7 +133,7 @@ def helper_assert_pickled_font_images(
@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_string(protocol: int) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
@ -147,11 +147,11 @@ def test_pickle_font_string(protocol: int) -> None:
@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_file(tmp_path: Path, protocol: int) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
filename = str(tmp_path / "temp.pkl")
filename = tmp_path / "temp.pkl"
# Act: roundtrip
with open(filename, "wb") as f:

View File

@ -35,7 +35,7 @@ def test_draw_postscript(tmp_path: Path) -> None:
# https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript
# Arrange
tempfile = str(tmp_path / "temp.ps")
tempfile = tmp_path / "temp.ps"
with open(tempfile, "wb") as fp:
# Act
ps = PSDraw.PSDraw(fp)

View File

@ -35,10 +35,11 @@ class TestShellInjection:
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
def test_load_djpeg_filename(self, tmp_path: Path) -> None:
for filename in test_filenames:
src_file = str(tmp_path / filename)
src_file = tmp_path / filename
shutil.copy(TEST_JPG, src_file)
with Image.open(src_file) as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
im.load_djpeg()
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")

View File

@ -65,11 +65,12 @@ def test_ifd_rational_save(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
) -> None:
im = hopper()
out = str(tmp_path / "temp.tiff")
out = tmp_path / "temp.tiff"
res = IFDRational(301, 1)
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
im.save(out, dpi=(res, res), compression="raw")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])

View File

@ -22,7 +22,7 @@ import PIL
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "8.1"
needs_sphinx = "8.2"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -121,7 +121,7 @@ nitpicky = True
# generating warnings in “nitpicky mode”. Note that type should include the domain name
# if present. Example entries would be ('py:func', 'int') or
# ('envvar', 'LD_LIBRARY_PATH').
nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")]
nitpick_ignore = [("py:class", "_CmsProfileCompatible")]
# -- Options for HTML output ----------------------------------------------

View File

@ -285,7 +285,7 @@ Image.register_decoder("DXT5", DXT5Decoder)
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS "
return prefix.startswith(b"DDS ")
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)

View File

@ -68,7 +68,7 @@ by DirectX.
DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
.. versionadded:: 3.4.0
DXT3 images can be read in ``RGB`` mode and DX10 images can be read in
DXT3 images can be read in ``RGBA`` mode and DX10 images can be read in
``RGB`` and ``RGBA`` mode.
.. versionadded:: 6.0.0
@ -93,6 +93,12 @@ DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
in ``P`` mode.
.. versionadded:: 11.2.0
DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved::
im.save(out, pixel_format="DXT1")
DIB
^^^
@ -229,8 +235,9 @@ following options are available::
im.save(out, save_all=True, append_images=[im1, im2, ...])
**save_all**
If present and true, all frames of the image will be saved. If
not, then only the first frame of a multiframe image will be saved.
If present and true, or if ``append_images`` is not empty, all frames of
the image will be saved. Otherwise, only the first frame of a multiframe
image will be saved.
**append_images**
A list of images to append as additional frames. Each of the
@ -454,7 +461,8 @@ The :py:meth:`~PIL.Image.open` method may set the following
Raw EXIF data from the image.
**comment**
A comment about the image.
A comment about the image, from the COM marker. This is separate from the
UserComment tag that may be stored in the EXIF data.
.. versionadded:: 7.1.0
@ -716,8 +724,8 @@ Saving
When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default
only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following
option will also be available.
argument is present and true, or if ``append_images`` is not empty, all frames
will be saved.
**append_images**
A list of images to append as additional pictures. Each of the
@ -927,7 +935,8 @@ Saving
When calling :py:meth:`~PIL.Image.Image.save`, by default only a single frame PNG file
will be saved. To save an APNG file (including a single frame APNG), the ``save_all``
parameter must be set to ``True``. The following parameters can also be set:
parameter should be set to ``True`` or ``append_images`` should not be empty. The
following parameters can also be set:
**default_image**
Boolean value, specifying whether or not the base image is a default image.
@ -1156,15 +1165,14 @@ Saving
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**save_all**
If true, Pillow will save all frames of the image to a multiframe tiff document.
If true, or if ``append_images`` is not empty, Pillow will save all frames of the
image to a multiframe tiff document.
.. versionadded:: 3.4.0
**append_images**
A list of images to append as additional frames. Each of the
images in the list can be single or multiframe images. Note however, that for
correct results, all the appended images should have the same
``encoderinfo`` and ``encoderconfig`` properties.
images in the list can be single or multiframe images.
.. versionadded:: 4.2.0
@ -1308,8 +1316,8 @@ Saving sequences
When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default
only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following
options will also be available.
argument is present and true, or if ``append_images`` is not empty, all frames
will be saved, and the following options will also be available.
**append_images**
A list of images to append as additional frames. Each of the
@ -1611,15 +1619,14 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
**save_all**
If a multiframe image is used, by default, only the first image will be saved.
To save all frames, each frame to a separate page of the PDF, the ``save_all``
parameter must be present and set to ``True``.
parameter should be present and set to ``True`` or ``append_images`` should not be
empty.
.. versionadded:: 3.0.0
**append_images**
A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each
of the images in the list can be single or multiframe images. The ``save_all``
parameter must be present and set to ``True`` in conjunction with
``append_images``.
of the images in the list can be single or multiframe images.
.. versionadded:: 4.2.0

View File

@ -534,7 +534,6 @@ You can create animated GIFs with Pillow, e.g.
# Save the images as an animated GIF
images[0].save(
"animated_hopper.gif",
save_all=True,
append_images=images[1:],
duration=500, # duration of each frame in milliseconds
loop=0, # loop forever

View File

@ -54,7 +54,7 @@ true color.
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"SPAM"
return prefix.startswith(b"SPAM")
class SpamImageFile(ImageFile.ImageFile):

View File

@ -44,14 +44,14 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.7.0**
* **libfreetype** provides type related services
* **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
above uses liblcms2. Tested with **1.19** and **2.7-2.16**.
above uses liblcms2. Tested with **1.19** and **2.7-2.17**.
* **libwebp** provides the WebP format.

View File

@ -286,6 +286,14 @@ can be easily displayed in a chromaticity diagram, for example).
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: media_white_point
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
This tag specifies the media white point and is used for
generating absolute colorimetry.
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: media_white_point_temperature
:type: float | None

View File

@ -4,21 +4,12 @@
Security
========
TODO
^^^^
Undefined shift when loading compressed DDS images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
When loading some compressed DDS formats, an integer was bitshifted by 24 places to
generate the 32 bits of the lookup table. This was undefined behaviour, and has been
present since Pillow 3.4.0.
Deprecations
============
@ -36,10 +27,14 @@ an :py:class:`PIL.ImageFile.ImageFile` instance.
API Changes
===========
TODO
^^^^
``append_images`` no longer requires ``save_all``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
Previously, ``save_all`` was required to in order to use ``append_images``. Now,
``save_all`` will default to ``True`` if ``append_images`` is not empty and the format
supports saving multiple frames::
im.save("out.gif", append_images=ims)
API Additions
=============
@ -66,10 +61,10 @@ libjpeg library, and what version of MozJPEG is being used::
features.check_feature("mozjpeg") # True or False
features.version_feature("mozjpeg") # "4.1.1" for example, or None
Other Changes
=============
Saving compressed DDS images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
^^^^
Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3,
DXT5, BC2, BC3 and BC5 are supported::
TODO
im.save("out.dds", pixel_format="DXT1")

View File

@ -43,7 +43,7 @@ dynamic = [
optional-dependencies.docs = [
"furo",
"olefile",
"sphinx>=8.1",
"sphinx>=8.2",
"sphinx-copybutton",
"sphinx-inline-tabs",
"sphinxext-opengraph",
@ -121,6 +121,7 @@ lint.select = [
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PT", # flake8-pytest-style
"PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa)
@ -133,6 +134,7 @@ lint.ignore = [
"E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ','
"PIE790", # flake8-pie: unnecessary-placeholder
"PT001", # pytest-fixture-incorrect-parentheses-style
"PT007", # pytest-parametrize-values-wrong-type
"PT011", # pytest-raises-too-broad

View File

@ -68,6 +68,7 @@ _LIB_IMAGING = (
"Reduce",
"Bands",
"BcnDecode",
"BcnEncode",
"BitDecode",
"Blend",
"Chops",

View File

@ -43,7 +43,7 @@ def bdf_char(
s = f.readline()
if not s:
return None
if s[:9] == b"STARTCHAR":
if s.startswith(b"STARTCHAR"):
break
id = s[9:].strip().decode("ascii")
@ -51,7 +51,7 @@ def bdf_char(
props = {}
while True:
s = f.readline()
if not s or s[:6] == b"BITMAP":
if not s or s.startswith(b"BITMAP"):
break
i = s.find(b" ")
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
@ -60,7 +60,7 @@ def bdf_char(
bitmap = bytearray()
while True:
s = f.readline()
if not s or s[:7] == b"ENDCHAR":
if not s or s.startswith(b"ENDCHAR"):
break
bitmap += s[:-1]
@ -96,7 +96,7 @@ class BdfFontFile(FontFile.FontFile):
super().__init__()
s = fp.readline()
if s[:13] != b"STARTFONT 2.1":
if not s.startswith(b"STARTFONT 2.1"):
msg = "not a valid BDF file"
raise SyntaxError(msg)
@ -105,7 +105,7 @@ class BdfFontFile(FontFile.FontFile):
while True:
s = fp.readline()
if not s or s[:13] == b"ENDPROPERTIES":
if not s or s.startswith(b"ENDPROPERTIES"):
break
i = s.find(b" ")
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")

View File

@ -246,7 +246,7 @@ class BLPFormatError(NotImplementedError):
def _accept(prefix: bytes) -> bool:
return prefix[:4] in (b"BLP1", b"BLP2")
return prefix.startswith((b"BLP1", b"BLP2"))
class BlpImageFile(ImageFile.ImageFile):
@ -291,7 +291,7 @@ class BlpImageFile(ImageFile.ImageFile):
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
class _BLPBaseDecoder(ImageFile.PyDecoder):
class _BLPBaseDecoder(abc.ABC, ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:

View File

@ -48,9 +48,11 @@ BIT2MODE = {
32: ("RGB", "BGRX"),
}
USE_RAW_ALPHA = False
def _accept(prefix: bytes) -> bool:
return prefix[:2] == b"BM"
return prefix.startswith(b"BM")
def _dib_accept(prefix: bytes) -> bool:
@ -242,7 +244,9 @@ class BmpImageFile(ImageFile.ImageFile):
msg = "Unsupported BMP bitfields layout"
raise OSError(msg)
elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
if file_info["bits"] == 32 and (
header == 22 or USE_RAW_ALPHA # 32-bit .cur offset
):
raw_mode, self._mode = "BGRA", "RGBA"
elif file_info["compression"] in (
self.COMPRESSIONS["RLE8"],

View File

@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
return prefix.startswith((b"BUFR", b"ZCZC"))
class BufrStubImageFile(ImageFile.StubImageFile):

View File

@ -26,7 +26,7 @@ from ._binary import i32le as i32
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\0\0\2\0"
return prefix.startswith(b"\0\0\2\0")
##

View File

@ -24,6 +24,7 @@ from __future__ import annotations
from . import Image
from ._binary import i32le as i32
from ._util import DeferredError
from .PcxImagePlugin import PcxImageFile
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
@ -66,6 +67,8 @@ class DcxImageFile(PcxImageFile):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.frame = frame
self.fp = self._fp
self.fp.seek(self._offset[frame])

View File

@ -419,6 +419,14 @@ class DdsImageFile(ImageFile.ImageFile):
self._mode = "RGBA"
self.pixel_format = "BC1"
n = 1
elif dxgi_format in (DXGI_FORMAT.BC2_TYPELESS, DXGI_FORMAT.BC2_UNORM):
self._mode = "RGBA"
self.pixel_format = "BC2"
n = 2
elif dxgi_format in (DXGI_FORMAT.BC3_TYPELESS, DXGI_FORMAT.BC3_UNORM):
self._mode = "RGBA"
self.pixel_format = "BC3"
n = 3
elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM):
self._mode = "L"
self.pixel_format = "BC4"
@ -518,30 +526,68 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg)
alpha = im.mode[-1] == "A"
if im.mode[0] == "L":
pixel_flags = DDPF.LUMINANCE
rawmode = im.mode
if alpha:
rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
else:
rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
else:
pixel_flags = DDPF.RGB
rawmode = im.mode[::-1]
rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
if alpha:
r, g, b, a = im.split()
im = Image.merge("RGBA", (a, r, g, b))
if alpha:
pixel_flags |= DDPF.ALPHAPIXELS
rgba_mask.append(0xFF000000 if alpha else 0)
flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT
flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
bitcount = len(im.getbands()) * 8
pitch = (im.width * bitcount + 7) // 8
pixel_format = im.encoderinfo.get("pixel_format")
args: tuple[int] | str
if pixel_format:
codec_name = "bcn"
flags |= DDSD.LINEARSIZE
pitch = (im.width + 3) * 4
rgba_mask = [0, 0, 0, 0]
pixel_flags = DDPF.FOURCC
if pixel_format == "DXT1":
fourcc = D3DFMT.DXT1
args = (1,)
elif pixel_format == "DXT3":
fourcc = D3DFMT.DXT3
args = (2,)
elif pixel_format == "DXT5":
fourcc = D3DFMT.DXT5
args = (3,)
else:
fourcc = D3DFMT.DX10
if pixel_format == "BC2":
args = (2,)
dxgi_format = DXGI_FORMAT.BC2_TYPELESS
elif pixel_format == "BC3":
args = (3,)
dxgi_format = DXGI_FORMAT.BC3_TYPELESS
elif pixel_format == "BC5":
args = (5,)
dxgi_format = DXGI_FORMAT.BC5_TYPELESS
if im.mode != "RGB":
msg = "only RGB mode can be written as BC5"
raise OSError(msg)
else:
msg = f"cannot write pixel format {pixel_format}"
raise OSError(msg)
else:
codec_name = "raw"
flags |= DDSD.PITCH
pitch = (im.width * bitcount + 7) // 8
alpha = im.mode[-1] == "A"
if im.mode[0] == "L":
pixel_flags = DDPF.LUMINANCE
args = im.mode
if alpha:
rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
else:
rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
else:
pixel_flags = DDPF.RGB
args = im.mode[::-1]
rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
if alpha:
r, g, b, a = im.split()
im = Image.merge("RGBA", (a, r, g, b))
if alpha:
pixel_flags |= DDPF.ALPHAPIXELS
rgba_mask.append(0xFF000000 if alpha else 0)
fourcc = D3DFMT.UNKNOWN
fp.write(
o32(DDS_MAGIC)
+ struct.pack(
@ -556,15 +602,20 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
)
+ struct.pack("11I", *((0,) * 11)) # reserved
# pfsize, pfflags, fourcc, bitcount
+ struct.pack("<4I", 32, pixel_flags, 0, bitcount)
+ struct.pack("<4I", 32, pixel_flags, fourcc, bitcount)
+ struct.pack("<4I", *rgba_mask) # dwRGBABitMask
+ struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
)
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
if fourcc == D3DFMT.DX10:
fp.write(
# dxgi_format, 2D resource, misc, array size, straight alpha
struct.pack("<5I", dxgi_format, 3, 0, 0, 1)
)
ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)])
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS "
return prefix.startswith(b"DDS ")
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)

Some files were not shown because too many files have changed in this diff Show More