Merge branch 'main' into libavif-plugin

This commit is contained in:
Andrew Murray 2025-03-31 11:59:25 +11:00 committed by GitHub
commit fdc68e6e42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
100 changed files with 1233 additions and 555 deletions

View File

@ -1 +1 @@
cibuildwheel==2.23.1 cibuildwheel==2.23.2

View File

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

View File

@ -38,13 +38,18 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds # Package versions for fresh source builds
FREETYPE_VERSION=2.13.3 FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.4.0 HARFBUZZ_VERSION=11.0.0
LIBPNG_VERSION=1.6.47 LIBPNG_VERSION=1.6.47
JPEGTURBO_VERSION=3.1.0 JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3 OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.4 if [[ $MB_ML_VER == 2014 ]]; then
XZ_VERSION=5.6.4
else
XZ_VERSION=5.8.0
fi
TIFF_VERSION=4.7.0 TIFF_VERSION=4.7.0
LCMS2_VERSION=2.17 LCMS2_VERSION=2.17
ZLIB_VERSION=1.3.1
ZLIB_NG_VERSION=2.2.4 ZLIB_NG_VERSION=2.2.4
LIBWEBP_VERSION=1.5.0 LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8 BZIP2_VERSION=1.0.8
@ -146,7 +151,11 @@ function build {
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel yum remove -y zlib-devel
fi fi
build_zlib_ng if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then
build_new_zlib
else
build_zlib_ng
fi
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then if [ -n "$IS_MACOS" ]; then

View File

@ -9,6 +9,6 @@ from PIL import Image
def test_j2k_overflow(tmp_path: Path) -> None: def test_j2k_overflow(tmp_path: Path) -> None:
im = Image.new("RGBA", (1024, 131584)) im = Image.new("RGBA", (1024, 131584))
target = str(tmp_path / "temp.jpc") target = tmp_path / "temp.jpc"
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(target) 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: 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 = Image.new("L", (xdim, ydim), 0)
im.save(f) 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: def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
dtype = np.uint8 dtype = np.uint8
a = np.zeros((xdim, ydim), dtype=dtype) a = np.zeros((xdim, ydim), dtype=dtype)
f = str(tmp_path / "temp.png") f = tmp_path / "temp.png"
im = Image.fromarray(a, "L") im = Image.fromarray(a, "L")
im.save(f) im.save(f)

View File

@ -6,6 +6,8 @@ import sys
from PIL import features from PIL import features
from .helper import is_pypy
def test_wheel_modules() -> None: def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"}
@ -47,5 +49,7 @@ def test_wheel_features() -> None:
if sys.platform == "win32": if sys.platform == "win32":
expected_features.remove("xcb") expected_features.remove("xcb")
elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
expected_features.remove("zlib_ng")
assert set(features.get_supported_features()) == expected_features assert set(features.get_supported_features()) == expected_features

View File

@ -13,6 +13,7 @@ import tempfile
from collections.abc import Sequence from collections.abc import Sequence
from functools import lru_cache from functools import lru_cache
from io import BytesIO from io import BytesIO
from pathlib import Path
from typing import Any, Callable from typing import Any, Callable
import pytest 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( 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: ) -> None:
with Image.open(filename) as img: with Image.open(filename) as img:
if mode: if mode:
@ -136,7 +140,7 @@ def assert_image_similar(
def assert_image_similar_tofile( def assert_image_similar_tofile(
a: Image.Image, a: Image.Image,
filename: str, filename: str | Path,
epsilon: float, epsilon: float,
msg: str | None = None, msg: str | None = 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) # (referenced from https://wiki.mozilla.org/APNG_Specification)
def test_apng_basic() -> None: def test_apng_basic() -> None:
with Image.open("Tests/images/apng/single_frame.png") as im: with Image.open("Tests/images/apng/single_frame.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
assert im.n_frames == 1 assert im.n_frames == 1
assert im.get_format_mimetype() == "image/apng" 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) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/single_frame_default.png") as im: with Image.open("Tests/images/apng/single_frame_default.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.is_animated assert im.is_animated
assert im.n_frames == 2 assert im.n_frames == 2
assert im.get_format_mimetype() == "image/apng" assert im.get_format_mimetype() == "image/apng"
@ -52,6 +54,7 @@ def test_apng_basic() -> None:
) )
def test_apng_fdat(filename: str) -> None: def test_apng_fdat(filename: str) -> None:
with Image.open(filename) as im: with Image.open(filename) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -59,31 +62,37 @@ def test_apng_fdat(filename: str) -> None:
def test_apng_dispose() -> None: def test_apng_dispose() -> None:
with Image.open("Tests/images/apng/dispose_op_none.png") as im: with Image.open("Tests/images/apng/dispose_op_none.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (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: with Image.open("Tests/images/apng/dispose_op_background.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (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: with Image.open("Tests/images/apng/dispose_op_background_final.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (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: with Image.open("Tests/images/apng/dispose_op_previous.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (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: with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (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: with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
@ -91,21 +100,25 @@ def test_apng_dispose() -> None:
def test_apng_dispose_region() -> None: def test_apng_dispose_region() -> None:
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (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: 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) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (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: with Image.open("Tests/images/apng/dispose_op_background_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 255, 255) assert im.getpixel((0, 0)) == (0, 0, 255, 255)
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -132,6 +145,7 @@ def test_apng_dispose_op_previous_frame() -> None:
# ], # ],
# ) # )
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (255, 0, 0, 255) assert im.getpixel((0, 0)) == (255, 0, 0, 255)
@ -145,26 +159,31 @@ def test_apng_dispose_op_background_p_mode() -> None:
def test_apng_blend() -> None: def test_apng_blend() -> None:
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (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: with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (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: 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) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 2) assert im.getpixel((0, 0)) == (0, 255, 0, 2)
assert im.getpixel((64, 32)) == (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: with Image.open("Tests/images/apng/blend_op_over.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (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: 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) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 97) assert im.getpixel((0, 0)) == (0, 255, 0, 97)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -178,6 +197,7 @@ def test_apng_blend_transparency() -> None:
def test_apng_chunk_order() -> None: def test_apng_chunk_order() -> None:
with Image.open("Tests/images/apng/fctl_actl.png") as im: with Image.open("Tests/images/apng/fctl_actl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -233,24 +253,28 @@ def test_apng_num_plays() -> None:
def test_apng_mode() -> None: def test_apng_mode() -> None:
with Image.open("Tests/images/apng/mode_16bit.png") as im: with Image.open("Tests/images/apng/mode_16bit.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "RGBA" assert im.mode == "RGBA"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 128, 191) assert im.getpixel((0, 0)) == (0, 0, 128, 191)
assert im.getpixel((64, 32)) == (0, 0, 128, 191) assert im.getpixel((64, 32)) == (0, 0, 128, 191)
with Image.open("Tests/images/apng/mode_grayscale.png") as im: with Image.open("Tests/images/apng/mode_grayscale.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "L" assert im.mode == "L"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == 128 assert im.getpixel((0, 0)) == 128
assert im.getpixel((64, 32)) == 255 assert im.getpixel((64, 32)) == 255
with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "LA" assert im.mode == "LA"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (128, 191) assert im.getpixel((0, 0)) == (128, 191)
assert im.getpixel((64, 32)) == (128, 191) assert im.getpixel((64, 32)) == (128, 191)
with Image.open("Tests/images/apng/mode_palette.png") as im: with Image.open("Tests/images/apng/mode_palette.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im = im.convert("RGB") im = im.convert("RGB")
@ -258,6 +282,7 @@ def test_apng_mode() -> None:
assert im.getpixel((64, 32)) == (0, 255, 0) assert im.getpixel((64, 32)) == (0, 255, 0)
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im = im.convert("RGBA") im = im.convert("RGBA")
@ -265,6 +290,7 @@ def test_apng_mode() -> None:
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im = im.convert("RGBA") im = im.convert("RGBA")
@ -274,25 +300,31 @@ def test_apng_mode() -> None:
def test_apng_chunk_errors() -> None: def test_apng_chunk_errors() -> None:
with Image.open("Tests/images/apng/chunk_no_actl.png") as im: with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
im.load() im.load()
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: with Image.open("Tests/images/apng/chunk_no_fctl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: with Image.open("Tests/images/apng/chunk_no_fdat.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
@ -300,26 +332,31 @@ def test_apng_chunk_errors() -> None:
def test_apng_syntax_errors() -> None: def test_apng_syntax_errors() -> None:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
with pytest.raises(OSError): with pytest.raises(OSError):
im.load() im.load()
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
im.load() im.load()
# we can handle this case gracefully # we can handle this case gracefully
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
with pytest.raises(OSError): with pytest.raises(OSError):
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: 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.seek(im.n_frames - 1)
im.load() im.load()
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
im.load() im.load()
@ -339,16 +376,18 @@ def test_apng_syntax_errors() -> None:
def test_apng_sequence_errors(test_file: str) -> None: def test_apng_sequence_errors(test_file: str) -> None:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
with Image.open(f"Tests/images/apng/{test_file}") as im: with Image.open(f"Tests/images/apng/{test_file}") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im.load() im.load()
def test_apng_save(tmp_path: Path) -> None: def test_apng_save(tmp_path: Path) -> None:
with Image.open("Tests/images/apng/single_frame.png") as im: 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) im.save(test_file, save_all=True)
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.load() im.load()
assert not im.is_animated assert not im.is_animated
assert im.n_frames == 1 assert im.n_frames == 1
@ -364,6 +403,7 @@ def test_apng_save(tmp_path: Path) -> None:
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.load() im.load()
assert im.is_animated assert im.is_animated
assert im.n_frames == 2 assert im.n_frames == 2
@ -375,7 +415,7 @@ def test_apng_save(tmp_path: Path) -> None:
def test_apng_save_alpha(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)) im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127))
@ -393,7 +433,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
# frames with image data spanning multiple fdAT chunks (in this case # frames with image data spanning multiple fdAT chunks (in this case
# both the default image and first animation frame will span multiple # both the default image and first animation frame will span multiple
# data chunks) # 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: with Image.open("Tests/images/old-style-jpeg-compression.png") as im:
frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))]
im.save( im.save(
@ -403,12 +443,13 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
append_images=frames, append_images=frames,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im.load() im.load()
def test_apng_save_duration_loop(tmp_path: Path) -> None: 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: with Image.open("Tests/images/apng/delay.png") as im:
frames = [] frames = []
durations = [] durations = []
@ -445,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] test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert "duration" not in im.info assert "duration" not in im.info
@ -456,6 +498,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
duration=[500, 100, 150], duration=[500, 100, 150],
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 2 assert im.n_frames == 2
assert im.info["duration"] == 600 assert im.info["duration"] == 600
@ -466,12 +509,13 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
frame.info["duration"] = 300 frame.info["duration"] = 300
frame.save(test_file, save_all=True, append_images=[frame, different_frame]) frame.save(test_file, save_all=True, append_images=[frame, different_frame])
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 2 assert im.n_frames == 2
assert im.info["duration"] == 600 assert im.info["duration"] == 600
def test_apng_save_disposal(tmp_path: Path) -> None: 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) size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255))
@ -572,7 +616,7 @@ def test_apng_save_disposal(tmp_path: Path) -> None:
def test_apng_save_disposal_previous(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) size = (128, 64)
blue = Image.new("RGBA", size, (0, 0, 255, 255)) blue = Image.new("RGBA", size, (0, 0, 255, 255))
red = Image.new("RGBA", size, (255, 0, 0, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255))
@ -594,7 +638,7 @@ def test_apng_save_disposal_previous(tmp_path: Path) -> None:
def test_apng_save_blend(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) size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255))
@ -662,7 +706,7 @@ def test_apng_save_blend(tmp_path: Path) -> None:
def test_apng_save_size(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 = Image.new("L", (100, 100))
im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))]) im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))])
@ -686,7 +730,7 @@ def test_seek_after_close() -> None:
def test_different_modes_in_later_frames( def test_different_modes_in_later_frames(
mode: str, default_image: bool, duplicate: bool, tmp_path: Path mode: str, default_image: bool, duplicate: bool, tmp_path: Path
) -> None: ) -> None:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
im.save( im.save(
@ -700,7 +744,7 @@ def test_different_modes_in_later_frames(
def test_different_durations(tmp_path: Path) -> None: 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: with Image.open("Tests/images/apng/different_durations.png") as im:
for _ in range(3): for _ in range(3):

View File

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

View File

@ -15,25 +15,19 @@ from .helper import (
) )
def test_sanity(tmp_path: Path) -> None: @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
def roundtrip(im: Image.Image) -> None: def test_sanity(mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.bmp") outfile = tmp_path / "temp.bmp"
im.save(outfile, "BMP") im = hopper(mode)
im.save(outfile, "BMP")
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
reloaded.load() reloaded.load()
assert im.mode == reloaded.mode assert im.mode == reloaded.mode
assert im.size == reloaded.size assert im.size == reloaded.size
assert reloaded.format == "BMP" assert reloaded.format == "BMP"
assert reloaded.get_format_mimetype() == "image/bmp" assert reloaded.get_format_mimetype() == "image/bmp"
roundtrip(hopper())
roundtrip(hopper("1"))
roundtrip(hopper("L"))
roundtrip(hopper("P"))
roundtrip(hopper("RGB"))
def test_invalid_file() -> None: 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] colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
im.putpalette(colors) im.putpalette(colors)
out = str(tmp_path / "temp.bmp") out = tmp_path / "temp.bmp"
im.save(out) im.save(out)
with Image.open(out) as reloaded: 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: 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: with Image.new("RGB", (1, 1)) as im:
im._size = (37838, 37838) im._size = (37838, 37838)
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -96,7 +90,7 @@ def test_dpi() -> None:
def test_save_bmp_with_dpi(tmp_path: Path) -> None: def test_save_bmp_with_dpi(tmp_path: Path) -> None:
# Test for #1301 # Test for #1301
# Arrange # Arrange
outfile = str(tmp_path / "temp.jpg") outfile = tmp_path / "temp.jpg"
with Image.open("Tests/images/hopper.bmp") as im: with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) 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: 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: with Image.open("Tests/images/hopper.bmp") as im:
im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
with Image.open(outfile) as reloaded: 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: 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: with Image.open("Tests/images/clipboard.dib") as im:
im.save(outfile) im.save(outfile)
@ -230,3 +224,13 @@ def test_offset() -> None:
# to exclude the palette size from the pixel data offset # to exclude the palette size from the pixel data offset
with Image.open("Tests/images/pal8_offset.bmp") as im: with Image.open("Tests/images/pal8_offset.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") 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: def test_save(tmp_path: Path) -> None:
# Arrange # Arrange
im = hopper() im = hopper()
tmpfile = str(tmp_path / "temp.bufr") tmpfile = tmp_path / "temp.bufr"
# Act / Assert: stub cannot save without an implemented handler # Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError): with pytest.raises(OSError):
@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None:
im.load() im.load()
assert handler.is_loaded() assert handler.is_loaded()
temp_file = str(tmp_path / "temp.bufr") temp_file = tmp_path / "temp.bufr"
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved

View File

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

View File

@ -116,7 +116,7 @@ def test_sanity_ati1_bc4u(image_path: str) -> None:
def test_dx10_bc2(tmp_path: Path) -> None: def test_dx10_bc2(tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds") out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT3) as im: with Image.open(TEST_FILE_DXT3) as im:
im.save(out, pixel_format="BC2") im.save(out, pixel_format="BC2")
@ -129,7 +129,7 @@ def test_dx10_bc2(tmp_path: Path) -> None:
def test_dx10_bc3(tmp_path: Path) -> None: def test_dx10_bc3(tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds") out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT5) as im: with Image.open(TEST_FILE_DXT5) as im:
im.save(out, pixel_format="BC3") im.save(out, pixel_format="BC3")
@ -400,7 +400,7 @@ def test_not_implemented(test_file: str) -> None:
def test_save_unsupported_mode(tmp_path: Path) -> None: def test_save_unsupported_mode(tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds") out = tmp_path / "temp.dds"
im = hopper("HSV") im = hopper("HSV")
with pytest.raises(OSError, match="cannot write mode HSV as DDS"): with pytest.raises(OSError, match="cannot write mode HSV as DDS"):
im.save(out) im.save(out)
@ -416,7 +416,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None:
], ],
) )
def test_save(mode: str, test_file: str, 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: with Image.open(test_file) as im:
assert im.mode == mode assert im.mode == mode
im.save(out) im.save(out)
@ -425,7 +425,7 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None:
def test_save_unsupported_pixel_format(tmp_path: Path) -> None: def test_save_unsupported_pixel_format(tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds") out = tmp_path / "temp.dds"
im = hopper() im = hopper()
with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"): with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"):
im.save(out, pixel_format="UNKNOWN") im.save(out, pixel_format="UNKNOWN")
@ -433,7 +433,7 @@ def test_save_unsupported_pixel_format(tmp_path: Path) -> None:
def test_save_dxt1(tmp_path: Path) -> None: def test_save_dxt1(tmp_path: Path) -> None:
# RGB # RGB
out = str(tmp_path / "temp.dds") out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT1) as im: with Image.open(TEST_FILE_DXT1) as im:
im.convert("RGB").save(out, pixel_format="DXT1") im.convert("RGB").save(out, pixel_format="DXT1")
assert_image_similar_tofile(im, out, 1.84) assert_image_similar_tofile(im, out, 1.84)
@ -458,7 +458,7 @@ def test_save_dxt1(tmp_path: Path) -> None:
def test_save_dxt3(tmp_path: Path) -> None: def test_save_dxt3(tmp_path: Path) -> None:
# RGB # RGB
out = str(tmp_path / "temp.dds") out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT3) as im: with Image.open(TEST_FILE_DXT3) as im:
im_rgb = im.convert("RGB") im_rgb = im.convert("RGB")
im_rgb.save(out, pixel_format="DXT3") im_rgb.save(out, pixel_format="DXT3")
@ -481,7 +481,7 @@ def test_save_dxt3(tmp_path: Path) -> None:
def test_save_dxt5(tmp_path: Path) -> None: def test_save_dxt5(tmp_path: Path) -> None:
# RGB # RGB
out = str(tmp_path / "temp.dds") out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT1) as im: with Image.open(TEST_FILE_DXT1) as im:
im.convert("RGB").save(out, pixel_format="DXT5") im.convert("RGB").save(out, pixel_format="DXT5")
assert_image_similar_tofile(im, out, 1.84) assert_image_similar_tofile(im, out, 1.84)
@ -503,7 +503,7 @@ def test_save_dxt5(tmp_path: Path) -> None:
def test_save_dx10_bc5(tmp_path: Path) -> None: def test_save_dx10_bc5(tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds") out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im: with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im:
im.save(out, pixel_format="BC5") im.save(out, pixel_format="BC5")
assert_image_similar_tofile(im, out, 9.56) assert_image_similar_tofile(im, out, 9.56)

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

View File

@ -22,6 +22,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
def test_sanity() -> None: def test_sanity() -> None:
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
im.load() im.load()
assert im.mode == "P" assert im.mode == "P"
assert im.size == (128, 128) assert im.size == (128, 128)
@ -29,6 +31,8 @@ def test_sanity() -> None:
assert not im.is_animated assert not im.is_animated
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.mode == "P" assert im.mode == "P"
assert im.size == (320, 200) assert im.size == (320, 200)
assert im.format == "FLI" assert im.format == "FLI"
@ -112,16 +116,19 @@ def test_palette_chunk_second() -> None:
def test_n_frames() -> None: def test_n_frames() -> None:
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.n_frames == 384 assert im.n_frames == 384
assert im.is_animated assert im.is_animated
def test_eoferror() -> None: def test_eoferror() -> None:
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
n_frames = im.n_frames n_frames = im.n_frames
# Test seeking past the last frame # Test seeking past the last frame
@ -166,6 +173,7 @@ def test_seek_tell() -> None:
def test_seek() -> None: def test_seek() -> None:
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
im.seek(50) im.seek(50)
assert_image_equal_tofile(im, "Tests/images/a_fli.png") assert_image_equal_tofile(im, "Tests/images/a_fli.png")

View File

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

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: 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)) im = Image.new("P", (1, 256))
full_palette_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: def test_roundtrip(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = hopper() im = hopper()
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
@ -258,7 +258,7 @@ def test_roundtrip(tmp_path: Path) -> None:
def test_roundtrip2(tmp_path: Path) -> None: def test_roundtrip2(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/403 # 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: with Image.open(TEST_GIF) as im:
im2 = im.copy() im2 = im.copy()
im2.save(out) im2.save(out)
@ -268,7 +268,7 @@ def test_roundtrip2(tmp_path: Path) -> None:
def test_roundtrip_save_all(tmp_path: Path) -> None: def test_roundtrip_save_all(tmp_path: Path) -> None:
# Single frame image # Single frame image
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = hopper() im = hopper()
im.save(out, save_all=True) im.save(out, save_all=True)
with Image.open(out) as reread: with Image.open(out) as reread:
@ -276,7 +276,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
# Multiframe image # Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im: 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) im.save(out, save_all=True)
with Image.open(out) as reread: 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: 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)) im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1) im2 = Image.new("1", (1, 1), 1)
im.save(out, save_all=True, append_images=[im2]) 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: with Image.open("Tests/images/dispose_bgnd.gif") as im:
info = im.info.copy() info = im.info.copy()
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.save(out, save_all=True) im.save(out, save_all=True)
with Image.open(out) as reread: with Image.open(out) as reread:
for header in important_headers: 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) im = im.resize((100, 100), Image.Resampling.LANCZOS)
im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) 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) im2.save(f, optimize=True)
with Image.open(f) as reloaded: 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 # see https://github.com/python-pillow/Pillow/issues/434
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: 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) im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out) reloaded = Image.open(out)
@ -402,6 +402,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
def test_seek() -> None: def test_seek() -> None:
with Image.open("Tests/images/dispose_none.gif") as img: with Image.open("Tests/images/dispose_none.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
frame_count = 0 frame_count = 0
try: try:
while True: while True:
@ -446,10 +447,12 @@ def test_seek_rewind() -> None:
def test_n_frames(path: str, n_frames: int) -> None: def test_n_frames(path: str, n_frames: int) -> None:
# Test is_animated before n_frames # Test is_animated before n_frames
with Image.open(path) as im: with Image.open(path) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.is_animated == (n_frames != 1) assert im.is_animated == (n_frames != 1)
# Test is_animated after n_frames # Test is_animated after n_frames
with Image.open(path) as im: with Image.open(path) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == n_frames assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1) assert im.is_animated == (n_frames != 1)
@ -459,6 +462,7 @@ def test_no_change() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(1) im.seek(1)
expected = im.copy() expected = im.copy()
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == 5 assert im.n_frames == 5
assert_image_equal(im, expected) assert_image_equal(im, expected)
@ -466,17 +470,20 @@ def test_no_change() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(3) im.seek(3)
expected = im.copy() expected = im.copy()
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.is_animated assert im.is_animated
assert_image_equal(im, expected) assert_image_equal(im, expected)
with Image.open("Tests/images/comment_after_only_frame.gif") as im: with Image.open("Tests/images/comment_after_only_frame.gif") as im:
expected = Image.new("P", (1, 1)) expected = Image.new("P", (1, 1))
assert isinstance(im, GifImagePlugin.GifImageFile)
assert not im.is_animated assert not im.is_animated
assert_image_equal(im, expected) assert_image_equal(im, expected)
def test_eoferror() -> None: def test_eoferror() -> None:
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
n_frames = im.n_frames n_frames = im.n_frames
# Test seeking past the last frame # Test seeking past the last frame
@ -495,6 +502,7 @@ def test_first_frame_transparency() -> None:
def test_dispose_none() -> None: def test_dispose_none() -> None:
with Image.open("Tests/images/dispose_none.gif") as img: with Image.open("Tests/images/dispose_none.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try: try:
while True: while True:
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
@ -518,6 +526,7 @@ def test_dispose_none_load_end() -> None:
def test_dispose_background() -> None: def test_dispose_background() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as img: with Image.open("Tests/images/dispose_bgnd.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try: try:
while True: while True:
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
@ -571,6 +580,7 @@ def test_transparent_dispose(
def test_dispose_previous() -> None: def test_dispose_previous() -> None:
with Image.open("Tests/images/dispose_prev.gif") as img: with Image.open("Tests/images/dispose_prev.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try: try:
while True: while True:
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
@ -599,7 +609,7 @@ def test_previous_frame_loaded() -> None:
def test_save_dispose(tmp_path: Path) -> None: def test_save_dispose(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#111"), Image.new("L", (100, 100), "#111"),
@ -608,6 +618,7 @@ def test_save_dispose(tmp_path: Path) -> None:
for method in range(4): for method in range(4):
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
with Image.open(out) as img: with Image.open(out) as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
for _ in range(2): for _ in range(2):
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
assert img.disposal_method == method assert img.disposal_method == method
@ -621,13 +632,14 @@ def test_save_dispose(tmp_path: Path) -> None:
) )
with Image.open(out) as img: with Image.open(out) as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
for i in range(2): for i in range(2):
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
assert img.disposal_method == i + 1 assert img.disposal_method == i + 1
def test_dispose2_palette(tmp_path: Path) -> None: 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 # Four colors: white, gray, black, red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
@ -661,7 +673,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
def test_dispose2_diff(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 # 4 frames: red/blue, red/red, blue/blue, red/blue
circles = [ circles = [
@ -703,7 +715,7 @@ def test_dispose2_diff(tmp_path: Path) -> None:
def test_dispose2_background(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 = [] im_list = []
@ -729,7 +741,7 @@ def test_dispose2_background(tmp_path: Path) -> None:
def test_dispose2_background_frame(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))] im_list = [Image.new("RGBA", (1, 20))]
@ -743,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) im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
with Image.open(out) as im: with Image.open(out) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == 3 assert im.n_frames == 3
def test_dispose2_previous_frame(tmp_path: Path) -> None: 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 = Image.new("P", (100, 100))
im.info["transparency"] = 0 im.info["transparency"] = 0
@ -766,7 +779,7 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None:
def test_dispose2_without_transparency(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)) im = Image.new("P", (100, 100))
@ -781,7 +794,7 @@ def test_dispose2_without_transparency(tmp_path: Path) -> None:
def test_transparency_in_second_frame(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: with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0 assert im.info["transparency"] == 0
@ -811,7 +824,7 @@ def test_no_transparency_in_second_frame() -> None:
def test_remapped_transparency(tmp_path: Path) -> 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)) im = Image.new("P", (1, 2))
im2 = im.copy() im2 = im.copy()
@ -829,7 +842,7 @@ def test_remapped_transparency(tmp_path: Path) -> None:
def test_duration(tmp_path: Path) -> None: def test_duration(tmp_path: Path) -> None:
duration = 1000 duration = 1000
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
# Check that the argument has priority over the info settings # Check that the argument has priority over the info settings
@ -843,7 +856,7 @@ def test_duration(tmp_path: Path) -> None:
def test_multiple_duration(tmp_path: Path) -> None: def test_multiple_duration(tmp_path: Path) -> None:
duration_list = [1000, 2000, 3000] duration_list = [1000, 2000, 3000]
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#111"), Image.new("L", (100, 100), "#111"),
@ -878,7 +891,7 @@ def test_multiple_duration(tmp_path: Path) -> None:
def test_roundtrip_info_duration(tmp_path: Path) -> None: def test_roundtrip_info_duration(tmp_path: Path) -> None:
duration_list = [100, 500, 500] 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: with Image.open("Tests/images/transparent_dispose.gif") as im:
assert [ assert [
frame.info["duration"] for frame in ImageSequence.Iterator(im) frame.info["duration"] for frame in ImageSequence.Iterator(im)
@ -893,7 +906,7 @@ def test_roundtrip_info_duration(tmp_path: Path) -> None:
def test_roundtrip_info_duration_combined(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: with Image.open("Tests/images/duplicate_frame.gif") as im:
assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
1000, 1000,
@ -911,7 +924,7 @@ def test_roundtrip_info_duration_combined(tmp_path: Path) -> None:
def test_identical_frames(tmp_path: Path) -> None: def test_identical_frames(tmp_path: Path) -> None:
duration_list = [1000, 1500, 2000, 4000] duration_list = [1000, 1500, 2000, 4000]
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
@ -924,6 +937,8 @@ def test_identical_frames(tmp_path: Path) -> None:
out, save_all=True, append_images=im_list[1:], duration=duration_list out, save_all=True, append_images=im_list[1:], duration=duration_list
) )
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
# Assert that the first three frames were combined # Assert that the first three frames were combined
assert reread.n_frames == 2 assert reread.n_frames == 2
@ -944,7 +959,7 @@ def test_identical_frames(tmp_path: Path) -> None:
def test_identical_frames_to_single_frame( def test_identical_frames_to_single_frame(
duration: int | list[int], tmp_path: Path duration: int | list[int], tmp_path: Path
) -> None: ) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
@ -953,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) im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
# Assert that all frames were combined # Assert that all frames were combined
assert reread.n_frames == 1 assert reread.n_frames == 1
@ -961,7 +978,7 @@ def test_identical_frames_to_single_frame(
def test_loop_none(tmp_path: Path) -> None: 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 = Image.new("L", (100, 100), "#000")
im.save(out, loop=None) im.save(out, loop=None)
with Image.open(out) as reread: with Image.open(out) as reread:
@ -971,7 +988,7 @@ def test_loop_none(tmp_path: Path) -> None:
def test_number_of_loops(tmp_path: Path) -> None: def test_number_of_loops(tmp_path: Path) -> None:
number_of_loops = 2 number_of_loops = 2
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.save(out, loop=number_of_loops) im.save(out, loop=number_of_loops)
with Image.open(out) as reread: with Image.open(out) as reread:
@ -987,7 +1004,7 @@ def test_number_of_loops(tmp_path: Path) -> None:
def test_background(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 = Image.new("L", (100, 100), "#000")
im.info["background"] = 1 im.info["background"] = 1
im.save(out) im.save(out)
@ -996,7 +1013,7 @@ def test_background(tmp_path: Path) -> None:
def test_webp_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 # Test opaque WebP background
if features.check("webp"): if features.check("webp"):
@ -1014,7 +1031,7 @@ def test_comment(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" 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 = Image.new("L", (100, 100), "#000")
im.info["comment"] = b"Test comment text" im.info["comment"] = b"Test comment text"
im.save(out) im.save(out)
@ -1031,7 +1048,7 @@ def test_comment(tmp_path: Path) -> None:
def test_comment_over_255(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") im = Image.new("L", (100, 100), "#000")
comment = b"Test comment text" comment = b"Test comment text"
while len(comment) < 256: while len(comment) < 256:
@ -1057,7 +1074,7 @@ def test_read_multiple_comment_blocks() -> None:
def test_empty_string_comment(tmp_path: Path) -> 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: with Image.open("Tests/images/chi.gif") as im:
assert "comment" in im.info assert "comment" in im.info
@ -1091,7 +1108,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
assert "comment" not in im.info assert "comment" not in im.info
# Test that a saved image keeps the comment # 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: with Image.open("Tests/images/dispose_prev.gif") as im:
im.save(out, save_all=True, comment="Test") im.save(out, save_all=True, comment="Test")
@ -1101,7 +1118,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
def test_version(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: def assert_version_after_save(im: Image.Image, version: bytes) -> None:
im.save(out) im.save(out)
@ -1131,7 +1148,7 @@ def test_version(tmp_path: Path) -> None:
def test_append_images(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 # Test appending single frame images
im = Image.new("RGB", (100, 100), "#f00") im = Image.new("RGB", (100, 100), "#f00")
@ -1139,6 +1156,14 @@ def test_append_images(tmp_path: Path) -> None:
im.copy().save(out, save_all=True, append_images=ims) im.copy().save(out, save_all=True, append_images=ims)
with Image.open(out) as reread: 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 assert reread.n_frames == 3
# Tests appending using a generator # Tests appending using a generator
@ -1148,6 +1173,7 @@ def test_append_images(tmp_path: Path) -> None:
im.save(out, save_all=True, append_images=im_generator(ims)) im.save(out, save_all=True, append_images=im_generator(ims))
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 3 assert reread.n_frames == 3
# Tests appending single and multiple frame images # Tests appending single and multiple frame images
@ -1156,11 +1182,12 @@ def test_append_images(tmp_path: Path) -> None:
im.save(out, save_all=True, append_images=[im2]) im.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 10 assert reread.n_frames == 10
def test_append_different_size_image(tmp_path: Path) -> None: 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)) im = Image.new("RGB", (100, 100))
bigger_im = Image.new("RGB", (200, 200), "#f00") bigger_im = Image.new("RGB", (200, 200), "#f00")
@ -1187,7 +1214,7 @@ def test_transparent_optimize(tmp_path: Path) -> None:
im.frombytes(data) im.frombytes(data)
im.putpalette(palette) im.putpalette(palette)
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.save(out, transparency=im.getpixel((252, 0))) im.save(out, transparency=im.getpixel((252, 0)))
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1195,7 +1222,7 @@ def test_transparent_optimize(tmp_path: Path) -> None:
def test_removed_transparency(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)) im = Image.new("RGB", (256, 1))
for x in range(256): for x in range(256):
@ -1210,7 +1237,7 @@ def test_removed_transparency(tmp_path: Path) -> None:
def test_rgb_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 # Single frame
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
@ -1232,7 +1259,7 @@ def test_rgb_transparency(tmp_path: Path) -> None:
def test_rgba_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 = hopper("P")
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
@ -1242,25 +1269,26 @@ def test_rgba_transparency(tmp_path: Path) -> None:
assert_image_equal(hopper("P").convert("RGB"), reloaded) 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: with Image.open("Tests/images/background_outside_palette.gif") as im:
im.seek(1) im.seek(1)
assert im.info["background"] == 255 assert im.info["background"] == 255
def test_bbox(tmp_path: Path) -> None: 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") im = Image.new("RGB", (100, 100), "#fff")
ims = [Image.new("RGB", (100, 100), "#000")] ims = [Image.new("RGB", (100, 100), "#000")]
im.save(out, save_all=True, append_images=ims) im.save(out, save_all=True, append_images=ims)
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 2 assert reread.n_frames == 2
def test_bbox_alpha(tmp_path: Path) -> None: 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 = Image.new("RGBA", (1, 2), (255, 0, 0, 255))
im.putpixel((0, 1), (255, 0, 0, 0)) im.putpixel((0, 1), (255, 0, 0, 0))
@ -1268,6 +1296,7 @@ def test_bbox_alpha(tmp_path: Path) -> None:
im.save(out, save_all=True, append_images=[im2]) im.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 2 assert reread.n_frames == 2
@ -1279,7 +1308,7 @@ def test_palette_save_L(tmp_path: Path) -> None:
palette = im.getpalette() palette = im.getpalette()
assert palette is not None assert palette is not None
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_l.save(out, palette=bytes(palette)) im_l.save(out, palette=bytes(palette))
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1290,7 +1319,7 @@ def test_palette_save_P(tmp_path: Path) -> None:
im = Image.new("P", (1, 2)) im = Image.new("P", (1, 2))
im.putpixel((0, 1), 1) 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))) im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1306,7 +1335,7 @@ def test_palette_save_duplicate_entries(tmp_path: Path) -> None:
im.putpalette((0, 0, 0, 0, 0, 0)) 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]) im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1])
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1321,7 +1350,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
frame.putpalette(color) frame.putpalette(color)
frames.append(frame) frames.append(frame)
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
frames[0].save( frames[0].save(
out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:] out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]
) )
@ -1344,7 +1373,7 @@ def test_palette_save_ImagePalette(tmp_path: Path) -> None:
im = hopper("P") im = hopper("P")
palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) 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) im.save(out, palette=palette)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1357,7 +1386,7 @@ def test_save_I(tmp_path: Path) -> None:
im = hopper("I") im = hopper("I")
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.save(out) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1419,6 +1448,7 @@ def test_extents(
) -> None: ) -> None:
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
with Image.open("Tests/images/" + test_file) as im: with Image.open("Tests/images/" + test_file) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.size == (100, 100) assert im.size == (100, 100)
# Check that n_frames does not change the size # Check that n_frames does not change the size
@ -1441,7 +1471,7 @@ def test_missing_background() -> None:
def test_saving_rgba(tmp_path: Path) -> 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: with Image.open("Tests/images/transparent.png") as im:
im.save(out) im.save(out)
@ -1452,7 +1482,7 @@ def test_saving_rgba(tmp_path: Path) -> None:
@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) @pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False}))
def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: 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)) im1 = Image.new("P", (100, 100))
d = ImageDraw.Draw(im1) d = ImageDraw.Draw(im1)
@ -1466,4 +1496,5 @@ def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
im1.save(out, save_all=True, append_images=[im2], **params) im1.save(out, save_all=True, append_images=[im2], **params)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, GifImagePlugin.GifImageFile)
assert reloaded.n_frames == 2 assert reloaded.n_frames == 2

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
import pytest import pytest
from PIL.GimpPaletteFile import GimpPaletteFile from PIL.GimpPaletteFile import GimpPaletteFile
@ -14,17 +16,20 @@ def test_sanity() -> None:
GimpPaletteFile(fp) GimpPaletteFile(fp)
with open("Tests/images/bad_palette_file.gpl", "rb") as 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) GimpPaletteFile(fp)
with open("Tests/images/bad_palette_entry.gpl", "rb") as 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) 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 # Arrange
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: with open("Tests/images/" + filename, "rb") as fp:
palette_file = GimpPaletteFile(fp) palette_file = GimpPaletteFile(fp)
# Act # Act
@ -32,4 +37,36 @@ def test_get_palette() -> None:
# Assert # Assert
assert mode == "RGB" assert mode == "RGB"
assert len(palette) / 3 == 8 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: def test_save(tmp_path: Path) -> None:
# Arrange # Arrange
im = hopper() im = hopper()
tmpfile = str(tmp_path / "temp.grib") tmpfile = tmp_path / "temp.grib"
# Act / Assert: stub cannot save without an implemented handler # Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError): with pytest.raises(OSError):
@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None:
im.load() im.load()
assert handler.is_loaded() assert handler.is_loaded()
temp_file = str(tmp_path / "temp.grib") temp_file = tmp_path / "temp.grib"
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved

