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 🚀
|
||||
|
||||
- Export diagrams to `.pptx` (PowerPoint)[#1139](https://github.com/terrastruct/d2/pull/1139)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
#### Bugfixes ⛑️
|
||||
|
|
|
|||
111
d2cli/main.go
111
d2cli/main.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -34,6 +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/pptx"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
"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)
|
||||
|
||||
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 +349,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 +359,27 @@ 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":
|
||||
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)
|
||||
if animateInterval <= 0 {
|
||||
// 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
|
||||
// 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)
|
||||
}
|
||||
|
|
@ -738,6 +757,77 @@ 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) ([]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 .
|
||||
func renameExt(fp string, newExt string) string {
|
||||
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
|
||||
func DiscardSlog(ctx context.Context) context.Context {
|
||||
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -244,6 +245,33 @@ layers: {
|
|||
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",
|
||||
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
|
||||
|
||||
import "regexp"
|
||||
|
||||
// Pre-built binaries will have version set correctly during build time.
|
||||
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