@lonik/oh-image 2.0.5 → 2.1.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.
Files changed (3) hide show
  1. package/dist/react.d.ts +384 -15
  2. package/dist/react.js +256 -55
  3. package/package.json +10 -4
package/dist/react.d.ts CHANGED
@@ -6,7 +6,6 @@ interface ImageLoaderOptions {
6
6
  src: string;
7
7
  width?: number | null | undefined;
8
8
  height?: number | null | undefined;
9
- isPlaceholder?: boolean;
10
9
  }
11
10
  type ImageLoader = (options: ImageLoaderOptions) => string;
12
11
  type ImageSrcType = string | ImageSrc;
@@ -20,7 +19,7 @@ interface ImageProps extends Partial<Pick<ImgHTMLAttributes<HTMLImageElement>, "
20
19
  /** */
21
20
  src: ImageSrcType;
22
21
  /** The URL of the placeholder image to display while loading. */
23
- placeholderUrl?: string | undefined;
22
+ placeholder?: string | undefined | ImageLoader | null;
24
23
  /**
25
24
  * Sets the image to "fill mode", which eliminates the height/width requirement and adds
26
25
  * styles such that the image fills its containing element.
@@ -77,24 +76,394 @@ declare function ImageProvider({
77
76
  children: React.ReactNode;
78
77
  } & Partial<ImageContextValue>): react_jsx_runtime0.JSX.Element;
79
78
  //#endregion
80
- //#region src/react/loaders/imgproxy-loader.d.ts
81
- interface ImgproxyLoaderOptions {
82
- path: string;
83
- placeholder: boolean;
84
- format: string;
85
- params?: Record<string, string>;
86
- placeholderParams?: Record<string, string>;
87
- breakpoints?: number[];
88
- paramsSeparator?: string;
79
+ //#region src/react/loaders/base-loader-options.d.ts
80
+ interface BaseLoaderOptions<T> {
81
+ path?: string;
82
+ transforms?: T;
83
+ /** @deprecated Use `transforms` instead. */
84
+ params?: any;
85
+ }
86
+ interface BaseGlobalLoaderOptions<T> extends BaseLoaderOptions<T> {
87
+ placeholderTransforms?: T;
88
+ }
89
+ //#endregion
90
+ //#region src/react/loaders/imgproxy/imgproxy-options.d.ts
91
+ type ResizeType = "fit" | "fill" | "fill-down" | "force" | "auto";
92
+ type ResizeAlgorithm = "nearest" | "linear" | "cubic" | "lanczos2" | "lanczos3";
93
+ type GravityType = "no" | "so" | "ea" | "we" | "noea" | "nowe" | "soea" | "sowe" | "ce";
94
+ interface ResizeOptions {
95
+ resizing_type?: ResizeType;
96
+ width?: number;
97
+ height?: number;
98
+ enlarge?: boolean;
99
+ extend?: boolean;
100
+ }
101
+ interface SizeOptions {
102
+ width?: number;
103
+ height?: number;
104
+ enlarge?: boolean;
105
+ extend?: boolean;
106
+ }
107
+ interface ExtendOptions {
108
+ extend?: boolean;
109
+ gravity?: GravityType;
110
+ }
111
+ interface GravityOptions {
112
+ type: GravityType;
113
+ x_offset?: number;
114
+ y_offset?: number;
115
+ }
116
+ interface CropOptions {
117
+ width: number;
118
+ height: number;
119
+ gravity?: GravityType;
120
+ }
121
+ interface TrimOptions {
122
+ threshold: number;
123
+ color?: string;
124
+ equal_hor?: boolean;
125
+ equal_ver?: boolean;
126
+ }
127
+ interface PaddingOptions {
128
+ top?: number;
129
+ right?: number;
130
+ bottom?: number;
131
+ left?: number;
132
+ }
133
+ interface BackgroundOptions {
134
+ r: number;
135
+ g: number;
136
+ b: number;
137
+ }
138
+ interface AdjustOptions {
139
+ brightness?: number;
140
+ contrast?: number;
141
+ saturation?: number;
142
+ }
143
+ interface BlurDetectionsOptions {
144
+ sigma: number;
145
+ class_names: string[];
89
146
  }
