From 35589e0a089ece6e044f4a66615a8edee3468061 Mon Sep 17 00:00:00 2001 From: Lloyd Wilson Date: Fri, 6 Dec 2019 15:14:11 +0000 Subject: [PATCH] Apply PR 130 from main repo --- source/image-handler/image-handler.js | 270 +++++++++--------- source/image-handler/image-request.js | 22 +- source/image-handler/package.json | 2 +- .../test/test-thumbor-mapping.js | 113 ++++---- source/image-handler/thumbor-mapping.js | 76 ++--- 5 files changed, 247 insertions(+), 236 deletions(-) diff --git a/source/image-handler/image-handler.js b/source/image-handler/image-handler.js index 4b4b15c59..c87b4ae43 100644 --- a/source/image-handler/image-handler.js +++ b/source/image-handler/image-handler.js @@ -1,65 +1,65 @@ -/********************************************************************************************************************* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * - * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * - * with the License. A copy of the License is located at * - * * - * http://aws.amazon.com/asl/ * - * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * - * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * - * and limitations under the License. * - *********************************************************************************************************************/ - -const AWS = require('aws-sdk'); -const sharp = require('sharp'); - -class ImageHandler { - - /** - * Main method for processing image requests and outputting modified images. - * @param {ImageRequest} request - An ImageRequest object. - */ - async process(request) { - const originalImage = request.originalImage; - const edits = request.edits; - if (edits !== undefined) { - const modifiedImage = await this.applyEdits(originalImage, edits); - if (request.outputFormat !== undefined) { - await modifiedImage.toFormat(request.outputFormat); - } - const bufferImage = await modifiedImage.toBuffer(); - return bufferImage.toString('base64'); - } else { - return originalImage.toString('base64'); - } - } - - /** - * Applies image modifications to the original image based on edits - * specified in the ImageRequest. - * @param {Buffer} originalImage - The original image. - * @param {Object} edits - The edits to be made to the original image. - */ - async applyEdits(originalImage, edits) { - const image = sharp(originalImage); - const keys = Object.keys(edits); - const values = Object.values(edits); - // Apply the image edits - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; - if (key === 'overlayWith') { - const overlay = await this.getOverlayImage(value.bucket, value.key); - image.overlayWith(overlay, value.options); - } else if (key === 'smartCrop') { +/********************************************************************************************************************* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * with the License. A copy of the License is located at * + * * + * http://aws.amazon.com/asl/ * + * * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * + * and limitations under the License. * + *********************************************************************************************************************/ + +const AWS = require('aws-sdk'); +const sharp = require('sharp'); + +class ImageHandler { + + /** + * Main method for processing image requests and outputting modified images. + * @param {ImageRequest} request - An ImageRequest object. + */ + async process(request) { + const originalImage = request.originalImage; + const edits = request.edits; + if (edits !== undefined) { + const modifiedImage = await this.applyEdits(originalImage, edits); + if (request.outputFormat !== undefined) { + await modifiedImage.toFormat(request.outputFormat); + } + const bufferImage = await modifiedImage.toBuffer(); + return bufferImage.toString('base64'); + } else { + return originalImage.toString('base64'); + } + } + + /** + * Applies image modifications to the original image based on edits + * specified in the ImageRequest. + * @param {Buffer} originalImage - The original image. + * @param {Object} edits - The edits to be made to the original image. + */ + async applyEdits(originalImage, edits) { + const image = sharp(originalImage, { failOnError: false }).rotate(); + const keys = Object.keys(edits); + const values = Object.values(edits); + // Apply the image edits + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + if (key === 'overlayWith') { + const overlay = await this.getOverlayImage(value.bucket, value.key); + image.overlayWith(overlay, value.options); + } else if (key === 'smartCrop') { const options = value; const imageBuffer = await image.toBuffer(); const metadata = await image.metadata(); // ---- - const boundingBox = await this.getBoundingBox(imageBuffer, options.faceIndex); - const cropArea = await this.getCropArea(boundingBox, options, metadata); - try { image.extract(cropArea) } + const boundingBox = await this.getBoundingBox(imageBuffer, options.faceIndex); + const cropArea = await this.getCropArea(boundingBox, options, metadata); + try { image.extract(cropArea) } catch (err) { throw ({ status: 400, @@ -67,94 +67,94 @@ class ImageHandler { message: 'The padding value you provided exceeds the boundaries of the original image. Please try choosing a smaller value or applying padding via Sharp for greater specificity.' }); } - } else { - image[key](value); - } - } - // Return the modified image - return image; - } - - /** - * Gets an image to be used as an overlay to the primary image from an - * Amazon S3 bucket. - * @param {string} bucket - The name of the bucket containing the overlay. - * @param {string} key - The keyname corresponding to the overlay. - */ - async getOverlayImage(bucket, key) { - const s3 = new AWS.S3(); - const params = { Bucket: bucket, Key: key }; - // Request - const request = s3.getObject(params).promise(); - // Response handling - try { - const overlayImage = await request; - return Promise.resolve(overlayImage.Body); - } catch (err) { - return Promise.reject({ - status: 500, - code: err.code, - message: err.message - }) - } - } - - /** + } else { + image[key](value); + } + } + // Return the modified image + return image; + } + + /** + * Gets an image to be used as an overlay to the primary image from an + * Amazon S3 bucket. + * @param {string} bucket - The name of the bucket containing the overlay. + * @param {string} key - The keyname corresponding to the overlay. + */ + async getOverlayImage(bucket, key) { + const s3 = new AWS.S3(); + const params = { Bucket: bucket, Key: key }; + // Request + const request = s3.getObject(params).promise(); + // Response handling + try { + const overlayImage = await request; + return Promise.resolve(overlayImage.Body); + } catch (err) { + return Promise.reject({ + status: 500, + code: err.code, + message: err.message + }) + } + } + + /** * Calculates the crop area for a smart-cropped image based on the bounding * box data returned by Amazon Rekognition, as well as padding options and * the image metadata. - * @param {Object} boundingBox - The boudning box of the detected face. - * @param {Object} options - Set of options for smart cropping. + * @param {Object} boundingBox - The boudning box of the detected face. + * @param {Object} options - Set of options for smart cropping. * @param {Object} metadata - Sharp image metadata. - */ - getCropArea(boundingBox, options, metadata) { + */ + getCropArea(boundingBox, options, metadata) { const padding = (options.padding !== undefined) ? parseFloat(options.padding) : 0; - // Calculate the smart crop area - const cropArea = { - left : parseInt((boundingBox.Left*metadata.width)-padding), - top : parseInt((boundingBox.Top*metadata.height)-padding), - width : parseInt((boundingBox.Width*metadata.width)+(padding*2)), - height : parseInt((boundingBox.Height*metadata.height)+(padding*2)), - } - // Return the crop area - return cropArea; - } - - /** - * Gets the bounding box of the specified face index within an image, if specified. - * @param {Sharp} imageBuffer - The original image. - * @param {Integer} faceIndex - The zero-based face index value, moving from 0 and up as - * confidence decreases for detected faces within the image. - */ - async getBoundingBox(imageBuffer, faceIndex) { - const rekognition = new AWS.Rekognition(); - const params = { Image: { Bytes: imageBuffer }}; + // Calculate the smart crop area + const cropArea = { + left : parseInt((boundingBox.Left*metadata.width)-padding), + top : parseInt((boundingBox.Top*metadata.height)-padding), + width : parseInt((boundingBox.Width*metadata.width)+(padding*2)), + height : parseInt((boundingBox.Height*metadata.height)+(padding*2)), + } + // Return the crop area + return cropArea; + } + + /** + * Gets the bounding box of the specified face index within an image, if specified. + * @param {Sharp} imageBuffer - The original image. + * @param {Integer} faceIndex - The zero-based face index value, moving from 0 and up as + * confidence decreases for detected faces within the image. + */ + async getBoundingBox(imageBuffer, faceIndex) { + const rekognition = new AWS.Rekognition(); + const params = { Image: { Bytes: imageBuffer }}; const faceIdx = (faceIndex !== undefined) ? faceIndex : 0; - // Request - const request = rekognition.detectFaces(params).promise(); - // Response handling - try { + // Request + const request = rekognition.detectFaces(params).promise(); + // Response handling + try { const response = (await request).FaceDetails[faceIdx].BoundingBox; - return Promise.resolve(await response); - } catch (err) { - console.log(err); + return Promise.resolve(await response); + } catch (err) { + console.log(err); if (err.message === "Cannot read property 'BoundingBox' of undefined") { - return Promise.reject({ + return Promise.reject({ status: 400, code: 'SmartCrop::FaceIndexOutOfRange', message: 'You have provided a FaceIndex value that exceeds the length of the zero-based detectedFaces array. Please specify a value that is in-range.' - }) + }) } else { - return Promise.reject({ - status: 500, - code: err.code, - message: err.message - }) + return Promise.reject({ + status: 500, + code: err.code, + message: err.message + }) } - } - } -} - -// Exports -module.exports = ImageHandler; + } + } +} + +// Exports +module.exports = ImageHandler; diff --git a/source/image-handler/image-request.js b/source/image-handler/image-request.js index 75b7da7e7..fccf40532 100644 --- a/source/image-handler/image-request.js +++ b/source/image-handler/image-request.js @@ -14,7 +14,7 @@ const ThumborMapping = require('./thumbor-mapping'); class ImageRequest { - + /** * Initializer function for creating a new image request, used by the image * handler to perform image modifications. @@ -136,9 +136,7 @@ class ImageRequest { const decoded = this.decodeRequest(event); return decoded.key; } else if (requestType === "Thumbor" || requestType === "Custom") { - // Parse the key from the end of the path - const key = (event["path"]).split("/"); - return key[key.length - 1]; + return decodeURIComponent(event["path"].replace(/\d+x\d+\/|filters[:-][^/;]+|\/fit-in\/+|^\/+/g, '').replace(/^\/+/, '')); } else { // Return an error for all other conditions throw ({ @@ -151,7 +149,7 @@ class ImageRequest { /** * Determines how to handle the request being made based on the URL path - * prefix to the image request. Categorizes a request as either "image" + * prefix to the image request. Categorizes a request as either "image" * (uses the Sharp library), "thumbor" (uses Thumbor mapping), or "custom" * (uses the rewrite function). * @param {Object} event - Lambda request body. @@ -160,12 +158,12 @@ class ImageRequest { const path = event["path"]; // ---- const matchDefault = new RegExp(/^(\/?)([0-9a-zA-Z+\/]{4})*(([0-9a-zA-Z+\/]{2}==)|([0-9a-zA-Z+\/]{3}=))?$/); - const matchThumbor = new RegExp(/^(\/?)((fit-in)?|(filters:.+\(.?\))?|(unsafe)?).*(.+jpg|.+png|.+webp|.+tiff|.+jpeg)$/); - const matchCustom = new RegExp(/(\/?)(.*)(jpg|png|webp|tiff|jpeg)/); + const matchThumbor = new RegExp(/^(\/?)((fit-in)?|(filters:.+\(.?\))?|(unsafe)?).*(.+jpg|.+png|.+webp|.+tiff|.+jpeg)$/i); + const matchCustom = new RegExp(/(\/?)(.*)(jpg|png|webp|tiff|jpeg)/i); const definedEnvironmentVariables = ( - (process.env.REWRITE_MATCH_PATTERN !== "") && - (process.env.REWRITE_SUBSTITUTION !== "") && - (process.env.REWRITE_MATCH_PATTERN !== undefined) && + (process.env.REWRITE_MATCH_PATTERN !== "") && + (process.env.REWRITE_SUBSTITUTION !== "") && + (process.env.REWRITE_MATCH_PATTERN !== undefined) && (process.env.REWRITE_SUBSTITUTION !== undefined) ); // ---- @@ -214,7 +212,7 @@ class ImageRequest { } /** - * Returns a formatted image source bucket whitelist as specified in the + * Returns a formatted image source bucket whitelist as specified in the * SOURCE_BUCKETS environment variable of the image handler Lambda * function. Provides error handling for missing/invalid values. */ @@ -235,4 +233,4 @@ class ImageRequest { } // Exports -module.exports = ImageRequest; \ No newline at end of file +module.exports = ImageRequest; diff --git a/source/image-handler/package.json b/source/image-handler/package.json index 3dee6a793..8f3ca5d16 100644 --- a/source/image-handler/package.json +++ b/source/image-handler/package.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "mocha": "^6.1.4", - "sharp": "^0.21.3", + "sharp": "^0.22.1", "sinon": "^7.3.2", "nyc": "^14.0.0" }, diff --git a/source/image-handler/test/test-thumbor-mapping.js b/source/image-handler/test/test-thumbor-mapping.js index 0968fd3ea..15392ae26 100644 --- a/source/image-handler/test/test-thumbor-mapping.js +++ b/source/image-handler/test/test-thumbor-mapping.js @@ -19,7 +19,7 @@ let assert = require('assert'); // ---------------------------------------------------------------------------- describe('process()', function() { describe('001/thumborRequest', function() { - it(`Should pass if the proper edit translations are applied and in the + it(`Should pass if the proper edit translations are applied and in the correct order`, function() { // Arrange const event = { @@ -30,12 +30,13 @@ describe('process()', function() { thumborMapping.process(event); // Assert const expectedResult = { - edits: { + edits: { + grayscale: true, resize: { width: 200, - height: 300 + height: 300, + fit: 'inside' }, - grayscale: true } }; assert.deepEqual(thumborMapping.edits, expectedResult.edits); @@ -48,7 +49,7 @@ describe('process()', function() { // ---------------------------------------------------------------------------- describe('parseCustomPath()', function() { describe('001/validPath', function() { - it(`Should pass if the proper edit translations are applied and in the + it(`Should pass if the proper edit translations are applied and in the correct order`, function() { const event = { path : '/filters-rotate(90)/filters-grayscale()/thumbor-image.jpg' @@ -111,7 +112,7 @@ describe('parseCustomPath()', function() { // ---------------------------------------------------------------------------- describe('mapFilter()', function() { describe('001/autojpg', function() { - it(`Should pass if the filter is successfully converted from + it(`Should pass if the filter is successfully converted from Thumbor:autojpg()`, function() { // Arrange const edit = 'filters:autojpg()'; @@ -127,7 +128,7 @@ describe('mapFilter()', function() { }); }); describe('002/background_color', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:background_color()`, function() { // Arrange const edit = 'filters:background_color(#ffff)'; @@ -143,7 +144,7 @@ describe('mapFilter()', function() { }); }); describe('003/blur/singleParameter', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:blur()`, function() { // Arrange const edit = 'filters:blur(60)'; @@ -159,7 +160,7 @@ describe('mapFilter()', function() { }); }); describe('004/blur/doubleParameter', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:blur()`, function() { // Arrange const edit = 'filters:blur(60, 2)'; @@ -175,7 +176,7 @@ describe('mapFilter()', function() { }); }); describe('005/convolution', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:convolution()`, function() { // Arrange const edit = 'filters:convolution(1;2;1;2;4;2;1;2;1,3,true)'; @@ -187,7 +188,7 @@ describe('mapFilter()', function() { const expectedResult = { edits: { convolve: { width: 3, - height: 3, + height: 3, kernel: [1,2,1,2,4,2,1,2,1] }} }; @@ -195,7 +196,7 @@ describe('mapFilter()', function() { }); }); describe('006/equalize', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:equalize()`, function() { // Arrange const edit = 'filters:equalize()'; @@ -211,7 +212,7 @@ describe('mapFilter()', function() { }); }); describe('007/fill/resizeUndefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:fill()`, function() { // Arrange const edit = 'filters:fill(#fff)'; @@ -228,7 +229,7 @@ describe('mapFilter()', function() { }); describe('008/fill/resizeDefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:fill()`, function() { // Arrange const edit = 'filters:fill(#fff)'; @@ -245,7 +246,7 @@ describe('mapFilter()', function() { }); }); describe('009/format/supportedFileType', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:format()`, function() { // Arrange const edit = 'filters:format(png)'; @@ -277,7 +278,7 @@ describe('mapFilter()', function() { }); }); describe('011/no_upscale/resizeUndefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:no_upscale()`, function() { // Arrange const edit = 'filters:no_upscale()'; @@ -287,7 +288,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'inside', height: undefined, @@ -299,7 +300,7 @@ describe('mapFilter()', function() { }); }); describe('012/no_upscale/resizeDefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:no_upscale()`, function() { // Arrange const edit = 'filters:no_upscale()'; @@ -310,7 +311,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'inside', height: undefined, @@ -322,7 +323,7 @@ describe('mapFilter()', function() { }); }); describe('013/proportion/resizeDefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:proportion()`, function() { // Arrange const edit = 'filters:proportion(0.3)'; @@ -338,7 +339,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { height: 60, width: 60 @@ -349,7 +350,7 @@ describe('mapFilter()', function() { }); }); describe('014/proportion/resizeUndefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:resize()`, function() { // Arrange const edit = 'filters:proportion(0.3)'; @@ -364,7 +365,7 @@ describe('mapFilter()', function() { }); }); describe('015/quality/jpg', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:quality()`, function() { // Arrange const edit = 'filters:quality(50)'; @@ -374,7 +375,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { jpeg: { quality: 50 } @@ -384,7 +385,7 @@ describe('mapFilter()', function() { }); }); describe('016/quality/png', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:quality()`, function() { // Arrange const edit = 'filters:quality(50)'; @@ -394,7 +395,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { png: { quality: 50 } @@ -404,7 +405,7 @@ describe('mapFilter()', function() { }); }); describe('017/quality/webp', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:quality()`, function() { // Arrange const edit = 'filters:quality(50)'; @@ -414,7 +415,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { webp: { quality: 50 } @@ -424,7 +425,7 @@ describe('mapFilter()', function() { }); }); describe('018/quality/tiff', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:quality()`, function() { // Arrange const edit = 'filters:quality(50)'; @@ -434,7 +435,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { tiff: { quality: 50 } @@ -460,7 +461,7 @@ describe('mapFilter()', function() { }); }); describe('020/rgb', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:rgb()`, function() { // Arrange const edit = 'filters:rgb(10, 10, 10)'; @@ -470,7 +471,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { tint: { r: 25.5, g: 25.5, @@ -482,7 +483,7 @@ describe('mapFilter()', function() { }); }); describe('021/rotate', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:rotate()`, function() { // Arrange const edit = 'filters:rotate(75)'; @@ -492,7 +493,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { rotate: 75 } }; @@ -500,7 +501,7 @@ describe('mapFilter()', function() { }); }); describe('022/sharpen', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:sharpen()`, function() { // Arrange const edit = 'filters:sharpen(75, 5)'; @@ -510,7 +511,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { sharpen: 3.5 } }; @@ -518,7 +519,7 @@ describe('mapFilter()', function() { }); }); describe('023/stretch/default', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:stretch()`, function() { // Arrange const edit = 'filters:stretch()'; @@ -528,7 +529,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'fill' } } }; @@ -536,7 +537,7 @@ describe('mapFilter()', function() { }); }); describe('024/stretch/resizeDefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:stretch()`, function() { // Arrange const edit = 'filters:stretch()'; @@ -547,7 +548,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'fill' } } }; @@ -555,7 +556,7 @@ describe('mapFilter()', function() { }); }); describe('025/stretch/sizingMethodUndefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:stretch()`, function() { // Arrange const edit = 'filters:stretch()'; @@ -567,7 +568,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'fill' } }, sizingMethod: undefined @@ -576,7 +577,7 @@ describe('mapFilter()', function() { }); }); describe('026/stretch/sizingMethodNotFitIn', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:stretch()`, function() { // Arrange const edit = 'filters:stretch()'; @@ -588,7 +589,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'fill' } }, sizingMethod: "cover" @@ -597,7 +598,7 @@ describe('mapFilter()', function() { }); }); describe('027/stretch/sizingMethodFitIn', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:stretch()`, function() { // Arrange const edit = 'filters:stretch()'; @@ -609,7 +610,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: {} }, sizingMethod: "fit-in" @@ -618,7 +619,7 @@ describe('mapFilter()', function() { }); }); describe('028/strip_exif', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:strip_exif()`, function() { // Arrange const edit = 'filters:strip_exif()'; @@ -628,7 +629,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { rotate: 0 } }; @@ -636,7 +637,7 @@ describe('mapFilter()', function() { }); }); describe('029/strip_icc', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:strip_icc()`, function() { // Arrange const edit = 'filters:strip_icc()'; @@ -646,7 +647,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { rotate: 0 } }; @@ -654,7 +655,7 @@ describe('mapFilter()', function() { }); }); describe('030/upscale', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:upscale()`, function() { // Arrange const edit = 'filters:upscale()'; @@ -664,7 +665,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'inside' } @@ -674,7 +675,7 @@ describe('mapFilter()', function() { }); }); describe('031/upscale/resizeNotUndefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:upscale()`, function() { // Arrange const edit = 'filters:upscale()'; @@ -685,7 +686,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'inside' } @@ -695,7 +696,7 @@ describe('mapFilter()', function() { }); }); describe('032/elseCondition', function() { - it(`Should pass if undefined is returned for an unsupported filter`, + it(`Should pass if undefined is returned for an unsupported filter`, function() { // Arrange const edit = 'filters:notSupportedFilter()'; @@ -708,4 +709,4 @@ describe('mapFilter()', function() { assert.deepEqual(thumborMapping, expectedResult); }); }); -}) \ No newline at end of file +}) diff --git a/source/image-handler/thumbor-mapping.js b/source/image-handler/thumbor-mapping.js index dc9e8a76c..60258c9be 100644 --- a/source/image-handler/thumbor-mapping.js +++ b/source/image-handler/thumbor-mapping.js @@ -18,7 +18,7 @@ class ThumborMapping { this.edits = {}; this.sizingMethod; } - + /** * Initializer function for creating a new Thumbor mapping, used by the image * handler to perform image modifications based on legacy URL path requests. @@ -29,26 +29,38 @@ class ThumborMapping { this.path = event.path; const edits = this.path.split('/'); const filetype = (this.path.split('.'))[(this.path.split('.')).length - 1]; + + //Process the Dimensions + const dimPath = this.path.match(/[^\/]\d+x\d+/g); + if (dimPath) { + const dims = dimPath[0].split('x'); + // Set only if the dimensions provided are valid resize arguments + if (isNaN(dims[0]) == false && isNaN(dims[1]) == false) { + this.edits.resize = {}; + // Assign dimenions from the first match only to avoid parsing dimension from image file names + this.edits.resize.width = Number(dims[0]); + this.edits.resize.height = Number(dims[1]); + + } + } + // Parse the image path for (let i = 0; i < edits.length; i++) { const edit = edits[i]; if (edit === ('fit-in')) { - this.edits.resize = {}; + if (this.edits.resize === undefined) { + this.edits.resize = {}; + } + this.edits.resize.fit = 'inside' this.sizingMethod = edit; - } - else if (edit.includes('x')) { - this.edits.resize = {}; - const dims = edit.split('x'); - this.edits.resize.width = Number(dims[0]); - this.edits.resize.height = Number(dims[1]); - } - if (edit.includes('filters:')) { + } + else if (edit.includes('filters:')) { this.mapFilter(edit, filetype); } } return this; } - + /** * Enables users to migrate their current image request model to the SIH solution, * without changing their legacy application code to accomodate new image requests. @@ -81,14 +93,14 @@ class ThumborMapping { // Find the proper filter if (key === ('autojpg')) { this.edits.toFormat = 'jpg'; - } + } else if (key === ('background_color')) { this.edits.flatten = { background: value }; - } + } else if (key === ('blur')) { const val = value.split(','); this.edits.blur = (val.length > 1) ? Number(val[1]) : Number(val[0]) / 2; - } + } else if (key === ('convolution')) { const arr = value.split(','); const strMatrix = (arr[0]).split(';'); @@ -112,26 +124,26 @@ class ThumborMapping { height: Number(matrixHeight), kernel: matrix } - } + } else if (key === ('equalize')) { this.edits.normalize = "true"; - } + } else if (key === ('fill')) { if (this.edits.resize === undefined) { this.edits.resize = {}; } this.edits.resize.background = value; - } + } else if (key === ('format')) { const formattedValue = value.replace(/[^0-9a-z]/gi, ''); const acceptedValues = ['jpeg', 'gif', 'jpg', 'webp', 'png']; if (acceptedValues.includes(formattedValue)) { this.edits.toFormat = formattedValue; } - } + } else if (key === ('grayscale')) { this.edits.grayscale = true; - } + } else if (key === ('no_upscale')) { if (this.edits.resize === undefined) { this.edits.resize = {}; @@ -139,7 +151,7 @@ class ThumborMapping { this.edits.resize.fit = "inside" this.edits.resize.width = undefined; this.edits.resize.height = undefined; - } + } else if (key === ('proportion')) { if (this.edits.resize === undefined) { this.edits.resize = {}; @@ -147,7 +159,7 @@ class ThumborMapping { const prop = Number(value); this.edits.resize.width = Number(this.edits.resize.width * prop); this.edits.resize.height = Number(this.edits.resize.height * prop); - } + } else if (key === ('quality')) { if (filetype === 'jpg') { this.edits.jpeg = { quality: Number(value) } @@ -158,7 +170,7 @@ class ThumborMapping { } else if (filetype === 'tiff') { this.edits.tiff = { quality: Number(value) } } - } + } else if (key === ('rgb')) { const percentages = value.split(','); const values = []; @@ -170,33 +182,33 @@ class ThumborMapping { this.edits.tint = { r: values[0], g: values[1], b: values[2] }; } else if (key === ('rotate')) { - this.edits.rotate = Number(value); - } + this.edits.rotate = Number(value); + } else if (key === ('sharpen')) { const sh = value.split(','); const sigma = 1 + Number(sh[1]) / 2; - this.edits.sharpen = sigma; - } + this.edits.sharpen = sigma; + } else if (key === ('stretch')) { if (this.edits.resize === undefined) { this.edits.resize = {}; } if (this.sizingMethod === undefined || this.sizingMethod !== 'fit-in') { this.edits.resize.fit = "fill"; - } - } + } + } else if (key === ('strip_exif')) { this.edits.rotate = 0; - } + } else if (key === ('strip_icc')) { this.edits.rotate = 0; - } + } else if (key === ('upscale')) { if (this.edits.resize === undefined) { this.edits.resize = {}; } this.edits.resize.fit = "inside" - } + } else { return undefined; } @@ -204,4 +216,4 @@ class ThumborMapping { } // Exports -module.exports = ThumborMapping; \ No newline at end of file +module.exports = ThumborMapping;