const fs = require('fs-extra'); const Sharp = require('sharp'); const debug = require('debug')('server-connect:image'); const { basename, extname, join } = require('path'); const { toAppPath, toSystemPath, parseTemplate, getUniqFile } = require('../core/path'); const positions = { 'center': 0, 'centre': 0, 'top': 1, 'north': 1, 'right': 2, 'east': 2, 'bottom': 3, 'south': 3, 'left': 4, 'west': 4, 'top right': 5, 'right top': 5, 'northeast': 5, 'bottom right': 6, 'right bottom': 6, 'southeast': 6, 'bottom left': 7, 'left bottom': 7, 'southwest': 7, 'top left': 8, 'left top': 8, 'northwest': 8, 'entropy': 16, 'attention': 17 }; function cw(w, meta) { if (typeof w == 'string') { if (/%$/.test(w)) { w = meta.width * parseFloat(w) / 100; } } if (w < 0) { w = meta.width + w; } return parseInt(w); } function ch(h, meta) { if (typeof h == 'string') { if (/%$/.test(h)) { h = meta.height * parseFloat(h) / 100; } } if (h < 0) { h = meta.height + h; } return parseInt(h); } function cx(x, w, meta) { if (typeof x == 'string') { switch (x) { case 'left': x = 0; break; case 'center': x = (meta.width - w) / 2; break; case 'right': x = meta.width - w; break; default: if (/%$/.test(x)) { x = (meta.width - w) * parseFloat(x) / 100; } } } if (x < 0) { x = meta.width - w + x; } return parseInt(x); } function cy(y, h, meta) { if (typeof y == 'string') { switch (y) { case 'top': y = 0; break; case 'middle': y = (meta.height - h) / 2; break; case 'bottom': y = meta.height - h; break; default: if (/%$/.test(y)) { y = (meta.height - h) * parseFloat(y) / 100; } } } if (y < 0) { y = meta.height - h + y; } return parseInt(y); } async function updateImage(sharp) { sharp.image = Sharp(await sharp.image.toBuffer()); sharp.metadata = await sharp.image.metadata(); } module.exports = { getImageSize: async function (options) { let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.getImageSize: path is required.')); const image = Sharp(path); const metadata = await image.metadata(); return { width: metadata.width, height: metadata.height }; }, load: async function (options, name) { let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.load: path is required.')); let orient = this.parseOptional(options.autoOrient, 'boolean', false); this.req.image = this.req.image || {}; this.req.image[name] = { name: basename(path), image: Sharp(path), metadata: null }; const sharp = this.req.image[name]; if (orient) sharp.image.rotate(); await updateImage(sharp); return { name: basename(path), width: sharp.metadata.width, height: sharp.metadata.height }; }, save: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.save: instance "${options.instance} doesn't exist.`); let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.save: path is required.')); let format = this.parseOptional(options.format, 'string', 'auto').toLowerCase(); let template = this.parseOptional(options.template, 'string', '{name}{ext}'); let overwrite = this.parseOptional(options.overwrite, 'boolean', false); let createPath = this.parseOptional(options.createPath, 'boolean', true); let background = this.parseOptional(options.background, 'string', '#FFFFFF'); let quality = this.parseOptional(options.quality, 'number', 75); if (!fs.existsSync(path)) { if (createPath) { await fs.ensureDir(path); } else { throw new Error(`image.save: path "${path}" doesn't exist.`); } } let file = join(path, sharp.name); if (template) { file = parseTemplate(file, template); } if (format == 'auto') { switch (extname(file).toLowerCase()) { case '.png': format = 'png'; break; case '.gif': format = 'gif'; break; case '.webp': format = 'webp'; break; default: format = 'jpeg'; } } if (format == 'jpeg') { sharp.image.flatten({ background }); sharp.image.toFormat(format, { quality }); } else if (format == 'webp') { sharp.image.toFormat(format, { quality }); } else { sharp.image.toFormat(format); } const data = await sharp.image.toBuffer(); file = file.replace(extname(file), '.' + format.replace('jpeg', 'jpg')); if (fs.existsSync(file)) { if (overwrite) { await fs.unlink(file); } else { file = getUniqFile(file); } } await fs.writeFile(file, data) //.toFile(file); return toAppPath(file); }, resize: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.resize: instance "${options.instance} doesn't exist.`); let width = this.parseOptional(cw(this.parse(options.width), sharp.metadata), 'number', null); let height = this.parseOptional(ch(this.parse(options.height), sharp.metadata), 'number', null); let upscale = this.parseOptional(options.upscale, 'boolean', false); if (isNaN(width)) width = null; if (isNaN(height)) height = null; sharp.image.resize(width, height, { fit: width && height ? 'fill' : 'cover', withoutEnlargement: !upscale }); await updateImage(sharp); }, crop: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.crop: instance "${options.instance} doesn't exist.`); let width = this.parseRequired(cw(this.parse(options.width)), 'number', 'image.crop: width is required.'); let height = this.parseRequired(ch(this.parse(options.height)), 'number', 'image.crop: height is required.'); if (width > sharp.metadata.width) width = sharp.metadata.width; if (height > sharp.metadata.height) height = sharp.metadata.height; let left = this.parseRequired(cx(this.parse(options.x), width, sharp.metadata), 'number', 'image.crop: x is required.'); let top = this.parseRequired(cy(this.parse(options.y), height, sharp.metadata), 'number', 'image.crop: y is required.'); sharp.image.extract({ left, top, width, height }); await updateImage(sharp); }, cover: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.cover: instance "${options.instance}" doesn't exist.`); let width = this.parseRequired(options.width, 'number', 'image.cover: width is required.'); let height = this.parseRequired(options.height, 'number', 'image.cover: height is required.'); // position: see positions object for options let position = this.parseOptional(options.position, 'string', 'center'); // kernel: 'nearest', 'cubic', 'mitchell', 'lanczos2', 'lanczos3' let kernel = this.parseOptional(options.kernel, 'string', 'lanczos3'); position = positions[position] || 0; sharp.image.resize({ width, height, position, kernel }); await updateImage(sharp); }, watermark: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.watermark: instance "${options.instance} doesn't exist.`); let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.watermark: path is required.')); let image = Sharp(path); let metadata = await image.metadata(); let input = await image.toBuffer(); let left = this.parseRequired(cx(this.parse(options.x), metadata.width, sharp.metadata), 'number', 'image.watermark: x is required.'); let top = this.parseRequired(cy(this.parse(options.y), metadata.height, sharp.metadata), 'number', 'image.watermark: y is required.'); sharp.image.composite([{ input, left, top }]); }, text: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.text: instance "${options.instance} doesn't exist.`); let x = this.parse(options.x); let y = this.parse(options.y); let text = this.parseRequired(options.text, 'string', 'image.text: text is required.'); let font = this.parseOptional(options.font, 'string', 'Verdana'); let size = this.parseOptional(options.size, 'number', 24); let color = this.parseOptional(options.color, 'string', '#ffffff'); let width = sharp.metadata.width; let height = sharp.metadata.height; let anchor = 'start'; switch (x) { case 'left': x = '0%'; anchor = 'start'; break; case 'center': x = '50%'; anchor = 'middle'; break; case 'right': x = '100%'; anchor = 'end'; break; default: if (x < 0) { x = width - x; anchor = 'end'; } } switch (y) { case 'top': y = size; break; case 'middle': y = (height / 2) - (size / 2); break; case 'bottom': y = height; break; default: if (y < 0) { y = height - size - y; } } let svg = ` ${text} `; const input = await Sharp(Buffer.from(svg)).toBuffer(); sharp.image.composite([{ input, left: 0, top: 0 }]); }, tiled: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.tiled: instance "${options.instance} doesn't exist.`); let input = toSystemPath(this.parseRequired(options.path, 'string', 'image.tiled: path is required.')); let padding = this.parseOptional(options.padding, 'number', 0); if (padding) { input = await Sharp(input).extend({ top: padding, left: padding, bottom: 0, right: 0, background: { r: 0, g: 0, b: 0, alpha: 0 } }).toBuffer(); } sharp.image.composite([{ input, left: 0, top: 0, tile: true }]); }, flip: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.flip: instance "${options.instance} doesn't exist.`); let horizontal = this.parseOptional(options.horizontal, 'boolean', false); let vertical = this.parseOptional(options.vertical, 'boolean', false); if (horizontal) sharp.image.flop(); if (vertical) sharp.image.flip(); }, rotateLeft: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.rotateLeft: instance "${options.instance} doesn't exist.`); sharp.image.rotate(-90); await updateImage(sharp); }, rotateRight: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.rotateRight: instance "${options.instance} doesn't exist.`); sharp.image.rotate(90); await updateImage(sharp); }, smooth: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.smooth: instance "${options.instance} doesn't exist.`); sharp.image.convolve({ width: 3, height: 3, kernel: [ 1, 1, 1, 1, 1, 1, 1, 1, 1 ] }); }, blur: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.blur: instance "${options.instance} doesn't exist.`); sharp.image.convolve({ width: 3, height: 3, kernel: [ 1, 2, 1, 2, 4, 2, 1, 2, 1 ] }); }, sharpen: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.sharpen: instance "${options.instance} doesn't exist.`); sharp.image.convolve({ width: 3, height: 3, kernel: [ 0, -2, 0, -2, 15, -2, 0, -2, 0 ] }); }, meanRemoval: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.meanRemoval: instance "${options.instance} doesn't exist.`); sharp.image.convolve({ width: 3, height: 3, kernel: [ -1, -1, -1, -1, 9, -1, -1, -1, -1 ] }); }, emboss: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.emboss: instance "${options.instance} doesn't exist.`); sharp.image.convolve({ width: 3, height: 3, kernel: [ -1, 0, -1, 0, 4, 0, -1, 0, -1 ], offset: 127 }); }, edgeDetect: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.edgeDetect: instance "${options.instance} doesn't exist.`); sharp.image.convolve({ width: 3, height: 3, kernel: [ -1, -1, -1, 0, 0, 0, 1, 1, 1 ], offset: 127 }); }, grayscale: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.grayscale: instance "${options.instance} doesn't exist.`); sharp.image.grayscale(); }, sepia: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.sepia: instance "${options.instance} doesn't exist.`); sharp.image.tint({ r: 112, g: 66, b: 20 }); }, invert: async function (options) { const sharp = this.req.image[options.instance]; if (!sharp) throw new Error(`image.invert: instance "${options.instance} doesn't exist.`); sharp.image.negate(); }, };