...

Source file src/oss.terrastruct.com/d2/d2cli/main.go

Documentation: oss.terrastruct.com/d2/d2cli

     1  package d2cli
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"os/exec"
    12  	"os/user"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/playwright-community/playwright-go"
    19  	"github.com/spf13/pflag"
    20  	"go.uber.org/multierr"
    21  
    22  	"oss.terrastruct.com/util-go/go2"
    23  	"oss.terrastruct.com/util-go/xmain"
    24  
    25  	"oss.terrastruct.com/d2/d2ast"
    26  	"oss.terrastruct.com/d2/d2graph"
    27  	"oss.terrastruct.com/d2/d2lib"
    28  	"oss.terrastruct.com/d2/d2parser"
    29  	"oss.terrastruct.com/d2/d2plugin"
    30  	"oss.terrastruct.com/d2/d2renderers/d2animate"
    31  	"oss.terrastruct.com/d2/d2renderers/d2fonts"
    32  	"oss.terrastruct.com/d2/d2renderers/d2svg"
    33  	"oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
    34  	"oss.terrastruct.com/d2/d2target"
    35  	"oss.terrastruct.com/d2/d2themes"
    36  	"oss.terrastruct.com/d2/d2themes/d2themescatalog"
    37  	"oss.terrastruct.com/d2/lib/background"
    38  	"oss.terrastruct.com/d2/lib/imgbundler"
    39  	ctxlog "oss.terrastruct.com/d2/lib/log"
    40  	"oss.terrastruct.com/d2/lib/pdf"
    41  	"oss.terrastruct.com/d2/lib/png"
    42  	"oss.terrastruct.com/d2/lib/pptx"
    43  	"oss.terrastruct.com/d2/lib/simplelog"
    44  	"oss.terrastruct.com/d2/lib/textmeasure"
    45  	timelib "oss.terrastruct.com/d2/lib/time"
    46  	"oss.terrastruct.com/d2/lib/version"
    47  	"oss.terrastruct.com/d2/lib/xgif"
    48  
    49  	"cdr.dev/slog"
    50  	"cdr.dev/slog/sloggers/sloghuman"
    51  )
    52  
    53  func Run(ctx context.Context, ms *xmain.State) (err error) {
    54  	// :(
    55  	ctx = DiscardSlog(ctx)
    56  
    57  	// These should be kept up-to-date with the d2 man page
    58  	watchFlag, err := ms.Opts.Bool("D2_WATCH", "watch", "w", false, "watch for changes to input and live reload. Use $HOST and $PORT to specify the listening address.\n(default localhost:0, which is will open on a randomly available local port).")
    59  	if err != nil {
    60  		return err
    61  	}
    62  	hostFlag := ms.Opts.String("HOST", "host", "h", "localhost", "host listening address when used with watch")
    63  	portFlag := ms.Opts.String("PORT", "port", "p", "0", "port listening address when used with watch")
    64  	bundleFlag, err := ms.Opts.Bool("D2_BUNDLE", "bundle", "b", true, "when outputting SVG, bundle all assets and layers into the output file")
    65  	if err != nil {
    66  		return err
    67  	}
    68  	forceAppendixFlag, err := ms.Opts.Bool("D2_FORCE_APPENDIX", "force-appendix", "", false, "an appendix for tooltips and links is added to PNG exports since they are not interactive. --force-appendix adds an appendix to SVG exports as well")
    69  	if err != nil {
    70  		return err
    71  	}
    72  	debugFlag, err := ms.Opts.Bool("DEBUG", "debug", "d", false, "print debug logs.")
    73  	if err != nil {
    74  		ms.Log.Warn.Printf("Invalid DEBUG flag value ignored")
    75  		debugFlag = go2.Pointer(false)
    76  	}
    77  	imgCacheFlag, err := ms.Opts.Bool("IMG_CACHE", "img-cache", "", true, "in watch mode, images used in icons are cached for subsequent compilations. This should be disabled if images might change.")
    78  	if err != nil {
    79  		return err
    80  	}
    81  	layoutFlag := ms.Opts.String("D2_LAYOUT", "layout", "l", "dagre", `the layout engine used`)
    82  	themeFlag, err := ms.Opts.Int64("D2_THEME", "theme", "t", 0, "the diagram theme ID")
    83  	if err != nil {
    84  		return err
    85  	}
    86  	darkThemeFlag, err := ms.Opts.Int64("D2_DARK_THEME", "dark-theme", "", -1, "the theme to use when the viewer's browser is in dark mode. When left unset -theme is used for both light and dark mode. Be aware that explicit styles set in D2 code will still be applied and this may produce unexpected results. We plan on resolving this by making style maps in D2 light/dark mode specific. See https://github.com/terrastruct/d2/issues/831.")
    87  	if err != nil {
    88  		return err
    89  	}
    90  	padFlag, err := ms.Opts.Int64("D2_PAD", "pad", "", d2svg.DEFAULT_PADDING, "pixels padded around the rendered diagram")
    91  	if err != nil {
    92  		return err
    93  	}
    94  	animateIntervalFlag, err := ms.Opts.Int64("D2_ANIMATE_INTERVAL", "animate-interval", "", 0, "if given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG exports.")
    95  	if err != nil {
    96  		return err
    97  	}
    98  	timeoutFlag, err := ms.Opts.Int64("D2_TIMEOUT", "timeout", "", 120, "the maximum number of seconds that D2 runs for before timing out and exiting. When rendering a large diagram, it is recommended to increase this value")
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version")
   104  	if err != nil {
   105  		return err
   106  	}
   107  	sketchFlag, err := ms.Opts.Bool("D2_SKETCH", "sketch", "s", false, "render the diagram to look like it was sketched by hand")
   108  	if err != nil {
   109  		return err
   110  	}
   111  	browserFlag := ms.Opts.String("BROWSER", "browser", "", "", "browser executable that watch opens. Setting to 0 opens no browser.")
   112  	centerFlag, err := ms.Opts.Bool("D2_CENTER", "center", "c", false, "center the SVG in the containing viewbox, such as your browser screen")
   113  	if err != nil {
   114  		return err
   115  	}
   116  	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.")
   117  	if err != nil {
   118  		return err
   119  	}
   120  	targetFlag := ms.Opts.String("", "target", "", "*", "target board to render. Pass an empty string to target root board. If target ends with '*', it will be rendered with all of its scenarios, steps, and layers. Otherwise, only the target board will be rendered. E.g. --target='' to render root board only or --target='layers.x.*' to render layer 'x' with all of its children.")
   121  
   122  	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.")
   123  	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.")
   124  	fontBoldFlag := ms.Opts.String("D2_FONT_BOLD", "font-bold", "", "", "path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used.")
   125  	fontSemiboldFlag := ms.Opts.String("D2_FONT_SEMIBOLD", "font-semibold", "", "", "path to .ttf file to use for the semibold font. If none provided, Source Sans Pro Semibold is used.")
   126  
   127  	plugins, err := d2plugin.ListPlugins(ctx)
   128  	if err != nil {
   129  		return err
   130  	}
   131  	err = populateLayoutOpts(ctx, ms, plugins)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	err = ms.Opts.Flags.Parse(ms.Opts.Args)
   137  	if !errors.Is(err, pflag.ErrHelp) && err != nil {
   138  		return xmain.UsageErrorf("failed to parse flags: %v", err)
   139  	}
   140  
   141  	if errors.Is(err, pflag.ErrHelp) {
   142  		help(ms)
   143  		return nil
   144  	}
   145  
   146  	fontFamily, err := loadFonts(ms, *fontRegularFlag, *fontItalicFlag, *fontBoldFlag, *fontSemiboldFlag)
   147  	if err != nil {
   148  		return xmain.UsageErrorf("failed to load specified fonts: %v", err)
   149  	}
   150  
   151  	if len(ms.Opts.Flags.Args()) > 0 {
   152  		switch ms.Opts.Flags.Arg(0) {
   153  		case "init-playwright":
   154  			return initPlaywright()
   155  		case "layout":
   156  			return layoutCmd(ctx, ms, plugins)
   157  		case "themes":
   158  			themesCmd(ctx, ms)
   159  			return nil
   160  		case "fmt":
   161  			return fmtCmd(ctx, ms)
   162  		case "version":
   163  			if len(ms.Opts.Flags.Args()) > 1 {
   164  				return xmain.UsageErrorf("version subcommand accepts no arguments")
   165  			}
   166  			fmt.Println(version.Version)
   167  			return nil
   168  		}
   169  	}
   170  
   171  	if *debugFlag {
   172  		ms.Env.Setenv("DEBUG", "1")
   173  	}
   174  	if *imgCacheFlag {
   175  		ms.Env.Setenv("IMG_CACHE", "1")
   176  	}
   177  	if *browserFlag != "" {
   178  		ms.Env.Setenv("BROWSER", *browserFlag)
   179  	}
   180  	if timeoutFlag != nil {
   181  		os.Setenv("D2_TIMEOUT", fmt.Sprintf("%d", *timeoutFlag))
   182  	}
   183  
   184  	var inputPath string
   185  	var outputPath string
   186  
   187  	if len(ms.Opts.Flags.Args()) == 0 {
   188  		if versionFlag != nil && *versionFlag {
   189  			fmt.Println(version.Version)
   190  			return nil
   191  		}
   192  		help(ms)
   193  		return nil
   194  	} else if len(ms.Opts.Flags.Args()) >= 3 {
   195  		return xmain.UsageErrorf("too many arguments passed")
   196  	}
   197  
   198  	if len(ms.Opts.Flags.Args()) >= 1 {
   199  		inputPath = ms.Opts.Flags.Arg(0)
   200  	}
   201  	if len(ms.Opts.Flags.Args()) >= 2 {
   202  		outputPath = ms.Opts.Flags.Arg(1)
   203  	} else {
   204  		if inputPath == "-" {
   205  			outputPath = "-"
   206  		} else {
   207  			outputPath = renameExt(inputPath, ".svg")
   208  		}
   209  	}
   210  	if inputPath != "-" {
   211  		inputPath = ms.AbsPath(inputPath)
   212  		d, err := os.Stat(inputPath)
   213  		if err == nil && d.IsDir() {
   214  			inputPath = filepath.Join(inputPath, "index.d2")
   215  		}
   216  	}
   217  	if filepath.Ext(outputPath) == ".ppt" {
   218  		return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?")
   219  	}
   220  	outputFormat := getExportExtension(outputPath)
   221  	if outputPath != "-" {
   222  		outputPath = ms.AbsPath(outputPath)
   223  		if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
   224  			return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG or GIF.\nYou provided: %s", filepath.Ext(outputPath))
   225  		} else if *animateIntervalFlag <= 0 && outputFormat.requiresAnimationInterval() {
   226  			return xmain.UsageErrorf("-animate-interval must be greater than 0 for %s outputs.\nYou provided: %d", outputFormat, *animateIntervalFlag)
   227  		}
   228  	}
   229  
   230  	match := d2themescatalog.Find(*themeFlag)
   231  	if match == (d2themes.Theme{}) {
   232  		return xmain.UsageErrorf("-t[heme] could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *themeFlag)
   233  	}
   234  	ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag)
   235  
   236  	// If flag is not explicitly set by user, set to nil.
   237  	// Later, configs from D2 code will only overwrite if they weren't explicitly set by user
   238  	flagSet := make(map[string]struct{})
   239  	ms.Opts.Flags.Visit(func(f *pflag.Flag) {
   240  		flagSet[f.Name] = struct{}{}
   241  	})
   242  	if ms.Env.Getenv("D2_LAYOUT") == "" {
   243  		if _, ok := flagSet["layout"]; !ok {
   244  			layoutFlag = nil
   245  		}
   246  	}
   247  	if ms.Env.Getenv("D2_THEME") == "" {
   248  		if _, ok := flagSet["theme"]; !ok {
   249  			themeFlag = nil
   250  		}
   251  	}
   252  	if ms.Env.Getenv("D2_SKETCH") == "" {
   253  		if _, ok := flagSet["sketch"]; !ok {
   254  			sketchFlag = nil
   255  		}
   256  	}
   257  	if ms.Env.Getenv("D2_PAD") == "" {
   258  		if _, ok := flagSet["pad"]; !ok {
   259  			padFlag = nil
   260  		}
   261  	}
   262  	if ms.Env.Getenv("D2_CENTER") == "" {
   263  		if _, ok := flagSet["center"]; !ok {
   264  			centerFlag = nil
   265  		}
   266  	}
   267  
   268  	if *darkThemeFlag == -1 {
   269  		darkThemeFlag = nil // TODO this is a temporary solution: https://github.com/terrastruct/util-go/issues/7
   270  	}
   271  	if darkThemeFlag != nil {
   272  		match = d2themescatalog.Find(*darkThemeFlag)
   273  		if match == (d2themes.Theme{}) {
   274  			return xmain.UsageErrorf("--dark-theme could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *darkThemeFlag)
   275  		}
   276  		ms.Log.Debug.Printf("using dark theme %s (ID: %d)", match.Name, *darkThemeFlag)
   277  	}
   278  	var scale *float64
   279  	if scaleFlag != nil && *scaleFlag > 0. {
   280  		scale = scaleFlag
   281  	}
   282  
   283  	if !outputFormat.supportsDarkTheme() {
   284  		if darkThemeFlag != nil {
   285  			ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg")
   286  			darkThemeFlag = nil
   287  		}
   288  	}
   289  	var pw png.Playwright
   290  	if outputFormat.requiresPNGRenderer() {
   291  		pw, err = png.InitPlaywright()
   292  		if err != nil {
   293  			return err
   294  		}
   295  		defer func() {
   296  			cleanupErr := pw.Cleanup()
   297  			if err == nil {
   298  				err = cleanupErr
   299  			}
   300  		}()
   301  	}
   302  
   303  	renderOpts := d2svg.RenderOpts{
   304  		Pad:         padFlag,
   305  		Sketch:      sketchFlag,
   306  		Center:      centerFlag,
   307  		ThemeID:     themeFlag,
   308  		DarkThemeID: darkThemeFlag,
   309  		Scale:       scale,
   310  	}
   311  
   312  	if *watchFlag {
   313  		if inputPath == "-" {
   314  			return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
   315  		}
   316  		if *targetFlag != "*" {
   317  			return xmain.UsageErrorf("-w[atch] cannot be combined with --target")
   318  		}
   319  		w, err := newWatcher(ctx, ms, watcherOpts{
   320  			plugins:         plugins,
   321  			layout:          layoutFlag,
   322  			renderOpts:      renderOpts,
   323  			animateInterval: *animateIntervalFlag,
   324  			host:            *hostFlag,
   325  			port:            *portFlag,
   326  			inputPath:       inputPath,
   327  			outputPath:      outputPath,
   328  			bundle:          *bundleFlag,
   329  			forceAppendix:   *forceAppendixFlag,
   330  			pw:              pw,
   331  			fontFamily:      fontFamily,
   332  		})
   333  		if err != nil {
   334  			return err
   335  		}
   336  		return w.run()
   337  	}
   338  
   339  	var boardPath []string
   340  	var noChildren bool
   341  	switch *targetFlag {
   342  	case "*":
   343  	case "":
   344  		noChildren = true
   345  	default:
   346  		target := *targetFlag
   347  		if strings.HasSuffix(target, ".*") {
   348  			target = target[:len(target)-2]
   349  		} else {
   350  			noChildren = true
   351  		}
   352  		key, err := d2parser.ParseKey(target)
   353  		if err != nil {
   354  			return xmain.UsageErrorf("invalid target: %s", *targetFlag)
   355  		}
   356  		boardPath = key.IDA()
   357  	}
   358  
   359  	ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
   360  	defer cancel()
   361  
   362  	_, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page)
   363  	if err != nil {
   364  		if written {
   365  			return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
   366  		}
   367  		return fmt.Errorf("failed to compile %s: %w", ms.HumanPath(inputPath), err)
   368  	}
   369  	return nil
   370  }
   371  
   372  func LayoutResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin) func(engine string) (d2graph.LayoutGraph, error) {
   373  	cached := make(map[string]d2graph.LayoutGraph)
   374  	return func(engine string) (d2graph.LayoutGraph, error) {
   375  		if c, ok := cached[engine]; ok {
   376  			return c, nil
   377  		}
   378  
   379  		plugin, err := d2plugin.FindPlugin(ctx, plugins, engine)
   380  		if err != nil {
   381  			if errors.Is(err, exec.ErrNotFound) {
   382  				return nil, layoutNotFound(ctx, plugins, engine)
   383  			}
   384  			return nil, err
   385  		}
   386  
   387  		err = d2plugin.HydratePluginOpts(ctx, ms, plugin)
   388  		if err != nil {
   389  			return nil, err
   390  		}
   391  
   392  		cached[engine] = plugin.Layout
   393  		return plugin.Layout, nil
   394  	}
   395  }
   396  
   397  func RouterResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin) func(engine string) (d2graph.RouteEdges, error) {
   398  	cached := make(map[string]d2graph.RouteEdges)
   399  	return func(engine string) (d2graph.RouteEdges, error) {
   400  		if c, ok := cached[engine]; ok {
   401  			return c, nil
   402  		}
   403  
   404  		plugin, err := d2plugin.FindPlugin(ctx, plugins, engine)
   405  		if err != nil {
   406  			if errors.Is(err, exec.ErrNotFound) {
   407  				return nil, layoutNotFound(ctx, plugins, engine)
   408  			}
   409  			return nil, err
   410  		}
   411  
   412  		pluginInfo, err := plugin.Info(ctx)
   413  		if err != nil {
   414  			return nil, err
   415  		}
   416  		hasRouter := false
   417  		for _, feat := range pluginInfo.Features {
   418  			if feat == d2plugin.ROUTES_EDGES {
   419  				hasRouter = true
   420  				break
   421  			}
   422  		}
   423  		if !hasRouter {
   424  			return nil, nil
   425  		}
   426  		routingPlugin, ok := plugin.(d2plugin.RoutingPlugin)
   427  		if !ok {
   428  			return nil, fmt.Errorf("plugin has routing feature but does not implement RoutingPlugin")
   429  		}
   430  
   431  		routeEdges := d2graph.RouteEdges(routingPlugin.RouteEdges)
   432  		cached[engine] = routeEdges
   433  		return routeEdges, nil
   434  	}
   435  }
   436  
   437  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) {
   438  	start := time.Now()
   439  	input, err := ms.ReadPath(inputPath)
   440  	if err != nil {
   441  		return nil, false, err
   442  	}
   443  
   444  	ruler, err := textmeasure.NewRuler()
   445  	if err != nil {
   446  		return nil, false, err
   447  	}
   448  
   449  	opts := &d2lib.CompileOptions{
   450  		Ruler:          ruler,
   451  		FontFamily:     fontFamily,
   452  		InputPath:      inputPath,
   453  		LayoutResolver: LayoutResolver(ctx, ms, plugins),
   454  		Layout:         layout,
   455  		RouterResolver: RouterResolver(ctx, ms, plugins),
   456  		FS:             fs,
   457  	}
   458  
   459  	if os.Getenv("D2_LSP_MODE") == "1" {
   460  		// only the parse result is needed if running d2 for lsp
   461  		ast, err := d2lib.Parse(ctx, string(input), opts)
   462  		if err != nil {
   463  			return nil, false, err
   464  		}
   465  
   466  		type LspOutputData struct {
   467  			Ast *d2ast.Map
   468  			Err error
   469  		}
   470  		jsonOutput, err := json.Marshal(LspOutputData{Ast: ast, Err: err})
   471  		if err != nil {
   472  			return nil, false, err
   473  		}
   474  		fmt.Print(string(jsonOutput))
   475  		os.Exit(42)
   476  		return nil, false, nil
   477  	}
   478  
   479  	cancel := background.Repeat(func() {
   480  		ms.Log.Info.Printf("compiling & running layout algorithms...")
   481  	}, time.Second*5)
   482  	defer cancel()
   483  
   484  	diagram, g, err := d2lib.Compile(ctx, string(input), opts, &renderOpts)
   485  	if err != nil {
   486  		return nil, false, err
   487  	}
   488  	cancel()
   489  
   490  	diagram = diagram.GetBoard(boardPath)
   491  	if diagram == nil {
   492  		return nil, false, fmt.Errorf(`render target "%s" not found`, strings.Join(boardPath, "."))
   493  	}
   494  	if noChildren {
   495  		diagram.Layers = nil
   496  		diagram.Scenarios = nil
   497  		diagram.Steps = nil
   498  	}
   499  
   500  	plugin, _ := d2plugin.FindPlugin(ctx, plugins, *opts.Layout)
   501  
   502  	if animateInterval > 0 {
   503  		masterID, err := diagram.HashID()
   504  		if err != nil {
   505  			return nil, false, err
   506  		}
   507  		renderOpts.MasterID = masterID
   508  	}
   509  
   510  	pinfo, err := plugin.Info(ctx)
   511  	if err != nil {
   512  		return nil, false, err
   513  	}
   514  	plocation := pinfo.Type
   515  	if pinfo.Type == "binary" {
   516  		plocation = fmt.Sprintf("executable plugin at %s", humanPath(pinfo.Path))
   517  	}
   518  	ms.Log.Debug.Printf("using layout plugin %s (%s)", *opts.Layout, plocation)
   519  
   520  	pluginInfo, err := plugin.Info(ctx)
   521  	if err != nil {
   522  		return nil, false, err
   523  	}
   524  
   525  	err = d2plugin.FeatureSupportCheck(pluginInfo, g)
   526  	if err != nil {
   527  		return nil, false, err
   528  	}
   529  
   530  	ext := getExportExtension(outputPath)
   531  	switch ext {
   532  	case GIF:
   533  		svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, diagram)
   534  		if err != nil {
   535  			return nil, false, err
   536  		}
   537  		out, err := AnimatePNGs(ms, pngs, int(animateInterval))
   538  		if err != nil {
   539  			return nil, false, err
   540  		}
   541  		err = os.MkdirAll(filepath.Dir(outputPath), 0755)
   542  		if err != nil {
   543  			return nil, false, err
   544  		}
   545  		err = ms.WritePath(outputPath, out)
   546  		if err != nil {
   547  			return nil, false, err
   548  		}
   549  		dur := time.Since(start)
   550  		ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
   551  		return svg, true, nil
   552  	case PDF:
   553  		pageMap := buildBoardIDToIndex(diagram, nil, nil)
   554  		path := []pdf.BoardTitle{
   555  			{Name: diagram.Root.Label, BoardID: "root"},
   556  		}
   557  		pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, path, pageMap, diagram.Root.Label != "")
   558  		if err != nil {
   559  			return pdf, false, err
   560  		}
   561  		dur := time.Since(start)
   562  		ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
   563  		return pdf, true, nil
   564  	case PPTX:
   565  		var username string
   566  		if user, err := user.Current(); err == nil {
   567  			username = user.Username
   568  		}
   569  		description := "Presentation generated with D2 - https://d2lang.com"
   570  		rootName := getFileName(outputPath)
   571  		// version must be only numbers to avoid issues with PowerPoint
   572  		p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers(), diagram.Root.Label != "")
   573  
   574  		boardIdToIndex := buildBoardIDToIndex(diagram, nil, nil)
   575  		path := []pptx.BoardTitle{
   576  			{Name: "root", BoardID: "root", LinkToSlide: boardIdToIndex["root"] + 1},
   577  		}
   578  		svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, path, boardIdToIndex)
   579  		if err != nil {
   580  			return nil, false, err
   581  		}
   582  		err = p.SaveTo(outputPath)
   583  		if err != nil {
   584  			return nil, false, err
   585  		}
   586  		dur := time.Since(start)
   587  		ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
   588  		return svg, true, nil
   589  	default:
   590  		compileDur := time.Since(start)
   591  		if animateInterval <= 0 {
   592  			// Rename all the "root.layers.x" to the paths that the boards get output to
   593  			linkToOutput, err := resolveLinks("root", outputPath, diagram)
   594  			if err != nil {
   595  				return nil, false, err
   596  			}
   597  			err = relink("root", diagram, linkToOutput)
   598  			if err != nil {
   599  				return nil, false, err
   600  			}
   601  		}
   602  
   603  		var boards [][]byte
   604  		var err error
   605  		if noChildren {
   606  			boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
   607  		} else {
   608  			boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
   609  		}
   610  		if err != nil {
   611  			return nil, false, err
   612  		}
   613  		var out []byte
   614  		if len(boards) > 0 {
   615  			out = boards[0]
   616  			if animateInterval > 0 {
   617  				out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval))
   618  				if err != nil {
   619  					return nil, false, err
   620  				}
   621  				err = os.MkdirAll(filepath.Dir(outputPath), 0755)
   622  				if err != nil {
   623  					return nil, false, err
   624  				}
   625  				err = ms.WritePath(outputPath, out)
   626  				if err != nil {
   627  					return nil, false, err
   628  				}
   629  				ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), time.Since(start))
   630  			}
   631  		}
   632  		return out, true, nil
   633  	}
   634  }
   635  
   636  func resolveLinks(currDiagramPath, outputPath string, diagram *d2target.Diagram) (linkToOutput map[string]string, err error) {
   637  	if diagram.Name != "" {
   638  		ext := filepath.Ext(outputPath)
   639  		outputPath = strings.TrimSuffix(outputPath, ext)
   640  		outputPath = filepath.Join(outputPath, diagram.Name)
   641  		outputPath += ext
   642  	}
   643  
   644  	boardOutputPath := outputPath
   645  	if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
   646  		ext := filepath.Ext(boardOutputPath)
   647  		boardOutputPath = strings.TrimSuffix(boardOutputPath, ext)
   648  		boardOutputPath = filepath.Join(boardOutputPath, "index")
   649  		boardOutputPath += ext
   650  	}
   651  
   652  	layersOutputPath := outputPath
   653  	if len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
   654  		ext := filepath.Ext(layersOutputPath)
   655  		layersOutputPath = strings.TrimSuffix(layersOutputPath, ext)
   656  		layersOutputPath = filepath.Join(layersOutputPath, "layers")
   657  		layersOutputPath += ext
   658  	}
   659  	scenariosOutputPath := outputPath
   660  	if len(diagram.Layers) > 0 || len(diagram.Steps) > 0 {
   661  		ext := filepath.Ext(scenariosOutputPath)
   662  		scenariosOutputPath = strings.TrimSuffix(scenariosOutputPath, ext)
   663  		scenariosOutputPath = filepath.Join(scenariosOutputPath, "scenarios")
   664  		scenariosOutputPath += ext
   665  	}
   666  	stepsOutputPath := outputPath
   667  	if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 {
   668  		ext := filepath.Ext(stepsOutputPath)
   669  		stepsOutputPath = strings.TrimSuffix(stepsOutputPath, ext)
   670  		stepsOutputPath = filepath.Join(stepsOutputPath, "steps")
   671  		stepsOutputPath += ext
   672  	}
   673  
   674  	linkToOutput = map[string]string{currDiagramPath: boardOutputPath}
   675  
   676  	for _, dl := range diagram.Layers {
   677  		m, err := resolveLinks(strings.Join([]string{currDiagramPath, "layers", dl.Name}, "."), layersOutputPath, dl)
   678  		if err != nil {
   679  			return nil, err
   680  		}
   681  		for k, v := range m {
   682  			linkToOutput[k] = v
   683  		}
   684  	}
   685  	for _, dl := range diagram.Scenarios {
   686  		m, err := resolveLinks(strings.Join([]string{currDiagramPath, "scenarios", dl.Name}, "."), scenariosOutputPath, dl)
   687  		if err != nil {
   688  			return nil, err
   689  		}
   690  		for k, v := range m {
   691  			linkToOutput[k] = v
   692  		}
   693  	}
   694  	for _, dl := range diagram.Steps {
   695  		m, err := resolveLinks(strings.Join([]string{currDiagramPath, "steps", dl.Name}, "."), stepsOutputPath, dl)
   696  		if err != nil {
   697  			return nil, err
   698  		}
   699  		for k, v := range m {
   700  			linkToOutput[k] = v
   701  		}
   702  	}
   703  
   704  	return linkToOutput, nil
   705  }
   706  
   707  func relink(currDiagramPath string, d *d2target.Diagram, linkToOutput map[string]string) error {
   708  	for i, shape := range d.Shapes {
   709  		if shape.Link != "" {
   710  			for k, v := range linkToOutput {
   711  				if shape.Link == k {
   712  					rel, err := filepath.Rel(filepath.Dir(linkToOutput[currDiagramPath]), v)
   713  					if err != nil {
   714  						return err
   715  					}
   716  					d.Shapes[i].Link = rel
   717  					break
   718  				}
   719  			}
   720  		}
   721  	}
   722  	for _, board := range d.Layers {
   723  		err := relink(strings.Join([]string{currDiagramPath, "layers", board.Name}, "."), board, linkToOutput)
   724  		if err != nil {
   725  			return err
   726  		}
   727  	}
   728  	for _, board := range d.Scenarios {
   729  		err := relink(strings.Join([]string{currDiagramPath, "scenarios", board.Name}, "."), board, linkToOutput)
   730  		if err != nil {
   731  			return err
   732  		}
   733  	}
   734  	for _, board := range d.Steps {
   735  		err := relink(strings.Join([]string{currDiagramPath, "steps", board.Name}, "."), board, linkToOutput)
   736  		if err != nil {
   737  			return err
   738  		}
   739  	}
   740  	return nil
   741  }
   742  
   743  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) {
   744  	if diagram.Name != "" {
   745  		ext := filepath.Ext(outputPath)
   746  		outputPath = strings.TrimSuffix(outputPath, ext)
   747  		outputPath = filepath.Join(outputPath, diagram.Name)
   748  		outputPath += ext
   749  	}
   750  
   751  	boardOutputPath := outputPath
   752  	if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
   753  		if outputPath == "-" {
   754  			// TODO it can if composed into one
   755  			return nil, fmt.Errorf("multiboard output cannot be written to stdout")
   756  		}
   757  		// Boards with subboards must be self-contained folders.
   758  		ext := filepath.Ext(boardOutputPath)
   759  		boardOutputPath = strings.TrimSuffix(boardOutputPath, ext)
   760  		os.RemoveAll(boardOutputPath)
   761  		boardOutputPath = filepath.Join(boardOutputPath, "index")
   762  		boardOutputPath += ext
   763  	}
   764  
   765  	layersOutputPath := outputPath
   766  	if len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
   767  		ext := filepath.Ext(layersOutputPath)
   768  		layersOutputPath = strings.TrimSuffix(layersOutputPath, ext)
   769  		layersOutputPath = filepath.Join(layersOutputPath, "layers")
   770  		layersOutputPath += ext
   771  	}
   772  	scenariosOutputPath := outputPath
   773  	if len(diagram.Layers) > 0 || len(diagram.Steps) > 0 {
   774  		ext := filepath.Ext(scenariosOutputPath)
   775  		scenariosOutputPath = strings.TrimSuffix(scenariosOutputPath, ext)
   776  		scenariosOutputPath = filepath.Join(scenariosOutputPath, "scenarios")
   777  		scenariosOutputPath += ext
   778  	}
   779  	stepsOutputPath := outputPath
   780  	if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 {
   781  		ext := filepath.Ext(stepsOutputPath)
   782  		stepsOutputPath = strings.TrimSuffix(stepsOutputPath, ext)
   783  		stepsOutputPath = filepath.Join(stepsOutputPath, "steps")
   784  		stepsOutputPath += ext
   785  	}
   786  
   787  	var boards [][]byte
   788  	for _, dl := range diagram.Layers {
   789  		childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
   790  		if err != nil {
   791  			return nil, err
   792  		}
   793  		boards = append(boards, childrenBoards...)
   794  	}
   795  	for _, dl := range diagram.Scenarios {
   796  		childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
   797  		if err != nil {
   798  			return nil, err
   799  		}
   800  		boards = append(boards, childrenBoards...)
   801  	}
   802  	for _, dl := range diagram.Steps {
   803  		childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
   804  		if err != nil {
   805  			return nil, err
   806  		}
   807  		boards = append(boards, childrenBoards...)
   808  	}
   809  
   810  	if !diagram.IsFolderOnly {
   811  		start := time.Now()
   812  		out, err := _render(ctx, ms, plugin, opts, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
   813  		if err != nil {
   814  			return boards, err
   815  		}
   816  		dur := compileDur + time.Since(start)
   817  		if opts.MasterID == "" {
   818  			ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(boardOutputPath), dur)
   819  		}
   820  		boards = append([][]byte{out}, boards...)
   821  	}
   822  
   823  	return boards, nil
   824  }
   825  
   826  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) {
   827  	start := time.Now()
   828  	out, err := _render(ctx, ms, plugin, opts, outputPath, bundle, forceAppendix, page, ruler, diagram)
   829  	if err != nil {
   830  		return [][]byte{}, err
   831  	}
   832  	dur := compileDur + time.Since(start)
   833  	if opts.MasterID == "" {
   834  		ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
   835  	}
   836  	return [][]byte{out}, nil
   837  }
   838  
   839  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) {
   840  	toPNG := getExportExtension(outputPath) == PNG
   841  	var scale *float64
   842  	if opts.Scale != nil {
   843  		scale = opts.Scale
   844  	} else if toPNG {
   845  		scale = go2.Pointer(1.)
   846  	}
   847  	svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
   848  		Pad:                opts.Pad,
   849  		Sketch:             opts.Sketch,
   850  		Center:             opts.Center,
   851  		ThemeID:            opts.ThemeID,
   852  		DarkThemeID:        opts.DarkThemeID,
   853  		MasterID:           opts.MasterID,
   854  		ThemeOverrides:     opts.ThemeOverrides,
   855  		DarkThemeOverrides: opts.DarkThemeOverrides,
   856  		Scale:              scale,
   857  	})
   858  	if err != nil {
   859  		return nil, err
   860  	}
   861  
   862  	svg, err = plugin.PostProcess(ctx, svg)
   863  	if err != nil {
   864  		return svg, err
   865  	}
   866  
   867  	cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
   868  	l := simplelog.FromCmdLog(ms.Log)
   869  	svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
   870  	if bundle {
   871  		var bundleErr2 error
   872  		svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
   873  		bundleErr = multierr.Combine(bundleErr, bundleErr2)
   874  	}
   875  	if forceAppendix && !toPNG {
   876  		svg = appendix.Append(diagram, ruler, svg)
   877  	}
   878  
   879  	out := svg
   880  	if toPNG {
   881  		svg := appendix.Append(diagram, ruler, svg)
   882  
   883  		if !bundle {
   884  			var bundleErr2 error
   885  			svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
   886  			bundleErr = multierr.Combine(bundleErr, bundleErr2)
   887  		}
   888  
   889  		out, err = ConvertSVG(ms, page, svg)
   890  		if err != nil {
   891  			return svg, err
   892  		}
   893  		out, err = png.AddExif(out)
   894  		if err != nil {
   895  			return svg, err
   896  		}
   897  	} else {
   898  		if len(out) > 0 && out[len(out)-1] != '\n' {
   899  			out = append(out, '\n')
   900  		}
   901  	}
   902  
   903  	if opts.MasterID == "" {
   904  		err = os.MkdirAll(filepath.Dir(outputPath), 0755)
   905  		if err != nil {
   906  			return svg, err
   907  		}
   908  		err = ms.WritePath(outputPath, out)
   909  		if err != nil {
   910  			return svg, err
   911  		}
   912  	}
   913  	if bundleErr != nil {
   914  		return svg, bundleErr
   915  	}
   916  	return svg, nil
   917  }
   918  
   919  func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, doc *pdf.GoFPDF, boardPath []pdf.BoardTitle, pageMap map[string]int, includeNav bool) (svg []byte, err error) {
   920  	var isRoot bool
   921  	if doc == nil {
   922  		doc = pdf.Init()
   923  		isRoot = true
   924  	}
   925  
   926  	if !diagram.IsFolderOnly {
   927  		rootFill := diagram.Root.Fill
   928  		// gofpdf will print the png img with a slight filter
   929  		// make the bg fill within the png transparent so that the pdf bg fill is the only bg color present
   930  		diagram.Root.Fill = "transparent"
   931  
   932  		var scale *float64
   933  		if opts.Scale != nil {
   934  			scale = opts.Scale
   935  		} else {
   936  			scale = go2.Pointer(1.)
   937  		}
   938  
   939  		svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
   940  			Pad:    opts.Pad,
   941  			Sketch: opts.Sketch,
   942  			Center: opts.Center,
   943  			Scale:  scale,
   944  		})
   945  		if err != nil {
   946  			return nil, err
   947  		}
   948  
   949  		svg, err = plugin.PostProcess(ctx, svg)
   950  		if err != nil {
   951  			return svg, err
   952  		}
   953  
   954  		cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
   955  		l := simplelog.FromCmdLog(ms.Log)
   956  		svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
   957  		svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
   958  		bundleErr = multierr.Combine(bundleErr, bundleErr2)
   959  		if bundleErr != nil {
   960  			return svg, bundleErr
   961  		}
   962  		svg = appendix.Append(diagram, ruler, svg)
   963  
   964  		pngImg, err := ConvertSVG(ms, page, svg)
   965  		if err != nil {
   966  			return svg, err
   967  		}
   968  
   969  		viewboxSlice := appendix.FindViewboxSlice(svg)
   970  		viewboxX, err := strconv.ParseFloat(viewboxSlice[0], 64)
   971  		if err != nil {
   972  			return svg, err
   973  		}
   974  		viewboxY, err := strconv.ParseFloat(viewboxSlice[1], 64)
   975  		if err != nil {
   976  			return svg, err
   977  		}
   978  		err = doc.AddPDFPage(pngImg, boardPath, *opts.ThemeID, rootFill, diagram.Shapes, *opts.Pad, viewboxX, viewboxY, pageMap, includeNav)
   979  		if err != nil {
   980  			return svg, err
   981  		}
   982  	}
   983  
   984  	for _, dl := range diagram.Layers {
   985  		path := append(boardPath, pdf.BoardTitle{
   986  			Name:    dl.Root.Label,
   987  			BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, LAYERS, dl.Name}, "."),
   988  		})
   989  		_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
   990  		if err != nil {
   991  			return nil, err
   992  		}
   993  	}
   994  	for _, dl := range diagram.Scenarios {
   995  		path := append(boardPath, pdf.BoardTitle{
   996  			Name:    dl.Root.Label,
   997  			BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, SCENARIOS, dl.Name}, "."),
   998  		})
   999  		_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
  1000  		if err != nil {
  1001  			return nil, err
  1002  		}
  1003  	}
  1004  	for _, dl := range diagram.Steps {
  1005  		path := append(boardPath, pdf.BoardTitle{
  1006  			Name:    dl.Root.Label,
  1007  			BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, STEPS, dl.Name}, "."),
  1008  		})
  1009  		_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
  1010  		if err != nil {
  1011  			return nil, err
  1012  		}
  1013  	}
  1014  
  1015  	if isRoot {
  1016  		err := doc.Export(outputPath)
  1017  		if err != nil {
  1018  			return nil, err
  1019  		}
  1020  	}
  1021  
  1022  	return svg, nil
  1023  }
  1024  
  1025  func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []pptx.BoardTitle, boardIDToIndex map[string]int) ([]byte, error) {
  1026  	var svg []byte
  1027  	if !diagram.IsFolderOnly {
  1028  		// gofpdf will print the png img with a slight filter
  1029  		// make the bg fill within the png transparent so that the pdf bg fill is the only bg color present
  1030  		diagram.Root.Fill = "transparent"
  1031  
  1032  		var scale *float64
  1033  		if opts.Scale != nil {
  1034  			scale = opts.Scale
  1035  		} else {
  1036  			scale = go2.Pointer(1.)
  1037  		}
  1038  
  1039  		var err error
  1040  
  1041  		svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
  1042  			Pad:    opts.Pad,
  1043  			Sketch: opts.Sketch,
  1044  			Center: opts.Center,
  1045  			Scale:  scale,
  1046  		})
  1047  		if err != nil {
  1048  			return nil, err
  1049  		}
  1050  
  1051  		svg, err = plugin.PostProcess(ctx, svg)
  1052  		if err != nil {
  1053  			return nil, err
  1054  		}
  1055  
  1056  		cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
  1057  		l := simplelog.FromCmdLog(ms.Log)
  1058  		svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
  1059  		svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
  1060  		bundleErr = multierr.Combine(bundleErr, bundleErr2)
  1061  		if bundleErr != nil {
  1062  			return nil, bundleErr
  1063  		}
  1064  
  1065  		svg = appendix.Append(diagram, ruler, svg)
  1066  
  1067  		pngImg, err := ConvertSVG(ms, page, svg)
  1068  		if err != nil {
  1069  			return nil, err
  1070  		}
  1071  
  1072  		slide, err := presentation.AddSlide(pngImg, boardPath)
  1073  		if err != nil {
  1074  			return nil, err
  1075  		}
  1076  
  1077  		viewboxSlice := appendix.FindViewboxSlice(svg)
  1078  		viewboxX, err := strconv.ParseFloat(viewboxSlice[0], 64)
  1079  		if err != nil {
  1080  			return nil, err
  1081  		}
  1082  		viewboxY, err := strconv.ParseFloat(viewboxSlice[1], 64)
  1083  		if err != nil {
  1084  			return nil, err
  1085  		}
  1086  
  1087  		// Draw links
  1088  		for _, shape := range diagram.Shapes {
  1089  			if shape.Link == "" {
  1090  				continue
  1091  			}
  1092  
  1093  			linkX := png.SCALE * (float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth))
  1094  			linkY := png.SCALE * (float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth))
  1095  			linkWidth := png.SCALE * (float64(shape.Width) + float64(shape.StrokeWidth*2))
  1096  			linkHeight := png.SCALE * (float64(shape.Height) + float64(shape.StrokeWidth*2))
  1097  			link := &pptx.Link{
  1098  				Left:    int(linkX),
  1099  				Top:     int(linkY),
  1100  				Width:   int(linkWidth),
  1101  				Height:  int(linkHeight),
  1102  				Tooltip: shape.Link,
  1103  			}
  1104  			slide.AddLink(link)
  1105  			key, err := d2parser.ParseKey(shape.Link)
  1106  			if err != nil || key.Path[0].Unbox().ScalarString() != "root" {
  1107  				// External link
  1108  				link.ExternalUrl = shape.Link
  1109  			} else if pageNum, ok := boardIDToIndex[shape.Link]; ok {
  1110  				// Internal link
  1111  				link.SlideIndex = pageNum + 1
  1112  			}
  1113  		}
  1114  	}
  1115  
  1116  	for _, dl := range diagram.Layers {
  1117  		boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, LAYERS, dl.Name}, ".")
  1118  		path := append(boardPath, pptx.BoardTitle{
  1119  			Name:        dl.Name,
  1120  			BoardID:     boardID,
  1121  			LinkToSlide: boardIDToIndex[boardID] + 1,
  1122  		})
  1123  		_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
  1124  		if err != nil {
  1125  			return nil, err
  1126  		}
  1127  	}
  1128  	for _, dl := range diagram.Scenarios {
  1129  		boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, SCENARIOS, dl.Name}, ".")
  1130  		path := append(boardPath, pptx.BoardTitle{
  1131  			Name:        dl.Name,
  1132  			BoardID:     boardID,
  1133  			LinkToSlide: boardIDToIndex[boardID] + 1,
  1134  		})
  1135  		_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
  1136  		if err != nil {
  1137  			return nil, err
  1138  		}
  1139  	}
  1140  	for _, dl := range diagram.Steps {
  1141  		boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, STEPS, dl.Name}, ".")
  1142  		path := append(boardPath, pptx.BoardTitle{
  1143  			Name:        dl.Name,
  1144  			BoardID:     boardID,
  1145  			LinkToSlide: boardIDToIndex[boardID] + 1,
  1146  		})
  1147  		_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
  1148  		if err != nil {
  1149  			return nil, err
  1150  		}
  1151  	}
  1152  
  1153  	return svg, nil
  1154  }
  1155  
  1156  // newExt must include leading .
  1157  func renameExt(fp string, newExt string) string {
  1158  	ext := filepath.Ext(fp)
  1159  	if ext == "" {
  1160  		return fp + newExt
  1161  	} else {
  1162  		return strings.TrimSuffix(fp, ext) + newExt
  1163  	}
  1164  }
  1165  
  1166  func getFileName(path string) string {
  1167  	ext := filepath.Ext(path)
  1168  	return strings.TrimSuffix(filepath.Base(path), ext)
  1169  }
  1170  
  1171  // TODO: remove after removing slog
  1172  func DiscardSlog(ctx context.Context) context.Context {
  1173  	return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
  1174  }
  1175  
  1176  func populateLayoutOpts(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
  1177  	pluginFlags, err := d2plugin.ListPluginFlags(ctx, ps)
  1178  	if err != nil {
  1179  		return err
  1180  	}
  1181  
  1182  	for _, f := range pluginFlags {
  1183  		f.AddToOpts(ms.Opts)
  1184  		// Don't pollute the main d2 flagset with these. It'll be a lot
  1185  		ms.Opts.Flags.MarkHidden(f.Name)
  1186  	}
  1187  
  1188  	return nil
  1189  }
  1190  
  1191  func initPlaywright() error {
  1192  	pw, err := png.InitPlaywright()
  1193  	if err != nil {
  1194  		return err
  1195  	}
  1196  	return pw.Cleanup()
  1197  }
  1198  
  1199  func loadFont(ms *xmain.State, path string) ([]byte, error) {
  1200  	if filepath.Ext(path) != ".ttf" {
  1201  		return nil, fmt.Errorf("expected .ttf file but %s has extension %s", path, filepath.Ext(path))
  1202  	}
  1203  	ttf, err := os.ReadFile(path)
  1204  	if err != nil {
  1205  		return nil, fmt.Errorf("failed to read font at %s: %v", path, err)
  1206  	}
  1207  	ms.Log.Info.Printf("font %s loaded", filepath.Base(path))
  1208  	return ttf, nil
  1209  }
  1210  
  1211  func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold, pathToSemibold string) (*d2fonts.FontFamily, error) {
  1212  	if pathToRegular == "" && pathToItalic == "" && pathToBold == "" && pathToSemibold == "" {
  1213  		return nil, nil
  1214  	}
  1215  
  1216  	var regularTTF []byte
  1217  	var italicTTF []byte
  1218  	var boldTTF []byte
  1219  	var semiboldTTF []byte
  1220  
  1221  	var err error
  1222  	if pathToRegular != "" {
  1223  		regularTTF, err = loadFont(ms, pathToRegular)
  1224  		if err != nil {
  1225  			return nil, err
  1226  		}
  1227  	}
  1228  	if pathToItalic != "" {
  1229  		italicTTF, err = loadFont(ms, pathToItalic)
  1230  		if err != nil {
  1231  			return nil, err
  1232  		}
  1233  	}
  1234  	if pathToBold != "" {
  1235  		boldTTF, err = loadFont(ms, pathToBold)
  1236  		if err != nil {
  1237  			return nil, err
  1238  		}
  1239  	}
  1240  	if pathToSemibold != "" {
  1241  		semiboldTTF, err = loadFont(ms, pathToSemibold)
  1242  		if err != nil {
  1243  			return nil, err
  1244  		}
  1245  	}
  1246  
  1247  	return d2fonts.AddFontFamily("custom", regularTTF, italicTTF, boldTTF, semiboldTTF)
  1248  }
  1249  
  1250  const LAYERS = "layers"
  1251  const STEPS = "steps"
  1252  const SCENARIOS = "scenarios"
  1253  
  1254  // buildBoardIDToIndex returns a map from board path to page int
  1255  // To map correctly, it must follow the same traversal of pdf/pptx building
  1256  func buildBoardIDToIndex(diagram *d2target.Diagram, dictionary map[string]int, path []string) map[string]int {
  1257  	newPath := append(path, diagram.Name)
  1258  	if dictionary == nil {
  1259  		dictionary = map[string]int{}
  1260  		newPath[0] = "root"
  1261  	}
  1262  
  1263  	key := strings.Join(newPath, ".")
  1264  	dictionary[key] = len(dictionary)
  1265  
  1266  	for _, dl := range diagram.Layers {
  1267  		buildBoardIDToIndex(dl, dictionary, append(newPath, LAYERS))
  1268  	}
  1269  	for _, dl := range diagram.Scenarios {
  1270  		buildBoardIDToIndex(dl, dictionary, append(newPath, SCENARIOS))
  1271  	}
  1272  	for _, dl := range diagram.Steps {
  1273  		buildBoardIDToIndex(dl, dictionary, append(newPath, STEPS))
  1274  	}
  1275  
  1276  	return dictionary
  1277  }
  1278  
  1279  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) {
  1280  	if !diagram.IsFolderOnly {
  1281  
  1282  		var scale *float64
  1283  		if opts.Scale != nil {
  1284  			scale = opts.Scale
  1285  		} else {
  1286  			scale = go2.Pointer(1.)
  1287  		}
  1288  		svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
  1289  			Pad:    opts.Pad,
  1290  			Sketch: opts.Sketch,
  1291  			Center: opts.Center,
  1292  			Scale:  scale,
  1293  		})
  1294  		if err != nil {
  1295  			return nil, nil, err
  1296  		}
  1297  
  1298  		svg, err = plugin.PostProcess(ctx, svg)
  1299  		if err != nil {
  1300  			return nil, nil, err
  1301  		}
  1302  
  1303  		cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
  1304  		l := simplelog.FromCmdLog(ms.Log)
  1305  		svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
  1306  		svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
  1307  		bundleErr = multierr.Combine(bundleErr, bundleErr2)
  1308  		if bundleErr != nil {
  1309  			return nil, nil, bundleErr
  1310  		}
  1311  
  1312  		svg = appendix.Append(diagram, ruler, svg)
  1313  
  1314  		pngImg, err := ConvertSVG(ms, page, svg)
  1315  		if err != nil {
  1316  			return nil, nil, err
  1317  		}
  1318  		pngs = append(pngs, pngImg)
  1319  	}
  1320  
  1321  	for _, dl := range diagram.Layers {
  1322  		_, layerPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
  1323  		if err != nil {
  1324  			return nil, nil, err
  1325  		}
  1326  		pngs = append(pngs, layerPNGs...)
  1327  	}
  1328  	for _, dl := range diagram.Scenarios {
  1329  		_, scenarioPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
  1330  		if err != nil {
  1331  			return nil, nil, err
  1332  		}
  1333  		pngs = append(pngs, scenarioPNGs...)
  1334  	}
  1335  	for _, dl := range diagram.Steps {
  1336  		_, stepsPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
  1337  		if err != nil {
  1338  			return nil, nil, err
  1339  		}
  1340  		pngs = append(pngs, stepsPNGs...)
  1341  	}
  1342  
  1343  	return svg, pngs, nil
  1344  }
  1345  
  1346  func ConvertSVG(ms *xmain.State, page playwright.Page, svg []byte) ([]byte, error) {
  1347  	cancel := background.Repeat(func() {
  1348  		ms.Log.Info.Printf("converting to PNG...")
  1349  	}, time.Second*5)
  1350  	defer cancel()
  1351  
  1352  	return png.ConvertSVG(page, svg)
  1353  }
  1354  
  1355  func AnimatePNGs(ms *xmain.State, pngs [][]byte, animIntervalMs int) ([]byte, error) {
  1356  	cancel := background.Repeat(func() {
  1357  		ms.Log.Info.Printf("generating GIF...")
  1358  	}, time.Second*5)
  1359  	defer cancel()
  1360  
  1361  	return xgif.AnimatePNGs(pngs, animIntervalMs)
  1362  }
  1363  
  1364  func init() {
  1365  	ctxlog.Init()
  1366  }
  1367  

View as plain text