Merge branch 'main' into fits

This commit is contained in:
Andrew Murray 2022-02-19 14:41:45 +11:00
commit 7dca0135dd
61 changed files with 1396 additions and 764 deletions

View File

@ -5,6 +5,18 @@ Changelog (Pillow)
9.1.0 (unreleased) 9.1.0 (unreleased)
------------------ ------------------
- Consider palette size when converting and in getpalette() #6060
[radarhere]
- Added enums #5954
[radarhere]
- Ensure image is opaque after converting P to PA with RGB palette #6052
[radarhere]
- Attach RGBA palettes from putpalette() when suitable #6054
[radarhere]
- Added get_photoshop_blocks() to parse Photoshop TIFF tag #6030 - Added get_photoshop_blocks() to parse Photoshop TIFF tag #6030
[radarhere] [radarhere]

Binary file not shown.

View File

@ -43,107 +43,158 @@ class TestColorLut3DCoreAPI:
im = Image.new("RGB", (10, 10), 0) im = Image.new("RGB", (10, 10), 0)
with pytest.raises(ValueError, match="filter"): with pytest.raises(ValueError, match="filter"):
im.im.color_lut_3d("RGB", Image.CUBIC, *self.generate_identity_table(3, 3)) im.im.color_lut_3d(
"RGB", Image.Resampling.BICUBIC, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="image mode"): with pytest.raises(ValueError, match="image mode"):
im.im.color_lut_3d( im.im.color_lut_3d(
"wrong", Image.LINEAR, *self.generate_identity_table(3, 3) "wrong", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
) )
with pytest.raises(ValueError, match="table_channels"): with pytest.raises(ValueError, match="table_channels"):
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(5, 3))
with pytest.raises(ValueError, match="table_channels"):
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(1, 3))
with pytest.raises(ValueError, match="table_channels"):
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(2, 3))
with pytest.raises(ValueError, match="Table size"):
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (1, 3, 3)) "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(5, 3)
)
with pytest.raises(ValueError, match="table_channels"):
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(1, 3)
)
with pytest.raises(ValueError, match="table_channels"):
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(2, 3)
) )
with pytest.raises(ValueError, match="Table size"): with pytest.raises(ValueError, match="Table size"):
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (66, 3, 3)) "RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (1, 3, 3)),
)
with pytest.raises(ValueError, match="Table size"):
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (66, 3, 3)),
) )
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 7) im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7
)
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 9) im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9
)
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8) im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8
)
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, 16) im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
def test_correct_args(self): def test_correct_args(self):
im = Image.new("RGB", (10, 10), 0) im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3))
im.im.color_lut_3d("CMYK", Image.LINEAR, *self.generate_identity_table(4, 3))
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 3, 3)) "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
) )
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (65, 3, 3)) "CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
) )
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 65, 3)) "RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (2, 3, 3)),
) )
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 3, 65)) "RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (65, 3, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 65, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 3, 65)),
) )
def test_wrong_mode(self): def test_wrong_mode(self):
with pytest.raises(ValueError, match="wrong mode"): with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0) im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3))
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3))
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3))
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d( im.im.color_lut_3d(
"RGBA", Image.LINEAR, *self.generate_identity_table(3, 3) "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
) )
with pytest.raises(ValueError, match="wrong mode"): with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0) im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(4, 3)) im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
def test_correct_mode(self): def test_correct_mode(self):
im = Image.new("RGBA", (10, 10), 0) im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(3, 3)) im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGBA", (10, 10), 0) im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3)) im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
im = Image.new("RGB", (10, 10), 0) im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d("HSV", Image.LINEAR, *self.generate_identity_table(3, 3)) im.im.color_lut_3d(
"HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGB", (10, 10), 0) im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3)) im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
def test_identities(self): def test_identities(self):
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] "RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
) )
# Fast test with small cubes # Fast test with small cubes
@ -152,7 +203,9 @@ class TestColorLut3DCoreAPI:
im, im,
im._new( im._new(
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, size) "RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, size),
) )
), ),
) )
@ -162,7 +215,9 @@ class TestColorLut3DCoreAPI:
im, im,
im._new( im._new(
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 2, 65)) "RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (2, 2, 65)),
) )
), ),
) )
@ -170,7 +225,12 @@ class TestColorLut3DCoreAPI:
def test_identities_4_channels(self): def test_identities_4_channels(self):
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] "RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
) )
# Red channel copied to alpha # Red channel copied to alpha
@ -178,7 +238,9 @@ class TestColorLut3DCoreAPI:
Image.merge("RGBA", (im.split() * 2)[:4]), Image.merge("RGBA", (im.split() * 2)[:4]),
im._new( im._new(
im.im.color_lut_3d( im.im.color_lut_3d(
"RGBA", Image.LINEAR, *self.generate_identity_table(4, 17) "RGBA",
Image.Resampling.BILINEAR,
*self.generate_identity_table(4, 17),
) )
), ),
) )
@ -189,9 +251,9 @@ class TestColorLut3DCoreAPI:
"RGBA", "RGBA",
[ [
g, g,
g.transpose(Image.ROTATE_90), g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.ROTATE_180), g.transpose(Image.Transpose.ROTATE_180),
g.transpose(Image.ROTATE_270), g.transpose(Image.Transpose.ROTATE_270),
], ],
) )
@ -199,7 +261,9 @@ class TestColorLut3DCoreAPI:
im, im,
im._new( im._new(
im.im.color_lut_3d( im.im.color_lut_3d(
"RGBA", Image.LINEAR, *self.generate_identity_table(3, 17) "RGBA",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, 17),
) )
), ),
) )
@ -207,14 +271,19 @@ class TestColorLut3DCoreAPI:
def test_channels_order(self): def test_channels_order(self):
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] "RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
) )
# Reverse channels by splitting and using table # Reverse channels by splitting and using table
# fmt: off # fmt: off
assert_image_equal( assert_image_equal(
Image.merge('RGB', im.split()[::-1]), Image.merge('RGB', im.split()[::-1]),
im._new(im.im.color_lut_3d('RGB', Image.LINEAR, im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2, [ 3, 2, 2, 2, [
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1,
@ -227,11 +296,16 @@ class TestColorLut3DCoreAPI:
def test_overflow(self): def test_overflow(self):
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] "RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
) )
# fmt: off # fmt: off
transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2, 3, 2, 2, 2,
[ [
-1, -1, -1, 2, -1, -1, -1, -1, -1, 2, -1, -1,
@ -251,7 +325,7 @@ class TestColorLut3DCoreAPI:
assert transformed[205, 205] == (255, 255, 0) assert transformed[205, 205] == (255, 255, 0)
# fmt: off # fmt: off
transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2, 3, 2, 2, 2,
[ [
-3, -3, -3, 5, -3, -3, -3, -3, -3, 5, -3, -3,
@ -354,7 +428,12 @@ class TestColorLut3DFilter:
def test_numpy_formats(self): def test_numpy_formats(self):
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] "RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
) )
lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b))
@ -445,7 +524,12 @@ class TestGenerateColorLut3D:
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] "RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
) )
assert im == im.filter(lut) assert im == im.filter(lut)

View File

@ -120,9 +120,9 @@ def test_apng_dispose_op_previous_frame():
# save_all=True, # save_all=True,
# append_images=[green, blue], # append_images=[green, blue],
# disposal=[ # disposal=[
# PngImagePlugin.APNG_DISPOSE_OP_NONE, # PngImagePlugin.Disposal.OP_NONE,
# PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, # PngImagePlugin.Disposal.OP_PREVIOUS,
# PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS # PngImagePlugin.Disposal.OP_PREVIOUS
# ], # ],
# ) # )
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
@ -455,31 +455,31 @@ def test_apng_save_disposal(tmp_path):
green = Image.new("RGBA", size, (0, 255, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255))
transparent = Image.new("RGBA", size, (0, 0, 0, 0)) transparent = Image.new("RGBA", size, (0, 0, 0, 0))
# test APNG_DISPOSE_OP_NONE # test OP_NONE
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[green, transparent], append_images=[green, transparent],
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, disposal=PngImagePlugin.Disposal.OP_NONE,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(2) im.seek(2)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test APNG_DISPOSE_OP_BACKGROUND # test OP_BACKGROUND
disposal = [ disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, PngImagePlugin.Disposal.OP_BACKGROUND,
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
] ]
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[red, transparent], append_images=[red, transparent],
disposal=disposal, disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(2) im.seek(2)
@ -487,26 +487,26 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
disposal = [ disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, PngImagePlugin.Disposal.OP_BACKGROUND,
] ]
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[green], append_images=[green],
disposal=disposal, disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(1) im.seek(1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test APNG_DISPOSE_OP_PREVIOUS # test OP_PREVIOUS
disposal = [ disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, PngImagePlugin.Disposal.OP_PREVIOUS,
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
] ]
red.save( red.save(
test_file, test_file,
@ -514,7 +514,7 @@ def test_apng_save_disposal(tmp_path):
append_images=[green, red, transparent], append_images=[green, red, transparent],
default_image=True, default_image=True,
disposal=disposal, disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(3) im.seek(3)
@ -522,15 +522,15 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
disposal = [ disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, PngImagePlugin.Disposal.OP_PREVIOUS,
] ]
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[green], append_images=[green],
disposal=disposal, disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(1) im.seek(1)
@ -538,7 +538,7 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test info disposal # test info disposal
red.info["disposal"] = PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND red.info["disposal"] = PngImagePlugin.Disposal.OP_BACKGROUND
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
@ -556,12 +556,12 @@ def test_apng_save_disposal_previous(tmp_path):
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))
# test APNG_DISPOSE_OP_NONE # test OP_NONE
transparent.save( transparent.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[red, green], append_images=[red, green],
disposal=PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, disposal=PngImagePlugin.Disposal.OP_PREVIOUS,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(2) im.seek(2)
@ -576,17 +576,17 @@ def test_apng_save_blend(tmp_path):
green = Image.new("RGBA", size, (0, 255, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255))
transparent = Image.new("RGBA", size, (0, 0, 0, 0)) transparent = Image.new("RGBA", size, (0, 0, 0, 0))
# test APNG_BLEND_OP_SOURCE on solid color # test OP_SOURCE on solid color
blend = [ blend = [
PngImagePlugin.APNG_BLEND_OP_OVER, PngImagePlugin.Blend.OP_OVER,
PngImagePlugin.APNG_BLEND_OP_SOURCE, PngImagePlugin.Blend.OP_SOURCE,
] ]
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[red, green], append_images=[red, green],
default_image=True, default_image=True,
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, disposal=PngImagePlugin.Disposal.OP_NONE,
blend=blend, blend=blend,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
@ -594,17 +594,17 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test APNG_BLEND_OP_SOURCE on transparent color # test OP_SOURCE on transparent color
blend = [ blend = [
PngImagePlugin.APNG_BLEND_OP_OVER, PngImagePlugin.Blend.OP_OVER,
PngImagePlugin.APNG_BLEND_OP_SOURCE, PngImagePlugin.Blend.OP_SOURCE,
] ]
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[red, transparent], append_images=[red, transparent],
default_image=True, default_image=True,
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, disposal=PngImagePlugin.Disposal.OP_NONE,
blend=blend, blend=blend,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
@ -612,14 +612,14 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
# test APNG_BLEND_OP_OVER # test OP_OVER
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[green, transparent], append_images=[green, transparent],
default_image=True, default_image=True,
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, disposal=PngImagePlugin.Disposal.OP_NONE,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(1) im.seek(1)
@ -630,8 +630,18 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test info blend # test info blend
red.info["blend"] = PngImagePlugin.APNG_BLEND_OP_OVER red.info["blend"] = PngImagePlugin.Blend.OP_OVER
red.save(test_file, save_all=True, append_images=[green, transparent]) red.save(test_file, save_all=True, append_images=[green, transparent])
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(2) im.seek(2)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
def test_constants_deprecation():
for enum, prefix in {
PngImagePlugin.Disposal: "APNG_DISPOSE_",
PngImagePlugin.Blend: "APNG_BLEND_",
}.items():
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(PngImagePlugin, prefix + name) == enum[name]

View File

@ -1,6 +1,6 @@
import pytest import pytest
from PIL import Image from PIL import BlpImagePlugin, Image
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
@ -37,3 +37,14 @@ def test_crashes(test_file):
with Image.open(f) as im: with Image.open(f) as im:
with pytest.raises(OSError): with pytest.raises(OSError):
im.load() im.load()
def test_constants_deprecation():
for enum, prefix in {
BlpImagePlugin.Format: "BLP_FORMAT_",
BlpImagePlugin.Encoding: "BLP_ENCODING_",
BlpImagePlugin.AlphaEncoding: "BLP_ALPHA_ENCODING_",
}.items():
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(BlpImagePlugin, prefix + name) == enum[name]

View File

@ -1,4 +1,6 @@
from PIL import Image import pytest
from PIL import FtexImagePlugin, Image
from .helper import assert_image_equal_tofile, assert_image_similar from .helper import assert_image_equal_tofile, assert_image_similar
@ -12,3 +14,12 @@ def test_load_dxt1():
with Image.open("Tests/images/ftex_dxt1.ftc") as im: with Image.open("Tests/images/ftex_dxt1.ftc") as im:
with Image.open("Tests/images/ftex_dxt1.png") as target: with Image.open("Tests/images/ftex_dxt1.png") as target:
assert_image_similar(im, target.convert("RGBA"), 15) assert_image_similar(im, target.convert("RGBA"), 15)
def test_constants_deprecation():
for enum, prefix in {
FtexImagePlugin.Format: "FORMAT_",
}.items():
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(FtexImagePlugin, prefix + name) == enum[name]

View File

@ -210,8 +210,8 @@ def test_palette_handling(tmp_path):
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im = im.convert("RGB") im = im.convert("RGB")
im = im.resize((100, 100), Image.LANCZOS) im = im.resize((100, 100), Image.Resampling.LANCZOS)
im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256) im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
f = str(tmp_path / "temp.gif") f = str(tmp_path / "temp.gif")
im2.save(f, optimize=True) im2.save(f, optimize=True)
@ -911,7 +911,7 @@ def test_save_I(tmp_path):
def test_getdata(): def test_getdata():
# Test getheader/getdata against legacy values. # Test getheader/getdata against legacy values.
# Create a 'P' image with holes in the palette. # Create a 'P' image with holes in the palette.
im = Image._wedge().resize((16, 16), Image.NEAREST) im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
im.putpalette(ImagePalette.ImagePalette("RGB")) im.putpalette(ImagePalette.ImagePalette("RGB"))
im.info = {"background": 0} im.info = {"background": 0}

View File

@ -112,12 +112,9 @@ def test_older_icon():
def test_jp2_icon(): def test_jp2_icon():
# This icon was made by using Uli Kusterer's oldiconutil to replace # This icon uses JPEG 2000 images instead of the PNG images.
# the PNG images with JPEG 2000 ones. The advantage of doing this is # The advantage of doing this is that OS X 10.5 supports JPEG 2000
# that OS X 10.5 supports JPEG 2000 but not PNG; some commercial # but not PNG; some commercial software therefore does just this.
# software therefore does just this.
# (oldiconutil is here: https://github.com/uliwitness/oldiconutil)
if not ENABLE_JPEG2K: if not ENABLE_JPEG2K:
return return

View File

@ -53,7 +53,9 @@ def test_save_to_bytes():
assert im.mode == reloaded.mode assert im.mode == reloaded.mode
assert (64, 64) == reloaded.size assert (64, 64) == reloaded.size
assert reloaded.format == "ICO" assert reloaded.format == "ICO"
assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS)) assert_image_equal(
reloaded, hopper().resize((64, 64), Image.Resampling.LANCZOS)
)
# The other one # The other one
output.seek(0) output.seek(0)
@ -63,7 +65,9 @@ def test_save_to_bytes():
assert im.mode == reloaded.mode assert im.mode == reloaded.mode
assert (32, 32) == reloaded.size assert (32, 32) == reloaded.size
assert reloaded.format == "ICO" assert reloaded.format == "ICO"
assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) assert_image_equal(
reloaded, hopper().resize((32, 32), Image.Resampling.LANCZOS)
)
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
@ -80,7 +84,7 @@ def test_save_to_bytes_bmp(mode):
assert "RGBA" == reloaded.mode assert "RGBA" == reloaded.mode
assert (64, 64) == reloaded.size assert (64, 64) == reloaded.size
assert reloaded.format == "ICO" assert reloaded.format == "ICO"
im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA") im = hopper(mode).resize((64, 64), Image.Resampling.LANCZOS).convert("RGBA")
assert_image_equal(reloaded, im) assert_image_equal(reloaded, im)
# The other one # The other one
@ -91,7 +95,7 @@ def test_save_to_bytes_bmp(mode):
assert "RGBA" == reloaded.mode assert "RGBA" == reloaded.mode
assert (32, 32) == reloaded.size assert (32, 32) == reloaded.size
assert reloaded.format == "ICO" assert reloaded.format == "ICO"
im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA") im = hopper(mode).resize((32, 32), Image.Resampling.LANCZOS).convert("RGBA")
assert_image_equal(reloaded, im) assert_image_equal(reloaded, im)

View File

@ -271,7 +271,7 @@ class TestFileJpeg:
del exif[0x8769] del exif[0x8769]
# Assert that it needs to be transposed # Assert that it needs to be transposed
assert exif[0x0112] == Image.TRANSVERSE assert exif[0x0112] == Image.Transpose.TRANSVERSE
# Assert that the GPS IFD is present and empty # Assert that the GPS IFD is present and empty
assert exif.get_ifd(0x8825) == {} assert exif.get_ifd(0x8825) == {}

View File

@ -291,7 +291,7 @@ def test_subsampling_decode(name):
# RGB reference images are downscaled # RGB reference images are downscaled
epsilon = 3e-3 epsilon = 3e-3
width, height = width * 2, height * 2 width, height = width * 2, height * 2
expected = im2.resize((width, height), Image.NEAREST) expected = im2.resize((width, height), Image.Resampling.NEAREST)
assert_image_similar(im, expected, epsilon) assert_image_similar(im, expected, epsilon)

View File

@ -112,7 +112,7 @@ class TestFileLibTiff(LibTiffTestCase):
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 = str(tmp_path / "temp.tif")
rot = orig.transpose(Image.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)

View File

@ -77,7 +77,7 @@ def to_rgb_colorsys(im):
def test_wedge(): def test_wedge():
src = wedge().resize((3 * 32, 32), Image.BILINEAR) src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR)
im = src.convert("HSV") im = src.convert("HSV")
comparable = to_hsv_colorsys(src) comparable = to_hsv_colorsys(src)

View File

@ -813,6 +813,31 @@ class TestImage:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert Image.CONTAINER == 2 assert Image.CONTAINER == 2
def test_constants_deprecation(self):
with pytest.warns(DeprecationWarning):
assert Image.NEAREST == 0
with pytest.warns(DeprecationWarning):
assert Image.NONE == 0
with pytest.warns(DeprecationWarning):
assert Image.LINEAR == Image.Resampling.BILINEAR
with pytest.warns(DeprecationWarning):
assert Image.CUBIC == Image.Resampling.BICUBIC
with pytest.warns(DeprecationWarning):
assert Image.ANTIALIAS == Image.Resampling.LANCZOS
for enum in (
Image.Transpose,
Image.Transform,
Image.Resampling,
Image.Dither,
Image.Palette,
Image.Quantize,
):
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(Image, name) == enum[name]
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [

View File

@ -76,6 +76,13 @@ def test_16bit_workaround():
_test_float_conversion(im.convert("I")) _test_float_conversion(im.convert("I"))
def test_opaque():
alpha = hopper("P").convert("PA").getchannel("A")
solid = Image.new("L", (128, 128), 255)
assert_image_equal(alpha, solid)
def test_rgba_p(): def test_rgba_p():
im = hopper("RGBA") im = hopper("RGBA")
im.putalpha(hopper("L")) im.putalpha(hopper("L"))
@ -136,7 +143,7 @@ def test_trns_l(tmp_path):
assert "transparency" in im_p.info assert "transparency" in im_p.info
im_p.save(f) im_p.save(f)
im_p = im.convert("P", palette=Image.ADAPTIVE) im_p = im.convert("P", palette=Image.Palette.ADAPTIVE)
assert "transparency" in im_p.info assert "transparency" in im_p.info
im_p.save(f) im_p.save(f)
@ -159,13 +166,13 @@ def test_trns_RGB(tmp_path):
assert "transparency" not in im_rgba.info assert "transparency" not in im_rgba.info
im_rgba.save(f) im_rgba.save(f)
im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE)
assert "transparency" not in im_p.info assert "transparency" not in im_p.info
im_p.save(f) im_p.save(f)
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
im.info["transparency"] = im.getpixel((0, 0)) im.info["transparency"] = im.getpixel((0, 0))
im_p = im.convert("P", palette=Image.ADAPTIVE) im_p = im.convert("P", palette=Image.Palette.ADAPTIVE)
assert im_p.info["transparency"] == im_p.getpixel((0, 0)) assert im_p.info["transparency"] == im_p.getpixel((0, 0))
im_p.save(f) im_p.save(f)