90
- declare function useImgproxyContext(): ImgproxyLoaderOptions;
147
+ interface DrawDetectionsOptions {
148
+ draw: boolean;
149
+ class_names: string[];
150
+ }
151
+ interface WatermarkOptions {
152
+ opacity: number;
153
+ position?: GravityType | "re";
154
+ x_offset?: number;
155
+ y_offset?: number;
156
+ scale?: number;
157
+ }
158
+ interface WatermarkSizeOptions {
159
+ width: number;
160
+ height: number;
161
+ }
162
+ interface UnsharpeningOptions {
163
+ mode?: string;
164
+ weight?: number;
165
+ dividor?: number;
166
+ }
167
+ interface AutoqualityOptions {
168
+ method?: string;
169
+ target?: number;
170
+ min_quality?: number;
171
+ max_quality?: number;
172
+ allowed_error?: number;
173
+ }
174
+ interface JpegOptions {
175
+ progressive?: boolean;
176
+ no_subsample?: boolean;
177
+ trellis_quant?: boolean;
178
+ overshoot_deringing?: boolean;
179
+ optimize_scans?: boolean;
180
+ quant_table?: number;
181
+ }
182
+ interface PngOptions {
183
+ interlaced?: boolean;
184
+ quantize?: boolean;
185
+ quantization_colors?: number;
186
+ }
187
+ interface ImgproxyTransforms {
188
+ /**
189
+ * Defines the resizing type, width, height, enlarge, and extend.
190
+ * All arguments are optional and can be omitted to use their default values.
191
+ */
192
+ resize?: ResizeOptions;
193
+ /**
194
+ * Defines the width, height, enlarge, and extend.
195
+ * All arguments are optional and can be omitted to use their default values.
196
+ */
197
+ size?: SizeOptions;
198
+ /**
199
+ * Defines how imgproxy will resize the source image.
200
+ * Default: fit
201
+ */
202
+ resizing_type?: ResizeType;
203
+ /**
204
+ * Defines the algorithm that imgproxy will use for resizing.
205
+ * Default: lanczos3
206
+ */
207
+ resizing_algorithm?: ResizeAlgorithm;
208
+ /**
209
+ * Defines the width of the resulting image.
210
+ * Default: 0
211
+ */
212
+ width?: number;
213
+ /**
214
+ * Defines the height of the resulting image.
215
+ * Default: 0
216
+ */
217
+ height?: number;
218
+ /**
219
+ * Defines the minimum width of the resulting image.
220
+ * Default: 0
221
+ */
222
+ "min-width"?: number;
223
+ /**
224
+ * Defines the minimum height of the resulting image.
225
+ * Default: 0
226
+ */
227
+ "min-height"?: number;
228
+ /**
229
+ * When set, imgproxy will multiply the image dimensions according to these factors.
230
+ * Default: 1
231
+ */
232
+ zoom?: number | {
233
+ x: number;
234
+ y: number;
235
+ };
236
+ /**
237
+ * When set, imgproxy will multiply the image dimensions according to this factor for HiDPI (Retina) devices.
238
+ * Default: 1
239
+ */
240
+ dpr?: number;
241
+ /**
242
+ * When set to true, imgproxy will enlarge the image if it is smaller than the given size.
243
+ * Default: false
244
+ */
245
+ enlarge?: boolean;
246
+ /**
247
+ * When set to true, imgproxy will extend the image if it is smaller than the given size.
248
+ * Can also specify gravity.
249
+ * Default: false:ce:0:0
250
+ */
251
+ extend?: boolean | ExtendOptions;
252
+ /**
253
+ * When imgproxy needs to cut some parts of the image, it is guided by the gravity option.
254
+ * Default: ce:0:0
255
+ */
256
+ gravity?: GravityOptions;
257
+ /**
258
+ * Defines an area of the image to be processed (crop before resize).
259
+ */
260
+ crop?: CropOptions;
261
+ /**
262
+ * Removes surrounding background.
263
+ */
264
+ trim?: TrimOptions;
265
+ /**
266
+ * Defines padding size.
267
+ */
268
+ padding?: PaddingOptions;
269
+ /**
270
+ * When set to true, imgproxy will automatically rotate images based on the EXIF Orientation parameter.
271
+ */
272
+ auto_rotate?: boolean;
273
+ /**
274
+ * Rotates the image on the specified angle.
275
+ * Default: 0
276
+ */
277
+ rotate?: number;
278
+ /**
279
+ * When set, imgproxy will fill the resulting image background with the specified color.
280
+ * Default: disabled
281
+ */
282
+ background?: string | BackgroundOptions;
283
+ /**
284
+ * Adds an alpha channel to background.
285
+ * Default: 1
286
+ */
287
+ background_alpha?: number;
288
+ /**
289
+ * Defines the brightness, contrast, and saturation.
290
+ */
291
+ adjust?: AdjustOptions;
292
+ /**
293
+ * When set, imgproxy will adjust brightness of the resulting image.
294
+ * Default: 0
295
+ */
296
+ brightness?: number;
297
+ /**
298
+ * When set, imgproxy will adjust the contrast of the resulting image.
299
+ * Default: 1
300
+ */
301
+ contrast?: number;
302
+ /**
303
+ * When set, imgproxy will adjust saturation of the resulting image.
304
+ * Default: 1
305
+ */
306
+ saturation?: number;
307
+ /**
308
+ * When set, imgproxy will apply a gaussian blur filter to the resulting image.
309
+ * Default: disabled
310
+ */
311
+ blur?: number;
312
+ /**
313
+ * When set, imgproxy will apply the sharpen filter to the resulting image.
314
+ * Default: disabled
315
+ */
316
+ sharpen?: number;
317
+ /**
318
+ * When set, imgproxy will apply the pixelate filter to the resulting image.
319
+ * Default: disabled
320
+ */
321
+ pixelate?: number;
322
+ /**
323
+ * Allows redefining unsharpening options.
324
+ */
325
+ unsharpening?: UnsharpeningOptions;
326
+ /**
327
+ * imgproxy detects objects of the provided classes and blurs them.
328
+ */
329
+ blur_detections?: BlurDetectionsOptions;
330
+ /**
331
+ * When draw is set to true, imgproxy detects objects of the provided classes and draws their bounding boxes.
332
+ */
333
+ draw_detections?: DrawDetectionsOptions;
334
+ /**
335
+ * Places a watermark on the processed image.
336
+ * Default: disabled
337
+ */
338
+ watermark?: WatermarkOptions;
339
+ /**
340
+ * When set, imgproxy will use the image from the specified URL as a watermark.
341
+ * Default: blank
342
+ */
343
+ watermark_url?: string;
344
+ /**
345
+ * When set, imgproxy will generate an image from the provided text and use it as a watermark.
346
+ * Default: blank
347
+ */
348
+ watermark_text?: string;
349
+ /**
350
+ * Defines the desired width and height of the watermark.
351
+ * Default: 0:0
352
+ */
353
+ watermark_size?: WatermarkSizeOptions;
354
+ /**
355
+ * When set, imgproxy will prepend a <style> node with the provided content to the <svg> node.
356
+ * Default: blank
357
+ */
358
+ style?: string;
359
+ /**
360
+ * When set to true, imgproxy will strip the metadata (EXIF, IPTC, etc.) on JPEG and WebP output images.
361
+ */
362
+ strip_metadata?: boolean;
363
+ /**
364
+ * When set to true, imgproxy will not remove copyright info while stripping metadata.
365
+ */
366
+ keep_copyright?: boolean;
367
+ /**
368
+ * When set to true, imgproxy will transform the embedded color profile (ICC) to sRGB and remove it from the image.
369
+ */
370
+ strip_color_profile?: boolean;
371
+ /**
372
+ * When set to true and the source image has an embedded thumbnail, imgproxy will always use the embedded thumbnail.
373
+ */
374
+ enforce_thumbnail?: boolean;
375
+ /**
376
+ * When set to true, imgproxy will return attachment in the Content-Disposition header.
377
+ */
378
+ return_attachment?: boolean;
379
+ /**
380
+ * Redefines quality of the resulting image, as a percentage.
381
+ * Default: 0
382
+ */
383
+ quality?: number;
384
+ /**
385
+ * Adds or redefines IMGPROXY_FORMAT_QUALITY values.
386
+ */
387
+ format_quality?: Record<string, number>;
388
+ /**
389
+ * Redefines autoquality settings.
390
+ */
391
+ autoquality?: AutoqualityOptions;
392
+ /**
393
+ * When set, imgproxy automatically degrades the quality of the image until the image size is under the specified amount of bytes.
394
+ * Default: 0
395
+ */
396
+ max_bytes?: number;
397
+ /**
398
+ * Allows redefining JPEG saving options.
399
+ */
400
+ jpeg_options?: JpegOptions;
401
+ /**
402
+ * Allows redefining PNG saving options.
403
+ */
404
+ png_options?: PngOptions;
405
+ /**
406
+ * Specifies the resulting image format.
407
+ * Default: jpg
408
+ */
409
+ format?: string;
410
+ /**
411
+ * When a source image supports pagination or animation, this option allows specifying the page to use it on.
412
+ * Default: 0
413
+ */
414
+ page?: number;
415
+ /**
416
+ * Allows redefining IMGPROXY_VIDEO_THUMBNAIL_SECOND config.
417
+ */
418
+ video_thumbnail_second?: number;
419
+ /**
420
+ * You can use a custom fallback image by specifying its URL.
421
+ * Default: blank
422
+ */
423
+ fallback_image_url?: string;
424
+ /**
425
+ * When set, imgproxy will skip the processing of the listed formats.
426
+ * Default: empty
427
+ */
428
+ skip_processing?: string[];
429
+ /**
430
+ * Cache buster doesn't affect image processing but its changing allows for bypassing the CDN, proxy server and browser cache.
431
+ * Default: empty
432
+ */
433
+ cachebuster?: string;
434
+ /**
435
+ * When set, imgproxy will check the provided unix timestamp and return 404 when expired.
436
+ * Default: empty
437
+ */
438
+ expires?: number;
439
+ /**
440
+ * Defines a filename for the Content-Disposition header.
441
+ * Default: empty
442
+ */
443
+ filename?: string;
444
+ /**
445
+ * Defines a list of presets to be used by imgproxy.
446
+ * Default: empty
447
+ */
448
+ preset?: string[];
449
+ }
450
+ type ImgproxyOptions = BaseLoaderOptions<ImgproxyTransforms>;
451
+ type ImgproxyGlobalOptions = BaseGlobalLoaderOptions<ImgproxyTransforms>;
452
+ //#endregion
453
+ //#region src/react/loaders/imgproxy/use-imgproxy-loader.d.ts
454
+ declare function useImgproxyLoader(options: ImgproxyOptions): (imageOptions: ImageLoaderOptions) => string;
455
+ //#endregion
456
+ //#region src/react/loaders/imgproxy/imgproxy-context.d.ts
457
+ declare function useImgproxyContext(): ImgproxyGlobalOptions;
91
458
  declare function ImgproxyLoaderProvider({
92
459
  children,
93
460
  ...props
94
461
  }: {
95
462
  children: React.ReactNode;
96
- } & Partial<ImgproxyLoaderOptions>): react_jsx_runtime0.JSX.Element;
97
- declare function useImgproxyLoader(options?: Partial<ImgproxyLoaderOptions>): ImageLoader;
463
+ } & Partial<ImgproxyOptions>): react_jsx_runtime0.JSX.Element;
464
+ //#endregion
465
+ //#region src/react/loaders/imgproxy/use-imgproxy-placeholder.d.ts
466
+ declare function useImgproxyPlaceholder(options: ImgproxyOptions): (imageOptions: ImageLoaderOptions) => string;
98
467
  //#endregion
