Merge branch 'main' into append_images

This commit is contained in:
Andrew Murray 2025-03-27 23:52:01 +11:00
commit 29c981919a
68 changed files with 1226 additions and 376 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,11 +38,15 @@ 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_NG_VERSION=2.2.4 ZLIB_NG_VERSION=2.2.4

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

@ -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:

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

View File

@ -345,7 +345,7 @@ def test_apng_sequence_errors(test_file: str) -> None:
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:
@ -375,7 +375,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 +393,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(
@ -408,7 +408,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
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 = []
@ -471,7 +471,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
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 +572,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 +594,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 +662,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 +686,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 +700,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

@ -17,7 +17,7 @@ from .helper import (
def test_sanity(tmp_path: Path) -> None: def test_sanity(tmp_path: Path) -> None:
def roundtrip(im: Image.Image) -> None: def roundtrip(im: Image.Image) -> None:
outfile = str(tmp_path / "temp.bmp") outfile = tmp_path / "temp.bmp"
im.save(outfile, "BMP") im.save(outfile, "BMP")
@ -66,7 +66,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 +74,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 +96,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 +112,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 +152,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)

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

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

View File

@ -239,7 +239,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 +274,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)

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)
@ -599,7 +599,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"),
@ -627,7 +627,7 @@ def test_save_dispose(tmp_path: Path) -> None:
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 +661,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 +703,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 +729,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))]
@ -747,7 +747,7 @@ def test_dispose2_background_frame(tmp_path: Path) -> None:
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 +766,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 +781,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 +811,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 +829,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 +843,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 +878,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 +893,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 +911,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"),
@ -944,7 +944,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"),
@ -961,7 +961,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 +971,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 +987,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 +996,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 +1014,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 +1031,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 +1057,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 +1091,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 +1101,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 +1131,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")
@ -1166,7 +1166,7 @@ def test_append_images(tmp_path: Path) -> None:
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")
@ -1193,7 +1193,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:
@ -1201,7 +1201,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):
@ -1216,7 +1216,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))
@ -1238,7 +1238,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)])
@ -1248,14 +1248,14 @@ 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")]
@ -1266,7 +1266,7 @@ def test_bbox(tmp_path: Path) -> None:
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))
@ -1285,7 +1285,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:
@ -1296,7 +1296,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:
@ -1312,7 +1312,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:
@ -1327,7 +1327,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:]
) )
@ -1350,7 +1350,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:
@ -1363,7 +1363,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:
@ -1447,7 +1447,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)
@ -1458,7 +1458,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)

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,3 +37,36 @@ def test_get_palette() -> None:
# Assert # Assert
assert mode == "RGB" assert mode == "RGB"
assert len(palette) / 3 == size
def test_frombytes() -> None:
# Test that __init__ stops reading after 260 lines
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
custom_data = fp.read()
custom_data += b"#\n" * 300 + b" 0 0 0 Index 12"
b = BytesIO(custom_data)
palette = GimpPaletteFile(b)
assert len(palette.palette) / 3 == 8
# Test that __init__ only reads 256 entries
with open("Tests/images/full_gimp_palette.gpl", "rb") as fp:
full_data = fp.read()
data = full_data.replace(b"#\n", b"") + b" 0 0 0 Index 256"
b = BytesIO(data)
palette = GimpPaletteFile(b)
assert len(palette.palette) / 3 == 256
# Test that frombytes() can read beyond that
palette = GimpPaletteFile.frombytes(data)
assert len(palette.palette) / 3 == 257
# Test that __init__ raises an error if a comment is too long
data = full_data[:-1] + b"a" * 100
b = BytesIO(data)
with pytest.raises(SyntaxError, match="bad palette file"):
palette = GimpPaletteFile(b)
# Test that frombytes() can read the data regardless
palette = GimpPaletteFile.frombytes(data)
assert len(palette.palette) / 3 == 256

View File

