Skip to content

Commit 0407738

Browse files
committed
MetaData Extractor
1 parent 8968602 commit 0407738

File tree

8 files changed

+720
-102
lines changed

8 files changed

+720
-102
lines changed

bun.lockb

19.9 KB
Binary file not shown.

package-lock.json

Lines changed: 596 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"author": "Shibam Dey",
1111
"license": "MIT",
1212
"dependencies": {
13-
"file-type": "^19.0.0"
13+
"file-type": "^19.0.0",
14+
"node-webpmux": "^3.2.0",
15+
"wa-sticker-formatter": "^4.4.4"
1416
},
1517
"devDependencies": {
1618
"@types/node": "^20.10.0",

src/index.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,23 @@ import { MetaDataType } from "./types/metaInfoType";
66
import { StickerTypes } from "./types/StickerTypes";
77
import MetaInfoChanger from "./lib/changeMetaInfo";
88
import extractMetaData from "./lib/extractMetaData";
9+
910
class Sticker {
1011
private buffer: Buffer;
1112
private mimeType: string | undefined;
1213
private extType: string | undefined;
1314
private utils = new Utils();
1415
private outBuffer: Buffer;
15-
16+
private activeBuff: boolean;
17+
private activeMeta: boolean;
1618
constructor(
1719
private data: Buffer | string | Readable,
1820
public metaInfo: Partial<MetaDataType> = {},
1921
) {
2022
this.buffer = Buffer.from([]);
2123
this.outBuffer = Buffer.from([]);
22-
this.metaInfo = this.metaInfo;
24+
this.activeBuff = false;
25+
this.activeMeta = false;
2326
}
2427

2528
/**
@@ -36,9 +39,9 @@ class Sticker {
3639
this.extType = fileType?.ext;
3740
// Set default values for metaInfo if not provided
3841
this.metaInfo.pack = this.metaInfo.pack ?? "";
39-
this.metaInfo.author = this.metaInfo.author ?? "";
40-
this.metaInfo.id = this.metaInfo.id ?? this.utils.getId();
41-
this.metaInfo.category = this.metaInfo.category ?? [];
42+
this.metaInfo.author = this.metaInfo.author || "";
43+
this.metaInfo.id = this.metaInfo.id || this.utils.getId();
44+
this.metaInfo.category = this.metaInfo.category || [];
4245
this.metaInfo.type = this.metaInfo.type ?? StickerTypes.DEFAULT;
4346
this.metaInfo.quality =
4447
this.metaInfo?.quality ?? this.utils.getQuality(this.buffer);
@@ -55,14 +58,17 @@ class Sticker {
5558
public async toBuffer(): Promise<Buffer> {
5659
try {
5760
await this.initialize();
58-
this.outBuffer = await convert(
61+
let buffer = await convert(
5962
this.buffer,
6063
this.metaInfo,
6164
this.extType,
6265
this.mimeType,
6366
);
67+
this.outBuffer = await new MetaInfoChanger(this.metaInfo).add(buffer);
68+
this.activeBuff = true;
6469
return this.outBuffer ?? Buffer.from([]);
6570
} catch (error) {
71+
this.activeBuff = false;
6672
throw new Error(`Conversion to buffer failed: ${error}`);
6773
}
6874
}
@@ -74,6 +80,9 @@ class Sticker {
7480
*/
7581
public async toFile(outputPath: string): Promise<void> {
7682
try {
83+
if (!this.activeBuff && !this.activeMeta) {
84+
await this.changeMetaInfo()
85+
}
7786
await fs.promises.writeFile(outputPath, this.outBuffer);
7887
} catch (error) {
7988
console.error(`Error converting to file: ${error}`);
@@ -82,27 +91,29 @@ class Sticker {
8291
}
8392

8493
public async changeMetaInfo(
85-
newMetaInfo: Partial<any | undefined>
94+
newMetaInfo: Partial<any> = {}
8695
) : Promise<any | undefined> {
8796
try {
8897
await this.initialize()
8998
this.metaInfo = { ...this.metaInfo, ...newMetaInfo };
90-
this.outBuffer = MetaInfoChanger(this.buffer, this.metaInfo);
99+
this.outBuffer = await new MetaInfoChanger(this.metaInfo).add(this.buffer);
100+
this.activeMeta = true;
91101
return this.outBuffer;
92102
} catch (error) {
103+
this.activeMeta = false;
93104
throw new Error(`Error changing meta info: ${error}`);
94105
}
95106
}
96107

97-
public async extractMetaData () {
108+
public async extractMetaData (data:Buffer) {
98109
try {
99110
await this.initialize();
100-
let metaData = extractMetaData(this.buffer);
111+
let metaData = extractMetaData(data);
101112
return metaData;
102113
} catch (error) {
103114
throw new Error(`Error extracting meta data: ${error}`);
104115
}
105116
}
106117
}
107118

108-
export default { Sticker, StickerTypes };
119+
export default { Sticker, StickerTypes };

src/lib/ToWebp.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { exec, spawn } from "child_process";
1+
import { exec } from "child_process";
22
import { join } from "path";
33
import { readFile, writeFile, unlink } from "fs/promises";
44
import { tmpdir } from "os";
@@ -23,15 +23,15 @@ const ToWebp = async (
2323
mimeExt: string | undefined,
2424
mimeType: string | undefined,
2525
): Promise<Buffer> => {
26-
let inputPath = join(tmpdir(), `${Date.now()}.${mimeExt}`);
27-
const outputPath = join(tmpdir(), `${Date.now()}.webp`);
26+
let inputPath = join(tmpdir(), `${Date.now()}input.${mimeExt}`);
27+
const outputPath = join(tmpdir(), `${Date.now()}output.webp`);
2828

2929
try {
3030
// Write input buffer to temporary file
3131
await writeFile(inputPath, buffer);
3232
inputPath = mimeType?.includes("video")
33-
? await toGif(inputPath)
34-
: inputPath;
33+
? await toGif(inputPath)
34+
: inputPath;
3535
// Construct arguments for ffmpeg conversion
3636
const args: string[] = [
3737
"-i",
@@ -65,18 +65,6 @@ const ToWebp = async (
6565
mimeExt === "gif" ? "-lossless 1" : "-lossless 0",
6666
].join(" ")
6767
: "-c:v libwebp",
68-
metaInfo.author
69-
? `-metadata sticker-pack-publisher="${metaInfo.author}"`
70-
: "",
71-
metaInfo.pack ? `-metadata sticker-pack-name="${metaInfo.pack}"` : "",
72-
metaInfo.id ? `-metadata sticker-pack-id="${metaInfo.id}"` : "",
73-
metaInfo.quality ? `-metadata sticker-quality="${metaInfo.quality}"` : "",
74-
metaInfo.background
75-
? `-metadata sticker-background-color="${metaInfo.background}"`
76-
: "",
77-
metaInfo.category?.length
78-
? `-metadata emojis="${metaInfo.category.join(",")}"`
79-
: "",
8068
"-q:v",
8169
metaInfo.quality?.toString() ?? "50",
8270
"-y",
@@ -92,7 +80,7 @@ const ToWebp = async (
9280
throw new Error(`Conversion failed: ${error}`);
9381
} finally {
9482
// Cleanup temporary files
95-
await cleanup(inputPath, outputPath);
83+
await cleanup(inputPath, outputPath).catch((e) => console.error(e));
9684
}
9785
};
9886

src/lib/changeMetaInfo.ts

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,74 @@
1-
import { MetaDataType } from "../types/metaInfoType";
2-
import { StickerTypes } from "../types/StickerTypes";
3-
4-
function changeMetaInfo(data: Buffer, metaInfo: Partial<MetaDataType>): Buffer {
5-
try {
6-
const type = metaInfo.type === StickerTypes.CIRCLE
7-
? "circle"
8-
: metaInfo.type === StickerTypes.SQUARE
9-
? "square"
10-
: metaInfo.type === StickerTypes.FIT
11-
? "fit"
12-
: "default";
13-
14-
const updatedMetaData: MetaDataType = { type, ...restMetaInfo };
15-
const metaDataString = JSON.stringify(updatedMetaData);
16-
const updatedData = Buffer.concat([data, Buffer.from(metaDataString)]);
17-
return updatedData;
18-
} catch (error: unknown) {
19-
throw new Error(`Error changing meta info: ${error}`);
20-
}
21-
}
22-
23-
export default changeMetaInfo;
1+
import Image from "node-webpmux";
2+
import { TextEncoder } from "util";
3+
4+
/**
5+
* The Exif class is responsible for handling the metadata (Exif data)
6+
* for sticker images, particularly those used in messaging applications.
7+
*/
8+
export default class Exif {
9+
private data: any = {};
10+
private exif: Buffer | null = null;
11+
12+
/**
13+
* Constructs an Exif instance with the given options.
14+
* @param options - An object containing metadata for the sticker.
15+
*/
16+
constructor(options: any) {
17+
this.data["sticker-pack-id"] = options.id;
18+
this.data["sticker-pack-name"] = options.pack || "";
19+
this.data["sticker-pack-publisher"] = options.author || "";
20+
this.data["emojis"] = options.category || [];
21+
this.data["sticker-quality"] = options.quality || "30";
22+
this.data["sticker-background"] = options.background || "";
23+
this.data = { ...this.data, ...options };
24+
}
25+
26+
/**
27+
* Builds the Exif metadata as a Buffer.
28+
* @returns A Buffer containing the constructed Exif data.
29+
*/
30+
build = (): Buffer => {
31+
const data = JSON.stringify(this.data);
32+
33+
// Construct the Exif header and data sections.
34+
const exif = Buffer.concat([
35+
Buffer.from([
36+
0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00,
37+
0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00,
38+
0x00, 0x00,
39+
]),
40+
Buffer.from(data, "utf-8"),
41+
]);
42+
43+
// Write the length of the data into the Exif header.
44+
exif.writeUIntLE(new TextEncoder().encode(data).length, 14, 4);
45+
return exif;
46+
};
47+
48+
/**
49+
* Adds the Exif metadata to the given image.
50+
* @param image - A Buffer or Image instance representing the image to which Exif data should be added.
51+
* @returns A Promise that resolves to a Buffer containing the image with the added Exif data.
52+
*/
53+
add = async (image: Buffer | Image.Image): Promise<Buffer> => {
54+
const exif = this.exif || this.build();
55+
56+
// Load the image if it is not already an instance of Image.Image.
57+
image = image instanceof Image.Image ? image : await this.load(image);
58+
59+
// Set the Exif data on the image and save it.
60+
image.exif = exif;
61+
return await image.save(null);
62+
};
63+
64+
/**
65+
* Loads the image from a Buffer or string and returns an Image instance.
66+
* @param image - A Buffer or string representing the image to be loaded.
67+
* @returns A Promise that resolves to an Image instance.
68+
*/
69+
load = async (image: Buffer | string): Promise<Image.Image> => {
70+
const img = new Image.Image();
71+
await img.load(image);
72+
return img;
73+
};
74+
}

src/lib/extractMetaData.ts

Lines changed: 11 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,13 @@
1-
2-
interface Metadata {
3-
[key: string]: string;
4-
}
5-
6-
function readCustomMetadata(buffer: Buffer): Metadata | null {
7-
try {
8-
if (buffer.slice(0, 4).toString('hex') !== '52494646') {
9-
throw new Error('Buffer does not contain WebP format');
10-
}
11-
12-
let offset = 12;
13-
while (offset < buffer.length) {
14-
const chunkID = buffer.slice(offset, offset + 4).toString('ascii');
15-
const chunkSize = buffer.readUIntLE(offset + 4, 4);
16-
if (chunkID === 'VP8X' || chunkID === 'EXIF' || chunkID === 'ICCP') {
17-
const metadataStartOffset = offset + 8;
18-
const metadata = buffer.slice(metadataStartOffset, metadataStartOffset + chunkSize);
19-
console.log(metadataStartOffset)
20-
const parsedMetadata = parseMetadataBuffer(metadata);
21-
return parsedMetadata;
22-
}
23-
24-
offset += 8 + chunkSize;
25-
}
26-
27-
throw new Error('No custom metadata found in WebP file');
28-
29-
} catch (error: unknown) {
30-
console.error('Error reading WebP custom metadata:', error);
31-
return null;
32-
}
33-
}
34-
35-
function parseMetadataBuffer(metadata: Buffer): Metadata {
36-
const metadataString = metadata.toString('utf8');
37-
const parsedMetadata: Metadata = {};
38-
const lines = metadataString.split('\n');
39-
lines.forEach(line => {
40-
const keyValue = line.split(':');
41-
if (keyValue.length === 2) {
42-
const key = keyValue[0].trim();
43-
const value = keyValue[1].trim();
44-
parsedMetadata[key] = value;
45-
}
46-
});
47-
return parsedMetadata;
1+
import Image from 'node-webpmux'
2+
/**
3+
* Extracts metadata from a WebP image.
4+
* @param {Buffer}image - The image buffer to extract metadata from
5+
*/
6+
const extractMetadata = async (image: Buffer): Promise<any> => {
7+
const img = new Image.Image()
8+
await img.load(image)
9+
const exif = img.exif?.toString('utf-8') ?? '{}'
10+
return JSON.parse(exif.substring(exif.indexOf('{'), exif.lastIndexOf('}') + 1) ?? '{}');
4811
}
4912

50-
export default readCustomMetadata;
13+
export default extractMetadata;

src/lib/node-webpmux.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
declare module 'node-webpmux' {
2+
export class Image {
3+
constructor()
4+
exif: Buffer
5+
load(buffer: Buffer | string): Promise<void>
6+
save(...args: unknown[]): Promise<Buffer>
7+
}
8+
}

0 commit comments

Comments
 (0)