@nonphoto/sanity-image 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,83 +1,126 @@
1
1
  // src/asset.ts
2
- function isAssetLike(input) {
3
- return input != null && typeof input === "object" && "_id" in input && typeof input._id === "string";
4
- }
5
- function isReference(input) {
6
- return input != null && typeof input === "object" && "_ref" in input && typeof input._ref === "string";
7
- }
8
- function isImageAsset(input) {
9
- return input != null && typeof input === "object" && "assetId" in input && typeof input.assetId === "string" && "width" in input && typeof input.width === "number" && "height" in input && typeof input.height === "number" && "extension" in input && typeof input.extension === "string";
10
- }
11
- function hasAssetProp(input) {
12
- return input != null && typeof input === "object" && "asset" in input;
13
- }
14
- function imageIdFromSource(source) {
15
- return typeof source === "string" ? source : isReference(source) ? source._ref : isAssetLike(source) ? source._id : imageIdFromSource(source.asset);
2
+ import {
3
+ is as is5,
4
+ nullish,
5
+ number as number5,
6
+ object as object5,
7
+ string as string2,
8
+ union as union2
9
+ } from "valibot";
10
+
11
+ // src/crop.ts
12
+ import { is, number, object, optional } from "valibot";
13
+ var cropSchema = object({
14
+ top: optional(number()),
15
+ bottom: optional(number()),
16
+ left: optional(number()),
17
+ right: optional(number())
18
+ });
19
+ function isCrop(input) {
20
+ return is(cropSchema, input);
16
21
  }
17
- function imageIdFromUnknown(input) {
18
- return typeof input === "string" ? input : isReference(input) ? input._ref : isAssetLike(input) ? input._id : hasAssetProp(input) ? imageIdFromUnknown(input.asset) : void 0;
22
+
23
+ // src/hotspot.ts
24
+ import { is as is2, number as number2, object as object2, optional as optional2 } from "valibot";
25
+ var hotspotSchema = object2({
26
+ x: optional2(number2()),
27
+ y: optional2(number2()),
28
+ width: optional2(number2()),
29
+ height: optional2(number2())
30
+ });
31
+ function isHotspot(input) {
32
+ return is2(hotspotSchema, input);
19
33
  }
20
- function imageAsset(id) {
21
- const matches = id.match(/^image-(\w+)-(\d+)x(\d+)-(\w+)$/);
22
- if (matches) {
23
- const [, assetId, width, height, extension] = matches;
24
- return {
25
- _id: id,
26
- assetId,
27
- width: Number(width),
28
- height: Number(height),
29
- extension
30
- };
31
- }
34
+
35
+ // src/rect.ts
36
+ import { is as is3, number as number3, object as object3, tuple } from "valibot";
37
+ var rectSchema = object3({
38
+ pos: tuple([number3(), number3()]),
39
+ size: tuple([number3(), number3()])
40
+ });
41
+ function isRect(input) {
42
+ return is3(rectSchema, input);
32
43
  }
33
- function imageAssetFromUnknown(input) {
34
- if (isImageAsset(input)) {
35
- return input;
36
- } else {
37
- const id = imageIdFromUnknown(input);
38
- return id ? imageAsset(id) : void 0;
39
- }
44
+ function rectFromCrop(asset, crop) {
45
+ const left = Math.max(crop.left ?? 0, 0);
46
+ const right = Math.max(crop.right ?? 0, 0);
47
+ const top = Math.max(crop.top ?? 0, 0);
48
+ const bottom = Math.max(crop.bottom ?? 0, 0);
49
+ return {
50
+ pos: [left * asset.width, top * asset.height],
51
+ size: [(1 - left - right) * asset.width, (1 - top - bottom) * asset.height]
52
+ };
40
53
  }
41
54
 
42
- // src/constants.ts
43
- var defaultSrcsetWidths = [
44
- 6016,
45
- // 6K
46
- 5120,
47
- // 5K
48
- 4480,
49
- // 4.5K
50
- 3840,
51
- // 4K
52
- 3200,
53
- // QHD+
54
- 2560,
55
- // WQXGA
56
- 2048,
57
- // QXGA
58
- 1920,
59
- // 1080p
60
- 1668,
61
- // iPad
62
- 1280,
63
- // 720p
64
- 1080,
65
- // iPhone 6-8 Plus
66
- 960,
67
- 720,
68
- // iPhone 6-8
69
- 640,
70
- // 480p
71
- 480,
72
- 360,
73
- 240
74
- ];
75
-
76
- // src/options.ts
77
- function isValidEntry(entry) {
78
- return typeof entry[0] === "string" && ["string", "number", "boolean"].includes(typeof entry[1]);
55
+ // src/transformations.ts
56
+ import {
57
+ boolean,
58
+ is as is4,
59
+ literal,
60
+ number as number4,
61
+ object as object4,
62
+ partial,
63
+ string,
64
+ tuple as tuple2,
65
+ union
66
+ } from "valibot";
67
+ var transformationsSchema = partial(
68
+ object4({
69
+ auto: literal("format"),
70
+ background: string(),
71
+ blur: number4(),
72
+ crop: union([
73
+ literal("top"),
74
+ literal("bottom"),
75
+ literal("left"),
76
+ literal("right"),
77
+ literal("center"),
78
+ literal("focalpoint"),
79
+ literal("entropy")
80
+ ]),
81
+ download: union([string(), boolean()]),
82
+ dpr: union([literal(1), literal(2), literal(3)]),
83
+ fit: union([
84
+ literal("clip"),
85
+ literal("crop"),
86
+ literal("fill"),
87
+ literal("fillmax"),
88
+ literal("max"),
89
+ literal("scale"),
90
+ literal("min")
91
+ ]),
92
+ flipHorizontal: boolean(),
93
+ flipVertical: boolean(),
94
+ focalPoint: tuple2([number4(), number4()]),
95
+ format: union([
96
+ literal("jpg"),
97
+ literal("pjpg"),
98
+ literal("png"),
99
+ literal("webp")
100
+ ]),
101
+ frame: number4(),
102
+ height: number4(),
103
+ invert: boolean(),
104
+ maxHeight: number4(),
105
+ maxWidth: number4(),
106
+ minHeight: number4(),
107
+ minWidth: number4(),
108
+ orientation: union([literal(0), literal(90), literal(180), literal(270)]),
109
+ pad: number4(),
110
+ quality: number4(),
111
+ rect: rectSchema,
112
+ saturation: number4(),
113
+ sharpen: number4(),
114
+ width: number4()
115
+ })
116
+ );
117
+ function isTransformations(input) {
118
+ return is4(transformationsSchema, input);
79
119
  }
80
- function imageOptionsToSearchParamEntries({
120
+ function entry(key, value) {
121
+ return value == null || value === false ? void 0 : [key, String(typeof value === "number" ? Math.round(value) : value)];
122
+ }
123
+ function transformationsToURLSearch({
81
124
  auto,
82
125
  background,
83
126
  blur,
@@ -104,92 +147,187 @@ function imageOptionsToSearchParamEntries({
104
147
  sharpen,
105
148
  width
106
149
  }) {
107
- return Object.entries({
108
- auto,
109
- bg: background,
110
- blur,
111
- crop,
112
- dl: download,
113
- dpr,
114
- fit,
115
- flip: [flipHorizontal && "h", flipVertical && "v"].filter(Boolean).join(""),
116
- fm: format,
117
- "fp-x": focalPoint?.[0],
118
- "fp-y": focalPoint?.[1],
119
- frame,
120
- h: height,
121
- invert,
122
- "max-h": maxHeight,
123
- "max-w": maxWidth,
124
- "min-h": minHeight,
125
- "min-w": minWidth,
126
- or: orientation,
127
- pad,
128
- q: quality,
129
- rect: rect ? [rect.pos[0], rect.pos[1], rect.size[0], rect.size[1]].map(Math.round).join(",") : void 0,
130
- sat: saturation,
131
- sharp: sharpen,
132
- w: width
133
- }).filter(isValidEntry).filter(([, value]) => typeof value === "number" || Boolean(value)).map(([key, value]) => [
134
- key,
135
- encodeURIComponent(typeof value === "number" ? Math.round(value) : value)
136
- ]);
150
+ return "?" + [
151
+ entry("auto", auto),
152
+ entry("bg", background),
153
+ entry("blur", blur),
154
+ entry("crop", crop),
155
+ entry("dl", download),
156
+ entry("dpr", dpr),
157
+ entry("fit", fit),
158
+ entry(
159
+ "flip",
160
+ flipHorizontal || flipVertical ? [flipHorizontal && "h", flipVertical && "v"].filter(Boolean).join("") : void 0
161
+ ),
162
+ entry("fm", format),
163
+ entry("fp-x", focalPoint?.[0]),
164
+ entry("fp-y", focalPoint?.[1]),
165
+ entry("frame", frame),
166
+ entry("h", height),
167
+ entry("invert", invert),
168
+ entry("max-h", maxHeight),
169
+ entry("max-w", maxWidth),
170
+ entry("min-h", minHeight),
171
+ entry("min-w", minWidth),
172
+ entry("or", orientation),
173
+ entry("pad", pad),
174
+ entry("q", quality),
175
+ entry(
176
+ "rect",
177
+ rect ? [rect.pos[0], rect.pos[1], rect.size[0], rect.size[1]].map(Math.round).join(",") : void 0
178
+ ),
179
+ entry("sat", saturation),
180
+ entry("sharp", sharpen),
181
+ entry("w", width)
182
+ ].filter((entry2) => entry2 != null).map((entry2) => entry2.join("=")).join("&");
183
+ }
184
+
185
+ // src/asset.ts
186
+ var assetLikeSchema = object5({
187
+ _id: string2()
188
+ });
189
+ var referenceLikeSchema = object5({
190
+ _ref: string2()
191
+ });
192
+ var imageObjectSchema = object5({
193
+ asset: nullish(union2([assetLikeSchema, referenceLikeSchema, string2()])),
194
+ crop: nullish(cropSchema),
195
+ hotspot: nullish(hotspotSchema)
196
+ });
197
+ var imageAssetSchema = object5({
198
+ _id: string2(),
199
+ assetId: string2(),
200
+ width: number5(),
201
+ height: number5(),
202
+ extension: string2(),
203
+ vanityName: nullish(string2()),
204
+ transformations: nullish(transformationsSchema)
205
+ });
206
+ function isAssetLike(input) {
207
+ return is5(assetLikeSchema, input);
208
+ }
209
+ function isReferenceLike(input) {
210
+ return is5(referenceLikeSchema, input);
211
+ }
212
+ function isImageObject(input) {
213
+ return is5(imageObjectSchema, input);
214
+ }
215
+ function isImageAsset(input) {
216
+ return is5(imageAssetSchema, input);
137
217
  }
218
+ function assetIdFromSource(source) {
219
+ return typeof source === "string" ? source : isAssetLike(source) ? source._id : isReferenceLike(source) ? source._ref : isImageObject(source) && source.asset ? assetIdFromSource(source.asset) : void 0;
220
+ }
221
+ function parseAssetId(id) {
222
+ const matches = id.match(/^image-(\w+)-(\d+)x(\d+)-(\w+)$/);
223
+ if (matches) {
224
+ const [, assetId, width, height, extension] = matches;
225
+ return {
226
+ _id: id,
227
+ assetId,
228
+ width: Number(width),
229
+ height: Number(height),
230
+ extension
231
+ };
232
+ }
233
+ }
234
+ function imageAssetFromSource(source) {
235
+ if (typeof source === "object" && "assetId" in source) {
236
+ return source;
237
+ } else {
238
+ const id = assetIdFromSource(source);
239
+ const asset = id ? parseAssetId(id) : void 0;
240
+ return asset ? imageAssetWithTransformations(asset, {
241
+ rect: typeof source === "object" && "crop" in source && source.crop ? rectFromCrop(asset, source.crop) : void 0
242
+ }) : void 0;
243
+ }
244
+ }
245
+ function imageAssetWithTransformations(asset, transformations) {
246
+ return {
247
+ ...asset,
248
+ transformations: { ...asset.transformations, ...transformations }
249
+ };
250
+ }
251
+
252
+ // src/constants.ts
253
+ var srcsetWidths = {
254
+ default: [2560, 1920, 1280, 960, 640, 480, 360, 240],
255
+ expanded: [
256
+ 3840,
257
+ 3200,
258
+ 2560,
259
+ 2048,
260
+ 1920,
261
+ 1668,
262
+ 1280,
263
+ 1080,
264
+ 960,
265
+ 720,
266
+ 640,
267
+ 480,
268
+ 360,
269
+ 240
270
+ ]
271
+ };
138
272
 
139
273
  // src/image.ts
140
- function imageUrl(client, asset, options) {
274
+ function imageUrl(client, asset) {
141
275
  const url = new URL(
142
276
  [
143
- `https://cdn.sanity.io/images`,
277
+ "https://cdn.sanity.io/images",
144
278
  client.projectId,
145
279
  client.dataset,
146
280
  `${asset.assetId}-${asset.width}x${asset.height}.${asset.extension}`,
147
- options?.vanityName
281
+ asset.vanityName
148
282
  ].filter(Boolean).join("/")
149
283
  );
150
- if (options) {
151
- url.search = new URLSearchParams(
152
- imageOptionsToSearchParamEntries(options)
153
- ).toString();
284
+ if (asset.transformations) {
285
+ url.search = transformationsToURLSearch(asset.transformations);
154
286
  }
155
287
  return url.href;
156
288
  }
157
- function imageSrcset(client, asset, params, widths = defaultSrcsetWidths) {
158
- return [
159
- ...widths.sort((a, b) => a - b).filter((width) => width < asset.width),
160
- asset.width
161
- ].map((width) => {
162
- const url = imageUrl(client, asset, {
163
- ...params?.(width),
164
- width
165
- });
289
+ function imageSrcset(client, asset, widths = srcsetWidths.default) {
290
+ return widths.sort((a, b) => a - b).filter((width) => width < asset.width).map(Math.round).map((width) => {
291
+ const url = imageUrl(
292
+ client,
293
+ imageAssetWithTransformations(asset, {
294
+ width
295
+ })
296
+ );
166
297
  return `${url} ${width}w`;
167
298
  }).join(",");
168
299
  }
169
-
170
- // src/rect.ts
171
- function rectFromCrop(asset, crop) {
172
- const left = crop.left ?? 0;
173
- const right = crop.right ?? 0;
174
- const top = crop.top ?? 0;
175
- const bottom = crop.bottom ?? 0;
176
- return {
177
- pos: [left * asset.width, right * asset.width],
178
- size: [(1 - left - right) * asset.width, (1 - top - bottom) * asset.height]
179
- };
300
+ function imageAspectRatio(asset) {
301
+ const size = asset.transformations && ["crop", "fill", "fillmax", "scale", "min"].includes(
302
+ asset.transformations.fit ?? ""
303
+ ) && asset.transformations.width != null && asset.transformations.height != null ? [asset.transformations.width, asset.transformations.height] : asset.transformations?.rect ? asset.transformations.rect.size : [asset.width, asset.height];
304
+ return size[0] / size[1];
180
305
  }
181
306
  export {
182
- defaultSrcsetWidths,
183
- hasAssetProp,
184
- imageAsset,
185
- imageAssetFromUnknown,
186
- imageIdFromSource,
187
- imageIdFromUnknown,
188
- imageOptionsToSearchParamEntries,
307
+ assetIdFromSource,
308
+ assetLikeSchema,
309
+ cropSchema,
310
+ hotspotSchema,
311
+ imageAspectRatio,
312
+ imageAssetFromSource,
313
+ imageAssetSchema,
314
+ imageAssetWithTransformations,
315
+ imageObjectSchema,
189
316
  imageSrcset,
190
317
  imageUrl,
191
318
  isAssetLike,
319
+ isCrop,
320
+ isHotspot,
192
321
  isImageAsset,
193
- isReference,
194
- rectFromCrop
322
+ isImageObject,
323
+ isRect,
324
+ isReferenceLike,
325
+ isTransformations,
326
+ parseAssetId,
327
+ rectFromCrop,
328
+ rectSchema,
329
+ referenceLikeSchema,
330
+ srcsetWidths,
331
+ transformationsSchema,
332
+ transformationsToURLSearch
195
333
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nonphoto/sanity-image",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "author": "Jonas Luebbers <jonas@jonasluebbers.com> (https://www.jonasluebbers.com)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,5 +36,8 @@
36
36
  "engines": {
37
37
  "node": ">=20"
38
38
  },
39
- "packageManager": "pnpm@10.12.1"
39
+ "packageManager": "pnpm@10.12.1",
40
+ "dependencies": {
41
+ "valibot": "^1.1.0"
42
+ }
40
43
  }
package/src/asset.ts CHANGED
@@ -1,90 +1,85 @@
1
- export interface AssetLike {
2
- _id: string;
3
- }
4
-
5
- export interface Reference {
6
- _ref: string;
7
- }
8
-
9
- export interface ImageAsset {
10
- _id: string;
11
- assetId: string;
12
- width: number;
13
- height: number;
14
- extension: string;
15
- }
1
+ import {
2
+ InferOutput,
3
+ is,
4
+ nullish,
5
+ number,
6
+ object,
7
+ string,
8
+ union,
9
+ } from "valibot";
10
+ import { cropSchema } from "./crop";
11
+ import { hotspotSchema } from "./hotspot";
12
+ import { rectFromCrop } from "./rect";
13
+ import { Transformations, transformationsSchema } from "./transformations";
16
14
 
17
- export type ImageAssetSource =
18
- | {
19
- asset: ImageAssetSource;
20
- }
15
+ export type ImageSource =
16
+ | ImageObject
21
17
  | ImageAsset
22
18
  | AssetLike
23
- | Reference
19
+ | ReferenceLike
24
20
  | string;
25
21
 
22
+ export type AssetLike = InferOutput<typeof assetLikeSchema>;
23
+
24
+ export type ReferenceLike = InferOutput<typeof referenceLikeSchema>;
25
+
26
+ export type ImageObject = InferOutput<typeof imageObjectSchema>;
27
+
28
+ export type ImageAsset = InferOutput<typeof imageAssetSchema>;
29
+
30
+ export const assetLikeSchema = object({
31
+ _id: string(),
32
+ });
33
+
34
+ export const referenceLikeSchema = object({
35
+ _ref: string(),
36
+ });
37
+
38
+ export const imageObjectSchema = object({
39
+ asset: nullish(union([assetLikeSchema, referenceLikeSchema, string()])),
40
+ crop: nullish(cropSchema),
41
+ hotspot: nullish(hotspotSchema),
42
+ });
43
+
44
+ export const imageAssetSchema = object({
45
+ _id: string(),
46
+ assetId: string(),
47
+ width: number(),
48
+ height: number(),
49
+ extension: string(),
50
+ vanityName: nullish(string()),
51
+ transformations: nullish(transformationsSchema),
52
+ });
53
+
26
54
  export function isAssetLike(input: unknown): input is AssetLike {
27
- return (
28
- input != null &&
29
- typeof input === "object" &&
30
- "_id" in input &&
31
- typeof input._id === "string"
32
- );
55
+ return is(assetLikeSchema, input);
33
56
  }
34
57
 
35
- export function isReference(input: unknown): input is Reference {
36
- return (
37
- input != null &&
38
- typeof input === "object" &&
39
- "_ref" in input &&
40
- typeof input._ref === "string"
41
- );
58
+ export function isReferenceLike(input: unknown): input is ReferenceLike {
59
+ return is(referenceLikeSchema, input);
42
60
  }
43
61
 
44
- export function isImageAsset(input: unknown): input is ImageAsset {
45
- return (
46
- input != null &&
47
- typeof input === "object" &&
48
- "assetId" in input &&
49
- typeof input.assetId === "string" &&
50
- "width" in input &&
51
- typeof input.width === "number" &&
52
- "height" in input &&
53
- typeof input.height === "number" &&
54
- "extension" in input &&
55
- typeof input.extension === "string"
56
- );
62
+ export function isImageObject(input: unknown): input is ImageObject {
63
+ return is(imageObjectSchema, input);
57
64
  }
58
65
 
59
- export function hasAssetProp(
60
- input: unknown,
61
- ): input is { asset: NonNullable<unknown> } {
62
- return input != null && typeof input === "object" && "asset" in input;
66
+ export function isImageAsset(input: unknown): input is ImageAsset {
67
+ return is(imageAssetSchema, input);
63
68
  }
64
69
 
65
- export function imageIdFromSource(source: ImageAssetSource): string {
70
+ export function assetIdFromSource(source: ImageSource): string | undefined {
66
71
  return typeof source === "string"
67
72
  ? source
68
- : isReference(source)
69
- ? source._ref
70
- : isAssetLike(source)
71
- ? source._id
72
- : imageIdFromSource(source.asset);
73
- }
74
-
75
- export function imageIdFromUnknown(input: unknown): string | undefined {
76
- return typeof input === "string"
77
- ? input
78
- : isReference(input)
79
- ? input._ref
80
- : isAssetLike(input)
81
- ? input._id
82
- : hasAssetProp(input)
83
- ? imageIdFromUnknown(input.asset)
73
+ : isAssetLike(source)
74
+ ? source._id
75
+ : isReferenceLike(source)
76
+ ? source._ref
77
+ : isImageObject(source) && source.asset
78
+ ? assetIdFromSource(source.asset)
84
79
  : undefined;
85
80
  }
86
81
 
87
- export function imageAsset(id: string): ImageAsset | undefined {
82
+ export function parseAssetId(id: string): ImageAsset | undefined {
88
83
  const matches = id.match(/^image-(\w+)-(\d+)x(\d+)-(\w+)$/);
89
84
  if (matches) {
90
85
  const [, assetId, width, height, extension] = matches;
@@ -98,11 +93,31 @@ export function imageAsset(id: string): ImageAsset | undefined {
98
93
  }
99
94
  }
100
95
 
101
- export function imageAssetFromUnknown(input: unknown): ImageAsset | undefined {
102
- if (isImageAsset(input)) {
103
- return input;
96
+ export function imageAssetFromSource(
97
+ source: ImageSource,
98
+ ): ImageAsset | undefined {
99
+ if (typeof source === "object" && "assetId" in source) {
100
+ return source;
104
101
  } else {
105
- const id = imageIdFromUnknown(input);
106
- return id ? imageAsset(id) : undefined;
102
+ const id = assetIdFromSource(source);
103
+ const asset = id ? parseAssetId(id) : undefined;
104
+ return asset
105
+ ? imageAssetWithTransformations(asset, {
106
+ rect:
107
+ typeof source === "object" && "crop" in source && source.crop
108
+ ? rectFromCrop(asset, source.crop)
109
+ : undefined,
110
+ })
111
+ : undefined;
107
112
  }
108
113
  }
114
+
115
+ export function imageAssetWithTransformations(
116
+ asset: ImageAsset,
117
+ transformations: Transformations,
118
+ ): ImageAsset {
119
+ return {
120
+ ...asset,
121
+ transformations: { ...asset.transformations, ...transformations },
122
+ };
123
+ }