jazz-tools 0.16.5 → 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.
Files changed (170) hide show
  1. package/.svelte-kit/__package__/index.d.ts +1 -0
  2. package/.svelte-kit/__package__/index.d.ts.map +1 -1
  3. package/.svelte-kit/__package__/index.js +1 -0
  4. package/.svelte-kit/__package__/media/image.svelte +131 -0
  5. package/.svelte-kit/__package__/media/image.svelte.d.ts +10 -0
  6. package/.svelte-kit/__package__/media/image.svelte.d.ts.map +1 -0
  7. package/.svelte-kit/__package__/media/index.d.ts +2 -0
  8. package/.svelte-kit/__package__/media/index.d.ts.map +1 -0
  9. package/.svelte-kit/__package__/media/index.js +1 -0
  10. package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts +2 -0
  11. package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts.map +1 -0
  12. package/.svelte-kit/__package__/tests/media/image.svelte.test.js +430 -0
  13. package/.svelte-kit/__package__/tests/testUtils.d.ts +11 -0
  14. package/.svelte-kit/__package__/tests/testUtils.d.ts.map +1 -0
  15. package/.svelte-kit/__package__/tests/testUtils.js +17 -0
  16. package/.svelte-kit/__package__/tests/types.d.ts +3 -0
  17. package/.turbo/turbo-build.log +44 -48
  18. package/CHANGELOG.md +28 -0
  19. package/dist/{chunk-H3BIFFQG.js → chunk-2SH44VLX.js} +35 -40
  20. package/dist/chunk-2SH44VLX.js.map +1 -0
  21. package/dist/index.js +1 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/inspector/{custom-element-TUXKXSZU.js → custom-element-I7L56H6B.js} +3 -5
  24. package/dist/inspector/{custom-element-TUXKXSZU.js.map → custom-element-I7L56H6B.js.map} +1 -1
  25. package/dist/inspector/index.js +2 -4
  26. package/dist/inspector/index.js.map +1 -1
  27. package/dist/inspector/register-custom-element.js +1 -1
  28. package/dist/inspector/viewer/co-plain-text-view.d.ts +1 -1
  29. package/dist/inspector/viewer/co-plain-text-view.d.ts.map +1 -1
  30. package/dist/inspector/viewer/role-display.d.ts.map +1 -1
  31. package/dist/media/chunk-BBSS3NEY.js +211 -0
  32. package/dist/media/chunk-BBSS3NEY.js.map +1 -0
  33. package/dist/media/create-image.d.ts +48 -0
  34. package/dist/media/create-image.d.ts.map +1 -0
  35. package/dist/media/create-image.test.d.ts +2 -0
  36. package/dist/media/create-image.test.d.ts.map +1 -0
  37. package/dist/media/index.browser.d.ts +15 -0
  38. package/dist/media/index.browser.d.ts.map +1 -0
  39. package/dist/media/index.browser.js +113 -0
  40. package/dist/media/index.browser.js.map +1 -0
  41. package/dist/media/index.d.ts +53 -0
  42. package/dist/media/index.d.ts.map +1 -0
  43. package/dist/media/index.js +13 -0
  44. package/dist/media/index.js.map +1 -0
  45. package/dist/media/index.native.d.ts +17 -0
  46. package/dist/media/index.native.d.ts.map +1 -0
  47. package/dist/media/index.native.js +126 -0
  48. package/dist/media/index.native.js.map +1 -0
  49. package/dist/media/utils.d.ts +17 -0
  50. package/dist/media/utils.d.ts.map +1 -0
  51. package/dist/media/utils.test.d.ts +2 -0
  52. package/dist/media/utils.test.d.ts.map +1 -0
  53. package/dist/react/index.d.ts +1 -2
  54. package/dist/react/index.d.ts.map +1 -1
  55. package/dist/react/index.js +176 -59
  56. package/dist/react/index.js.map +1 -1
  57. package/dist/react/media/image.d.ts +62 -0
  58. package/dist/react/media/image.d.ts.map +1 -0
  59. package/dist/react/ssr.d.ts.map +1 -1
  60. package/dist/react/ssr.js.map +1 -1
  61. package/dist/react/tests/media/image.test.d.ts +2 -0
  62. package/dist/react/tests/media/image.test.d.ts.map +1 -0
  63. package/dist/react/tests/testUtils.d.ts.map +1 -1
  64. package/dist/react-core/auth/PassphraseAuth.d.ts +1 -1
  65. package/dist/react-core/auth/PassphraseAuth.d.ts.map +1 -1
  66. package/dist/react-core/index.js +1 -3
  67. package/dist/react-core/index.js.map +1 -1
  68. package/dist/react-core/tests/testUtils.d.ts.map +1 -1
  69. package/dist/react-core/tests/useDemoAuth.test.d.ts +2 -0
  70. package/dist/react-core/tests/useDemoAuth.test.d.ts.map +1 -0
  71. package/dist/react-native-core/index.d.ts +1 -1
  72. package/dist/react-native-core/index.d.ts.map +1 -1
  73. package/dist/react-native-core/index.js +84 -66
  74. package/dist/react-native-core/index.js.map +1 -1
  75. package/dist/react-native-core/media/image.d.ts +93 -0
  76. package/dist/react-native-core/media/image.d.ts.map +1 -0
  77. package/dist/react-native-core/testing.d.ts +2 -0
  78. package/dist/react-native-core/testing.d.ts.map +1 -0
  79. package/dist/svelte/index.d.ts +1 -0
  80. package/dist/svelte/index.d.ts.map +1 -1
  81. package/dist/svelte/index.js +1 -0
  82. package/dist/svelte/media/image.svelte +131 -0
  83. package/dist/svelte/media/image.svelte.d.ts +10 -0
  84. package/dist/svelte/media/image.svelte.d.ts.map +1 -0
  85. package/dist/svelte/media/index.d.ts +2 -0
  86. package/dist/svelte/media/index.d.ts.map +1 -0
  87. package/dist/svelte/media/index.js +1 -0
  88. package/dist/svelte/tests/media/image.svelte.test.d.ts +2 -0
  89. package/dist/svelte/tests/media/image.svelte.test.d.ts.map +1 -0
  90. package/dist/svelte/tests/media/image.svelte.test.js +430 -0
  91. package/dist/svelte/tests/testUtils.d.ts +11 -0
  92. package/dist/svelte/tests/testUtils.d.ts.map +1 -0
  93. package/dist/svelte/tests/testUtils.js +17 -0
  94. package/dist/svelte/tests/types.d.ts +3 -0
  95. package/dist/testing.js +1 -1
  96. package/dist/testing.js.map +1 -1
  97. package/dist/tools/coValues/coFeed.d.ts +15 -0
  98. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  99. package/dist/tools/coValues/deepLoading.d.ts +10 -10
  100. package/dist/tools/coValues/deepLoading.d.ts.map +1 -1
  101. package/dist/tools/coValues/extensions/imageDef.d.ts +3 -9
  102. package/dist/tools/coValues/extensions/imageDef.d.ts.map +1 -1
  103. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +1 -0
  104. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
  105. package/dist/tools/index.d.ts +1 -1
  106. package/dist/tools/index.d.ts.map +1 -1
  107. package/dist/tools/testing.d.ts.map +1 -1
  108. package/package.json +12 -12
  109. package/src/inspector/viewer/co-plain-text-view.tsx +1 -5
  110. package/src/inspector/viewer/co-stream-view.tsx +1 -1
  111. package/src/inspector/viewer/role-display.tsx +4 -1
  112. package/src/{browser-media-images/index.test.browser.ts → media/create-image.test.ts} +146 -24
  113. package/src/media/create-image.ts +180 -0
  114. package/src/media/index.browser.ts +150 -0
  115. package/src/media/index.native.ts +153 -0
  116. package/src/media/index.ts +61 -0
  117. package/src/media/utils.test.ts +327 -0
  118. package/src/media/utils.ts +202 -0
  119. package/src/react/index.ts +1 -2
  120. package/src/react/media/image.tsx +210 -0
  121. package/src/react/ssr.ts +1 -3
  122. package/src/react/tests/media/image.test.tsx +588 -0
  123. package/src/react/tests/testUtils.tsx +2 -10
  124. package/src/react-core/auth/PassphraseAuth.tsx +1 -5
  125. package/src/react-core/tests/testUtils.tsx +2 -10
  126. package/src/react-native-core/index.ts +1 -1
  127. package/src/react-native-core/media/image.tsx +159 -0
  128. package/src/svelte/index.ts +1 -0
  129. package/src/svelte/media/image.svelte +131 -0
  130. package/src/svelte/media/index.ts +1 -0
  131. package/src/svelte/tests/media/image.svelte.test.ts +583 -0
  132. package/src/svelte/tests/testUtils.ts +33 -0
  133. package/src/svelte/tests/types.d.ts +3 -0
  134. package/src/tools/coValues/coFeed.ts +40 -7
  135. package/src/tools/coValues/deepLoading.ts +46 -32
  136. package/src/tools/coValues/extensions/imageDef.ts +3 -49
  137. package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +6 -0
  138. package/src/tools/index.ts +0 -1
  139. package/src/tools/testing.ts +3 -1
  140. package/src/tools/tests/coList.test.ts +1 -1
  141. package/src/tools/tests/coMap.record.test-d.ts +105 -0
  142. package/src/tools/tests/coMap.record.test.ts +48 -2
  143. package/src/tools/tests/coMap.test-d.ts +50 -0
  144. package/src/tools/tests/coOptional.test.ts +3 -1
  145. package/tsconfig.json +1 -0
  146. package/tsup.config.ts +4 -9
  147. package/vitest.config.ts +14 -21
  148. package/dist/browser-media-images/index.d.ts +0 -9
  149. package/dist/browser-media-images/index.d.ts.map +0 -1
  150. package/dist/browser-media-images/index.js +0 -72
  151. package/dist/browser-media-images/index.js.map +0 -1
  152. package/dist/browser-media-images/index.test.browser.d.ts +0 -2
  153. package/dist/browser-media-images/index.test.browser.d.ts.map +0 -1
  154. package/dist/chunk-H3BIFFQG.js.map +0 -1
  155. package/dist/react/media.d.ts +0 -24
  156. package/dist/react/media.d.ts.map +0 -1
  157. package/dist/react-native-core/media.d.ts +0 -24
  158. package/dist/react-native-core/media.d.ts.map +0 -1
  159. package/dist/react-native-media-images/index.d.ts +0 -7
  160. package/dist/react-native-media-images/index.d.ts.map +0 -1
  161. package/dist/react-native-media-images/index.js +0 -177
  162. package/dist/react-native-media-images/index.js.map +0 -1
  163. package/dist/tools/tests/imageDef.test.d.ts +0 -2
  164. package/dist/tools/tests/imageDef.test.d.ts.map +0 -1
  165. package/src/browser-media-images/index.ts +0 -131
  166. package/src/react/media.tsx +0 -74
  167. package/src/react/scratch.tsx +0 -50
  168. package/src/react-native-core/media.tsx +0 -79
  169. package/src/react-native-media-images/index.ts +0 -238
  170. package/src/tools/tests/imageDef.test.ts +0 -278
