diff --git a/d2cli/main.go b/d2cli/main.go index 89abb9a00..9b781e0d2 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -33,6 +33,7 @@ import ( "oss.terrastruct.com/d2/lib/background" "oss.terrastruct.com/d2/lib/imgbundler" ctxlog "oss.terrastruct.com/d2/lib/log" + "oss.terrastruct.com/d2/lib/pdf" pdflib "oss.terrastruct.com/d2/lib/pdf" "oss.terrastruct.com/d2/lib/png" "oss.terrastruct.com/d2/lib/pptx" @@ -353,7 +354,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende switch filepath.Ext(outputPath) { case ".pdf": pageMap := buildBoardIDToIndex(diagram, nil, nil) - pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap) + pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, "", pageMap) if err != nil { return pdf, false, err } @@ -684,19 +685,26 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts return svg, nil } -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, pdf *pdflib.GoFPDF, boardPath []string, pageMap map[string]int) (svg []byte, err error) { +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, pdf *pdflib.GoFPDF, boardPath []pdf.BoardTitle, boardType string, pageMap map[string]int) (svg []byte, err error) { var isRoot bool if pdf == nil { pdf = pdflib.Init() isRoot = true } - var currBoardPath []string + var currBoardPath []pdflib.BoardTitle // Root board doesn't have a name, so we use the output filename if diagram.Name == "" { - currBoardPath = append(boardPath, "root") + currBoardPath = append(boardPath, pdflib.BoardTitle{ + Name: "root", + BoardID: "root", + }) } else { - currBoardPath = append(boardPath, diagram.Name) + prev := boardPath[len(boardPath)-1] + currBoardPath = append(boardPath, pdflib.BoardTitle{ + Name: diagram.Name, + BoardID: strings.Join([]string{prev.BoardID, boardType, diagram.Name}, "."), + }) } if !diagram.IsFolderOnly { @@ -749,19 +757,19 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt } for _, dl := range diagram.Layers { - _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap) + _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, "layers", pageMap) if err != nil { return nil, err } } for _, dl := range diagram.Scenarios { - _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap) + _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, "scenarios", pageMap) if err != nil { return nil, err } } for _, dl := range diagram.Steps { - _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap) + _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, "steps", pageMap) if err != nil { return nil, err } diff --git a/lib/pdf/pdf.go b/lib/pdf/pdf.go index 0d90ff09b..91e7ca9aa 100644 --- a/lib/pdf/pdf.go +++ b/lib/pdf/pdf.go @@ -15,10 +15,17 @@ import ( "oss.terrastruct.com/d2/lib/color" ) +const TITLE_SEP = " / " + type GoFPDF struct { pdf *gofpdf.Fpdf } +type BoardTitle struct { + Name string + BoardID string +} + func Init() *GoFPDF { newGofPDF := gofpdf.NewCustom(&gofpdf.InitType{ UnitStr: "pt", @@ -59,9 +66,13 @@ func (g *GoFPDF) GetFillRGB(themeID int64, fill string) (color.RGB, error) { return color.Hex2RGB(fill) } -func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill string, shapes []d2target.Shape, pad int64, viewboxX, viewboxY float64, pageMap map[string]int) error { +func (g *GoFPDF) AddPDFPage(png []byte, titlePath []BoardTitle, themeID int64, fill string, shapes []d2target.Shape, pad int64, viewboxX, viewboxY float64, pageMap map[string]int) error { var opt gofpdf.ImageOptions opt.ImageType = "png" + boardPath := make([]string, len(titlePath)) + for i, t := range titlePath { + boardPath[i] = t.Name + } imageInfo := g.pdf.RegisterImageOptionsReader(strings.Join(boardPath, "/"), opt, bytes.NewReader(png)) if g.pdf.Err() { return g.pdf.Error() @@ -102,20 +113,30 @@ func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill g.pdf.SetFont("source", "", 14) // Draw board path prefix - var prefixWidth float64 - prefixPath := boardPath[:len(boardPath)-1] - if len(prefixPath) > 0 { - prefix := strings.Join(boardPath[:len(boardPath)-1], " / ") + " / " - prefixWidth = g.pdf.GetStringWidth(prefix) + prefixWidth := headerMargin + if len(titlePath) > 1 { + for _, t := range titlePath[:len(titlePath)-1] { + g.pdf.SetXY(prefixWidth, 0) + w := g.pdf.GetStringWidth(t.Name) + var linkID int + if pageNum, ok := pageMap[t.BoardID]; ok { + linkID = g.pdf.AddLink() + g.pdf.SetLink(linkID, 0, pageNum+1) + } + g.pdf.CellFormat(w, headerHeight, t.Name, "", 0, "", false, linkID, "") + prefixWidth += w - g.pdf.SetXY(headerMargin, 0) - g.pdf.CellFormat(prefixWidth, headerHeight, prefix, "", 0, "", false, 0, "") + g.pdf.SetXY(prefixWidth, 0) + w = g.pdf.GetStringWidth(TITLE_SEP) + g.pdf.CellFormat(prefixWidth, headerHeight, TITLE_SEP, "", 0, "", false, 0, "") + prefixWidth += w + } } // Draw board name boardName := boardPath[len(boardPath)-1] g.pdf.SetFont("source", "B", 14) - g.pdf.SetXY(prefixWidth+headerMargin, 0) + g.pdf.SetXY(prefixWidth, 0) g.pdf.CellFormat(pageWidth-prefixWidth-headerMargin, headerHeight, boardName, "", 0, "", false, 0, "") // Draw image