(inputBuffer, isAnimated, cropSquare)
| 9 | const { stickercropFromBuffer } = require('./stickercrop'); |
| 10 | |
| 11 | async function convertBufferToStickerWebp(inputBuffer, isAnimated, cropSquare) { |
| 12 | const tmpDir = path.join(process.cwd(), 'tmp'); |
| 13 | if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true }); |
| 14 | |
| 15 | const tempInputBase = path.join(tmpDir, `igs_${Date.now()}_${Math.random().toString(36).slice(2)}`); |
| 16 | const tempInput = isAnimated ? `${tempInputBase}.mp4` : `${tempInputBase}.jpg`; |
| 17 | const tempOutput = path.join(tmpDir, `igs_out_${Date.now()}_${Math.random().toString(36).slice(2)}.webp`); |
| 18 | |
| 19 | fs.writeFileSync(tempInput, inputBuffer); |
| 20 | |
| 21 | // Deferred cleanup to avoid race with WhatsApp download |
| 22 | const filesToDelete = []; |
| 23 | const scheduleDelete = (p) => { |
| 24 | if (!p) return; |
| 25 | filesToDelete.push(p); |
| 26 | setTimeout(() => { |
| 27 | try { fs.unlinkSync(p); } catch {} |
| 28 | }, 5000); |
| 29 | }; |
| 30 | |
| 31 | // Image filters |
| 32 | const vfCropSquareImg = "crop=min(iw\\,ih):min(iw\\,ih),scale=512:512"; |
| 33 | const vfPadSquareImg = "scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:(ow-iw)/2:(oh-ih)/2:color=#00000000"; |
| 34 | |
| 35 | let ffmpegCommand; |
| 36 | if (isAnimated) { |
| 37 | // For videos/GIFs |
| 38 | const isLargeVideo = inputBuffer.length > (5 * 1024 * 1024); // >5MB |
| 39 | const maxDuration = isLargeVideo ? 2 : 3; |
| 40 | // Match stickercrop.js style compression |
| 41 | if (cropSquare) { |
| 42 | if (isLargeVideo) { |
| 43 | ffmpegCommand = `ffmpeg -y -i "${tempInput}" -t 2 -vf "crop=min(iw\\,ih):min(iw\\,ih),scale=512:512,fps=8" -c:v libwebp -preset default -loop 0 -vsync 0 -pix_fmt yuva420p -quality 30 -compression_level 6 -b:v 100k -max_muxing_queue_size 1024 "${tempOutput}"`; |
| 44 | } else { |
| 45 | ffmpegCommand = `ffmpeg -y -i "${tempInput}" -t 3 -vf "crop=min(iw\\,ih):min(iw\\,ih),scale=512:512,fps=12" -c:v libwebp -preset default -loop 0 -vsync 0 -pix_fmt yuva420p -quality 50 -compression_level 6 -b:v 150k -max_muxing_queue_size 1024 "${tempOutput}"`; |
| 46 | } |
| 47 | } else { |
| 48 | if (isLargeVideo) { |
| 49 | ffmpegCommand = `ffmpeg -y -i "${tempInput}" -t 2 -vf "scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:(ow-iw)/2:(oh-ih)/2:color=#00000000,fps=8" -c:v libwebp -preset default -loop 0 -vsync 0 -pix_fmt yuva420p -quality 35 -compression_level 6 -b:v 100k -max_muxing_queue_size 1024 "${tempOutput}"`; |
| 50 | } else { |
| 51 | ffmpegCommand = `ffmpeg -y -i "${tempInput}" -t 3 -vf "scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:(ow-iw)/2:(oh-ih)/2:color=#00000000,fps=12" -c:v libwebp -preset default -loop 0 -vsync 0 -pix_fmt yuva420p -quality 45 -compression_level 6 -b:v 150k -max_muxing_queue_size 1024 "${tempOutput}"`; |
| 52 | } |
| 53 | } |
| 54 | } else { |
| 55 | // For images |
| 56 | const vf = `${cropSquare ? vfCropSquareImg : vfPadSquareImg},format=rgba`; |
| 57 | ffmpegCommand = `ffmpeg -y -i "${tempInput}" -vf "${vf}" -c:v libwebp -preset default -loop 0 -vsync 0 -pix_fmt yuva420p -quality 75 -compression_level 6 "${tempOutput}"`; |
| 58 | } |
| 59 | |
| 60 | await new Promise((resolve, reject) => { |
| 61 | exec(ffmpegCommand, (error, _stdout, _stderr) => { |
| 62 | if (error) return reject(error); |
| 63 | resolve(); |
| 64 | }); |
| 65 | }); |
| 66 | |
| 67 | // If output is too large (> ~1MB), do a harsher second pass for videos |
| 68 | let webpBuffer = fs.readFileSync(tempOutput); |
no test coverage detected