diff --git a/src/gsi/gsi/gsiDeclTlPixelBuffer.cc b/src/gsi/gsi/gsiDeclTlPixelBuffer.cc index a0fae9f7a..a02a98eeb 100644 --- a/src/gsi/gsi/gsiDeclTlPixelBuffer.cc +++ b/src/gsi/gsi/gsiDeclTlPixelBuffer.cc @@ -126,12 +126,32 @@ static std::vector pixel_buffer_to_png (const tl::PixelBuffer *pb) #endif } -// Returns the raw ARGB32 pixel data as a byte vector (no encoding overhead) +// 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) { - const char *p = (const char *) pb->data (); - size_t n = (size_t) pb->width () * (size_t) pb->height () * 4; - return std::vector (p, p + n); + 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); + } + 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; } @@ -199,10 +219,19 @@ Class decl_PixelBuffer ("lay", "PixelBuffer", gsi::method_ext ("to_bytes", &pixel_buffer_to_bytes, "@brief Converts the pixel buffer to a raw byte stream\n" "\n" - "Returns 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.\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.9." + "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.\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" diff --git a/testdata/python/layPixelBuffer.py b/testdata/python/layPixelBuffer.py index 8a91ea189..77e24183d 100644 --- a/testdata/python/layPixelBuffer.py +++ b/testdata/python/layPixelBuffer.py @@ -57,6 +57,27 @@ 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]) + # run unit tests if __name__ == '__main__': diff --git a/testdata/ruby/layPixelBuffer.rb b/testdata/ruby/layPixelBuffer.rb index abcb3254b..e8c0db63f 100644 --- a/testdata/ruby/layPixelBuffer.rb +++ b/testdata/ruby/layPixelBuffer.rb @@ -152,6 +152,32 @@ 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) + + end + def test_11 pb = RBA::BitmapBuffer::new