jazz-tools 0.16.6 → 0.17.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/.svelte-kit/__package__/index.d.ts +1 -0
- package/.svelte-kit/__package__/index.d.ts.map +1 -1
- package/.svelte-kit/__package__/index.js +1 -0
- package/.svelte-kit/__package__/media/image.svelte +131 -0
- package/.svelte-kit/__package__/media/image.svelte.d.ts +10 -0
- package/.svelte-kit/__package__/media/image.svelte.d.ts.map +1 -0
- package/.svelte-kit/__package__/media/index.d.ts +2 -0
- package/.svelte-kit/__package__/media/index.d.ts.map +1 -0
- package/.svelte-kit/__package__/media/index.js +1 -0
- package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts +2 -0
- package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts.map +1 -0
- package/.svelte-kit/__package__/tests/media/image.svelte.test.js +430 -0
- package/.svelte-kit/__package__/tests/testUtils.d.ts +11 -0
- package/.svelte-kit/__package__/tests/testUtils.d.ts.map +1 -0
- package/.svelte-kit/__package__/tests/testUtils.js +17 -0
- package/.svelte-kit/__package__/tests/types.d.ts +3 -0
- package/.turbo/turbo-build.log +42 -46
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-R2VNCMG6.js → chunk-2SH44VLX.js} +33 -38
- package/dist/chunk-2SH44VLX.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/media/chunk-BBSS3NEY.js +211 -0
- package/dist/media/chunk-BBSS3NEY.js.map +1 -0
- package/dist/media/create-image.d.ts +48 -0
- package/dist/media/create-image.d.ts.map +1 -0
- package/dist/media/create-image.test.d.ts +2 -0
- package/dist/media/create-image.test.d.ts.map +1 -0
- package/dist/media/index.browser.d.ts +15 -0
- package/dist/media/index.browser.d.ts.map +1 -0
- package/dist/media/index.browser.js +113 -0
- package/dist/media/index.browser.js.map +1 -0
- package/dist/media/index.d.ts +53 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +13 -0
- package/dist/media/index.js.map +1 -0
- package/dist/media/index.native.d.ts +17 -0
- package/dist/media/index.native.d.ts.map +1 -0
- package/dist/media/index.native.js +126 -0
- package/dist/media/index.native.js.map +1 -0
- package/dist/media/utils.d.ts +17 -0
- package/dist/media/utils.d.ts.map +1 -0
- package/dist/media/utils.test.d.ts +2 -0
- package/dist/media/utils.test.d.ts.map +1 -0
- package/dist/react/index.d.ts +1 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +176 -59
- package/dist/react/index.js.map +1 -1
- package/dist/react/media/image.d.ts +62 -0
- package/dist/react/media/image.d.ts.map +1 -0
- package/dist/react/tests/media/image.test.d.ts +2 -0
- package/dist/react/tests/media/image.test.d.ts.map +1 -0
- package/dist/react-core/tests/useDemoAuth.test.d.ts +2 -0
- package/dist/react-core/tests/useDemoAuth.test.d.ts.map +1 -0
- package/dist/react-native-core/index.d.ts +1 -1
- package/dist/react-native-core/index.d.ts.map +1 -1
- package/dist/react-native-core/index.js +84 -66
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/media/image.d.ts +93 -0
- package/dist/react-native-core/media/image.d.ts.map +1 -0
- package/dist/react-native-core/testing.d.ts +2 -0
- package/dist/react-native-core/testing.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -0
- package/dist/svelte/media/image.svelte +131 -0
- package/dist/svelte/media/image.svelte.d.ts +10 -0
- package/dist/svelte/media/image.svelte.d.ts.map +1 -0
- package/dist/svelte/media/index.d.ts +2 -0
- package/dist/svelte/media/index.d.ts.map +1 -0
- package/dist/svelte/media/index.js +1 -0
- package/dist/svelte/tests/media/image.svelte.test.d.ts +2 -0
- package/dist/svelte/tests/media/image.svelte.test.d.ts.map +1 -0
- package/dist/svelte/tests/media/image.svelte.test.js +430 -0
- package/dist/svelte/tests/testUtils.d.ts +11 -0
- package/dist/svelte/tests/testUtils.d.ts.map +1 -0
- package/dist/svelte/tests/testUtils.js +17 -0
- package/dist/svelte/tests/types.d.ts +3 -0
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/coFeed.d.ts +15 -0
- package/dist/tools/coValues/coFeed.d.ts.map +1 -1
- package/dist/tools/coValues/extensions/imageDef.d.ts +3 -9
- package/dist/tools/coValues/extensions/imageDef.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +1 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/media/create-image.test.ts +195 -0
- package/src/media/create-image.ts +180 -0
- package/src/media/index.browser.ts +150 -0
- package/src/media/index.native.ts +153 -0
- package/src/media/index.ts +61 -0
- package/src/media/utils.test.ts +327 -0
- package/src/media/utils.ts +202 -0
- package/src/react/index.ts +1 -2
- package/src/react/media/image.tsx +210 -0
- package/src/react/tests/media/image.test.tsx +588 -0
- package/src/react-native-core/index.ts +1 -1
- package/src/react-native-core/media/image.tsx +159 -0
- package/src/svelte/index.ts +1 -0
- package/src/svelte/media/image.svelte +131 -0
- package/src/svelte/media/index.ts +1 -0
- package/src/svelte/tests/media/image.svelte.test.ts +583 -0
- package/src/svelte/tests/testUtils.ts +33 -0
- package/src/svelte/tests/types.d.ts +3 -0
- package/src/tools/coValues/coFeed.ts +37 -5
- package/src/tools/coValues/extensions/imageDef.ts +3 -49
- package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +6 -0
- package/src/tools/tests/coMap.record.test.ts +3 -2
- package/src/tools/tests/coOptional.test.ts +3 -1
- package/tsconfig.json +1 -0
- package/tsup.config.ts +4 -9
- package/vitest.config.ts +14 -1
- package/dist/browser-media-images/index.d.ts +0 -9
- package/dist/browser-media-images/index.d.ts.map +0 -1
- package/dist/browser-media-images/index.js +0 -72
- package/dist/browser-media-images/index.js.map +0 -1
- package/dist/chunk-R2VNCMG6.js.map +0 -1
- package/dist/react/media.d.ts +0 -24
- package/dist/react/media.d.ts.map +0 -1
- package/dist/react-native-core/media.d.ts +0 -24
- package/dist/react-native-core/media.d.ts.map +0 -1
- package/dist/react-native-media-images/index.d.ts +0 -7
- package/dist/react-native-media-images/index.d.ts.map +0 -1
- package/dist/react-native-media-images/index.js +0 -177
- package/dist/react-native-media-images/index.js.map +0 -1
- package/dist/tools/tests/imageDef.test.d.ts +0 -2
- package/dist/tools/tests/imageDef.test.d.ts.map +0 -1
- package/src/browser-media-images/index.ts +0 -131
- package/src/react/media.tsx +0 -74
- package/src/react/scratch.tsx +0 -50
- package/src/react-native-core/media.tsx +0 -79
- package/src/react-native-media-images/index.ts +0 -238
- package/src/tools/tests/imageDef.test.ts +0 -278
@@ -0,0 +1,61 @@
|
|
1
|
+
import type { ImageDefinition } from "jazz-tools";
|
2
|
+
import {
|
3
|
+
CreateImageOptions,
|
4
|
+
SourceType,
|
5
|
+
createImageFactory,
|
6
|
+
} from "./create-image.js";
|
7
|
+
|
8
|
+
export { highestResAvailable, loadImage, loadImageBySize } from "./utils.js";
|
9
|
+
export { createImageFactory };
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Creates an ImageDefinition from an image file or blob with built-in UX features.
|
13
|
+
*
|
14
|
+
* This function creates a specialized CoValue for managing images in Jazz applications.
|
15
|
+
* It supports blurry placeholders, built-in resizing, and progressive loading patterns.
|
16
|
+
*
|
17
|
+
* @returns Promise that resolves to an ImageDefinition
|
18
|
+
*
|
19
|
+
* @example
|
20
|
+
* ```ts
|
21
|
+
* import { createImage } from "jazz-tools/media";
|
22
|
+
*
|
23
|
+
* // Create an image from a file input
|
24
|
+
* async function handleFileUpload(event: React.ChangeEvent<HTMLInputElement>) {
|
25
|
+
* const file = event.target.files?.[0];
|
26
|
+
* if (file) {
|
27
|
+
* // Creates ImageDefinition with a blurry placeholder, limited to 1024px
|
28
|
+
* // on the longest side, and multiple resolutions automatically
|
29
|
+
* const image = await createImage(file, {
|
30
|
+
* owner: me._owner,
|
31
|
+
* maxSize: 1024,
|
32
|
+
* placeholder: "blur",
|
33
|
+
* progressive: true,
|
34
|
+
* });
|
35
|
+
*
|
36
|
+
* // Store the image in your application data
|
37
|
+
* me.profile.image = image;
|
38
|
+
* }
|
39
|
+
* }
|
40
|
+
* ```
|
41
|
+
*
|
42
|
+
* @example
|
43
|
+
* ```ts
|
44
|
+
* // React Native example
|
45
|
+
* import { createImage } from "jazz-tools/media";
|
46
|
+
*
|
47
|
+
* async function uploadImageFromCamera(imagePath: string) {
|
48
|
+
* const image = await createImage(imagePath, {
|
49
|
+
* maxSize: 800,
|
50
|
+
* placeholder: "blur",
|
51
|
+
* progressive: false,
|
52
|
+
* });
|
53
|
+
*
|
54
|
+
* return image;
|
55
|
+
* }
|
56
|
+
* ```
|
57
|
+
*/
|
58
|
+
export declare function createImage(
|
59
|
+
imageBlobOrFile: SourceType,
|
60
|
+
options?: CreateImageOptions,
|
61
|
+
): Promise<ImageDefinition>;
|
@@ -0,0 +1,327 @@
|
|
1
|
+
import { Account, FileStream, ImageDefinition } from "jazz-tools";
|
2
|
+
import { highestResAvailable, loadImageBySize } from "jazz-tools/media";
|
3
|
+
import { createJazzTestAccount, setupJazzTestSync } from "jazz-tools/testing";
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
5
|
+
|
6
|
+
const createFileStream = (account: any, blobSize?: number) => {
|
7
|
+
return FileStream.createFromBlob(
|
8
|
+
new Blob([new Uint8Array(blobSize || 1)], { type: "image/png" }),
|
9
|
+
{
|
10
|
+
owner: account,
|
11
|
+
},
|
12
|
+
);
|
13
|
+
};
|
14
|
+
|
15
|
+
describe("highestResAvailable", async () => {
|
16
|
+
let account: Account;
|
17
|
+
|
18
|
+
beforeEach(async () => {
|
19
|
+
account = await createJazzTestAccount({
|
20
|
+
isCurrentActiveAccount: true,
|
21
|
+
});
|
22
|
+
vi.spyOn(Account, "getMe").mockReturnValue(account);
|
23
|
+
await setupJazzTestSync();
|
24
|
+
});
|
25
|
+
|
26
|
+
it("returns original if progressive is false", async () => {
|
27
|
+
const original = await createFileStream(account._owner);
|
28
|
+
const imageDef = ImageDefinition.create(
|
29
|
+
{
|
30
|
+
originalSize: [1920, 1080],
|
31
|
+
progressive: false,
|
32
|
+
original,
|
33
|
+
},
|
34
|
+
{ owner: account._owner },
|
35
|
+
);
|
36
|
+
|
37
|
+
imageDef["1920x1080"] = original;
|
38
|
+
|
39
|
+
const result = highestResAvailable(imageDef, 256, 256);
|
40
|
+
expect(result?.image.id).toBe(original.id);
|
41
|
+
});
|
42
|
+
|
43
|
+
it("returns original if progressive is true but no resizes present", async () => {
|
44
|
+
const original = await createFileStream(account._owner, 1);
|
45
|
+
const imageDef = ImageDefinition.create(
|
46
|
+
{
|
47
|
+
originalSize: [1920, 1080],
|
48
|
+
progressive: true,
|
49
|
+
original,
|
50
|
+
},
|
51
|
+
{ owner: account._owner },
|
52
|
+
);
|
53
|
+
|
54
|
+
imageDef["1920x1080"] = original;
|
55
|
+
|
56
|
+
const result = highestResAvailable(imageDef, 256, 256);
|
57
|
+
expect(result?.image.id).toBe(original.id);
|
58
|
+
});
|
59
|
+
|
60
|
+
it("returns closest available resize if progressive is true", async () => {
|
61
|
+
const original = await createFileStream(account._owner);
|
62
|
+
const resize256 = await createFileStream(account._owner, 1);
|
63
|
+
const imageDef = ImageDefinition.create(
|
64
|
+
{
|
65
|
+
originalSize: [1920, 1080],
|
66
|
+
progressive: true,
|
67
|
+
original,
|
68
|
+
},
|
69
|
+
{ owner: account._owner },
|
70
|
+
);
|
71
|
+
|
72
|
+
imageDef["1920x1080"] = original;
|
73
|
+
imageDef["256x256"] = resize256;
|
74
|
+
|
75
|
+
const result = highestResAvailable(imageDef, 256, 256);
|
76
|
+
expect(result?.image.id).toBe(resize256.id);
|
77
|
+
});
|
78
|
+
|
79
|
+
it("returns original if wanted size matches original size", async () => {
|
80
|
+
const original = await createFileStream(account._owner);
|
81
|
+
const imageDef = ImageDefinition.create(
|
82
|
+
{
|
83
|
+
originalSize: [1024, 1024],
|
84
|
+
progressive: true,
|
85
|
+
original,
|
86
|
+
},
|
87
|
+
{ owner: account._owner },
|
88
|
+
);
|
89
|
+
|
90
|
+
imageDef["1024x1024"] = original;
|
91
|
+
|
92
|
+
const result = highestResAvailable(imageDef, 1024, 1024);
|
93
|
+
expect(result?.image.id).toBe(original.id);
|
94
|
+
});
|
95
|
+
|
96
|
+
it("returns best fit among multiple resizes", async () => {
|
97
|
+
const original = await createFileStream(account._owner);
|
98
|
+
const resize256 = await createFileStream(account._owner, 1);
|
99
|
+
const resize1024 = await createFileStream(account._owner, 1);
|
100
|
+
const resize2048 = await createFileStream(account._owner, 1);
|
101
|
+
const imageDef = ImageDefinition.create(
|
102
|
+
{
|
103
|
+
originalSize: [2048, 2048],
|
104
|
+
progressive: true,
|
105
|
+
original,
|
106
|
+
},
|
107
|
+
{ owner: account._owner },
|
108
|
+
);
|
109
|
+
|
110
|
+
imageDef["256x256"] = resize256;
|
111
|
+
imageDef["1024x1024"] = resize1024;
|
112
|
+
imageDef["2048x2048"] = resize2048;
|
113
|
+
|
114
|
+
// Closest to 900x900 is 1024
|
115
|
+
const result = highestResAvailable(imageDef, 900, 900);
|
116
|
+
expect(result?.image.id).toBe(resize1024.id);
|
117
|
+
});
|
118
|
+
|
119
|
+
it("returns the best fit resolution", async () => {
|
120
|
+
const original = await createFileStream(account._owner, 1);
|
121
|
+
const resize256 = await createFileStream(account._owner, 1);
|
122
|
+
const resize2048 = await createFileStream(account._owner, 1);
|
123
|
+
// 1024 is not loaded yet
|
124
|
+
const resize1024 = FileStream.create({ owner: account._owner });
|
125
|
+
resize1024.start({ mimeType: "image/jpeg" });
|
126
|
+
// Don't end resize1024, so it has no chunks
|
127
|
+
|
128
|
+
const imageDef = ImageDefinition.create(
|
129
|
+
{
|
130
|
+
originalSize: [2048, 2048],
|
131
|
+
progressive: true,
|
132
|
+
original,
|
133
|
+
},
|
134
|
+
{ owner: account._owner },
|
135
|
+
);
|
136
|
+
imageDef["256x256"] = resize256;
|
137
|
+
imageDef["1024x1024"] = resize1024;
|
138
|
+
imageDef["2048x2048"] = resize2048;
|
139
|
+
|
140
|
+
// Closest to 900x900 is 1024
|
141
|
+
const result = highestResAvailable(imageDef, 900, 900);
|
142
|
+
expect(result?.image.id).toBe(resize2048.id);
|
143
|
+
});
|
144
|
+
|
145
|
+
it("returns original if no resizes are loaded (missing chunks)", async () => {
|
146
|
+
const original = await createFileStream(account._owner);
|
147
|
+
const imageDef = ImageDefinition.create(
|
148
|
+
{
|
149
|
+
originalSize: [256, 256],
|
150
|
+
progressive: true,
|
151
|
+
original,
|
152
|
+
},
|
153
|
+
{ owner: account._owner },
|
154
|
+
);
|
155
|
+
|
156
|
+
imageDef["256x256"] = original;
|
157
|
+
// 1024 is not loaded yet
|
158
|
+
const resize1024 = FileStream.create({ owner: account._owner });
|
159
|
+
resize1024.start({ mimeType: "image/jpeg" });
|
160
|
+
// Don't end resize1024, so it has no chunks
|
161
|
+
imageDef["1024x1024"] = resize1024;
|
162
|
+
|
163
|
+
const result = highestResAvailable(imageDef, 1024, 1024);
|
164
|
+
// Only original is valid
|
165
|
+
expect(result?.image.id).toBe(original.id);
|
166
|
+
});
|
167
|
+
|
168
|
+
it("returns the first loaded resize if original is not loaded yet(missing chunks)", async () => {
|
169
|
+
const original = FileStream.create({ owner: account._owner });
|
170
|
+
original.start({ mimeType: "image/jpeg" });
|
171
|
+
// Don't call .end(), so it has no chunks
|
172
|
+
|
173
|
+
const imageDef = ImageDefinition.create(
|
174
|
+
{
|
175
|
+
originalSize: [300, 300],
|
176
|
+
progressive: true,
|
177
|
+
original,
|
178
|
+
},
|
179
|
+
{ owner: account._owner },
|
180
|
+
);
|
181
|
+
|
182
|
+
imageDef["256x256"] = await createFileStream(account._owner, 1);
|
183
|
+
|
184
|
+
const result = highestResAvailable(imageDef, 1024, 1024);
|
185
|
+
// Only original is valid
|
186
|
+
expect(result?.image.id).toBe(imageDef["256x256"].id);
|
187
|
+
});
|
188
|
+
|
189
|
+
it("returns the highest resolution if no good match is found", async () => {
|
190
|
+
const original = await createFileStream(account._owner, 1);
|
191
|
+
|
192
|
+
const imageDef = ImageDefinition.create(
|
193
|
+
{
|
194
|
+
originalSize: [300, 300],
|
195
|
+
progressive: true,
|
196
|
+
original,
|
197
|
+
},
|
198
|
+
{ owner: account._owner },
|
199
|
+
);
|
200
|
+
|
201
|
+
imageDef["256x256"] = await createFileStream(account._owner, 1);
|
202
|
+
imageDef["300x300"] = original;
|
203
|
+
|
204
|
+
const result = highestResAvailable(imageDef, 1024, 1024);
|
205
|
+
expect(result?.image.id).toBe(original.id);
|
206
|
+
});
|
207
|
+
});
|
208
|
+
|
209
|
+
describe("loadImageBySize", async () => {
|
210
|
+
let account: Account;
|
211
|
+
beforeEach(async () => {
|
212
|
+
account = await createJazzTestAccount({
|
213
|
+
isCurrentActiveAccount: true,
|
214
|
+
});
|
215
|
+
vi.spyOn(Account, "getMe").mockReturnValue(account);
|
216
|
+
await setupJazzTestSync();
|
217
|
+
});
|
218
|
+
|
219
|
+
const createImageDef = async (
|
220
|
+
sizes: Array<[number, number]>,
|
221
|
+
progressive = true,
|
222
|
+
) => {
|
223
|
+
if (sizes.length === 0) throw new Error("sizes array must not be empty");
|
224
|
+
|
225
|
+
const originalSize = sizes[sizes.length - 1]!;
|
226
|
+
sizes = sizes.slice(0, -1);
|
227
|
+
|
228
|
+
const original = await createFileStream(account, 1);
|
229
|
+
// Ensure sizes array is not empty
|
230
|
+
const imageDef = ImageDefinition.create(
|
231
|
+
{
|
232
|
+
originalSize,
|
233
|
+
progressive,
|
234
|
+
original,
|
235
|
+
},
|
236
|
+
{ owner: account },
|
237
|
+
);
|
238
|
+
imageDef[`${originalSize[0]}x${originalSize[1]}`] = original;
|
239
|
+
|
240
|
+
for (const size of sizes) {
|
241
|
+
if (!size) continue;
|
242
|
+
const [w, h] = size;
|
243
|
+
imageDef[`${w}x${h}`] = await createFileStream(account, 1);
|
244
|
+
}
|
245
|
+
return imageDef;
|
246
|
+
};
|
247
|
+
|
248
|
+
it("returns original if progressive is false", async () => {
|
249
|
+
const imageDef = await createImageDef([[1920, 1080]], false);
|
250
|
+
const result = await loadImageBySize(imageDef, 256, 256);
|
251
|
+
expect(result?.image.id).toBe(imageDef["1920x1080"]!.id);
|
252
|
+
});
|
253
|
+
|
254
|
+
it("returns null if no sizes are available", async () => {
|
255
|
+
const original = await createFileStream(account._owner, 1);
|
256
|
+
const imageDef = ImageDefinition.create(
|
257
|
+
{
|
258
|
+
originalSize: [1920, 1080],
|
259
|
+
progressive: true,
|
260
|
+
original,
|
261
|
+
},
|
262
|
+
{ owner: account._owner },
|
263
|
+
);
|
264
|
+
const result = await loadImageBySize(imageDef, 256, 256);
|
265
|
+
expect(result).toBeNull();
|
266
|
+
});
|
267
|
+
|
268
|
+
it("returns the closest available resize if progressive is true", async () => {
|
269
|
+
const imageDef = await createImageDef([
|
270
|
+
[256, 256],
|
271
|
+
[1920, 1080],
|
272
|
+
]);
|
273
|
+
const result = await loadImageBySize(imageDef.id, 256, 256);
|
274
|
+
expect(result?.image.id).toBe(imageDef["256x256"]!.id);
|
275
|
+
expect(result?.width).toBe(256);
|
276
|
+
expect(result?.height).toBe(256);
|
277
|
+
});
|
278
|
+
|
279
|
+
it("returns the best fit among multiple resizes", async () => {
|
280
|
+
const imageDef = await createImageDef([
|
281
|
+
[256, 256],
|
282
|
+
[1024, 1024],
|
283
|
+
[2048, 2048],
|
284
|
+
]);
|
285
|
+
const result = await loadImageBySize(imageDef, 900, 900);
|
286
|
+
expect(result?.image.id).toBe(imageDef["1024x1024"]!.id);
|
287
|
+
expect(result?.width).toBe(1024);
|
288
|
+
expect(result?.height).toBe(1024);
|
289
|
+
});
|
290
|
+
|
291
|
+
it("returns the highest resolution if no good match is found", async () => {
|
292
|
+
const imageDef = await createImageDef([
|
293
|
+
[256, 256],
|
294
|
+
[300, 300],
|
295
|
+
]);
|
296
|
+
const result = await loadImageBySize(imageDef, 1024, 1024);
|
297
|
+
expect(result?.image.id).toBe(imageDef["300x300"]!.id);
|
298
|
+
expect(result?.width).toBe(300);
|
299
|
+
expect(result?.height).toBe(300);
|
300
|
+
});
|
301
|
+
|
302
|
+
it("returns null if the best target is not loaded", async () => {
|
303
|
+
const original = await createFileStream(account._owner, 1);
|
304
|
+
const imageDef = ImageDefinition.create(
|
305
|
+
{
|
306
|
+
originalSize: [256, 256],
|
307
|
+
progressive: true,
|
308
|
+
original,
|
309
|
+
},
|
310
|
+
{ owner: account._owner },
|
311
|
+
);
|
312
|
+
// No resizes added
|
313
|
+
const result = await loadImageBySize(imageDef, 1024, 1024);
|
314
|
+
expect(result).toBeNull();
|
315
|
+
});
|
316
|
+
|
317
|
+
it("returns the correct size when wanted size matches available size exactly", async () => {
|
318
|
+
const imageDef = await createImageDef([
|
319
|
+
[512, 512],
|
320
|
+
[1024, 1024],
|
321
|
+
]);
|
322
|
+
const result = await loadImageBySize(imageDef, 1024, 1024);
|
323
|
+
expect(result?.image.id).toBe(imageDef["1024x1024"]!.id);
|
324
|
+
expect(result?.width).toBe(1024);
|
325
|
+
expect(result?.height).toBe(1024);
|
326
|
+
});
|
327
|
+
});
|
@@ -0,0 +1,202 @@
|
|
1
|
+
import type { CoID } from "cojson";
|
2
|
+
import { Account, FileStream, ImageDefinition } from "jazz-tools";
|
3
|
+
|
4
|
+
export function highestResAvailable(
|
5
|
+
image: ImageDefinition,
|
6
|
+
wantedWidth: number,
|
7
|
+
wantedHeight: number,
|
8
|
+
): { width: number; height: number; image: FileStream } | null {
|
9
|
+
const availableSizes: [number, number, string][] = image._raw
|
10
|
+
.keys()
|
11
|
+
.filter((key) => /^\d+x\d+$/.test(key))
|
12
|
+
.map((key) => {
|
13
|
+
const [w, h] = key.split("x").map(Number) as [number, number];
|
14
|
+
return [w, h, key];
|
15
|
+
});
|
16
|
+
|
17
|
+
if (availableSizes.length === 0) {
|
18
|
+
return image.original
|
19
|
+
? {
|
20
|
+
width: image.originalSize[0],
|
21
|
+
height: image.originalSize[1],
|
22
|
+
image: image.original,
|
23
|
+
}
|
24
|
+
: null;
|
25
|
+
}
|
26
|
+
|
27
|
+
const sortedSizes = availableSizes
|
28
|
+
.map((size) => {
|
29
|
+
return {
|
30
|
+
size,
|
31
|
+
match: sizesMatchWanted(size[0], size[1], wantedWidth, wantedHeight),
|
32
|
+
isLoaded: isLoaded(image._raw.get(size[2]) as CoID<any> | undefined),
|
33
|
+
};
|
34
|
+
})
|
35
|
+
.sort((a, b) => a.match - b.match);
|
36
|
+
|
37
|
+
// We try to find the better already loaded image
|
38
|
+
// note: `toReversed` is not available in react-native.
|
39
|
+
const bestLoaded = [...sortedSizes]
|
40
|
+
.reverse()
|
41
|
+
.find((el) => el.isLoaded && image[el.size[2]]?.getChunks());
|
42
|
+
|
43
|
+
// if I can't find a good match, let's use the highest resolution
|
44
|
+
const bestTarget =
|
45
|
+
sortedSizes.find((el) => el.match > 0.95) || sortedSizes.at(-1);
|
46
|
+
|
47
|
+
// if the best target is already loaded, we are done
|
48
|
+
if (image[bestTarget!.size[2]]?.getChunks()) {
|
49
|
+
return image[bestTarget!.size[2]]
|
50
|
+
? {
|
51
|
+
width: bestTarget!.size[0],
|
52
|
+
height: bestTarget!.size[1],
|
53
|
+
image: image[bestTarget!.size[2]]!,
|
54
|
+
}
|
55
|
+
: null;
|
56
|
+
}
|
57
|
+
|
58
|
+
// if the best already loaded is not the best target
|
59
|
+
// let's trigger the load of the best target
|
60
|
+
if (bestLoaded) {
|
61
|
+
image[bestTarget!.size[2]]?.getChunks();
|
62
|
+
return image[bestLoaded.size[2]]
|
63
|
+
? {
|
64
|
+
width: bestLoaded.size[0],
|
65
|
+
height: bestLoaded.size[1],
|
66
|
+
image: image[bestLoaded.size[2]]!,
|
67
|
+
}
|
68
|
+
: null;
|
69
|
+
}
|
70
|
+
|
71
|
+
// if nothing is loaded, then start fetching all the images till the best
|
72
|
+
for (let size of sortedSizes) {
|
73
|
+
if (size.match <= bestTarget!.match) {
|
74
|
+
image[size.size[2]]?.getChunks();
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
return null;
|
79
|
+
}
|
80
|
+
|
81
|
+
function sizesMatchWanted(
|
82
|
+
w: number,
|
83
|
+
h: number,
|
84
|
+
wantedW: number,
|
85
|
+
wantedH: number,
|
86
|
+
): number {
|
87
|
+
const area1 = w * h;
|
88
|
+
const area2 = wantedW * wantedH;
|
89
|
+
|
90
|
+
const areaRatio = area1 / area2;
|
91
|
+
|
92
|
+
// // Below 0.95 means the image is too small, we don't want to upscale it
|
93
|
+
// if (areaRatio < 0.95) {
|
94
|
+
// return 9999;
|
95
|
+
// }
|
96
|
+
|
97
|
+
return areaRatio;
|
98
|
+
}
|
99
|
+
|
100
|
+
function isLoaded(id: CoID<any> | null | undefined): boolean {
|
101
|
+
if (!id) {
|
102
|
+
return false;
|
103
|
+
}
|
104
|
+
|
105
|
+
return !!Account.getMe()._raw.core.node.getLoaded(id);
|
106
|
+
}
|
107
|
+
|
108
|
+
export async function loadImage(
|
109
|
+
imageOrId: ImageDefinition | string,
|
110
|
+
): Promise<{ width: number; height: number; image: FileStream } | null> {
|
111
|
+
if (typeof imageOrId === "string") {
|
112
|
+
const image = await ImageDefinition.load(imageOrId, {
|
113
|
+
resolve: {
|
114
|
+
original: true,
|
115
|
+
},
|
116
|
+
});
|
117
|
+
|
118
|
+
if (image === null || image.original === null) {
|
119
|
+
return null;
|
120
|
+
}
|
121
|
+
|
122
|
+
return {
|
123
|
+
width: image.originalSize[0],
|
124
|
+
height: image.originalSize[1],
|
125
|
+
image: image.original,
|
126
|
+
};
|
127
|
+
}
|
128
|
+
|
129
|
+
await imageOrId.ensureLoaded({
|
130
|
+
resolve: {
|
131
|
+
original: true,
|
132
|
+
},
|
133
|
+
});
|
134
|
+
|
135
|
+
if (!imageOrId.original) {
|
136
|
+
console.warn("Unable to find the original image");
|
137
|
+
return null;
|
138
|
+
}
|
139
|
+
|
140
|
+
return {
|
141
|
+
width: imageOrId.originalSize[0],
|
142
|
+
height: imageOrId.originalSize[1],
|
143
|
+
image: imageOrId.original,
|
144
|
+
};
|
145
|
+
}
|
146
|
+
|
147
|
+
export async function loadImageBySize(
|
148
|
+
imageOrId: ImageDefinition | string,
|
149
|
+
wantedWidth: number,
|
150
|
+
wantedHeight: number,
|
151
|
+
): Promise<{ width: number; height: number; image: FileStream } | null> {
|
152
|
+
const image =
|
153
|
+
typeof imageOrId === "string"
|
154
|
+
? await ImageDefinition.load(imageOrId)
|
155
|
+
: imageOrId;
|
156
|
+
|
157
|
+
if (image === null) {
|
158
|
+
return null;
|
159
|
+
}
|
160
|
+
|
161
|
+
if (image.progressive === false) {
|
162
|
+
return loadImage(imageOrId);
|
163
|
+
}
|
164
|
+
|
165
|
+
const availableSizes: [number, number, string][] = image._raw
|
166
|
+
.keys()
|
167
|
+
.filter((key) => /^\d+x\d+$/.test(key))
|
168
|
+
.map((key) => {
|
169
|
+
const [w, h] = key.split("x").map(Number) as [number, number];
|
170
|
+
return [w, h, key];
|
171
|
+
});
|
172
|
+
|
173
|
+
if (availableSizes.length === 0) {
|
174
|
+
return null;
|
175
|
+
}
|
176
|
+
|
177
|
+
const sortedSizes = availableSizes
|
178
|
+
.map((size) => ({
|
179
|
+
size,
|
180
|
+
match: sizesMatchWanted(size[0], size[1], wantedWidth, wantedHeight),
|
181
|
+
}))
|
182
|
+
.sort((a, b) => a.match - b.match);
|
183
|
+
|
184
|
+
const bestTarget =
|
185
|
+
sortedSizes.find((el) => el.match > 0.95) || sortedSizes.at(-1)!;
|
186
|
+
|
187
|
+
const deepLoaded = await ImageDefinition.load(image.id, {
|
188
|
+
resolve: {
|
189
|
+
[bestTarget.size[2]]: true,
|
190
|
+
},
|
191
|
+
});
|
192
|
+
|
193
|
+
if (deepLoaded === null || deepLoaded[bestTarget.size[2]] === undefined) {
|
194
|
+
return null;
|
195
|
+
}
|
196
|
+
|
197
|
+
return {
|
198
|
+
width: bestTarget.size[0],
|
199
|
+
height: bestTarget.size[1],
|
200
|
+
image: deepLoaded[bestTarget.size[2]]!,
|
201
|
+
};
|
202
|
+
}
|
package/src/react/index.ts
CHANGED