diff --git a/packages/url-loader/src/constants/parameters.ts b/packages/url-loader/src/constants/parameters.ts index ad8510c..7dc5e3f 100644 --- a/packages/url-loader/src/constants/parameters.ts +++ b/packages/url-loader/src/constants/parameters.ts @@ -1,45 +1,6 @@ import { z } from 'zod'; -/** Angle - a */ - -export const angle = { - qualifier: 'a', - schema: z.union([ - z.string(), - z.number() - ]) - .describe(JSON.stringify({ - text: 'Rotates or flips an asset by the specified number of degrees or automatically according to its orientation or available metadata.', - url: 'https://cloudinary.com/documentation/transformation_reference#a_angle' - })), -} - - -/** Aspect Ratio */ - -export const aspectRatioModesEnum = z.enum([ - 'vflip', - 'hflip', - 'ignore', - 'auto_right', - 'auto_left', -]); - -export const aspectRatio = { - qualifier: 'ar', - schema: z.union([ - z.number(), - aspectRatioModesEnum, - z.string(), - ]) - .describe(JSON.stringify({ - text: 'Crops or resizes the asset to a new aspect ratio.', - url: 'https://cloudinary.com/documentation/transformation_reference#ar_aspect_ratio' - })), -} - - -/** Crop */ +/** enum */ export const cropModesEnum = z.enum([ 'fill', @@ -58,18 +19,6 @@ export const cropModesEnum = z.enum([ 'imagga_crop', ]); -export const crop = { - qualifier: 'c', - schema: cropModesEnum - .describe(JSON.stringify({ - text: 'Mode to use when cropping an asset.', - url: 'https://cloudinary.com/documentation/transformation_reference#c_crop_resize' - })), -} - - -/** Flags */ - export const flagsEnum = z.enum([ 'animated', 'any_format', @@ -114,6 +63,63 @@ export const flagsEnum = z.enum([ 'waveform', ]); +/** Angle - a */ + +export const angle = { + qualifier: 'a', + schema: z.union([ + z.string(), + z.number() + ]) + .describe(JSON.stringify({ + text: 'Rotates or flips an asset by the specified number of degrees or automatically according to its orientation or available metadata.', + url: 'https://cloudinary.com/documentation/transformation_reference#a_angle' + })), +} + + +/** Aspect Ratio */ + +export const aspectRatioModesEnum = z.enum([ + 'vflip', + 'hflip', + 'ignore', + 'auto_right', + 'auto_left', +]); + +const aspectRatioSchema = z.union([ + z.number(), + aspectRatioModesEnum, + z.string(), +]); + +export const aspectRatio = { + qualifier: 'ar', + schema: aspectRatioSchema + .describe(JSON.stringify({ + text: 'Crops or resizes the asset to a new aspect ratio.', + url: 'https://cloudinary.com/documentation/transformation_reference#ar_aspect_ratio' + })), +} + + +/** Crop */ + +const cropSchema = cropModesEnum; + +export const crop = { + qualifier: 'c', + schema: cropSchema + .describe(JSON.stringify({ + text: 'Mode to use when cropping an asset.', + url: 'https://cloudinary.com/documentation/transformation_reference#c_crop_resize' + })), +} + + +/** Flags */ + export const flags = { qualifier: 'fl', schema: z.union([ @@ -126,12 +132,25 @@ export const flags = { })) } +/** Format */ + +export const format = { + qualifier: 'f', + // @TODO: enum + schema: z.string() + .describe(JSON.stringify({ + text: 'Converts (if necessary) and delivers an asset in the specified format regardless of the file extension used in the delivery URL.', + url: 'https://cloudinary.com/documentation/transformation_reference#f_format' + })), +} /** Gravity */ +const gravitySchema = z.string(); + export const gravity = { qualifier: 'g', - schema: z.string() + schema: gravitySchema .describe(JSON.stringify({ text: 'Determines which part of an asset to focus on. Note: Default of auto is applied for supported crop modes only.', url: 'https://cloudinary.com/documentation/transformation_reference#g_gravity' @@ -141,12 +160,14 @@ export const gravity = { /** Height */ +const heightSchema = z.union([ + z.number(), + z.string() +]) + export const height = { qualifier: 'h', - schema: z.union([ - z.number(), - z.string() - ]) + schema: heightSchema .describe(JSON.stringify({ text: 'A qualifier that determines the height of a transformed asset or an overlay.', url: 'https://cloudinary.com/documentation/transformation_reference#h_height', @@ -156,32 +177,20 @@ export const height = { /** Width */ +const widthSchema = z.union([ + z.number(), + z.string() +]); + export const width = { qualifier: 'w', - schema: z.union([ - z.number(), - z.string() - ]) + schema: widthSchema .describe(JSON.stringify({ text: 'A qualifier that sets the desired width of an asset using a specified value, or automatically based on the available width.', url: 'https://cloudinary.com/documentation/transformation_reference#w_width', })), } - -/** Width Resize */ - -export const widthResize = { - schema: z.union([ - z.string(), - z.number() - ]) - .describe(JSON.stringify({ - text: 'Width to resize the asset after all transformations are applied. Useful for responsive resizing.', - })), -} - - /** X */ export const x = { @@ -214,10 +223,63 @@ export const y = { /** Zoom */ +const zoomSchema = z.string() + export const zoom = { - schema: z.string() + schema: zoomSchema .describe(JSON.stringify({ text: 'Controls how close to crop to the detected coordinates when using face-detection, custom-coordinate, or object-specific gravity.', url: 'https://cloudinary.com/documentation/transformation_reference#z_zoom' })), -} \ No newline at end of file +} + + +/** Base Resizing */ + +export const baseAspectRatio = { + schema: aspectRatioSchema + .describe(JSON.stringify({ + text: 'Crops or resizes the asset to a new aspect ratio. This option is applied to the base layer / original asset.', + url: 'https://cloudinary.com/documentation/transformation_reference#ar_aspect_ratio' + })), +}; + +export const baseCrop = { + schema: cropSchema + .describe(JSON.stringify({ + text: 'Mode to use when cropping an asset. This option is applied to the base layer / original asset.', + url: 'https://cloudinary.com/documentation/transformation_reference#c_crop_resize' + })), +}; + +export const baseGravity = { + schema: gravitySchema + .describe(JSON.stringify({ + text: 'Determines which part of an asset to focus on. Note: Default of auto is applied for supported crop modes only. This option is applied to the base layer / original asset.', + url: 'https://cloudinary.com/documentation/transformation_reference#g_gravity' + })), +}; + +export const baseHeight = { + schema: heightSchema + .describe(JSON.stringify({ + text: 'A qualifier that determines the height of a transformed asset or an overlay. This option is applied to the base layer / original asset.', + url: 'https://cloudinary.com/documentation/transformation_reference#h_height', + })), +}; + +export const baseWidth = { + schema: widthSchema + .describe(JSON.stringify({ + text: 'A qualifier that sets the desired width of an asset using a specified value, or automatically based on the available width. This option is applied to the base layer / original asset.', + url: 'https://cloudinary.com/documentation/transformation_reference#w_width', + })), +}; + +export const baseZoom = { + schema: zoomSchema + .describe(JSON.stringify({ + text: 'Controls how close to crop to the detected coordinates when using face-detection, custom-coordinate, or object-specific gravity. This option is applied to the base layer / original asset.', + url: 'https://cloudinary.com/documentation/transformation_reference#z_zoom' + })), +}; \ No newline at end of file diff --git a/packages/url-loader/src/index.ts b/packages/url-loader/src/index.ts index 8155ba0..c4e819f 100644 --- a/packages/url-loader/src/index.ts +++ b/packages/url-loader/src/index.ts @@ -1,5 +1,5 @@ export { constructCloudinaryUrl, transformationPlugins } from './lib/cloudinary'; -export type { ConstructUrlProps, PluginOptionsResize, PluginOptions, PluginResults } from './lib/cloudinary'; +export type { ConstructUrlProps } from './lib/cloudinary'; export { effects, position, primary, text } from './constants/qualifiers'; @@ -9,5 +9,5 @@ export type { VideoOptions } from './types/video'; export type { AnalyticsOptions, CloudinaryAnalyticsOptions } from './types/analytics'; export type { ConfigOptions, CloudinaryConfigurationOptions } from './types/config'; -export type { PluginSettings, PluginOverrides } from './types/plugins'; +export type { PluginSettings, PluginOptions, PluginResults } from './types/plugins'; export type { Qualifier, QualiferConverters } from './types/qualifiers'; \ No newline at end of file diff --git a/packages/url-loader/src/lib/cloudinary.ts b/packages/url-loader/src/lib/cloudinary.ts index cd4e2ec..4c0f0ae 100644 --- a/packages/url-loader/src/lib/cloudinary.ts +++ b/packages/url-loader/src/lib/cloudinary.ts @@ -28,7 +28,7 @@ import { videoOptionsSchema } from '../types/video'; import { analyticsOptionsSchema } from '../types/analytics'; import { configOptionsSchema } from '../types/config'; -import { TransformationPlugin } from '../types/plugins'; +import { TransformationPlugin, PluginResults, PluginOptions } from '../types/plugins'; export const transformationPlugins = [ @@ -42,6 +42,12 @@ export const transformationPlugins = [ replacePlugin, restorePlugin, + // Cropping needs to be before any other general transformations + // as it provides the option of 2-step resizing where someone + // can resize the "base" canvas as well as the final resize + // mechanism commonly used for responsive resizing + croppingPlugin, + // Raw transformations should always come before // other arguments to avoid conflicting with // added options via the component @@ -49,7 +55,6 @@ export const transformationPlugins = [ rawTransformationsPlugin, abrPlugin, - croppingPlugin, defaultImagePlugin, effectsPlugin, fillBackgroundPlugin, @@ -98,21 +103,6 @@ export const constructUrlPropsSchema = z.object({ export type ConstructUrlProps = z.infer; -export interface PluginOptionsResize { - crop?: string; - width?: string | number; -} - -export interface PluginOptions { - format?: string; - resize?: PluginOptionsResize; - width?: string | number; -} - -export interface PluginResults { - options?: PluginOptions; -} - export function constructCloudinaryUrl({ options, config = {}, analytics }: ConstructUrlProps): string { // If someone is explicitly passing in undefined for analytics via the analytics option, // ensure that the URL Gen SDK option is being passed in as false as well @@ -219,23 +209,14 @@ export function constructCloudinaryUrl({ options, config = {}, analytics }: Cons const { options: pluginOptions } = results || { options: undefined }; - if ( pluginOptions?.format && options ) { - pluginEffects.format = pluginOptions.format; - } - - if ( pluginOptions?.width && options ) { - pluginEffects.resize = { - width: pluginOptions?.width - }; - } + Object.assign(pluginEffects, pluginOptions); }); // We want to perform any resizing at the end of the end of the transformation // sets to allow consistent use of positioning / sizing, especially responsively - if ( pluginEffects?.resize && !options.strictTransformations ) { - const { width, crop = 'limit' } = pluginEffects.resize; - cldAsset.effect(`c_${crop},w_${width}`); + if ( typeof pluginEffects.resize === 'string' ) { + cldAsset.addTransformation(pluginEffects.resize); } cldAsset.setDeliveryType(options?.deliveryType || 'upload'); diff --git a/packages/url-loader/src/lib/transformations.ts b/packages/url-loader/src/lib/transformations.ts index 892ae95..af5b49e 100644 --- a/packages/url-loader/src/lib/transformations.ts +++ b/packages/url-loader/src/lib/transformations.ts @@ -47,4 +47,14 @@ export function constructTransformation({ prefix, qualifier, value, converters } export function promptArrayToString(promptArray: Array) { return `(${promptArray.join(';')})`; +} + +/** + * normalizeNumberParameter + * @TODO: move into util + */ + +export function normalizeNumberParameter(param: number | string | undefined) { + if ( typeof param !== 'string' ) return param; + return parseInt(param) } \ No newline at end of file diff --git a/packages/url-loader/src/plugins/cropping.ts b/packages/url-loader/src/plugins/cropping.ts index d3109bf..27911b1 100644 --- a/packages/url-loader/src/plugins/cropping.ts +++ b/packages/url-loader/src/plugins/cropping.ts @@ -1,124 +1,149 @@ import * as parameters from '../constants/parameters'; -import { PluginSettings, PluginOverrides } from '../types/plugins'; +import { PluginSettings, PluginResults } from '../types/plugins'; -const cropsAspectRatio = [ 'crop', 'fill', 'lfill', 'fill_pad', 'thumb' ]; -const cropsGravityAuto = [ 'crop', 'fill', 'lfill', 'fill_pad', 'thumb' ]; +import { normalizeNumberParameter } from '../lib/transformations'; + +const cropsAspectRatio = [ 'auto', 'crop', 'fill', 'lfill', 'fill_pad', 'thumb' ]; +const cropsGravityAuto = [ 'auto', 'crop', 'fill', 'lfill', 'fill_pad', 'thumb' ]; const cropsWithZoom = ['crop', 'thumb']; const DEFAULT_CROP = 'limit'; export const props = { aspectRatio: parameters.aspectRatio.schema.optional(), + baseAspectRatio: parameters.baseAspectRatio.schema.optional(), + baseCrop: parameters.baseCrop.schema.optional(), + baseGravity: parameters.baseGravity.schema.optional(), + baseHeight: parameters.baseHeight.schema.optional(), + baseWidth: parameters.baseWidth.schema.optional(), + baseZoom: parameters.baseZoom.schema.optional(), crop: parameters.crop.schema.default(DEFAULT_CROP).optional(), gravity: parameters.gravity.schema.optional(), - widthResize: parameters.widthResize.schema.optional(), zoom: parameters.zoom.schema.optional(), }; export const assetTypes = ['image', 'images', 'video', 'videos']; -/** - * normalizeNumberParameter - * @TODO: move into util - */ - -export function normalizeNumberParameter(param: number | string | undefined) { - if ( typeof param !== 'string' ) return param; - return parseInt(param) -} export function plugin(props: PluginSettings) { const { cldAsset, options } = props; - const { - aspectRatio, - width: defaultWidth, - height: defaultHeight, - widthResize: defaultWidthResize, - // Default the crop to "limit" to avoid upscaling, even when widthResize is passed in. + + // Standard Cropping/Resizing + // The final crop and resize for the image to be delivered after any transformations are applied + + const overrideTransformations = collectTransformations({ + aspectRatio: options.aspectRatio, + width: options.width, + height: options.height, + gravity: options.gravity, + crop: options.crop, + zoom: options.zoom, + }); + + // Base Resizing/Cropping + // This stage provides the option for someone to crop and resize the image before any transformations + // are applied. this could be handy if you want a consistent size on different assets to share + // transformations for instance + + const baseTransformations = collectTransformations({ + aspectRatio: options.baseAspectRatio, + width: options.baseWidth, + height: options.baseHeight, + gravity: options.baseGravity, + crop: options.baseCrop, + zoom: options.baseZoom, + }); + + function collectTransformations(collectOptions: Pick) { + // Default the crop to "limit" to avoid upscaling // This avoid further distorting the image since the browser will resize in that case. // If caller wants actual resize, can explicitly pass in "scale". - crop = DEFAULT_CROP - } = options; + const { aspectRatio, crop = DEFAULT_CROP, zoom } = collectOptions; - const overrides: PluginOverrides = { - width: undefined - }; - - // Normalize sizing parameters + // Normalize sizing parameters - let height = normalizeNumberParameter(defaultHeight); - let width = normalizeNumberParameter(defaultWidth); - let widthResize = normalizeNumberParameter(defaultWidthResize); + let gravity = collectOptions.gravity; + let height = normalizeNumberParameter(collectOptions.height); + let width = normalizeNumberParameter(collectOptions.width); - const hasDefinedDimensions = height || width; - const hasValidAspectRatio = aspectRatio && cropsAspectRatio.includes(crop); + const transformations = []; - let transformationString = ''; + const hasDefinedDimensions = height || width; + const hasValidAspectRatio = aspectRatio && cropsAspectRatio.includes(crop); - // Only apply a crop if we're defining some type of dimension attribute - // where the crop would make sense + // Only apply a crop if we're defining some type of dimension attribute + // where the crop would make sense - if ( crop && ( hasDefinedDimensions || hasValidAspectRatio ) ) { - transformationString = `c_${crop}`; - } + if ( crop && ( hasDefinedDimensions || hasValidAspectRatio ) ) { + transformations.push(`c_${crop}`); + } - // Aspect Ratio requires a crop mode to be applied so we want to make - // sure a valid one is included + // Aspect Ratio requires a crop mode to be applied so we want to make + // sure a valid one is included - if ( hasValidAspectRatio ) { - transformationString = `${transformationString},ar_${aspectRatio}`; - } + if ( hasValidAspectRatio ) { + transformations.push(`ar_${aspectRatio}`); + } - if ( width ) { - transformationString = `${transformationString},w_${width}`; - } + if ( width ) { + transformations.push(`w_${width}`); + } - // Gravity of auto only applies to certain crop types otherewise - // errors, so default to auto only when crop matches type + // Some crop types don't need a height and will resize based + // on the aspect ratio - if ( !options.gravity && cropsGravityAuto.includes(crop) ) { - options.gravity = 'auto'; - } + if ( !['limit'].includes(crop) && typeof height === 'number' ) { + transformations.push(`h_${height}`); + } - // Some crop types don't need a height and will resize based - // on the aspect ratio + // Gravity of auto only applies to certain crop types otherewise + // errors, so default to auto only when crop matches type - if ( !['limit'].includes(crop) && typeof height === 'number' ) { - transformationString = `${transformationString},h_${height}`; - } + if ( !gravity && cropsGravityAuto.includes(crop) ) { + gravity = 'auto'; + } - // If we have gravity, apply it, but check that the gravity passed - // in doesn't conflict with the crop mode + // If we have gravity, apply it, but check that the gravity passed + // in doesn't conflict with the crop mode - if ( options.gravity ) { - if ( options.gravity === 'auto' && !cropsGravityAuto.includes(crop) ) { - console.warn(`Auto gravity can only be used with crop modes: ${cropsGravityAuto.join(', ')}. Not applying gravity.`); - } else { - transformationString = `${transformationString},g_${options.gravity}`; + if ( gravity ) { + if ( gravity === 'auto' && !cropsGravityAuto.includes(crop) ) { + console.warn(`Auto gravity can only be used with crop modes: ${cropsGravityAuto.join(', ')}. Not applying gravity.`); + } else { + transformations.push(`g_${gravity}`); + } } - } - // Some zoom types don't work with some crop types + // Some zoom types don't work with some crop types - if ( options.zoom ) { - if ( options.zoom === 'auto' && !cropsWithZoom.includes(crop) ) { - console.warn(`Zoom can only be used with crop modes: ${cropsWithZoom.join(', ')}. Not applying zoom.`); - } else { - transformationString = `${transformationString},z_${options.zoom}`; + if ( zoom ) { + if ( zoom === 'auto' && !cropsWithZoom.includes(crop) ) { + console.warn(`Zoom can only be used with crop modes: ${cropsWithZoom.join(', ')}. Not applying zoom.`); + } else { + transformations.push(`z_${zoom}`); + } } - } - // Finally apply the constructed transformation string to the image instance + return transformations; + } - cldAsset.effect(transformationString); + // If we have any base canvas transformations, add them to the image instance - if ( widthResize ) { - overrides.width = widthResize; + if ( baseTransformations.length > 0 ) { + cldAsset.addTransformation(baseTransformations.join(',')); } - return { - options: overrides + // If we have any overrides, which are the the standard width/height options, apply + + const results: PluginResults = { + options: {} + }; + + if ( results.options && overrideTransformations.length > 0 ) { + results.options.resize = overrideTransformations.join(','); } + + return results } diff --git a/packages/url-loader/src/plugins/fill-background.ts b/packages/url-loader/src/plugins/fill-background.ts index d198635..cef9ec9 100644 --- a/packages/url-loader/src/plugins/fill-background.ts +++ b/packages/url-loader/src/plugins/fill-background.ts @@ -5,6 +5,8 @@ import { PluginSettings } from '../types/plugins'; import { crop, gravity } from '../constants/parameters'; +import { normalizeNumberParameter } from '../lib/transformations'; + export const props = { fillBackground: z.union([ z.boolean(), @@ -29,11 +31,21 @@ export function plugin(props: PluginSettings) { const { cldAsset, options } = props; const { fillBackground } = options; + const width = normalizeNumberParameter(options.width); + const height = normalizeNumberParameter(options.height); + const hasDefinedDimensions = typeof height === 'number' && typeof width === 'number'; + let aspectRatio = options.aspectRatio; + + if ( !aspectRatio && hasDefinedDimensions ) { + aspectRatio = `${width}:${height}`; + } + + if ( !aspectRatio ) return; + if ( fillBackground === true ) { const properties = [ 'b_gen_fill', - `ar_${options.width}:${options.height}`, - `w_${options.width}`, + `ar_${aspectRatio}`, `c_${defaultCrop}` ] @@ -42,8 +54,7 @@ export function plugin(props: PluginSettings) { const { crop = defaultCrop, gravity, prompt } = fillBackground; const properties = [ - `ar_${options.width}:${options.height}`, - `w_${options.width}`, + `ar_${aspectRatio}`, `c_${crop}` ] diff --git a/packages/url-loader/src/plugins/zoompan.ts b/packages/url-loader/src/plugins/zoompan.ts index 21364b9..2509b54 100644 --- a/packages/url-loader/src/plugins/zoompan.ts +++ b/packages/url-loader/src/plugins/zoompan.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { ImageOptions } from '../types/image'; -import { PluginSettings, PluginOverrides } from '../types/plugins'; +import { PluginSettings, PluginOptions } from '../types/plugins'; export const props = { zoompan: z.union([ @@ -25,7 +25,7 @@ export function plugin(props: PluginSettings) { const { cldAsset, options } = props; const { zoompan = false } = options; - const overrides: PluginOverrides = { + const overrides: PluginOptions = { format: undefined }; diff --git a/packages/url-loader/src/types/plugins.ts b/packages/url-loader/src/types/plugins.ts index d1714dc..c219731 100644 --- a/packages/url-loader/src/types/plugins.ts +++ b/packages/url-loader/src/types/plugins.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; +import { aspectRatio, crop, gravity, height, format, width, zoom } from '../constants/parameters'; import { AssetOptions } from './asset'; import { ImageOptions } from './image'; import { VideoOptions } from './video'; @@ -9,14 +11,27 @@ export interface PluginSettings { options: Options; } -export interface PluginOverrides { - format?: string; - width?: number; -} - export interface TransformationPlugin { assetTypes: Array; plugin: Function; strict?: boolean; props: object; +} + + +export const pluginOptionsSchema = z.object({ + aspectRatio: aspectRatio.schema.optional(), + crop: crop.schema.optional(), + gravity: gravity.schema.optional(), + height: height.schema.optional(), + format: format.schema.optional(), + resize: z.string().optional(), + width: width.schema.optional(), + zoom: zoom.schema.optional(), +}) + +export type PluginOptions = z.infer; + +export interface PluginResults { + options?: PluginOptions; } \ No newline at end of file diff --git a/packages/url-loader/tests/lib/cloudinary.spec.js b/packages/url-loader/tests/lib/cloudinary.spec.js index af2a684..e424501 100644 --- a/packages/url-loader/tests/lib/cloudinary.spec.js +++ b/packages/url-loader/tests/lib/cloudinary.spec.js @@ -182,18 +182,20 @@ describe('Cloudinary', () => { describe('cropping, resizing', () => { - it('should create a Cloudinary URL with widthResize smaller than width', () => { + it('should create a Cloudinary URL with a width, height, and custom crop', () => { const cloudName = 'customtestcloud'; const deliveryType = 'upload'; const publicId = 'myimage'; const width = 900; - const widthResize = 600; + const height = 900; + const crop = 'auto'; const url = constructCloudinaryUrl({ options: { src: publicId, width, - widthResize + height, + crop }, config: { cloud: { @@ -201,21 +203,23 @@ describe('Cloudinary', () => { } } }); - expect(url).toContain(`https://res.cloudinary.com/${cloudName}/image/${deliveryType}/c_limit,w_${width}/c_limit,w_${widthResize}/f_auto/q_auto/${publicId}`); + expect(url).toContain(`https://res.cloudinary.com/${cloudName}/image/${deliveryType}/c_${crop},w_${width},h_${height},g_auto/f_auto/q_auto/${publicId}`); }); - it('should create a Cloudinary URL with widthResize same as width', () => { + it('should create a Cloudinary URL with a aspect ratio, custom crop, zoom, and default gravity', () => { const cloudName = 'customtestcloud'; const deliveryType = 'upload'; const publicId = 'myimage'; - const width = 900; - const widthResize = 900; + const aspectRatio = '16:9'; + const crop = 'fill'; + const zoom = '1.2'; const url = constructCloudinaryUrl({ options: { src: publicId, - width, - widthResize + aspectRatio, + crop, + zoom }, config: { cloud: { @@ -223,29 +227,8 @@ describe('Cloudinary', () => { } } }); - expect(url).toContain(`https://res.cloudinary.com/${cloudName}/image/${deliveryType}/c_limit,w_${width}/c_limit,w_${widthResize}/f_auto/q_auto/${publicId}`); - }); - - it('should create a Cloudinary URL with widthResize larger than width', () => { - const cloudName = 'customtestcloud'; - const deliveryType = 'upload'; - const publicId = 'myimage'; - const width = 900; - const widthResize = 1200; - const url = constructCloudinaryUrl({ - options: { - src: publicId, - width, - widthResize - }, - config: { - cloud: { - cloudName - } - } - }); - expect(url).toContain(`https://res.cloudinary.com/${cloudName}/image/${deliveryType}/c_limit,w_${width}/c_limit,w_${widthResize}/f_auto/q_auto/${publicId}`); + expect(url).toContain(`https://res.cloudinary.com/${cloudName}/image/${deliveryType}/c_${crop},ar_${aspectRatio},g_auto,z_${zoom}/f_auto/q_auto/${publicId}`); }); }); @@ -747,10 +730,17 @@ describe('Cloudinary', () => { it('Kitchen Sink - Image', () => { const cloudName = 'customtestcloud'; const assetType = 'image'; + const crop = 'auto'; + const defaultImage = 'my-image.jpg'; + const height = 321; const src = 'turtle'; const width = 123; - const height = 321; - const defaultImage = 'my-image.jpg'; + const zoom = '2.0'; + + const baseCrop = 'fill'; + const baseHeight = 8765; + const baseWidth = 4321; + const baseZoom = 3; const cartoonify = '50'; const gradientFade = true; @@ -766,11 +756,17 @@ describe('Cloudinary', () => { const url = constructCloudinaryUrl({ options: { - src, - width, - height, assetType, + baseCrop, + baseHeight, + baseWidth, + baseZoom, + crop, defaultImage, + height, + src, + width, + zoom, effects: [ { shear, @@ -832,13 +828,14 @@ describe('Cloudinary', () => { `e_gen_remove:prompt_apple;multiple_true;remove-shadow_true`, `e_background_removal`, `e_gen_restore`, - `c_limit,w_${width}`, + `c_${baseCrop},w_${baseWidth},h_${baseHeight},g_auto,z_${baseZoom}`, `d_${defaultImage}`, `o_${opacity},e_shear:${shear}`, `e_cartoonify:${cartoonify},e_gradient_fade,r_${radius}`, `l_${overlaySrc},co_${overlayColor},e_shadow:${overlayShadow},x_${overlayX},y_${overlayY}`, `fl_layer_apply,fl_no_overflow,co_${overlayColor},e_shadow:${overlayShadow},x_${overlayX},y_${overlayY}`, `e_zoompan`, + `c_${crop},w_${width},h_${height},g_auto,z_${zoom}`, `f_auto:animated`, // Effect of zoompan `q_auto`, src diff --git a/packages/url-loader/tests/plugins/cropping.spec.js b/packages/url-loader/tests/plugins/cropping.spec.js index 621fb9e..e326bb1 100644 --- a/packages/url-loader/tests/plugins/cropping.spec.js +++ b/packages/url-loader/tests/plugins/cropping.spec.js @@ -22,8 +22,8 @@ describe('Cropping plugin', () => { crop: 'crop', gravity: 'auto' }; - plugin({ cldAsset: cldImage, options }); - expect(cldImage.toURL()).toContain(`c_${options.crop},w_${options.width},h_${options.height},g_${options.gravity}/${TEST_PUBLIC_ID}`); + const { options: { resize } } = plugin({ cldAsset: cldImage, options }); + expect(resize).toContain(`c_${options.crop},w_${options.width},h_${options.height},g_${options.gravity}`); }); it('should apply a gravity of auto by default if not set explicitly', () => { @@ -33,8 +33,8 @@ describe('Cropping plugin', () => { height: 100, crop: 'fill' }; - plugin({ cldAsset: cldImage, options }); - expect(cldImage.toURL()).toContain(`c_${options.crop},w_${options.width},h_${options.height},g_auto/${TEST_PUBLIC_ID}`); + const { options: { resize } } = plugin({ cldAsset: cldImage, options }); + expect(resize).toContain(`c_${options.crop},w_${options.width},h_${options.height},g_auto`); }); it('should apply a zoom if set explicitly', () => { @@ -45,60 +45,47 @@ describe('Cropping plugin', () => { crop: 'fill', zoom: 0.5 }; - plugin({ cldAsset: cldImage, options }); - expect(cldImage.toURL()).toContain(`c_${options.crop},w_${options.width},h_${options.height},g_auto,z_${options.zoom}/${TEST_PUBLIC_ID}`); + const { options: { resize } } = plugin({ cldAsset: cldImage, options }); + expect(resize).toContain(`c_${options.crop},w_${options.width},h_${options.height},g_auto,z_${options.zoom}`); }); it('should not include a width if not set', () => { const cldImage = cld.image(TEST_PUBLIC_ID); const options = {}; - plugin({ cldAsset: cldImage, options }); - expect(cldImage.toURL()).toContain(`image/upload/${TEST_PUBLIC_ID}`); + const { options: { resize } } = plugin({ cldAsset: cldImage, options }); + expect(resize).toBeUndefined(); }); - it('should return resize override with original size in URL if resize is smaller than width', () => { + it('should return resize override width a base width', () => { const cldImage = cld.image(TEST_PUBLIC_ID); const options = { + baseWidth: 600, width: 900, - widthResize: 600 }; - const { options: pluginOptions } = plugin({ cldAsset: cldImage, options }); + const { options: { resize } } = plugin({ cldAsset: cldImage, options }); - expect(cldImage.toURL()).toContain(`image/upload/c_limit,w_${options.width}/${TEST_PUBLIC_ID}`); - expect(pluginOptions).toMatchObject({ - width: options.widthResize - }) + expect(resize).toContain(`c_limit,w_${options.width}`); + expect(cldImage.toURL()).toContain(`image/upload/c_limit,w_${options.baseWidth}`); }); - it('should return resize override with original size in URL if resize is larger than width', () => { + it('should return resize override width a base width', () => { const cldImage = cld.image(TEST_PUBLIC_ID); const options = { - width: 900, - widthResize: 1200 - }; - - const { options: pluginOptions } = plugin({ cldAsset: cldImage, options }); - - expect(cldImage.toURL()).toContain(`image/upload/c_limit,w_${options.width}/${TEST_PUBLIC_ID}`); - expect(pluginOptions).toMatchObject({ - width: options.widthResize - }) - }); - - it('should return resize override with original size in URL if resize is the same as width', () => { - const cldImage = cld.image(TEST_PUBLIC_ID); - const options = { - width: 900, - widthResize: 900 + baseCrop: 'auto', + baseHeight: 8765, + baseWidth: 4321, + baseZoom: 3, + crop: 'fill', + height: 5678, + width: 1234, + zoom: 2, }; - const { options: pluginOptions } = plugin({ cldAsset: cldImage, options }); + const { options: { resize } } = plugin({ cldAsset: cldImage, options }); - expect(cldImage.toURL()).toContain(`image/upload/c_limit,w_${options.width}/${TEST_PUBLIC_ID}`); - expect(pluginOptions).toMatchObject({ - width: options.widthResize - }) + expect(resize).toContain(`c_${options.crop},w_${options.width},h_${options.height},g_auto,z_${options.zoom}`); + expect(cldImage.toURL()).toContain(`image/upload/c_${options.baseCrop},w_${options.baseWidth},h_${options.baseHeight},g_auto,z_${options.baseZoom}`); }); it('should not apply a height when crop is fill if isnt set', () => { @@ -107,8 +94,8 @@ describe('Cropping plugin', () => { width: 100, crop: 'fill', }; - plugin({ cldAsset: cldImage, options }); - expect(cldImage.toURL()).toContain(`c_${options.crop},w_${options.width},g_auto/${TEST_PUBLIC_ID}`); + const { options: { resize } } = plugin({ cldAsset: cldImage, options }); + expect(resize).toContain(`c_${options.crop},w_${options.width},g_auto`); }); it('should apply aspect ratio as a string and valid crop', () => { @@ -117,8 +104,8 @@ describe('Cropping plugin', () => { aspectRatio: '16:9', crop: 'fill' }; - plugin({ cldAsset: cldImage, options }); - expect(cldImage.toURL()).toContain(`c_fill,ar_16:9,g_auto/${TEST_PUBLIC_ID}`); + const { options: { resize } } = plugin({ cldAsset: cldImage, options }); + expect(resize).toContain(`c_fill,ar_16:9,g_auto`); }); it('should apply aspect ratio as a float and valid crop', () => { @@ -127,19 +114,19 @@ describe('Cropping plugin', () => { aspectRatio: .5, crop: 'fill' }; - plugin({ cldAsset: cldImage, options }); + const { options: { resize } } = plugin({ cldAsset: cldImage, options }); // for my own sanity that float => string will add the leading 0 expect(`${options.aspectRatio}`).toMatch('0.5'); - expect(cldImage.toURL()).toContain(`c_fill,ar_${options.aspectRatio},g_auto/${TEST_PUBLIC_ID}`); + expect(resize).toContain(`c_fill,ar_${options.aspectRatio},g_auto`); }); it('should not apply aspect ratio if not a valid crop', () => { const cldImage = cld.image(TEST_PUBLIC_ID); const options = { aspectRatio: '16:9', + crop: 'fit' }; - plugin({ cldAsset: cldImage, options }); - expect(cldImage.toURL()).not.toContain(`ar_16:9`); + const { options: { resize } } = plugin({ cldAsset: cldImage, options }); + expect(resize).toBeUndefined(); }); - }); diff --git a/packages/url-loader/tests/plugins/fill-background.spec.js b/packages/url-loader/tests/plugins/fill-background.spec.js index 0094a4a..1dc57d1 100644 --- a/packages/url-loader/tests/plugins/fill-background.spec.js +++ b/packages/url-loader/tests/plugins/fill-background.spec.js @@ -16,7 +16,6 @@ const TEST_PUBLIC_ID = 'test-public-id'; describe('Plugins', () => { describe('Fill Background', () => { it('should generate a background with basic settings', () => { - const cldImage = cld.image(TEST_PUBLIC_ID); const options = { @@ -30,11 +29,10 @@ describe('Plugins', () => { options }); - expect(cldImage.toURL()).toContain(`b_gen_fill,ar_${options.width}:${options.height},w_${options.width},c_pad/${TEST_PUBLIC_ID}`); + expect(cldImage.toURL()).toContain(`b_gen_fill,ar_${options.width}:${options.height},c_pad/${TEST_PUBLIC_ID}`); }); it('should generate with custom options', () => { - const cldImage = cld.image(TEST_PUBLIC_ID); const options = { @@ -52,7 +50,7 @@ describe('Plugins', () => { options }); - expect(cldImage.toURL()).toContain(`b_gen_fill:${encodeURIComponent(options.fillBackground.prompt)},ar_${options.width}:${options.height},w_${options.width},c_${options.fillBackground.crop},g_${options.fillBackground.gravity}/${TEST_PUBLIC_ID}`); + expect(cldImage.toURL()).toContain(`b_gen_fill:${encodeURIComponent(options.fillBackground.prompt)},ar_${options.width}:${options.height},c_${options.fillBackground.crop},g_${options.fillBackground.gravity}/${TEST_PUBLIC_ID}`); }); }); });