jazz-tools 0.15.2 → 0.15.3

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > jazz-tools@0.15.2 build /home/runner/_work/jazz/jazz/packages/jazz-tools
2
+ > jazz-tools@0.15.3 build /home/runner/_work/jazz/jazz/packages/jazz-tools
3
3
  > tsup && pnpm types && pnpm build:svelte
4
4
 
5
5
  CLI Building entry: {"index":"src/index.ts","testing":"src/testing.ts"}
@@ -109,83 +109,83 @@
109
109
  ESM Build start
110
110
  ESM dist/worker/index.js 2.34 KB
111
111
  ESM dist/worker/index.js.map 4.82 KB
112
- ESM ⚡️ Build success in 13ms
112
+ ESM ⚡️ Build success in 22ms
113
113
  ESM dist/tiptap/index.js 564.00 B
114
114
  ESM dist/tiptap/index.js.map 1.21 KB
115
- ESM ⚡️ Build success in 14ms
116
- ESM dist/browser-media-images/index.js 3.01 KB
117
- ESM dist/browser-media-images/index.js.map 5.68 KB
118
- ESM ⚡️ Build success in 19ms
115
+ ESM ⚡️ Build success in 23ms
116
+ ESM dist/browser-media-images/index.js 2.55 KB
117
+ ESM dist/browser-media-images/index.js.map 5.44 KB
118
+ ESM ⚡️ Build success in 30ms
119
119
  ESM dist/react-native-media-images/index.js 6.19 KB
120
120
  ESM dist/react-native-media-images/index.js.map 10.96 KB
121
- ESM ⚡️ Build success in 16ms
121
+ ESM ⚡️ Build success in 24ms
122
122
  ESM dist/react-core/index.js 8.37 KB
123
- ESM dist/react-core/chunk-7DYMJ74I.js 279.00 B
124
123
  ESM dist/react-core/testing.js 1.17 KB
124
+ ESM dist/react-core/chunk-7DYMJ74I.js 279.00 B
125
125
  ESM dist/react-core/index.js.map 16.39 KB
126
- ESM dist/react-core/chunk-7DYMJ74I.js.map 533.00 B
127
126
  ESM dist/react-core/testing.js.map 1.82 KB
128
- ESM ⚡️ Build success in 19ms
127
+ ESM dist/react-core/chunk-7DYMJ74I.js.map 533.00 B
128
+ ESM ⚡️ Build success in 35ms
129
+ ESM dist/react-native/index.js 2.53 KB
130
+ ESM dist/react-native/testing.js 120.00 B
131
+ ESM dist/react-native/crypto.js 117.00 B
132
+ ESM dist/react-native/index.js.map 5.68 KB
133
+ ESM dist/react-native/testing.js.map 176.00 B
134
+ ESM dist/react-native/crypto.js.map 174.00 B
135
+ ESM ⚡️ Build success in 35ms
129
136
  ESM dist/expo/index.js 4.65 KB
130
137
  ESM dist/expo/testing.js 112.00 B
131
138
  ESM dist/expo/crypto.js 109.00 B
132
139
  ESM dist/expo/index.js.map 10.17 KB
133
- ESM dist/expo/crypto.js.map 166.00 B
134
140
  ESM dist/expo/testing.js.map 168.00 B
135
- ESM ⚡️ Build success in 24ms
136
- ESM dist/react-native/index.js 2.53 KB
137
- ESM dist/react-native/crypto.js 117.00 B
138
- ESM dist/react-native/testing.js 120.00 B
139
- ESM dist/react-native/index.js.map 5.68 KB
140
- ESM dist/react-native/crypto.js.map 174.00 B
141
- ESM dist/react-native/testing.js.map 176.00 B
142
- ESM ⚡️ Build success in 22ms
141
+ ESM dist/expo/crypto.js.map 166.00 B
142
+ ESM ⚡️ Build success in 39ms
143
143
  ESM dist/browser/index.js 13.36 KB
144
144
  ESM dist/browser/index.js.map 28.57 KB
145
- ESM ⚡️ Build success in 26ms
145
+ ESM ⚡️ Build success in 42ms
146
146
  ESM dist/react-native-core/index.js 17.42 KB
147
- ESM dist/react-native-core/crypto.js 2.10 KB
148
147
  ESM dist/react-native-core/testing.js 119.00 B
148
+ ESM dist/react-native-core/crypto.js 2.10 KB
149
149
  ESM dist/react-native-core/index.js.map 34.49 KB
150
- ESM dist/react-native-core/crypto.js.map 4.25 KB
151
150
  ESM dist/react-native-core/testing.js.map 175.00 B
152
- ESM ⚡️ Build success in 34ms
151
+ ESM dist/react-native-core/crypto.js.map 4.25 KB
152
+ ESM ⚡️ Build success in 50ms
153
153
  ESM dist/react/index.js 20.64 KB
154
- ESM dist/react/ssr.js 688.00 B
155
154
  ESM dist/react/testing.js 107.00 B
155
+ ESM dist/react/ssr.js 688.00 B
156
156
  ESM dist/react/index.js.map 34.07 KB
157
- ESM dist/react/ssr.js.map 1.12 KB
158
157
  ESM dist/react/testing.js.map 163.00 B
159
- ESM ⚡️ Build success in 35ms
158
+ ESM dist/react/ssr.js.map 1.12 KB
159
+ ESM ⚡️ Build success in 53ms
160
160
  ESM dist/prosemirror/index.js 76.90 KB
161
161
  ESM dist/prosemirror/index.js.map 305.53 KB
162
- ESM ⚡️ Build success in 37ms
162
+ ESM ⚡️ Build success in 54ms
163
163
  ESM dist/react/index.js 20.66 KB
164
164
  ESM dist/react/testing.js 122.00 B
