summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-08 01:07:36 +0300
committerPaul Buetow <paul@buetow.org>2026-07-08 01:07:36 +0300
commit6bd28f285708e7bf3567bd82783665fd3ff53a3d (patch)
tree39e07494e9a0dddf3d0f9c4db7be7aa177688d48
parent394647f756d089fbe2ed5e58ba8da2583b699256 (diff)
jo
-rw-r--r--internal/processor/image.go70
1 files changed, 48 insertions, 22 deletions
diff --git a/internal/processor/image.go b/internal/processor/image.go
index f68f533..fe5615f 100644
--- a/internal/processor/image.go
+++ b/internal/processor/image.go
@@ -71,6 +71,7 @@ func writeImageAsset(img image.Image, postDir string) error {
}
// decodeImage decodes a JPEG, PNG, or GIF (first frame) from srcPath.
+// It sniffs the actual file type from magic bytes instead of trusting the extension.
func decodeImage(srcPath string) (image.Image, error) {
f, err := os.Open(srcPath)
if err != nil {
@@ -78,33 +79,58 @@ func decodeImage(srcPath string) (image.Image, error) {
}
defer f.Close()
- ext := filepath.Ext(srcPath)
- switch ext {
- case ".jpg", ".jpeg":
- img, err := jpeg.Decode(f)
- if err != nil {
- return nil, fmt.Errorf("decode JPEG %s: %w", srcPath, err)
- }
- return img, nil
+ // Read the first 512 bytes to detect the actual file format.
+ const sniffLen = 512
+ head := make([]byte, sniffLen)
+ n, err := f.Read(head)
+ if err != nil {
+ return nil, fmt.Errorf("read image %s: %w", srcPath, err)
+ }
+ if n == 0 {
+ return nil, fmt.Errorf("empty image file: %s", srcPath)
+ }
- case ".png":
- img, err := png.Decode(f)
- if err != nil {
- return nil, fmt.Errorf("decode PNG %s: %w", srcPath, err)
- }
- return img, nil
+ // Determine the actual image format from magic bytes.
+ sniff := head[:n]
+ var format string
+ switch {
+ case len(sniff) >= 2 && sniff[0] == 0xFF && sniff[1] == 0xD8:
+ format = "jpeg"
+ case len(sniff) >= 8 && string(sniff[:8]) == "\x89PNG\r\n\x1a\n":
+ format = "png"
+ case len(sniff) >= 6 && string(sniff[:6]) == "GIF87a" || string(sniff[:6]) == "GIF89a":
+ format = "gif"
+ default:
+ return nil, fmt.Errorf("unsupported image format for %s (extension: %s sniff: %q)", srcPath, filepath.Ext(srcPath), string(sniff))
+ }
- case ".gif":
+ // Rewind to the beginning so the decoder sees the full stream.
+ if _, err := f.Seek(0, 0); err != nil {
+ return nil, fmt.Errorf("rewind image %s: %w", srcPath, err)
+ }
+
+ var img image.Image
+ var decodeErr error
+ switch format {
+ case "jpeg":
+ img, decodeErr = jpeg.Decode(f)
+ if decodeErr != nil {
+ return nil, fmt.Errorf("decode JPEG %s: %w", srcPath, decodeErr)
+ }
+ case "png":
+ img, decodeErr = png.Decode(f)
+ if decodeErr != nil {
+ return nil, fmt.Errorf("decode PNG %s: %w", srcPath, decodeErr)
+ }
+ case "gif":
// Use only the first frame of animated GIFs.
- g, err := gif.Decode(f)
- if err != nil {
- return nil, fmt.Errorf("decode GIF %s: %w", srcPath, err)
+ img, decodeErr = gif.Decode(f)
+ if decodeErr != nil {
+ return nil, fmt.Errorf("decode GIF %s: %w", srcPath, decodeErr)
}
- return g, nil
-
- default:
- return nil, fmt.Errorf("unsupported image format: %s", ext)
}
+
+ return img, nil
}
// resizeIfNeeded returns a resized copy of img if its width exceeds maxImageWidth,