@shibam/sticker-maker 1.3.0-rc1 → 1.3.0-rc3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/index.js +57 -0
- package/src/lib/ToWebp.js +61 -0
- package/src/lib/changeMetaInfo.js +43 -0
- package/src/lib/extractMetaData.js +14 -0
- package/src/lib/textOnGif.js +98 -0
- package/src/lib/textOnImg.js +98 -0
- package/src/lib/toGif.js +32 -0
- package/src/lib/utils.js +37 -0
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shibam/sticker-maker",
|
|
3
|
-
"version": "1.3.0-
|
|
3
|
+
"version": "1.3.0-rc3",
|
|
4
4
|
"description": "A package for creating stickers",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
8
|
-
"
|
|
8
|
+
"src"
|
|
9
9
|
],
|
|
10
10
|
"scripts": {},
|
|
11
11
|
"author": "Shibam Dey",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"typescript"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"file-type": "^
|
|
27
|
+
"file-type": "^22.0.1",
|
|
28
28
|
"fs-extra": "^11.3.5",
|
|
29
29
|
"node-webpmux": "^3.2.0"
|
|
30
30
|
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import Utils from './lib/utils.js'
|
|
3
|
+
import ToWebp from './lib/ToWebp.js'
|
|
4
|
+
import MetaInfoChanger from './lib/changeMetaInfo.js'
|
|
5
|
+
|
|
6
|
+
export default class Sticker {
|
|
7
|
+
constructor(data, metaInfo = {}) {
|
|
8
|
+
this.data = data
|
|
9
|
+
this.metaInfo = { ...metaInfo }
|
|
10
|
+
this.utils = new Utils()
|
|
11
|
+
this.buffer = null
|
|
12
|
+
this.mimeType = ''
|
|
13
|
+
this.extType = ''
|
|
14
|
+
this.initialized = false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async initialize() {
|
|
18
|
+
if (this.initialized) return
|
|
19
|
+
this.buffer = await this.utils.buffer(this.data)
|
|
20
|
+
const fileType = await this.utils.getMimeType(this.buffer)
|
|
21
|
+
this.mimeType = fileType?.mime || ''
|
|
22
|
+
this.extType = fileType?.ext || ''
|
|
23
|
+
this.metaInfo.pack ??= ''
|
|
24
|
+
this.metaInfo.author ??= ''
|
|
25
|
+
this.metaInfo.id ??= this.utils.getId()
|
|
26
|
+
this.metaInfo.category ??= []
|
|
27
|
+
this.metaInfo.type ??= 'DEFAULT'
|
|
28
|
+
this.metaInfo.quality ??= this.utils.getQuality(this.buffer)
|
|
29
|
+
this.metaInfo.text ??= ''
|
|
30
|
+
this.initialized = true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async toBuffer() {
|
|
34
|
+
await this.initialize()
|
|
35
|
+
const toWebp = await ToWebp(this.buffer, this.metaInfo, this.extType, this.mimeType)
|
|
36
|
+
return new MetaInfoChanger(this.metaInfo).add(toWebp)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async toFile(outputPath) {
|
|
40
|
+
await this.initialize()
|
|
41
|
+
const buffer = this.extType === 'webp' ? await this.changeMetaInfo() : await this.toBuffer()
|
|
42
|
+
await fs.writeFile(outputPath, buffer)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async changeMetaInfo() {
|
|
46
|
+
await this.initialize()
|
|
47
|
+
return new MetaInfoChanger(this.metaInfo).add(this.buffer)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const StickerTypes = {
|
|
52
|
+
DEFAULT: 'DEFAULT',
|
|
53
|
+
CIRCLE: 'CIRCLE',
|
|
54
|
+
SQUARE: 'SQUARE'
|
|
55
|
+
}
|
|
56
|
+
export { default as extractMetaData } from './lib/extractMetaData.js'
|
|
57
|
+
export { StickerTypes }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { promisify } from 'util'
|
|
2
|
+
import { execFile } from 'child_process'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import toGif from './toGif.js'
|
|
7
|
+
import TextOnImg from './textOnImg.js'
|
|
8
|
+
|
|
9
|
+
const exec = promisify(execFile)
|
|
10
|
+
const textOnImg = new TextOnImg()
|
|
11
|
+
|
|
12
|
+
export default async function ToWebp(buffer, metaInfo, mimeExt, mimeType) {
|
|
13
|
+
const timestamp = Date.now()
|
|
14
|
+
const inputPath = path.join(os.tmpdir(), `input_${timestamp}.${mimeExt}`)
|
|
15
|
+
const outputPath = path.join(os.tmpdir(), `output_${timestamp}.webp`)
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
if (mimeExt === 'webp') return buffer
|
|
19
|
+
|
|
20
|
+
let processedData = buffer
|
|
21
|
+
let currentExt = mimeExt
|
|
22
|
+
const isAnimated = mimeType?.includes('video') || mimeExt === 'gif'
|
|
23
|
+
|
|
24
|
+
if (mimeType?.includes('video')) {
|
|
25
|
+
processedData = await toGif(buffer, mimeExt, metaInfo.type || 'DEFAULT', metaInfo.text ?? '')
|
|
26
|
+
currentExt = 'gif'
|
|
27
|
+
} else if (metaInfo.text) {
|
|
28
|
+
processedData = await textOnImg.drawText(buffer, metaInfo.text)
|
|
29
|
+
currentExt = 'png'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await fs.writeFile(inputPath, processedData)
|
|
33
|
+
|
|
34
|
+
let videoFilter = ''
|
|
35
|
+
if (metaInfo.type === 'CIRCLE') {
|
|
36
|
+
videoFilter = 'scale=512:512:force_original_aspect_ratio=increase,crop=512:512,format=rgba,geq=r=\'r(X,Y)\':g=\'g(X,Y)\':b=\'b(X,Y)\':a=\'if(lte((X-256)*(X-256)+(Y-256)*(Y-256),65536),255,0)\''
|
|
37
|
+
} else if (metaInfo.type === 'SQUARE') {
|
|
38
|
+
videoFilter = 'scale=512:512:force_original_aspect_ratio=decrease'
|
|
39
|
+
} else {
|
|
40
|
+
videoFilter = 'scale=512:512:force_original_aspect_ratio=decrease'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ffmpegArgs = ['-y', '-i', inputPath, '-vf', videoFilter]
|
|
44
|
+
|
|
45
|
+
if (isAnimated) {
|
|
46
|
+
ffmpegArgs.push('-loop', '0', '-pix_fmt', 'yuva420p')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ffmpegArgs.push(
|
|
50
|
+
'-c:v', 'libwebp',
|
|
51
|
+
'-quality', String(metaInfo.quality || 80),
|
|
52
|
+
'-lossless', currentExt === 'gif' ? '1' : '0',
|
|
53
|
+
outputPath
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
await exec('ffmpeg', ffmpegArgs)
|
|
57
|
+
return await fs.readFile(outputPath)
|
|
58
|
+
} finally {
|
|
59
|
+
await Promise.all([fs.remove(inputPath), fs.remove(outputPath)]).catch(() => {})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import Image from 'node-webpmux'
|
|
2
|
+
import { TextEncoder } from 'util'
|
|
3
|
+
|
|
4
|
+
export default class Exif {
|
|
5
|
+
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.data = {}
|
|
8
|
+
this.data['sticker-pack-id'] = options.id || ''
|
|
9
|
+
this.data['sticker-pack-name'] = options.pack || ''
|
|
10
|
+
this.data['sticker-pack-publisher'] = options.author || ''
|
|
11
|
+
this.data['emojis'] = options.category || ['😹']
|
|
12
|
+
this.exif = null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
build = () => {
|
|
16
|
+
const data = JSON.stringify(this.data)
|
|
17
|
+
const exif = Buffer.concat([
|
|
18
|
+
Buffer.from([
|
|
19
|
+
0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00,
|
|
20
|
+
0x00, 0x16, 0x00, 0x00, 0x00
|
|
21
|
+
]),
|
|
22
|
+
Buffer.from(data, 'utf-8')
|
|
23
|
+
])
|
|
24
|
+
exif.writeUIntLE(new TextEncoder().encode(data).length, 14, 4)
|
|
25
|
+
return exif
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
add = async (image) => {
|
|
29
|
+
const exif = this.exif || this.build()
|
|
30
|
+
|
|
31
|
+
// Load the image if it is not already an instance of Image.Image.
|
|
32
|
+
image = image instanceof Image.Image
|
|
33
|
+
? image
|
|
34
|
+
: await (async () => {
|
|
35
|
+
const img = new Image.Image()
|
|
36
|
+
await img.load(image)
|
|
37
|
+
return img
|
|
38
|
+
})()
|
|
39
|
+
|
|
40
|
+
image.exif = exif
|
|
41
|
+
return await image.save(null)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Image from 'node-webpmux';
|
|
2
|
+
import Utils from './utils.js';
|
|
3
|
+
|
|
4
|
+
const utils = new Utils();
|
|
5
|
+
const extractMetadata = async (image) => {
|
|
6
|
+
const img = new Image.Image();
|
|
7
|
+
const buffer = await utils.buffer(image);
|
|
8
|
+
const fileType = await utils.getMimeType(buffer);
|
|
9
|
+
if (!fileType || fileType.ext !== 'webp') throw new Error('Unsupported file type for metadata extraction');
|
|
10
|
+
await img.load(image);
|
|
11
|
+
const exif = img.exif?.toString('utf-8') ?? '{}';
|
|
12
|
+
return JSON.parse(exif.substring(exif.indexOf('{'), exif.lastIndexOf('}') + 1) ?? '{}');
|
|
13
|
+
};
|
|
14
|
+
export default extractMetadata;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { promisify } from 'util'
|
|
2
|
+
import { execFile } from 'child_process'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
|
|
7
|
+
const exec = promisify(execFile)
|
|
8
|
+
|
|
9
|
+
export default class TextOnGif {
|
|
10
|
+
constructor(maxCharsPerLine = 20) {
|
|
11
|
+
this.maxCharsPerLine = maxCharsPerLine
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
wrapText(text, maxChars = 20) {
|
|
15
|
+
const words = text.split(' ')
|
|
16
|
+
const lines = []
|
|
17
|
+
let currentLine = ''
|
|
18
|
+
for (const word of words) {
|
|
19
|
+
const test = currentLine ? `${currentLine} ${word}` : word
|
|
20
|
+
if (test.length <= maxChars) currentLine = test
|
|
21
|
+
else {
|
|
22
|
+
if (currentLine) lines.push(currentLine)
|
|
23
|
+
currentLine = word.length > maxChars ? word.substring(0, maxChars) : word
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (currentLine) lines.push(currentLine)
|
|
27
|
+
return lines
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
escapeText(text) {
|
|
31
|
+
return text.replace(/'/g, "'\\''").replace(/:/g, '\\:').replace(/,/g, '\\,').replace(/%/g, '\\%')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
escapeFontPath(fontPath) {
|
|
35
|
+
return fontPath
|
|
36
|
+
.replace(/\\/g, '/')
|
|
37
|
+
.replace(/:/g, '\\:')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getFontPath() {
|
|
41
|
+
const fonts = {
|
|
42
|
+
win32: [
|
|
43
|
+
'C:/Windows/Fonts/arialbd.ttf',
|
|
44
|
+
'C:/Windows/Fonts/arial.ttf',
|
|
45
|
+
'C:/Windows/Fonts/segoeui.ttf'
|
|
46
|
+
],
|
|
47
|
+
darwin: [
|
|
48
|
+
'/Library/Fonts/Arial Bold.ttf',
|
|
49
|
+
'/System/Library/Fonts/Helvetica.ttc',
|
|
50
|
+
'/Library/Fonts/Arial.ttf'
|
|
51
|
+
],
|
|
52
|
+
linux: [
|
|
53
|
+
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
|
|
54
|
+
'/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf',
|
|
55
|
+
'/usr/share/fonts/truetype/freefont/FreeSansBold.ttf'
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
const list = fonts[process.platform] || fonts.linux
|
|
59
|
+
for (const f of list) if (fs.existsSync(f)) return f
|
|
60
|
+
return 'arial.ttf'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async drawText(gifBuffer, text, padding = { x: 10, y: 10 }) {
|
|
64
|
+
const inputPath = path.join(os.tmpdir(), `input_${Date.now()}.gif`)
|
|
65
|
+
const outputPath = path.join(os.tmpdir(), `output_${Date.now()}.gif`)
|
|
66
|
+
const fontPath = this.escapeFontPath(this.getFontPath())
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await fs.writeFile(inputPath, gifBuffer)
|
|
70
|
+
const { stdout } = await exec('ffprobe', [
|
|
71
|
+
'-v', 'error',
|
|
72
|
+
'-select_streams', 'v:0',
|
|
73
|
+
'-show_entries', 'stream=width,height',
|
|
74
|
+
'-of', 'csv=p=0:s=x',
|
|
75
|
+
inputPath
|
|
76
|
+
])
|
|
77
|
+
const [width, height] = stdout.trim().split('x').map(Number)
|
|
78
|
+
if (!width || !height) throw new Error('Invalid GIF dimensions')
|
|
79
|
+
|
|
80
|
+
const fontSize = Math.floor(height / 10)
|
|
81
|
+
const lineHeight = fontSize * 1.2
|
|
82
|
+
const lines = this.wrapText(text, this.maxCharsPerLine)
|
|
83
|
+
|
|
84
|
+
const filters = lines.map((line, i) => {
|
|
85
|
+
const y = Math.floor(height - padding.y - (lines.length - i) * lineHeight)
|
|
86
|
+
return `drawtext=fontfile='${fontPath}':text='${this.escapeText(line)}':x=(w-text_w)/2:y=${y}:fontsize=${fontSize}:fontcolor=white:borderw=2:bordercolor=black`
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await exec('ffmpeg', ['-y', '-i', inputPath, '-vf', filters.join(','), '-c:v', 'gif', outputPath])
|
|
90
|
+
return await fs.readFile(outputPath)
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error('TextOnGif error:', err.message)
|
|
93
|
+
throw err
|
|
94
|
+
} finally {
|
|
95
|
+
await Promise.all([fs.remove(inputPath), fs.remove(outputPath)]).catch(() => {})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { promisify } from 'util'
|
|
2
|
+
import { execFile } from 'child_process'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
|
|
7
|
+
const exec = promisify(execFile)
|
|
8
|
+
|
|
9
|
+
export default class TextOnImg {
|
|
10
|
+
constructor(maxCharsPerLine = 25) {
|
|
11
|
+
this.maxCharsPerLine = maxCharsPerLine
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
wrapText(text, maxChars = 25) {
|
|
15
|
+
const words = text.split(' ')
|
|
16
|
+
const lines = []
|
|
17
|
+
let currentLine = ''
|
|
18
|
+
for (const word of words) {
|
|
19
|
+
const test = currentLine ? `${currentLine} ${word}` : word
|
|
20
|
+
if (test.length <= maxChars) currentLine = test
|
|
21
|
+
else {
|
|
22
|
+
if (currentLine) lines.push(currentLine)
|
|
23
|
+
currentLine = word.length > maxChars ? word.substring(0, maxChars) : word
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (currentLine) lines.push(currentLine)
|
|
27
|
+
return lines
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
escapeText(text) {
|
|
31
|
+
return text.replace(/'/g, "'\\''").replace(/:/g, '\\:').replace(/,/g, '\\,').replace(/%/g, '\\%')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
escapeFontPath(fontPath) {
|
|
35
|
+
return fontPath
|
|
36
|
+
.replace(/\\/g, '/')
|
|
37
|
+
.replace(/:/g, '\\:')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getFontPath() {
|
|
41
|
+
const fonts = {
|
|
42
|
+
win32: [
|
|
43
|
+
'C:/Windows/Fonts/arialbd.ttf',
|
|
44
|
+
'C:/Windows/Fonts/arial.ttf',
|
|
45
|
+
'C:/Windows/Fonts/segoeui.ttf'
|
|
46
|
+
],
|
|
47
|
+
darwin: [
|
|
48
|
+
'/Library/Fonts/Arial Bold.ttf',
|
|
49
|
+
'/System/Library/Fonts/Helvetica.ttc',
|
|
50
|
+
'/Library/Fonts/Arial.ttf'
|
|
51
|
+
],
|
|
52
|
+
linux: [
|
|
53
|
+
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
|
|
54
|
+
'/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf',
|
|
55
|
+
'/usr/share/fonts/truetype/freefont/FreeSansBold.ttf'
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
const list = fonts[process.platform] || fonts.linux
|
|
59
|
+
for (const f of list) if (fs.existsSync(f)) return f
|
|
60
|
+
return 'arial.ttf'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async drawText(imageBuffer, text, padding = { x: 10, y: 10 }) {
|
|
64
|
+
const inputPath = path.join(os.tmpdir(), `input_${Date.now()}.png`)
|
|
65
|
+
const outputPath = path.join(os.tmpdir(), `output_${Date.now()}.png`)
|
|
66
|
+
const fontPath = this.escapeFontPath(this.getFontPath())
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await fs.writeFile(inputPath, imageBuffer)
|
|
70
|
+
const { stdout } = await exec('ffprobe', [
|
|
71
|
+
'-v', 'error',
|
|
72
|
+
'-select_streams', 'v:0',
|
|
73
|
+
'-show_entries', 'stream=width,height',
|
|
74
|
+
'-of', 'csv=p=0:s=x',
|
|
75
|
+
inputPath
|
|
76
|
+
])
|
|
77
|
+
const [width, height] = stdout.trim().split('x').map(Number)
|
|
78
|
+
if (!width || !height) throw new Error('Invalid image dimensions')
|
|
79
|
+
|
|
80
|
+
const fontSize = Math.floor(height / 10)
|
|
81
|
+
const lineHeight = fontSize * 1.2
|
|
82
|
+
const lines = this.wrapText(text, this.maxCharsPerLine)
|
|
83
|
+
|
|
84
|
+
const filters = lines.map((line, i) => {
|
|
85
|
+
const y = Math.floor(height - padding.y - (lines.length - i) * lineHeight)
|
|
86
|
+
return `drawtext=fontfile='${fontPath}':text='${this.escapeText(line)}':x=(w-text_w)/2:y=${y}:fontsize=${fontSize}:fontcolor=white:borderw=2:bordercolor=black`
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await exec('ffmpeg', ['-y', '-i', inputPath, '-vf', filters.join(','), outputPath])
|
|
90
|
+
return await fs.readFile(outputPath)
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error('TextOnImg error:', err.message)
|
|
93
|
+
throw err
|
|
94
|
+
} finally {
|
|
95
|
+
await Promise.all([fs.remove(inputPath), fs.remove(outputPath)]).catch(() => {})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/lib/toGif.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { promisify } from 'util'
|
|
2
|
+
import { execFile } from 'child_process'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import TextOnGif from './textOnGif.js'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const exec = promisify(execFile)
|
|
10
|
+
const textOnGif = new TextOnGif()
|
|
11
|
+
|
|
12
|
+
export default async function videoToGif(buffer, extType, type, text = '') {
|
|
13
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
14
|
+
const ts = Date.now()
|
|
15
|
+
const inputPath = path.join(os.tmpdir(), `in_${ts}_${attempt}.${extType}`)
|
|
16
|
+
const outputPath = path.join(os.tmpdir(), `out_${ts}_${attempt}.gif`)
|
|
17
|
+
try {
|
|
18
|
+
await fs.writeFile(inputPath, buffer)
|
|
19
|
+
const vf = type === 'SQUARE'
|
|
20
|
+
? 'scale=320:-1:flags=lanczos,fps=10,crop=min(iw\\,ih):min(iw\\,ih)'
|
|
21
|
+
: 'scale=320:-1:flags=lanczos,fps=20'
|
|
22
|
+
await exec('ffmpeg', ['-y', '-i', inputPath, '-vf', vf, '-t', '7', '-loop', '0', '-f', 'gif', outputPath])
|
|
23
|
+
let result = await fs.readFile(outputPath)
|
|
24
|
+
if (text) result = await textOnGif.drawText(result, text)
|
|
25
|
+
return result
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (attempt === 2) throw error
|
|
28
|
+
} finally {
|
|
29
|
+
await Promise.all([fs.remove(inputPath), fs.remove(outputPath)]).catch(() => {})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Readable } from 'stream'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
export default class Utils {
|
|
4
|
+
async buffer(data) {
|
|
5
|
+
try {
|
|
6
|
+
if (typeof data === 'string') return await fs.readFile(data)
|
|
7
|
+
if (data instanceof Readable) return await this.streamToBuffer(data)
|
|
8
|
+
return Buffer.from(data)
|
|
9
|
+
} catch (error) {
|
|
10
|
+
throw new Error(`Error converting to buffer: ${error}`)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async streamToBuffer(stream) {
|
|
14
|
+
const chunks = []
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
stream.on('data', chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))
|
|
17
|
+
stream.on('end', () => resolve(Buffer.concat(chunks)))
|
|
18
|
+
stream.on('error', reject)
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
getQuality(data) {
|
|
22
|
+
const bytes = Buffer.from(data).length / 1024
|
|
23
|
+
return bytes > 4096 ? 8 : bytes > 3072 ? 10 : bytes > 2048 ? 12 : 15
|
|
24
|
+
}
|
|
25
|
+
async getMimeType(data) {
|
|
26
|
+
try {
|
|
27
|
+
const { fileTypeFromBuffer } = await import('file-type')
|
|
28
|
+
return await fileTypeFromBuffer(data)
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(`Error getting MIME type: ${error}`)
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
getId() {
|
|
35
|
+
return [...Array(5)].map(() => Math.random().toString(36).substring(2, 15)).join('')
|
|
36
|
+
}
|
|
37
|
+
}
|