@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/src/constants.ts CHANGED
@@ -1,19 +1,7 @@
1
- export const defaultSrcsetWidths = [
2
- 6016, // 6K
3
- 5120, // 5K
4
- 4480, // 4.5K
5
- 3840, // 4K
6
- 3200, // QHD+
7
- 2560, // WQXGA
8
- 2048, // QXGA
9
- 1920, // 1080p
10
- 1668, // iPad
11
- 1280, // 720p
12
- 1080, // iPhone 6-8 Plus
13
- 960,
14
- 720, // iPhone 6-8
15
- 640, // 480p
16
- 480,
17
- 360,
18
- 240,
19
- ];
1
+ export const srcsetWidths = {
2
+ default: [2560, 1920, 1280, 960, 640, 480, 360, 240],
3
+ expanded: [
4
+ 3840, 3200, 2560, 2048, 1920, 1668, 1280, 1080, 960, 720, 640, 480, 360,
5
+ 240,
6
+ ],
7
+ };
package/src/crop.ts CHANGED
@@ -1,6 +1,14 @@
1
- export type Crop = {
2
- top?: number;
3
- bottom?: number;
4
- left?: number;
5
- right?: number;
6
- };
1
+ import { InferOutput, is, number, object, optional } from "valibot";
2
+
3
+ export const cropSchema = object({
4
+ top: optional(number()),
5
+ bottom: optional(number()),
6
+ left: optional(number()),
7
+ right: optional(number()),
8
+ });
9
+
10
+ export type Crop = InferOutput<typeof cropSchema>;
11
+
12
+ export function isCrop(input: unknown): input is Crop {
13
+ return is(cropSchema, input);
14
+ }
package/src/hotspot.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { InferOutput, is, number, object, optional } from "valibot";
2
+
3
+ export const hotspotSchema = object({
4
+ x: optional(number()),
5
+ y: optional(number()),
6
+ width: optional(number()),
7
+ height: optional(number()),
8
+ });
9
+
10
+ export type Hotspot = InferOutput<typeof hotspotSchema>;
11
+
12
+ export function isHotspot(input: unknown): input is Hotspot {
13
+ return is(hotspotSchema, input);
14
+ }
package/src/image.ts CHANGED
@@ -1,36 +1,26 @@
1
- import { ImageAsset } from "./asset";
2
- import { defaultSrcsetWidths } from "./constants";
3
- import { ImageOptions, imageOptionsToSearchParamEntries } from "./options";
1
+ import { ImageAsset, imageAssetWithTransformations } from "./asset";
2
+ import { srcsetWidths } from "./constants";
3
+ import { transformationsToURLSearch } from "./transformations";
4
4
 
5
5
  export interface SanityClientLike {
6
6
  projectId: string;
7
7
  dataset: string;
8
8
  }
9
9
 
10
- export interface ImageOptionsWithVanityName extends ImageOptions {
11
- vanityName?: string;
12
- }
13
-
14
- export function imageUrl(
15
- client: SanityClientLike,
16
- asset: ImageAsset,
17
- options?: ImageOptionsWithVanityName,
18
- ): string | undefined {
10
+ export function imageUrl(client: SanityClientLike, asset: ImageAsset): string {
19
11
  const url = new URL(
20
12
  [
21
- `https://cdn.sanity.io/images`,
13
+ "https://cdn.sanity.io/images",
22
14
  client.projectId,
23
15
  client.dataset,
24
16
  `${asset.assetId}-${asset.width}x${asset.height}.${asset.extension}`,
25
- options?.vanityName,
17
+ asset.vanityName,
26
18
  ]
27
19
  .filter(Boolean)
28
20
  .join("/"),
29
21
  );
30
- if (options) {
31
- url.search = new URLSearchParams(
32
- imageOptionsToSearchParamEntries(options),
33
- ).toString();
22
+ if (asset.transformations) {
23
+ url.search = transformationsToURLSearch(asset.transformations);
34
24
  }
35
25
  return url.href;
36
26
  }
@@ -38,19 +28,42 @@ export function imageUrl(
38
28
  export function imageSrcset(
39
29
  client: SanityClientLike,
40
30
  asset: ImageAsset,
41
- params?: (width: number) => Omit<ImageOptionsWithVanityName, "width">,
42
- widths: number[] = defaultSrcsetWidths,
31
+ widths: number[] = srcsetWidths.default,
43
32
  ): string | undefined {
44
- return [
45
- ...widths.sort((a, b) => a - b).filter((width) => width < asset.width),
46
- asset.width,
47
- ]
33
+ return widths
34
+ .sort((a, b) => a - b)
35
+ .filter((width) => width < asset.width)
36
+ .map(Math.round)
48
37
  .map((width) => {
49
- const url = imageUrl(client, asset, {
50
- ...params?.(width),
51
- width,
52
- });
38
+ const url = imageUrl(
39
+ client,
40
+ imageAssetWithTransformations(asset, {
41
+ width,
42
+ }),
43
+ );
53
44
  return `${url} ${width}w`;
54
45
  })
55
46
  .join(",");
56
47
  }
48
+
49
+ /**
50
+ * Calculates the aspect ratio of an image, taking its transformations into account.
51
+ * @param asset - The asset to calculate the aspect ratio of
52
+ * @returns The aspect ratio of the image
53
+ * @todo This function currently ignores the `crop` mode settings including focal point
54
+ * and min/max height/width.
55
+ */
56
+ export function imageAspectRatio(asset: ImageAsset): number {
57
+ const size: [number, number] =
58
+ asset.transformations &&
59
+ ["crop", "fill", "fillmax", "scale", "min"].includes(
60
+ asset.transformations.fit ?? "",
61
+ ) &&
62
+ asset.transformations.width != null &&
63
+ asset.transformations.height != null
64
+ ? [asset.transformations.width, asset.transformations.height]
65
+ : asset.transformations?.rect
66
+ ? asset.transformations.rect.size
67
+ : [asset.width, asset.height];
68
+ return size[0] / size[1];
69
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./asset";
2
2
  export * from "./constants";
3
3
  export * from "./crop";
4
+ export * from "./hotspot";
4
5
  export * from "./image";
5
- export * from "./options";
6
6
  export * from "./rect";
7
+ export * from "./transformations";
package/src/rect.ts CHANGED
@@ -1,21 +1,28 @@
1
+ import { InferOutput, is, number, object, tuple } from "valibot";
1
2
  import { ImageAsset } from "./asset";
2
3
  import { Crop } from "./crop";
3
4
 
4
- export interface RectLike {
5
- pos: ArrayLike<number>;
6
- size: ArrayLike<number>;
5
+ export const rectSchema = object({
6
+ pos: tuple([number(), number()]),
7
+ size: tuple([number(), number()]),
8
+ });
9
+
10
+ export type Rect = InferOutput<typeof rectSchema>;
11
+
12
+ export function isRect(input: unknown): input is Rect {
13
+ return is(rectSchema, input);
7
14
  }
8
15
 
9
16
  export function rectFromCrop(
10
17
  asset: Pick<ImageAsset, "width" | "height">,
11
18
  crop: Crop,
12
- ): RectLike {
13
- const left = crop.left ?? 0;
14
- const right = crop.right ?? 0;
15
- const top = crop.top ?? 0;
16
- const bottom = crop.bottom ?? 0;
19
+ ): Rect {
20
+ const left = Math.max(crop.left ?? 0, 0);
21
+ const right = Math.max(crop.right ?? 0, 0);
22
+ const top = Math.max(crop.top ?? 0, 0);
23
+ const bottom = Math.max(crop.bottom ?? 0, 0);
17
24
  return {
18
- pos: [left * asset.width, right * asset.width],
25
+ pos: [left * asset.width, top * asset.height],
19
26
  size: [(1 - left - right) * asset.width, (1 - top - bottom) * asset.height],
20
27
  };
21
28
  }
@@ -0,0 +1,155 @@
1
+ import {
2
+ boolean,
3
+ InferOutput,
4
+ is,
5
+ literal,
6
+ number,
7
+ object,
8
+ partial,
9
+ string,
10
+ tuple,
11
+ union,
12
+ } from "valibot";
13
+ import { rectSchema } from "./rect";
14
+
15
+ export const transformationsSchema = partial(
16
+ object({
17
+ auto: literal("format"),
18
+ background: string(),
19
+ blur: number(),
20
+ crop: union([
21
+ literal("top"),
22
+ literal("bottom"),
23
+ literal("left"),
24
+ literal("right"),
25
+ literal("center"),
26
+ literal("focalpoint"),
27
+ literal("entropy"),
28
+ ]),
29
+ download: union([string(), boolean()]),
30
+ dpr: union([literal(1), literal(2), literal(3)]),
31
+ fit: union([
32
+ literal("clip"),
33
+ literal("crop"),
34
+ literal("fill"),
35
+ literal("fillmax"),
36
+ literal("max"),
37
+ literal("scale"),
38
+ literal("min"),
39
+ ]),
40
+ flipHorizontal: boolean(),
41
+ flipVertical: boolean(),
42
+ focalPoint: tuple([number(), number()]),
43
+ format: union([
44
+ literal("jpg"),
45
+ literal("pjpg"),
46
+ literal("png"),
47
+ literal("webp"),
48
+ ]),
49
+ frame: number(),
50
+ height: number(),
51
+ invert: boolean(),
52
+ maxHeight: number(),
53
+ maxWidth: number(),
54
+ minHeight: number(),
55
+ minWidth: number(),
56
+ orientation: union([literal(0), literal(90), literal(180), literal(270)]),
57
+ pad: number(),
58
+ quality: number(),
59
+ rect: rectSchema,
60
+ saturation: number(),
61
+ sharpen: number(),
62
+ width: number(),
63
+ }),
64
+ );
65
+
66
+ export type Transformations = InferOutput<typeof transformationsSchema>;
67
+
68
+ export function isTransformations(input: unknown): input is Transformations {
69
+ return is(transformationsSchema, input);
70
+ }
71
+
72
+ function entry(
73
+ key: string,
74
+ value: string | number | boolean | null | undefined,
75
+ ): [string, string] | undefined {
76
+ return value == null || value === false
77
+ ? undefined
78
+ : [key, String(typeof value === "number" ? Math.round(value) : value)];
79
+ }
80
+
81
+ export function transformationsToURLSearch({
82
+ auto,
83
+ background,
84
+ blur,
85
+ crop,
86
+ download,
87
+ dpr,
88
+ fit,
89
+ flipHorizontal,
90
+ flipVertical,
91
+ focalPoint,
92
+ format,
93
+ frame,
94
+ height,
95
+ invert,
96
+ maxHeight,
97
+ maxWidth,
98
+ minHeight,
99
+ minWidth,
100
+ orientation,
101
+ pad,
102
+ quality,
103
+ rect,
104
+ saturation,
105
+ sharpen,
106
+ width,
107
+ }: Transformations): string {
108
+ return (
109
+ "?" +
110
+ [
111
+ entry("auto", auto),
112
+ entry("bg", background),
113
+ entry("blur", blur),
114
+ entry("crop", crop),
115
+ entry("dl", download),
116
+ entry("dpr", dpr),
117
+ entry("fit", fit),
118
+ entry(
119
+ "flip",
120
+ flipHorizontal || flipVertical
121
+ ? [flipHorizontal && "h", flipVertical && "v"]
122
+ .filter(Boolean)
123
+ .join("")
124
+ : undefined,
125
+ ),
126
+ entry("fm", format),
127
+ entry("fp-x", focalPoint?.[0]),
128
+ entry("fp-y", focalPoint?.[1]),
129
+ entry("frame", frame),
130
+ entry("h", height),
131
+ entry("invert", invert),
132
+ entry("max-h", maxHeight),
133
+ entry("max-w", maxWidth),
134
+ entry("min-h", minHeight),
135
+ entry("min-w", minWidth),
136
+ entry("or", orientation),
137
+ entry("pad", pad),
138
+ entry("q", quality),
139
+ entry(
140
+ "rect",
141
+ rect
142
+ ? [rect.pos[0], rect.pos[1], rect.size[0], rect.size[1]]
143
+ .map(Math.round)
144
+ .join(",")
145
+ : undefined,
146
+ ),
147
+ entry("sat", saturation),
148
+ entry("sharp", sharpen),
149
+ entry("w", width),
150
+ ]
151
+ .filter((entry) => entry != null)
152
+ .map((entry) => entry.join("="))
153
+ .join("&")
154
+ );
155
+ }
package/src/options.ts DELETED
@@ -1,130 +0,0 @@
1
- import { RectLike } from "./rect";
2
-
3
- export type ImageFormat = "jpg" | "pjpg" | "png" | "webp";
4
-
5
- export type FitMode =
6
- | "clip"
7
- | "crop"
8
- | "fill"
9
- | "fillmax"
10
- | "max"
11
- | "scale"
12
- | "min";
13
-
14
- export type CropMode =
15
- | "top"
16
- | "bottom"
17
- | "left"
18
- | "right"
19
- | "center"
20
- | "focalpoint"
21
- | "entropy";
22
-
23
- export type AutoMode = "format";
24
-
25
- export type Orientation = 0 | 90 | 180 | 270;
26
-
27
- export type Dpr = 1 | 2 | 3;
28
-
29
- export type ImageOptions = {
30
- auto?: AutoMode;
31
- background?: string;
32
- blur?: number;
33
- crop?: CropMode;
34
- download?: boolean | string;
35
- dpr?: Dpr;
36
- fit?: FitMode;
37
- flipHorizontal?: boolean;
38
- flipVertical?: boolean;
39
- focalPoint?: ArrayLike<number>;
40
- format?: ImageFormat;
41
- frame?: number;
42
- height?: number;
43
- invert?: boolean;
44
- maxHeight?: number;
45
- maxWidth?: number;
46
- minHeight?: number;
47
- minWidth?: number;
48
- orientation?: Orientation;
49
- pad?: number;
50
- quality?: number;
51
- rect?: RectLike;
52
- saturation?: number;
53
- sharpen?: number;
54
- width?: number;
55
- };
56
-
57
- type ValidEntry = [string, string | number | boolean];
58
-
59
- function isValidEntry(entry: [unknown, unknown]): entry is ValidEntry {
60
- return (
61
- typeof entry[0] === "string" &&
62
- ["string", "number", "boolean"].includes(typeof entry[1])
63
- );
64
- }
65
-
66
- export function imageOptionsToSearchParamEntries({
67
- auto,
68
- background,
69
- blur,
70
- crop,
71
- download,
72
- dpr,
73
- fit,
74
- flipHorizontal,
75
- flipVertical,
76
- focalPoint,
77
- format,
78
- frame,
79
- height,
80
- invert,
81
- maxHeight,
82
- maxWidth,
83
- minHeight,
84
- minWidth,
85
- orientation,
86
- pad,
87
- quality,
88
- rect,
89
- saturation,
90
- sharpen,
91
- width,
92
- }: ImageOptions): string[][] {
93
- return Object.entries({
94
- auto,
95
- bg: background,
96
- blur,
97
- crop,
98
- dl: download,
99
- dpr,
100
- fit,
101
- flip: [flipHorizontal && "h", flipVertical && "v"].filter(Boolean).join(""),
102
- fm: format,
103
- "fp-x": focalPoint?.[0],
104
- "fp-y": focalPoint?.[1],
105
- frame,
106
- h: height,
107
- invert,
108
- "max-h": maxHeight,
109
- "max-w": maxWidth,
110
- "min-h": minHeight,
111
- "min-w": minWidth,
112
- or: orientation,
113
- pad,
114
- q: quality,
115
- rect: rect
116
- ? [rect.pos[0], rect.pos[1], rect.size[0], rect.size[1]]
117
- .map(Math.round)
118
- .join(",")
119
- : undefined,
120
- sat: saturation,
121
- sharp: sharpen,
122
- w: width,
123
- })
124
- .filter(isValidEntry)
125
- .filter(([, value]) => typeof value === "number" || Boolean(value))
126
- .map(([key, value]) => [
127
- key,
128
- encodeURIComponent(typeof value === "number" ? Math.round(value) : value),
129
- ]);
130
- }