fix(baoyu-post-to-wechat): detect actual image format from buffer magic bytes

CDNs may serve WebP for URLs with .png extension. Detect real format
from magic bytes and correct content-type/extension before upload.
Also treat .webp as PNG-preferred for transparency handling.
This commit is contained in:
Jim Liu 宝玉 2026-04-05 13:36:51 -05:00
parent c44a524fa6
commit 2a0bba6161
2 changed files with 46 additions and 1 deletions

View File

@ -7,6 +7,7 @@ import {
type WechatUploadAsset,
prepareWechatBodyImageUpload,
needsWechatBodyImageProcessing,
detectImageFormatFromBuffer,
} from "./wechat-image-processor.ts";
interface AccessTokenResponse {
@ -138,6 +139,16 @@ async function loadUploadAsset(
contentType = mimeTypes[fileExt] || "image/jpeg";
}
// Detect actual format from magic bytes to fix extension/content-type mismatches
// (e.g. CDNs serving WebP for URLs with .png extension)
const detected = detectImageFormatFromBuffer(fileBuffer);
if (detected && detected.contentType !== contentType) {
console.error(`[wechat-api] Format mismatch: ${filename} declared as ${contentType}, actual ${detected.contentType}`);
contentType = detected.contentType;
fileExt = detected.fileExt;
filename = `${path.basename(filename, path.extname(filename))}${detected.fileExt}`;
}
return {
buffer: fileBuffer,
filename,

View File

@ -52,6 +52,39 @@ const MIME_TO_EXT: Record<string, string> = {
const JPEG_QUALITY_STEPS = [82, 74, 66, 58, 50, 42, 34];
const MAX_WIDTH_STEPS = [2560, 2048, 1600, 1280, 1024, 800, 640, 480];
/**
* Detect actual image format from buffer magic bytes.
* Returns corrected { contentType, fileExt } or null if unknown.
*/
export function detectImageFormatFromBuffer(buffer: Buffer): { contentType: string; fileExt: string } | null {
if (buffer.length < 12) return null;
// WebP: RIFF....WEBP
if (
buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46 &&
buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50
) {
return { contentType: "image/webp", fileExt: ".webp" };
}
// PNG: 89 50 4E 47
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
return { contentType: "image/png", fileExt: ".png" };
}
// JPEG: FF D8 FF
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
return { contentType: "image/jpeg", fileExt: ".jpg" };
}
// GIF: GIF8
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) {
return { contentType: "image/gif", fileExt: ".gif" };
}
// BMP: BM
if (buffer[0] === 0x42 && buffer[1] === 0x4d) {
return { contentType: "image/bmp", fileExt: ".bmp" };
}
return null;
}
let webpDecoderReady: Promise<void> | undefined;
type JimpImage = Awaited<ReturnType<typeof Jimp.read>>;
@ -209,7 +242,8 @@ export async function prepareWechatBodyImageUpload(
const image = await loadImageForProcessing(asset);
const widths = buildCandidateWidths(image.bitmap.width);
const preferPng = imageHasTransparency(image) || ensureFileExt(asset) === ".png";
const ext = ensureFileExt(asset);
const preferPng = imageHasTransparency(image) || ext === ".png" || ext === ".webp";
const processingNotes = buildProcessingNotes(asset);
for (const width of widths) {