@webstudio-is/image 0.191.0 → 0.191.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webstudio-is/image",
3
- "version": "0.191.0",
3
+ "version": "0.191.4",
4
4
  "description": "Image optimization",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
package/lib/index.js DELETED
@@ -1,220 +0,0 @@
1
- // src/image.tsx
2
- import { forwardRef } from "react";
3
-
4
- // src/image-optimize.ts
5
- var imageSizes = [16, 32, 48, 64, 96, 128, 256, 384];
6
- var deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
7
- var allSizes = [...imageSizes, ...deviceSizes];
8
- var getWidths = (width, sizes) => {
9
- if (sizes) {
10
- const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g;
11
- const percentSizes = [];
12
- for (let match; match = viewportWidthRe.exec(sizes); match) {
13
- percentSizes.push(Number.parseInt(match[2], 10));
14
- }
15
- if (percentSizes.length) {
16
- const smallestRatio = Math.min(...percentSizes) * 0.01;
17
- return {
18
- widths: allSizes.filter(
19
- (size) => size >= deviceSizes[0] * smallestRatio
20
- ),
21
- kind: "w"
22
- };
23
- }
24
- return { widths: allSizes, kind: "w" };
25
- }
26
- if (width == null) {
27
- return { widths: deviceSizes, kind: "w" };
28
- }
29
- const MAX_DEVICE_PIXEL_RATIO = 2;
30
- let index = allSizes.findIndex(
31
- (size) => size >= MAX_DEVICE_PIXEL_RATIO * width
32
- );
33
- index = index < 0 ? allSizes.length : index;
34
- return {
35
- widths: allSizes.slice(0, index + 1),
36
- kind: "w"
37
- };
38
- };
39
- var generateImgAttrs = ({
40
- src,
41
- width,
42
- quality,
43
- sizes,
44
- loader
45
- }) => {
46
- const { widths, kind } = getWidths(width, sizes);
47
- return {
48
- sizes: !sizes && kind === "w" ? "100vw" : sizes,
49
- srcSet: widths.map(
50
- (w, i) => `${loader({ src, quality, width: w })} ${kind === "w" ? w : i + 1}${kind}`
51
- ).join(", "),
52
- // Must be last, to prevent Safari to load images twice
53
- src: loader({
54
- src,
55
- quality,
56
- width: widths[widths.length - 1]
57
- })
58
- };
59
- };
60
- var getInt = (value) => {
61
- if (typeof value === "number") {
62
- return Math.round(value);
63
- }
64
- if (typeof value === "string") {
65
- const vNum = Number.parseFloat(value);
66
- if (!Number.isNaN(vNum)) {
67
- return Math.round(vNum);
68
- }
69
- }
70
- return void 0;
71
- };
72
- var DEFAULT_SIZES = "(min-width: 1280px) 50vw, 100vw";
73
- var DEFAULT_QUALITY = 80;
74
- var UrlCanParse = (url) => {
75
- try {
76
- new URL(url);
77
- return true;
78
- } catch {
79
- return false;
80
- }
81
- };
82
- var getImageAttributes = (props) => {
83
- const width = getInt(props.width);
84
- const quality = Math.max(
85
- Math.min(getInt(props.quality) ?? DEFAULT_QUALITY, 100),
86
- 0
87
- );
88
- if (props.src != null && props.src !== "") {
89
- if (props.srcSet == null && props.optimize) {
90
- const sizes = props.sizes ?? (props.width == null ? DEFAULT_SIZES : void 0);
91
- return generateImgAttrs({
92
- src: props.src,
93
- width,
94
- quality,
95
- sizes,
96
- loader: props.loader
97
- });
98
- }
99
- const resAttrs = {
100
- src: UrlCanParse(props.src) ? props.src : props.loader({ src: props.src, format: "raw" })
101
- };
102
- if (props.srcSet != null) {
103
- resAttrs.srcSet = props.srcSet;
104
- }
105
- if (props.sizes != null) {
106
- resAttrs.sizes = props.sizes;
107
- }
108
- return resAttrs;
109
- }
110
- };
111
-
112
- // src/image.tsx
113
- import { jsx } from "react/jsx-runtime";
114
- var Image = forwardRef(
115
- ({
116
- quality,
117
- loader,
118
- optimize = true,
119
- loading = "lazy",
120
- decoding = "async",
121
- ...imageProps
122
- }, ref) => {
123
- const imageAttributes = getImageAttributes({
124
- src: imageProps.src,
125
- srcSet: imageProps.srcSet,
126
- sizes: imageProps.sizes,
127
- width: imageProps.width,
128
- quality,
129
- loader,
130
- optimize
131
- }) ?? { src: imagePlaceholderDataUrl };
132
- return /* @__PURE__ */ jsx(
133
- "img",
134
- {
135
- alt: "",
136
- ...imageProps,
137
- ...imageAttributes,
138
- decoding,
139
- loading,
140
- ref
141
- }
142
- );
143
- }
144
- );
145
- Image.displayName = "Image";
146
- var imagePlaceholderDataUrl = `data:image/svg+xml;base64,${btoa(`<svg
147
- width="140"
148
- height="140"
149
- viewBox="0 0 600 600"
150
- fill="none"
151
- xmlns="http://www.w3.org/2000/svg"
152
- >
153
- <rect width="600" height="600" fill="#DFE3E6" />
154
- <path
155
- fill-rule="evenodd"
156
- clip-rule="evenodd"
157
- d="M450 170H150C141.716 170 135 176.716 135 185V415C135 423.284 141.716 430 150 430H450C458.284 430 465 423.284 465 415V185C465 176.716 458.284 170 450 170ZM150 145C127.909 145 110 162.909 110 185V415C110 437.091 127.909 455 150 455H450C472.091 455 490 437.091 490 415V185C490 162.909 472.091 145 450 145H150Z"
158
- fill="#C1C8CD"
159
- />
160
- <path
161
- d="M237.135 235.012C237.135 255.723 220.345 272.512 199.635 272.512C178.924 272.512 162.135 255.723 162.135 235.012C162.135 214.301 178.924 197.512 199.635 197.512C220.345 197.512 237.135 214.301 237.135 235.012Z"
162
- fill="#C1C8CD"
163
- />
164
- <path
165
- d="M160 405V367.205L221.609 306.364L256.552 338.628L358.161 234L440 316.043V405H160Z"
166
- fill="#C1C8CD"
167
- />
168
- </svg>`)}`;
169
-
170
- // src/image-loaders.ts
171
- import warnOnce from "warn-once";
172
- var NON_EXISTING_DOMAIN = "https://a3cbcbec-cdb1-4ea4-ad60-43c795308ddc.ddc";
173
- var joinPath = (...segments) => {
174
- return segments.filter((segment) => segment !== "").map((segment) => segment.replace(/(^\/+|\/+$)/g, "")).join("/");
175
- };
176
- var encodePathFragment = (fragment) => {
177
- return encodeURIComponent(fragment).replace(/%2F/g, "/");
178
- };
179
- var createImageLoader = (loaderOptions) => (props) => {
180
- const width = props.format === "raw" ? 16 : props.width;
181
- const quality = props.format === "raw" ? 100 : props.quality;
182
- const { format, src } = props;
183
- if (true) {
184
- warnOnce(
185
- allSizes.includes(width) === false,
186
- "Width must be only from allowed values"
187
- );
188
- }
189
- const { imageBaseUrl } = loaderOptions;
190
- let resultUrl;
191
- try {
192
- resultUrl = new URL(imageBaseUrl, NON_EXISTING_DOMAIN);
193
- } catch {
194
- return src;
195
- }
196
- if (format !== "raw") {
197
- resultUrl.searchParams.set("width", width.toString());
198
- resultUrl.searchParams.set("quality", quality.toString());
199
- if (props.height != null) {
200
- resultUrl.searchParams.set("height", props.height.toString());
201
- }
202
- if (props.fit != null) {
203
- resultUrl.searchParams.set("fit", props.fit);
204
- }
205
- }
206
- resultUrl.searchParams.set("format", format ?? "auto");
207
- resultUrl.pathname = joinPath(resultUrl.pathname, encodePathFragment(src));
208
- if (resultUrl.href.startsWith(NON_EXISTING_DOMAIN)) {
209
- return `${resultUrl.pathname}?${resultUrl.searchParams.toString()}`;
210
- }
211
- return resultUrl.href;
212
- };
213
- export {
214
- Image,
215
- UrlCanParse,
216
- allSizes,
217
- createImageLoader,
218
- getImageAttributes,
219
- imagePlaceholderDataUrl
220
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,9 +0,0 @@
1
- import { type ImageLoader } from "./image-optimize";
2
- export type ImageLoaderOptions = {
3
- imageBaseUrl: string;
4
- };
5
- /**
6
- * Default image loader in case of no loader provided
7
- * https://developers.cloudflare.com/images/image-resizing/url-format/
8
- **/
9
- export declare const createImageLoader: (loaderOptions: ImageLoaderOptions) => ImageLoader;
@@ -1,115 +0,0 @@
1
- /**
2
- * # Responsive Image component helpers.
3
- *
4
- * ## Quick summary about img srcset and sizes attributes:
5
- *
6
- * There are 2 ways to define what image will be loaded in the img property srcset.
7
- *
8
- * 1. via pixel density descriptor 'x', like `srcset="photo-small.jpg 1x, photo-medium.jpg 1.5x, photo-huge.jpg 2x"`
9
- * src will be selected depending on `device-pixel-ratio`.
10
- *
11
- * 2. via viewport width descriptor 'w' and sizes property containing source size descriptors, like
12
- * `srcset="photo-small.jpg 320w, photo-medium.jpg 640w, photo-huge.jpg 1280w"`
13
- * `sizes="(max-width: 600px) 400px, (max-width: 1200px) 70vw, 50vw"`
14
- *
15
- * The browser finds the first matching media query from source size descriptors,
16
- * then use source size value to generate internally srcset
17
- * with pixel density descriptors dividing width descriptor value by source size value.
18
- *
19
- * Using the example above for viewport width 800px.
20
- * The first matching media query is (max-width: 1200px)
21
- * source size value is 70vw equal to 800px * 0,7 = 560px
22
- *
23
- * browser internal srcset will be (we divide `w` descriptor by source size value):
24
- * photo-small.jpg 320w/560px, photo-medium.jpg 640w/560px, photo-huge.jpg 1280w/560px =>
25
- * photo-small.jpg 0.57x, photo-medium.jpg 1.14x, photo-huge.jpg 2.28x
26
- *
27
- * Finally same rules as for pixel density descriptor 'x' are applied.
28
- *
29
- * ## Algorithm (without optimizations):
30
- *
31
- * We have a predefined array of all supported image sizes allSizes, this is the real width of an image in pixels.
32
- * This is good for caching, as we can cache image with specific width and then use it for different devices.
33
- *
34
- * > allSizes array is a tradeoff between cache and the best possible image size you deliver to the user.
35
- * > If allSizes.length is too small, you will deliver too big images to the user,
36
- * > if allSizes.length is too big, you will have many caches misses.
37
- *
38
- * If img has a defined width property.
39
- * 1. filter allSizes to exclude loading images higher that maxDevicePixelRatio * img.width
40
- *
41
- *
42
- * If img has no defined width property.
43
- * 1. Generate srcset = allSizes.map((w) => `${getImageSrcAtWidth(w)} ${w}w`)
44
- * 2. Use sizes property, or if it is not defined use opinionated DEFAULT_SIZES = "(min-width: 1280px) 50vw, 100vw";
45
- *
46
- * Optimizations applied now:
47
- *
48
- * - If the sizes property is defined, we can exclude from `srcsets` all images
49
- * which are smaller than the `smallestRatio * smallesDeviceSize`
50
- *
51
- * Future (not implemented) optimizations and improvements:
52
- *
53
- * - Knowing image size on different viewport widths we can provide nondefault sizes property
54
- * - Knowledge of Image aspect-ratio would allow cropping images serverside.
55
- * - Early hints for high priority images https://blog.cloudflare.com/early-hints/
56
- * - Slow networks optimizations
57
- * - 404 etc processing with CSS - https://bitsofco.de/styling-broken-images/ (has some opinionated issues) or js solution with custom user fallback.
58
- *
59
- * # Attributions
60
- *
61
- * The MIT License (MIT)
62
- *
63
- * applies to:
64
- *
65
- * - https://github.com/vercel/next.js, Copyright (c) 2022 Vercel, Inc.
66
- *
67
- * The MIT License (MIT)
68
- *
69
- * Copyright (c) 2022 Vercel, Inc.
70
- *
71
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
72
- * and associated documentation files (the "Software"), to deal in the Software without restriction,
73
- * including without limitation the rights to use, copy, modify, merge, publish, distribute,
74
- * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
75
- * is furnished to do so, subject to the following conditions:
76
- *
77
- * The above copyright notice and this permission notice shall be included in all copies
78
- * or substantial portions of the Software.
79
- *
80
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
81
- * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
82
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
83
- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
84
- * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
85
- * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
86
- **/
87
- export type ImageLoader = (props: {
88
- width: number;
89
- quality: number;
90
- src: string;
91
- format?: "auto";
92
- height?: number;
93
- fit?: "pad";
94
- } | {
95
- src: string;
96
- format: "raw";
97
- }) => string;
98
- export declare const allSizes: number[];
99
- /**
100
- * URL.canParse(props.src)
101
- */
102
- export declare const UrlCanParse: (url: string) => boolean;
103
- export declare const getImageAttributes: (props: {
104
- src: string | undefined;
105
- srcSet: string | undefined;
106
- sizes: string | undefined;
107
- width: string | number | undefined;
108
- quality: string | number | undefined;
109
- loader: ImageLoader;
110
- optimize: boolean;
111
- }) => {
112
- src: string;
113
- srcSet?: string;
114
- sizes?: string;
115
- } | undefined;
@@ -1 +0,0 @@
1
- export {};
@@ -1,11 +0,0 @@
1
- import { type ComponentProps } from "react";
2
- import { type ImageLoader } from "./image-optimize";
3
- declare const defaultTag = "img";
4
- type ImageProps = ComponentProps<typeof defaultTag> & {
5
- quality?: number;
6
- optimize?: boolean;
7
- loader: ImageLoader;
8
- };
9
- export declare const Image: import("react").ForwardRefExoticComponent<Omit<ImageProps, "ref"> & import("react").RefAttributes<HTMLImageElement>>;
10
- export declare const imagePlaceholderDataUrl: string;
11
- export {};
@@ -1,4 +0,0 @@
1
- export { Image, imagePlaceholderDataUrl } from "./image";
2
- export type { ImageLoader } from "./image-optimize";
3
- export * from "./image-loaders";
4
- export * from "./image-optimize";