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)