forked from sunnoy/openclaw-plugin-wecom
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathimage-processor.js
More file actions
179 lines (159 loc) · 5.11 KB
/
image-processor.js
File metadata and controls
179 lines (159 loc) · 5.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import { readFile } from "fs/promises";
import { createHash } from "crypto";
import { logger } from "./logger.js";
/**
* Image Processing Module for WeCom
*
* Handles loading, validating, and encoding images for WeCom msg_item
* Supports JPG and PNG formats up to 2MB
*/
// Image format signatures (magic bytes)
const IMAGE_SIGNATURES = {
JPG: [0xFF, 0xD8, 0xFF],
PNG: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
};
// 2MB size limit (before base64 encoding)
const MAX_IMAGE_SIZE = 2 * 1024 * 1024;
/**
* Load image file from filesystem
* @param {string} filePath - Absolute path to image file
* @returns {Promise<Buffer>} Image data buffer
* @throws {Error} If file not found or cannot be read
*/
export async function loadImageFromPath(filePath) {
try {
logger.debug("Loading image from path", { filePath });
const buffer = await readFile(filePath);
logger.debug("Image loaded successfully", {
filePath,
size: buffer.length
});
return buffer;
} catch (error) {
if (error.code === "ENOENT") {
throw new Error(`Image file not found: ${filePath}`);
} else if (error.code === "EACCES") {
throw new Error(`Permission denied reading image: ${filePath}`);
} else {
throw new Error(`Failed to read image file: ${error.message}`);
}
}
}
/**
* Convert buffer to base64 string
* @param {Buffer} buffer - Image data buffer
* @returns {string} Base64-encoded string
*/
export function encodeImageToBase64(buffer) {
return buffer.toString("base64");
}
/**
* Calculate MD5 checksum of buffer
* @param {Buffer} buffer - Image data buffer
* @returns {string} MD5 hash in hexadecimal
*/
export function calculateMD5(buffer) {
return createHash("md5").update(buffer).digest("hex");
}
/**
* Validate image size is within limits
* @param {Buffer} buffer - Image data buffer
* @throws {Error} If size exceeds 2MB limit
*/
export function validateImageSize(buffer) {
const sizeBytes = buffer.length;
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
if (sizeBytes > MAX_IMAGE_SIZE) {
throw new Error(
`Image size ${sizeMB}MB exceeds 2MB limit (actual: ${sizeBytes} bytes)`
);
}
logger.debug("Image size validated", { sizeBytes, sizeMB });
}
/**
* Detect image format from magic bytes
* @param {Buffer} buffer - Image data buffer
* @returns {string} Format: "JPG" or "PNG"
* @throws {Error} If format is not supported
*/
export function detectImageFormat(buffer) {
// Check PNG signature
if (buffer.length >= IMAGE_SIGNATURES.PNG.length) {
const isPNG = IMAGE_SIGNATURES.PNG.every(
(byte, index) => buffer[index] === byte
);
if (isPNG) {
logger.debug("Image format detected: PNG");
return "PNG";
}
}
// Check JPG signature
if (buffer.length >= IMAGE_SIGNATURES.JPG.length) {
const isJPG = IMAGE_SIGNATURES.JPG.every(
(byte, index) => buffer[index] === byte
);
if (isJPG) {
logger.debug("Image format detected: JPG");
return "JPG";
}
}
// Unknown format
const header = buffer.slice(0, 16).toString("hex");
throw new Error(
`Unsupported image format. Only JPG and PNG are supported. ` +
`File header: ${header}`
);
}
/**
* Complete image processing pipeline
*
* Loads image from filesystem, validates format and size,
* then encodes to base64 and calculates MD5 checksum.
*
* @param {string} filePath - Absolute path to image file
* @returns {Promise<Object>} Processed image data
* @returns {string} return.base64 - Base64-encoded image data
* @returns {string} return.md5 - MD5 checksum
* @returns {string} return.format - Image format (JPG or PNG)
* @returns {number} return.size - Original size in bytes
*
* @throws {Error} If any step fails (file not found, invalid format, size exceeded, etc.)
*
* @example
* const result = await prepareImageForMsgItem('/path/to/image.jpg');
* // Returns: { base64: "...", md5: "...", format: "JPG", size: 123456 }
*/
export async function prepareImageForMsgItem(filePath) {
logger.debug("Starting image processing pipeline", { filePath });
try {
// Step 1: Load image
const buffer = await loadImageFromPath(filePath);
// Step 2: Validate size
validateImageSize(buffer);
// Step 3: Detect format
const format = detectImageFormat(buffer);
// Step 4: Encode to base64
const base64 = encodeImageToBase64(buffer);
// Step 5: Calculate MD5
const md5 = calculateMD5(buffer);
logger.info("Image processed successfully", {
filePath,
format,
size: buffer.length,
md5,
base64Length: base64.length
});
return {
base64,
md5,
format,
size: buffer.length
};
} catch (error) {
logger.error("Image processing failed", {
filePath,
error: error.message
});
throw error;
}
}