PythonSTB commited on
Commit
10fb34a
·
verified ·
1 Parent(s): 16dfa75

Upload pillow-simd/Test_Pillow-SIMD.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. pillow-simd/Test_Pillow-SIMD.py +629 -0
pillow-simd/Test_Pillow-SIMD.py ADDED
@@ -0,0 +1,629 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ test_pillow.py — standalone pure-Python functional test for the android wheels.
4
+
5
+ Works with both Pillow (12.3.0) and Pillow-SIMD (9.5.0.post2). Only uses the
6
+ Python stdlib plus the PIL package itself (no numpy / third-party deps).
7
+
8
+ Run on the device/emulator:
9
+ python /path/to/test_pillow.py
10
+
11
+ Exit code: 0 = all critical tests passed, 1 = at least one failure.
12
+ Optional/skippable tests (missing font, Tk/Qt, missing codec feature) are
13
+ reported as SKIP and do not affect the exit code.
14
+
15
+ Generated by RIMI
16
+ """
17
+
18
+ import os
19
+ import sys
20
+ import tempfile
21
+ import traceback
22
+ import io
23
+
24
+ # --------------------------------------------------------------------------
25
+ # tiny test harness
26
+ # --------------------------------------------------------------------------
27
+
28
+ RESULTS = [] # (kind, name, detail)
29
+ KIND_OK = "ok"
30
+ KIND_FAIL = "fail"
31
+ KIND_SKIP = "skip"
32
+
33
+
34
+ def check(name, fn, *args, **kwargs):
35
+ try:
36
+ fn(*args, **kwargs)
37
+ RESULTS.append((KIND_OK, name, ""))
38
+ except SkipTest as e:
39
+ RESULTS.append((KIND_SKIP, name, str(e)))
40
+ except Exception:
41
+ RESULTS.append((KIND_FAIL, name, traceback.format_exc().strip()))
42
+
43
+
44
+ class SkipTest(Exception):
45
+ pass
46
+
47
+
48
+ def require(cond, why):
49
+ if not cond:
50
+ raise SkipTest(why)
51
+
52
+
53
+ def ok_or_skip(cond, why):
54
+ if not cond:
55
+ raise SkipTest(why)
56
+
57
+
58
+ # --------------------------------------------------------------------------
59
+ # 1. imports
60
+ # --------------------------------------------------------------------------
61
+
62
+ CORE_MODULES = [
63
+ "PIL", "PIL._version", "PIL._binary", "PIL._util", "PIL._deprecate",
64
+ "PIL.Image", "PIL.ImageMode", "PIL.ImageFile", "PIL.ImageFilter",
65
+ "PIL.ImageDraw", "PIL.ImageDraw2", "PIL.ImageFont", "PIL.ImageColor",
66
+ "PIL.ImageChops", "PIL.ImageEnhance", "PIL.ImageOps", "PIL.ImageStat",
67
+ "PIL.ImageMath", "PIL.ImagePath", "PIL.ImageSequence", "PIL.ImageMorph",
68
+ "PIL.ImageTransform", "PIL.ImagePalette", "PIL.ImageShow", "PIL.ImageGrab",
69
+ "PIL.ExifTags", "PIL.TiffTags",
70
+ "PIL.features", "PIL.BdfFontFile", "PIL.FontFile", "PIL.PcfFontFile",
71
+ "PIL.TarIO", "PIL.WalImageFile",
72
+ # plugins
73
+ "PIL.BlpImagePlugin", "PIL.BmpImagePlugin", "PIL.BufrStubImagePlugin",
74
+ "PIL.CurImagePlugin", "PIL.DcxImagePlugin", "PIL.DdsImagePlugin",
75
+ "PIL.EpsImagePlugin", "PIL.FliImagePlugin",
76
+ "PIL.FtexImagePlugin",
77
+ "PIL.GbrImagePlugin", "PIL.GdImageFile", "PIL.GifImagePlugin",
78
+ "PIL.GimpGradientFile", "PIL.GimpPaletteFile", "PIL.GribStubImagePlugin",
79
+ "PIL.Hdf5StubImagePlugin", "PIL.IcnsImagePlugin", "PIL.IcoImagePlugin",
80
+ "PIL.ImImagePlugin", "PIL.ImtImagePlugin", "PIL.IptcImagePlugin",
81
+ "PIL.Jpeg2KImagePlugin", "PIL.JpegImagePlugin", "PIL.JpegPresets",
82
+ "PIL.McIdasImagePlugin", "PIL.MpegImagePlugin",
83
+ "PIL.MpoImagePlugin", "PIL.MspImagePlugin", "PIL.PalmImagePlugin",
84
+ "PIL.PcdImagePlugin", "PIL.PcxImagePlugin", "PIL.PdfImagePlugin",
85
+ "PIL.PdfParser", "PIL.PixarImagePlugin", "PIL.PngImagePlugin",
86
+ "PIL.PpmImagePlugin", "PIL.PsdImagePlugin", "PIL.PSDraw",
87
+ "PIL.QoiImagePlugin", "PIL.SgiImagePlugin", "PIL.SpiderImagePlugin",
88
+ "PIL.SunImagePlugin", "PIL.TgaImagePlugin", "PIL.TiffImagePlugin",
89
+ "PIL.WebPImagePlugin", "PIL.WmfImagePlugin", "PIL.XbmImagePlugin",
90
+ "PIL.XpmImagePlugin", "PIL.XVThumbImagePlugin",
91
+ ]
92
+
93
+ OPTIONAL_MODULES = [
94
+ # these may legitimately fail to import on a headless/android build
95
+ ("PIL.ImageQt", "Qt bindings not installed"),
96
+ ("PIL.ImageTk", "tkinter not available"),
97
+ ("PIL.ImageWin", "Windows-only"),
98
+ ("PIL.MicImagePlugin", "requires olefile (optional dep)"),
99
+ ("PIL.FitsStubImagePlugin", "stub module absent in Pillow 12.3"),
100
+ ("PIL.FpxImagePlugin", "requires olefile (optional dep)"),
101
+ ]
102
+
103
+ FAILED_IMPORTS = []
104
+
105
+
106
+ def test_imports():
107
+ for name in CORE_MODULES:
108
+ try:
109
+ __import__(name)
110
+ except Exception:
111
+ FAILED_IMPORTS.append(name)
112
+ RESULTS.append((KIND_FAIL, "import " + name, traceback.format_exc().strip()))
113
+ for name, why in OPTIONAL_MODULES:
114
+ try:
115
+ __import__(name)
116
+ except Exception:
117
+ RESULTS.append((KIND_SKIP, "import " + name, why))
118
+
119
+
120
+ # --------------------------------------------------------------------------
121
+ # 2. version / features
122
+ # --------------------------------------------------------------------------
123
+
124
+ def test_version_and_features():
125
+ import PIL
126
+ from PIL import features
127
+
128
+ ver = getattr(PIL, "__version__", "?")
129
+ print(f"[info] PIL package: {ver}")
130
+ print(f"[info] PIL.__file__: {PIL.__file__}")
131
+ print(f"[info] PIL.__version__: {ver}")
132
+
133
+ feature_names = features.get_supported()
134
+ print("[info] supported features: " + ", ".join(sorted(feature_names)))
135
+ for feat in ("jpg", "jpg_2000", "zlib", "libtiff", "webp", "webp_mux",
136
+ "libimagequant", "raqm", "freetype2", "littlecms2", "png"):
137
+ try:
138
+ avail = features.check(feat)
139
+ except Exception:
140
+ avail = None
141
+ print(f"[info] feature {feat}: {avail}")
142
+
143
+ # sanity: extension modules were actually imported (native build present)
144
+ from PIL import Image as _Img
145
+ core = _Img.core
146
+ assert hasattr(core, "blend"), "_imaging native module not loaded"
147
+ from PIL import _imagingft, _imagingcms, _webp, _imagingmath, _imagingmorph
148
+ for mod, pyinit in ((_imagingft, "getfont"), (_imagingcms, "createProfile"),
149
+ (_webp, "WebPEncode"), (_imagingmath, "unop"),
150
+ (_imagingmorph, "apply")):
151
+ 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]}"
152
+ RESULTS.append((KIND_OK, "version+features+native modules", ""))
153
+
154
+
155
+ # --------------------------------------------------------------------------
156
+ # helpers
157
+ # --------------------------------------------------------------------------
158
+
159
+ def make_test_image(mode="RGB", size=(64, 48)):
160
+ from PIL import Image
161
+ im = Image.new(mode, size)
162
+ px = im.load()
163
+ w, h = size
164
+ for y in range(h):
165
+ for x in range(w):
166
+ if mode == "L":
167
+ px[x, y] = (x * 3 + y * 5) % 256
168
+ elif mode == "RGB":
169
+ px[x, y] = ((x * 4) % 256, (y * 6) % 256, (x * y) % 256)
170
+ elif mode == "RGBA":
171
+ px[x, y] = ((x * 4) % 256, (y * 6) % 256, (x * y) % 256, (x + y) % 256)
172
+ elif mode == "1":
173
+ px[x, y] = (x + y) % 2
174
+ elif mode == "P":
175
+ px[x, y] = (x + y) % 256
176
+ else:
177
+ px[x, y] = (x * y) % 256
178
+ return im
179
+
180
+
181
+ def roundtrip(name, ext, save_kwargs, load_kwargs=None, mode="RGB", check_size=None):
182
+ from PIL import Image
183
+ im = make_test_image(mode)
184
+ tmpdir = tempfile.mkdtemp(prefix="pillow_test_")
185
+ try:
186
+ path = os.path.join(tmpdir, "img" + ext)
187
+ im.save(path, **save_kwargs)
188
+ with Image.open(path, **(load_kwargs or {})) as out:
189
+ out.load()
190
+ sz = check_size or im.size
191
+ assert out.size == sz, f"size {out.size} != {sz}"
192
+ assert out.format, "format not detected"
193
+ with open(path, "rb") as fh:
194
+ buf = io.BytesIO(fh.read())
195
+ with Image.open(buf) as out2:
196
+ out2.load()
197
+ assert out2.size == sz
198
+ RESULTS.append((KIND_OK, "roundtrip " + ext, ""))
199
+ finally:
200
+ import shutil
201
+ shutil.rmtree(tmpdir, ignore_errors=True)
202
+
203
+
204
+ def roundtrip_assert_approx(im1, im2, tol=6, frac=0.05):
205
+ """Check pixel match within tolerance on a sampled grid."""
206
+ from PIL import Image
207
+ assert im1.size == im2.size, f"size mismatch {im1.size} vs {im2.size}"
208
+ p1 = im1.convert("RGB").load()
209
+ p2 = im2.convert("RGB").load()
210
+ w, h = im1.size
211
+ mism = 0
212
+ total = 0
213
+ for y in range(0, h, max(1, h // 8)):
214
+ for x in range(0, w, max(1, w // 8)):
215
+ total += 1
216
+ c1 = p1[x, y]
217
+ c2 = p2[x, y]
218
+ if max(abs(a - b) for a, b in zip(c1, c2)) > tol:
219
+ mism += 1
220
+ assert mism <= max(1, total * frac), f"{mism}/{total} pixels differ > {tol}"
221
+
222
+
223
+ # --------------------------------------------------------------------------
224
+ # 3. codec round-trips (needs merged libpillow_codecs.so + zlib)
225
+ # --------------------------------------------------------------------------
226
+
227
+ def test_codecs():
228
+ from PIL import features
229
+
230
+ def codec(cond, why):
231
+ if not cond:
232
+ raise SkipTest(why)
233
+
234
+ codec(features.check("zlib") or True, "") # png needs zlib (always present)
235
+
236
+ codec(features.check("jpg"), "no jpeg")
237
+ roundtrip("jpeg", ".jpg", {"quality": 90})
238
+ roundtrip("jpeg-gray", ".jpg", {"quality": 90}, mode="L")
239
+
240
+ codec(features.check("zlib"), "no zlib")
241
+ roundtrip("png", ".png", {})
242
+ roundtrip("png-rgba", ".png", {}, mode="RGBA")
243
+ roundtrip("png-palette", ".png", {}, mode="P")
244
+
245
+ codec(features.check("zlib"), "no zlib")
246
+ roundtrip("gif", ".gif", {}, mode="P")
247
+ roundtrip("tiff-lzw", ".tiff", {"compression": "tiff_lzw"})
248
+ roundtrip("tiff-packbits", ".tiff", {"compression": "packbits"})
249
+
250
+ codec(features.check("libtiff"), "no libtiff")
251
+ roundtrip("tiff-none", ".tiff", {})
252
+
253
+ codec(features.check("webp"), "no webp")
254
+ roundtrip("webp-lossless", ".webp", {"lossless": True})
255
+ roundtrip("webp-lossy", ".webp", {"quality": 80})
256
+
257
+ codec(features.check("jpg_2000"), "no openjpeg")
258
+ roundtrip("jpeg2000", ".jp2", {"irreversible": False})
259
+
260
+ roundtrip("bmp", ".bmp", {})
261
+ roundtrip("ppm", ".ppm", {})
262
+ roundtrip("pcx", ".pcx", {})
263
+ roundtrip("tga", ".tga", {})
264
+ roundtrip("dib", ".dib", {})
265
+ roundtrip("sgi", ".sgi", {}, mode="L")
266
+ roundtrip("xbm", ".xbm", {}, mode="1")
267
+
268
+ # ICO / ICO with embedded PNG
269
+ from PIL import Image as _Image
270
+ ico = make_test_image("RGB").resize((64, 48))
271
+ _tmp_ico = io.BytesIO()
272
+ ico.save(_tmp_ico, format="ICO", sizes=[(16, 16), (32, 32)])
273
+ _tmp_ico.seek(0)
274
+ ico_back = _Image.open(_tmp_ico)
275
+ assert ico_back.size[0] <= 64 and ico_back.size[1] <= 48 and ico_back.size[0] > 0
276
+ assert ico_back.load()[0, 0] is not None
277
+
278
+ RESULTS.append((KIND_OK, "codec round-trips", ""))
279
+
280
+
281
+ # --------------------------------------------------------------------------
282
+ # 4. core image API
283
+ # --------------------------------------------------------------------------
284
+
285
+ def test_core_api():
286
+ from PIL import (Image, ImageFilter, ImageChops, ImageEnhance, ImageStat,
287
+ ImagePalette, ImageOps)
288
+
289
+ im = make_test_image("RGB")
290
+ w, h = im.size
291
+
292
+ # copy / crop / resize / rotate / transpose
293
+ im.copy().crop((0, 0, 10, 10)).load()
294
+ r = im.resize((32, 24))
295
+ assert r.size == (32, 24)
296
+ r = im.resize((16, 12), getattr(Image, "Resampling", object).LANCZOS)
297
+ assert r.size == (16, 12)
298
+ assert im.rotate(90, expand=True).size == (h, w)
299
+ assert im.transpose(Image.Transpose.FLIP_LEFT_RIGHT).size == (w, h)
300
+
301
+ # point / convert / quantize / palette
302
+ g = im.convert("L")
303
+ g.point(lambda v: 255 - v)
304
+ g.convert("1")
305
+ q = im.quantize(colors=16, method=Image.Quantize.MEDIANCUT)
306
+ assert q.mode == "P"
307
+ if hasattr(Image.Quantize, "LIBIMAGEQUANT"):
308
+ try:
309
+ q2 = im.quantize(colors=16, method=Image.Quantize.LIBIMAGEQUANT)
310
+ assert q2.mode == "P"
311
+ except Exception:
312
+ raise SkipTest("LIBIMAGEQUANT quantize failed at runtime")
313
+
314
+ # split / merge
315
+ bands = im.split()
316
+ merged = Image.merge("RGB", bands)
317
+ assert merged.size == im.size
318
+
319
+ # paste / composite / alpha
320
+ im2 = Image.new("RGB", im.size, (255, 0, 0))
321
+ im2.paste(im, (0, 0))
322
+ composite = Image.composite(im, im2, Image.new("L", im.size, 128))
323
+ assert composite.size == im.size
324
+ overlay = make_test_image("RGBA")
325
+ base = Image.new("RGBA", im.size, (0, 0, 0, 255))
326
+ base.alpha_composite(overlay)
327
+ assert base.size == im.size
328
+
329
+ # filters
330
+ for f in (ImageFilter.BLUR, ImageFilter.GaussianBlur(2.0),
331
+ ImageFilter.BoxBlur(2), ImageFilter.SMOOTH,
332
+ ImageFilter.SHARPEN, ImageFilter.EDGE_ENHANCE,
333
+ ImageFilter.FIND_EDGES, ImageFilter.EMBOSS,
334
+ ImageFilter.CONTOUR, ImageFilter.UnsharpMask(radius=2, percent=120),
335
+ ImageFilter.MaxFilter(3), ImageFilter.MedianFilter(3),
336
+ ImageFilter.MinFilter(3), ImageFilter.ModeFilter(3)):
337
+ fimg = im.filter(f)
338
+ assert fimg.size == im.size
339
+ # convolution kernel
340
+ kern = [1 / 9] * 9
341
+ assert im.filter(ImageFilter.Kernel((3, 3), kern)).size == im.size
342
+
343
+ # histogram / stat / getbbox
344
+ im.histogram()
345
+ ImageStat.Stat(im)
346
+ assert im.getbbox() is not None
347
+
348
+ # enhancers / chops
349
+ ImageEnhance.Brightness(im).enhance(1.2)
350
+ ImageEnhance.Contrast(im).enhance(1.2)
351
+ ImageEnhance.Color(im).enhance(1.2)
352
+ ImageEnhance.Sharpness(im).enhance(1.5)
353
+ ImageChops.add(im, im2)
354
+ ImageChops.subtract(im, im2)
355
+ ImageChops.difference(im, im2)
356
+ ImageChops.multiply(im, im2)
357
+ ImageChops.offset(im, 5, 5)
358
+ ImageChops.invert(im)
359
+
360
+ # pixel access, getdata/putdata, getpixel/putpixel
361
+ pa = im.load()
362
+ px0 = pa[0, 0]
363
+ pa[0, 0] = (0, 0, 0)
364
+ pa[0, 0] = px0
365
+ im.putpixel((1, 1), im.getpixel((1, 1)))
366
+ im.getdata()
367
+
368
+ # ops
369
+ ImageOps.invert(g)
370
+ ImageOps.autocontrast(g)
371
+ ImageOps.equalize(g)
372
+ ImageOps.grayscale(im)
373
+ ImageOps.flip(im)
374
+ ImageOps.mirror(im)
375
+ ImageOps.crop(im, border=2)
376
+ ImageOps.scale(im, 0.5)
377
+ ImageOps.fit(im, (20, 20))
378
+ ImageOps.pad(im, (20, 20))
379
+ ImageOps.expand(im, border=2, fill=0)
380
+
381
+ # transform
382
+ im.transform((32, 24), Image.Transform.AFFINE, (1, 0, 0, 0, 1, 0))
383
+ im.transform((32, 24), Image.Transform.QUAD, (0, 0, 0, h, w, h, w, 0))
384
+
385
+ # info / metadata style
386
+ assert im.format is None
387
+ assert im.mode == "RGB"
388
+ assert im.size == (w, h)
389
+ RESULTS.append((KIND_OK, "core image API", ""))
390
+
391
+
392
+ # --------------------------------------------------------------------------
393
+ # 5. ImageDraw + ImageFont (freetype)
394
+ # --------------------------------------------------------------------------
395
+
396
+ def find_font():
397
+ candidates = [
398
+ "/system/fonts/Roboto-Regular.ttf",
399
+ "/system/fonts/Roboto-Medium.ttf",
400
+ "/system/fonts/DroidSans.ttf",
401
+ "/system/fonts/NotoSans-Regular.ttf",
402
+ "/system/fonts/NotoSansCJK-Regular.ttc",
403
+ ]
404
+ import glob
405
+ for c in candidates:
406
+ if os.path.isfile(c):
407
+ return c
408
+ hits = sorted(glob.glob("/system/fonts/*.ttf"))
409
+ return hits[0] if hits else None
410
+
411
+
412
+ def test_draw_font():
413
+ from PIL import Image, ImageDraw, ImageFont
414
+
415
+ font_path = find_font()
416
+ im = Image.new("RGB", (200, 100), "white")
417
+ d = ImageDraw.Draw(im)
418
+
419
+ d.ellipse((10, 10, 50, 50), fill="red", outline="blue")
420
+ d.rectangle((60, 10, 120, 50), fill="green")
421
+ d.line((130, 10, 190, 50), fill="black", width=3)
422
+ d.polygon([(10, 60), (60, 90), (110, 60)], fill="orange")
423
+ d.point((150, 80), fill="purple")
424
+ d.arc((160, 60, 190, 90), start=0, end=180, fill="black")
425
+ d.rounded_rectangle((10, 60, 80, 95), radius=5, fill="cyan")
426
+
427
+ if font_path is None:
428
+ raise SkipTest("no system font available")
429
+
430
+ font = ImageFont.truetype(font_path, 18)
431
+ assert hasattr(font, "getbbox") or hasattr(font, "getsize")
432
+ d.text((10, 10), "Hello PIL", font=font, fill="black")
433
+ d.multiline_text((10, 40), "Line1\nLine2", font=font, fill="black")
434
+ if hasattr(d, "textbbox"):
435
+ d.textbbox((10, 10), "Hello", font=font)
436
+ if hasattr(d, "textlength"):
437
+ d.textlength("Hello", font=font)
438
+ if hasattr(d, "textsize"):
439
+ d.textsize("Hello", font=font)
440
+
441
+ # draw2
442
+ from PIL import ImageDraw2
443
+ im2 = Image.new("RGB", (100, 60), "white")
444
+ d2 = ImageDraw2.Draw(im2)
445
+ d2.polygon([(10, 10), (40, 50), (70, 10)], ImageDraw2.Brush("blue"))
446
+ d2.line([(0, 0), (90, 50)], ImageDraw2.Pen("black", 2))
447
+ RESULTS.append((KIND_OK, "ImageDraw + ImageFont", ""))
448
+
449
+
450
+ # --------------------------------------------------------------------------
451
+ # 6. ImageMath / ImagePath / ImageMorph / ImageSequence
452
+ # --------------------------------------------------------------------------
453
+
454
+ def test_misc_modules():
455
+ from PIL import Image, ImageMath, ImagePath, ImageSequence
456
+
457
+ l1 = make_test_image("L")
458
+ l2 = l1.point(lambda v: v // 2 + 10)
459
+ if hasattr(ImageMath, 'eval'):
460
+ res = ImageMath.eval("a + b", a=l1, b=l2)
461
+ elif hasattr(ImageMath, 'unsafe_eval'):
462
+ res = ImageMath.unsafe_eval("a + b", a=l1, b=l2)
463
+ else:
464
+ raise SkipTest("ImageMath.eval/unsafe_eval not available")
465
+ assert res.size == l1.size
466
+
467
+ p = ImagePath.Path([(0, 0), (10, 0), (10, 10), (0, 10)])
468
+ assert len(p) == 4
469
+ p.transform((1, 0, 1, 0, 1, 1))
470
+
471
+ seq = list(ImageSequence.Iterator(l1))
472
+ assert len(seq) == 1
473
+
474
+ # ImageMorph
475
+ from PIL import ImageMorph
476
+ mm = ImageMorph.MorphOp(op_name="dilation4")
477
+ bc, out = mm.apply(l1.convert("L").point(lambda v: 255 if v > 128 else 0))
478
+ assert out.size == l1.size
479
+ mm2 = ImageMorph.MorphOp(lut=mm.lut)
480
+ _, out2 = mm2.apply(l1.convert("L").point(lambda v: 255 if v > 128 else 0))
481
+ assert out2.size == l1.size
482
+ mm.match(l1.convert("L").point(lambda v: 255 if v > 128 else 0))
483
+ RESULTS.append((KIND_OK, "ImageMath/Path/Morph/Sequence", ""))
484
+
485
+
486
+ # --------------------------------------------------------------------------
487
+ # 7. ImageCms (littlecms2)
488
+ # --------------------------------------------------------------------------
489
+
490
+ def test_cms():
491
+ try:
492
+ from PIL import ImageCms
493
+ except ImportError:
494
+ raise SkipTest("no PIL.ImageCms")
495
+ from PIL import Image
496
+
497
+ sRGB = ImageCms.createProfile("sRGB")
498
+ lab = ImageCms.createProfile("LAB")
499
+ tf = ImageCms.buildTransform(sRGB, lab, "RGB", "LAB")
500
+ im = make_test_image("RGB")
501
+ out = ImageCms.applyTransform(im, tf)
502
+ assert out.size == im.size
503
+ try:
504
+ prof = ImageCms.ImageCmsProfile(sRGB)
505
+ assert prof.profile.profile_id
506
+ except Exception:
507
+ pass
508
+ RESULTS.append((KIND_OK, "ImageCms (littlecms)", ""))
509
+
510
+
511
+ # --------------------------------------------------------------------------
512
+ # 8. numpy interop (optional, only if numpy installed)
513
+ # --------------------------------------------------------------------------
514
+
515
+ def test_numpy():
516
+ try:
517
+ import numpy as np
518
+ except ImportError:
519
+ raise SkipTest("numpy not installed")
520
+ from PIL import Image
521
+
522
+ arr = np.zeros((32, 32, 3), dtype=np.uint8)
523
+ arr[..., 0] = 255
524
+ im = Image.fromarray(arr)
525
+ assert im.size == (32, 32) and im.mode == "RGB"
526
+ back = np.asarray(im)
527
+ assert back.shape == (32, 32, 3)
528
+ assert back[0, 0, 0] == 255
529
+ RESULTS.append((KIND_OK, "numpy interop", ""))
530
+
531
+
532
+ # --------------------------------------------------------------------------
533
+ # 9. pillow-simd specific exercise (resample/boxblur/reduce paths)
534
+ # --------------------------------------------------------------------------
535
+
536
+ def test_simd_paths():
537
+ from PIL import Image, ImageFilter
538
+
539
+ im = make_test_image("RGB", (256, 192))
540
+ Res = getattr(Image, "Resampling", None)
541
+ if Res is None:
542
+ raise SkipTest("Resampling enum missing")
543
+
544
+ for filt in (Res.BOX, Res.HAMMING, Res.BILINEAR, Res.BICUBIC, Res.LANCZOS):
545
+ small = im.resize((48, 36), filt)
546
+ assert small.size == (48, 36)
547
+ up = small.resize((256, 192), filt)
548
+ assert up.size == (256, 192)
549
+
550
+ # reduce via thumbnail
551
+ th = im.copy()
552
+ th.thumbnail((32, 32))
553
+ assert max(th.size) <= 32
554
+
555
+ # box blur
556
+ for n in (1, 2, 3):
557
+ assert im.filter(ImageFilter.BoxBlur(n)).size == im.size
558
+
559
+ # unsharp mask (uses Reduce-like / convolution paths in SIMD builds)
560
+ assert im.filter(ImageFilter.UnsharpMask(radius=3, percent=150)).size == im.size
561
+
562
+ # rank / min / max filters
563
+ for f in (ImageFilter.MinFilter(3), ImageFilter.MaxFilter(3), ImageFilter.MedianFilter(3)):
564
+ assert im.filter(f).size == im.size
565
+
566
+ # ImageOps scale/fit (uses resize internally)
567
+ ImageOps_scale = __import__("PIL.ImageOps", fromlist=["scale"])
568
+ ImageOps_scale.scale(im, 0.5)
569
+ ImageOps_scale.fit(im, (24, 24))
570
+
571
+ # color LUT / point LUT path
572
+ lut = list(range(256)) * 3
573
+ assert im.point(lut).size == im.size
574
+ RESULTS.append((KIND_OK, "SIMD/exercise paths", ""))
575
+
576
+
577
+ # --------------------------------------------------------------------------
578
+ # main
579
+ # --------------------------------------------------------------------------
580
+
581
+ def main():
582
+ print("=" * 64)
583
+ print("Pillow / Pillow-SIMD functional test")
584
+ print("python:", sys.version.split()[0])
585
+ print("platform:", sys.platform)
586
+ print("=" * 64)
587
+
588
+ test_imports()
589
+ check("version+features", test_version_and_features)
590
+ check("codecs", test_codecs)
591
+ check("core API", test_core_api)
592
+ check("draw+font", test_draw_font)
593
+ check("misc modules", test_misc_modules)
594
+ check("ImageCms", test_cms)
595
+ check("numpy interop", test_numpy)
596
+ check("SIMD paths", test_simd_paths)
597
+
598
+ print()
599
+ print("=" * 64)
600
+ ok = sum(1 for k, _, _ in RESULTS if k == KIND_OK)
601
+ fail = sum(1 for k, _, _ in RESULTS if k == KIND_FAIL)
602
+ skip = sum(1 for k, _, _ in RESULTS if k == KIND_SKIP)
603
+ print(f"RESULT: {ok} ok, {fail} failed, {skip} skipped (of {len(RESULTS)} total)")
604
+
605
+ # print any skips
606
+ for k, name, detail in RESULTS:
607
+ if k == KIND_SKIP:
608
+ print(f" [skip] {name}: {detail}")
609
+
610
+ # print failures with traceback
611
+ for k, name, detail in RESULTS:
612
+ if k == KIND_FAIL:
613
+ print()
614
+ print(f"### FAIL: {name}")
615
+ print(detail)
616
+
617
+ # separate top-level import failures
618
+ if FAILED_IMPORTS:
619
+ print()
620
+ print("### FAILED MODULE IMPORTS:")
621
+ for n in FAILED_IMPORTS:
622
+ print(" " + n)
623
+
624
+ print("=" * 64)
625
+ sys.exit(1 if fail else 0)
626
+
627
+
628
+ if __name__ == "__main__":
629
+ main()