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

feat(storage-resize-images): add WEBP and GIF animation #875

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
3 changes: 3 additions & 0 deletions storage-resize-images/POSTINSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Be aware of the following when using this extension:
* image/png
* image/tiff
* image/webp
* image/gif

If you are using raw image data in your application, you need to ensure you set the correct content type when uploading to the Firebase Storage bucket to trigger the extension image resize. Below is an example of how to set the content type:

Expand Down Expand Up @@ -74,6 +75,8 @@ function uploadImageToStorage(rawImage){

- If you configured the `Cache-Control header for resized images` parameter, your specified value will overwrite the value copied from the original image. Learn more about image metadata in the [Cloud Storage documentation](https://firebase.google.com/docs/storage/).

-

### Monitoring

As a best practice, you can [monitor the activity](https://firebase.google.com/docs/extensions/manage-installed-extensions#monitor) of your installed extension, including checks on its health, usage, and logs.
5 changes: 5 additions & 0 deletions storage-resize-images/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ If you prefer to resize every image uploaded to your Storage bucket, leave this
* Convert image to preferred types: The image types you'd like your source image to convert to. The default for this option will be to keep the original file type.


* GIF and WEBP animated option: Keep animation of GIF and WEBP formats.


* Cloud Function memory: Memory of the function responsible of resizing images. Choose how much memory to give to the function that resize images. (For animated GIF => GIF we recommend using a minimum of 2GB).



**Cloud Functions:**
Expand Down
34 changes: 33 additions & 1 deletion storage-resize-images/extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ resources:
properties:
location: ${param:LOCATION}
runtime: nodejs14
availableMemoryMb: 1024
availableMemoryMb: ${param:FUNCTION_MEMORY}
eventTrigger:
eventType: google.storage.object.finalize
resource: projects/_/buckets/${param:IMG_BUCKET}
Expand Down Expand Up @@ -239,7 +239,39 @@ params:
value: png
- label: tiff
value: tiff
- label: gif
value: gif
- label: original
value: false
default: false
required: true

- param: IS_ANIMATED
label: GIF and WEBP animated option
description: >
Keep animation of GIF and WEBP formats.
type: select
options:
- label: Yes
value: true
- label: No (1st frame only)
value: false
default: true
required: false

- param: FUNCTION_MEMORY
label: Cloud Function memory
description: >-
Memory of the function responsible of resizing images.
Choose how much memory to give to the function that resize images. (For animated GIF => GIF we recommend using a minimum of 2GB).
type: select
options:
- label: 512 MB
value: 512
- label: 1 GB
value: 1024
- label: 2 GB
value: 2048
default: 1024
required: true
immutable: false
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

exports[`extension configuration detected from environment variables 1`] = `
Object {
"animated": false,
"bucket": "extensions-testing.appspot.com",
"cacheControlHeader": undefined,
"deleteOriginalFile": 0,
Expand Down
14 changes: 14 additions & 0 deletions storage-resize-images/functions/__tests__/convert-image.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ const readFile = util.promisify(fs.readFile);

let bufferJPG;
let bufferPNG;
let bufferGIF;
beforeAll(async () => {
bufferJPG = await readFile(__dirname + "/test-image.jpeg");
bufferPNG = await readFile(__dirname + "/test-image.png");
bufferGIF = await readFile(__dirname + "/test-image.gif");
});

describe("convertType", () => {
Expand Down Expand Up @@ -51,9 +53,21 @@ describe("convertType", () => {
expect(imageType(buffer).mime).toBe("image/tiff");
});

it("converts to gif image type", async () => {
const buffer = await convertType(bufferGIF, "gif");

expect(imageType(buffer).mime).toBe("image/gif");
});

it("remains jpeg image type when different image type is not supported", async () => {
const buffer = await convertType(bufferJPG, "raw");

expect(imageType(buffer).mime).toBe("image/jpeg");
});

it("remains gif image type when different image type is not supported", async () => {
const buffer = await convertType(bufferGIF, "raw");

expect(imageType(buffer).mime).toBe("image/gif");
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions storage-resize-images/functions/lib/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use strict";
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.deleteImage = void 0;
var deleteImage;
(function (deleteImage) {
deleteImage[deleteImage["always"] = 0] = "always";
deleteImage[deleteImage["never"] = 1] = "never";
deleteImage[deleteImage["onSuccess"] = 2] = "onSuccess";
})(deleteImage = exports.deleteImage || (exports.deleteImage = {}));
function deleteOriginalFile(deleteType) {
switch (deleteType) {
case "true":
return deleteImage.always;
case "false":
return deleteImage.never;
default:
return deleteImage.onSuccess;
}
}
function paramToArray(param) {
return typeof param === "string" ? param.split(",") : undefined;
}
exports.default = {
bucket: process.env.IMG_BUCKET,
cacheControlHeader: process.env.CACHE_CONTROL_HEADER,
imageSizes: process.env.IMG_SIZES.split(","),
resizedImagesPath: process.env.RESIZED_IMAGES_PATH,
includePathList: paramToArray(process.env.INCLUDE_PATH_LIST),
excludePathList: paramToArray(process.env.EXCLUDE_PATH_LIST),
deleteOriginalFile: deleteOriginalFile(process.env.DELETE_ORIGINAL_FILE),
imageTypes: paramToArray(process.env.IMAGE_TYPE),
animated: process.env.IS_ANIMATED === "true" || undefined ? true : false,
};
159 changes: 159 additions & 0 deletions storage-resize-images/functions/lib/resize-image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.modifyImage = exports.supportedImageContentTypeMap = exports.supportedContentTypes = exports.convertType = exports.resize = void 0;
const os = require("os");
const sharp = require("sharp");
const path = require("path");
const fs = require("fs");
const uuidv4_1 = require("uuidv4");
const config_1 = require("./config");
const logs = require("./logs");
function resize(file, size) {
let height, width;
if (size.indexOf(",") !== -1) {
[width, height] = size.split(",");
}
else if (size.indexOf("x") !== -1) {
[width, height] = size.split("x");
}
else {
throw new Error("height and width are not delimited by a ',' or a 'x'");
}
return sharp(file, { failOnError: false, animated: config_1.default.animated })
.rotate()
.resize(parseInt(width, 10), parseInt(height, 10), {
fit: "inside",
withoutEnlargement: true,
})
.toBuffer();
}
exports.resize = resize;
function convertType(buffer, format) {
if (format === "jpg" || format === "jpeg") {
return sharp(buffer)
.jpeg()
.toBuffer();
}
if (format === "png") {
return sharp(buffer)
.png()
.toBuffer();
}
if (format === "webp") {
return sharp(buffer, { animated: config_1.default.animated })
.webp()
.toBuffer();
}
if (format === "tiff" || format === "tif") {
return sharp(buffer)
.tiff()
.toBuffer();
}
if (format === "gif") {
return sharp(buffer, { animated: config_1.default.animated })
.gif()
.toBuffer();
}
return buffer;
}
exports.convertType = convertType;
/**
* Supported file types
*/
exports.supportedContentTypes = [
"image/jpeg",
"image/png",
"image/tiff",
"image/webp",
"image/gif",
];
exports.supportedImageContentTypeMap = {
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
tif: "image/tif",
tiff: "image/tiff",
webp: "image/webp",
gif: "image/gif",
};
const supportedExtensions = Object.keys(exports.supportedImageContentTypeMap).map((type) => `.${type}`);
exports.modifyImage = async ({ bucket, originalFile, fileDir, fileNameWithoutExtension, fileExtension, contentType, size, objectMetadata, format, }) => {
const shouldFormatImage = format !== "false";
const imageContentType = shouldFormatImage
? exports.supportedImageContentTypeMap[format]
: contentType;
const modifiedExtensionName = fileExtension && shouldFormatImage ? `.${format}` : fileExtension;
let modifiedFileName;
if (supportedExtensions.includes(fileExtension.toLowerCase())) {
modifiedFileName = `${fileNameWithoutExtension}_${size}${modifiedExtensionName}`;
}
else {
// Fixes https://github.com/firebase/extensions/issues/476
modifiedFileName = `${fileNameWithoutExtension}${fileExtension}_${size}`;
}
// Path where modified image will be uploaded to in Storage.
const modifiedFilePath = path.normalize(config_1.default.resizedImagesPath
? path.join(fileDir, config_1.default.resizedImagesPath, modifiedFileName)
: path.join(fileDir, modifiedFileName));
let modifiedFile;
try {
modifiedFile = path.join(os.tmpdir(), modifiedFileName);
// Cloud Storage files.
const metadata = {
contentDisposition: objectMetadata.contentDisposition,
contentEncoding: objectMetadata.contentEncoding,
contentLanguage: objectMetadata.contentLanguage,
contentType: imageContentType,
metadata: objectMetadata.metadata || {},
};
metadata.metadata.resizedImage = true;
if (config_1.default.cacheControlHeader) {
metadata.cacheControl = config_1.default.cacheControlHeader;
}
else {
metadata.cacheControl = objectMetadata.cacheControl;
}
// If the original image has a download token, add a
// new token to the image being resized #323
if (metadata.metadata.firebaseStorageDownloadTokens) {
metadata.metadata.firebaseStorageDownloadTokens = uuidv4_1.uuid();
}
// Generate a resized image buffer using Sharp.
logs.imageResizing(modifiedFile, size);
let modifiedImageBuffer = await resize(originalFile, size);
logs.imageResized(modifiedFile);
// Generate a converted image type buffer using Sharp.
if (shouldFormatImage) {
logs.imageConverting(fileExtension, format);
modifiedImageBuffer = await convertType(modifiedImageBuffer, format);
logs.imageConverted(format);
}
// Generate a image file using Sharp.
await sharp(modifiedImageBuffer, { animated: config_1.default.animated }).toFile(modifiedFile);
// Uploading the modified image.
logs.imageUploading(modifiedFilePath);
await bucket.upload(modifiedFile, {
destination: modifiedFilePath,
metadata,
});
logs.imageUploaded(modifiedFile);
return { size, success: true };
}
catch (err) {
logs.error(err);
return { size, success: false };
}
finally {
try {
// Make sure the local resized file is cleaned up to free up disk space.
if (modifiedFile) {
logs.tempResizedFileDeleting(modifiedFilePath);
fs.unlinkSync(modifiedFile);
logs.tempResizedFileDeleted(modifiedFilePath);
}
}
catch (err) {
logs.errorDeleting(err);
}
}
};
Loading