@ -43,7 +43,7 @@ def test_load() -> None:
def test_save(tmp_path: Path) -> None: 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:

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)
@ -88,7 +88,7 @@ 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)])
@ -101,8 +101,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 +115,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 +132,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(
@ -186,7 +186,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 +202,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,7 +215,7 @@ 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:
@ -235,7 +235,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")
@ -87,7 +87,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 +98,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 +106,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)
@ -194,7 +194,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 +238,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 +250,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 +268,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 +288,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 +307,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)
@ -335,7 +335,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)
@ -505,15 +505,15 @@ class TestFileJpeg:
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:
@ -726,7 +726,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 +762,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 +773,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 +859,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")
@ -1005,7 +1005,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:
@ -1094,7 +1094,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)
@ -289,7 +289,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

@ -39,7 +39,7 @@ class LibTiffTestCase:
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 +123,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,7 +151,7 @@ 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:
img.save(f, tiffinfo=img.tag) img.save(f, tiffinfo=img.tag)
@ -247,7 +247,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,7 +313,7 @@ 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:
@ -347,13 +347,13 @@ 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:
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:
im.tag_v2[SUBIFD] = 10000 im.tag_v2[SUBIFD] = 10000
@ -365,7 +365,7 @@ 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:
@ -375,7 +375,7 @@ class TestFileLibTiff(LibTiffTestCase):
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 +383,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 +400,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 +420,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,7 +430,7 @@ 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") out = tmp_path / "temp.tif"
orig.tag[269] = "temp.tif" orig.tag[269] = "temp.tif"
orig.save(out) orig.save(out)
@ -457,7 +457,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 +470,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 +494,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 +502,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 +510,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 +525,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,7 +534,7 @@ 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)
@ -546,7 +546,7 @@ class TestFileLibTiff(LibTiffTestCase):
@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)
@ -686,7 +686,7 @@ 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:
@ -713,7 +713,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 +724,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 +736,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,7 +779,7 @@ 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"]
@ -802,7 +802,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 +864,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 +1008,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,
@ -1147,7 +1147,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 +1160,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 +1176,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

@ -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
) )

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)
@ -661,7 +661,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 +671,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,7 +721,7 @@ 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)
@ -741,7 +741,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 +750,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

@ -73,11 +73,11 @@ def test_invalid_file() -> None:
def test_write(tmp_path: Path) -> None: def test_write(tmp_path: Path) -> None:
def roundtrip(img: Image.Image) -> None: def roundtrip(img: Image.Image) -> None:
out = str(tmp_path / "temp.sgi") out = tmp_path / "temp.sgi"
img.save(out, format="sgi") img.save(out, format="sgi")
assert_image_equal_tofile(img, out) assert_image_equal_tofile(img, out)
out = str(tmp_path / "fp.sgi") out = tmp_path / "fp.sgi"
with open(out, "wb") as fp: with open(out, "wb") as fp:
img.save(fp) img.save(fp)
assert_image_equal_tofile(img, out) assert_image_equal_tofile(img, out)
@ -95,7 +95,7 @@ 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 +103,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

View File

