/* * Test for HDR PNG encoding (16-bit + cICP) in ImageLib */ // PNG signature + IHDR chunk layout: // Bytes 0-7: PNG signature // Bytes 8-11: IHDR chunk length (always 13) // Bytes 12-15: "IHDR" // Bytes 16-19: width (4 bytes big-endian) // Bytes 20-23: height (4 bytes big-endian) // Byte 24: bit depth // Byte 25: color type const PNG_SIG = [137, 80, 78, 71, 13, 10, 26, 10]; const WIDTH = 5; const HEIGHT = 4; function strideForFormat(format) { if (format == Ci.imgIEncoder.INPUT_FORMAT_R10G10B10A2) { return WIDTH * 4; } return WIDTH * 8; } function getPngBytes(encoder) { var rawStream = encoder.QueryInterface(Ci.nsIInputStream); var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(); stream.QueryInterface(Ci.nsIBinaryInputStream); stream.setInputStream(rawStream); return stream.readByteArray(stream.available()); } function verifyPngSignature(bytes) { for (var i = 0; i < PNG_SIG.length; i++) { Assert.equal(bytes[i], PNG_SIG[i], "PNG signature byte " + i); } } function getPngBitDepth(bytes) { return bytes[24]; } function getPngColorType(bytes) { return bytes[25]; } // Find a chunk by its 4-byte type name. Returns the offset of the chunk data, // or -1 if not found. function findPngChunk(bytes, name) { var offset = 8; // skip PNG signature while (offset < bytes.length) { var len = (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]; var type = String.fromCharCode( bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7] ); if (type == name) { return offset + 8; // start of chunk data } // skip: 4 (length) + 4 (type) + len (data) + 4 (CRC) offset += 12 + len; } return -1; } // 5x4 test image with 10 distinct colors and mixed alpha: // Row 0 (opaque): red, green, blue, orange, purple // Row 1 (opaque): white, gray, cyan, yellow, lime // Row 2 (alpha ~1/3): red, green, blue, orange, purple // Row 3 (alpha ~2/3): white, gray, cyan, yellow, lime // RGB triples as fractions, 5 per row. var COLORS_ROW0 = [ [1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 0.5, 0], [0.5, 0, 0.5], ]; var COLORS_ROW1 = [ [1, 1, 1], [0.5, 0.5, 0.5], [0, 1, 1], [1, 1, 0], [0.5, 1, 0], ]; function setPixelU16(pixels, index, r, g, b, a) { pixels[index * 4 + 0] = r; pixels[index * 4 + 1] = g; pixels[index * 4 + 2] = b; pixels[index * 4 + 3] = a; } function fillU16Row(pixels, rowStart, colors, alpha, max) { for (var i = 0; i < colors.length; i++) { setPixelU16( pixels, rowStart + i, Math.round(colors[i][0] * max), Math.round(colors[i][1] * max), Math.round(colors[i][2] * max), alpha ); } } function makeU16TestPixels() { var pixels = new Uint16Array(WIDTH * HEIGHT * 4); fillU16Row(pixels, 0, COLORS_ROW0, 65535, 65535); fillU16Row(pixels, 5, COLORS_ROW1, 65535, 65535); fillU16Row(pixels, 10, COLORS_ROW0, 21845, 65535); // alpha ~1/3 -> 85 in 8-bit fillU16Row(pixels, 15, COLORS_ROW1, 43690, 65535); // alpha ~2/3 -> 170 in 8-bit return new Uint8Array(pixels.buffer); } function makeU10TestPixels() { var pixels = new Uint16Array(WIDTH * HEIGHT * 4); fillU16Row(pixels, 0, COLORS_ROW0, 1023, 1023); fillU16Row(pixels, 5, COLORS_ROW1, 1023, 1023); fillU16Row(pixels, 10, COLORS_ROW0, 341, 1023); // 341/1023 ~1/3 fillU16Row(pixels, 15, COLORS_ROW1, 682, 1023); // 682/1023 ~2/3 return new Uint8Array(pixels.buffer); } function makeU12TestPixels() { var pixels = new Uint16Array(WIDTH * HEIGHT * 4); fillU16Row(pixels, 0, COLORS_ROW0, 4095, 4095); fillU16Row(pixels, 5, COLORS_ROW1, 4095, 4095); fillU16Row(pixels, 10, COLORS_ROW0, 1365, 4095); // 1365/4095 ~1/3 fillU16Row(pixels, 15, COLORS_ROW1, 2730, 4095); // 2730/4095 ~2/3 return new Uint8Array(pixels.buffer); } // Bit layout per uint32: 0bAARRRRRRRRRRGGGGGGGGGGBBBBBBBBBB function packR10G10B10A2(r, g, b, a) { return ( (b & 0x3ff) | ((g & 0x3ff) << 10) | ((r & 0x3ff) << 20) | ((a & 0x3) << 30) ); } function fillR10G10B10A2Row(pixels, rowStart, colors, alpha) { for (var i = 0; i < colors.length; i++) { pixels[rowStart + i] = packR10G10B10A2( Math.round(colors[i][0] * 1023), Math.round(colors[i][1] * 1023), Math.round(colors[i][2] * 1023), alpha ); } } function makeR10G10B10A2TestPixels() { var pixels = new Uint32Array(WIDTH * HEIGHT); fillR10G10B10A2Row(pixels, 0, COLORS_ROW0, 3); fillR10G10B10A2Row(pixels, 5, COLORS_ROW1, 3); fillR10G10B10A2Row(pixels, 10, COLORS_ROW0, 1); // 1/3 fillR10G10B10A2Row(pixels, 15, COLORS_ROW1, 2); // 2/3 return new Uint8Array(pixels.buffer); } // float16 constants: 0=0x0000, 0.5=0x3800, 1.0=0x3C00 // Use a lookup table to avoid float equality comparisons. var F16_VALUES = { 0: 0x0000, 5: 0x3800, // 0.5 10: 0x3c00, // 1.0 }; // ~1/3 and ~2/3 as float16 bit patterns var F16_ALPHA_THIRD = 0x3555; var F16_ALPHA_TWOTHIRDS = 0x3955; function fracToF16(frac10) { var val = F16_VALUES[frac10]; if (val !== undefined) { return val; } throw new Error("unexpected fraction " + frac10); } function fillF16Row(pixels, rowStart, colors, alphaF16) { for (var i = 0; i < colors.length; i++) { var idx = (rowStart + i) * 4; // Colors use fractions 0, 0.5, 1.0 encoded as integers 0, 5, 10 // to avoid float equality issues. pixels[idx + 0] = fracToF16(Math.round(colors[i][0] * 10)); pixels[idx + 1] = fracToF16(Math.round(colors[i][1] * 10)); pixels[idx + 2] = fracToF16(Math.round(colors[i][2] * 10)); pixels[idx + 3] = alphaF16; } } function makeF16TestPixels() { var pixels = new Uint16Array(WIDTH * HEIGHT * 4); fillF16Row(pixels, 0, COLORS_ROW0, 0x3c00); // alpha = 1.0 fillF16Row(pixels, 5, COLORS_ROW1, 0x3c00); // alpha = 1.0 fillF16Row(pixels, 10, COLORS_ROW0, F16_ALPHA_THIRD); // alpha ~1/3 fillF16Row(pixels, 15, COLORS_ROW1, F16_ALPHA_TWOTHIRDS); // alpha ~2/3 return new Uint8Array(pixels.buffer); } // Encode pixels and verify the PNG structure. function encodeAndVerify(pixels, format, options, expectedColorType) { var encoder = Cc[ "@mozilla.org/image/encoder;2?type=image/png" ].createInstance(Ci.imgIEncoder); encoder.initFromData( pixels, pixels.length, WIDTH, HEIGHT, strideForFormat(format), format, options, null ); var bytes = getPngBytes(encoder); verifyPngSignature(bytes); Assert.equal(getPngBitDepth(bytes), 16, "bit depth should be 16"); Assert.equal(getPngColorType(bytes), expectedColorType, "color type"); return bytes; } // Encode, decode, and verify the decoded image dimensions. function encodeDecodeVerify(pixels, format) { var encoder = Cc[ "@mozilla.org/image/encoder;2?type=image/png" ].createInstance(Ci.imgIEncoder); encoder.initFromData( pixels, pixels.length, WIDTH, HEIGHT, strideForFormat(format), format, "", null ); var pngBytes = getPngBytes(encoder); var imgTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools); var container = imgTools.decodeImageFromArrayBuffer( new Uint8Array(pngBytes).buffer, "image/png" ); Assert.equal(container.width, WIDTH, "decoded width"); Assert.equal(container.height, HEIGHT, "decoded height"); } function run_test() { dump("test R10G10B10A2...\n"); encodeAndVerify( makeR10G10B10A2TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_R10G10B10A2, "", 6 ); dump("test U10 RGBA...\n"); encodeAndVerify( makeU10TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_U10, "", 6 ); dump("test U12 RGBA...\n"); encodeAndVerify( makeU12TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_U12, "", 6 ); dump("test U16 RGBA...\n"); encodeAndVerify( makeU16TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_U16, "", 6 ); dump("test F16 RGBA...\n"); encodeAndVerify( makeF16TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_F16, "", 6 ); dump("test R10G10B10A2 RGB (no transparency)...\n"); encodeAndVerify( makeR10G10B10A2TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_R10G10B10A2, "transparency=none", 2 ); dump("test U10 RGB (no transparency)...\n"); encodeAndVerify( makeU10TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_U10, "transparency=none", 2 ); dump("test U12 RGB (no transparency)...\n"); encodeAndVerify( makeU12TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_U12, "transparency=none", 2 ); dump("test U16 RGB (no transparency)...\n"); encodeAndVerify( makeU16TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_U16, "transparency=none", 2 ); dump("test F16 RGB (no transparency)...\n"); encodeAndVerify( makeF16TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_F16, "transparency=none", 2 ); dump("test cICP chunk...\n"); test_cicp_chunk(); dump("test setColorSpaceInfo on all encoders...\n"); test_setColorSpaceInfo_on_all_encoders(); dump("test R10G10B10A2 roundtrip decode...\n"); encodeDecodeVerify( makeR10G10B10A2TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_R10G10B10A2 ); dump("test U10 roundtrip decode...\n"); encodeDecodeVerify(makeU10TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_U10); dump("test U12 roundtrip decode...\n"); encodeDecodeVerify(makeU12TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_U12); dump("test U16 roundtrip decode...\n"); encodeDecodeVerify(makeU16TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_U16); dump("test F16 roundtrip decode...\n"); encodeDecodeVerify(makeF16TestPixels(), Ci.imgIEncoder.INPUT_FORMAT_RGBA_F16); } function test_cicp_chunk() { dump("test_cicp_chunk\n"); var encoder = Cc[ "@mozilla.org/image/encoder;2?type=image/png" ].createInstance(Ci.imgIEncoder); encoder.setColorSpaceInfo( Ci.imgIEncoder.CP_BT2020, Ci.imgIEncoder.TC_SMPTE2084, Ci.imgIEncoder.MC_IDENTITY, true ); var pixels = makeU16TestPixels(); encoder.initFromData( pixels, pixels.length, WIDTH, HEIGHT, strideForFormat(Ci.imgIEncoder.INPUT_FORMAT_RGBA_U16), Ci.imgIEncoder.INPUT_FORMAT_RGBA_U16, "", null ); var bytes = getPngBytes(encoder); verifyPngSignature(bytes); // Find the cICP chunk. var cicpOffset = findPngChunk(bytes, "cICP"); Assert.notEqual(cicpOffset, -1, "cICP chunk should be present"); // cICP chunk is 4 bytes: primaries, transfer, matrix, full_range Assert.equal(bytes[cicpOffset], 9, "primaries should be BT.2020 (9)"); Assert.equal( bytes[cicpOffset + 1], 16, "transfer should be SMPTE2084/PQ (16)" ); Assert.equal(bytes[cicpOffset + 2], 0, "matrix should be Identity (0)"); Assert.equal(bytes[cicpOffset + 3], 1, "full range should be 1"); } function test_setColorSpaceInfo_on_all_encoders() { dump("test_setColorSpaceInfo_on_all_encoders\n"); var types = [ "image/png", "image/jpeg", "image/webp", "image/bmp", "image/vnd.microsoft.icon", ]; for (var type of types) { var encoder = Cc[ "@mozilla.org/image/encoder;2?type=" + type ].createInstance(Ci.imgIEncoder); // Should not throw on any encoder. encoder.setColorSpaceInfo( Ci.imgIEncoder.CP_BT709, Ci.imgIEncoder.TC_SRGB, Ci.imgIEncoder.MC_IDENTITY, true ); } }