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}}