@flighthq/image-codec 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/decodeImage.d.ts +4 -0
- package/dist/decodeImage.d.ts.map +1 -0
- package/dist/decodeImage.js +26 -0
- package/dist/decodeImage.js.map +1 -0
- package/dist/detectImageMimeType.d.ts +2 -0
- package/dist/detectImageMimeType.d.ts.map +1 -0
- package/dist/detectImageMimeType.js +33 -0
- package/dist/detectImageMimeType.js.map +1 -0
- package/dist/encodeImage.d.ts +3 -0
- package/dist/encodeImage.d.ts.map +1 -0
- package/dist/encodeImage.js +10 -0
- package/dist/encodeImage.js.map +1 -0
- package/dist/imageDecoderRegistry.d.ts +7 -0
- package/dist/imageDecoderRegistry.d.ts.map +1 -0
- package/dist/imageDecoderRegistry.js +20 -0
- package/dist/imageDecoderRegistry.js.map +1 -0
- package/dist/imageEncoderRegistry.d.ts +7 -0
- package/dist/imageEncoderRegistry.d.ts.map +1 -0
- package/dist/imageEncoderRegistry.js +20 -0
- package/dist/imageEncoderRegistry.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/registerWebImageDecoders.d.ts +2 -0
- package/dist/registerWebImageDecoders.d.ts.map +1 -0
- package/dist/registerWebImageDecoders.js +48 -0
- package/dist/registerWebImageDecoders.js.map +1 -0
- package/dist/registerWebImageEncoders.d.ts +2 -0
- package/dist/registerWebImageEncoders.d.ts.map +1 -0
- package/dist/registerWebImageEncoders.js +23 -0
- package/dist/registerWebImageEncoders.js.map +1 -0
- package/package.json +37 -0
- package/src/decodeImage.test.ts +63 -0
- package/src/detectImageMimeType.test.ts +60 -0
- package/src/encodeImage.test.ts +37 -0
- package/src/imageDecoderRegistry.test.ts +69 -0
- package/src/imageEncoderRegistry.test.ts +65 -0
- package/src/registerWebImageDecoders.test.ts +67 -0
- package/src/registerWebImageEncoders.test.ts +74 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DecodedImage } from '@flighthq/types';
|
|
2
|
+
export declare function decodeImage(bytes: Readonly<Uint8Array>, mimeType?: string): Promise<DecodedImage | null>;
|
|
3
|
+
export declare function decodeImagePremultiplied(bytes: Readonly<Uint8Array>, mimeType?: string): Promise<DecodedImage | null>;
|
|
4
|
+
//# sourceMappingURL=decodeImage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decodeImage.d.ts","sourceRoot":"","sources":["../src/decodeImage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAgB,MAAM,iBAAiB,CAAC;AAQlE,wBAAsB,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAI9G;AAID,wBAAsB,wBAAwB,CAC5C,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC3B,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAI9B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { detectImageMimeType } from './detectImageMimeType';
|
|
2
|
+
import { getImageDecoder } from './imageDecoderRegistry';
|
|
3
|
+
// Decodes encoded image bytes to straight (non-premultiplied) RGBA. Resolves the MIME type via
|
|
4
|
+
// detectImageMimeType when mimeType is omitted, then dispatches to the registered decoder. Returns null
|
|
5
|
+
// when the type cannot be determined or no decoder is registered for it.
|
|
6
|
+
export async function decodeImage(bytes, mimeType) {
|
|
7
|
+
const decoder = resolveImageDecoder(bytes, mimeType);
|
|
8
|
+
if (decoder === null)
|
|
9
|
+
return null;
|
|
10
|
+
return decoder(bytes);
|
|
11
|
+
}
|
|
12
|
+
// As decodeImage, but requests premultiplied RGBA via the decoder's premultiplyAlpha option. A decoder
|
|
13
|
+
// that can premultiply in one pass does so; the web/canvas decoder falls back to a JS premultiply pass.
|
|
14
|
+
export async function decodeImagePremultiplied(bytes, mimeType) {
|
|
15
|
+
const decoder = resolveImageDecoder(bytes, mimeType);
|
|
16
|
+
if (decoder === null)
|
|
17
|
+
return null;
|
|
18
|
+
return decoder(bytes, { premultiplyAlpha: true });
|
|
19
|
+
}
|
|
20
|
+
function resolveImageDecoder(bytes, mimeType) {
|
|
21
|
+
const type = mimeType ?? detectImageMimeType(bytes);
|
|
22
|
+
if (type === null)
|
|
23
|
+
return null;
|
|
24
|
+
return getImageDecoder(type);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=decodeImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decodeImage.js","sourceRoot":"","sources":["../src/decodeImage.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,+FAA+F;AAC/F,wGAAwG;AACxG,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAA2B,EAAE,QAAiB;IAC9E,MAAM,OAAO,GAAG,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACrD,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC;AAED,uGAAuG;AACvG,wGAAwG;AACxG,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,KAA2B,EAC3B,QAAiB;IAEjB,MAAM,OAAO,GAAG,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACrD,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,OAAO,CAAC,KAAK,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC;AACpD,CAAC;AAED,SAAS,mBAAmB,CAAC,KAA2B,EAAE,QAAiB;IACzE,MAAM,IAAI,GAAG,QAAQ,IAAI,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/B,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detectImageMimeType.d.ts","sourceRoot":"","sources":["../src/detectImageMimeType.ts"],"names":[],"mappings":"AAGA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,WAAW,GAAG,MAAM,GAAG,IAAI,CA+B3F"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Sniffs the MIME type of encoded image bytes from their magic-number header, or null when the header is
|
|
2
|
+
// too short or unrecognized. The single point of image-format identification in the SDK, used by
|
|
3
|
+
// decodeImage to auto-select a registered decoder and by @flighthq/image to type a Blob.
|
|
4
|
+
export function detectImageMimeType(data) {
|
|
5
|
+
const b = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
6
|
+
if (b.byteLength < 4)
|
|
7
|
+
return null;
|
|
8
|
+
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
9
|
+
if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47)
|
|
10
|
+
return 'image/png';
|
|
11
|
+
// JPEG: FF D8 FF
|
|
12
|
+
if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
|
|
13
|
+
return 'image/jpeg';
|
|
14
|
+
// GIF87a / GIF89a: 47 49 46 38
|
|
15
|
+
if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x38)
|
|
16
|
+
return 'image/gif';
|
|
17
|
+
// WebP: RIFF....WEBP (bytes 0-3 and 8-11)
|
|
18
|
+
if (b.byteLength >= 12 &&
|
|
19
|
+
b[0] === 0x52 &&
|
|
20
|
+
b[1] === 0x49 &&
|
|
21
|
+
b[2] === 0x46 &&
|
|
22
|
+
b[3] === 0x46 &&
|
|
23
|
+
b[8] === 0x57 &&
|
|
24
|
+
b[9] === 0x45 &&
|
|
25
|
+
b[10] === 0x42 &&
|
|
26
|
+
b[11] === 0x50)
|
|
27
|
+
return 'image/webp';
|
|
28
|
+
// BMP: 42 4D
|
|
29
|
+
if (b[0] === 0x42 && b[1] === 0x4d)
|
|
30
|
+
return 'image/bmp';
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=detectImageMimeType.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detectImageMimeType.js","sourceRoot":"","sources":["../src/detectImageMimeType.ts"],"names":[],"mappings":"AAAA,yGAAyG;AACzG,iGAAiG;AACjG,yFAAyF;AACzF,MAAM,UAAU,mBAAmB,CAAC,IAAwC;IAC1E,MAAM,CAAC,GAAG,IAAI,YAAY,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;IACnE,IAAI,CAAC,CAAC,UAAU,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAElC,+BAA+B;IAC/B,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,WAAW,CAAC;IAEzF,iBAAiB;IACjB,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,YAAY,CAAC;IAEzE,+BAA+B;IAC/B,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,WAAW,CAAC;IAEzF,0CAA0C;IAC1C,IACE,CAAC,CAAC,UAAU,IAAI,EAAE;QAClB,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QACb,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QACb,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QACb,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QACb,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QACb,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QACb,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI;QACd,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI;QAEd,OAAO,YAAY,CAAC;IAEtB,aAAa;IACb,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,WAAW,CAAC;IAEvD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encodeImage.d.ts","sourceRoot":"","sources":["../src/encodeImage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAMxE,wBAAsB,WAAW,CAC/B,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GACrC,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAI5B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getImageEncoder } from './imageEncoderRegistry';
|
|
2
|
+
// Encodes raw RGBA pixels to bytes of the given MIME type via the registered encoder. Returns null when
|
|
3
|
+
// no encoder is registered for the type.
|
|
4
|
+
export async function encodeImage(image, mimeType, options) {
|
|
5
|
+
const encoder = getImageEncoder(mimeType);
|
|
6
|
+
if (encoder === null)
|
|
7
|
+
return null;
|
|
8
|
+
return encoder(image, options);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=encodeImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encodeImage.js","sourceRoot":"","sources":["../src/encodeImage.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,wGAAwG;AACxG,yCAAyC;AACzC,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,KAA6B,EAC7B,QAAgB,EAChB,OAAsC;IAEtC,MAAM,OAAO,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IAC1C,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ImageDecoder } from '@flighthq/types';
|
|
2
|
+
export declare function clearImageDecoders(): void;
|
|
3
|
+
export declare function getImageDecoder(mimeType: string): ImageDecoder | null;
|
|
4
|
+
export declare function hasImageDecoder(mimeType: string): boolean;
|
|
5
|
+
export declare function registerImageDecoder(mimeType: string, decoder: ImageDecoder): void;
|
|
6
|
+
export declare function unregisterImageDecoder(mimeType: string): void;
|
|
7
|
+
//# sourceMappingURL=imageDecoderRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageDecoderRegistry.d.ts","sourceRoot":"","sources":["../src/imageDecoderRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAErE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAElF;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE7D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function clearImageDecoders() {
|
|
2
|
+
decoders.clear();
|
|
3
|
+
}
|
|
4
|
+
export function getImageDecoder(mimeType) {
|
|
5
|
+
return decoders.get(mimeType) ?? null;
|
|
6
|
+
}
|
|
7
|
+
export function hasImageDecoder(mimeType) {
|
|
8
|
+
return decoders.has(mimeType);
|
|
9
|
+
}
|
|
10
|
+
export function registerImageDecoder(mimeType, decoder) {
|
|
11
|
+
decoders.set(mimeType, decoder);
|
|
12
|
+
}
|
|
13
|
+
export function unregisterImageDecoder(mimeType) {
|
|
14
|
+
decoders.delete(mimeType);
|
|
15
|
+
}
|
|
16
|
+
// Global MIME-keyed decoder registry. Empty at import — only register* populates it, so importing this
|
|
17
|
+
// package has no side effects. Last-write-wins: a native host can override the web decoder for a MIME
|
|
18
|
+
// type by registering after registerWebImageDecoders.
|
|
19
|
+
const decoders = new Map();
|
|
20
|
+
//# sourceMappingURL=imageDecoderRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageDecoderRegistry.js","sourceRoot":"","sources":["../src/imageDecoderRegistry.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,kBAAkB;IAChC,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,OAAqB;IAC1E,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC5B,CAAC;AAED,uGAAuG;AACvG,sGAAsG;AACtG,sDAAsD;AACtD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ImageEncoder } from '@flighthq/types';
|
|
2
|
+
export declare function clearImageEncoders(): void;
|
|
3
|
+
export declare function getImageEncoder(mimeType: string): ImageEncoder | null;
|
|
4
|
+
export declare function hasImageEncoder(mimeType: string): boolean;
|
|
5
|
+
export declare function registerImageEncoder(mimeType: string, encoder: ImageEncoder): void;
|
|
6
|
+
export declare function unregisterImageEncoder(mimeType: string): void;
|
|
7
|
+
//# sourceMappingURL=imageEncoderRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageEncoderRegistry.d.ts","sourceRoot":"","sources":["../src/imageEncoderRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAErE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAElF;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE7D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function clearImageEncoders() {
|
|
2
|
+
encoders.clear();
|
|
3
|
+
}
|
|
4
|
+
export function getImageEncoder(mimeType) {
|
|
5
|
+
return encoders.get(mimeType) ?? null;
|
|
6
|
+
}
|
|
7
|
+
export function hasImageEncoder(mimeType) {
|
|
8
|
+
return encoders.has(mimeType);
|
|
9
|
+
}
|
|
10
|
+
export function registerImageEncoder(mimeType, encoder) {
|
|
11
|
+
encoders.set(mimeType, encoder);
|
|
12
|
+
}
|
|
13
|
+
export function unregisterImageEncoder(mimeType) {
|
|
14
|
+
encoders.delete(mimeType);
|
|
15
|
+
}
|
|
16
|
+
// Global MIME-keyed encoder registry. Empty at import — only register* populates it, so importing this
|
|
17
|
+
// package has no side effects. Last-write-wins: a native host can override the web encoder for a MIME
|
|
18
|
+
// type by registering after registerWebImageEncoders.
|
|
19
|
+
const encoders = new Map();
|
|
20
|
+
//# sourceMappingURL=imageEncoderRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageEncoderRegistry.js","sourceRoot":"","sources":["../src/imageEncoderRegistry.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,kBAAkB;IAChC,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,OAAqB;IAC1E,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC5B,CAAC;AAED,uGAAuG;AACvG,sGAAsG;AACtG,sDAAsD;AACtD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './decodeImage';
|
|
2
|
+
export * from './detectImageMimeType';
|
|
3
|
+
export * from './encodeImage';
|
|
4
|
+
export * from './imageDecoderRegistry';
|
|
5
|
+
export * from './imageEncoderRegistry';
|
|
6
|
+
export * from './registerWebImageDecoders';
|
|
7
|
+
export * from './registerWebImageEncoders';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './decodeImage';
|
|
2
|
+
export * from './detectImageMimeType';
|
|
3
|
+
export * from './encodeImage';
|
|
4
|
+
export * from './imageDecoderRegistry';
|
|
5
|
+
export * from './imageEncoderRegistry';
|
|
6
|
+
export * from './registerWebImageDecoders';
|
|
7
|
+
export * from './registerWebImageEncoders';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registerWebImageDecoders.d.ts","sourceRoot":"","sources":["../src/registerWebImageDecoders.ts"],"names":[],"mappings":"AAOA,wBAAgB,wBAAwB,IAAI,IAAI,CAI/C"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { registerImageDecoder } from './imageDecoderRegistry';
|
|
2
|
+
// Registers the browser's createImageBitmap + OffscreenCanvas decoder under every MIME type the platform
|
|
3
|
+
// can decode. Opt-in — nothing runs until the caller invokes this. A native host registers wasm codecs
|
|
4
|
+
// instead and never calls this.
|
|
5
|
+
export function registerWebImageDecoders() {
|
|
6
|
+
for (const mimeType of webDecodableMimeTypes) {
|
|
7
|
+
registerImageDecoder(mimeType, decodeImageWithCanvas);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
// One decoder shared across every browser-decodable MIME type: createImageBitmap sniffs the format from
|
|
11
|
+
// the bytes, so the registration key does not need to reach the Blob. getImageData is always straight
|
|
12
|
+
// (non-premultiplied) RGBA, so premultiplied output cannot come from a single canvas pass — when
|
|
13
|
+
// premultiplyAlpha is requested we decode straight, then premultiply the pixels in JS.
|
|
14
|
+
const decodeImageWithCanvas = async (bytes, options) => {
|
|
15
|
+
// slice() yields a fresh, non-readonly Uint8Array for Blob and detaches from any pooled input buffer.
|
|
16
|
+
const bitmap = await createImageBitmap(new Blob([bytes.slice()]));
|
|
17
|
+
const width = bitmap.width;
|
|
18
|
+
const height = bitmap.height;
|
|
19
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
20
|
+
const context = canvas.getContext('2d');
|
|
21
|
+
context.drawImage(bitmap, 0, 0);
|
|
22
|
+
bitmap.close();
|
|
23
|
+
const data = context.getImageData(0, 0, width, height).data;
|
|
24
|
+
if (options?.premultiplyAlpha === true)
|
|
25
|
+
premultiplyRgbaInPlace(data);
|
|
26
|
+
return { data, width, height };
|
|
27
|
+
};
|
|
28
|
+
// In-place straight → premultiplied RGBA: scale each color channel by alpha/255. Only the premultiplied
|
|
29
|
+
// decode path calls this, since canvas getImageData always yields straight alpha.
|
|
30
|
+
function premultiplyRgbaInPlace(data) {
|
|
31
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
32
|
+
const alpha = data[i + 3];
|
|
33
|
+
if (alpha === 255)
|
|
34
|
+
continue;
|
|
35
|
+
data[i] = (data[i] * alpha) / 255;
|
|
36
|
+
data[i + 1] = (data[i + 1] * alpha) / 255;
|
|
37
|
+
data[i + 2] = (data[i + 2] * alpha) / 255;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const webDecodableMimeTypes = [
|
|
41
|
+
'image/png',
|
|
42
|
+
'image/jpeg',
|
|
43
|
+
'image/webp',
|
|
44
|
+
'image/gif',
|
|
45
|
+
'image/avif',
|
|
46
|
+
'image/bmp',
|
|
47
|
+
];
|
|
48
|
+
//# sourceMappingURL=registerWebImageDecoders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registerWebImageDecoders.js","sourceRoot":"","sources":["../src/registerWebImageDecoders.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE9D,yGAAyG;AACzG,uGAAuG;AACvG,gCAAgC;AAChC,MAAM,UAAU,wBAAwB;IACtC,KAAK,MAAM,QAAQ,IAAI,qBAAqB,EAAE,CAAC;QAC7C,oBAAoB,CAAC,QAAQ,EAAE,qBAAqB,CAAC,CAAC;IACxD,CAAC;AACH,CAAC;AAED,wGAAwG;AACxG,sGAAsG;AACtG,iGAAiG;AACjG,uFAAuF;AACvF,MAAM,qBAAqB,GAAiB,KAAK,EAC/C,KAA2B,EAC3B,OAAsC,EACf,EAAE;IACzB,sGAAsG;IACtG,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAClE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAsC,CAAC;IAC7E,OAAO,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,CAAC;IACf,MAAM,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC;IAC5D,IAAI,OAAO,EAAE,gBAAgB,KAAK,IAAI;QAAE,sBAAsB,CAAC,IAAI,CAAC,CAAC;IACrE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AACjC,CAAC,CAAC;AAEF,wGAAwG;AACxG,kFAAkF;AAClF,SAAS,sBAAsB,CAAC,IAAuB;IACrD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1B,IAAI,KAAK,KAAK,GAAG;YAAE,SAAS;QAC5B,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC;QAClC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC;QAC1C,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC;IAC5C,CAAC;AACH,CAAC;AAED,MAAM,qBAAqB,GAAsB;IAC/C,WAAW;IACX,YAAY;IACZ,YAAY;IACZ,WAAW;IACX,YAAY;IACZ,WAAW;CACZ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registerWebImageEncoders.d.ts","sourceRoot":"","sources":["../src/registerWebImageEncoders.ts"],"names":[],"mappings":"AAMA,wBAAgB,wBAAwB,IAAI,IAAI,CAI/C"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { registerImageEncoder } from './imageEncoderRegistry';
|
|
2
|
+
// Registers the OffscreenCanvas + convertToBlob encoder under every MIME type the platform can encode.
|
|
3
|
+
// Opt-in — nothing runs until the caller invokes this. A native host registers wasm encoders instead.
|
|
4
|
+
export function registerWebImageEncoders() {
|
|
5
|
+
for (const mimeType of webEncodableMimeTypes) {
|
|
6
|
+
registerImageEncoder(mimeType, createCanvasImageEncoder(mimeType));
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
// Each encoder closes over its MIME type so convertToBlob emits that format (unlike decode, encode must
|
|
10
|
+
// name the target format). quality is forwarded as the lossy-format hint; lossless PNG ignores it.
|
|
11
|
+
function createCanvasImageEncoder(mimeType) {
|
|
12
|
+
return async (image, options) => {
|
|
13
|
+
const canvas = new OffscreenCanvas(image.width, image.height);
|
|
14
|
+
const context = canvas.getContext('2d');
|
|
15
|
+
// ImageData needs a mutable Uint8ClampedArray; copy so a Readonly input is honored and not aliased.
|
|
16
|
+
const pixels = new Uint8ClampedArray(image.data);
|
|
17
|
+
context.putImageData(new ImageData(pixels, image.width, image.height), 0, 0);
|
|
18
|
+
const blob = await canvas.convertToBlob({ type: mimeType, quality: options?.quality });
|
|
19
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const webEncodableMimeTypes = ['image/png', 'image/jpeg', 'image/webp'];
|
|
23
|
+
//# sourceMappingURL=registerWebImageEncoders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registerWebImageEncoders.js","sourceRoot":"","sources":["../src/registerWebImageEncoders.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE9D,uGAAuG;AACvG,sGAAsG;AACtG,MAAM,UAAU,wBAAwB;IACtC,KAAK,MAAM,QAAQ,IAAI,qBAAqB,EAAE,CAAC;QAC7C,oBAAoB,CAAC,QAAQ,EAAE,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrE,CAAC;AACH,CAAC;AAED,wGAAwG;AACxG,mGAAmG;AACnG,SAAS,wBAAwB,CAAC,QAAgB;IAChD,OAAO,KAAK,EAAE,KAA6B,EAAE,OAAsC,EAAuB,EAAE;QAC1G,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAC9D,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAsC,CAAC;QAC7E,oGAAoG;QACpG,MAAM,MAAM,GAAG,IAAI,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACjD,OAAO,CAAC,YAAY,CAAC,IAAI,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC7E,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QACvF,OAAO,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAClD,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,qBAAqB,GAAsB,CAAC,WAAW,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/image-codec",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/types": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.3.0"
|
|
34
|
+
},
|
|
35
|
+
"description": "DOM-free image decode/encode seam: MIME-keyed decoder/encoder registry over swappable per-format backends",
|
|
36
|
+
"sideEffects": false
|
|
37
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { DecodedImage, ImageDecodeOptions } from '@flighthq/types';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { decodeImage, decodeImagePremultiplied } from './decodeImage';
|
|
5
|
+
import { clearImageDecoders, registerImageDecoder } from './imageDecoderRegistry';
|
|
6
|
+
|
|
7
|
+
// PNG magic bytes so the omitted-mimeType path resolves through detectImageMimeType.
|
|
8
|
+
const pngBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
9
|
+
|
|
10
|
+
function fakeDecoded(): DecodedImage {
|
|
11
|
+
return { data: new Uint8ClampedArray([1, 2, 3, 4]), width: 1, height: 1 };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
clearImageDecoders();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('decodeImage', () => {
|
|
19
|
+
it('auto-detects the MIME type when omitted and dispatches to the registered decoder', async () => {
|
|
20
|
+
const decoder = vi.fn(async () => fakeDecoded());
|
|
21
|
+
registerImageDecoder('image/png', decoder);
|
|
22
|
+
const result = await decodeImage(pngBytes);
|
|
23
|
+
expect(result).toEqual(fakeDecoded());
|
|
24
|
+
expect(decoder).toHaveBeenCalledWith(pngBytes);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('uses an explicit MIME type over detection', async () => {
|
|
28
|
+
const decoder = vi.fn(async () => fakeDecoded());
|
|
29
|
+
registerImageDecoder('image/jpeg', decoder);
|
|
30
|
+
await decodeImage(pngBytes, 'image/jpeg');
|
|
31
|
+
expect(decoder).toHaveBeenCalledOnce();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns null when no decoder is registered', async () => {
|
|
35
|
+
expect(await decodeImage(pngBytes)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns null when the MIME type cannot be determined', async () => {
|
|
39
|
+
expect(await decodeImage(new Uint8Array([0, 1, 2, 3]))).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not request premultiplied output', async () => {
|
|
43
|
+
const decoder = vi.fn(async () => fakeDecoded());
|
|
44
|
+
registerImageDecoder('image/png', decoder);
|
|
45
|
+
await decodeImage(pngBytes);
|
|
46
|
+
expect(decoder).toHaveBeenCalledWith(pngBytes);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('decodeImagePremultiplied', () => {
|
|
51
|
+
it('passes premultiplyAlpha true to the decoder', async () => {
|
|
52
|
+
const decoder = vi.fn(async (_bytes: Readonly<Uint8Array>, _options?: Readonly<ImageDecodeOptions>) =>
|
|
53
|
+
fakeDecoded(),
|
|
54
|
+
);
|
|
55
|
+
registerImageDecoder('image/png', decoder);
|
|
56
|
+
await decodeImagePremultiplied(pngBytes);
|
|
57
|
+
expect(decoder).toHaveBeenCalledWith(pngBytes, { premultiplyAlpha: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns null when no decoder is registered', async () => {
|
|
61
|
+
expect(await decodeImagePremultiplied(pngBytes)).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { detectImageMimeType } from './detectImageMimeType';
|
|
2
|
+
|
|
3
|
+
describe('detectImageMimeType', () => {
|
|
4
|
+
it('returns null for a buffer that is too small', () => {
|
|
5
|
+
expect(detectImageMimeType(new ArrayBuffer(2))).toBeNull();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('returns null for an unrecognised header', () => {
|
|
9
|
+
const buf = new ArrayBuffer(16);
|
|
10
|
+
new Uint8Array(buf).set([0x00, 0x01, 0x02, 0x03]);
|
|
11
|
+
expect(detectImageMimeType(buf)).toBeNull();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('detects PNG', () => {
|
|
15
|
+
const buf = new ArrayBuffer(16);
|
|
16
|
+
new Uint8Array(buf).set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
17
|
+
expect(detectImageMimeType(buf)).toBe('image/png');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('detects JPEG', () => {
|
|
21
|
+
const buf = new ArrayBuffer(16);
|
|
22
|
+
new Uint8Array(buf).set([0xff, 0xd8, 0xff, 0xe0]);
|
|
23
|
+
expect(detectImageMimeType(buf)).toBe('image/jpeg');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('detects GIF', () => {
|
|
27
|
+
const buf = new ArrayBuffer(16);
|
|
28
|
+
new Uint8Array(buf).set([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]);
|
|
29
|
+
expect(detectImageMimeType(buf)).toBe('image/gif');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('detects WebP', () => {
|
|
33
|
+
const buf = new ArrayBuffer(16);
|
|
34
|
+
new Uint8Array(buf).set([
|
|
35
|
+
0x52,
|
|
36
|
+
0x49,
|
|
37
|
+
0x46,
|
|
38
|
+
0x46, // RIFF
|
|
39
|
+
0x00,
|
|
40
|
+
0x00,
|
|
41
|
+
0x00,
|
|
42
|
+
0x00, // size (ignored)
|
|
43
|
+
0x57,
|
|
44
|
+
0x45,
|
|
45
|
+
0x42,
|
|
46
|
+
0x50, // WEBP
|
|
47
|
+
]);
|
|
48
|
+
expect(detectImageMimeType(buf)).toBe('image/webp');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('detects BMP', () => {
|
|
52
|
+
const buf = new ArrayBuffer(16);
|
|
53
|
+
new Uint8Array(buf).set([0x42, 0x4d]);
|
|
54
|
+
expect(detectImageMimeType(buf)).toBe('image/bmp');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('accepts a Uint8Array directly', () => {
|
|
58
|
+
expect(detectImageMimeType(new Uint8Array([0x89, 0x50, 0x4e, 0x47]))).toBe('image/png');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DecodedImage } from '@flighthq/types';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { encodeImage } from './encodeImage';
|
|
5
|
+
import { clearImageEncoders, registerImageEncoder } from './imageEncoderRegistry';
|
|
6
|
+
|
|
7
|
+
function fakeImage(): DecodedImage {
|
|
8
|
+
return { data: new Uint8ClampedArray([1, 2, 3, 4]), width: 1, height: 1 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
clearImageEncoders();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('encodeImage', () => {
|
|
16
|
+
it('dispatches to the registered encoder and returns its bytes', async () => {
|
|
17
|
+
const bytes = new Uint8Array([9, 8, 7]);
|
|
18
|
+
const encoder = vi.fn(async () => bytes);
|
|
19
|
+
registerImageEncoder('image/png', encoder);
|
|
20
|
+
const image = fakeImage();
|
|
21
|
+
const result = await encodeImage(image, 'image/png');
|
|
22
|
+
expect(result).toBe(bytes);
|
|
23
|
+
expect(encoder).toHaveBeenCalledWith(image, undefined);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('forwards encode options to the encoder', async () => {
|
|
27
|
+
const encoder = vi.fn(async () => new Uint8Array(0));
|
|
28
|
+
registerImageEncoder('image/jpeg', encoder);
|
|
29
|
+
const image = fakeImage();
|
|
30
|
+
await encodeImage(image, 'image/jpeg', { quality: 0.5 });
|
|
31
|
+
expect(encoder).toHaveBeenCalledWith(image, { quality: 0.5 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns null when no encoder is registered', async () => {
|
|
35
|
+
expect(await encodeImage(fakeImage(), 'image/png')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { DecodedImage, ImageDecoder } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
clearImageDecoders,
|
|
5
|
+
getImageDecoder,
|
|
6
|
+
hasImageDecoder,
|
|
7
|
+
registerImageDecoder,
|
|
8
|
+
unregisterImageDecoder,
|
|
9
|
+
} from './imageDecoderRegistry';
|
|
10
|
+
|
|
11
|
+
const fakeDecoder: ImageDecoder = async (): Promise<DecodedImage> => ({
|
|
12
|
+
data: new Uint8ClampedArray(4),
|
|
13
|
+
width: 1,
|
|
14
|
+
height: 1,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
clearImageDecoders();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('clearImageDecoders', () => {
|
|
22
|
+
it('removes every registered decoder', () => {
|
|
23
|
+
registerImageDecoder('image/png', fakeDecoder);
|
|
24
|
+
registerImageDecoder('image/jpeg', fakeDecoder);
|
|
25
|
+
clearImageDecoders();
|
|
26
|
+
expect(hasImageDecoder('image/png')).toBe(false);
|
|
27
|
+
expect(hasImageDecoder('image/jpeg')).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('getImageDecoder', () => {
|
|
32
|
+
it('returns the registered decoder', () => {
|
|
33
|
+
registerImageDecoder('image/png', fakeDecoder);
|
|
34
|
+
expect(getImageDecoder('image/png')).toBe(fakeDecoder);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns null when no decoder is registered', () => {
|
|
38
|
+
expect(getImageDecoder('image/png')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('hasImageDecoder', () => {
|
|
43
|
+
it('reflects registration state', () => {
|
|
44
|
+
expect(hasImageDecoder('image/png')).toBe(false);
|
|
45
|
+
registerImageDecoder('image/png', fakeDecoder);
|
|
46
|
+
expect(hasImageDecoder('image/png')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('registerImageDecoder', () => {
|
|
51
|
+
it('is empty until called', () => {
|
|
52
|
+
expect(hasImageDecoder('image/png')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('overwrites an existing decoder (last-write-wins)', () => {
|
|
56
|
+
const other: ImageDecoder = async () => ({ data: new Uint8ClampedArray(4), width: 1, height: 1 });
|
|
57
|
+
registerImageDecoder('image/png', fakeDecoder);
|
|
58
|
+
registerImageDecoder('image/png', other);
|
|
59
|
+
expect(getImageDecoder('image/png')).toBe(other);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('unregisterImageDecoder', () => {
|
|
64
|
+
it('removes a single decoder', () => {
|
|
65
|
+
registerImageDecoder('image/png', fakeDecoder);
|
|
66
|
+
unregisterImageDecoder('image/png');
|
|
67
|
+
expect(hasImageDecoder('image/png')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ImageEncoder } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
clearImageEncoders,
|
|
5
|
+
getImageEncoder,
|
|
6
|
+
hasImageEncoder,
|
|
7
|
+
registerImageEncoder,
|
|
8
|
+
unregisterImageEncoder,
|
|
9
|
+
} from './imageEncoderRegistry';
|
|
10
|
+
|
|
11
|
+
const fakeEncoder: ImageEncoder = async (): Promise<Uint8Array> => new Uint8Array(0);
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
clearImageEncoders();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('clearImageEncoders', () => {
|
|
18
|
+
it('removes every registered encoder', () => {
|
|
19
|
+
registerImageEncoder('image/png', fakeEncoder);
|
|
20
|
+
registerImageEncoder('image/jpeg', fakeEncoder);
|
|
21
|
+
clearImageEncoders();
|
|
22
|
+
expect(hasImageEncoder('image/png')).toBe(false);
|
|
23
|
+
expect(hasImageEncoder('image/jpeg')).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('getImageEncoder', () => {
|
|
28
|
+
it('returns the registered encoder', () => {
|
|
29
|
+
registerImageEncoder('image/png', fakeEncoder);
|
|
30
|
+
expect(getImageEncoder('image/png')).toBe(fakeEncoder);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns null when no encoder is registered', () => {
|
|
34
|
+
expect(getImageEncoder('image/png')).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('hasImageEncoder', () => {
|
|
39
|
+
it('reflects registration state', () => {
|
|
40
|
+
expect(hasImageEncoder('image/png')).toBe(false);
|
|
41
|
+
registerImageEncoder('image/png', fakeEncoder);
|
|
42
|
+
expect(hasImageEncoder('image/png')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('registerImageEncoder', () => {
|
|
47
|
+
it('is empty until called', () => {
|
|
48
|
+
expect(hasImageEncoder('image/png')).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('overwrites an existing encoder (last-write-wins)', () => {
|
|
52
|
+
const other: ImageEncoder = async () => new Uint8Array(1);
|
|
53
|
+
registerImageEncoder('image/png', fakeEncoder);
|
|
54
|
+
registerImageEncoder('image/png', other);
|
|
55
|
+
expect(getImageEncoder('image/png')).toBe(other);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('unregisterImageEncoder', () => {
|
|
60
|
+
it('removes a single encoder', () => {
|
|
61
|
+
registerImageEncoder('image/png', fakeEncoder);
|
|
62
|
+
unregisterImageEncoder('image/png');
|
|
63
|
+
expect(hasImageEncoder('image/png')).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { clearImageDecoders, getImageDecoder, hasImageDecoder } from './imageDecoderRegistry';
|
|
4
|
+
import { registerWebImageDecoders } from './registerWebImageDecoders';
|
|
5
|
+
|
|
6
|
+
// jsdom / node lack createImageBitmap + OffscreenCanvas + Blob; these minimal stand-ins let the
|
|
7
|
+
// canvas decoder run. getImageData always returns straight (non-premultiplied) RGBA, matching the browser.
|
|
8
|
+
const straightPixels = [200, 100, 50, 128];
|
|
9
|
+
|
|
10
|
+
class MockOffscreenCanvas {
|
|
11
|
+
constructor(
|
|
12
|
+
readonly width: number,
|
|
13
|
+
readonly height: number,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
getContext(): unknown {
|
|
17
|
+
return {
|
|
18
|
+
drawImage: vi.fn(),
|
|
19
|
+
getImageData: () => ({ data: new Uint8ClampedArray(straightPixels) }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.stubGlobal(
|
|
26
|
+
'createImageBitmap',
|
|
27
|
+
vi.fn(async () => ({ width: 1, height: 1, close: vi.fn() })),
|
|
28
|
+
);
|
|
29
|
+
vi.stubGlobal('OffscreenCanvas', MockOffscreenCanvas);
|
|
30
|
+
vi.stubGlobal(
|
|
31
|
+
'Blob',
|
|
32
|
+
class {
|
|
33
|
+
constructor(readonly parts: readonly unknown[]) {}
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.unstubAllGlobals();
|
|
40
|
+
clearImageDecoders();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('registerWebImageDecoders', () => {
|
|
44
|
+
it('registers a decoder under every browser-decodable MIME type', () => {
|
|
45
|
+
registerWebImageDecoders();
|
|
46
|
+
for (const mimeType of ['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/avif', 'image/bmp']) {
|
|
47
|
+
expect(hasImageDecoder(mimeType)).toBe(true);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('decodes to straight (non-premultiplied) RGBA', async () => {
|
|
52
|
+
registerWebImageDecoders();
|
|
53
|
+
const decoder = getImageDecoder('image/png');
|
|
54
|
+
const result = await decoder!(new Uint8Array([0x89, 0x50, 0x4e, 0x47]));
|
|
55
|
+
expect(result.width).toBe(1);
|
|
56
|
+
expect(result.height).toBe(1);
|
|
57
|
+
expect(Array.from(result.data)).toEqual(straightPixels);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('premultiplies the straight result when premultiplyAlpha is requested', async () => {
|
|
61
|
+
registerWebImageDecoders();
|
|
62
|
+
const decoder = getImageDecoder('image/png');
|
|
63
|
+
const result = await decoder!(new Uint8Array([0x89, 0x50, 0x4e, 0x47]), { premultiplyAlpha: true });
|
|
64
|
+
// 200*128/255 -> 100, 100*128/255 -> 50, 50*128/255 -> 25, alpha unchanged.
|
|
65
|
+
expect(Array.from(result.data)).toEqual([100, 50, 25, 128]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { DecodedImage } from '@flighthq/types';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { clearImageEncoders, getImageEncoder, hasImageEncoder } from './imageEncoderRegistry';
|
|
5
|
+
import { registerWebImageEncoders } from './registerWebImageEncoders';
|
|
6
|
+
|
|
7
|
+
// node lacks OffscreenCanvas + ImageData; these stand-ins record the convertToBlob call and hand back
|
|
8
|
+
// fixed bytes so the encoder's byte extraction can be verified.
|
|
9
|
+
const convertToBlobMock = vi.fn(async (_options: { type: string; quality?: number }) => ({
|
|
10
|
+
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
class MockOffscreenCanvas {
|
|
14
|
+
constructor(
|
|
15
|
+
readonly width: number,
|
|
16
|
+
readonly height: number,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
getContext(): unknown {
|
|
20
|
+
return { putImageData: vi.fn() };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
convertToBlob(options: { type: string; quality?: number }): Promise<{ arrayBuffer: () => Promise<ArrayBuffer> }> {
|
|
24
|
+
return convertToBlobMock(options);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fakeImage(): DecodedImage {
|
|
29
|
+
return { data: new Uint8ClampedArray([10, 20, 30, 255]), width: 1, height: 1 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.stubGlobal('OffscreenCanvas', MockOffscreenCanvas);
|
|
34
|
+
vi.stubGlobal(
|
|
35
|
+
'ImageData',
|
|
36
|
+
class {
|
|
37
|
+
constructor(
|
|
38
|
+
readonly data: Uint8ClampedArray,
|
|
39
|
+
readonly width: number,
|
|
40
|
+
readonly height: number,
|
|
41
|
+
) {}
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.unstubAllGlobals();
|
|
48
|
+
convertToBlobMock.mockClear();
|
|
49
|
+
clearImageEncoders();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('registerWebImageEncoders', () => {
|
|
53
|
+
it('registers an encoder under every browser-encodable MIME type', () => {
|
|
54
|
+
registerWebImageEncoders();
|
|
55
|
+
for (const mimeType of ['image/png', 'image/jpeg', 'image/webp']) {
|
|
56
|
+
expect(hasImageEncoder(mimeType)).toBe(true);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('encodes to bytes via convertToBlob under the registered MIME type', async () => {
|
|
61
|
+
registerWebImageEncoders();
|
|
62
|
+
const encoder = getImageEncoder('image/png');
|
|
63
|
+
const bytes = await encoder!(fakeImage());
|
|
64
|
+
expect(Array.from(bytes)).toEqual([1, 2, 3]);
|
|
65
|
+
expect(convertToBlobMock).toHaveBeenCalledWith({ type: 'image/png', quality: undefined });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('forwards the quality option to convertToBlob', async () => {
|
|
69
|
+
registerWebImageEncoders();
|
|
70
|
+
const encoder = getImageEncoder('image/jpeg');
|
|
71
|
+
await encoder!(fakeImage(), { quality: 0.7 });
|
|
72
|
+
expect(convertToBlobMock).toHaveBeenCalledWith({ type: 'image/jpeg', quality: 0.7 });
|
|
73
|
+
});
|
|
74
|
+
});
|