diff --git a/d2cli/main.go b/d2cli/main.go
index c28b95a9b..2f87fee80 100644
--- a/d2cli/main.go
+++ b/d2cli/main.go
@@ -33,7 +33,7 @@ import (
"oss.terrastruct.com/d2/lib/background"
"oss.terrastruct.com/d2/lib/imgbundler"
ctxlog "oss.terrastruct.com/d2/lib/log"
- pdflib "oss.terrastruct.com/d2/lib/pdf"
+ "oss.terrastruct.com/d2/lib/pdf"
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/pptx"
"oss.terrastruct.com/d2/lib/textmeasure"
@@ -380,7 +380,10 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
return svg, true, nil
case PDF:
pageMap := buildBoardIDToIndex(diagram, nil, nil)
- pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap)
+ path := []pdf.BoardTitle{
+ {Name: "root", BoardID: "root"},
+ }
+ pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, path, pageMap)
if err != nil {
return pdf, false, err
}
@@ -398,7 +401,10 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers())
boardIdToIndex := buildBoardIDToIndex(diagram, nil, nil)
- svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, nil, boardIdToIndex)
+ path := []pptx.BoardTitle{
+ {Name: "root", BoardID: "root", LinkToSlide: boardIdToIndex["root"] + 1},
+ }
+ svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, path, boardIdToIndex)
if err != nil {
return nil, false, err
}
@@ -711,21 +717,13 @@ 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, doc *pdf.GoFPDF, boardPath []pdf.BoardTitle, pageMap map[string]int) (svg []byte, err error) {
var isRoot bool
- if pdf == nil {
- pdf = pdflib.Init()
+ if doc == nil {
+ doc = pdf.Init()
isRoot = true
}
- var currBoardPath []string
- // Root board doesn't have a name, so we use the output filename
- if diagram.Name == "" {
- currBoardPath = append(boardPath, getFileName(outputPath))
- } else {
- currBoardPath = append(boardPath, diagram.Name)
- }
-
if !diagram.IsFolderOnly {
rootFill := diagram.Root.Fill
// gofpdf will print the png img with a slight filter
@@ -769,33 +767,45 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
if err != nil {
return svg, err
}
- err = pdf.AddPDFPage(pngImg, currBoardPath, opts.ThemeID, rootFill, diagram.Shapes, int64(opts.Pad), viewboxX, viewboxY, pageMap)
+ err = doc.AddPDFPage(pngImg, boardPath, opts.ThemeID, rootFill, diagram.Shapes, int64(opts.Pad), viewboxX, viewboxY, pageMap)
if err != nil {
return svg, err
}
}
for _, dl := range diagram.Layers {
- _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
+ path := append(boardPath, pdf.BoardTitle{
+ Name: dl.Name,
+ BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, LAYERS, dl.Name}, "."),
+ })
+ _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap)
if err != nil {
return nil, err
}
}
for _, dl := range diagram.Scenarios {
- _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
+ path := append(boardPath, pdf.BoardTitle{
+ Name: dl.Name,
+ BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, SCENARIOS, dl.Name}, "."),
+ })
+ _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap)
if err != nil {
return nil, err
}
}
for _, dl := range diagram.Steps {
- _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
+ path := append(boardPath, pdf.BoardTitle{
+ Name: dl.Name,
+ BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, STEPS, dl.Name}, "."),
+ })
+ _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap)
if err != nil {
return nil, err
}
}
if isRoot {
- err := pdf.Export(outputPath)
+ err := doc.Export(outputPath)
if err != nil {
return nil, err
}
@@ -804,15 +814,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
return svg, nil
}
-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 []string, boardIdToIndex map[string]int) ([]byte, error) {
- var currBoardPath []string
- // Root board doesn't have a name, so we use the output filename
- if diagram.Name == "" {
- currBoardPath = append(boardPath, getFileName(outputPath))
- } else {
- currBoardPath = append(boardPath, diagram.Name)
- }
-
+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) {
var svg []byte
if !diagram.IsFolderOnly {
// gofpdf will print the png img with a slight filter
@@ -849,7 +851,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
return nil, err
}
- slide, err := presentation.AddSlide(pngImg, currBoardPath)
+ slide, err := presentation.AddSlide(pngImg, boardPath)
if err != nil {
return nil, err
}
@@ -886,7 +888,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
if err != nil || key.Path[0].Unbox().ScalarString() != "root" {
// External link
link.ExternalUrl = shape.Link
- } else if pageNum, ok := boardIdToIndex[shape.Link]; ok {
+ } else if pageNum, ok := boardIDToIndex[shape.Link]; ok {
// Internal link
link.SlideIndex = pageNum + 1
}
@@ -894,19 +896,37 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
}
for _, dl := range diagram.Layers {
- _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex)
+ boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, LAYERS, dl.Name}, ".")
+ path := append(boardPath, pptx.BoardTitle{
+ Name: dl.Name,
+ BoardID: boardID,
+ LinkToSlide: boardIDToIndex[boardID] + 1,
+ })
+ _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
if err != nil {
return nil, err
}
}
for _, dl := range diagram.Scenarios {
- _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex)
+ boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, SCENARIOS, dl.Name}, ".")
+ path := append(boardPath, pptx.BoardTitle{
+ Name: dl.Name,
+ BoardID: boardID,
+ LinkToSlide: boardIDToIndex[boardID] + 1,
+ })
+ _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
if err != nil {
return nil, err
}
}
for _, dl := range diagram.Steps {
- _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex)
+ boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, STEPS, dl.Name}, ".")
+ path := append(boardPath, pptx.BoardTitle{
+ Name: dl.Name,
+ BoardID: boardID,
+ LinkToSlide: boardIDToIndex[boardID] + 1,
+ })
+ _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
if err != nil {
return nil, err
}
@@ -1009,6 +1029,10 @@ func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold, pathToS
return d2fonts.AddFontFamily("custom", regularTTF, italicTTF, boldTTF, semiboldTTF)
}
+const LAYERS = "layers"
+const STEPS = "steps"
+const SCENARIOS = "scenarios"
+
// buildBoardIDToIndex returns a map from board path to page int
// To map correctly, it must follow the same traversal of pdf/pptx building
func buildBoardIDToIndex(diagram *d2target.Diagram, dictionary map[string]int, path []string) map[string]int {
@@ -1022,13 +1046,13 @@ func buildBoardIDToIndex(diagram *d2target.Diagram, dictionary map[string]int, p
dictionary[key] = len(dictionary)
for _, dl := range diagram.Layers {
- buildBoardIDToIndex(dl, dictionary, append(newPath, "layers"))
+ buildBoardIDToIndex(dl, dictionary, append(newPath, LAYERS))
}
for _, dl := range diagram.Scenarios {
- buildBoardIDToIndex(dl, dictionary, append(newPath, "scenarios"))
+ buildBoardIDToIndex(dl, dictionary, append(newPath, SCENARIOS))
}
for _, dl := range diagram.Steps {
- buildBoardIDToIndex(dl, dictionary, append(newPath, "steps"))
+ buildBoardIDToIndex(dl, dictionary, append(newPath, STEPS))
}
return dictionary
diff --git a/lib/pdf/pdf.go b/lib/pdf/pdf.go
index 5f1a47928..9d9eabe69 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
diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go
index 9bc885fd8..66d02e317 100644
--- a/lib/pptx/pptx.go
+++ b/lib/pptx/pptx.go
@@ -16,11 +16,17 @@ import (
"fmt"
"image/png"
"os"
- "strings"
"text/template"
"time"
)
+type BoardTitle struct {
+ LinkID string
+ Name string
+ BoardID string
+ LinkToSlide int
+}
+
type Presentation struct {
Title string
Description string
@@ -32,8 +38,9 @@ type Presentation struct {
Slides []*Slide
}
+
type Slide struct {
- BoardPath []string
+ BoardTitle []BoardTitle
Links []*Link
Image []byte
ImageId string
@@ -76,7 +83,7 @@ func NewPresentation(title, description, subject, creator, d2Version string) *Pr
}
}
-func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) (*Slide, error) {
+func (p *Presentation) AddSlide(pngContent []byte, titlePath []BoardTitle) (*Slide, error) {
src, err := png.Decode(bytes.NewReader(pngContent))
if err != nil {
return nil, fmt.Errorf("error decoding PNG image: %v", err)
@@ -127,7 +134,7 @@ func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) (*Slide,
left := (SLIDE_WIDTH - width) / 2
slide := &Slide{
- BoardPath: make([]string, len(boardPath)),
+ BoardTitle: make([]BoardTitle, len(titlePath)),
ImageId: fmt.Sprintf("slide%dImage", len(p.Slides)+1),
Image: pngContent,
ImageWidth: width,
@@ -137,7 +144,10 @@ func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) (*Slide,
ImageScaleFactor: float64(width) / srcWidth,
}
// it must copy the board path to avoid slice reference issues
- copy(slide.BoardPath, boardPath)
+ for i := 0; i < len(titlePath); i++ {
+ titlePath[i].LinkID = fmt.Sprintf("navLink%d", i)
+ slide.BoardTitle[i] = titlePath[i]
+ }
p.Slides = append(p.Slides, slide)
return slide, nil
@@ -215,11 +225,11 @@ func (p *Presentation) SaveTo(filePath string) error {
titles := make([]string, 0, len(p.Slides))
for _, slide := range p.Slides {
- titles = append(titles, strings.Join(slide.BoardPath, "/"))
+ titles = append(titles, slide.BoardTitle[len(slide.BoardTitle)-1].BoardID)
}
err = addFileFromTemplate(zipWriter, "docProps/app.xml", APP_XML, AppXmlContent{
SlideCount: len(p.Slides),
- TitlesOfPartsCount: len(p.Slides) + 3,
+ TitlesOfPartsCount: len(p.Slides) + 3, // + 3 for fonts and theme
D2Version: p.D2Version,
Titles: titles,
})
@@ -291,6 +301,13 @@ func getSlideXmlRelsContent(imageID string, slide *Slide) RelsSlideXmlContent {
})
}
+ for _, t := range slide.BoardTitle {
+ content.Links = append(content.Links, RelsSlideXmlLinkContent{
+ RelationshipID: t.LinkID,
+ SlideIndex: t.LinkToSlide,
+ })
+ }
+
return content
}
@@ -308,9 +325,14 @@ type SlideLinkXmlContent struct {
Height int
}
+type SlideXmlTitlePathContent struct {
+ Name string
+ RelationshipID string
+}
+
type SlideXmlContent struct {
Title string
- TitlePrefix string
+ TitlePrefix []SlideXmlTitlePathContent
Description string
HeaderHeight int
ImageID string
@@ -323,17 +345,18 @@ type SlideXmlContent struct {
}
func getSlideXmlContent(imageID string, slide *Slide) SlideXmlContent {
- boardPath := slide.BoardPath
- boardName := boardPath[len(boardPath)-1]
- prefixPath := boardPath[:len(boardPath)-1]
- var prefix string
- if len(prefixPath) > 0 {
- prefix = strings.Join(prefixPath, " / ") + " / "
+ title := make([]SlideXmlTitlePathContent, len(slide.BoardTitle)-1)
+ for i := 0; i < len(slide.BoardTitle)-1; i++ {
+ t := slide.BoardTitle[i]
+ title[i] = SlideXmlTitlePathContent{
+ Name: t.Name,
+ RelationshipID: t.LinkID,
+ }
}
content := SlideXmlContent{
- Title: boardName,
- TitlePrefix: prefix,
- Description: strings.Join(boardPath, " / "),
+ Title: slide.BoardTitle[len(slide.BoardTitle)-1].Name,
+ TitlePrefix: title,
+ Description: slide.BoardTitle[len(slide.BoardTitle)-1].BoardID,
HeaderHeight: HEADER_HEIGHT,
ImageID: imageID,
ImageLeft: slide.ImageLeft,
diff --git a/lib/pptx/template.pptx b/lib/pptx/template.pptx
index f87fcd9e2..783f7014c 100644
Binary files a/lib/pptx/template.pptx and b/lib/pptx/template.pptx differ
diff --git a/lib/pptx/templates/slide.xml b/lib/pptx/templates/slide.xml
index d138670ea..eace2f28e 100644
--- a/lib/pptx/templates/slide.xml
+++ b/lib/pptx/templates/slide.xml
@@ -75,9 +75,19 @@
- {{if .TitlePrefix}}
- {{.TitlePrefix}}
- {{end}}
+
+ {{range .TitlePrefix}}
+
+
+
+
+ {{.Name}}
+
+ /
+ {{end}}
+
{{.Title}}