/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import sharp from 'sharp'; import {rgbaToThumbHash} from 'thumbhash'; export const generatePlaceholder = async (imageBuffer: Buffer): Promise => { const {data, info} = await sharp(imageBuffer) .blur(10) .resize(100, 100, {fit: 'inside', withoutEnlargement: true}) .ensureAlpha() .raw() .toBuffer({resolveWithObject: true}); if (data.length !== info.width * info.height * 4) { throw new Error('Unexpected data length'); } const placeholder = rgbaToThumbHash(info.width, info.height, data); return Buffer.from(placeholder).toString('base64'); }; export const processImage = async (opts: { buffer: Buffer; width: number; height: number; format: string; quality: string; animated: boolean; }): Promise => { const metadata = await sharp(opts.buffer).metadata(); const resizeWidth = Math.min(opts.width, metadata.width || 0); const resizeHeight = Math.min(opts.height, metadata.height || 0); return sharp(opts.buffer, { animated: opts.format === 'gif' || (opts.format === 'webp' && opts.animated), }) .resize(resizeWidth, resizeHeight, { fit: 'cover', withoutEnlargement: true, }) .toFormat(opts.format as keyof sharp.FormatEnum, { quality: opts.quality === 'high' ? 80 : opts.quality === 'low' ? 20 : 100, }) .toBuffer(); };