View File

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

View File

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

View File

@ -41,7 +41,7 @@ def test_black_and_white() -> None:
def test_palette(tmp_path: Path) -> 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 = Image.new("P", (16, 16))
im.save(temp_file) im.save(temp_file)
@ -77,6 +77,7 @@ def test_save_to_bytes() -> None:
# The other one # The other one
output.seek(0) output.seek(0)
with Image.open(output) as reloaded: with Image.open(output) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.size = (32, 32) reloaded.size = (32, 32)
assert im.mode == reloaded.mode assert im.mode == reloaded.mode
@ -88,12 +89,13 @@ def test_save_to_bytes() -> None:
def test_getpixel(tmp_path: Path) -> None: def test_getpixel(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico") temp_file = tmp_path / "temp.ico"
im = hopper() im = hopper()
im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)])
with Image.open(temp_file) as reloaded: with Image.open(temp_file) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.load() reloaded.load()
reloaded.size = (32, 32) reloaded.size = (32, 32)
@ -101,8 +103,8 @@ def test_getpixel(tmp_path: Path) -> None:
def test_no_duplicates(tmp_path: Path) -> None: def test_no_duplicates(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico") temp_file = tmp_path / "temp.ico"
temp_file2 = str(tmp_path / "temp2.ico") temp_file2 = tmp_path / "temp2.ico"
im = hopper() im = hopper()
sizes = [(32, 32), (64, 64)] 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: def test_different_bit_depths(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico") temp_file = tmp_path / "temp.ico"
temp_file2 = str(tmp_path / "temp2.ico") temp_file2 = tmp_path / "temp2.ico"
im = hopper() im = hopper()
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) 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) assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
# Test that only matching sizes of different bit depths are saved # Test that only matching sizes of different bit depths are saved
temp_file3 = str(tmp_path / "temp3.ico") temp_file3 = tmp_path / "temp3.ico"
temp_file4 = str(tmp_path / "temp4.ico") temp_file4 = tmp_path / "temp4.ico"
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
im.save( im.save(
@ -167,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
# The other one # The other one
output.seek(0) output.seek(0)
with Image.open(output) as reloaded: with Image.open(output) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.size = (32, 32) reloaded.size = (32, 32)
assert "RGBA" == reloaded.mode assert "RGBA" == reloaded.mode
@ -178,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
def test_incorrect_size() -> None: def test_incorrect_size() -> None:
with Image.open(TEST_ICO_FILE) as im: with Image.open(TEST_ICO_FILE) as im:
assert isinstance(im, IcoImagePlugin.IcoImageFile)
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.size = (1, 1) 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""" """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264"""
# Arrange # Arrange
with Image.open("Tests/images/hopper_256x256.ico") as im: 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 # Act
im.save(outfile) im.save(outfile)
@ -202,7 +206,7 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None:
""" """
# Arrange # Arrange
with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 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 # Act
im.save(outfile) 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 # append_images should be used for scaled down versions of the image
im = hopper("RGBA") im = hopper("RGBA")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) 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]) im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im])
with Image.open(outfile) as reread: with Image.open(outfile) as reread:
assert isinstance(reread, IcoImagePlugin.IcoImageFile)
assert_image_equal(reread, hopper("RGBA")) assert_image_equal(reread, hopper("RGBA"))
reread.size = (32, 32) reread.size = (32, 32)
@ -235,7 +240,7 @@ def test_unexpected_size() -> None:
def test_draw_reloaded(tmp_path: Path) -> None: def test_draw_reloaded(tmp_path: Path) -> None:
with Image.open(TEST_ICO_FILE) as im: 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 = ImageDraw.Draw(im)
draw.line((0, 0) + im.size, "#f00") 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: 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: with Image.open(TEST_IM) as im:
im.save(out) im.save(out)
assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") assert filecmp.cmp(out, "Tests/images/hopper_long_name.im")
@ -68,12 +68,14 @@ def test_tell() -> None:
def test_n_frames() -> None: def test_n_frames() -> None:
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
assert isinstance(im, ImImagePlugin.ImImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
def test_eoferror() -> None: def test_eoferror() -> None:
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
assert isinstance(im, ImImagePlugin.ImImageFile)
n_frames = im.n_frames n_frames = im.n_frames
# Test seeking past the last frame # Test seeking past the last frame
@ -87,7 +89,7 @@ def test_eoferror() -> None:
@pytest.mark.parametrize("mode", ("RGB", "P", "PA")) @pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
def test_roundtrip(mode: str, tmp_path: Path) -> None: def test_roundtrip(mode: str, tmp_path: Path) -> None:
out = str(tmp_path / "temp.im") out = tmp_path / "temp.im"
im = hopper(mode) im = hopper(mode)
im.save(out) im.save(out)
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
@ -98,7 +100,7 @@ def test_small_palette(tmp_path: Path) -> None:
colors = [0, 1, 2] colors = [0, 1, 2]
im.putpalette(colors) im.putpalette(colors)
out = str(tmp_path / "temp.im") out = tmp_path / "temp.im"
im.save(out) im.save(out)
with Image.open(out) as reloaded: 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: def test_save_unsupported_mode(tmp_path: Path) -> None:
out = str(tmp_path / "temp.im") out = tmp_path / "temp.im"
im = hopper("HSV") im = hopper("HSV")
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(out) im.save(out)

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ from typing import Any
import pytest import pytest
from PIL import Image, ImageFile, MpoImagePlugin from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -80,6 +80,7 @@ def test_context_manager() -> None:
def test_app(test_file: str) -> None: def test_app(test_file: str) -> None:
# Test APP/COM reader (@PIL135) # Test APP/COM reader (@PIL135)
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
assert im.applist[0][0] == "APP1" assert im.applist[0][0] == "APP1"
assert im.applist[1][0] == "APP2" assert im.applist[1][0] == "APP2"
assert im.applist[1][1].startswith( assert im.applist[1][1].startswith(
@ -220,12 +221,14 @@ def test_seek(test_file: str) -> None:
def test_n_frames() -> None: def test_n_frames() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im: with Image.open("Tests/images/sugarshack.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
assert im.n_frames == 2 assert im.n_frames == 2
assert im.is_animated assert im.is_animated
def test_eoferror() -> None: def test_eoferror() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im: with Image.open("Tests/images/sugarshack.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
n_frames = im.n_frames n_frames = im.n_frames
# Test seeking past the last frame # Test seeking past the last frame
@ -239,6 +242,8 @@ def test_eoferror() -> None:
def test_adopt_jpeg() -> None: def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
with pytest.raises(ValueError): with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im) MpoImagePlugin.MpoImageFile.adopt(im)

View File

@ -15,7 +15,7 @@ YA_EXTRA_DIR = "Tests/images/msp"
def test_sanity(tmp_path: Path) -> None: 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) hopper("1").save(test_file)
@ -84,7 +84,7 @@ def test_msp_v2() -> None:
def test_cannot_save_wrong_mode(tmp_path: Path) -> None: def test_cannot_save_wrong_mode(tmp_path: Path) -> None:
# Arrange # Arrange
im = hopper() im = hopper()
filename = str(tmp_path / "temp.msp") filename = tmp_path / "temp.msp"
# Act/Assert # Act/Assert
with pytest.raises(OSError): 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: def helper_save_as_palm(tmp_path: Path, mode: str) -> None:
# Arrange # Arrange
im = hopper(mode) im = hopper(mode)
outfile = str(tmp_path / ("temp_" + mode + ".palm")) outfile = tmp_path / ("temp_" + mode + ".palm")
# Act # Act
im.save(outfile) 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: 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( rc = subprocess.call(
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
) )
@ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None:
im.save(outfile) im.save(outfile)
converted = open_with_magick(magick, tmp_path, 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) assert_image_equal(converted, im)
@ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None:
roundtrip(tmp_path, mode) roundtrip(tmp_path, mode)
@pytest.mark.xfail(reason="Palm P image is wrong")
def test_p_mode(tmp_path: Path) -> None: def test_p_mode(tmp_path: Path) -> None:
# Arrange # Arrange
mode = "P" mode = "P"

View File

@ -11,7 +11,7 @@ from .helper import assert_image_equal, hopper
def _roundtrip(tmp_path: Path, im: Image.Image) -> None: def _roundtrip(tmp_path: Path, im: Image.Image) -> None:
f = str(tmp_path / "temp.pcx") f = tmp_path / "temp.pcx"
im.save(f) im.save(f)
with Image.open(f) as im2: with Image.open(f) as im2:
assert im2.mode == im.mode assert im2.mode == im.mode
@ -31,7 +31,7 @@ def test_sanity(tmp_path: Path) -> None:
_roundtrip(tmp_path, im) _roundtrip(tmp_path, im)
# Test an unsupported mode # Test an unsupported mode
f = str(tmp_path / "temp.pcx") f = tmp_path / "temp.pcx"
im = hopper("RGBA") im = hopper("RGBA")
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(f) im.save(f)

View File

@ -55,7 +55,7 @@ def test_save_alpha(tmp_path: Path, mode: str) -> None:
def test_p_alpha(tmp_path: Path) -> None: def test_p_alpha(tmp_path: Path) -> None:
# Arrange # Arrange
outfile = str(tmp_path / "temp.pdf") outfile = tmp_path / "temp.pdf"
with Image.open("Tests/images/pil123p.png") as im: with Image.open("Tests/images/pil123p.png") as im:
assert im.mode == "P" assert im.mode == "P"
assert isinstance(im.info["transparency"], bytes) 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: def test_unsupported_mode(tmp_path: Path) -> None:
im = hopper("PA") im = hopper("PA")
outfile = str(tmp_path / "temp_PA.pdf") outfile = tmp_path / "temp_PA.pdf"
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(outfile) im.save(outfile)
@ -89,7 +89,7 @@ def test_unsupported_mode(tmp_path: Path) -> None:
def test_resolution(tmp_path: Path) -> None: def test_resolution(tmp_path: Path) -> None:
im = hopper() im = hopper()
outfile = str(tmp_path / "temp.pdf") outfile = tmp_path / "temp.pdf"
im.save(outfile, resolution=150) im.save(outfile, resolution=150)
with open(outfile, "rb") as fp: 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: def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper() im = hopper()
outfile = str(tmp_path / "temp.pdf") outfile = tmp_path / "temp.pdf"
im.save(outfile, "PDF", **params) im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp: with open(outfile, "rb") as fp:
@ -144,7 +144,7 @@ def test_save_all(tmp_path: Path) -> None:
# Multiframe image # Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im: 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) im.save(outfile, save_all=True)
assert os.path.isfile(outfile) 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: def test_multiframe_normal_save(tmp_path: Path) -> None:
# Test saving a multiframe image without save_all # Test saving a multiframe image without save_all
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
outfile = str(tmp_path / "temp.pdf") outfile = tmp_path / "temp.pdf"
im.save(outfile) im.save(outfile)
assert os.path.isfile(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") @skip_unless_feature("zlib")
class TestFilePng: class TestFilePng:
def get_chunks(self, filename: str) -> list[bytes]: def get_chunks(self, filename: Path) -> list[bytes]:
chunks = [] chunks = []
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
fp.read(8) fp.read(8)
@ -89,7 +89,7 @@ class TestFilePng:
assert version is not None assert version is not None
assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version) 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) hopper("RGB").save(test_file)
@ -250,7 +250,7 @@ class TestFilePng:
# each palette entry # each palette entry
assert len(im.info["transparency"]) == 256 assert len(im.info["transparency"]) == 256
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file) im.save(test_file)
# check if saved image contains same transparency # check if saved image contains same transparency
@ -271,7 +271,7 @@ class TestFilePng:
assert im.info["transparency"] == 164 assert im.info["transparency"] == 164
assert im.getpixel((31, 31)) == 164 assert im.getpixel((31, 31)) == 164
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file) im.save(test_file)
# check if saved image contains same transparency # check if saved image contains same transparency
@ -294,7 +294,7 @@ class TestFilePng:
assert im.getcolors() == [(100, (0, 0, 0, 0))] assert im.getcolors() == [(100, (0, 0, 0, 0))]
im = im.convert("P") im = im.convert("P")
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file) im.save(test_file)
# check if saved image contains same transparency # check if saved image contains same transparency
@ -315,7 +315,7 @@ class TestFilePng:
im_rgba = im.convert("RGBA") im_rgba = im.convert("RGBA")
assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent 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) im.save(test_file)
with Image.open(test_file) as test_im: with Image.open(test_file) as test_im:
@ -329,7 +329,7 @@ class TestFilePng:
def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: def test_save_rgb_single_transparency(self, tmp_path: Path) -> None:
in_file = "Tests/images/caption_6_33_22.png" in_file = "Tests/images/caption_6_33_22.png"
with Image.open(in_file) as im: with Image.open(in_file) as im:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file) im.save(test_file)
def test_load_verify(self) -> None: def test_load_verify(self) -> None:
@ -488,7 +488,7 @@ class TestFilePng:
im = hopper("P") im = hopper("P")
im.info["transparency"] = 0 im.info["transparency"] = 0
f = str(tmp_path / "temp.png") f = tmp_path / "temp.png"
im.save(f) im.save(f)
with Image.open(f) as im2: with Image.open(f) as im2:
@ -549,7 +549,7 @@ class TestFilePng:
def test_chunk_order(self, tmp_path: Path) -> None: def test_chunk_order(self, tmp_path: Path) -> None:
with Image.open("Tests/images/icc_profile.png") as im: 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)) im.convert("P").save(test_file, dpi=(100, 100))
chunks = self.get_chunks(test_file) chunks = self.get_chunks(test_file)
@ -576,6 +576,7 @@ class TestFilePng:
def test_read_private_chunks(self) -> None: def test_read_private_chunks(self) -> None:
with Image.open("Tests/images/exif.png") as im: with Image.open("Tests/images/exif.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.private_chunks == [(b"orNT", b"\x01")] assert im.private_chunks == [(b"orNT", b"\x01")]
def test_roundtrip_private_chunk(self) -> None: def test_roundtrip_private_chunk(self) -> None:
@ -598,6 +599,7 @@ class TestFilePng:
def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/hopper.png") as im: with Image.open("Tests/images/hopper.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert "comment" in im.text assert "comment" in im.text
for k, v in { for k, v in {
"date:create": "2014-09-04T09:37:08+03:00", "date:create": "2014-09-04T09:37:08+03:00",
@ -607,15 +609,19 @@ class TestFilePng:
# Raises a SyntaxError in load_end # Raises a SyntaxError in load_end
with Image.open("Tests/images/broken_data_stream.png") as im: with Image.open("Tests/images/broken_data_stream.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(OSError): with pytest.raises(OSError):
assert isinstance(im.text, dict) assert isinstance(im.text, dict)
# Raises an EOFError in load_end # Raises an EOFError in load_end
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: 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"} assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
# Raises a UnicodeDecodeError in load_end # Raises a UnicodeDecodeError in load_end
with Image.open("Tests/images/truncated_image.png") as im: with Image.open("Tests/images/truncated_image.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
# The file is truncated # The file is truncated
with pytest.raises(OSError): with pytest.raises(OSError):
im.text im.text
@ -661,7 +667,7 @@ class TestFilePng:
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
im = hopper("P") im = hopper("P")
out = str(tmp_path / "temp.png") out = tmp_path / "temp.png"
im.save(out, bits=4, save_all=save_all) im.save(out, bits=4, save_all=save_all)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -671,8 +677,8 @@ class TestFilePng:
im = Image.new("P", (1, 1)) im = Image.new("P", (1, 1))
im.putpalette((1, 1, 1)) im.putpalette((1, 1, 1))
out = str(tmp_path / "temp.png") out = tmp_path / "temp.png"
im.save(str(tmp_path / "temp.png")) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 3 assert len(reloaded.png.im_palette[1]) == 3
@ -721,11 +727,12 @@ class TestFilePng:
def test_exif_save(self, tmp_path: Path) -> None: def test_exif_save(self, tmp_path: Path) -> None:
# Test exif is not saved from info # 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: with Image.open("Tests/images/exif.png") as im:
im.save(test_file) im.save(test_file)
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
assert reloaded._getexif() is None assert reloaded._getexif() is None
# Test passing in exif # Test passing in exif
@ -741,7 +748,7 @@ class TestFilePng:
) )
def test_exif_from_jpg(self, tmp_path: Path) -> None: def test_exif_from_jpg(self, tmp_path: Path) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im: 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()) im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
@ -750,7 +757,7 @@ class TestFilePng:
def test_exif_argument(self, tmp_path: Path) -> None: def test_exif_argument(self, tmp_path: Path) -> None:
with Image.open(TEST_PNG_FILE) as im: 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") im.save(test_file, exif=b"exifstring")
with Image.open(test_file) as reloaded: 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: def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im: 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") im.save(filename, "PPM")
assert_image_equal_tofile(im, filename) 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: with Image.open("Tests/images/hopper.pnm") as im:
assert_image_similar(im, hopper(), 0.0001) assert_image_similar(im, hopper(), 0.0001)
filename = str(tmp_path / "temp.pnm") filename = tmp_path / "temp.pnm"
im.save(filename) im.save(filename)
assert_image_equal_tofile(im, 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 im.info["scale"] == 1.0
assert_image_equal(im, hopper("F")) assert_image_equal(im, hopper("F"))
filename = str(tmp_path / "tmp.pfm") filename = tmp_path / "tmp.pfm"
im.save(filename) im.save(filename)
assert_image_equal_tofile(im, 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 im.info["scale"] == 2.5
assert_image_equal(im, hopper("F")) assert_image_equal(im, hopper("F"))
filename = str(tmp_path / "tmp.pfm") filename = tmp_path / "tmp.pfm"
im.save(filename) im.save(filename)
assert_image_equal_tofile(im, filename) assert_image_equal_tofile(im, filename)
@ -194,8 +194,8 @@ def test_16bit_plain_pgm() -> None:
def test_plain_data_with_comment( def test_plain_data_with_comment(
tmp_path: Path, header: bytes, data: bytes, comment_count: int tmp_path: Path, header: bytes, data: bytes, comment_count: int
) -> None: ) -> None:
path1 = str(tmp_path / "temp1.ppm") path1 = tmp_path / "temp1.ppm"
path2 = str(tmp_path / "temp2.ppm") path2 = tmp_path / "temp2.ppm"
comment = b"# comment" * comment_count comment = b"# comment" * comment_count
with open(path1, "wb") as f1, open(path2, "wb") as f2: with open(path1, "wb") as f1, open(path2, "wb") as f2:
f1.write(header + b"\n\n" + data) 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")) @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: 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: with open(path, "wb") as f:
f.write(data) 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")) @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: 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: with open(path, "wb") as f:
f.write(data) 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: 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: with open(path, "wb") as f:
f.write(data) 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: 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: with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n-1") 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: 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: with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n256") 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: 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: with open(path, "wb") as f:
f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") 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: 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: with open(path, "wb") as f:
f.write(b"P6\nTEST") f.write(b"P6\nTEST")
@ -289,7 +289,7 @@ def test_non_integer_token(tmp_path: Path) -> None:
def test_header_token_too_long(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: with open(path, "wb") as f:
f.write(b"P6\n 01234567890") f.write(b"P6\n 01234567890")
@ -300,7 +300,7 @@ def test_header_token_too_long(tmp_path: Path) -> None:
def test_truncated_file(tmp_path: Path) -> None: def test_truncated_file(tmp_path: Path) -> None:
# Test EOF in header # Test EOF in header
path = str(tmp_path / "temp.pgm") path = tmp_path / "temp.pgm"
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P6") f.write(b"P6")
@ -316,7 +316,7 @@ def test_truncated_file(tmp_path: Path) -> None:
def test_not_enough_image_data(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: with open(path, "wb") as f:
f.write(b"P2 1 2 255 255") f.write(b"P2 1 2 255 255")
@ -327,7 +327,7 @@ def test_not_enough_image_data(tmp_path: Path) -> None:
@pytest.mark.parametrize("maxval", (b"0", b"65536")) @pytest.mark.parametrize("maxval", (b"0", b"65536"))
def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: 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: with open(path, "wb") as f:
f.write(b"P6\n3 1 " + maxval) f.write(b"P6\n3 1 " + maxval)
@ -350,7 +350,7 @@ def test_neg_ppm() -> None:
def test_mimetypes(tmp_path: Path) -> 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: with open(path, "wb") as f:
f.write(b"P4\n128 128\n255") f.write(b"P4\n128 128\n255")

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,13 @@ from types import ModuleType
import pytest 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 PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
from .helper import ( from .helper import (
@ -31,7 +37,7 @@ except ImportError:
class TestFileTiff: class TestFileTiff:
def test_sanity(self, tmp_path: Path) -> None: def test_sanity(self, tmp_path: Path) -> None:
filename = str(tmp_path / "temp.tif") filename = tmp_path / "temp.tif"
hopper("RGB").save(filename) hopper("RGB").save(filename)
@ -112,20 +118,23 @@ class TestFileTiff:
assert_image_equal_tofile(im, "Tests/images/hopper.tif") assert_image_equal_tofile(im, "Tests/images/hopper.tif")
with Image.open("Tests/images/hopper_bigtiff.tif") as im: 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) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_bigtiff_save(self, tmp_path: Path) -> None: def test_bigtiff_save(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
im = hopper() im = hopper()
im.save(outfile, big_tiff=True) im.save(outfile, big_tiff=True)
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2._bigtiff is True assert reloaded.tag_v2._bigtiff is True
im.save(outfile, save_all=True, append_images=[im], big_tiff=True) im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2._bigtiff is True assert reloaded.tag_v2._bigtiff is True
def test_seek_too_large(self) -> None: def test_seek_too_large(self) -> None:
@ -140,6 +149,8 @@ class TestFileTiff:
def test_xyres_tiff(self) -> None: def test_xyres_tiff(self) -> None:
filename = "Tests/images/pil168.tif" filename = "Tests/images/pil168.tif"
with Image.open(filename) as im: with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# legacy api # legacy api
assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[X_RESOLUTION][0], tuple)
assert isinstance(im.tag[Y_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
@ -153,6 +164,8 @@ class TestFileTiff:
def test_xyres_fallback_tiff(self) -> None: def test_xyres_fallback_tiff(self) -> None:
filename = "Tests/images/compression.tif" filename = "Tests/images/compression.tif"
with Image.open(filename) as im: with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# v2 api # v2 api
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
@ -167,6 +180,8 @@ class TestFileTiff:
def test_int_resolution(self) -> None: def test_int_resolution(self) -> None:
filename = "Tests/images/pil168.tif" filename = "Tests/images/pil168.tif"
with Image.open(filename) as im: with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# Try to read a file where X,Y_RESOLUTION are ints # Try to read a file where X,Y_RESOLUTION are ints
im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[X_RESOLUTION] = 71
im.tag_v2[Y_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71
@ -181,11 +196,12 @@ class TestFileTiff:
with Image.open( with Image.open(
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
) as im: ) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
assert im.info["dpi"] == (dpi, dpi) assert im.info["dpi"] == (dpi, dpi)
def test_save_float_dpi(self, tmp_path: Path) -> None: 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: with Image.open("Tests/images/hopper.tif") as im:
dpi = (72.2, 72.2) dpi = (72.2, 72.2)
im.save(outfile, dpi=dpi) im.save(outfile, dpi=dpi)
@ -198,6 +214,7 @@ class TestFileTiff:
with Image.open("Tests/images/10ct_32bit_128.tiff") as im: with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
im.save(b, format="tiff", resolution=123.45) im.save(b, format="tiff", resolution=123.45)
with Image.open(b) as im: with Image.open(b) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[X_RESOLUTION] == 123.45 assert im.tag_v2[X_RESOLUTION] == 123.45
assert im.tag_v2[Y_RESOLUTION] == 123.45 assert im.tag_v2[Y_RESOLUTION] == 123.45
@ -213,19 +230,21 @@ class TestFileTiff:
TiffImagePlugin.PREFIXES.pop() TiffImagePlugin.PREFIXES.pop()
def test_bad_exif(self) -> None: 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. # Should not raise struct.error.
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
i._getexif() im._getexif()
def test_save_rgba(self, tmp_path: Path) -> None: def test_save_rgba(self, tmp_path: Path) -> None:
im = hopper("RGBA") im = hopper("RGBA")
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
im.save(outfile) im.save(outfile)
def test_save_unsupported_mode(self, tmp_path: Path) -> None: def test_save_unsupported_mode(self, tmp_path: Path) -> None:
im = hopper("HSV") im = hopper("HSV")
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(outfile) im.save(outfile)
@ -307,11 +326,13 @@ class TestFileTiff:
) )
def test_n_frames(self, path: str, n_frames: int) -> None: def test_n_frames(self, path: str, n_frames: int) -> None:
with Image.open(path) as im: with Image.open(path) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == n_frames assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1) assert im.is_animated == (n_frames != 1)
def test_eoferror(self) -> None: def test_eoferror(self) -> None:
with Image.open("Tests/images/multipage-lastframe.tif") as im: with Image.open("Tests/images/multipage-lastframe.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
n_frames = im.n_frames n_frames = im.n_frames
# Test seeking past the last frame # Test seeking past the last frame
@ -355,19 +376,24 @@ class TestFileTiff:
def test_frame_order(self) -> None: def test_frame_order(self) -> None:
# A frame can't progress to itself after reading # A frame can't progress to itself after reading
with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
# A frame can't progress to a frame that has already been read # 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: with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 2 assert im.n_frames == 2
# Frames don't have to be in sequence # Frames don't have to be in sequence
with Image.open("Tests/images/multipage_out_of_order.tiff") as im: with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 3 assert im.n_frames == 3
def test___str__(self) -> None: def test___str__(self) -> None:
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im: with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# Act # Act
ret = str(im.ifd) ret = str(im.ifd)
@ -378,6 +404,8 @@ class TestFileTiff:
# Arrange # Arrange
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im: with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# v2 interface # v2 interface
v2_tags = { v2_tags = {
256: 55, 256: 55,
@ -417,6 +445,7 @@ class TestFileTiff:
def test__delitem__(self) -> None: def test__delitem__(self) -> None:
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im: with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
len_before = len(dict(im.ifd)) len_before = len(dict(im.ifd))
del im.ifd[256] del im.ifd[256]
len_after = len(dict(im.ifd)) len_after = len(dict(im.ifd))
@ -449,6 +478,7 @@ class TestFileTiff:
def test_ifd_tag_type(self) -> None: def test_ifd_tag_type(self) -> None:
with Image.open("Tests/images/ifd_tag_type.tiff") as im: with Image.open("Tests/images/ifd_tag_type.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 0x8825 in im.tag_v2 assert 0x8825 in im.tag_v2
def test_exif(self, tmp_path: Path) -> None: def test_exif(self, tmp_path: Path) -> None:
@ -485,14 +515,14 @@ class TestFileTiff:
assert gps[0] == b"\x03\x02\x00\x00" assert gps[0] == b"\x03\x02\x00\x00"
assert gps[18] == "WGS-84" 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: with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif() exif = im.getexif()
check_exif(exif) check_exif(exif)
im.save(outfile, exif=exif) im.save(outfile, exif=exif)
outfile2 = str(tmp_path / "temp2.tif") outfile2 = tmp_path / "temp2.tif"
with Image.open(outfile) as im: with Image.open(outfile) as im:
exif = im.getexif() exif = im.getexif()
check_exif(exif) check_exif(exif)
@ -504,7 +534,7 @@ class TestFileTiff:
check_exif(exif) check_exif(exif)
def test_modify_exif(self, tmp_path: Path) -> None: 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: with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif() exif = im.getexif()
exif[264] = 100 exif[264] = 100
@ -533,10 +563,11 @@ class TestFileTiff:
@pytest.mark.parametrize("mode", ("1", "L")) @pytest.mark.parametrize("mode", ("1", "L"))
def test_photometric(self, mode: str, tmp_path: Path) -> None: 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 = hopper(mode)
im.save(filename, tiffinfo={262: 0}) im.save(filename, tiffinfo={262: 0})
with Image.open(filename) as reloaded: with Image.open(filename) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[262] == 0 assert reloaded.tag_v2[262] == 0
assert_image_equal(im, reloaded) assert_image_equal(im, reloaded)
@ -612,9 +643,11 @@ class TestFileTiff:
def test_with_underscores(self, tmp_path: Path) -> None: def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} 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) hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im: with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# legacy interface # legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[X_RESOLUTION][0][0] == 72
assert im.tag[Y_RESOLUTION][0][0] == 36 assert im.tag[Y_RESOLUTION][0][0] == 36
@ -630,14 +663,14 @@ class TestFileTiff:
with Image.open(infile) as im: with Image.open(infile) as im:
assert im.getpixel((0, 0)) == pixel_value assert im.getpixel((0, 0)) == pixel_value
tmpfile = str(tmp_path / "temp.tif") tmpfile = tmp_path / "temp.tif"
im.save(tmpfile) im.save(tmpfile)
assert_image_equal_tofile(im, tmpfile) assert_image_equal_tofile(im, tmpfile)
def test_iptc(self, tmp_path: Path) -> None: def test_iptc(self, tmp_path: Path) -> None:
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG # 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: with Image.open("Tests/images/hopper.tif") as im:
im.load() im.load()
assert isinstance(im, TiffImagePlugin.TiffImageFile) assert isinstance(im, TiffImagePlugin.TiffImageFile)
@ -652,7 +685,7 @@ class TestFileTiff:
assert 33723 not in im.tag_v2 assert 33723 not in im.tag_v2
def test_rowsperstrip(self, tmp_path: Path) -> None: def test_rowsperstrip(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
im = hopper() im = hopper()
im.save(outfile, tiffinfo={278: 256}) im.save(outfile, tiffinfo={278: 256})
@ -701,9 +734,10 @@ class TestFileTiff:
def test_planar_configuration_save(self, tmp_path: Path) -> None: def test_planar_configuration_save(self, tmp_path: Path) -> None:
infile = "Tests/images/tiff_tiled_planar_raw.tif" infile = "Tests/images/tiff_tiled_planar_raw.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im._planar_configuration == 2 assert im._planar_configuration == 2
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
im.save(outfile) im.save(outfile)
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
@ -718,7 +752,7 @@ class TestFileTiff:
@pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode: str, tmp_path: Path) -> None: 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 = hopper(mode)
im.save(outfile) im.save(outfile)
@ -733,6 +767,7 @@ class TestFileTiff:
mp.seek(0, os.SEEK_SET) mp.seek(0, os.SEEK_SET)
with Image.open(mp) as im: with Image.open(mp) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 3 assert im.n_frames == 3
# Test appending images # Test appending images
@ -743,6 +778,7 @@ class TestFileTiff:
mp.seek(0, os.SEEK_SET) mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread: with Image.open(mp) as reread:
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
assert reread.n_frames == 3 assert reread.n_frames == 3
# Test appending using a generator # Test appending using a generator
@ -754,6 +790,7 @@ class TestFileTiff:
mp.seek(0, os.SEEK_SET) mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread: with Image.open(mp) as reread:
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
assert reread.n_frames == 3 assert reread.n_frames == 3
def test_fixoffsets(self) -> None: def test_fixoffsets(self) -> None:
@ -812,7 +849,7 @@ class TestFileTiff:
im.info["icc_profile"] = "Dummy value" im.info["icc_profile"] = "Dummy value"
# Try save-load round trip to make sure both handle icc_profile. # 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") im.save(tmpfile, "TIFF", compression="raw")
with Image.open(tmpfile) as reloaded: with Image.open(tmpfile) as reloaded:
assert b"Dummy value" == reloaded.info["icc_profile"] assert b"Dummy value" == reloaded.info["icc_profile"]
@ -821,7 +858,7 @@ class TestFileTiff:
im = hopper() im = hopper()
assert "icc_profile" not in im.info assert "icc_profile" not in im.info
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
icc_profile = b"Dummy value" icc_profile = b"Dummy value"
im.save(outfile, icc_profile=icc_profile) im.save(outfile, icc_profile=icc_profile)
@ -832,11 +869,11 @@ class TestFileTiff:
with Image.open("Tests/images/hopper.bmp") as im: with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["compression"] == 0 assert im.info["compression"] == 0
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
im.save(outfile) im.save(outfile)
def test_discard_icc_profile(self, tmp_path: Path) -> None: 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: with Image.open("Tests/images/icc_profile.png") as im:
assert "icc_profile" in im.info assert "icc_profile" in im.info
@ -864,6 +901,7 @@ class TestFileTiff:
def test_get_photoshop_blocks(self) -> None: def test_get_photoshop_blocks(self) -> None:
with Image.open("Tests/images/lab.tif") as im: with Image.open("Tests/images/lab.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert list(im.get_photoshop_blocks().keys()) == [ assert list(im.get_photoshop_blocks().keys()) == [
1061, 1061,
1002, 1002,
@ -889,7 +927,7 @@ class TestFileTiff:
] ]
def test_tiff_chunks(self, tmp_path: Path) -> None: def test_tiff_chunks(self, tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.tif") tmpfile = tmp_path / "temp.tif"
im = hopper() im = hopper()
with open(tmpfile, "wb") as fp: with open(tmpfile, "wb") as fp:
@ -911,7 +949,7 @@ class TestFileTiff:
def test_close_on_load_exclusive(self, tmp_path: Path) -> None: def test_close_on_load_exclusive(self, tmp_path: Path) -> None:
# similar to test_fd_leak, but runs on unixlike os # 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: with Image.open("Tests/images/uint16_1_4660.tif") as im:
im.save(tmpfile) im.save(tmpfile)
@ -923,7 +961,7 @@ class TestFileTiff:
assert fp.closed assert fp.closed
def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: 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: with Image.open("Tests/images/uint16_1_4660.tif") as im:
im.save(tmpfile) im.save(tmpfile)
@ -974,7 +1012,7 @@ class TestFileTiff:
@pytest.mark.skipif(not is_win32(), reason="Windows only") @pytest.mark.skipif(not is_win32(), reason="Windows only")
class TestFileTiffW32: class TestFileTiffW32:
def test_fd_leak(self, tmp_path: Path) -> None: 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. # this is an mmaped file.
with Image.open("Tests/images/uint16_1_4660.tif") as im: 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 info[ImageDescription] = text_data
f = str(tmp_path / "temp.tif") f = tmp_path / "temp.tif"
img.save(f, tiffinfo=info) img.save(f, tiffinfo=info)
with Image.open(f) as loaded: with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
assert loaded.tag_v2[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) info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
img.save(f, tiffinfo=info) img.save(f, tiffinfo=info)
with Image.open(f) as loaded: with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
def test_read_metadata() -> None: def test_read_metadata() -> None:
with Image.open("Tests/images/hopper_g4.tif") as img: with Image.open("Tests/images/hopper_g4.tif") as img:
assert isinstance(img, TiffImagePlugin.TiffImageFile)
assert { assert {
"YResolution": IFDRational(4294967295, 113653537), "YResolution": IFDRational(4294967295, 113653537),
"PlanarConfiguration": 1, "PlanarConfiguration": 1,
@ -128,13 +131,15 @@ def test_read_metadata() -> None:
def test_write_metadata(tmp_path: Path) -> None: def test_write_metadata(tmp_path: Path) -> None:
"""Test metadata writing through the python code""" """Test metadata writing through the python code"""
with Image.open("Tests/images/hopper.tif") as img: 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] del img.tag[278]
img.save(f, tiffinfo=img.tag) img.save(f, tiffinfo=img.tag)
original = img.tag_v2.named() original = img.tag_v2.named()
with Image.open(f) as loaded: with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
reloaded = loaded.tag_v2.named() reloaded = loaded.tag_v2.named()
ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] 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: 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: with Image.open("Tests/images/hopper.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
info = im.tag_v2 info = im.tag_v2
del info[278] del info[278]
@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
im.save(out, tiffinfo=info) im.save(out, tiffinfo=info)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG 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: 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: with Image.open("Tests/images/hopper.Lab.tif") as im:
im.save(out) im.save(out)
@ -227,10 +234,11 @@ def test_writing_other_types_to_ascii(
info[271] = value info[271] = value
im = hopper() im = hopper()
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info) im.save(out, tiffinfo=info)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[271] == expected 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 info[700] = value
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info) im.save(out, tiffinfo=info)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[700] == b"\x01" assert reloaded.tag_v2[700] == b"\x01"
@ -263,10 +272,11 @@ def test_writing_other_types_to_undefined(
info[33723] = value info[33723] = value
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info) im.save(out, tiffinfo=info)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[33723] == b"1" assert reloaded.tag_v2[33723] == b"1"
@ -296,7 +306,7 @@ def test_empty_metadata() -> None:
def test_iccprofile(tmp_path: Path) -> None: def test_iccprofile(tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/1462 # 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: with Image.open("Tests/images/hopper.iccprofile.tif") as im:
im.save(out) im.save(out)
@ -311,19 +321,20 @@ def test_iccprofile_binary() -> None:
# but probably won't be able to save it. # but probably won't be able to save it.
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: 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.tag_v2.tagtype[34675] == 1
assert im.info["icc_profile"] assert im.info["icc_profile"]
def test_iccprofile_save_png(tmp_path: Path) -> None: def test_iccprofile_save_png(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.iccprofile.tif") as im: with Image.open("Tests/images/hopper.iccprofile.tif") as im:
outfile = str(tmp_path / "temp.png") outfile = tmp_path / "temp.png"
im.save(outfile) im.save(outfile)
def test_iccprofile_binary_save_png(tmp_path: Path) -> None: def test_iccprofile_binary_save_png(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: 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) im.save(outfile)
@ -332,10 +343,11 @@ def test_exif_div_zero(tmp_path: Path) -> None:
info = TiffImagePlugin.ImageFileDirectory_v2() info = TiffImagePlugin.ImageFileDirectory_v2()
info[41988] = TiffImagePlugin.IFDRational(0, 0) info[41988] = TiffImagePlugin.IFDRational(0, 0)
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw") im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert 0 == reloaded.tag_v2[41988].numerator assert 0 == reloaded.tag_v2[41988].numerator
assert 0 == reloaded.tag_v2[41988].denominator 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) info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw") im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert max_long == reloaded.tag_v2[41493].numerator assert max_long == reloaded.tag_v2[41493].numerator
assert 1 == reloaded.tag_v2[41493].denominator 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) info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw") im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert max_long == reloaded.tag_v2[41493].numerator assert max_long == reloaded.tag_v2[41493].numerator
assert 1 == reloaded.tag_v2[41493].denominator 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) info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw") im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert numerator == reloaded.tag_v2[37380].numerator assert numerator == reloaded.tag_v2[37380].numerator
assert denominator == reloaded.tag_v2[37380].denominator 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) info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw") im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert numerator == reloaded.tag_v2[37380].numerator assert numerator == reloaded.tag_v2[37380].numerator
assert denominator == reloaded.tag_v2[37380].denominator 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) info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw") im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
assert -1 == reloaded.tag_v2[37380].denominator assert -1 == reloaded.tag_v2[37380].denominator
@ -420,10 +437,11 @@ def test_ifd_signed_long(tmp_path: Path) -> None:
info[37000] = -60000 info[37000] = -60000
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info, compression="raw") im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[37000] == -60000 assert reloaded.tag_v2[37000] == -60000
@ -444,11 +462,13 @@ def test_empty_values() -> None:
def test_photoshop_info(tmp_path: Path) -> None: def test_photoshop_info(tmp_path: Path) -> None:
with Image.open("Tests/images/issue_2278.tif") as im: with Image.open("Tests/images/issue_2278.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[34377]) == 70 assert len(im.tag_v2[34377]) == 70
assert isinstance(im.tag_v2[34377], bytes) assert isinstance(im.tag_v2[34377], bytes)
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
im.save(out) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert len(reloaded.tag_v2[34377]) == 70 assert len(reloaded.tag_v2[34377]) == 70
assert isinstance(reloaded.tag_v2[34377], bytes) 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: def test_empty_subifd(tmp_path: Path) -> None:
out = str(tmp_path / "temp.jpg") out = tmp_path / "temp.jpg"
im = hopper() im = hopper()
exif = im.getexif() exif = im.getexif()

View File

@ -42,7 +42,7 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
Does it have the bits we expect? Does it have the bits we expect?
""" """
temp_file = str(tmp_path / "temp.webp") temp_file = tmp_path / "temp.webp"
# temp_file = "temp.webp" # temp_file = "temp.webp"
pil_image = hopper("RGBA") pil_image = hopper("RGBA")
@ -71,7 +71,7 @@ def test_write_rgba(tmp_path: Path) -> None:
Does it have the bits we expect? 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 = Image.new("RGBA", (10, 10), (255, 0, 0, 20))
pil_image.save(temp_file) 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) half_transparent_image.putalpha(new_alpha)
# save with transparent area preserved # 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) half_transparent_image.save(temp_file, exact=True, lossless=True)
with Image.open(temp_file) as reloaded: 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. 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" file_path = "Tests/images/transparent.gif"
with Image.open(file_path) as im: with Image.open(file_path) as im:
im.save(temp_file) 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: def test_alpha_quality(tmp_path: Path) -> None:
with Image.open("Tests/images/transparent.png") as im: with Image.open("Tests/images/transparent.png") as im:
out = str(tmp_path / "temp.webp") out = tmp_path / "temp.webp"
im.save(out) im.save(out)
out_quality = str(tmp_path / "quality.webp") out_quality = tmp_path / "quality.webp"
im.save(out_quality, alpha_quality=50) im.save(out_quality, alpha_quality=50)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
with Image.open(out_quality) as reloaded_quality: with Image.open(out_quality) as reloaded_quality:

View File

@ -6,7 +6,7 @@ from pathlib import Path
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
from PIL import Image, features from PIL import GifImagePlugin, Image, WebPImagePlugin, features
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -22,10 +22,12 @@ def test_n_frames() -> None:
"""Ensure that WebP format sets n_frames and is_animated attributes correctly.""" """Ensure that WebP format sets n_frames and is_animated attributes correctly."""
with Image.open("Tests/images/hopper.webp") as im: with Image.open("Tests/images/hopper.webp") as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
with Image.open("Tests/images/iss634.webp") as im: with Image.open("Tests/images/iss634.webp") as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 42 assert im.n_frames == 42
assert im.is_animated 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: with Image.open("Tests/images/iss634.gif") as orig:
assert isinstance(orig, GifImagePlugin.GifImageFile)
assert orig.n_frames > 1 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) orig.save(temp_file, save_all=True)
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == orig.n_frames assert im.n_frames == orig.n_frames
# Compare first and last frames to the original animated GIF # 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. 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: with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 2 assert im.n_frames == 2
# Compare first frame to original # 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_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2: 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( frame1.copy().save(
temp_file1, save_all=True, append_images=[frame2], lossless=True 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]: ) -> Generator[Image.Image, None, None]:
yield from ims yield from ims
temp_file2 = str(tmp_path / "temp_generator.webp") temp_file2 = tmp_path / "temp_generator.webp"
frame1.copy().save( frame1.copy().save(
temp_file2, temp_file2,
save_all=True, save_all=True,
@ -116,7 +121,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
""" """
durations = [0, 10, 20, 30, 40] 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_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2: with Image.open("Tests/images/anim_frame2.webp") as frame2:
frame1.save( frame1.save(
@ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
) )
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 5 assert im.n_frames == 5
assert im.is_animated 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: 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: with Image.open("Tests/images/iss634.apng") as im:
assert im.info["duration"] == 70.0 assert im.info["duration"] == 70.0
@ -159,7 +165,7 @@ def test_seeking(tmp_path: Path) -> None:
""" """
dur = 33 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_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2: with Image.open("Tests/images/anim_frame2.webp") as frame2:
frame1.save( frame1.save(
@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None:
) )
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 5 assert im.n_frames == 5
assert im.is_animated 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: with Image.open("Tests/images/transparent.png") as im:
first_frame = Image.new("L", im.size) 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]) 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( first_frame.save(
out_quality, save_all=True, append_images=[im], alpha_quality=50 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: 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) hopper(RGB_MODE).save(temp_file, lossless=True)

View File

@ -6,7 +6,7 @@ from types import ModuleType
import pytest import pytest
from PIL import Image from PIL import Image, WebPImagePlugin
from .helper import mark_if_feature_version, skip_unless_feature from .helper import mark_if_feature_version, skip_unless_feature
@ -110,6 +110,7 @@ def test_read_no_exif() -> None:
test_buffer.seek(0) test_buffer.seek(0)
with Image.open(test_buffer) as webp_image: with Image.open(test_buffer) as webp_image:
assert isinstance(webp_image, WebPImagePlugin.WebPImageFile)
assert not webp_image._getexif() assert not webp_image._getexif()
@ -146,7 +147,7 @@ def test_write_animated_metadata(tmp_path: Path) -> None:
exif_data = b"<exif_data>" exif_data = b"<exif_data>"
xmp_data = b"<xmp_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_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2: with Image.open("Tests/images/anim_frame2.webp") as frame2:
frame1.save( frame1.save(

View File

@ -8,7 +8,7 @@ import pytest
from PIL import Image, ImageFile, WmfImagePlugin 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: def test_load_raw() -> None:
@ -44,6 +44,15 @@ def test_load_zero_inch() -> None:
pass 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: def test_register_handler(tmp_path: Path) -> None:
class TestHandler(ImageFile.StubHandler): class TestHandler(ImageFile.StubHandler):
methodCalled = False methodCalled = False
@ -59,7 +68,7 @@ def test_register_handler(tmp_path: Path) -> None:
WmfImagePlugin.register_handler(handler) WmfImagePlugin.register_handler(handler)
im = hopper() im = hopper()
tmpfile = str(tmp_path / "temp.wmf") tmpfile = tmp_path / "temp.wmf"
im.save(tmpfile) im.save(tmpfile)
assert handler.methodCalled assert handler.methodCalled
@ -80,6 +89,7 @@ def test_load_float_dpi() -> None:
def test_load_set_dpi() -> None: def test_load_set_dpi() -> None:
with Image.open("Tests/images/drawing.wmf") as im: with Image.open("Tests/images/drawing.wmf") as im:
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
assert im.size == (82, 82) assert im.size == (82, 82)
if hasattr(Image.core, "drawwmf"): 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) 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")) @pytest.mark.parametrize("ext", (".wmf", ".emf"))
def test_save(ext: str, tmp_path: Path) -> None: def test_save(ext: str, tmp_path: Path) -> None:
im = hopper() im = hopper()
tmpfile = str(tmp_path / ("temp" + ext)) tmpfile = tmp_path / ("temp" + ext)
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(tmpfile) im.save(tmpfile)

View File

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

View File

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

View File

@ -175,6 +175,13 @@ class TestImage:
with Image.open(io.StringIO()): # type: ignore[arg-type] with Image.open(io.StringIO()): # type: ignore[arg-type]
pass 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: def test_pathlib(self, tmp_path: Path) -> None:
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
assert im.mode == "P" assert im.mode == "P"
@ -187,14 +194,13 @@ class TestImage:
for ext in (".jpg", ".jp2"): for ext in (".jpg", ".jp2"):
if ext == ".jp2" and not features.check_codec("jpg_2000"): if ext == ".jp2" and not features.check_codec("jpg_2000"):
pytest.skip("jpg_2000 not available") pytest.skip("jpg_2000 not available")
temp_file = str(tmp_path / ("temp." + ext)) im.save(tmp_path / ("temp." + ext))
im.save(Path(temp_file))
def test_fp_name(self, tmp_path: Path) -> None: 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): class FP(io.BytesIO):
name: str name: Path
if sys.version_info >= (3, 12): if sys.version_info >= (3, 12):
from collections.abc import Buffer from collections.abc import Buffer
@ -224,10 +230,10 @@ class TestImage:
assert_image_similar(im, reloaded, 20) assert_image_similar(im, reloaded, 20)
def test_unknown_extension(self, tmp_path: Path) -> None: def test_unknown_extension(self, tmp_path: Path) -> None:
im = hopper() temp_file = tmp_path / "temp.unknown"
temp_file = str(tmp_path / "temp.unknown") with hopper() as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(temp_file) im.save(temp_file)
def test_internals(self) -> None: def test_internals(self) -> None:
im = Image.new("L", (100, 100)) im = Image.new("L", (100, 100))
@ -245,7 +251,7 @@ class TestImage:
reason="Test requires opening an mmaped file for writing", reason="Test requires opening an mmaped file for writing",
) )
def test_readonly_save(self, tmp_path: Path) -> None: 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) shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file)
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
@ -728,7 +734,7 @@ class TestImage:
# https://github.com/python-pillow/Pillow/issues/835 # https://github.com/python-pillow/Pillow/issues/835
# Arrange # Arrange
test_file = "Tests/images/hopper.png" test_file = "Tests/images/hopper.png"
temp_file = str(tmp_path / "temp.jpg") temp_file = tmp_path / "temp.jpg"
# Act/Assert # Act/Assert
with Image.open(test_file) as im: with Image.open(test_file) as im:
@ -738,7 +744,7 @@ class TestImage:
im.save(temp_file) im.save(temp_file)
def test_no_new_file_on_error(self, tmp_path: Path) -> None: 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)) im = Image.new("RGB", (0, 0))
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -805,7 +811,7 @@ class TestImage:
assert exif[296] == 2 assert exif[296] == 2
assert exif[11] == "gThumb 3.0.1" assert exif[11] == "gThumb 3.0.1"
out = str(tmp_path / "temp.jpg") out = tmp_path / "temp.jpg"
exif[258] = 8 exif[258] = 8
del exif[274] del exif[274]
del exif[282] del exif[282]
@ -827,7 +833,7 @@ class TestImage:
assert exif[274] == 1 assert exif[274] == 1
assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)"
out = str(tmp_path / "temp.jpg") out = tmp_path / "temp.jpg"
exif[258] = 8 exif[258] = 8
del exif[306] del exif[306]
exif[274] = 455 exif[274] = 455
@ -846,7 +852,7 @@ class TestImage:
exif = im.getexif() exif = im.getexif()
assert exif == {} assert exif == {}
out = str(tmp_path / "temp.webp") out = tmp_path / "temp.webp"
exif[258] = 8 exif[258] = 8
exif[40963] = 455 exif[40963] = 455
exif[305] = "Pillow test" exif[305] = "Pillow test"
@ -868,7 +874,7 @@ class TestImage:
exif = im.getexif() exif = im.getexif()
assert exif == {274: 1} assert exif == {274: 1}
out = str(tmp_path / "temp.png") out = tmp_path / "temp.png"
exif[258] = 8 exif[258] = 8
del exif[274] del exif[274]
exif[40963] = 455 exif[40963] = 455

View File

@ -118,7 +118,7 @@ def test_trns_p(tmp_path: Path) -> None:
im = hopper("P") im = hopper("P")
im.info["transparency"] = 0 im.info["transparency"] = 0
f = str(tmp_path / "temp.png") f = tmp_path / "temp.png"
im_l = im.convert("L") im_l = im.convert("L")
assert im_l.info["transparency"] == 0 assert im_l.info["transparency"] == 0
@ -154,7 +154,7 @@ def test_trns_l(tmp_path: Path) -> None:
im = hopper("L") im = hopper("L")
im.info["transparency"] = 128 im.info["transparency"] = 128
f = str(tmp_path / "temp.png") f = tmp_path / "temp.png"
im_la = im.convert("LA") im_la = im.convert("LA")
assert "transparency" not in im_la.info assert "transparency" not in im_la.info
@ -177,7 +177,7 @@ def test_trns_RGB(tmp_path: Path) -> None:
im = hopper("RGB") im = hopper("RGB")
im.info["transparency"] = im.getpixel((0, 0)) im.info["transparency"] = im.getpixel((0, 0))
f = str(tmp_path / "temp.png") f = tmp_path / "temp.png"
im_l = im.convert("L") im_l = im.convert("L")
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone 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 # platforms. So if a future Pillow change requires that the test file
# be updated, that is okay. # be updated, that is okay.
im = hopper().resize((64, 64)) im = hopper().resize((64, 64))
temp_file = str(tmp_path / "temp.gif") temp_file = tmp_path / "temp.gif"
im.save(temp_file) im.save(temp_file)
with Image.open(temp_file) as reloaded: 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: def test_split_open(tmp_path: Path) -> None:
if features.check("zlib"): if features.check("zlib"):
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
else: else:
test_file = str(tmp_path / "temp.pcx") test_file = tmp_path / "temp.pcx"
def split_open(mode: str) -> int: def split_open(mode: str) -> int:
hopper(mode).save(test_file) hopper(mode).save(test_file)

View File

@ -1704,7 +1704,7 @@ def test_discontiguous_corners_polygon() -> None:
BLACK, BLACK,
) )
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png") 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: def test_polygon2() -> None:

View File

@ -131,6 +131,26 @@ class TestImageFile:
assert_image_equal(im1, im2) 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: def test_raise_oserror(self) -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
with pytest.raises(OSError): with pytest.raises(OSError):

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: 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: try:
shutil.copy(FONT_PATH, tempfile) shutil.copy(FONT_PATH, tempfile)
except UnicodeEncodeError: except UnicodeEncodeError:

View File

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

View File

@ -88,7 +88,7 @@ if is_win32():
def test_pointer(tmp_path: Path) -> None: def test_pointer(tmp_path: Path) -> None:
im = hopper() im = hopper()
(width, height) = im.size (width, height) = im.size
opath = str(tmp_path / "temp.png") opath = tmp_path / "temp.png"
imdib = ImageWin.Dib(im) imdib = ImageWin.Dib(im)
hdr = BITMAPINFOHEADER() 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)) im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(im_out) # transform verify(im_out) # transform
filename = str(tmp_path / "temp.im") filename = tmp_path / "temp.im"
im_in.save(filename) im_in.save(filename)
with Image.open(filename) as im_out: with Image.open(filename) as im_out:

View File

@ -18,7 +18,7 @@ def helper_pickle_file(
) -> None: ) -> None:
# Arrange # Arrange
with Image.open(test_file) as im: with Image.open(test_file) as im:
filename = str(tmp_path / "temp.pkl") filename = tmp_path / "temp.pkl"
if mode: if mode:
im = im.convert(mode) im = im.convert(mode)
@ -87,7 +87,7 @@ def test_pickle_jpeg() -> None:
def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
# Arrange # Arrange
filename = str(tmp_path / "temp.pkl") filename = tmp_path / "temp.pkl"
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
im = im.convert("PA") im = im.convert("PA")
@ -151,7 +151,7 @@ def test_pickle_font_string(protocol: int) -> None:
def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: def test_pickle_font_file(tmp_path: Path, protocol: int) -> None:
# Arrange # Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE) font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
filename = str(tmp_path / "temp.pkl") filename = tmp_path / "temp.pkl"
# Act: roundtrip # Act: roundtrip
with open(filename, "wb") as f: 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 # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript
# Arrange # Arrange
tempfile = str(tmp_path / "temp.ps") tempfile = tmp_path / "temp.ps"
with open(tempfile, "wb") as fp: with open(tempfile, "wb") as fp:
# Act # Act
ps = PSDraw.PSDraw(fp) ps = PSDraw.PSDraw(fp)

View File

@ -35,10 +35,11 @@ class TestShellInjection:
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
def test_load_djpeg_filename(self, tmp_path: Path) -> None: def test_load_djpeg_filename(self, tmp_path: Path) -> None:
for filename in test_filenames: for filename in test_filenames:
src_file = str(tmp_path / filename) src_file = tmp_path / filename
shutil.copy(TEST_JPG, src_file) shutil.copy(TEST_JPG, src_file)
with Image.open(src_file) as im: with Image.open(src_file) as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
im.load_djpeg() im.load_djpeg()
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") @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 monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
) -> None: ) -> None:
im = hopper() im = hopper()
out = str(tmp_path / "temp.tiff") out = tmp_path / "temp.tiff"
res = IFDRational(301, 1) res = IFDRational(301, 1)
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
im.save(out, dpi=(res, res), compression="raw") im.save(out, dpi=(res, res), compression="raw")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])

View File

@ -312,8 +312,9 @@ following options are available::
im.save(out, save_all=True, append_images=[im1, im2, ...]) im.save(out, save_all=True, append_images=[im1, im2, ...])
**save_all** **save_all**
If present and true, all frames of the image will be saved. If If present and true, or if ``append_images`` is not empty, all frames of
not, then only the first frame of a multiframe image will be saved. the image will be saved. Otherwise, only the first frame of a multiframe
image will be saved.
**append_images** **append_images**
A list of images to append as additional frames. Each of the A list of images to append as additional frames. Each of the
@ -800,8 +801,8 @@ Saving
When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default 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`` 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 argument is present and true, or if ``append_images`` is not empty, all frames
option will also be available. will be saved.
**append_images** **append_images**
A list of images to append as additional pictures. Each of the A list of images to append as additional pictures. Each of the
@ -1011,7 +1012,8 @@ Saving
When calling :py:meth:`~PIL.Image.Image.save`, by default only a single frame PNG file 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`` 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** **default_image**
Boolean value, specifying whether or not the base image is a default image. Boolean value, specifying whether or not the base image is a default image.
@ -1240,7 +1242,8 @@ Saving
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**save_all** **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 .. versionadded:: 3.4.0
@ -1390,8 +1393,8 @@ Saving sequences
When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default 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`` 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 argument is present and true, or if ``append_images`` is not empty, all frames
options will also be available. will be saved, and the following options will also be available.
**append_images** **append_images**
A list of images to append as additional frames. Each of the A list of images to append as additional frames. Each of the
@ -1693,15 +1696,14 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
**save_all** **save_all**
If a multiframe image is used, by default, only the first image will be saved. 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`` 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 .. versionadded:: 3.0.0
**append_images** **append_images**
A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each 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`` of the images in the list can be single or multiframe images.
parameter must be present and set to ``True`` in conjunction with
``append_images``.
.. versionadded:: 4.2.0 .. 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 # Save the images as an animated GIF
images[0].save( images[0].save(
"animated_hopper.gif", "animated_hopper.gif",
save_all=True,
append_images=images[1:], append_images=images[1:],
duration=500, # duration of each frame in milliseconds duration=500, # duration of each frame in milliseconds
loop=0, # loop forever loop=0, # loop forever

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. 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 .. py:attribute:: media_white_point_temperature
:type: float | None :type: float | None

View File

@ -4,21 +4,12 @@
Security Security
======== ========
TODO Undefined shift when loading compressed DDS images
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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
:cve:`YYYY-XXXXX`: TODO present since Pillow 3.4.0.
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations Deprecations
============ ============
@ -36,10 +27,14 @@ an :py:class:`PIL.ImageFile.ImageFile` instance.
API Changes 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 API Additions
============= =============
@ -73,11 +68,3 @@ Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1,
DXT5, BC2, BC3 and BC5 are supported:: DXT5, BC2, BC3 and BC5 are supported::
im.save("out.dds", pixel_format="DXT1") im.save("out.dds", pixel_format="DXT1")
Other Changes
=============
TODO
^^^^
TODO

View File

@ -48,6 +48,8 @@ BIT2MODE = {
32: ("RGB", "BGRX"), 32: ("RGB", "BGRX"),
} }
USE_RAW_ALPHA = False
def _accept(prefix: bytes) -> bool: def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"BM") return prefix.startswith(b"BM")
@ -242,7 +244,9 @@ class BmpImageFile(ImageFile.ImageFile):
msg = "Unsupported BMP bitfields layout" msg = "Unsupported BMP bitfields layout"
raise OSError(msg) raise OSError(msg)
elif file_info["compression"] == self.COMPRESSIONS["RAW"]: 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" raw_mode, self._mode = "BGRA", "RGBA"
elif file_info["compression"] in ( elif file_info["compression"] in (
self.COMPRESSIONS["RLE8"], self.COMPRESSIONS["RLE8"],

View File

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

View File

@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16 from ._binary import i16le as i16
from ._binary import i32le as i32 from ._binary import i32le as i32
from ._binary import o8 from ._binary import o8
from ._util import DeferredError
# #
# decoder # decoder
@ -134,6 +135,8 @@ class FliImageFile(ImageFile.ImageFile):
self._seek(f) self._seek(f)
def _seek(self, frame: int) -> None: def _seek(self, frame: int) -> None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
if frame == 0: if frame == 0:
self.__frame = -1 self.__frame = -1
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)

View File

@ -31,7 +31,7 @@ import os
import subprocess import subprocess
from enum import IntEnum from enum import IntEnum
from functools import cached_property from functools import cached_property
from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union from typing import IO, Any, Literal, NamedTuple, Union
from . import ( from . import (
Image, Image,
@ -45,7 +45,9 @@ from . import (
from ._binary import i16le as i16 from ._binary import i16le as i16
from ._binary import o8 from ._binary import o8
from ._binary import o16le as o16 from ._binary import o16le as o16
from ._util import DeferredError
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from . import _imaging from . import _imaging
from ._typing import Buffer from ._typing import Buffer
@ -167,6 +169,8 @@ class GifImageFile(ImageFile.ImageFile):
raise EOFError(msg) from e raise EOFError(msg) from e
def _seek(self, frame: int, update_image: bool = True) -> None: def _seek(self, frame: int, update_image: bool = True) -> None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
if frame == 0: if frame == 0:
# rewind # rewind
self.__offset = 0 self.__offset = 0

View File

@ -16,6 +16,7 @@
from __future__ import annotations from __future__ import annotations
import re import re
from io import BytesIO
from typing import IO from typing import IO
@ -24,13 +25,18 @@ class GimpPaletteFile:
rawmode = "RGB" rawmode = "RGB"
def __init__(self, fp: IO[bytes]) -> None: def _read(self, fp: IO[bytes], limit: bool = True) -> None:
if not fp.readline().startswith(b"GIMP Palette"): if not fp.readline().startswith(b"GIMP Palette"):
msg = "not a GIMP palette file" msg = "not a GIMP palette file"
raise SyntaxError(msg) raise SyntaxError(msg)
palette: list[int] = [] palette: list[int] = []
for _ in range(256): i = 0
while True:
if limit and i == 256 + 3:
break
i += 1
s = fp.readline() s = fp.readline()
if not s: if not s:
break break
@ -38,7 +44,7 @@ class GimpPaletteFile:
# skip fields and comment lines # skip fields and comment lines
if re.match(rb"\w+:|#", s): if re.match(rb"\w+:|#", s):
continue continue
if len(s) > 100: if limit and len(s) > 100:
msg = "bad palette file" msg = "bad palette file"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -48,8 +54,19 @@ class GimpPaletteFile:
raise ValueError(msg) raise ValueError(msg)
palette += (int(v[i]) for i in range(3)) palette += (int(v[i]) for i in range(3))
if limit and len(palette) == 768:
break
self.palette = bytes(palette) self.palette = bytes(palette)
def __init__(self, fp: IO[bytes]) -> None:
self._read(fp)
@classmethod
def frombytes(cls, data: bytes) -> GimpPaletteFile:
self = cls.__new__(cls)
self._read(BytesIO(data), False)
return self
def getpalette(self) -> tuple[bytes, str]: def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode return self.palette, self.rawmode

View File

@ -31,6 +31,7 @@ import re
from typing import IO, Any from typing import IO, Any
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._util import DeferredError
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Standard tags # Standard tags
@ -290,6 +291,8 @@ class ImImageFile(ImageFile.ImageFile):
def seek(self, frame: int) -> None: def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.frame = frame self.frame = frame

View File

@ -41,14 +41,7 @@ import warnings
from collections.abc import Callable, Iterator, MutableMapping, Sequence from collections.abc import Callable, Iterator, MutableMapping, Sequence
from enum import IntEnum from enum import IntEnum
from types import ModuleType from types import ModuleType
from typing import ( from typing import IO, Any, Literal, Protocol, cast
IO,
TYPE_CHECKING,
Any,
Literal,
Protocol,
cast,
)
# VERSION was removed in Pillow 6.0.0. # VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
@ -218,6 +211,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Registries # Registries
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
import mmap import mmap
from xml.etree.ElementTree import Element from xml.etree.ElementTree import Element
@ -548,7 +542,6 @@ class Image:
def __init__(self) -> None: def __init__(self) -> None:
# FIXME: take "new" parameters / other image? # FIXME: take "new" parameters / other image?
# FIXME: turn mode and size into delegating properties?
self._im: core.ImagingCore | DeferredError | None = None self._im: core.ImagingCore | DeferredError | None = None
self._mode = "" self._mode = ""
self._size = (0, 0) self._size = (0, 0)
@ -622,6 +615,8 @@ class Image:
more information. more information.
""" """
if getattr(self, "map", None): if getattr(self, "map", None):
if sys.platform == "win32" and hasattr(sys, "pypy_version_info"):
self.map.close()
self.map: mmap.mmap | None = None self.map: mmap.mmap | None = None
# Instead of simply setting to None, we're setting up a # Instead of simply setting to None, we're setting up a
@ -2513,13 +2508,6 @@ class Image:
# only set the name for metadata purposes # only set the name for metadata purposes
filename = os.fspath(fp.name) filename = os.fspath(fp.name)
# may mutate self!
self._ensure_mutable()
save_all = params.pop("save_all", False)
self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params}
self.encoderconfig: tuple[Any, ...] = ()
preinit() preinit()
filename_ext = os.path.splitext(filename)[1].lower() filename_ext = os.path.splitext(filename)[1].lower()
@ -2534,9 +2522,20 @@ class Image:
msg = f"unknown file extension: {ext}" msg = f"unknown file extension: {ext}"
raise ValueError(msg) from e raise ValueError(msg) from e
# may mutate self!
self._ensure_mutable()
save_all = params.pop("save_all", None)
self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params}
self.encoderconfig: tuple[Any, ...] = ()
if format.upper() not in SAVE: if format.upper() not in SAVE:
init() init()
if save_all: if save_all or (
save_all is None
and params.get("append_images")
and format.upper() in SAVE_ALL
):
save_handler = SAVE_ALL[format.upper()] save_handler = SAVE_ALL[format.upper()]
else: else:
save_handler = SAVE[format.upper()] save_handler = SAVE[format.upper()]

View File

@ -35,7 +35,7 @@ import math
import struct import struct
from collections.abc import Sequence from collections.abc import Sequence
from types import ModuleType from types import ModuleType
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast from typing import Any, AnyStr, Callable, Union, cast
from . import Image, ImageColor from . import Image, ImageColor
from ._deprecate import deprecate from ._deprecate import deprecate
@ -44,6 +44,7 @@ from ._typing import Coords
# experimental access to the outline API # experimental access to the outline API
Outline: Callable[[], Image.core._Outline] = Image.core.outline Outline: Callable[[], Image.core._Outline] = Image.core.outline
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ImageDraw2, ImageFont from . import ImageDraw2, ImageFont

View File

@ -34,13 +34,13 @@ import itertools
import logging import logging
import os import os
import struct import struct
import sys from typing import IO, Any, NamedTuple, cast
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
from . import ExifTags, Image from . import ExifTags, Image
from ._deprecate import deprecate from ._deprecate import deprecate
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
@ -167,7 +167,7 @@ class ImageFile(Image.Image):
pass pass
def _close_fp(self): def _close_fp(self):
if getattr(self, "_fp", False): if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError):
if self._fp != self.fp: if self._fp != self.fp:
self._fp.close() self._fp.close()
self._fp = DeferredError(ValueError("Operation on closed image")) self._fp = DeferredError(ValueError("Operation on closed image"))
@ -278,8 +278,6 @@ class ImageFile(Image.Image):
self.map: mmap.mmap | None = None self.map: mmap.mmap | None = None
use_mmap = self.filename and len(self.tile) == 1 use_mmap = self.filename and len(self.tile) == 1
# As of pypy 2.1.0, memory mapping was failing here.
use_mmap = use_mmap and not hasattr(sys, "pypy_version_info")
readonly = 0 readonly = 0
@ -345,7 +343,7 @@ class ImageFile(Image.Image):
self.tile, lambda tile: (tile[0], tile[1], tile[3]) self.tile, lambda tile: (tile[0], tile[1], tile[3])
) )
] ]
for decoder_name, extents, offset, args in self.tile: for i, (decoder_name, extents, offset, args) in enumerate(self.tile):
seek(offset) seek(offset)
decoder = Image._getdecoder( decoder = Image._getdecoder(
self.mode, decoder_name, args, self.decoderconfig self.mode, decoder_name, args, self.decoderconfig
@ -358,8 +356,13 @@ class ImageFile(Image.Image):
else: else:
b = prefix b = prefix
while True: while True:
read_bytes = self.decodermaxblock
if i + 1 < len(self.tile):
next_offset = self.tile[i + 1].offset
if next_offset > offset:
read_bytes = next_offset - offset
try: try:
s = read(self.decodermaxblock) s = read(read_bytes)
except (IndexError, struct.error) as e: except (IndexError, struct.error) as e:
# truncated png/gif # truncated png/gif
if LOAD_TRUNCATED_IMAGES: if LOAD_TRUNCATED_IMAGES:

View File

@ -20,8 +20,9 @@ import abc
import functools import functools
from collections.abc import Sequence from collections.abc import Sequence
from types import ModuleType from types import ModuleType
from typing import TYPE_CHECKING, Any, Callable, cast from typing import Any, Callable, cast
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from . import _imaging from . import _imaging
from ._typing import NumpyArray from ._typing import NumpyArray

View File

@ -34,12 +34,13 @@ import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from types import ModuleType from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast from typing import IO, Any, BinaryIO, TypedDict, cast
from . import Image, features from . import Image, features
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ImageFile from . import ImageFile
from ._imaging import ImagingFont from ._imaging import ImagingFont

View File

@ -19,10 +19,11 @@ from __future__ import annotations
import array import array
from collections.abc import Sequence from collections.abc import Sequence
from typing import IO, TYPE_CHECKING from typing import IO
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from . import Image from . import Image

View File

@ -19,11 +19,12 @@ from __future__ import annotations
import sys import sys
from io import BytesIO from io import BytesIO
from typing import TYPE_CHECKING, Any, Callable, Union from typing import Any, Callable, Union
from . import Image from . import Image
from ._util import is_path from ._util import is_path
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
import PyQt6 import PyQt6
import PySide6 import PySide6

View File

@ -28,10 +28,11 @@ from __future__ import annotations
import tkinter import tkinter
from io import BytesIO from io import BytesIO
from typing import TYPE_CHECKING, Any from typing import Any
from . import Image, ImageFile from . import Image, ImageFile
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from ._typing import CapsuleType from ._typing import CapsuleType

View File

@ -42,7 +42,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
import warnings import warnings
from typing import IO, TYPE_CHECKING, Any from typing import IO, Any
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -52,6 +52,7 @@ from ._binary import o16be as o16
from ._deprecate import deprecate from ._deprecate import deprecate
from .JpegPresets import presets from .JpegPresets import presets
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from .MpoImagePlugin import MpoImageFile from .MpoImagePlugin import MpoImageFile

View File

@ -32,6 +32,7 @@ from . import (
TiffImagePlugin, TiffImagePlugin,
) )
from ._binary import o32le from ._binary import o32le
from ._util import DeferredError
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
@ -125,11 +126,15 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self.readonly = 1 self.readonly = 1
def load_seek(self, pos: int) -> None: def load_seek(self, pos: int) -> None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self._fp.seek(pos) self._fp.seek(pos)
def seek(self, frame: int) -> None: def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.fp = self._fp self.fp = self._fp
self.offset = self.__mpoffsets[frame] self.offset = self.__mpoffsets[frame]

View File

@ -17,10 +17,13 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from typing import IO, TYPE_CHECKING from typing import IO
from . import EpsImagePlugin from . import EpsImagePlugin
TYPE_CHECKING = False
## ##
# Simple PostScript graphics interface. # Simple PostScript graphics interface.

View File

@ -116,9 +116,6 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00}
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode == "P": if im.mode == "P":
# we assume this is a color Palm image with the standard colormap,
# unless the "info" dict has a "custom-colormap" field
rawmode = "P" rawmode = "P"
bpp = 8 bpp = 8
version = 1 version = 1
@ -172,12 +169,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
compression_type = _COMPRESSION_TYPES["none"] compression_type = _COMPRESSION_TYPES["none"]
flags = 0 flags = 0
if im.mode == "P" and "custom-colormap" in im.info: if im.mode == "P":
assert im.palette is not None flags |= _FLAGS["custom-colormap"]
flags = flags & _FLAGS["custom-colormap"] colormap = im.im.getpalette()
colormapsize = 4 * 256 + 2 colors = len(colormap) // 3
colormapmode = im.palette.mode colormapsize = 4 * colors + 2
colormap = im.getdata().getpalette()
else: else:
colormapsize = 0 colormapsize = 0
@ -196,22 +192,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# now write colormap if necessary # now write colormap if necessary
if colormapsize > 0: if colormapsize:
fp.write(o16b(256)) fp.write(o16b(colors))
for i in range(256): for i in range(colors):
fp.write(o8(i)) fp.write(o8(i))
if colormapmode == "RGB": fp.write(colormap[3 * i : 3 * i + 3])
fp.write(
o8(colormap[3 * i])
+ o8(colormap[3 * i + 1])
+ o8(colormap[3 * i + 2])
)
elif colormapmode == "RGBA":
fp.write(
o8(colormap[4 * i])
+ o8(colormap[4 * i + 1])
+ o8(colormap[4 * i + 2])
)
# now convert data to raw form # now convert data to raw form
ImageFile._save( ImageFile._save(

View File

@ -8,7 +8,7 @@ import os
import re import re
import time import time
import zlib import zlib
from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union from typing import IO, Any, NamedTuple, Union
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
@ -251,6 +251,7 @@ class PdfArray(list[Any]):
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
_DictBase = collections.UserDict[Union[str, bytes], Any] _DictBase = collections.UserDict[Union[str, bytes], Any]
else: else:

View File

@ -40,7 +40,7 @@ import warnings
import zlib import zlib
from collections.abc import Callable from collections.abc import Callable
from enum import IntEnum from enum import IntEnum
from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast from typing import IO, Any, NamedTuple, NoReturn, cast
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -48,7 +48,9 @@ from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._binary import o16be as o16 from ._binary import o16be as o16
from ._binary import o32be as o32 from ._binary import o32be as o32
from ._util import DeferredError
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from . import _imaging from . import _imaging
@ -869,6 +871,8 @@ class PngImageFile(ImageFile.ImageFile):
def _seek(self, frame: int, rewind: bool = False) -> None: def _seek(self, frame: int, rewind: bool = False) -> None:
assert self.png is not None assert self.png is not None
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.dispose: _imaging.ImagingCore | None self.dispose: _imaging.ImagingCore | None
dispose_extent = None dispose_extent = None

View File

@ -27,6 +27,7 @@ from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import si16be as si16 from ._binary import si16be as si16
from ._binary import si32be as si32 from ._binary import si32be as si32
from ._util import DeferredError
MODES = { MODES = {
# (photoshop mode, bits) -> (pil mode, required channels) # (photoshop mode, bits) -> (pil mode, required channels)
@ -148,6 +149,8 @@ class PsdImageFile(ImageFile.ImageFile):
) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]:
layers = [] layers = []
if self._layers_position is not None: if self._layers_position is not None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self._fp.seek(self._layers_position) self._fp.seek(self._layers_position)
_layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size)) _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size))
layers = _layerinfo(_layer_data, self._layers_size) layers = _layerinfo(_layer_data, self._layers_size)
@ -167,6 +170,8 @@ class PsdImageFile(ImageFile.ImageFile):
def seek(self, layer: int) -> None: def seek(self, layer: int) -> None:
if not self._seek_check(layer): if not self._seek_check(layer):
return return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
# seek to given layer (1..max) # seek to given layer (1..max)
_, mode, _, tile = self.layers[layer - 1] _, mode, _, tile = self.layers[layer - 1]

View File

@ -37,9 +37,12 @@ from __future__ import annotations
import os import os
import struct import struct
import sys import sys
from typing import IO, TYPE_CHECKING, Any, cast from typing import IO, Any, cast
from . import Image, ImageFile from . import Image, ImageFile
from ._util import DeferredError
TYPE_CHECKING = False
def isInt(f: Any) -> int: def isInt(f: Any) -> int:
@ -178,6 +181,8 @@ class SpiderImageFile(ImageFile.ImageFile):
raise EOFError(msg) raise EOFError(msg)
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
self.fp = self._fp self.fp = self._fp
self.fp.seek(self.stkoffset) self.fp.seek(self.stkoffset)

View File

@ -35,12 +35,16 @@ class TarIO(ContainerIO.ContainerIO[bytes]):
while True: while True:
s = self.fh.read(512) s = self.fh.read(512)
if len(s) != 512: if len(s) != 512:
self.fh.close()
msg = "unexpected end of tar file" msg = "unexpected end of tar file"
raise OSError(msg) raise OSError(msg)
name = s[:100].decode("utf-8") name = s[:100].decode("utf-8")
i = name.find("\0") i = name.find("\0")
if i == 0: if i == 0:
self.fh.close()
msg = "cannot find subfile" msg = "cannot find subfile"
raise OSError(msg) raise OSError(msg)
if i > 0: if i > 0:

View File

@ -50,7 +50,7 @@ import warnings
from collections.abc import Iterator, MutableMapping from collections.abc import Iterator, MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational from numbers import Number, Rational
from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast from typing import IO, Any, Callable, NoReturn, cast
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -58,9 +58,10 @@ from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._deprecate import deprecate from ._deprecate import deprecate
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
from ._util import is_path from ._util import DeferredError, is_path
from .TiffTags import TYPES from .TiffTags import TYPES
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from ._typing import Buffer, IntegralLike from ._typing import Buffer, IntegralLike
@ -1222,6 +1223,8 @@ class TiffImageFile(ImageFile.ImageFile):
self._im = None self._im = None
def _seek(self, frame: int) -> None: def _seek(self, frame: int) -> None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.fp = self._fp self.fp = self._fp
while len(self._frame_pos) <= frame: while len(self._frame_pos) <= frame:

View File

@ -80,8 +80,6 @@ class WmfStubImageFile(ImageFile.StubImageFile):
format_description = "Windows Metafile" format_description = "Windows Metafile"
def _open(self) -> None: def _open(self) -> None:
self._inch = None
# check placable header # check placable header
s = self.fp.read(80) s = self.fp.read(80)
@ -89,10 +87,11 @@ class WmfStubImageFile(ImageFile.StubImageFile):
# placeable windows metafile # placeable windows metafile
# get units per inch # get units per inch
self._inch = word(s, 14) inch = word(s, 14)
if self._inch == 0: if inch == 0:
msg = "Invalid inch" msg = "Invalid inch"
raise ValueError(msg) raise ValueError(msg)
self._inch: tuple[float, float] = inch, inch
# get bounding box # get bounding box
x0 = short(s, 6) x0 = short(s, 6)
@ -103,8 +102,8 @@ class WmfStubImageFile(ImageFile.StubImageFile):
# normalize size to 72 dots per inch # normalize size to 72 dots per inch
self.info["dpi"] = 72 self.info["dpi"] = 72
size = ( size = (
(x1 - x0) * self.info["dpi"] // self._inch, (x1 - x0) * self.info["dpi"] // inch,
(y1 - y0) * self.info["dpi"] // self._inch, (y1 - y0) * self.info["dpi"] // inch,
) )
self.info["wmf_bbox"] = x0, y0, x1, y1 self.info["wmf_bbox"] = x0, y0, x1, y1
@ -138,6 +137,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
self.info["dpi"] = xdpi self.info["dpi"] = xdpi
else: else:
self.info["dpi"] = xdpi, ydpi self.info["dpi"] = xdpi, ydpi
self._inch = xdpi, ydpi
else: else:
msg = "Unsupported file format" msg = "Unsupported file format"
@ -153,13 +153,17 @@ class WmfStubImageFile(ImageFile.StubImageFile):
def _load(self) -> ImageFile.StubHandler | None: def _load(self) -> ImageFile.StubHandler | None:
return _handler return _handler
def load(self, dpi: int | None = None) -> Image.core.PixelAccess | None: def load(
if dpi is not None and self._inch is not None: self, dpi: float | tuple[float, float] | None = None
) -> Image.core.PixelAccess | None:
if dpi is not None:
self.info["dpi"] = dpi self.info["dpi"] = dpi
x0, y0, x1, y1 = self.info["wmf_bbox"] x0, y0, x1, y1 = self.info["wmf_bbox"]
if not isinstance(dpi, tuple):
dpi = dpi, dpi
self._size = ( self._size = (
(x1 - x0) * self.info["dpi"] // self._inch, int((x1 - x0) * dpi[0] / self._inch[0]),
(y1 - y0) * self.info["dpi"] // self._inch, int((y1 - y0) * dpi[1] / self._inch[1]),
) )
return super().load() return super().load()

View File

@ -3,8 +3,9 @@ from __future__ import annotations
import os import os
import sys import sys
from collections.abc import Sequence from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union from typing import Any, Protocol, TypeVar, Union
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from numbers import _IntegralLike as IntegralLike from numbers import _IntegralLike as IntegralLike

View File

@ -687,6 +687,14 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) {
#define GET32(p, o) ((DWORD *)(p + o))[0] #define GET32(p, o) ((DWORD *)(p + o))[0]
static int CALLBACK
enhMetaFileProc(
HDC hdc, HANDLETABLE *lpht, const ENHMETARECORD *lpmr, int nHandles, LPARAM data
) {
PlayEnhMetaFileRecord(hdc, lpht, lpmr, nHandles);
return 1;
}
PyObject * PyObject *
PyImaging_DrawWmf(PyObject *self, PyObject *args) { PyImaging_DrawWmf(PyObject *self, PyObject *args) {
HBITMAP bitmap; HBITMAP bitmap;
@ -767,10 +775,7 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) {
/* FIXME: make background transparent? configurable? */ /* FIXME: make background transparent? configurable? */
FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); FillRect(dc, &rect, GetStockObject(WHITE_BRUSH));
if (!PlayEnhMetaFile(dc, meta, &rect)) { EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect);
PyErr_SetString(PyExc_OSError, "cannot render metafile");
goto error;
}
/* step 4: extract bits from bitmap */ /* step 4: extract bits from bitmap */

View File

@ -25,7 +25,6 @@ typedef struct {
typedef struct { typedef struct {
UINT16 c0, c1; UINT16 c0, c1;
UINT32 lut;
} bc1_color; } bc1_color;
typedef struct { typedef struct {
@ -40,13 +39,10 @@ typedef struct {
#define LOAD16(p) (p)[0] | ((p)[1] << 8) #define LOAD16(p) (p)[0] | ((p)[1] << 8)
#define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24)
static void static void
bc1_color_load(bc1_color *dst, const UINT8 *src) { bc1_color_load(bc1_color *dst, const UINT8 *src) {
dst->c0 = LOAD16(src); dst->c0 = LOAD16(src);
dst->c1 = LOAD16(src + 2); dst->c1 = LOAD16(src + 2);
dst->lut = LOAD32(src + 4);
} }
static rgba static rgba
@ -70,7 +66,7 @@ static void
decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) {
bc1_color col; bc1_color col;
rgba p[4]; rgba p[4];
int n, cw; int n, o, cw;
UINT16 r0, g0, b0, r1, g1, b1; UINT16 r0, g0, b0, r1, g1, b1;
bc1_color_load(&col, src); bc1_color_load(&col, src);
@ -103,9 +99,11 @@ decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) {
p[3].b = 0; p[3].b = 0;
p[3].a = 0; p[3].a = 0;
} }
for (n = 0; n < 16; n++) { for (n = 0; n < 4; n++) {
cw = 3 & (col.lut >> (2 * n)); for (o = 0; o < 4; o++) {
dst[n] = p[cw]; cw = 3 & ((src + 4)[n] >> (2 * o));
dst[n * 4 + o] = p[cw];
}
} }
} }

View File

@ -501,55 +501,49 @@ polygon_generic(
// Needed to draw consistent polygons // Needed to draw consistent polygons
xx[j] = xx[j - 1]; xx[j] = xx[j - 1];
j++; j++;
} else if (current->dx != 0 && j % 2 == 1 && } else if ((ymin == current->ymin || ymin == current->ymax) &&
roundf(xx[j - 1]) == xx[j - 1]) { current->dx != 0) {
// Connect discontiguous corners // Connect discontiguous corners
for (k = 0; k < i; k++) { for (k = 0; k < i; k++) {
Edge *other_edge = edge_table[k]; Edge *other_edge = edge_table[k];
if ((current->dx > 0 && other_edge->dx <= 0) || if ((ymin != other_edge->ymin && ymin != other_edge->ymax) ||
(current->dx < 0 && other_edge->dx >= 0)) { other_edge->dx == 0) {
continue; continue;
} }
// Check if the two edges join to make a corner // Check if the two edges join to make a corner
if (xx[j - 1] == if (roundf(xx[j - 1]) ==
(ymin - other_edge->y0) * other_edge->dx + other_edge->x0) { roundf(
(ymin - other_edge->y0) * other_edge->dx +
other_edge->x0
)) {
// Determine points from the edges on the next row // Determine points from the edges on the next row
// Or if this is the last row, check the previous row // Or if this is the last row, check the previous row
int offset = ymin == ymax ? -1 : 1; int offset = ymin == current->ymax ? -1 : 1;
adjacent_line_x = adjacent_line_x =
(ymin + offset - current->y0) * current->dx + (ymin + offset - current->y0) * current->dx +
current->x0; current->x0;
adjacent_line_x_other_edge = if (ymin + offset >= other_edge->ymin &&
(ymin + offset - other_edge->y0) * other_edge->dx + ymin + offset <= other_edge->ymax) {
other_edge->x0; adjacent_line_x_other_edge =
if (ymin == current->ymax) { (ymin + offset - other_edge->y0) * other_edge->dx +
if (current->dx > 0) { other_edge->x0;
xx[k] = if (xx[j - 1] > adjacent_line_x + 1 &&
fmax( xx[j - 1] > adjacent_line_x_other_edge + 1) {
xx[j - 1] =
roundf(fmax(
adjacent_line_x, adjacent_line_x_other_edge adjacent_line_x, adjacent_line_x_other_edge
) + )) +
1; 1;
} else { } else if (xx[j - 1] < adjacent_line_x - 1 &&
xx[k] = xx[j - 1] < adjacent_line_x_other_edge - 1) {
fmin( xx[j - 1] =
roundf(fmin(
adjacent_line_x, adjacent_line_x_other_edge adjacent_line_x, adjacent_line_x_other_edge
) - )) -
1;
}
} else {
if (current->dx > 0) {
xx[k] = fmin(
adjacent_line_x, adjacent_line_x_other_edge
);
} else {
xx[k] =
fmax(
adjacent_line_x, adjacent_line_x_other_edge
) +
1; 1;
} }
break;
} }
break;
} }
} }
} }

View File

@ -299,6 +299,7 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) {
return -1; return -1;
} }
img.orientation = ORIENTATION_TOPLEFT;
img.req_orientation = ORIENTATION_TOPLEFT; img.req_orientation = ORIENTATION_TOPLEFT;
img.col_offset = 0; img.col_offset = 0;

View File

@ -113,7 +113,7 @@ V = {
"BROTLI": "1.1.0", "BROTLI": "1.1.0",
"FREETYPE": "2.13.3", "FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16", "FRIBIDI": "1.0.16",
"HARFBUZZ": "10.4.0", "HARFBUZZ": "11.0.0",
"JPEGTURBO": "3.1.0", "JPEGTURBO": "3.1.0",
"LCMS2": "2.17", "LCMS2": "2.17",
"LIBAVIF": "1.2.1", "LIBAVIF": "1.2.1",