@ -24,7 +24,7 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
@pytest.mark.parametrize("mode", _MODES) @pytest.mark.parametrize("mode", _MODES)
def test_sanity(mode: str, tmp_path: Path) -> None: def test_sanity(mode: 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:
@ -65,7 +65,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
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 +76,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 +122,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 +141,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 +155,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 +172,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 +202,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 +229,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 +266,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

@ -31,7 +31,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,11 +112,11 @@ 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"
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)
@ -185,7 +185,7 @@ class TestFileTiff:
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)
@ -220,12 +220,12 @@ class TestFileTiff:
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)
@ -485,14 +485,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 +504,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,7 +533,7 @@ 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:
@ -612,7 +612,7 @@ 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:
# legacy interface # legacy interface
@ -630,14 +630,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 +652,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})
@ -703,7 +703,7 @@ class TestFileTiff:
with Image.open(infile) as im: with Image.open(infile) as im:
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 +718,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)
@ -812,7 +812,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 +821,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 +832,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
@ -889,7 +889,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 +911,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 +923,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 +974,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,7 +56,7 @@ 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)
@ -128,7 +128,7 @@ 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") 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)
@ -163,7 +163,7 @@ 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:
info = im.tag_v2 info = im.tag_v2
del info[278] del info[278]
@ -210,7 +210,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,7 +227,7 @@ 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:
@ -244,7 +244,7 @@ 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:
@ -263,7 +263,7 @@ 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:
@ -296,7 +296,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)
@ -317,13 +317,13 @@ def test_iccprofile_binary() -> None:
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,7 +332,7 @@ 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:
@ -351,7 +351,7 @@ 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:
@ -363,7 +363,7 @@ 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:
@ -381,7 +381,7 @@ 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:
@ -393,7 +393,7 @@ 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:
@ -406,7 +406,7 @@ 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:
@ -420,7 +420,7 @@ 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:
@ -446,7 +446,7 @@ 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 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 len(reloaded.tag_v2[34377]) == 70 assert len(reloaded.tag_v2[34377]) == 70
@ -480,7 +480,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

@ -39,7 +39,7 @@ 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 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 im.n_frames == orig.n_frames assert im.n_frames == orig.n_frames
@ -67,7 +67,7 @@ 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 im.n_frames == 2 assert im.n_frames == 2
@ -87,7 +87,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 +99,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 +116,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(
@ -141,7 +141,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 +159,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(
@ -196,10 +196,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

@ -146,7 +146,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

@ -59,7 +59,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
@ -93,6 +93,6 @@ def test_load_set_dpi() -> None:
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

@ -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
@ -225,7 +231,7 @@ class TestImage:
def test_unknown_extension(self, tmp_path: Path) -> None: def test_unknown_extension(self, tmp_path: Path) -> None:
im = hopper() im = hopper()
temp_file = str(tmp_path / "temp.unknown") temp_file = tmp_path / "temp.unknown"
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(temp_file) im.save(temp_file)
@ -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

@ -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

@ -10,7 +10,7 @@ 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)

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,7 +35,7 @@ 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:

View File

@ -65,7 +65,7 @@ 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)

View File

@ -68,7 +68,7 @@ by DirectX.
DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode. DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
.. versionadded:: 3.4.0 .. versionadded:: 3.4.0
DXT3 images can be read in ``RGB`` mode and DX10 images can be read in DXT3 images can be read in ``RGBA`` mode and DX10 images can be read in
``RGB`` and ``RGBA`` mode. ``RGB`` and ``RGBA`` mode.
.. versionadded:: 6.0.0 .. versionadded:: 6.0.0
@ -93,6 +93,12 @@ DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
in ``P`` mode. in ``P`` mode.
.. versionadded:: 11.2.0
DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved::
im.save(out, pixel_format="DXT1")
DIB DIB
^^^ ^^^

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

@ -66,6 +66,14 @@ libjpeg library, and what version of MozJPEG is being used::
features.check_feature("mozjpeg") # True or False features.check_feature("mozjpeg") # True or False
features.version_feature("mozjpeg") # "4.1.1" for example, or None features.version_feature("mozjpeg") # "4.1.1" for example, or None
Saving compressed DDS images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3,
DXT5, BC2, BC3 and BC5 are supported::
im.save("out.dds", pixel_format="DXT1")
Other Changes Other Changes
============= =============

View File

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

View File

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

View File

