test-kivy-app/kivy_venv/lib/python3.11/site-packages/kivy/tests/test_imageloader.py

486 lines
18 KiB
Python
Raw Normal View History

2024-09-15 12:12:16 +00:00
# You need an image testsuite to run this, for information see:
# kivy/tools/image-testsuite/README.md
import os
import re
import sys
import unittest
from collections import defaultdict
from kivy.core.image import ImageLoader
DEBUG = False
ASSETDIR = 'image-testsuite'
LOADERS = {x.__name__: x for x in ImageLoader.loaders}
if 'ImageLoaderPygame' not in LOADERS:
try:
from kivy.core.image.img_pygame import ImageLoaderPygame
LOADERS['ImageLoaderPygame'] = ImageLoaderPygame
except:
pass
# Kivy image test protocol v0: Pixel values
v0_PIXELS = { # NOTE: 't' is not included here, see match_prediction()
'w': [0xFF, 0xFF, 0xFF], 'x': [0x00, 0x00, 0x00], 'r': [0xFF, 0x00, 0x00],
'g': [0x00, 0xFF, 0x00], 'b': [0x00, 0x00, 0xFF], 'y': [0xFF, 0xFF, 0x00],
'c': [0x00, 0xFF, 0xFF], 'p': [0xFF, 0x00, 0xFF], '0': [0x00, 0x00, 0x00],
'1': [0x11, 0x11, 0x11], '2': [0x22, 0x22, 0x22], '3': [0x33, 0x33, 0x33],
'4': [0x44, 0x44, 0x44], '5': [0x55, 0x55, 0x55], '6': [0x66, 0x66, 0x66],
'7': [0x77, 0x77, 0x77], '8': [0x88, 0x88, 0x88], '9': [0x99, 0x99, 0x99],
'A': [0xAA, 0xAA, 0xAA], 'B': [0xBB, 0xBB, 0xBB], 'C': [0xCC, 0xCC, 0xCC],
'D': [0xDD, 0xDD, 0xDD], 'E': [0xEE, 0xEE, 0xEE], 'F': [0xFF, 0xFF, 0xFF]}
# Kivy image test protocol v0: File name
# width x height _ pattern _ alpha _ fmtinfo _ testname _ encoder . ext
v0_FILE_RE = re.compile(r'^v0_(\d+)x(\d+)_' r'([wxrgbycptA-F0-9]+)_'
r'([0-9a-fA-F]{2})_' r'([a-zA-Z0-9-]+)_'
r'([a-zA-Z0-9-]+)_' r'([a-zA-Z0-9-]+)'
r'\.([a-z]+)$')
def asset(*fn):
return os.path.abspath(os.path.join(os.path.dirname(__file__), *fn))
def has_alpha(fmt):
return fmt in ('rgba', 'bgra', 'argb', 'abgr')
def bytes_per_pixel(fmt):
if fmt in ('rgb', 'bgr'):
return 3
if fmt in ('rgba', 'bgra', 'argb', 'abgr'):
return 4
raise Exception('bytes_per_pixel: unknown format {}'.format(fmt))
def get_pixel_alpha(pix, fmt):
if fmt in ('rgba', 'bgra'):
return pix[3]
elif fmt in ('abgr', 'argb'):
return pix[0]
return 0xFF
# Converts (predicted) rgba pixels to the format claimed by image loader
def rgba_to(pix_in, target_fmt, w, h, pitch=None):
if not isinstance(pix_in, (bytes, bytearray)):
pix_in = bytearray(pix_in)
assert w > 0 and h > 0, "Must specify w and h"
assert len(pix_in) == w * h * 4, "Invalid rgba data {}".format(pix_in)
assert target_fmt in ('rgba', 'bgra', 'argb', 'abgr', 'rgb', 'bgr')
if target_fmt == 'rgba':
return pix_in
pixels = [pix_in[i:i + 4] for i in range(0, len(pix_in), 4)]
if target_fmt == 'bgra':
return b''.join([bytes(p[:3][::-1] + p[3:]) for p in pixels])
elif target_fmt == 'abgr':
return b''.join([bytes(p[3:] + p[:3][::-1]) for p in pixels])
elif target_fmt == 'argb':
return b''.join([bytes(p[3:] + p[:3]) for p in pixels])
# rgb/bgr, default to 4 byte alignment
if pitch is None:
pitch = ((3 * w) + 3) & ~3
# Assume pitch 0 == unaligned
elif pitch == 0:
pitch = 3 * w
out = b''
padding = b'\x00' * (pitch - w * 3)
for row in [pix_in[i:i + w * 4] for i in range(0, len(pix_in), w * 4)]:
pixelrow = [row[i:i + 4] for i in range(0, len(row), 4)]
if target_fmt == 'rgb':
out += b''.join([bytes(p[:3]) for p in pixelrow])
elif target_fmt == 'bgr':
out += b''.join([bytes(p[:3][::-1]) for p in pixelrow])
out += padding
return out
def match_prediction(pixels, fmt, fd, pitch):
assert len(fd['alpha']) == 2
assert len(fd['pattern']) > 0
bpp = bytes_per_pixel(fmt)
rowlen = fd['w'] * bpp
if pitch is None:
pitch = (rowlen + 3) & ~3
elif pitch == 0:
pitch = fd['w'] * bpp
pitchalign = pitch - rowlen
errors = []
fail = errors.append
if len(pixels) != pitch * fd['h']:
fail("Pitch error: pitch {} * {} height != {} pixelbytes"
.format(pitch, fd['h'], len(pixels)))
ptr = 0
pixnum = 0
for char in fd['pattern']:
pix = list(bytearray(pixels[ptr:ptr + bpp]))
# print("PIXNUM {} ptr={} bpp={} : {}".format(pixnum, ptr, bpp, pix))
if len(pix) != bpp:
fail("Want {} bytes per pixel, got {}: {}"
.format(bpp, len(pix), pix))
break
if char == 't':
if get_pixel_alpha(pix, fmt) != 0:
fail("pixel {} nonzero 't' pixel alpha {:02X}: {}".format(
pixnum, get_pixel_alpha(pix, fmt), pix))
else:
srcpix = v0_PIXELS[char] + list(bytearray.fromhex(fd['alpha']))
predict = rgba_to(srcpix, fmt, 1, 1, pitch=0)
predict = list(bytearray(predict))
if not predict or not pix or predict != pix:
fail("pixel {} {} format mismatch: want {} ({}) -- got {}"
.format(pixnum, fmt, predict, char, pix))
if pitchalign and (pixnum + 1) % fd['w'] == 0:
check = list(bytearray(pixels[ptr + bpp:ptr + bpp + pitchalign]))
if check != [0] * pitchalign:
fail("Want {} 0x00 pitch align pixnum={}, pos={} got: {}"
.format(pitchalign, pixnum, ptr + bpp, check))
ptr += pitchalign
ptr += bpp
pixnum += 1
if ptr != len(pixels):
fail("Excess data: pixnum={} ptr={} bytes={}, bpp={} pitchalign={}"
.format(pixnum, ptr, len(pixels), bpp, pitchalign))
return (len(errors) == 0, errors)
class _TestContext(object):
def __init__(self, loadercls):
self.loadercls = loadercls
self._fd = None
self._fn = None
self._ok = 0
self._skip = 0
self._fail = 0
self._stats = defaultdict(dict)
@property
def stats(self):
return self._stats
@property
def results(self):
return (self._ok, self._skip, self._fail, self._stats)
def start(self, fn, fd):
assert not self._fn, "unexpected ctx.start(), already started"
assert isinstance(fd, dict)
self._fn = fn
self._fd = fd
def end(self, fn=None):
assert not fn or self._fn == fn, "unexpected ctx.end(), fn mismatch"
self._fn = None
self._fd = None
def ok(self, info):
assert self._fn, "unexpected ctx.ok(), fn=None"
self._ok += 1
self.dbg('PASS', info)
self._incstat('ok')
self.end(self._fn)
def skip(self, info):
assert self._fn, "unexpected ctx.skip(), fn=None"
self._skip += 1
self.dbg('SKIP', info)
self._incstat('skip')
self.end(self._fn)
def fail(self, info):
assert self._fn, "unexpected ctx.fail(), fn=None"
self._fail += 1
self.dbg('FAIL', info)
self._incstat('fail')
self.end(self._fn)
def dbg(self, msgtype, info):
assert self._fn, "unexpected ctx.dbg(), fn=None"
if DEBUG:
print("{} {} {}: {}"
.format(self.loadercls.__name__, msgtype, self._fn, info))
def _incstat(self, s):
assert self._fd, "unexpected ctx._incstat(), fd=None"
fd = self._fd
def IS(key):
self._stats.setdefault(s, defaultdict(int))[key] += 1
IS('total')
IS('extension:{}'.format(fd['ext']))
IS('encoder:{}'.format(fd['encoder']))
IS('fmtinfo:{}'.format(fd['fmtinfo']))
IS('testname:{}'.format(fd['testname']))
IS('testname+ext:{}+{}'.format(fd['testname'], fd['ext']))
IS('encoder+ext:{}+{}'.format(fd['encoder'], fd['ext']))
IS('encoder+testname:{}+{}'.format(fd['encoder'], fd['testname']))
IS('fmtinfo+ext:{}+{}'.format(fd['fmtinfo'], fd['ext']))
@unittest.skipIf(not os.path.isdir(asset(ASSETDIR)),
"Need 'make image-testsuite' to run test")
class ImageLoaderTestCase(unittest.TestCase):
def setUp(self):
self._context = None
self._prepare_images()
def tearDown(self):
if not DEBUG or not self._context:
return
ctx = self._context
il = ctx.loadercls.__name__
stats = ctx.stats
keys = set([k for x in stats.values() for k in x.keys()])
sg = stats.get
for k in sorted(keys):
ok, skip, fail = sg('ok', {}), sg('skip', {}), sg('fail', {})
print("REPORT {} {}: ok={}, skip={}, fail={}".format(
il, k, ok.get(k, 0), skip.get(k, 0), fail.get(k, 0)))
def _prepare_images(self):
if hasattr(self, '_image_files'):
return
self._image_files = {}
for filename in os.listdir(asset(ASSETDIR)):
matches = v0_FILE_RE.match(filename)
if not matches:
continue
w, h, pat, alpha, fmtinfo, tst, encoder, ext = matches.groups()
self._image_files[filename] = {
'filename': filename,
'w': int(w),
'h': int(h),
'pattern': pat,
'alpha': alpha,
'fmtinfo': fmtinfo,
'testname': tst,
'encoder': encoder,
'ext': ext,
'require_alpha': 'BINARY' in tst or 'ALPHA' in tst,
}
def _test_imageloader(self, loadercls, extensions=None):
if not loadercls:
return
if not extensions:
extensions = loadercls.extensions()
ctx = _TestContext(loadercls)
self._context = ctx
for filename in sorted(self._image_files.keys()):
filedata = self._image_files[filename]
if filedata['ext'] not in extensions:
continue
try:
ctx.start(filename, filedata)
result = loadercls(asset(ASSETDIR, filename), keep_data=True)
if not result:
raise Exception('invalid result')
except:
ctx.skip('Error loading file, result=None')
continue
self._test_image(filedata, ctx, loadercls, result)
ctx.end()
ok, skip, fail, stats = ctx.results
if fail:
self.fail('{}: {} passed, {} skipped, {} failed'
.format(loadercls.__name__, ok, skip, fail))
return ctx
def _test_image(self, fd, ctx, loadercls, imgdata):
w, h, pixels, pitch = imgdata._data[0].get_mipmap(0)
fmt = imgdata._data[0].fmt
# required for FFPy memview
# FIXME: bytearray() for py2 compat, I can't be bothered to research
if not isinstance(pixels, bytes):
pixels = bytearray(pixels)
def debug():
if not DEBUG:
return
print(" format: {}x{} {}".format(w, h, fmt))
print(" pitch: got {}, want {}".format(pitch, want_pitch))
print(" want: {} in {}".format(fd['pattern'], fmt))
print(" got: {}".format(bytearray(pixels)))
# Assume pitch 0 = unaligned
want_pitch = (pitch == 0) and bytes_per_pixel(fmt) * w or pitch
if pitch == 0 and bytes_per_pixel(fmt) * w * h != len(pixels):
ctx.dbg("PITCH", "pitch=0, expected fmt={} to be "
"unaligned @ ({}bpp) = {} bytes, got {}"
.format(fmt, bytes_per_pixel(fmt),
bytes_per_pixel(fmt) * w * h,
len(pixels)))
elif pitch and want_pitch != pitch:
ctx.dbg("PITCH", "fmt={}, pitch={}, expected {}"
.format(fmt, pitch, want_pitch))
success, msgs = match_prediction(pixels, fmt, fd, pitch)
if not success:
if not msgs:
ctx.fail("Unknown error")
elif len(msgs) == 1:
ctx.fail(msgs[0])
else:
for m in msgs:
ctx.dbg('PREDICT', m)
ctx.fail('{} errors, see debug output: {}'
.format(len(msgs), msgs[-1]))
debug()
elif fd['require_alpha'] and not has_alpha(fmt):
ctx.fail('Missing expected alpha channel')
debug()
elif fd['w'] != w:
ctx.fail('Width mismatch, want {} got {}'
.format(fd['w'], w))
debug()
elif fd['h'] != h:
ctx.fail('Height mismatch, want {} got {}'
.format(fd['h'], h))
debug()
elif w != 1 and h != 1:
ctx.fail('v0 test protocol mandates w=1 or h=1')
debug()
else:
ctx.ok("Passed test as {}x{} {}".format(w, h, fmt))
sys.stdout.flush()
def test_ImageLoaderSDL2(self):
loadercls = LOADERS.get('ImageLoaderSDL2')
# GIF format not listed as supported in sdl2 loader
if loadercls:
exts = list(loadercls.extensions()) + ['gif']
ctx = self._test_imageloader(loadercls, exts)
def test_ImageLoaderPIL(self):
loadercls = LOADERS.get('ImageLoaderPIL')
ctx = self._test_imageloader(loadercls)
def test_ImageLoaderPygame(self):
loadercls = LOADERS.get('ImageLoaderPygame')
ctx = self._test_imageloader(loadercls)
def test_ImageLoaderFFPy(self):
loadercls = LOADERS.get('ImageLoaderFFPy')
ctx = self._test_imageloader(loadercls)
def test_ImageLoaderGIF(self):
loadercls = LOADERS.get('ImageLoaderGIF')
ctx = self._test_imageloader(loadercls)
def test_ImageLoaderDDS(self):
loadercls = LOADERS.get('ImageLoaderDDS')
ctx = self._test_imageloader(loadercls)
def test_ImageLoaderTex(self):
loadercls = LOADERS.get('ImageLoaderTex')
ctx = self._test_imageloader(loadercls)
def test_ImageLoaderImageIO(self):
loadercls = LOADERS.get('ImageLoaderImageIO')
ctx = self._test_imageloader(loadercls)
def test_missing_tests(self):
for loader in ImageLoader.loaders:
key = 'test_{}'.format(loader.__name__)
msg = "Missing ImageLoader test case: {}".format(key)
self.assertTrue(hasattr(self, key), msg)
self.assertTrue(callable(getattr(self, key)), msg)
class ConverterTestCase(unittest.TestCase):
def test_internal_converter_2x1(self):
correct = {
'rgba': b'\x01\x02\x03\xA1\x04\x05\x06\xA2',
'abgr': b'\xA1\x03\x02\x01\xA2\x06\x05\x04',
'bgra': b'\x03\x02\x01\xA1\x06\x05\x04\xA2',
'argb': b'\xA1\x01\x02\x03\xA2\x04\x05\x06',
'rgb': b'\x01\x02\x03\x04\x05\x06',
'bgr': b'\x03\x02\x01\x06\x05\x04',
'rgb_align4': b'\x01\x02\x03\x04\x05\x06\x00\x00',
'bgr_align4': b'\x03\x02\x01\x06\x05\x04\x00\x00'}
src = correct.get
rgba = src('rgba')
self.assertEqual(rgba_to(rgba, 'rgba', 2, 1, 0), src('rgba'))
self.assertEqual(rgba_to(rgba, 'abgr', 2, 1, 0), src('abgr'))
self.assertEqual(rgba_to(rgba, 'bgra', 2, 1, 0), src('bgra'))
self.assertEqual(rgba_to(rgba, 'argb', 2, 1, 0), src('argb'))
self.assertEqual(rgba_to(rgba, 'rgb', 2, 1, 0), src('rgb'))
self.assertEqual(rgba_to(rgba, 'bgr', 2, 1, 0), src('bgr'))
self.assertEqual(rgba_to(rgba, 'rgb', 2, 1, None), src('rgb_align4'))
self.assertEqual(rgba_to(rgba, 'bgr', 2, 1, None), src('bgr_align4'))
def test_internal_converter_3x1(self):
pad6 = b'\x00' * 6
correct = {
'rgba': b'\x01\x02\x03\xFF\x04\x05\x06\xFF\x07\x08\x09\xFF',
'abgr': b'\xFF\x03\x02\x01\xFF\x06\x05\x04\xFF\x09\x08\x07',
'bgra': b'\x03\x02\x01\xFF\x06\x05\x04\xFF\x09\x08\x07\xFF',
'argb': b'\xFF\x01\x02\x03\xFF\x04\x05\x06\xFF\x07\x08\x09',
'rgb_align2': b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x00',
'bgr_align2': b'\x03\x02\x01\x06\x05\x04\x09\x08\x07\x00',
'rgb_align8': b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x00' + pad6,
'bgr_align8': b'\x03\x02\x01\x06\x05\x04\x09\x08\x07\x00' + pad6}
src = correct.get
rgba = src('rgba')
self.assertEqual(rgba_to(rgba, 'bgra', 3, 1, 0), src('bgra'))
self.assertEqual(rgba_to(rgba, 'argb', 3, 1, 0), src('argb'))
self.assertEqual(rgba_to(rgba, 'abgr', 3, 1, 0), src('abgr'))
self.assertEqual(rgba_to(rgba, 'rgb', 3, 1, 10), src('rgb_align2'))
self.assertEqual(rgba_to(rgba, 'bgr', 3, 1, 10), src('bgr_align2'))
self.assertEqual(rgba_to(rgba, 'rgb', 3, 1, 16), src('rgb_align8'))
self.assertEqual(rgba_to(rgba, 'bgr', 3, 1, 16), src('bgr_align8'))
def test_internal_converter_1x3(self):
pad5 = b'\x00' * 5
correct = {
'rgba': b'\x01\x02\x03\xFF\x04\x05\x06\xFF\x07\x08\x09\xFF',
'rgb_raw': b'\x01\x02\x03\x04\x05\x06\x07\x08\x09',
'bgr_raw': b'\x03\x02\x01\x06\x05\x04\x09\x08\x07',
'rgb_align2': b'\x01\x02\x03\x00\x04\x05\x06\x00\x07\x08\x09\x00',
'bgr_align2': b'\x03\x02\x01\x00\x06\x05\x04\x00\x09\x08\x07\x00',
'rgb_align4': b'\x01\x02\x03\x00\x04\x05\x06\x00\x07\x08\x09\x00',
'bgr_align4': b'\x03\x02\x01\x00\x06\x05\x04\x00\x09\x08\x07\x00',
'rgb_align8': (b'\x01\x02\x03' + pad5 +
b'\x04\x05\x06' + pad5 +
b'\x07\x08\x09' + pad5),
'bgr_align8': (b'\x03\x02\x01' + pad5 +
b'\x06\x05\x04' + pad5 +
b'\x09\x08\x07' + pad5),
}
src = correct.get
rgba = src('rgba')
self.assertEqual(rgba_to(rgba, 'rgb', 1, 3, 4), src('rgb_align2'))
self.assertEqual(rgba_to(rgba, 'bgr', 1, 3, 4), src('bgr_align2'))
self.assertEqual(rgba_to(rgba, 'rgb', 1, 3, None), src('rgb_align4'))
self.assertEqual(rgba_to(rgba, 'bgr', 1, 3, None), src('bgr_align4'))
self.assertEqual(rgba_to(rgba, 'rgb', 1, 3, 0), src('rgb_raw'))
self.assertEqual(rgba_to(rgba, 'bgr', 1, 3, 0), src('bgr_raw'))
self.assertEqual(rgba_to(rgba, 'rgb', 1, 3, 8), src('rgb_align8'))
self.assertEqual(rgba_to(rgba, 'bgr', 1, 3, 8), src('bgr_align8'))
if __name__ == '__main__':
accept_filter = ['ImageLoader{}'.format(x) for x in sys.argv[1:]]
if accept_filter:
LOADERS = {x: LOADERS[x] for x in accept_filter}
DEBUG = True
unittest.main(argv=sys.argv[:1])