View File

@ -14,7 +14,7 @@ def test_sanity():
def test_roundtrip(): def test_roundtrip():
def getdata(mode): def getdata(mode):
im = hopper(mode).resize((32, 30), Image.NEAREST) im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST)
data = im.getdata() data = im.getdata()
return data[0], len(data), len(list(data)) return data[0], len(data), len(list(data))

View File

@ -45,7 +45,7 @@ class TestImagingPaste:
@cached_property @cached_property
def mask_L(self): def mask_L(self):
return self.gradient_L.transpose(Image.ROTATE_270) return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
@cached_property @cached_property
def gradient_L(self): def gradient_L(self):
@ -62,8 +62,8 @@ class TestImagingPaste:
"RGB", "RGB",
[ [
self.gradient_L, self.gradient_L,
self.gradient_L.transpose(Image.ROTATE_90), self.gradient_L.transpose(Image.Transpose.ROTATE_90),
self.gradient_L.transpose(Image.ROTATE_180), self.gradient_L.transpose(Image.Transpose.ROTATE_180),
], ],
) )
@ -73,9 +73,9 @@ class TestImagingPaste:
"RGBA", "RGBA",
[ [
self.gradient_L, self.gradient_L,
self.gradient_L.transpose(Image.ROTATE_90), self.gradient_L.transpose(Image.Transpose.ROTATE_90),
self.gradient_L.transpose(Image.ROTATE_180), self.gradient_L.transpose(Image.Transpose.ROTATE_180),
self.gradient_L.transpose(Image.ROTATE_270), self.gradient_L.transpose(Image.Transpose.ROTATE_270),
], ],
) )
@ -85,9 +85,9 @@ class TestImagingPaste:
"RGBa", "RGBa",
[ [
self.gradient_L, self.gradient_L,
self.gradient_L.transpose(Image.ROTATE_90), self.gradient_L.transpose(Image.Transpose.ROTATE_90),
self.gradient_L.transpose(Image.ROTATE_180), self.gradient_L.transpose(Image.Transpose.ROTATE_180),
self.gradient_L.transpose(Image.ROTATE_270), self.gradient_L.transpose(Image.Transpose.ROTATE_270),
], ],
) )

View File

@ -62,3 +62,17 @@ def test_putpalette_with_alpha_values():
im.putpalette(palette_with_alpha_values, "RGBA") im.putpalette(palette_with_alpha_values, "RGBA")
assert_image_equal(im.convert("RGBA"), expected) assert_image_equal(im.convert("RGBA"), expected)
@pytest.mark.parametrize(
"mode, palette",
(
("RGBA", (1, 2, 3, 4)),
("RGBAX", (1, 2, 3, 4, 0)),
),
)
def test_rgba_palette(mode, palette):
im = Image.new("P", (1, 1))
im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3]
assert im.palette.colors == {(1, 2, 3, 4): 0}

View File

@ -25,7 +25,7 @@ def test_libimagequant_quantize():
libimagequant = parse_version(features.version_feature("libimagequant")) libimagequant = parse_version(features.version_feature("libimagequant"))
if libimagequant < parse_version("4"): if libimagequant < parse_version("4"):
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
converted = image.quantize(100, Image.LIBIMAGEQUANT) converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P" assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15) assert_image_similar(converted.convert("RGB"), image, 15)
assert len(converted.getcolors()) == 100 assert len(converted.getcolors()) == 100
@ -33,7 +33,7 @@ def test_libimagequant_quantize():
def test_octree_quantize(): def test_octree_quantize():
image = hopper() image = hopper()
converted = image.quantize(100, Image.FASTOCTREE) converted = image.quantize(100, Image.Quantize.FASTOCTREE)
assert converted.mode == "P" assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 20) assert_image_similar(converted.convert("RGB"), image, 20)
assert len(converted.getcolors()) == 100 assert len(converted.getcolors()) == 100
@ -96,10 +96,10 @@ def test_transparent_colors_equal():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"method, color", "method, color",
( (
(Image.MEDIANCUT, (0, 0, 0)), (Image.Quantize.MEDIANCUT, (0, 0, 0)),
(Image.MAXCOVERAGE, (0, 0, 0)), (Image.Quantize.MAXCOVERAGE, (0, 0, 0)),
(Image.FASTOCTREE, (0, 0, 0)), (Image.Quantize.FASTOCTREE, (0, 0, 0)),
(Image.FASTOCTREE, (0, 0, 0, 0)), (Image.Quantize.FASTOCTREE, (0, 0, 0, 0)),
), ),
) )
def test_palette(method, color): def test_palette(method, color):
@ -108,3 +108,18 @@ def test_palette(method, color):
converted = im.quantize(method=method) converted = im.quantize(method=method)
converted_px = converted.load() converted_px = converted.load()
assert converted_px[0, 0] == converted.palette.colors[color] assert converted_px[0, 0] == converted.palette.colors[color]
def test_small_palette():
# Arrange
im = hopper()
colors = (255, 0, 0, 0, 0, 255)
p = Image.new("P", (1, 1))
p.putpalette(colors)
# Act
im = im.quantize(palette=p)
# Assert
assert len(im.getcolors()) == 2

View File

@ -97,7 +97,7 @@ def get_image(mode):
bands = [gradients_image] bands = [gradients_image]
for _ in mode_info.bands[1:]: for _ in mode_info.bands[1:]:
# rotate previous image # rotate previous image
band = bands[-1].transpose(Image.ROTATE_90) band = bands[-1].transpose(Image.Transpose.ROTATE_90)
bands.append(band) bands.append(band)
# Correct alpha channel by transforming completely transparent pixels. # Correct alpha channel by transforming completely transparent pixels.
# Low alpha values also emphasize error after alpha multiplication. # Low alpha values also emphasize error after alpha multiplication.
@ -138,24 +138,26 @@ def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1):
reference = Image.new(im.mode, reduced.size) reference = Image.new(im.mode, reduced.size)
area_size = (im.size[0] // factor[0], im.size[1] // factor[1]) area_size = (im.size[0] // factor[0], im.size[1] // factor[1])
area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1]) area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1])
area = im.resize(area_size, Image.BOX, area_box) area = im.resize(area_size, Image.Resampling.BOX, area_box)
reference.paste(area, (0, 0)) reference.paste(area, (0, 0))
if area_size[0] < reduced.size[0]: if area_size[0] < reduced.size[0]:
assert reduced.size[0] - area_size[0] == 1 assert reduced.size[0] - area_size[0] == 1
last_column_box = (area_box[2], 0, im.size[0], area_box[3]) last_column_box = (area_box[2], 0, im.size[0], area_box[3])
last_column = im.resize((1, area_size[1]), Image.BOX, last_column_box) last_column = im.resize(
(1, area_size[1]), Image.Resampling.BOX, last_column_box
)
reference.paste(last_column, (area_size[0], 0)) reference.paste(last_column, (area_size[0], 0))
if area_size[1] < reduced.size[1]: if area_size[1] < reduced.size[1]:
assert reduced.size[1] - area_size[1] == 1 assert reduced.size[1] - area_size[1] == 1
last_row_box = (0, area_box[3], area_box[2], im.size[1]) last_row_box = (0, area_box[3], area_box[2], im.size[1])
last_row = im.resize((area_size[0], 1), Image.BOX, last_row_box) last_row = im.resize((area_size[0], 1), Image.Resampling.BOX, last_row_box)
reference.paste(last_row, (0, area_size[1])) reference.paste(last_row, (0, area_size[1]))
if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]: if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]:
last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1]) last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1])
last_pixel = im.resize((1, 1), Image.BOX, last_pixel_box) last_pixel = im.resize((1, 1), Image.Resampling.BOX, last_pixel_box)
reference.paste(last_pixel, area_size) reference.paste(last_pixel, area_size)
assert_compare_images(reduced, reference, average_diff, max_diff) assert_compare_images(reduced, reference, average_diff, max_diff)

View File

@ -24,7 +24,7 @@ class TestImagingResampleVulnerability:
): ):
with pytest.raises(MemoryError): with pytest.raises(MemoryError):
# any resampling filter will do here # any resampling filter will do here
im.im.resize((xsize, ysize), Image.BILINEAR) im.im.resize((xsize, ysize), Image.Resampling.BILINEAR)
def test_invalid_size(self): def test_invalid_size(self):
im = hopper() im = hopper()
@ -103,7 +103,7 @@ class TestImagingCoreResampleAccuracy:
def test_reduce_box(self): def test_reduce_box(self):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (8, 8), 0xE1) case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.BOX) case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off # fmt: off
data = ("e1 e1" data = ("e1 e1"
"e1 e1") "e1 e1")
@ -114,7 +114,7 @@ class TestImagingCoreResampleAccuracy:
def test_reduce_bilinear(self): def test_reduce_bilinear(self):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (8, 8), 0xE1) case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.BILINEAR) case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off # fmt: off
data = ("e1 c9" data = ("e1 c9"
"c9 b7") "c9 b7")
@ -125,7 +125,7 @@ class TestImagingCoreResampleAccuracy:
def test_reduce_hamming(self): def test_reduce_hamming(self):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (8, 8), 0xE1) case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.HAMMING) case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off # fmt: off
data = ("e1 da" data = ("e1 da"
"da d3") "da d3")
@ -136,7 +136,7 @@ class TestImagingCoreResampleAccuracy:
def test_reduce_bicubic(self): def test_reduce_bicubic(self):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (12, 12), 0xE1) case = self.make_case(mode, (12, 12), 0xE1)
case = case.resize((6, 6), Image.BICUBIC) case = case.resize((6, 6), Image.Resampling.BICUBIC)
# fmt: off # fmt: off
data = ("e1 e3 d4" data = ("e1 e3 d4"
"e3 e5 d6" "e3 e5 d6"
@ -148,7 +148,7 @@ class TestImagingCoreResampleAccuracy:
def test_reduce_lanczos(self): def test_reduce_lanczos(self):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (16, 16), 0xE1) case = self.make_case(mode, (16, 16), 0xE1)
case = case.resize((8, 8), Image.LANCZOS) case = case.resize((8, 8), Image.Resampling.LANCZOS)
# fmt: off # fmt: off
data = ("e1 e0 e4 d7" data = ("e1 e0 e4 d7"
"e0 df e3 d6" "e0 df e3 d6"
@ -161,7 +161,7 @@ class TestImagingCoreResampleAccuracy:
def test_enlarge_box(self): def test_enlarge_box(self):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (2, 2), 0xE1) case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.BOX) case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off # fmt: off
data = ("e1 e1" data = ("e1 e1"
"e1 e1") "e1 e1")
@ -172,7 +172,7 @@ class TestImagingCoreResampleAccuracy:
def test_enlarge_bilinear(self): def test_enlarge_bilinear(self):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (2, 2), 0xE1) case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.BILINEAR) case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off # fmt: off
data = ("e1 b0" data = ("e1 b0"
"b0 98") "b0 98")
@ -183,7 +183,7 @@ class TestImagingCoreResampleAccuracy:
def test_enlarge_hamming(self): def test_enlarge_hamming(self):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (2, 2), 0xE1) case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.HAMMING) case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off # fmt: off
data = ("e1 d2" data = ("e1 d2"
"d2 c5") "d2 c5")
@ -194,7 +194,7 @@ class TestImagingCoreResampleAccuracy:
def test_enlarge_bicubic(self): def test_enlarge_bicubic(self):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (4, 4), 0xE1) case = self.make_case(mode, (4, 4), 0xE1)
case = case.resize((8, 8), Image.BICUBIC) case = case.resize((8, 8), Image.Resampling.BICUBIC)
# fmt: off # fmt: off
data = ("e1 e5 ee b9" data = ("e1 e5 ee b9"
"e5 e9 f3 bc" "e5 e9 f3 bc"
@ -207,7 +207,7 @@ class TestImagingCoreResampleAccuracy:
def test_enlarge_lanczos(self): def test_enlarge_lanczos(self):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (6, 6), 0xE1) case = self.make_case(mode, (6, 6), 0xE1)
case = case.resize((12, 12), Image.LANCZOS) case = case.resize((12, 12), Image.Resampling.LANCZOS)
data = ( data = (
"e1 e0 db ed f5 b8" "e1 e0 db ed f5 b8"
"e0 df da ec f3 b7" "e0 df da ec f3 b7"
@ -220,7 +220,9 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (12, 12))) self.check_case(channel, self.make_sample(data, (12, 12)))
def test_box_filter_correct_range(self): def test_box_filter_correct_range(self):
im = Image.new("RGB", (8, 8), "#1688ff").resize((100, 100), Image.BOX) im = Image.new("RGB", (8, 8), "#1688ff").resize(
(100, 100), Image.Resampling.BOX
)
ref = Image.new("RGB", (100, 100), "#1688ff") ref = Image.new("RGB", (100, 100), "#1688ff")
assert_image_equal(im, ref) assert_image_equal(im, ref)
@ -228,7 +230,7 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency: class TestCoreResampleConsistency:
def make_case(self, mode, fill): def make_case(self, mode, fill):
im = Image.new(mode, (512, 9), fill) im = Image.new(mode, (512, 9), fill)
return im.resize((9, 512), Image.LANCZOS), im.load()[0, 0] return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
def run_case(self, case): def run_case(self, case):
channel, color = case channel, color = case
@ -283,20 +285,20 @@ class TestCoreResampleAlphaCorrect:
@pytest.mark.xfail(reason="Current implementation isn't precise enough") @pytest.mark.xfail(reason="Current implementation isn't precise enough")
def test_levels_rgba(self): def test_levels_rgba(self):
case = self.make_levels_case("RGBA") case = self.make_levels_case("RGBA")
self.run_levels_case(case.resize((512, 32), Image.BOX)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX))
self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR))
self.run_levels_case(case.resize((512, 32), Image.HAMMING)) self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING))
self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS))
@pytest.mark.xfail(reason="Current implementation isn't precise enough") @pytest.mark.xfail(reason="Current implementation isn't precise enough")
def test_levels_la(self): def test_levels_la(self):
case = self.make_levels_case("LA") case = self.make_levels_case("LA")
self.run_levels_case(case.resize((512, 32), Image.BOX)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX))
self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR))
self.run_levels_case(case.resize((512, 32), Image.HAMMING)) self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING))
self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS))
def make_dirty_case(self, mode, clean_pixel, dirty_pixel): def make_dirty_case(self, mode, clean_pixel, dirty_pixel):
i = Image.new(mode, (64, 64), dirty_pixel) i = Image.new(mode, (64, 64), dirty_pixel)
@ -321,19 +323,27 @@ class TestCoreResampleAlphaCorrect:
def test_dirty_pixels_rgba(self): def test_dirty_pixels_rgba(self):
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))
self.run_dirty_case(case.resize((20, 20), Image.BOX), (255, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0))
self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0)) self.run_dirty_case(
self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255, 255, 0)) case.resize((20, 20), Image.Resampling.BILINEAR), (255, 255, 0)
self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0)) )
self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0)) self.run_dirty_case(
case.resize((20, 20), Image.Resampling.HAMMING), (255, 255, 0)
)
self.run_dirty_case(
case.resize((20, 20), Image.Resampling.BICUBIC), (255, 255, 0)
)
self.run_dirty_case(
case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0)
)
def test_dirty_pixels_la(self): def test_dirty_pixels_la(self):
case = self.make_dirty_case("LA", (255, 128), (0, 0)) case = self.make_dirty_case("LA", (255, 128), (0, 0))
self.run_dirty_case(case.resize((20, 20), Image.BOX), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,))
self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,))
self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.HAMMING), (255,))
self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BICUBIC), (255,))
self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.LANCZOS), (255,))
class TestCoreResamplePasses: class TestCoreResamplePasses:
@ -346,26 +356,26 @@ class TestCoreResamplePasses:
def test_horizontal(self): def test_horizontal(self):
im = hopper("L") im = hopper("L")
with self.count(1): with self.count(1):
im.resize((im.size[0] - 10, im.size[1]), Image.BILINEAR) im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR)
def test_vertical(self): def test_vertical(self):
im = hopper("L") im = hopper("L")
with self.count(1): with self.count(1):
im.resize((im.size[0], im.size[1] - 10), Image.BILINEAR) im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR)
def test_both(self): def test_both(self):
im = hopper("L") im = hopper("L")
with self.count(2): with self.count(2):
im.resize((im.size[0] - 10, im.size[1] - 10), Image.BILINEAR) im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR)
def test_box_horizontal(self): def test_box_horizontal(self):
im = hopper("L") im = hopper("L")
box = (20, 0, im.size[0] - 20, im.size[1]) box = (20, 0, im.size[0] - 20, im.size[1])
with self.count(1): with self.count(1):
# the same size, but different box # the same size, but different box
with_box = im.resize(im.size, Image.BILINEAR, box) with_box = im.resize(im.size, Image.Resampling.BILINEAR, box)
with self.count(2): with self.count(2):
cropped = im.crop(box).resize(im.size, Image.BILINEAR) cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR)
assert_image_similar(with_box, cropped, 0.1) assert_image_similar(with_box, cropped, 0.1)
def test_box_vertical(self): def test_box_vertical(self):
@ -373,9 +383,9 @@ class TestCoreResamplePasses:
box = (0, 20, im.size[0], im.size[1] - 20) box = (0, 20, im.size[0], im.size[1] - 20)
with self.count(1): with self.count(1):
# the same size, but different box # the same size, but different box
with_box = im.resize(im.size, Image.BILINEAR, box) with_box = im.resize(im.size, Image.Resampling.BILINEAR, box)
with self.count(2): with self.count(2):
cropped = im.crop(box).resize(im.size, Image.BILINEAR) cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR)
assert_image_similar(with_box, cropped, 0.1) assert_image_similar(with_box, cropped, 0.1)
@ -388,7 +398,7 @@ class TestCoreResampleCoefficients:
draw = ImageDraw.Draw(i) draw = ImageDraw.Draw(i)
draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color)
px = i.resize((5, i.size[1]), Image.BICUBIC).load() px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load()
if px[2, 0] != test_color // 2: if px[2, 0] != test_color // 2:
assert test_color // 2 == px[2, 0] assert test_color // 2 == px[2, 0]
@ -396,7 +406,7 @@ class TestCoreResampleCoefficients:
# regression test for the wrong coefficients calculation # regression test for the wrong coefficients calculation
# due to bug https://github.com/python-pillow/Pillow/issues/2161 # due to bug https://github.com/python-pillow/Pillow/issues/2161
im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF))
histogram = im.resize((256, 256), Image.BICUBIC).histogram() histogram = im.resize((256, 256), Image.Resampling.BICUBIC).histogram()
# first channel # first channel
assert histogram[0x100 * 0 + 0x20] == 0x10000 assert histogram[0x100 * 0 + 0x20] == 0x10000
@ -412,12 +422,12 @@ class TestCoreResampleBox:
def test_wrong_arguments(self): def test_wrong_arguments(self):
im = hopper() im = hopper()
for resample in ( for resample in (
Image.NEAREST, Image.Resampling.NEAREST,
Image.BOX, Image.Resampling.BOX,
Image.BILINEAR, Image.Resampling.BILINEAR,
Image.HAMMING, Image.Resampling.HAMMING,
Image.BICUBIC, Image.Resampling.BICUBIC,
Image.LANCZOS, Image.Resampling.LANCZOS,
): ):
im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (0, 0, im.width, im.height))
im.resize((32, 32), resample, (20, 20, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height))
@ -456,7 +466,7 @@ class TestCoreResampleBox:
for y0, y1 in split_range(dst_size[1], ytiles): for y0, y1 in split_range(dst_size[1], ytiles):
for x0, x1 in split_range(dst_size[0], xtiles): for x0, x1 in split_range(dst_size[0], xtiles):
box = (x0 * scale[0], y0 * scale[1], x1 * scale[0], y1 * scale[1]) box = (x0 * scale[0], y0 * scale[1], x1 * scale[0], y1 * scale[1])
tile = im.resize((x1 - x0, y1 - y0), Image.BICUBIC, box) tile = im.resize((x1 - x0, y1 - y0), Image.Resampling.BICUBIC, box)
tiled.paste(tile, (x0, y0)) tiled.paste(tile, (x0, y0))
return tiled return tiled
@ -467,7 +477,7 @@ class TestCoreResampleBox:
with Image.open("Tests/images/flower.jpg") as im: with Image.open("Tests/images/flower.jpg") as im:
assert im.size == (480, 360) assert im.size == (480, 360)
dst_size = (251, 188) dst_size = (251, 188)
reference = im.resize(dst_size, Image.BICUBIC) reference = im.resize(dst_size, Image.Resampling.BICUBIC)
for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]: for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]:
tiled = self.resize_tiled(im, dst_size, *tiles) tiled = self.resize_tiled(im, dst_size, *tiles)
@ -483,12 +493,16 @@ class TestCoreResampleBox:
assert im.size == (480, 360) assert im.size == (480, 360)
dst_size = (48, 36) dst_size = (48, 36)
# Reference is cropped image resized to destination # Reference is cropped image resized to destination
reference = im.crop((0, 0, 473, 353)).resize(dst_size, Image.BICUBIC) reference = im.crop((0, 0, 473, 353)).resize(
# Image.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) dst_size, Image.Resampling.BICUBIC
supersampled = im.resize((60, 45), Image.BOX) )
# Image.Resampling.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45)
supersampled = im.resize((60, 45), Image.Resampling.BOX)
with_box = supersampled.resize(dst_size, Image.BICUBIC, (0, 0, 59.125, 44.125)) with_box = supersampled.resize(
without_box = supersampled.resize(dst_size, Image.BICUBIC) dst_size, Image.Resampling.BICUBIC, (0, 0, 59.125, 44.125)
)
without_box = supersampled.resize(dst_size, Image.Resampling.BICUBIC)
# error with box should be much smaller than without # error with box should be much smaller than without
assert_image_similar(reference, with_box, 6) assert_image_similar(reference, with_box, 6)
@ -496,7 +510,7 @@ class TestCoreResampleBox:
assert_image_similar(reference, without_box, 5) assert_image_similar(reference, without_box, 5)
def test_formats(self): def test_formats(self):
for resample in [Image.NEAREST, Image.BILINEAR]: for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]:
for mode in ["RGB", "L", "RGBA", "LA", "I", ""]: for mode in ["RGB", "L", "RGBA", "LA", "I", ""]:
im = hopper(mode) im = hopper(mode)
box = (20, 20, im.size[0] - 20, im.size[1] - 20) box = (20, 20, im.size[0] - 20, im.size[1] - 20)
@ -514,7 +528,7 @@ class TestCoreResampleBox:
((40, 50), (10, 0, 50, 50)), ((40, 50), (10, 0, 50, 50)),
((40, 50), (10, 20, 50, 70)), ((40, 50), (10, 20, 50, 70)),
]: ]:
res = im.resize(size, Image.LANCZOS, box) res = im.resize(size, Image.Resampling.LANCZOS, box)
assert res.size == size assert res.size == size
assert_image_equal(res, im.crop(box), f">>> {size} {box}") assert_image_equal(res, im.crop(box), f">>> {size} {box}")
@ -528,7 +542,7 @@ class TestCoreResampleBox:
((40, 50), (10.4, 0.4, 50.4, 50.4)), ((40, 50), (10.4, 0.4, 50.4, 50.4)),
((40, 50), (10.4, 20.4, 50.4, 70.4)), ((40, 50), (10.4, 20.4, 50.4, 70.4)),
]: ]:
res = im.resize(size, Image.LANCZOS, box) res = im.resize(size, Image.Resampling.LANCZOS, box)
assert res.size == size assert res.size == size
with pytest.raises(AssertionError, match=r"difference \d"): with pytest.raises(AssertionError, match=r"difference \d"):
# check that the difference at least that much # check that the difference at least that much
@ -538,7 +552,7 @@ class TestCoreResampleBox:
# Can skip resize for one dimension # Can skip resize for one dimension
im = hopper() im = hopper()
for flt in [Image.NEAREST, Image.BICUBIC]: for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]:
for size, box in [ for size, box in [
((40, 50), (0, 0, 40, 90)), ((40, 50), (0, 0, 40, 90)),
((40, 50), (0, 20, 40, 90)), ((40, 50), (0, 20, 40, 90)),
@ -559,7 +573,7 @@ class TestCoreResampleBox:
# Can skip resize for one dimension # Can skip resize for one dimension
im = hopper() im = hopper()
for flt in [Image.NEAREST, Image.BICUBIC]: for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]:
for size, box in [ for size, box in [
((40, 50), (0, 0, 90, 50)), ((40, 50), (0, 0, 90, 50)),
((40, 50), (20, 0, 90, 50)), ((40, 50), (20, 0, 90, 50)),

View File

@ -35,33 +35,33 @@ class TestImagingCoreResize:
"I;16", "I;16",
]: # exotic mode ]: # exotic mode
im = hopper(mode) im = hopper(mode)
r = self.resize(im, (15, 12), Image.NEAREST) r = self.resize(im, (15, 12), Image.Resampling.NEAREST)
assert r.mode == mode assert r.mode == mode
assert r.size == (15, 12) assert r.size == (15, 12)
assert r.im.bands == im.im.bands assert r.im.bands == im.im.bands
def test_convolution_modes(self): def test_convolution_modes(self):
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.resize(hopper("1"), (15, 12), Image.BILINEAR) self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR)
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.resize(hopper("P"), (15, 12), Image.BILINEAR) self.resize(hopper("P"), (15, 12), Image.Resampling.BILINEAR)
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.resize(hopper("I;16"), (15, 12), Image.BILINEAR) self.resize(hopper("I;16"), (15, 12), Image.Resampling.BILINEAR)
for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]: for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]:
im = hopper(mode) im = hopper(mode)
r = self.resize(im, (15, 12), Image.BILINEAR) r = self.resize(im, (15, 12), Image.Resampling.BILINEAR)
assert r.mode == mode assert r.mode == mode
assert r.size == (15, 12) assert r.size == (15, 12)
assert r.im.bands == im.im.bands assert r.im.bands == im.im.bands
def test_reduce_filters(self): def test_reduce_filters(self):
for f in [ for f in [
Image.NEAREST, Image.Resampling.NEAREST,
Image.BOX, Image.Resampling.BOX,
Image.BILINEAR, Image.Resampling.BILINEAR,
Image.HAMMING, Image.Resampling.HAMMING,
Image.BICUBIC, Image.Resampling.BICUBIC,
Image.LANCZOS, Image.Resampling.LANCZOS,
]: ]:
r = self.resize(hopper("RGB"), (15, 12), f) r = self.resize(hopper("RGB"), (15, 12), f)
assert r.mode == "RGB" assert r.mode == "RGB"
@ -69,12 +69,12 @@ class TestImagingCoreResize:
def test_enlarge_filters(self): def test_enlarge_filters(self):
for f in [ for f in [
Image.NEAREST, Image.Resampling.NEAREST,
Image.BOX, Image.Resampling.BOX,
Image.BILINEAR, Image.Resampling.BILINEAR,
Image.HAMMING, Image.Resampling.HAMMING,
Image.BICUBIC, Image.Resampling.BICUBIC,
Image.LANCZOS, Image.Resampling.LANCZOS,
]: ]:
r = self.resize(hopper("RGB"), (212, 195), f) r = self.resize(hopper("RGB"), (212, 195), f)
assert r.mode == "RGB" assert r.mode == "RGB"
@ -95,12 +95,12 @@ class TestImagingCoreResize:
samples["dirty"].putpixel((1, 1), 128) samples["dirty"].putpixel((1, 1), 128)
for f in [ for f in [
Image.NEAREST, Image.Resampling.NEAREST,
Image.BOX, Image.Resampling.BOX,
Image.BILINEAR, Image.Resampling.BILINEAR,
Image.HAMMING, Image.Resampling.HAMMING,
Image.BICUBIC, Image.Resampling.BICUBIC,
Image.LANCZOS, Image.Resampling.LANCZOS,
]: ]:
# samples resized with current filter # samples resized with current filter
references = { references = {
@ -124,12 +124,12 @@ class TestImagingCoreResize:
def test_enlarge_zero(self): def test_enlarge_zero(self):
for f in [ for f in [
Image.NEAREST, Image.Resampling.NEAREST,
Image.BOX, Image.Resampling.BOX,
Image.BILINEAR, Image.Resampling.BILINEAR,
Image.HAMMING, Image.Resampling.HAMMING,
Image.BICUBIC, Image.Resampling.BICUBIC,
Image.LANCZOS, Image.Resampling.LANCZOS,
]: ]:
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f) r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f)
assert r.mode == "RGB" assert r.mode == "RGB"
@ -164,15 +164,19 @@ def gradients_image():
class TestReducingGapResize: class TestReducingGapResize:
def test_reducing_gap_values(self, gradients_image): def test_reducing_gap_values(self, gradients_image):
ref = gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=None) ref = gradients_image.resize(
im = gradients_image.resize((52, 34), Image.BICUBIC) (52, 34), Image.Resampling.BICUBIC, reducing_gap=None
)
im = gradients_image.resize((52, 34), Image.Resampling.BICUBIC)
assert_image_equal(ref, im) assert_image_equal(ref, im)
with pytest.raises(ValueError): with pytest.raises(ValueError):
gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0) gradients_image.resize((52, 34), Image.Resampling.BICUBIC, reducing_gap=0)
with pytest.raises(ValueError): with pytest.raises(ValueError):
gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0.99) gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99
)
def test_reducing_gap_1(self, gradients_image): def test_reducing_gap_1(self, gradients_image):
for box, epsilon in [ for box, epsilon in [
@ -180,9 +184,9 @@ class TestReducingGapResize:
((1.1, 2.2, 510.8, 510.9), 4), ((1.1, 2.2, 510.8, 510.9), 4),
((3, 10, 410, 256), 10), ((3, 10, 410, 256), 10),
]: ]:
ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=1.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
) )
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
@ -196,9 +200,9 @@ class TestReducingGapResize:
((1.1, 2.2, 510.8, 510.9), 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5),
((3, 10, 410, 256), 1), ((3, 10, 410, 256), 1),
]: ]:
ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=2.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
) )
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
@ -212,9 +216,9 @@ class TestReducingGapResize:
((1.1, 2.2, 510.8, 510.9), 1), ((1.1, 2.2, 510.8, 510.9), 1),
((3, 10, 410, 256), 0.5), ((3, 10, 410, 256), 0.5),
]: ]:
ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=3.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
) )
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
@ -224,9 +228,9 @@ class TestReducingGapResize:
def test_reducing_gap_8(self, gradients_image): def test_reducing_gap_8(self, gradients_image):
for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]: for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]:
ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=8.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0
) )
assert_image_equal(ref, im) assert_image_equal(ref, im)
@ -236,8 +240,10 @@ class TestReducingGapResize:
((0, 0, 512, 512), 5.5), ((0, 0, 512, 512), 5.5),
((0.9, 1.7, 128, 128), 9.5), ((0.9, 1.7, 128, 128), 9.5),
]: ]:
ref = gradients_image.resize((52, 34), Image.BOX, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box)
im = gradients_image.resize((52, 34), Image.BOX, box=box, reducing_gap=1.0) im = gradients_image.resize(
(52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0
)
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
@ -261,12 +267,12 @@ class TestImageResize:
def test_default_filter(self): def test_default_filter(self):
for mode in "L", "RGB", "I", "F": for mode in "L", "RGB", "I", "F":
im = hopper(mode) im = hopper(mode)
assert im.resize((20, 20), Image.BICUBIC) == im.resize((20, 20)) assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20))
for mode in "1", "P": for mode in "1", "P":
im = hopper(mode) im = hopper(mode)
assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20)) assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))
for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16": for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16":
im = hopper(mode) im = hopper(mode)
assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20)) assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))