@ -16,24 +16,27 @@
from __future__ import annotations from __future__ import annotations
import re import re
from io import BytesIO
from typing import IO from typing import IO
from ._binary import o8
class GimpPaletteFile: class GimpPaletteFile:
"""File handler for GIMP's palette format.""" """File handler for GIMP's palette format."""
rawmode = "RGB" rawmode = "RGB"
def __init__(self, fp: IO[bytes]) -> None: def _read(self, fp: IO[bytes], limit: bool = True) -> None:
palette = [o8(i) * 3 for i in range(256)]
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)
for i in range(256): palette: list[int] = []
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
@ -41,18 +44,29 @@ 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)
v = tuple(map(int, s.split()[:3])) v = s.split(maxsplit=3)
if len(v) != 3: if len(v) < 3:
msg = "bad palette entry" msg = "bad palette entry"
raise ValueError(msg) raise ValueError(msg)
palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) palette += (int(v[i]) for i in range(3))
if limit and len(palette) == 768:
break
self.palette = b"".join(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

@ -548,7 +548,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)
@ -2511,13 +2510,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", None)
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()
@ -2532,6 +2524,13 @@ 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 or ( if save_all or (

View File

@ -1608,6 +1608,10 @@ class TiffImageFile(ImageFile.ImageFile):
raise ValueError(msg) raise ValueError(msg)
w = tilewidth w = tilewidth
if w == xsize and h == ysize and self._planar_configuration != 2:
# Every tile covers the image. Only use the last offset
offsets = offsets[-1:]
for offset in offsets: for offset in offsets:
if x + w > xsize: if x + w > xsize:
stride = w * sum(bps_tuple) / 8 # bytes per line stride = w * sum(bps_tuple) / 8 # bytes per line
@ -1630,11 +1634,11 @@ class TiffImageFile(ImageFile.ImageFile):
args, args,
) )
) )
x = x + w x += w
if x >= xsize: if x >= xsize:
x, y = 0, y + h x, y = 0, y + h
if y >= ysize: if y >= ysize:
x = y = 0 y = 0
layer += 1 layer += 1
else: else:
logger.debug("- unsupported data organization") logger.debug("- unsupported data organization")

View File

@ -238,7 +238,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
cur_idx = im.tell() cur_idx = im.tell()
try: try:
for ims in [im] + append_images: for ims in [im] + append_images:
# Get # of frames in this image # Get number of frames in this image
nfr = getattr(ims, "n_frames", 1) nfr = getattr(ims, "n_frames", 1)
for idx in range(nfr): for idx in range(nfr):

View File

@ -4041,6 +4041,8 @@ PyImaging_ZipDecoderNew(PyObject *self, PyObject *args);
/* Encoders (in encode.c) */ /* Encoders (in encode.c) */
extern PyObject * extern PyObject *
PyImaging_BcnEncoderNew(PyObject *self, PyObject *args);
extern PyObject *
PyImaging_EpsEncoderNew(PyObject *self, PyObject *args); PyImaging_EpsEncoderNew(PyObject *self, PyObject *args);
extern PyObject * extern PyObject *
PyImaging_GifEncoderNew(PyObject *self, PyObject *args); PyImaging_GifEncoderNew(PyObject *self, PyObject *args);
@ -4109,6 +4111,7 @@ static PyMethodDef functions[] = {
/* Codecs */ /* Codecs */
{"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS}, {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS},
{"bcn_encoder", (PyCFunction)PyImaging_BcnEncoderNew, METH_VARARGS},
{"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS}, {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS},
{"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS},
{"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS}, {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS},

View File

@ -27,6 +27,7 @@
#include "thirdparty/pythoncapi_compat.h" #include "thirdparty/pythoncapi_compat.h"
#include "libImaging/Imaging.h" #include "libImaging/Imaging.h"
#include "libImaging/Bcn.h"
#include "libImaging/Gif.h" #include "libImaging/Gif.h"
#ifdef HAVE_UNISTD_H #ifdef HAVE_UNISTD_H
@ -350,6 +351,31 @@ get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode)
return 0; return 0;
} }
/* -------------------------------------------------------------------- */
/* BCN */
/* -------------------------------------------------------------------- */
PyObject *
PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) {
ImagingEncoderObject *encoder;
char *mode;
int n;
if (!PyArg_ParseTuple(args, "si", &mode, &n)) {
return NULL;
}
encoder = PyImaging_EncoderNew(0);
if (encoder == NULL) {
return NULL;
}
encoder->encode = ImagingBcnEncode;
encoder->state.state = n;
return (PyObject *)encoder;
}
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
/* EPS */ /* EPS */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */

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];
}
} }
} }

