Recently Written · git

scan-subread

PDF reader that follows an audiobook with the SubRead suite

git clone https://github.com/equwal/scan-subread

Log | Files | Refs


test/ppm.test.ts (3002 bytes)

1 import fc from 'fast-check';
2 import { describe, expect, it } from 'vitest';
3 import { imageToPpm, type RgbaImage } from '../src/ppm';
4 
5 /** Reads a binary PPM file: the header fields and the RGB bytes after the header. */
6 function decodePpm(ppm: Uint8Array): { width: number; height: number; max: number; rgb: number[] } {
7   // The header is ASCII: "P6", then width, height and maximum value, each after white space,
8   // then one white space character.
9   const head = new TextDecoder('latin1').decode(ppm.subarray(0, 64));
10   const match = /^P6\s+(\d+)\s+(\d+)\s+(\d+)\s/.exec(head);
11   if (!match) throw new Error(`No PPM header: ${JSON.stringify(head)}`);
12   return {
13     width: Number(match[1]),
14     height: Number(match[2]),
15     max: Number(match[3]),
16     rgb: Array.from(ppm.subarray(match[0].length)),
17   };
18 }
19 
20 /** The RGB bytes of RGBA pixels. */
21 function rgbOf(data: Uint8ClampedArray): number[] {
22   return Array.from(data).filter((_, i) => i % 4 !== 3);
23 }
24 
25 describe('imageToPpm', () => {
26   it('writes the header, then the RGB of each pixel row by row, without the alpha', () => {
27     // 3 x 2 pixels. Each byte holds its own index, so the order shows.
28     const data = new Uint8ClampedArray(Array.from({ length: 24 }, (_, i) => i));
29     const ppm = imageToPpm({ width: 3, height: 2, data });
30     const header = 'P6\n3 2\n255\n';
31     expect(new TextDecoder('latin1').decode(ppm.subarray(0, header.length))).toBe(header);
32     expect(Array.from(ppm.subarray(header.length))).toEqual([
33       0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 16, 17, 18, 20, 21, 22,
34     ]);
35   });
36 
37   it('has 3 bytes for each pixel of a page canvas of the app, after the header', () => {
38     const width = 1600;
39     const height = 2263;
40     const ppm = imageToPpm({ width, height, data: new Uint8ClampedArray(width * height * 4) });
41     const header = 'P6\n1600 2263\n255\n';
42     expect(new TextDecoder('latin1').decode(ppm.subarray(0, header.length))).toBe(header);
43     expect(ppm.length).toBe(header.length + width * height * 3);
44   });
45 
46   it('refuses pixel data of the wrong length', () => {
47     const data = new Uint8ClampedArray(4 * 5);
48     expect(() => imageToPpm({ width: 2, height: 2, data })).toThrow(RangeError);
49     expect(() => imageToPpm({ width: 3, height: 2, data })).toThrow(RangeError);
50   });
51 
52   it('gives back the size and the RGB of each pixel when the file is read', () => {
53     const image: fc.Arbitrary<RgbaImage> = fc
54       .record({ width: fc.integer({ min: 1, max: 12 }), height: fc.integer({ min: 1, max: 12 }) })
55       .chain(({ width, height }) =>
56         fc
57           .uint8Array({ minLength: width * height * 4, maxLength: width * height * 4 })
58           .map((bytes) => ({ width, height, data: new Uint8ClampedArray(bytes) })),
59       );
60     fc.assert(
61       fc.property(image, (input) => {
62         expect(decodePpm(imageToPpm(input))).toEqual({
63           width: input.width,
64           height: input.height,
65           max: 255,
66           rgb: rgbOf(input.data),
67         });
68       }),
69     );
70   });
71 });