was-sticker 0.1.0
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/LICENSE +21 -0
- package/README.md +114 -0
- package/package.json +40 -0
- package/src/cli.js +73 -0
- package/src/index.js +235 -0
- package/src/mime.js +33 -0
- package/templates/pulse/animation.json +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thxmxx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# was-sticker
|
|
2
|
+
|
|
3
|
+
[](https://github.com/thxmxx/was-sticker/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/was-sticker)
|
|
5
|
+
|
|
6
|
+
Build WhatsApp animated stickers (`.was`) from an image plus a Lottie template — pure JS, no shell `zip` binary required.
|
|
7
|
+
|
|
8
|
+
- ✅ **Zero shell dependencies** — uses [JSZip](https://stuk.github.io/jszip/), works on Linux, macOS, Windows, Termux.
|
|
9
|
+
- ✅ **Async everywhere** — non-blocking I/O.
|
|
10
|
+
- ✅ **Flexible inputs** — template can be a folder, a JSON file, a `Buffer`, or a parsed object.
|
|
11
|
+
- ✅ **Flexible asset selection** — by index, asset id, or a predicate. Defaults to the first base64 image.
|
|
12
|
+
- ✅ **Returns a `Buffer`** — feed it directly to Baileys without touching disk.
|
|
13
|
+
- ✅ **MIME sniffing** — detects PNG/JPG/WEBP from extension *or* magic bytes.
|
|
14
|
+
- ✅ **CLI included** — `npx was-sticker -i face.png -t ./template -o out.was`.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install was-sticker
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Node.js ≥ 18.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { buildLottieSticker, bundledTemplate } from 'was-sticker';
|
|
28
|
+
|
|
29
|
+
const { buffer } = await buildLottieSticker({
|
|
30
|
+
image: 'face.png',
|
|
31
|
+
template: bundledTemplate('pulse'),
|
|
32
|
+
});
|
|
33
|
+
// → buffer is a ready-to-send `.was` sticker
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The package ships with a `pulse` template (1-second scale pulse, 30 fps) so you can produce a sticker out of the box without sourcing a Lottie file.
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { buildLottieSticker } from 'was-sticker';
|
|
42
|
+
|
|
43
|
+
const { buffer, output, mime } = await buildLottieSticker({
|
|
44
|
+
image: 'face.png', // path | Buffer | { buffer, path, mime }
|
|
45
|
+
template: './templates/heart', // folder | JSON path | Buffer | parsed object
|
|
46
|
+
output: './out/heart.was', // optional — omit to keep it in-memory
|
|
47
|
+
assetSelector: 0, // optional — number | string (id) | (asset) => boolean
|
|
48
|
+
jsonEntryName: 'animation/anim.json', // optional — path inside the .was for the JSON
|
|
49
|
+
extraFiles: { 'meta.json': '{}' }, // optional — additional files to bundle
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Input form | What it does |
|
|
54
|
+
| ------------------- | ----------------------------------------------------------- |
|
|
55
|
+
| `image: 'face.png'` | Reads file, sniffs MIME from extension and magic bytes. |
|
|
56
|
+
| `image: buffer` | Sniffs MIME from magic bytes. |
|
|
57
|
+
| `image: { buffer, mime }` | Trust caller-provided MIME. |
|
|
58
|
+
| `template: './folder'` | Walks folder; treats `*.json` as the Lottie, bundles the rest. |
|
|
59
|
+
| `template: './lottie.json'` | Reads and parses the single file. |
|
|
60
|
+
| `template: object` | Uses the parsed Lottie (deep-cloned, not mutated). |
|
|
61
|
+
|
|
62
|
+
The return value:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
{ buffer: Buffer, output?: string, mime: 'application/was' }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## CLI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
was-sticker --image face.png --template ./templates/heart --out heart.was
|
|
72
|
+
was-sticker -i face.png -t lottie.json -s image_0
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Baileys integration
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import { buildLottieSticker } from 'was-sticker';
|
|
79
|
+
|
|
80
|
+
const { buffer } = await buildLottieSticker({
|
|
81
|
+
image: incomingPhotoBuffer, // a Buffer you already have in memory
|
|
82
|
+
template: './templates/heart',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await sock.sendMessage(jid, {
|
|
86
|
+
sticker: buffer,
|
|
87
|
+
mimetype: 'application/was',
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Bring your own template
|
|
92
|
+
|
|
93
|
+
A `.was` is just a ZIP archive containing a Lottie JSON (and any sibling assets the animation references). To use a custom template:
|
|
94
|
+
|
|
95
|
+
1. Find or design a Lottie animation that embeds the image you want to replace as a base64 `data:` URI inside its `assets[]` entry.
|
|
96
|
+
2. Drop it into a folder, e.g. `./templates/heart/animation/animation.json`.
|
|
97
|
+
3. Pass that folder as `template`.
|
|
98
|
+
|
|
99
|
+
If your Lottie file references external assets (sibling PNGs, fonts, etc.), keep them in the same folder — they are bundled into the `.was` automatically.
|
|
100
|
+
|
|
101
|
+
## Roadmap / suggested improvements
|
|
102
|
+
|
|
103
|
+
These are deliberately *not* in v0.1 to keep the surface area small, but each one would be a natural follow-up:
|
|
104
|
+
|
|
105
|
+
- **`sharp` preprocessing hook** — auto-resize to 512×512, convert to WebP, enforce WhatsApp's ~500 KB sticker budget. Keep it as an optional peer dep.
|
|
106
|
+
- **Template registry** — ship a few permissively-licensed Lottie templates (heart, fire, sparkles) so users don't have to source one.
|
|
107
|
+
- **Multi-frame stickers** — accept an array of images, write each into a separate animated layer.
|
|
108
|
+
- **Validation pass** — warn when the produced `.was` exceeds WhatsApp's recommended size, or when frame rate × duration looks off.
|
|
109
|
+
- **TypeScript declarations** — emit `.d.ts` alongside the JSDoc.
|
|
110
|
+
- **Streaming output** — for very large bundles, stream the ZIP to disk instead of buffering.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT.
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "was-sticker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Build WhatsApp animated stickers (.was) from images and Lottie templates. Pure JS, no shell dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"was-sticker": "./src/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"templates",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"example": "node examples/build.js",
|
|
24
|
+
"test": "node --test test/smoke.test.js"
|
|
25
|
+
},
|
|
26
|
+
"keywords": ["whatsapp", "sticker", "lottie", "was", "animated-sticker", "baileys"],
|
|
27
|
+
"author": "thxmxx",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/thxmxx/was-sticker.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/thxmxx/was-sticker#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/thxmxx/was-sticker/issues"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"jszip": "^3.10.1"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { buildLottieSticker } from './index.js';
|
|
6
|
+
|
|
7
|
+
const USAGE = `was-sticker — build WhatsApp animated stickers from a Lottie template.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
was-sticker --image <path> --template <path> [--out sticker.was]
|
|
11
|
+
[--selector <id|index>] [--json-entry <path-in-archive>]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-i, --image Image file (PNG, JPG, WEBP). [required]
|
|
15
|
+
-t, --template Lottie folder, JSON file, or pre-parsed JSON. [required]
|
|
16
|
+
-o, --out Output .was path. Default: ./sticker.was
|
|
17
|
+
-s, --selector Asset id (string) or 0-based index.
|
|
18
|
+
--json-entry Path of the Lottie JSON inside the .was.
|
|
19
|
+
-h, --help Show this help.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
was-sticker -i face.png -t ./templates/heart -o heart.was
|
|
23
|
+
was-sticker -i face.png -t lottie.json -s image_0
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
function fail(msg, code = 1) {
|
|
27
|
+
process.stderr.write(`${msg}\n`);
|
|
28
|
+
process.exit(code);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { values } = parseArgs({
|
|
32
|
+
options: {
|
|
33
|
+
image: { type: 'string', short: 'i' },
|
|
34
|
+
template: { type: 'string', short: 't' },
|
|
35
|
+
out: { type: 'string', short: 'o' },
|
|
36
|
+
selector: { type: 'string', short: 's' },
|
|
37
|
+
'json-entry': { type: 'string' },
|
|
38
|
+
help: { type: 'boolean', short: 'h' },
|
|
39
|
+
},
|
|
40
|
+
allowPositionals: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (values.help) {
|
|
44
|
+
process.stdout.write(USAGE);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!values.image || !values.template) {
|
|
49
|
+
process.stderr.write(USAGE);
|
|
50
|
+
fail('\nMissing required --image or --template.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const output = resolve(values.out ?? './sticker.was');
|
|
54
|
+
|
|
55
|
+
const selector =
|
|
56
|
+
values.selector == null
|
|
57
|
+
? undefined
|
|
58
|
+
: /^\d+$/.test(values.selector)
|
|
59
|
+
? Number(values.selector)
|
|
60
|
+
: values.selector;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const { output: written, buffer } = await buildLottieSticker({
|
|
64
|
+
image: values.image,
|
|
65
|
+
template: values.template,
|
|
66
|
+
output,
|
|
67
|
+
assetSelector: selector,
|
|
68
|
+
jsonEntryName: values['json-entry'],
|
|
69
|
+
});
|
|
70
|
+
process.stdout.write(`${written} (${buffer.length.toLocaleString()} bytes)\n`);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
fail(`Error: ${err.message}`);
|
|
73
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, stat, readdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import JSZip from 'jszip';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
SUPPORTED_MIMES,
|
|
8
|
+
detectMimeFromBuffer,
|
|
9
|
+
detectMimeFromExtension,
|
|
10
|
+
} from './mime.js';
|
|
11
|
+
|
|
12
|
+
const TEMPLATES_DIR = fileURLToPath(new URL('../templates/', import.meta.url));
|
|
13
|
+
const BUNDLED_TEMPLATE_NAMES = Object.freeze(['pulse']);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the absolute path of a Lottie template bundled with this package.
|
|
17
|
+
* @param {'pulse'} [name='pulse']
|
|
18
|
+
* @returns {string} absolute path to the template's JSON file
|
|
19
|
+
*/
|
|
20
|
+
export function bundledTemplate(name = 'pulse') {
|
|
21
|
+
if (!BUNDLED_TEMPLATE_NAMES.includes(name)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Unknown bundled template "${name}". Available: ${BUNDLED_TEMPLATE_NAMES.join(', ')}.`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return join(TEMPLATES_DIR, name, 'animation.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_JSON_ENTRY = 'animation/animation.json';
|
|
30
|
+
|
|
31
|
+
const toDataUri = (buffer, mime) =>
|
|
32
|
+
`data:${mime};base64,${buffer.toString('base64')}`;
|
|
33
|
+
|
|
34
|
+
async function resolveImage(image) {
|
|
35
|
+
if (image == null) throw new Error('image is required.');
|
|
36
|
+
|
|
37
|
+
let buffer;
|
|
38
|
+
let mime;
|
|
39
|
+
let path;
|
|
40
|
+
|
|
41
|
+
if (typeof image === 'string') {
|
|
42
|
+
path = image;
|
|
43
|
+
} else if (Buffer.isBuffer(image)) {
|
|
44
|
+
buffer = image;
|
|
45
|
+
} else if (typeof image === 'object') {
|
|
46
|
+
({ buffer, mime, path } = image);
|
|
47
|
+
} else {
|
|
48
|
+
throw new Error('image must be a string path, Buffer, or { buffer | path, mime? }.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!buffer) {
|
|
52
|
+
if (!path) throw new Error('image must include `buffer` or `path`.');
|
|
53
|
+
buffer = await readFile(path);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
mime ??= detectMimeFromExtension(path) ?? detectMimeFromBuffer(buffer);
|
|
57
|
+
if (!mime) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Unable to detect image mime. Supported: ${SUPPORTED_MIMES.join(', ')}.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!SUPPORTED_MIMES.includes(mime)) {
|
|
63
|
+
throw new Error(`Unsupported mime "${mime}". Use one of: ${SUPPORTED_MIMES.join(', ')}.`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { buffer, mime };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function walkFiles(root) {
|
|
70
|
+
const out = [];
|
|
71
|
+
async function visit(dir) {
|
|
72
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
73
|
+
const full = join(dir, entry.name);
|
|
74
|
+
if (entry.isDirectory()) await visit(full);
|
|
75
|
+
else if (entry.isFile()) out.push(full);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
await visit(root);
|
|
79
|
+
return out.map((abs) => ({ abs, rel: relative(root, abs).split(sep).join('/') }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function resolveTemplate(template, jsonEntryName) {
|
|
83
|
+
if (template == null) {
|
|
84
|
+
throw new Error('template is required (folder path, JSON file path, Buffer, or parsed object).');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof template === 'string') {
|
|
88
|
+
const info = await stat(template);
|
|
89
|
+
if (info.isDirectory()) {
|
|
90
|
+
const files = await walkFiles(template);
|
|
91
|
+
const candidate =
|
|
92
|
+
files.find((f) => f.rel === jsonEntryName) ??
|
|
93
|
+
files.find((f) => f.rel.toLowerCase().endsWith('.json'));
|
|
94
|
+
if (!candidate) {
|
|
95
|
+
throw new Error(`No JSON file found in template folder "${template}".`);
|
|
96
|
+
}
|
|
97
|
+
const lottie = JSON.parse(await readFile(candidate.abs, 'utf8'));
|
|
98
|
+
const extras = await Promise.all(
|
|
99
|
+
files
|
|
100
|
+
.filter((f) => f.abs !== candidate.abs)
|
|
101
|
+
.map(async (f) => [f.rel, await readFile(f.abs)]),
|
|
102
|
+
);
|
|
103
|
+
return {
|
|
104
|
+
lottie,
|
|
105
|
+
jsonEntryName: candidate.rel,
|
|
106
|
+
extraFiles: Object.fromEntries(extras),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
lottie: JSON.parse(await readFile(template, 'utf8')),
|
|
111
|
+
jsonEntryName: jsonEntryName ?? DEFAULT_JSON_ENTRY,
|
|
112
|
+
extraFiles: {},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Buffer.isBuffer(template)) {
|
|
117
|
+
return {
|
|
118
|
+
lottie: JSON.parse(template.toString('utf8')),
|
|
119
|
+
jsonEntryName: jsonEntryName ?? DEFAULT_JSON_ENTRY,
|
|
120
|
+
extraFiles: {},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof template === 'object') {
|
|
125
|
+
return {
|
|
126
|
+
lottie: structuredClone(template),
|
|
127
|
+
jsonEntryName: jsonEntryName ?? DEFAULT_JSON_ENTRY,
|
|
128
|
+
extraFiles: {},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw new Error('template must be a path, Buffer, or parsed Lottie object.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function findImageAsset(lottie, selector) {
|
|
136
|
+
if (!lottie || !Array.isArray(lottie.assets) || lottie.assets.length === 0) {
|
|
137
|
+
throw new Error('Lottie template has no `assets` array.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const isImageAsset = (a) => typeof a?.p === 'string' && a.p.startsWith('data:image/');
|
|
141
|
+
|
|
142
|
+
if (selector == null) {
|
|
143
|
+
const found = lottie.assets.find(isImageAsset);
|
|
144
|
+
if (!found) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
'No embedded base64 image asset found. Pass assetSelector to target a specific asset.',
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return found;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof selector === 'number') {
|
|
153
|
+
const found = lottie.assets[selector];
|
|
154
|
+
if (!found) throw new Error(`No asset at index ${selector}.`);
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (typeof selector === 'string') {
|
|
159
|
+
const found = lottie.assets.find((a) => a?.id === selector);
|
|
160
|
+
if (!found) throw new Error(`No asset with id "${selector}".`);
|
|
161
|
+
return found;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof selector === 'function') {
|
|
165
|
+
const found = lottie.assets.find(selector);
|
|
166
|
+
if (!found) throw new Error('assetSelector did not match any asset.');
|
|
167
|
+
return found;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new Error('assetSelector must be a number, string, function, or undefined.');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Build a WhatsApp animated sticker (`.was`) from an image and a Lottie template.
|
|
175
|
+
*
|
|
176
|
+
* @param {object} options
|
|
177
|
+
* @param {string | Buffer | { buffer?: Buffer, path?: string, mime?: string }} options.image
|
|
178
|
+
* Image source. Either a file path, a raw Buffer, or an object with `buffer`/`path` and optional `mime`.
|
|
179
|
+
* @param {string | Buffer | object} options.template
|
|
180
|
+
* Template source: a folder path, a JSON file path, a raw Buffer, or a parsed Lottie object.
|
|
181
|
+
* @param {string} [options.output]
|
|
182
|
+
* If provided, the built `.was` is written here. The directory is created if missing.
|
|
183
|
+
* @param {number | string | ((asset: object) => boolean)} [options.assetSelector]
|
|
184
|
+
* Which asset to swap. By default, the first asset with an embedded base64 image.
|
|
185
|
+
* @param {string} [options.jsonEntryName]
|
|
186
|
+
* Path of the Lottie JSON inside the resulting archive. Defaults to the template's own path,
|
|
187
|
+
* or "animation/animation.json" when the template is a single JSON.
|
|
188
|
+
* @param {Record<string, Buffer | string>} [options.extraFiles]
|
|
189
|
+
* Additional files to include in the archive (merged on top of folder-template files).
|
|
190
|
+
* @returns {Promise<{ buffer: Buffer, output?: string, mime: 'application/was' }>}
|
|
191
|
+
*/
|
|
192
|
+
export async function buildLottieSticker({
|
|
193
|
+
image,
|
|
194
|
+
template,
|
|
195
|
+
output,
|
|
196
|
+
assetSelector,
|
|
197
|
+
jsonEntryName,
|
|
198
|
+
extraFiles,
|
|
199
|
+
} = {}) {
|
|
200
|
+
const [{ buffer: imageBuffer, mime }, resolved] = await Promise.all([
|
|
201
|
+
resolveImage(image),
|
|
202
|
+
resolveTemplate(template, jsonEntryName),
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
const asset = findImageAsset(resolved.lottie, assetSelector);
|
|
206
|
+
asset.p = toDataUri(imageBuffer, mime);
|
|
207
|
+
if ('u' in asset) asset.u = '';
|
|
208
|
+
|
|
209
|
+
const zip = new JSZip();
|
|
210
|
+
for (const [name, contents] of Object.entries(resolved.extraFiles)) {
|
|
211
|
+
zip.file(name, contents);
|
|
212
|
+
}
|
|
213
|
+
zip.file(resolved.jsonEntryName, JSON.stringify(resolved.lottie));
|
|
214
|
+
if (extraFiles) {
|
|
215
|
+
for (const [name, contents] of Object.entries(extraFiles)) {
|
|
216
|
+
zip.file(name, contents);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const wasBuffer = await zip.generateAsync({
|
|
221
|
+
type: 'nodebuffer',
|
|
222
|
+
compression: 'DEFLATE',
|
|
223
|
+
compressionOptions: { level: 6 },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (output) {
|
|
227
|
+
await mkdir(dirname(resolve(output)), { recursive: true });
|
|
228
|
+
await writeFile(output, wasBuffer);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { buffer: wasBuffer, output, mime: 'application/was' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export { SUPPORTED_MIMES, detectMimeFromBuffer, detectMimeFromExtension };
|
|
235
|
+
export { BUNDLED_TEMPLATE_NAMES };
|
package/src/mime.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
|
|
3
|
+
const MIME_BY_EXT = Object.freeze({
|
|
4
|
+
'.png': 'image/png',
|
|
5
|
+
'.jpg': 'image/jpeg',
|
|
6
|
+
'.jpeg': 'image/jpeg',
|
|
7
|
+
'.webp': 'image/webp',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const MAGIC = [
|
|
11
|
+
{ mime: 'image/png', bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
|
|
12
|
+
{ mime: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },
|
|
13
|
+
{ mime: 'image/webp', bytes: [0x52, 0x49, 0x46, 0x46], suffixAt: 8, suffix: [0x57, 0x45, 0x42, 0x50] },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const SUPPORTED_MIMES = Object.freeze([...new Set(Object.values(MIME_BY_EXT))]);
|
|
17
|
+
|
|
18
|
+
export function detectMimeFromExtension(filePath) {
|
|
19
|
+
if (!filePath) return null;
|
|
20
|
+
return MIME_BY_EXT[extname(String(filePath)).toLowerCase()] ?? null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function detectMimeFromBuffer(buffer) {
|
|
24
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 12) return null;
|
|
25
|
+
|
|
26
|
+
for (const { mime, bytes, suffixAt, suffix } of MAGIC) {
|
|
27
|
+
const headOk = bytes.every((b, i) => buffer[i] === b);
|
|
28
|
+
if (!headOk) continue;
|
|
29
|
+
if (suffix && !suffix.every((b, i) => buffer[suffixAt + i] === b)) continue;
|
|
30
|
+
return mime;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"v":"5.7.1","fr":30,"ip":0,"op":30,"w":512,"h":512,"nm":"pulse","ddd":0,"assets":[{"id":"image_0","w":512,"h":512,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGD4DwABBAEAfbLI3wAAAABJRU5ErkJggg=="}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"image","refId":"image_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[256,256,0]},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":15,"s":[115,115,100]},{"t":30,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":30,"st":0,"bm":0}],"markers":[]}
|