From 5ce991f2ce012c2471b97961adec32f9021fdc48 Mon Sep 17 00:00:00 2001
From: Allen Ray <dusk125@users.noreply.github.com>
Date: Sat, 31 Aug 2024 16:02:54 -0400
Subject: [PATCH] Add convenience function for loading a picture directly from
 a file (#111)

* Add convenience function for loading pictures and images directly from a file
---
 data.go            | 92 ++++++++++++++++++++++++++++++++++++++++++++++
 ext/atlas/atlas.go | 24 ++++++------
 ext/atlas/entry.go | 10 ++++-
 ext/atlas/group.go | 28 ++++++++------
 ext/atlas/help.go  | 28 --------------
 5 files changed, 128 insertions(+), 54 deletions(-)

diff --git a/data.go b/data.go
index c18516d..05bcef7 100644
--- a/data.go
+++ b/data.go
@@ -1,11 +1,14 @@
 package pixel
 
 import (
+	"embed"
 	"fmt"
 	"image"
 	"image/color"
 	"image/draw"
+	"io"
 	"math"
+	"os"
 )
 
 // zeroValueTriangleData is the default value of a TriangleData element
@@ -179,6 +182,95 @@ func verticalFlip(rgba *image.RGBA) {
 	}
 }
 
+type DecoderFunc func(io.Reader) (image.Image, error)
+
+// DefaultDecoderFunc is a DecoderFunc that uses image.Decode to decode images.
+// In order to decode, you must import the image formats you wish to use.
+// ex. import _ "image/png"
+func DefaultDecoderFunc(r io.Reader) (image.Image, error) {
+	i, _, err := image.Decode(r)
+	return i, err
+}
+
+// ImageFromEmbed loads an image from an embedded file using the given decoder.
+//
+// We take a decoder function (png.Decode, jpeg.Decode, etc.) as an argument; in order to decode images,
+// you have to register the format (png, jpeg, etc.) with the image package, this will increase the number
+// of dependencies imposed on a project. We want to avoid importing these in Pixel as it will increase the
+// size of the project and it will increase maintanence if we miss a format, or if a new format is added.
+//
+// With this argument, you implicitly import and register the file formats you need and the Pixel project
+// doesn't have to carry all formats around.
+//
+// The decoder can be nil, and Pixel will fallback onto using image.Decode and require you to import the
+// formats you wish to use.
+//
+// See the example https://github.com/gopxl/pixel-examples/tree/main/core/loadingpictures.
+func ImageFromEmbed(fs embed.FS, path string, decoder DecoderFunc) (image.Image, error) {
+	f, err := fs.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	if decoder == nil {
+		decoder = DefaultDecoderFunc
+	}
+
+	return decoder(f)
+}
+
+// ImageFromFile loads an image from a file using the given decoder.
+//
+// We take a decoder function (png.Decode, jpeg.Decode, etc.) as an argument; in order to decode images,
+// you have to register the format (png, jpeg, etc.) with the image package, this will increase the number
+// of dependencies imposed on a project. We want to avoid importing these in Pixel as it will increase the
+// size of the project and it will increase maintanence if we miss a format, or if a new format is added.
+//
+// With this argument, you implicitly import and register the file formats you need and the Pixel project
+// doesn't have to carry all formats around.
+//
+// The decoder can be nil, and Pixel will fallback onto using image.Decode and require you to import the
+// formats you wish to use.
+//
+// See the example https://github.com/gopxl/pixel-examples/tree/main/core/loadingpictures.
+func ImageFromFile(path string, decoder DecoderFunc) (image.Image, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	if decoder == nil {
+		decoder = DefaultDecoderFunc
+	}
+
+	return decoder(f)
+}
+
+// PictureDataFromFile loads an image from a file using the given decoder and converts it into PictureData.
+//
+// We take a decoder function (png.Decode, jpeg.Decode, etc.) as an argument; in order to decode images,
+// you have to register the format (png, jpeg, etc.) with the image package, this will increase the number
+// of dependencies imposed on a project. We want to avoid importing these in Pixel as it will increase the
+// size of the project and it will increase maintanence if we miss a format, or if a new format is added.
+//
+// With this argument, you implicitly import and register the file formats you need and the Pixel project
+// doesn't have to carry all formats around.
+//
+// The decoder can be nil, and Pixel will fallback onto using image.Decode and require you to import the
+// formats you wish to use.
+//
+// See the example https://github.com/gopxl/pixel-examples/tree/main/core/loadingpictures.
+func PictureDataFromFile(path string, decoder DecoderFunc) (*PictureData, error) {
+	img, err := ImageFromFile(path, decoder)
+	if err != nil {
+		return nil, err
+	}
+
+	return PictureDataFromImage(img), nil
+}
+
 // PictureDataFromImage converts an image.Image into PictureData.
 //
 // The resulting PictureData's Bounds will be the equivalent of the supplied image.Image's Bounds.
