From 2a0bba616117617d90d1edb4ad7b1c62d3ec8bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Liu=20=E5=AE=9D=E7=8E=89?= Date: Sun, 5 Apr 2026 13:36:51 -0500 Subject: [PATCH] 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. --- .../scripts/wechat-api.ts | 11 ++++++ .../scripts/wechat-image-processor.ts | 36 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts index 981fd85..8e40e8d 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts @@ -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, diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-image-processor.ts b/skills/baoyu-post-to-wechat/scripts/wechat-image-processor.ts index fba7be3..3f8c8f4 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-image-processor.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-image-processor.ts @@ -52,6 +52,39 @@ const MIME_TO_EXT: Record = { 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 | undefined; type JimpImage = Awaited>; @@ -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) {