UNPKG

purified-image

Version:

Load, save and draw on image with API similar to HTML Canvas Context 2D. No native dependencies.

314 lines (266 loc) 7.13 kB
'use strict'; const fs = require('fs'); const path = require('path'); const stream = require('stream'); const pImage = require('pureimage'); const EventEmitter = require('events').EventEmitter; const LoadState = { IMAGE: 'image', FONT: 'font', }; /** * Steam that saves data to a buffer */ class BufferWritable extends stream.Writable { constructor() { super(); this.buffer = Buffer.alloc(0); } _write(chunk, encoding, callback) { this.buffer = Buffer.concat([this.buffer, chunk]); callback(); } } /** * Steam that reads data from a buffer */ class BufferReadable extends stream.Readable { constructor(buffer) { super(); this.buffer = buffer; } _read() { this.push(this.buffer); this.push(null); } } class Image extends EventEmitter { /** * Optionally load a file * @param {string} [imagePath] * @param {string} [imageType=auto] * @param {boolean} [silent=false] */ constructor(imagePath, imageType = 'auto', silent = false) { super(); this.silent = silent; this._isImageLoaded = false; this._isFontsLoaded = false; this._fontPromises = {}; if (imagePath) { this.load(imagePath, imageType); } this.error = this.error.bind(this); } error(e) { this.emit('error', e); if (!this.silent) { // eslint-disable-next-line no-console console.error(e); } } /** * Load an image from a file * @param {string} imagePath * @param {string} imageType png or jpeg (now only png is supported) * @returns {Image} */ load(imagePath, imageType = 'auto') { imageType = Image._getTypeByPath(imagePath, imageType); const imageStream = fs.createReadStream(imagePath); if (imageType === 'jpg') { this._imgPromise = pImage.decodeJPEGFromStream(imageStream); } else { this._imgPromise = pImage.decodePNGFromStream(imageStream); } this._imgPromise.then((bitmap) => { this._updateLoadState(LoadState.IMAGE, bitmap); }); return this; } /** * Load a TTF font file * @param {string} fontPath * @param {string} [fontName] By default it's base name of file */ loadFont(fontPath, fontName) { if (!fontName) { fontName = path.parse(fontPath).name .replace(/([a-z])([A-Z])/g, '$1 $2') .replace(/([a-z])[-_]([A-Z])/g, '$1 $2'); } this._fontPromises[fontName] = new Promise((resolve) => { pImage.registerFont(fontPath, fontName).load(() => { this._updateLoadState(LoadState.FONT); resolve(); }); }); return this; } /** * Make a copy of the current image * @return Image */ copy() { const image = new this.constructor(); image._imgPromise = this._imgPromise .then((bitmap) => { const copy = new bitmap.constructor(bitmap.width, bitmap.height); copy.data = Buffer.from(bitmap.data); return copy; }) .catch(this.error); image._fontPromises = this._fontPromises; return image; } /** * This callback is called when a all resources are loaded * @callback Image~readyCallback * @param {Bitmap} [image] * @param {...Object} [fonts] * @return {Bitmap} image */ /** * Shortcut for handling loaded event * @param {Image~readyCallback} callback * @param {Boolean} [modifyImage] Is an image will be modified in a callback */ ready(callback, modifyImage) { const promises = [this._imgPromise].concat( Object.values(this._fontPromises) ); const newPromise = Promise.all(promises) .then((values) => callback(...values)) .catch(this.error); if (modifyImage) { this._imgPromise = newPromise; } return this; } /** * This callback is called when a pureimage canvas is ready * @callback Image~drawCallback * @param {Context} ctx */ /** * Draw on image canvas * @param {Image~drawCallback} callback * @returns {Image} */ draw(callback) { this.ready((bitmap) => { const ctx = bitmap.getContext('2d'); // Now pureimage doesn't render font without this ctx.USE_FONT_GLYPH_CACHING = false; callback(ctx); return bitmap; }, true); return this; } /** * Change canvas size of the current image * @param {Number} width * @param {Number} height * @returns {Image} */ resize(width, height) { this.ready((bitmap) => { /** @type Bitmap */ const resized = pImage.make(width, height, { fillval: 0x00000000, }); const ctx = resized.getContext('2d'); ctx.mode = 'REPLACE'; ctx.drawImage(bitmap, 0, 0); return resized; }, true); return this; } /** * Save image data to buffer * @param {string} [imageType=png] * @returns {Promise.<Buffer>} */ toBuffer(imageType = 'png') { return this._encode(new BufferWritable(), imageType) .then((buffer) => buffer.buffer) .catch(this.error); } /** * Load an image from a buffer * @param {Buffer} buffer * @param {string} [imageType=png] * @returns {Image} */ fromBuffer(buffer, imageType = 'png') { const imageStream = new BufferReadable(buffer); if (imageType === 'jpg') { this._imgPromise = pImage.decodeJPEGFromStream(imageStream); } else { this._imgPromise = pImage.decodePNGFromStream(imageStream); } return this; } /** * Save image data to file * @param {string} imagePath * @param {string} [imageType=auto] * @returns {Promise.<stream.Writable>} */ save(imagePath, imageType = 'auto') { imageType = Image._getTypeByPath(imagePath, imageType); return this._encode(fs.createWriteStream(imagePath), imageType); } _updateLoadState(target, object) { /** * @event Image#image-loaded * @param {Bitmap} image */ /** * @event Image#font-loaded */ /** * Fired when both image data and font are loaded * @event Image#loaded */ switch (target) { case LoadState.IMAGE: this.emit('image-loaded', object); this._isImageLoaded = true; break; case LoadState.FONT: this.emit('font-loaded'); this._isFontsLoaded = true; break; default: throw new Error(`Unknown load target ${target}`); } if (!this._isImageLoaded) { return; } if (this._isFontsLoaded || !this._fontPromises.length) { this.emit('loaded'); } } async _encode(writeStream, imageType = 'png') { const bitmap = await this._imgPromise; if (imageType === 'jpg') { await pImage.encodeJPEGToStream(bitmap, writeStream); } else { await pImage.encodePNGToStream(bitmap, writeStream); } this.emit('encoded'); return writeStream; } static _getTypeByPath(imagePath, imageType = 'auto') { if (['png, jpg'].indexOf(imageType) === -1) { if (imagePath.endsWith('jpg') || imagePath.endsWith('jpeg')) { return 'jpg'; } return 'png'; } return imageType; } } module.exports = Image; module.exports.default = Image;