99
468
  //#region src/react/loaders/cloudflare-loader.d.ts
100
469
  interface CloudflareLoaderOptions {
@@ -132,4 +501,4 @@ declare function CloudinaryLoaderProvider({
132
501
  } & Partial<CloudinaryLoaderOptions>): react_jsx_runtime0.JSX.Element;
133
502
  declare function useCloudinaryLoader(options?: Partial<CloudinaryLoaderOptions>): ImageLoader;
134
503
  //#endregion
135
- export { type CloudflareLoaderOptions, CloudflareLoaderProvider, type CloudinaryLoaderOptions, CloudinaryLoaderProvider, Image, type ImageLoader, type ImageLoaderOptions, type ImageProps, ImageProvider, type ImageSrcType, type ImgproxyLoaderOptions, ImgproxyLoaderProvider, useCloudflareContext, useCloudflareLoader, useCloudinaryContext, useCloudinaryLoader, useImageContext, useImgLoaded, useImgproxyContext, useImgproxyLoader };
504
+ export { type CloudflareLoaderOptions, CloudflareLoaderProvider, type CloudinaryLoaderOptions, CloudinaryLoaderProvider, Image, type ImageLoader, type ImageLoaderOptions, type ImageProps, ImageProvider, type ImageSrcType, type ImgproxyGlobalOptions, ImgproxyLoaderProvider, type ImgproxyOptions, type ImgproxyTransforms, useCloudflareContext, useCloudflareLoader, useCloudinaryContext, useCloudinaryLoader, useImageContext, useImgLoaded, useImgproxyContext, useImgproxyLoader, useImgproxyPlaceholder };
package/dist/react.js CHANGED
@@ -95,7 +95,7 @@ function resolveOptions(prop, defaultOptions) {
95
95
  resolved.loading = resolveLoading(resolved);
96
96
  resolved.srcSet = resolveSrcSet(resolved);
97
97
  resolved.sizes = resolveSizes(resolved, resolved.srcSet, resolved.loading);
98
- resolved.placeholderUrl = resolvePlaceholderURL(resolved);
98
+ resolved.placeholder = resolvePlaceholder(resolved, resolved.src);
99
99
  resolved.height = resolveHeight(resolved);
100
100
  resolved.width = resolveWidth(resolved);
101
101
  resolved.src = resolveSrc(resolved);
@@ -117,8 +117,7 @@ function resolveSrcSet(prop) {
117
117
  for (const breakpoint of prop.breakpoints) if (prop.loader) entries.push(`${prop.loader({
118
118
  src: baseSrc,
119
119
  width: breakpoint,
120
- height: prop.height,
121
- isPlaceholder: false
120
+ height: prop.height
122
121
  })} ${breakpoint}w`);
123
122
  if (entries.length === 0) return;
124
123
  return entries.join(", ");
@@ -155,15 +154,15 @@ function resolveHeight(prop) {
155
154
  if (prop.height) return prop.height;
156
155
  if (typeof prop.src === "object") return prop.src.height;
157
156
  }
158
- function resolvePlaceholderURL(prop) {
159
- if (prop.placeholderUrl) return prop.placeholderUrl;
160
- if (typeof prop.src === "object") return prop.src.placeholderUrl;
161
- if (prop.loader) return prop.loader({
162
- isPlaceholder: true,
163
- src: prop.src,
157
+ function resolvePlaceholder(prop, src) {
158
+ if (!prop.placeholder) return null;
159
+ if (typeof prop.placeholder === "string") return prop.placeholder;
160
+ if (typeof prop.placeholder === "function") return prop.placeholder({
161
+ src,
164
162
  width: prop.width,
165
163
  height: prop.height
166
164
  });
165
+ return null;
167
166
  }
168
167
 
169
168
  //#endregion
@@ -242,11 +241,11 @@ function ImageProvider({ children, ...props }) {
242
241
  //#region src/react/image.tsx
243
242
  const preload = "preload" in ReactDOM && typeof ReactDOM.preload === "function" ? ReactDOM.preload : null;
244
243
  function getPlaceholderStyles(props) {
245
- if (!props.placeholderUrl) return {};
244
+ if (!props.placeholder) return {};
246
245
  return {
247
246
  backgroundPosition: "50% 50%",
248
247
  backgroundRepeat: "no-repeat",
249
- backgroundImage: `url(${props.placeholderUrl})`,
248
+ backgroundImage: `url(${props.placeholder})`,
250
249
  backgroundSize: "cover"
251
250
  };
252
251
  }
@@ -290,32 +289,216 @@ function Image(props) {
290
289
  }
291
290
 
292
291
  //#endregion
293
- //#region src/react/loaders/image-loader-utils.ts
294
- function normalizeLoaderParams(params, separator) {
295
- return Object.entries(params).map(([key, value]) => `${key}${separator}${value}`);
296
- }
297
- function isAbsoluteUrl(src) {
298
- return /^https?:\/\//.test(src);
292
+ //#region src/react/loaders/loader-utils.ts
293
+ function resolveOption(key, value, separator) {
294
+ return `${key}${separator}${value}`;
295
+ }
296
+ function resolveDeprecatedParams(source, separator) {
297
+ const params = [];
298
+ for (const key of Object.keys(params)) {
299
+ const value = source[key];
300
+ if (value !== void 0) params.push(resolveOption(key, value, separator));
301
+ }
302
+ return params;
299
303
  }
300
- function assertPath(path) {
301
- assert(() => !path?.trim(), import.meta.env.DEV && `Path is required`);
302
- assert(() => {
303
- try {
304
- new URL(path);
305
- return !isAbsoluteUrl(path);
306
- } catch {
307
- return true;
308
- }
309
- }, import.meta.env.DEV && `Path is invalid url: ${path}`);
304
+
305
+ //#endregion
306
+ //#region src/react/loaders/imgproxy/create-imgproxy-url.ts
307
+ const stringifyOptions = (opCode, values) => {
308
+ return [opCode, ...values.map((v) => v == null ? "" : encodeURIComponent(v))].join(":").replace(/:+$/, "");
309
+ };
310
+ const resolveObjectParam = (key, source) => {
311
+ if (source === void 0) return;
312
+ if (key === "size") {
313
+ const tSource = source;
314
+ if (!tSource) return;
315
+ return stringifyOptions(key, [
316
+ tSource.width,
317
+ tSource.height,
318
+ tSource.enlarge,
319
+ tSource.extend
320
+ ]);
321
+ }
322
+ if (key === "resize") {
323
+ const tSource = source;
324
+ if (!tSource) return;
325
+ return stringifyOptions(key, [
326
+ tSource.resizing_type,
327
+ tSource.width,
328
+ tSource.height,
329
+ tSource.enlarge,
330
+ tSource.extend
331
+ ]);
332
+ }
333
+ if (key === "extend") {
334
+ const tSource = source;
335
+ if (!tSource) return;
336
+ if (typeof tSource === "boolean") return stringifyOptions(key, [tSource]);
337
+ return stringifyOptions(key, [tSource.extend, tSource.gravity]);
338
+ }
339
+ if (key === "gravity") {
340
+ const tSource = source;
341
+ if (!tSource) return;
342
+ return stringifyOptions(key, [
343
+ tSource.type,
344
+ tSource.x_offset,
345
+ tSource.y_offset
346
+ ]);
347
+ }
348
+ if (key === "crop") {
349
+ const tSource = source;
350
+ if (!tSource) return;
351
+ return stringifyOptions(key, [
352
+ tSource.width,
353
+ tSource.height,
354
+ tSource.gravity
355
+ ]);
356
+ }
357
+ if (key === "trim") {
358
+ const tSource = source;
359
+ if (!tSource) return;
360
+ return stringifyOptions(key, [
361
+ tSource.threshold,
362
+ tSource.color,
363
+ tSource.equal_hor,
364
+ tSource.equal_ver
365
+ ]);
366
+ }
367
+ if (key === "padding") {
368
+ const tSource = source;
369
+ if (!tSource) return;
370
+ return stringifyOptions(key, [
371
+ tSource.top,
372
+ tSource.right,
373
+ tSource.bottom,
374
+ tSource.left
375
+ ]);
376
+ }
377
+ if (key === "background") {
378
+ const tSource = source;
379
+ if (!tSource) return;
380
+ if (typeof tSource === "string") return stringifyOptions(key, [tSource]);
381
+ return stringifyOptions(key, [
382
+ tSource.r,
383
+ tSource.g,
384
+ tSource.b
385
+ ]);
386
+ }
387
+ if (key === "adjust") {
388
+ const tSource = source;
389
+ if (!tSource) return;
390
+ return stringifyOptions(key, [
391
+ tSource.brightness,
392
+ tSource.contrast,
393
+ tSource.saturation
394
+ ]);
395
+ }
396
+ if (key === "blur_detections") {
397
+ const tSource = source;
398
+ if (!tSource) return;
399
+ return stringifyOptions(key, [tSource.sigma, ...tSource.class_names]);
400
+ }
401
+ if (key === "draw_detections") {
402
+ const tSource = source;
403
+ if (!tSource) return;
404
+ return stringifyOptions(key, [tSource.draw, ...tSource.class_names]);
405
+ }
406
+ if (key === "watermark") {
407
+ const tSource = source;
408
+ if (!tSource) return;
409
+ return stringifyOptions(key, [
410
+ tSource.opacity,
411
+ tSource.position,
412
+ tSource.x_offset,
413
+ tSource.y_offset,
414
+ tSource.scale
415
+ ]);
416
+ }
417
+ if (key === "watermark_size") {
418
+ const tSource = source;
419
+ if (!tSource) return;
420
+ return stringifyOptions(key, [tSource.width, tSource.height]);
421
+ }
422
+ if (key === "unsharpening") {
423
+ const tSource = source;
424
+ if (!tSource) return;
425
+ return stringifyOptions(key, [
426
+ tSource.mode,
427
+ tSource.weight,
428
+ tSource.dividor
429
+ ]);
430
+ }
431
+ if (key === "autoquality") {
432
+ const tSource = source;
433
+ if (!tSource) return;
434
+ return stringifyOptions(key, [
435
+ tSource.method,
436
+ tSource.target,
437
+ tSource.min_quality,
438
+ tSource.max_quality,
439
+ tSource.allowed_error
440
+ ]);
441
+ }
442
+ if (key === "jpeg_options") {
443
+ const tSource = source;
444
+ if (!tSource) return;
445
+ return stringifyOptions(key, [
446
+ tSource.progressive,
447
+ tSource.no_subsample,
448
+ tSource.trellis_quant,
449
+ tSource.overshoot_deringing,
450
+ tSource.optimize_scans,
451
+ tSource.quant_table
452
+ ]);
453
+ }
454
+ if (key === "png_options") {
455
+ const tSource = source;
456
+ if (!tSource) return;
457
+ return stringifyOptions(key, [
458
+ tSource.interlaced,
459
+ tSource.quantize,
460
+ tSource.quantization_colors
461
+ ]);
462
+ }
463
+ if (key === "zoom") {
464
+ const tSource = source;
465
+ if (!tSource) return;
466
+ if (typeof tSource === "number") return stringifyOptions(key, [tSource]);
467
+ return stringifyOptions(key, [tSource.x, tSource.y]);
468
+ }
469
+ };
470
+ const resolveTransforms = (transforms) => {
471
+ if (!transforms) return "";
472
+ const params = [];
473
+ for (const key of Object.keys(transforms)) {
474
+ const value = transforms[key];
475
+ const keyCast = key;
476
+ if (value === void 0) continue;
477
+ if (typeof value === "object") {
478
+ const objectParams = resolveObjectParam(keyCast, value);
479
+ if (objectParams) params.push(objectParams);
480
+ } else params.push(stringifyOptions(key, [value]));
481
+ }
482
+ return params;
483
+ };
484
+ function createImgproxyUrl(path, transforms, imageOptions) {
485
+ if (!path) throw new Error("Path must be provided");
486
+ const params = [];
487
+ if (imageOptions.width) params.push(stringifyOptions("width", [imageOptions.width]));
488
+ if (imageOptions.height) params.push(stringifyOptions("height", [imageOptions.height]));
489
+ params.push(...resolveTransforms(transforms));
490
+ if (params) params.push(...resolveDeprecatedParams(params, ":"));
491
+ return `${path}/${params.join("/")}/plain/${imageOptions.src}`;
310
492
  }
311
493
 
312
494
  //#endregion
313
- //#region src/react/loaders/imgproxy-loader.tsx
495
+ //#region src/react/loaders/imgproxy/imgproxy-context.tsx
314
496
  const ImgproxyContext = createContext({
315
- path: "",
316
- placeholder: true,
317
- format: "webp",
318
- placeholderParams: { quality: "20" }
497
+ transforms: { format: "webp" },
498
+ placeholderTransforms: {
499
+ quality: 10,
500
+ format: "webp"
501
+ }
319
502
  });
320
503
  function useImgproxyContext() {
321
504
  return useContext(ImgproxyContext);
@@ -330,31 +513,49 @@ function ImgproxyLoaderProvider({ children, ...props }) {
330
513
  children
331
514
  });
332
515
  }
516
+
517
+ //#endregion
518
+ //#region src/react/loaders/imgproxy/use-imgproxy-loader.tsx
333
519
  function useImgproxyLoader(options) {
334
- const resolvedOptions = {
335
- ...useImgproxyContext(),
336
- ...options
520
+ const context = useImgproxyContext();
521
+ const path = options.path || context.path;
522
+ const transforms = {
523
+ ...context.transforms,
524
+ ...options.transforms
337
525
  };
338
- assertPath(resolvedOptions.path);
339
- return (imageOptions) => {
340
- const parts = [];
341
- const format = resolvedOptions.format;
342
- const paramsSeparator = resolvedOptions.paramsSeparator ?? "/";
343
- if (format) parts.push(`format:${format}`);
344
- if (imageOptions.width) parts.push(`width:${imageOptions.width}`);
345
- if (imageOptions.height) parts.push(`height:${imageOptions.height}`);
346
- if (imageOptions.isPlaceholder) {
347
- if (resolvedOptions.placeholderParams) {
348
- const placeholderParams = normalizeLoaderParams(resolvedOptions.placeholderParams, ":");
349
- parts.push(...placeholderParams);
350
- }
351
- } else if (resolvedOptions.params) {
352
- const params = normalizeLoaderParams(resolvedOptions.params, ":");
353
- parts.push(...params);
354
- }
355
- const processingOptions = parts.join(paramsSeparator);
356
- return `${resolvedOptions.path}/${processingOptions}/plain/${imageOptions.src}`;
526
+ return (imageOptions) => createImgproxyUrl(path, transforms, imageOptions);
527
+ }
528
+
529
+ //#endregion
530
+ //#region src/react/loaders/imgproxy/use-imgproxy-placeholder.tsx
531
+ function useImgproxyPlaceholder(options) {
532
+ const context = useImgproxyContext();
533
+ const path = options.path || context.path;
534
+ const transforms = {
535
+ ...context.placeholderTransforms,
536
+ ...options.transforms
357
537
  };
538
+ return (imageOptions) => createImgproxyUrl(path, transforms, imageOptions);
539
+ }
540
+
541
+ //#endregion
542
+ //#region src/react/loaders/image-loader-utils.ts
543
+ function normalizeLoaderParams(params, separator) {
544
+ return Object.entries(params).map(([key, value]) => `${key}${separator}${value}`);
545
+ }
546
+ function isAbsoluteUrl(src) {
547
+ return /^https?:\/\//.test(src);
548
+ }
549
+ function assertPath(path) {
550
+ assert(() => !path?.trim(), import.meta.env.DEV && `Path is required`);
551
+ assert(() => {
552
+ try {
553
+ new URL(path);
554
+ return !isAbsoluteUrl(path);
555
+ } catch {
556
+ return true;
557
+ }
558
+ }, import.meta.env.DEV && `Path is invalid url: ${path}`);
358
559
  }
359
560
 
360
561
  //#endregion
@@ -457,4 +658,4 @@ function useCloudinaryLoader(options) {
457
658
  }
458
659
 
459
660
  //#endregion
460
- export { CloudflareLoaderProvider, CloudinaryLoaderProvider, Image, ImageProvider, ImgproxyLoaderProvider, useCloudflareContext, useCloudflareLoader, useCloudinaryContext, useCloudinaryLoader, useImageContext, useImgLoaded, useImgproxyContext, useImgproxyLoader };
661
+ export { CloudflareLoaderProvider, CloudinaryLoaderProvider, Image, ImageProvider, ImgproxyLoaderProvider, useCloudflareContext, useCloudflareLoader, useCloudinaryContext, useCloudinaryLoader, useImageContext, useImgLoaded, useImgproxyContext, useImgproxyLoader, useImgproxyPlaceholder };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lonik/oh-image",
3
3
  "type": "module",
4
- "version": "2.0.5",
4
+ "version": "2.1.0",
5
5
  "description": "A React component library for optimized image handling.",
6
6
  "author": "Luka Onikadze <lukonik@gmail.com>",
7
7
  "license": "MIT",
@@ -40,6 +40,7 @@
40
40
  "play": "vite",
41
41
  "play:build": "vite build",
42
42
  "play:preview": "vite preview",
43
+ "play:host": "vite --host",
43
44
  "test": "vitest",
44
45
  "coverage": "vitest --coverage",
45
46
  "typecheck": "tsc --noEmit",
@@ -54,12 +55,15 @@
54
55
  "devDependencies": {
55
56
  "@commitlint/config-conventional": "^20.4.1",
56
57
  "@eslint/js": "^9.39.2",
58
+ "@tanstack/react-router": "^1.160.0",
59
+ "@tanstack/router-plugin": "^1.160.0",
57
60
  "@testing-library/jest-dom": "^6.9.1",
58
61
  "@testing-library/react": "^16.3.2",
59
62
  "@tsconfig/strictest": "^2.0.8",
60
63
  "@types/node": "^25.0.3",
61
64
  "@types/react": "^18",
62
65
  "@types/react-dom": "^18",
66
+ "@types/supertest": "^6.0.3",
63
67
  "@vitejs/plugin-react": "^5.1.2",
64
68
  "@vitest/coverage-v8": "4.0.18",
65
69
  "bumpp": "^10.3.2",
@@ -70,14 +74,16 @@
70
74
  "eslint-plugin-react-refresh": "^0.5.0",
71
75
  "globals": "^17.3.0",
72
76
  "happy-dom": "^20.6.0",
77
+ "memfs": "^4.56.10",
78
+ "supertest": "^7.2.2",
73
79
  "tsdown": "^0.18.1",
74
80
  "typescript": "^5.9.3",
75
81
  "typescript-eslint": "^8.54.0",
76
82
  "vite": "^7.3.0",
77
83
  "vitest": "^4.0.16",
78
- "@types/supertest": "^6.0.3",
79
- "memfs": "^4.56.10",
80
- "supertest": "^7.2.2"
84
+ "tailwindcss": "^4.1.18",
85
+ "@tailwindcss/vite": "^4.1.18",
86
+ "json-edit-react": "^1.29.0"
81
87
  },
82
88
  "dependencies": {
83
89
  "p-limit": "^7.3.0",