View File

@ -46,14 +46,14 @@ def test_zero():
def test_resample(): def test_resample():
# Target image creation, inspected by eye. # Target image creation, inspected by eye.
# >>> im = Image.open('Tests/images/hopper.ppm') # >>> im = Image.open('Tests/images/hopper.ppm')
# >>> im = im.rotate(45, resample=Image.BICUBIC, expand=True) # >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True)
# >>> im.save('Tests/images/hopper_45.png') # >>> im.save('Tests/images/hopper_45.png')
with Image.open("Tests/images/hopper_45.png") as target: with Image.open("Tests/images/hopper_45.png") as target:
for (resample, epsilon) in ( for (resample, epsilon) in (
(Image.NEAREST, 10), (Image.Resampling.NEAREST, 10),
(Image.BILINEAR, 5), (Image.Resampling.BILINEAR, 5),
(Image.BICUBIC, 0), (Image.Resampling.BICUBIC, 0),
): ):
im = hopper() im = hopper()
im = im.rotate(45, resample=resample, expand=True) im = im.rotate(45, resample=resample, expand=True)
@ -62,7 +62,7 @@ def test_resample():
def test_center_0(): def test_center_0():
im = hopper() im = hopper()
im = im.rotate(45, center=(0, 0), resample=Image.BICUBIC) im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC)
with Image.open("Tests/images/hopper_45.png") as target: with Image.open("Tests/images/hopper_45.png") as target:
target_origin = target.size[1] / 2 target_origin = target.size[1] / 2
@ -73,7 +73,7 @@ def test_center_0():
def test_center_14(): def test_center_14():
im = hopper() im = hopper()
im = im.rotate(45, center=(14, 14), resample=Image.BICUBIC) im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC)
with Image.open("Tests/images/hopper_45.png") as target: with Image.open("Tests/images/hopper_45.png") as target:
target_origin = target.size[1] / 2 - 14 target_origin = target.size[1] / 2 - 14
@ -90,7 +90,7 @@ def test_translate():
(target_origin, target_origin, target_origin + 128, target_origin + 128) (target_origin, target_origin, target_origin + 128, target_origin + 128)
) )
im = im.rotate(45, translate=(5, 5), resample=Image.BICUBIC) im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC)
assert_image_similar(im, target, 1) assert_image_similar(im, target, 1)

View File

@ -97,24 +97,24 @@ def test_DCT_scaling_edges():
thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0)) thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0))
# small reducing_gap to amplify the effect # small reducing_gap to amplify the effect
thumb.thumbnail((32, 32), Image.BICUBIC, reducing_gap=1.0) thumb.thumbnail((32, 32), Image.Resampling.BICUBIC, reducing_gap=1.0)
ref = im.resize((32, 32), Image.BICUBIC) ref = im.resize((32, 32), Image.Resampling.BICUBIC)
# This is still JPEG, some error is present. Without the fix it is 11.5 # This is still JPEG, some error is present. Without the fix it is 11.5
assert_image_similar(thumb, ref, 1.5) assert_image_similar(thumb, ref, 1.5)
def test_reducing_gap_values(): def test_reducing_gap_values():
im = hopper() im = hopper()
im.thumbnail((18, 18), Image.BICUBIC) im.thumbnail((18, 18), Image.Resampling.BICUBIC)
ref = hopper() ref = hopper()
ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=2.0) ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=2.0)
# reducing_gap=2.0 should be the default # reducing_gap=2.0 should be the default
assert_image_equal(ref, im) assert_image_equal(ref, im)
ref = hopper() ref = hopper()
ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=None) ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
assert_image_equal(ref, im) assert_image_equal(ref, im)
@ -125,9 +125,9 @@ def test_reducing_gap_for_DCT_scaling():
with Image.open("Tests/images/hopper.jpg") as ref: with Image.open("Tests/images/hopper.jpg") as ref:
# thumbnail should call draft with reducing_gap scale # thumbnail should call draft with reducing_gap scale
ref.draft(None, (18 * 3, 18 * 3)) ref.draft(None, (18 * 3, 18 * 3))
ref = ref.resize((18, 18), Image.BICUBIC) ref = ref.resize((18, 18), Image.Resampling.BICUBIC)
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
im.thumbnail((18, 18), Image.BICUBIC, reducing_gap=3.0) im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
assert_image_equal(ref, im) assert_image_equal(ref, im)

View File

