diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 823e8dd45a..5ecb3a68f2 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -1,6 +1,7 @@ #### Features ๐Ÿš€ - Configure timeout value with D2_TIMEOUT env var. [#1392](https://github.com/terrastruct/d2/pull/1392) +- Scale renders and disable fit to screen with `--scale` flag. [#1413](https://github.com/terrastruct/d2/pull/1413) #### Improvements ๐Ÿงน diff --git a/ci/release/template/man/d2.1 b/ci/release/template/man/d2.1 index 81e1557cb5..d1ebc59395 100644 --- a/ci/release/template/man/d2.1 +++ b/ci/release/template/man/d2.1 @@ -80,6 +80,9 @@ Renders the diagram to look like it was sketched by hand .It Fl -center Ar flag Center the SVG in the containing viewbox, such as your browser screen .Ns . +.It Fl -scale Ar -1 +Scale the output. E.g., 0.5 to halve the default size. Default -1 means that SVG's will fit to screen and all others will use their default render size. Setting to 1 turns off SVG fitting to screen +.Ns . .It Fl -font-regular Path to .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used .Ns . diff --git a/d2cli/main.go b/d2cli/main.go index 5fcf94c5dc..3d4fa60542 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -107,6 +107,10 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { if err != nil { return err } + scaleFlag, err := ms.Opts.Float64("SCALE", "scale", "", -1, "scale the output. E.g., 0.5 to halve the default size. Default -1 means that SVG's will fit to screen and all others will use their default render size. Setting to 1 turns off SVG fitting to screen.") + if err != nil { + return err + } fontRegularFlag := ms.Opts.String("D2_FONT_REGULAR", "font-regular", "", "", "path to .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used.") fontItalicFlag := ms.Opts.String("D2_FONT_ITALIC", "font-italic", "", "", "path to .ttf file to use for the italic font. If none provided, Source Sans Pro Regular-Italic is used.") @@ -232,6 +236,10 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { } ms.Log.Debug.Printf("using dark theme %s (ID: %d)", match.Name, *darkThemeFlag) } + var scale *float64 + if scaleFlag != nil && *scaleFlag > 0. { + scale = scaleFlag + } plugin, err := d2plugin.FindPlugin(ctx, ps, *layoutFlag) if err != nil { @@ -282,6 +290,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { Center: *centerFlag, ThemeID: *themeFlag, DarkThemeID: darkThemeFlag, + Scale: scale, } if *watchFlag { @@ -665,14 +674,20 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) { toPNG := getExportExtension(outputPath) == PNG + var scale *float64 + if opts.Scale != nil { + scale = opts.Scale + } else if toPNG { + scale = go2.Pointer(1.) + } svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{ - Pad: opts.Pad, - Sketch: opts.Sketch, - Center: opts.Center, - ThemeID: opts.ThemeID, - DarkThemeID: opts.DarkThemeID, - MasterID: opts.MasterID, - SetDimensions: toPNG, + Pad: opts.Pad, + Sketch: opts.Sketch, + Center: opts.Center, + ThemeID: opts.ThemeID, + DarkThemeID: opts.DarkThemeID, + MasterID: opts.MasterID, + Scale: scale, }) if err != nil { return nil, err @@ -746,11 +761,18 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt // make the bg fill within the png transparent so that the pdf bg fill is the only bg color present diagram.Root.Fill = "transparent" + var scale *float64 + if opts.Scale != nil { + scale = opts.Scale + } else { + scale = go2.Pointer(1.) + } + svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{ - Pad: opts.Pad, - Sketch: opts.Sketch, - Center: opts.Center, - SetDimensions: true, + Pad: opts.Pad, + Sketch: opts.Sketch, + Center: opts.Center, + Scale: scale, }) if err != nil { return nil, err @@ -837,12 +859,20 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present // make the bg fill within the png transparent so that the pdf bg fill is the only bg color present diagram.Root.Fill = "transparent" + var scale *float64 + if opts.Scale != nil { + scale = opts.Scale + } else { + scale = go2.Pointer(1.) + } + var err error + svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{ - Pad: opts.Pad, - Sketch: opts.Sketch, - Center: opts.Center, - SetDimensions: true, + Pad: opts.Pad, + Sketch: opts.Sketch, + Center: opts.Center, + Scale: scale, }) if err != nil { return nil, err @@ -1076,11 +1106,18 @@ func buildBoardIDToIndex(diagram *d2target.Diagram, dictionary map[string]int, p func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, page playwright.Page, diagram *d2target.Diagram) (svg []byte, pngs [][]byte, err error) { if !diagram.IsFolderOnly { + + var scale *float64 + if opts.Scale != nil { + scale = opts.Scale + } else { + scale = go2.Pointer(1.) + } svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{ - Pad: opts.Pad, - Sketch: opts.Sketch, - Center: opts.Center, - SetDimensions: true, + Pad: opts.Pad, + Sketch: opts.Sketch, + Center: opts.Center, + Scale: scale, }) if err != nil { return nil, nil, err diff --git a/d2cli/static/watch.js b/d2cli/static/watch.js index 3cb3a75e18..a954039715 100644 --- a/d2cli/static/watch.js +++ b/d2cli/static/watch.js @@ -35,12 +35,16 @@ function init(reconnectDelay) { let width = parseInt(svgEl.getAttribute("width"), 10); let height = parseInt(svgEl.getAttribute("height"), 10); if (isInit) { - if (width > height) { - if (width > window.innerWidth) { - ratio = window.innerWidth / width; + if (msg.scale) { + ratio = msg.scale; + } else { + if (width > height) { + if (width > window.innerWidth) { + ratio = window.innerWidth / width; + } + } else if (height > window.innerHeight) { + ratio = window.innerHeight / height; } - } else if (height > window.innerHeight) { - ratio = window.innerHeight / height; } // Scale svg fit to zoom isInit = false; diff --git a/d2cli/watch.go b/d2cli/watch.go index 047e415d54..bb006eb17a 100644 --- a/d2cli/watch.go +++ b/d2cli/watch.go @@ -83,8 +83,9 @@ type watcher struct { } type compileResult struct { - SVG string `json:"svg"` - Err string `json:"err"` + SVG string `json:"svg"` + Scale *float64 `json:"scale,omitEmpty"` + Err string `json:"err"` } func newWatcher(ctx context.Context, ms *xmain.State, opts watcherOpts) (*watcher, error) { @@ -372,8 +373,9 @@ func (w *watcher) compileLoop(ctx context.Context) error { w.ms.Log.Error.Print(errs) } w.broadcast(&compileResult{ - SVG: string(svg), - Err: errs, + SVG: string(svg), + Scale: w.renderOpts.Scale, + Err: errs, }) if firstCompile { diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 11996eb54d..5531d8716b 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -75,8 +75,8 @@ type RenderOpts struct { ThemeID int64 DarkThemeID *int64 Font string - // disables the fit to screen behavior and ensures the exported svg has the exact dimensions - SetDimensions bool + // the svg will be scaled by this factor, if unset the svg will fit to screen + Scale *float64 // MasterID is passed when the diagram should use something other than its own hash for unique targeting // Currently, that's when multi-boards are collapsed @@ -1648,7 +1648,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { pad := DEFAULT_PADDING themeID := DEFAULT_THEME darkThemeID := DEFAULT_DARK_THEME - setDimensions := false + var scale *float64 if opts != nil { pad = opts.Pad if opts.Sketch { @@ -1660,7 +1660,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { } themeID = opts.ThemeID darkThemeID = opts.DarkThemeID - setDimensions = opts.SetDimensions + scale = opts.Scale } buf := &bytes.Buffer{} @@ -1851,8 +1851,11 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { } var dimensions string - if setDimensions { - dimensions = fmt.Sprintf(` width="%d" height="%d"`, w, h) + if scale != nil { + dimensions = fmt.Sprintf(` width="%d" height="%d"`, + int(math.Ceil((*scale)*float64(w))), + int(math.Ceil((*scale)*float64(h))), + ) } alignment := "xMinYMin"