From 5cd261c199ab162dea90fad2d1b4e1953c8baab4 Mon Sep 17 00:00:00 2001
From: David Vogel <Dadido3@aol.com>
Date: Fri, 20 Dec 2024 21:12:59 +0100
Subject: [PATCH] Add image.Image wrapper

---
 README.md     |  2 +-
 cli.go        |  2 +-
 image.go      | 33 +++++++++++++++++++++++++++++++++
 image_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 85 insertions(+), 2 deletions(-)
 create mode 100644 image.go
 create mode 100644 image_test.go

diff --git a/README.md b/README.md
index 407fb19..73a6ca2 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ The supported and tested versions right now are:
 
 - PDF, SVG and PNG generation.
 - All typst-cli parameters are [available as a struct](cli-options.go), which makes it easy to discover all available options.
-- Encoder to convert go values into typst markup which can be injected into typst documents.
+- Encoder to convert go values into typst markup which can be injected into typst documents. This includes image.Image by using the [Image wrapper](image.go).
 - Any stderr will be returned as go error value, including line number, column and file path of the error.
 - Uses stdio; No temporary files will be created.
 - Good unit test coverage.
diff --git a/cli.go b/cli.go
index bad70ac..d9800b8 100644
--- a/cli.go
+++ b/cli.go
@@ -12,7 +12,7 @@ import (
 	"os/exec"
 )
 
-// TODO: Add docker support to CLI
+// TODO: Add docker support to CLI, by calling docker run instead
 
 type CLI struct {
 	ExecutablePath string // The typst executable path can be overridden here. Otherwise the default path will be used.
diff --git a/image.go b/image.go
new file mode 100644
index 0000000..3433439
--- /dev/null
+++ b/image.go
@@ -0,0 +1,33 @@
+package typst
+
+import (
+	"bytes"
+	"fmt"
+	"image"
+	"image/png"
+	"strconv"
+)
+
+// Image can be used to encode any image.Image into a typst image.
+//
+// For this, just wrap any image.Image with this type before passing it to MarshalVariable or a VariableEncoder.
+type Image struct{ image.Image }
+
+func (i Image) MarshalTypstVariable() ([]byte, error) {
+	var buffer bytes.Buffer
+
+	if err := png.Encode(&buffer, i); err != nil {
+		return nil, fmt.Errorf("failed to encode image as PNG: %w", err)
+	}
+
+	// TODO: Make image encoding more efficient: Use reader/writer, baseXX encoding
+
+	var buf bytes.Buffer
+	buf.WriteString("image.decode(bytes((")
+	for _, b := range buffer.Bytes() {
+		buf.WriteString(strconv.FormatUint(uint64(b), 10) + ",")
+	}
+	buf.WriteString(")))")
+
+	return buf.Bytes(), nil
+}
diff --git a/image_test.go b/image_test.go
new file mode 100644
index 0000000..1c6f8d3
--- /dev/null
+++ b/image_test.go
@@ -0,0 +1,50 @@
+package typst_test
+
+import (
+	"bytes"
+	"image"
+	"image/color"
+	"io"
+	"testing"
+
+	"github.com/Dadido3/go-typst"
+)
+
+type testImage struct {
+	Rect image.Rectangle
+}
+
+func (p *testImage) ColorModel() color.Model { return color.RGBAModel }
+
+func (p *testImage) Bounds() image.Rectangle { return p.Rect }
+
+func (p *testImage) At(x, y int) color.Color { return p.RGBAAt(x, y) }
+
+func (p *testImage) RGBAAt(x, y int) color.RGBA {
+	if !(image.Point{x, y}.In(p.Rect)) {
+		return color.RGBA{}
+	}
+	return color.RGBA{uint8(x), uint8(y), uint8(x + y), 255}
+}
+
+// Opaque scans the entire image and reports whether it is fully opaque.
+func (p *testImage) Opaque() bool {
+	return true
+}
+
+func TestImage(t *testing.T) {
+	img := &testImage{image.Rect(0, 0, 255, 255)}
+
+	// Wrap image.
+	typstImage := typst.Image{img}
+
+	cli := typst.CLI{}
+
+	r := bytes.NewBufferString(`= Image test
+
+#TestImage`)
+
+	if err := cli.CompileWithVariables(r, io.Discard, nil, map[string]any{"TestImage": typstImage}); err != nil {
+		t.Fatalf("Failed to compile document: %v.", err)
+	}
+}