Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 11 additions & 53 deletions base/iox/imagex/base64.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,21 @@ package imagex
import (
"bytes"
"encoding/base64"
"errors"
"image"
"image/jpeg"
"image/png"
"log"
"strings"
)

// ToBase64PNG returns bytes of image encoded as a PNG in Base64 format
// with "image/png" mimetype returned
func ToBase64PNG(img image.Image) ([]byte, string) {
ibuf := &bytes.Buffer{}
png.Encode(ibuf, img)
ib := ibuf.Bytes()
eb := make([]byte, base64.StdEncoding.EncodedLen(len(ib)))
base64.StdEncoding.Encode(eb, ib)
return eb, "image/png"
}
"cogentcore.org/core/base/errors"
)

// ToBase64JPG returns bytes image encoded as a JPG in Base64 format
// with "image/jpeg" mimetype returned
func ToBase64JPG(img image.Image) ([]byte, string) {
// ToBase64 returns bytes of image encoded in given format,
// in Base64 encoding with "image/format" mimetype returned
func ToBase64(img image.Image, f Formats) ([]byte, string) {
ibuf := &bytes.Buffer{}
jpeg.Encode(ibuf, img, &jpeg.Options{Quality: 90})
Write(img, ibuf, f)
ib := ibuf.Bytes()
eb := make([]byte, base64.StdEncoding.EncodedLen(len(ib)))
base64.StdEncoding.Encode(eb, ib)
return eb, "image/jpeg"
return eb, "image/" + strings.ToLower(f.String())
}

// Base64SplitLines splits the encoded Base64 bytes into standard lines of 76
Expand All @@ -58,44 +45,15 @@ func Base64SplitLines(b []byte) []byte {
return rb
}

// FromBase64PNG returns image from Base64-encoded bytes in PNG format
func FromBase64PNG(eb []byte) (image.Image, error) {
if eb[76] == ' ' {
eb = bytes.ReplaceAll(eb, []byte(" "), []byte("\n"))
}
db := make([]byte, base64.StdEncoding.DecodedLen(len(eb)))
_, err := base64.StdEncoding.Decode(db, eb)
if err != nil {
log.Println(err)
return nil, err
}
rb := bytes.NewReader(db)
return png.Decode(rb)
}

// FromBase64JPG returns image from Base64-encoded bytes in PNG format
func FromBase64JPG(eb []byte) (image.Image, error) {
// FromBase64 returns image from Base64-encoded bytes
func FromBase64(eb []byte) (image.Image, Formats, error) {
if eb[76] == ' ' {
eb = bytes.ReplaceAll(eb, []byte(" "), []byte("\n"))
}
db := make([]byte, base64.StdEncoding.DecodedLen(len(eb)))
_, err := base64.StdEncoding.Decode(db, eb)
if err != nil {
log.Println(err)
return nil, err
}
rb := bytes.NewReader(db)
return jpeg.Decode(rb)
}

// FromBase64 returns image from Base64-encoded bytes in either PNG or JPEG format
// based on fmt which must end in either png, jpg, or jpeg
func FromBase64(fmt string, eb []byte) (image.Image, error) {
if strings.HasSuffix(fmt, "png") {
return FromBase64PNG(eb)
}
if strings.HasSuffix(fmt, "jpg") || strings.HasSuffix(fmt, "jpeg") {
return FromBase64JPG(eb)
return nil, None, errors.Log(err)
}
return nil, errors.New("image format must be either png or jpeg")
return Read(bytes.NewReader(db))
}
97 changes: 97 additions & 0 deletions base/iox/imagex/imagex_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) 2025, Cogent Core. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package imagex

import (
"encoding/json"
"image"
"image/color"
"testing"

"github.com/stretchr/testify/assert"
)

type testObj struct {
Name string
Image *JSON
Another string
}

func testImage() *image.RGBA {
im := image.NewRGBA(image.Rect(0, 0, 16, 16))
for y := range 16 {
for x := range 16 {
im.Set(x, y, color.RGBA{uint8(x * 16), uint8(y * 16), 128, 255})
}
}
return im
}

func TestSave(t *testing.T) {
im := testImage()
// this tests Save and Open etc for all formats
Assert(t, im, "test.png", 1) // should be exact
Assert(t, im, "test.jpg", 20) // quite bad
Assert(t, im, "test.gif", 50) // even worse
Assert(t, im, "test.tif", 1)
Assert(t, im, "test.bmp", 1)
// Assert(t, im, "test.webp") // only for reading, not writing
}

func TestBase64(t *testing.T) {
im := testImage()
b, mime := ToBase64(im, PNG)
assert.Equal(t, "image/png", mime)
bim, f, err := FromBase64(b)
assert.NoError(t, err)
assert.Equal(t, PNG, f)
bounds, content, _, _, _, _ := ImagesEqual(im, bim, 1)
assert.True(t, bounds)
assert.True(t, content)

b, mime = ToBase64(im, JPEG)
assert.Equal(t, "image/jpeg", mime)
bim, f, err = FromBase64(b)
assert.NoError(t, err)
assert.Equal(t, JPEG, f)
bounds, content, _, _, _, _ = ImagesEqual(im, bim, 20)
assert.True(t, bounds)
assert.True(t, content)
}

func TestJSON(t *testing.T) {
im := testImage()
jsi := &JSON{Image: im}

b, err := json.Marshal(jsi)
assert.NoError(t, err)

nsi := &JSON{}
err = json.Unmarshal(b, nsi)
assert.NoError(t, err)

ri := nsi.Image.(*image.RGBA)

assert.Equal(t, im, ri)

bounds, content, _, _, _, _ := ImagesEqual(im, ri, 1)
assert.True(t, bounds)
assert.True(t, content)

jo := &testObj{Name: "testy", Another: "guy"}
jo.Image = NewJSON(im)

b, err = json.Marshal(jo)
assert.NoError(t, err)

no := &testObj{}
err = json.Unmarshal(b, no)
assert.NoError(t, err)

assert.Equal(t, jo, no)
bounds, content, _, _, _, _ = ImagesEqual(jo.Image.Image, no.Image.Image, 1)
assert.True(t, bounds)
assert.True(t, content)
}
67 changes: 67 additions & 0 deletions base/iox/imagex/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) 2025, Cogent Core. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package imagex