@ -34,20 +34,22 @@ class TestImageTransform:
def test_palette(self): def test_palette(self):
with Image.open("Tests/images/hopper.gif") as im: with Image.open("Tests/images/hopper.gif") as im:
transformed = im.transform(im.size, Image.AFFINE, [1, 0, 0, 0, 1, 0]) transformed = im.transform(
im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0]
)
assert im.palette.palette == transformed.palette.palette assert im.palette.palette == transformed.palette.palette
def test_extent(self): def test_extent(self):
im = hopper("RGB") im = hopper("RGB")
(w, h) = im.size (w, h) = im.size
# fmt: off # fmt: off
transformed = im.transform(im.size, Image.EXTENT, transformed = im.transform(im.size, Image.Transform.EXTENT,
(0, 0, (0, 0,
w//2, h//2), # ul -> lr w//2, h//2), # ul -> lr
Image.BILINEAR) Image.Resampling.BILINEAR)
# fmt: on # fmt: on
scaled = im.resize((w * 2, h * 2), Image.BILINEAR).crop((0, 0, w, h)) scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h))
# undone -- precision? # undone -- precision?
assert_image_similar(transformed, scaled, 23) assert_image_similar(transformed, scaled, 23)
@ -57,15 +59,18 @@ class TestImageTransform:
im = hopper("RGB") im = hopper("RGB")
(w, h) = im.size (w, h) = im.size
# fmt: off # fmt: off
transformed = im.transform(im.size, Image.QUAD, transformed = im.transform(im.size, Image.Transform.QUAD,
(0, 0, 0, h//2, (0, 0, 0, h//2,
# ul -> ccw around quad: # ul -> ccw around quad:
w//2, h//2, w//2, 0), w//2, h//2, w//2, 0),
Image.BILINEAR) Image.Resampling.BILINEAR)
# fmt: on # fmt: on
scaled = im.transform( scaled = im.transform(
(w, h), Image.AFFINE, (0.5, 0, 0, 0, 0.5, 0), Image.BILINEAR (w, h),
Image.Transform.AFFINE,
(0.5, 0, 0, 0, 0.5, 0),
Image.Resampling.BILINEAR,
) )
assert_image_equal(transformed, scaled) assert_image_equal(transformed, scaled)
@ -80,9 +85,9 @@ class TestImageTransform:
(w, h) = im.size (w, h) = im.size
transformed = im.transform( transformed = im.transform(
im.size, im.size,
Image.EXTENT, Image.Transform.EXTENT,
(0, 0, w * 2, h * 2), (0, 0, w * 2, h * 2),
Image.BILINEAR, Image.Resampling.BILINEAR,
fillcolor="red", fillcolor="red",
) )
@ -93,18 +98,21 @@ class TestImageTransform:
im = hopper("RGBA") im = hopper("RGBA")
(w, h) = im.size (w, h) = im.size
# fmt: off # fmt: off
transformed = im.transform(im.size, Image.MESH, transformed = im.transform(im.size, Image.Transform.MESH,
[((0, 0, w//2, h//2), # box [((0, 0, w//2, h//2), # box
(0, 0, 0, h, (0, 0, 0, h,
w, h, w, 0)), # ul -> ccw around quad w, h, w, 0)), # ul -> ccw around quad
((w//2, h//2, w, h), # box ((w//2, h//2, w, h), # box
(0, 0, 0, h, (0, 0, 0, h,
w, h, w, 0))], # ul -> ccw around quad w, h, w, 0))], # ul -> ccw around quad
Image.BILINEAR) Image.Resampling.BILINEAR)
# fmt: on # fmt: on
scaled = im.transform( scaled = im.transform(
(w // 2, h // 2), Image.AFFINE, (2, 0, 0, 0, 2, 0), Image.BILINEAR (w // 2, h // 2),
Image.Transform.AFFINE,
(2, 0, 0, 0, 2, 0),
Image.Resampling.BILINEAR,
) )
checker = Image.new("RGBA", im.size) checker = Image.new("RGBA", im.size)
@ -137,14 +145,16 @@ class TestImageTransform:
def test_alpha_premult_resize(self): def test_alpha_premult_resize(self):
def op(im, sz): def op(im, sz):
return im.resize(sz, Image.BILINEAR) return im.resize(sz, Image.Resampling.BILINEAR)
self._test_alpha_premult(op) self._test_alpha_premult(op)
def test_alpha_premult_transform(self): def test_alpha_premult_transform(self):
def op(im, sz): def op(im, sz):
(w, h) = im.size (w, h) = im.size
return im.transform(sz, Image.EXTENT, (0, 0, w, h), Image.BILINEAR) return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR
)
self._test_alpha_premult(op) self._test_alpha_premult(op)
@ -171,7 +181,7 @@ class TestImageTransform:
@pytest.mark.parametrize("mode", ("RGBA", "LA")) @pytest.mark.parametrize("mode", ("RGBA", "LA"))
def test_nearest_resize(self, mode): def test_nearest_resize(self, mode):
def op(im, sz): def op(im, sz):
return im.resize(sz, Image.NEAREST) return im.resize(sz, Image.Resampling.NEAREST)
self._test_nearest(op, mode) self._test_nearest(op, mode)
@ -179,7 +189,9 @@ class TestImageTransform:
def test_nearest_transform(self, mode): def test_nearest_transform(self, mode):
def op(im, sz): def op(im, sz):
(w, h) = im.size (w, h) = im.size
return im.transform(sz, Image.EXTENT, (0, 0, w, h), Image.NEAREST) return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST
)
self._test_nearest(op, mode) self._test_nearest(op, mode)
@ -213,13 +225,15 @@ class TestImageTransform:
def test_unknown_resampling_filter(self): def test_unknown_resampling_filter(self):
with hopper() as im: with hopper() as im:
(w, h) = im.size (w, h) = im.size
for resample in (Image.BOX, "unknown"): for resample in (Image.Resampling.BOX, "unknown"):
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.transform((100, 100), Image.EXTENT, (0, 0, w, h), resample) im.transform(
(100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample
)
class TestImageTransformAffine: class TestImageTransformAffine:
transform = Image.AFFINE transform = Image.Transform.AFFINE
def _test_image(self): def _test_image(self):
im = hopper("RGB") im = hopper("RGB")
@ -247,7 +261,11 @@ class TestImageTransformAffine:
else: else:
transposed = im transposed = im
for resample in [Image.NEAREST, Image.BILINEAR, Image.BICUBIC]: for resample in [
Image.Resampling.NEAREST,
Image.Resampling.BILINEAR,
Image.Resampling.BICUBIC,
]:
transformed = im.transform( transformed = im.transform(
transposed.size, self.transform, matrix, resample transposed.size, self.transform, matrix, resample
) )
@ -257,13 +275,13 @@ class TestImageTransformAffine:
self._test_rotate(0, None) self._test_rotate(0, None)
def test_rotate_90_deg(self): def test_rotate_90_deg(self):
self._test_rotate(90, Image.ROTATE_90) self._test_rotate(90, Image.Transpose.ROTATE_90)
def test_rotate_180_deg(self): def test_rotate_180_deg(self):
self._test_rotate(180, Image.ROTATE_180) self._test_rotate(180, Image.Transpose.ROTATE_180)
def test_rotate_270_deg(self): def test_rotate_270_deg(self):
self._test_rotate(270, Image.ROTATE_270) self._test_rotate(270, Image.Transpose.ROTATE_270)
def _test_resize(self, scale, epsilonscale): def _test_resize(self, scale, epsilonscale):
im = self._test_image() im = self._test_image()
@ -273,9 +291,9 @@ class TestImageTransformAffine:
matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0]
for resample, epsilon in [ for resample, epsilon in [
(Image.NEAREST, 0), (Image.Resampling.NEAREST, 0),
(Image.BILINEAR, 2), (Image.Resampling.BILINEAR, 2),
(Image.BICUBIC, 1), (Image.Resampling.BICUBIC, 1),
]: ]:
transformed = im.transform(size_up, self.transform, matrix_up, resample) transformed = im.transform(size_up, self.transform, matrix_up, resample)
transformed = transformed.transform( transformed = transformed.transform(
@ -306,9 +324,9 @@ class TestImageTransformAffine:
matrix_down = [1, 0, x, 0, 1, y, 0, 0] matrix_down = [1, 0, x, 0, 1, y, 0, 0]
for resample, epsilon in [ for resample, epsilon in [
(Image.NEAREST, 0), (Image.Resampling.NEAREST, 0),
(Image.BILINEAR, 1.5), (Image.Resampling.BILINEAR, 1.5),
(Image.BICUBIC, 1), (Image.Resampling.BICUBIC, 1),
]: ]:
transformed = im.transform(size_up, self.transform, matrix_up, resample) transformed = im.transform(size_up, self.transform, matrix_up, resample)
transformed = transformed.transform( transformed = transformed.transform(
@ -328,4 +346,4 @@ class TestImageTransformAffine:
class TestImageTransformPerspective(TestImageTransformAffine): class TestImageTransformPerspective(TestImageTransformAffine):
# Repeat all tests for AFFINE transformations with PERSPECTIVE # Repeat all tests for AFFINE transformations with PERSPECTIVE
transform = Image.PERSPECTIVE transform = Image.Transform.PERSPECTIVE

View File

@ -1,12 +1,4 @@
from PIL.Image import ( from PIL.Image import Transpose
FLIP_LEFT_RIGHT,
FLIP_TOP_BOTTOM,
ROTATE_90,
ROTATE_180,
ROTATE_270,
TRANSPOSE,
TRANSVERSE,
)
from . import helper from . import helper
from .helper import assert_image_equal from .helper import assert_image_equal
@ -20,7 +12,7 @@ HOPPER = {
def test_flip_left_right(): def test_flip_left_right():
def transpose(mode): def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(FLIP_LEFT_RIGHT) out = im.transpose(Transpose.FLIP_LEFT_RIGHT)
assert out.mode == mode assert out.mode == mode
assert out.size == im.size assert out.size == im.size
@ -37,7 +29,7 @@ def test_flip_left_right():
def test_flip_top_bottom(): def test_flip_top_bottom():
def transpose(mode): def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(FLIP_TOP_BOTTOM) out = im.transpose(Transpose.FLIP_TOP_BOTTOM)
assert out.mode == mode assert out.mode == mode
assert out.size == im.size assert out.size == im.size
@ -54,7 +46,7 @@ def test_flip_top_bottom():
def test_rotate_90(): def test_rotate_90():
def transpose(mode): def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(ROTATE_90) out = im.transpose(Transpose.ROTATE_90)
assert out.mode == mode assert out.mode == mode
assert out.size == im.size[::-1] assert out.size == im.size[::-1]
@ -71,7 +63,7 @@ def test_rotate_90():
def test_rotate_180(): def test_rotate_180():
def transpose(mode): def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(ROTATE_180) out = im.transpose(Transpose.ROTATE_180)
assert out.mode == mode assert out.mode == mode
assert out.size == im.size assert out.size == im.size
@ -88,7 +80,7 @@ def test_rotate_180():
def test_rotate_270(): def test_rotate_270():
def transpose(mode): def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(ROTATE_270) out = im.transpose(Transpose.ROTATE_270)
assert out.mode == mode assert out.mode == mode
assert out.size == im.size[::-1] assert out.size == im.size[::-1]
@ -105,7 +97,7 @@ def test_rotate_270():
def test_transpose(): def test_transpose():
def transpose(mode): def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(TRANSPOSE) out = im.transpose(Transpose.TRANSPOSE)
assert out.mode == mode assert out.mode == mode
assert out.size == im.size[::-1] assert out.size == im.size[::-1]
@ -122,7 +114,7 @@ def test_transpose():
def test_tranverse(): def test_tranverse():
def transpose(mode): def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(TRANSVERSE) out = im.transpose(Transpose.TRANSVERSE)
assert out.mode == mode assert out.mode == mode
assert out.size == im.size[::-1] assert out.size == im.size[::-1]
@ -143,20 +135,31 @@ def test_roundtrip():
def transpose(first, second): def transpose(first, second):
return im.transpose(first).transpose(second) return im.transpose(first).transpose(second)
assert_image_equal(im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT))
assert_image_equal(im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM))
assert_image_equal(im, transpose(ROTATE_90, ROTATE_270))
assert_image_equal(im, transpose(ROTATE_180, ROTATE_180))
assert_image_equal( assert_image_equal(
im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM) im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT)
) )
assert_image_equal( assert_image_equal(
im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT) im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM)
)
assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270))
assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180))
assert_image_equal(
im.transpose(Transpose.TRANSPOSE),
transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM),
) )
assert_image_equal( assert_image_equal(
im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT) im.transpose(Transpose.TRANSPOSE),
transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT),
) )
assert_image_equal( assert_image_equal(
im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM) im.transpose(Transpose.TRANSVERSE),
transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT),
)
assert_image_equal(
im.transpose(Transpose.TRANSVERSE),
transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM),
)
assert_image_equal(
im.transpose(Transpose.TRANSVERSE),
transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE),
) )
assert_image_equal(im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE))

View File

@ -140,7 +140,7 @@ def test_intent():
skip_missing() skip_missing()
assert ImageCms.getDefaultIntent(SRGB) == 0 assert ImageCms.getDefaultIntent(SRGB) == 0
support = ImageCms.isIntentSupported( support = ImageCms.isIntentSupported(
SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT SRGB, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT
) )
assert support == 1 assert support == 1
@ -153,7 +153,7 @@ def test_profile_object():
# ["sRGB built-in", "", "WhitePoint : D65 (daylight)", "", ""] # ["sRGB built-in", "", "WhitePoint : D65 (daylight)", "", ""]
assert ImageCms.getDefaultIntent(p) == 0 assert ImageCms.getDefaultIntent(p) == 0
support = ImageCms.isIntentSupported( support = ImageCms.isIntentSupported(
p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT p, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT
) )
assert support == 1 assert support == 1
@ -593,3 +593,13 @@ def test_auxiliary_channels_isolated():
) )
assert_image_equal(test_image.convert(dst_format[2]), reference_image) assert_image_equal(test_image.convert(dst_format[2]), reference_image)
def test_constants_deprecation():
for enum, prefix in {
ImageCms.Intent: "INTENT_",
ImageCms.Direction: "DIRECTION_",
}.items():
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(ImageCms, prefix + name) == enum[name]

View File

@ -183,7 +183,7 @@ def test_bitmap():
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
with Image.open("Tests/images/pil123rgba.png") as small: with Image.open("Tests/images/pil123rgba.png") as small:
small = small.resize((50, 50), Image.NEAREST) small = small.resize((50, 50), Image.Resampling.NEAREST)
# Act # Act
draw.bitmap((10, 10), small) draw.bitmap((10, 10), small)
@ -319,7 +319,7 @@ def test_ellipse_symmetric():
im = Image.new("RGB", (width, 100)) im = Image.new("RGB", (width, 100))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
draw.ellipse(bbox, fill="green", outline="blue") draw.ellipse(bbox, fill="green", outline="blue")
assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT)) assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT))
def test_ellipse_width(): def test_ellipse_width():

View File

@ -23,7 +23,7 @@ class TestImageFile:
def test_parser(self): def test_parser(self):
def roundtrip(format): def roundtrip(format):
im = hopper("L").resize((1000, 1000), Image.NEAREST) im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
if format in ("MSP", "XBM"): if format in ("MSP", "XBM"):
im = im.convert("1") im = im.convert("1")

View File

@ -29,7 +29,7 @@ pytestmark = skip_unless_feature("freetype2")
class TestImageFont: class TestImageFont:
LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC LAYOUT_ENGINE = ImageFont.Layout.BASIC
def get_font(self): def get_font(self):
return ImageFont.truetype( return ImageFont.truetype(
@ -94,12 +94,12 @@ class TestImageFont:
try: try:
ttf = ImageFont.truetype( ttf = ImageFont.truetype(
FONT_PATH, FONT_SIZE, layout_engine=ImageFont.LAYOUT_RAQM FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM
) )
finally: finally:
ImageFont.core.HAVE_RAQM = have_raqm ImageFont.core.HAVE_RAQM = have_raqm
assert ttf.layout_engine == ImageFont.LAYOUT_BASIC assert ttf.layout_engine == ImageFont.Layout.BASIC
def _render(self, font): def _render(self, font):
txt = "Hello World!" txt = "Hello World!"
@ -182,7 +182,7 @@ class TestImageFont:
im = Image.new(mode, (1, 1), 0) im = Image.new(mode, (1, 1), 0)
d = ImageDraw.Draw(im) d = ImageDraw.Draw(im)
if self.LAYOUT_ENGINE == ImageFont.LAYOUT_BASIC: if self.LAYOUT_ENGINE == ImageFont.Layout.BASIC:
length = d.textlength(text, f) length = d.textlength(text, f)
assert length == length_basic assert length == length_basic
else: else:
@ -294,7 +294,7 @@ class TestImageFont:
word = "testing" word = "testing"
font = self.get_font() font = self.get_font()
orientation = Image.ROTATE_90 orientation = Image.Transpose.ROTATE_90
transposed_font = ImageFont.TransposedFont(font, orientation=orientation) transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
# Original font # Original font
@ -333,7 +333,7 @@ class TestImageFont:
# Arrange # Arrange
text = "mask this" text = "mask this"
font = self.get_font() font = self.get_font()
orientation = Image.ROTATE_90 orientation = Image.Transpose.ROTATE_90
transposed_font = ImageFont.TransposedFont(font, orientation=orientation) transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
# Act # Act
@ -604,7 +604,7 @@ class TestImageFont:
# Arrange # Arrange
t = self.get_font() t = self.get_font()
# Act / Assert # Act / Assert
if t.layout_engine == ImageFont.LAYOUT_BASIC: if t.layout_engine == ImageFont.Layout.BASIC:
with pytest.raises(KeyError): with pytest.raises(KeyError):
t.getmask("абвг", direction="rtl") t.getmask("абвг", direction="rtl")
with pytest.raises(KeyError): with pytest.raises(KeyError):
@ -753,7 +753,7 @@ class TestImageFont:
name, text = "quick", "Quick" name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png" path = f"Tests/images/test_anchor_{name}_{anchor}.png"
if self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM: if self.LAYOUT_ENGINE == ImageFont.Layout.RAQM:
width, height = (129, 44) width, height = (129, 44)
else: else:
width, height = (128, 44) width, height = (128, 44)
@ -993,7 +993,7 @@ class TestImageFont:
@skip_unless_feature("raqm") @skip_unless_feature("raqm")
class TestImageFont_RaqmLayout(TestImageFont): class TestImageFont_RaqmLayout(TestImageFont):
LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM LAYOUT_ENGINE = ImageFont.Layout.RAQM
def test_render_mono_size(): def test_render_mono_size():
@ -1004,7 +1004,7 @@ def test_render_mono_size():
ttf = ImageFont.truetype( ttf = ImageFont.truetype(
"Tests/fonts/DejaVuSans/DejaVuSans.ttf", "Tests/fonts/DejaVuSans/DejaVuSans.ttf",
18, 18,
layout_engine=ImageFont.LAYOUT_BASIC, layout_engine=ImageFont.Layout.BASIC,
) )
draw.text((10, 10), "r" * 10, "black", ttf) draw.text((10, 10), "r" * 10, "black", ttf)
@ -1028,10 +1028,19 @@ def test_raqm_missing_warning(monkeypatch):
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
with pytest.warns(UserWarning) as record: with pytest.warns(UserWarning) as record:
font = ImageFont.truetype( font = ImageFont.truetype(
FONT_PATH, FONT_SIZE, layout_engine=ImageFont.LAYOUT_RAQM FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM
) )
assert font.layout_engine == ImageFont.LAYOUT_BASIC assert font.layout_engine == ImageFont.Layout.BASIC
assert str(record[-1].message) == ( assert str(record[-1].message) == (
"Raqm layout was requested, but Raqm is not available. " "Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout." "Falling back to basic layout."
) )
def test_constants_deprecation():
for enum, prefix in {
ImageFont.Layout: "LAYOUT_",
}.items():
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(ImageFont, prefix + name) == enum[name]

View File

@ -34,7 +34,7 @@ def test_basic(tmp_path):
imOut = imIn.copy() imOut = imIn.copy()
verify(imOut) # copy verify(imOut) # copy
imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) imOut = imIn.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(imOut) # transform verify(imOut) # transform
filename = str(tmp_path / "temp.im") filename = str(tmp_path / "temp.im")

View File

@ -66,6 +66,73 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest ``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest
``viewer.show_file(path="test.jpg")`` instead. ``viewer.show_file(path="test.jpg")`` instead.
Constants
~~~~~~~~~
.. deprecated:: 9.2.0
A number of constants have been deprecated and will be removed in Pillow 10.0.0
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
===================================================== ============================================================
Deprecated Use instead
===================================================== ============================================================
``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
``Image.ORDERED`` ``Image.Dither.ORDERED``
``Image.RASTERIZE`` ``Image.Dither.RASTERIZE``
``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG``
``Image.WEB`` ``Image.Palette.WEB``
``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE``
``Image.AFFINE`` ``Image.Transform.AFFINE``
``Image.EXTENT`` ``Image.Transform.EXTENT``
``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE``
``Image.QUAD`` ``Image.Transform.QUAD``
``Image.MESH`` ``Image.Transform.MESH``
``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT``
``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM``
``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90``
``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180``
``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270``
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
``Image.BOX`` ``Image.Resampling.BOX``
``Image.BILINEAR`` ``Image.Resampling.BILNEAR``
``Image.LINEAR`` ``Image.Resampling.BILNEAR``
``Image.HAMMING`` ``Image.Resampling.HAMMING``
``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
``Image.CUBIC`` ``Image.Resampling.BICUBIC``
``Image.LANCZOS`` ``Image.Resampling.LANCZOS``
``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS``
``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT``
``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE``
``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE``
``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT``
``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL``
``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC``
``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION``
``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``
``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT``
``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT``
``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF``
``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC``
``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM``
``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG``
``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED``
``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT``
``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5``
``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1``
``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED``
``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE``
``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND``
``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS``
``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE``
``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER``
===================================================== ============================================================
FitsStubImagePlugin FitsStubImagePlugin
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~

View File

@ -696,12 +696,12 @@ parameter must be set to ``True``. The following parameters can also be set:
operation to be used for this frame before rendering the next frame. operation to be used for this frame before rendering the next frame.
Defaults to 0. Defaults to 0.
* 0 (:py:data:`~PIL.PngImagePlugin.APNG_DISPOSE_OP_NONE`, default) - * 0 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_NONE`, default) -
No disposal is done on this frame before rendering the next frame. No disposal is done on this frame before rendering the next frame.
* 1 (:py:data:`PIL.PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`) - * 1 (:py:data:`PIL.PngImagePlugin.Disposal.OP_BACKGROUND`) -
This frame's modified region is cleared to fully transparent black before This frame's modified region is cleared to fully transparent black before
rendering the next frame. rendering the next frame.
* 2 (:py:data:`~PIL.PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`) - * 2 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_PREVIOUS`) -
This frame's modified region is reverted to the previous frame's contents before This frame's modified region is reverted to the previous frame's contents before
rendering the next frame. rendering the next frame.
@ -710,10 +710,10 @@ parameter must be set to ``True``. The following parameters can also be set:
operation to be used for this frame before rendering the next frame. operation to be used for this frame before rendering the next frame.
Defaults to 0. Defaults to 0.
* 0 (:py:data:`~PIL.PngImagePlugin.APNG_BLEND_OP_SOURCE`) - * 0 (:py:data:`~PIL.PngImagePlugin.Blend.OP_SOURCE`) -
All color components of this frame, including alpha, overwrite the previous output All color components of this frame, including alpha, overwrite the previous output
image contents. image contents.
* 1 (:py:data:`~PIL.PngImagePlugin.APNG_BLEND_OP_OVER`) - * 1 (:py:data:`~PIL.PngImagePlugin.Blend.OP_OVER`) -
This frame should be alpha composited with the previous output image contents. This frame should be alpha composited with the previous output image contents.
.. note:: .. note::

View File

@ -155,7 +155,7 @@ Processing a subrectangle, and pasting it back
:: ::
region = region.transpose(Image.ROTATE_180) region = region.transpose(Image.Transpose.ROTATE_180)
im.paste(region, box) im.paste(region, box)
When pasting regions back, the size of the region must match the given region When pasting regions back, the size of the region must match the given region
@ -238,11 +238,11 @@ Transposing an image
:: ::
out = im.transpose(Image.FLIP_LEFT_RIGHT) out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
out = im.transpose(Image.FLIP_TOP_BOTTOM) out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
out = im.transpose(Image.ROTATE_90) out = im.transpose(Image.Transpose.ROTATE_90)
out = im.transpose(Image.ROTATE_180) out = im.transpose(Image.Transpose.ROTATE_180)
out = im.transpose(Image.ROTATE_270) out = im.transpose(Image.Transpose.ROTATE_270)
``transpose(ROTATE)`` operations can also be performed identically with ``transpose(ROTATE)`` operations can also be performed identically with
:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is :py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is

View File

@ -215,7 +215,7 @@ Many of Pillow's features require external libraries:
Once you have installed the prerequisites, run:: Once you have installed the prerequisites, run::
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow python3 -m pip install --upgrade Pillow --no-binary :all:
If the prerequisites are installed in the standard library locations If the prerequisites are installed in the standard library locations
for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
@ -225,7 +225,7 @@ those locations by editing :file:`setup.py` or
:file:`setup.cfg`, or by adding environment variables on the command :file:`setup.cfg`, or by adding environment variables on the command
line:: line::
CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
If Pillow has been previously built without the required If Pillow has been previously built without the required
prerequisites, it may be necessary to manually clear the pip cache or prerequisites, it may be necessary to manually clear the pip cache or
@ -291,7 +291,7 @@ tools.
The easiest way to install external libraries is via `Homebrew The easiest way to install external libraries is via `Homebrew
<https://brew.sh/>`_. After you install Homebrew, run:: <https://brew.sh/>`_. After you install Homebrew, run::
brew install libtiff libjpeg webp little-cms2 brew install libjpeg libtiff little-cms2 openjpeg webp
To install libraqm on macOS use Homebrew to install its dependencies:: To install libraqm on macOS use Homebrew to install its dependencies::
@ -302,7 +302,7 @@ Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
Now install Pillow with:: Now install Pillow with::
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow python3 -m pip install --upgrade Pillow --no-binary :all:
or from within the uncompressed source directory:: or from within the uncompressed source directory::
@ -349,7 +349,7 @@ Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
Now install Pillow with:: Now install Pillow with::
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow python3 -m pip install --upgrade Pillow --no-binary :all:
Building on FreeBSD Building on FreeBSD

View File

