Skip to content

Commit

Permalink
fix(cloud-image-editor): ignore unsupported cdn operations and print …
Browse files Browse the repository at this point in the history
…console warning (#587)

* fix(cloud-image-editor): ignore unsupported cdn operations and print console warning

* chore: rename OPERATIONS_DEFAULTS to OPERATIONS_ZEROS

---------

Co-authored-by: nd0ut <[email protected]>
  • Loading branch information
nd0ut and nd0ut authored Jan 18, 2024
1 parent 9b94aae commit 503eaae
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 63 deletions.
129 changes: 80 additions & 49 deletions blocks/CloudImageEditor/src/lib/transformationUtils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// @ts-check
import { joinCdnOperations } from '../../../../utils/cdn-utils.js';
import { stringToArray } from '../../../../utils/stringToArray.js';

export const OPERATIONS_ZEROS = {
export const OPERATIONS_ZEROS = Object.freeze({
brightness: 0,
exposure: 0,
gamma: 100,
Expand All @@ -12,61 +13,62 @@ export const OPERATIONS_ZEROS = {
enhance: 0,
filter: 0,
rotate: 0,
};
mirror: false,
});

const SUPPORTED_OPERATIONS_ORDERED = /** @type {const} */ ([
'enhance',
'brightness',
'exposure',
'gamma',
'contrast',
'saturation',
'vibrance',
'warmth',
'filter',
'mirror',
'flip',
'rotate',
'crop',
]);

/**
* @param {String} operation
* @param {Number | String | object} options
* @returns {String}
* @template {keyof import('../types').Transformations} T
* @param {T} operation
* @param {import('../types').Transformations[T]} options
*/
function transformationToStr(operation, options) {
if (typeof options === 'number') {
return OPERATIONS_ZEROS[operation] !== options ? `${operation}/${options}` : '';
const value = options;
return OPERATIONS_ZEROS[/** @type {keyof typeof OPERATIONS_ZEROS} */ (operation)] !== value
? `${operation}/${value}`
: '';
}

if (typeof options === 'boolean') {
return options && OPERATIONS_ZEROS[operation] !== options ? `${operation}` : '';
const value = options;
return OPERATIONS_ZEROS[/** @type {keyof typeof OPERATIONS_ZEROS} */ (operation)] !== value ? `${operation}` : '';
}

if (operation === 'filter') {
if (!options || OPERATIONS_ZEROS[operation] === options.amount) {
if (operation === 'filter' && options) {
const { name, amount } = /** @type {NonNullable<import('../types').Transformations['filter']>} */ (options);
if (OPERATIONS_ZEROS.filter === amount) {
return '';
}
let { name, amount } = options;
return `${operation}/${name}/${amount}`;
}

if (operation === 'crop') {
if (!options) {
return '';
}
let { dimensions, coords } = options;
if (operation === 'crop' && options) {
let { dimensions, coords } = /** @type {NonNullable<import('../types').Transformations['crop']>} */ (options);
return `${operation}/${dimensions.join('x')}/${coords.join(',')}`;
}

return '';
}

// TODO: refactor all the operations constants
const SUPPORTED_OPERATIONS_ORDERED = [
'enhance',
'brightness',
'exposure',
'gamma',
'contrast',
'saturation',
'vibrance',
'warmth',
'filter',
'mirror',
'flip',
'rotate',
'crop',
];

/**
* @param {import('../types').Transformations} transformations
* @returns {String}
* @returns {string}
*/
export function transformationsToOperations(transformations) {
return joinCdnOperations(
Expand All @@ -83,22 +85,39 @@ export function transformationsToOperations(transformations) {

export const COMMON_OPERATIONS = joinCdnOperations('format/auto', 'progressive/yes');

/** @param {[unknown]} arg */
const asNumber = ([value]) => (typeof value !== 'undefined' ? Number(value) : undefined);
const asBoolean = () => true;
/** @param {[string, unknown]} arg */
const asFilter = ([name, amount]) => ({
name,
amount: typeof amount !== 'undefined' ? Number(amount) : 100,
});

// Docs: https://uploadcare.com/docs/transformations/image/resize-crop/#operation-crop
// We don't support percentages and aligment presets,
// Because it's unclear how to handle them in the Editor UI
// TODO: add support for percentages and aligment presets
const asCrop = ([dimensions, coords]) => {
return { dimensions: stringToArray(dimensions, 'x').map(Number), coords: stringToArray(coords).map(Number) };
/**
* Docs: https://uploadcare.com/docs/transformations/image/resize-crop/#operation-crop We don't support percentages and
* alignment presets, Because it's unclear how to handle them in the Editor UI TODO: add support for percentages and
* alignment presets
*
* @param {[string, string]} arg
*/
const asCrop = ([dimensions, alignment]) => {
if (!/\d+x\d+/.test(dimensions) || !/\d+,\d+/.test(alignment)) {
throw new Error('Crop by aspect ratio, percentage or alignment shortcuts is not supported.');
}

return /** @type {{ dimensions: [number, number]; coords: [number, number] }} */ ({
dimensions: stringToArray(dimensions, 'x').map(Number),
coords: stringToArray(alignment).map(Number),
});
};

const OPERATION_PROCESSORS = {
/**
* @type {{
* [K in keyof Required<import('../types').Transformations>]: (args: any) => import('../types').Transformations[K];
* }}
*/
const OPERATION_PROCESSORS = Object.freeze({
enhance: asNumber,
brightness: asNumber,
exposure: asNumber,
Expand All @@ -112,23 +131,35 @@ const OPERATION_PROCESSORS = {
flip: asBoolean,
rotate: asNumber,
crop: asCrop,
};
});

/**
* @param {string[]} operations
* @returns {import('../types.js').Transformations}
*/
export function operationsToTransformations(operations) {
/** @type {import('../types.js').Transformations} */
let transformations = {};
for (let operation of operations) {
let [name, ...args] = operation.split('/');
/** @type {Record<string, unknown>} */
const transformations = {};
for (const operation of operations) {
const [name, ...args] = operation.split('/');
if (!SUPPORTED_OPERATIONS_ORDERED.includes(name)) {
continue;
}
const processor = OPERATION_PROCESSORS[name];
const value = processor(args);
transformations[name] = value;
const operationName = /** @type {(typeof SUPPORTED_OPERATIONS_ORDERED)[number]} */ (name);
const processor = OPERATION_PROCESSORS[operationName];
try {
const value = processor(args);
transformations[operationName] = value;
} catch (err) {
console.warn(
[
`Failed to parse URL operation "${operation}". It will be ignored.`,
err instanceof Error ? `Error message: "${err.message}"` : err,
'If you need this functionality, please feel free to open an issue at https://github.com/uploadcare/blocks/issues/new',
].join('\n')
);
}
}
return transformations;

return /** @type {import('../types.js').Transformations} */ (transformations);
}
29 changes: 15 additions & 14 deletions blocks/CloudImageEditor/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,21 @@
*/

/**
* @typedef {Object} Transformations
* @property {number} [enhance]
* @property {number} [brightness]
* @property {number} [exposure]
* @property {number} [gamma]
* @property {number} [contrast]
* @property {number} [saturation]
* @property {number} [vibrance]
* @property {number} [warmth]
* @property {number} [rotate]
* @property {boolean} [mirror]
* @property {boolean} [flip]
* @property {{ name: string; amount: number }} [filter]
* @property {{ dimensions: [number, number]; coords: [number, number] }} [crop]
* @typedef {{
* enhance?: number;
* brightness?: number;
* exposure?: number;
* gamma?: number;
* contrast?: number;
* saturation?: number;
* vibrance?: number;
* warmth?: number;
* rotate?: number;
* mirror?: boolean;
* flip?: boolean;
* filter?: { name: string; amount: number };
* crop?: { dimensions: [number, number]; coords: [number, number] };
* }} Transformations
*/

/**
Expand Down

0 comments on commit 503eaae

Please sign in to comment.