# SPDX-FileCopyrightText: 2025 Blender Authors # # SPDX-License-Identifier: GPL-2.0-or-later # ./blender.bin --background --python tests/python/bl_imbuf_py_api.py -- --verbose __all__ = ( "main", ) import copy import io import os import tempfile import unittest import imbuf from imbuf.types import ImBuf DEFAULT_SIZE = 32, 32 def rgba_bytes_from_packed_int(value): return ( (value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF, ) # Test color in linear float, sRGB byte, and the expected conversions between them. COLOR_FLOAT = (1.0, 0.5, 0.25, 1.0) COLOR_BYTE = (255, 128, 196, 255) # sRGB byte -> linear float. COLOR_BYTE_AS_FLOAT = (1.0, 0.2159, 0.5520, 1.0) # Linear float -> sRGB byte. COLOR_FLOAT_AS_BYTE = (255, 188, 137, 255) # NOTE: an invalid file format & missing files is aborting on the build-bot. # Disable as it makes tests fail, although we could consider demoting this to a warning # since it prevents valid code-paths from being tested. USE_TESTS_THAT_ABORT = False class TestImBufNew(unittest.TestCase): def test_new_basic(self): ibuf = imbuf.new(DEFAULT_SIZE) self.assertIsInstance(ibuf, ImBuf) self.assertEqual(ibuf.size, DEFAULT_SIZE) ibuf.free() def test_new_single_pixel(self): size = (1, 1) ibuf = imbuf.new(size) self.assertEqual(ibuf.size, size) ibuf.free() def test_new_default_properties(self): ibuf = imbuf.new(DEFAULT_SIZE) self.assertEqual(ibuf.planes, 32) self.assertEqual(ibuf.channels, 4) self.assertEqual(ibuf.filepath, "") ibuf.free() def test_new_large(self): size = (4096, 4096) ibuf = imbuf.new(size) self.assertEqual(ibuf.size, size) ibuf.free() def test_new_invalid_size(self): with self.assertRaises(ValueError): imbuf.new((0, 10)) with self.assertRaises(ValueError): imbuf.new((-1, 10)) class TestImBufProperties(unittest.TestCase): def setUp(self): self.ibuf = imbuf.new(DEFAULT_SIZE) def tearDown(self): self.ibuf.free() def test_readonly_properties(self): readonly_attrs = ( ("size", DEFAULT_SIZE, (10, 10)), ("planes", 32, 24), ("channels", 4, 3), ) for attr, expected, new_value in readonly_attrs: self.assertTrue(hasattr(self.ibuf, attr)) self.assertEqual(getattr(self.ibuf, attr), expected) with self.assertRaises(AttributeError): setattr(self.ibuf, attr, new_value) def test_filepath_read_write(self): filepath = "/tmp/test.png" self.ibuf.filepath = filepath self.assertEqual(self.ibuf.filepath, filepath) def test_filepath_read_write_bytes(self): filepath = b"/tmp/test.png" self.ibuf.filepath = filepath self.assertEqual(self.ibuf.filepath, filepath.decode('ascii')) def test_filepath_empty_default(self): self.assertEqual(self.ibuf.filepath, "") def test_filepath_type_error(self): with self.assertRaises(TypeError): self.ibuf.filepath = 123 def test_ppm_read_write(self): ppm = (3780.0, 3780.0) self.ibuf.ppm = ppm self.assertEqual(self.ibuf.ppm, ppm) def test_ppm_invalid(self): with self.assertRaises(ValueError): self.ibuf.ppm = (0.0, 0.0) with self.assertRaises(ValueError): self.ibuf.ppm = (-1.0, 100.0) def test_file_type(self): self.assertEqual(self.ibuf.file_type, 'PNG') self.ibuf.file_type = 'PNG' self.assertEqual(self.ibuf.file_type, 'PNG') with self.assertRaises(ValueError): self.ibuf.file_type = 'INVALID' with self.assertRaises(ValueError): self.ibuf.file_type = "" def test_quality(self): self.assertEqual(self.ibuf.quality, 90) self.ibuf.quality = 50 self.assertEqual(self.ibuf.quality, 50) self.ibuf.quality = -1 self.assertEqual(self.ibuf.quality, 0) self.ibuf.quality = 101 self.assertEqual(self.ibuf.quality, 100) def test_compress(self): self.assertEqual(self.ibuf.compress, 15) self.ibuf.compress = 50 self.assertEqual(self.ibuf.compress, 50) self.ibuf.compress = -1 self.assertEqual(self.ibuf.compress, 0) self.ibuf.compress = 101 self.assertEqual(self.ibuf.compress, 100) class TestImBufResize(unittest.TestCase): def test_resize_basic(self): ibuf = imbuf.new((64, 64)) ibuf.resize(DEFAULT_SIZE) self.assertEqual(ibuf.size, DEFAULT_SIZE) ibuf.free() def test_resize_upscale(self): size = (128, 128) ibuf = imbuf.new(DEFAULT_SIZE) ibuf.resize(size) self.assertEqual(ibuf.size, size) ibuf.free() def test_resize_non_square(self): size = (100, 50) ibuf = imbuf.new(DEFAULT_SIZE) ibuf.resize(size) self.assertEqual(ibuf.size, size) ibuf.free() def test_resize_method_bilinear(self): ibuf = imbuf.new((64, 64)) ibuf.resize(DEFAULT_SIZE, method='BILINEAR') self.assertEqual(ibuf.size, DEFAULT_SIZE) ibuf.free() def test_resize_invalid(self): ibuf = imbuf.new(DEFAULT_SIZE) with self.assertRaises(ValueError): ibuf.resize((0, 64)) with self.assertRaises(ValueError): ibuf.resize((-1, 64)) with self.assertRaises(ValueError): ibuf.resize(DEFAULT_SIZE, method='INVALID') ibuf.free() class TestImBufCrop(unittest.TestCase): def test_crop_basic(self): size = (64, 64) ibuf = imbuf.new(size) crop_max = (DEFAULT_SIZE[0] - 1, DEFAULT_SIZE[1] - 1) ibuf.crop((0, 0), crop_max) self.assertEqual(ibuf.size, DEFAULT_SIZE) ibuf.free() def test_crop_partial(self): crop_min = (10, 20) crop_max = (49, 79) expected_size = (crop_max[0] - crop_min[0] + 1, crop_max[1] - crop_min[1] + 1) ibuf = imbuf.new((100, 100)) ibuf.crop(crop_min, crop_max) self.assertEqual(ibuf.size, expected_size) ibuf.free() def test_crop_single_pixel(self): ibuf = imbuf.new(DEFAULT_SIZE) ibuf.crop((0, 0), (0, 0)) self.assertEqual(ibuf.size, (1, 1)) ibuf.free() def test_crop_full_image(self): ibuf = imbuf.new(DEFAULT_SIZE) crop_max = (DEFAULT_SIZE[0] - 1, DEFAULT_SIZE[1] - 1) ibuf.crop((0, 0), crop_max) self.assertEqual(ibuf.size, DEFAULT_SIZE) ibuf.free() def test_crop_invalid(self): ibuf = imbuf.new(DEFAULT_SIZE) with self.assertRaises(ValueError): ibuf.crop((0, 0), DEFAULT_SIZE) with self.assertRaises(ValueError): ibuf.crop((20, 20), (10, 10)) with self.assertRaises(ValueError): ibuf.crop((-1, 0), (10, 10)) ibuf.free() class TestImBufCopy(unittest.TestCase): def test_copy(self): ibuf = imbuf.new(DEFAULT_SIZE) ibuf_copy = ibuf.copy() self.assertIsInstance(ibuf_copy, ImBuf) self.assertEqual(ibuf_copy.size, DEFAULT_SIZE) # Copy should be independent. ibuf.free() self.assertEqual(ibuf_copy.size, DEFAULT_SIZE) ibuf_copy.free() def test_copy_and_deepcopy(self): ibuf = imbuf.new(DEFAULT_SIZE) ibuf_copy = copy.copy(ibuf) ibuf_deepcopy = copy.deepcopy(ibuf) self.assertEqual(ibuf_copy.size, ibuf.size) self.assertEqual(ibuf_deepcopy.size, ibuf.size) ibuf.free() ibuf_copy.free() ibuf_deepcopy.free() class TestImBufFree(unittest.TestCase): def test_free_then_access(self): ibuf = imbuf.new(DEFAULT_SIZE) ibuf.free() with self.assertRaises(ReferenceError): _ = ibuf.size with self.assertRaises(ReferenceError): ibuf.resize(DEFAULT_SIZE) with self.assertRaises(ReferenceError): ibuf.crop((0, 0), (10, 10)) with self.assertRaises(ReferenceError): ibuf.copy() with self.assertRaises(ReferenceError): imbuf.write_to_buffer(ibuf, io.BytesIO()) with self.assertRaises(ReferenceError): ibuf.filepath = "/tmp/test.png" def test_double_free(self): ibuf = imbuf.new(DEFAULT_SIZE) ibuf.free() # Double free should not crash. ibuf.free() class TestImBufPythonProtocols(unittest.TestCase): def test_repr(self): ibuf = imbuf.new(DEFAULT_SIZE) r = repr(ibuf) self.assertIn("imbuf", r) self.assertIn("size=({:d}, {:d})".format(*DEFAULT_SIZE), r) ibuf.free() def test_repr_after_free(self): ibuf = imbuf.new(DEFAULT_SIZE) ibuf.free() r = repr(ibuf) self.assertIn("address=0x0", r) def test_hash(self): ibuf1 = imbuf.new(DEFAULT_SIZE) ibuf2 = imbuf.new(DEFAULT_SIZE) self.assertIsInstance(hash(ibuf1), int) # Different image buffers should (almost certainly) have different hashes. self.assertNotEqual(hash(ibuf1), hash(ibuf2)) ibuf1.free() ibuf2.free() def test_isinstance(self): ibuf = imbuf.new(DEFAULT_SIZE) self.assertIsInstance(ibuf, ImBuf) self.assertEqual(type(ibuf).__name__, "ImBuf") ibuf.free() class TestImBufIO(unittest.TestCase): def test_write_and_load(self): ibuf = imbuf.new(DEFAULT_SIZE) with tempfile.TemporaryDirectory() as tempdir: filepath = os.path.join(tempdir, "test.png") imbuf.write(ibuf, filepath=filepath) ibuf.free() ibuf_loaded = imbuf.load(filepath) self.assertIsInstance(ibuf_loaded, ImBuf) self.assertEqual(ibuf_loaded.size, DEFAULT_SIZE) ibuf_loaded.free() def test_write_uses_image_filepath(self): ibuf = imbuf.new(DEFAULT_SIZE) with tempfile.TemporaryDirectory() as tempdir: filepath = os.path.join(tempdir, "test.png") ibuf.filepath = filepath imbuf.write(ibuf) ibuf.free() ibuf_loaded = imbuf.load(filepath) self.assertEqual(ibuf_loaded.size, DEFAULT_SIZE) ibuf_loaded.free() def test_load_nonexistent(self): import bpy # The binary path is a file, not a directory, so this sub-path will not exist in practice. filepath = os.path.join(bpy.app.binary_path, "image.png") with self.assertRaises(IOError): imbuf.load(filepath) def test_load_from_buffer(self): ibuf = imbuf.new(DEFAULT_SIZE) with tempfile.TemporaryDirectory() as tempdir: filepath = os.path.join(tempdir, "test.png") imbuf.write(ibuf, filepath=filepath) ibuf.free() with open(filepath, "rb") as f: data = f.read() ibuf_loaded = imbuf.load_from_buffer(data) self.assertIsInstance(ibuf_loaded, ImBuf) self.assertEqual(ibuf_loaded.size, DEFAULT_SIZE) ibuf_loaded.free() def test_load_from_buffer_bytearray(self): ibuf = imbuf.new(DEFAULT_SIZE) with tempfile.TemporaryDirectory() as tempdir: filepath = os.path.join(tempdir, "test.png") imbuf.write(ibuf, filepath=filepath) ibuf.free() with open(filepath, "rb") as f: data = bytearray(f.read()) ibuf_loaded = imbuf.load_from_buffer(data) self.assertEqual(ibuf_loaded.size, DEFAULT_SIZE) ibuf_loaded.free() def test_load_from_buffer_invalid(self): if USE_TESTS_THAT_ABORT: with self.assertRaises(ValueError): imbuf.load_from_buffer(b"not an image") with self.assertRaises(TypeError): imbuf.load_from_buffer(12345) def test_write_to_invalid_path(self): import bpy ibuf = imbuf.new(DEFAULT_SIZE) # The binary path is a file, not a directory, so this sub-path will not exist in practice. filepath = os.path.join(bpy.app.binary_path, "test.png") if USE_TESTS_THAT_ABORT: with self.assertRaises(IOError): imbuf.write(ibuf, filepath=filepath) ibuf.free() def test_write_to_buffer(self): size = (16, 24) ibuf = imbuf.new(size) buf = io.BytesIO() imbuf.write_to_buffer(ibuf, buf) ibuf.free() # Round-trip: the written data should be loadable. ibuf_loaded = imbuf.load_from_buffer(buf.getvalue()) self.assertEqual(ibuf_loaded.size, size) ibuf_loaded.free() def test_write_to_buffer_not_writable(self): ibuf = imbuf.new(DEFAULT_SIZE) with self.assertRaises(AttributeError): imbuf.write_to_buffer(ibuf, object()) ibuf.free() def test_load_sets_filepath(self): ibuf = imbuf.new(DEFAULT_SIZE) with tempfile.TemporaryDirectory() as tempdir: filepath = os.path.join(tempdir, "test.png") imbuf.write(ibuf, filepath=filepath) ibuf.free() ibuf_loaded = imbuf.load(filepath) self.assertEqual(ibuf_loaded.filepath, filepath) ibuf_loaded.free() class TestImBufPlanes(unittest.TestCase): def test_new_planes_values(self): for p in (8, 16, 24, 32): ibuf = imbuf.new((2, 2), planes=p) self.assertEqual(ibuf.planes, p) ibuf.free() def test_new_planes_invalid(self): for p in (0, 4, 12, 64): with self.assertRaises(ValueError): imbuf.new((2, 2), planes=p) def test_planes_roundtrip(self): from collections import namedtuple # Per-plane pixel data (2x2 = 4 pixels, packed as 0xRRGGBBAA). # 8: gray-scale (R=G=B, A ignored on save, becomes 0xFF on load). # 16: gray-scale + alpha (R=G=B, A preserved). # 24: RGB (A ignored on save, becomes 0xFF on load). # 32: RGBA (all channels preserved). pixel_data = { 8: (0x404040FF, 0x808080FF, 0xC0C0C0FF, 0xFFFFFFFF), 16: (0x404040C0, 0x808080FF, 0xC0C0C080, 0xFFFFFF40), 24: (0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xAABBCCFF), 32: (0xFF000080, 0x00FF00C0, 0x0000FF40, 0xAABBCCDD), } # Each format declares the plane modes it natively round-trips, whether it needs the # lossy-codec setup (quality=100 + 64x64 tiled image so each tile lies in a uniform # DCT region; JPEG2000 also needs >= 64x64 for the default 6 resolution levels), and # the max allowed per-channel pixel error. JPEG planes=24 and AVIF planes=24/32 have # irreducible RGB->YCbCr->RGB integer rounding (~1) even at quality=100 (AVIF/HEIC # has no truly lossless mode in our build). FormatTest = namedtuple('FormatTest', ('file_type', 'supported_planes', 'is_lossy', 'tolerance')) formats = ( FormatTest( file_type='IRIS', supported_planes=(8, 16, 24, 32), is_lossy=False, tolerance=0, ), FormatTest( file_type='TGA', supported_planes=(8, 16, 24, 32), is_lossy=False, tolerance=0, ), FormatTest( file_type='PNG', supported_planes=(8, 16, 24, 32), is_lossy=False, tolerance=0, ), FormatTest( file_type='TIFF', supported_planes=(8, 16, 24, 32), is_lossy=False, tolerance=0, ), FormatTest( file_type='JPEG2000', supported_planes=(8, 16, 24, 32), is_lossy=True, tolerance=0, ), FormatTest( file_type='JPEG', supported_planes=(8, 24), is_lossy=True, tolerance=1, ), FormatTest( file_type='BMP', supported_planes=(24, 32), is_lossy=False, tolerance=0, ), FormatTest( file_type='WEBP', supported_planes=(24, 32), is_lossy=True, tolerance=0, ), FormatTest( file_type='AVIF', supported_planes=(24, 32), is_lossy=True, tolerance=1, ), ) with tempfile.TemporaryDirectory() as tempdir: for fmt in formats: if fmt.file_type not in imbuf.file_types: continue file_ext = imbuf.file_types[fmt.file_type].file_extensions[0] # Each of the 4 test pixels is tiled into a `scale x scale` block; verification # samples the tile center, which lies away from chroma-subsampling boundaries. scale = 32 if fmt.is_lossy else 1 quality = 100 if fmt.is_lossy else 90 size = (2 * scale, 2 * scale) for pixel_planes in fmt.supported_planes: pixels = pixel_data[pixel_planes] with self.subTest(file_type=fmt.file_type, planes=pixel_planes): ibuf = imbuf.new(size, planes=pixel_planes) ibuf.file_type = fmt.file_type ibuf.quality = quality with ibuf.with_buffer(write=True) as buf: # Flatten via `cast('B')` to use 1-D slice-assignment # (memoryview sub-views are not implemented). flat = buf.cast('B') for ty in range(2): for tx in range(2): pix_bytes = bytes( rgba_bytes_from_packed_int(pixels[ty * 2 + tx])) for y in range(scale): row = (ty * scale + y) * size[0] for x in range(scale): index = (row + tx * scale + x) * 4 flat[index:index + 4] = pix_bytes filepath = os.path.join( tempdir, "test_{:s}_{:d}{:s}".format(fmt.file_type, pixel_planes, file_ext)) imbuf.write(ibuf, filepath=filepath) ibuf.free() ibuf_loaded = imbuf.load(filepath) self.assertEqual(ibuf_loaded.planes, pixel_planes) with ibuf_loaded.with_buffer() as buf: flat = buf.cast('B') for ty in range(2): for tx in range(2): expected = rgba_bytes_from_packed_int(pixels[ty * 2 + tx]) # Center of each tile (avoids chroma-boundary artifacts). cx = tx * scale + scale // 2 cy = ty * scale + scale // 2 index = (cy * size[0] + cx) * 4 actual = tuple(flat[index:index + 4]) if fmt.tolerance == 0: self.assertEqual(actual, expected) else: for ch_a, ch_e in zip(actual, expected): self.assertLessEqual(abs(ch_a - ch_e), fmt.tolerance) ibuf_loaded.free() class TestImBufPixelsBytes(unittest.TestCase): def test_read_only_by_default(self): ibuf = imbuf.new((2, 2)) with ibuf.with_buffer() as buf: self.assertEqual(buf.readonly, True) with self.assertRaises(TypeError): buf[0, 0, 0] = 0xFF ibuf.free() def test_write_mode(self): ibuf = imbuf.new((2, 2)) with ibuf.with_buffer(write=True) as buf: self.assertEqual(buf.readonly, False) buf[0, 0, 0] = 42 self.assertEqual(buf[0, 0, 0], 42) ibuf.free() def test_buffer_shape_and_size(self): w, h = 8, 6 ibuf = imbuf.new((w, h)) with ibuf.with_buffer() as buf: self.assertEqual(buf.shape, (h, w, 4)) self.assertEqual(buf.nbytes, w * h * 4) ibuf.free() def test_freed_image(self): ibuf = imbuf.new((2, 2)) ibuf.free() with self.assertRaises(ReferenceError): ibuf.with_buffer() def test_exports_block_resize(self): ibuf = imbuf.new((4, 4)) with ibuf.with_buffer() as buf: with self.assertRaises(BufferError): ibuf.resize((2, 2)) # After context exit, resize works. ibuf.resize((2, 2)) self.assertEqual(ibuf.size, (2, 2)) ibuf.free() def test_exports_block_crop(self): ibuf = imbuf.new((4, 4)) with ibuf.with_buffer() as buf: with self.assertRaises(BufferError): ibuf.crop((0, 0), (1, 1)) ibuf.free() def test_exports_block_free(self): ibuf = imbuf.new((4, 4)) with ibuf.with_buffer() as buf: with self.assertRaises(BufferError): ibuf.free() ibuf.free() def test_double_enter_blocked(self): ibuf = imbuf.new((2, 2)) ctx = ibuf.with_buffer() ctx.__enter__() with self.assertRaises(RuntimeError): ctx.__enter__() ctx.__exit__(None, None, None) ibuf.free() def test_reentry_after_exit(self): ibuf = imbuf.new((2, 2)) ctx = ibuf.with_buffer(write=True) buf1 = ctx.__enter__() buf1[0, 0, 0] = 10 ctx.__exit__(None, None, None) buf2 = ctx.__enter__() self.assertEqual(buf2[0, 0, 0], 10) ctx.__exit__(None, None, None) ibuf.free() class TestImBufPixelsRegion(unittest.TestCase): def setUp(self): self.ibuf = imbuf.new((8, 6)) with self.ibuf.with_buffer(write=True) as buf: for y in range(6): for x in range(8): buf[y, x, 0] = (y * 8 + x) % 256 def tearDown(self): self.ibuf.free() def test_full_image_shape(self): with self.ibuf.with_buffer() as buf: self.assertEqual(buf.ndim, 3) self.assertEqual(buf.shape, (6, 8, 4)) self.assertEqual(buf.nbytes, 8 * 6 * 4) def test_region_none_full_shape(self): with self.ibuf.with_buffer(region=None) as buf: self.assertEqual(buf.shape, (6, 8, 4)) def test_sub_region_shape(self): with self.ibuf.with_buffer(region=((2, 1), (6, 4))) as buf: self.assertEqual(buf.ndim, 3) self.assertEqual(buf.shape, (3, 4, 4)) def test_region_pixel_values(self): with self.ibuf.with_buffer(region=((2, 1), (6, 4))) as buf: # Pixel (2, 1) has index 1*8+2 = 10. self.assertEqual(buf[0, 0, 0], 10) # Pixel (2, 2) has index 2*8+2 = 18. self.assertEqual(buf[1, 0, 0], 18) def test_full_image_region_matches_no_region(self): with self.ibuf.with_buffer(region=((0, 0), (8, 6))) as buf: self.assertEqual(buf.shape, (6, 8, 4)) def test_oversized_region_clamped_to_image(self): with self.ibuf.with_buffer(region=((-5, -5), (100, 100))) as buf: self.assertEqual(buf.shape, (6, 8, 4)) def test_zero_area_region(self): # Fully outside image bounds. with self.ibuf.with_buffer(region=((100, 100), (200, 200))) as buf: self.assertEqual(buf.ndim, 3) self.assertEqual(buf.shape[0] * buf.shape[1], 0) # Start == end (zero-size). with self.ibuf.with_buffer(region=((3, 3), (3, 3))) as buf: self.assertEqual(buf.shape[0] * buf.shape[1], 0) def test_inverted_region_sanitized(self): # Inverted region is sanitized (min/max swapped), not treated as empty. with self.ibuf.with_buffer(region=((5, 5), (2, 2))) as buf: self.assertEqual(buf.ndim, 3) self.assertEqual(buf.shape, (3, 3, 4)) def test_region_write_does_not_corrupt_neighbors(self): with self.ibuf.with_buffer(write=True, region=((1, 1), (3, 3))) as buf: for r in range(buf.shape[0]): for c in range(buf.shape[1]): buf[r, c, 0] = 0xFF with self.ibuf.with_buffer() as buf: # Pixel (0, 0) - outside the written region. self.assertEqual(buf[0, 0, 0], 0) # Pixel (1, 1) - inside the written region. self.assertEqual(buf[1, 1, 0], 0xFF) class TestImBufPixelsFloat(unittest.TestCase): def test_float_buffer_access(self): ibuf = imbuf.new((2, 2), buffer_type='FLOAT') with ibuf.with_buffer() as buf: self.assertEqual(buf.format, "f") ibuf.free() def test_float_write(self): ibuf = imbuf.new((2, 2), buffer_type='FLOAT') with ibuf.with_buffer(write=True) as buf: buf[0, 0, 0] = 0.5 self.assertAlmostEqual(buf[0, 0, 0], 0.5, places=5) ibuf.free() class TestImBufBufferTypeConvert(unittest.TestCase): def test_convert_byte_to_float(self): ibuf = imbuf.new((2, 2)) with ibuf.with_buffer(write=True) as buf: for c, v in enumerate(COLOR_BYTE): buf[0, 0, c] = v ibuf.convert_buffer_type('FLOAT') self.assertEqual(ibuf.buffer_type, 'FLOAT') with ibuf.with_buffer() as buf: for i, expected in enumerate(COLOR_BYTE_AS_FLOAT): self.assertAlmostEqual(buf[0, 0, i], expected, places=3) ibuf.free() def test_convert_float_to_byte(self): ibuf = imbuf.new((2, 2)) ibuf.convert_buffer_type('FLOAT') self.assertEqual(ibuf.buffer_type, 'FLOAT') with ibuf.with_buffer(write=True) as buf: for i, v in enumerate(COLOR_FLOAT): buf[0, 0, i] = v ibuf.convert_buffer_type('BYTE') self.assertEqual(ibuf.buffer_type, 'BYTE') with ibuf.with_buffer() as buf: for i, expected in enumerate(COLOR_FLOAT_AS_BYTE): self.assertEqual(buf[0, 0, i], expected) ibuf.free() def test_convert_noop_when_same_type(self): ibuf = imbuf.new((2, 2)) ibuf.convert_buffer_type('BYTE') self.assertEqual(ibuf.buffer_type, 'BYTE') ibuf.free() def test_convert_blocked_while_exported(self): ibuf = imbuf.new((2, 2)) with ibuf.with_buffer() as buf: with self.assertRaises(BufferError): ibuf.convert_buffer_type('FLOAT') ibuf.free() class TestImBufBufferType(unittest.TestCase): def test_buffer_type_default_is_byte(self): ibuf = imbuf.new((2, 2)) self.assertEqual(ibuf.buffer_type, 'BYTE') ibuf.free() def test_buffer_type_float_from_new(self): ibuf = imbuf.new((2, 2), buffer_type='FLOAT') self.assertEqual(ibuf.buffer_type, 'FLOAT') ibuf.free() def test_buffer_type_after_convert(self): ibuf = imbuf.new((2, 2)) self.assertEqual(ibuf.buffer_type, 'BYTE') ibuf.convert_buffer_type('FLOAT') self.assertEqual(ibuf.buffer_type, 'FLOAT') ibuf.convert_buffer_type('BYTE') self.assertEqual(ibuf.buffer_type, 'BYTE') ibuf.free() def test_buffer_type_is_read_only(self): ibuf = imbuf.new((2, 2)) with self.assertRaises(AttributeError): ibuf.buffer_type = 'FLOAT' ibuf.free() def test_new_rejects_bogus_buffer_type(self): with self.assertRaises(ValueError): imbuf.new((2, 2), buffer_type='BOGUS') def test_convert_round_trip_preserves_pixels(self): ibuf = imbuf.new((2, 2)) with ibuf.with_buffer(write=True) as buf: for c, v in enumerate(COLOR_BYTE): buf[0, 0, c] = v ibuf.convert_buffer_type('FLOAT') ibuf.convert_buffer_type('BYTE') with ibuf.with_buffer() as buf: for c, v in enumerate(COLOR_BYTE): # sRGB <-> linear through color management may drift by up to 2. self.assertLessEqual(abs(buf[0, 0, c] - v), 2) ibuf.free() class TestImBufNoExitSync(unittest.TestCase): # A writable `with_buffer` exit must NOT auto-sync to a shadow of the other type. def test_write_does_not_change_buffer_type(self): ibuf = imbuf.new((2, 2)) self.assertEqual(ibuf.buffer_type, 'BYTE') with ibuf.with_buffer(write=True) as buf: for i, v in enumerate(COLOR_BYTE): buf[0, 0, i] = v # Type is still BYTE; no float shadow was synthesized on exit. self.assertEqual(ibuf.buffer_type, 'BYTE') ibuf.convert_buffer_type('FLOAT') with ibuf.with_buffer(write=True) as buf: for i, v in enumerate(COLOR_FLOAT): buf[0, 0, i] = v # After write through the float buffer, type stays FLOAT. self.assertEqual(ibuf.buffer_type, 'FLOAT') ibuf.free() class TestImBufFileTypes(unittest.TestCase): def test_file_types_have_id_and_extensions(self): for type_id, file_type in imbuf.file_types.items(): with self.subTest(type_id=type_id): self.assertEqual(file_type.id, type_id) self.assertIsInstance(file_type.file_extensions, tuple) if type_id != 'NONE': self.assertGreater(len(file_type.file_extensions), 0) def test_none_file_type(self): # NONE is accessible in the file_types dict. self.assertIn('NONE', imbuf.file_types) none_type = imbuf.file_types['NONE'] self.assertEqual(none_type.id, 'NONE') self.assertEqual(none_type.file_extensions, ()) self.assertFalse(none_type.has_write_file) self.assertFalse(none_type.has_write_memory) # Can set file_type to NONE and read it back. ibuf = imbuf.new(DEFAULT_SIZE) ibuf.file_type = 'NONE' self.assertEqual(ibuf.file_type, 'NONE') # Writing as NONE fails for both file and memory. with tempfile.TemporaryDirectory() as tempdir: filepath = os.path.join(tempdir, 'test.bin') with self.assertRaises(ValueError): imbuf.write(ibuf, filepath=filepath) with self.assertRaises(ValueError): imbuf.write_to_buffer(ibuf, io.BytesIO()) ibuf.free() def test_write_and_detect_all_types(self): size = (32, 32) with tempfile.TemporaryDirectory() as tempdir: for type_id, file_type in imbuf.file_types.items(): if not (file_type.has_write_file or file_type.has_write_memory): continue ext = file_type.file_extensions[0] with self.subTest(type_id=type_id): ibuf = imbuf.new(size) ibuf.file_type = type_id image_data = [] if file_type.has_write_file: filepath = os.path.join(tempdir, "test" + ext) imbuf.write(ibuf, filepath=filepath) with open(filepath, "rb") as fh: image_data.append(("file", fh.read())) if file_type.has_write_memory: buf = io.BytesIO() imbuf.write_to_buffer(ibuf, buf) image_data.append(("memory", buf.getvalue())) ibuf.free() for kind, data in image_data: msg = "{:s} ({:s})".format(type_id, kind) detected = imbuf.file_type_from_buffer(data) self.assertIsNotNone(detected, msg=msg) self.assertEqual(detected.id, type_id, msg=msg) ibuf_loaded = imbuf.load_from_buffer(data) self.assertEqual(ibuf_loaded.size, size, msg=msg) self.assertEqual(ibuf_loaded.file_type, type_id, msg=msg) ibuf_loaded.free() def main(): import sys sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []) unittest.main() if __name__ == "__main__": main()