DEV Community

loading...
Cover image for Getting started with WebAssembly and Go by building an Image to ASCII converter

Getting started with WebAssembly and Go by building an Image to ASCII converter

subeshb1 profile image Subesh ・8 min read

TL;DR This blog illustrates how you can import your existing go code to Wasm, and run it in the browser. In this blog, I will show you how I made a tool to convert Image to Ascii characters on the browser that was written in Go. Link to the Github repo: wasm-go-image-to-ascii. Here is the Demo: Image to Ascii

What is WebAssembly?

Before moving on to writing the code, let's first understand what WebAssembly is. WebAssembly or WASM is an assembly-like language that can run in near-native performance in the browser. It is not to be written manually but to be treated as a compilation target for languages such as C/C++, Golang, Rust, .Net, etc. This means first we write a program in a language, then convert it to WASM and then run it in the browser. This will allow the program to run in near-native speed and give the ability to run a program written in any language to run on the browser. You can create web applications in the language you are familiar with. It doesn't mean it will remove javascript but exist hand in hand with JavaScript. The list of languages that support WASM compilation is in awesome-wasm-langs and more info on WebAssembly Webpage and WebAssembly Concepts.

Running go on the browser

Now, let's get our hands dirty with some basic WASM and Golang.

Writing Go Code

Let's write our first hello world program.

package main

import "fmt"

func main() {
    fmt.Println("Hi from the browser console!!")
}

Compiling to WebAssembly

Let's compile it to Wasm.

 GOOS=js GOARCH=wasm go build -o main.wasm main.go

This will create a main.wasm WebAssembly file that we can import and run on the browser.

Integrating with javascript

After we write our Go code and compile it to WASM we can then start integrating it on the browser.

We will need a Go runtime wrapper written in javascript to interact with Go through wasm. The code is shipped with Go 1.11+ and can be copied using the following command:

    cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

Now, let's integrate it into the browser.

<html>
  <head>
    <meta charset="utf-8" />
    <script src="wasm_exec.js"></script>
    <script>
      const go = new Go()
      WebAssembly.instantiateStreaming(
        fetch('main.wasm'),
        go.importObject
      ).then(result => {
        go.run(result.instance)
      })
    </script>
  </head>
  <body></body>
</html>

WebAssembly.instantiateStreaming compiles and instantiates WebAssembly code. After the code is instantiated, we will run the Go program with go.run(result.instance). For more information visit the WebAssembly.instantiateStreaming docs and Go WebAssembly.
Now if we run a server to serve the content we can view the output in the browser console.

The mime type for .wasm file should be application/wasm when served from the server.

We can use goexec to serve the files:

# Install go exec
go get -u github.com/shurcooL/goexec

# Start the server at 8080 port
goexec 'http.ListenAndServe(`:8080`, http.FileServer(http.Dir(`.`)))'

If we open localhost:8080 on the browser and open the console we will see our message sent from Go:
Wasm Example 1

Accessing Web APIs and exposing Go functions

Now that we know how to compile and run Go code to Wasm and run it on the web, let's get started with building an Image to Ascii converter in the browser by accessing Web APIs. WebAssembly can interact with different Web APIs like DOM, CSSOM, WebGL, IndexedDB, Web Audio API etc. In this tutorial, we will use the DOM APIs in Go code with the help of syscall/js package provided in Golang.

package main

import (
  "syscall/js"
)

func main() {
  c := make(chan bool)
  //1. Adding an <h1> element in the HTML document
  document := js.Global().Get("document")
  p := document.Call("createElement", "h1")
  p.Set("innerHTML", "Hello from Golang!")
  document.Get("body").Call("appendChild", p)

  //2. Exposing go functions/values in javascript variables.
  js.Global().Set("goVar", "I am a variable set from Go")
  js.Global().Set("sayHello", js.FuncOf(sayHello))

  //3. This channel will prevent the go program to exit
  <-c
}

func sayHello(this js.Value, inputs []js.Value) interface{} {
  firstArg := inputs[0].String()
  return "Hi " + firstArg + " from Go!"
}

The above code shows how we can interact fully with the browser API using Go's experimental package syscall/js. Let's discuss the above example.

The js.Global() method is used to get the Javascript global object that is window or global. We can then access global objects or variables like document, window, and other javascript APIs. If we want to get any property from a javascript element we will use obj.Get("property") and to set a property obj.Set("property", jsDataType). We can also call a javascript function with Call method and pass args as obj.Call("functionName", arg1,arg1). In the above example, we have accessed the document object, created an h1 tag, and appended it into HTML body using DOM API.

In the second portion of the code, we have exposed Go function and set a variable that can be accessed by javascript. The goVar is a string type variable and sayHello is a function type. We can open up our console and interact with the exposed variables. The function definition for sayHello can be seen in the last section of the code that takes an argument and returns a string.

At the end of the main block, we are waiting for a channel that will never receive a message. This is done to keep the Go code running so that we can access the exposed function. In other languages like C++ and Rust Wasm treats them like a library i.e we can directly import them and start using exposed functions. However in Go, the importing is treated as an application i.e you can access the program when it has started and run, and then the interaction is over when the program is exited. If we don't add the channel at the end of the block then we won't we able to call the function that has been defined in Go.