@ -254,7 +254,8 @@ This rotates the input image by ``theta`` degrees counter clockwise:
.. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transform
.. automethod:: PIL.Image.Image.transpose .. automethod:: PIL.Image.Image.transpose
This flips the input image by using the :data:`FLIP_LEFT_RIGHT` method. This flips the input image by using the :data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`
method.
.. code-block:: python .. code-block:: python
@ -263,9 +264,9 @@ This flips the input image by using the :data:`FLIP_LEFT_RIGHT` method.
with Image.open("hopper.jpg") as im: with Image.open("hopper.jpg") as im:
# Flip the image from left to right # Flip the image from left to right
im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT) im_flipped = im.transpose(method=Image.Transpose.FLIP_LEFT_RIGHT)
# To flip the image from top to bottom, # To flip the image from top to bottom,
# use the method "Image.FLIP_TOP_BOTTOM" # use the method "Image.Transpose.FLIP_TOP_BOTTOM"
.. automethod:: PIL.Image.Image.verify .. automethod:: PIL.Image.Image.verify
@ -389,68 +390,57 @@ Transpose methods
Used to specify the :meth:`Image.transpose` method to use. Used to specify the :meth:`Image.transpose` method to use.
.. data:: FLIP_LEFT_RIGHT .. autoclass:: Transpose
.. data:: FLIP_TOP_BOTTOM :members:
.. data:: ROTATE_90 :undoc-members:
.. data:: ROTATE_180
.. data:: ROTATE_270
.. data:: TRANSPOSE
.. data:: TRANSVERSE
Transform methods Transform methods
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
Used to specify the :meth:`Image.transform` method to use. Used to specify the :meth:`Image.transform` method to use.
.. data:: AFFINE .. py:class:: Transform
Affine transform .. py:attribute:: AFFINE
.. data:: EXTENT Affine transform
Cut out a rectangular subregion .. py:attribute:: EXTENT
.. data:: PERSPECTIVE Cut out a rectangular subregion
Perspective transform .. py:attribute:: PERSPECTIVE
.. data:: QUAD Perspective transform
Map a quadrilateral to a rectangle .. py:attribute:: QUAD
.. data:: MESH Map a quadrilateral to a rectangle
Map a number of source quadrilaterals in one operation .. py:attribute:: MESH
Map a number of source quadrilaterals in one operation
Resampling filters Resampling filters
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
See :ref:`concept-filters` for details. See :ref:`concept-filters` for details.
.. data:: NEAREST .. autoclass:: Resampling
:noindex: :members:
.. data:: BOX :undoc-members:
:noindex:
.. data:: BILINEAR
:noindex:
.. data:: HAMMING
:noindex:
.. data:: BICUBIC
:noindex:
.. data:: LANCZOS
:noindex:
Some filters are also available under the following names for backwards compatibility: Some deprecated filters are also available under the following names:
.. data:: NONE .. data:: NONE
:noindex: :noindex:
:value: NEAREST :value: Resampling.NEAREST
.. data:: LINEAR .. data:: LINEAR
:value: BILINEAR :value: Resampling.BILINEAR
.. data:: CUBIC .. data:: CUBIC
:value: BICUBIC :value: Resampling.BICUBIC
.. data:: ANTIALIAS .. data:: ANTIALIAS
:value: LANCZOS :value: Resampling.LANCZOS
Dither modes Dither modes
^^^^^^^^^^^^ ^^^^^^^^^^^^
@ -458,48 +448,56 @@ Dither modes
Used to specify the dithering method to use for the Used to specify the dithering method to use for the
:meth:`~Image.convert` and :meth:`~Image.quantize` methods. :meth:`~Image.convert` and :meth:`~Image.quantize` methods.
.. data:: NONE .. py:class:: Dither
:noindex:
No dither .. py:attribute:: NONE
.. comment: (not implemented) No dither
.. data:: ORDERED
.. data:: RASTERIZE
.. data:: FLOYDSTEINBERG .. py:attribute:: ORDERED
Floyd-Steinberg dither Not implemented
.. py:attribute:: RASTERIZE
Not implemented
.. py:attribute:: FLOYDSTEINBERG
Floyd-Steinberg dither
Palettes Palettes
^^^^^^^^ ^^^^^^^^
Used to specify the pallete to use for the :meth:`~Image.convert` method. Used to specify the pallete to use for the :meth:`~Image.convert` method.
.. data:: WEB .. autoclass:: Palette
.. data:: ADAPTIVE :members:
:undoc-members:
Quantization methods Quantization methods
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
Used to specify the quantization method to use for the :meth:`~Image.quantize` method. Used to specify the quantization method to use for the :meth:`~Image.quantize` method.
.. data:: MEDIANCUT .. py:class:: Quantize
Median cut. Default method, except for RGBA images. This method does not support .. py:attribute:: MEDIANCUT
RGBA images.
.. data:: MAXCOVERAGE Median cut. Default method, except for RGBA images. This method does not support
RGBA images.
Maximum coverage. This method does not support RGBA images. .. py:attribute:: MAXCOVERAGE
.. data:: FASTOCTREE Maximum coverage. This method does not support RGBA images.
Fast octree. Default method for RGBA images. .. py:attribute:: FASTOCTREE
.. data:: LIBIMAGEQUANT Fast octree. Default method for RGBA images.
libimagequant .. py:attribute:: LIBIMAGEQUANT
Check support using :py:func:`PIL.features.check_feature` libimagequant
with ``feature="libimagequant"``.
Check support using :py:func:`PIL.features.check_feature` with
``feature="libimagequant"``.

View File

@ -118,8 +118,8 @@ can be easily displayed in a chromaticity diagram, for example).
another profile (usually overridden at run-time, but provided here another profile (usually overridden at run-time, but provided here
for DeviceLink and embedded source profiles, see 7.2.15 of ICC.1:2010). for DeviceLink and embedded source profiles, see 7.2.15 of ICC.1:2010).
One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, ``ImageCms.INTENT_PERCEPTUAL``, One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, ``ImageCms.Intent.PERCEPTUAL``,
``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and ``ImageCms.INTENT_SATURATION``. ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and ``ImageCms.Intent.SATURATION``.
.. py:attribute:: profile_id .. py:attribute:: profile_id
:type: bytes :type: bytes
@ -313,14 +313,14 @@ can be easily displayed in a chromaticity diagram, for example).
the CLUT model. the CLUT model.
The dictionary is indexed by intents The dictionary is indexed by intents
(``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``,
``ImageCms.INTENT_PERCEPTUAL``, ``ImageCms.Intent.PERCEPTUAL``,
``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and
``ImageCms.INTENT_SATURATION``). ``ImageCms.Intent.SATURATION``).
The values are 3-tuples indexed by directions The values are 3-tuples indexed by directions
(``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``,
``ImageCms.DIRECTION_PROOF``). ``ImageCms.Direction.PROOF``).
The elements of the tuple are booleans. If the value is ``True``, The elements of the tuple are booleans. If the value is ``True``,
that intent is supported for that direction. that intent is supported for that direction.
@ -331,14 +331,14 @@ can be easily displayed in a chromaticity diagram, for example).
Returns a dictionary of all supported intents and directions. Returns a dictionary of all supported intents and directions.
The dictionary is indexed by intents The dictionary is indexed by intents
(``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``,
``ImageCms.INTENT_PERCEPTUAL``, ``ImageCms.Intent.PERCEPTUAL``,
``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and
``ImageCms.INTENT_SATURATION``). ``ImageCms.Intent.SATURATION``).
The values are 3-tuples indexed by directions The values are 3-tuples indexed by directions
(``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``,
``ImageCms.DIRECTION_PROOF``). ``ImageCms.Direction.PROOF``).
The elements of the tuple are booleans. If the value is ``True``, The elements of the tuple are booleans. If the value is ``True``,
that intent is supported for that direction. that intent is supported for that direction.
@ -352,11 +352,11 @@ can be easily displayed in a chromaticity diagram, for example).
Note that you can also get this information for all intents and directions Note that you can also get this information for all intents and directions
with :py:attr:`.intent_supported`. with :py:attr:`.intent_supported`.
:param intent: One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, :param intent: One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``,
``ImageCms.INTENT_PERCEPTUAL``, ``ImageCms.Intent.PERCEPTUAL``,
``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` ``ImageCms.Intent.RELATIVE_COLORIMETRIC``
and ``ImageCms.INTENT_SATURATION``. and ``ImageCms.Intent.SATURATION``.
:param direction: One of ``ImageCms.DIRECTION_INPUT``, :param direction: One of ``ImageCms.Direction.INPUT``,
``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT``
and ``ImageCms.DIRECTION_PROOF`` and ``ImageCms.Direction.PROOF``
:return: Boolean if the intent and direction is supported. :return: Boolean if the intent and direction is supported.

View File

@ -60,12 +60,12 @@ Methods
Constants Constants
--------- ---------
.. data:: PIL.ImageFont.LAYOUT_BASIC .. data:: PIL.ImageFont.Layout.BASIC
Use basic text layout for TrueType font. Use basic text layout for TrueType font.
Advanced features such as text direction are not supported. Advanced features such as text direction are not supported.
.. data:: PIL.ImageFont.LAYOUT_RAQM .. data:: PIL.ImageFont.Layout.RAQM
Use Raqm text layout for TrueType font. Use Raqm text layout for TrueType font.
Advanced features are supported. Advanced features are supported.

View File

@ -57,7 +57,7 @@ Support for the following features can be checked:
* ``transp_webp``: Support for transparency in WebP images. * ``transp_webp``: Support for transparency in WebP images.
* ``webp_mux``: (compile time) Support for EXIF data in WebP images. * ``webp_mux``: (compile time) Support for EXIF data in WebP images.
* ``webp_anim``: (compile time) Support for animated WebP images. * ``webp_anim``: (compile time) Support for animated WebP images.
* ``raqm``: Raqm library, required for ``ImageFont.LAYOUT_RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library.

View File

@ -230,8 +230,7 @@ Plugin reference
.. automodule:: PIL.PngImagePlugin .. automodule:: PIL.PngImagePlugin
:members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk, :members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk,
MAX_TEXT_CHUNK, MAX_TEXT_MEMORY, APNG_BLEND_OP_SOURCE, APNG_BLEND_OP_OVER, Blend, Disposal, MAX_TEXT_CHUNK, MAX_TEXT_MEMORY
APNG_DISPOSE_OP_NONE, APNG_DISPOSE_OP_BACKGROUND, APNG_DISPOSE_OP_PREVIOUS
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:member-order: groupwise :member-order: groupwise

View File

@ -111,16 +111,14 @@ downscaling with libjpeg, which uses supersampling internally, not convolutions.
Image transposition Image transposition
------------------- -------------------
A new method :py:data:`PIL.Image.TRANSPOSE` has been added for the A new method ``TRANSPOSE`` has been added for the
:py:meth:`~PIL.Image.Image.transpose` operation in addition to :py:meth:`~PIL.Image.Image.transpose` operation in addition to
:py:data:`~PIL.Image.FLIP_LEFT_RIGHT`, :py:data:`~PIL.Image.FLIP_TOP_BOTTOM`, ``FLIP_LEFT_RIGHT``, ``FLIP_TOP_BOTTOM``, ``ROTATE_90``, ``ROTATE_180``,
:py:data:`~PIL.Image.ROTATE_90`, :py:data:`~PIL.Image.ROTATE_180`, ``ROTATE_270``. ``TRANSPOSE`` is an algebra transpose, with an image reflected
:py:data:`~PIL.Image.ROTATE_270`. :py:data:`~PIL.Image.TRANSPOSE` is an algebra across its main diagonal.
transpose, with an image reflected across its main diagonal.
The speed of :py:data:`~PIL.Image.ROTATE_90`, :py:data:`~PIL.Image.ROTATE_270` The speed of ``ROTATE_90``, ``ROTATE_270`` and ``TRANSPOSE`` has been significantly
and :py:data:`~PIL.Image.TRANSPOSE` has been significantly improved for large improved for large images which don't fit in the processor cache.
images which don't fit in the processor cache.
Gaussian blur and unsharp mask Gaussian blur and unsharp mask
------------------------------ ------------------------------

View File

@ -21,11 +21,77 @@ coordinate type".
Deprecations Deprecations
^^^^^^^^^^^^ ^^^^^^^^^^^^
Constants
~~~~~~~~~
A number of constants have been deprecated and will be removed in Pillow 10.0.0
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
===================================================== ============================================================
Deprecated Use instead
===================================================== ============================================================
``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
``Image.ORDERED`` ``Image.Dither.ORDERED``
``Image.RASTERIZE`` ``Image.Dither.RASTERIZE``
``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG``
``Image.WEB`` ``Image.Palette.WEB``
``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE``
``Image.AFFINE`` ``Image.Transform.AFFINE``
``Image.EXTENT`` ``Image.Transform.EXTENT``
``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE``
``Image.QUAD`` ``Image.Transform.QUAD``
``Image.MESH`` ``Image.Transform.MESH``
``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT``
``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM``
``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90``
``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180``
``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270``
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
``Image.BOX`` ``Image.Resampling.BOX``
``Image.BILINEAR`` ``Image.Resampling.BILNEAR``
``Image.LINEAR`` ``Image.Resampling.BILNEAR``
``Image.HAMMING`` ``Image.Resampling.HAMMING``
``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
``Image.CUBIC`` ``Image.Resampling.BICUBIC``
``Image.LANCZOS`` ``Image.Resampling.LANCZOS``
``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS``
``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT``
``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE``
``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE``
``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT``
``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL``
``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC``
``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION``
``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``
``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT``
``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT``
``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF``
``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC``
``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM``
``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG``
``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED``
``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT``
``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5``
``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1``
``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED``
``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE``
``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND``
``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS``
``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE``
``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER``
===================================================== ============================================================
ImageShow.Viewer.show_file file argument ImageShow.Viewer.show_file file argument
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been
deprecated, replaced by ``path``. deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by
``path``.
In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest ``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest

View File

@ -97,9 +97,9 @@ def testimage():
10456 10456
>>> len(im.tobytes()) >>> len(im.tobytes())
49152 49152
>>> _info(im.transform((512, 512), Image.AFFINE, (1,0,0,0,1,0))) >>> _info(im.transform((512, 512), Image.Transform.AFFINE, (1,0,0,0,1,0)))
(None, 'RGB', (512, 512)) (None, 'RGB', (512, 512))
>>> _info(im.transform((512, 512), Image.EXTENT, (32,32,96,96))) >>> _info(im.transform((512, 512), Image.Transform.EXTENT, (32,32,96,96)))
(None, 'RGB', (512, 512)) (None, 'RGB', (512, 512))
The ImageDraw module lets you draw stuff in raster images: The ImageDraw module lets you draw stuff in raster images:

View File

@ -30,19 +30,54 @@ BLP files come in many different flavours:
""" """
import struct import struct
import warnings
from enum import IntEnum
from io import BytesIO from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
BLP_FORMAT_JPEG = 0
BLP_ENCODING_UNCOMPRESSED = 1 class Format(IntEnum):
BLP_ENCODING_DXT = 2 JPEG = 0
BLP_ENCODING_UNCOMPRESSED_RAW_BGRA = 3
BLP_ALPHA_ENCODING_DXT1 = 0
BLP_ALPHA_ENCODING_DXT3 = 1 class Encoding(IntEnum):
BLP_ALPHA_ENCODING_DXT5 = 7 UNCOMPRESSED = 1
DXT = 2
UNCOMPRESSED_RAW_BGRA = 3
class AlphaEncoding(IntEnum):
DXT1 = 0
DXT3 = 1
DXT5 = 7
def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {
Format: "BLP_FORMAT_",
Encoding: "BLP_ENCODING_",
AlphaEncoding: "BLP_ALPHA_ENCODING_",
}.items():
if name.startswith(prefix):
name = name[len(prefix) :]
if name in enum.__members__:
warnings.warn(
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
def unpack_565(i): def unpack_565(i):
@ -320,7 +355,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
class BLP1Decoder(_BLPBaseDecoder): class BLP1Decoder(_BLPBaseDecoder):
def _load(self): def _load(self):
if self._blp_compression == BLP_FORMAT_JPEG: if self._blp_compression == Format.JPEG:
self._decode_jpeg_stream() self._decode_jpeg_stream()
elif self._blp_compression == 1: elif self._blp_compression == 1:
@ -347,7 +382,7 @@ class BLP1Decoder(_BLPBaseDecoder):
) )
def _decode_jpeg_stream(self): def _decode_jpeg_stream(self):
from PIL.JpegImagePlugin import JpegImageFile from .JpegImagePlugin import JpegImageFile
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4)) (jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size) jpeg_header = self._safe_read(jpeg_header_size)
@ -372,7 +407,7 @@ class BLP2Decoder(_BLPBaseDecoder):
if self._blp_compression == 1: if self._blp_compression == 1:
# Uncompressed or DirectX compression # Uncompressed or DirectX compression
if self._blp_encoding == BLP_ENCODING_UNCOMPRESSED: if self._blp_encoding == Encoding.UNCOMPRESSED:
_data = BytesIO(self._safe_read(self._blp_lengths[0])) _data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True: while True:
try: try:
@ -382,8 +417,8 @@ class BLP2Decoder(_BLPBaseDecoder):
b, g, r, a = palette[offset] b, g, r, a = palette[offset]
data.extend((r, g, b)) data.extend((r, g, b))
elif self._blp_encoding == BLP_ENCODING_DXT: elif self._blp_encoding == Encoding.DXT:
if self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT1: if self._blp_alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.size[0] + 3) // 4 * 8 linesize = (self.size[0] + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4): for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt1( for d in decode_dxt1(
@ -391,13 +426,13 @@ class BLP2Decoder(_BLPBaseDecoder):
): ):
data += d data += d
elif self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT3: elif self._blp_alpha_encoding == AlphaEncoding.DXT3:
linesize = (self.size[0] + 3) // 4 * 16 linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4): for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt3(self._safe_read(linesize)): for d in decode_dxt3(self._safe_read(linesize)):
data += d data += d
elif self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT5: elif self._blp_alpha_encoding == AlphaEncoding.DXT5:
linesize = (self.size[0] + 3) // 4 * 16 linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4): for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt5(self._safe_read(linesize)): for d in decode_dxt5(self._safe_read(linesize)):

View File

@ -52,13 +52,41 @@ Note: All data is stored in little-Endian (Intel) byte order.
""" """
import struct import struct
import warnings
from enum import IntEnum
from io import BytesIO from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
MAGIC = b"FTEX" MAGIC = b"FTEX"
FORMAT_DXT1 = 0
FORMAT_UNCOMPRESSED = 1
class Format(IntEnum):
DXT1 = 0
UNCOMPRESSED = 1
def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Format: "FORMAT_"}.items():
if name.startswith(prefix):
name = name[len(prefix) :]
if name in enum.__members__:
warnings.warn(
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
class FtexImageFile(ImageFile.ImageFile): class FtexImageFile(ImageFile.ImageFile):
@ -83,10 +111,10 @@ class FtexImageFile(ImageFile.ImageFile):
data = self.fp.read(mipmap_size) data = self.fp.read(mipmap_size)
if format == FORMAT_DXT1: if format == Format.DXT1:
self.mode = "RGBA" self.mode = "RGBA"
self.tile = [("bcn", (0, 0) + self.size, 0, (1))] self.tile = [("bcn", (0, 0) + self.size, 0, (1))]
elif format == FORMAT_UNCOMPRESSED: elif format == Format.UNCOMPRESSED:
self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
else: else:
raise ValueError(f"Invalid texture compression format: {repr(format)}") raise ValueError(f"Invalid texture compression format: {repr(format)}")

View File

@ -169,12 +169,12 @@ class GifImageFile(ImageFile.ImageFile):
if "transparency" in self.info: if "transparency" in self.info:
self.mode = "RGBA" self.mode = "RGBA"
self.im.putpalettealpha(self.info["transparency"], 0) self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.FLOYDSTEINBERG) self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
del self.info["transparency"] del self.info["transparency"]
else: else:
self.mode = "RGB" self.mode = "RGB"
self.im = self.im.convert("RGB", Image.FLOYDSTEINBERG) self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
if self.dispose: if self.dispose:
self.im.paste(self.dispose, self.dispose_extent) self.im.paste(self.dispose, self.dispose_extent)
@ -425,7 +425,7 @@ def _normalize_mode(im, initial_call=False):
palette_size = 256 palette_size = 256
if im.palette: if im.palette:
palette_size = len(im.palette.getdata()[1]) // 3 palette_size = len(im.palette.getdata()[1]) // 3
im = im.convert("P", palette=Image.ADAPTIVE, colors=palette_size) im = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=palette_size)
if im.palette.mode == "RGBA": if im.palette.mode == "RGBA":
for rgba in im.palette.colors.keys(): for rgba in im.palette.colors.keys():
if rgba[3] == 0: if rgba[3] == 0:

View File

@ -69,7 +69,7 @@ def _save(im, fp, filename):
if not tmp: if not tmp:
# TODO: invent a more convenient method for proportional scalings # TODO: invent a more convenient method for proportional scalings
tmp = im.copy() tmp = im.copy()
tmp.thumbnail(size, Image.LANCZOS, reducing_gap=None) tmp.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32 bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32
fp.write(struct.pack("<H", bits)) # wBitCount(2) fp.write(struct.pack("<H", bits)) # wBitCount(2)