165
- ESM dist/react/testing.js.map 165.00 B
166
165
  ESM dist/react/index.js.map 34.07 KB
167
- ESM ⚡️ Build success in 36ms
166
+ ESM dist/react/testing.js.map 165.00 B
167
+ ESM ⚡️ Build success in 54ms
168
168
  ESM dist/inspector/index.js 60.37 KB
169
169
  ESM dist/inspector/index.js.map 107.85 KB
170
- ESM ⚡️ Build success in 46ms
171
- ESM dist/index.js 16.19 KB
170
+ ESM ⚡️ Build success in 72ms
172
171
  ESM dist/testing.js 6.32 KB
172
+ ESM dist/index.js 16.19 KB
173
173
  ESM dist/chunk-VBDJM6Z5.js 140.80 KB
174
174
  ESM dist/testing.js.map 12.22 KB
175
175
  ESM dist/index.js.map 29.03 KB
176
176
  ESM dist/chunk-VBDJM6Z5.js.map 320.73 KB
177
- ESM ⚡️ Build success in 59ms
177
+ ESM ⚡️ Build success in 88ms
178
178
  ESM dist/inspector/register-custom-element.js 218.00 B
179
179
  ESM dist/inspector/register-custom-element.js.map 314.00 B
180
180
  ESM dist/inspector/custom-element-CWW72LEG.js 1.47 MB
181
181
  ESM dist/inspector/custom-element-CWW72LEG.js.map 2.26 MB
182
- ESM ⚡️ Build success in 89ms
182
+ ESM ⚡️ Build success in 149ms
183
183
 
184
- > jazz-tools@0.15.2 types /home/runner/_work/jazz/jazz/packages/jazz-tools
184
+ > jazz-tools@0.15.3 types /home/runner/_work/jazz/jazz/packages/jazz-tools
185
185
  > tsc --outDir dist
186
186
 
187
187
 
188
- > jazz-tools@0.15.2 build:svelte /home/runner/_work/jazz/jazz/packages/jazz-tools
188
+ > jazz-tools@0.15.3 build:svelte /home/runner/_work/jazz/jazz/packages/jazz-tools
189
189
  > rm -rf dist/svelte && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json
190
190
 
191
191
  src/svelte -> dist/svelte
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # jazz-tools
2
2
 
3
+ ## 0.15.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 45f73a7: fix image `originalSize` to be coherent with the highest resolution
8
+ - Updated dependencies [535c460]
9
+ - cojson-storage-indexeddb@0.15.3
10
+ - cojson@0.15.3
11
+ - cojson-storage@0.15.3
12
+ - cojson-transport-ws@0.15.3
13
+
3
14
  ## 0.15.2
4
15
 
5
16
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser-media-images/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,OAAO,EAEP,KAAK,EACL,eAAe,EACf,MAAM,EACP,MAAM,YAAY,CAAC;AAKpB,+BAA+B;AAC/B,wBAAsB,WAAW,CAC/B,eAAe,EAAE,IAAI,GAAG,IAAI,EAC5B,OAAO,CAAC,EAAE;IACR,KAAK,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;CAC7B,GACA,OAAO,CAAC,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC,CA8GzC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser-media-images/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,OAAO,EAEP,KAAK,EACL,eAAe,EACf,MAAM,EACP,MAAM,YAAY,CAAC;AAKpB,+BAA+B;AAC/B,wBAAsB,WAAW,CAC/B,eAAe,EAAE,IAAI,GAAG,IAAI,EAC5B,OAAO,CAAC,EAAE;IACR,KAAK,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;CAC7B,GACA,OAAO,CAAC,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC,CA8CzC"}
@@ -5,65 +5,67 @@ import {
5
5
  ImageDefinition
6
6
  } from "jazz-tools";
7
7
  import Pica from "pica";
