The format pre-pass and the importer read the same inflated archive, where each unzipped the file for itself, and a picture is encoded through the browser's Uint8Array.toBase64, the chunked loop standing in where there is none. On a 14 MB file holding 600 pictures that takes 250 ms of inflating and 285 ms of encoding off the open: it lands 480 ms earlier and settles 240 ms earlier, and a small illustrated file lands 170 ms earlier.
92 lines
4.1 KiB
TypeScript
92 lines
4.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { zipSync } from 'fflate';
|
|
import { displayableImageMime, imageDataUrl, imageExtOf, isConvertibleImage, unzipArchive } from '../../src/lib/import/imageFormats';
|
|
|
|
const bytesOf = (...b: number[]) => new Uint8Array(b);
|
|
const PNG = bytesOf(0x89, 0x50, 0x4e, 0x47, 0, 0, 0, 0);
|
|
const JPEG = bytesOf(0xff, 0xd8, 0xff, 0xe0, 0, 0, 0, 0);
|
|
const BMP = bytesOf(0x42, 0x4d, 0, 0, 0, 0, 0, 0);
|
|
const SVM = bytesOf(0x56, 0x43, 0x4c, 0x4d, 0x54, 0x46, 1, 0); // "VCLMTF"
|
|
const TIFF = bytesOf(0x49, 0x49, 0x2a, 0x00, 0, 0, 0, 0);
|
|
const WEBP = bytesOf(0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50);
|
|
|
|
describe('imageExtOf', () => {
|
|
it('lowercases and strips query/fragment', () => {
|
|
expect(imageExtOf('Pictures/A.PNG')).toBe('png');
|
|
expect(imageExtOf('word/media/image1.jpeg?x=1')).toBe('jpeg');
|
|
expect(imageExtOf('noext')).toBe('noext');
|
|
});
|
|
});
|
|
|
|
describe('displayableImageMime', () => {
|
|
it('resolves renderable formats by extension', () => {
|
|
expect(displayableImageMime(PNG, 'a.png')).toBe('image/png');
|
|
expect(displayableImageMime(JPEG, 'a.jfif')).toBe('image/jpeg');
|
|
expect(displayableImageMime(new Uint8Array(4), 'a.avif')).toBe('image/avif');
|
|
});
|
|
it('returns null for formats the browser cannot display', () => {
|
|
expect(displayableImageMime(SVM, 'a.svm')).toBeNull();
|
|
expect(displayableImageMime(TIFF, 'a.tiff')).toBeNull();
|
|
expect(displayableImageMime(bytesOf(0,0,0,0), 'a.wmf')).toBeNull();
|
|
expect(displayableImageMime(bytesOf(0,0,0,0), 'a.emf')).toBeNull();
|
|
});
|
|
it('recovers a renderable format from magic bytes when the extension is unknown', () => {
|
|
expect(displayableImageMime(PNG, 'blob')).toBe('image/png');
|
|
expect(displayableImageMime(BMP, 'Pictures/x')).toBe('image/bmp');
|
|
expect(displayableImageMime(WEBP, 'x')).toBe('image/webp');
|
|
});
|
|
it('does not mislabel a metafile as a bitmap when the extension is unknown', () => {
|
|
expect(displayableImageMime(SVM, 'Pictures/x')).toBeNull();
|
|
});
|
|
});
|
|
|
|
const TIFF_LE = bytesOf(0x49, 0x49, 0x2a, 0x00, 8, 0, 0, 0);
|
|
const TIFF_BE = bytesOf(0x4d, 0x4d, 0x00, 0x2a, 0, 0, 0, 8);
|
|
|
|
describe('isConvertibleImage', () => {
|
|
it('flags TIFF (by extension or magic) as client-side convertible', () => {
|
|
expect(isConvertibleImage(TIFF_LE, 'scan.tiff')).toBe(true);
|
|
expect(isConvertibleImage(TIFF_BE, 'scan.tif')).toBe(true);
|
|
expect(isConvertibleImage(TIFF_LE, 'noext')).toBe(true); // magic-byte fallback
|
|
});
|
|
it('does not flag renderable or truly-unsupported formats', () => {
|
|
expect(isConvertibleImage(PNG, 'a.png')).toBe(false); // already displayable
|
|
expect(isConvertibleImage(SVM, 'a.svm')).toBe(false); // no client-side decoder
|
|
expect(isConvertibleImage(bytesOf(0, 0, 0, 0), 'a.wmf')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('imageDataUrl', () => {
|
|
it('emits a data-URI for renderable bytes, null otherwise', () => {
|
|
expect(imageDataUrl(PNG, 'a.png')).toMatch(/^data:image\/png;base64,/);
|
|
expect(imageDataUrl(SVM, 'a.svm')).toBeNull();
|
|
});
|
|
|
|
// The encoder the browser brings and the loop that stands in for it must agree,
|
|
// padding included — a picture is bytes, and a wrong tail is a broken picture.
|
|
it('encodes alike with and without the native encoder', () => {
|
|
const proto = Uint8Array.prototype as Uint8Array & { toBase64?: () => string };
|
|
const native = proto.toBase64;
|
|
for (const len of [70000, 70001, 70002]) {
|
|
const bytes = new Uint8Array(len).map((_, i) => (i * 7) % 256);
|
|
const want = `data:image/png;base64,${Buffer.from(bytes).toString('base64')}`;
|
|
expect(imageDataUrl(bytes, 'a.png')).toBe(want);
|
|
try {
|
|
delete proto.toBase64;
|
|
expect(imageDataUrl(bytes, 'a.png')).toBe(want);
|
|
} finally {
|
|
if (native) proto.toBase64 = native;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('unzipArchive', () => {
|
|
it('inflates one archive once: the pre-pass and the importer share the entries', () => {
|
|
const zip = zipSync({ 'content.xml': new Uint8Array([1, 2, 3]) });
|
|
const once = unzipArchive(zip);
|
|
expect(unzipArchive(zip)).toBe(once);
|
|
expect(unzipArchive(zipSync({ 'content.xml': new Uint8Array([1, 2, 3]) }))).not.toBe(once);
|
|
});
|
|
});
|