diff --git a/ext/atlas/atlas.go b/ext/atlas/atlas.go
index eaed6e0..f61c287 100644
--- a/ext/atlas/atlas.go
+++ b/ext/atlas/atlas.go
@@ -99,13 +99,13 @@ func (a *Atlas) AddImage(img image.Image) (id TextureId) {
 }
 
 // AddEmbed loads an embed.FS image to the atlas.
-func (a *Atlas) AddEmbed(fs embed.FS, path string) (id TextureId) {
-	return a.DefaultGroup().AddEmbed(fs, path)
+func (a *Atlas) AddEmbed(fs embed.FS, path string, decoder pixel.DecoderFunc) (id TextureId) {
+	return a.DefaultGroup().AddEmbed(fs, path, decoder)
 }
 
 // AddFile loads an image file to the atlas.
-func (a *Atlas) AddFile(path string) (id TextureId) {
-	return a.DefaultGroup().AddFile(path)
+func (a *Atlas) AddFile(path string, decoder pixel.DecoderFunc) (id TextureId) {
+	return a.DefaultGroup().AddFile(path, decoder)
 }
 
 // SliceImage evenly divides the given image into cells of the given size.
@@ -114,13 +114,13 @@ func (a *Atlas) SliceImage(img image.Image, cellSize pixel.Vec) (id SliceId) {
 }
 
 // Slice loads an image and evenly divides it into cells of the given size.
-func (a *Atlas) SliceFile(path string, cellSize pixel.Vec) (id SliceId) {
-	return a.DefaultGroup().SliceFile(path, cellSize)
+func (a *Atlas) SliceFile(path string, cellSize pixel.Vec, decoder pixel.DecoderFunc) (id SliceId) {
+	return a.DefaultGroup().SliceFile(path, cellSize, decoder)
 }
 
 // SliceEmbed loads an embeded image and evenly divides it into cells of the given size.
-func (a *Atlas) SliceEmbed(fs embed.FS, path string, cellSize pixel.Vec) (id SliceId) {
-	return a.DefaultGroup().SliceEmbed(fs, path, cellSize)
+func (a *Atlas) SliceEmbed(fs embed.FS, path string, cellSize pixel.Vec, decoder pixel.DecoderFunc) (id SliceId) {
+	return a.DefaultGroup().SliceEmbed(fs, path, cellSize, decoder)
 }
 
 // Pack takes all of the added textures and adds them to the atlas largest to smallest,
@@ -133,7 +133,7 @@ func (a *Atlas) Pack() {
 	}
 
 	// If we've already packed the textures, we need to convert them back to images to repack them
-	if a.internal != nil && len(a.internal) > 0 {
+	if len(a.internal) > 0 {
 		images := make([]*image.RGBA, len(a.internal))
 		for i, data := range a.internal {
 			images[i] = data.Image()
@@ -260,10 +260,10 @@ func (a *Atlas) Pack() {
 		case iImageEntry:
 			sprite = add.Data()
 		case iEmbedEntry:
-			sprite, err = loadEmbedSprite(add.FS(), add.Path())
+			sprite, err = pixel.ImageFromEmbed(add.FS(), add.Path(), add.DecoderFunc())
 			err = errors.Wrapf(err, "failed to load embed sprite: %v", add.Path())
 		case iFileEntry:
-			sprite, err = loadSprite(add.Path())
+			sprite, err = pixel.ImageFromFile(add.Path(), add.DecoderFunc())
 			err = errors.Wrapf(err, "failed to load sprite file: %v", add.Path())
 		}
 		if err != nil {
@@ -281,6 +281,4 @@ func (a *Atlas) Pack() {
 
 	a.adding = nil
 	a.clean = true
-
-	return
 }
diff --git a/ext/atlas/entry.go b/ext/atlas/entry.go
index 1b5b663..fa8be28 100644
--- a/ext/atlas/entry.go
+++ b/ext/atlas/entry.go
@@ -3,6 +3,8 @@ package atlas
 import (
 	"embed"
 	"image"
+
+	"github.com/gopxl/pixel/v2"
 )
 
 type iEntry interface {
@@ -54,17 +56,23 @@ func (i imageEntry) Data() image.Image {
 type iFileEntry interface {
 	iEntry
 	Path() string
+	DecoderFunc() pixel.DecoderFunc
 }
 
 type fileEntry struct {
 	entry
-	path string
+	path        string
+	decoderFunc pixel.DecoderFunc
 }
 
 func (f fileEntry) Path() string {
 	return f.path
 }
 
+func (f fileEntry) DecoderFunc() pixel.DecoderFunc {
+	return f.decoderFunc
+}
+
 type iSliceEntry interface {
 	iEntry
 	Frame() image.Point
diff --git a/ext/atlas/group.go b/ext/atlas/group.go
index 7d1a21f..4dedac8 100644
--- a/ext/atlas/group.go
+++ b/ext/atlas/group.go
@@ -84,8 +84,8 @@ func (g *Group) AddImage(img image.Image) (id TextureId) {
 }
 
 // AddEmbed loads an embed.FS image to the atlas.
-func (g *Group) AddEmbed(fs embed.FS, path string) (id TextureId) {
-	img, err := loadEmbedSprite(fs, path)
+func (g *Group) AddEmbed(fs embed.FS, path string, decoder pixel.DecoderFunc) (id TextureId) {
+	img, err := pixel.ImageFromEmbed(fs, path, decoder)
 	if err != nil {
 		panic(err)
 	}
@@ -95,7 +95,8 @@ func (g *Group) AddEmbed(fs embed.FS, path string) (id TextureId) {
 				id:     g.atlas.id,
 				bounds: img.Bounds(),
 			},
-			path: path,
+			path:        path,
+			decoderFunc: decoder,
 		},
 		fs: fs,
 	}
@@ -103,8 +104,8 @@ func (g *Group) AddEmbed(fs embed.FS, path string) (id TextureId) {
 }
 
 // AddFile loads an image file to the atlas.
-func (g *Group) AddFile(path string) (id TextureId) {
-	img, err := loadSprite(path)
+func (g *Group) AddFile(path string, decoder pixel.DecoderFunc) (id TextureId) {
+	img, err := pixel.ImageFromFile(path, decoder)
 	if err != nil {
 		panic(err)
 	}
@@ -113,7 +114,8 @@ func (g *Group) AddFile(path string) (id TextureId) {
 			id:     g.atlas.id,
 			bounds: img.Bounds(),
 		},
-		path: path,
+		path:        path,
+		decoderFunc: decoder,
 	}
 	return g.addEntry(e)
 }
@@ -145,9 +147,9 @@ func (g *Group) SliceImage(img image.Image, cellSize pixel.Vec) (id SliceId) {
 }
 
 // SliceFile loads an image and evenly divides it into cells of the given size.
-func (g *Group) SliceFile(path string, cellSize pixel.Vec) (id SliceId) {
+func (g *Group) SliceFile(path string, cellSize pixel.Vec, decoder pixel.DecoderFunc) (id SliceId) {
 	frame := image.Pt(int(cellSize.X), int(cellSize.Y))
-	img, err := loadSprite(path)
+	img, err := pixel.ImageFromFile(path, decoder)
 	if err != nil {
 		panic(err)
 	}
@@ -162,7 +164,8 @@ func (g *Group) SliceFile(path string, cellSize pixel.Vec) (id SliceId) {
 				id:     g.atlas.id,
 				bounds: bounds,
 			},
-			path: path,
+			path:        path,
+			decoderFunc: decoder,
 		},
 		sliceEntry: sliceEntry{
 			frame: frame,
@@ -176,8 +179,8 @@ func (g *Group) SliceFile(path string, cellSize pixel.Vec) (id SliceId) {
 }
 
 // SliceEmbed loads an embeded image and evenly divides it into cells of the given size.
-func (g *Group) SliceEmbed(fs embed.FS, path string, cellSize pixel.Vec) (id SliceId) {
-	img, err := loadEmbedSprite(fs, path)
+func (g *Group) SliceEmbed(fs embed.FS, path string, cellSize pixel.Vec, decoder pixel.DecoderFunc) (id SliceId) {
+	img, err := pixel.ImageFromEmbed(fs, path, decoder)
 	if err != nil {
 		panic(err)
 	}
@@ -194,7 +197,8 @@ func (g *Group) SliceEmbed(fs embed.FS, path string, cellSize pixel.Vec) (id Sli
 					id:     g.atlas.id,
 					bounds: bounds,
 				},
-				path: path,
+				path:        path,
+				decoderFunc: decoder,
 			},
 			fs: fs,
 		},
diff --git a/ext/atlas/help.go b/ext/atlas/help.go
index 5f27b4b..a9cf687 100644
--- a/ext/atlas/help.go
+++ b/ext/atlas/help.go
@@ -1,13 +1,7 @@
 package atlas
 
 import (
-	"embed"
 	"image"
-	"os"
-
-	// need the following to automatically register for image.decode
-	_ "image/jpeg"
-	_ "image/png"
 
 	"github.com/gopxl/pixel/v2"
 	"golang.org/x/exp/constraints"
@@ -29,28 +23,6 @@ func image2PixelRect(r image.Rectangle) pixel.Rect {
 	return pixelRect(r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
 }
 
-func loadEmbedSprite(fs embed.FS, file string) (i image.Image, err error) {
-	f, err := fs.Open(file)
-	if err != nil {
-		return
-	}
-	defer f.Close()
-
-	i, _, err = image.Decode(f)
-	return
-}
-
-func loadSprite(file string) (i image.Image, err error) {
-	f, err := os.Open(file)
-	if err != nil {
-		return
-	}
-	defer f.Close()
-
-	i, _, err = image.Decode(f)
-	return
-}
-
 // split is the actual algorithm for splitting a given space (by j in spcs) to fit the given width and height.
 // Will return an empty rectangle if a space wasn't available
 // This function is based on this project (https://github.com/TeamHypersomnia/rectpack2D)