8
- var pica;
8
+ var reducer;
9
9
  async function createImage(imageBlobOrFile, options) {
10
- if (!pica) {
11
- pica = new Pica();
12
- }
13
- let originalWidth;
14
- let originalHeight;
15
- const Reducer = new ImageBlobReduce({ pica });
16
- Reducer.after("_blob_to_image", (env) => {
17
- originalWidth = env.orientation & 4 ? env.image.height : env.image.width;
18
- originalHeight = env.orientation & 4 ? env.image.width : env.image.height;
19
- return Promise.resolve(env);
20
- });
21
- const placeholderDataURL = (await Reducer.toCanvas(imageBlobOrFile, { max: 8 })).toDataURL("image/png");
10
+ const { width: originalWidth, height: originalHeight } = await getImageSize(imageBlobOrFile);
11
+ const highestDimension = Math.max(originalWidth, originalHeight);
12
+ const resizes = [256, 1024, 2048, highestDimension].filter((s) => s <= (options?.maxSize ?? highestDimension)).toSorted((a, b) => a - b);
13
+ const { width: finalWidth, height: finalHeight } = getNewDimensions(
14
+ originalWidth,
15
+ originalHeight,
16
+ resizes.at(-1)
17
+ );
22
18
  const imageDefinition = ImageDefinition.create(
23
- {
24
- originalSize: [originalWidth, originalHeight],
25
- placeholderDataURL
26
- },
19
+ { originalSize: [finalWidth, finalHeight] },
27
20
  options?.owner
28
21
  );
29
22
  const owner = imageDefinition._owner;
30
- const fillImageResolutions = async () => {
31
- const max256 = await Reducer.toBlob(imageBlobOrFile, { max: 256 });
32
- if (originalWidth > 256 || originalHeight > 256) {
33
- const width = originalWidth > originalHeight ? 256 : Math.round(256 * (originalWidth / originalHeight));
34
- const height = originalHeight > originalWidth ? 256 : Math.round(256 * (originalHeight / originalWidth));
35
- const binaryStream = await FileStream.createFromBlob(max256, owner);
36
- imageDefinition[`${width}x${height}`] = binaryStream;
37
- }
38
- await new Promise((resolve) => setTimeout(resolve, 0));
39
- if (options?.maxSize === 256) return;
40
- const max1024 = await Reducer.toBlob(imageBlobOrFile, { max: 1024 });
41
- if (originalWidth > 1024 || originalHeight > 1024) {
42
- const width = originalWidth > originalHeight ? 1024 : Math.round(1024 * (originalWidth / originalHeight));
43
- const height = originalHeight > originalWidth ? 1024 : Math.round(1024 * (originalHeight / originalWidth));
44
- const binaryStream = await FileStream.createFromBlob(max1024, owner);
45
- imageDefinition[`${width}x${height}`] = binaryStream;
46
- }
47
- await new Promise((resolve) => setTimeout(resolve, 0));
48
- if (options?.maxSize === 1024) return;
49
- const max2048 = await Reducer.toBlob(imageBlobOrFile, { max: 2048 });
50
- if (originalWidth > 2048 || originalHeight > 2048) {
51
- const width = originalWidth > originalHeight ? 2048 : Math.round(2048 * (originalWidth / originalHeight));
52
- const height = originalHeight > originalWidth ? 2048 : Math.round(2048 * (originalHeight / originalWidth));
53
- const binaryStream = await FileStream.createFromBlob(max2048, owner);
54
- imageDefinition[`${width}x${height}`] = binaryStream;
55
- }
56
- await new Promise((resolve) => setTimeout(resolve, 0));
57
- if (options?.maxSize === 2048) return;
58
- const originalBinaryStream = await FileStream.createFromBlob(
59
- imageBlobOrFile,
60
- owner
23
+ imageDefinition.placeholderDataURL = await getPlaceholderBase64(imageBlobOrFile);
24
+ for (let size of resizes) {
25
+ const { width, height } = getNewDimensions(
26
+ originalWidth,
27
+ originalHeight,
28
+ size
61
29
  );
62
- imageDefinition[`${originalWidth}x${originalHeight}`] = originalBinaryStream;
63
- };
64
- await fillImageResolutions();
30
+ const image = await resize(imageBlobOrFile, width, height);
31
+ const binaryStream = await FileStream.createFromBlob(image, owner);
32
+ imageDefinition[`${width}x${height}`] = binaryStream;
33
+ }
65
34
  return imageDefinition;
66
35
  }
36
+ async function getImageSize(imageBlobOrFile) {
37
+ const { width, height } = await new Promise((resolve, reject) => {
38
+ const img = new Image();
39
+ img.onload = () => {
40
+ resolve({ width: img.width, height: img.height });
41
+ URL.revokeObjectURL(img.src);
42
+ };
43
+ img.onerror = () => {
44
+ reject(new Error("Failed to load image"));
45
+ URL.revokeObjectURL(img.src);
46
+ };
47
+ img.src = URL.createObjectURL(imageBlobOrFile);
48
+ });
49
+ return { width, height };
50
+ }
51
+ async function getPlaceholderBase64(imageBlobOrFile) {
52
+ if (!reducer) {
53
+ reducer = new ImageBlobReduce({ pica: new Pica() });
54
+ }
55
+ const canvas = await reducer.toCanvas(imageBlobOrFile, { max: 8 });
56
+ return canvas.toDataURL("image/png");
57
+ }
58
+ async function resize(imageBlobOrFile, width, height) {
59
+ if (!reducer) {
60
+ reducer = new ImageBlobReduce({ pica: new Pica() });
61
+ }
62
+ return reducer.toBlob(imageBlobOrFile, { max: Math.max(width, height) });
63
+ }
64
+ var getNewDimensions = (originalWidth, originalHeight, maxSize) => {
65
+ const width = originalWidth > originalHeight ? maxSize : Math.round(maxSize * (originalWidth / originalHeight));
66
+ const height = originalHeight > originalWidth ? maxSize : Math.round(maxSize * (originalHeight / originalWidth));
67
+ return { width, height };
68
+ };
67
69
  export {
68
70
  createImage
69
71
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/browser-media-images/index.ts"],"sourcesContent":["import ImageBlobReduce from \"image-blob-reduce\";\nimport {\n Account,\n FileStream,\n Group,\n ImageDefinition,\n Loaded,\n} from \"jazz-tools\";\nimport Pica from \"pica\";\n\nlet pica: Pica.Pica | undefined;\n\n/** @category Image creation */\nexport async function createImage(\n imageBlobOrFile: Blob | File,\n options?: {\n owner?: Group | Account;\n maxSize?: 256 | 1024 | 2048;\n },\n): Promise<Loaded<typeof ImageDefinition>> {\n // Inizialize Pica here to not have module side effects\n if (!pica) {\n pica = new Pica();\n }\n\n let originalWidth!: number;\n let originalHeight!: number;\n const Reducer = new ImageBlobReduce({ pica });\n Reducer.after(\"_blob_to_image\", (env) => {\n originalWidth =\n (env as unknown as { orientation: number }).orientation & 4\n ? env.image.height\n : env.image.width;\n originalHeight =\n (env as unknown as { orientation: number }).orientation & 4\n ? env.image.width\n : env.image.height;\n return Promise.resolve(env);\n });\n\n const placeholderDataURL = (\n await Reducer.toCanvas(imageBlobOrFile, { max: 8 })\n ).toDataURL(\"image/png\");\n\n const imageDefinition = ImageDefinition.create(\n {\n originalSize: [originalWidth, originalHeight],\n placeholderDataURL,\n },\n options?.owner,\n );\n const owner = imageDefinition._owner;\n\n const fillImageResolutions = async () => {\n const max256 = await Reducer.toBlob(imageBlobOrFile, { max: 256 });\n\n if (originalWidth > 256 || originalHeight > 256) {\n const width =\n originalWidth > originalHeight\n ? 256\n : Math.round(256 * (originalWidth / originalHeight));\n const height =\n originalHeight > originalWidth\n ? 256\n : Math.round(256 * (originalHeight / originalWidth));\n\n const binaryStream = await FileStream.createFromBlob(max256, owner);\n\n imageDefinition[`${width}x${height}`] = binaryStream;\n }\n\n await new Promise((resolve) => setTimeout(resolve, 0));\n\n if (options?.maxSize === 256) return;\n\n const max1024 = await Reducer.toBlob(imageBlobOrFile, { max: 1024 });\n\n if (originalWidth > 1024 || originalHeight > 1024) {\n const width =\n originalWidth > originalHeight\n ? 1024\n : Math.round(1024 * (originalWidth / originalHeight));\n const height =\n originalHeight > originalWidth\n ? 1024\n : Math.round(1024 * (originalHeight / originalWidth));\n\n const binaryStream = await FileStream.createFromBlob(max1024, owner);\n\n imageDefinition[`${width}x${height}`] = binaryStream;\n }\n\n await new Promise((resolve) => setTimeout(resolve, 0));\n\n if (options?.maxSize === 1024) return;\n\n const max2048 = await Reducer.toBlob(imageBlobOrFile, { max: 2048 });\n\n if (originalWidth > 2048 || originalHeight > 2048) {\n const width =\n originalWidth > originalHeight\n ? 2048\n : Math.round(2048 * (originalWidth / originalHeight));\n const height =\n originalHeight > originalWidth\n ? 2048\n : Math.round(2048 * (originalHeight / originalWidth));\n\n const binaryStream = await FileStream.createFromBlob(max2048, owner);\n\n imageDefinition[`${width}x${height}`] = binaryStream;\n }\n\n await new Promise((resolve) => setTimeout(resolve, 0));\n\n if (options?.maxSize === 2048) return;\n\n const originalBinaryStream = await FileStream.createFromBlob(\n imageBlobOrFile,\n owner,\n );\n\n imageDefinition[`${originalWidth}x${originalHeight}`] =\n originalBinaryStream;\n };\n\n await fillImageResolutions();\n\n return imageDefinition;\n}\n"],"mappings":";AAAA,OAAO,qBAAqB;AAC5B;AAAA,EAEE;AAAA,EAEA;AAAA,OAEK;AACP,OAAO,UAAU;AAEjB,IAAI;AAGJ,eAAsB,YACpB,iBACA,SAIyC;AAEzC,MAAI,CAAC,MAAM;AACT,WAAO,IAAI,KAAK;AAAA,EAClB;AAEA,MAAI;AACJ,MAAI;AACJ,QAAM,UAAU,IAAI,gBAAgB,EAAE,KAAK,CAAC;AAC5C,UAAQ,MAAM,kBAAkB,CAAC,QAAQ;AACvC,oBACG,IAA2C,cAAc,IACtD,IAAI,MAAM,SACV,IAAI,MAAM;AAChB,qBACG,IAA2C,cAAc,IACtD,IAAI,MAAM,QACV,IAAI,MAAM;AAChB,WAAO,QAAQ,QAAQ,GAAG;AAAA,EAC5B,CAAC;AAED,QAAM,sBACJ,MAAM,QAAQ,SAAS,iBAAiB,EAAE,KAAK,EAAE,CAAC,GAClD,UAAU,WAAW;AAEvB,QAAM,kBAAkB,gBAAgB;AAAA,IACtC;AAAA,MACE,cAAc,CAAC,eAAe,cAAc;AAAA,MAC5C;AAAA,IACF;AAAA,IACA,SAAS;AAAA,EACX;AACA,QAAM,QAAQ,gBAAgB;AAE9B,QAAM,uBAAuB,YAAY;AACvC,UAAM,SAAS,MAAM,QAAQ,OAAO,iBAAiB,EAAE,KAAK,IAAI,CAAC;AAEjE,QAAI,gBAAgB,OAAO,iBAAiB,KAAK;AAC/C,YAAM,QACJ,gBAAgB,iBACZ,MACA,KAAK,MAAM,OAAO,gBAAgB,eAAe;AACvD,YAAM,SACJ,iBAAiB,gBACb,MACA,KAAK,MAAM,OAAO,iBAAiB,cAAc;AAEvD,YAAM,eAAe,MAAM,WAAW,eAAe,QAAQ,KAAK;AAElE,sBAAgB,GAAG,KAAK,IAAI,MAAM,EAAE,IAAI;AAAA,IAC1C;AAEA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,CAAC,CAAC;AAErD,QAAI,SAAS,YAAY,IAAK;AAE9B,UAAM,UAAU,MAAM,QAAQ,OAAO,iBAAiB,EAAE,KAAK,KAAK,CAAC;AAEnE,QAAI,gBAAgB,QAAQ,iBAAiB,MAAM;AACjD,YAAM,QACJ,gBAAgB,iBACZ,OACA,KAAK,MAAM,QAAQ,gBAAgB,eAAe;AACxD,YAAM,SACJ,iBAAiB,gBACb,OACA,KAAK,MAAM,QAAQ,iBAAiB,cAAc;AAExD,YAAM,eAAe,MAAM,WAAW,eAAe,SAAS,KAAK;AAEnE,sBAAgB,GAAG,KAAK,IAAI,MAAM,EAAE,IAAI;AAAA,IAC1C;AAEA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,CAAC,CAAC;AAErD,QAAI,SAAS,YAAY,KAAM;AAE/B,UAAM,UAAU,MAAM,QAAQ,OAAO,iBAAiB,EAAE,KAAK,KAAK,CAAC;AAEnE,QAAI,gBAAgB,QAAQ,iBAAiB,MAAM;AACjD,YAAM,QACJ,gBAAgB,iBACZ,OACA,KAAK,MAAM,QAAQ,gBAAgB,eAAe;AACxD,YAAM,SACJ,iBAAiB,gBACb,OACA,KAAK,MAAM,QAAQ,iBAAiB,cAAc;AAExD,YAAM,eAAe,MAAM,WAAW,eAAe,SAAS,KAAK;AAEnE,sBAAgB,GAAG,KAAK,IAAI,MAAM,EAAE,IAAI;AAAA,IAC1C;AAEA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,CAAC,CAAC;AAErD,QAAI,SAAS,YAAY,KAAM;AAE/B,UAAM,uBAAuB,MAAM,WAAW;AAAA,MAC5C;AAAA,MACA;AAAA,IACF;AAEA,oBAAgB,GAAG,aAAa,IAAI,cAAc,EAAE,IAClD;AAAA,EACJ;AAEA,QAAM,qBAAqB;AAE3B,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/browser-media-images/index.ts"],"sourcesContent":["import ImageBlobReduce from \"image-blob-reduce\";\nimport {\n Account,\n FileStream,\n Group,\n ImageDefinition,\n Loaded,\n} from \"jazz-tools\";\nimport Pica from \"pica\";\n\nlet reducer: ImageBlobReduce.ImageBlobReduce | undefined;\n\n/** @category Image creation */\nexport async function createImage(\n imageBlobOrFile: Blob | File,\n options?: {\n owner?: Group | Account;\n maxSize?: 256 | 1024 | 2048;\n },\n): Promise<Loaded<typeof ImageDefinition>> {\n // Get the original size of the image\n const { width: originalWidth, height: originalHeight } =\n await getImageSize(imageBlobOrFile);\n\n const highestDimension = Math.max(originalWidth, originalHeight);\n\n // Calculate the sizes to resize the image to\n const resizes = [256, 1024, 2048, highestDimension]\n .filter((s) => s <= (options?.maxSize ?? highestDimension))\n .toSorted((a, b) => a - b);\n\n // Get the highest resolution to use as final original size\n // In case of options.maxSize, it's not the originalWidth/Height\n const { width: finalWidth, height: finalHeight } = getNewDimensions(\n originalWidth,\n originalHeight,\n resizes.at(-1)!,\n );\n\n const imageDefinition = ImageDefinition.create(\n { originalSize: [finalWidth, finalHeight] },\n options?.owner,\n );\n const owner = imageDefinition._owner;\n\n // Placeholder 8x8\n imageDefinition.placeholderDataURL =\n await getPlaceholderBase64(imageBlobOrFile);\n\n // Resizes for progressive loading\n for (let size of resizes) {\n // Calculate width and height respecting the aspect ratio\n const { width, height } = getNewDimensions(\n originalWidth,\n originalHeight,\n size,\n );\n\n const image = await resize(imageBlobOrFile, width, height);\n\n const binaryStream = await FileStream.createFromBlob(image, owner);\n imageDefinition[`${width}x${height}`] = binaryStream;\n }\n\n return imageDefinition;\n}\n\nasync function getImageSize(\n imageBlobOrFile: Blob | File,\n): Promise<{ width: number; height: number }> {\n const { width, height } = await new Promise<{\n width: number;\n height: number;\n }>((resolve, reject) => {\n const img = new Image();\n img.onload = () => {\n resolve({ width: img.width, height: img.height });\n URL.revokeObjectURL(img.src);\n };\n img.onerror = () => {\n reject(new Error(\"Failed to load image\"));\n URL.revokeObjectURL(img.src);\n };\n img.src = URL.createObjectURL(imageBlobOrFile);\n });\n\n return { width, height };\n}\n\nasync function getPlaceholderBase64(\n imageBlobOrFile: Blob | File,\n): Promise<string> {\n // Inizialize Reducer here to not have module side effects\n if (!reducer) {\n reducer = new ImageBlobReduce({ pica: new Pica() });\n }\n\n const canvas = await reducer.toCanvas(imageBlobOrFile, { max: 8 });\n return canvas.toDataURL(\"image/png\");\n}\n\nasync function resize(\n imageBlobOrFile: Blob | File,\n width: number,\n height: number,\n): Promise<Blob> {\n // Inizialize Reducer here to not have module side effects\n if (!reducer) {\n reducer = new ImageBlobReduce({ pica: new Pica() });\n }\n\n return reducer.toBlob(imageBlobOrFile, { max: Math.max(width, height) });\n}\n\nconst getNewDimensions = (\n originalWidth: number,\n originalHeight: number,\n maxSize: number,\n) => {\n const width =\n originalWidth > originalHeight\n ? maxSize\n : Math.round(maxSize * (originalWidth / originalHeight));\n\n const height =\n originalHeight > originalWidth\n ? maxSize\n : Math.round(maxSize * (originalHeight / originalWidth));\n\n return { width, height };\n};\n"],"mappings":";AAAA,OAAO,qBAAqB;AAC5B;AAAA,EAEE;AAAA,EAEA;AAAA,OAEK;AACP,OAAO,UAAU;AAEjB,IAAI;AAGJ,eAAsB,YACpB,iBACA,SAIyC;AAEzC,QAAM,EAAE,OAAO,eAAe,QAAQ,eAAe,IACnD,MAAM,aAAa,eAAe;AAEpC,QAAM,mBAAmB,KAAK,IAAI,eAAe,cAAc;AAG/D,QAAM,UAAU,CAAC,KAAK,MAAM,MAAM,gBAAgB,EAC/C,OAAO,CAAC,MAAM,MAAM,SAAS,WAAW,iBAAiB,EACzD,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;AAI3B,QAAM,EAAE,OAAO,YAAY,QAAQ,YAAY,IAAI;AAAA,IACjD;AAAA,IACA;AAAA,IACA,QAAQ,GAAG,EAAE;AAAA,EACf;AAEA,QAAM,kBAAkB,gBAAgB;AAAA,IACtC,EAAE,cAAc,CAAC,YAAY,WAAW,EAAE;AAAA,IAC1C,SAAS;AAAA,EACX;AACA,QAAM,QAAQ,gBAAgB;AAG9B,kBAAgB,qBACd,MAAM,qBAAqB,eAAe;AAG5C,WAAS,QAAQ,SAAS;AAExB,UAAM,EAAE,OAAO,OAAO,IAAI;AAAA,MACxB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,QAAQ,MAAM,OAAO,iBAAiB,OAAO,MAAM;AAEzD,UAAM,eAAe,MAAM,WAAW,eAAe,OAAO,KAAK;AACjE,oBAAgB,GAAG,KAAK,IAAI,MAAM,EAAE,IAAI;AAAA,EAC1C;AAEA,SAAO;AACT;AAEA,eAAe,aACb,iBAC4C;AAC5C,QAAM,EAAE,OAAO,OAAO,IAAI,MAAM,IAAI,QAGjC,CAAC,SAAS,WAAW;AACtB,UAAM,MAAM,IAAI,MAAM;AACtB,QAAI,SAAS,MAAM;AACjB,cAAQ,EAAE,OAAO,IAAI,OAAO,QAAQ,IAAI,OAAO,CAAC;AAChD,UAAI,gBAAgB,IAAI,GAAG;AAAA,IAC7B;AACA,QAAI,UAAU,MAAM;AAClB,aAAO,IAAI,MAAM,sBAAsB,CAAC;AACxC,UAAI,gBAAgB,IAAI,GAAG;AAAA,IAC7B;AACA,QAAI,MAAM,IAAI,gBAAgB,eAAe;AAAA,EAC/C,CAAC;AAED,SAAO,EAAE,OAAO,OAAO;AACzB;AAEA,eAAe,qBACb,iBACiB;AAEjB,MAAI,CAAC,SAAS;AACZ,cAAU,IAAI,gBAAgB,EAAE,MAAM,IAAI,KAAK,EAAE,CAAC;AAAA,EACpD;AAEA,QAAM,SAAS,MAAM,QAAQ,SAAS,iBAAiB,EAAE,KAAK,EAAE,CAAC;AACjE,SAAO,OAAO,UAAU,WAAW;AACrC;AAEA,eAAe,OACb,iBACA,OACA,QACe;AAEf,MAAI,CAAC,SAAS;AACZ,cAAU,IAAI,gBAAgB,EAAE,MAAM,IAAI,KAAK,EAAE,CAAC;AAAA,EACpD;AAEA,SAAO,QAAQ,OAAO,iBAAiB,EAAE,KAAK,KAAK,IAAI,OAAO,MAAM,EAAE,CAAC;AACzE;AAEA,IAAM,mBAAmB,CACvB,eACA,gBACA,YACG;AACH,QAAM,QACJ,gBAAgB,iBACZ,UACA,KAAK,MAAM,WAAW,gBAAgB,eAAe;AAE3D,QAAM,SACJ,iBAAiB,gBACb,UACA,KAAK,MAAM,WAAW,iBAAiB,cAAc;AAE3D,SAAO,EAAE,OAAO,OAAO;AACzB;","names":[]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.browser.d.ts","sourceRoot":"","sources":["../../src/browser-media-images/index.test.browser.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -139,7 +139,7 @@
139
139
  },
