2023-04-05 18:34:50 +00:00
|
|
|
package pptx
|
2023-04-04 14:13:47 +00:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"archive/zip"
|
|
|
|
|
"bytes"
|
|
|
|
|
_ "embed"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
2023-04-05 18:22:12 +00:00
|
|
|
"time"
|
2023-04-04 14:13:47 +00:00
|
|
|
)
|
|
|
|
|
|
2023-04-10 14:45:04 +00:00
|
|
|
// 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).
|
2023-04-04 14:13:47 +00:00
|
|
|
// Office Open XML (OOXML) http://officeopenxml.com/prPresentation.php
|
2023-04-10 14:45:04 +00:00
|
|
|
// 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)
|
2023-04-04 14:13:47 +00:00
|
|
|
|
|
|
|
|
//go:embed template.pptx
|
2023-04-10 16:49:30 +00:00
|
|
|
var PPTX_TEMPLATE []byte
|
2023-04-04 14:13:47 +00:00
|
|
|
|
|
|
|
|
func copyPptxTemplateTo(w *zip.Writer) error {
|
2023-04-10 16:49:30 +00:00
|
|
|
reader := bytes.NewReader(PPTX_TEMPLATE)
|
2023-04-04 14:13:47 +00:00
|
|
|
zipReader, err := zip.NewReader(reader, reader.Size())
|
|
|
|
|
if err != nil {
|
2023-04-06 18:16:32 +00:00
|
|
|
fmt.Printf("error creating zip reader: %v", err)
|
2023-04-04 14:13:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, f := range zipReader.File {
|
2023-04-06 18:16:32 +00:00
|
|
|
if err := w.Copy(f); err != nil {
|
|
|
|
|
return fmt.Errorf("error copying %s: %v", f.Name, err)
|
2023-04-04 14:13:47 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addFile(zipFile *zip.Writer, filePath, content string) error {
|
|
|
|
|
w, err := zipFile.Create(filePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
w.Write([]byte(content))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-10 16:49:30 +00:00
|
|
|
//go:embed templates/slide.xml.rels
|
|
|
|
|
var RELS_SLIDE_XML string
|
2023-04-04 14:13:47 +00:00
|
|
|
|
2023-04-10 14:45:04 +00:00
|
|
|
func getRelsSlideXml(imageID string) string {
|
|
|
|
|
return fmt.Sprintf(RELS_SLIDE_XML, imageID, imageID)
|
2023-04-04 14:13:47 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-10 16:49:30 +00:00
|
|
|
//go:embed templates/slide.xml
|
|
|
|
|
var SLIDE_XML string
|
2023-04-04 14:13:47 +00:00
|
|
|
|
2023-04-10 14:45:04 +00:00
|
|
|
func getSlideXml(boardPath []string, imageID string, top, left, width, height int) string {
|
2023-04-05 14:40:21 +00:00
|
|
|
var slideTitle string
|
|
|
|
|
boardName := boardPath[len(boardPath)-1]
|
|
|
|
|
prefixPath := boardPath[:len(boardPath)-1]
|
|
|
|
|
if len(prefixPath) > 0 {
|
|
|
|
|
prefix := strings.Join(prefixPath, " / ") + " / "
|
|
|
|
|
slideTitle = fmt.Sprintf(`<a:r><a:t>%s</a:t></a:r><a:r><a:rPr b="1" /><a:t>%s</a:t></a:r>`, prefix, boardName)
|
|
|
|
|
} else {
|
|
|
|
|
slideTitle = fmt.Sprintf(`<a:r><a:rPr b="1" /><a:t>%s</a:t></a:r>`, boardName)
|
|
|
|
|
}
|
|
|
|
|
slideDescription := strings.Join(boardPath, " / ")
|
2023-04-04 21:47:22 +00:00
|
|
|
top += HEADER_HEIGHT
|
2023-04-10 14:45:04 +00:00
|
|
|
return fmt.Sprintf(SLIDE_XML, slideDescription, slideDescription, imageID, left, top, width, height, slideDescription, HEADER_HEIGHT, slideTitle)
|
2023-04-04 14:13:47 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-10 16:49:30 +00:00
|
|
|
//go:embed templates/rels_presentation.xml
|
|
|
|
|
var RELS_PRESENTATION_XML string
|
|
|
|
|
|
2023-04-04 14:13:47 +00:00
|
|
|
func getPresentationXmlRels(slideFileNames []string) string {
|
|
|
|
|
var builder strings.Builder
|
|
|
|
|
for _, name := range slideFileNames {
|
|
|
|
|
builder.WriteString(fmt.Sprintf(
|
|
|
|
|
`<Relationship Id="%s" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/%s.xml" />`, name, name,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-10 16:49:30 +00:00
|
|
|
return fmt.Sprintf(RELS_PRESENTATION_XML, builder.String())
|
2023-04-04 14:13:47 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-10 16:49:30 +00:00
|
|
|
//go:embed templates/content_types.xml
|
|
|
|
|
var CONTENT_TYPES_XML string
|
|
|
|
|
|
2023-04-04 14:13:47 +00:00
|
|
|
func getContentTypesXml(slideFileNames []string) string {
|
|
|
|
|
var builder strings.Builder
|
|
|
|
|
for _, name := range slideFileNames {
|
|
|
|
|
builder.WriteString(fmt.Sprintf(
|
|
|
|
|
`<Override PartName="/ppt/slides/%s.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml" />`, name,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-10 16:49:30 +00:00
|
|
|
return fmt.Sprintf(CONTENT_TYPES_XML, builder.String())
|
2023-04-04 14:13:47 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-10 16:49:30 +00:00
|
|
|
//go:embed templates/presentation.xml
|
|
|
|
|
var PRESENTATION_XML string
|
|
|
|
|
|
2023-04-04 14:13:47 +00:00
|
|
|
func getPresentationXml(slideFileNames []string) string {
|
|
|
|
|
var builder strings.Builder
|
|
|
|
|
for i, name := range slideFileNames {
|
2023-04-06 18:16:32 +00:00
|
|
|
// in the exported presentation, the first slide ID was 256, so keeping it here for compatibility
|
2023-04-06 14:01:44 +00:00
|
|
|
builder.WriteString(fmt.Sprintf(`<p:sldId id="%d" r:id="%s" />`, 256+i, name))
|
2023-04-04 14:13:47 +00:00
|
|
|
}
|
2023-04-10 16:49:30 +00:00
|
|
|
return fmt.Sprintf(PRESENTATION_XML, builder.String(), SLIDE_WIDTH, SLIDE_HEIGHT)
|
2023-04-04 14:13:47 +00:00
|
|
|
}
|
2023-04-05 18:22:12 +00:00
|
|
|
|
2023-04-10 16:49:30 +00:00
|
|
|
//go:embed templates/core.xml
|
|
|
|
|
var CORE_XML string
|
2023-04-05 18:22:12 +00:00
|
|
|
|
2023-04-10 16:49:30 +00:00
|
|
|
func getCoreXml(title, subject, description, creator string) string {
|
2023-04-06 14:01:44 +00:00
|
|
|
dateTime := time.Now().Format(time.RFC3339)
|
2023-04-10 16:49:30 +00:00
|
|
|
return fmt.Sprintf(
|
|
|
|
|
CORE_XML,
|
|
|
|
|
title,
|
|
|
|
|
subject,
|
|
|
|
|
creator,
|
|
|
|
|
description,
|
|
|
|
|
creator,
|
|
|
|
|
dateTime,
|
|
|
|
|
dateTime,
|
|
|
|
|
)
|
2023-04-05 18:22:12 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-10 16:49:30 +00:00
|
|
|
//go:embed templates/app.xml
|
|
|
|
|
var APP_XML string
|
|
|
|
|
|
2023-04-06 14:01:44 +00:00
|
|
|
func getAppXml(slides []*Slide, d2version string) string {
|
2023-04-05 18:22:12 +00:00
|
|
|
var builder strings.Builder
|
2023-04-06 14:01:44 +00:00
|
|
|
for _, slide := range slides {
|
|
|
|
|
builder.WriteString(fmt.Sprintf(`<vt:lpstr>%s</vt:lpstr>`, strings.Join(slide.BoardPath, "/")))
|
|
|
|
|
}
|
2023-04-10 16:49:30 +00:00
|
|
|
return fmt.Sprintf(
|
|
|
|
|
APP_XML,
|
|
|
|
|
len(slides),
|
|
|
|
|
len(slides),
|
|
|
|
|
len(slides)+3, // number of entries, len(slides) + Office Theme + 2 Fonts
|
|
|
|
|
builder.String(),
|
|
|
|
|
d2version,
|
|
|
|
|
)
|
2023-04-05 18:22:12 +00:00
|
|
|
}
|