PyAPI: add ImBuf.file_type read/write access

Support different file types in the imbuf module, previously it wasn't
possible to use imbuf to save an image as a different file format.

- Add `ImBuf.file_type` read/write property to get/set the image file
  format (e.g. 'PNG', 'JPEG', 'TIFF', ... etc).
- Add `imbuf.file_types` read-only dict mapping type identifiers to
  `ImBufFileType` objects which expose `id` and `file_extensions`.
- Add `imbuf.file_type_from_buffer()` to detect the file type from
  image data in memory.

Ref !155740
This commit is contained in:
Campbell Barton 2026-03-17 02:26:52 +00:00
parent d2fe1d4302
commit 3abf54cf3f
5 changed files with 380 additions and 1 deletions

View file

@ -19,6 +19,16 @@ 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)
@ -115,6 +125,15 @@ class TestImBufProperties(unittest.TestCase):
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
@ -414,6 +433,40 @@ class TestImBufPlanes(unittest.TestCase):
with self.assertRaises(ValueError):
imbuf.new((2, 2), planes=p)
def test_planes_roundtrip(self):
file_type = 'IRIS'
file_ext = imbuf.file_types[file_type].file_extensions[0]
# 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),
}
with tempfile.TemporaryDirectory() as tempdir:
for pixel_planes, pixels in pixel_data.items():
with self.subTest(planes=pixel_planes):
ibuf = imbuf.new((2, 2), planes=pixel_planes)
ibuf.file_type = file_type
with ibuf.with_buffer('BYTE', write=True) as buf:
for i, pixel in enumerate(pixels):
buf[i * 4:(i + 1) * 4] = bytes(rgba_bytes_from_packed_int(pixel))
filepath = os.path.join(tempdir, "test_{:d}{:s}".format(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('BYTE') as buf:
for i, expected in enumerate(pixels):
actual = tuple(buf[i * 4:(i + 1) * 4])
self.assertEqual(actual, rgba_bytes_from_packed_int(expected))
ibuf_loaded.free()
class TestImBufPixelsBytes(unittest.TestCase):
@ -699,6 +752,47 @@ class TestImBufExitSync(unittest.TestCase):
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)
self.assertGreater(len(file_type.file_extensions), 0)
def test_write_and_detect_all_types(self):
# TODO: make this meta-data available.
read_only_types = {'PSD', 'DDS'}
size = (32, 32)
with tempfile.TemporaryDirectory() as tempdir:
for type_id, file_type in imbuf.file_types.items():
if type_id in read_only_types:
continue
ext = file_type.file_extensions[0]
with self.subTest(type_id=type_id):
ibuf = imbuf.new(size)
ibuf.file_type = type_id
filepath = os.path.join(tempdir, "test" + ext)
imbuf.write(ibuf, filepath=filepath)
ibuf.free()
with open(filepath, "rb") as fh:
data = fh.read()
# Detect type from buffer.
detected = imbuf.file_type_from_buffer(data)
self.assertIsNotNone(detected, msg=type_id)
self.assertEqual(detected.id, type_id)
# Load from buffer and verify size and type.
ibuf_loaded = imbuf.load_from_buffer(data)
self.assertEqual(ibuf_loaded.size, size)
self.assertEqual(ibuf_loaded.file_type, type_id)
ibuf_loaded.free()
def main():
import sys
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])