import (
"bytes"
"encoding/json"
"image"
)

// JSON is a wrapper around an [image.Image] that defines JSON
// Marshal and Unmarshal methods, so that the image will automatically
// be properly saved / loaded when used as a struct field, for example.
// Must be a pointer type to support custom unmarshal function.
// The original image is not anonymously embedded so that you have to
// extract it, otherwise it will be processed inefficiently.
type JSON struct {
Image image.Image
}

// JSONEncoded is a representation of an image encoded into a byte stream,
// using the PNG encoder. This can be Marshal and Unmarshal'd directly.
type JSONEncoded struct {
Width int
Height int

// Image is the encoded byte stream, which will be encoded in JSON
// using Base64
Image []byte
}

// NewJSON returns a new JSON wrapper around given image,
// to support automatic wrapping and unwrapping.
func NewJSON(im image.Image) *JSON {
return &JSON{Image: im}
}

func (js *JSON) MarshalJSON() ([]byte, error) {
id := &JSONEncoded{}
if js.Image != nil {
sz := js.Image.Bounds().Size()
id.Width = sz.X
id.Height = sz.Y
ibuf := &bytes.Buffer{}
Write(js.Image, ibuf, PNG)
id.Image = ibuf.Bytes()
}
return json.Marshal(id)
}

