diff --git a/internal/providers/gofpdf/parseimage.go b/internal/providers/gofpdf/parseimage.go index b061f295..477b1657 100644 --- a/internal/providers/gofpdf/parseimage.go +++ b/internal/providers/gofpdf/parseimage.go @@ -1,7 +1,11 @@ package gofpdf import ( + "bytes" "errors" + "image" + "image/draw" + "image/png" "github.com/johnfercher/maroto/v2/pkg/consts/extension" "github.com/johnfercher/maroto/v2/pkg/core/entity" @@ -14,8 +18,51 @@ func FromBytes(bytes []byte, ext extension.Type) (*entity.Image, error) { return nil, ErrInvalidImageFormat } + data := bytes + if ext == extension.Png { + var err error + data, err = normalizePNGBytes(bytes) + if err != nil { + return nil, err + } + } + return &entity.Image{ - Bytes: bytes, + Bytes: data, Extension: ext, }, nil } + +func pngBitDepth(data []byte) (int, bool) { + const pngSignature = "\x89PNG\r\n\x1a\n" + if len(data) < 26 || string(data[:8]) != pngSignature { + return 0, false + } + if string(data[12:16]) != "IHDR" { + return 0, false + } + return int(data[24]), true +} + +func normalizePNGBytes(data []byte) ([]byte, error) { + depth, ok := pngBitDepth(data) + if !ok || depth != 16 { + return data, nil + } + + src, err := png.Decode(bytes.NewReader(data)) + if err != nil { + return data, nil + } + + b := src.Bounds() + dst := image.NewNRGBA(b) + draw.Draw(dst, b, src, b.Min, draw.Src) + + var buf bytes.Buffer + if err := png.Encode(&buf, dst); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/internal/providers/gofpdf/parseimage_test.go b/internal/providers/gofpdf/parseimage_test.go index 9c5103fd..57ff5a0b 100644 --- a/internal/providers/gofpdf/parseimage_test.go +++ b/internal/providers/gofpdf/parseimage_test.go @@ -1,12 +1,16 @@ package gofpdf_test import ( + "bytes" + "image" + "image/color" + "image/png" "testing" "github.com/johnfercher/maroto/v2/internal/providers/gofpdf" - "github.com/johnfercher/maroto/v2/pkg/consts/extension" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFromBytes(t *testing.T) { @@ -20,7 +24,7 @@ func TestFromBytes(t *testing.T) { assert.Nil(t, img) assert.NotNil(t, err) }) - t.Run("when extension is not valid, should return error", func(t *testing.T) { + t.Run("when extension is valid, should return image", func(t *testing.T) { t.Parallel() // Act img, err := gofpdf.FromBytes([]byte{1, 2, 3}, extension.Jpg) @@ -30,3 +34,92 @@ func TestFromBytes(t *testing.T) { assert.Nil(t, err) }) } + +func TestFromBytes_Normalizes16BitPNG(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + build func() image.Image + assertPx func(t *testing.T, decoded image.Image) + }{ + { + name: "NRGBA64", + build: func() image.Image { + m := image.NewNRGBA64(image.Rect(0, 0, 1, 1)) + m.SetNRGBA64(0, 0, color.NRGBA64{R: 65535, G: 32768, B: 16384, A: 65535}) + return m + }, + assertPx: func(t *testing.T, decoded image.Image) { + t.Helper() + assert.Equal(t, image.Rect(0, 0, 1, 1), decoded.Bounds()) + c := color.NRGBAModel.Convert(decoded.At(0, 0)).(color.NRGBA) + assert.Equal(t, color.NRGBA{R: 255, G: 128, B: 64, A: 255}, c) + }, + }, + { + name: "RGBA64", + build: func() image.Image { + m := image.NewRGBA64(image.Rect(0, 0, 1, 1)) + m.SetRGBA64(0, 0, color.RGBA64{R: 65535, G: 32768, B: 16384, A: 65535}) + return m + }, + assertPx: func(t *testing.T, decoded image.Image) { + t.Helper() + assert.Equal(t, image.Rect(0, 0, 1, 1), decoded.Bounds()) + c := color.NRGBAModel.Convert(decoded.At(0, 0)).(color.NRGBA) + assert.Equal(t, color.NRGBA{R: 255, G: 128, B: 64, A: 255}, c) + }, + }, + { + name: "Gray16", + build: func() image.Image { + m := image.NewGray16(image.Rect(0, 0, 1, 1)) + m.SetGray16(0, 0, color.Gray16{Y: 32768}) + return m + }, + assertPx: func(t *testing.T, decoded image.Image) { + t.Helper() + assert.Equal(t, image.Rect(0, 0, 1, 1), decoded.Bounds()) + c := color.NRGBAModel.Convert(decoded.At(0, 0)).(color.NRGBA) + assert.Equal(t, color.NRGBA{R: 128, G: 128, B: 128, A: 255}, c) + }, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var input bytes.Buffer + require.NoError(t, png.Encode(&input, tc.build())) + + img, err := gofpdf.FromBytes(input.Bytes(), extension.Png) + require.NoError(t, err) + + decoded, err := png.Decode(bytes.NewReader(img.Bytes)) + require.NoError(t, err) + switch decoded.(type) { + case *image.NRGBA64, *image.RGBA64, *image.Gray16: + t.Fatalf("expected 8-bit PNG, got %T", decoded) + } + tc.assertPx(t, decoded) + }) + } +} + +func TestFromBytes_Leaves8BitPNGUnchanged(t *testing.T) { + t.Parallel() + + m := image.NewNRGBA(image.Rect(0, 0, 2, 2)) + m.SetNRGBA(0, 0, color.NRGBA{R: 255, G: 128, B: 64, A: 255}) + + var input bytes.Buffer + require.NoError(t, png.Encode(&input, m)) + original := append([]byte(nil), input.Bytes()...) + + img, err := gofpdf.FromBytes(input.Bytes(), extension.Png) + require.NoError(t, err) + assert.Equal(t, original, img.Bytes) +}