Skip to content

Commit f548a15

Browse files
authored
Merge pull request #20698 from calixteman/image_utils_test
Add some unit tests for functions in image_utils.js
2 parents aaf3ad5 + 0bb59f1 commit f548a15

3 files changed

Lines changed: 334 additions & 0 deletions

File tree

test/unit/clitests.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"fetch_stream_spec.js",
2828
"font_substitutions_spec.js",
2929
"function_spec.js",
30+
"image_utils_spec.js",
3031
"message_handler_spec.js",
3132
"metadata_spec.js",
3233
"murmurhash3_spec.js",

test/unit/image_utils_spec.js

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
/* Copyright 2026 Mozilla Foundation
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
import {
17+
convertBlackAndWhiteToRGBA,
18+
convertToRGBA,
19+
grayToRGBA,
20+
} from "../../src/shared/image_utils.js";
21+
import { FeatureTest, ImageKind } from "../../src/shared/util.js";
22+
23+
describe("image_utils", function () {
24+
// Precompute endian-dependent constants once for all tests.
25+
const isLE = FeatureTest.isLittleEndian;
26+
const BLACK = isLE ? 0xff000000 : 0x000000ff;
27+
const WHITE = 0xffffffff;
28+
const RED = 0xff0000ff;
29+
30+
describe("convertBlackAndWhiteToRGBA", function () {
31+
it("converts a single byte (width=8) with alternating bits", function () {
32+
// 0b10101010: bits 7..0 = 1,0,1,0,1,0,1,0 → W,B,W,B,W,B,W,B
33+
const src = new Uint8Array([0b10101010]);
34+
const dest = new Uint8ClampedArray(8 * 4);
35+
const { srcPos, destPos } = convertBlackAndWhiteToRGBA({
36+
src,
37+
dest,
38+
width: 8,
39+
height: 1,
40+
});
41+
expect(srcPos).toEqual(1);
42+
expect(destPos).toEqual(8);
43+
44+
const dest32 = new Uint32Array(dest.buffer);
45+
expect(dest32[0]).toEqual(WHITE);
46+
expect(dest32[1]).toEqual(BLACK);
47+
expect(dest32[2]).toEqual(WHITE);
48+
expect(dest32[3]).toEqual(BLACK);
49+
expect(dest32[4]).toEqual(WHITE);
50+
expect(dest32[5]).toEqual(BLACK);
51+
expect(dest32[6]).toEqual(WHITE);
52+
expect(dest32[7]).toEqual(BLACK);
53+
});
54+
55+
it("converts two rows (width=8, height=2)", function () {
56+
// Row 0: 0b10101010 → W,B,W,B,W,B,W,B
57+
// Row 1: 0b01010101 → B,W,B,W,B,W,B,W
58+
const src = new Uint8Array([0b10101010, 0b01010101]);
59+
const dest = new Uint8ClampedArray(16 * 4);
60+
const { srcPos, destPos } = convertBlackAndWhiteToRGBA({
61+
src,
62+
dest,
63+
width: 8,
64+
height: 2,
65+
});
66+
expect(srcPos).toEqual(2);
67+
expect(destPos).toEqual(16);
68+
69+
const dest32 = new Uint32Array(dest.buffer);
70+
// Row 0
71+
expect(dest32[0]).toEqual(WHITE);
72+
expect(dest32[1]).toEqual(BLACK);
73+
// Row 1
74+
expect(dest32[8]).toEqual(BLACK);
75+
expect(dest32[9]).toEqual(WHITE);
76+
});
77+
78+
it("handles width not divisible by 8 (width=5)", function () {
79+
// 0b11100000: bits 7..3 = 1,1,1,0,0 → W,W,W,B,B (only 5 pixels)
80+
const src = new Uint8Array([0b11100000]);
81+
const dest = new Uint8ClampedArray(5 * 4);
82+
const { srcPos, destPos } = convertBlackAndWhiteToRGBA({
83+
src,
84+
dest,
85+
width: 5,
86+
height: 1,
87+
});
88+
expect(srcPos).toEqual(1);
89+
expect(destPos).toEqual(5);
90+
91+
const dest32 = new Uint32Array(dest.buffer);
92+
expect(dest32[0]).toEqual(WHITE);
93+
expect(dest32[1]).toEqual(WHITE);
94+
expect(dest32[2]).toEqual(WHITE);
95+
expect(dest32[3]).toEqual(BLACK);
96+
expect(dest32[4]).toEqual(BLACK);
97+
});
98+
99+
it("handles width=10 spanning two bytes", function () {
100+
// widthInSource = 1, widthRemainder = 2
101+
// Byte 0: 0b11111111 → 8 white pixels
102+
// Byte 1: 0b11000000 → bits 7,6 = 1,1 → W,W (only 2 pixels consumed)
103+
const src = new Uint8Array([0b11111111, 0b11000000]);
104+
const dest = new Uint8ClampedArray(10 * 4);
105+
const { srcPos, destPos } = convertBlackAndWhiteToRGBA({
106+
src,
107+
dest,
108+
width: 10,
109+
height: 1,
110+
});
111+
expect(srcPos).toEqual(2);
112+
expect(destPos).toEqual(10);
113+
114+
const dest32 = new Uint32Array(dest.buffer);
115+
for (let i = 0; i < 10; i++) {
116+
expect(dest32[i]).withContext(`pixel ${i}`).toEqual(WHITE);
117+
}
118+
});
119+
120+
it("handles srcPos offset", function () {
121+
// Skip the first 2 bytes; read from byte 2 = 0b10000000 → W,B,B,B,B,B,B,B
122+
const src = new Uint8Array([0x00, 0x00, 0b10000000]);
123+
const dest = new Uint8ClampedArray(8 * 4);
124+
const { srcPos, destPos } = convertBlackAndWhiteToRGBA({
125+
src,
126+
srcPos: 2,
127+
dest,
128+
width: 8,
129+
height: 1,
130+
});
131+
expect(srcPos).toEqual(3);
132+
expect(destPos).toEqual(8);
133+
134+
const dest32 = new Uint32Array(dest.buffer);
135+
expect(dest32[0]).toEqual(WHITE);
136+
for (let i = 1; i < 8; i++) {
137+
expect(dest32[i]).withContext(`pixel ${i}`).toEqual(BLACK);
138+
}
139+
});
140+
141+
it("applies inverseDecode correctly", function () {
142+
// 0b10101010 normally → W,B,W,B,...
143+
// With inverseDecode: 1→black, 0→white, so → B,W,B,W,...
144+
const src = new Uint8Array([0b10101010]);
145+
const dest = new Uint8ClampedArray(8 * 4);
146+
convertBlackAndWhiteToRGBA({
147+
src,
148+
dest,
149+
width: 8,
150+
height: 1,
151+
inverseDecode: true,
152+
});
153+
154+
const dest32 = new Uint32Array(dest.buffer);
155+
expect(dest32[0]).toEqual(BLACK);
156+
expect(dest32[1]).toEqual(WHITE);
157+
expect(dest32[2]).toEqual(BLACK);
158+
expect(dest32[3]).toEqual(WHITE);
159+
});
160+
161+
it("uses nonBlackColor for the one-bits", function () {
162+
// Custom color for non-black pixels.
163+
const CUSTOM = isLE ? 0xff0000ff : 0xff0000ff; // red (LE) / different (BE)
164+
// 0b11110000 → 1,1,1,1,0,0,0,0
165+
// → CUSTOM,CUSTOM,CUSTOM,CUSTOM,BLACK,BLACK,BLACK,BLACK
166+
const src = new Uint8Array([0b11110000]);
167+
const dest = new Uint8ClampedArray(8 * 4);
168+
convertBlackAndWhiteToRGBA({
169+
src,
170+
dest,
171+
width: 8,
172+
height: 1,
173+
nonBlackColor: CUSTOM,
174+
});
175+
176+
const dest32 = new Uint32Array(dest.buffer);
177+
expect(dest32[0]).toEqual(CUSTOM);
178+
expect(dest32[1]).toEqual(CUSTOM);
179+
expect(dest32[2]).toEqual(CUSTOM);
180+
expect(dest32[3]).toEqual(CUSTOM);
181+
expect(dest32[4]).toEqual(BLACK);
182+
expect(dest32[5]).toEqual(BLACK);
183+
expect(dest32[6]).toEqual(BLACK);
184+
expect(dest32[7]).toEqual(BLACK);
185+
});
186+
187+
it("uses 0xff (all-white byte) when src is shorter than expected", function () {
188+
// width=10 needs 2 bytes but only 1 provided.
189+
// widthInSource=1: byte 0 = 0b11110000 → W,W,W,W,B,B,B,B
190+
// widthRemainder=2: missing byte treated as 0xff → bits 7,6 = 1,1 → W,W
191+
const src = new Uint8Array([0b11110000]);
192+
const dest = new Uint8ClampedArray(10 * 4);
193+
convertBlackAndWhiteToRGBA({ src, dest, width: 10, height: 1 });
194+
195+
const dest32 = new Uint32Array(dest.buffer);
196+
expect(dest32[0]).toEqual(WHITE);
197+
expect(dest32[1]).toEqual(WHITE);
198+
expect(dest32[2]).toEqual(WHITE);
199+
expect(dest32[3]).toEqual(WHITE);
200+
expect(dest32[4]).toEqual(BLACK);
201+
expect(dest32[5]).toEqual(BLACK);
202+
expect(dest32[6]).toEqual(BLACK);
203+
expect(dest32[7]).toEqual(BLACK);
204+
// Missing second byte → treated as 0xff, so bits 7,6 → W,W
205+
expect(dest32[8]).toEqual(WHITE);
206+
expect(dest32[9]).toEqual(WHITE);
207+
});
208+
});
209+
210+
describe("grayToRGBA", function () {
211+
it("converts black (0), mid-gray (128), and white (255)", function () {
212+
const src = new Uint8Array([0, 128, 255]);
213+
const dest = new Uint32Array(3);
214+
grayToRGBA(src, dest);
215+
216+
expect(dest[0]).toEqual(BLACK);
217+
expect(dest[1]).toEqual(isLE ? 0xff808080 : 0x808080ff);
218+
expect(dest[2]).toEqual(WHITE);
219+
});
220+
221+
it("handles an empty input array", function () {
222+
grayToRGBA(new Uint8Array(0), new Uint32Array(0));
223+
// No crash, nothing to check beyond reaching here.
224+
});
225+
226+
it("alpha channel is always 0xff for every pixel", function () {
227+
const N = 256;
228+
const src = new Uint8Array(N);
229+
const dest = new Uint32Array(N);
230+
for (let i = 0; i < N; i++) {
231+
src[i] = i;
232+
}
233+
grayToRGBA(src, dest);
234+
235+
// Extract the alpha byte: high byte in LE, low byte in BE.
236+
const alphaShift = isLE ? 24 : 0;
237+
for (let i = 0; i < N; i++) {
238+
expect((dest[i] >>> alphaShift) & 0xff)
239+
.withContext(`alpha for value ${i}`)
240+
.toEqual(0xff);
241+
}
242+
});
243+
244+
it("RGB channels are equal for each gray level", function () {
245+
const src = new Uint8Array([51, 102, 204]);
246+
const dest = new Uint32Array(3);
247+
grayToRGBA(src, dest);
248+
249+
// In LE: 0xffRRGGBB where RR=GG=BB=value
250+
// In BE: 0xRRGGBBff where RR=GG=BB=value
251+
for (let i = 0; i < src.length; i++) {
252+
const v = src[i];
253+
const expected = isLE
254+
? 0xff000000 | (v << 16) | (v << 8) | v
255+
: (v << 24) | (v << 16) | (v << 8) | 0xff;
256+
expect(dest[i])
257+
.withContext(`gray value ${v}`)
258+
.toEqual(expected >>> 0);
259+
}
260+
});
261+
});
262+
263+
describe("convertToRGBA", function () {
264+
it("dispatches to convertBlackAndWhiteToRGBA for GRAYSCALE_1BPP", function () {
265+
const src = new Uint8Array([0b11110000]);
266+
const dest = new Uint8ClampedArray(8 * 4);
267+
const result = convertToRGBA({
268+
src,
269+
dest,
270+
width: 8,
271+
height: 1,
272+
kind: ImageKind.GRAYSCALE_1BPP,
273+
});
274+
expect(result).not.toBeNull();
275+
expect(result.destPos).toEqual(8);
276+
277+
const dest32 = new Uint32Array(dest.buffer);
278+
expect(dest32[0]).toEqual(WHITE);
279+
expect(dest32[4]).toEqual(BLACK);
280+
});
281+
282+
it("dispatches to convertRGBToRGBA for RGB_24BPP", function () {
283+
// Three pixels: white, black, red.
284+
const src = new Uint8Array([255, 255, 255, 0, 0, 0, 255, 0, 0]);
285+
const dest = new Uint32Array(3);
286+
const result = convertToRGBA({
287+
src,
288+
dest,
289+
width: 3,
290+
height: 1,
291+
kind: ImageKind.RGB_24BPP,
292+
});
293+
expect(result).not.toBeNull();
294+
expect(result.srcPos).toEqual(9);
295+
expect(result.destPos).toEqual(3);
296+
297+
expect(dest[0]).toEqual(WHITE);
298+
expect(dest[1]).toEqual(BLACK);
299+
expect(dest[2]).toEqual(RED);
300+
});
301+
302+
it("returns null for an unknown kind", function () {
303+
const result = convertToRGBA({
304+
src: new Uint8Array(4),
305+
dest: new Uint32Array(1),
306+
width: 1,
307+
height: 1,
308+
kind: 999,
309+
});
310+
expect(result).toBeNull();
311+
});
312+
313+
it("handles destPos offset for RGB_24BPP", function () {
314+
// One red pixel written at destPos=2 in a 4-pixel buffer.
315+
const src = new Uint8Array([255, 0, 0]);
316+
const dest = new Uint32Array(4);
317+
const result = convertToRGBA({
318+
src,
319+
dest,
320+
destPos: 2,
321+
width: 1,
322+
height: 1,
323+
kind: ImageKind.RGB_24BPP,
324+
});
325+
expect(result.destPos).toEqual(3);
326+
expect(dest[0]).toEqual(0); // untouched
327+
expect(dest[1]).toEqual(0); // untouched
328+
expect(dest[2]).toEqual(RED); // red
329+
expect(dest[3]).toEqual(0); // untouched
330+
});
331+
});
332+
});

test/unit/jasmine-boot.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ async function initializePDFJS(callback) {
7070
"pdfjs-test/unit/fetch_stream_spec.js",
7171
"pdfjs-test/unit/font_substitutions_spec.js",
7272
"pdfjs-test/unit/function_spec.js",
73+
"pdfjs-test/unit/image_utils_spec.js",
7374
"pdfjs-test/unit/message_handler_spec.js",
7475
"pdfjs-test/unit/metadata_spec.js",
7576
"pdfjs-test/unit/murmurhash3_spec.js",

0 commit comments

Comments
 (0)