Skip to content

Commit

Permalink
Merge pull request #2291 from Maricaya/output-png
Browse files Browse the repository at this point in the history
cli: add format flag to support PNG output to stdout
  • Loading branch information
alixander authored Jan 17, 2025
2 parents ac7dd41 + 3001d2c commit 6f0287d
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 17 deletions.
1 change: 1 addition & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Vars: vars in markdown blocks are substituted [#2218](https://github.com/terrastruct/d2/pull/2218)
- Markdown: Github-flavored tables work in `md` blocks [#2221](https://github.com/terrastruct/d2/pull/2221)
- `d2 fmt` now supports a `--check` flag [#2253](https://github.com/terrastruct/d2/pull/2253)
- CLI: PNG output to stdout is supported using `--stdout-format png -` [#2291](https://github.com/terrastruct/d2/pull/2291)

#### Improvements 🧹

Expand Down
5 changes: 5 additions & 0 deletions ci/release/template/man/d2.1
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ Print usage information and exit
.It Fl v , -version
Print version information and exit
.Ns .
.It Fl -stdout-format Ar string
Set the output format when writing to stdout. Supported formats are: png, svg. Only used when output is set to stdout (-)
.Ns .
.El
.Sh SUBCOMMANDS
.Bl -tag -width Fl
Expand Down Expand Up @@ -197,6 +200,8 @@ See -h[ost] flag.
See -p[ort] flag.
.It Ev Sy BROWSER
See --browser flag.
.It Ev Sy D2_STDOUT_FORMAT
See --stdout-format flag.
.El
.Sh SEE ALSO
.Xr d2plugin-tala 1
Expand Down
20 changes: 20 additions & 0 deletions d2cli/export.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package d2cli

import (
"fmt"
"path/filepath"
"strings"
)

type exportExtension string
Expand All @@ -14,6 +16,24 @@ const SVG exportExtension = ".svg"

var SUPPORTED_EXTENSIONS = []exportExtension{SVG, PNG, PDF, PPTX, GIF}

var STDOUT_FORMAT_MAP = map[string]exportExtension{
"png": PNG,
"svg": SVG,
}

var SUPPORTED_STDOUT_FORMATS = []string{"png", "svg"}

func getOutputFormat(stdoutFormatFlag *string, outputPath string) (exportExtension, error) {
if *stdoutFormatFlag != "" {
format := strings.ToLower(*stdoutFormatFlag)
if ext, ok := STDOUT_FORMAT_MAP[format]; ok {
return ext, nil
}
return "", fmt.Errorf("%s is not a supported format. Supported formats are: %s", *stdoutFormatFlag, SUPPORTED_STDOUT_FORMATS)
}
return getExportExtension(outputPath), nil
}

func getExportExtension(outputPath string) exportExtension {
ext := filepath.Ext(outputPath)
for _, kext := range SUPPORTED_EXTENSIONS {
Expand Down
13 changes: 12 additions & 1 deletion d2cli/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

func TestOutputFormat(t *testing.T) {
type testCase struct {
stdoutFormatFlag string
outputPath string
extension exportExtension
supportsDarkTheme bool
Expand Down Expand Up @@ -41,6 +42,15 @@ func TestOutputFormat(t *testing.T) {
requiresAnimationInterval: false,
requiresPngRender: false,
},
{
stdoutFormatFlag: "png",
outputPath: "-",
extension: PNG,
supportsDarkTheme: false,
supportsAnimation: false,
requiresAnimationInterval: false,
requiresPngRender: true,
},
{
outputPath: "/out.png",
extension: PNG,
Expand Down Expand Up @@ -78,7 +88,8 @@ func TestOutputFormat(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.outputPath, func(t *testing.T) {
extension := getExportExtension(tc.outputPath)
extension, err := getOutputFormat(&tc.stdoutFormatFlag, tc.outputPath)
assert.NoError(t, err)
assert.Equal(t, tc.extension, extension)
assert.Equal(t, tc.supportsAnimation, extension.supportsAnimation())
assert.Equal(t, tc.supportsDarkTheme, extension.supportsDarkTheme())
Expand Down
41 changes: 26 additions & 15 deletions d2cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if err != nil {
return err
}
stdoutFormatFlag := ms.Opts.String("", "stdout-format", "", "", "output format when writing to stdout (svg, png). Usage: d2 input.d2 --stdout-format png - > output.png")
if err != nil {
return err
}

browserFlag := ms.Opts.String("BROWSER", "browser", "", "", "browser executable that watch opens. Setting to 0 opens no browser.")
centerFlag, err := ms.Opts.Bool("D2_CENTER", "center", "c", false, "center the SVG in the containing viewbox, such as your browser screen")
if err != nil {
Expand Down Expand Up @@ -218,7 +223,12 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if filepath.Ext(outputPath) == ".ppt" {
return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?")
}
outputFormat := getExportExtension(outputPath)

outputFormat, err := getOutputFormat(stdoutFormatFlag, outputPath)
if err != nil {
return xmain.UsageErrorf("%v", err)
}

if outputPath != "-" {
outputPath = ms.AbsPath(outputPath)
if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
Expand Down Expand Up @@ -330,6 +340,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
forceAppendix: *forceAppendixFlag,
pw: pw,
fontFamily: fontFamily,
outputFormat: outputFormat,
})
if err != nil {
return err
Expand Down Expand Up @@ -360,7 +371,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
defer cancel()

_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page)
_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page, outputFormat)
if err != nil {
if written {
return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
Expand Down Expand Up @@ -435,7 +446,7 @@ func RouterResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plu
}
}

func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, boardPath []string, noChildren, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, boardPath []string, noChildren, bundle, forceAppendix bool, page playwright.Page, ext exportExtension) (_ []byte, written bool, _ error) {
start := time.Now()
input, err := ms.ReadPath(inputPath)
if err != nil {
Expand Down Expand Up @@ -527,7 +538,6 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
return nil, false, err
}

ext := getExportExtension(outputPath)
switch ext {
case GIF:
svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, inputPath, diagram)
Expand Down Expand Up @@ -603,9 +613,9 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
var boards [][]byte
var err error
if noChildren {
boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, ext)
} else {
boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, ext)
}
if err != nil {
return nil, false, err
Expand Down Expand Up @@ -744,7 +754,7 @@ func relink(currDiagramPath string, d *d2target.Diagram, linkToOutput map[string
return nil
}

func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, ext exportExtension) ([][]byte, error) {
if diagram.Name != "" {
ext := filepath.Ext(outputPath)
outputPath = strings.TrimSuffix(outputPath, ext)
Expand Down Expand Up @@ -790,21 +800,21 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug

var boards [][]byte
for _, dl := range diagram.Layers {
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Scenarios {
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Steps {
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl, ext)
if err != nil {
return nil, err
}
Expand All @@ -813,7 +823,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug

if !diagram.IsFolderOnly {
start := time.Now()
out, err := _render(ctx, ms, plugin, opts, inputPath, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
out, err := _render(ctx, ms, plugin, opts, inputPath, boardOutputPath, bundle, forceAppendix, page, ruler, diagram, ext)
if err != nil {
return boards, err
}
Expand All @@ -827,9 +837,9 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
return boards, nil
}

func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, outputFormat exportExtension) ([][]byte, error) {
start := time.Now()
out, err := _render(ctx, ms, plugin, opts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
out, err := _render(ctx, ms, plugin, opts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram, outputFormat)
if err != nil {
return [][]byte{}, err
}
Expand All @@ -840,8 +850,9 @@ func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration
return [][]byte{out}, nil
}

func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
toPNG := getExportExtension(outputPath) == PNG
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, outputFormat exportExtension) ([]byte, error) {
toPNG := outputFormat == PNG

var scale *float64
if opts.Scale != nil {
scale = opts.Scale
Expand Down
3 changes: 2 additions & 1 deletion d2cli/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type watcherOpts struct {
forceAppendix bool
pw png.Playwright
fontFamily *d2fonts.FontFamily
outputFormat exportExtension
}

type watcher struct {
Expand Down Expand Up @@ -430,7 +431,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
if w.boardPath != "" {
boardPath = strings.Split(w.boardPath, string(os.PathSeparator))
}
svg, _, err := compile(ctx, w.ms, w.plugins, &fs, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, boardPath, false, w.bundle, w.forceAppendix, w.pw.Page)
svg, _, err := compile(ctx, w.ms, w.plugins, &fs, w.layout, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, boardPath, false, w.bundle, w.forceAppendix, w.pw.Page, w.outputFormat)
w.boardpathMu.Unlock()
errs := ""
if err != nil {
Expand Down

0 comments on commit 6f0287d

Please sign in to comment.