diff --git a/src/gsi/gsi/gsiDeclTlPixelBuffer.cc b/src/gsi/gsi/gsiDeclTlPixelBuffer.cc index c78cd1b1d..56b45a655 100644 --- a/src/gsi/gsi/gsiDeclTlPixelBuffer.cc +++ b/src/gsi/gsi/gsiDeclTlPixelBuffer.cc @@ -126,6 +126,40 @@ static std::vector 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 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 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 &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 decl_PixelBuffer ("lay", "PixelBuffer", gsi::constructor ("new", &create_pixel_buffer, gsi::arg ("width"), gsi::arg ("height"), @@ -188,6 +222,24 @@ Class 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" diff --git a/testdata/python/layPixelBuffer.py b/testdata/python/layPixelBuffer.py index 8a91ea189..f04ebfde1 100644 --- a/testdata/python/layPixelBuffer.py +++ b/testdata/python/layPixelBuffer.py @@ -57,6 +57,31 @@ def test_1(self): 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__': diff --git a/testdata/ruby/layPixelBuffer.rb b/testdata/ruby/layPixelBuffer.rb index abcb3254b..d6054c453 100644 --- a/testdata/ruby/layPixelBuffer.rb +++ b/testdata/ruby/layPixelBuffer.rb @@ -152,6 +152,41 @@ def test_4 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