@coko/server
Version:
Reusable server for use by Coko's projects
226 lines • 8.57 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = __importDefault(require("path"));
const child_process_1 = require("child_process");
const consumers_1 = require("stream/consumers");
const mime_types_1 = __importDefault(require("mime-types"));
const sharp_1 = __importDefault(require("sharp"));
const command_exists_1 = require("command-exists");
const fs_extra_1 = __importDefault(require("fs-extra"));
const exifr_1 = __importDefault(require("exifr"));
const config_1 = __importDefault(require("../configManager/config"));
const logger_1 = __importDefault(require("../logger"));
// #region helpers
const getMetadata = async (fileBuffer) => {
return (0, sharp_1.default)(fileBuffer, { limitInputPixels: false }).metadata();
};
const imageSizeConversionMapper = {
tiff: {
small: 'png',
medium: 'png',
full: 'png',
},
tif: {
small: 'png',
medium: 'png',
full: 'png',
},
svg: {
small: 'svg',
medium: 'svg',
full: 'png',
},
png: {
small: 'png',
medium: 'png',
full: 'png',
},
default: {
small: 'jpeg',
medium: 'jpeg',
full: 'png',
},
};
const orientationMap = {
'Horizontal (normal)': 1,
'Mirror horizontal': 2,
'Rotate 180': 3,
'Mirror vertical': 4,
'Mirror horizontal and rotate 270 CW': 5,
'Rotate 90 CW': 6,
'Mirror horizontal and rotate 90 CW': 7,
'Rotate 270 CW': 8,
};
// #endregion helpers
class Image {
conversionMapper = { eps: 'svg' };
dir;
extension;
filename;
maxWidth;
mimetype;
name;
outputExtension;
path;
shouldConvert;
// properties: filename, dir
constructor(properties) {
this.name = path_1.default.parse(properties.filename).name;
this.extension = path_1.default.extname(properties.filename).slice(1);
this.path = path_1.default.join(properties.dir, properties.filename);
this.filename = properties.filename;
this.dir = properties.dir;
const convertTo = this.conversionMapper[this.extension];
this.shouldConvert = !!convertTo;
this.outputExtension = this.shouldConvert ? convertTo : this.extension;
const { maximumWidthForSmallImages, maximumWidthForMediumImages } = config_1.default.get('fileStorage');
this.maxWidth = {
small: parseInt(maximumWidthForSmallImages, 10) || 180,
medium: parseInt(maximumWidthForMediumImages, 10) || 640,
};
this.mimetype = mime_types_1.default.lookup(this.extension);
}
/**
* Takes the image file and writes a converted image in the directory.
* eg. dir/file.eps will generate dir/file.svg
*/
async #createConvertedFile() {
if (!(0, command_exists_1.sync)('magick') && !(0, command_exists_1.sync)('convert')) {
throw new Error('ImageMagick needs to be installed on your OS or container');
}
const targetExtension = this.conversionMapper[this.extension];
const targetPath = `${path_1.default.join(this.dir, this.name)}.${targetExtension}`;
return new Promise((resolve, reject) => {
(0, child_process_1.exec)(`convert ${this.path} ${targetPath}`, (error, stdout, stderr) => {
if (error)
return reject(error);
logger_1.default.info(stdout || stderr);
return resolve(targetPath);
});
});
}
/* eslint-disable class-methods-use-this */
async #rotate(fileBuffer, filePath, metadata) {
if (!metadata.exif)
return fileBuffer;
let exifMetadata;
try {
// more reliable to use fileBuffer with exifr, but we still want to see if metadata.exif exists or not
exifMetadata = await exifr_1.default.parse(fileBuffer);
}
catch (e) {
logger_1.default.error(`FILE_STORAGE: generateVersions: failed to get exif metadata`, e.message);
}
if (!exifMetadata)
return fileBuffer;
const orientationString = exifMetadata?.Orientation;
const orientation = orientationMap[orientationString] ?? 1;
if ([3, 6, 8].includes(orientation)) {
const image = (0, sharp_1.default)(fileBuffer);
switch (orientation) {
case 3:
image.rotate(180);
break;
case 6:
image.rotate(90);
break;
case 8:
image.rotate(270);
break;
default:
}
// replace original file with rotated original
const updatedFileBuffer = await image.toBuffer();
await (0, sharp_1.default)(updatedFileBuffer).toFile(filePath);
return updatedFileBuffer;
}
return fileBuffer;
}
async generateVersions() {
let filePath = this.path;
if (this.shouldConvert)
filePath = await this.#createConvertedFile();
const fileReadStream = fs_extra_1.default.createReadStream(filePath);
let fileBuffer = (await (0, consumers_1.buffer)(fileReadStream));
const metadata = await getMetadata(fileBuffer);
const originalImageWidth = metadata.width;
fileBuffer = await this.#rotate(fileBuffer, filePath, metadata);
const sizes = ['small', 'medium', 'full'];
const [smallPath, mediumPath, fullPath] = sizes.map(size => {
return path_1.default.join(this.dir, `${this.name}_${size}.${imageSizeConversionMapper[this.outputExtension]
? imageSizeConversionMapper[this.outputExtension].small
: imageSizeConversionMapper.default.small}`);
});
if (this.outputExtension === 'svg') {
await (0, sharp_1.default)(fileBuffer).toFile(smallPath);
await (0, sharp_1.default)(fileBuffer).toFile(mediumPath);
await (0, sharp_1.default)(fileBuffer).toFile(fullPath);
}
else {
await (0, sharp_1.default)(fileBuffer)
.resize({ width: this.maxWidth.small })
.toFile(smallPath);
if (originalImageWidth < this.maxWidth.medium) {
await (0, sharp_1.default)(fileBuffer).toFile(mediumPath);
}
else {
await (0, sharp_1.default)(fileBuffer)
.resize({ width: this.maxWidth.medium })
.toFile(mediumPath);
}
await (0, sharp_1.default)(fileBuffer).toFile(fullPath);
}
const { width, height, space, density, size } = metadata;
const originalData = {
type: 'original',
path: filePath,
filename: this.filename,
extension: this.extension,
mimetype: this.mimetype,
size,
imageMetadata: {
density,
height,
space,
width,
},
};
const sizesData = await Promise.all([smallPath, mediumPath, fullPath].map(async (p, i) => {
const pReadStream = fs_extra_1.default.createReadStream(p);
const pBuffer = await (0, consumers_1.buffer)(pReadStream);
const { width: pWidth, height: pHeight, space: pSpace, density: pDensity, size: pSize, } = await getMetadata(pBuffer);
let type;
switch (i) {
case 0:
type = 'small';
break;
case 1:
type = 'medium';
break;
default:
type = 'full';
break;
}
return {
type,
path: p,
filename: path_1.default.basename(p),
extension: path_1.default.extname(p).slice(1),
mimetype: mime_types_1.default.lookup(p),
size: pSize,
imageMetadata: {
density: pDensity,
height: pHeight,
space: pSpace,
width: pWidth,
},
};
}));
return [originalData, ...sizesData];
}
}
exports.default = Image;
//# sourceMappingURL=Image.js.map