140
140
  "type": "module",
141
141
  "license": "MIT",
142
- "version": "0.15.2",
142
+ "version": "0.15.3",
143
143
  "dependencies": {
144
144
  "@manuscripts/prosemirror-recreate-steps": "^0.1.4",
145
145
  "@op-engineering/op-sqlite": "^11.4.8",
@@ -164,10 +164,10 @@
164
164
  "react-native-nitro-modules": "0.25.2",
165
165
  "react-native-quick-crypto": "1.0.0-beta.16",
166
166
  "zod": "3.25.28",
167
- "cojson": "0.15.2",
168
- "cojson-storage": "0.15.2",
169
- "cojson-storage-indexeddb": "0.15.2",
170
- "cojson-transport-ws": "0.15.2"
167
+ "cojson": "0.15.3",
168
+ "cojson-storage-indexeddb": "0.15.3",
169
+ "cojson-storage": "0.15.3",
170
+ "cojson-transport-ws": "0.15.3"
171
171
  },
172
172
  "devDependencies": {
173
173
  "@scure/bip39": "^1.3.0",
@@ -177,6 +177,8 @@
177
177
  "@testing-library/react": "16.2.0",
178
178
  "@types/react": "19.0.0",
179
179
  "@types/react-dom": "19.0.0",
180
+ "@vitest/browser": "^3.2.4",
181
+ "playwright": "^1.50.1",
180
182
  "tsup": "8.5.0",
181
183
  "typescript": "5.6.2",
182
184
  "vitest": "3.1.3",
@@ -0,0 +1,73 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { createJazzTestAccount } from "jazz-tools/testing";
4
+ import { describe, expect, it } from "vitest";
5
+ import { createImage } from "./index.js";
6
+
7
+ describe("createImage", () => {
8
+ it("should create an image with a single size if width/height < 256", async () => {
9
+ const OnePixel =
10
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
11
+ const imageBlob = new Blob(
12
+ [Uint8Array.from(atob(OnePixel), (c) => c.charCodeAt(0))],
13
+ { type: "image/png" },
14
+ );
15
+
16
+ const account = await createJazzTestAccount();
17
+
18
+ const image = await createImage(imageBlob, { owner: account._owner });
19
+ expect(image).toBeDefined();
20
+
21
+ expect(image.originalSize).toEqual([1, 1]);
22
+ expect(image.placeholderDataURL).toBeDefined();
23
+
24
+ expect(image[`1x1`]).toBeDefined();
25
+ expect(image[`1x1`]!.getMetadata()!.mimeType).toBe("image/png");
26
+ expect(image["256x256"]).not.toBeDefined();
27
+ expect(image["1024x1024"]).not.toBeDefined();
28
+ });
29
+
30
+ it("should create an image with three sizes", async () => {
31
+ const imageBlob = new Blob(
32
+ [Uint8Array.from(White1920, (c) => c.charCodeAt(0))],
33
+ { type: "image/png" },
34
+ );
35
+
36
+ const account = await createJazzTestAccount();
37
+
38
+ const image = await createImage(imageBlob, { owner: account._owner });
39
+ expect(image).toBeDefined();
40
+
41
+ expect(image.originalSize).toEqual([1920, 400]);
42
+ expect(image.placeholderDataURL).toBeDefined();
43
+ expect(image[`256x53`]).toBeDefined();
44
+ expect(image[`1024x213`]).toBeDefined();
45
+ expect(image[`1920x400`]).toBeDefined();
46
+ });
47
+
48
+ it("should lose the original size and create image based on maxSize", async () => {
49
+ const imageBlob = new Blob(
50
+ [Uint8Array.from(White1920, (c) => c.charCodeAt(0))],
51
+ { type: "image/png" },
52
+ );
53
+
54
+ const account = await createJazzTestAccount();
55
+
56
+ const image = await createImage(imageBlob, {
57
+ owner: account._owner,
58
+ maxSize: 256,
59
+ });
60
+ expect(image).toBeDefined();
61
+
62
+ expect(image.originalSize).toEqual([256, 53]);
63
+ expect(image.placeholderDataURL).toBeDefined();
64
+ expect(image[`256x53`]).toBeDefined();
65
+ expect(image[`1024x213`]).not.toBeDefined();
66
+ expect(image[`1920x400`]).not.toBeDefined();
67
+ });
68
+ });
69
+
70
+ // Image 1920x400
71
+ const White1920 = atob(
72
+ "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCAGQB4ADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAP/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//2Q==",
73
+ );
@@ -8,7 +8,7 @@ import {
8
8
  } from "jazz-tools";
9
9
  import Pica from "pica";
10
10
 
11
- let pica: Pica.Pica | undefined;
11
+ let reducer: ImageBlobReduce.ImageBlobReduce | undefined;
12
12
 
13
13
  /** @category Image creation */
14
14
  export async function createImage(
@@ -18,113 +18,114 @@ export async function createImage(
18
18
  maxSize?: 256 | 1024 | 2048;
19
19
  },
20
20
  ): Promise<Loaded<typeof ImageDefinition>> {
21
- // Inizialize Pica here to not have module side effects
22
- if (!pica) {
23
- pica = new Pica();
24
- }
25
-
26
- let originalWidth!: number;
27
- let originalHeight!: number;
28
- const Reducer = new ImageBlobReduce({ pica });
29
- Reducer.after("_blob_to_image", (env) => {
30
- originalWidth =
31
- (env as unknown as { orientation: number }).orientation & 4
32
- ? env.image.height
33
- : env.image.width;
34
- originalHeight =
35
- (env as unknown as { orientation: number }).orientation & 4
36
- ? env.image.width
37
- : env.image.height;
38
- return Promise.resolve(env);
39
- });
40
-
41
- const placeholderDataURL = (
42
- await Reducer.toCanvas(imageBlobOrFile, { max: 8 })
43
- ).toDataURL("image/png");
21
+ // Get the original size of the image
22
+ const { width: originalWidth, height: originalHeight } =
23
+ await getImageSize(imageBlobOrFile);
24
+
25
+ const highestDimension = Math.max(originalWidth, originalHeight);
26
+
27
+ // Calculate the sizes to resize the image to
28
+ const resizes = [256, 1024, 2048, highestDimension]
29
+ .filter((s) => s <= (options?.maxSize ?? highestDimension))
30
+ .toSorted((a, b) => a - b);
31
+
32
+ // Get the highest resolution to use as final original size
33
+ // In case of options.maxSize, it's not the originalWidth/Height
34
+ const { width: finalWidth, height: finalHeight } = getNewDimensions(
35
+ originalWidth,
36
+ originalHeight,
37
+ resizes.at(-1)!,
38
+ );
44
39
 
45
40
  const imageDefinition = ImageDefinition.create(
46
- {
47
- originalSize: [originalWidth, originalHeight],
48
- placeholderDataURL,
49
- },
41
+ { originalSize: [finalWidth, finalHeight] },
50
42
  options?.owner,
51
43
  );
52
44
  const owner = imageDefinition._owner;
53
45
 
54
- const fillImageResolutions = async () => {
55
- const max256 = await Reducer.toBlob(imageBlobOrFile, { max: 256 });
56
-
57
- if (originalWidth > 256 || originalHeight > 256) {
58
- const width =
59
- originalWidth > originalHeight
60
- ? 256
61
- : Math.round(256 * (originalWidth / originalHeight));
62
- const height =
63
- originalHeight > originalWidth
64
- ? 256
65
- : Math.round(256 * (originalHeight / originalWidth));
66
-
67
- const binaryStream = await FileStream.createFromBlob(max256, owner);
68
-
69
- imageDefinition[`${width}x${height}`] = binaryStream;
70
- }
71
-
72
- await new Promise((resolve) => setTimeout(resolve, 0));
73
-
74
- if (options?.maxSize === 256) return;
75
-
76
- const max1024 = await Reducer.toBlob(imageBlobOrFile, { max: 1024 });
77
-
78
- if (originalWidth > 1024 || originalHeight > 1024) {
79
- const width =
80
- originalWidth > originalHeight
81
- ? 1024
82
- : Math.round(1024 * (originalWidth / originalHeight));
83
- const height =
84
- originalHeight > originalWidth
85
- ? 1024
86
- : Math.round(1024 * (originalHeight / originalWidth));
87
-
88
- const binaryStream = await FileStream.createFromBlob(max1024, owner);
89
-
90
- imageDefinition[`${width}x${height}`] = binaryStream;
91
- }
92
-
93
- await new Promise((resolve) => setTimeout(resolve, 0));
94
-
95
- if (options?.maxSize === 1024) return;
96
-
97
- const max2048 = await Reducer.toBlob(imageBlobOrFile, { max: 2048 });
46
+ // Placeholder 8x8
47
+ imageDefinition.placeholderDataURL =
48
+ await getPlaceholderBase64(imageBlobOrFile);
49
+
50
+ // Resizes for progressive loading
51
+ for (let size of resizes) {
52
+ // Calculate width and height respecting the aspect ratio
53
+ const { width, height } = getNewDimensions(
54
+ originalWidth,
55
+ originalHeight,
56
+ size,
57
+ );
98
58
 
99
- if (originalWidth > 2048 || originalHeight > 2048) {
100
- const width =
101
- originalWidth > originalHeight
102
- ? 2048
103
- : Math.round(2048 * (originalWidth / originalHeight));
104
- const height =
105
- originalHeight > originalWidth
106
- ? 2048
107
- : Math.round(2048 * (originalHeight / originalWidth));
59
+ const image = await resize(imageBlobOrFile, width, height);
108
60
 
109
- const binaryStream = await FileStream.createFromBlob(max2048, owner);
61
+ const binaryStream = await FileStream.createFromBlob(image, owner);
62
+ imageDefinition[`${width}x${height}`] = binaryStream;
63
+ }
110
64
 
111
- imageDefinition[`${width}x${height}`] = binaryStream;
112
- }
65
+ return imageDefinition;
66
+ }
113
67
 
114
- await new Promise((resolve) => setTimeout(resolve, 0));
68
+ async function getImageSize(
69
+ imageBlobOrFile: Blob | File,
70
+ ): Promise<{ width: number; height: number }> {
71
+ const { width, height } = await new Promise<{
72
+ width: number;
73
+ height: number;
74
+ }>((resolve, reject) => {
75
+ const img = new Image();
76
+ img.onload = () => {
77
+ resolve({ width: img.width, height: img.height });
78
+ URL.revokeObjectURL(img.src);
79
+ };
80
+ img.onerror = () => {
81
+ reject(new Error("Failed to load image"));
82
+ URL.revokeObjectURL(img.src);
83
+ };
84
+ img.src = URL.createObjectURL(imageBlobOrFile);
85
+ });
115
86
 
116
- if (options?.maxSize === 2048) return;
87
+ return { width, height };
88
+ }
117
89
 
118
- const originalBinaryStream = await FileStream.createFromBlob(
119
- imageBlobOrFile,
120
- owner,
121
- );
90
+ async function getPlaceholderBase64(
91
+ imageBlobOrFile: Blob | File,
92
+ ): Promise<string> {
93
+ // Inizialize Reducer here to not have module side effects
94
+ if (!reducer) {
95
+ reducer = new ImageBlobReduce({ pica: new Pica() });
96
+ }
122
97
 
123
- imageDefinition[`${originalWidth}x${originalHeight}`] =
124
- originalBinaryStream;
125
- };
98
+ const canvas = await reducer.toCanvas(imageBlobOrFile, { max: 8 });
99
+ return canvas.toDataURL("image/png");
100
+ }
126
101
 
127
- await fillImageResolutions();
102
+ async function resize(
103
+ imageBlobOrFile: Blob | File,
104
+ width: number,
105
+ height: number,
106
+ ): Promise<Blob> {
107
+ // Inizialize Reducer here to not have module side effects
108
+ if (!reducer) {
109
+ reducer = new ImageBlobReduce({ pica: new Pica() });
110
+ }
128
111
 
129
- return imageDefinition;
112
+ return reducer.toBlob(imageBlobOrFile, { max: Math.max(width, height) });
130
113
  }
114
+
115
+ const getNewDimensions = (
116
+ originalWidth: number,
117
+ originalHeight: number,
118
+ maxSize: number,
119
+ ) => {
120
+ const width =
121
+ originalWidth > originalHeight
122
+ ? maxSize
123
+ : Math.round(maxSize * (originalWidth / originalHeight));
124
+
125
+ const height =
126
+ originalHeight > originalWidth
127
+ ? maxSize
128
+ : Math.round(maxSize * (originalHeight / originalWidth));
129
+
130
+ return { width, height };
131
+ };
package/vitest.config.ts CHANGED
@@ -2,9 +2,30 @@ import { defineConfig } from "vitest/config";
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
- typecheck: {
6
- enabled: true,
7
- checker: "tsc",
8
- },
5
+ workspace: [
6
+ {
7
+ test: {
8
+ typecheck: {
9
+ enabled: true,
10
+ checker: "tsc",
11
+ },
12
+ include: ["src/**/*.test.ts"],
13
+ name: "unit",
14
+ },
15
+ },
16
+ {
17
+ test: {
18
+ include: ["src/**/*.test.browser.ts"],
19
+ name: "browser",
20
+ browser: {
21
+ enabled: true,
22
+ provider: "playwright",
23
+ headless: true,
24
+ screenshotFailures: false,
25
+ instances: [{ browser: "chromium" }],
26
+ },
27
+ },
28
+ },
29
+ ],
9
30
  },
10
31
  });