Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading