UNPKG

@bubblewrap/core

Version:

Core Library to generate, build and sign TWA projects

120 lines (119 loc) 5.61 kB
"use strict"; /* * Copyright 2020 Google Inc. All Rights Reserved. * * 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 * * http://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.ImageHelper = void 0; const path = require("path"); const fs = require("fs"); const Jimp = require("jimp"); const plugin_color_1 = require("@jimp/plugin-color"); const FetchUtils_1 = require("./FetchUtils"); const util_1 = require("util"); const svg2img_1 = require("./wrappers/svg2img"); // fs.promises is marked as experimental. This should be replaced when stable. const fsMkDir = (0, util_1.promisify)(fs.mkdir); class ImageHelper { async saveIcon(icon, size, fileName, backgroundColor) { const image = await Jimp.read(icon.data); // Jimp creates artifacts when upscaling images that have an alpha channel. This happens // because even when a pixel is fully transparent the RGB part of the color still gets blended // with other pixels. To avoid this, we apply the RGB part of `backgroundColor` to transparent // pixels so they are blended with that color instead and match a background. // See https://github.com/GoogleChromeLabs/bubblewrap/issues/488#issuecomment-806560923. if (backgroundColor && image.hasAlpha()) { // The replacement colour is the same as the background colour, but fully transparent. const replacementColor = ((backgroundColor.rgbNumber() << 8) & 0xFFFFFF00) >>> 0; // Iterate over the pixels, check for fully transparent ones and replace with // replacementColor. for (let y = 0; y < image.getHeight(); y++) { for (let x = 0; x < image.getWidth(); x++) { const color = image.getPixelColour(x, y); // Apply replacement color if the pixel is fully transparent. if ((color & 0xFF) === 0) { image.setPixelColour(replacementColor, x, y); } } } } image.resize(size, size); await image.writeAsync(fileName); } /** * Generate a file for the given icon inside targetDir. * * @param {Object} icon Object containing the original URL and the icon image data. * @param {string} targetDir Path to the directory the image will be saved in. * @param {Object} iconDef Icon definitions specifying the size the icon should be exported as. */ async generateIcon(icon, targetDir, iconDef, backgroundColor) { const destFile = path.join(targetDir, iconDef.dest); await fsMkDir(path.dirname(destFile), { recursive: true }); return await this.saveIcon(icon, iconDef.size, destFile, backgroundColor); } /** * Set the color of a monochrome icon to be the theme color. * * @param {Object} icon Original monochrome icon to use. * @param {Color} themeColor Color to use for the icon. * @returns New image data. */ async monochromeFilter(icon, themeColor) { const image = await Jimp.read(icon.data); image.color([ // Set all pixels to black/0 { apply: plugin_color_1.ColorActionName.RED, params: [-255] }, { apply: plugin_color_1.ColorActionName.GREEN, params: [-255] }, { apply: plugin_color_1.ColorActionName.BLUE, params: [-255] }, // Add to channels using theme color { apply: plugin_color_1.ColorActionName.RED, params: [themeColor.red()] }, { apply: plugin_color_1.ColorActionName.GREEN, params: [themeColor.green()] }, { apply: plugin_color_1.ColorActionName.BLUE, params: [themeColor.blue()] }, ]); return { url: icon.url, data: image }; } /** * Fetches an Icon. * * @param {string} iconUrl the URL to fetch the icon from. * @returns an Object containing the original URL and the icon image data. */ async fetchIcon(iconUrl) { const response = await FetchUtils_1.fetchUtils.fetch(iconUrl); if (response.status !== 200) { throw new Error(`Failed to download icon ${iconUrl}. Responded with status ${response.status}`); } const contentType = response.headers.get('content-type'); if (!(contentType === null || contentType === void 0 ? void 0 : contentType.startsWith('image/'))) { throw new Error(`Received icon "${iconUrl}" with invalid Content-Type.` + ` Responded with Content-Type "${contentType}"`); } let body = await response.arrayBuffer(); if (contentType.startsWith('image/svg')) { const textDecoder = new TextDecoder(); try { body = await (0, svg2img_1.svg2img)(textDecoder.decode(body)); } catch (error) { throw new Error(`Problem reading ${iconUrl}: ${error}`); } } return { url: iconUrl, data: await Jimp.read(Buffer.from(body)), }; } } exports.ImageHelper = ImageHelper;