Skip to content

Commit

Permalink
Merge branch 'display_image' of https://github.com/cosmos72/gophernotes
Browse files Browse the repository at this point in the history
… into cosmos72-display_image
  • Loading branch information
cosmos72 committed Jun 14, 2018
2 parents 9a9ff36 + 9e18fed commit d6c102d
Show file tree
Hide file tree
Showing 5 changed files with 386 additions and 68 deletions.
24 changes: 12 additions & 12 deletions README.md
Expand Up @@ -4,7 +4,7 @@

`gophernotes` is a Go kernel for [Jupyter](http://jupyter.org/) notebooks and [nteract](https://nteract.io/). It lets you use Go interactively in a browser-based notebook or desktop app. Use `gophernotes` to create and share documents that contain live Go code, equations, visualizations and explanatory text. These notebooks, with the live Go code, can then be shared with others via email, Dropbox, GitHub and the [Jupyter Notebook Viewer](http://nbviewer.jupyter.org/). Go forth and do data science, or anything else interesting, with Go notebooks!

**Acknowledgements** - This project utilizes a Go interpreter called [gomacro](https://github.com/cosmos72/gomacro) under the hood to evaluate Go code interactively. The gophernotes logo was designed by the brilliant [Marcus Olsson](https://github.com/marcusolsson) and was inspired by Renee French's original Go Gopher design.
**Acknowledgements** - This project utilizes a Go interpreter called [gomacro](https://github.com/cosmos72/gomacro) under the hood to evaluate Go code interactively. The gophernotes logo was designed by the brilliant [Marcus Olsson](https://github.com/marcusolsson) and was inspired by Renee French's original Go Gopher design.

- [Examples](#examples)
- Install gophernotes:
Expand All @@ -19,9 +19,9 @@

## Examples

### Jupyter Notebook:
### Jupyter Notebook:

![](files/jupyter.gif)
![](files/jupyter.gif)

### nteract:

Expand All @@ -46,7 +46,7 @@
```sh
$ go get -u github.com/gopherdata/gophernotes
$ mkdir -p ~/.local/share/jupyter/kernels/gophernotes
$ cp $GOPATH/src/github.com/gopherdata/gophernotes/kernel/* ~/.local/share/jupyter/kernels/gophernotes
$ cp $GOPATH/src/github.com/gopherdata/gophernotes/kernel/* ~/.local/share/jupyter/kernels/gophernotes
```

To confirm that the `gophernotes` binary is installed and in your PATH, you should see the following when running `gophernotes` directly:
Expand All @@ -57,7 +57,7 @@ $ gophernotes
```

**Note** - if you have the `JUPYTER_PATH` environmental variable set or if you are using an older version of Jupyter, you may need to copy this kernel config to another directory. You can check which directories will be searched by executing:

```sh
$ jupyter --data-dir
```
Expand All @@ -80,7 +80,7 @@ $ gophernotes
```

**Note** - if you have the `JUPYTER_PATH` environmental variable set or if you are using an older version of Jupyter, you may need to copy this kernel config to another directory. You can check which directories will be searched by executing:

```sh
$ jupyter --data-dir
```
Expand All @@ -102,12 +102,12 @@ Then:
REM Download w/o building.
go get -d github.com/gopherdata/gophernotes
cd %GOPATH%\src\github.com\gopherdata\gophernotes\zmq-win
REM Build x64 version.
build.bat amd64
move gophernotes.exe %GOPATH%\bin
copy lib-amd64\libzmq.dll %GOPATH%\bin
REM Build x86 version.
build.bat 386
move gophernotes.exe %GOPATH%\bin
Expand All @@ -120,9 +120,9 @@ Then:
mkdir %APPDATA%\jupyter\kernels\gophernotes
xcopy %GOPATH%\src\github.com\gopherdata\gophernotes\kernel %APPDATA%\jupyter\kernels\gophernotes /s
```

Note, if you have the `JUPYTER_PATH` environmental variable set or if you are using an older version of Jupyter, you may need to copy this kernel config to another directory. You can check which directories will be searched by executing:

```
jupyter --data-dir
```
Expand All @@ -143,7 +143,7 @@ Then:

### Docker

You can try out or run Jupyter + gophernotes without installing anything using Docker. To run a Go notebook that only needs things from the standard library, run:
You can try out or run Jupyter + gophernotes without installing anything using Docker. To run a Go notebook that only needs things from the standard library, run:

```
$ docker run -it -p 8888:8888 gopherdata/gophernotes
Expand All @@ -159,7 +159,7 @@ In either case, running this command should output a link that you can follow to

```
$ docker run -it -p 8888:8888 -v /path/to/local/notebooks:/path/to/notebooks/in/docker gopherdata/gophernotes
```
```

## Getting Started

Expand Down
152 changes: 152 additions & 0 deletions display.go
@@ -0,0 +1,152 @@
package main

import (
"errors"
"fmt"
r "reflect"
"strings"

"github.com/cosmos72/gomacro/imports"
)

// Support an interface similar - but not identical - to the IPython (canonical Jupyter kernel).
// See http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.display
// for a good overview of the support types. Note: This is missing _repr_markdown_ and _repr_javascript_.

const (
MIMETypeHTML = "text/html"
MIMETypeJavaScript = "application/javascript"
MIMETypeJPEG = "image/jpeg"
MIMETypeJSON = "application/json"
MIMETypeLatex = "text/latex"
MIMETypeMarkdown = "text/markdown"
MIMETypePNG = "image/png"
MIMETypePDF = "application/pdf"
MIMETypeSVG = "image/svg+xml"
)

// injected as placeholder in the interpreter, it's then replaced at runtime
// by a closure that knows how to talk with Jupyter
func stubDisplay(Data) error {
return errors.New("cannot display: connection with Jupyter not available")
}

// TODO handle the metadata

func MakeData(mimeType string, data interface{}) Data {
return Data{
Data: BundledMIMEData{
"text/plain": fmt.Sprint(data),
mimeType: data,
},
}
}

func MakeData3(mimeType string, plaintext string, data interface{}) Data {
return Data{
Data: BundledMIMEData{
"text/plain": plaintext,
mimeType: data,
},
}
}

func Bytes(mimeType string, bytes []byte) Data {
return MakeData3(mimeType, mimeType, bytes)
}

func HTML(html string) Data {
return MakeData(MIMETypeHTML, html)
}

func JSON(json map[string]interface{}) Data {
return MakeData(MIMETypeJSON, json)
}

func JavaScript(javascript string) Data {
return MakeData(MIMETypeJavaScript, javascript)
}

func JPEG(jpeg []byte) Data {
return MakeData3(MIMETypeJPEG, "jpeg image", jpeg) // []byte are encoded as base64 by the marshaller
}

func Latex(latex string) Data {
return MakeData3(MIMETypeLatex, latex, "$"+strings.Trim(latex, "$")+"$")
}

func Markdown(markdown string) Data {
return MakeData(MIMETypeMarkdown, markdown)
}

func Math(latex string) Data {
return MakeData3(MIMETypeLatex, latex, "$$"+strings.Trim(latex, "$")+"$$")
}

func PDF(pdf []byte) Data {
return MakeData3(MIMETypePDF, "pdf document", pdf) // []byte are encoded as base64 by the marshaller
}

func PNG(png []byte) Data {
return MakeData3(MIMETypePNG, "png image", png) // []byte are encoded as base64 by the marshaller
}

func String(mimeType string, s string) Data {
return MakeData(mimeType, s)
}

func SVG(svg string) Data {
return MakeData(MIMETypeSVG, svg)
}

// MIME encapsulates the data and metadata into a Data.
// The 'data' map is expected to contain at least one {key,value} pair,
// with value being a string, []byte or some other JSON serializable representation,
// and key equal to the MIME type of such value.
// The exact structure of value is determined by what the frontend expects.
// Some easier-to-use functions for common formats supported by the Jupyter frontend
// are provided by the various functions above.
func MIME(data, metadata map[string]interface{}) Data {
return Data{data, metadata, nil}
}

// prepare imports.Package for interpreted code
var display = imports.Package{
Binds: map[string]r.Value{
"Bytes": r.ValueOf(Bytes),
"HTML": r.ValueOf(HTML),
"Image": r.ValueOf(Image),
"JPEG": r.ValueOf(JPEG),
"JSON": r.ValueOf(JSON),
"JavaScript": r.ValueOf(JavaScript),
"Latex": r.ValueOf(Latex),
"MakeData": r.ValueOf(MakeData),
"MakeData3": r.ValueOf(MakeData3),
"Markdown": r.ValueOf(Markdown),
"Math": r.ValueOf(Math),
"MIME": r.ValueOf(MIME),
"MIMETypeHTML": r.ValueOf(MIMETypeHTML),
"MIMETypeJavaScript": r.ValueOf(MIMETypeJavaScript),
"MIMETypeJPEG": r.ValueOf(MIMETypeJPEG),
"MIMETypeJSON": r.ValueOf(MIMETypeJSON),
"MIMETypeLatex": r.ValueOf(MIMETypeLatex),
"MIMETypeMarkdown": r.ValueOf(MIMETypeMarkdown),
"MIMETypePDF": r.ValueOf(MIMETypePDF),
"MIMETypePNG": r.ValueOf(MIMETypePNG),
"MIMETypeSVG": r.ValueOf(MIMETypeSVG),
"PDF": r.ValueOf(PDF),
"PNG": r.ValueOf(PNG),
"String": r.ValueOf(String),
"SVG": r.ValueOf(SVG),
},
Types: map[string]r.Type{
"BundledMIMEData": r.TypeOf((*BundledMIMEData)(nil)).Elem(),
"Data": r.TypeOf((*Data)(nil)).Elem(),
},
}

// allow importing "display" and "github.com/gopherdata/gophernotes" packages
func init() {
imports.Packages["display"] = display
imports.Packages["github.com/gopherdata/gophernotes"] = display
}
103 changes: 103 additions & 0 deletions image.go
@@ -0,0 +1,103 @@
package main

import (
"bytes"
"fmt"
"image"
"image/png"
)

// Image converts an image.Image to DisplayData containing PNG []byte,
// or to DisplayData containing error if the conversion fails
func Image(img image.Image) Data {
data, err := image0(img)
if err != nil {
return Data{
Data: BundledMIMEData{
"ename": "ERROR",
"evalue": err.Error(),
"traceback": nil,
"status": "error",
},
}
}
return data
}

// Image converts an image.Image to Data containing PNG []byte,
// or error if the conversion fails
func image0(img image.Image) (Data, error) {
bytes, mime, err := encodePng(img)
if err != nil {
return Data{}, err
}
return Data{
Data: BundledMIMEData{
mime: bytes,
},
Metadata: BundledMIMEData{
mime: imageMetadata(img),
},
}, nil
}

// encodePng converts an image.Image to PNG []byte
func encodePng(img image.Image) (data []byte, mime string, err error) {
var buf bytes.Buffer
err = png.Encode(&buf, img)
if err != nil {
return nil, "", err
}
return buf.Bytes(), "image/png", nil
}

// imageMetadata returns image size, represented as BundledMIMEData{"width": width, "height": height}
func imageMetadata(img image.Image) BundledMIMEData {
rect := img.Bounds()
return BundledMIMEData{
"width": rect.Dx(),
"height": rect.Dy(),
}
}

// PublishImage sends a "display_data" broadcast message for given image.Image.
func (receipt *msgReceipt) PublishImage(img image.Image) error {
data, err := image0(img)
if err != nil {
return err
}
return receipt.PublishDisplayData(data)
}

// if vals[] contain a single non-nil value which is an image.Image,
// convert it to Data and return it.
// if instead the single non-nil value is a Data, return it.
// otherwise return MakeData("text/plain", fmt.Sprint(vals...))
func renderResults(vals []interface{}) Data {
var nilcount int
var obj interface{}
for _, val := range vals {
switch val.(type) {
case image.Image, Data:
obj = val
case nil:
nilcount++
}
}
if obj != nil && nilcount == len(vals)-1 {
switch val := obj.(type) {
case image.Image:
data, err := image0(val)
if err == nil {
return data
}
case Data:
return val
}
}
if nilcount == len(vals) {
// if all values are nil, return empty Data
return Data{}
}
return MakeData("text/plain", fmt.Sprint(vals...))
}

0 comments on commit d6c102d

Please sign in to comment.