RocketChat/Rocket.Chat

View on GitHub
apps/meteor/server/services/image/service.ts

Summary

Maintainability
A
0 mins
Test Coverage
import type { Readable } from 'stream';
import stream from 'stream';

import { ServiceClassInternal } from '@rocket.chat/core-services';
import type { IMediaService, ResizeResult } from '@rocket.chat/core-services';
import ExifTransformer from 'exif-be-gone';
import ft from 'file-type';
import isSvg from 'is-svg';
import sharp from 'sharp';

export class MediaService extends ServiceClassInternal implements IMediaService {
    protected name = 'media';

    private imageExts = new Set([
        'jpg',
        'png',
        'gif',
        'webp',
        'flif',
        'cr2',
        'tif',
        'bmp',
        'jxr',
        'psd',
        'ico',
        'bpg',
        'jp2',
        'jpm',
        'jpx',
        'heic',
        'cur',
        'dcm',
    ]);

    async resizeFromBuffer(
        input: Buffer,
        width: number,
        height: number,
        keepType: boolean,
        blur: boolean,
        enlarge: boolean,
        fit?: keyof sharp.FitEnum | undefined,
    ): Promise<ResizeResult> {
        const stream = this.bufferToStream(input);
        return this.resizeFromStream(stream, width, height, keepType, blur, enlarge, fit);
    }

    async resizeFromStream(
        input: stream.Stream,
        width: number,
        height: number,
        keepType: boolean,
        blur: boolean,
        enlarge: boolean,
        fit?: keyof sharp.FitEnum | undefined,
    ): Promise<ResizeResult> {
        const transformer = sharp().resize({ width, height, fit, withoutEnlargement: !enlarge });

        if (!keepType) {
            transformer.jpeg();
        }

        if (blur) {
            transformer.blur();
        }

        const result = transformer.toBuffer({ resolveWithObject: true });
        input.pipe(transformer);

        const {
            data,
            info: { width: widthInfo, height: heightInfo },
        } = await result;
        return {
            data,
            width: widthInfo,
            height: heightInfo,
        };
    }

    async isImage(buff: Buffer): Promise<boolean> {
        const data = await ft.fromBuffer(buff);
        if (!data?.ext) {
            return false || this.isSvgImage(buff);
        }
        return this.imageExts.has(data.ext) || this.isSvgImage(buff);
    }

    isSvgImage(buff: Buffer): boolean {
        return isSvg(buff);
    }

    stripExifFromBuffer(buffer: Buffer): Promise<Buffer> {
        return this.streamToBuffer(this.stripExifFromImageStream(this.bufferToStream(buffer)));
    }

    stripExifFromImageStream(stream: stream.Stream): Readable {
        return stream.pipe(new ExifTransformer());
    }

    private bufferToStream(buffer: Buffer): stream.PassThrough {
        const bufferStream = new stream.PassThrough();
        bufferStream.end(buffer);
        return bufferStream;
    }

    private streamToBuffer(stream: stream.Stream): Promise<Buffer> {
        return new Promise((resolve) => {
            const chunks: Array<Buffer> = [];
            stream.on('data', (data) => chunks.push(data)).on('end', () => resolve(Buffer.concat(chunks)));
        });
    }
}