@@ -0,0 +1,150 @@
1
+ import { Account, FileStream, Group, ImageDefinition } from "jazz-tools";
2
+ import { CreateImageOptions, createImageFactory } from "./create-image.js";
3
+
4
+ export { highestResAvailable, loadImage, loadImageBySize } from "./utils.js";
5
+
6
+ export { createImageFactory };
7
+
8
+ export async function createImage(
9
+ imageBlobOrFile: Blob | File | string,
10
+ options?: CreateImageOptions,
11
+ ) {
12
+ return createImageFactory({
13
+ createFileStreamFromSource,
14
+ getImageSize,
15
+ getPlaceholderBase64,
16
+ resize,
17
+ })(imageBlobOrFile, options || {});
18
+ }
19
+
20
+ // Image Manipulations
21
+ async function createFileStreamFromSource(
22
+ imageBlobOrFile: Blob | File | string,
23
+ owner?: Account | Group,
24
+ ): Promise<FileStream> {
25
+ if (typeof imageBlobOrFile === "string") {
26
+ throw new Error(
27
+ "createFileStreamFromSource(string) is not supported on this platform",
28
+ );
29
+ }
30
+
31
+ return FileStream.createFromBlob(imageBlobOrFile, owner);
32
+ }
33
+
34
+ // using createImageBitmap is ~10x slower than Image object
35
+ // Image object: 640 milliseconds
36
+ // createImageBitmap: 8128 milliseconds
37
+ function getImageFromBlob(blob: Blob): Promise<HTMLImageElement> {
38
+ return new Promise((resolve, reject) => {
39
+ const img = new Image();
40
+ img.onload = () => {
41
+ resolve(img);
42
+ URL.revokeObjectURL(img.src);
43
+ };
44
+ img.onerror = () => {
45
+ reject(new Error("Failed to load image"));
46
+ URL.revokeObjectURL(img.src);
47
+ };
48
+ img.src = URL.createObjectURL(blob);
49
+ });
50
+ }
51
+
52
+ async function getImageSize(
53
+ imageBlobOrFile: Blob | File | string,
54
+ ): Promise<{ width: number; height: number }> {
55
+ if (typeof imageBlobOrFile === "string") {
56
+ throw new Error("getImageSize(string) is not supported on browser");
57
+ }
58
+
59
+ const image = await getImageFromBlob(imageBlobOrFile);
60
+
61
+ return { width: image.width, height: image.height };
62
+ }
63
+
64
+ async function getPlaceholderBase64(
65
+ imageBlobOrFile: Blob | File | string,
66
+ ): Promise<string> {
67
+ if (typeof imageBlobOrFile === "string") {
68
+ throw new Error("getPlaceholderBase64(string) is not supported on browser");
69
+ }
70
+
71
+ const image = await getImageFromBlob(imageBlobOrFile);
72
+
73
+ const { width, height } = resizeDimensionsKeepingAspectRatio(
74
+ image.width,
75
+ image.height,
76
+ 8,
77
+ );
78
+
79
+ const canvas = document.createElement("canvas");
80
+ canvas.width = width;
81
+ canvas.height = height;
82
+
83
+ const ctx = canvas.getContext("2d");
84
+
85
+ if (!ctx) {
86
+ throw new Error("Failed to get context");
87
+ }
88
+
89
+ ctx.drawImage(image, 0, 0, width, height);
90
+
91
+ return canvas.toDataURL("image/png");
92
+ }
93
+
94
+ const resizeDimensionsKeepingAspectRatio = (
95
+ width: number,
96
+ height: number,
97
+ maxSize: number,
98
+ ): { width: number; height: number } => {
99
+ if (width <= maxSize && height <= maxSize) {
100
+ return { width, height };
101
+ }
102
+
103
+ const aspectRatio = width / height;
104
+
105
+ if (width >= height) {
106
+ return { width: maxSize, height: Math.round(maxSize / aspectRatio) };
107
+ } else {
108
+ return { width: Math.round(maxSize * aspectRatio), height: maxSize };
109
+ }
110
+ };
111
+
112
+ async function resize(
113
+ imageBlobOrFile: Blob | File | string,
114
+ width: number,
115
+ height: number,
116
+ ): Promise<Blob> {
117
+ if (typeof imageBlobOrFile === "string") {
118
+ throw new Error("resize(string) is not supported on browser");
119
+ }
120
+
121
+ const mimeType = imageBlobOrFile.type;
122
+
123
+ const image = await getImageFromBlob(imageBlobOrFile);
124
+
125
+ const canvas = document.createElement("canvas");
126
+ canvas.width = width;
127
+ canvas.height = height;
128
+
129
+ const ctx = canvas.getContext("2d");
130
+
131
+ if (!ctx) {
132
+ throw new Error("Failed to get context");
133
+ }
134
+
135
+ ctx.drawImage(image, 0, 0, width, height);
136
+
137
+ return new Promise<Blob>((resolve, reject) => {
138
+ canvas.toBlob(
139
+ (blob) => {
140
+ if (!blob) {
141
+ reject(new Error("Failed to convert canvas to blob"));
142
+ return;
143
+ }
144
+ resolve(blob);
145
+ },
146
+ mimeType,
147
+ 0.8,
148
+ );
149
+ });
150
+ }
@@ -0,0 +1,153 @@
1
+ import ImageResizer from "@bam.tech/react-native-image-resizer";
2
+ import type { Account, Group, ImageDefinition } from "jazz-tools";
3
+ import { FileStream } from "jazz-tools";
4
+ import { Image } from "react-native";
5
+ import {
6
+ CreateImageOptions,
7
+ SourceType,
8
+ createImageFactory,
9
+ } from "./create-image.js";
10
+
11
+ export { highestResAvailable, loadImage, loadImageBySize } from "./utils.js";
12
+ export { createImageFactory };
13
+
14
+ export async function createImage(
15
+ imageBlobOrFile: Blob | File | string,
16
+ options?: CreateImageOptions,
17
+ ) {
18
+ return createImageFactory({
19
+ getImageSize,
20
+ getPlaceholderBase64,
21
+ createFileStreamFromSource,
22
+ resize,
23
+ })(imageBlobOrFile, options || {});
24
+ }
25
+
26
+ async function getImageSize(
27
+ filePath: SourceType,
28
+ ): Promise<{ width: number; height: number }> {
29
+ if (typeof filePath !== "string") {
30
+ throw new Error(
31
+ "createImage(Blob | File) is not supported on this platform",
32
+ );
33
+ }
34
+
35
+ const { width, height } = await Image.getSize(filePath);
36
+
37
+ return { width, height };
38
+ }
39
+
40
+ async function getPlaceholderBase64(filePath: SourceType): Promise<string> {
41
+ if (typeof filePath !== "string") {
42
+ throw new Error(
43
+ "createImage(Blob | File) is not supported on this platform",
44
+ );
45
+ }
46
+
47
+ if (typeof ImageResizer === "undefined" || ImageResizer === null) {
48
+ throw new Error(
49
+ "ImageResizer is not installed, please run `npm install @bam.tech/react-native-image-resizer`",
50
+ );
51
+ }
52
+
53
+ const { uri } = await ImageResizer.createResizedImage(
54
+ filePath,
55
+ 8,
56
+ 8,
57
+ "PNG",
58
+ 100,
59
+ );
60
+
61
+ return imageUrlToBase64(uri);
62
+ }
63
+
64
+ async function resize(
65
+ filePath: SourceType,
66
+ width: number,
67
+ height: number,
68
+ ): Promise<string> {
69
+ if (typeof filePath !== "string") {
70
+ throw new Error(
71
+ "createImage(Blob | File) is not supported on this platform",
72
+ );
73
+ }
74
+
75
+ if (typeof ImageResizer === "undefined" || ImageResizer === null) {
76
+ throw new Error(
77
+ "ImageResizer is not installed, please run `npm install @bam.tech/react-native-image-resizer`",
78
+ );
79
+ }
80
+
81
+ const mimeType = await getMimeType(filePath);
82
+
83
+ const { uri } = await ImageResizer.createResizedImage(
84
+ filePath,
85
+ width,
86
+ height,
87
+ contentTypeToFormat(mimeType),
88
+ 80,
89
+ );
90
+
91
+ return uri;
92
+ }
93
+
94
+ function getMimeType(filePath: string): Promise<string> {
95
+ return fetch(filePath)
96
+ .then((res) => res.blob())
97
+ .then((blob) => blob.type);
98
+ }
99
+
100
+ function contentTypeToFormat(contentType: string) {
101
+ if (contentType.includes("image/png")) return "PNG";
102
+ if (contentType.includes("image/jpeg")) return "JPEG";
103
+ if (contentType.includes("image/webp")) return "WEBP";
104
+ return "PNG";
105
+ }
106
+
107
+ export async function createFileStreamFromSource(
108
+ filePath: SourceType,
109
+ owner?: Account | Group,
110
+ ): Promise<FileStream> {
111
+ if (typeof filePath !== "string") {
112
+ throw new Error(
113
+ "createImage(Blob | File) is not supported on this platform",
114
+ );
115
+ }
116
+
117
+ const blob = await fetch(filePath).then((res) => res.blob());
118
+ const arrayBuffer = await toArrayBuffer(blob);
119
+
120
+ return FileStream.createFromArrayBuffer(arrayBuffer, blob.type, undefined, {
121
+ owner,
122
+ });
123
+ }
124
+
125
+ // TODO: look for more efficient way to do this as React Native hasn't blob.arrayBuffer()
126
+ function toArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
127
+ return new Promise((resolve, reject) => {
128
+ const reader = new FileReader();
129
+ reader.onloadend = () => {
130
+ resolve(reader.result as ArrayBuffer);
131
+ };
132
+ reader.onerror = (error) => {
133
+ reject(error);
134
+ };
135
+ reader.readAsArrayBuffer(blob);
136
+ });
137
+ }
138
+
139
+ async function imageUrlToBase64(url: string): Promise<string> {
140
+ const response = await fetch(url);
141
+ const blob = await response.blob();
142
+ return new Promise((onSuccess, onError) => {
143
+ try {
144
+ const reader = new FileReader();
145
+ reader.onload = function () {
146
+ onSuccess(reader.result as string);
147
+ };
148
+ reader.readAsDataURL(blob);
149
+ } catch (e) {
150
+ onError(e);
151
+ }
152
+ });
153
+ }
@@ -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
+ });