sticker.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import { dirname } from 'path'
  2. import { fileURLToPath } from 'url'
  3. import * as fs from 'fs'
  4. import * as path from 'path'
  5. import * as crypto from 'crypto'
  6. import { ffmpeg } from './converter.js'
  7. import fluent_ffmpeg from 'fluent-ffmpeg'
  8. import { spawn } from 'child_process'
  9. import uploadFile from './uploadFile.js'
  10. import uploadImage from './uploadImage.js'
  11. import { fileTypeFromBuffer } from 'file-type'
  12. import webp from 'node-webpmux'
  13. import fetch from 'node-fetch'
  14. const __dirname = dirname(fileURLToPath(import.meta.url))
  15. const tmp = path.join(__dirname, '../tmp')
  16. /**
  17. * Image to Sticker
  18. * @param {Buffer} img Image Buffer
  19. * @param {String} url Image URL
  20. */
  21. function sticker2(img, url) {
  22. return new Promise(async (resolve, reject) => {
  23. try {
  24. if (url) {
  25. const res = await fetch(url)
  26. if (res.status !== 200) throw await res.text()
  27. img = await res.buffer()
  28. }
  29. const inp = path.join(tmp, +new Date + '.jpeg')
  30. await fs.promises.writeFile(inp, img)
  31. const ff = spawn('ffmpeg', [
  32. '-y',
  33. '-i', inp,
  34. '-vf', 'scale=512:512:flags=lanczos:force_original_aspect_ratio=decrease,format=rgba,pad=512:512:(ow-iw)/2:(oh-ih)/2:color=#00000000,setsar=1',
  35. '-f', 'png',
  36. '-'
  37. ])
  38. ff.on('error', reject)
  39. ff.on('close', async () => {
  40. await fs.promises.unlink(inp)
  41. })
  42. const bufs = []
  43. const [_spawnprocess, ..._spawnargs] = [...(module.exports.support.gm ? ['gm'] : module.exports.magick ? ['magick'] : []), 'convert', 'png:-', 'webp:-']
  44. const im = spawn(_spawnprocess, _spawnargs)
  45. im.on('error', e => conn.reply(m.chat, util.format(e), m))
  46. im.stdout.on('data', chunk => bufs.push(chunk))
  47. ff.stdout.pipe(im.stdin)
  48. im.on('exit', () => {
  49. resolve(Buffer.concat(bufs))
  50. })
  51. } catch (e) {
  52. reject(e)
  53. }
  54. })
  55. }
  56. /**
  57. * Image/Video to Sticker
  58. * @param {Buffer} img Image/Video Buffer
  59. * @param {String} url Image/Video URL
  60. * @param {String} packname EXIF Packname
  61. * @param {String} author EXIF Author
  62. */
  63. async function sticker3(img, url, packname, author) {
  64. url = url ? url : await uploadFile(img)
  65. const res = await fetch('https://api.xteam.xyz/sticker/wm?' + new URLSearchParams(Object.entries({
  66. url,
  67. packname,
  68. author
  69. })))
  70. return await res.buffer()
  71. }
  72. /**
  73. * Image to Sticker
  74. * @param {Buffer} img Image/Video Buffer
  75. * @param {String} url Image/Video URL
  76. */
  77. async function sticker4(img, url) {
  78. if (url) {
  79. const res = await fetch(url)
  80. if (res.status !== 200) throw await res.text()
  81. img = await res.buffer()
  82. }
  83. return await ffmpeg(img, [
  84. '-vf', 'scale=512:512:flags=lanczos:force_original_aspect_ratio=decrease,format=rgba,pad=512:512:(ow-iw)/2:(oh-ih)/2:color=#00000000,setsar=1'
  85. ], 'jpeg', 'webp')
  86. }
  87. async function sticker5(img, url, packname, author, categories = [''], extra = {}) {
  88. const { Sticker } = await import('wa-sticker-formatter')
  89. const stickerMetadata = {
  90. type: 'default',
  91. pack: packname,
  92. author,
  93. categories,
  94. ...extra
  95. }
  96. return (new Sticker(img ? img : url, stickerMetadata)).toBuffer()
  97. }
  98. /**
  99. * Convert using fluent-ffmpeg
  100. * @param {string} img
  101. * @param {string} url
  102. */
  103. function sticker6(img, url) {
  104. return new Promise(async (resolve, reject) => {
  105. if (url) {
  106. const res = await fetch(url)
  107. if (res.status !== 200) throw await res.text()
  108. img = await res.buffer()
  109. }
  110. const type = await fileTypeFromBuffer(img) || {
  111. mime: 'application/octet-stream',
  112. ext: 'bin'
  113. }
  114. if (type.ext == 'bin') reject(img)
  115. const tmp = path.join(__dirname, `../tmp/${+ new Date()}.${type.ext}`)
  116. const out = path.join(tmp + '.webp')
  117. await fs.promises.writeFile(tmp, img)
  118. // https://github.com/MhankBarBar/termux-wabot/blob/main/index.js#L313#L368
  119. const Fffmpeg = /video/i.test(type.mime) ? fluent_ffmpeg(tmp).inputFormat(type.ext) : fluent_ffmpeg(tmp).input(tmp)
  120. Fffmpeg
  121. .on('error', function (err) {
  122. console.error(err)
  123. fs.promises.unlink(tmp)
  124. reject(img)
  125. })
  126. .on('end', async function () {
  127. fs.promises.unlink(tmp)
  128. resolve(await fs.promises.readFile(out))
  129. })
  130. .addOutputOptions([
  131. `-vcodec`, `libwebp`, `-vf`,
  132. `scale='min(320,iw)':min'(320,ih)':force_original_aspect_ratio=decrease,fps=15, pad=320:320:-1:-1:color=white@0.0, split [a][b]; [a] palettegen=reserve_transparent=on:transparency_color=ffffff [p]; [b][p] paletteuse`
  133. ])
  134. .toFormat('webp')
  135. .save(out)
  136. })
  137. }
  138. /**
  139. * Add WhatsApp JSON Exif Metadata
  140. * Taken from https://github.com/pedroslopez/whatsapp-web.js/pull/527/files
  141. * @param {Buffer} webpSticker
  142. * @param {String} packname
  143. * @param {String} author
  144. * @param {String} categories
  145. * @param {Object} extra
  146. * @returns
  147. */
  148. async function addExif(webpSticker, packname, author, categories = [''], extra = {}) {
  149. const img = new webp.Image();
  150. const stickerPackId = crypto.randomBytes(32).toString('hex');
  151. const json = { 'sticker-pack-id': stickerPackId, 'sticker-pack-name': packname, 'sticker-pack-publisher': author, 'emojis': categories, ...extra };
  152. const exifAttr = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00]);
  153. const jsonBuffer = Buffer.from(JSON.stringify(json), 'utf8');
  154. const exif = Buffer.concat([exifAttr, jsonBuffer]);
  155. exif.writeUIntLE(jsonBuffer.length, 14, 4);
  156. await img.load(webpSticker)
  157. img.exif = exif
  158. return await img.save(null)
  159. }
  160. /**
  161. * Image/Video to Sticker
  162. * @param {Buffer} img Image/Video Buffer
  163. * @param {String} url Image/Video URL
  164. * @param {...String}
  165. */
  166. async function sticker(img, url, ...args) {
  167. let lastError, stiker
  168. for (const func of [
  169. sticker3, global.support.ffmpeg && sticker6, sticker5,
  170. global.support.ffmpeg && global.support.ffmpegWebp && sticker4,
  171. global.support.ffmpeg && (global.support.convert || global.support.magick || global.support.gm) && sticker2,
  172. ].filter(f => f)) {
  173. try {
  174. stiker = await func(img, url, ...args)
  175. if (stiker.includes('html')) continue
  176. if (stiker.includes('WEBP')) {
  177. try {
  178. return await addExif(stiker, ...args)
  179. } catch (e) {
  180. console.error(e)
  181. return stiker
  182. }
  183. }
  184. throw stiker.toString()
  185. } catch (err) {
  186. lastError = err
  187. continue
  188. }
  189. }
  190. console.error(lastError)
  191. return lastError
  192. }
  193. const support = {
  194. ffmpeg: true,
  195. ffprobe: true,
  196. ffmpegWebp: true,
  197. convert: true,
  198. magick: false,
  199. gm: false,
  200. find: false
  201. }
  202. export {
  203. sticker,
  204. sticker2,
  205. sticker3,
  206. sticker4,
  207. sticker6,
  208. addExif,
  209. support
  210. }