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 #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", Class<tl::PixelBuffer> decl_PixelBuffer ("lay", "PixelBuffer",
gsi::constructor ("new", &create_pixel_buffer, gsi::arg ("width"), gsi::arg ("height"), gsi::constructor ("new", &create_pixel_buffer, gsi::arg ("width"), gsi::arg ("height"),
@ -188,6 +222,24 @@ Class<tl::PixelBuffer> decl_PixelBuffer ("lay", "PixelBuffer",
"\n" "\n"
"This method may not be available if PNG support is not compiled into KLayout." "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"), gsi::method ("patch", &tl::PixelBuffer::patch, gsi::arg ("other"),
"@brief Patches another pixel buffer into this one\n" "@brief Patches another pixel buffer into this one\n"
"\n" "\n"

View File

@ -57,6 +57,31 @@ class LAYPixelBufferTests(unittest.TestCase):
pb_copy = pya.PixelBuffer.read_png(tmp) pb_copy = pya.PixelBuffer.read_png(tmp)
self.assertEqual(compare(pb, pb_copy), True) 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 # run unit tests
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -152,6 +152,41 @@ class LAYPixelBuffer_TestClass < TestBase
end 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 def test_11
pb = RBA::BitmapBuffer::new pb = RBA::BitmapBuffer::new