From 029d0bef08df49264995c2061e082610f39babe1 Mon Sep 17 00:00:00 2001 From: Milan Nikolic Date: Tue, 30 Jun 2026 16:13:32 +0200 Subject: [PATCH] Skip leading bytes before JPEG SOI marker, issue #58 --- image/decode.go | 13 ++++++- image/decode_test.go | 82 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 image/decode_test.go diff --git a/image/decode.go b/image/decode.go index 7261fcb..09f856a 100644 --- a/image/decode.go +++ b/image/decode.go @@ -4,6 +4,7 @@ package image import ( + "bytes" "image" "io" @@ -22,5 +23,15 @@ type Decoder struct { // Decode decodes image from JPEG. func (d Decoder) Decode() (image.Image, error) { - return jpegn.Decode(d.r) + data, err := io.ReadAll(d.r) + if err != nil { + return nil, err + } + + // Some cameras emit MJPEG frames with leading bytes before the SOI marker. + if i := bytes.Index(data, []byte{0xFF, 0xD8}); i > 0 { + data = data[i:] + } + + return jpegn.Decode(bytes.NewReader(data)) } diff --git a/image/decode_test.go b/image/decode_test.go new file mode 100644 index 0000000..0381b8b --- /dev/null +++ b/image/decode_test.go @@ -0,0 +1,82 @@ +//go:build !libjpeg + +package image_test + +import ( + "bytes" + "image/jpeg" + "testing" + + "github.com/gen2brain/jpegn" + + "github.com/gen2brain/cam2ip/image" +) + +// stripDHT removes the Huffman table segments, like webcams that rely on the standard tables (issue #20). +func stripDHT(data []byte) []byte { + out := append([]byte{}, data[0], data[1]) + + for i := 2; i+4 <= len(data); { + if data[i] != 0xFF { + break + } + + marker := data[i+1] + if marker == 0xDA { + return append(out, data[i:]...) + } + + length := int(data[i+2])<<8 | int(data[i+3]) + if marker != 0xC4 { + out = append(out, data[i:i+2+length]...) + } + + i += 2 + length + } + + return out +} + +func TestDecodeMissingDHT(t *testing.T) { + src, err := jpeg.Decode(bytes.NewReader(testJpg)) + if err != nil { + t.Fatal(err) + } + + // stdlib uses the standard tables; strip the DHT so the stream relies on the decoder's defaults. + var buf bytes.Buffer + if err := jpeg.Encode(&buf, src, &jpeg.Options{Quality: 90}); err != nil { + t.Fatal(err) + } + data := stripDHT(buf.Bytes()) + + if _, err := jpeg.Decode(bytes.NewReader(data)); err == nil { + t.Log("note: stdlib decoded a DHT-less stream") + } + + img, err := image.NewDecoder(bytes.NewReader(data)).Decode() + if err != nil { + t.Fatalf("decode without DHT: %v", err) + } + + if img.Bounds() != src.Bounds() { + t.Fatalf("bounds = %v, want %v", img.Bounds(), src.Bounds()) + } +} + +func TestDecodeLeadingGarbage(t *testing.T) { + data := append([]byte{0x00, 0x00, 0xFF, 0x12, 0x34}, testJpg...) + + if _, err := jpegn.Decode(bytes.NewReader(data)); err == nil { + t.Log("note: jpegn decoded leading garbage directly") + } + + img, err := image.NewDecoder(bytes.NewReader(data)).Decode() + if err != nil { + t.Fatalf("decode with leading garbage: %v", err) + } + + if img.Bounds().Dx() == 0 || img.Bounds().Dy() == 0 { + t.Fatal("empty image") + } +}