diff --git a/package-lock.json b/package-lock.json index f0f0e60..719a908 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,16 @@ { "name": "serverless-sharp", - "version": "2.0.6", + "version": "2.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "2.0.6", + "name": "serverless-sharp", + "version": "2.1.1", "license": "MIT", + "dependencies": { + "buffer-image-size": "^0.6.4" + }, "devDependencies": { "@types/jest": "^25.2.1", "aws-sdk": "^2.668.0", @@ -1734,8 +1738,7 @@ "node_modules/@types/node": { "version": "14.14.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz", - "integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==", - "dev": true + "integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.0", @@ -2823,6 +2826,17 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/buffermaker": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/buffermaker/-/buffermaker-1.2.1.tgz", @@ -16049,8 +16063,7 @@ "@types/node": { "version": "14.14.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz", - "integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==", - "dev": true + "integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -16941,6 +16954,14 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "requires": { + "@types/node": "*" + } + }, "buffermaker": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/buffermaker/-/buffermaker-1.2.1.tgz", diff --git a/package.json b/package.json index 169b29a..03d1f5c 100644 --- a/package.json +++ b/package.json @@ -23,5 +23,8 @@ "serverless": "^2.30.1", "sharp": "0.28.0", "standard": "^16.0.3" + }, + "dependencies": { + "buffer-image-size": "^0.6.4" } } diff --git a/src/image-ops/blending.js b/src/image-ops/blending.js index f9c313c..946b8d8 100644 --- a/src/image-ops/blending.js +++ b/src/image-ops/blending.js @@ -1,8 +1,25 @@ const axios = require('axios') +const sharp = require('sharp') +const sizeOf = require('buffer-image-size') exports.apply = async (image, edits) => { - if (edits.blend.processedValue) { - await this.blend(image, edits.blend.processedValue) + await this.beforeApply(image, edits) + const { blend, w, h, fit } = edits + const blendAlign = edits["blend-align"] + const blendAlpha = edits["blend-alpha"] + const blendRatio = edits["blend-ratio"] + console.log("RATIO: "+blendRatio) + + if (blend.processedValue) { + await this.blend( + image, + blend.processedValue, + blendAlign.processedValue, + blendAlpha.processedValue, + {width: w.processedValue, height: h.processedValue}, + fit, + blendRatio.processedValue + ) } } @@ -10,12 +27,119 @@ exports.apply = async (image, edits) => { * * @param {Sharp} image * @param {String} url - */ -exports.blend = async (image, url) => { - const compositeInput = (await axios({ + * @param {String} gravity + * @param {number} alpha + * @param {Object} targetDimensions + * @param {String} targetFit + * @param {number} ratio + **/ +exports.blend = async (image, url, gravity, alpha, targetDimensions, targetFit, ratio) => { + let compositeImage = (await axios({ url: url, responseType: "arraybuffer" })).data; - image.composite([{input: compositeInput, tile: true}]) + if (alpha >= 0) { + console.log("APPLYING OPACITY TO WATERMARK: "+alpha) + compositeImage = await sharp(compositeImage).composite( + [{ + input: Buffer.from([0,0,0,Math.round(alpha*256)]), + raw: { + width: 1, + height: 1, + channels: 4, + }, + tile: true, + blend: 'dest-in', + }] + ).toBuffer() + } + + if (ratio > 0) { + const compositeDimensions = calculateCompositeDimensions(targetDimensions, targetFit, ratio) + console.log("ADJUSTING WATERMARK IMAGE TO DIMENSIONS: "+JSON.stringify(compositeDimensions)) + compositeImage = await sharp(compositeImage).resize(compositeDimensions.width, compositeDimensions.height, {fit: "inside"}).toBuffer() + } + + image.composite([{input: compositeImage, tile: false, gravity: gravity}]) } + +function calculateCompositeDimensions (image, targetDimensions, targetFit, ratio) { + const originalDimensions = sizeOf(image) + let resultDimensions + if (targetFit) { + if (targetFit === "fill") { + //https://docs.imgix.com/apis/rendering/size/resize-fit-mode#fill + resultDimensions = targetDimensions + } else if (targetFit === "max") { + //https://docs.imgix.com/apis/rendering/size/resize-fit-mode#max + resultDimensions = calculateResultDimensions(originalDimensions, targetDimensions, false) + } else if (targetFit === "clip") { + //https://docs.imgix.com/apis/rendering/size/resize-fit-mode#clip + resultDimensions = calculateResultDimensions(originalDimensions, targetDimensions, true) + } + } else { + resultDimensions = originalDimensions + } + return {width: resultDimensions.width * ratio, height: resultDimensions.height * ratio} +} + +function calculateResultDimensions(originalDimensions, targetDimensions, allowUpscaling) { + const originalAspectRatio = originalDimensions.width / originalDimensions.height; + const targetAspectRatio = targetDimensions.width / targetDimensions.height; + + let resultWidth, resultHeight; + + if (originalAspectRatio > targetAspectRatio) { + // The original image is wider compared to the target aspect ratio + resultWidth = targetDimensions.width; + resultHeight = Math.floor(targetDimensions.width / originalAspectRatio); + } else { + // The original image is taller compared to the target aspect ratio + resultHeight = targetDimensions.height; + resultWidth = Math.floor(targetDimensions.height * originalAspectRatio); + } + + if (!allowUpscaling && (resultWidth > originalDimensions.width || resultHeight > originalDimensions.height)) + { + resultWidth = originalDimensions.width; + resultHeight = originalDimensions.height; + } + + return { width: resultWidth, height: resultHeight }; +} + + +const alignmentToGravity = new Map([ + ["center", "centre"], ["middle", "centre"], ["center,middle", "centre"], ["middle,center", "centre"], + ["left", "west"], ["left,middle", "west"], ["middle,left", "west"], + ["right", "east"], ["right,middle", "east"], ["middle,right", "east"], + ["top", "north"], ["top,middle", "north"], ["middle,top", "north"], + ["top,left", "northwest"], ["left,top", "northwest"], + ["top,right", "northeast"], ["right,top", "northeast"], + ["bottom", "south"], ["bottom,middle", "south"], ["middle,bottom", "south"], + ["bottom,left", "southwest"], ["left,bottom", "southwest"], + ["bottom,right", "southeast"], ["right,bottom", "southeast"], +]) + +exports.beforeApply = async function (image, edits) { + const blendAlign = edits["blend-align"] + const blendAlpha = edits["blend-alpha"] + const blendRatio = edits["blend-ratio"] + + const alignment = blendAlign.processedValue.join(",") + if (alignment) { + const gravity = alignmentToGravity.get(alignment) + blendAlign.processedValue = gravity ? gravity : "centre" + } + + blendAlpha.processedValue = percentageAsDecimal(blendAlpha.processedValue); + blendRatio.processedValue = 0.5 //percentageAsDecimal(blendRatio.processedValue); +} + +function percentageAsDecimal(percentageValue) { + if (percentageValue) { + return parseFloat(percentageValue) / 100 + } + return -1 +} \ No newline at end of file