Merge branch 'master' into dagre-image-label

This commit is contained in:
Alexander Wang 2022-11-21 13:55:48 -08:00
commit 7fe5fb095b
No known key found for this signature in database
GPG key ID: D89FA31966BDBECE
9 changed files with 180 additions and 12 deletions

View file

@ -2,6 +2,7 @@ For v0.0.99 we focused on X, Y and Z. Enjoy!
#### Features 💸
- Ability to export to PNG
- Now you can easily do x, y and z #9999
#### Improvements 🔧

View file

@ -10,6 +10,8 @@
.Op Fl -theme Em 0
.Ar file.d2
.Op Ar file.svg
|
.Op Ar file.png
.Nm d2
.Op Fl -watch Ar false
.Op Fl -theme Em 0
@ -23,6 +25,8 @@ compiles and renders
.Ar file.d2
to
.Ar file.svg
|
.Ar file.png
.Ns .
.Pp
Pass - to have

View file

@ -15,9 +15,9 @@ import (
func help(ms *xmain.State) {
fmt.Fprintf(ms.Stdout, `Usage:
%s [--watch=false] [--theme=0] file.d2 [file.svg]
%s [--watch=false] [--theme=0] file.d2 [file.svg|file.png]
%[1]s compiles and renders file.d2 to file.svg
%[1]s compiles and renders file.d2 to file.svg|file.png.
Use - to have d2 read from stdin or write to stdout.
Flags:

View file

@ -9,8 +9,7 @@ import (
"strings"
"time"
_ "embed"
"github.com/playwright-community/playwright-go"
"github.com/spf13/pflag"
"oss.terrastruct.com/d2"
@ -19,6 +18,7 @@ import (
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/version"
"oss.terrastruct.com/d2/lib/xmain"
)
@ -38,7 +38,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
}
hostFlag := ms.Opts.String("HOST", "host", "h", "localhost", "host listening address when used with watch")
portFlag := ms.Opts.String("PORT", "port", "p", "0", "port listening address when used with watch")
bundleFlag, err := ms.Opts.Bool("D2_BUNDLE", "bundle", "b", true, "bundle all assets and layers into the output svg.")
bundleFlag, err := ms.Opts.Bool("D2_BUNDLE", "bundle", "b", true, "when outputting SVG, bundle all assets and layers into the output file.")
if err != nil {
return xmain.UsageErrorf(err.Error())
}
@ -127,12 +127,25 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
}
ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, pluginLocation)
var pw png.Playwright
if filepath.Ext(outputPath) == ".png" {
pw, err = png.InitPlaywright()
if err != nil {
return err
}
defer func() {
cleanupErr := pw.Cleanup()
if err == nil {
err = cleanupErr
}
}()
}
if *watchFlag {
if inputPath == "-" {
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
}
ms.Env.Setenv("LOG_TIMESTAMPS", "1")
w, err := newWatcher(ctx, ms, watcherOpts{
layoutPlugin: plugin,
themeID: *themeFlag,
@ -140,6 +153,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
port: *portFlag,
inputPath: inputPath,
outputPath: outputPath,
pw: pw,
})
if err != nil {
return err
@ -154,7 +168,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
_ = 343
}
_, err = compile(ctx, ms, plugin, *themeFlag, inputPath, outputPath)
_, err = compile(ctx, ms, plugin, *themeFlag, inputPath, outputPath, pw.Page)
if err != nil {
return err
}
@ -162,7 +176,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
return nil
}
func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, themeID int64, inputPath, outputPath string) ([]byte, error) {
func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, themeID int64, inputPath, outputPath string, page playwright.Page) ([]byte, error) {
input, err := ms.ReadPath(inputPath)
if err != nil {
return nil, err
@ -191,7 +205,15 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, theme
return nil, err
}
err = ms.WritePath(outputPath, svg)
out := svg
if filepath.Ext(outputPath) == ".png" {
out, err = png.ConvertSVG(ms, page, svg)
if err != nil {
return nil, err
}
}
err = ms.WritePath(outputPath, out)
if err != nil {
return nil, err
}

View file

@ -20,6 +20,7 @@ import (
"nhooyr.io/websocket/wsjson"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/xbrowser"
"oss.terrastruct.com/d2/lib/xhttp"
"oss.terrastruct.com/d2/lib/xmain"
@ -41,6 +42,7 @@ type watcherOpts struct {
port string
inputPath string
outputPath string
pw png.Playwright
}
type watcher struct {
@ -330,7 +332,20 @@ func (w *watcher) compileLoop(ctx context.Context) error {
recompiledPrefix = "re"
}
b, err := compile(ctx, w.ms, w.layoutPlugin, w.themeID, w.inputPath, w.outputPath)
if filepath.Ext(w.outputPath) == ".png" && !w.pw.Browser.IsConnected() {
newPW, err := w.pw.RestartBrowser()
if err != nil {
broadcastErr := fmt.Errorf("issue encountered with PNG exporter: %w", err)
w.ms.Log.Error.Print(broadcastErr)
w.broadcast(&compileResult{
Err: broadcastErr.Error(),
})
continue
}
w.pw = newPW
}
b, err := compile(ctx, w.ms, w.layoutPlugin, w.themeID, w.inputPath, w.outputPath, w.pw.Page)
if err != nil {
err = fmt.Errorf("failed to %scompile: %w", recompiledPrefix, err)
w.ms.Log.Error.Print(err)

5
go.mod
View file

@ -11,6 +11,7 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mazznoer/csscolorparser v0.1.3
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/playwright-community/playwright-go v0.2000.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/yuin/goldmark v1.5.2
@ -33,13 +34,14 @@ require (
require (
cloud.google.com/go/compute v1.7.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/gin-gonic/gin v1.7.7 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/kr/pretty v0.3.0 // indirect
@ -57,6 +59,7 @@ require (
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

9
go.sum
View file

@ -88,6 +88,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -130,6 +132,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
@ -223,6 +227,7 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -266,6 +271,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/playwright-community/playwright-go v0.2000.1 h1:2JViSHpJQ/UL/PO1Gg6gXV5IcXAAsoBJ3KG9L3wKXto=
github.com/playwright-community/playwright-go v0.2000.1/go.mod h1:1y9cM9b9dVHnuRWzED1KLM7FtbwTJC8ibDjI6MNqewU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -751,6 +758,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

22
lib/png/generate_png.js Normal file
View file

@ -0,0 +1,22 @@
async (imgString) => {
const tempImg = new Image();
const loadImage = () => {
return new Promise((resolve, reject) => {
tempImg.onload = (event) => resolve(event.currentTarget);
tempImg.onerror = () => {
reject("error loading string as an image");
};
tempImg.src = imgString;
});
};
const img = await loadImage();
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
if (!ctx) {
return new Error("could not get canvas context");
}
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL("image/png");
}

92
lib/png/png.go Normal file
View file

@ -0,0 +1,92 @@
package png
import (
"encoding/base64"
"fmt"
"strings"
_ "embed"
"github.com/playwright-community/playwright-go"
"oss.terrastruct.com/d2/lib/xmain"
)
type Playwright struct {
PW *playwright.Playwright
Browser playwright.Browser
Page playwright.Page
}
func (pw *Playwright) RestartBrowser() (Playwright, error) {
if err := pw.Browser.Close(); err != nil {
return Playwright{}, fmt.Errorf("failed to close Playwright browser: %w", err)
}
return startPlaywright(pw.PW)
}
func (pw *Playwright) Cleanup() error {
if err := pw.Browser.Close(); err != nil {
return fmt.Errorf("failed to close Playwright browser: %w", err)
}
if err := pw.PW.Stop(); err != nil {
return fmt.Errorf("failed to stop Playwright: %w", err)
}
return nil
}
func startPlaywright(pw *playwright.Playwright) (Playwright, error) {
browser, err := pw.Chromium.Launch()
if err != nil {
return Playwright{}, fmt.Errorf("failed to launch Chromium: %w", err)
}
context, err := browser.NewContext()
if err != nil {
return Playwright{}, fmt.Errorf("failed to start new Playwright browser context: %w", err)
}
page, err := context.NewPage()
if err != nil {
return Playwright{}, fmt.Errorf("failed to start new Playwright page: %w", err)
}
return Playwright{
PW: pw,
Browser: browser,
Page: page,
}, nil
}
func InitPlaywright() (Playwright, error) {
err := playwright.Install(&playwright.RunOptions{Verbose: false})
if err != nil {
return Playwright{}, fmt.Errorf("failed to install Playwright: %w", err)
}
pw, err := playwright.Run()
if err != nil {
return Playwright{}, fmt.Errorf("failed to run Playwright: %w", err)
}
return startPlaywright(pw)
}
//go:embed generate_png.js
var genPNGScript string
const pngPrefix = "data:image/png;base64,"
func ConvertSVG(ms *xmain.State, page playwright.Page, svg []byte) ([]byte, error) {
encodedSVG := base64.StdEncoding.EncodeToString(svg)
pngInterface, err := page.Evaluate(genPNGScript, "data:image/svg+xml;charset=utf-8;base64,"+encodedSVG)
if err != nil {
return nil, fmt.Errorf("failed to generate png: %w", err)
}
pngString := pngInterface.(string)
if !strings.HasPrefix(pngString, pngPrefix) {
if len(pngString) > 50 {
pngString = pngString[0:50] + "..."
}
return nil, fmt.Errorf("invalid PNG: %q", pngString)
}
splicedPNGString := pngString[len(pngPrefix):]
return base64.StdEncoding.DecodeString(splicedPNGString)
}