diff --git a/d2cli/export.go b/d2cli/export.go index 6da34bdb8..95eee59ab 100644 --- a/d2cli/export.go +++ b/d2cli/export.go @@ -1,6 +1,7 @@ package d2cli import ( + "fmt" "path/filepath" ) @@ -14,6 +15,24 @@ const SVG exportExtension = ".svg" var SUPPORTED_EXTENSIONS = []exportExtension{SVG, PNG, PDF, PPTX, GIF} +func getOutputFormat(formatFlag *string, outputPath string) (exportExtension, error) { + var formatMap = map[string]exportExtension{ + "png": PNG, + "svg": SVG, + "pdf": PDF, + "pptx": PPTX, + "gif": GIF, + } + + if *formatFlag != "" { + if format, ok := formatMap[*formatFlag]; ok { + return format, nil + } + return "", fmt.Errorf("unsupported format: %s", *formatFlag) + } + return getExportExtension(outputPath), nil +} + func getExportExtension(outputPath string) exportExtension { ext := filepath.Ext(outputPath) for _, kext := range SUPPORTED_EXTENSIONS { diff --git a/d2cli/export_test.go b/d2cli/export_test.go index eb7ac44ee..cd8df49a0 100644 --- a/d2cli/export_test.go +++ b/d2cli/export_test.go @@ -8,6 +8,7 @@ import ( func TestOutputFormat(t *testing.T) { type testCase struct { + formatFlag string outputPath string extension exportExtension supportsDarkTheme bool @@ -41,6 +42,15 @@ func TestOutputFormat(t *testing.T) { requiresAnimationInterval: false, requiresPngRender: false, }, + { + formatFlag: "png", + outputPath: "-", + extension: PNG, + supportsDarkTheme: false, + supportsAnimation: false, + requiresAnimationInterval: false, + requiresPngRender: true, + }, { outputPath: "/out.png", extension: PNG, @@ -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.formatFlag, 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()) diff --git a/d2cli/main.go b/d2cli/main.go index eeefd6ae9..e292efa7a 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -103,6 +103,11 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { if err != nil { return err } + formatFlag := ms.Opts.String("", "format", "f", "", "stdout output format (svg, 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 { @@ -213,7 +218,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(formatFlag, outputPath) + if err != nil { + return xmain.UsageErrorf("%v", err) + } + if outputPath != "-" { outputPath = ms.AbsPath(outputPath) if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() { @@ -325,6 +335,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { forceAppendix: *forceAppendixFlag, pw: pw, fontFamily: fontFamily, + outputFormat: outputFormat, }) if err != nil { return err @@ -355,7 +366,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) @@ -430,7 +441,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 { @@ -522,7 +533,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) @@ -598,9 +608,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 @@ -739,7 +749,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) @@ -785,21 +795,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 } @@ -808,7 +818,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 } @@ -822,9 +832,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 } @@ -835,8 +845,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 diff --git a/d2cli/watch.go b/d2cli/watch.go index 61b236348..35188970e 100644 --- a/d2cli/watch.go +++ b/d2cli/watch.go @@ -3,7 +3,6 @@ package d2cli import ( "context" "embed" - _ "embed" "errors" "fmt" "io/fs" @@ -57,6 +56,7 @@ type watcherOpts struct { forceAppendix bool pw png.Playwright fontFamily *d2fonts.FontFamily + outputFormat exportExtension } type watcher struct { @@ -430,7 +430,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 {