Skip to content

Commit

Permalink
🧪 test(lib/pdf): Check thumbnail helper functions outputs.
Browse files Browse the repository at this point in the history
  • Loading branch information
make-github-pseudonymous-again committed May 13, 2024
1 parent 4b932fa commit d8a48fc
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 23 deletions.
164 changes: 164 additions & 0 deletions imports/_test/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import {assert} from 'chai';

import {type Sharp} from 'sharp';

import {createContextIso, destroyContextIso} from '../lib/canvas';
import blobToImage from '../lib/blob/blobToImage';

type TypedArray =
| Uint8Array
| Uint8ClampedArray
| Int8Array
| Uint16Array
| Int16Array
| Uint32Array
| Int32Array
| Float32Array
| Float64Array;
type SharpInput = ArrayBuffer | TypedArray;
type S = Sharp | SharpInput;

type I = HTMLImageElement | ArrayBuffer;

type Image = S | I;

const _s = async (x: S): Promise<Sharp> => {
const {default: sharp} = await import('sharp');
return x instanceof sharp ? (x as Sharp) : sharp(x as SharpInput);
};

const _i = async (x: I): Promise<HTMLImageElement> => {
if (x instanceof HTMLImageElement) return x;
const blob = new Blob([x], {type: 'image/png'});
const image = await blobToImage(blob);
return image;
};

const _pixels = async (image: HTMLImageElement): Promise<Uint8ClampedArray> => {
const {width, height} = image;
const context = await createContextIso({width, height});
context.drawImage(image, 0, 0);
const data = context.getImageData(0, 0, width, height).data;
destroyContextIso(context);
return data;
};

const _xorClient = (
a: Uint8ClampedArray,
b: Uint8ClampedArray,
): Uint8ClampedArray => {
const n = a.length;
assert.equal(b.length, n);
const delta = new Uint8ClampedArray(n);
for (let i = 0; i < n; ++i) {
// eslint-disable-next-line no-bitwise
delta[i] = a[i]! ^ b[i]!;
}

return delta;
};

const _diffClient = async (a: Image, b: Image) => {
const _a = await _i(a as I);
const _b = await _i(b as I);
const aPixels = await _pixels(_a);
const bPixels = await _pixels(_b);
return _xorClient(aPixels, bPixels);
};

const _diffServer = async (a: Image, b: Image) => {
const _a = await _s(a as S);
const _b = await _s(b as S);
const delta = await _xorServer(_a, _b);
return delta.raw().toBuffer();
};

export const diff = Meteor.isServer ? _diffServer : _diffClient;

export const assertEqual = async (a: Image, b: Image) => {
const delta = await diff(a, b);
assert(
!delta.some((value, index) => index % 4 !== 3 && value !== 0),
`Input images are not equal.`,
);
};

const _assertSameDimensions = async (a: Sharp, b: Sharp): Promise<void> => {
const {
width: aWidth,
height: aHeight,
channels: aChannels,
hasAlpha: aHasAlpha,
} = await a.metadata();
const {
width: bWidth,
height: bHeight,
channels: bChannels,
hasAlpha: bHasAlpha,
} = await b.metadata();

assert.equal(
aWidth,
bWidth,
`Images have different widths: ${aWidth} !== ${bWidth}`,
);

assert.equal(
aHeight,
bHeight,
`Images have different heights: ${aHeight} !== ${bHeight}`,
);

assert.equal(
aChannels,
bChannels,
`Images have different number of channels: ${aChannels} !== ${bChannels}`,
);

assert.equal(
aHasAlpha,
bHasAlpha,
`Images have different alpha channel settings: ${aHasAlpha} !== ${bHasAlpha}`,
);
};

const _xorServer = async (a: Sharp, b: Sharp): Promise<Sharp> => {
await _assertSameDimensions(a, b);
const _b = await b.toBuffer();
return a.boolean(_b, 'eor');
};

const _whiteRectanglePNGServer = async (options: {
width: number;
height: number;
}) => {
const {default: sharp} = await import('sharp');
return sharp({
create: {
...options,
channels: 4,
background: {r: 255, g: 255, b: 255, alpha: 0},
},
}).png();
};

const _whiteRectanglePNGClient = async ({
width,
height,
}: {
width: number;
height: number;
}): Promise<HTMLImageElement> => {
const context = await createContextIso({width, height});
context.fillStyle = '#FFFFFF';
context.fillRect(0, 0, width, height);
const url = context.canvas.toDataURL('image/png');
destroyContextIso(context);
const img = new Image();
img.src = url;
return img;
};