View File

@ -37,6 +37,7 @@ import sys
import tempfile import tempfile
import warnings import warnings
from collections.abc import Callable, MutableMapping from collections.abc import Callable, MutableMapping
from enum import IntEnum
from pathlib import Path from pathlib import Path
try: try:
@ -53,15 +54,57 @@ from ._util import deferred_error, isPath
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2}
if name in categories: if name in categories:
warnings.warn( warnings.warn(
"Image categories are deprecated and will be removed in Pillow 10 " "Image categories are " + deprecated + "Use is_animated instead.",
"(2023-07-01). Use is_animated instead.",
DeprecationWarning, DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
return categories[name] return categories[name]
elif name in ("NEAREST", "NONE"):
warnings.warn(
name
+ " is "
+ deprecated
+ "Use Resampling.NEAREST or Dither.NONE instead.",
DeprecationWarning,
stacklevel=2,
)
return 0
old_resampling = {
"LINEAR": "BILINEAR",
"CUBIC": "BICUBIC",
"ANTIALIAS": "LANCZOS",
}
if name in old_resampling:
warnings.warn(
name
+ " is "
+ deprecated
+ "Use Resampling."
+ old_resampling[name]
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return Resampling[old_resampling[name]]
for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize):
if name in enum.__members__:
warnings.warn(
name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@ -139,46 +182,64 @@ def isImageType(t):
# Constants # Constants
# transpose # transpose
FLIP_LEFT_RIGHT = 0 class Transpose(IntEnum):
FLIP_TOP_BOTTOM = 1 FLIP_LEFT_RIGHT = 0
ROTATE_90 = 2 FLIP_TOP_BOTTOM = 1
ROTATE_180 = 3 ROTATE_90 = 2
ROTATE_270 = 4 ROTATE_180 = 3
TRANSPOSE = 5 ROTATE_270 = 4
TRANSVERSE = 6 TRANSPOSE = 5
TRANSVERSE = 6
# transforms (also defined in Imaging.h) # transforms (also defined in Imaging.h)
AFFINE = 0 class Transform(IntEnum):
EXTENT = 1 AFFINE = 0
PERSPECTIVE = 2 EXTENT = 1
QUAD = 3 PERSPECTIVE = 2
MESH = 4 QUAD = 3
MESH = 4
# resampling filters (also defined in Imaging.h) # resampling filters (also defined in Imaging.h)
NEAREST = NONE = 0 class Resampling(IntEnum):
BOX = 4 NEAREST = 0
BILINEAR = LINEAR = 2 BOX = 4
HAMMING = 5 BILINEAR = 2
BICUBIC = CUBIC = 3 HAMMING = 5
LANCZOS = ANTIALIAS = 1 BICUBIC = 3
LANCZOS = 1
_filters_support = {BOX: 0.5, BILINEAR: 1.0, HAMMING: 1.0, BICUBIC: 2.0, LANCZOS: 3.0}
_filters_support = {
Resampling.BOX: 0.5,
Resampling.BILINEAR: 1.0,
Resampling.HAMMING: 1.0,
Resampling.BICUBIC: 2.0,
Resampling.LANCZOS: 3.0,
}
# dithers # dithers
NEAREST = NONE = 0 class Dither(IntEnum):
ORDERED = 1 # Not yet implemented NONE = 0
RASTERIZE = 2 # Not yet implemented ORDERED = 1 # Not yet implemented
FLOYDSTEINBERG = 3 # default RASTERIZE = 2 # Not yet implemented
FLOYDSTEINBERG = 3 # default
# palettes/quantizers # palettes/quantizers
WEB = 0 class Palette(IntEnum):
ADAPTIVE = 1 WEB = 0
ADAPTIVE = 1
class Quantize(IntEnum):
MEDIANCUT = 0
MAXCOVERAGE = 1
FASTOCTREE = 2
LIBIMAGEQUANT = 3
MEDIANCUT = 0
MAXCOVERAGE = 1
FASTOCTREE = 2
LIBIMAGEQUANT = 3
if hasattr(core, "DEFAULT_STRATEGY"): if hasattr(core, "DEFAULT_STRATEGY"):
DEFAULT_STRATEGY = core.DEFAULT_STRATEGY DEFAULT_STRATEGY = core.DEFAULT_STRATEGY
@ -821,13 +882,7 @@ class Image:
if self.im and self.palette and self.palette.dirty: if self.im and self.palette and self.palette.dirty:
# realize palette # realize palette
mode, arr = self.palette.getdata() mode, arr = self.palette.getdata()
if mode == "RGBA": self.im.putpalette(mode, arr)
mode = "RGB"
self.info["transparency"] = arr[3::4]
arr = bytes(
value for (index, value) in enumerate(arr) if index % 4 != 3
)
palette_length = self.im.putpalette(mode, arr)
self.palette.dirty = 0 self.palette.dirty = 0
self.palette.rawmode = None self.palette.rawmode = None
if "transparency" in self.info and mode in ("LA", "PA"): if "transparency" in self.info and mode in ("LA", "PA"):
@ -837,8 +892,9 @@ class Image:
self.im.putpalettealphas(self.info["transparency"]) self.im.putpalettealphas(self.info["transparency"])
self.palette.mode = "RGBA" self.palette.mode = "RGBA"
else: else:
self.palette.mode = "RGB" palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB"
self.palette.palette = self.im.getpalette()[: palette_length * 3] self.palette.mode = palette_mode
self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
if self.im: if self.im:
if cffi and USE_CFFI_ACCESS: if cffi and USE_CFFI_ACCESS:
@ -862,7 +918,9 @@ class Image:
""" """
pass pass
def convert(self, mode=None, matrix=None, dither=None, palette=WEB, colors=256): def convert(
self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256
):
""" """
Returns a converted copy of this image. For the "P" mode, this Returns a converted copy of this image. For the "P" mode, this
method translates pixels through the palette. If mode is method translates pixels through the palette. If mode is
@ -881,7 +939,7 @@ class Image:
The default method of converting a greyscale ("L") or "RGB" The default method of converting a greyscale ("L") or "RGB"
image into a bilevel (mode "1") image uses Floyd-Steinberg image into a bilevel (mode "1") image uses Floyd-Steinberg
dither to approximate the original image luminosity levels. If dither to approximate the original image luminosity levels. If
dither is :data:`NONE`, all values larger than 127 are set to 255 (white), dither is ``None``, all values larger than 127 are set to 255 (white),
all other values to 0 (black). To use other thresholds, use the all other values to 0 (black). To use other thresholds, use the
:py:meth:`~PIL.Image.Image.point` method. :py:meth:`~PIL.Image.Image.point` method.
@ -894,12 +952,13 @@ class Image:
should be 4- or 12-tuple containing floating point values. should be 4- or 12-tuple containing floating point values.
:param dither: Dithering method, used when converting from :param dither: Dithering method, used when converting from
mode "RGB" to "P" or from "RGB" or "L" to "1". mode "RGB" to "P" or from "RGB" or "L" to "1".
Available methods are :data:`NONE` or :data:`FLOYDSTEINBERG` (default). Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG`
Note that this is not used when ``matrix`` is supplied. (default). Note that this is not used when ``matrix`` is supplied.
:param palette: Palette to use when converting from mode "RGB" :param palette: Palette to use when converting from mode "RGB"
to "P". Available palettes are :data:`WEB` or :data:`ADAPTIVE`. to "P". Available palettes are :data:`Palette.WEB` or
:param colors: Number of colors to use for the :data:`ADAPTIVE` palette. :data:`Palette.ADAPTIVE`.
Defaults to 256. :param colors: Number of colors to use for the :data:`Palette.ADAPTIVE`
palette. Defaults to 256.
:rtype: :py:class:`~PIL.Image.Image` :rtype: :py:class:`~PIL.Image.Image`
:returns: An :py:class:`~PIL.Image.Image` object. :returns: An :py:class:`~PIL.Image.Image` object.
""" """
@ -1006,7 +1065,7 @@ class Image:
else: else:
raise ValueError("Transparency for P mode should be bytes or int") raise ValueError("Transparency for P mode should be bytes or int")
if mode == "P" and palette == ADAPTIVE: if mode == "P" and palette == Palette.ADAPTIVE:
im = self.im.quantize(colors) im = self.im.quantize(colors)
new = self._new(im) new = self._new(im)
from . import ImagePalette from . import ImagePalette
@ -1028,7 +1087,7 @@ class Image:
# colorspace conversion # colorspace conversion
if dither is None: if dither is None:
dither = FLOYDSTEINBERG dither = Dither.FLOYDSTEINBERG
try: try:
im = self.im.convert(mode, dither) im = self.im.convert(mode, dither)
@ -1041,7 +1100,7 @@ class Image:
raise ValueError("illegal conversion") from e raise ValueError("illegal conversion") from e
new_im = self._new(im) new_im = self._new(im)
if mode == "P" and palette != ADAPTIVE: if mode == "P" and palette != Palette.ADAPTIVE:
from . import ImagePalette from . import ImagePalette
new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
@ -1070,24 +1129,25 @@ class Image:
of colors. of colors.
:param colors: The desired number of colors, <= 256 :param colors: The desired number of colors, <= 256
:param method: :data:`MEDIANCUT` (median cut), :param method: :data:`Quantize.MEDIANCUT` (median cut),
:data:`MAXCOVERAGE` (maximum coverage), :data:`Quantize.MAXCOVERAGE` (maximum coverage),
:data:`FASTOCTREE` (fast octree), :data:`Quantize.FASTOCTREE` (fast octree),
:data:`LIBIMAGEQUANT` (libimagequant; check support using :data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support
:py:func:`PIL.features.check_feature` using :py:func:`PIL.features.check_feature` with
with ``feature="libimagequant"``). ``feature="libimagequant"``).
By default, :data:`MEDIANCUT` will be used. By default, :data:`Quantize.MEDIANCUT` will be used.
The exception to this is RGBA images. :data:`MEDIANCUT` and The exception to this is RGBA images. :data:`Quantize.MEDIANCUT`
:data:`MAXCOVERAGE` do not support RGBA images, so and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so
:data:`FASTOCTREE` is used by default instead. :data:`Quantize.FASTOCTREE` is used by default instead.
:param kmeans: Integer :param kmeans: Integer
:param palette: Quantize to the palette of given :param palette: Quantize to the palette of given
:py:class:`PIL.Image.Image`. :py:class:`PIL.Image.Image`.
:param dither: Dithering method, used when converting from :param dither: Dithering method, used when converting from
mode "RGB" to "P" or from "RGB" or "L" to "1". mode "RGB" to "P" or from "RGB" or "L" to "1".
Available methods are :data:`NONE` or :data:`FLOYDSTEINBERG` (default). Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG`
(default).
Default: 1 (legacy setting) Default: 1 (legacy setting)
:returns: A new image :returns: A new image
@ -1097,11 +1157,14 @@ class Image:
if method is None: if method is None:
# defaults: # defaults:
method = MEDIANCUT method = Quantize.MEDIANCUT
if self.mode == "RGBA": if self.mode == "RGBA":
method = FASTOCTREE method = Quantize.FASTOCTREE
if self.mode == "RGBA" and method not in (FASTOCTREE, LIBIMAGEQUANT): if self.mode == "RGBA" and method not in (
Quantize.FASTOCTREE,
Quantize.LIBIMAGEQUANT,
):
# Caller specified an invalid mode. # Caller specified an invalid mode.
raise ValueError( raise ValueError(
"Fast Octree (method == 2) and libimagequant (method == 3) " "Fast Octree (method == 2) and libimagequant (method == 3) "
@ -1761,8 +1824,8 @@ class Image:
Alternatively, an 8-bit string may be used instead of an integer sequence. Alternatively, an 8-bit string may be used instead of an integer sequence.
:param data: A palette sequence (either a list or a string). :param data: A palette sequence (either a list or a string).
:param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode
mode that can be transformed to "RGB" (e.g. "R", "BGR;15", "RGBA;L"). that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L").
""" """
from . import ImagePalette from . import ImagePalette
@ -1911,15 +1974,18 @@ class Image:
:param size: The requested size in pixels, as a 2-tuple: :param size: The requested size in pixels, as a 2-tuple:
(width, height). (width, height).
:param resample: An optional resampling filter. This can be :param resample: An optional resampling filter. This can be
one of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`, one of :py:data:`PIL.Image.Resampling.NEAREST`,
:py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`, :py:data:`PIL.Image.Resampling.BOX`,
:py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`. :py:data:`PIL.Image.Resampling.BILINEAR`,
:py:data:`PIL.Image.Resampling.HAMMING`,
:py:data:`PIL.Image.Resampling.BICUBIC` or
:py:data:`PIL.Image.Resampling.LANCZOS`.
If the image has mode "1" or "P", it is always set to If the image has mode "1" or "P", it is always set to
:py:data:`PIL.Image.NEAREST`. :py:data:`PIL.Image.Resampling.NEAREST`.
If the image mode specifies a number of bits, such as "I;16", then the If the image mode specifies a number of bits, such as "I;16", then the
default filter is :py:data:`PIL.Image.NEAREST`. default filter is :py:data:`PIL.Image.Resampling.NEAREST`.
Otherwise, the default filter is :py:data:`PIL.Image.BICUBIC`. Otherwise, the default filter is
See: :ref:`concept-filters`. :py:data:`PIL.Image.Resampling.BICUBIC`. See: :ref:`concept-filters`.
:param box: An optional 4-tuple of floats providing :param box: An optional 4-tuple of floats providing
the source image region to be scaled. the source image region to be scaled.
The values must be within (0, 0, width, height) rectangle. The values must be within (0, 0, width, height) rectangle.
@ -1941,19 +2007,26 @@ class Image:
if resample is None: if resample is None:
type_special = ";" in self.mode type_special = ";" in self.mode
resample = NEAREST if type_special else BICUBIC resample = Resampling.NEAREST if type_special else Resampling.BICUBIC
elif resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING): elif resample not in (
Resampling.NEAREST,
Resampling.BILINEAR,
Resampling.BICUBIC,
Resampling.LANCZOS,
Resampling.BOX,
Resampling.HAMMING,
):
message = f"Unknown resampling filter ({resample})." message = f"Unknown resampling filter ({resample})."
filters = [ filters = [
f"{filter[1]} ({filter[0]})" f"{filter[1]} ({filter[0]})"
for filter in ( for filter in (
(NEAREST, "Image.NEAREST"), (Resampling.NEAREST, "Image.Resampling.NEAREST"),
(LANCZOS, "Image.LANCZOS"), (Resampling.LANCZOS, "Image.Resampling.LANCZOS"),
(BILINEAR, "Image.BILINEAR"), (Resampling.BILINEAR, "Image.Resampling.BILINEAR"),
(BICUBIC, "Image.BICUBIC"), (Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
(BOX, "Image.BOX"), (Resampling.BOX, "Image.Resampling.BOX"),
(HAMMING, "Image.HAMMING"), (Resampling.HAMMING, "Image.Resampling.HAMMING"),
) )
] ]
raise ValueError( raise ValueError(
@ -1974,16 +2047,16 @@ class Image:
return self.copy() return self.copy()
if self.mode in ("1", "P"): if self.mode in ("1", "P"):
resample = NEAREST resample = Resampling.NEAREST
if self.mode in ["LA", "RGBA"] and resample != NEAREST: if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST:
im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode])
im = im.resize(size, resample, box) im = im.resize(size, resample, box)
return im.convert(self.mode) return im.convert(self.mode)
self.load() self.load()
if reducing_gap is not None and resample != NEAREST: if reducing_gap is not None and resample != Resampling.NEAREST:
factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1
factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1
if factor_x > 1 or factor_y > 1: if factor_x > 1 or factor_y > 1:
@ -2038,7 +2111,7 @@ class Image:
def rotate( def rotate(
self, self,
angle, angle,
resample=NEAREST, resample=Resampling.NEAREST,
expand=0, expand=0,
center=None, center=None,
translate=None, translate=None,
@ -2051,12 +2124,12 @@ class Image:
:param angle: In degrees counter clockwise. :param angle: In degrees counter clockwise.
:param resample: An optional resampling filter. This can be :param resample: An optional resampling filter. This can be
one of :py:data:`PIL.Image.NEAREST` (use nearest neighbour), one of :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour),
:py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 :py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2
environment), or :py:data:`PIL.Image.BICUBIC` environment), or :py:data:`PIL.Image.Resampling.BICUBIC`
(cubic spline interpolation in a 4x4 environment). (cubic spline interpolation in a 4x4 environment).
If omitted, or if the image has mode "1" or "P", it is If omitted, or if the image has mode "1" or "P", it is
set to :py:data:`PIL.Image.NEAREST`. See :ref:`concept-filters`. set to :py:data:`PIL.Image.Resampling.NEAREST`. See :ref:`concept-filters`.
:param expand: Optional expansion flag. If true, expands the output :param expand: Optional expansion flag. If true, expands the output
image to make it large enough to hold the entire rotated image. image to make it large enough to hold the entire rotated image.
If false or omitted, make the output image the same size as the If false or omitted, make the output image the same size as the
@ -2077,9 +2150,11 @@ class Image:
if angle == 0: if angle == 0:
return self.copy() return self.copy()
if angle == 180: if angle == 180:
return self.transpose(ROTATE_180) return self.transpose(Transpose.ROTATE_180)
if angle in (90, 270) and (expand or self.width == self.height): if angle in (90, 270) and (expand or self.width == self.height):
return self.transpose(ROTATE_90 if angle == 90 else ROTATE_270) return self.transpose(
Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270
)
# Calculate the affine matrix. Note that this is the reverse # Calculate the affine matrix. Note that this is the reverse
# transformation (from destination image to source) because we # transformation (from destination image to source) because we
@ -2148,7 +2223,9 @@ class Image:
matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix) matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix)
w, h = nw, nh w, h = nw, nh
return self.transform((w, h), AFFINE, matrix, resample, fillcolor=fillcolor) return self.transform(
(w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor
)
def save(self, fp, format=None, **params): def save(self, fp, format=None, **params):
""" """
@ -2334,7 +2411,7 @@ class Image:
""" """
return 0 return 0
def thumbnail(self, size, resample=BICUBIC, reducing_gap=2.0): def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0):
""" """
Make this image into a thumbnail. This method modifies the Make this image into a thumbnail. This method modifies the
image to contain a thumbnail version of itself, no larger than image to contain a thumbnail version of itself, no larger than
@ -2350,11 +2427,14 @@ class Image:
:param size: Requested size. :param size: Requested size.
:param resample: Optional resampling filter. This can be one :param resample: Optional resampling filter. This can be one
of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`, of :py:data:`PIL.Image.Resampling.NEAREST`,
:py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`, :py:data:`PIL.Image.Resampling.BOX`,
:py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`. :py:data:`PIL.Image.Resampling.BILINEAR`,
If omitted, it defaults to :py:data:`PIL.Image.BICUBIC`. :py:data:`PIL.Image.Resampling.HAMMING`,
(was :py:data:`PIL.Image.NEAREST` prior to version 2.5.0). :py:data:`PIL.Image.Resampling.BICUBIC` or
:py:data:`PIL.Image.Resampling.LANCZOS`.
If omitted, it defaults to :py:data:`PIL.Image.Resampling.BICUBIC`.
(was :py:data:`PIL.Image.Resampling.NEAREST` prior to version 2.5.0).
See: :ref:`concept-filters`. See: :ref:`concept-filters`.
:param reducing_gap: Apply optimization by resizing the image :param reducing_gap: Apply optimization by resizing the image
in two steps. First, reducing the image by integer times in two steps. First, reducing the image by integer times
@ -2409,7 +2489,13 @@ class Image:
# FIXME: the different transform methods need further explanation # FIXME: the different transform methods need further explanation
# instead of bloating the method docs, add a separate chapter. # instead of bloating the method docs, add a separate chapter.
def transform( def transform(
self, size, method, data=None, resample=NEAREST, fill=1, fillcolor=None self,
size,
method,
data=None,
resample=Resampling.NEAREST,
fill=1,
fillcolor=None,
): ):
""" """
Transforms this image. This method creates a new image with the Transforms this image. This method creates a new image with the
@ -2418,11 +2504,11 @@ class Image:
:param size: The output size. :param size: The output size.
:param method: The transformation method. This is one of :param method: The transformation method. This is one of
:py:data:`PIL.Image.EXTENT` (cut out a rectangular subregion), :py:data:`PIL.Image.Transform.EXTENT` (cut out a rectangular subregion),
:py:data:`PIL.Image.AFFINE` (affine transform), :py:data:`PIL.Image.Transform.AFFINE` (affine transform),
:py:data:`PIL.Image.PERSPECTIVE` (perspective transform), :py:data:`PIL.Image.Transform.PERSPECTIVE` (perspective transform),
:py:data:`PIL.Image.QUAD` (map a quadrilateral to a rectangle), or :py:data:`PIL.Image.Transform.QUAD` (map a quadrilateral to a rectangle), or
:py:data:`PIL.Image.MESH` (map a number of source quadrilaterals :py:data:`PIL.Image.Transform.MESH` (map a number of source quadrilaterals
in one operation). in one operation).
It may also be an :py:class:`~PIL.Image.ImageTransformHandler` It may also be an :py:class:`~PIL.Image.ImageTransformHandler`
@ -2437,16 +2523,16 @@ class Image:
class Example: class Example:
def getdata(self): def getdata(self):
method = Image.EXTENT method = Image.Transform.EXTENT
data = (0, 0, 100, 100) data = (0, 0, 100, 100)
return method, data return method, data
:param data: Extra data to the transformation method. :param data: Extra data to the transformation method.
:param resample: Optional resampling filter. It can be one of :param resample: Optional resampling filter. It can be one of
:py:data:`PIL.Image.NEAREST` (use nearest neighbour), :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour),
:py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 :py:data:`PIL.Image.Resampling.BILINEAR` (linear interpolation in a 2x2
environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline
interpolation in a 4x4 environment). If omitted, or if the image interpolation in a 4x4 environment). If omitted, or if the image
has mode "1" or "P", it is set to :py:data:`PIL.Image.NEAREST`. has mode "1" or "P", it is set to :py:data:`PIL.Image.Resampling.NEAREST`.
See: :ref:`concept-filters`. See: :ref:`concept-filters`.
:param fill: If ``method`` is an :param fill: If ``method`` is an
:py:class:`~PIL.Image.ImageTransformHandler` object, this is one of :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of
@ -2456,7 +2542,7 @@ class Image:
:returns: An :py:class:`~PIL.Image.Image` object. :returns: An :py:class:`~PIL.Image.Image` object.
""" """
if self.mode in ("LA", "RGBA") and resample != NEAREST: if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST:
return ( return (
self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode])
.transform(size, method, data, resample, fill, fillcolor) .transform(size, method, data, resample, fill, fillcolor)
@ -2477,10 +2563,12 @@ class Image:
if self.mode == "P" and self.palette: if self.mode == "P" and self.palette:
im.palette = self.palette.copy() im.palette = self.palette.copy()
im.info = self.info.copy() im.info = self.info.copy()
if method == MESH: if method == Transform.MESH:
# list of quads # list of quads
for box, quad in data: for box, quad in data:
im.__transformer(box, self, QUAD, quad, resample, fillcolor is None) im.__transformer(
box, self, Transform.QUAD, quad, resample, fillcolor is None
)
else: else:
im.__transformer( im.__transformer(
(0, 0) + size, self, method, data, resample, fillcolor is None (0, 0) + size, self, method, data, resample, fillcolor is None
@ -2488,25 +2576,27 @@ class Image:
return im return im
def __transformer(self, box, image, method, data, resample=NEAREST, fill=1): def __transformer(
self, box, image, method, data, resample=Resampling.NEAREST, fill=1
):
w = box[2] - box[0] w = box[2] - box[0]
h = box[3] - box[1] h = box[3] - box[1]
if method == AFFINE: if method == Transform.AFFINE:
data = data[0:6] data = data[0:6]
elif method == EXTENT: elif method == Transform.EXTENT:
# convert extent to an affine transform # convert extent to an affine transform
x0, y0, x1, y1 = data x0, y0, x1, y1 = data
xs = (x1 - x0) / w xs = (x1 - x0) / w
ys = (y1 - y0) / h ys = (y1 - y0) / h
method = AFFINE method = Transform.AFFINE
data = (xs, 0, x0, 0, ys, y0) data = (xs, 0, x0, 0, ys, y0)
elif method == PERSPECTIVE: elif method == Transform.PERSPECTIVE:
data = data[0:8] data = data[0:8]
elif method == QUAD: elif method == Transform.QUAD:
# quadrilateral warp. data specifies the four corners # quadrilateral warp. data specifies the four corners
# given as NW, SW, SE, and NE. # given as NW, SW, SE, and NE.
nw = data[0:2] nw = data[0:2]
@ -2530,12 +2620,16 @@ class Image:
else: else:
raise ValueError("unknown transformation method") raise ValueError("unknown transformation method")
if resample not in (NEAREST, BILINEAR, BICUBIC): if resample not in (
if resample in (BOX, HAMMING, LANCZOS): Resampling.NEAREST,
Resampling.BILINEAR,
Resampling.BICUBIC,
):
if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS):
message = { message = {
BOX: "Image.BOX", Resampling.BOX: "Image.Resampling.BOX",
HAMMING: "Image.HAMMING", Resampling.HAMMING: "Image.Resampling.HAMMING",
LANCZOS: "Image.LANCZOS/Image.ANTIALIAS", Resampling.LANCZOS: "Image.Resampling.LANCZOS",
}[resample] + f" ({resample}) cannot be used." }[resample] + f" ({resample}) cannot be used."
else: else:
message = f"Unknown resampling filter ({resample})." message = f"Unknown resampling filter ({resample})."
@ -2543,9 +2637,9 @@ class Image:
filters = [ filters = [
f"{filter[1]} ({filter[0]})" f"{filter[1]} ({filter[0]})"
for filter in ( for filter in (
(NEAREST, "Image.NEAREST"), (Resampling.NEAREST, "Image.Resampling.NEAREST"),
(BILINEAR, "Image.BILINEAR"), (Resampling.BILINEAR, "Image.Resampling.BILINEAR"),
(BICUBIC, "Image.BICUBIC"), (Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
) )
] ]
raise ValueError( raise ValueError(
@ -2557,7 +2651,7 @@ class Image:
self.load() self.load()
if image.mode in ("1", "P"): if image.mode in ("1", "P"):
resample = NEAREST resample = Resampling.NEAREST
self.im.transform2(box, image.im, method, data, resample, fill) self.im.transform2(box, image.im, method, data, resample, fill)
@ -2565,10 +2659,13 @@ class Image:
""" """
Transpose image (flip or rotate in 90 degree steps) Transpose image (flip or rotate in 90 degree steps)
:param method: One of :py:data:`PIL.Image.FLIP_LEFT_RIGHT`, :param method: One of :py:data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`,
:py:data:`PIL.Image.FLIP_TOP_BOTTOM`, :py:data:`PIL.Image.ROTATE_90`, :py:data:`PIL.Image.Transpose.FLIP_TOP_BOTTOM`,
:py:data:`PIL.Image.ROTATE_180`, :py:data:`PIL.Image.ROTATE_270`, :py:data:`PIL.Image.Transpose.ROTATE_90`,
:py:data:`PIL.Image.TRANSPOSE` or :py:data:`PIL.Image.TRANSVERSE`. :py:data:`PIL.Image.Transpose.ROTATE_180`,
:py:data:`PIL.Image.Transpose.ROTATE_270`,
:py:data:`PIL.Image.Transpose.TRANSPOSE` or
:py:data:`PIL.Image.Transpose.TRANSVERSE`.
:returns: Returns a flipped or rotated copy of this image. :returns: Returns a flipped or rotated copy of this image.
""" """

View File

@ -16,6 +16,8 @@
# below for the original description. # below for the original description.
import sys import sys
import warnings
from enum import IntEnum
from PIL import Image from PIL import Image
@ -100,14 +102,42 @@ core = _imagingcms
# #
# intent/direction values # intent/direction values
INTENT_PERCEPTUAL = 0
INTENT_RELATIVE_COLORIMETRIC = 1
INTENT_SATURATION = 2
INTENT_ABSOLUTE_COLORIMETRIC = 3
DIRECTION_INPUT = 0 class Intent(IntEnum):
DIRECTION_OUTPUT = 1 PERCEPTUAL = 0
DIRECTION_PROOF = 2 RELATIVE_COLORIMETRIC = 1
SATURATION = 2
ABSOLUTE_COLORIMETRIC = 3
class Direction(IntEnum):
INPUT = 0
OUTPUT = 1
PROOF = 2
def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items():
if name.startswith(prefix):
name = name[len(prefix) :]
if name in enum.__members__:
warnings.warn(
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
# #
# flags # flags
@ -211,9 +241,9 @@ class ImageCmsTransform(Image.ImagePointHandler):
output, output,
input_mode, input_mode,
output_mode, output_mode,
intent=INTENT_PERCEPTUAL, intent=Intent.PERCEPTUAL,
proof=None, proof=None,
proof_intent=INTENT_ABSOLUTE_COLORIMETRIC, proof_intent=Intent.ABSOLUTE_COLORIMETRIC,
flags=0, flags=0,
): ):
if proof is None: if proof is None:
@ -295,7 +325,7 @@ def profileToProfile(
im, im,
inputProfile, inputProfile,
outputProfile, outputProfile,
renderingIntent=INTENT_PERCEPTUAL, renderingIntent=Intent.PERCEPTUAL,
outputMode=None, outputMode=None,
inPlace=False, inPlace=False,
flags=0, flags=0,
@ -331,10 +361,10 @@ def profileToProfile(
:param renderingIntent: Integer (0-3) specifying the rendering intent you :param renderingIntent: Integer (0-3) specifying the rendering intent you
wish to use for the transform wish to use for the transform
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2 ImageCms.Intent.SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what see the pyCMS documentation for details on rendering intents and what
they do. they do.
@ -412,7 +442,7 @@ def buildTransform(
outputProfile, outputProfile,
inMode, inMode,
outMode, outMode,
renderingIntent=INTENT_PERCEPTUAL, renderingIntent=Intent.PERCEPTUAL,
flags=0, flags=0,
): ):
""" """
@ -458,10 +488,10 @@ def buildTransform(
:param renderingIntent: Integer (0-3) specifying the rendering intent you :param renderingIntent: Integer (0-3) specifying the rendering intent you
wish to use for the transform wish to use for the transform
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2 ImageCms.Intent.SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what see the pyCMS documentation for details on rendering intents and what
they do. they do.
@ -494,8 +524,8 @@ def buildProofTransform(
proofProfile, proofProfile,
inMode, inMode,
outMode, outMode,
renderingIntent=INTENT_PERCEPTUAL, renderingIntent=Intent.PERCEPTUAL,
proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC, proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC,
flags=FLAGS["SOFTPROOFING"], flags=FLAGS["SOFTPROOFING"],
): ):
""" """
@ -550,20 +580,20 @@ def buildProofTransform(
:param renderingIntent: Integer (0-3) specifying the rendering intent you :param renderingIntent: Integer (0-3) specifying the rendering intent you
wish to use for the input->proof (simulated) transform wish to use for the input->proof (simulated) transform
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2 ImageCms.Intent.SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what see the pyCMS documentation for details on rendering intents and what
they do. they do.
:param proofRenderingIntent: Integer (0-3) specifying the rendering intent :param proofRenderingIntent: Integer (0-3) specifying the rendering intent
you wish to use for proof->output transform you wish to use for proof->output transform
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2 ImageCms.Intent.SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what see the pyCMS documentation for details on rendering intents and what
they do. they do.
@ -922,10 +952,10 @@ def getDefaultIntent(profile):
:returns: Integer 0-3 specifying the default rendering intent for this :returns: Integer 0-3 specifying the default rendering intent for this
profile. profile.
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2 ImageCms.Intent.SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what see the pyCMS documentation for details on rendering intents and what
they do. they do.
@ -960,19 +990,19 @@ def isIntentSupported(profile, intent, direction):
:param intent: Integer (0-3) specifying the rendering intent you wish to :param intent: Integer (0-3) specifying the rendering intent you wish to
use with this profile use with this profile
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2 ImageCms.Intent.SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what see the pyCMS documentation for details on rendering intents and what
they do. they do.
:param direction: Integer specifying if the profile is to be used for :param direction: Integer specifying if the profile is to be used for
input, output, or proof input, output, or proof
INPUT = 0 (or use ImageCms.DIRECTION_INPUT) INPUT = 0 (or use ImageCms.Direction.INPUT)
OUTPUT = 1 (or use ImageCms.DIRECTION_OUTPUT) OUTPUT = 1 (or use ImageCms.Direction.OUTPUT)
PROOF = 2 (or use ImageCms.DIRECTION_PROOF) PROOF = 2 (or use ImageCms.Direction.PROOF)
:returns: 1 if the intent/direction are supported, -1 if they are not. :returns: 1 if the intent/direction are supported, -1 if they are not.
:exception PyCMSError: :exception PyCMSError:

View File

@ -529,7 +529,7 @@ class Color3DLUT(MultibandFilter):
return image.color_lut_3d( return image.color_lut_3d(
self.mode or image.mode, self.mode or image.mode,
Image.LINEAR, Image.Resampling.BILINEAR,
self.channels, self.channels,
self.size[0], self.size[0],
self.size[1], self.size[1],

View File

@ -28,13 +28,40 @@
import base64 import base64
import os import os
import sys import sys
import warnings
from enum import IntEnum
from io import BytesIO from io import BytesIO
from . import Image from . import Image
from ._util import isDirectory, isPath from ._util import isDirectory, isPath
LAYOUT_BASIC = 0
LAYOUT_RAQM = 1 class Layout(IntEnum):
BASIC = 0
RAQM = 1
def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Layout: "LAYOUT_"}.items():
if name.startswith(prefix):
name = name[len(prefix) :]
if name in enum.__members__:
warnings.warn(
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
class _imagingft_not_installed: class _imagingft_not_installed:
@ -164,18 +191,18 @@ class FreeTypeFont:
self.index = index self.index = index
self.encoding = encoding self.encoding = encoding
if layout_engine not in (LAYOUT_BASIC, LAYOUT_RAQM): if layout_engine not in (Layout.BASIC, Layout.RAQM):
layout_engine = LAYOUT_BASIC layout_engine = Layout.BASIC
if core.HAVE_RAQM: if core.HAVE_RAQM:
layout_engine = LAYOUT_RAQM layout_engine = Layout.RAQM
elif layout_engine == LAYOUT_RAQM and not core.HAVE_RAQM: elif layout_engine == Layout.RAQM and not core.HAVE_RAQM:
import warnings import warnings
warnings.warn( warnings.warn(
"Raqm layout was requested, but Raqm is not available. " "Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout." "Falling back to basic layout."
) )
layout_engine = LAYOUT_BASIC layout_engine = Layout.BASIC
self.layout_engine = layout_engine self.layout_engine = layout_engine
@ -757,15 +784,16 @@ class TransposedFont:
:param font: A font object. :param font: A font object.
:param orientation: An optional orientation. If given, this should :param orientation: An optional orientation. If given, this should
be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM, be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM,
Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270. Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or
Image.Transpose.ROTATE_270.
""" """
self.font = font self.font = font
self.orientation = orientation # any 'transpose' argument, or None self.orientation = orientation # any 'transpose' argument, or None
def getsize(self, text, *args, **kwargs): def getsize(self, text, *args, **kwargs):
w, h = self.font.getsize(text) w, h = self.font.getsize(text)
if self.orientation in (Image.ROTATE_90, Image.ROTATE_270): if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
return h, w return h, w
return w, h return w, h
@ -833,7 +861,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
This specifies the character set to use. It does not alter the This specifies the character set to use. It does not alter the
encoding of any text provided in subsequent operations. encoding of any text provided in subsequent operations.
:param layout_engine: Which layout engine to use, if available: :param layout_engine: Which layout engine to use, if available:
:data:`.ImageFont.LAYOUT_BASIC` or :data:`.ImageFont.LAYOUT_RAQM`. :data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`.
You can check support for Raqm layout using You can check support for Raqm layout using
:py:func:`PIL.features.check_feature` with ``feature="raqm"``. :py:func:`PIL.features.check_feature` with ``feature="raqm"``.

View File

@ -237,7 +237,7 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
return _lut(image, red + green + blue) return _lut(image, red + green + blue)
def contain(image, size, method=Image.BICUBIC): def contain(image, size, method=Image.Resampling.BICUBIC):
""" """
Returns a resized version of the image, set to the maximum width and height Returns a resized version of the image, set to the maximum width and height
within the requested size, while maintaining the original aspect ratio. within the requested size, while maintaining the original aspect ratio.
@ -265,7 +265,7 @@ def contain(image, size, method=Image.BICUBIC):
return image.resize(size, resample=method) return image.resize(size, resample=method)
def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)): def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)):
""" """
Returns a resized and padded version of the image, expanded to fill the Returns a resized and padded version of the image, expanded to fill the
requested aspect ratio and size. requested aspect ratio and size.
@ -315,7 +315,7 @@ def crop(image, border=0):
return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
def scale(image, factor, resample=Image.BICUBIC): def scale(image, factor, resample=Image.Resampling.BICUBIC):
""" """
Returns a rescaled image by a specific factor given in parameter. Returns a rescaled image by a specific factor given in parameter.
A factor greater than 1 expands the image, between 0 and 1 contracts the A factor greater than 1 expands the image, between 0 and 1 contracts the
@ -336,7 +336,7 @@ def scale(image, factor, resample=Image.BICUBIC):
return image.resize(size, resample) return image.resize(size, resample)
def deform(image, deformer, resample=Image.BILINEAR): def deform(image, deformer, resample=Image.Resampling.BILINEAR):
""" """
Deform the image. Deform the image.
@ -347,7 +347,9 @@ def deform(image, deformer, resample=Image.BILINEAR):
in the PIL.Image.transform function. in the PIL.Image.transform function.
:return: An image. :return: An image.
""" """
return image.transform(image.size, Image.MESH, deformer.getmesh(image), resample) return image.transform(
image.size, Image.Transform.MESH, deformer.getmesh(image), resample
)
def equalize(image, mask=None): def equalize(image, mask=None):
@ -408,7 +410,7 @@ def expand(image, border=0, fill=0):
return out return out
def fit(image, size, method=Image.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):
""" """
Returns a resized and cropped version of the image, cropped to the Returns a resized and cropped version of the image, cropped to the
requested aspect ratio and size. requested aspect ratio and size.
@ -500,7 +502,7 @@ def flip(image):
:param image: The image to flip. :param image: The image to flip.
:return: An image. :return: An image.
""" """
return image.transpose(Image.FLIP_TOP_BOTTOM) return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
def grayscale(image): def grayscale(image):
@ -533,7 +535,7 @@ def mirror(image):
:param image: The image to mirror. :param image: The image to mirror.
:return: An image. :return: An image.
""" """
return image.transpose(Image.FLIP_LEFT_RIGHT) return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
def posterize(image, bits): def posterize(image, bits):
@ -579,13 +581,13 @@ def exif_transpose(image):
exif = image.getexif() exif = image.getexif()
orientation = exif.get(0x0112) orientation = exif.get(0x0112)
method = { method = {
2: Image.FLIP_LEFT_RIGHT, 2: Image.Transpose.FLIP_LEFT_RIGHT,
3: Image.ROTATE_180, 3: Image.Transpose.ROTATE_180,
4: Image.FLIP_TOP_BOTTOM, 4: Image.Transpose.FLIP_TOP_BOTTOM,
5: Image.TRANSPOSE, 5: Image.Transpose.TRANSPOSE,
6: Image.ROTATE_270, 6: Image.Transpose.ROTATE_270,
7: Image.TRANSVERSE, 7: Image.Transpose.TRANSVERSE,
8: Image.ROTATE_90, 8: Image.Transpose.ROTATE_90,
}.get(orientation) }.get(orientation)
if method is not None: if method is not None:
transposed_image = image.transpose(method) transposed_image = image.transpose(method)

View File

@ -47,7 +47,7 @@ class AffineTransform(Transform):
from an affine transform matrix. from an affine transform matrix.
""" """
method = Image.AFFINE method = Image.Transform.AFFINE
class ExtentTransform(Transform): class ExtentTransform(Transform):
@ -69,7 +69,7 @@ class ExtentTransform(Transform):
input image's coordinate system. See :ref:`coordinate-system`. input image's coordinate system. See :ref:`coordinate-system`.
""" """
method = Image.EXTENT method = Image.Transform.EXTENT
class QuadTransform(Transform): class QuadTransform(Transform):
@ -86,7 +86,7 @@ class QuadTransform(Transform):
source quadrilateral. source quadrilateral.
""" """
method = Image.QUAD method = Image.Transform.QUAD
class MeshTransform(Transform): class MeshTransform(Transform):
@ -99,4 +99,4 @@ class MeshTransform(Transform):
:param data: A list of (bbox, quad) tuples. :param data: A list of (bbox, quad) tuples.
""" """
method = Image.MESH method = Image.Transform.MESH

View File

@ -37,6 +37,7 @@ import re
import struct import struct
import warnings import warnings
import zlib import zlib
from enum import IntEnum
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -94,36 +95,62 @@ See :ref:`Text in PNG File Format<png-text>`.
# APNG frame disposal modes # APNG frame disposal modes
APNG_DISPOSE_OP_NONE = 0 class Disposal(IntEnum):
""" OP_NONE = 0
No disposal is done on this frame before rendering the next frame. """
See :ref:`Saving APNG sequences<apng-saving>`. No disposal is done on this frame before rendering the next frame.
""" See :ref:`Saving APNG sequences<apng-saving>`.
APNG_DISPOSE_OP_BACKGROUND = 1 """
""" OP_BACKGROUND = 1
This frames modified region is cleared to fully transparent black before rendering """
the next frame. This frames modified region is cleared to fully transparent black before rendering
See :ref:`Saving APNG sequences<apng-saving>`. the next frame.
""" See :ref:`Saving APNG sequences<apng-saving>`.
APNG_DISPOSE_OP_PREVIOUS = 2 """
""" OP_PREVIOUS = 2
This frames modified region is reverted to the previous frames contents before """
rendering the next frame. This frames modified region is reverted to the previous frames contents before
See :ref:`Saving APNG sequences<apng-saving>`. rendering the next frame.
""" See :ref:`Saving APNG sequences<apng-saving>`.
"""
# APNG frame blend modes # APNG frame blend modes
APNG_BLEND_OP_SOURCE = 0 class Blend(IntEnum):
""" OP_SOURCE = 0
All color components of this frame, including alpha, overwrite the previous output """
image contents. All color components of this frame, including alpha, overwrite the previous output
See :ref:`Saving APNG sequences<apng-saving>`. image contents.
""" See :ref:`Saving APNG sequences<apng-saving>`.
APNG_BLEND_OP_OVER = 1 """
""" OP_OVER = 1
This frame should be alpha composited with the previous output image contents. """
See :ref:`Saving APNG sequences<apng-saving>`. This frame should be alpha composited with the previous output image contents.
""" See :ref:`Saving APNG sequences<apng-saving>`.
"""
def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items():
if name.startswith(prefix):
name = name[len(prefix) :]
if name in enum.__members__:
warnings.warn(
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
def _safe_zlib_decompress(s): def _safe_zlib_decompress(s):
@ -861,13 +888,13 @@ class PngImageFile(ImageFile.ImageFile):
raise EOFError raise EOFError
# setup frame disposal (actual disposal done when needed in the next _seek()) # setup frame disposal (actual disposal done when needed in the next _seek())
if self._prev_im is None and self.dispose_op == APNG_DISPOSE_OP_PREVIOUS: if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
self.dispose_op = APNG_DISPOSE_OP_BACKGROUND self.dispose_op = Disposal.OP_BACKGROUND
if self.dispose_op == APNG_DISPOSE_OP_PREVIOUS: if self.dispose_op == Disposal.OP_PREVIOUS:
self.dispose = self._prev_im.copy() self.dispose = self._prev_im.copy()
self.dispose = self._crop(self.dispose, self.dispose_extent) self.dispose = self._crop(self.dispose, self.dispose_extent)
elif self.dispose_op == APNG_DISPOSE_OP_BACKGROUND: elif self.dispose_op == Disposal.OP_BACKGROUND:
self.dispose = Image.core.fill(self.mode, self.size) self.dispose = Image.core.fill(self.mode, self.size)
self.dispose = self._crop(self.dispose, self.dispose_extent) self.dispose = self._crop(self.dispose, self.dispose_extent)
else: else:
@ -956,7 +983,7 @@ class PngImageFile(ImageFile.ImageFile):
self.png.close() self.png.close()
self.png = None self.png = None
else: else:
if self._prev_im and self.blend_op == APNG_BLEND_OP_OVER: if self._prev_im and self.blend_op == Blend.OP_OVER:
updated = self._crop(self.im, self.dispose_extent) updated = self._crop(self.im, self.dispose_extent)
self._prev_im.paste( self._prev_im.paste(
updated, self.dispose_extent, updated.convert("RGBA") updated, self.dispose_extent, updated.convert("RGBA")
@ -1062,10 +1089,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
default_image = im.encoderinfo.get("default_image", im.info.get("default_image")) default_image = im.encoderinfo.get("default_image", im.info.get("default_image"))
duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
disposal = im.encoderinfo.get( disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
"disposal", im.info.get("disposal", APNG_DISPOSE_OP_NONE) blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
)
blend = im.encoderinfo.get("blend", im.info.get("blend", APNG_BLEND_OP_SOURCE))
if default_image: if default_image:
chain = itertools.chain(im.encoderinfo.get("append_images", [])) chain = itertools.chain(im.encoderinfo.get("append_images", []))
@ -1095,10 +1120,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
previous = im_frames[-1] previous = im_frames[-1]
prev_disposal = previous["encoderinfo"].get("disposal") prev_disposal = previous["encoderinfo"].get("disposal")
prev_blend = previous["encoderinfo"].get("blend") prev_blend = previous["encoderinfo"].get("blend")
if prev_disposal == APNG_DISPOSE_OP_PREVIOUS and len(im_frames) < 2: if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:
prev_disposal = APNG_DISPOSE_OP_BACKGROUND prev_disposal = Disposal.OP_BACKGROUND
if prev_disposal == APNG_DISPOSE_OP_BACKGROUND: if prev_disposal == Disposal.OP_BACKGROUND:
base_im = previous["im"] base_im = previous["im"]
dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
bbox = previous["bbox"] bbox = previous["bbox"]
@ -1107,7 +1132,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
else: else:
bbox = (0, 0) + im.size bbox = (0, 0) + im.size
base_im.paste(dispose, bbox) base_im.paste(dispose, bbox)
elif prev_disposal == APNG_DISPOSE_OP_PREVIOUS: elif prev_disposal == Disposal.OP_PREVIOUS:
base_im = im_frames[-2]["im"] base_im = im_frames[-2]["im"]
else: else:
base_im = previous["im"] base_im = previous["im"]

View File

@ -314,7 +314,7 @@ if __name__ == "__main__":
outfile = sys.argv[2] outfile = sys.argv[2]
# perform some image operation # perform some image operation
im = im.transpose(Image.FLIP_LEFT_RIGHT) im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
print( print(
f"saving a flipped version of {os.path.basename(filename)} " f"saving a flipped version of {os.path.basename(filename)} "
f"as {outfile} " f"as {outfile} "

View File

@ -152,7 +152,7 @@ class TgaImageFile(ImageFile.ImageFile):
def load_end(self): def load_end(self):
if self._flip_horizontally: if self._flip_horizontally:
self.im = self.im.transpose(Image.FLIP_LEFT_RIGHT) self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
# #

View File

@ -1161,13 +1161,13 @@ class TiffImageFile(ImageFile.ImageFile):
def load_end(self): def load_end(self):
if self._tile_orientation: if self._tile_orientation:
method = { method = {
2: Image.FLIP_LEFT_RIGHT, 2: Image.Transpose.FLIP_LEFT_RIGHT,
3: Image.ROTATE_180, 3: Image.Transpose.ROTATE_180,
4: Image.FLIP_TOP_BOTTOM, 4: Image.Transpose.FLIP_TOP_BOTTOM,
5: Image.TRANSPOSE, 5: Image.Transpose.TRANSPOSE,
6: Image.ROTATE_270, 6: Image.Transpose.ROTATE_270,
7: Image.TRANSVERSE, 7: Image.Transpose.TRANSVERSE,
8: Image.ROTATE_90, 8: Image.Transpose.ROTATE_90,
}.get(self._tile_orientation) }.get(self._tile_orientation)
if method is not None: if method is not None:
self.im = self.im.transpose(method) self.im = self.im.transpose(method)

View File

@ -1063,7 +1063,7 @@ _gaussian_blur(ImagingObject *self, PyObject *args) {
static PyObject * static PyObject *
_getpalette(ImagingObject *self, PyObject *args) { _getpalette(ImagingObject *self, PyObject *args) {
PyObject *palette; PyObject *palette;
int palettesize = 256; int palettesize;
int bits; int bits;
ImagingShuffler pack; ImagingShuffler pack;
@ -1084,6 +1084,7 @@ _getpalette(ImagingObject *self, PyObject *args) {
return NULL; return NULL;
} }
palettesize = self->image->palette->size;
palette = PyBytes_FromStringAndSize(NULL, palettesize * bits / 8); palette = PyBytes_FromStringAndSize(NULL, palettesize * bits / 8);
if (!palette) { if (!palette) {
return NULL; return NULL;
@ -1641,7 +1642,7 @@ _putpalette(ImagingObject *self, PyObject *args) {
ImagingShuffler unpack; ImagingShuffler unpack;
int bits; int bits;
char *rawmode; char *rawmode, *palette_mode;
UINT8 *palette; UINT8 *palette;
Py_ssize_t palettesize; Py_ssize_t palettesize;
if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) { if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) {
@ -1654,7 +1655,8 @@ _putpalette(ImagingObject *self, PyObject *args) {
return NULL; return NULL;
} }
unpack = ImagingFindUnpacker("RGB", rawmode, &bits); palette_mode = strncmp("RGBA", rawmode, 4) == 0 ? "RGBA" : "RGB";
unpack = ImagingFindUnpacker(palette_mode, rawmode, &bits);
if (!unpack) { if (!unpack) {
PyErr_SetString(PyExc_ValueError, wrong_raw_mode); PyErr_SetString(PyExc_ValueError, wrong_raw_mode);
return NULL; return NULL;
@ -1669,11 +1671,13 @@ _putpalette(ImagingObject *self, PyObject *args) {
strcpy(self->image->mode, strlen(self->image->mode) == 2 ? "PA" : "P"); strcpy(self->image->mode, strlen(self->image->mode) == 2 ? "PA" : "P");
self->image->palette = ImagingPaletteNew("RGB"); self->image->palette = ImagingPaletteNew(palette_mode);
unpack(self->image->palette->palette, palette, palettesize * 8 / bits); self->image->palette->size = palettesize * 8 / bits;
unpack(self->image->palette->palette, palette, self->image->palette->size);
return PyLong_FromLong(palettesize * 8 / bits); Py_INCREF(Py_None);
return Py_None;
} }
static PyObject * static PyObject *

View File

@ -991,115 +991,116 @@ static struct {
/* ------------------- */ /* ------------------- */
static void static void
p2bit(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
/* FIXME: precalculate greyscale palette? */ /* FIXME: precalculate greyscale palette? */
for (x = 0; x < xsize; x++) { for (x = 0; x < xsize; x++) {
*out++ = (L(&palette[in[x] * 4]) >= 128000) ? 255 : 0; *out++ = (L(&palette->palette[in[x] * 4]) >= 128000) ? 255 : 0;
} }
} }
static void static void
pa2bit(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { pa2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
/* FIXME: precalculate greyscale palette? */ /* FIXME: precalculate greyscale palette? */
for (x = 0; x < xsize; x++, in += 4) { for (x = 0; x < xsize; x++, in += 4) {
*out++ = (L(&palette[in[0] * 4]) >= 128000) ? 255 : 0; *out++ = (L(&palette->palette[in[0] * 4]) >= 128000) ? 255 : 0;
} }
} }
static void static void
p2l(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { p2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
/* FIXME: precalculate greyscale palette? */ /* FIXME: precalculate greyscale palette? */
for (x = 0; x < xsize; x++) { for (x = 0; x < xsize; x++) {
*out++ = L24(&palette[in[x] * 4]) >> 16; *out++ = L24(&palette->palette[in[x] * 4]) >> 16;
} }
} }
static void static void
pa2l(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
/* FIXME: precalculate greyscale palette? */ /* FIXME: precalculate greyscale palette? */
for (x = 0; x < xsize; x++, in += 4) { for (x = 0; x < xsize; x++, in += 4) {
*out++ = L24(&palette[in[0] * 4]) >> 16; *out++ = L24(&palette->palette[in[0] * 4]) >> 16;
} }
} }
static void static void
p2pa(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
int rgb = strcmp(palette->mode, "RGB");
for (x = 0; x < xsize; x++, in++) { for (x = 0; x < xsize; x++, in++) {
const UINT8 *rgba = &palette[in[0]]; const UINT8 *rgba = &palette->palette[in[0]];
*out++ = in[0]; *out++ = in[0];
*out++ = in[0]; *out++ = in[0];
*out++ = in[0]; *out++ = in[0];
*out++ = rgba[3]; *out++ = rgb == 0 ? 255 : rgba[3];
} }
} }
static void static void
p2la(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { p2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
/* FIXME: precalculate greyscale palette? */ /* FIXME: precalculate greyscale palette? */
for (x = 0; x < xsize; x++, out += 4) { for (x = 0; x < xsize; x++, out += 4) {
const UINT8 *rgba = &palette[*in++ * 4]; const UINT8 *rgba = &palette->palette[*in++ * 4];
out[0] = out[1] = out[2] = L24(rgba) >> 16; out[0] = out[1] = out[2] = L24(rgba) >> 16;
out[3] = rgba[3]; out[3] = rgba[3];
} }
} }
static void static void
pa2la(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { pa2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
/* FIXME: precalculate greyscale palette? */ /* FIXME: precalculate greyscale palette? */
for (x = 0; x < xsize; x++, in += 4, out += 4) { for (x = 0; x < xsize; x++, in += 4, out += 4) {
out[0] = out[1] = out[2] = L24(&palette[in[0] * 4]) >> 16; out[0] = out[1] = out[2] = L24(&palette->palette[in[0] * 4]) >> 16;
out[3] = in[3]; out[3] = in[3];
} }
} }
static void static void
p2i(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) { p2i(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
for (x = 0; x < xsize; x++, out_ += 4) { for (x = 0; x < xsize; x++, out_ += 4) {
INT32 v = L24(&palette[in[x] * 4]) >> 16; INT32 v = L24(&palette->palette[in[x] * 4]) >> 16;
memcpy(out_, &v, sizeof(v)); memcpy(out_, &v, sizeof(v));
} }
} }
static void static void
pa2i(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) { pa2i(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
INT32 *out = (INT32 *)out_; INT32 *out = (INT32 *)out_;
for (x = 0; x < xsize; x++, in += 4) { for (x = 0; x < xsize; x++, in += 4) {
*out++ = L24(&palette[in[0] * 4]) >> 16; *out++ = L24(&palette->palette[in[0] * 4]) >> 16;
} }
} }
static void static void
p2f(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) { p2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
for (x = 0; x < xsize; x++, out_ += 4) { for (x = 0; x < xsize; x++, out_ += 4) {
FLOAT32 v = L(&palette[in[x] * 4]) / 1000.0F; FLOAT32 v = L(&palette->palette[in[x] * 4]) / 1000.0F;
memcpy(out_, &v, sizeof(v)); memcpy(out_, &v, sizeof(v));
} }
} }
static void static void
pa2f(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) { pa2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
FLOAT32 *out = (FLOAT32 *)out_; FLOAT32 *out = (FLOAT32 *)out_;
for (x = 0; x < xsize; x++, in += 4) { for (x = 0; x < xsize; x++, in += 4) {
*out++ = (float)L(&palette[in[0] * 4]) / 1000.0F; *out++ = (float)L(&palette->palette[in[0] * 4]) / 1000.0F;
} }
} }
static void static void
p2rgb(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { p2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
for (x = 0; x < xsize; x++) { for (x = 0; x < xsize; x++) {
const UINT8 *rgb = &palette[*in++ * 4]; const UINT8 *rgb = &palette->palette[*in++ * 4];
*out++ = rgb[0]; *out++ = rgb[0];
*out++ = rgb[1]; *out++ = rgb[1];
*out++ = rgb[2]; *out++ = rgb[2];
@ -1108,10 +1109,10 @@ p2rgb(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) {
} }
static void static void
pa2rgb(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { pa2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
for (x = 0; x < xsize; x++, in += 4) { for (x = 0; x < xsize; x++, in += 4) {
const UINT8 *rgb = &palette[in[0] * 4]; const UINT8 *rgb = &palette->palette[in[0] * 4];
*out++ = rgb[0]; *out++ = rgb[0];
*out++ = rgb[1]; *out++ = rgb[1];
*out++ = rgb[2]; *out++ = rgb[2];
@ -1120,30 +1121,30 @@ pa2rgb(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) {
} }
static void static void
p2hsv(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { p2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
for (x = 0; x < xsize; x++, out += 4) { for (x = 0; x < xsize; x++, out += 4) {
const UINT8 *rgb = &palette[*in++ * 4]; const UINT8 *rgb = &palette->palette[*in++ * 4];
rgb2hsv_row(out, rgb); rgb2hsv_row(out, rgb);
out[3] = 255; out[3] = 255;
} }
} }
static void static void
pa2hsv(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { pa2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
for (x = 0; x < xsize; x++, in += 4, out += 4) { for (x = 0; x < xsize; x++, in += 4, out += 4) {
const UINT8 *rgb = &palette[in[0] * 4]; const UINT8 *rgb = &palette->palette[in[0] * 4];
rgb2hsv_row(out, rgb); rgb2hsv_row(out, rgb);
out[3] = 255; out[3] = 255;
} }
} }
static void static void
p2rgba(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { p2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
for (x = 0; x < xsize; x++) { for (x = 0; x < xsize; x++) {
const UINT8 *rgba = &palette[*in++ * 4]; const UINT8 *rgba = &palette->palette[*in++ * 4];
*out++ = rgba[0]; *out++ = rgba[0];
*out++ = rgba[1]; *out++ = rgba[1];
*out++ = rgba[2]; *out++ = rgba[2];
@ -1152,10 +1153,10 @@ p2rgba(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) {
} }
static void static void
pa2rgba(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { pa2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
for (x = 0; x < xsize; x++, in += 4) { for (x = 0; x < xsize; x++, in += 4) {
const UINT8 *rgb = &palette[in[0] * 4]; const UINT8 *rgb = &palette->palette[in[0] * 4];
*out++ = rgb[0]; *out++ = rgb[0];
*out++ = rgb[1]; *out++ = rgb[1];
*out++ = rgb[2]; *out++ = rgb[2];
@ -1164,25 +1165,25 @@ pa2rgba(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) {
} }
static void static void
p2cmyk(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { p2cmyk(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
p2rgb(out, in, xsize, palette); p2rgb(out, in, xsize, palette);
rgb2cmyk(out, out, xsize); rgb2cmyk(out, out, xsize);
} }
static void static void
pa2cmyk(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { pa2cmyk(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
pa2rgb(out, in, xsize, palette); pa2rgb(out, in, xsize, palette);
rgb2cmyk(out, out, xsize); rgb2cmyk(out, out, xsize);
} }
static void static void
p2ycbcr(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { p2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
p2rgb(out, in, xsize, palette); p2rgb(out, in, xsize, palette);
ImagingConvertRGB2YCbCr(out, out, xsize); ImagingConvertRGB2YCbCr(out, out, xsize);
} }
static void static void
pa2ycbcr(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { pa2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
pa2rgb(out, in, xsize, palette); pa2rgb(out, in, xsize, palette);
ImagingConvertRGB2YCbCr(out, out, xsize); ImagingConvertRGB2YCbCr(out, out, xsize);
} }
@ -1192,7 +1193,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) {
ImagingSectionCookie cookie; ImagingSectionCookie cookie;
int alpha; int alpha;
int y; int y;
void (*convert)(UINT8 *, const UINT8 *, int, const UINT8 *); void (*convert)(UINT8 *, const UINT8 *, int, ImagingPalette);
/* Map palette image to L, RGB, RGBA, or CMYK */ /* Map palette image to L, RGB, RGBA, or CMYK */
@ -1239,7 +1240,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) {
(UINT8 *)imOut->image[y], (UINT8 *)imOut->image[y],
(UINT8 *)imIn->image[y], (UINT8 *)imIn->image[y],
imIn->xsize, imIn->xsize,
imIn->palette->palette); imIn->palette);
} }
ImagingSectionLeave(&cookie); ImagingSectionLeave(&cookie);

View File

@ -143,6 +143,7 @@ struct ImagingPaletteInstance {
char mode[IMAGING_MODE_LENGTH]; /* Band names */ char mode[IMAGING_MODE_LENGTH]; /* Band names */
/* Data */ /* Data */
int size;
UINT8 palette[1024]; /* Palette data (same format as image data) */ UINT8 palette[1024]; /* Palette data (same format as image data) */
INT16 *cache; /* Palette cache (used for predefined palettes) */ INT16 *cache; /* Palette cache (used for predefined palettes) */

View File

@ -40,6 +40,7 @@ ImagingPaletteNew(const char *mode) {
palette->mode[IMAGING_MODE_LENGTH - 1] = 0; palette->mode[IMAGING_MODE_LENGTH - 1] = 0;
/* Initialize to ramp */ /* Initialize to ramp */
palette->size = 256;
for (i = 0; i < 256; i++) { for (i = 0; i < 256; i++) {
palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] = palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] =
palette->palette[i * 4 + 2] = (UINT8)i; palette->palette[i * 4 + 2] = (UINT8)i;
@ -193,7 +194,7 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) {
dmax = (unsigned int)~0; dmax = (unsigned int)~0;
for (i = 0; i < 256; i++) { for (i = 0; i < palette->size; i++) {
int r, g, b; int r, g, b;
unsigned int tmin, tmax; unsigned int tmin, tmax;
@ -226,7 +227,7 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) {
d[i] = (unsigned int)~0; d[i] = (unsigned int)~0;
} }
for (i = 0; i < 256; i++) { for (i = 0; i < palette->size; i++) {
if (dmin[i] <= dmax) { if (dmin[i] <= dmax) {
int rd, gd, bd; int rd, gd, bd;
int ri, gi, bi; int ri, gi, bi;