jazz-tools 0.18.2 → 0.18.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.
- package/.turbo/turbo-build.log +46 -44
- package/CHANGELOG.md +10 -0
- package/dist/media/{chunk-KR2V6X2N.js → chunk-W3S526L3.js} +96 -93
- package/dist/media/chunk-W3S526L3.js.map +1 -0
- package/dist/media/create-image/browser.d.ts +43 -0
- package/dist/media/create-image/browser.d.ts.map +1 -0
- package/dist/media/create-image/react-native.d.ts +37 -0
- package/dist/media/create-image/react-native.d.ts.map +1 -0
- package/dist/media/create-image/server.d.ts +34 -0
- package/dist/media/create-image/server.d.ts.map +1 -0
- package/dist/media/create-image/server.test.d.ts +2 -0
- package/dist/media/create-image/server.test.d.ts.map +1 -0
- package/dist/media/{create-image.d.ts → create-image-factory.d.ts} +8 -7
- package/dist/media/create-image-factory.d.ts.map +1 -0
- package/dist/media/create-image-factory.test.d.ts +2 -0
- package/dist/media/create-image-factory.test.d.ts.map +1 -0
- package/dist/media/exports.d.ts +3 -0
- package/dist/media/exports.d.ts.map +1 -0
- package/dist/media/index.browser.d.ts +2 -14
- package/dist/media/index.browser.d.ts.map +1 -1
- package/dist/media/index.browser.js +11 -20
- package/dist/media/index.browser.js.map +1 -1
- package/dist/media/index.d.ts +12 -4
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/index.native.d.ts +2 -16
- package/dist/media/index.native.d.ts.map +1 -1
- package/dist/media/index.native.js +23 -42
- package/dist/media/index.native.js.map +1 -1
- package/dist/media/index.server.d.ts +3 -0
- package/dist/media/index.server.d.ts.map +1 -0
- package/dist/media/index.server.js +103 -0
- package/dist/media/index.server.js.map +1 -0
- package/dist/react/index.js +7 -7
- package/dist/react/index.js.map +1 -1
- package/package.json +27 -11
- package/src/media/create-image/browser.ts +161 -0
- package/src/media/create-image/react-native.ts +158 -0
- package/src/media/create-image/server.test.ts +74 -0
- package/src/media/create-image/server.ts +181 -0
- package/src/media/{create-image.test.ts → create-image-factory.test.ts} +1 -1
- package/src/media/{create-image.ts → create-image-factory.ts} +22 -12
- package/src/media/exports.ts +2 -0
- package/src/media/index.browser.ts +2 -150
- package/src/media/index.native.ts +2 -166
- package/src/media/index.server.ts +2 -0
- package/src/media/index.ts +16 -8
- package/tsup.config.ts +1 -0
- package/dist/media/chunk-KR2V6X2N.js.map +0 -1
- package/dist/media/create-image.d.ts.map +0 -1
- package/dist/media/create-image.test.d.ts +0 -2
- package/dist/media/create-image.test.d.ts.map +0 -1
@@ -0,0 +1,158 @@
|
|
1
|
+
import type ImageResizerType from "@bam.tech/react-native-image-resizer";
|
2
|
+
import type { Account, Group } from "jazz-tools";
|
3
|
+
import { FileStream } from "jazz-tools";
|
4
|
+
import { Image } from "react-native";
|
5
|
+
import { createImageFactory } from "../create-image-factory";
|
6
|
+
|
7
|
+
let ImageResizer: typeof ImageResizerType | undefined;
|
8
|
+
|
9
|
+
/**
|
10
|
+
* Creates an ImageDefinition from an image file path with built-in UX features.
|
11
|
+
*
|
12
|
+
* This function creates a specialized CoValue for managing images in Jazz applications.
|
13
|
+
* It supports blurry placeholders, built-in resizing, and progressive loading patterns.
|
14
|
+
*
|
15
|
+
* @returns Promise that resolves to an ImageDefinition
|
16
|
+
*
|
17
|
+
* @example
|
18
|
+
* ```ts
|
19
|
+
* import { createImage } from "jazz-tools/media";
|
20
|
+
*
|
21
|
+
* async function uploadImageFromCamera(imagePath: string) {
|
22
|
+
* const image = await createImage(imagePath, {
|
23
|
+
* maxSize: 800,
|
24
|
+
* placeholder: "blur",
|
25
|
+
* progressive: false,
|
26
|
+
* });
|
27
|
+
*
|
28
|
+
* return image;
|
29
|
+
* }
|
30
|
+
* ```
|
31
|
+
*/
|
32
|
+
export const createImage = createImageFactory(
|
33
|
+
{
|
34
|
+
getImageSize,
|
35
|
+
getPlaceholderBase64,
|
36
|
+
createFileStreamFromSource,
|
37
|
+
resize,
|
38
|
+
},
|
39
|
+
(filePath) => {
|
40
|
+
if (typeof filePath !== "string") {
|
41
|
+
throw new Error(
|
42
|
+
"createImage(Blob | File) is not supported on this platform",
|
43
|
+
);
|
44
|
+
}
|
45
|
+
},
|
46
|
+
);
|
47
|
+
|
48
|
+
async function getResizer(): Promise<typeof ImageResizerType> {
|
49
|
+
if (!ImageResizer) {
|
50
|
+
try {
|
51
|
+
ImageResizer = (await import("@bam.tech/react-native-image-resizer"))
|
52
|
+
.default;
|
53
|
+
} catch (e) {
|
54
|
+
throw new Error(
|
55
|
+
"ImageResizer is not installed, please run `npm install @bam.tech/react-native-image-resizer`",
|
56
|
+
);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
return ImageResizer;
|
61
|
+
}
|
62
|
+
|
63
|
+
async function getImageSize(
|
64
|
+
filePath: string,
|
65
|
+
): Promise<{ width: number; height: number }> {
|
66
|
+
const { width, height } = await Image.getSize(filePath);
|
67
|
+
|
68
|
+
return { width, height };
|
69
|
+
}
|
70
|
+
|
71
|
+
async function getPlaceholderBase64(filePath: string): Promise<string> {
|
72
|
+
const ImageResizer = await getResizer();
|
73
|
+
|
74
|
+
const { uri } = await ImageResizer.createResizedImage(
|
75
|
+
filePath,
|
76
|
+
8,
|
77
|
+
8,
|
78
|
+
"PNG",
|
79
|
+
100,
|
80
|
+
);
|
81
|
+
|
82
|
+
return imageUrlToBase64(uri);
|
83
|
+
}
|
84
|
+
|
85
|
+
async function resize(
|
86
|
+
filePath: string,
|
87
|
+
width: number,
|
88
|
+
height: number,
|
89
|
+
): Promise<string> {
|
90
|
+
const ImageResizer = await getResizer();
|
91
|
+
|
92
|
+
const mimeType = await getMimeType(filePath);
|
93
|
+
|
94
|
+
const { uri } = await ImageResizer.createResizedImage(
|
95
|
+
filePath,
|
96
|
+
width,
|
97
|
+
height,
|
98
|
+
contentTypeToFormat(mimeType),
|
99
|
+
80,
|
100
|
+
);
|
101
|
+
|
102
|
+
return uri;
|
103
|
+
}
|
104
|
+
|
105
|
+
function getMimeType(filePath: string): Promise<string> {
|
106
|
+
return fetch(filePath)
|
107
|
+
.then((res) => res.blob())
|
108
|
+
.then((blob) => blob.type);
|
109
|
+
}
|
110
|
+
|
111
|
+
function contentTypeToFormat(contentType: string) {
|
112
|
+
if (contentType.includes("image/png")) return "PNG";
|
113
|
+
if (contentType.includes("image/jpeg")) return "JPEG";
|
114
|
+
if (contentType.includes("image/webp")) return "WEBP";
|
115
|
+
return "PNG";
|
116
|
+
}
|
117
|
+
|
118
|
+
export async function createFileStreamFromSource(
|
119
|
+
filePath: string,
|
120
|
+
owner?: Account | Group,
|
121
|
+
): Promise<FileStream> {
|
122
|
+
const blob = await fetch(filePath).then((res) => res.blob());
|
123
|
+
const arrayBuffer = await toArrayBuffer(blob);
|
124
|
+
|
125
|
+
return FileStream.createFromArrayBuffer(arrayBuffer, blob.type, undefined, {
|
126
|
+
owner,
|
127
|
+
});
|
128
|
+
}
|
129
|
+
|
130
|
+
// TODO: look for more efficient way to do this as React Native hasn't blob.arrayBuffer()
|
131
|
+
function toArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
|
132
|
+
return new Promise((resolve, reject) => {
|
133
|
+
const reader = new FileReader();
|
134
|
+
reader.onloadend = () => {
|
135
|
+
resolve(reader.result as ArrayBuffer);
|
136
|
+
};
|
137
|
+
reader.onerror = (error) => {
|
138
|
+
reject(error);
|
139
|
+
};
|
140
|
+
reader.readAsArrayBuffer(blob);
|
141
|
+
});
|
142
|
+
}
|
143
|
+
|
144
|
+
async function imageUrlToBase64(url: string): Promise<string> {
|
145
|
+
const response = await fetch(url);
|
146
|
+
const blob = await response.blob();
|
147
|
+
return new Promise((onSuccess, onError) => {
|
148
|
+
try {
|
149
|
+
const reader = new FileReader();
|
150
|
+
reader.onload = function () {
|
151
|
+
onSuccess(reader.result as string);
|
152
|
+
};
|
153
|
+
reader.readAsDataURL(blob);
|
154
|
+
} catch (e) {
|
155
|
+
onError(e);
|
156
|
+
}
|
157
|
+
});
|
158
|
+
}
|
@@ -0,0 +1,74 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
|
3
|
+
import { createJazzTestAccount } from "jazz-tools/testing";
|
4
|
+
import sharp from "sharp";
|
5
|
+
import { describe, expect, it } from "vitest";
|
6
|
+
import { createImage } from "./server";
|
7
|
+
|
8
|
+
describe("createImage (server)", async () => {
|
9
|
+
const account = await createJazzTestAccount();
|
10
|
+
|
11
|
+
const getImageSize = async (blobOrBuffer: Blob | Buffer) => {
|
12
|
+
const imageBuffer =
|
13
|
+
blobOrBuffer instanceof Blob
|
14
|
+
? await blobOrBuffer.arrayBuffer()
|
15
|
+
: blobOrBuffer;
|
16
|
+
|
17
|
+
const image = sharp(imageBuffer);
|
18
|
+
const { width, height } = await image.metadata();
|
19
|
+
return [width, height];
|
20
|
+
};
|
21
|
+
|
22
|
+
it("should create an image with intermediate sizes for progressive loading and placeholder", async () => {
|
23
|
+
const imageBlob = new Blob([Buffer.from(White1920, "base64")], {
|
24
|
+
type: "image/png",
|
25
|
+
});
|
26
|
+
|
27
|
+
const image = await createImage(imageBlob, {
|
28
|
+
owner: account,
|
29
|
+
progressive: true,
|
30
|
+
placeholder: "blur",
|
31
|
+
});
|
32
|
+
|
33
|
+
expect(image).toBeDefined();
|
34
|
+
expect(image.originalSize).toEqual([1920, 400]);
|
35
|
+
expect(image.placeholderDataURL).toBeDefined();
|
36
|
+
|
37
|
+
expect(image[`256x53`]).toBeDefined();
|
38
|
+
expect(image[`1024x213`]).toBeDefined();
|
39
|
+
expect(image[`2048x427`]).not.toBeDefined();
|
40
|
+
});
|
41
|
+
|
42
|
+
it("should correctly resize images for intermediate sizes", async () => {
|
43
|
+
const imageBlob = new Blob([Buffer.from(White1920, "base64")], {
|
44
|
+
type: "image/png",
|
45
|
+
});
|
46
|
+
|
47
|
+
const image = await createImage(imageBlob, {
|
48
|
+
owner: account,
|
49
|
+
progressive: true,
|
50
|
+
placeholder: "blur",
|
51
|
+
});
|
52
|
+
|
53
|
+
const {
|
54
|
+
original,
|
55
|
+
"256x53": s1,
|
56
|
+
"1024x213": s2,
|
57
|
+
} = await image.$jazz.ensureLoaded({
|
58
|
+
resolve: { original: true, [`256x53`]: true, [`1024x213`]: true },
|
59
|
+
});
|
60
|
+
|
61
|
+
expect(
|
62
|
+
await getImageSize(
|
63
|
+
Buffer.from(image.placeholderDataURL!.split(",")[1]!, "base64"),
|
64
|
+
),
|
65
|
+
).toEqual([8, 2]);
|
66
|
+
expect(await getImageSize(original.toBlob()!)).toEqual([1920, 400]);
|
67
|
+
expect(await getImageSize(s1.toBlob()!)).toEqual([256, 53]);
|
68
|
+
expect(await getImageSize(s2.toBlob()!)).toEqual([1024, 213]);
|
69
|
+
});
|
70
|
+
});
|
71
|
+
|
72
|
+
// Image 1920x400
|
73
|
+
const White1920 =
|
74
|
+
"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCAGQB4ADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAP/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//2Q==";
|
@@ -0,0 +1,181 @@
|
|
1
|
+
import { Account, FileStream, Group } from "jazz-tools";
|
2
|
+
import type sharp from "sharp";
|
3
|
+
import { createImageFactory } from "../create-image-factory";
|
4
|
+
|
5
|
+
export type SharpImageType =
|
6
|
+
| File
|
7
|
+
| Blob
|
8
|
+
| Buffer
|
9
|
+
| ArrayBuffer
|
10
|
+
| Uint8Array
|
11
|
+
| Uint8ClampedArray
|
12
|
+
| Int8Array
|
13
|
+
| Uint16Array
|
14
|
+
| Int16Array
|
15
|
+
| Uint32Array
|
16
|
+
| Int32Array
|
17
|
+
| Float32Array
|
18
|
+
| Float64Array;
|
19
|
+
|
20
|
+
let sharpModule: typeof import("sharp");
|
21
|
+
|
22
|
+
async function getSharp() {
|
23
|
+
if (!sharpModule) {
|
24
|
+
sharpModule = await import("sharp").then((m) => m.default);
|
25
|
+
}
|
26
|
+
|
27
|
+
return sharpModule;
|
28
|
+
}
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Creates an ImageDefinition from an image File, Blob or Buffer with built-in UX features.
|
32
|
+
*
|
33
|
+
* This function creates a specialized CoValue for managing images in Jazz applications.
|
34
|
+
* It supports blurry placeholders, built-in resizing, and progressive loading patterns.
|
35
|
+
*
|
36
|
+
* @returns Promise that resolves to an ImageDefinition
|
37
|
+
*
|
38
|
+
* @example
|
39
|
+
* ```ts
|
40
|
+
* import fs from "node:fs";
|
41
|
+
* import { createImage } from "jazz-tools/media";
|
42
|
+
*
|
43
|
+
* const imageBuffer = fs.readFileSync("path/to/image.jpg");
|
44
|
+
* const image = await createImage(imageBuffer, {
|
45
|
+
* maxSize: 800,
|
46
|
+
* placeholder: "blur",
|
47
|
+
* progressive: false,
|
48
|
+
* });
|
49
|
+
* ```
|
50
|
+
*/
|
51
|
+
export const createImage = createImageFactory<SharpImageType, Blob>(
|
52
|
+
{
|
53
|
+
createFileStreamFromSource,
|
54
|
+
getImageSize,
|
55
|
+
getPlaceholderBase64,
|
56
|
+
resize,
|
57
|
+
},
|
58
|
+
(imageBlobOrFile) => {
|
59
|
+
if (typeof imageBlobOrFile === "string") {
|
60
|
+
throw new Error("createImage(string) is not supported on this platform");
|
61
|
+
}
|
62
|
+
},
|
63
|
+
);
|
64
|
+
|
65
|
+
function formatToMimeType(format: keyof sharp.FormatEnum | undefined) {
|
66
|
+
const formatToTypeMap: Record<
|
67
|
+
Extract<
|
68
|
+
keyof sharp.FormatEnum,
|
69
|
+
"avif" | "gif" | "jpeg" | "jpg" | "png" | "webp"
|
70
|
+
>,
|
71
|
+
string
|
72
|
+
> = {
|
73
|
+
avif: "image/avif",
|
74
|
+
gif: "image/gif",
|
75
|
+
jpeg: "image/jpeg",
|
76
|
+
jpg: "image/jpeg",
|
77
|
+
png: "image/png",
|
78
|
+
webp: "image/webp",
|
79
|
+
};
|
80
|
+
|
81
|
+
if (!format) {
|
82
|
+
throw new Error("Could not determine image format");
|
83
|
+
}
|
84
|
+
|
85
|
+
if (!(format in formatToTypeMap)) {
|
86
|
+
throw new Error(`Unsupported image format: ${format}`);
|
87
|
+
}
|
88
|
+
|
89
|
+
return formatToTypeMap[format as keyof typeof formatToTypeMap];
|
90
|
+
}
|
91
|
+
|
92
|
+
async function createFileStreamFromSource(
|
93
|
+
imageBlobOrBuffer: SharpImageType,
|
94
|
+
owner?: Account | Group,
|
95
|
+
): Promise<FileStream> {
|
96
|
+
// `File` is also an instance of `Blob`
|
97
|
+
if (imageBlobOrBuffer instanceof Blob) {
|
98
|
+
return FileStream.createFromBlob(imageBlobOrBuffer, { owner });
|
99
|
+
}
|
100
|
+
|
101
|
+
const sharp = await getSharp();
|
102
|
+
|
103
|
+
const image = sharp(imageBlobOrBuffer);
|
104
|
+
const metadata = await image.metadata();
|
105
|
+
const format = metadata.format;
|
106
|
+
const mimeType = formatToMimeType(format);
|
107
|
+
|
108
|
+
return FileStream.createFromArrayBuffer(
|
109
|
+
imageBlobOrBuffer,
|
110
|
+
mimeType,
|
111
|
+
undefined,
|
112
|
+
{ owner },
|
113
|
+
);
|
114
|
+
}
|
115
|
+
|
116
|
+
async function getImageSize(
|
117
|
+
imageBlobOrBuffer: SharpImageType,
|
118
|
+
): Promise<{ width: number; height: number }> {
|
119
|
+
const imageBuffer =
|
120
|
+
imageBlobOrBuffer instanceof Blob
|
121
|
+
? await imageBlobOrBuffer.arrayBuffer()
|
122
|
+
: imageBlobOrBuffer;
|
123
|
+
|
124
|
+
const sharp = await getSharp();
|
125
|
+
const image = sharp(imageBuffer);
|
126
|
+
const metadata = await image.metadata();
|
127
|
+
|
128
|
+
return { width: metadata.width!, height: metadata.height! };
|
129
|
+
}
|
130
|
+
|
131
|
+
async function getPlaceholderBase64(
|
132
|
+
imageBlobOrBuffer: SharpImageType,
|
133
|
+
): Promise<string> {
|
134
|
+
const imageBuffer =
|
135
|
+
imageBlobOrBuffer instanceof Blob
|
136
|
+
? await imageBlobOrBuffer.arrayBuffer()
|
137
|
+
: imageBlobOrBuffer;
|
138
|
+
|
139
|
+
const sharp = await getSharp();
|
140
|
+
|
141
|
+
const image = sharp(imageBuffer);
|
142
|
+
const placeholder = await image
|
143
|
+
.resize({
|
144
|
+
width: 8,
|
145
|
+
height: 8,
|
146
|
+
fit: "inside",
|
147
|
+
})
|
148
|
+
.toFormat("png")
|
149
|
+
.toBuffer();
|
150
|
+
|
151
|
+
return `data:image/png;base64,${placeholder.toString("base64")}`;
|
152
|
+
}
|
153
|
+
|
154
|
+
async function resize(
|
155
|
+
imageBlobOrBuffer: SharpImageType,
|
156
|
+
width: number,
|
157
|
+
height: number,
|
158
|
+
): Promise<Blob> {
|
159
|
+
const imageBuffer =
|
160
|
+
imageBlobOrBuffer instanceof Blob
|
161
|
+
? await imageBlobOrBuffer.arrayBuffer()
|
162
|
+
: imageBlobOrBuffer;
|
163
|
+
|
164
|
+
const sharp = await getSharp();
|
165
|
+
|
166
|
+
const image = sharp(imageBuffer);
|
167
|
+
const metadata = await image.metadata();
|
168
|
+
const format = metadata.format;
|
169
|
+
const mimeType = formatToMimeType(format);
|
170
|
+
|
171
|
+
const resizedBuffer = await image
|
172
|
+
.resize({
|
173
|
+
width,
|
174
|
+
height,
|
175
|
+
})
|
176
|
+
.toBuffer();
|
177
|
+
|
178
|
+
return new Blob([new Uint8Array(resizedBuffer)], {
|
179
|
+
type: mimeType,
|
180
|
+
});
|
181
|
+
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { FileStream } from "jazz-tools";
|
2
2
|
import { createJazzTestAccount } from "jazz-tools/testing";
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
4
|
-
import { createImageFactory } from "./create-image
|
4
|
+
import { createImageFactory } from "./create-image-factory";
|
5
5
|
|
6
6
|
describe("createImage", async () => {
|
7
7
|
const account = await createJazzTestAccount();
|
@@ -8,6 +8,8 @@ import {
|
|
8
8
|
|
9
9
|
export type SourceType = Blob | File | string;
|
10
10
|
|
11
|
+
export type ResizeOutput = Blob | string;
|
12
|
+
|
11
13
|
export type CreateImageOptions = {
|
12
14
|
/** The owner of the image. Can be either a Group or Account. If not specified, the current user will be the owner. */
|
13
15
|
owner?: Group | Account;
|
@@ -35,31 +37,39 @@ export type CreateImageOptions = {
|
|
35
37
|
progressive?: boolean;
|
36
38
|
};
|
37
39
|
|
38
|
-
export type CreateImageImpl
|
40
|
+
export type CreateImageImpl<
|
41
|
+
TSourceType = SourceType,
|
42
|
+
TResizeOutput = ResizeOutput,
|
43
|
+
> = {
|
39
44
|
createFileStreamFromSource: (
|
40
|
-
imageBlobOrFile:
|
45
|
+
imageBlobOrFile: TSourceType | TResizeOutput,
|
41
46
|
owner?: Group | Account,
|
42
47
|
) => Promise<FileStream>;
|
43
48
|
getImageSize: (
|
44
|
-
imageBlobOrFile:
|
49
|
+
imageBlobOrFile: TSourceType,
|
45
50
|
) => Promise<{ width: number; height: number }>;
|
46
|
-
getPlaceholderBase64: (imageBlobOrFile:
|
51
|
+
getPlaceholderBase64: (imageBlobOrFile: TSourceType) => Promise<string>;
|
47
52
|
resize: (
|
48
|
-
imageBlobOrFile:
|
53
|
+
imageBlobOrFile: TSourceType,
|
49
54
|
width: number,
|
50
55
|
height: number,
|
51
|
-
) => Promise<
|
56
|
+
) => Promise<TResizeOutput>;
|
52
57
|
};
|
53
58
|
|
54
|
-
export function createImageFactory(
|
55
|
-
|
56
|
-
|
59
|
+
export function createImageFactory<TSourceType, TResizeOutput>(
|
60
|
+
impl: CreateImageImpl<TSourceType, TResizeOutput>,
|
61
|
+
imageTypeGuard?: (imageBlobOrFile: TSourceType) => void,
|
62
|
+
) {
|
63
|
+
return (source: TSourceType, options?: CreateImageOptions) => {
|
64
|
+
imageTypeGuard?.(source);
|
65
|
+
return createImage(source, options ?? {}, impl);
|
66
|
+
};
|
57
67
|
}
|
58
68
|
|
59
|
-
async function createImage(
|
60
|
-
imageBlobOrFile:
|
69
|
+
async function createImage<TSourceType, TResizeOutput>(
|
70
|
+
imageBlobOrFile: TSourceType,
|
61
71
|
options: CreateImageOptions,
|
62
|
-
impl: CreateImageImpl,
|
72
|
+
impl: CreateImageImpl<TSourceType, TResizeOutput>,
|
63
73
|
): Promise<Loaded<typeof ImageDefinition, { $each: true }>> {
|
64
74
|
// Get the original size of the image
|
65
75
|
const { width: originalWidth, height: originalHeight } =
|
@@ -1,150 +1,2 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
}
|
1
|
+
export * from "./exports";
|
2
|
+
export { createImage } from "./create-image/browser";
|