298
src/libImaging/BcnEncode.c Normal file
View File

@ -0,0 +1,298 @@
/*
* The Python Imaging Library
*
* encoder for DXT1-compressed data
*
* Format documentation:
* https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
*
*/
#include "Imaging.h"
typedef struct {
UINT8 color[3];
} rgb;
typedef struct {
UINT8 color[4];
} rgba;
static rgb
decode_565(UINT16 x) {
rgb item;
int r, g, b;
r = (x & 0xf800) >> 8;
r |= r >> 5;
item.color[0] = r;
g = (x & 0x7e0) >> 3;
g |= g >> 6;
item.color[1] = g;
b = (x & 0x1f) << 3;
b |= b >> 5;
item.color[2] = b;
return item;
}
static UINT16
encode_565(rgba item) {
UINT8 r, g, b;
r = item.color[0] >> (8 - 5);
g = item.color[1] >> (8 - 6);
b = item.color[2] >> (8 - 5);
return (r << (5 + 6)) | (g << 5) | b;
}
static void
encode_bc1_color(Imaging im, ImagingCodecState state, UINT8 *dst, int separate_alpha) {
int i, j, k;
UINT16 color_min = 0, color_max = 0;
rgb color_min_rgb, color_max_rgb;
rgba block[16], *current_rgba;
// Determine the min and max colors in this 4x4 block
int first = 1;
int transparency = 0;
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
current_rgba = &block[i + j * 4];
int x = state->x + i * im->pixelsize;
int y = state->y + j;
if (x >= state->xsize * im->pixelsize || y >= state->ysize) {
// The 4x4 block extends past the edge of the image
for (k = 0; k < 3; k++) {
current_rgba->color[k] = 0;
}
continue;
}
for (k = 0; k < 3; k++) {
current_rgba->color[k] =
(UINT8)im->image[y][x + (im->pixelsize == 1 ? 0 : k)];
}
if (separate_alpha) {
if ((UINT8)im->image[y][x + 3] == 0) {
current_rgba->color[3] = 0;
transparency = 1;
continue;
} else {
current_rgba->color[3] = 1;
}
}
UINT16 color = encode_565(*current_rgba);
if (first || color < color_min) {
color_min = color;
}
if (first || color > color_max) {
color_max = color;
}
first = 0;
}
}
if (transparency) {
*dst++ = color_min;
*dst++ = color_min >> 8;
}
*dst++ = color_max;
*dst++ = color_max >> 8;
if (!transparency) {
*dst++ = color_min;
*dst++ = color_min >> 8;
}
color_min_rgb = decode_565(color_min);
color_max_rgb = decode_565(color_max);
for (i = 0; i < 4; i++) {
UINT8 l = 0;
for (j = 3; j > -1; j--) {
current_rgba = &block[i * 4 + j];
if (transparency && !current_rgba->color[3]) {
l |= 3 << (j * 2);
continue;
}
float distance = 0;
int total = 0;
for (k = 0; k < 3; k++) {
float denom =
(float)abs(color_max_rgb.color[k] - color_min_rgb.color[k]);
if (denom != 0) {
distance +=
abs(current_rgba->color[k] - color_min_rgb.color[k]) / denom;
total += 1;
}
}
if (total == 0) {
continue;
}
if (transparency) {
distance *= 4 / total;
if (distance < 1) {
// color_max
} else if (distance < 3) {
l |= 2 << (j * 2); // 1/2 * color_min + 1/2 * color_max
} else {
l |= 1 << (j * 2); // color_min
}
} else {
distance *= 6 / total;
if (distance < 1) {
l |= 1 << (j * 2); // color_min
} else if (distance < 3) {
l |= 3 << (j * 2); // 1/3 * color_min + 2/3 * color_max
} else if (distance < 5) {
l |= 2 << (j * 2); // 2/3 * color_min + 1/3 * color_max
} else {
// color_max
}
}
}
*dst++ = l;
}
}
static void
encode_bc2_block(Imaging im, ImagingCodecState state, UINT8 *dst) {
int i, j;
UINT8 block[16], current_alpha;
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
int x = state->x + i * im->pixelsize;
int y = state->y + j;
if (x >= state->xsize * im->pixelsize || y >= state->ysize) {
// The 4x4 block extends past the edge of the image
block[i + j * 4] = 0;
continue;
}
current_alpha = (UINT8)im->image[y][x + 3];
block[i + j * 4] = current_alpha;
}
}
for (i = 0; i < 4; i++) {
UINT16 l = 0;
for (j = 3; j > -1; j--) {
current_alpha = block[i * 4 + j];
l |= current_alpha << (j * 4);
}
*dst++ = l;
*dst++ = l >> 8;
}
}
static void
encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst, int o) {
int i, j;
UINT8 alpha_min = 0, alpha_max = 0;
UINT8 block[16], current_alpha;
// Determine the min and max colors in this 4x4 block
int first = 1;
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
int x = state->x + i * im->pixelsize;
int y = state->y + j;
if (x >= state->xsize * im->pixelsize || y >= state->ysize) {
// The 4x4 block extends past the edge of the image
block[i + j * 4] = 0;
continue;
}
current_alpha = (UINT8)im->image[y][x + o];
block[i + j * 4] = current_alpha;
if (first || current_alpha < alpha_min) {
alpha_min = current_alpha;
}
if (first || current_alpha > alpha_max) {
alpha_max = current_alpha;
}
first = 0;
}
}
*dst++ = alpha_min;
*dst++ = alpha_max;
float denom = (float)abs(alpha_max - alpha_min);
for (i = 0; i < 2; i++) {
UINT32 l = 0;
for (j = 7; j > -1; j--) {
current_alpha = block[i * 8 + j];
if (!current_alpha) {
l |= 6 << (j * 3);
continue;
} else if (current_alpha == 255) {
l |= 7 << (j * 3);
continue;
}
float distance =
denom == 0 ? 0 : abs(current_alpha - alpha_min) / denom * 10;
if (distance < 3) {
l |= 2 << (j * 3); // 4/5 * alpha_min + 1/5 * alpha_max
} else if (distance < 5) {
l |= 3 << (j * 3); // 3/5 * alpha_min + 2/5 * alpha_max
} else if (distance < 7) {
l |= 4 << (j * 3); // 2/5 * alpha_min + 3/5 * alpha_max
} else {
l |= 5 << (j * 3); // 1/5 * alpha_min + 4/5 * alpha_max
}
}
*dst++ = l;
*dst++ = l >> 8;
*dst++ = l >> 16;
}
}
int
ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
int n = state->state;
int has_alpha_channel =
strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0;
UINT8 *dst = buf;
for (;;) {
if (n == 5) {
encode_bc3_alpha(im, state, dst, 0);
dst += 8;
encode_bc3_alpha(im, state, dst, 1);
} else {
if (n == 2 || n == 3) {
if (has_alpha_channel) {
if (n == 2) {
encode_bc2_block(im, state, dst);
} else {
encode_bc3_alpha(im, state, dst, 3);
}
dst += 8;
} else {
for (int i = 0; i < 8; i++) {
*dst++ = 0xff;
}
}
}
encode_bc1_color(im, state, dst, n == 1 && has_alpha_channel);
}
dst += 8;
state->x += im->pixelsize * 4;
if (state->x >= state->xsize * im->pixelsize) {
state->x = 0;
state->y += 4;
if (state->y >= state->ysize) {
state->errcode = IMAGING_CODEC_END;
break;
}
}
}
return dst - buf;
}

View File

@ -567,6 +567,8 @@ typedef int (*ImagingCodec)(
extern int extern int
ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes);
extern int extern int
ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes);
extern int
ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes);
extern int extern int
ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes);

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",
"LIBIMAGEQUANT": "4.3.4", "LIBIMAGEQUANT": "4.3.4",