From b9ce8324a44bc77b164d3f9747cc08ef8b8f6cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 4 Apr 2023 11:13:47 -0300 Subject: [PATCH 01/20] base ppt export --- lib/ppt/pptx.go | 111 ++++++++++++++++++++++++++++++++++++++++ lib/ppt/presentation.go | 100 ++++++++++++++++++++++++++++++++++++ lib/ppt/template.pptx | Bin 0 -> 29492 bytes 3 files changed, 211 insertions(+) create mode 100644 lib/ppt/pptx.go create mode 100644 lib/ppt/presentation.go create mode 100644 lib/ppt/template.pptx diff --git a/lib/ppt/pptx.go b/lib/ppt/pptx.go new file mode 100644 index 000000000..04f30b03e --- /dev/null +++ b/lib/ppt/pptx.go @@ -0,0 +1,111 @@ +package ppt + +import ( + "archive/zip" + "bytes" + _ "embed" + "fmt" + "io" + "strings" +) + +// Office Open XML (OOXML) http://officeopenxml.com/prPresentation.php + +//go:embed template.pptx +var pptx_template []byte + +func copyPptxTemplateTo(w *zip.Writer) error { + reader := bytes.NewReader(pptx_template) + zipReader, err := zip.NewReader(reader, reader.Size()) + if err != nil { + fmt.Printf("%v", err) + } + + for _, f := range zipReader.File { + fw, err := w.Create(f.Name) + if err != nil { + return err + } + fr, err := f.Open() + if err != nil { + return err + } + _, err = io.Copy(fw, fr) + if err != nil { + return err + } + } + return nil +} + +func addFile(zipFile *zip.Writer, filePath, content string) error { + w, err := zipFile.Create(filePath) + if err != nil { + return err + } + w.Write([]byte(content)) + return nil +} + +// https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/ +const SLIDE_WIDTH = 9144000 +const SLIDE_HEIGHT = 5143500 + +const RELS_SLIDE_XML = `` + +func getRelsSlideXml(imageId string) string { + return fmt.Sprintf(RELS_SLIDE_XML, imageId, imageId) +} + +const SLIDE_XML = `` + +func getSlideXml(imageId, imageName string, top, left, width, height int) string { + return fmt.Sprintf(SLIDE_XML, imageName, imageName, imageId, left, top, width, height) +} + +func getPresentationXmlRels(slideFileNames []string) string { + var builder strings.Builder + builder.WriteString(``) + + for _, name := range slideFileNames { + builder.WriteString(fmt.Sprintf( + ``, name, name, + )) + } + + builder.WriteString("") + + return builder.String() +} + +func getContentTypesXml(slideFileNames []string) string { + var builder strings.Builder + builder.WriteString(``) + + for _, name := range slideFileNames { + builder.WriteString(fmt.Sprintf( + ``, name, + )) + } + + builder.WriteString(``) + return builder.String() +} + +func getPresentationXml(slideFileNames []string) string { + var builder strings.Builder + builder.WriteString(``) + + builder.WriteString("") + for i, name := range slideFileNames { + builder.WriteString(fmt.Sprintf(``, i, name)) + } + builder.WriteString("") + + builder.WriteString(fmt.Sprintf( + ``, + SLIDE_WIDTH, + SLIDE_HEIGHT, + )) + return builder.String() +} diff --git a/lib/ppt/presentation.go b/lib/ppt/presentation.go new file mode 100644 index 000000000..256bcafa7 --- /dev/null +++ b/lib/ppt/presentation.go @@ -0,0 +1,100 @@ +package ppt + +import ( + "archive/zip" + "bytes" + _ "embed" + "fmt" + "image/png" + "os" +) + +type Pptx struct { + Slides []*Slide +} + +type Slide struct { + Image []byte + Width int + Height int +} + +func NewPresentation() *Pptx { + return &Pptx{} +} + +func (p *Pptx) AddSlide(pngContent []byte) error { + src, err := png.Decode(bytes.NewReader(pngContent)) + if err != nil { + return fmt.Errorf("error decoding PNG image: %v", err) + } + + srcSize := src.Bounds().Size() + height := int(float64(SLIDE_WIDTH) * (float64(srcSize.X) / float64(srcSize.Y))) + + p.Slides = append(p.Slides, &Slide{ + Image: pngContent, + Width: SLIDE_WIDTH, + Height: height, + }) + + return nil +} + +func (p *Pptx) SaveTo(filePath string) error { + // TODO: update core files with metadata + + f, err := os.Create(filePath) + if err != nil { + return err + } + defer f.Close() + zipFile := zip.NewWriter(f) + defer zipFile.Close() + + copyPptxTemplateTo(zipFile) + + 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 := zipFile.Create(fmt.Sprintf("ppt/media/%s.png", imageId)) + if err != nil { + return err + } + _, err = imageWriter.Write(slide.Image) + if err != nil { + return err + } + + err = addFile(zipFile, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), getRelsSlideXml(imageId)) + if err != nil { + return err + } + + // TODO: center the image? + err = addFile(zipFile, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), getSlideXml(imageId, imageId, 0, 0, slide.Width, slide.Height)) + if err != nil { + return err + } + } + + err = addFile(zipFile, "[Content_Types].xml", getContentTypesXml(slideFileNames)) + if err != nil { + return err + } + + err = addFile(zipFile, "ppt/_rels/presentation.xml.rels", getPresentationXmlRels(slideFileNames)) + if err != nil { + return err + } + + err = addFile(zipFile, "ppt/presentation.xml", getPresentationXml(slideFileNames)) + if err != nil { + return err + } + + return nil +} diff --git a/lib/ppt/template.pptx b/lib/ppt/template.pptx new file mode 100644 index 0000000000000000000000000000000000000000..cdb16e97f7e01114a3c2f95379a99ae98530cd92 GIT binary patch literal 29492 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 WE`;cvZV-qM{eK6o`*Pi4?bm+^y}Nn< literal 0 HcmV?d00001 From d4feac75b48289af1ecb9aa29b274ca02d2efa81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 4 Apr 2023 16:44:57 -0300 Subject: [PATCH 02/20] add pptx export --- d2cli/main.go | 95 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 400d81aa5..21d084ce1 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -34,6 +34,7 @@ import ( "oss.terrastruct.com/d2/lib/pdf" pdflib "oss.terrastruct.com/d2/lib/pdf" "oss.terrastruct.com/d2/lib/png" + "oss.terrastruct.com/d2/lib/ppt" "oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/d2/lib/version" @@ -234,7 +235,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, plocation) var pw png.Playwright - if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" { + if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" || filepath.Ext(outputPath) == ".pptx" { if darkThemeFlag != nil { ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg") darkThemeFlag = nil @@ -347,7 +348,8 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende return nil, false, err } - if filepath.Ext(outputPath) == ".pdf" { + switch filepath.Ext(outputPath) { + case ".pdf": pageMap := pdf.BuildPDFPageMap(diagram, nil, nil) pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap) if err != nil { @@ -356,7 +358,20 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende dur := time.Since(start) ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur) return pdf, true, nil - } else { + case ".pptx": + p := ppt.NewPresentation() + err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil) + if err != nil { + return nil, false, err + } + err = p.SaveTo(outputPath) + if err != nil { + return nil, false, err + } + dur := time.Since(start) + ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur) + return nil, true, nil + default: compileDur := time.Since(start) if animateInterval <= 0 { // Rename all the "root.layers.x" to the paths that the boards get output to @@ -735,6 +750,80 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt return svg, nil } +func renderPPTX(ctx context.Context, ms *xmain.State, presentation *ppt.Pptx, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []string) error { + var currBoardPath []string + // Root board doesn't have a name, so we use the output filename + if diagram.Name == "" { + ext := filepath.Ext(outputPath) + trimmedPath := strings.TrimSuffix(outputPath, ext) + splitPath := strings.Split(trimmedPath, "/") + rootName := splitPath[len(splitPath)-1] + currBoardPath = append(boardPath, rootName) + } else { + currBoardPath = append(boardPath, diagram.Name) + } + + if !diagram.IsFolderOnly { + // gofpdf will print the png img with a slight filter + // make the bg fill within the png transparent so that the pdf bg fill is the only bg color present + diagram.Root.Fill = "transparent" + + svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{ + Pad: opts.Pad, + Sketch: opts.Sketch, + Center: opts.Center, + SetDimensions: true, + }) + if err != nil { + return err + } + + svg, err = plugin.PostProcess(ctx, svg) + if err != nil { + return err + } + + svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg) + svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg) + bundleErr = multierr.Combine(bundleErr, bundleErr2) + if bundleErr != nil { + return bundleErr + } + + pngImg, err := png.ConvertSVG(ms, page, svg) + if err != nil { + return err + } + + err = presentation.AddSlide(pngImg) + if err != nil { + return err + } + } + + for _, dl := range diagram.Layers { + err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) + if err != nil { + return err + } + } + for _, dl := range diagram.Scenarios { + err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) + if err != nil { + return err + } + } + for _, dl := range diagram.Steps { + err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) + if err != nil { + return err + } + } + + // TODO: return SVG? + return nil +} + // newExt must include leading . func renameExt(fp string, newExt string) string { ext := filepath.Ext(fp) From 3a5c389d2b16d76b237bd2abb9037eb85e1c0449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 4 Apr 2023 18:00:45 -0300 Subject: [PATCH 03/20] fix size and position --- d2cli/main.go | 2 +- lib/ppt/presentation.go | 38 ++++++++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 21d084ce1..91a1e3181 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -750,7 +750,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 *ppt.Pptx, 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 *ppt.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []string) error { var currBoardPath []string // Root board doesn't have a name, so we use the output filename if diagram.Name == "" { diff --git a/lib/ppt/presentation.go b/lib/ppt/presentation.go index 256bcafa7..e1a9dcd86 100644 --- a/lib/ppt/presentation.go +++ b/lib/ppt/presentation.go @@ -9,7 +9,10 @@ import ( "os" ) -type Pptx struct { +// TODO: comments / references / assumptions +// TODO: update core files with metadata + +type Presentation struct { Slides []*Slide } @@ -17,33 +20,48 @@ type Slide struct { Image []byte Width int Height int + Top int + Left int } -func NewPresentation() *Pptx { - return &Pptx{} +func NewPresentation() *Presentation { + return &Presentation{} } -func (p *Pptx) AddSlide(pngContent []byte) error { +func (p *Presentation) AddSlide(pngContent []byte) error { src, err := png.Decode(bytes.NewReader(pngContent)) if err != nil { return fmt.Errorf("error decoding PNG image: %v", err) } + var width, height, top, left int srcSize := src.Bounds().Size() - height := int(float64(SLIDE_WIDTH) * (float64(srcSize.X) / float64(srcSize.Y))) + + // compute the size and position to fit the slide + if srcSize.X > srcSize.Y { + width = SLIDE_WIDTH + height = int(float64(width) * (float64(srcSize.X) / float64(srcSize.Y))) + left = 0 + top = (SLIDE_HEIGHT - height) / 2 + } else { + height = SLIDE_HEIGHT + width = int(float64(height) * (float64(srcSize.X) / float64(srcSize.Y))) + top = 0 + left = (SLIDE_WIDTH - width) / 2 + } p.Slides = append(p.Slides, &Slide{ Image: pngContent, - Width: SLIDE_WIDTH, + Width: width, Height: height, + Top: top, + Left: left, }) return nil } -func (p *Pptx) SaveTo(filePath string) error { - // TODO: update core files with metadata - +func (p *Presentation) SaveTo(filePath string) error { f, err := os.Create(filePath) if err != nil { return err @@ -75,7 +93,7 @@ func (p *Pptx) SaveTo(filePath string) error { } // TODO: center the image? - err = addFile(zipFile, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), getSlideXml(imageId, imageId, 0, 0, slide.Width, slide.Height)) + err = addFile(zipFile, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), getSlideXml(imageId, imageId, slide.Top, slide.Left, slide.Width, slide.Height)) if err != nil { return err } From 0f362c302463b803a1f79082caea8c322bd0ec89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 4 Apr 2023 18:47:22 -0300 Subject: [PATCH 04/20] slide title --- d2cli/main.go | 2 +- lib/ppt/pptx.go | 11 ++++++++--- lib/ppt/presentation.go | 38 ++++++++++++++++++++++---------------- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 91a1e3181..4e377bdef 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -795,7 +795,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *ppt.Presenta return err } - err = presentation.AddSlide(pngImg) + err = presentation.AddSlide(strings.Join(boardPath, " / "), pngImg) if err != nil { return err } diff --git a/lib/ppt/pptx.go b/lib/ppt/pptx.go index 04f30b03e..f6d13b403 100644 --- a/lib/ppt/pptx.go +++ b/lib/ppt/pptx.go @@ -50,6 +50,10 @@ func addFile(zipFile *zip.Writer, filePath, content string) error { // https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/ const SLIDE_WIDTH = 9144000 const SLIDE_HEIGHT = 5143500 +const HEADER_HEIGHT = 392471 + +const IMAGE_WIDTH = SLIDE_WIDTH +const IMAGE_HEIGHT = SLIDE_HEIGHT - HEADER_HEIGHT const RELS_SLIDE_XML = `` @@ -57,10 +61,11 @@ func getRelsSlideXml(imageId string) string { return fmt.Sprintf(RELS_SLIDE_XML, imageId, imageId) } -const SLIDE_XML = `` +const SLIDE_XML = `%s` -func getSlideXml(imageId, imageName string, top, left, width, height int) string { - return fmt.Sprintf(SLIDE_XML, imageName, imageName, imageId, left, top, width, height) +func getSlideXml(slideTitle, imageId string, top, left, width, height int) string { + top += HEADER_HEIGHT + return fmt.Sprintf(SLIDE_XML, slideTitle, slideTitle, imageId, left, top, width, height, slideTitle, HEADER_HEIGHT, slideTitle) } func getPresentationXmlRels(slideFileNames []string) string { diff --git a/lib/ppt/presentation.go b/lib/ppt/presentation.go index e1a9dcd86..aee9e7b63 100644 --- a/lib/ppt/presentation.go +++ b/lib/ppt/presentation.go @@ -17,18 +17,19 @@ type Presentation struct { } type Slide struct { - Image []byte - Width int - Height int - Top int - Left int + Title string + Image []byte + ImageWidth int + ImageHeight int + ImageTop int + ImageLeft int } func NewPresentation() *Presentation { return &Presentation{} } -func (p *Presentation) AddSlide(pngContent []byte) error { +func (p *Presentation) AddSlide(title string, pngContent []byte) error { src, err := png.Decode(bytes.NewReader(pngContent)) if err != nil { return fmt.Errorf("error decoding PNG image: %v", err) @@ -39,23 +40,24 @@ func (p *Presentation) AddSlide(pngContent []byte) error { // compute the size and position to fit the slide if srcSize.X > srcSize.Y { - width = SLIDE_WIDTH + width = IMAGE_WIDTH height = int(float64(width) * (float64(srcSize.X) / float64(srcSize.Y))) left = 0 - top = (SLIDE_HEIGHT - height) / 2 + top = (IMAGE_HEIGHT - height) / 2 } else { - height = SLIDE_HEIGHT + height = IMAGE_HEIGHT width = int(float64(height) * (float64(srcSize.X) / float64(srcSize.Y))) top = 0 - left = (SLIDE_WIDTH - width) / 2 + left = (IMAGE_WIDTH - width) / 2 } p.Slides = append(p.Slides, &Slide{ - Image: pngContent, - Width: width, - Height: height, - Top: top, - Left: left, + Title: title, + Image: pngContent, + ImageWidth: width, + ImageHeight: height, + ImageTop: top, + ImageLeft: left, }) return nil @@ -93,7 +95,11 @@ func (p *Presentation) SaveTo(filePath string) error { } // TODO: center the image? - err = addFile(zipFile, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), getSlideXml(imageId, imageId, slide.Top, slide.Left, slide.Width, slide.Height)) + err = addFile( + zipFile, + fmt.Sprintf("ppt/slides/%s.xml", slideFileName), + getSlideXml(slide.Title, imageId, slide.ImageTop, slide.ImageLeft, slide.ImageWidth, slide.ImageHeight), + ) if err != nil { return err } From 2976c9b0a2a7a0fc342ecd0cb4499f64dd52bf12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 4 Apr 2023 19:26:50 -0300 Subject: [PATCH 05/20] todos --- lib/ppt/presentation.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/ppt/presentation.go b/lib/ppt/presentation.go index aee9e7b63..2f6cde1a1 100644 --- a/lib/ppt/presentation.go +++ b/lib/ppt/presentation.go @@ -11,6 +11,10 @@ import ( // TODO: comments / references / assumptions // TODO: update core files with metadata +// TODO: first slide title +// TODO: steps number title +// TODO: links? +// TODO: appendix? type Presentation struct { Slides []*Slide @@ -94,7 +98,6 @@ func (p *Presentation) SaveTo(filePath string) error { return err } - // TODO: center the image? err = addFile( zipFile, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), From ace4715f043eb188cc846510d820fe2a74b481a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Wed, 5 Apr 2023 11:40:21 -0300 Subject: [PATCH 06/20] update title --- d2cli/main.go | 2 +- lib/ppt/pptx.go | 16 +++++++++++++--- lib/ppt/presentation.go | 15 +++++++++------ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 4e377bdef..47e6744f5 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -795,7 +795,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *ppt.Presenta return err } - err = presentation.AddSlide(strings.Join(boardPath, " / "), pngImg) + err = presentation.AddSlide(pngImg, currBoardPath) if err != nil { return err } diff --git a/lib/ppt/pptx.go b/lib/ppt/pptx.go index f6d13b403..1de5d589e 100644 --- a/lib/ppt/pptx.go +++ b/lib/ppt/pptx.go @@ -61,11 +61,21 @@ func getRelsSlideXml(imageId string) string { return fmt.Sprintf(RELS_SLIDE_XML, imageId, imageId) } -const SLIDE_XML = `%s` +const SLIDE_XML = `%s` -func getSlideXml(slideTitle, imageId string, top, left, width, height int) string { +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] + if len(prefixPath) > 0 { + prefix := strings.Join(prefixPath, " / ") + " / " + slideTitle = fmt.Sprintf(`%s%s`, prefix, boardName) + } else { + slideTitle = fmt.Sprintf(`%s`, boardName) + } + slideDescription := strings.Join(boardPath, " / ") top += HEADER_HEIGHT - return fmt.Sprintf(SLIDE_XML, slideTitle, slideTitle, imageId, left, top, width, height, slideTitle, HEADER_HEIGHT, slideTitle) + return fmt.Sprintf(SLIDE_XML, slideDescription, slideDescription, imageId, left, top, width, height, slideDescription, HEADER_HEIGHT, slideTitle) } func getPresentationXmlRels(slideFileNames []string) string { diff --git a/lib/ppt/presentation.go b/lib/ppt/presentation.go index 2f6cde1a1..5003df26d 100644 --- a/lib/ppt/presentation.go +++ b/lib/ppt/presentation.go @@ -21,7 +21,7 @@ type Presentation struct { } type Slide struct { - Title string + BoardPath []string Image []byte ImageWidth int ImageHeight int @@ -33,7 +33,7 @@ func NewPresentation() *Presentation { return &Presentation{} } -func (p *Presentation) AddSlide(title string, pngContent []byte) error { +func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { src, err := png.Decode(bytes.NewReader(pngContent)) if err != nil { return fmt.Errorf("error decoding PNG image: %v", err) @@ -55,15 +55,18 @@ func (p *Presentation) AddSlide(title string, pngContent []byte) error { left = (IMAGE_WIDTH - width) / 2 } - p.Slides = append(p.Slides, &Slide{ - Title: title, + slide := &Slide{ + BoardPath: make([]string, len(boardPath)), Image: pngContent, ImageWidth: width, ImageHeight: height, ImageTop: top, ImageLeft: left, - }) + } + // it must copy the board path to avoid slice reference issues + copy(slide.BoardPath, boardPath) + p.Slides = append(p.Slides, slide) return nil } @@ -101,7 +104,7 @@ func (p *Presentation) SaveTo(filePath string) error { err = addFile( zipFile, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), - getSlideXml(slide.Title, imageId, slide.ImageTop, slide.ImageLeft, slide.ImageWidth, slide.ImageHeight), + getSlideXml(slide.BoardPath, imageId, slide.ImageTop, slide.ImageLeft, slide.ImageWidth, slide.ImageHeight), ) if err != nil { return err From f0adecf1dc952f54669b9f3abda5268f78c6be78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Wed, 5 Apr 2023 15:22:12 -0300 Subject: [PATCH 07/20] metadata --- d2cli/main.go | 11 ++++++- lib/ppt/pptx.go | 68 ++++++++++++++++++++++++++++++++++++++++ lib/ppt/presentation.go | 28 ++++++++++++++--- lib/ppt/template.pptx | Bin 29492 -> 28298 bytes 4 files changed, 102 insertions(+), 5 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 47e6744f5..9f5f7484f 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -7,6 +7,7 @@ import ( "io" "os" "os/exec" + "os/user" "path/filepath" "strconv" "strings" @@ -359,7 +360,15 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur) return pdf, true, nil case ".pptx": - p := ppt.NewPresentation() + ext := filepath.Ext(outputPath) + trimmedPath := strings.TrimSuffix(outputPath, ext) + splitPath := strings.Split(trimmedPath, "/") + rootName := splitPath[len(splitPath)-1] + var username string + if user, err := user.Current(); err != nil { + username = user.Username + } + p := ppt.NewPresentation(rootName, rootName, rootName, username, version.Version) err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil) if err != nil { return nil, false, err diff --git a/lib/ppt/pptx.go b/lib/ppt/pptx.go index 1de5d589e..34fbe6ab4 100644 --- a/lib/ppt/pptx.go +++ b/lib/ppt/pptx.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "strings" + "time" ) // Office Open XML (OOXML) http://officeopenxml.com/prPresentation.php @@ -124,3 +125,70 @@ func getPresentationXml(slideFileNames []string) string { )) return builder.String() } + +func getCoreXml(title, subject, description, creator string) string { + var builder strings.Builder + + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(fmt.Sprintf(`%s`, title)) + builder.WriteString(fmt.Sprintf(`%s`, subject)) + builder.WriteString(fmt.Sprintf(`%s`, creator)) + builder.WriteString(``) + builder.WriteString(fmt.Sprintf(`%s`, description)) + builder.WriteString(fmt.Sprintf(`%s`, creator)) + builder.WriteString(`1`) + dateTime := time.Now().Format("RFC3339") + builder.WriteString(fmt.Sprintf(`%s`, dateTime)) + builder.WriteString(fmt.Sprintf(`%s`, dateTime)) + builder.WriteString(``) + builder.WriteString(``) + + return builder.String() +} + +func getAppXml(nSlides int, d2version string) string { + var builder strings.Builder + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`1`) + builder.WriteString(`0`) + builder.WriteString(`D2`) + builder.WriteString(`On-screen Show (4:3)`) + builder.WriteString(`0`) + builder.WriteString(fmt.Sprintf(`%d`, nSlides)) + builder.WriteString(`0`) + builder.WriteString(`0`) + builder.WriteString(`0`) + builder.WriteString(`false`) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`Theme`) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`1`) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`Slide Titles`) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`0`) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`Office Theme`) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`false`) + builder.WriteString(`false`) + builder.WriteString(``) + builder.WriteString(`false`) + builder.WriteString(fmt.Sprintf(`%s`, d2version)) + builder.WriteString(``) + return builder.String() +} diff --git a/lib/ppt/presentation.go b/lib/ppt/presentation.go index 5003df26d..81775135c 100644 --- a/lib/ppt/presentation.go +++ b/lib/ppt/presentation.go @@ -11,12 +11,16 @@ import ( // TODO: comments / references / assumptions // TODO: update core files with metadata -// TODO: first slide title -// TODO: steps number title // TODO: links? // TODO: appendix? type Presentation struct { + Title string + Description string + Subject string + Creator string + D2Version string + Slides []*Slide } @@ -29,8 +33,14 @@ type Slide struct { ImageLeft int } -func NewPresentation() *Presentation { - return &Presentation{} +func NewPresentation(title, description, subject, creator, d2Version string) *Presentation { + return &Presentation{ + Title: title, + Description: description, + Subject: subject, + Creator: creator, + D2Version: d2Version, + } } func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { @@ -126,5 +136,15 @@ func (p *Presentation) SaveTo(filePath string) error { return err } + err = addFile(zipFile, "docProps/core.xml", getCoreXml(p.Title, p.Subject, p.Description, p.Creator)) + if err != nil { + return err + } + + err = addFile(zipFile, "docProps/app.xml", getAppXml(len(p.Slides), p.D2Version)) + if err != nil { + return err + } + return nil } diff --git a/lib/ppt/template.pptx b/lib/ppt/template.pptx index cdb16e97f7e01114a3c2f95379a99ae98530cd92..783c5dea0ab7157b440e4bd6d37c0c60893bf08e 100644 GIT binary patch delta 678 zcmYk3Ur1A77{+<^UH-hd+qJhU)RKS|euwxYM z^*ds#u}T=lim>yg5=i1_h*EbIx4|3}kaSe_%f$qfkz?|z94@V)4_#`mbka6>+)@_B zmh;E-$*!U8Iz36}xRYr~@Z_RHi(xifxys4SKIQRyyh{rt(~1H2Ci>}*0ly{!^xla6 zaDW{#amcibSPiS}pSjF(L}i8s4u`FFB0Ux6Z z-M45mP0y{mYZL_}H20fLmq(|(w5t(HycLU6US7t+WGnW>2FWAp@41+lE;Q-5)GS~- zrqE?cD^IhVj`B|Hag~j4-6Jv!c*QQ3-z!;3x{uj7Z{Ov?1P7yDB6h$cOBs!zw%zFE*t8G zEVz|Y=!_qqQhvJOM`t=fU;N0YeXQ*?hm&V5_?cFyFbsFbNAHF)mkCfTh_{&$8V$jn e_0jDR=CVQh9>Rz0fVqR~;I{U1T>f0?-~I!kw&AA$ delta 1774 zcmY+Ec~DbV6vkiP1H*3EA%+A<*hE$rR00I61_S~KVU<7#OCkgjw+1RHjOJShr-xGE5YfpAd5E>=*+uJk=l%=C1?)G+6t2)$bTz!}p{e7(Fp zFr5uH34)#`sm7ekJh^6j(;Z%;O_6{_ZF!M4muaouB@D9grb&Cq?WT7nx^96Nm^QQC z?w6hw@VZT-Ui{Q@YwZ=MS4q+hn!5Gy_^R`BrpT2p#$`;~0qyzIF3zGxX3trZ zNTxQE#__ATKZoU82J`)QMDEOMrDZqV>zoLvyzDn)_Y3`WQV;uf)~!CfrG~l;(Xzh17Q7Fj+L#XUztCtE(r<-RTFS?4%e7L`A8r~Tu&JXG8 zcZK?mcTk?X?;qIr^Yrv8pAz=N+KiH^zKnP8FZi5S+oFT#7vn3>q$FOuag{BInKjm{ ziJ3w?Fwy4>29t+XjV}#%P73|Bf^a00A@R2&J%5S_kHi;<&wHY$eR=t3^}8p@n)7G} ze%HfrHHpr?p1{$ylG|C4fg4jRF$crM*KTJ`m{oNPBb*+!&yF@+8#(K4X1{lh$x`j+ z%=XQ$cre3%3eD>+l!fUv{KaxPK^;J9_O?kKtuJ}z){g8l+#ffV%Z=Z>BHS(Rt^xL* z4fl#*L73i(*)4gJ7=&9nb9yqeFj_W|pLW(a^Ci6pvnKsK?>E+U;Dfb|_|}8yx8lzp zqMI4j_Xj)*E&4}3j727Up@RGn6$1!@)F5`%K_EuCAUb6GCKb3<)#g7ttiWJ#oF~i8 zBN&2ozs?P7S4QHO;1reh(5^dU4ok^fdi`j1<3`)^%FIzuRP?qNP=*TI1GMwF4<8A6 zSyiV3=3eAPhpk@VkdM!1h9A9ZR1cyOEzE8)q5KpJda2;NeETEW&8jYid=Y$`RGfSY zPB>&ibZ~Y>b~jTsJk5JL%gre1uvO?uvF=9U#N?L7qwZ~DyT?^WZ*+F}zVyZox!a&w zF$3e7gOLZbw=oC=qu~%@-Qbz^g>OPR5BJf2HfP19o#yxxQ9=K29!gEi+H6{4S?aFj{WccWR*$uVQ4xK}ww?2_M)B?@eTw##+siYnxiv4VQc( zHV&dcdcC%6JH{vCp_uGfV}h05X2l7$ZXKIgh@QGTm9mLMoz0~}^x;Y9?T zWVp$ty>d>!?l7bNRR=|CIksS!frpzl!6$|f{6-T{W9hI&8&t>A5uy$Rt~!{3I1Mzw z#!-+DdJ3HxM@3XjAmEKN0Yh<{V7dt~XQm?=rgDo2TA2Pw30A?j@%~8t3I$7zwLm`0 z6=0Y;NEc4wX|eoK&%o~qRv??@k01oOO9HQDY+){EXONyiLA)#!)%^+n$PP;d*Wmx} z#v>~BwG(8=U|EByL@ZKHQVeUIL`6dF6kH@@oP&aAWnAkbXA(%};J*CG++-~Bc%9<= zwPY%?$z8#GH#Bf&Q;-Hvh5nK4gE|JhIRx;L?SnLLRk(2+D*VnD)N$xAWn0L0tD3F^ Date: Wed, 5 Apr 2023 15:34:50 -0300 Subject: [PATCH 08/20] rename and docs --- d2cli/main.go | 6 +++--- lib/{ppt => pptx}/pptx.go | 2 +- lib/{ppt => pptx}/presentation.go | 16 ++++++++++------ lib/{ppt => pptx}/template.pptx | Bin 4 files changed, 14 insertions(+), 10 deletions(-) rename lib/{ppt => pptx}/pptx.go (99%) rename lib/{ppt => pptx}/presentation.go (77%) rename lib/{ppt => pptx}/template.pptx (100%) diff --git a/d2cli/main.go b/d2cli/main.go index 9f5f7484f..dc9f0e229 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -35,7 +35,7 @@ import ( "oss.terrastruct.com/d2/lib/pdf" pdflib "oss.terrastruct.com/d2/lib/pdf" "oss.terrastruct.com/d2/lib/png" - "oss.terrastruct.com/d2/lib/ppt" + "oss.terrastruct.com/d2/lib/pptx" "oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/d2/lib/version" @@ -368,7 +368,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende if user, err := user.Current(); err != nil { username = user.Username } - p := ppt.NewPresentation(rootName, rootName, rootName, username, version.Version) + p := pptx.NewPresentation(rootName, rootName, rootName, username, version.Version) err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil) if err != nil { return nil, false, err @@ -759,7 +759,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 *ppt.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, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []string) error { var currBoardPath []string // Root board doesn't have a name, so we use the output filename if diagram.Name == "" { diff --git a/lib/ppt/pptx.go b/lib/pptx/pptx.go similarity index 99% rename from lib/ppt/pptx.go rename to lib/pptx/pptx.go index 34fbe6ab4..825c34da1 100644 --- a/lib/ppt/pptx.go +++ b/lib/pptx/pptx.go @@ -1,4 +1,4 @@ -package ppt +package pptx import ( "archive/zip" diff --git a/lib/ppt/presentation.go b/lib/pptx/presentation.go similarity index 77% rename from lib/ppt/presentation.go rename to lib/pptx/presentation.go index 81775135c..63cf860ce 100644 --- a/lib/ppt/presentation.go +++ b/lib/pptx/presentation.go @@ -1,4 +1,13 @@ -package ppt +// pptx is a package to create slide presentations in pptx (Microsoft Power Point) format. +// A `.pptx` file is just a bunch of zip compressed `.xml` files following the Office Open XML (OOXML) format. +// To see its content, you can just `unzip .pptx -d `. +// With this package, it is possible to create a `Presentation` and add `Slide`s to it. +// Then, when saving the presentation, it will generate the required `.xml` files, compress them and write to the disk. +// Note that this isn't a full implementation of the OOXML format, but a wrapper around it. +// There's a base template with common files to the presentation and then when saving, the package generate only the slides and relationships. +// The base template and slide templates were generated using https://python-pptx.readthedocs.io/en/latest/ +// For more information about OOXML, check http://officeopenxml.com/index.php +package pptx import ( "archive/zip" @@ -9,11 +18,6 @@ import ( "os" ) -// TODO: comments / references / assumptions -// TODO: update core files with metadata -// TODO: links? -// TODO: appendix? - type Presentation struct { Title string Description string diff --git a/lib/ppt/template.pptx b/lib/pptx/template.pptx similarity index 100% rename from lib/ppt/template.pptx rename to lib/pptx/template.pptx From 356ab9394162a23cfe83884b4e4325e2c5b399e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Wed, 5 Apr 2023 16:09:33 -0300 Subject: [PATCH 09/20] fix image size --- lib/pptx/pptx.go | 4 +++- lib/pptx/presentation.go | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index 825c34da1..c23493631 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -53,9 +53,11 @@ const SLIDE_WIDTH = 9144000 const SLIDE_HEIGHT = 5143500 const HEADER_HEIGHT = 392471 -const IMAGE_WIDTH = SLIDE_WIDTH const IMAGE_HEIGHT = SLIDE_HEIGHT - HEADER_HEIGHT +// keep the right aspect ratio: SLIDE_WIDTH / SLIDE_HEIGHT = IMAGE_WIDTH / IMAGE_HEIGHT +const IMAGE_WIDTH = IMAGE_HEIGHT * (SLIDE_WIDTH / SLIDE_HEIGHT) + const RELS_SLIDE_XML = `` func getRelsSlideXml(imageId string) string { diff --git a/lib/pptx/presentation.go b/lib/pptx/presentation.go index 63cf860ce..0f4943b3c 100644 --- a/lib/pptx/presentation.go +++ b/lib/pptx/presentation.go @@ -53,21 +53,19 @@ func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { return fmt.Errorf("error decoding PNG image: %v", err) } - var width, height, top, left int + var width, height int srcSize := src.Bounds().Size() // compute the size and position to fit the slide if srcSize.X > srcSize.Y { width = IMAGE_WIDTH - height = int(float64(width) * (float64(srcSize.X) / float64(srcSize.Y))) - left = 0 - top = (IMAGE_HEIGHT - height) / 2 + height = int(float64(width) * (float64(srcSize.Y) / float64(srcSize.X))) } else { height = IMAGE_HEIGHT width = int(float64(height) * (float64(srcSize.X) / float64(srcSize.Y))) - top = 0 - left = (IMAGE_WIDTH - width) / 2 } + top := (IMAGE_HEIGHT - height) / 2 + left := (SLIDE_WIDTH - width) / 2 slide := &Slide{ BoardPath: make([]string, len(boardPath)), From e4bbc269596f11e2c527dfd5ee7e270d0219b1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Thu, 6 Apr 2023 11:01:44 -0300 Subject: [PATCH 10/20] minor fixes for MS PowerPoint --- d2cli/main.go | 5 +++-- lib/pptx/pptx.go | 40 +++++++++++++++++++++++++-------------- lib/pptx/presentation.go | 2 +- lib/pptx/template.pptx | Bin 28298 -> 26731 bytes lib/version/version.go | 10 ++++++++++ 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index dc9f0e229..015645578 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -365,10 +365,11 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende splitPath := strings.Split(trimmedPath, "/") rootName := splitPath[len(splitPath)-1] var username string - if user, err := user.Current(); err != nil { + if user, err := user.Current(); err == nil { username = user.Username } - p := pptx.NewPresentation(rootName, rootName, rootName, username, version.Version) + description := "Presentation auto-generated by D2 - https://d2lang.com/" + p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers()) err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil) if err != nil { return nil, false, err diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index c23493631..b37503113 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -58,13 +58,13 @@ const IMAGE_HEIGHT = SLIDE_HEIGHT - HEADER_HEIGHT // keep the right aspect ratio: SLIDE_WIDTH / SLIDE_HEIGHT = IMAGE_WIDTH / IMAGE_HEIGHT const IMAGE_WIDTH = IMAGE_HEIGHT * (SLIDE_WIDTH / SLIDE_HEIGHT) -const RELS_SLIDE_XML = `` +const RELS_SLIDE_XML = `` func getRelsSlideXml(imageId string) string { return fmt.Sprintf(RELS_SLIDE_XML, imageId, imageId) } -const SLIDE_XML = `%s` +const SLIDE_XML = `%s` func getSlideXml(boardPath []string, imageId string, top, left, width, height int) string { var slideTitle string @@ -83,7 +83,7 @@ func getSlideXml(boardPath []string, imageId string, top, left, width, height in func getPresentationXmlRels(slideFileNames []string) string { var builder strings.Builder - builder.WriteString(``) + builder.WriteString(``) for _, name := range slideFileNames { builder.WriteString(fmt.Sprintf( @@ -98,7 +98,7 @@ func getPresentationXmlRels(slideFileNames []string) string { func getContentTypesXml(slideFileNames []string) string { var builder strings.Builder - builder.WriteString(``) + builder.WriteString(``) for _, name := range slideFileNames { builder.WriteString(fmt.Sprintf( @@ -112,16 +112,16 @@ func getContentTypesXml(slideFileNames []string) string { func getPresentationXml(slideFileNames []string) string { var builder strings.Builder - builder.WriteString(``) + builder.WriteString(``) builder.WriteString("") for i, name := range slideFileNames { - builder.WriteString(fmt.Sprintf(``, i, name)) + builder.WriteString(fmt.Sprintf(``, 256+i, name)) } builder.WriteString("") builder.WriteString(fmt.Sprintf( - ``, + ``, SLIDE_WIDTH, SLIDE_HEIGHT, )) @@ -131,7 +131,7 @@ func getPresentationXml(slideFileNames []string) string { func getCoreXml(title, subject, description, creator string) string { var builder strings.Builder - builder.WriteString(``) + builder.WriteString(``) builder.WriteString(``) builder.WriteString(fmt.Sprintf(`%s`, title)) builder.WriteString(fmt.Sprintf(`%s`, subject)) @@ -140,7 +140,7 @@ func getCoreXml(title, subject, description, creator string) string { builder.WriteString(fmt.Sprintf(`%s`, description)) builder.WriteString(fmt.Sprintf(`%s`, creator)) builder.WriteString(`1`) - dateTime := time.Now().Format("RFC3339") + dateTime := time.Now().Format(time.RFC3339) builder.WriteString(fmt.Sprintf(`%s`, dateTime)) builder.WriteString(fmt.Sprintf(`%s`, dateTime)) builder.WriteString(``) @@ -149,7 +149,7 @@ func getCoreXml(title, subject, description, creator string) string { return builder.String() } -func getAppXml(nSlides int, d2version string) string { +func getAppXml(slides []*Slide, d2version string) string { var builder strings.Builder builder.WriteString(``) builder.WriteString(``) @@ -158,13 +158,19 @@ func getAppXml(nSlides int, d2version string) string { builder.WriteString(`D2`) builder.WriteString(`On-screen Show (4:3)`) builder.WriteString(`0`) - builder.WriteString(fmt.Sprintf(`%d`, nSlides)) + builder.WriteString(fmt.Sprintf(`%d`, len(slides))) builder.WriteString(`0`) builder.WriteString(`0`) builder.WriteString(`0`) builder.WriteString(`false`) builder.WriteString(``) - builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`Fonts`) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`2`) + builder.WriteString(``) builder.WriteString(``) builder.WriteString(`Theme`) builder.WriteString(``) @@ -175,13 +181,19 @@ func getAppXml(nSlides int, d2version string) string { builder.WriteString(`Slide Titles`) builder.WriteString(``) builder.WriteString(``) - builder.WriteString(`0`) + builder.WriteString(fmt.Sprintf(`%d`, len(slides))) builder.WriteString(``) builder.WriteString(``) builder.WriteString(``) builder.WriteString(``) - builder.WriteString(``) + // number of entries, len(slides) + Office Theme + 2 Fonts + builder.WriteString(fmt.Sprintf(``, len(slides)+3)) + builder.WriteString(`Arial`) + builder.WriteString(`Calibri`) builder.WriteString(`Office Theme`) + for _, slide := range slides { + builder.WriteString(fmt.Sprintf(`%s`, strings.Join(slide.BoardPath, "/"))) + } builder.WriteString(``) builder.WriteString(``) builder.WriteString(``) diff --git a/lib/pptx/presentation.go b/lib/pptx/presentation.go index 0f4943b3c..1ec4286ab 100644 --- a/lib/pptx/presentation.go +++ b/lib/pptx/presentation.go @@ -143,7 +143,7 @@ func (p *Presentation) SaveTo(filePath string) error { return err } - err = addFile(zipFile, "docProps/app.xml", getAppXml(len(p.Slides), p.D2Version)) + err = addFile(zipFile, "docProps/app.xml", getAppXml(p.Slides, p.D2Version)) if err != nil { return err } diff --git a/lib/pptx/template.pptx b/lib/pptx/template.pptx index 783c5dea0ab7157b440e4bd6d37c0c60893bf08e..f87fcd9e2e5c66f49ea051c9e21444cd77454715 100644 GIT binary patch literal 26731 zcmeFZb981~wmuxEV%rtlwr$(0*tT7slNJ`F^83x-e}MsD1L)ctTRPAxD?tDN3ocL_scupm zIk`ds0D{~B0sw#@fBvQ1=kvdPe)+rz0L&g8b6-F2c7Ol?p#J-Ln%}JaZiH@n67jd| zPev-^RwaPx;e#(hHwnhQSXU{&&RW_C9c2zb0~j*2W36M070q^a=5FchQ~P(0kMrVf z5~jvvwr8S8oMNWi{lFVz`YvROmgT6T(+>dlaoN;j}TCZ_2Cn#V2ljHk(A zz%)$eB-RjGDn;64j5cF%5{8nJgD1(3honppkb$O=sTvxR>2Yl~gppXSbB9^_A#L{L zk~{6BGUqkTDVlZIQ50rVjIKT?)QTK)tjOxFCMNivzS&wOu$aGO3j#V zFRXR=;1jw#IXy0u9lAU&o+djTSYr)itMy%lODmDlgZ=D1@$EHMt?JcOmT~3_n^bH8UZ12BOa&B&XuQ0dmr=A4704)THR2?Wm(2T3`55bNb@wNzP&xh{S;K-XtUEbu&@U=w15?2 zQ}k1<{>unYIQa^7H)~QrzF>nnbmw|casOhEEuLT;x zjHspac&kQv56zseGL6hJXJ#-o_PmTYnh}5IfUrOzUAzOeF46zBfdP&}l7=x@xXRgzc9>^~pZHojZN9Q=R*dsR;SCY6Uo zB%E)M+}XH)qQxLOD6MoDmx!xdNkN4^UJ=r0%32?b<}0lnqRK(~tygusn^<%dTNUYEj)Ofe+*876HV-3?? zy9GYr$I-2RZveL* zq77wAcOWc#BwP&$eXsdKilda+8q63>AC`NJX7pwZpN^tY^74MG17~|Y+xC|AwL`rv z_ZFQCL2KFvum_!4o%Ki0{s)~IEJaD~(r?thx~2R|7Qj5qIH_Rx=LpO6$g1Yqg3*|DIr(lqQL@ITYsiUR&riqi*DxQd6>ZU2?A{G## zD^a{)be84xm?oK1t`v#UEdduRFu;m8ZRE7#_>qbDEm7C%eZ9fwDWCHMpStLm!4X;9 z=zqNGUQKeO)Ko$PchCUtjms?ET*+YHedT+=R#mf%-gtuk1?sy;P9^7KoOW%^ffA4TUvgogpJymImjfJ#To#!C#icMnL`bhq6X18b;I3Rh}X|wX7gm&xsDzTI8!TQU9dD3NiX*U*SdTLzZoka?ADhD#vOeNox8?8 zc#bian-14uUs>R+Z?pz`n|Y-{{}@ATi5*)lcvr3Wnv36j9QX)4?@&}~nAKc_#_k(w z)d+=Jvz_D$(XZ$Y`DnpI610T~oW9w52a%YOOa>>74edG8O>R?b`D^YZL<@8E*2=u1 zChKE{HJxP5D<)U3a$@$iwb{0&^V|?TH^(=_7~Wd*Uy8E4zs?+{x~ChRRwZe>>)z%U zH#|?zGcP+xfgsqw{BTYyWTEddg^&Ix^1r)H)-D9 zhq8U-wUtaO3V#L(>!}H_e~a4HpV6ZBuOh~vAnSR~XFv}J1^__#`_sb5M$@GWn-dKhM zfd;q9JWgl5?7Wb<&+Phakc0?@Ac5oeO(CsBgA}6Dl7iZfY`Bk9 zm$5?mtD~Q7Kv26X1M#=Ebr^6T=x+zWbu4cKf{-{K4${gaD?Vz>UHoubQGg(#lA9sO86wcU^ z&3F9lL~od-U2~voBe)n_oT8OOF!!L8xLbk=Q>hE|!b>+`NyJ#FaJvBPq?v(^Qvqvi zHeK0qnKY$I=~YuX^mLRQYi7qQ<`%*EiE4rP;eY)wZWf(aq?${_{wq2G7Ti2r68t!A z_uBi=PYC&O*8HX8c+&o6v6hW8H-|LMqx2}kG(6a5 zPQPBz>R~9l@FmKG_a7VO&jjzkmfFG6+{jo;-_6F!@t?R}(f^+o{E=s9QSS@8fdBw7 zBmTzx{aLA>Qq~`&SyjK(ly@%w3B7Wpqp>V=NtW2D1#-<3Ag1RpR?UC zC?2mJ#Z=ws&>F)aTh|Q(lGdci5^zt#cJN^8^RZoj-Tu(drP!1(Z?3LYa*&!!(GWUD zM6H5$Qv4#;SEk^gKtx0d<(_+CUfI?CdY9}n@0LltXryc10)#H?B}4O*`Eb6qYVkVl zpn!*xQsYOv@rZrzHm&9{1YwMCrfHiodR10@gWT%1<@lq0tCS%J?XI*8)P^rz8UpNT z^`g15i`hJSL--7)-riBM~0cBdK@5`KtI+Z^FgVNUL-S86^u02Vr1!_(ev@7;}M(Q$9q(IsIc1b@dA)v<$j zj-9D<(ps#)rr&SN^uP3HSdQp{y8{i^~8^hKZiWRz=EI?wO~1uCEz_ z0k@2wt^yZ?EvpvQTYm*Vb#i@%JH?8C85I)j>Kqf|&NX*0<-(Lh4zpWnu?&5g$4|LR2XxA zwwl0e7|Q1d-)q=iU$z?0tKA8~bkCNp?V-UT|Yd@l$gG{S}>FpczqN^I{`1IKrv3Vh9k}t+c z0L!>0%oD1%)|}h7PnK-)*Zts#$1c`kDO0>XY}<2$jLCs`pd%QC)sixp?Vbz;(Z{EQ z4x$_md>|$oaIZOH){M;s64RyMZTHYzAiIBnM@cVS>VD9?Ki$8bHbc}Y<9YuKc-30$up-Aw0!qZc z(x&Y9Lro!@onCurP~YRw*jxG%1eg_cKaXa|u9x>KMc(c7%42q2HRWhIN;qqw6KM3C;hARB>8dvtnVRN9Liv1pJ5(wW#tUEBYNb@X zR`@rv_Ob@GM)leBqkz+_F`wV#=s)t;TS^h@hEGfz{0}gV?XNIxLT%lChXuLoQfjkj z*JCJ&`ooxIxrgM!ktE$H=?wUbrf+-GzJxy?Cue>2eCJvIS-eu56@H+vc*_wC>dsuT z!=%e(>`X|^Slu*gN?nGMDo0C=BaABf$jvwRLLWD&NWMjMQRQGrg>6c&pw##4=S5wf zpUD-&)-&Pbtt4y|&S6!z7>9R-=5>z2lKnJdQIyxKfzya~*NHL{k$I#&+NecV>RIY} z9kZ)<4O8w6se9zOagI7W;jBc+XrWLq%T(3sLn;sY<>V&*B9-3Oh`RH=nhfu|cBK}O zB4`FcM~l6`9F>eTf+Xk>;=`f9&dCoMsYM!%6wp<86(aXbG+sqc<=^<`XWIvW=WnDG z8j;(U-=}-WO=2-qILC_D;k9W<8xmv@5Ukt^p|(|S<`NuSM;aSarC95a&%EJSGS|=e z)7QZc7K&!e2V0UamNGeG*lBCP7Id8M>zFjwqqQ-?QEjVJd>u}7lN}TDLSD7 zUT4-eGR|W_9K8q%>}T$A#@55a4AuRBGX_-wxY~s+J;qmX9vQwaOH|=T6kj1LA4i4? z;jm_a?hrcgd=eg*1sW0P zPrNoJf~?)M^OJ8}h}Inv&hnmTo3TxDI7c$TR+Ju)PN>vNIMX`MC9_N9#t-MYq+>;$ zAAOiKtyg?;n!xcYhQx(T*ufxY&<}j;f*!8l2b zDt)(zyCgdwK3knpgE>!D<6IhC!K$bShia~!1cQ3etF)q8(_1%Ga|yVM*6)N_;0?ZG zUD`rtS<>q>T)LXaj2j>WI5luHl^%gsARfq?Pgp*`x0JNqNg~XMqnLy2_#q7E{vz}| zq%QMP&q-QGY9upoyqJueC6O(AUzgpU-g5^*Qn+DlJjo8rxGdb9t7`I3Dd zz)?#NhhrlL4QR~6C16fM$Y%b9U&lCaYAjcSxf_W#=s?B`$TjDqQ7IFx^YILryKWx} zFtGEf0@$lzA2=GWqbqRRPiOTS)EU0xQz^k~_H4p&T}Xd)c6S~+6fADW-}dX;xDl`0 z@DBuc_Zd2DeP6wO^vQrLcKX_UDkRu{T`vPNH5110o(BhbYD>rv(*)&!vl#a1K-BrT z8nx3i4XL$Dg6=8;&$k#Fd^H$Kh&}{71{06&efs{*xK}wVnQf%lA^h@C>yi?jN&9+g z!kI-M4K?G#^@>9tQJ`RJwXWsxyK%31{#dqy&;V>Elc@biUs3#HSU;v%e(N z7FpGOyg!PcABLtbc_+VD9^6w0nDI78HWOOOIBZKG2W8=a$07ixpxdH958_{I5!&H= z*(F2opPXQI(*)cymZ)t5d*VK&kpu9w3sN{BCytbQ6qXphrx~mmK`LS%nRkM&S%FFb zjjQefZ17xl=&b7evf5T$w_r)5s1%2Mi&S=rMj4X{7U-@r}vzsG9Z-638AM)_!cmFZv||XXfN+*HGRfEVAt#IU8~+lI^R{n z0IeffLj`|0T`%*Jd97+_9u>s&7IkP#ye}R7{@i{>%dB53+SN8zSf0xQ-9DPLa0!}; zU<`gFSsZOPur;An6=4F$L*cY+rR3I%kCSm=-WHa0TxsLM{$at{(t46`VXB$_rKCP{~5z! z`9I(MOEhNvKi~XI-~8_Z99UU5R-;c>`oA%{Y4d0`e!PY~fp&O)3 z0AZKV-VkG09CVJ0Z-N`yo0X@Vp3#oD>ta!9twaLkA*D{s4b!N0`Ups5Q6`jo-G#{LkX{6uPt2p@~ zco}{bpp)A2Q_Lf~fj^6MdYcZAw>7IfQW};X4X-@tQk6BPsQN{$s7|4osx+LsreLsO zal(=oNTY-yuX%}L^sZtR#*2(v;f%DR& z43{!h-v3k^eP38)?6s3PRXAKh!=~nYpypsunR}uTwKgV>2vKQ-=J@q0@CbF@plfb3 z?ya8>$Ll-I>&iqk@}bd42?f{~r!a}x<*LMt3I0cJ;1wde$hWtsz%TfYyGQLvJMYJV zC3M{k^U4cB9Ok#PU(~eAcagCR!iwkmUBhPTQj!wV(7e23Bke7sN=Lh*EX<75$DBza z@z0=KMAy1t?D?vX^%qhLS@DJW_j(Ao+pZaV@4#B#0a)!2I54drBW>RRX)w3iyp80r zL(sShb(pqb?ym-Qu)D=NdVE>2uiSO8d(px^Z$vy?3HR`Ht*63RF|P)Jbztrv-92PF zu6?=f#wwckMzX4BDE+k^K4M$|){n+f__$htU&XRTy|T0Q#Bp!kK9Ie-zBh&S zAZktrIh$C|gQ+1b~}-PdmO;mZLB9l}|wev$Ecf_%=A!k7cxgoEM`Ry&AmwtMml zYMq@9Incc@*hEU3b0<3EW>DN0tvFS7^4Ut>LiU#5twww+vFP|9uwZoIfFa%O_oGu$ zbPG2iy;gcJ(%*6ukIZAsyr@$#8?y+ySTcIMIj(-boYLj-;^A-de*;v*;Z+rIrnDH4 zG?qvs$xR+8jc+qM$q`3n-!(^PoBJV3Cb?^Op!=ynH$;fAlbUdRR306=qj@nes~S(H z>N&V2)t9|Q05F{aZ^KYdbc4{G9k@FfkrF`5pSk2``z`Ros7=E@l8BP|1f-`oj>L^- zl%_6!8+D%{9^nJJe@AJ31pi}VPmwZTX!BWGO@sPt`1(C<{de%ys4^3s&W7xzYyQ!* zlSvORCjL_jWw=Jed5sVVZu$m84l{OPy1jHRw1ncRKKGT%2YP-*TK!Au1-X& z!2t8}uP71UnKE{Dw!{U4=-N@r6?mr}P^)dt5o1B7j!|$eu^WgQtYF}&^qt_Jm2;QL zhi8Yz>W7X4zoZ2<*ZsssLxUfcm7$n(!W^ap3n2B*F6O@9>hLwazQcj56SfAaZng$? z{9>k*lf+bODQC|Avcykr!;G&EOngC9XtH4K6K_CmUAX>W?LjH8jh^A%9q1I$b;)1!qZt&icrph|CbTB{9xZrB*8=yzarz)_zd-9cmO~zWE z8QMq&dFO(M$aw@E99D3kD+YaUQRRz1Os}=$D4982;L%vZ{F7(_d!BIRjAkBF;xHI+ zHy|ITbHo0yz-&_saa@Ow|5`1X<+Na+YtB`7SO&Dv{WpQ;$i0T03SWHx?EFR==NJ+~ zItr;#P3N2oQVf>5Mv!u(;OnjP{=yG|WQ#P1M^TJ*1!pkND}l%MvZTX&S)I_V>!_de z%U9cy+ow0`x3!Hpuq!$U(z1cv7`q*0OhylA8Bc+UGPKS%T#d=Ld+Ai~(Q%HgH#@_h zEl-N9(S5_xD%*+aB>HJIon}de+lSlFWcbS3tW4#2EO8r_<9D_?$^zIuR>)Ggl0XQN zx(Q}v5`(QzFfn6@(egRIeGcM6FS!1?s2uk2%`kEjQ?U zvrQ2!7XoJ3@>dijKbeh3wAHCoYoRog%|_piY!@@72h!w-1n z;y39x%LZbaVphDoCbcNaAWEb<-Xf@`Nv7V{cMjE)51pI`XnNK zWPg2?tl8i>;~V5HEh zKv`C$I z;RvFPKT~Afli2&?9;I9ox~O=lSlU)>M~{@Z%dh(hCl({34#9NC5#n4zeL0 zW#b;8p-}BY3>l$KszUmm9a#=l!Ehd^b$WRi>H2we;jq{HC?cLh#1&C-j&xzbtb!hf zK?quAc`0}_ks9HIS~QA!9ty_r)M@1s*IKqP8wP#W7`C1|rO=r~v+4bGd&KQ9JqZDF z{#rBd(ftDQFaa$nB>^grJapETvU{UDsEMLw!u8WKr_E~IQskhg$a(C#f~HZGwiTkd zdA}};MlTeD@eVIHvzDxxM?TqkO5eBzLiO+86g}}Yl!?O^wIC&yw@PelT#u^R)z~Wtl|NvW1-*N)JNpt0)Iv5!b&}Q#dq_!$OMM%1q{~I_QbxlW zw2@5uEagGPe;g}fkZLnFkx{j;0Q)&r)WwG+3JnNjX~_q&JU-hMem~X zm5UVsi_W!yHBXnaG!9_vlj*jbx0tiusoypUxf1iEXWvBWDTa&_?69=8<5vs!fBiE0 z0Fgn4S8R|S+A2yX!2w*C>$~Yn1j!@Lm1v7@cg+1+H!2ltG?+sG+vC~OhEi@~Ol*cW}TN%8g zZP;h*2gy_y)agE+X*kU3)g%YSgUD#&oE_ z^CXdhF?MZ>uAbEor z{P%95Cq}(Ib5Xy9d5O%XG$Y&}pvC&&bQT#*sqJ+)jAk@O(N&t?)>tF|833 zUg!6aVqPU-$T^H`mU%2|AFgr4LO%BFFXB31(^Lw+%a+*-(WB-Y++$IP>=PzJU6{rw zi6a5Z(JI|N^HUemqK)`+Fz^w>qInjq4N`)oI+i_ya;yMox z#;@Ycy4U-4HI>$UD_yg2s2I$UA7!XWp)AMrD!+;D&!#NIU&GaZpY2HOpEa?gC5<`- zO;~B+=lZ$GT8(L1>&6!sw6qttxleRrjDl4s9K7$El-h_1f$O8y=4_?0iA*~nETL$v z9V~>iCGf(u=k&dvuCGL`HD*0f9n(hzUEVg1VIQ~*y(nBMQx!%O`AF)5fWR)6>uWfV zH32O%MPXPJxBKqZn{~6QH5$FoxLhPDkz@T<)K~gpx zQ0G>)~a5bfnBRzqXPw{sphxLZN#ak4|Q7I z(-!TbF#OsXNy4%XM0vsyyDfwv>0i!*eqpKxT~eNG*3&YNC1jE^(ln`ZsSuc}I zKXwann*q#Z@)A7~kxv=Z`BwC8*&L7dd?&-pvOYrm-fp-`YgH5)+_<^yE$2)H0xw~` zY%HKobHG{dzFVGuRWnW0n!TT%9Fe$>F~iBXIXVzeT44oeod=QotKuk0=5|l*)v+t- zb89=Jsf`QRrPXVk zlZfU6x-#(h;9!monoBOsU;Oexfo;o!0KfEgZ9mF?>0;SzMzW@mbigj0QVho{k~+sk z$>D4>R&&Netm=6R07}mh3Jhh?f{Sp8u8G80pBGNI@x10DVLH4E-E=idDTGkDD1Mqt z(%oWzRWODK**H#P?aF&qS_n2lZVl+F#sG_CLI)^$LY>}U zjpQ`O#s}@g^W*yb1*1+`mv>{EHmE&(p3wMn|4P6=R-yFrK}o@z>uq^x@Y>^0+gk$k z0^AF}+?TVFF!0kBsbz7$Wib`2khnToCl=EaqhQpRr|jJUrs%|>NlC_yGSbbJ$X1$! zB2`>s6#;XvR_^vITU;opd$~ChI}qjL$vx4*B7of~6PQF^dgZ>dd zp$SYFAV1;L@;_)y`n@dtZ{bsIJudxkjY)pnS^KhjXJM>{SF^|CB<+fva5Tzq>*N5kg8e5W1?r^GC}1w%zJ%Y1LE z#*l^H;k1d0%?VT&@S5eF1AvtakR?umMCB-Z+KFophl9c&!?w*TPR}>bNhWx${3Q*d zshcUy!b~jX7M(|HRVCw-FegQwlo>9Qh^HwBSj2n$I(H=4`ZOj60kQ9j<#?4~@y`Y& zb(jdJeNrY)#E6mj%N#;lq%Gq-nEse#v_aRf_I@e3iLuQ15(4Lj14(#$r7%-hbr&lD!~0guQt^^O6ht-Z*C%^3n4s^O1;iP7)F@)(?y95_eRji!# z3Tig~6+d6dAclsvSx(a)-$Xjgt+)qmoUjKbLK!HV^DtSEO)KHd(e6XyA(1RaB~t`` zIGa4ce=qsKe3jpIpxaRGcKFZ;FH{|M-dNko(zH0}Y;E|7v@AhD=T6MFVaZ!sDYP4_ z8DpKuXg5$I>envtr17OlpbOGW5S3&=ekig%QHlNd;v_P3Qb<>=FZiQ{rFj){8ay-@ zZJK2^S#x!m`s5%MJ2#7n4CJ{`6R|EQ4gwD9 zG0+?_K*z>w>e!H`$=M zY|#Dqk&9O=_DAi%HgYnncY8gBy#Aitq7AEfxRBuEK{jhOP#TU%m`h;83DSlcP&)H} z^E?OK`N7ZdAGg}}{ z$?>xQN?>J4Xb3!|Uz-qnb;;VCU+fi?>xSGDVo_!;k-mUCA$h5_tBdDw?R2ffwbhD! zh0Y3-yoikhiryeE&WZ4%=yBZ&!w4NiQdqhb*)rT^d@K2ID!Wq-sqa*>E#4vmKbQ`Q zV}R0PJ`i59QcZFqRY~f{io!{fhU>gb!NM(4zcqVUpRJh2%9oDK>g+mQnq=vLgF207 zsU@0FlMIa?HY!mJUQBlhW)h6R`mYzf1k0H~O-;2K+E876!|f+9=!BlIXXT*Pu{qS} z)|qHG*46_hniDDMVH1>5}+g$t8Z^6Ik3*wZ+u5CIRyoSKe_$kLMmPo35 z(TRy|?H;+@!s6nXvX;%)UbQ6mP{-}|Nz*2+AJlEM3U2frMLG6HX)ocJz6-a8gyaI! z7FSiJe%YYTsnXQ66&c`%_t3d2diJdls<5VN%Nt~f;zDl$g`X`o>=fZ4Lb`*Qzda8a z&I}(7xIl>0>GgqPKMKlF9GiwIM$r`40rr1HPFp}F^9i=#e?si@`~E=2l{qNc0vA1y z%tUMQi%v2i!jc8Lj!HbhGfN9ZI171zN@~bB*7Mv(4MxdouCA_C)55v4>9Mp_LJSg( z1$V6&1U`d@Ts%8LK|&RJ$Q&c%E45^Kdxxi%A`IzmX_!oM+8jmPeK?h?BIB*edY4a* zq&=i|PAA`yXkplg;5@3lI_zP+9egl`V<*>$za8{lI@}x^uIUKb*qGov=8zUZTQTZs zwawdYoC=v;81C5bEmu<4OhY6T{MXqpB zeJ)HK3G|%1d~nIO`(um1Q_8X`)-DN+k51|i@gCjYF0l*k;ThebC!0Tp^e$j$@959d zHuEV9fBm0&4F0K`iT?Kj`oBMJ@;0k1@ZNYeAACpJxkQrDmOsg<*fGHhq~+SZOo=&2 zB*L)8+nqyz-ZW2ly(N4`q7(L(%z@a_AHFL5yl!v5yl{LtxobKw6HGM8OLg5A-Xd5@ z$TOID>-Xp;d;KZ6^1bM*7L()7dC9$w?%VSiCHq208v$o6%NIIKZW1IL<4A17iZPVR zurByGmMbfAC28o6G7l~C8;lx@=zM(iowS<4(~EM&i-`TWL`RZQCuyuXf3ndqz?Zvl z@%8)>#p!}POue;p!}xI-|7N~ObxZEOy`Nw*0+*0lw1*u6Fs0jiJ8~`PK6MC^BC2 zpI@$q+BVyolg+%E%C(#b=uk&b&1>+kbM4pTjhj+SoK;3d)z%3x9IOsZhDtqR@3+V0 z^k_+0`pkE`vju|}Ne&a3JOsv-C)J-IF#OC0pxp%>kP3)VdGflapd>b z3HOqOk0URr`?-Urkl2sHxJJnxhs4EI{Z;iO9AWLLm2nZn`4JkHFZ|xm`}ULRtbGW5 z1;UtDJc519Ay%%qXFlk0lTgrLy^!J7EI1V~y5#mVl7LRq=3w?oe~8is>Q`nGM0Y7|MrOK67qkWgE z^W=b^fE2D${4CUV77*S#7i;DHbLkgkY*Q zyMjlo*?Q#{f6hS`SYqIUmh_CSp^Eypdoz?qHvL)t*?djPf|zi_z_^bbXD=snZ?EFz z*&${Jr=_z8yp8`U^dY!#pf3+?Iv0xvLN4X6p7EUj<%SiDM4m}vcZ`YhUcJ2X5Jt`A zTiyHPz3Q)r2fWR$Yy!>e?HW+&ZIN{AkcDtRN_7@Y5x2A_b#yduE-dZmIfIl(VTRCM zF3IAjt>7uaA1j-Oqn20Le(G znf831^jhfu;H3XUH~tsH@{Ov0tN`H?eJg@ADp5&u7Li_YLCb(B>-p(CTku%`Qh8~~ zo~2#>EC3P3H-)iyLFiS*OmPpPZE|^Z(O_uE6F})iQ9X>-!dEh6q0RRVyE96j4+|*7 zQjkWfIvQD(G-T<1ye-zdlc{@4vUrxl>nYibi7W_|x`)<|qc#xTgfBMuyExED;xK_Ck@ajcX|sWA0d z@KUYR33b=lgkpS|Kheqgx@bln9<+*Pg>Frfisec7mfKF;cc+!`40)1sHw=Wt`iyo; zkMCD!Lg#_~$>K!~>IWub&DN-WT~O!MnfJ1acGZW3?V69B`i;HD>K_o5u@UDunB1%h z)AsHfbsDnBV2lZ9AZ@E^hfjXaJ~z`=SExwo?}!i{k?SIeu_K;|XPzONp(AR_ ze#&gEvZ=Yi;~-8k&F)c(zAf(z#Pw3@aXAmZIR9Z%NcKsgWMJg#z4dC~mF&4ta{N-l zQUH0qykdixf-*!RE_;iN$Z}&o;n!(i@6i3btn&+YG;ij>A->EY5-H?ax*7J0{2sV7 z*R{jH`vm$yaMy<8suB+Qn-;ht!q1gkcLH%1`u?ujf5OV;(e{; z?JDYbV#`#@na3M_agd5>^+B0!DzP<+Mx)^wd8%VK>wYvn#EyPRh&eG5yKjEl z=`H4%GK0XYc3+Pi4SDOpanF7y-SAIuWHQa|DktNVdM+m>Z`XI4cb5-%y4|{Z%{%KP zh_FGLr7N5b$e;y?Ljlv0c}vmSyp3-oIW{qKbTxB1hcDvTQSloSXzP(}?{r8%9Xmeu zw5!jaW!v`ahCz}&tEw1unygr>o7-1qI<4rq3Ew_&I&x2)-hpSz|EO%oY-%x2 zo*>cRAuhdCF-cE2D3KhOgYkh`(Y`_VcX|J&_36Z;#d9+RzF*;!Vnqbu+Pk|Lr_E;}ozxfo-TZP?3;s-;LPNlGaPEd~jZXE?F< z%a6MuiaZI*-q9|Wl~lw0`|vSmi3*;f-^W!725$oG1hzGMgrZP7!ih&gy8_mnf;<;- zaxjyBTJH{c?5N8pV5HVrY9p1U5&0{r@xR$C+bv4-3_);Rx6Z@ETp)dyDoD`GT11^kb=Z&(YS5?lnw{K_*g(VyO z=$%%(p%`a=Uz^cT{>mWL=CPoS96QeaZ( zfG#B2fYkVY{y3>M?*x(_uSGa^K4oL+S^HMhJ^M<_Oi2L3X|St4_!BMKoC^(NlpaUf zR!r~`xbyu7rm4{y7!>q{2>FMG^C%n)v@1^2(K>FUd!V2N#okDm-+hi&`X%Rj91+>8uzDk- z1E#|~k2YPWAKu8Id4lZPE;UMH6-zEUC!TMwBHq0{-Cel^n)CWbz8Icpl{uSM3PZ^4 zuwg!mqjT1x70A#<@vz4`PP@!{(|(C#%OL7p?#!yup&JLGuYZ=@kanYWWj;J*K$5Bx z$TX_3riJcb#zDwPJea&%a~`#^Ed;%JB?}jzwG8rKX`5GyNDH$`D?rqx?_Nt`@$2I| z=uctO`ev2P)t(a|Y5)5B3HqO3TJwjQW@&u~M`QbcnrU|VtNE0UDDr?)FaUrBnm--J z|1$MEC;!`*zYNsp8aCFuO^6>lTkHJOi{RDovCoDTiv5#q+W6~W((V8nc~;C!g$j;o zmhyU+(^$8t=PwKSXW|>*5viB$c0f)WP8WXk<^|_(kH4A(?-}X(xV7soz@RAWT=|`M z+FM23Gz{DGU@dLxMtRg)DaTz!*7(o)d5KcKjKzO=Ubmg8QoRH##X&+DU6sj#-D{23 zU9u^26hkR=Qb>&ITWf@Z#$HjF$dC-n;(PR9Z#oH@^4nXpZblS)>Pi*6Nlb-1w&rLp z2!I$1`M=L|(NYptdWJVBOhfh`RWB-W0KUvhCY2rjQO#vO-3 zoCeqUai_X9Iozz6r=-U|w@_4r6n^Ws=|bE$b|bijytJkP83u`M8W;h3EeAOY3!W(D zodq`C{gsLl_kONK^%H5$&@b3`C-d0}Ich_KvNFtEQu=VydRzL?rpcDm1hBZiq^?Zm zqKmzY{*WR$QUfirz)`0L+F(Fa-ORCi^w-zUDk>dct5KB3Qp)HR?>nl)qz+2{3MB)p zT}0+%N0ibg576x|@Cyge2#^7;>%6s>q`9k0=Wb-S>rSWQ-N=cr;iyoOFknqNr9b`n ztS2ee>oHL)73Rr^Y(j*ci`hu8KuGxqdmerXbTm`nC#eE+Uc>u07JAR5tt1^yN-dZW zVWCLn3%NqtgE>+(~zyu?M!&jgty0h0H+Fu4m*lULwiHc~jiIl}g5`zm- ztbrsAQf5ti^%9D=|Gr*uUks86KC|0?K`0+o6BAGO_?myR?59LgV6#MFz(Ze?@ZhMW zY~i<`GQW^?o7K8UEDe>V9=RcR4|4s^IG{V>aP4^9!&n#-QTY8AX@uw$O15mX#HNb` zVf?JAP?ZgQ503&zyxzLUFlcX)^tYGnxD|ch?M;zyFFNoQ3Y=eR4|k_{>lg2#iqps4 zJ^Q#&I0Gl}DN_qu-))kYE@t-l^M2k(WBRy<(=4#M}zB~k*9!c9U3FXg}M#Nu#c{4~)@fC}I z@{j(ttT1hp%#1864!NhthV{&!j-cqowB{nC7mu`?C#PJKj$($VP7gZF9L-A;=8KXx z{v|6D!|yPENRd$HikE|V-2C*HWc1shk(t2_qGRqt`@TUPLaHlXEQE_Vd?z*AOnm@j zKii|r;`tOald6-)UewU`viYpTy6aj*2-yWOkJ6QX=ynTsJ{fMbs#Ci{><|G*mPQq? z_haVA)6fu{&snYx=lU2Y2>jM&WJTd{Pr^1G#G5?V84NemMQ%OONoKU*8rCjA?hG9d z4ycW{{2K_|og%+e`L>lw8ZvI%Vv*WjDOs5Tg>K zyEvlol4P0`;QF|t#c4xjWI~vYn7OnGuI0rnFx+M@S$pt|(E(*zwOLnQPiO)8J|bJ1 zU+!aY-3|maF&(lI*|1zquAU$S3~1SS#{Mg2MlNpCXG*uo+!J|10qnGsv4$e~#A5ON zNbY1IAml4DIDrJd$8-4C4U>(|>P6Su)$Kc%D|eg?J@D$yZDCH=09e|Mo(PVPZCIVP z?I`D~V4MwCaGRgo(#}_d+Ei;e;}$o7Y!~s@R%ZLj~}jVB8;LS-m zFM3r9nfGdUNvV!X~ZFm|7B zus9{1xY(XOeuo)8e)YMH|M-29@qZ4;e;6D6uPFNeIyP#ar%M3$IWju*$&vpsRPjfH ze-6%n998-s#<&LNrN8yj!Gmsj<+{+-#}IRd#U~6e2MXkr$Kmp+?c#lt6j6}ydI?Vw zfYvu7$cg$9cXzOB)xhfs>viL|;3NVytN;sfGFl?T3bZjf6+uljDOqV`!vvY~Ni01* zm%0sTy;}BGtR2P0W@PwI-yZu@$~CYHBqu^SGJX!i&W6aY*2t?|*6}Af=+Jp&0`5~W zvXcYe7xcdVV%pKIlh$@Y!IgHxkbA)*OgZHYqP1s0!!szfnuZdaJAGT0U1f>{aMo;! zB-d1^la^Wzjx&DZHs@Izz&P8j^(p{2wPnA*HA$c~_XK5|n>LUdFRncfa>qs}AUS-*7q@7z1Br^ug-0nXg?O6z^C z?lomhM-_qh`-C^v(=;tqt}#CbfxJ8Cj$2MqJPm_wXtp#P8xK@E{cR?%F7n9NN@~?Yy!#(3f7H8201_c`Zzp6{IRd7kG3&$26K>D*)bgCv}SRgmMb#L3#d-q(h# zRfGJ~NXIBES|WYwBKK=%9O02a&iJWNK}W=}iu(KDY*p_B%WUPT7`4J)KFzZ%igb*odO!lpk8$oR-hCTnP)#3}Ay zq@_ir^g{6Em*ODV`!UVuBP#_MGD(jzYQ7e5eQ~RXN=-G?z3Q)Sp=_ndh&jeiqhAor z%CcPO&Vpg z(U*$F1S+zaBi?hP&D}2dQ7aD1)i5l2H-7i@ZuY<~ouk zn+(N(=!~@z>WRl1@i*UpT#VVz_aL{W^XkjC2H^zWIA<1hGrhxhPcxrZ-;rYM?Jj3d zm3d+u>zyfnc7m_*!0UlODI6Xrgaj&Apl34LJt#3x=R>$JIGLc8h8ZjK>=Q9Fr(X;t zXre;gFSu0HO<7Ci7RjDabNLhdxRm9)+dqY>ZqEKzQ82}rOC0a~i(I?}q{fC#?&n{| z-Z#>zH?wzBQT{chrCRJ=MxGj`Nnl)SsiakF!A+EsEw;|dMBL>4(xV&la1${OdR-yo z80`=TpX}gf+J(d7wl=vz$4BiG%oOf43=CmV*V~UEVak17u_R16k)&dh%O%dDmvNSCvW`7?hFcF(z$;!%>D$`O}q{khEh`Qc5w7+@! zsPO|<|2%agr6a!V@q-#P4!(kYx21}?486sX{z~Zv54mhlGha%mc$YEM`YlTTql3mp zyND&4I&Q0gf-6mb%n32+l%DJirj~+>w;GyNK3HObo5tom8-GVG@gaX!;#pdb3&qg+ zwATWhPx+p>78Z>luwK2-TgYA6e_!yg*1l0=Ujtmq2XLvbp5 zbl$%?I>#ULj=!wUPwpMAW^hPEj7-MkIDBvkBTnAA$}YG=5%nyKQC*dLxV$-^NZH)Y zsU^3cwLEQ0Nn5PQAE?zU#O+yCkeYuv3{5u~mTnb4ac(?Ew>W0BhxCtD&DYgmu2Wns zL`79i=A$%k4jjzkdGG4ZN9oKbQ`4u9IBPq=&+-cO_LSb&VKJMjCxdy+q>~&7gPQcx z?6D{p4Y>>1MdKIGWJBzH~PTB-mg;Wwf7GE*HG9EmHEKGIZGrc`CRBji?x{q$7*XHSLxN6|b6 zx32D0K@?Q!Om##>jMCj4OnHnJ?3;|3=3~v)@?yVrj{3U-+|t3&x{XV)p7hD=SnP{9 zzGQLFLFc0#x#|rQDh#^SHR(=^5}&T(zELOUPZZ<_;%X z{)fE3n244KMmmLxR%Dz0Z|*i<0jIX1706Z$+%uek(02T25?xCfBuk{P~X?PkA62-hoWBt@SYwiPE^ z>L6h#k-(XfrH-k3W)etic~U6GWHsDuisOnzaR1iQ-jr%l=h?&8-(K z-rm=ZT9~WGmlIh8ygsZbV*-|zl`*$TBUBd?oW?1?S%3>jxdG#|ZZJ2r$bwNcVskcy&fX-mi@vR3Z(GfdPWkgfMu)G(woomjf## zNwk138!$UtA5f_sa154P8wVu}EJ{QuoVBcNP4r>jG8_;QEcLTn;yo5(h+qMo-4Y8} zi6MezX?9C&00wo7Ndn3$SnOrD#9?+~h+qkoofC!d3t{D1zL%32B1D*Ew@6;#_Qv*U zT{*2p#1{-P>q7}>9c+&Ti)idT$Rm)vnja#D2$rVUE%6GR7~;=v$&RuL6;1#@=k9WC zLIkZ0yo>^Ifk7`ggE2b!{1xlKAPptMd*osFec_GVGRC!KXDl7nO-nvQHn4If5MkI4D>J~7~_15FlO~= zBXJz)Q7SO!l;Sp=wMnpL!MpZ6f}Y9%b39HE=HSn05DI7WDhFB$K2rr@R@>afmV(f2 zY_Rt*wN1?aoWhVz?VWoMt~T@d3YWG%%Q#@$!HF0YaicApigX literal 28298 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 KkPvh1um1xk71j~} diff --git a/lib/version/version.go b/lib/version/version.go index c11435e09..996d93823 100644 --- a/lib/version/version.go +++ b/lib/version/version.go @@ -1,4 +1,14 @@ package version +import "regexp" + // Pre-built binaries will have version set correctly during build time. var Version = "v0.3.0-HEAD" + +func OnlyNumbers() string { + re, err := regexp.Compile("[0-9]+.[0-9]+.[0-9]+") + if err != nil { + return "" + } + return re.FindString(Version) +} From 4b2041f759c7bae2593a5f70f53ce1e1575e252c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Thu, 6 Apr 2023 11:04:17 -0300 Subject: [PATCH 11/20] minor refactor --- d2cli/main.go | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 015645578..178e564c8 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -360,15 +360,12 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur) return pdf, true, nil case ".pptx": - ext := filepath.Ext(outputPath) - trimmedPath := strings.TrimSuffix(outputPath, ext) - splitPath := strings.Split(trimmedPath, "/") - rootName := splitPath[len(splitPath)-1] var username string if user, err := user.Current(); err == nil { username = user.Username } 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) if err != nil { @@ -673,11 +670,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt var currBoardPath []string // Root board doesn't have a name, so we use the output filename if diagram.Name == "" { - ext := filepath.Ext(outputPath) - trimmedPath := strings.TrimSuffix(outputPath, ext) - splitPath := strings.Split(trimmedPath, "/") - rootName := splitPath[len(splitPath)-1] - currBoardPath = append(boardPath, rootName) + currBoardPath = append(boardPath, getFileName(outputPath)) } else { currBoardPath = append(boardPath, diagram.Name) } @@ -764,11 +757,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present var currBoardPath []string // Root board doesn't have a name, so we use the output filename if diagram.Name == "" { - ext := filepath.Ext(outputPath) - trimmedPath := strings.TrimSuffix(outputPath, ext) - splitPath := strings.Split(trimmedPath, "/") - rootName := splitPath[len(splitPath)-1] - currBoardPath = append(boardPath, rootName) + currBoardPath = append(boardPath, getFileName(outputPath)) } else { currBoardPath = append(boardPath, diagram.Name) } @@ -844,6 +833,13 @@ func renameExt(fp string, newExt string) string { } } +func getFileName(path string) string { + ext := filepath.Ext(path) + trimmedPath := strings.TrimSuffix(path, ext) + splitPath := strings.Split(trimmedPath, "/") + return splitPath[len(splitPath)-1] +} + // TODO: remove after removing slog func DiscardSlog(ctx context.Context) context.Context { return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard))) From 37c46b7435310bd51e9e194bd9dee755c5a7e724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Thu, 6 Apr 2023 15:16:32 -0300 Subject: [PATCH 12/20] add CLI test --- e2etests-cli/main_test.go | 27 +++++++++++++ lib/pptx/pptx.go | 17 ++------ lib/pptx/presentation.go | 24 ++++++------ lib/pptx/validate.go | 82 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 lib/pptx/validate.go diff --git a/e2etests-cli/main_test.go b/e2etests-cli/main_test.go index dc75a4f22..0601d591e 100644 --- a/e2etests-cli/main_test.go +++ b/e2etests-cli/main_test.go @@ -9,6 +9,7 @@ import ( "time" "oss.terrastruct.com/d2/d2cli" + "oss.terrastruct.com/d2/lib/pptx" "oss.terrastruct.com/util-go/assert" "oss.terrastruct.com/util-go/diff" "oss.terrastruct.com/util-go/xmain" @@ -234,6 +235,32 @@ layers: { testdataIgnoreDiff(t, ".pdf", pdf) }, }, + { + name: "how_to_solve_problems_pptx", + run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { + writeFile(t, dir, "in.d2", `how to solve a hard problem? +steps: { + 1: { + w: write down the problem + } + 2: { + w -> t + t: think really hard about it + } + 3: { + t -> w2 + w2: write down the solution + } +} +`) + err := runTestMain(t, ctx, dir, env, "in.d2", "how_to_solve_problems.pptx") + assert.Success(t, err) + + file := readFile(t, dir, "how_to_solve_problems.pptx") + err = pptx.Validate(file, 4) + assert.Success(t, err) + }, + }, { name: "stdin", run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) { diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index b37503113..9deea80b6 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -5,7 +5,6 @@ import ( "bytes" _ "embed" "fmt" - "io" "strings" "time" ) @@ -19,21 +18,12 @@ func copyPptxTemplateTo(w *zip.Writer) error { reader := bytes.NewReader(pptx_template) zipReader, err := zip.NewReader(reader, reader.Size()) if err != nil { - fmt.Printf("%v", err) + fmt.Printf("error creating zip reader: %v", err) } for _, f := range zipReader.File { - fw, err := w.Create(f.Name) - if err != nil { - return err - } - fr, err := f.Open() - if err != nil { - return err - } - _, err = io.Copy(fw, fr) - if err != nil { - return err + if err := w.Copy(f); err != nil { + return fmt.Errorf("error copying %s: %v", f.Name, err) } } return nil @@ -116,6 +106,7 @@ func getPresentationXml(slideFileNames []string) string { builder.WriteString("") for i, name := range slideFileNames { + // in the exported presentation, the first slide ID was 256, so keeping it here for compatibility builder.WriteString(fmt.Sprintf(``, 256+i, name)) } builder.WriteString("") diff --git a/lib/pptx/presentation.go b/lib/pptx/presentation.go index 1ec4286ab..ac16ad52a 100644 --- a/lib/pptx/presentation.go +++ b/lib/pptx/presentation.go @@ -88,10 +88,12 @@ func (p *Presentation) SaveTo(filePath string) error { return err } defer f.Close() - zipFile := zip.NewWriter(f) - defer zipFile.Close() + zipWriter := zip.NewWriter(f) + defer zipWriter.Close() - copyPptxTemplateTo(zipFile) + if err = copyPptxTemplateTo(zipWriter); err != nil { + return err + } var slideFileNames []string for i, slide := range p.Slides { @@ -99,7 +101,7 @@ func (p *Presentation) SaveTo(filePath string) error { slideFileName := fmt.Sprintf("slide%d", i+1) slideFileNames = append(slideFileNames, slideFileName) - imageWriter, err := zipFile.Create(fmt.Sprintf("ppt/media/%s.png", imageId)) + imageWriter, err := zipWriter.Create(fmt.Sprintf("ppt/media/%s.png", imageId)) if err != nil { return err } @@ -108,13 +110,13 @@ func (p *Presentation) SaveTo(filePath string) error { return err } - err = addFile(zipFile, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), getRelsSlideXml(imageId)) + err = addFile(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), getRelsSlideXml(imageId)) if err != nil { return err } err = addFile( - zipFile, + zipWriter, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), getSlideXml(slide.BoardPath, imageId, slide.ImageTop, slide.ImageLeft, slide.ImageWidth, slide.ImageHeight), ) @@ -123,27 +125,27 @@ func (p *Presentation) SaveTo(filePath string) error { } } - err = addFile(zipFile, "[Content_Types].xml", getContentTypesXml(slideFileNames)) + err = addFile(zipWriter, "[Content_Types].xml", getContentTypesXml(slideFileNames)) if err != nil { return err } - err = addFile(zipFile, "ppt/_rels/presentation.xml.rels", getPresentationXmlRels(slideFileNames)) + err = addFile(zipWriter, "ppt/_rels/presentation.xml.rels", getPresentationXmlRels(slideFileNames)) if err != nil { return err } - err = addFile(zipFile, "ppt/presentation.xml", getPresentationXml(slideFileNames)) + err = addFile(zipWriter, "ppt/presentation.xml", getPresentationXml(slideFileNames)) if err != nil { return err } - err = addFile(zipFile, "docProps/core.xml", getCoreXml(p.Title, p.Subject, p.Description, p.Creator)) + err = addFile(zipWriter, "docProps/core.xml", getCoreXml(p.Title, p.Subject, p.Description, p.Creator)) if err != nil { return err } - err = addFile(zipFile, "docProps/app.xml", getAppXml(p.Slides, p.D2Version)) + err = addFile(zipWriter, "docProps/app.xml", getAppXml(p.Slides, p.D2Version)) if err != nil { return err } diff --git a/lib/pptx/validate.go b/lib/pptx/validate.go new file mode 100644 index 000000000..72a84859a --- /dev/null +++ b/lib/pptx/validate.go @@ -0,0 +1,82 @@ +package pptx + +import ( + "archive/zip" + "bytes" + "encoding/xml" + "fmt" + "io" + "strings" +) + +func Validate(pptxContent []byte, nSlides int) error { + reader := bytes.NewReader(pptxContent) + zipReader, err := zip.NewReader(reader, reader.Size()) + if err != nil { + fmt.Printf("error reading pptx content: %v", err) + } + + expectedCount := getExpectedPptxFileCount(nSlides) + if len(zipReader.File) != expectedCount { + return fmt.Errorf("expected %d files, got %d", expectedCount, len(zipReader.File)) + } + + for i := 0; i < nSlides; i++ { + if err := checkFile(zipReader, fmt.Sprintf("ppt/slides/slide%d.xml", i+1)); err != nil { + return err + } + if err := checkFile(zipReader, fmt.Sprintf("ppt/slides/_rels/slide%d.xml.rels", i+1)); err != nil { + return err + } + if err := checkFile(zipReader, fmt.Sprintf("ppt/media/slide%dImage.png", i+1)); err != nil { + return err + } + } + + for _, file := range zipReader.File { + if !strings.Contains(file.Name, ".xml") { + continue + } + // checks if the XML content is valid + f, err := file.Open() + if err != nil { + return fmt.Errorf("error opening %s: %v", file.Name, err) + } + decoder := xml.NewDecoder(f) + for { + if err := decoder.Decode(new(interface{})); err != nil { + if err == io.EOF { + break + } + return fmt.Errorf("error parsing xml content in %s: %v", file.Name, err) + } + } + defer f.Close() + } + + return nil +} + +func checkFile(reader *zip.Reader, fname string) error { + f, err := reader.Open(fname) + if err != nil { + return fmt.Errorf("error opening file %s: %v", fname, err) + } + defer f.Close() + if _, err = f.Stat(); err != nil { + return fmt.Errorf("error getting file info %s: %v", fname, err) + } + return nil +} + +func getExpectedPptxFileCount(nSlides int) int { + reader := bytes.NewReader(pptx_template) + zipReader, err := zip.NewReader(reader, reader.Size()) + if err != nil { + return -1 + } + baseFiles := len(zipReader.File) + presentationFiles := 5 // presentation, rels, app, core, content types + slideFiles := 3 * nSlides // slides, rels, images + return baseFiles + presentationFiles + slideFiles +} From 944fb0c47627fdd724b1c3b21488f701a4ed7968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Fri, 7 Apr 2023 10:48:33 -0300 Subject: [PATCH 13/20] fix image resizing --- lib/pptx/pptx.go | 9 +++++---- lib/pptx/presentation.go | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index 9deea80b6..1d0c0f44c 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -39,14 +39,15 @@ func addFile(zipFile *zip.Writer, filePath, content string) error { } // https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/ -const SLIDE_WIDTH = 9144000 -const SLIDE_HEIGHT = 5143500 -const HEADER_HEIGHT = 392471 +const SLIDE_WIDTH = 9_144_000 +const SLIDE_HEIGHT = 5_143_500 +const HEADER_HEIGHT = 392_471 const IMAGE_HEIGHT = SLIDE_HEIGHT - HEADER_HEIGHT // keep the right aspect ratio: SLIDE_WIDTH / SLIDE_HEIGHT = IMAGE_WIDTH / IMAGE_HEIGHT -const IMAGE_WIDTH = IMAGE_HEIGHT * (SLIDE_WIDTH / SLIDE_HEIGHT) +const IMAGE_WIDTH = 8_446_273 +const IMAGE_ASPECT_RATIO = float64(IMAGE_WIDTH) / float64(IMAGE_HEIGHT) const RELS_SLIDE_XML = `` diff --git a/lib/pptx/presentation.go b/lib/pptx/presentation.go index ac16ad52a..a79e6253d 100644 --- a/lib/pptx/presentation.go +++ b/lib/pptx/presentation.go @@ -55,14 +55,23 @@ func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { var width, height int srcSize := src.Bounds().Size() + srcWidth, srcHeight := float64(srcSize.X), float64(srcSize.Y) // compute the size and position to fit the slide - if srcSize.X > srcSize.Y { - width = IMAGE_WIDTH - height = int(float64(width) * (float64(srcSize.Y) / float64(srcSize.X))) + // 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 + if srcWidth/srcHeight >= IMAGE_ASPECT_RATIO { + width = SLIDE_WIDTH + height = int(float64(width) * (srcHeight / srcWidth)) + if height > IMAGE_HEIGHT { + // this would overflow with the title, so we need to adjust to use only IMAGE_WIDTH + width = IMAGE_WIDTH + height = int(float64(width) * (srcHeight / srcWidth)) + } } else { + // otherwise, this image could overflow the slide height/header height = IMAGE_HEIGHT - width = int(float64(height) * (float64(srcSize.X) / float64(srcSize.Y))) + width = int(float64(height) * (srcWidth / srcHeight)) } top := (IMAGE_HEIGHT - height) / 2 left := (SLIDE_WIDTH - width) / 2 From 6993c0e557ad3ba787e7637cf9823c392fb47dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 10 Apr 2023 11:45:04 -0300 Subject: [PATCH 14/20] PR comments --- d2cli/main.go | 7 +++---- lib/pptx/pptx.go | 31 ++++++++++++++++--------------- lib/pptx/presentation.go | 37 ++++++++++++++++++++++++++++++------- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 9b7339f8a..d2998e0b5 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -364,8 +364,9 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende if user, err := user.Current(); err == nil { username = user.Username } - description := "Presentation auto-generated by D2 - https://d2lang.com/" + description := "Presentation generated with D2 - https://d2lang.com/" rootName := getFileName(outputPath) + // version must be only numbers to avoid issues with PowerPoint p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers()) err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil) if err != nil { @@ -838,9 +839,7 @@ func renameExt(fp string, newExt string) string { func getFileName(path string) string { ext := filepath.Ext(path) - trimmedPath := strings.TrimSuffix(path, ext) - splitPath := strings.Split(trimmedPath, "/") - return splitPath[len(splitPath)-1] + return strings.TrimSuffix(filepath.Base(path), ext) } // TODO: remove after removing slog diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index 1d0c0f44c..08a23d188 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -9,7 +9,19 @@ import ( "time" ) +// Measurements in OOXML are made in English Metric Units (EMUs) where 1 inch = 914,400 EMUs +// The intent is to have a measurement unit that doesn't require floating points when dealing with centimeters, inches, points (DPI). // Office Open XML (OOXML) http://officeopenxml.com/prPresentation.php +// https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/ +const SLIDE_WIDTH = 9_144_000 +const SLIDE_HEIGHT = 5_143_500 +const HEADER_HEIGHT = 392_471 + +const IMAGE_HEIGHT = SLIDE_HEIGHT - HEADER_HEIGHT + +// keep the right aspect ratio: SLIDE_WIDTH / SLIDE_HEIGHT = IMAGE_WIDTH / IMAGE_HEIGHT +const IMAGE_WIDTH = 8_446_273 +const IMAGE_ASPECT_RATIO = float64(IMAGE_WIDTH) / float64(IMAGE_HEIGHT) //go:embed template.pptx var pptx_template []byte @@ -38,26 +50,15 @@ func addFile(zipFile *zip.Writer, filePath, content string) error { return nil } -// https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/ -const SLIDE_WIDTH = 9_144_000 -const SLIDE_HEIGHT = 5_143_500 -const HEADER_HEIGHT = 392_471 - -const IMAGE_HEIGHT = SLIDE_HEIGHT - HEADER_HEIGHT - -// keep the right aspect ratio: SLIDE_WIDTH / SLIDE_HEIGHT = IMAGE_WIDTH / IMAGE_HEIGHT -const IMAGE_WIDTH = 8_446_273 -const IMAGE_ASPECT_RATIO = float64(IMAGE_WIDTH) / float64(IMAGE_HEIGHT) - const RELS_SLIDE_XML = `` -func getRelsSlideXml(imageId string) string { - return fmt.Sprintf(RELS_SLIDE_XML, imageId, imageId) +func getRelsSlideXml(imageID string) string { + return fmt.Sprintf(RELS_SLIDE_XML, imageID, imageID) } const SLIDE_XML = `%s` -func getSlideXml(boardPath []string, imageId string, top, left, width, height int) string { +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] @@ -69,7 +70,7 @@ func getSlideXml(boardPath []string, imageId string, top, left, width, height in } slideDescription := strings.Join(boardPath, " / ") top += HEADER_HEIGHT - return fmt.Sprintf(SLIDE_XML, slideDescription, slideDescription, imageId, left, top, width, height, slideDescription, HEADER_HEIGHT, slideTitle) + return fmt.Sprintf(SLIDE_XML, slideDescription, slideDescription, imageID, left, top, width, height, slideDescription, HEADER_HEIGHT, slideTitle) } func getPresentationXmlRels(slideFileNames []string) string { diff --git a/lib/pptx/presentation.go b/lib/pptx/presentation.go index a79e6253d..fea845fd5 100644 --- a/lib/pptx/presentation.go +++ b/lib/pptx/presentation.go @@ -23,7 +23,9 @@ type Presentation struct { Description string Subject string Creator string - D2Version string + // D2Version can't have letters, only numbers (`[0-9]`) and `.` + // Otherwise, it may fail to open in PowerPoint + D2Version string Slides []*Slide } @@ -60,16 +62,37 @@ func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { // 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 + // ┌──────────────────────────────────────────────────┐ ─┬─ + // │ HEADER │ │ + // ├──┬────────────────────────────────────────────┬──┤ │ ─┬─ + // │ │ │ │ │ │ + // │ │ │ │ SLIDE │ + // │ │ │ │ HEIGHT │ + // │ │ │ │ │ IMAGE + // │ │ │ │ │ HEIGHT + // │ │ │ │ │ │ + // │ │ │ │ │ │ + // │ │ │ │ │ │ + // │ │ │ │ │ │ + // └──┴────────────────────────────────────────────┴──┘ ─┴─ ─┴─ + // ├────────────────────SLIDE WIDTH───────────────────┤ + // ├─────────────────IMAGE WIDTH────────────────┤ if srcWidth/srcHeight >= IMAGE_ASPECT_RATIO { + // here, the image aspect ratio is, at least, equal to the slide aspect ratio + // so, it makes sense to expand the image horizontally to use as much as space as possible width = SLIDE_WIDTH height = int(float64(width) * (srcHeight / srcWidth)) + // first, try to make the image as wide as the slide + // but, if this results in a tall image, use only the + // image adjusted width to avoid overlapping with the header if height > IMAGE_HEIGHT { - // this would overflow with the title, so we need to adjust to use only IMAGE_WIDTH width = IMAGE_WIDTH height = int(float64(width) * (srcHeight / srcWidth)) } } else { - // otherwise, this image could overflow the slide height/header + // here, the aspect ratio could be 4x3, in which the image is still wider than taller, + // but expanding horizontally would result in an overflow + // so, we expand to make it fit the available vertical space height = IMAGE_HEIGHT width = int(float64(height) * (srcWidth / srcHeight)) } @@ -106,11 +129,11 @@ func (p *Presentation) SaveTo(filePath string) error { var slideFileNames []string for i, slide := range p.Slides { - imageId := fmt.Sprintf("slide%dImage", i+1) + 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", imageID)) if err != nil { return err } @@ -119,7 +142,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(imageID)) if err != nil { return err } @@ -127,7 +150,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.BoardPath, imageID, slide.ImageTop, slide.ImageLeft, slide.ImageWidth, slide.ImageHeight), ) if err != nil { return err From e39b9c15c2f7d8f3fc592c25f629107f53e68dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 10 Apr 2023 13:49:30 -0300 Subject: [PATCH 15/20] move xml to templates --- lib/pptx/pptx.go | 134 +++++++---------------- lib/pptx/templates/app.xml | 51 +++++++++ lib/pptx/templates/content_types.xml | 66 +++++++++++ lib/pptx/templates/core.xml | 17 +++ lib/pptx/templates/presentation.xml | 132 ++++++++++++++++++++++ lib/pptx/templates/rels_presentation.xml | 19 ++++ lib/pptx/templates/slide.xml | 86 +++++++++++++++ lib/pptx/templates/slide.xml.rels | 9 ++ lib/pptx/validate.go | 2 +- 9 files changed, 423 insertions(+), 93 deletions(-) create mode 100644 lib/pptx/templates/app.xml create mode 100644 lib/pptx/templates/content_types.xml create mode 100644 lib/pptx/templates/core.xml create mode 100644 lib/pptx/templates/presentation.xml create mode 100644 lib/pptx/templates/rels_presentation.xml create mode 100644 lib/pptx/templates/slide.xml create mode 100644 lib/pptx/templates/slide.xml.rels diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index 08a23d188..349d09393 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -24,10 +24,10 @@ const IMAGE_WIDTH = 8_446_273 const IMAGE_ASPECT_RATIO = float64(IMAGE_WIDTH) / float64(IMAGE_HEIGHT) //go:embed template.pptx -var pptx_template []byte +var PPTX_TEMPLATE []byte func copyPptxTemplateTo(w *zip.Writer) error { - reader := bytes.NewReader(pptx_template) + reader := bytes.NewReader(PPTX_TEMPLATE) zipReader, err := zip.NewReader(reader, reader.Size()) if err != nil { fmt.Printf("error creating zip reader: %v", err) @@ -50,13 +50,15 @@ func addFile(zipFile *zip.Writer, filePath, content string) error { return nil } -const RELS_SLIDE_XML = `` +//go:embed templates/slide.xml.rels +var RELS_SLIDE_XML string func getRelsSlideXml(imageID string) string { return fmt.Sprintf(RELS_SLIDE_XML, imageID, imageID) } -const SLIDE_XML = `%s` +//go:embed templates/slide.xml +var SLIDE_XML string func getSlideXml(boardPath []string, imageID string, top, left, width, height int) string { var slideTitle string @@ -73,129 +75,77 @@ func getSlideXml(boardPath []string, imageID string, top, left, width, height in return fmt.Sprintf(SLIDE_XML, slideDescription, slideDescription, imageID, left, top, width, height, slideDescription, HEADER_HEIGHT, slideTitle) } +//go:embed templates/rels_presentation.xml +var RELS_PRESENTATION_XML string + func getPresentationXmlRels(slideFileNames []string) string { var builder strings.Builder - builder.WriteString(``) - for _, name := range slideFileNames { builder.WriteString(fmt.Sprintf( ``, name, name, )) } - builder.WriteString("") - - return builder.String() + return fmt.Sprintf(RELS_PRESENTATION_XML, builder.String()) } +//go:embed templates/content_types.xml +var CONTENT_TYPES_XML string + func getContentTypesXml(slideFileNames []string) string { var builder strings.Builder - builder.WriteString(``) - for _, name := range slideFileNames { builder.WriteString(fmt.Sprintf( ``, name, )) } - builder.WriteString(``) - return builder.String() + return fmt.Sprintf(CONTENT_TYPES_XML, builder.String()) } +//go:embed templates/presentation.xml +var PRESENTATION_XML string + func getPresentationXml(slideFileNames []string) string { var builder strings.Builder - builder.WriteString(``) - - builder.WriteString("") for i, name := range slideFileNames { // in the exported presentation, the first slide ID was 256, so keeping it here for compatibility builder.WriteString(fmt.Sprintf(``, 256+i, name)) } - builder.WriteString("") - - builder.WriteString(fmt.Sprintf( - ``, - SLIDE_WIDTH, - SLIDE_HEIGHT, - )) - return builder.String() + return fmt.Sprintf(PRESENTATION_XML, builder.String(), SLIDE_WIDTH, SLIDE_HEIGHT) } +//go:embed templates/core.xml +var CORE_XML string + func getCoreXml(title, subject, description, creator string) string { - var builder strings.Builder - - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(fmt.Sprintf(`%s`, title)) - builder.WriteString(fmt.Sprintf(`%s`, subject)) - builder.WriteString(fmt.Sprintf(`%s`, creator)) - builder.WriteString(``) - builder.WriteString(fmt.Sprintf(`%s`, description)) - builder.WriteString(fmt.Sprintf(`%s`, creator)) - builder.WriteString(`1`) dateTime := time.Now().Format(time.RFC3339) - builder.WriteString(fmt.Sprintf(`%s`, dateTime)) - builder.WriteString(fmt.Sprintf(`%s`, dateTime)) - builder.WriteString(``) - builder.WriteString(``) - - return builder.String() + return fmt.Sprintf( + CORE_XML, + title, + subject, + creator, + description, + creator, + dateTime, + dateTime, + ) } +//go:embed templates/app.xml +var APP_XML string + func getAppXml(slides []*Slide, d2version string) string { var builder strings.Builder - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(`1`) - builder.WriteString(`0`) - builder.WriteString(`D2`) - builder.WriteString(`On-screen Show (4:3)`) - builder.WriteString(`0`) - builder.WriteString(fmt.Sprintf(`%d`, len(slides))) - builder.WriteString(`0`) - builder.WriteString(`0`) - builder.WriteString(`0`) - builder.WriteString(`false`) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(`Fonts`) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(`2`) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(`Theme`) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(`1`) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(`Slide Titles`) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(fmt.Sprintf(`%d`, len(slides))) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(``) - // number of entries, len(slides) + Office Theme + 2 Fonts - builder.WriteString(fmt.Sprintf(``, len(slides)+3)) - builder.WriteString(`Arial`) - builder.WriteString(`Calibri`) - builder.WriteString(`Office Theme`) for _, slide := range slides { builder.WriteString(fmt.Sprintf(`%s`, strings.Join(slide.BoardPath, "/"))) } - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(``) - builder.WriteString(`false`) - builder.WriteString(`false`) - builder.WriteString(``) - builder.WriteString(`false`) - builder.WriteString(fmt.Sprintf(`%s`, d2version)) - builder.WriteString(``) - return builder.String() + return fmt.Sprintf( + APP_XML, + len(slides), + len(slides), + len(slides)+3, // number of entries, len(slides) + Office Theme + 2 Fonts + builder.String(), + d2version, + ) } diff --git a/lib/pptx/templates/app.xml b/lib/pptx/templates/app.xml new file mode 100644 index 000000000..0400cf42f --- /dev/null +++ b/lib/pptx/templates/app.xml @@ -0,0 +1,51 @@ + + + 1 + 0 + D2 + On-screen Show (4:3) + 0 + %d + 0 + 0 + 0 + false + + + + Fonts + + + 2 + + + Theme + + + 1 + + + Slide Titles + + + %d + + + + + + Arial + Calibri + Office Theme + %s + + + + + false + false + + false + %s + \ No newline at end of file diff --git a/lib/pptx/templates/content_types.xml b/lib/pptx/templates/content_types.xml new file mode 100644 index 000000000..2a1b29ed7 --- /dev/null +++ b/lib/pptx/templates/content_types.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + %s + + + + \ No newline at end of file diff --git a/lib/pptx/templates/core.xml b/lib/pptx/templates/core.xml new file mode 100644 index 000000000..c61f6e489 --- /dev/null +++ b/lib/pptx/templates/core.xml @@ -0,0 +1,17 @@ + + + %s + %s + %s + + %s + %s + 1 + %s + %s + + \ No newline at end of file diff --git a/lib/pptx/templates/presentation.xml b/lib/pptx/templates/presentation.xml new file mode 100644 index 000000000..edd06d463 --- /dev/null +++ b/lib/pptx/templates/presentation.xml @@ -0,0 +1,132 @@ + + + + + + %s + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/pptx/templates/rels_presentation.xml b/lib/pptx/templates/rels_presentation.xml new file mode 100644 index 000000000..6f85deb57 --- /dev/null +++ b/lib/pptx/templates/rels_presentation.xml @@ -0,0 +1,19 @@ + + + + + + + + %s + \ No newline at end of file diff --git a/lib/pptx/templates/slide.xml b/lib/pptx/templates/slide.xml new file mode 100644 index 000000000..6bdaa177c --- /dev/null +++ b/lib/pptx/templates/slide.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %s + + + + + + + + \ No newline at end of file diff --git a/lib/pptx/templates/slide.xml.rels b/lib/pptx/templates/slide.xml.rels new file mode 100644 index 000000000..f7ea83ea6 --- /dev/null +++ b/lib/pptx/templates/slide.xml.rels @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/lib/pptx/validate.go b/lib/pptx/validate.go index 72a84859a..2d1dcf272 100644 --- a/lib/pptx/validate.go +++ b/lib/pptx/validate.go @@ -70,7 +70,7 @@ func checkFile(reader *zip.Reader, fname string) error { } func getExpectedPptxFileCount(nSlides int) int { - reader := bytes.NewReader(pptx_template) + reader := bytes.NewReader(PPTX_TEMPLATE) zipReader, err := zip.NewReader(reader, reader.Size()) if err != nil { return -1 From 0fad458858b6513820602f4bf948e143a6f3442f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 10 Apr 2023 15:45:10 -0300 Subject: [PATCH 16/20] generate files from templates --- lib/pptx/pptx.go | 165 ++++++++++++++--------- lib/pptx/presentation.go | 43 ++++-- lib/pptx/templates/app.xml | 16 +-- lib/pptx/templates/content_types.xml | 4 +- lib/pptx/templates/core.xml | 14 +- lib/pptx/templates/presentation.xml | 8 +- lib/pptx/templates/rels_presentation.xml | 4 +- lib/pptx/templates/slide.xml | 24 +++- lib/pptx/templates/slide.xml.rels | 4 +- 9 files changed, 176 insertions(+), 106 deletions(-) diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index 349d09393..897720e75 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -6,7 +6,7 @@ import ( _ "embed" "fmt" "strings" - "time" + "text/template" ) // Measurements in OOXML are made in English Metric Units (EMUs) where 1 inch = 914,400 EMUs @@ -41,111 +41,142 @@ func copyPptxTemplateTo(w *zip.Writer) error { return nil } -func addFile(zipFile *zip.Writer, filePath, content string) error { - w, err := zipFile.Create(filePath) - if err != nil { - return err - } - w.Write([]byte(content)) - return nil -} - //go:embed templates/slide.xml.rels var RELS_SLIDE_XML string -func getRelsSlideXml(imageID string) string { - return fmt.Sprintf(RELS_SLIDE_XML, imageID, imageID) +type RelsSlideXmlContent struct { + FileName string + RelationshipID string } //go:embed templates/slide.xml var SLIDE_XML string -func getSlideXml(boardPath []string, imageID string, top, left, width, height int) string { - var slideTitle string +type SlideXmlContent struct { + Title string + TitlePrefix string + Description string + HeaderHeight int + ImageID string + ImageLeft int + ImageTop int + ImageWidth int + ImageHeight int +} + +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, " / ") + " / " - slideTitle = fmt.Sprintf(`%s%s`, prefix, boardName) - } else { - slideTitle = fmt.Sprintf(`%s`, boardName) + prefix = strings.Join(prefixPath, " / ") + " / " + } + return SlideXmlContent{ + Title: boardName, + TitlePrefix: prefix, + Description: strings.Join(boardPath, " / "), + HeaderHeight: HEADER_HEIGHT, + ImageID: imageID, + ImageLeft: slide.ImageLeft, + ImageTop: slide.ImageTop + HEADER_HEIGHT, + ImageWidth: slide.ImageWidth, + ImageHeight: slide.ImageHeight, } - slideDescription := strings.Join(boardPath, " / ") - top += HEADER_HEIGHT - return fmt.Sprintf(SLIDE_XML, slideDescription, slideDescription, imageID, left, top, width, height, slideDescription, HEADER_HEIGHT, slideTitle) } //go:embed templates/rels_presentation.xml var RELS_PRESENTATION_XML string -func getPresentationXmlRels(slideFileNames []string) string { - var builder strings.Builder +type RelsPresentationSlideXmlContent struct { + RelationshipID string + FileName string +} + +type RelsPresentationXmlContent struct { + Slides []RelsPresentationSlideXmlContent +} + +func getRelsPresentationXmlContent(slideFileNames []string) RelsPresentationXmlContent { + var content RelsPresentationXmlContent for _, name := range slideFileNames { - builder.WriteString(fmt.Sprintf( - ``, name, name, - )) + content.Slides = append(content.Slides, RelsPresentationSlideXmlContent{ + RelationshipID: name, + FileName: name, + }) } - return fmt.Sprintf(RELS_PRESENTATION_XML, builder.String()) + return content } //go:embed templates/content_types.xml var CONTENT_TYPES_XML string -func getContentTypesXml(slideFileNames []string) string { - var builder strings.Builder - for _, name := range slideFileNames { - builder.WriteString(fmt.Sprintf( - ``, name, - )) - } - - return fmt.Sprintf(CONTENT_TYPES_XML, builder.String()) +type ContentTypesXmlContent struct { + FileNames []string } //go:embed templates/presentation.xml var PRESENTATION_XML string -func getPresentationXml(slideFileNames []string) string { - var builder strings.Builder - for i, name := range slideFileNames { - // in the exported presentation, the first slide ID was 256, so keeping it here for compatibility - builder.WriteString(fmt.Sprintf(``, 256+i, name)) +type PresentationSlideXmlContent struct { + ID int + RelationshipID string +} + +type PresentationXmlContent struct { + SlideWidth int + SlideHeight int + Slides []PresentationSlideXmlContent +} + +func getPresentationXmlContent(slideFileNames []string) PresentationXmlContent { + content := PresentationXmlContent{ + SlideWidth: SLIDE_WIDTH, + SlideHeight: SLIDE_HEIGHT, } - return fmt.Sprintf(PRESENTATION_XML, builder.String(), SLIDE_WIDTH, SLIDE_HEIGHT) + for i, name := range slideFileNames { + content.Slides = append(content.Slides, PresentationSlideXmlContent{ + // in the exported presentation, the first slide ID was 256, so keeping it here for compatibility + ID: 256 + i, + RelationshipID: name, + }) + } + return content } //go:embed templates/core.xml var CORE_XML string -func getCoreXml(title, subject, description, creator string) string { - dateTime := time.Now().Format(time.RFC3339) - return fmt.Sprintf( - CORE_XML, - title, - subject, - creator, - description, - creator, - dateTime, - dateTime, - ) +type CoreXmlContent struct { + Title string + Subject string + Creator string + Description string + LastModifiedBy string + Created string + Modified string } //go:embed templates/app.xml var APP_XML string -func getAppXml(slides []*Slide, d2version string) string { - var builder strings.Builder - for _, slide := range slides { - builder.WriteString(fmt.Sprintf(`%s`, strings.Join(slide.BoardPath, "/"))) - } - return fmt.Sprintf( - APP_XML, - len(slides), - len(slides), - len(slides)+3, // number of entries, len(slides) + Office Theme + 2 Fonts - builder.String(), - d2version, - ) +type AppXmlContent struct { + SlideCount int + TitlesOfPartsCount int + Titles []string + D2Version string +} + +func addFileFromTemplate(zipFile *zip.Writer, filePath, templateContent string, templateData interface{}) error { + w, err := zipFile.Create(filePath) + if err != nil { + return err + } + + tmpl, err := template.New(filePath).Parse(templateContent) + if err != nil { + return err + } + return tmpl.Execute(w, templateData) } diff --git a/lib/pptx/presentation.go b/lib/pptx/presentation.go index fea845fd5..43f3522da 100644 --- a/lib/pptx/presentation.go +++ b/lib/pptx/presentation.go @@ -16,6 +16,8 @@ import ( "fmt" "image/png" "os" + "strings" + "time" ) type Presentation struct { @@ -142,42 +144,61 @@ func (p *Presentation) SaveTo(filePath string) error { return err } - err = addFile(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), getRelsSlideXml(imageID)) + err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), RELS_SLIDE_XML, RelsSlideXmlContent{ + FileName: imageID, + RelationshipID: imageID, + }) if err != nil { return err } - err = addFile( - zipWriter, - fmt.Sprintf("ppt/slides/%s.xml", slideFileName), - getSlideXml(slide.BoardPath, imageID, slide.ImageTop, slide.ImageLeft, slide.ImageWidth, slide.ImageHeight), - ) + err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), SLIDE_XML, getSlideXmlContent(imageID, slide)) if err != nil { return err } } - err = addFile(zipWriter, "[Content_Types].xml", getContentTypesXml(slideFileNames)) + err = addFileFromTemplate(zipWriter, "[Content_Types].xml", CONTENT_TYPES_XML, ContentTypesXmlContent{ + FileNames: slideFileNames, + }) if err != nil { return err } - err = addFile(zipWriter, "ppt/_rels/presentation.xml.rels", getPresentationXmlRels(slideFileNames)) + err = addFileFromTemplate(zipWriter, "ppt/_rels/presentation.xml.rels", RELS_PRESENTATION_XML, getRelsPresentationXmlContent(slideFileNames)) if err != nil { return err } - err = addFile(zipWriter, "ppt/presentation.xml", getPresentationXml(slideFileNames)) + err = addFileFromTemplate(zipWriter, "ppt/presentation.xml", PRESENTATION_XML, getPresentationXmlContent(slideFileNames)) if err != nil { return err } - err = addFile(zipWriter, "docProps/core.xml", getCoreXml(p.Title, p.Subject, p.Description, p.Creator)) + dateTime := time.Now().Format(time.RFC3339) + err = addFileFromTemplate(zipWriter, "docProps/core.xml", CORE_XML, CoreXmlContent{ + Creator: p.Creator, + Subject: p.Subject, + Description: p.Description, + LastModifiedBy: p.Creator, + Title: p.Title, + Created: dateTime, + Modified: dateTime, + }) if err != nil { return err } - err = addFile(zipWriter, "docProps/app.xml", getAppXml(p.Slides, p.D2Version)) + titles := make([]string, 0, len(p.Slides)) + for _, slide := range p.Slides { + titles = append(titles, strings.Join(slide.BoardPath, "/")) + } + err = addFileFromTemplate(zipWriter, "docProps/app.xml", APP_XML, AppXmlContent{ + SlideCount: len(p.Slides), + TitlesOfPartsCount: len(p.Slides) + 3, + D2Version: p.D2Version, + Titles: titles, + }) if err != nil { return err } diff --git a/lib/pptx/templates/app.xml b/lib/pptx/templates/app.xml index 0400cf42f..ada0cf3db 100644 --- a/lib/pptx/templates/app.xml +++ b/lib/pptx/templates/app.xml @@ -4,9 +4,9 @@ 1 0 D2 - On-screen Show (4:3) + On-screen Show (16:9) 0 - %d + {{.SlideCount}} 0 0 0 @@ -29,23 +29,23 @@ Slide Titles - %d + {{.SlideCount}} - + Arial Calibri Office Theme - %s + {{range .Titles}} + {{.}} + {{end}} - - false false false - %s + {{.D2Version}} \ No newline at end of file diff --git a/lib/pptx/templates/content_types.xml b/lib/pptx/templates/content_types.xml index 2a1b29ed7..6d6106c76 100644 --- a/lib/pptx/templates/content_types.xml +++ b/lib/pptx/templates/content_types.xml @@ -54,7 +54,9 @@ - %s + {{range .FileNames}} + + {{end}} - %s - %s - %s + {{.Title}} + {{.Subject}} + {{.Creator}} - %s - %s + {{.Description}} + {{.LastModifiedBy}} 1 - %s - %s + {{.Created}} + {{.Modified}} \ No newline at end of file diff --git a/lib/pptx/templates/presentation.xml b/lib/pptx/templates/presentation.xml index edd06d463..4f3b0e744 100644 --- a/lib/pptx/templates/presentation.xml +++ b/lib/pptx/templates/presentation.xml @@ -6,8 +6,12 @@ - %s - + + {{range .Slides}} + + {{end}} + + diff --git a/lib/pptx/templates/rels_presentation.xml b/lib/pptx/templates/rels_presentation.xml index 6f85deb57..f7010c2c5 100644 --- a/lib/pptx/templates/rels_presentation.xml +++ b/lib/pptx/templates/rels_presentation.xml @@ -15,5 +15,7 @@ - %s + {{range .Slides}} + + {{end}} \ No newline at end of file diff --git a/lib/pptx/templates/slide.xml b/lib/pptx/templates/slide.xml index 6bdaa177c..2721068a1 100644 --- a/lib/pptx/templates/slide.xml +++ b/lib/pptx/templates/slide.xml @@ -19,22 +19,22 @@ - + - + - - + + @@ -43,14 +43,14 @@ - + - + @@ -75,7 +75,17 @@ - %s + + {{if .TitlePrefix}} + + {{.TitlePrefix}} + + {{end}} + + + {{.Title}} + + diff --git a/lib/pptx/templates/slide.xml.rels b/lib/pptx/templates/slide.xml.rels index f7ea83ea6..92475c305 100644 --- a/lib/pptx/templates/slide.xml.rels +++ b/lib/pptx/templates/slide.xml.rels @@ -3,7 +3,7 @@ - + Target="../media/{{.FileName}}.png" /> \ No newline at end of file From 442f61331e160cf4370108ac7cfa4d7f2813499a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 10 Apr 2023 15:47:37 -0300 Subject: [PATCH 17/20] move to a single file --- lib/pptx/pptx.go | 198 +++++++++++++++++++++++++++++++++++++ lib/pptx/presentation.go | 207 --------------------------------------- 2 files changed, 198 insertions(+), 207 deletions(-) delete mode 100644 lib/pptx/presentation.go diff --git a/lib/pptx/pptx.go b/lib/pptx/pptx.go index 897720e75..0063e0764 100644 --- a/lib/pptx/pptx.go +++ b/lib/pptx/pptx.go @@ -1,3 +1,12 @@ +// pptx is a package to create slide presentations in pptx (Microsoft Power Point) format. +// A `.pptx` file is just a bunch of zip compressed `.xml` files following the Office Open XML (OOXML) format. +// To see its content, you can just `unzip .pptx -d `. +// With this package, it is possible to create a `Presentation` and add `Slide`s to it. +// Then, when saving the presentation, it will generate the required `.xml` files, compress them and write to the disk. +// Note that this isn't a full implementation of the OOXML format, but a wrapper around it. +// There's a base template with common files to the presentation and then when saving, the package generate only the slides and relationships. +// The base template and slide templates were generated using https://python-pptx.readthedocs.io/en/latest/ +// For more information about OOXML, check http://officeopenxml.com/index.php package pptx import ( @@ -5,10 +14,199 @@ import ( "bytes" _ "embed" "fmt" + "image/png" + "os" "strings" "text/template" + "time" ) +type Presentation struct { + Title string + Description string + Subject string + Creator string + // D2Version can't have letters, only numbers (`[0-9]`) and `.` + // Otherwise, it may fail to open in PowerPoint + D2Version string + + Slides []*Slide +} + +type Slide struct { + BoardPath []string + Image []byte + ImageWidth int + ImageHeight int + ImageTop int + ImageLeft int +} + +func NewPresentation(title, description, subject, creator, d2Version string) *Presentation { + return &Presentation{ + Title: title, + Description: description, + Subject: subject, + Creator: creator, + D2Version: d2Version, + } +} + +func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { + src, err := png.Decode(bytes.NewReader(pngContent)) + if err != nil { + return fmt.Errorf("error decoding PNG image: %v", err) + } + + var width, height int + srcSize := src.Bounds().Size() + srcWidth, srcHeight := float64(srcSize.X), float64(srcSize.Y) + + // 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 + // ┌──────────────────────────────────────────────────┐ ─┬─ + // │ HEADER │ │ + // ├──┬────────────────────────────────────────────┬──┤ │ ─┬─ + // │ │ │ │ │ │ + // │ │ │ │ SLIDE │ + // │ │ │ │ HEIGHT │ + // │ │ │ │ │ IMAGE + // │ │ │ │ │ HEIGHT + // │ │ │ │ │ │ + // │ │ │ │ │ │ + // │ │ │ │ │ │ + // │ │ │ │ │ │ + // └──┴────────────────────────────────────────────┴──┘ ─┴─ ─┴─ + // ├────────────────────SLIDE WIDTH───────────────────┤ + // ├─────────────────IMAGE WIDTH────────────────┤ + if srcWidth/srcHeight >= IMAGE_ASPECT_RATIO { + // here, the image aspect ratio is, at least, equal to the slide aspect ratio + // so, it makes sense to expand the image horizontally to use as much as space as possible + width = SLIDE_WIDTH + height = int(float64(width) * (srcHeight / srcWidth)) + // first, try to make the image as wide as the slide + // but, if this results in a tall image, use only the + // image adjusted width to avoid overlapping with the header + if height > IMAGE_HEIGHT { + width = IMAGE_WIDTH + height = int(float64(width) * (srcHeight / srcWidth)) + } + } else { + // here, the aspect ratio could be 4x3, in which the image is still wider than taller, + // but expanding horizontally would result in an overflow + // so, we expand to make it fit the available vertical space + height = IMAGE_HEIGHT + width = int(float64(height) * (srcWidth / srcHeight)) + } + top := (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, + } + // it must copy the board path to avoid slice reference issues + copy(slide.BoardPath, boardPath) + + p.Slides = append(p.Slides, slide) + return nil +} + +func (p *Presentation) SaveTo(filePath string) error { + f, err := os.Create(filePath) + if err != nil { + return err + } + defer f.Close() + zipWriter := zip.NewWriter(f) + defer zipWriter.Close() + + if err = copyPptxTemplateTo(zipWriter); err != nil { + return err + } + + 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)) + if err != nil { + return err + } + _, err = imageWriter.Write(slide.Image) + if err != nil { + return err + } + + err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), RELS_SLIDE_XML, RelsSlideXmlContent{ + FileName: imageID, + RelationshipID: imageID, + }) + if err != nil { + return err + } + + err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), SLIDE_XML, getSlideXmlContent(imageID, slide)) + if err != nil { + return err + } + } + + err = addFileFromTemplate(zipWriter, "[Content_Types].xml", CONTENT_TYPES_XML, ContentTypesXmlContent{ + FileNames: slideFileNames, + }) + if err != nil { + return err + } + + err = addFileFromTemplate(zipWriter, "ppt/_rels/presentation.xml.rels", RELS_PRESENTATION_XML, getRelsPresentationXmlContent(slideFileNames)) + if err != nil { + return err + } + + err = addFileFromTemplate(zipWriter, "ppt/presentation.xml", PRESENTATION_XML, getPresentationXmlContent(slideFileNames)) + if err != nil { + return err + } + + dateTime := time.Now().Format(time.RFC3339) + err = addFileFromTemplate(zipWriter, "docProps/core.xml", CORE_XML, CoreXmlContent{ + Creator: p.Creator, + Subject: p.Subject, + Description: p.Description, + LastModifiedBy: p.Creator, + Title: p.Title, + Created: dateTime, + Modified: dateTime, + }) + if err != nil { + return err + } + + titles := make([]string, 0, len(p.Slides)) + for _, slide := range p.Slides { + titles = append(titles, strings.Join(slide.BoardPath, "/")) + } + err = addFileFromTemplate(zipWriter, "docProps/app.xml", APP_XML, AppXmlContent{ + SlideCount: len(p.Slides), + TitlesOfPartsCount: len(p.Slides) + 3, + D2Version: p.D2Version, + Titles: titles, + }) + if err != nil { + return err + } + + return nil +} + // Measurements in OOXML are made in English Metric Units (EMUs) where 1 inch = 914,400 EMUs // The intent is to have a measurement unit that doesn't require floating points when dealing with centimeters, inches, points (DPI). // Office Open XML (OOXML) http://officeopenxml.com/prPresentation.php diff --git a/lib/pptx/presentation.go b/lib/pptx/presentation.go deleted file mode 100644 index 43f3522da..000000000 --- a/lib/pptx/presentation.go +++ /dev/null @@ -1,207 +0,0 @@ -// pptx is a package to create slide presentations in pptx (Microsoft Power Point) format. -// A `.pptx` file is just a bunch of zip compressed `.xml` files following the Office Open XML (OOXML) format. -// To see its content, you can just `unzip .pptx -d `. -// With this package, it is possible to create a `Presentation` and add `Slide`s to it. -// Then, when saving the presentation, it will generate the required `.xml` files, compress them and write to the disk. -// Note that this isn't a full implementation of the OOXML format, but a wrapper around it. -// There's a base template with common files to the presentation and then when saving, the package generate only the slides and relationships. -// The base template and slide templates were generated using https://python-pptx.readthedocs.io/en/latest/ -// For more information about OOXML, check http://officeopenxml.com/index.php -package pptx - -import ( - "archive/zip" - "bytes" - _ "embed" - "fmt" - "image/png" - "os" - "strings" - "time" -) - -type Presentation struct { - Title string - Description string - Subject string - Creator string - // D2Version can't have letters, only numbers (`[0-9]`) and `.` - // Otherwise, it may fail to open in PowerPoint - D2Version string - - Slides []*Slide -} - -type Slide struct { - BoardPath []string - Image []byte - ImageWidth int - ImageHeight int - ImageTop int - ImageLeft int -} - -func NewPresentation(title, description, subject, creator, d2Version string) *Presentation { - return &Presentation{ - Title: title, - Description: description, - Subject: subject, - Creator: creator, - D2Version: d2Version, - } -} - -func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error { - src, err := png.Decode(bytes.NewReader(pngContent)) - if err != nil { - return fmt.Errorf("error decoding PNG image: %v", err) - } - - var width, height int - srcSize := src.Bounds().Size() - srcWidth, srcHeight := float64(srcSize.X), float64(srcSize.Y) - - // 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 - // ┌──────────────────────────────────────────────────┐ ─┬─ - // │ HEADER │ │ - // ├──┬────────────────────────────────────────────┬──┤ │ ─┬─ - // │ │ │ │ │ │ - // │ │ │ │ SLIDE │ - // │ │ │ │ HEIGHT │ - // │ │ │ │ │ IMAGE - // │ │ │ │ │ HEIGHT - // │ │ │ │ │ │ - // │ │ │ │ │ │ - // │ │ │ │ │ │ - // │ │ │ │ │ │ - // └──┴────────────────────────────────────────────┴──┘ ─┴─ ─┴─ - // ├────────────────────SLIDE WIDTH───────────────────┤ - // ├─────────────────IMAGE WIDTH────────────────┤ - if srcWidth/srcHeight >= IMAGE_ASPECT_RATIO { - // here, the image aspect ratio is, at least, equal to the slide aspect ratio - // so, it makes sense to expand the image horizontally to use as much as space as possible - width = SLIDE_WIDTH - height = int(float64(width) * (srcHeight / srcWidth)) - // first, try to make the image as wide as the slide - // but, if this results in a tall image, use only the - // image adjusted width to avoid overlapping with the header - if height > IMAGE_HEIGHT { - width = IMAGE_WIDTH - height = int(float64(width) * (srcHeight / srcWidth)) - } - } else { - // here, the aspect ratio could be 4x3, in which the image is still wider than taller, - // but expanding horizontally would result in an overflow - // so, we expand to make it fit the available vertical space - height = IMAGE_HEIGHT - width = int(float64(height) * (srcWidth / srcHeight)) - } - top := (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, - } - // it must copy the board path to avoid slice reference issues - copy(slide.BoardPath, boardPath) - - p.Slides = append(p.Slides, slide) - return nil -} - -func (p *Presentation) SaveTo(filePath string) error { - f, err := os.Create(filePath) - if err != nil { - return err - } - defer f.Close() - zipWriter := zip.NewWriter(f) - defer zipWriter.Close() - - if err = copyPptxTemplateTo(zipWriter); err != nil { - return err - } - - 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)) - if err != nil { - return err - } - _, err = imageWriter.Write(slide.Image) - if err != nil { - return err - } - - err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), RELS_SLIDE_XML, RelsSlideXmlContent{ - FileName: imageID, - RelationshipID: imageID, - }) - if err != nil { - return err - } - - err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), SLIDE_XML, getSlideXmlContent(imageID, slide)) - if err != nil { - return err - } - } - - err = addFileFromTemplate(zipWriter, "[Content_Types].xml", CONTENT_TYPES_XML, ContentTypesXmlContent{ - FileNames: slideFileNames, - }) - if err != nil { - return err - } - - err = addFileFromTemplate(zipWriter, "ppt/_rels/presentation.xml.rels", RELS_PRESENTATION_XML, getRelsPresentationXmlContent(slideFileNames)) - if err != nil { - return err - } - - err = addFileFromTemplate(zipWriter, "ppt/presentation.xml", PRESENTATION_XML, getPresentationXmlContent(slideFileNames)) - if err != nil { - return err - } - - dateTime := time.Now().Format(time.RFC3339) - err = addFileFromTemplate(zipWriter, "docProps/core.xml", CORE_XML, CoreXmlContent{ - Creator: p.Creator, - Subject: p.Subject, - Description: p.Description, - LastModifiedBy: p.Creator, - Title: p.Title, - Created: dateTime, - Modified: dateTime, - }) - if err != nil { - return err - } - - titles := make([]string, 0, len(p.Slides)) - for _, slide := range p.Slides { - titles = append(titles, strings.Join(slide.BoardPath, "/")) - } - err = addFileFromTemplate(zipWriter, "docProps/app.xml", APP_XML, AppXmlContent{ - SlideCount: len(p.Slides), - TitlesOfPartsCount: len(p.Slides) + 3, - D2Version: p.D2Version, - Titles: titles, - }) - if err != nil { - return err - } - - return nil -} From 4613ea36352390e25a61defd44d77af323fef6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 10 Apr 2023 15:51:38 -0300 Subject: [PATCH 18/20] return svg --- d2cli/main.go | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index d2998e0b5..06f4afb6f 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -368,7 +368,7 @@ 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()) - err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil) + svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil) if err != nil { return nil, false, err } @@ -378,7 +378,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende } dur := time.Since(start) ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur) - return nil, true, nil + return svg, true, nil default: compileDur := time.Since(start) if animateInterval <= 0 { @@ -757,7 +757,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, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []string) ([]byte, error) { var currBoardPath []string // Root board doesn't have a name, so we use the output filename if diagram.Name == "" { @@ -766,65 +766,66 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present currBoardPath = append(boardPath, diagram.Name) } + var svg []byte if !diagram.IsFolderOnly { // gofpdf will print the png img with a slight filter // make the bg fill within the png transparent so that the pdf bg fill is the only bg color present diagram.Root.Fill = "transparent" - svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{ + var err error + svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{ Pad: opts.Pad, Sketch: opts.Sketch, Center: opts.Center, SetDimensions: true, }) if err != nil { - return err + return nil, err } svg, err = plugin.PostProcess(ctx, svg) if err != nil { - return err + return nil, err } svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg) svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg) bundleErr = multierr.Combine(bundleErr, bundleErr2) if bundleErr != nil { - return bundleErr + return nil, bundleErr } pngImg, err := png.ConvertSVG(ms, page, svg) if err != nil { - return err + return nil, err } err = presentation.AddSlide(pngImg, currBoardPath) if err != nil { - return err + return nil, err } } for _, dl := range diagram.Layers { - err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) + _, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) if err != nil { - return err + return nil, err } } for _, dl := range diagram.Scenarios { - err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) + _, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) if err != nil { - return err + return nil, err } } for _, dl := range diagram.Steps { - err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) + _, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath) if err != nil { - return err + return nil, err } } - // TODO: return SVG? - return nil + return svg, nil } // newExt must include leading . From 1fdadd7e1b57317bf6b7a53b2b9cd568d58a0e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 10 Apr 2023 15:59:52 -0300 Subject: [PATCH 19/20] rebuild From 596d03315e1554075ef1ca5d9422034b333d1770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 10 Apr 2023 16:29:27 -0300 Subject: [PATCH 20/20] changelog and ci --- ci/release/changelogs/next.md | 2 ++ e2etests-cli/main_test.go | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index f3c0d2a77..e5fc0495e 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -1,5 +1,7 @@ #### Features 🚀 +- Export diagrams to `.pptx` (PowerPoint)[#1139](https://github.com/terrastruct/d2/pull/1139) + #### Improvements 🧹 #### Bugfixes ⛑️ diff --git a/e2etests-cli/main_test.go b/e2etests-cli/main_test.go index fbe8a7162..3bf8d24a3 100644 --- a/e2etests-cli/main_test.go +++ b/e2etests-cli/main_test.go @@ -246,7 +246,8 @@ layers: { }, }, { - name: "how_to_solve_problems_pptx", + 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? steps: {