Merge pull request #2387 from querielo/kirill/pixelbuffer-to-bytes

Add PixelBuffer#to_bytes for zero-overhead pixel access
This commit is contained in:
Matthias Köfferlein 2026-07-16 22:58:35 +02:00 committed by GitHub
commit 3c6b342448
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 112 additions and 0 deletions

View File

@ -126,6 +126,40 @@ static std::vector<char> pixel_buffer_to_png (const tl::PixelBuffer *pb)
#endif
}
// Returns an 8-byte header (width, height as uint32) followed by the raw ARGB32 pixel data
static std::vector<char> pixel_buffer_to_bytes (const tl::PixelBuffer *pb)
{
uint32_t w = pb->width (), h = pb->height ();
size_t n = (size_t) w * (size_t) h * 4;
std::vector<char> data (8 + n);
memcpy (data.data (), &w, 4);
memcpy (data.data () + 4, &h, 4);
memcpy (data.data () + 8, pb->data (), n);
return data;
}
// Inverse of to_bytes: reads width/height from the header and checks the length
static tl::PixelBuffer pixel_buffer_from_bytes (const std::vector<char> &data)
{
uint32_t w = 0, h = 0;
if (data.size () >= 8) {
memcpy (&w, data.data (), 4);
memcpy (&h, data.data () + 4, 4);
}
// sanity guard against malformed headers: reader caps dimensions at 64k x 64k
// (generous for finely-resolved drawings), writer stays full 32-bit
const uint32_t max_dim = 65536;
if (w > max_dim || h > max_dim) {
throw tl::Exception (tl::to_string (tr ("Invalid PixelBuffer byte stream: width or height exceeds the 65536 x 65536 limit")));
}
if (data.size () != 8 + (size_t) w * (size_t) h * 4) {
throw tl::Exception (tl::to_string (tr ("Invalid PixelBuffer byte stream: length does not match the width and height in the header")));
}
tl::PixelBuffer pb (w, h, (const tl::color_t *) (data.data () + 8));
pb.set_transparent (true);
return pb;
}
Class<tl::PixelBuffer> decl_PixelBuffer ("lay", "PixelBuffer",
gsi::constructor ("new", &create_pixel_buffer, gsi::arg ("width"), gsi::arg ("height"),
@ -188,6 +222,24 @@ Class<tl::PixelBuffer> decl_PixelBuffer ("lay", "PixelBuffer",
"\n"
"This method may not be available if PNG support is not compiled into KLayout."
) +
gsi::method_ext ("to_bytes", &pixel_buffer_to_bytes,
"@brief Converts the pixel buffer to a raw byte stream\n"
"\n"
"The stream starts with an 8-byte header (width and height as 32-bit unsigned integers) "
"followed by the raw ARGB32 pixel data with 4 bytes per pixel in row-major order, "
"top to bottom. Unlike \\to_png_data this method has zero encoding overhead. "
"Use \\from_bytes to reconstruct the pixel buffer.\n"
"\n"
"This method has been added in version 0.30.10."
) +
gsi::method ("from_bytes", &pixel_buffer_from_bytes, gsi::arg ("data"),
"@brief Reconstructs a pixel buffer from a byte stream produced by \\to_bytes\n"
"\n"
"The width and height are taken from the header and the stream length is checked against them. "
"The dimensions are capped at 65536 x 65536; a header exceeding this limit raises an error.\n"
"\n"
"This method has been added in version 0.30.10."
) +
gsi::method ("patch", &tl::PixelBuffer::patch, gsi::arg ("other"),
"@brief Patches another pixel buffer into this one\n"
"\n"

View File

@ -57,6 +57,31 @@ class LAYPixelBufferTests(unittest.TestCase):
pb_copy = pya.PixelBuffer.read_png(tmp)
self.assertEqual(compare(pb, pb_copy), True)
def test_2(self):
# to_bytes / from_bytes round-trip
import struct
pb = pya.PixelBuffer(10, 20)
pb.transparent = True
pb.fill(0xf0010203)
pb.set_pixel(1, 2, 0x80102030)
data = bytes(pb.to_bytes())
self.assertEqual(len(data), 8 + 10 * 20 * 4)
self.assertEqual(struct.unpack("=II", data[0:8]), (10, 20)) # width, height header
self.assertEqual(pb == pya.PixelBuffer.from_bytes(data), True)
# a mismatched stream is rejected
with self.assertRaises(Exception):
pya.PixelBuffer.from_bytes(data[:-4])
# a header beyond the 64k x 64k limit is rejected
with self.assertRaises(Exception):
pya.PixelBuffer.from_bytes(struct.pack("=II", 65537, 1))
# run unit tests
if __name__ == '__main__':

View File

@ -152,6 +152,41 @@ class LAYPixelBuffer_TestClass < TestBase
end
def test_5
# to_bytes / from_bytes round-trip
pb = RBA::PixelBuffer::new(10, 20)
pb.transparent = true
pb.fill(0xf0010203)
pb.set_pixel(1, 2, 0x80102030)
bytes = pb.to_bytes
assert_equal(bytes.size, 8 + 10 * 20 * 4)
assert_equal(bytes[0, 8].unpack("VV"), [ 10, 20 ]) # width, height header
assert_equal(pb == RBA::PixelBuffer.from_bytes(bytes), true)
# a mismatched stream is rejected
error = false
begin
RBA::PixelBuffer.from_bytes(bytes[0, bytes.size - 4])
rescue
error = true
end
assert_equal(error, true)
# a header beyond the 64k x 64k limit is rejected
error = false
begin
RBA::PixelBuffer.from_bytes([ 65537, 1 ].pack("VV"))
rescue
error = true
end
assert_equal(error, true)
end
def test_11
pb = RBA::BitmapBuffer::new