export const whiteRectanglePNG = Meteor.isServer
? _whiteRectanglePNGServer
: _whiteRectanglePNGClient;
22 changes: 22 additions & 0 deletions imports/lib/blob/blobToImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const blobToImage = async (blob: Blob): Promise<HTMLImageElement> =>
new Promise((resolve, reject) => {
const url = URL.createObjectURL(blob);
const img = new Image();
img.addEventListener('load', () => {
URL.revokeObjectURL(url);
resolve(img);
});

img.addEventListener('error', (error) => {

Check warning on line 10 in imports/lib/blob/blobToImage.ts

View check run for this annotation

Codecov / codecov/patch

imports/lib/blob/blobToImage.ts#L9-L10

Added lines #L9 - L10 were not covered by tests
URL.revokeObjectURL(url);
reject(error);
});

Check warning on line 13 in imports/lib/blob/blobToImage.ts

View check run for this annotation

Codecov / codecov/patch

imports/lib/blob/blobToImage.ts#L13

Added line #L13 was not covered by tests

img.addEventListener('abort', (_e) => {
reject(new Error(`Image load aborted for ${url}`));
});

img.src = url;
});

export default blobToImage;
72 changes: 49 additions & 23 deletions imports/lib/pdf/pdfthumbmails.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ import {assert} from 'chai';

import {client, server, throws} from '../../_test/fixtures';
import {randomPDFUint8Array, randomPDFDataURI} from '../../_test/pdf';
import {
assertEqual as assertEqualImages,
whiteRectanglePNG,
} from '../../_test/image';

import dataURL from '../dataURL';
import blobFromDataURL from '../blob/blobFromDataURL';
import blobToBuffer from '../blob/blobToBuffer';
import streamToBuffer from '../stream/streamToBuffer';

import {
thumbnailDataURL,
Expand All @@ -15,29 +21,39 @@ import {
thumbnailBuffer,
} from './pdfthumbnails';

const width = 200;
const height = 200;

client(__filename, () => {
it('thumbnailDataURL should work', async () => {
const url = await randomPDFDataURI();
const result = await thumbnailDataURL(
url,
{minWidth: 10, minHeight: 10},
const pdfDataURL = await randomPDFDataURI();
const pngDataURL = await thumbnailDataURL(
pdfDataURL,
{minWidth: width, minHeight: height},
{type: 'image/png'},
);
const expected = dataURL(
'image/png',
'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAAXSURBVBhXY/wPBAxEAMZRhfhCifrBAwDBjyfjq7VgpgAAAABJRU5ErkJggg==',
);
assert.equal(result, expected);

assert.typeOf(pngDataURL, 'string');

const buffer = await blobFromDataURL(pngDataURL).then(blobToBuffer);
const expected = await whiteRectanglePNG({width, height});

await assertEqualImages(buffer, expected);
});

it('thumbnailBlob should work', async () => {
const data = randomPDFUint8Array();
const result = await thumbnailBlob(
const blob = await thumbnailBlob(
{data},
{minWidth: 200, minHeight: 200},
{minWidth: width, minHeight: height},
{type: 'image/png'},
);
assert.instanceOf(result, Blob);
assert.instanceOf(blob, Blob);

const buffer = await blobToBuffer(blob);
const expected = await whiteRectanglePNG({width, height});

await assertEqualImages(buffer, expected);
});

it('thumbnailBuffer should NOT be implemented', async () => {
Expand All @@ -46,7 +62,7 @@ client(__filename, () => {
async () =>
thumbnailBuffer(
{data},
{minWidth: 200, minHeight: 200},
{minWidth: width, minHeight: height},
{type: 'image/png'},
),
/not implemented/i,
Expand All @@ -59,7 +75,7 @@ client(__filename, () => {
async () =>
thumbnailStream(
{data},
{minWidth: 200, minHeight: 200},
{minWidth: width, minHeight: height},
{type: 'image/png'},
),
/not implemented/i,
Expand All @@ -75,7 +91,7 @@ server(__filename, () => {
async () =>
thumbnailDataURL(
url,
{minWidth: 200, minHeight: 200},
{minWidth: width, minHeight: height},
{type: 'image/png'},
),
/not implemented/i,
Expand All @@ -89,7 +105,7 @@ server(__filename, () => {
async () =>
thumbnailBlob(
{data},
{minWidth: 200, minHeight: 200},
{minWidth: width, minHeight: height},
{type: 'image/png'},
),
/not implemented/i,
Expand All @@ -98,21 +114,31 @@ server(__filename, () => {

it('thumbnailBuffer should work', async () => {
const data = randomPDFUint8Array();
const result = await thumbnailBuffer(
const buffer = await thumbnailBuffer(
{data},
{minWidth: 200, minHeight: 200},
{minWidth: width, minHeight: height},
{type: 'image/png'},
);
assert.instanceOf(result, Buffer);
assert.instanceOf(buffer, Buffer);

const expected = await whiteRectanglePNG({width, height});

await assertEqualImages(buffer, expected);
});

it('thumbnailStream should work', async () => {
const data = randomPDFUint8Array();
const result = await thumbnailStream(
const stream = await thumbnailStream(
{data},
{minWidth: 200, minHeight: 200},
{minWidth: width, minHeight: height},
{type: 'image/png'},
);
assert.instanceOf(result, Readable);
assert.instanceOf(stream, Readable);

const buffer = await streamToBuffer(stream);

const expected = await whiteRectanglePNG({width, height});

await assertEqualImages(buffer, expected);
});
});

0 comments on commit d8a48fc

Please sign in to comment.