func (js *JSON) UnmarshalJSON(b []byte) error {
id := &JSONEncoded{}
err := json.Unmarshal(b, id)
if err != nil || (id.Width == 0 && id.Height == 0) {
js.Image = nil
return err
}
im, _, err := image.Decode(bytes.NewReader(id.Image))
if err != nil {
js.Image = nil
return err
}
js.Image = im
return nil
}
84 changes: 50 additions & 34 deletions base/iox/imagex/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ type TestingT interface {
// and it should only be set once and then turned back off.
var UpdateTestImages = updateTestImages

// CompareUint8 returns true if two numbers are more different than tol
func CompareUint8(cc, ic uint8, tol int) bool {
// Uint8Equal returns true if two numbers are within tol
func Uint8Equal(cc, ic uint8, tol int) bool {
d := int(cc) - int(ic)
if d < -tol {
return false
Expand All @@ -42,25 +42,26 @@ func CompareUint8(cc, ic uint8, tol int) bool {
return true
}

// CompareColors returns true if two colors are more different than tol
func CompareColors(cc, ic color.RGBA, tol int) bool {
if !CompareUint8(cc.R, ic.R, tol) {
// ColorsEqual returns true if two colors equal within tol
func ColorsEqual(cc, ic color.RGBA, tol int) bool {
if !Uint8Equal(cc.R, ic.R, tol) {
return false
}
if !CompareUint8(cc.G, ic.G, tol) {
if !Uint8Equal(cc.G, ic.G, tol) {
return false
}
if !CompareUint8(cc.B, ic.B, tol) {
if !Uint8Equal(cc.B, ic.B, tol) {
return false
}
if !CompareUint8(cc.A, ic.A, tol) {
if !Uint8Equal(cc.A, ic.A, tol) {
return false
}
return true
}

// DiffImage returns the difference between two images,
// with pixels having the abs of the difference between pixels.
// Images must have the same bounds.
func DiffImage(a, b image.Image) image.Image {
ab := a.Bounds()
di := image.NewRGBA(ab)
Expand All @@ -78,6 +79,31 @@ func DiffImage(a, b image.Image) image.Image {
return di
}

// ImagesEqual compares two images, returns false if not equal,
// based on bounds or content. If a content difference, then
// the colors and pixel where they first differ are returned.
// The first image is considered the reference, correct one.
// Tol is the color tolerance.
func ImagesEqual(a, b image.Image, tol int) (bounds, content bool, ac, bc color.RGBA, x, y int) {
abounds := a.Bounds()
bbounds := b.Bounds()
if bbounds != abounds {
return
}
bounds = true
for y = abounds.Min.Y; y < abounds.Max.Y; y++ {
for x = abounds.Min.X; x < abounds.Max.X; x++ {
ac = color.RGBAModel.Convert(a.At(x, y)).(color.RGBA)
bc = color.RGBAModel.Convert(b.At(x, y)).(color.RGBA)
if !ColorsEqual(ac, bc, tol) {
return
}
}
}
content = true
return
}

// Assert asserts that the given image is equivalent
// to the image stored at the given filename in the testdata directory,
// with ".png" added to the filename if there is no extension
Expand All @@ -86,17 +112,23 @@ func DiffImage(a, b image.Image) image.Image {
// If it is not, it fails the test with an error, but continues its
// execution. If there is no image at the given filename in the testdata
// directory, it creates the image.
func Assert(t TestingT, img image.Image, filename string) {
// optional tolerance argument specifies the maximum color difference,
// which defaults to 10.
func Assert(t TestingT, img image.Image, filename string, tols ...int) {
filename = filepath.Join("testdata", filename)
if filepath.Ext(filename) == "" {
filename += ".png"
}

err := os.MkdirAll(filepath.Dir(filename), 0750)
if err != nil {
t.Errorf("error making testdata directory: %v", err)
}

tol := 10
if len(tols) == 1 {
tol = tols[0]
}

ext := filepath.Ext(filename)
failFilename := strings.TrimSuffix(filename, ext) + ".fail" + ext
diffFilename := strings.TrimSuffix(filename, ext) + ".diff" + ext
Expand Down Expand Up @@ -128,32 +160,16 @@ func Assert(t TestingT, img image.Image, filename string) {
return
}

failed := false
// TODO(#1456): reduce tolerance to 1 after we fix rendering inconsistencies
bounds, content, ac, bc, x, y := ImagesEqual(fimg, img, tol)

ibounds := img.Bounds()
fbounds := fimg.Bounds()
if ibounds != fbounds {
t.Errorf("AssertImage: expected bounds %v for image for %s, but got bounds %v; see %s", fbounds, filename, ibounds, failFilename)
failed = true
} else {
for y := ibounds.Min.Y; y < ibounds.Max.Y; y++ {
for x := ibounds.Min.X; x < ibounds.Max.X; x++ {
cc := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA)
ic := color.RGBAModel.Convert(fimg.At(x, y)).(color.RGBA)
// TODO(#1456): reduce tolerance to 1 after we fix rendering inconsistencies
if !CompareColors(cc, ic, 10) {
t.Errorf("AssertImage: image for %s is not the same as expected; see %s; expected color %v at (%d, %d), but got %v", filename, failFilename, ic, x, y, cc)
failed = true
break
}
}
if failed {
break
}
}
}
if !bounds || !content {
if !bounds {
t.Errorf("AssertImage: expected bounds %v for image for %s, but got bounds %v; see %s", fimg.Bounds(), filename, img.Bounds(), failFilename)
} else {
t.Errorf("AssertImage: image for %s is not the same as expected; see %s; expected color %v at (%d, %d), but got %v", filename, failFilename, ac, x, y, bc)

if failed {
}
err := Save(img, failFilename)
if err != nil {
t.Errorf("AssertImage: error saving fail image: %v", err)
Expand Down
Loading
Loading