Skip to content

Commit

Permalink
Split up runtime and encoding packages (#8)
Browse files Browse the repository at this point in the history
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
rohansingh authored Oct 20, 2020
1 parent aa63165 commit e6b97fd
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 219 deletions.
5 changes: 3 additions & 2 deletions doc/gen_widget_imgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io/ioutil"
"strings"

"tidbyt.dev/pixlet/encode"
"tidbyt.dev/pixlet/runtime"
)

Expand Down Expand Up @@ -72,12 +73,12 @@ def main():
panic(err)
}

screens, err := app.Run(nil)
roots, err := app.Run(nil)
if err != nil {
panic(err)
}

gif, err := screens.RenderGIF(Magnify)
gif, err := encode.ScreensFromRoots(roots).EncodeGIF(Magnify)
if err != nil {
panic(err)
}
Expand Down
177 changes: 177 additions & 0 deletions encode/encode.go
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
}
10 changes: 6 additions & 4 deletions runtime/runtime_bench_test.go → encode/encode_bench_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package runtime
package encode

import (
"testing"

"tidbyt.dev/pixlet/runtime"
)

var BenchmarkDotStar = `
Expand Down Expand Up @@ -63,20 +65,20 @@ def main():
`

func BenchmarkRunAndRender(b *testing.B) {
app := &Applet{}
app := &runtime.Applet{}
err := app.Load("benchmark.star", []byte(BenchmarkDotStar), nil)
if err != nil {
b.Error(err)
}

config := map[string]string{}
for i := 0; i < b.N; i++ {
screens, err := app.Run(config)
roots, err := app.Run(config)
if err != nil {
b.Error(err)
}

webp, err := screens.RenderWebP()
webp, err := ScreensFromRoots(roots).EncodeWebP()
if err != nil {
b.Error(err)
}
Expand Down
127 changes: 127 additions & 0 deletions encode/encode_test.go
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)
}
8 changes: 5 additions & 3 deletions render.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/spf13/cobra"

"tidbyt.dev/pixlet/encode"
"tidbyt.dev/pixlet/runtime"
)

Expand Down Expand Up @@ -82,11 +83,12 @@ func render(cmd *cobra.Command, args []string) {
os.Exit(1)
}

screens, err := applet.Run(config)
roots, err := applet.Run(config)
if err != nil {
log.Printf("Error running script: %s\n", err)
os.Exit(1)
}
screens := encode.ScreensFromRoots(roots)

filter := func(input image.Image) (image.Image, error) {
if magnify <= 1 {
Expand Down Expand Up @@ -123,9 +125,9 @@ func render(cmd *cobra.Command, args []string) {
var buf []byte

if renderGif {
buf, err = screens.RenderGIF(filter)
buf, err = screens.EncodeGIF(filter)
} else {
buf, err = screens.RenderWebP(filter)
buf, err = screens.EncodeWebP(filter)
}
if err != nil {
fmt.Printf("Error rendering: %s\n", err)
Expand Down
Loading

0 comments on commit e6b97fd

Please sign in to comment.