The above code produces the following output:
Wasm Example 2

Importing Image to Ascii library to the browser

Now, that we know how to interact back and forth between Go and the browser, let's build a real-world application. We will be importing an existing library, image2Ascii that converts an image to ASCII characters. It is a Go CLI application that takes the path of an image and converts it to Ascii characters. Since we can't access the file system in the browser directly, I have altered some of the code in the library to take bytes of the image instead of the file path. The source to the repo with changes: wasm-go-image-to-ascii. We only need to worry about the exposed API from the library rather than how the algorithm works for now. It exposes the following:

func ImageFile2ASCIIString(imgByte []byte, option *Options) string
type Options struct {
  Colored         bool    `json:"colored"`
  FixedWidth      int     `json:"fixedWidth"`
  FixedHeight     int     `json:"fixedHeight"`
  Reversed        bool    `json:"reversed"`
}

Let's divide the whole process into the following tasks:

  1. Create an event listener for file input that passes the selected image to our Go function.
  2. Write the Go function to convert image to ASCII and expose it to the browser.
  3. Build and Integrate into the browser.

Create an event listener for file input

We will move ahead assuming a function named convert(image, options) will be created by Go.

document.querySelector('#file').addEventListener(
  'change',
  function() {
    const reader = new FileReader()
    reader.onload = function() {
      // Converting the image to Unit8Array
      const arrayBuffer = this.result,
        array = new Uint8Array(arrayBuffer)
      // Call wasm exported function
      const txt = convert(
        array,
        JSON.stringify({
          fixedWidth: 100,
          colored: true,
          fixedHeight: 40,
        })
      )
      // To convert Ansi characters to html
      const ansi_up = new AnsiUp()
      const html = ansi_up.ansi_to_html(txt)
      // Showing the ascii image in the browser
      const cdiv = document.getElementById('console')
      cdiv.innerHTML = html
    }
    reader.readAsArrayBuffer(this.files[0])
  },
  false
)

We have added a change listener to input with id file. Once the image is selected by the user we will send the image by converting it to Unit8Array to the convert function.

Go function to convert image to ASCII


package main

import (
  "encoding/json"
  _ "image/jpeg"
  _ "image/png"
  "syscall/js"

  "github.com/subeshb1/wasm-go-image-to-ascii/convert"
)

func converter(this js.Value, inputs []js.Value) interface{} {
  imageArr := inputs[0]
  options := inputs[1].String()
  inBuf := make([]uint8, imageArr.Get("byteLength").Int())
  js.CopyBytesToGo(inBuf, imageArr)
  convertOptions := convert.Options{}
  err := json.Unmarshal([]byte(options), &convertOptions)
  if err != nil {
    convertOptions = convert.DefaultOptions
  }

  converter := convert.NewImageConverter()
  return converter.ImageFile2ASCIIString(inBuf, &convertOptions)
}

func main() {
  c := make(chan bool)
  js.Global().Set("convert", js.FuncOf(converter))
  <-c
}

We expose a convert function that takes image bytes and options. We use js.CopyBytesToGo to convert javascript Uint8Array to Go []byte. After the image is converted the function returns a string of Ascii/Ansi characters.

Build and Integrate into the browser

Finally, we can build the code to wasm and import it to the browser.

<html>
  <head>
    <meta charset="utf-8" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/BrowserFS/2.0.0/browserfs.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/drudru/ansi_up/ansi_up.js"></script>
    <script src="wasm_exec.js"></script>
  </head>
  <body>
    <!-- ASCII Image container  -->
    <pre
      id="console"
      style="background: black; color: white; overflow: scroll;"
    ></pre>
    <!-- Input to select file -->
    <input type="file" name="file" id="file" />
    <script>
      // Integrating WebAssembly
      const go = new Go()
      WebAssembly.instantiateStreaming(
        fetch('main.wasm'),
        go.importObject
      ).then(result => {
        go.run(result.instance)
      })
      // Adding image change listener
      document.querySelector('#file').addEventListener(
        'change',
        function() {
          const reader = new FileReader()
          reader.onload = function() {
            // Converting the image to Unit8Array
            const arrayBuffer = this.result,
              array = new Uint8Array(arrayBuffer)
            // Call wasm exported function
            const txt = convert(
              array,
              JSON.stringify({
                fixedWidth: 100,
                colored: true,
                fixedHeight: 40,
              })
            )
            // To convert Ansi characters to html
            const ansi_up = new AnsiUp()
            const html = ansi_up.ansi_to_html(txt)
            // Showing the ascii image in the browser
            const cdiv = document.getElementById('console')
            cdiv.innerHTML = html
          }
          reader.readAsArrayBuffer(this.files[0])
        },
        false
      )
    </script>
  </body>
</html>

Image to ASCII
Here is the link to the repository: https://github.com/subeshb1/wasm-go-image-to-ascii

Conclusion

We looked at the basics of Wasm and how to use it to import Go code into the browser. We also looked at how we can import an existing library and create a real-world application to convert images to ASCII characters. Do share your thoughts and feedback in the comment section, and share your project in WebAssembly as well. Although Wasm is in an early stage, we can see how useful it can be to remove language dependency on the browser and improve performance by running at near-native speed.

More Resources on WebAssembly:

Discussion

pic
Editor guide