pypi312 / pillow /Test_Pillow.py
PythonSTB's picture
Upload pillow/Test_Pillow.py with huggingface_hub
e7c4196 verified
Raw
History Blame Contribute Delete
22.6 kB
#!/usr/bin/env python3
"""
test_pillow.py — standalone pure-Python functional test for the android wheels.
Works with both Pillow (12.3.0) and Pillow-SIMD (9.5.0.post2). Only uses the
Python stdlib plus the PIL package itself (no numpy / third-party deps).
Run on the device/emulator:
python /path/to/test_pillow.py
Exit code: 0 = all critical tests passed, 1 = at least one failure.
Optional/skippable tests (missing font, Tk/Qt, missing codec feature) are
reported as SKIP and do not affect the exit code.
Generated by RIMI
"""
import os
import sys
import tempfile
import traceback
import io
# --------------------------------------------------------------------------
# tiny test harness
# --------------------------------------------------------------------------
RESULTS = [] # (kind, name, detail)
KIND_OK = "ok"
KIND_FAIL = "fail"
KIND_SKIP = "skip"
def check(name, fn, *args, **kwargs):
try:
fn(*args, **kwargs)
RESULTS.append((KIND_OK, name, ""))
except SkipTest as e:
RESULTS.append((KIND_SKIP, name, str(e)))
except Exception:
RESULTS.append((KIND_FAIL, name, traceback.format_exc().strip()))
class SkipTest(Exception):
pass
def require(cond, why):
if not cond:
raise SkipTest(why)
def ok_or_skip(cond, why):
if not cond:
raise SkipTest(why)
# --------------------------------------------------------------------------
# 1. imports
# --------------------------------------------------------------------------
CORE_MODULES = [
"PIL", "PIL._version", "PIL._binary", "PIL._util", "PIL._deprecate",
"PIL.Image", "PIL.ImageMode", "PIL.ImageFile", "PIL.ImageFilter",
"PIL.ImageDraw", "PIL.ImageDraw2", "PIL.ImageFont", "PIL.ImageColor",
"PIL.ImageChops", "PIL.ImageEnhance", "PIL.ImageOps", "PIL.ImageStat",
"PIL.ImageMath", "PIL.ImagePath", "PIL.ImageSequence", "PIL.ImageMorph",
"PIL.ImageTransform", "PIL.ImagePalette", "PIL.ImageShow", "PIL.ImageGrab",
"PIL.ExifTags", "PIL.TiffTags",
"PIL.features", "PIL.BdfFontFile", "PIL.FontFile", "PIL.PcfFontFile",
"PIL.TarIO", "PIL.WalImageFile",
# plugins
"PIL.BlpImagePlugin", "PIL.BmpImagePlugin", "PIL.BufrStubImagePlugin",
"PIL.CurImagePlugin", "PIL.DcxImagePlugin", "PIL.DdsImagePlugin",
"PIL.EpsImagePlugin", "PIL.FliImagePlugin",
"PIL.FtexImagePlugin",
"PIL.GbrImagePlugin", "PIL.GdImageFile", "PIL.GifImagePlugin",
"PIL.GimpGradientFile", "PIL.GimpPaletteFile", "PIL.GribStubImagePlugin",
"PIL.Hdf5StubImagePlugin", "PIL.IcnsImagePlugin", "PIL.IcoImagePlugin",
"PIL.ImImagePlugin", "PIL.ImtImagePlugin", "PIL.IptcImagePlugin",
"PIL.Jpeg2KImagePlugin", "PIL.JpegImagePlugin", "PIL.JpegPresets",
"PIL.McIdasImagePlugin", "PIL.MpegImagePlugin",
"PIL.MpoImagePlugin", "PIL.MspImagePlugin", "PIL.PalmImagePlugin",
"PIL.PcdImagePlugin", "PIL.PcxImagePlugin", "PIL.PdfImagePlugin",
"PIL.PdfParser", "PIL.PixarImagePlugin", "PIL.PngImagePlugin",
"PIL.PpmImagePlugin", "PIL.PsdImagePlugin", "PIL.PSDraw",
"PIL.QoiImagePlugin", "PIL.SgiImagePlugin", "PIL.SpiderImagePlugin",
"PIL.SunImagePlugin", "PIL.TgaImagePlugin", "PIL.TiffImagePlugin",
"PIL.WebPImagePlugin", "PIL.WmfImagePlugin", "PIL.XbmImagePlugin",
"PIL.XpmImagePlugin", "PIL.XVThumbImagePlugin",
]
OPTIONAL_MODULES = [
# these may legitimately fail to import on a headless/android build
("PIL.ImageQt", "Qt bindings not installed"),
("PIL.ImageTk", "tkinter not available"),
("PIL.ImageWin", "Windows-only"),
("PIL.MicImagePlugin", "requires olefile (optional dep)"),
("PIL.FitsStubImagePlugin", "stub module absent in Pillow 12.3"),
("PIL.FpxImagePlugin", "requires olefile (optional dep)"),
]
FAILED_IMPORTS = []
def test_imports():
for name in CORE_MODULES:
try:
__import__(name)
except Exception:
FAILED_IMPORTS.append(name)
RESULTS.append((KIND_FAIL, "import " + name, traceback.format_exc().strip()))
for name, why in OPTIONAL_MODULES:
try:
__import__(name)
except Exception:
RESULTS.append((KIND_SKIP, "import " + name, why))
# --------------------------------------------------------------------------
# 2. version / features
# --------------------------------------------------------------------------
def test_version_and_features():
import PIL
from PIL import features
ver = getattr(PIL, "__version__", "?")
print(f"[info] PIL package: {ver}")
print(f"[info] PIL.__file__: {PIL.__file__}")
print(f"[info] PIL.__version__: {ver}")
feature_names = features.get_supported()
print("[info] supported features: " + ", ".join(sorted(feature_names)))
for feat in ("jpg", "jpg_2000", "zlib", "libtiff", "webp", "webp_mux",
"libimagequant", "raqm", "freetype2", "littlecms2", "png"):
try:
avail = features.check(feat)
except Exception:
avail = None
print(f"[info] feature {feat}: {avail}")
# sanity: extension modules were actually imported (native build present)
from PIL import Image as _Img
core = _Img.core
assert hasattr(core, "blend"), "_imaging native module not loaded"
from PIL import _imagingft, _imagingcms, _webp, _imagingmath, _imagingmorph
for mod, pyinit in ((_imagingft, "getfont"), (_imagingcms, "createProfile"),
(_webp, "WebPEncode"), (_imagingmath, "unop"),
(_imagingmorph, "apply")):
assert any(hasattr(mod, n) for n in (pyinit,)), f"{mod.__name__} looks empty — attrs: {[a for a in dir(mod) if not a.startswith('_')][:10]}"
RESULTS.append((KIND_OK, "version+features+native modules", ""))
# --------------------------------------------------------------------------
# helpers
# --------------------------------------------------------------------------
def make_test_image(mode="RGB", size=(64, 48)):
from PIL import Image
im = Image.new(mode, size)
px = im.load()
w, h = size
for y in range(h):
for x in range(w):
if mode == "L":
px[x, y] = (x * 3 + y * 5) % 256
elif mode == "RGB":
px[x, y] = ((x * 4) % 256, (y * 6) % 256, (x * y) % 256)
elif mode == "RGBA":
px[x, y] = ((x * 4) % 256, (y * 6) % 256, (x * y) % 256, (x + y) % 256)
elif mode == "1":
px[x, y] = (x + y) % 2
elif mode == "P":
px[x, y] = (x + y) % 256
else:
px[x, y] = (x * y) % 256
return im
def roundtrip(name, ext, save_kwargs, load_kwargs=None, mode="RGB", check_size=None):
from PIL import Image
im = make_test_image(mode)
tmpdir = tempfile.mkdtemp(prefix="pillow_test_")
try:
path = os.path.join(tmpdir, "img" + ext)
im.save(path, **save_kwargs)
with Image.open(path, **(load_kwargs or {})) as out:
out.load()
sz = check_size or im.size
assert out.size == sz, f"size {out.size} != {sz}"
assert out.format, "format not detected"
with open(path, "rb") as fh:
buf = io.BytesIO(fh.read())
with Image.open(buf) as out2:
out2.load()
assert out2.size == sz
RESULTS.append((KIND_OK, "roundtrip " + ext, ""))
finally:
import shutil
shutil.rmtree(tmpdir, ignore_errors=True)
def roundtrip_assert_approx(im1, im2, tol=6, frac=0.05):
"""Check pixel match within tolerance on a sampled grid."""
from PIL import Image
assert im1.size == im2.size, f"size mismatch {im1.size} vs {im2.size}"
p1 = im1.convert("RGB").load()
p2 = im2.convert("RGB").load()
w, h = im1.size
mism = 0
total = 0
for y in range(0, h, max(1, h // 8)):
for x in range(0, w, max(1, w // 8)):
total += 1
c1 = p1[x, y]
c2 = p2[x, y]
if max(abs(a - b) for a, b in zip(c1, c2)) > tol:
mism += 1
assert mism <= max(1, total * frac), f"{mism}/{total} pixels differ > {tol}"
# --------------------------------------------------------------------------
# 3. codec round-trips (needs merged libpillow_codecs.so + zlib)
# --------------------------------------------------------------------------
def test_codecs():
from PIL import features
def codec(cond, why):
if not cond:
raise SkipTest(why)
codec(features.check("zlib") or True, "") # png needs zlib (always present)
codec(features.check("jpg"), "no jpeg")
roundtrip("jpeg", ".jpg", {"quality": 90})
roundtrip("jpeg-gray", ".jpg", {"quality": 90}, mode="L")
codec(features.check("zlib"), "no zlib")
roundtrip("png", ".png", {})
roundtrip("png-rgba", ".png", {}, mode="RGBA")
roundtrip("png-palette", ".png", {}, mode="P")
codec(features.check("zlib"), "no zlib")
roundtrip("gif", ".gif", {}, mode="P")
roundtrip("tiff-lzw", ".tiff", {"compression": "tiff_lzw"})
roundtrip("tiff-packbits", ".tiff", {"compression": "packbits"})
codec(features.check("libtiff"), "no libtiff")
roundtrip("tiff-none", ".tiff", {})
codec(features.check("webp"), "no webp")
roundtrip("webp-lossless", ".webp", {"lossless": True})
roundtrip("webp-lossy", ".webp", {"quality": 80})
codec(features.check("jpg_2000"), "no openjpeg")
roundtrip("jpeg2000", ".jp2", {"irreversible": False})
roundtrip("bmp", ".bmp", {})
roundtrip("ppm", ".ppm", {})
roundtrip("pcx", ".pcx", {})
roundtrip("tga", ".tga", {})
roundtrip("dib", ".dib", {})
roundtrip("sgi", ".sgi", {}, mode="L")
roundtrip("xbm", ".xbm", {}, mode="1")
# ICO / ICO with embedded PNG
from PIL import Image as _Image
ico = make_test_image("RGB").resize((64, 48))
_tmp_ico = io.BytesIO()
ico.save(_tmp_ico, format="ICO", sizes=[(16, 16), (32, 32)])
_tmp_ico.seek(0)
ico_back = _Image.open(_tmp_ico)
assert ico_back.size[0] <= 64 and ico_back.size[1] <= 48 and ico_back.size[0] > 0
assert ico_back.load()[0, 0] is not None
RESULTS.append((KIND_OK, "codec round-trips", ""))
# --------------------------------------------------------------------------
# 4. core image API
# --------------------------------------------------------------------------
def test_core_api():
from PIL import (Image, ImageFilter, ImageChops, ImageEnhance, ImageStat,
ImagePalette, ImageOps)
im = make_test_image("RGB")
w, h = im.size
# copy / crop / resize / rotate / transpose
im.copy().crop((0, 0, 10, 10)).load()
r = im.resize((32, 24))
assert r.size == (32, 24)
r = im.resize((16, 12), getattr(Image, "Resampling", object).LANCZOS)
assert r.size == (16, 12)
assert im.rotate(90, expand=True).size == (h, w)
assert im.transpose(Image.Transpose.FLIP_LEFT_RIGHT).size == (w, h)
# point / convert / quantize / palette
g = im.convert("L")
g.point(lambda v: 255 - v)
g.convert("1")
q = im.quantize(colors=16, method=Image.Quantize.MEDIANCUT)
assert q.mode == "P"
if hasattr(Image.Quantize, "LIBIMAGEQUANT"):
try:
q2 = im.quantize(colors=16, method=Image.Quantize.LIBIMAGEQUANT)
assert q2.mode == "P"
except Exception:
raise SkipTest("LIBIMAGEQUANT quantize failed at runtime")
# split / merge
bands = im.split()
merged = Image.merge("RGB", bands)
assert merged.size == im.size
# paste / composite / alpha
im2 = Image.new("RGB", im.size, (255, 0, 0))
im2.paste(im, (0, 0))
composite = Image.composite(im, im2, Image.new("L", im.size, 128))
assert composite.size == im.size
overlay = make_test_image("RGBA")
base = Image.new("RGBA", im.size, (0, 0, 0, 255))
base.alpha_composite(overlay)
assert base.size == im.size
# filters
for f in (ImageFilter.BLUR, ImageFilter.GaussianBlur(2.0),
ImageFilter.BoxBlur(2), ImageFilter.SMOOTH,
ImageFilter.SHARPEN, ImageFilter.EDGE_ENHANCE,
ImageFilter.FIND_EDGES, ImageFilter.EMBOSS,
ImageFilter.CONTOUR, ImageFilter.UnsharpMask(radius=2, percent=120),
ImageFilter.MaxFilter(3), ImageFilter.MedianFilter(3),
ImageFilter.MinFilter(3), ImageFilter.ModeFilter(3)):
fimg = im.filter(f)
assert fimg.size == im.size
# convolution kernel
kern = [1 / 9] * 9
assert im.filter(ImageFilter.Kernel((3, 3), kern)).size == im.size
# histogram / stat / getbbox
im.histogram()
ImageStat.Stat(im)
assert im.getbbox() is not None
# enhancers / chops
ImageEnhance.Brightness(im).enhance(1.2)
ImageEnhance.Contrast(im).enhance(1.2)
ImageEnhance.Color(im).enhance(1.2)
ImageEnhance.Sharpness(im).enhance(1.5)
ImageChops.add(im, im2)
ImageChops.subtract(im, im2)
ImageChops.difference(im, im2)
ImageChops.multiply(im, im2)
ImageChops.offset(im, 5, 5)
ImageChops.invert(im)
# pixel access, getdata/putdata, getpixel/putpixel
pa = im.load()
px0 = pa[0, 0]
pa[0, 0] = (0, 0, 0)
pa[0, 0] = px0
im.putpixel((1, 1), im.getpixel((1, 1)))
im.getdata()
# ops
ImageOps.invert(g)
ImageOps.autocontrast(g)
ImageOps.equalize(g)
ImageOps.grayscale(im)
ImageOps.flip(im)
ImageOps.mirror(im)
ImageOps.crop(im, border=2)
ImageOps.scale(im, 0.5)
ImageOps.fit(im, (20, 20))
ImageOps.pad(im, (20, 20))
ImageOps.expand(im, border=2, fill=0)
# transform
im.transform((32, 24), Image.Transform.AFFINE, (1, 0, 0, 0, 1, 0))
im.transform((32, 24), Image.Transform.QUAD, (0, 0, 0, h, w, h, w, 0))
# info / metadata style
assert im.format is None
assert im.mode == "RGB"
assert im.size == (w, h)
RESULTS.append((KIND_OK, "core image API", ""))
# --------------------------------------------------------------------------
# 5. ImageDraw + ImageFont (freetype)
# --------------------------------------------------------------------------
def find_font():
candidates = [
"/system/fonts/Roboto-Regular.ttf",
"/system/fonts/Roboto-Medium.ttf",
"/system/fonts/DroidSans.ttf",
"/system/fonts/NotoSans-Regular.ttf",
"/system/fonts/NotoSansCJK-Regular.ttc",
]
import glob
for c in candidates:
if os.path.isfile(c):
return c
hits = sorted(glob.glob("/system/fonts/*.ttf"))
return hits[0] if hits else None
def test_draw_font():
from PIL import Image, ImageDraw, ImageFont
font_path = find_font()
im = Image.new("RGB", (200, 100), "white")
d = ImageDraw.Draw(im)
d.ellipse((10, 10, 50, 50), fill="red", outline="blue")
d.rectangle((60, 10, 120, 50), fill="green")
d.line((130, 10, 190, 50), fill="black", width=3)
d.polygon([(10, 60), (60, 90), (110, 60)], fill="orange")
d.point((150, 80), fill="purple")
d.arc((160, 60, 190, 90), start=0, end=180, fill="black")
d.rounded_rectangle((10, 60, 80, 95), radius=5, fill="cyan")
if font_path is None:
raise SkipTest("no system font available")
font = ImageFont.truetype(font_path, 18)
assert hasattr(font, "getbbox") or hasattr(font, "getsize")
d.text((10, 10), "Hello PIL", font=font, fill="black")
d.multiline_text((10, 40), "Line1\nLine2", font=font, fill="black")
if hasattr(d, "textbbox"):
d.textbbox((10, 10), "Hello", font=font)
if hasattr(d, "textlength"):
d.textlength("Hello", font=font)
if hasattr(d, "textsize"):
d.textsize("Hello", font=font)
# draw2
from PIL import ImageDraw2
im2 = Image.new("RGB", (100, 60), "white")
d2 = ImageDraw2.Draw(im2)
d2.polygon([(10, 10), (40, 50), (70, 10)], ImageDraw2.Brush("blue"))
d2.line([(0, 0), (90, 50)], ImageDraw2.Pen("black", 2))
RESULTS.append((KIND_OK, "ImageDraw + ImageFont", ""))
# --------------------------------------------------------------------------
# 6. ImageMath / ImagePath / ImageMorph / ImageSequence
# --------------------------------------------------------------------------
def test_misc_modules():
from PIL import Image, ImageMath, ImagePath, ImageSequence
l1 = make_test_image("L")
l2 = l1.point(lambda v: v // 2 + 10)
if hasattr(ImageMath, 'eval'):
res = ImageMath.eval("a + b", a=l1, b=l2)
elif hasattr(ImageMath, 'unsafe_eval'):
res = ImageMath.unsafe_eval("a + b", a=l1, b=l2)
else:
raise SkipTest("ImageMath.eval/unsafe_eval not available")
assert res.size == l1.size
p = ImagePath.Path([(0, 0), (10, 0), (10, 10), (0, 10)])
assert len(p) == 4
p.transform((1, 0, 1, 0, 1, 1))
seq = list(ImageSequence.Iterator(l1))
assert len(seq) == 1
# ImageMorph
from PIL import ImageMorph
mm = ImageMorph.MorphOp(op_name="dilation4")
bc, out = mm.apply(l1.convert("L").point(lambda v: 255 if v > 128 else 0))
assert out.size == l1.size
mm2 = ImageMorph.MorphOp(lut=mm.lut)
_, out2 = mm2.apply(l1.convert("L").point(lambda v: 255 if v > 128 else 0))
assert out2.size == l1.size
mm.match(l1.convert("L").point(lambda v: 255 if v > 128 else 0))
RESULTS.append((KIND_OK, "ImageMath/Path/Morph/Sequence", ""))
# --------------------------------------------------------------------------
# 7. ImageCms (littlecms2)
# --------------------------------------------------------------------------
def test_cms():
try:
from PIL import ImageCms
except ImportError:
raise SkipTest("no PIL.ImageCms")
from PIL import Image
sRGB = ImageCms.createProfile("sRGB")
lab = ImageCms.createProfile("LAB")
tf = ImageCms.buildTransform(sRGB, lab, "RGB", "LAB")
im = make_test_image("RGB")
out = ImageCms.applyTransform(im, tf)
assert out.size == im.size
try:
prof = ImageCms.ImageCmsProfile(sRGB)
assert prof.profile.profile_id
except Exception:
pass
RESULTS.append((KIND_OK, "ImageCms (littlecms)", ""))
# --------------------------------------------------------------------------
# 8. numpy interop (optional, only if numpy installed)
# --------------------------------------------------------------------------
def test_numpy():
try:
import numpy as np
except ImportError:
raise SkipTest("numpy not installed")
from PIL import Image
arr = np.zeros((32, 32, 3), dtype=np.uint8)
arr[..., 0] = 255
im = Image.fromarray(arr)
assert im.size == (32, 32) and im.mode == "RGB"
back = np.asarray(im)
assert back.shape == (32, 32, 3)
assert back[0, 0, 0] == 255
RESULTS.append((KIND_OK, "numpy interop", ""))
# --------------------------------------------------------------------------
# 9. pillow-simd specific exercise (resample/boxblur/reduce paths)
# --------------------------------------------------------------------------
def test_simd_paths():
from PIL import Image, ImageFilter
im = make_test_image("RGB", (256, 192))
Res = getattr(Image, "Resampling", None)
if Res is None:
raise SkipTest("Resampling enum missing")
for filt in (Res.BOX, Res.HAMMING, Res.BILINEAR, Res.BICUBIC, Res.LANCZOS):
small = im.resize((48, 36), filt)
assert small.size == (48, 36)
up = small.resize((256, 192), filt)
assert up.size == (256, 192)
# reduce via thumbnail
th = im.copy()
th.thumbnail((32, 32))
assert max(th.size) <= 32
# box blur
for n in (1, 2, 3):
assert im.filter(ImageFilter.BoxBlur(n)).size == im.size
# unsharp mask (uses Reduce-like / convolution paths in SIMD builds)
assert im.filter(ImageFilter.UnsharpMask(radius=3, percent=150)).size == im.size
# rank / min / max filters
for f in (ImageFilter.MinFilter(3), ImageFilter.MaxFilter(3), ImageFilter.MedianFilter(3)):
assert im.filter(f).size == im.size
# ImageOps scale/fit (uses resize internally)
ImageOps_scale = __import__("PIL.ImageOps", fromlist=["scale"])
ImageOps_scale.scale(im, 0.5)
ImageOps_scale.fit(im, (24, 24))
# color LUT / point LUT path
lut = list(range(256)) * 3
assert im.point(lut).size == im.size
RESULTS.append((KIND_OK, "SIMD/exercise paths", ""))
# --------------------------------------------------------------------------
# main
# --------------------------------------------------------------------------
def main():
print("=" * 64)
print("Pillow / Pillow-SIMD functional test")
print("python:", sys.version.split()[0])
print("platform:", sys.platform)
print("=" * 64)
test_imports()
check("version+features", test_version_and_features)
check("codecs", test_codecs)
check("core API", test_core_api)
check("draw+font", test_draw_font)
check("misc modules", test_misc_modules)
check("ImageCms", test_cms)
check("numpy interop", test_numpy)
check("SIMD paths", test_simd_paths)
print()
print("=" * 64)
ok = sum(1 for k, _, _ in RESULTS if k == KIND_OK)
fail = sum(1 for k, _, _ in RESULTS if k == KIND_FAIL)
skip = sum(1 for k, _, _ in RESULTS if k == KIND_SKIP)
print(f"RESULT: {ok} ok, {fail} failed, {skip} skipped (of {len(RESULTS)} total)")
# print any skips
for k, name, detail in RESULTS:
if k == KIND_SKIP:
print(f" [skip] {name}: {detail}")
# print failures with traceback
for k, name, detail in RESULTS:
if k == KIND_FAIL:
print()
print(f"### FAIL: {name}")
print(detail)
# separate top-level import failures
if FAILED_IMPORTS:
print()
print("### FAILED MODULE IMPORTS:")
for n in FAILED_IMPORTS:
print(" " + n)
print("=" * 64)
sys.exit(1 if fail else 0)
if __name__ == "__main__":
main()