From da1403746693b81447c5e6b1ef8da567891b19d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Fri, 7 Apr 2023 18:13:53 -0300 Subject: [PATCH 1/6] add links to pptx --- d2cli/main.go | 87 ++++++++++++++++++++++++++++++++---- lib/pdf/pdf.go | 25 ----------- lib/png/png.go | 2 + lib/pptx/pptx.go | 95 +++++++++++++++++++++++++++++++++------- lib/pptx/presentation.go | 76 ++++++++++++++++++++++---------- 5 files changed, 214 insertions(+), 71 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 1504d402b..3504ae8e0 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -21,6 +21,7 @@ import ( "oss.terrastruct.com/util-go/xmain" "oss.terrastruct.com/d2/d2lib" + "oss.terrastruct.com/d2/d2parser" "oss.terrastruct.com/d2/d2plugin" "oss.terrastruct.com/d2/d2renderers/d2animate" "oss.terrastruct.com/d2/d2renderers/d2fonts" @@ -32,7 +33,6 @@ 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" @@ -189,7 +189,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { outputPath = ms.AbsPath(outputPath) if *animateIntervalFlag > 0 { // Not checking for extension == "svg", because users may want to write SVG data to a non-svg-extension file - if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" { + if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" || filepath.Ext(outputPath) == ".pptx" { return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG.\nYou provided: %s", filepath.Ext(outputPath)) } } @@ -351,7 +351,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende switch filepath.Ext(outputPath) { case ".pdf": - pageMap := pdf.BuildPDFPageMap(diagram, nil, nil) + pageMap := buildBoardIdToIndex(diagram, nil, nil) pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap) if err != nil { return pdf, false, err @@ -367,7 +367,9 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende description := "Presentation auto-generated by D2 - https://d2lang.com/" rootName := getFileName(outputPath) p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers()) - err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil) + + boardIdToIndex := buildBoardIdToIndex(diagram, nil, nil) + err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, nil, boardIdToIndex) if err != nil { return nil, false, err } @@ -756,7 +758,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, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []string) error { +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) error { var currBoardPath []string // Root board doesn't have a name, so we use the output filename if diagram.Name == "" { @@ -792,31 +794,73 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present return bundleErr } + svg = appendix.Append(diagram, ruler, svg) + + // png.ConvertSVG scales the image by 2x + pngScale := 2. pngImg, err := png.ConvertSVG(ms, page, svg) if err != nil { return err } - err = presentation.AddSlide(pngImg, currBoardPath) + slide, err := presentation.AddSlide(pngImg, diagram, currBoardPath) if err != nil { return err } + + viewboxSlice := appendix.FindViewboxSlice(svg) + viewboxX, err := strconv.ParseFloat(viewboxSlice[0], 64) + if err != nil { + return err + } + viewboxY, err := strconv.ParseFloat(viewboxSlice[1], 64) + if err != nil { + return err + } + + // Draw links + for _, shape := range diagram.Shapes { + if shape.Link == "" { + continue + } + + linkX := pngScale * (float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth)) + linkY := pngScale * (float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth)) + linkWidth := pngScale * (float64(shape.Width) + float64(shape.StrokeWidth*2)) + linkHeight := pngScale * (float64(shape.Height) + float64(shape.StrokeWidth*2)) + link := &pptx.Link{ + Left: int(linkX), + Top: int(linkY), + Width: int(linkWidth), + Height: int(linkHeight), + Tooltip: shape.Link, + } + slide.AddLink(link) + key, err := d2parser.ParseKey(shape.Link) + if err != nil || key.Path[0].Unbox().ScalarString() != "root" { + // External link + link.ExternalUrl = shape.Link + } else if pageNum, ok := boardIdToIndex[shape.Link]; ok { + // Internal link + link.SlideIndex = pageNum + 1 + } + } } for _, dl := range diagram.Layers { - err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) + err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex) if err != nil { return err } } for _, dl := range diagram.Scenarios { - err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) + err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex) if err != nil { return err } } for _, dl := range diagram.Steps { - err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) + err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex) if err != nil { return err } @@ -914,3 +958,28 @@ func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold string) return d2fonts.AddFontFamily("custom", regularTTF, italicTTF, boldTTF) } + +// buildBoardIdToIndex returns a map from board path to page int +// To map correctly, it must follow the same traversal of PDF building +func buildBoardIdToIndex(diagram *d2target.Diagram, dictionary map[string]int, path []string) map[string]int { + newPath := append(path, diagram.Name) + if dictionary == nil { + dictionary = map[string]int{} + newPath[0] = "root" + } + + key := strings.Join(newPath, ".") + dictionary[key] = len(dictionary) + + for _, dl := range diagram.Layers { + buildBoardIdToIndex(dl, dictionary, append(newPath, "layers")) + } + for _, dl := range diagram.Scenarios { + buildBoardIdToIndex(dl, dictionary, append(newPath, "scenarios")) + } + for _, dl := range diagram.Steps { + buildBoardIdToIndex(dl, dictionary, append(newPath, "steps")) + } + + return dictionary +} diff --git a/lib/pdf/pdf.go b/lib/pdf/pdf.go index 7f1f25cc7..0d90ff09b 100644 --- a/lib/pdf/pdf.go +++ b/lib/pdf/pdf.go @@ -164,28 +164,3 @@ func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill func (g *GoFPDF) Export(outputPath string) error { return g.pdf.OutputFileAndClose(outputPath) } - -// BuildPDFPageMap returns a map from board path to page int -// To map correctly, it must follow the same traversal of PDF building -func BuildPDFPageMap(diagram *d2target.Diagram, dictionary map[string]int, path []string) map[string]int { - newPath := append(path, diagram.Name) - if dictionary == nil { - dictionary = map[string]int{} - newPath[0] = "root" - } - - key := strings.Join(newPath, ".") - dictionary[key] = len(dictionary) - - for _, dl := range diagram.Layers { - BuildPDFPageMap(dl, dictionary, append(newPath, "layers")) - } - for _, dl := range diagram.Scenarios { - BuildPDFPageMap(dl, dictionary, append(newPath, "scenarios")) - } - for _, dl := range diagram.Steps { - BuildPDFPageMap(dl, dictionary, append(newPath, "steps")) - } - - return dictionary -} diff --git a/lib/png/png.go b/lib/png/png.go index 8f89c0b20..01d332e1a 100644 --- a/lib/png/png.go +++ b/lib/png/png.go @@ -83,6 +83,8 @@ var genPNGScript string const pngPrefix = "data:image/png;base64," +// ConvertSVG converts the given SVG into a PNG. +// Note that the resulting PNG has 2x the size (width and height) of the original SVG (see generate_png.js) func ConvertSVG(ms *xmain.State, page playwright.Page, svg []byte) ([]byte, error) { cancel := background.Repeat(func() { ms.Log.Info.Printf("converting to PNG...") diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index 1d0c0f44c..d7e4b527d 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -49,27 +49,69 @@ const IMAGE_HEIGHT = SLIDE_HEIGHT - HEADER_HEIGHT const IMAGE_WIDTH = 8_446_273 const IMAGE_ASPECT_RATIO = float64(IMAGE_WIDTH) / float64(IMAGE_HEIGHT) -const RELS_SLIDE_XML = `` +func getRelsSlideXml(slide *Slide) string { + var builder strings.Builder + builder.WriteString(``) + builder.WriteString( + fmt.Sprintf( + ``, + slide.ImageId, + slide.ImageId, + ), + ) + for _, link := range slide.Links { + if link.isExternal() { + builder.WriteString( + fmt.Sprintf( + ``, + link.Id, + link.ExternalUrl, + ), + ) + } else { + builder.WriteString( + fmt.Sprintf( + ``, + link.Id, + link.SlideIndex, + ), + ) + } + } -func getRelsSlideXml(imageId string) string { - return fmt.Sprintf(RELS_SLIDE_XML, imageId, imageId) + builder.WriteString(``) + return builder.String() } -const SLIDE_XML = `%s` - -func getSlideXml(boardPath []string, imageId string, top, left, width, height int) string { - var slideTitle string - boardName := boardPath[len(boardPath)-1] - prefixPath := boardPath[:len(boardPath)-1] +func getSlideXml(slide *Slide) string { + var builder strings.Builder + builder.WriteString(``) + slideDescription := strings.Join(slide.BoardPath, " / ") + builder.WriteString(fmt.Sprintf(``, slideDescription, slideDescription)) + builder.WriteString(``) + builder.WriteString(fmt.Sprintf(``, slide.ImageId)) + builder.WriteString(``) + builder.WriteString(fmt.Sprintf(``, slide.ImageLeft, slide.ImageTop)) + builder.WriteString(fmt.Sprintf(``, slide.ImageWidth, slide.ImageHeight)) + builder.WriteString(``) + builder.WriteString(fmt.Sprintf(``, slideDescription)) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(fmt.Sprintf(``, HEADER_HEIGHT)) + builder.WriteString(``) + boardName := slide.BoardPath[len(slide.BoardPath)-1] + prefixPath := slide.BoardPath[:len(slide.BoardPath)-1] if len(prefixPath) > 0 { prefix := strings.Join(prefixPath, " / ") + " / " - slideTitle = fmt.Sprintf(`%s%s`, prefix, boardName) + builder.WriteString(fmt.Sprintf(`%s%s`, prefix, boardName)) } else { - slideTitle = fmt.Sprintf(`%s`, boardName) + builder.WriteString(fmt.Sprintf(`%s`, boardName)) } - slideDescription := strings.Join(boardPath, " / ") - top += HEADER_HEIGHT - return fmt.Sprintf(SLIDE_XML, slideDescription, slideDescription, imageId, left, top, width, height, slideDescription, HEADER_HEIGHT, slideTitle) + for _, link := range slide.Links { + builder.WriteString(getLinkXml(link)) + } + builder.WriteString(``) + return builder.String() } func getPresentationXmlRels(slideFileNames []string) string { @@ -113,7 +155,7 @@ func getPresentationXml(slideFileNames []string) string { builder.WriteString("") builder.WriteString(fmt.Sprintf( - ``, + ``, SLIDE_WIDTH, SLIDE_HEIGHT, )) @@ -198,3 +240,26 @@ func getAppXml(slides []*Slide, d2version string) string { builder.WriteString(``) return builder.String() } + +func getLinkXml(link *Link) string { + var builder strings.Builder + + builder.WriteString("") + builder.WriteString(fmt.Sprintf(``, link.Index, link.Tooltip)) + var linkAction string + if !link.isExternal() { + linkAction = "ppaction://hlinksldjump" + } + builder.WriteString( + fmt.Sprintf(``, + link.Id, + linkAction, + link.Tooltip, + ), + ) + builder.WriteString("") + builder.WriteString(fmt.Sprintf(``, link.Left, link.Top)) + builder.WriteString(fmt.Sprintf(``, link.Width, link.Height)) + builder.WriteString(``) + return builder.String() +} diff --git a/lib/pptx/presentation.go b/lib/pptx/presentation.go index a79e6253d..c562d6115 100644 --- a/lib/pptx/presentation.go +++ b/lib/pptx/presentation.go @@ -16,6 +16,8 @@ import ( "fmt" "image/png" "os" + + "oss.terrastruct.com/d2/d2target" ) type Presentation struct { @@ -29,12 +31,41 @@ type Presentation struct { } type Slide struct { - BoardPath []string - Image []byte - ImageWidth int - ImageHeight int - ImageTop int - ImageLeft int + BoardPath []string + Links []*Link + Image []byte + ImageId string + ImageWidth int + ImageHeight int + ImageTop int + ImageLeft int + ImageScaleFactor float64 +} + +func (s *Slide) AddLink(link *Link) { + link.Index = len(s.Links) + s.Links = append(s.Links, link) + link.Id = fmt.Sprintf("link%d", len(s.Links)) + link.Height *= int(s.ImageScaleFactor) + link.Width *= int(s.ImageScaleFactor) + link.Top = s.ImageTop + int(float64(link.Top)*s.ImageScaleFactor) + link.Left = s.ImageLeft + int(float64(link.Left)*s.ImageScaleFactor) +} + +type Link struct { + Id string + Index int + Top int + Left int + Width int + Height int + SlideIndex int + ExternalUrl string + Tooltip string +} + +func (l *Link) isExternal() bool { + return l.ExternalUrl != "" } func NewPresentation(title, description, subject, creator, d2Version string) *Presentation { @@ -47,16 +78,16 @@ func NewPresentation(title, description, subject, creator, d2Version string) *Pr } } -func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { +func (p *Presentation) AddSlide(pngContent []byte, diagram *d2target.Diagram, boardPath []string) (*Slide, error) { src, err := png.Decode(bytes.NewReader(pngContent)) if err != nil { - return fmt.Errorf("error decoding PNG image: %v", err) + return nil, fmt.Errorf("error decoding PNG image: %v", err) } - - var width, height int srcSize := src.Bounds().Size() srcWidth, srcHeight := float64(srcSize.X), float64(srcSize.Y) + var width, height int + // compute the size and position to fit the slide // if the image is wider than taller and its aspect ratio is, at least, the same as the available image space aspect ratio // then, set the image width to the available space and compute the height @@ -73,22 +104,24 @@ func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { height = IMAGE_HEIGHT width = int(float64(height) * (srcWidth / srcHeight)) } - top := (IMAGE_HEIGHT - height) / 2 + top := HEADER_HEIGHT + ((IMAGE_HEIGHT - height) / 2) left := (SLIDE_WIDTH - width) / 2 slide := &Slide{ - BoardPath: make([]string, len(boardPath)), - Image: pngContent, - ImageWidth: width, - ImageHeight: height, - ImageTop: top, - ImageLeft: left, + BoardPath: make([]string, len(boardPath)), + ImageId: fmt.Sprintf("slide%dImage", len(p.Slides)+1), + Image: pngContent, + ImageWidth: width, + ImageHeight: height, + ImageTop: top, + ImageLeft: left, + ImageScaleFactor: float64(width) / srcWidth, } // it must copy the board path to avoid slice reference issues copy(slide.BoardPath, boardPath) p.Slides = append(p.Slides, slide) - return nil + return slide, nil } func (p *Presentation) SaveTo(filePath string) error { @@ -106,11 +139,10 @@ func (p *Presentation) SaveTo(filePath string) error { var slideFileNames []string for i, slide := range p.Slides { - imageId := fmt.Sprintf("slide%dImage", i+1) slideFileName := fmt.Sprintf("slide%d", i+1) slideFileNames = append(slideFileNames, slideFileName) - imageWriter, err := zipWriter.Create(fmt.Sprintf("ppt/media/%s.png", imageId)) + imageWriter, err := zipWriter.Create(fmt.Sprintf("ppt/media/%s.png", slide.ImageId)) if err != nil { return err } @@ -119,7 +151,7 @@ func (p *Presentation) SaveTo(filePath string) error { return err } - err = addFile(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), getRelsSlideXml(imageId)) + err = addFile(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), getRelsSlideXml(slide)) if err != nil { return err } @@ -127,7 +159,7 @@ func (p *Presentation) SaveTo(filePath string) error { err = addFile( zipWriter, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), - getSlideXml(slide.BoardPath, imageId, slide.ImageTop, slide.ImageLeft, slide.ImageWidth, slide.ImageHeight), + getSlideXml(slide), ) if err != nil { return err From 7c06d1bc2e5bf5bcfa8c1a03b8468c3df8695616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 10 Apr 2023 10:35:54 -0300 Subject: [PATCH 2/6] fix links in keynote --- lib/pptx/pptx.go | 29 ++++++++++++++--------------- lib/pptx/xmlTemplates/link.xml | 27 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 lib/pptx/xmlTemplates/link.xml diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index d7e4b527d..f923276bb 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -241,25 +241,24 @@ func getAppXml(slides []*Slide, d2version string) string { return builder.String() } -func getLinkXml(link *Link) string { - var builder strings.Builder +//go:embed xmlTemplates/link.xml +var linkTemplate string - builder.WriteString("") - builder.WriteString(fmt.Sprintf(``, link.Index, link.Tooltip)) +func getLinkXml(link *Link) string { var linkAction string if !link.isExternal() { linkAction = "ppaction://hlinksldjump" } - builder.WriteString( - fmt.Sprintf(``, - link.Id, - linkAction, - link.Tooltip, - ), + return fmt.Sprintf( + linkTemplate, + link.Index, + link.Tooltip, + link.Id, + linkAction, + link.Tooltip, + link.Left, + link.Top, + link.Width, + link.Height, ) - builder.WriteString("") - builder.WriteString(fmt.Sprintf(``, link.Left, link.Top)) - builder.WriteString(fmt.Sprintf(``, link.Width, link.Height)) - builder.WriteString(``) - return builder.String() } diff --git a/lib/pptx/xmlTemplates/link.xml b/lib/pptx/xmlTemplates/link.xml new file mode 100644 index 000000000..645500adc --- /dev/null +++ b/lib/pptx/xmlTemplates/link.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 8322261d24d1a80db598f3fc3f454c6829478365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 10 Apr 2023 17:16:01 -0300 Subject: [PATCH 3/6] fix conflicts --- d2cli/main.go | 36 ++------- e2etests-cli/main_test.go | 7 +- lib/pptx/pptx.go | 125 ++++++++++++++++++++++++------ lib/pptx/templates/slide.xml | 37 +++++++-- lib/pptx/templates/slide.xml.rels | 7 ++ lib/pptx/xmlTemplates/link.xml | 27 ------- 6 files changed, 154 insertions(+), 85 deletions(-) delete mode 100644 lib/pptx/xmlTemplates/link.xml diff --git a/d2cli/main.go b/d2cli/main.go index 0b3765baf..4619fa1bd 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -368,13 +368,9 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende rootName := getFileName(outputPath) // version must be only numbers to avoid issues with PowerPoint p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers()) -<<<<<<< HEAD boardIdToIndex := buildBoardIdToIndex(diagram, nil, nil) - err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, nil, boardIdToIndex) -======= - svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil) ->>>>>>> gh-821-ppt + svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, nil, boardIdToIndex) if err != nil { return nil, false, err } @@ -763,11 +759,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt return svg, nil } -<<<<<<< HEAD -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) error { -======= -func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []string) ([]byte, error) { ->>>>>>> gh-821-ppt +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 == "" { @@ -814,7 +806,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present return nil, err } - slide, err := presentation.AddSlide(pngImg, diagram, currBoardPath) + slide, err := presentation.AddSlide(pngImg, currBoardPath) if err != nil { return nil, err } @@ -822,11 +814,11 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present viewboxSlice := appendix.FindViewboxSlice(svg) viewboxX, err := strconv.ParseFloat(viewboxSlice[0], 64) if err != nil { - return err + return nil, err } viewboxY, err := strconv.ParseFloat(viewboxSlice[1], 64) if err != nil { - return err + return nil, err } // Draw links @@ -859,31 +851,19 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present } for _, dl := range diagram.Layers { -<<<<<<< HEAD - err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex) -======= - _, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) ->>>>>>> gh-821-ppt + _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex) if err != nil { return nil, err } } for _, dl := range diagram.Scenarios { -<<<<<<< HEAD - err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex) -======= - _, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) ->>>>>>> gh-821-ppt + _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex) if err != nil { return nil, err } } for _, dl := range diagram.Steps { -<<<<<<< HEAD - err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex) -======= - _, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) ->>>>>>> gh-821-ppt + _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex) if err != nil { return nil, err } diff --git a/e2etests-cli/main_test.go b/e2etests-cli/main_test.go index 3bf8d24a3..11e1b6f0e 100644 --- a/e2etests-cli/main_test.go +++ b/e2etests-cli/main_test.go @@ -249,7 +249,9 @@ layers: { name: "how_to_solve_problems_pptx", skipCI: true, run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { - writeFile(t, dir, "in.d2", `how to solve a hard problem? + writeFile(t, dir, "in.d2", `how to solve a hard problem? { + link: steps.2 +} steps: { 1: { w: write down the problem @@ -261,6 +263,9 @@ steps: { 3: { t -> w2 w2: write down the solution + w2: { + link: https://d2lang.com + } } } `) diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index 0063e0764..9bc885fd8 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -32,14 +32,38 @@ type Presentation struct { Slides []*Slide } - type Slide struct { - BoardPath []string - Image []byte - ImageWidth int - ImageHeight int - ImageTop int - ImageLeft int + BoardPath []string + Links []*Link + Image []byte + ImageId string + ImageWidth int + ImageHeight int + ImageTop int + ImageLeft int + ImageScaleFactor float64 +} + +func (s *Slide) AddLink(link *Link) { + link.Index = len(s.Links) + s.Links = append(s.Links, link) + link.ID = fmt.Sprintf("link%d", len(s.Links)) + link.Height *= int(s.ImageScaleFactor) + link.Width *= int(s.ImageScaleFactor) + link.Top = s.ImageTop + int(float64(link.Top)*s.ImageScaleFactor) + link.Left = s.ImageLeft + int(float64(link.Left)*s.ImageScaleFactor) +} + +type Link struct { + ID string + Index int + Top int + Left int + Width int + Height int + SlideIndex int + ExternalUrl string + Tooltip string } func NewPresentation(title, description, subject, creator, d2Version string) *Presentation { @@ -52,10 +76,10 @@ func NewPresentation(title, description, subject, creator, d2Version string) *Pr } } -func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { +func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) (*Slide, error) { src, err := png.Decode(bytes.NewReader(pngContent)) if err != nil { - return fmt.Errorf("error decoding PNG image: %v", err) + return nil, fmt.Errorf("error decoding PNG image: %v", err) } var width, height int @@ -99,22 +123,24 @@ func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { height = IMAGE_HEIGHT width = int(float64(height) * (srcWidth / srcHeight)) } - top := (IMAGE_HEIGHT - height) / 2 + top := HEADER_HEIGHT + ((IMAGE_HEIGHT - height) / 2) left := (SLIDE_WIDTH - width) / 2 slide := &Slide{ - BoardPath: make([]string, len(boardPath)), - Image: pngContent, - ImageWidth: width, - ImageHeight: height, - ImageTop: top, - ImageLeft: left, + BoardPath: make([]string, len(boardPath)), + ImageId: fmt.Sprintf("slide%dImage", len(p.Slides)+1), + Image: pngContent, + ImageWidth: width, + ImageHeight: height, + ImageTop: top, + ImageLeft: left, + ImageScaleFactor: float64(width) / srcWidth, } // it must copy the board path to avoid slice reference issues copy(slide.BoardPath, boardPath) p.Slides = append(p.Slides, slide) - return nil + return slide, nil } func (p *Presentation) SaveTo(filePath string) error { @@ -145,10 +171,7 @@ func (p *Presentation) SaveTo(filePath string) error { return err } - err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), RELS_SLIDE_XML, RelsSlideXmlContent{ - FileName: imageID, - RelationshipID: imageID, - }) + err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), RELS_SLIDE_XML, getSlideXmlRelsContent(imageID, slide)) if err != nil { return err } @@ -242,14 +265,49 @@ func copyPptxTemplateTo(w *zip.Writer) error { //go:embed templates/slide.xml.rels var RELS_SLIDE_XML string +type RelsSlideXmlLinkContent struct { + RelationshipID string + ExternalUrl string + SlideIndex int +} + type RelsSlideXmlContent struct { FileName string RelationshipID string + Links []RelsSlideXmlLinkContent +} + +func getSlideXmlRelsContent(imageID string, slide *Slide) RelsSlideXmlContent { + content := RelsSlideXmlContent{ + FileName: imageID, + RelationshipID: imageID, + } + + for _, link := range slide.Links { + content.Links = append(content.Links, RelsSlideXmlLinkContent{ + RelationshipID: link.ID, + ExternalUrl: link.ExternalUrl, + SlideIndex: link.SlideIndex, + }) + } + + return content } //go:embed templates/slide.xml var SLIDE_XML string +type SlideLinkXmlContent struct { + ID int + RelationshipID string + Name string + Action string + Left int + Top int + Width int + Height int +} + type SlideXmlContent struct { Title string TitlePrefix string @@ -260,6 +318,8 @@ type SlideXmlContent struct { ImageTop int ImageWidth int ImageHeight int + + Links []SlideLinkXmlContent } func getSlideXmlContent(imageID string, slide *Slide) SlideXmlContent { @@ -270,17 +330,36 @@ func getSlideXmlContent(imageID string, slide *Slide) SlideXmlContent { if len(prefixPath) > 0 { prefix = strings.Join(prefixPath, " / ") + " / " } - return SlideXmlContent{ + content := SlideXmlContent{ Title: boardName, TitlePrefix: prefix, Description: strings.Join(boardPath, " / "), HeaderHeight: HEADER_HEIGHT, ImageID: imageID, ImageLeft: slide.ImageLeft, - ImageTop: slide.ImageTop + HEADER_HEIGHT, + ImageTop: slide.ImageTop, ImageWidth: slide.ImageWidth, ImageHeight: slide.ImageHeight, } + + for _, link := range slide.Links { + var action string + if link.ExternalUrl == "" { + action = "ppaction://hlinksldjump" + } + content.Links = append(content.Links, SlideLinkXmlContent{ + ID: link.Index, + RelationshipID: link.ID, + Name: link.Tooltip, + Action: action, + Left: link.Left, + Top: link.Top, + Width: link.Width, + Height: link.Height, + }) + } + + return content } //go:embed templates/rels_presentation.xml diff --git a/lib/pptx/templates/slide.xml b/lib/pptx/templates/slide.xml index 2721068a1..d138670ea 100644 --- a/lib/pptx/templates/slide.xml +++ b/lib/pptx/templates/slide.xml @@ -75,19 +75,44 @@ - - {{if .TitlePrefix}} - + {{if .TitlePrefix}} {{.TitlePrefix}} - - {{end}} - + {{end}} {{.Title}} + {{range .Links}} + + + + + + + + + + + + + + + + + + + + + + + + + + + {{end}} diff --git a/lib/pptx/templates/slide.xml.rels b/lib/pptx/templates/slide.xml.rels index 92475c305..bf8b08a03 100644 --- a/lib/pptx/templates/slide.xml.rels +++ b/lib/pptx/templates/slide.xml.rels @@ -6,4 +6,11 @@ + {{range .Links}} + {{if .ExternalUrl}} + + {{else}} + + {{end}} + {{end}} \ No newline at end of file diff --git a/lib/pptx/xmlTemplates/link.xml b/lib/pptx/xmlTemplates/link.xml deleted file mode 100644 index 645500adc..000000000 --- a/lib/pptx/xmlTemplates/link.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 989c3f88ace7f46e679663a52bf72819cdb4a20b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 10 Apr 2023 17:24:34 -0300 Subject: [PATCH 4/6] add blank line --- lib/pptx/templates/slide.xml.rels | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pptx/templates/slide.xml.rels b/lib/pptx/templates/slide.xml.rels index bf8b08a03..01d526e4c 100644 --- a/lib/pptx/templates/slide.xml.rels +++ b/lib/pptx/templates/slide.xml.rels @@ -13,4 +13,4 @@ {{end}} {{end}} - \ No newline at end of file + From cbd1afeaefefbefa13cd8d685a337cf04273c372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 11 Apr 2023 10:25:04 -0300 Subject: [PATCH 5/6] pr comments --- d2cli/main.go | 26 ++++++++++++-------------- lib/png/png.go | 3 +++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 4619fa1bd..58706a341 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -351,7 +351,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende switch filepath.Ext(outputPath) { case ".pdf": - pageMap := buildBoardIdToIndex(diagram, nil, nil) + pageMap := buildBoardIDToIndex(diagram, nil, nil) pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap) if err != nil { return pdf, false, err @@ -369,7 +369,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende // version must be only numbers to avoid issues with PowerPoint p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers()) - boardIdToIndex := buildBoardIdToIndex(diagram, nil, nil) + boardIdToIndex := buildBoardIDToIndex(diagram, nil, nil) svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, nil, boardIdToIndex) if err != nil { return nil, false, err @@ -799,8 +799,6 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present svg = appendix.Append(diagram, ruler, svg) - // png.ConvertSVG scales the image by 2x - pngScale := 2. pngImg, err := png.ConvertSVG(ms, page, svg) if err != nil { return nil, err @@ -827,10 +825,10 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present continue } - linkX := pngScale * (float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth)) - linkY := pngScale * (float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth)) - linkWidth := pngScale * (float64(shape.Width) + float64(shape.StrokeWidth*2)) - linkHeight := pngScale * (float64(shape.Height) + float64(shape.StrokeWidth*2)) + linkX := png.SCALE * (float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth)) + linkY := png.SCALE * (float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth)) + linkWidth := png.SCALE * (float64(shape.Width) + float64(shape.StrokeWidth*2)) + linkHeight := png.SCALE * (float64(shape.Height) + float64(shape.StrokeWidth*2)) link := &pptx.Link{ Left: int(linkX), Top: int(linkY), @@ -959,9 +957,9 @@ func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold string) return d2fonts.AddFontFamily("custom", regularTTF, italicTTF, boldTTF) } -// buildBoardIdToIndex returns a map from board path to page int -// To map correctly, it must follow the same traversal of PDF building -func buildBoardIdToIndex(diagram *d2target.Diagram, dictionary map[string]int, path []string) map[string]int { +// 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 { newPath := append(path, diagram.Name) if dictionary == nil { dictionary = map[string]int{} @@ -972,13 +970,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/png/png.go b/lib/png/png.go index 01d332e1a..ecb13c5a4 100644 --- a/lib/png/png.go +++ b/lib/png/png.go @@ -19,6 +19,9 @@ import ( "oss.terrastruct.com/util-go/xmain" ) +// ConvertSVG scales the image by 2x +const SCALE = 2. + type Playwright struct { PW *playwright.Playwright Browser playwright.Browser From 1b8122b0f95fb1b063d763463568fa7050c4cc96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 11 Apr 2023 14:38:44 -0300 Subject: [PATCH 6/6] set scale when generating png --- lib/png/generate_png.js | 8 ++++---- lib/png/png.go | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/png/generate_png.js b/lib/png/generate_png.js index 8c1c40806..31cf1e439 100644 --- a/lib/png/generate_png.js +++ b/lib/png/generate_png.js @@ -1,18 +1,18 @@ -async (imgString) => { +async ({imgString, scale}) => { const tempImg = new Image(); const loadImage = () => { return new Promise((resolve, reject) => { tempImg.onload = (event) => resolve(event.currentTarget); tempImg.onerror = () => { - reject("error loading string as an image"); + reject("error loading string as an image:\n" + imgString); }; tempImg.src = imgString; }); }; const img = await loadImage(); const canvas = document.createElement("canvas"); - canvas.width = img.width * 2; - canvas.height = img.height * 2; + canvas.width = img.width * scale; + canvas.height = img.height * scale; const ctx = canvas.getContext("2d"); if (!ctx) { return new Error("could not get canvas context"); diff --git a/lib/png/png.go b/lib/png/png.go index ecb13c5a4..c0cc23687 100644 --- a/lib/png/png.go +++ b/lib/png/png.go @@ -95,7 +95,10 @@ func ConvertSVG(ms *xmain.State, page playwright.Page, svg []byte) ([]byte, erro defer cancel() encodedSVG := base64.StdEncoding.EncodeToString(svg) - pngInterface, err := page.Evaluate(genPNGScript, "data:image/svg+xml;charset=utf-8;base64,"+encodedSVG) + pngInterface, err := page.Evaluate(genPNGScript, map[string]interface{}{ + "imgString": "data:image/svg+xml;charset=utf-8;base64," + encodedSVG, + "scale": int(SCALE), + }) if err != nil { return nil, fmt.Errorf("failed to generate png: %w", err) }