Merge pull request #1139 from ejulio-ts/gh-821-ppt

GH 821: PowerPoint Export
This commit is contained in:
Júlio César Batista 2023-04-10 17:21:37 -03:00 committed by GitHub
commit b84095b963
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1003 additions and 8 deletions

View file

@ -1,5 +1,7 @@
#### Features 🚀
- Export diagrams to `.pptx` (PowerPoint)[#1139](https://github.com/terrastruct/d2/pull/1139)
#### Improvements 🧹
#### Bugfixes ⛑️

View file

@ -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)))

View file

@ -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
View 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

Binary file not shown.

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

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

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

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

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

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

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

View file

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