@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.d.ts +515 -76
- package/dist/index.js +277 -139
- package/package.json +5 -2
- package/src/asset.ts +88 -73
- package/src/constants.ts +7 -19
- package/src/crop.ts +14 -6
- package/src/hotspot.ts +14 -0
- package/src/image.ts +41 -28
- package/src/index.ts +2 -1
- package/src/rect.ts +16 -9
- package/src/transformations.ts +155 -0
- package/src/options.ts +0 -130
package/src/constants.ts
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
asset.vanityName,
|
|
26
18
|
]
|
|
27
19
|
.filter(Boolean)
|
|
28
20
|
.join("/"),
|
|
29
21
|
);
|
|
30
|
-
if (
|
|
31
|
-
url.search =
|
|
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
|
-
|
|
42
|
-
widths: number[] = defaultSrcsetWidths,
|
|
31
|
+
widths: number[] = srcsetWidths.default,
|
|
43
32
|
): string | undefined {
|
|
44
|
-
return
|
|
45
|
-
|
|
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(
|
|
50
|
-
|
|
51
|
-
|
|
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
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
|
|
5
|
-
pos:
|
|
6
|
-
size:
|
|
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
|
-
):
|
|
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,
|
|
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
|
-
}
|