/* Copyright 2026 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { convertBlackAndWhiteToRGBA, convertToRGBA, grayToRGBA, } from "../../src/shared/image_utils.js"; import { FeatureTest, ImageKind } from "../../src/shared/util.js"; describe("image_utils", function () { // Precompute endian-dependent constants once for all tests. const isLE = FeatureTest.isLittleEndian; const BLACK = isLE ? 0xff000000 : 0x000000ff; const WHITE = 0xffffffff; const RED = 0xff0000ff; describe("convertBlackAndWhiteToRGBA", function () { it("converts a single byte (width=8) with alternating bits", function () { // 0b10101010: bits 7..0 = 1,0,1,0,1,0,1,0 → W,B,W,B,W,B,W,B const src = new Uint8Array([0b10101010]); const dest = new Uint8ClampedArray(8 * 4); const { srcPos, destPos } = convertBlackAndWhiteToRGBA({ src, dest, width: 8, height: 1, }); expect(srcPos).toEqual(1); expect(destPos).toEqual(8); const dest32 = new Uint32Array(dest.buffer); expect(dest32[0]).toEqual(WHITE); expect(dest32[1]).toEqual(BLACK); expect(dest32[2]).toEqual(WHITE); expect(dest32[3]).toEqual(BLACK); expect(dest32[4]).toEqual(WHITE); expect(dest32[5]).toEqual(BLACK); expect(dest32[6]).toEqual(WHITE); expect(dest32[7]).toEqual(BLACK); }); it("converts two rows (width=8, height=2)", function () { // Row 0: 0b10101010 → W,B,W,B,W,B,W,B // Row 1: 0b01010101 → B,W,B,W,B,W,B,W const src = new Uint8Array([0b10101010, 0b01010101]); const dest = new Uint8ClampedArray(16 * 4); const { srcPos, destPos } = convertBlackAndWhiteToRGBA({ src, dest, width: 8, height: 2, }); expect(srcPos).toEqual(2); expect(destPos).toEqual(16); const dest32 = new Uint32Array(dest.buffer); // Row 0 expect(dest32[0]).toEqual(WHITE); expect(dest32[1]).toEqual(BLACK); // Row 1 expect(dest32[8]).toEqual(BLACK); expect(dest32[9]).toEqual(WHITE); }); it("handles width not divisible by 8 (width=5)", function () { // 0b11100000: bits 7..3 = 1,1,1,0,0 → W,W,W,B,B (only 5 pixels) const src = new Uint8Array([0b11100000]); const dest = new Uint8ClampedArray(5 * 4); const { srcPos, destPos } = convertBlackAndWhiteToRGBA({ src, dest, width: 5, height: 1, }); expect(srcPos).toEqual(1); expect(destPos).toEqual(5); const dest32 = new Uint32Array(dest.buffer); expect(dest32[0]).toEqual(WHITE); expect(dest32[1]).toEqual(WHITE); expect(dest32[2]).toEqual(WHITE); expect(dest32[3]).toEqual(BLACK); expect(dest32[4]).toEqual(BLACK); }); it("handles width=10 spanning two bytes", function () { // widthInSource = 1, widthRemainder = 2 // Byte 0: 0b11111111 → 8 white pixels // Byte 1: 0b11000000 → bits 7,6 = 1,1 → W,W (only 2 pixels consumed) const src = new Uint8Array([0b11111111, 0b11000000]); const dest = new Uint8ClampedArray(10 * 4); const { srcPos, destPos } = convertBlackAndWhiteToRGBA({ src, dest, width: 10, height: 1, }); expect(srcPos).toEqual(2); expect(destPos).toEqual(10); const dest32 = new Uint32Array(dest.buffer); for (let i = 0; i < 10; i++) { expect(dest32[i]).withContext(`pixel ${i}`).toEqual(WHITE); } }); it("handles srcPos offset", function () { // Skip the first 2 bytes; read from byte 2 = 0b10000000 → W,B,B,B,B,B,B,B const src = new Uint8Array([0x00, 0x00, 0b10000000]); const dest = new Uint8ClampedArray(8 * 4); const { srcPos, destPos } = convertBlackAndWhiteToRGBA({ src, srcPos: 2, dest, width: 8, height: 1, }); expect(srcPos).toEqual(3); expect(destPos).toEqual(8); const dest32 = new Uint32Array(dest.buffer); expect(dest32[0]).toEqual(WHITE); for (let i = 1; i < 8; i++) { expect(dest32[i]).withContext(`pixel ${i}`).toEqual(BLACK); } }); it("applies inverseDecode correctly", function () { // 0b10101010 normally → W,B,W,B,... // With inverseDecode: 1→black, 0→white, so → B,W,B,W,... const src = new Uint8Array([0b10101010]); const dest = new Uint8ClampedArray(8 * 4); convertBlackAndWhiteToRGBA({ src, dest, width: 8, height: 1, inverseDecode: true, }); const dest32 = new Uint32Array(dest.buffer); expect(dest32[0]).toEqual(BLACK); expect(dest32[1]).toEqual(WHITE); expect(dest32[2]).toEqual(BLACK); expect(dest32[3]).toEqual(WHITE); }); it("uses nonBlackColor for the one-bits", function () { // Custom color for non-black pixels. const CUSTOM = isLE ? 0xff0000ff : 0xff0000ff; // red (LE) / different (BE) // 0b11110000 → 1,1,1,1,0,0,0,0 // → CUSTOM,CUSTOM,CUSTOM,CUSTOM,BLACK,BLACK,BLACK,BLACK const src = new Uint8Array([0b11110000]); const dest = new Uint8ClampedArray(8 * 4); convertBlackAndWhiteToRGBA({ src, dest, width: 8, height: 1, nonBlackColor: CUSTOM, }); const dest32 = new Uint32Array(dest.buffer); expect(dest32[0]).toEqual(CUSTOM); expect(dest32[1]).toEqual(CUSTOM); expect(dest32[2]).toEqual(CUSTOM); expect(dest32[3]).toEqual(CUSTOM); expect(dest32[4]).toEqual(BLACK); expect(dest32[5]).toEqual(BLACK); expect(dest32[6]).toEqual(BLACK); expect(dest32[7]).toEqual(BLACK); }); it("uses 0xff (all-white byte) when src is shorter than expected", function () { // width=10 needs 2 bytes but only 1 provided. // widthInSource=1: byte 0 = 0b11110000 → W,W,W,W,B,B,B,B // widthRemainder=2: missing byte treated as 0xff → bits 7,6 = 1,1 → W,W const src = new Uint8Array([0b11110000]); const dest = new Uint8ClampedArray(10 * 4); convertBlackAndWhiteToRGBA({ src, dest, width: 10, height: 1 }); const dest32 = new Uint32Array(dest.buffer); expect(dest32[0]).toEqual(WHITE); expect(dest32[1]).toEqual(WHITE); expect(dest32[2]).toEqual(WHITE); expect(dest32[3]).toEqual(WHITE); expect(dest32[4]).toEqual(BLACK); expect(dest32[5]).toEqual(BLACK); expect(dest32[6]).toEqual(BLACK); expect(dest32[7]).toEqual(BLACK); // Missing second byte → treated as 0xff, so bits 7,6 → W,W expect(dest32[8]).toEqual(WHITE); expect(dest32[9]).toEqual(WHITE); }); }); describe("grayToRGBA", function () { it("converts black (0), mid-gray (128), and white (255)", function () { const src = new Uint8Array([0, 128, 255]); const dest = new Uint32Array(3); grayToRGBA(src, dest); expect(dest[0]).toEqual(BLACK); expect(dest[1]).toEqual(isLE ? 0xff808080 : 0x808080ff); expect(dest[2]).toEqual(WHITE); }); it("handles an empty input array", function () { grayToRGBA(new Uint8Array(0), new Uint32Array(0)); // No crash, nothing to check beyond reaching here. }); it("alpha channel is always 0xff for every pixel", function () { const N = 256; const src = new Uint8Array(N); const dest = new Uint32Array(N); for (let i = 0; i < N; i++) { src[i] = i; } grayToRGBA(src, dest); // Extract the alpha byte: high byte in LE, low byte in BE. const alphaShift = isLE ? 24 : 0; for (let i = 0; i < N; i++) { expect((dest[i] >>> alphaShift) & 0xff) .withContext(`alpha for value ${i}`) .toEqual(0xff); } }); it("RGB channels are equal for each gray level", function () { const src = new Uint8Array([51, 102, 204]); const dest = new Uint32Array(3); grayToRGBA(src, dest); // In LE: 0xffRRGGBB where RR=GG=BB=value // In BE: 0xRRGGBBff where RR=GG=BB=value for (let i = 0; i < src.length; i++) { const v = src[i]; const expected = isLE ? 0xff000000 | (v << 16) | (v << 8) | v : (v << 24) | (v << 16) | (v << 8) | 0xff; expect(dest[i]) .withContext(`gray value ${v}`) .toEqual(expected >>> 0); } }); }); describe("convertToRGBA", function () { it("dispatches to convertBlackAndWhiteToRGBA for GRAYSCALE_1BPP", function () { const src = new Uint8Array([0b11110000]); const dest = new Uint8ClampedArray(8 * 4); const result = convertToRGBA({ src, dest, width: 8, height: 1, kind: ImageKind.GRAYSCALE_1BPP, }); expect(result).not.toBeNull(); expect(result.destPos).toEqual(8); const dest32 = new Uint32Array(dest.buffer); expect(dest32[0]).toEqual(WHITE); expect(dest32[4]).toEqual(BLACK); }); it("dispatches to convertRGBToRGBA for RGB_24BPP", function () { // Three pixels: white, black, red. const src = new Uint8Array([255, 255, 255, 0, 0, 0, 255, 0, 0]); const dest = new Uint32Array(3); const result = convertToRGBA({ src, dest, width: 3, height: 1, kind: ImageKind.RGB_24BPP, }); expect(result).not.toBeNull(); expect(result.srcPos).toEqual(9); expect(result.destPos).toEqual(3); expect(dest[0]).toEqual(WHITE); expect(dest[1]).toEqual(BLACK); expect(dest[2]).toEqual(RED); }); it("returns null for an unknown kind", function () { const result = convertToRGBA({ src: new Uint8Array(4), dest: new Uint32Array(1), width: 1, height: 1, kind: 999, }); expect(result).toBeNull(); }); it("handles destPos offset for RGB_24BPP", function () { // One red pixel written at destPos=2 in a 4-pixel buffer. const src = new Uint8Array([255, 0, 0]); const dest = new Uint32Array(4); const result = convertToRGBA({ src, dest, destPos: 2, width: 1, height: 1, kind: ImageKind.RGB_24BPP, }); expect(result.destPos).toEqual(3); expect(dest[0]).toEqual(0); // untouched expect(dest[1]).toEqual(0); // untouched expect(dest[2]).toEqual(RED); // red expect(dest[3]).toEqual(0); // untouched }); }); });