UNPKG

@coko/server

Version:

Reusable server for use by Coko's projects

226 lines 8.57 kB
"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