feat(cli): add format flag to support PNG output to stdout
This commit is contained in:
parent
103d745764
commit
ec2e1f1c1e
4 changed files with 59 additions and 16 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
- Connections now support `link` [#1955](https://github.com/terrastruct/d2/pull/1955)
|
- Connections now support `link` [#1955](https://github.com/terrastruct/d2/pull/1955)
|
||||||
- Vars: vars in markdown blocks are substituted [#2218](https://github.com/terrastruct/d2/pull/2218)
|
- 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)
|
- Markdown: Github-flavored tables work in `md` blocks [#2221](https://github.com/terrastruct/d2/pull/2221)
|
||||||
|
- CLI: PNG output to stdout is supported using `--stdout-format png -` [#2260](https://github.com/terrastruct/d2/pull/2260)
|
||||||
- `d2 fmt` now supports a `--check` flag [#2253](https://github.com/terrastruct/d2/pull/2253)
|
- `d2 fmt` now supports a `--check` flag [#2253](https://github.com/terrastruct/d2/pull/2253)
|
||||||
|
|
||||||
#### Improvements 🧹
|
#### Improvements 🧹
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package d2cli
|
package d2cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type exportExtension string
|
type exportExtension string
|
||||||
|
|
@ -14,6 +16,24 @@ const SVG exportExtension = ".svg"
|
||||||
|
|
||||||
var SUPPORTED_EXTENSIONS = []exportExtension{SVG, PNG, PDF, PPTX, GIF}
|
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 {
|
func getExportExtension(outputPath string) exportExtension {
|
||||||
ext := filepath.Ext(outputPath)
|
ext := filepath.Ext(outputPath)
|
||||||
for _, kext := range SUPPORTED_EXTENSIONS {
|
for _, kext := range SUPPORTED_EXTENSIONS {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
func TestOutputFormat(t *testing.T) {
|
func TestOutputFormat(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
|
stdoutFormatFlag string
|
||||||
outputPath string
|
outputPath string
|
||||||
extension exportExtension
|
extension exportExtension
|
||||||
supportsDarkTheme bool
|
supportsDarkTheme bool
|
||||||
|
|
@ -41,6 +42,15 @@ func TestOutputFormat(t *testing.T) {
|
||||||
requiresAnimationInterval: false,
|
requiresAnimationInterval: false,
|
||||||
requiresPngRender: false,
|
requiresPngRender: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
stdoutFormatFlag: "png",
|
||||||
|
outputPath: "-",
|
||||||
|
extension: PNG,
|
||||||
|
supportsDarkTheme: false,
|
||||||
|
supportsAnimation: false,
|
||||||
|
requiresAnimationInterval: false,
|
||||||
|
requiresPngRender: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
outputPath: "/out.png",
|
outputPath: "/out.png",
|
||||||
extension: PNG,
|
extension: PNG,
|
||||||
|
|
@ -78,7 +88,8 @@ func TestOutputFormat(t *testing.T) {
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.outputPath, func(t *testing.T) {
|
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.extension, extension)
|
||||||
assert.Equal(t, tc.supportsAnimation, extension.supportsAnimation())
|
assert.Equal(t, tc.supportsAnimation, extension.supportsAnimation())
|
||||||
assert.Equal(t, tc.supportsDarkTheme, extension.supportsDarkTheme())
|
assert.Equal(t, tc.supportsDarkTheme, extension.supportsDarkTheme())
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,11 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.")
|
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")
|
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 {
|
if err != nil {
|
||||||
|
|
@ -218,7 +223,12 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
if filepath.Ext(outputPath) == ".ppt" {
|
if filepath.Ext(outputPath) == ".ppt" {
|
||||||
return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?")
|
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 != "-" {
|
if outputPath != "-" {
|
||||||
outputPath = ms.AbsPath(outputPath)
|
outputPath = ms.AbsPath(outputPath)
|
||||||
if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
|
if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
|
||||||
|
|
@ -330,6 +340,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
forceAppendix: *forceAppendixFlag,
|
forceAppendix: *forceAppendixFlag,
|
||||||
pw: pw,
|
pw: pw,
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
|
outputFormat: outputFormat,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -360,7 +371,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
|
ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
|
||||||
defer cancel()
|
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 err != nil {
|
||||||
if written {
|
if written {
|
||||||
return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
|
return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
|
||||||
|
|
@ -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()
|
start := time.Now()
|
||||||
input, err := ms.ReadPath(inputPath)
|
input, err := ms.ReadPath(inputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -527,7 +538,6 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ext := getExportExtension(outputPath)
|
|
||||||
switch ext {
|
switch ext {
|
||||||
case GIF:
|
case GIF:
|
||||||
svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, inputPath, diagram)
|
svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, inputPath, diagram)
|
||||||
|
|
@ -603,9 +613,9 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
||||||
var boards [][]byte
|
var boards [][]byte
|
||||||
var err error
|
var err error
|
||||||
if noChildren {
|
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 {
|
} 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 {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
|
|
@ -744,7 +754,7 @@ func relink(currDiagramPath string, d *d2target.Diagram, linkToOutput map[string
|
||||||
return nil
|
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 != "" {
|
if diagram.Name != "" {
|
||||||
ext := filepath.Ext(outputPath)
|
ext := filepath.Ext(outputPath)
|
||||||
outputPath = strings.TrimSuffix(outputPath, ext)
|
outputPath = strings.TrimSuffix(outputPath, ext)
|
||||||
|
|
@ -790,21 +800,21 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
||||||
|
|
||||||
var boards [][]byte
|
var boards [][]byte
|
||||||
for _, dl := range diagram.Layers {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
boards = append(boards, childrenBoards...)
|
boards = append(boards, childrenBoards...)
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Scenarios {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
boards = append(boards, childrenBoards...)
|
boards = append(boards, childrenBoards...)
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Steps {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -813,7 +823,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
||||||
|
|
||||||
if !diagram.IsFolderOnly {
|
if !diagram.IsFolderOnly {
|
||||||
start := time.Now()
|
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 {
|
if err != nil {
|
||||||
return boards, err
|
return boards, err
|
||||||
}
|
}
|
||||||
|
|
@ -827,9 +837,9 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
||||||
return boards, nil
|
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()
|
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 {
|
if err != nil {
|
||||||
return [][]byte{}, err
|
return [][]byte{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -840,8 +850,9 @@ func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration
|
||||||
return [][]byte{out}, nil
|
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) {
|
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 := getExportExtension(outputPath) == PNG
|
toPNG := outputFormat == PNG
|
||||||
|
|
||||||
var scale *float64
|
var scale *float64
|
||||||
if opts.Scale != nil {
|
if opts.Scale != nil {
|
||||||
scale = opts.Scale
|
scale = opts.Scale
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue