-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split up runtime and encoding packages (#8)
Move encoding into its own package. This makes `runtime` no longer depend on image processing stuff like `go-libwebp`, so it is now pure Go. This is necessary prerequisite for building a WebAssembly module of the runtime package.
- Loading branch information
1 parent
aa63165
commit e6b97fd
Showing
10 changed files
with
346 additions
and
219 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
package encode | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"image" | ||
"image/color" | ||
"image/gif" | ||
"time" | ||
|
||
"github.com/harukasan/go-libwebp/webp" | ||
"github.com/pkg/errors" | ||
"tidbyt.dev/pixlet/render" | ||
) | ||
|
||
const ( | ||
WebPKMin = 0 | ||
WebPKMax = 0 | ||
DefaultScreenDelayMillis = 50 | ||
) | ||
|
||
type Screens struct { | ||
roots []render.Root | ||
images []image.Image | ||
delay int32 | ||
} | ||
|
||
type ImageFilter func(image.Image) (image.Image, error) | ||
|
||
func ScreensFromRoots(roots []render.Root) *Screens { | ||
screens := Screens{ | ||
roots: roots, | ||
delay: DefaultScreenDelayMillis, | ||
} | ||
if len(roots) > 0 { | ||
if roots[0].Delay > 0 { | ||
screens.delay = roots[0].Delay | ||
} | ||
} | ||
return &screens | ||
} | ||
|
||
func ScreensFromImages(images ...image.Image) *Screens { | ||
screens := Screens{ | ||
images: images, | ||
delay: DefaultScreenDelayMillis, | ||
} | ||
return &screens | ||
} | ||
|
||
// Renders a screen to WebP. Optionally pass filters for | ||
// postprocessing each individual frame. | ||
func (s *Screens) EncodeWebP(filters ...ImageFilter) ([]byte, error) { | ||
images, err := s.render(filters...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if len(images) == 0 { | ||
return []byte{}, nil | ||
} | ||
|
||
bounds := images[0].Bounds() | ||
anim, err := webp.NewAnimationEncoder( | ||
bounds.Dx(), | ||
bounds.Dy(), | ||
WebPKMin, | ||
WebPKMax, | ||
) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "initializing encoder") | ||
} | ||
defer anim.Close() | ||
|
||
frameDuration := time.Duration(s.delay) * time.Millisecond | ||
for _, im := range images { | ||
if err := anim.AddFrame(im, frameDuration); err != nil { | ||
return nil, errors.Wrap(err, "adding frame") | ||
} | ||
} | ||
|
||
buf, err := anim.Assemble() | ||
if err != nil { | ||
return nil, errors.Wrap(err, "encoding animation") | ||
} | ||
|
||
return buf, nil | ||
} | ||
|
||
// Renders a screen to GIF. Optionally pass filters for postprocessing | ||
// each individual frame. | ||
func (s *Screens) EncodeGIF(filters ...ImageFilter) ([]byte, error) { | ||
images, err := s.render(filters...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if len(images) == 0 { | ||
return []byte{}, nil | ||
} | ||
|
||
g := &gif.GIF{} | ||
|
||
for imIdx, im := range images { | ||
imRGBA, ok := im.(*image.RGBA) | ||
if !ok { | ||
return nil, fmt.Errorf("image %d is %T, require RGBA", imIdx, im) | ||
} | ||
|
||
palette := color.Palette{} | ||
idxByColor := map[color.RGBA]int{} | ||
|
||
// Create the palette | ||
for x := 0; x < imRGBA.Bounds().Dx(); x++ { | ||
for y := 0; y < imRGBA.Bounds().Dy(); y++ { | ||
c := imRGBA.RGBAAt(x, y) | ||
if _, found := idxByColor[c]; !found { | ||
idxByColor[c] = len(palette) | ||
palette = append(palette, c) | ||
} | ||
} | ||
} | ||
if len(palette) > 256 { | ||
return nil, fmt.Errorf( | ||
"require <=256 colors, found %d in image %d", | ||
len(palette), imIdx, | ||
) | ||
} | ||
|
||
// Construct the paletted image | ||
imPaletted := image.NewPaletted(imRGBA.Bounds(), palette) | ||
for x := 0; x < imRGBA.Bounds().Dx(); x++ { | ||
for y := 0; y < imRGBA.Bounds().Dy(); y++ { | ||
imPaletted.SetColorIndex(x, y, uint8(idxByColor[imRGBA.RGBAAt(x, y)])) | ||
} | ||
} | ||
|
||
g.Image = append(g.Image, imPaletted) | ||
g.Delay = append(g.Delay, int(s.delay/10)) // in 100ths of a second | ||
} | ||
|
||
buf := &bytes.Buffer{} | ||
err = gif.EncodeAll(buf, g) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "encoding") | ||
} | ||
|
||
return buf.Bytes(), nil | ||
} | ||
|
||
func (s *Screens) render(filters ...ImageFilter) ([]image.Image, error) { | ||
if s.images == nil { | ||
s.images = render.PaintRoots(true, s.roots...) | ||
} | ||
|
||
if len(s.images) == 0 { | ||
return nil, nil | ||
} | ||
|
||
images := s.images | ||
|
||
if len(filters) > 0 { | ||
images = []image.Image{} | ||
for _, im := range s.images { | ||
for _, f := range filters { | ||
imFiltered, err := f(im) | ||
if err != nil { | ||
return nil, err | ||
} | ||
im = imFiltered | ||
} | ||
images = append(images, im) | ||
} | ||
} | ||
|
||
return images, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package encode | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"tidbyt.dev/pixlet/runtime" | ||
) | ||
|
||
var TestDotStar = ` | ||
load("render.star", "render") | ||
load("encoding/base64.star", "base64") | ||
def assert(success, message=None): | ||
if not success: | ||
fail(message or "assertion failed") | ||
# Font tests | ||
assert(render.fonts["6x13"] == "6x13") | ||
assert(render.fonts["Dina_r400-6"] == "Dina_r400-6") | ||
# Box tests | ||
b1 = render.Box( | ||
width = 64, | ||
height = 32, | ||
color = "#000", | ||
) | ||
assert(b1.width == 64) | ||
assert(b1.height == 32) | ||
assert(b1.color == "#000000") | ||
b2 = render.Box( | ||
child = b1, | ||
) | ||
assert(b2.child == b1) | ||
# Text tests | ||
t1 = render.Text( | ||
height = 10, | ||
font = render.fonts["6x13"], | ||
color = "#fff", | ||
content = "foo", | ||
) | ||
assert(t1.height == 10) | ||
assert(t1.font == "6x13") | ||
assert(t1.color == "#ffffff") | ||
assert(0 < t1.size()[0]) | ||
assert(0 < t1.size()[1]) | ||
# WrappedText | ||
tw = render.WrappedText( | ||
height = 16, | ||
width = 64, | ||
font = render.fonts["6x13"], | ||
color = "#f00", | ||
content = "hey ho foo bar wrap this line it's very long wrap it please", | ||
) | ||
# Root tests | ||
f = render.Root( | ||
child = render.Box( | ||
width = 123, | ||
child = render.Text( | ||
content = "hello", | ||
), | ||
), | ||
) | ||
assert(f.child.width == 123) | ||
assert(f.child.child.content == "hello") | ||
# Padding | ||
p = render.Padding(pad=3, child=render.Box(width=1, height=2)) | ||
p2 = render.Padding(pad=(1,2,3,4), child=render.Box(width=1, height=2)) | ||
p3 = render.Padding(pad=1, child=render.Box(width=1, height=2), expanded=True) | ||
# PNG tests | ||
png_src = base64.decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/AAAZ4gk3AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==") | ||
png = render.PNG(src = png_src) | ||
assert(png.src == png_src) | ||
assert(0 < png.size()[0]) | ||
assert(0 < png.size()[1]) | ||
# Row and Column | ||
r1 = render.Row( | ||
expanded = True, | ||
main_align = "space_evenly", | ||
cross_align = "center", | ||
children = [ | ||
render.Box(width=12, height=14), | ||
render.Column( | ||
expanded = True, | ||
main_align = "start", | ||
cross_align = "end", | ||
children = [ | ||
render.Box(width=6, height=7), | ||
render.Box(width=4, height=5), | ||
], | ||
), | ||
], | ||
) | ||
assert(r1.main_align == "space_evenly") | ||
assert(r1.cross_align == "center") | ||
assert(r1.children[1].main_align == "start") | ||
assert(r1.children[1].cross_align == "end") | ||
assert(len(r1.children) == 2) | ||
assert(len(r1.children[1].children) == 2) | ||
def main(): | ||
return render.Root(child=r1) | ||
` | ||
|
||
func TestFile(t *testing.T) { | ||
app := runtime.Applet{} | ||
err := app.Load("test.star", []byte(TestDotStar), nil) | ||
assert.NoError(t, err) | ||
|
||
roots, err := app.Run(map[string]string{}) | ||
assert.NoError(t, err) | ||
|
||
webp, err := ScreensFromRoots(roots).EncodeWebP() | ||
assert.NoError(t, err) | ||
assert.True(t, len(webp) > 0) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.