DockerERTFF/lib/modules/image.js

505 lines
16 KiB
JavaScript

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', 'jpeg').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 = `
<svg width="${width}" height="${height}">
<style>
.text {
fill: ${color};
font-family: "${font}";
font-size: ${size}px;
line-height: 1;
}
</style>
<text x="${x}" y="${y}" text-anchor="${anchor}" class="text">${text}</text>
</svg>
`;
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();
},
};