Skip to content

Commit

Permalink
Add convenience function for loading a picture directly from a file (#…
Browse files Browse the repository at this point in the history
…111)

* Add convenience function for loading pictures and images directly from a file
  • Loading branch information
dusk125 authored Aug 31, 2024
1 parent 40ac60a commit 5ce991f
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 54 deletions.
92 changes: 92 additions & 0 deletions data.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down
24 changes: 11 additions & 13 deletions ext/atlas/atlas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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 {
Expand All @@ -281,6 +281,4 @@ func (a *Atlas) Pack() {

a.adding = nil
a.clean = true

return
}
10 changes: 9 additions & 1 deletion ext/atlas/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package atlas
import (
"embed"
"image"

"github.com/gopxl/pixel/v2"
)

type iEntry interface {
Expand Down Expand Up @@ -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
Expand Down
28 changes: 16 additions & 12 deletions ext/atlas/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -95,16 +95,17 @@ 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,
}
return g.addEntry(e)
}

// 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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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,
Expand All @@ -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)
}
Expand All @@ -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,
},
Expand Down
28 changes: 0 additions & 28 deletions ext/atlas/help.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
Expand Down

0 comments on commit 5ce991f

Please sign in to comment.