Merge pull request #1139 from ejulio-ts/gh-821-ppt
GH 821: PowerPoint Export
This commit is contained in:
commit
b84095b963
14 changed files with 1003 additions and 8 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
#### Features 🚀
|
#### Features 🚀
|
||||||
|
|
||||||
|
- Export diagrams to `.pptx` (PowerPoint)[#1139](https://github.com/terrastruct/d2/pull/1139)
|
||||||
|
|
||||||
#### Improvements 🧹
|
#### Improvements 🧹
|
||||||
|
|
||||||
#### Bugfixes ⛑️
|
#### Bugfixes ⛑️
|
||||||
|
|
|
||||||
111
d2cli/main.go
111
d2cli/main.go
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -34,6 +35,7 @@ import (
|
||||||
"oss.terrastruct.com/d2/lib/pdf"
|
"oss.terrastruct.com/d2/lib/pdf"
|
||||||
pdflib "oss.terrastruct.com/d2/lib/pdf"
|
pdflib "oss.terrastruct.com/d2/lib/pdf"
|
||||||
"oss.terrastruct.com/d2/lib/png"
|
"oss.terrastruct.com/d2/lib/png"
|
||||||
|
"oss.terrastruct.com/d2/lib/pptx"
|
||||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||||
"oss.terrastruct.com/d2/lib/version"
|
"oss.terrastruct.com/d2/lib/version"
|
||||||
|
|
||||||
|
|
@ -234,7 +236,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, plocation)
|
ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, plocation)
|
||||||
|
|
||||||
var pw png.Playwright
|
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 {
|
if darkThemeFlag != nil {
|
||||||
ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg")
|
ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg")
|
||||||
darkThemeFlag = nil
|
darkThemeFlag = nil
|
||||||
|
|
@ -347,7 +349,8 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if filepath.Ext(outputPath) == ".pdf" {
|
switch filepath.Ext(outputPath) {
|
||||||
|
case ".pdf":
|
||||||
pageMap := pdf.BuildPDFPageMap(diagram, nil, nil)
|
pageMap := pdf.BuildPDFPageMap(diagram, nil, nil)
|
||||||
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap)
|
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -356,7 +359,27 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
|
||||||
dur := time.Since(start)
|
dur := time.Since(start)
|
||||||
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
|
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
|
||||||
return pdf, true, nil
|
return pdf, true, nil
|
||||||
} else {
|
case ".pptx":
|
||||||
|
var username string
|
||||||
|
if user, err := user.Current(); err == nil {
|
||||||
|
username = user.Username
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
svg, 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 svg, true, nil
|
||||||
|
default:
|
||||||
compileDur := time.Since(start)
|
compileDur := time.Since(start)
|
||||||
if animateInterval <= 0 {
|
if animateInterval <= 0 {
|
||||||
// Rename all the "root.layers.x" to the paths that the boards get output to
|
// Rename all the "root.layers.x" to the paths that the boards get output to
|
||||||
|
|
@ -651,11 +674,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
||||||
var currBoardPath []string
|
var currBoardPath []string
|
||||||
// Root board doesn't have a name, so we use the output filename
|
// Root board doesn't have a name, so we use the output filename
|
||||||
if diagram.Name == "" {
|
if diagram.Name == "" {
|
||||||
ext := filepath.Ext(outputPath)
|
currBoardPath = append(boardPath, getFileName(outputPath))
|
||||||
trimmedPath := strings.TrimSuffix(outputPath, ext)
|
|
||||||
splitPath := strings.Split(trimmedPath, "/")
|
|
||||||
rootName := splitPath[len(splitPath)-1]
|
|
||||||
currBoardPath = append(boardPath, rootName)
|
|
||||||
} else {
|
} else {
|
||||||
currBoardPath = append(boardPath, diagram.Name)
|
currBoardPath = append(boardPath, diagram.Name)
|
||||||
}
|
}
|
||||||
|
|
@ -738,6 +757,77 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
||||||
return svg, nil
|
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) ([]byte, error) {
|
||||||
|
var currBoardPath []string
|
||||||
|
// Root board doesn't have a name, so we use the output filename
|
||||||
|
if diagram.Name == "" {
|
||||||
|
currBoardPath = append(boardPath, getFileName(outputPath))
|
||||||
|
} else {
|
||||||
|
currBoardPath = append(boardPath, diagram.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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 nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
svg, err = plugin.PostProcess(ctx, svg)
|
||||||
|
if err != nil {
|
||||||
|
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 nil, bundleErr
|
||||||
|
}
|
||||||
|
|
||||||
|
pngImg, err := png.ConvertSVG(ms, page, svg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = presentation.AddSlide(pngImg, currBoardPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dl := range diagram.Layers {
|
||||||
|
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, dl := range diagram.Scenarios {
|
||||||
|
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, dl := range diagram.Steps {
|
||||||
|
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return svg, nil
|
||||||
|
}
|
||||||
|
|
||||||
// newExt must include leading .
|
// newExt must include leading .
|
||||||
func renameExt(fp string, newExt string) string {
|
func renameExt(fp string, newExt string) string {
|
||||||
ext := filepath.Ext(fp)
|
ext := filepath.Ext(fp)
|
||||||
|
|
@ -748,6 +838,11 @@ func renameExt(fp string, newExt string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getFileName(path string) string {
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
return strings.TrimSuffix(filepath.Base(path), ext)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove after removing slog
|
// TODO: remove after removing slog
|
||||||
func DiscardSlog(ctx context.Context) context.Context {
|
func DiscardSlog(ctx context.Context) context.Context {
|
||||||
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
|
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2cli"
|
"oss.terrastruct.com/d2/d2cli"
|
||||||
|
"oss.terrastruct.com/d2/lib/pptx"
|
||||||
"oss.terrastruct.com/util-go/assert"
|
"oss.terrastruct.com/util-go/assert"
|
||||||
"oss.terrastruct.com/util-go/diff"
|
"oss.terrastruct.com/util-go/diff"
|
||||||
"oss.terrastruct.com/util-go/xmain"
|
"oss.terrastruct.com/util-go/xmain"
|
||||||
|
|
@ -244,6 +245,33 @@ layers: {
|
||||||
testdataIgnoreDiff(t, ".pdf", pdf)
|
testdataIgnoreDiff(t, ".pdf", pdf)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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: {
|
||||||
|
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",
|
name: "stdin",
|
||||||
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
|
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
|
||||||
|
|
|
||||||
380
lib/pptx/pptx.go
Normal file
380
lib/pptx/pptx.go
Normal file
|
|
@ -0,0 +1,380 @@
|
||||||
|
// 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 <path/to/file>.pptx -d <folder>`.
|
||||||
|
// 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"
|
||||||
|
"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
|
||||||
|
// 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
|
||||||
|
|
||||||
|
func copyPptxTemplateTo(w *zip.Writer) error {
|
||||||
|
reader := bytes.NewReader(PPTX_TEMPLATE)
|
||||||
|
zipReader, err := zip.NewReader(reader, reader.Size())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error creating zip reader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range zipReader.File {
|
||||||
|
if err := w.Copy(f); err != nil {
|
||||||
|
return fmt.Errorf("error copying %s: %v", f.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed templates/slide.xml.rels
|
||||||
|
var RELS_SLIDE_XML string
|
||||||
|
|
||||||
|
type RelsSlideXmlContent struct {
|
||||||
|
FileName string
|
||||||
|
RelationshipID string
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed templates/slide.xml
|
||||||
|
var SLIDE_XML 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, " / ") + " / "
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed templates/rels_presentation.xml
|
||||||
|
var RELS_PRESENTATION_XML string
|
||||||
|
|
||||||
|
type RelsPresentationSlideXmlContent struct {
|
||||||
|
RelationshipID string
|
||||||
|
FileName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RelsPresentationXmlContent struct {
|
||||||
|
Slides []RelsPresentationSlideXmlContent
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRelsPresentationXmlContent(slideFileNames []string) RelsPresentationXmlContent {
|
||||||
|
var content RelsPresentationXmlContent
|
||||||
|
for _, name := range slideFileNames {
|
||||||
|
content.Slides = append(content.Slides, RelsPresentationSlideXmlContent{
|
||||||
|
RelationshipID: name,
|
||||||
|
FileName: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed templates/content_types.xml
|
||||||
|
var CONTENT_TYPES_XML string
|
||||||
|
|
||||||
|
type ContentTypesXmlContent struct {
|
||||||
|
FileNames []string
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed templates/presentation.xml
|
||||||
|
var PRESENTATION_XML string
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
BIN
lib/pptx/template.pptx
Normal file
BIN
lib/pptx/template.pptx
Normal file
Binary file not shown.
51
lib/pptx/templates/app.xml
Normal file
51
lib/pptx/templates/app.xml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
|
||||||
|
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
|
||||||
|
<TotalTime>1</TotalTime>
|
||||||
|
<Words>0</Words>
|
||||||
|
<Application>D2</Application>
|
||||||
|
<PresentationFormat>On-screen Show (16:9)</PresentationFormat>
|
||||||
|
<Paragraphs>0</Paragraphs>
|
||||||
|
<Slides>{{.SlideCount}}</Slides>
|
||||||
|
<Notes>0</Notes>
|
||||||
|
<HiddenSlides>0</HiddenSlides>
|
||||||
|
<MMClips>0</MMClips>
|
||||||
|
<ScaleCrop>false</ScaleCrop>
|
||||||
|
<HeadingPairs>
|
||||||
|
<vt:vector size="6" baseType="variant">
|
||||||
|
<vt:variant>
|
||||||
|
<vt:lpstr>Fonts</vt:lpstr>
|
||||||
|
</vt:variant>
|
||||||
|
<vt:variant>
|
||||||
|
<vt:i4>2</vt:i4>
|
||||||
|
</vt:variant>
|
||||||
|
<vt:variant>
|
||||||
|
<vt:lpstr>Theme</vt:lpstr>
|
||||||
|
</vt:variant>
|
||||||
|
<vt:variant>
|
||||||
|
<vt:i4>1</vt:i4>
|
||||||
|
</vt:variant>
|
||||||
|
<vt:variant>
|
||||||
|
<vt:lpstr>Slide Titles</vt:lpstr>
|
||||||
|
</vt:variant>
|
||||||
|
<vt:variant>
|
||||||
|
<vt:i4>{{.SlideCount}}</vt:i4>
|
||||||
|
</vt:variant>
|
||||||
|
</vt:vector>
|
||||||
|
</HeadingPairs>
|
||||||
|
<TitlesOfParts>
|
||||||
|
<vt:vector size="{{.TitlesOfPartsCount}}" baseType="lpstr">
|
||||||
|
<vt:lpstr>Arial</vt:lpstr>
|
||||||
|
<vt:lpstr>Calibri</vt:lpstr>
|
||||||
|
<vt:lpstr>Office Theme</vt:lpstr>
|
||||||
|
{{range .Titles}}
|
||||||
|
<vt:lpstr>{{.}}</vt:lpstr>
|
||||||
|
{{end}}
|
||||||
|
</vt:vector>
|
||||||
|
</TitlesOfParts>
|
||||||
|
<LinksUpToDate>false</LinksUpToDate>
|
||||||
|
<SharedDoc>false</SharedDoc>
|
||||||
|
<HyperlinkBase></HyperlinkBase>
|
||||||
|
<HyperlinksChanged>false</HyperlinksChanged>
|
||||||
|
<AppVersion>{{.D2Version}}</AppVersion>
|
||||||
|
</Properties>
|
||||||
68
lib/pptx/templates/content_types.xml
Normal file
68
lib/pptx/templates/content_types.xml
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||||
|
<Default
|
||||||
|
Extension="jpeg" ContentType="image/jpeg" />
|
||||||
|
<Default Extension="png" ContentType="image/png" />
|
||||||
|
<Default
|
||||||
|
Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
|
||||||
|
<Default
|
||||||
|
Extension="xml" ContentType="application/xml" />
|
||||||
|
<Override PartName="/docProps/app.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/docProps/core.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-package.core-properties+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/presProps.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/presentation.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout1.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout10.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout11.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout2.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout3.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout4.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout5.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout6.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout7.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout8.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideLayouts/slideLayout9.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/slideMasters/slideMaster1.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml" />
|
||||||
|
{{range .FileNames}}
|
||||||
|
<Override PartName="/ppt/slides/{{.}}.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml" />
|
||||||
|
{{end}}
|
||||||
|
<Override PartName="/ppt/tableStyles.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/theme/theme1.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.theme+xml" />
|
||||||
|
<Override
|
||||||
|
PartName="/ppt/viewProps.xml"
|
||||||
|
ContentType="application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml" />
|
||||||
|
</Types>
|
||||||
17
lib/pptx/templates/core.xml
Normal file
17
lib/pptx/templates/core.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<cp:coreProperties
|
||||||
|
xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"
|
||||||
|
xmlns:dcmitype="http://purl.org/dc/dcmitype/"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<dc:title>{{.Title}}</dc:title>
|
||||||
|
<dc:subject>{{.Subject}}</dc:subject>
|
||||||
|
<dc:creator>{{.Creator}}</dc:creator>
|
||||||
|
<cp:keywords />
|
||||||
|
<dc:description>{{.Description}}</dc:description>
|
||||||
|
<cp:lastModifiedBy>{{.LastModifiedBy}}</cp:lastModifiedBy>
|
||||||
|
<cp:revision>1</cp:revision>
|
||||||
|
<dcterms:created xsi:type="dcterms:W3CDTF">{{.Created}}</dcterms:created>
|
||||||
|
<dcterms:modified xsi:type="dcterms:W3CDTF">{{.Modified}}</dcterms:modified>
|
||||||
|
<cp:category />
|
||||||
|
</cp:coreProperties>
|
||||||
136
lib/pptx/templates/presentation.xml
Normal file
136
lib/pptx/templates/presentation.xml
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||||
|
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" saveSubsetFonts="1"
|
||||||
|
autoCompressPictures="0">
|
||||||
|
<p:sldMasterIdLst>
|
||||||
|
<p:sldMasterId id="2147483648" r:id="rId1" />
|
||||||
|
</p:sldMasterIdLst>
|
||||||
|
<p:sldIdLst>
|
||||||
|
{{range .Slides}}
|
||||||
|
<p:sldId id="{{.ID}}" r:id="{{.RelationshipID}}" />
|
||||||
|
{{end}}
|
||||||
|
</p:sldIdLst>
|
||||||
|
<p:sldSz cx="{{.SlideWidth}}" cy="{{.SlideHeight}}" type="screen4x3" />
|
||||||
|
<p:notesSz cx="6858000" cy="9144000" />
|
||||||
|
<p:defaultTextStyle>
|
||||||
|
<a:defPPr>
|
||||||
|
<a:defRPr lang="en-US" />
|
||||||
|
</a:defPPr>
|
||||||
|
<a:lvl1pPr marL="0" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0"
|
||||||
|
hangingPunct="1">
|
||||||
|
<a:defRPr sz="1800" kern="1200">
|
||||||
|
<a:solidFill>
|
||||||
|
<a:schemeClr val="tx1" />
|
||||||
|
</a:solidFill>
|
||||||
|
<a:latin typeface="+mn-lt" />
|
||||||
|
<a:ea typeface="+mn-ea" />
|
||||||
|
<a:cs typeface="+mn-cs" />
|
||||||
|
</a:defRPr>
|
||||||
|
</a:lvl1pPr>
|
||||||
|
<a:lvl2pPr marL="457200" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0"
|
||||||
|
hangingPunct="1">
|
||||||
|
<a:defRPr sz="1800" kern="1200">
|
||||||
|
<a:solidFill>
|
||||||
|
<a:schemeClr val="tx1" />
|
||||||
|
</a:solidFill>
|
||||||
|
<a:latin typeface="+mn-lt" />
|
||||||
|
<a:ea typeface="+mn-ea" />
|
||||||
|
<a:cs typeface="+mn-cs" />
|
||||||
|
</a:defRPr>
|
||||||
|
</a:lvl2pPr>
|
||||||
|
<a:lvl3pPr marL="914400" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0"
|
||||||
|
hangingPunct="1">
|
||||||
|
<a:defRPr sz="1800" kern="1200">
|
||||||
|
<a:solidFill>
|
||||||
|
<a:schemeClr val="tx1" />
|
||||||
|
</a:solidFill>
|
||||||
|
<a:latin typeface="+mn-lt" />
|
||||||
|
<a:ea typeface="+mn-ea" />
|
||||||
|
<a:cs typeface="+mn-cs" />
|
||||||
|
</a:defRPr>
|
||||||
|
</a:lvl3pPr>
|
||||||
|
<a:lvl4pPr marL="1371600" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0"
|
||||||
|
hangingPunct="1">
|
||||||
|
<a:defRPr sz="1800" kern="1200">
|
||||||
|
<a:solidFill>
|
||||||
|
<a:schemeClr val="tx1" />
|
||||||
|
</a:solidFill>
|
||||||
|
<a:latin typeface="+mn-lt" />
|
||||||
|
<a:ea typeface="+mn-ea" />
|
||||||
|
<a:cs typeface="+mn-cs" />
|
||||||
|
</a:defRPr>
|
||||||
|
</a:lvl4pPr>
|
||||||
|
<a:lvl5pPr marL="1828800" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0"
|
||||||
|
hangingPunct="1">
|
||||||
|
<a:defRPr sz="1800" kern="1200">
|
||||||
|
<a:solidFill>
|
||||||
|
<a:schemeClr val="tx1" />
|
||||||
|
</a:solidFill>
|
||||||
|
<a:latin typeface="+mn-lt" />
|
||||||
|
<a:ea typeface="+mn-ea" />
|
||||||
|
<a:cs typeface="+mn-cs" />
|
||||||
|
</a:defRPr>
|
||||||
|
</a:lvl5pPr>
|
||||||
|
<a:lvl6pPr marL="2286000" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0"
|
||||||
|
hangingPunct="1">
|
||||||
|
<a:defRPr sz="1800" kern="1200">
|
||||||
|
<a:solidFill>
|
||||||
|
<a:schemeClr val="tx1" />
|
||||||
|
</a:solidFill>
|
||||||
|
<a:latin typeface="+mn-lt" />
|
||||||
|
<a:ea typeface="+mn-ea" />
|
||||||
|
<a:cs typeface="+mn-cs" />
|
||||||
|
</a:defRPr>
|
||||||
|
</a:lvl6pPr>
|
||||||
|
<a:lvl7pPr marL="2743200" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0"
|
||||||
|
hangingPunct="1">
|
||||||
|
<a:defRPr sz="1800" kern="1200">
|
||||||
|
<a:solidFill>
|
||||||
|
<a:schemeClr val="tx1" />
|
||||||
|
</a:solidFill>
|
||||||
|
<a:latin typeface="+mn-lt" />
|
||||||
|
<a:ea typeface="+mn-ea" />
|
||||||
|
<a:cs typeface="+mn-cs" />
|
||||||
|
</a:defRPr>
|
||||||
|
</a:lvl7pPr>
|
||||||
|
<a:lvl8pPr marL="3200400" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0"
|
||||||
|
hangingPunct="1">
|
||||||
|
<a:defRPr sz="1800" kern="1200">
|
||||||
|
<a:solidFill>
|
||||||
|
<a:schemeClr val="tx1" />
|
||||||
|
</a:solidFill>
|
||||||
|
<a:latin typeface="+mn-lt" />
|
||||||
|
<a:ea typeface="+mn-ea" />
|
||||||
|
<a:cs typeface="+mn-cs" />
|
||||||
|
</a:defRPr>
|
||||||
|
</a:lvl8pPr>
|
||||||
|
<a:lvl9pPr marL="3657600" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0"
|
||||||
|
hangingPunct="1">
|
||||||
|
<a:defRPr sz="1800" kern="1200">
|
||||||
|
<a:solidFill>
|
||||||
|
<a:schemeClr val="tx1" />
|
||||||
|
</a:solidFill>
|
||||||
|
<a:latin typeface="+mn-lt" />
|
||||||
|
<a:ea typeface="+mn-ea" />
|
||||||
|
<a:cs typeface="+mn-cs" />
|
||||||
|
</a:defRPr>
|
||||||
|
</a:lvl9pPr>
|
||||||
|
</p:defaultTextStyle>
|
||||||
|
<p:extLst>
|
||||||
|
<p:ext uri="{EFAFB233-063F-42B5-8137-9DF3F51BA10A}">
|
||||||
|
<p15:sldGuideLst xmlns:p15="http://schemas.microsoft.com/office/powerpoint/2012/main">
|
||||||
|
<p15:guide id="1" orient="horz" pos="2160">
|
||||||
|
<p15:clr>
|
||||||
|
<a:srgbClr val="A4A3A4" />
|
||||||
|
</p15:clr>
|
||||||
|
</p15:guide>
|
||||||
|
<p15:guide id="2" pos="2880">
|
||||||
|
<p15:clr>
|
||||||
|
<a:srgbClr val="A4A3A4" />
|
||||||
|
</p15:clr>
|
||||||
|
</p15:guide>
|
||||||
|
</p15:sldGuideLst>
|
||||||
|
</p:ext>
|
||||||
|
</p:extLst>
|
||||||
|
</p:presentation>
|
||||||
21
lib/pptx/templates/rels_presentation.xml
Normal file
21
lib/pptx/templates/rels_presentation.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Id="rId3"
|
||||||
|
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps"
|
||||||
|
Target="presProps.xml" />
|
||||||
|
<Relationship Id="rId4"
|
||||||
|
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps"
|
||||||
|
Target="viewProps.xml" />
|
||||||
|
<Relationship Id="rId5"
|
||||||
|
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
|
||||||
|
Target="theme/theme1.xml" />
|
||||||
|
<Relationship Id="rId6"
|
||||||
|
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles"
|
||||||
|
Target="tableStyles.xml" />
|
||||||
|
<Relationship Id="rId1"
|
||||||
|
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"
|
||||||
|
Target="slideMasters/slideMaster1.xml" />
|
||||||
|
{{range .Slides}}
|
||||||
|
<Relationship Id="{{.RelationshipID}}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/{{.FileName}}.xml" />
|
||||||
|
{{end}}
|
||||||
|
</Relationships>
|
||||||
96
lib/pptx/templates/slide.xml
Normal file
96
lib/pptx/templates/slide.xml
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||||
|
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||||
|
<p:cSld>
|
||||||
|
<p:spTree>
|
||||||
|
<p:nvGrpSpPr>
|
||||||
|
<p:cNvPr id="1" name="" />
|
||||||
|
<p:cNvGrpSpPr />
|
||||||
|
<p:nvPr />
|
||||||
|
</p:nvGrpSpPr>
|
||||||
|
<p:grpSpPr>
|
||||||
|
<a:xfrm>
|
||||||
|
<a:off x="0" y="0" />
|
||||||
|
<a:ext cx="0" cy="0" />
|
||||||
|
<a:chOff x="0" y="0" />
|
||||||
|
<a:chExt cx="0" cy="0" />
|
||||||
|
</a:xfrm>
|
||||||
|
</p:grpSpPr>
|
||||||
|
<p:pic>
|
||||||
|
<p:nvPicPr>
|
||||||
|
<p:cNvPr id="2" name="{{.Description}}" descr="{{.Description}}" />
|
||||||
|
<p:cNvPicPr>
|
||||||
|
<a:picLocks noChangeAspect="1" />
|
||||||
|
</p:cNvPicPr>
|
||||||
|
<p:nvPr />
|
||||||
|
</p:nvPicPr>
|
||||||
|
<p:blipFill>
|
||||||
|
<a:blip r:embed="{{.ImageID}}" />
|
||||||
|
<a:stretch>
|
||||||
|
<a:fillRect />
|
||||||
|
</a:stretch>
|
||||||
|
</p:blipFill>
|
||||||
|
<p:spPr>
|
||||||
|
<a:xfrm>
|
||||||
|
<a:off x="{{.ImageLeft}}" y="{{.ImageTop}}" />
|
||||||
|
<a:ext cx="{{.ImageWidth}}" cy="{{.ImageHeight}}" />
|
||||||
|
</a:xfrm>
|
||||||
|
<a:prstGeom prst="rect">
|
||||||
|
<a:avLst />
|
||||||
|
</a:prstGeom>
|
||||||
|
</p:spPr>
|
||||||
|
</p:pic>
|
||||||
|
<p:sp>
|
||||||
|
<p:nvSpPr>
|
||||||
|
<p:cNvPr id="95" name="{{.Description}}" />
|
||||||
|
<p:cNvSpPr txBox="1" />
|
||||||
|
<p:nvPr />
|
||||||
|
</p:nvSpPr>
|
||||||
|
<p:spPr>
|
||||||
|
<a:xfrm>
|
||||||
|
<a:off x="4001" y="6239" />
|
||||||
|
<a:ext cx="9135998" cy="{{.HeaderHeight}}" />
|
||||||
|
</a:xfrm>
|
||||||
|
<a:prstGeom prst="rect">
|
||||||
|
<a:avLst />
|
||||||
|
</a:prstGeom>
|
||||||
|
<a:ln w="12700">
|
||||||
|
<a:miter lim="400000" />
|
||||||
|
</a:ln>
|
||||||
|
<a:extLst>
|
||||||
|
<a:ext uri="{C572A759-6A51-4108-AA02-DFA0A04FC94B}">
|
||||||
|
<ma14:wrappingTextBoxFlag
|
||||||
|
xmlns:ma14="http://schemas.microsoft.com/office/mac/drawingml/2011/main"
|
||||||
|
xmlns="" val="1" />
|
||||||
|
</a:ext>
|
||||||
|
</a:extLst>
|
||||||
|
</p:spPr>
|
||||||
|
<p:txBody>
|
||||||
|
<a:bodyPr lIns="45719" rIns="45719">
|
||||||
|
<a:spAutoFit />
|
||||||
|
</a:bodyPr>
|
||||||
|
<a:lstStyle>
|
||||||
|
<a:lvl1pPr>
|
||||||
|
<a:defRPr sz="2400" />
|
||||||
|
</a:lvl1pPr>
|
||||||
|
</a:lstStyle>
|
||||||
|
<a:p>
|
||||||
|
{{if .TitlePrefix}}
|
||||||
|
<a:r>
|
||||||
|
<a:t>{{.TitlePrefix}}</a:t>
|
||||||
|
</a:r>
|
||||||
|
{{end}}
|
||||||
|
<a:r>
|
||||||
|
<a:rPr b="1" />
|
||||||
|
<a:t>{{.Title}}</a:t>
|
||||||
|
</a:r>
|
||||||
|
</a:p>
|
||||||
|
</p:txBody>
|
||||||
|
</p:sp>
|
||||||
|
</p:spTree>
|
||||||
|
</p:cSld>
|
||||||
|
<p:clrMapOvr>
|
||||||
|
<a:masterClrMapping />
|
||||||
|
</p:clrMapOvr>
|
||||||
|
</p:sld>
|
||||||
9
lib/pptx/templates/slide.xml.rels
Normal file
9
lib/pptx/templates/slide.xml.rels
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Id="rId1"
|
||||||
|
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"
|
||||||
|
Target="../slideLayouts/slideLayout7.xml" />
|
||||||
|
<Relationship Id="{{.RelationshipID}}"
|
||||||
|
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
|
||||||
|
Target="../media/{{.FileName}}.png" />
|
||||||
|
</Relationships>
|
||||||
82
lib/pptx/validate.go
Normal file
82
lib/pptx/validate.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,14 @@
|
||||||
package version
|
package version
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
// Pre-built binaries will have version set correctly during build time.
|
// Pre-built binaries will have version set correctly during build time.
|
||||||
var Version = "v0.4.0-HEAD"
|
var Version = "v0.4.0-HEAD"
|
||||||
|
|
||||||
|
func OnlyNumbers() string {
|
||||||
|
re, err := regexp.Compile("[0-9]+.[0-9]+.[0-9]+")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return re.FindString(Version)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue