@flighthq/image 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/imageResource.d.ts +11 -0
- package/dist/imageResource.d.ts.map +1 -0
- package/dist/imageResource.js +84 -0
- package/dist/imageResource.js.map +1 -0
- package/dist/imageResourceFrom.d.ts +11 -0
- package/dist/imageResourceFrom.d.ts.map +1 -0
- package/dist/imageResourceFrom.js +112 -0
- package/dist/imageResourceFrom.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/imageResource.test.ts +169 -0
- package/src/imageResourceFrom.test.ts +273 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ImageResource } from '@flighthq/types';
|
|
2
|
+
export declare function cloneImageResource(resource: Readonly<ImageResource>): ImageResource;
|
|
3
|
+
export declare function createImageResource(image?: CanvasImageSource): ImageResource;
|
|
4
|
+
export declare function disposeImageResource(resource: ImageResource): void;
|
|
5
|
+
export declare function getImageResourceByteSize(resource: Readonly<ImageResource>): number;
|
|
6
|
+
export declare function hasImageResourceData(resource: Readonly<ImageResource>): boolean;
|
|
7
|
+
export declare function hasImageResourceSource(resource: Readonly<ImageResource>): boolean;
|
|
8
|
+
export declare function invalidateImageResource(resource: ImageResource): void;
|
|
9
|
+
export declare function isImageResourceEmpty(resource: Readonly<ImageResource>): boolean;
|
|
10
|
+
export declare function setImageResourceSource(resource: ImageResource, element: CanvasImageSource | null): void;
|
|
11
|
+
//# sourceMappingURL=imageResource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageResource.d.ts","sourceRoot":"","sources":["../src/imageResource.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAMrD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,aAAa,CAUnF;AAED,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE,iBAAiB,GAAG,aAAa,CAY5E;AAMD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAIlE;AAMD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAElF;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,OAAO,CAE/E;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,OAAO,CAEjF;AAKD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAErE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,OAAO,CAE/E;AAID,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAIvG"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
|
+
// Allocates a new resource identity over the SAME underlying pixels. The element and the `data` array
|
|
3
|
+
// are shared by reference, not duplicated — clone gives you an independent version counter and entity
|
|
4
|
+
// identity over the same pixels, e.g. to upload one image into two render states with separate
|
|
5
|
+
// invalidation. Use a Surface copy when you need the pixels themselves duplicated.
|
|
6
|
+
export function cloneImageResource(resource) {
|
|
7
|
+
return createEntity({
|
|
8
|
+
alphaType: resource.alphaType,
|
|
9
|
+
data: resource.data,
|
|
10
|
+
format: resource.format,
|
|
11
|
+
height: resource.height,
|
|
12
|
+
source: resource.source,
|
|
13
|
+
version: resource.version,
|
|
14
|
+
width: resource.width,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function createImageResource(image) {
|
|
18
|
+
const resource = createEntity({
|
|
19
|
+
alphaType: 'straight',
|
|
20
|
+
data: null,
|
|
21
|
+
format: 'rgba8unorm',
|
|
22
|
+
height: 0,
|
|
23
|
+
source: image ?? null,
|
|
24
|
+
version: 0,
|
|
25
|
+
width: 0,
|
|
26
|
+
});
|
|
27
|
+
if (resource.source !== null)
|
|
28
|
+
updateImageResourceSize(resource);
|
|
29
|
+
return resource;
|
|
30
|
+
}
|
|
31
|
+
// Releases the element and raw-data references so they become eligible for GC, and marks the resource
|
|
32
|
+
// changed. This does NOT free the backend GPU texture (that is owned per render state — call the
|
|
33
|
+
// backend's destroy*Texture) and does NOT close an owned ImageBitmap (ownership is ambiguous; close it
|
|
34
|
+
// explicitly if you own it). Dimensions are left intact; setImageResourceSource repopulates the resource.
|
|
35
|
+
export function disposeImageResource(resource) {
|
|
36
|
+
resource.data = null;
|
|
37
|
+
resource.source = null;
|
|
38
|
+
invalidateImageResource(resource);
|
|
39
|
+
}
|
|
40
|
+
// Returns the byte footprint of the CPU-side pixel data (`data` array). Returns 0 when `data` is
|
|
41
|
+
// null (element-only resource — the GPU texture footprint is tracked by the render state, not here).
|
|
42
|
+
// For `rgba8unorm` (the default) the result equals width × height × 4; other formats are not yet
|
|
43
|
+
// exercised but the formula still applies (byteLength of the actual buffer is authoritative).
|
|
44
|
+
export function getImageResourceByteSize(resource) {
|
|
45
|
+
return resource.data !== null ? resource.data.byteLength : 0;
|
|
46
|
+
}
|
|
47
|
+
export function hasImageResourceData(resource) {
|
|
48
|
+
return resource.data !== null;
|
|
49
|
+
}
|
|
50
|
+
export function hasImageResourceSource(resource) {
|
|
51
|
+
return resource.source !== null;
|
|
52
|
+
}
|
|
53
|
+
// Bumps the resource content revision so consumers (renderer texture caches) know the pixels behind
|
|
54
|
+
// this image changed even though the object identity is the same. Call after mutating the backing
|
|
55
|
+
// pixels in place; the Surface API calls it for you. Resource-tier analog of invalidateNodeLocalContent.
|
|
56
|
+
export function invalidateImageResource(resource) {
|
|
57
|
+
resource.version = (resource.version + 1) >>> 0;
|
|
58
|
+
}
|
|
59
|
+
export function isImageResourceEmpty(resource) {
|
|
60
|
+
return resource.width <= 0 || resource.height <= 0;
|
|
61
|
+
}
|
|
62
|
+
// Swaps the element representation, re-reads its dimensions, and marks the resource changed. Pass
|
|
63
|
+
// null to clear the element (dimensions are left intact; use disposeImageResource to release for GC).
|
|
64
|
+
export function setImageResourceSource(resource, element) {
|
|
65
|
+
resource.source = element;
|
|
66
|
+
if (element !== null)
|
|
67
|
+
updateImageResourceSize(resource);
|
|
68
|
+
invalidateImageResource(resource);
|
|
69
|
+
}
|
|
70
|
+
// Reads pixel dimensions from the current (non-null) element. Video sources carry their size on
|
|
71
|
+
// videoWidth/videoHeight; every other CanvasImageSource exposes width/height directly.
|
|
72
|
+
function updateImageResourceSize(resource) {
|
|
73
|
+
const element = resource.source;
|
|
74
|
+
if (element instanceof HTMLVideoElement) {
|
|
75
|
+
resource.width = element.videoWidth;
|
|
76
|
+
resource.height = element.videoHeight;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const sized = element;
|
|
80
|
+
resource.width = sized.width;
|
|
81
|
+
resource.height = sized.height;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=imageResource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageResource.js","sourceRoot":"","sources":["../src/imageResource.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ImageResource } from '@flighthq/types';
|
|
2
|
+
export declare function createImageResourceFromCanvas(canvas: HTMLCanvasElement): ImageResource;
|
|
3
|
+
export declare function createImageResourceFromImageBitmap(bitmap: ImageBitmap): ImageResource;
|
|
4
|
+
export declare function createImageResourceFromImageElement(img: HTMLImageElement): ImageResource;
|
|
5
|
+
export declare function detectImageMimeType(data: ArrayBuffer | Uint8Array): string | null;
|
|
6
|
+
export declare function isImageResourceSameOrigin(url: string): boolean;
|
|
7
|
+
export declare function loadImageResourceFromBase64(base64: string, mimeType: string, signal?: AbortSignal): Promise<ImageResource>;
|
|
8
|
+
export declare function loadImageResourceFromBlob(blob: Blob, signal?: AbortSignal): Promise<ImageResource>;
|
|
9
|
+
export declare function loadImageResourceFromBytes(bytes: Uint8Array, mimeType?: string, signal?: AbortSignal): Promise<ImageResource>;
|
|
10
|
+
export declare function loadImageResourceFromUrl(url: string, crossOrigin?: string, signal?: AbortSignal): Promise<ImageResource>;
|
|
11
|
+
//# sourceMappingURL=imageResourceFrom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageResourceFrom.d.ts","sourceRoot":"","sources":["../src/imageResourceFrom.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,iBAAiB,GAAG,aAAa,CAUtF;AAED,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa,CAUrF;AAED,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,gBAAgB,GAAG,aAAa,CAUxF;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,CA+BjF;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAO9D;AAED,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAOxG;AAED,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,UAAU,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAOxB;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,MAAM,EACX,WAAW,CAAC,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAexB"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
|
+
export function createImageResourceFromCanvas(canvas) {
|
|
3
|
+
return createEntity({
|
|
4
|
+
alphaType: 'straight',
|
|
5
|
+
data: null,
|
|
6
|
+
format: 'rgba8unorm',
|
|
7
|
+
height: canvas.height,
|
|
8
|
+
source: canvas,
|
|
9
|
+
version: 0,
|
|
10
|
+
width: canvas.width,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function createImageResourceFromImageBitmap(bitmap) {
|
|
14
|
+
return createEntity({
|
|
15
|
+
alphaType: 'straight',
|
|
16
|
+
data: null,
|
|
17
|
+
format: 'rgba8unorm',
|
|
18
|
+
height: bitmap.height,
|
|
19
|
+
source: bitmap,
|
|
20
|
+
version: 0,
|
|
21
|
+
width: bitmap.width,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function createImageResourceFromImageElement(img) {
|
|
25
|
+
return createEntity({
|
|
26
|
+
alphaType: 'straight',
|
|
27
|
+
data: null,
|
|
28
|
+
format: 'rgba8unorm',
|
|
29
|
+
height: img.height,
|
|
30
|
+
source: img,
|
|
31
|
+
version: 0,
|
|
32
|
+
width: img.width,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function detectImageMimeType(data) {
|
|
36
|
+
const b = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
37
|
+
if (b.byteLength < 4)
|
|
38
|
+
return null;
|
|
39
|
+
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
40
|
+
if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47)
|
|
41
|
+
return 'image/png';
|
|
42
|
+
// JPEG: FF D8 FF
|
|
43
|
+
if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
|
|
44
|
+
return 'image/jpeg';
|
|
45
|
+
// GIF87a / GIF89a: 47 49 46 38
|
|
46
|
+
if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x38)
|
|
47
|
+
return 'image/gif';
|
|
48
|
+
// WebP: RIFF....WEBP (bytes 0-3 and 8-11)
|
|
49
|
+
if (b.byteLength >= 12 &&
|
|
50
|
+
b[0] === 0x52 &&
|
|
51
|
+
b[1] === 0x49 &&
|
|
52
|
+
b[2] === 0x46 &&
|
|
53
|
+
b[3] === 0x46 &&
|
|
54
|
+
b[8] === 0x57 &&
|
|
55
|
+
b[9] === 0x45 &&
|
|
56
|
+
b[10] === 0x42 &&
|
|
57
|
+
b[11] === 0x50)
|
|
58
|
+
return 'image/webp';
|
|
59
|
+
// BMP: 42 4D
|
|
60
|
+
if (b[0] === 0x42 && b[1] === 0x4d)
|
|
61
|
+
return 'image/bmp';
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
export function isImageResourceSameOrigin(url) {
|
|
65
|
+
if (url.startsWith('data:') || url.startsWith('blob:'))
|
|
66
|
+
return true;
|
|
67
|
+
try {
|
|
68
|
+
return new URL(url, location.href).origin === location.origin;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function loadImageResourceFromBase64(base64, mimeType, signal) {
|
|
75
|
+
return loadImageResourceFromUrl(`data:${mimeType};base64,${base64}`, undefined, signal);
|
|
76
|
+
}
|
|
77
|
+
export async function loadImageResourceFromBlob(blob, signal) {
|
|
78
|
+
const url = URL.createObjectURL(blob);
|
|
79
|
+
try {
|
|
80
|
+
return await loadImageResourceFromUrl(url, undefined, signal);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
URL.revokeObjectURL(url);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function loadImageResourceFromBytes(bytes, mimeType, signal) {
|
|
87
|
+
const type = mimeType ?? detectImageMimeType(bytes);
|
|
88
|
+
if (type === null) {
|
|
89
|
+
throw new Error('Unable to determine image type from bytes');
|
|
90
|
+
}
|
|
91
|
+
const buf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
92
|
+
return loadImageResourceFromBlob(new Blob([buf], { type }), signal);
|
|
93
|
+
}
|
|
94
|
+
export async function loadImageResourceFromUrl(url, crossOrigin, signal) {
|
|
95
|
+
signal?.throwIfAborted();
|
|
96
|
+
const img = new Image();
|
|
97
|
+
if (crossOrigin !== undefined)
|
|
98
|
+
img.crossOrigin = crossOrigin;
|
|
99
|
+
img.src = url;
|
|
100
|
+
// Wire abort to cancel the pending decode by rejecting the promise.
|
|
101
|
+
if (signal !== undefined) {
|
|
102
|
+
await Promise.race([
|
|
103
|
+
img.decode(),
|
|
104
|
+
new Promise((_, reject) => signal.addEventListener('abort', () => reject(signal.reason), { once: true })),
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
await img.decode();
|
|
109
|
+
}
|
|
110
|
+
return createImageResourceFromImageElement(img);
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=imageResourceFrom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageResourceFrom.js","sourceRoot":"","sources":["../src/imageResourceFrom.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/image",
|
|
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/entity": "0.1.0",
|
|
31
|
+
"@flighthq/types": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.3.0"
|
|
35
|
+
},
|
|
36
|
+
"description": "Image resources: pixel sources, MIME detection, and constructors from canvas/bitmap/element",
|
|
37
|
+
"sideEffects": false
|
|
38
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneImageResource,
|
|
3
|
+
createImageResource,
|
|
4
|
+
disposeImageResource,
|
|
5
|
+
getImageResourceByteSize,
|
|
6
|
+
hasImageResourceData,
|
|
7
|
+
hasImageResourceSource,
|
|
8
|
+
invalidateImageResource,
|
|
9
|
+
isImageResourceEmpty,
|
|
10
|
+
setImageResourceSource,
|
|
11
|
+
} from './imageResource';
|
|
12
|
+
|
|
13
|
+
describe('cloneImageResource', () => {
|
|
14
|
+
it('copies fields, shares the element, and gives an independent identity', () => {
|
|
15
|
+
const element = { width: 4, height: 5 } as HTMLImageElement;
|
|
16
|
+
const resource = createImageResource(element);
|
|
17
|
+
resource.version = 3;
|
|
18
|
+
|
|
19
|
+
const copy = cloneImageResource(resource);
|
|
20
|
+
|
|
21
|
+
expect(copy).not.toBe(resource);
|
|
22
|
+
expect(copy.source).toBe(element);
|
|
23
|
+
expect(copy.width).toStrictEqual(4);
|
|
24
|
+
expect(copy.height).toStrictEqual(5);
|
|
25
|
+
expect(copy.version).toStrictEqual(3);
|
|
26
|
+
|
|
27
|
+
copy.version++;
|
|
28
|
+
expect(resource.version).toStrictEqual(3);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('shares the data buffer by reference and copies the format and alphaType', () => {
|
|
32
|
+
const resource = createImageResource();
|
|
33
|
+
resource.data = new Uint8ClampedArray([1, 2, 3, 4]);
|
|
34
|
+
resource.format = 'bgra8unorm';
|
|
35
|
+
resource.alphaType = 'premultiplied';
|
|
36
|
+
|
|
37
|
+
const copy = cloneImageResource(resource);
|
|
38
|
+
|
|
39
|
+
expect(copy.data).toBe(resource.data);
|
|
40
|
+
expect(copy.format).toStrictEqual('bgra8unorm');
|
|
41
|
+
expect(copy.alphaType).toStrictEqual('premultiplied');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('createImageResource', () => {
|
|
46
|
+
it('returns an object', () => {
|
|
47
|
+
const resource = createImageResource();
|
|
48
|
+
expect(resource).not.toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('sets source, width and height if you pass in a source element', () => {
|
|
52
|
+
const element = { width: 100, height: 100 } as HTMLImageElement;
|
|
53
|
+
const resource = createImageResource(element);
|
|
54
|
+
expect(resource.source).toStrictEqual(element);
|
|
55
|
+
expect(resource.width).toStrictEqual(element.width);
|
|
56
|
+
expect(resource.height).toStrictEqual(element.height);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('starts at version 0', () => {
|
|
60
|
+
expect(createImageResource().version).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('defaults to null data, rgba8unorm format, and straight alphaType', () => {
|
|
64
|
+
const resource = createImageResource();
|
|
65
|
+
expect(resource.data).toBeNull();
|
|
66
|
+
expect(resource.format).toStrictEqual('rgba8unorm');
|
|
67
|
+
expect(resource.alphaType).toStrictEqual('straight');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('disposeImageResource', () => {
|
|
72
|
+
it('releases the element and data and marks the resource changed', () => {
|
|
73
|
+
const resource = createImageResource({ width: 4, height: 5 } as HTMLImageElement);
|
|
74
|
+
resource.data = new Uint8ClampedArray(4 * 5 * 4);
|
|
75
|
+
const before = resource.version;
|
|
76
|
+
|
|
77
|
+
disposeImageResource(resource);
|
|
78
|
+
|
|
79
|
+
expect(resource.source).toBeNull();
|
|
80
|
+
expect(resource.data).toBeNull();
|
|
81
|
+
expect(resource.version).toStrictEqual(before + 1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('getImageResourceByteSize', () => {
|
|
86
|
+
it('returns 0 when data is null (element-only resource)', () => {
|
|
87
|
+
const resource = createImageResource();
|
|
88
|
+
expect(getImageResourceByteSize(resource)).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns the byteLength of the data array when present', () => {
|
|
92
|
+
const resource = createImageResource();
|
|
93
|
+
resource.data = new Uint8ClampedArray(100);
|
|
94
|
+
expect(getImageResourceByteSize(resource)).toBe(100);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('reflects width × height × 4 for a typical rgba8unorm resource', () => {
|
|
98
|
+
const resource = createImageResource();
|
|
99
|
+
resource.width = 4;
|
|
100
|
+
resource.height = 4;
|
|
101
|
+
resource.data = new Uint8ClampedArray(4 * 4 * 4);
|
|
102
|
+
expect(getImageResourceByteSize(resource)).toBe(64);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('hasImageResourceData', () => {
|
|
107
|
+
it('is false without data and true with it', () => {
|
|
108
|
+
const resource = createImageResource();
|
|
109
|
+
expect(hasImageResourceData(resource)).toStrictEqual(false);
|
|
110
|
+
resource.data = new Uint8ClampedArray(4);
|
|
111
|
+
expect(hasImageResourceData(resource)).toStrictEqual(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('hasImageResourceSource', () => {
|
|
116
|
+
it('is false without an element and true with one', () => {
|
|
117
|
+
expect(hasImageResourceSource(createImageResource())).toStrictEqual(false);
|
|
118
|
+
expect(hasImageResourceSource(createImageResource({ width: 1, height: 1 } as HTMLImageElement))).toStrictEqual(
|
|
119
|
+
true,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('invalidateImageResource', () => {
|
|
125
|
+
it('increments the version', () => {
|
|
126
|
+
const resource = createImageResource();
|
|
127
|
+
invalidateImageResource(resource);
|
|
128
|
+
expect(resource.version).toBe(1);
|
|
129
|
+
invalidateImageResource(resource);
|
|
130
|
+
expect(resource.version).toBe(2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('wraps around with >>> 0', () => {
|
|
134
|
+
const resource = createImageResource();
|
|
135
|
+
resource.version = 0xffffffff;
|
|
136
|
+
invalidateImageResource(resource);
|
|
137
|
+
expect(resource.version).toBe(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('isImageResourceEmpty', () => {
|
|
142
|
+
it('is true with no dimensions and false once sized', () => {
|
|
143
|
+
expect(isImageResourceEmpty(createImageResource())).toStrictEqual(true);
|
|
144
|
+
expect(isImageResourceEmpty(createImageResource({ width: 2, height: 2 } as HTMLImageElement))).toStrictEqual(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('setImageResourceSource', () => {
|
|
149
|
+
it('swaps the element, re-reads dimensions, and marks the resource changed', () => {
|
|
150
|
+
const resource = createImageResource();
|
|
151
|
+
const before = resource.version;
|
|
152
|
+
const element = { width: 8, height: 6 } as HTMLImageElement;
|
|
153
|
+
|
|
154
|
+
setImageResourceSource(resource, element);
|
|
155
|
+
|
|
156
|
+
expect(resource.source).toBe(element);
|
|
157
|
+
expect(resource.width).toStrictEqual(8);
|
|
158
|
+
expect(resource.height).toStrictEqual(6);
|
|
159
|
+
expect(resource.version).toStrictEqual(before + 1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('clears the element when passed null', () => {
|
|
163
|
+
const resource = createImageResource({ width: 8, height: 6 } as HTMLImageElement);
|
|
164
|
+
|
|
165
|
+
setImageResourceSource(resource, null);
|
|
166
|
+
|
|
167
|
+
expect(resource.source).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createImageResourceFromCanvas,
|
|
3
|
+
createImageResourceFromImageBitmap,
|
|
4
|
+
createImageResourceFromImageElement,
|
|
5
|
+
detectImageMimeType,
|
|
6
|
+
isImageResourceSameOrigin,
|
|
7
|
+
loadImageResourceFromBase64,
|
|
8
|
+
loadImageResourceFromBlob,
|
|
9
|
+
loadImageResourceFromBytes,
|
|
10
|
+
loadImageResourceFromUrl,
|
|
11
|
+
} from './imageResourceFrom';
|
|
12
|
+
|
|
13
|
+
// Stub img.decode() so async load functions resolve immediately in jsdom.
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
HTMLImageElement.prototype.decode = vi.fn().mockResolvedValue(undefined);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
delete (HTMLImageElement.prototype as Partial<HTMLImageElement>).decode;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('createImageResourceFromCanvas', () => {
|
|
24
|
+
it('wraps a canvas with correct dimensions', () => {
|
|
25
|
+
const canvas = document.createElement('canvas');
|
|
26
|
+
canvas.width = 320;
|
|
27
|
+
canvas.height = 240;
|
|
28
|
+
const resource = createImageResourceFromCanvas(canvas);
|
|
29
|
+
|
|
30
|
+
expect(resource.source).toBe(canvas);
|
|
31
|
+
expect(resource.width).toBe(320);
|
|
32
|
+
expect(resource.height).toBe(240);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('reflects the canvas dimensions at wrap time', () => {
|
|
36
|
+
const canvas = document.createElement('canvas');
|
|
37
|
+
canvas.width = 0;
|
|
38
|
+
canvas.height = 0;
|
|
39
|
+
const resource = createImageResourceFromCanvas(canvas);
|
|
40
|
+
|
|
41
|
+
expect(resource.width).toBe(0);
|
|
42
|
+
expect(resource.height).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns a new object each call', () => {
|
|
46
|
+
const canvas = document.createElement('canvas');
|
|
47
|
+
expect(createImageResourceFromCanvas(canvas)).not.toBe(createImageResourceFromCanvas(canvas));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('createImageResourceFromImageBitmap', () => {
|
|
52
|
+
it('wraps an ImageBitmap with correct dimensions', () => {
|
|
53
|
+
const bitmap = { width: 64, height: 128, close: () => {} } as ImageBitmap;
|
|
54
|
+
const resource = createImageResourceFromImageBitmap(bitmap);
|
|
55
|
+
|
|
56
|
+
expect(resource.source).toBe(bitmap);
|
|
57
|
+
expect(resource.width).toBe(64);
|
|
58
|
+
expect(resource.height).toBe(128);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns a new object each call', () => {
|
|
62
|
+
const bitmap = { width: 1, height: 1, close: () => {} } as ImageBitmap;
|
|
63
|
+
expect(createImageResourceFromImageBitmap(bitmap)).not.toBe(createImageResourceFromImageBitmap(bitmap));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('createImageResourceFromImageElement', () => {
|
|
68
|
+
it('wraps an HTMLImageElement with correct dimensions', () => {
|
|
69
|
+
const img = { width: 200, height: 100 } as HTMLImageElement;
|
|
70
|
+
const resource = createImageResourceFromImageElement(img);
|
|
71
|
+
|
|
72
|
+
expect(resource.source).toBe(img);
|
|
73
|
+
expect(resource.width).toBe(200);
|
|
74
|
+
expect(resource.height).toBe(100);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('reflects zero dimensions for an unloaded image element', () => {
|
|
78
|
+
const img = document.createElement('img');
|
|
79
|
+
const resource = createImageResourceFromImageElement(img);
|
|
80
|
+
|
|
81
|
+
expect(resource.width).toBe(0);
|
|
82
|
+
expect(resource.height).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns a new object each call', () => {
|
|
86
|
+
const img = document.createElement('img');
|
|
87
|
+
expect(createImageResourceFromImageElement(img)).not.toBe(createImageResourceFromImageElement(img));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('detectImageMimeType', () => {
|
|
92
|
+
it('returns null for a buffer that is too small', () => {
|
|
93
|
+
expect(detectImageMimeType(new ArrayBuffer(2))).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns null for an unrecognised header', () => {
|
|
97
|
+
const buf = new ArrayBuffer(16);
|
|
98
|
+
new Uint8Array(buf).set([0x00, 0x01, 0x02, 0x03]);
|
|
99
|
+
expect(detectImageMimeType(buf)).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('detects PNG', () => {
|
|
103
|
+
const buf = new ArrayBuffer(16);
|
|
104
|
+
new Uint8Array(buf).set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
105
|
+
expect(detectImageMimeType(buf)).toBe('image/png');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('detects JPEG', () => {
|
|
109
|
+
const buf = new ArrayBuffer(16);
|
|
110
|
+
new Uint8Array(buf).set([0xff, 0xd8, 0xff, 0xe0]);
|
|
111
|
+
expect(detectImageMimeType(buf)).toBe('image/jpeg');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('detects GIF', () => {
|
|
115
|
+
const buf = new ArrayBuffer(16);
|
|
116
|
+
new Uint8Array(buf).set([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]);
|
|
117
|
+
expect(detectImageMimeType(buf)).toBe('image/gif');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('detects WebP', () => {
|
|
121
|
+
const buf = new ArrayBuffer(16);
|
|
122
|
+
new Uint8Array(buf).set([
|
|
123
|
+
0x52,
|
|
124
|
+
0x49,
|
|
125
|
+
0x46,
|
|
126
|
+
0x46, // RIFF
|
|
127
|
+
0x00,
|
|
128
|
+
0x00,
|
|
129
|
+
0x00,
|
|
130
|
+
0x00, // size (ignored)
|
|
131
|
+
0x57,
|
|
132
|
+
0x45,
|
|
133
|
+
0x42,
|
|
134
|
+
0x50, // WEBP
|
|
135
|
+
]);
|
|
136
|
+
expect(detectImageMimeType(buf)).toBe('image/webp');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('detects BMP', () => {
|
|
140
|
+
const buf = new ArrayBuffer(16);
|
|
141
|
+
new Uint8Array(buf).set([0x42, 0x4d]);
|
|
142
|
+
expect(detectImageMimeType(buf)).toBe('image/bmp');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('isImageResourceSameOrigin', () => {
|
|
147
|
+
it('returns true for data: URLs', () => {
|
|
148
|
+
expect(isImageResourceSameOrigin('data:image/png;base64,abc')).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns true for blob: URLs', () => {
|
|
152
|
+
expect(isImageResourceSameOrigin('blob:http://localhost/some-id')).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns true for relative URLs (same origin)', () => {
|
|
156
|
+
expect(isImageResourceSameOrigin('/images/logo.png')).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('returns false for a different-origin absolute URL', () => {
|
|
160
|
+
expect(isImageResourceSameOrigin('https://cdn.other-domain.com/image.png')).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('loadImageResourceFromBase64', () => {
|
|
165
|
+
it('resolves to an ImageResource', async () => {
|
|
166
|
+
const resource = await loadImageResourceFromBase64('abc123', 'image/png');
|
|
167
|
+
expect(resource).not.toBeNull();
|
|
168
|
+
expect(resource.source).toBeInstanceOf(HTMLImageElement);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('builds a data: URL from the base64 string and mime type', async () => {
|
|
172
|
+
const resource = await loadImageResourceFromBase64('aGVsbG8=', 'image/png');
|
|
173
|
+
expect(resource.source).toBeInstanceOf(HTMLImageElement);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('loadImageResourceFromBlob', () => {
|
|
178
|
+
it('resolves to an ImageResource', async () => {
|
|
179
|
+
const blob = new Blob([], { type: 'image/png' });
|
|
180
|
+
const resource = await loadImageResourceFromBlob(blob);
|
|
181
|
+
expect(resource).not.toBeNull();
|
|
182
|
+
expect(resource.source).toBeInstanceOf(HTMLImageElement);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('revokes the object URL after loading', async () => {
|
|
186
|
+
const revokeSpy = vi.spyOn(URL, 'revokeObjectURL');
|
|
187
|
+
const blob = new Blob([], { type: 'image/png' });
|
|
188
|
+
await loadImageResourceFromBlob(blob);
|
|
189
|
+
expect(revokeSpy).toHaveBeenCalledOnce();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('revokes the object URL even if loading fails', async () => {
|
|
193
|
+
HTMLImageElement.prototype.decode = vi.fn().mockRejectedValue(new Error('load failed'));
|
|
194
|
+
const revokeSpy = vi.spyOn(URL, 'revokeObjectURL');
|
|
195
|
+
const blob = new Blob([], { type: 'image/png' });
|
|
196
|
+
await expect(loadImageResourceFromBlob(blob)).rejects.toThrow('load failed');
|
|
197
|
+
expect(revokeSpy).toHaveBeenCalledOnce();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('loadImageResourceFromBytes', () => {
|
|
202
|
+
it('throws when mime type cannot be detected and none is provided', async () => {
|
|
203
|
+
const bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
|
|
204
|
+
await expect(loadImageResourceFromBytes(bytes)).rejects.toThrow('Unable to determine image type');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('uses the provided mimeType and bypasses detection', async () => {
|
|
208
|
+
const bytes = new Uint8Array(16);
|
|
209
|
+
const resource = await loadImageResourceFromBytes(bytes, 'image/png');
|
|
210
|
+
expect(resource.source).toBeInstanceOf(HTMLImageElement);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('detects PNG and resolves', async () => {
|
|
214
|
+
const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
215
|
+
const resource = await loadImageResourceFromBytes(bytes);
|
|
216
|
+
expect(resource.source).toBeInstanceOf(HTMLImageElement);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('detects JPEG and resolves', async () => {
|
|
220
|
+
const bytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
221
|
+
const resource = await loadImageResourceFromBytes(bytes);
|
|
222
|
+
expect(resource.source).toBeInstanceOf(HTMLImageElement);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('loadImageResourceFromUrl', () => {
|
|
227
|
+
it('does not set crossOrigin when no crossOrigin parameter is given', async () => {
|
|
228
|
+
let capturedImg: HTMLImageElement | undefined;
|
|
229
|
+
const origImage = globalThis.Image;
|
|
230
|
+
globalThis.Image = new Proxy(origImage, {
|
|
231
|
+
construct(Target, args) {
|
|
232
|
+
const img = new Target(...(args as []));
|
|
233
|
+
capturedImg = img;
|
|
234
|
+
return img;
|
|
235
|
+
},
|
|
236
|
+
}) as typeof Image;
|
|
237
|
+
|
|
238
|
+
await loadImageResourceFromUrl('/images/logo.png');
|
|
239
|
+
expect(capturedImg?.crossOrigin).toBeNull();
|
|
240
|
+
|
|
241
|
+
globalThis.Image = origImage;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('rejects with an abort reason when the signal is already aborted before the call', async () => {
|
|
245
|
+
const controller = new AbortController();
|
|
246
|
+
controller.abort(new Error('cancelled'));
|
|
247
|
+
await expect(loadImageResourceFromUrl('data:image/png;base64,abc', undefined, controller.signal)).rejects.toThrow(
|
|
248
|
+
'cancelled',
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('resolves to an ImageResource whose source is an HTMLImageElement', async () => {
|
|
253
|
+
const resource = await loadImageResourceFromUrl('data:image/png;base64,abc');
|
|
254
|
+
expect(resource.source).toBeInstanceOf(HTMLImageElement);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('sets crossOrigin when the crossOrigin parameter is provided', async () => {
|
|
258
|
+
let capturedImg: HTMLImageElement | undefined;
|
|
259
|
+
const origImage = globalThis.Image;
|
|
260
|
+
globalThis.Image = new Proxy(origImage, {
|
|
261
|
+
construct(Target, args) {
|
|
262
|
+
const img = new Target(...(args as []));
|
|
263
|
+
capturedImg = img;
|
|
264
|
+
return img;
|
|
265
|
+
},
|
|
266
|
+
}) as typeof Image;
|
|
267
|
+
|
|
268
|
+
await loadImageResourceFromUrl('https://cdn.other-domain.com/image.png', 'anonymous');
|
|
269
|
+
expect(capturedImg?.crossOrigin).toBe('anonymous');
|
|
270
|
+
|
|
271
|
+
globalThis.Image = origImage;
|
|
272
|
+
});
|
|
273
|
+
});
|