diff --git a/lib/ppt/pptx.go b/lib/ppt/pptx.go
new file mode 100644
index 000000000..04f30b03e
--- /dev/null
+++ b/lib/ppt/pptx.go
@@ -0,0 +1,111 @@
+package ppt
+
+import (
+ "archive/zip"
+ "bytes"
+ _ "embed"
+ "fmt"
+ "io"
+ "strings"
+)
+
+// Office Open XML (OOXML) http://officeopenxml.com/prPresentation.php
+
+//go:embed template.pptx
+var pptx_template []byte
+
+func copyPptxTemplateTo(w *zip.Writer) error {
+ reader := bytes.NewReader(pptx_template)
+ zipReader, err := zip.NewReader(reader, reader.Size())
+ if err != nil {
+ fmt.Printf("%v", err)
+ }
+
+ for _, f := range zipReader.File {
+ fw, err := w.Create(f.Name)
+ if err != nil {
+ return err
+ }
+ fr, err := f.Open()
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(fw, fr)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func addFile(zipFile *zip.Writer, filePath, content string) error {
+ w, err := zipFile.Create(filePath)
+ if err != nil {
+ return err
+ }
+ w.Write([]byte(content))
+ return nil
+}
+
+// https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/
+const SLIDE_WIDTH = 9144000
+const SLIDE_HEIGHT = 5143500
+
+const RELS_SLIDE_XML = ``
+
+func getRelsSlideXml(imageId string) string {
+ return fmt.Sprintf(RELS_SLIDE_XML, imageId, imageId)
+}
+
+const SLIDE_XML = ``
+
+func getSlideXml(imageId, imageName string, top, left, width, height int) string {
+ return fmt.Sprintf(SLIDE_XML, imageName, imageName, imageId, left, top, width, height)
+}
+
+func getPresentationXmlRels(slideFileNames []string) string {
+ var builder strings.Builder
+ builder.WriteString(``)
+
+ for _, name := range slideFileNames {
+ builder.WriteString(fmt.Sprintf(
+ ``, name, name,
+ ))
+ }
+
+ builder.WriteString("")
+
+ return builder.String()
+}
+
+func getContentTypesXml(slideFileNames []string) string {
+ var builder strings.Builder
+ builder.WriteString(``)
+
+ for _, name := range slideFileNames {
+ builder.WriteString(fmt.Sprintf(
+ ``, name,
+ ))
+ }
+
+ builder.WriteString(``)
+ return builder.String()
+}
+
+func getPresentationXml(slideFileNames []string) string {
+ var builder strings.Builder
+ builder.WriteString(``)
+
+ builder.WriteString("")
+ for i, name := range slideFileNames {
+ builder.WriteString(fmt.Sprintf(``, i, name))
+ }
+ builder.WriteString("")
+
+ builder.WriteString(fmt.Sprintf(
+ ``,
+ SLIDE_WIDTH,
+ SLIDE_HEIGHT,
+ ))
+ return builder.String()
+}
diff --git a/lib/ppt/presentation.go b/lib/ppt/presentation.go
new file mode 100644
index 000000000..256bcafa7
--- /dev/null
+++ b/lib/ppt/presentation.go
@@ -0,0 +1,100 @@
+package ppt
+
+import (
+ "archive/zip"
+ "bytes"
+ _ "embed"
+ "fmt"
+ "image/png"
+ "os"
+)
+
+type Pptx struct {
+ Slides []*Slide
+}
+
+type Slide struct {
+ Image []byte
+ Width int
+ Height int
+}
+
+func NewPresentation() *Pptx {
+ return &Pptx{}
+}
+
+func (p *Pptx) AddSlide(pngContent []byte) error {
+ src, err := png.Decode(bytes.NewReader(pngContent))
+ if err != nil {
+ return fmt.Errorf("error decoding PNG image: %v", err)
+ }
+
+ srcSize := src.Bounds().Size()
+ height := int(float64(SLIDE_WIDTH) * (float64(srcSize.X) / float64(srcSize.Y)))
+
+ p.Slides = append(p.Slides, &Slide{
+ Image: pngContent,
+ Width: SLIDE_WIDTH,
+ Height: height,
+ })
+
+ return nil
+}
+
+func (p *Pptx) SaveTo(filePath string) error {
+ // TODO: update core files with metadata
+
+ f, err := os.Create(filePath)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ zipFile := zip.NewWriter(f)
+ defer zipFile.Close()
+
+ copyPptxTemplateTo(zipFile)
+
+ var slideFileNames []string
+ for i, slide := range p.Slides {
+ imageId := fmt.Sprintf("slide%dImage", i+1)
+ slideFileName := fmt.Sprintf("slide%d", i+1)
+ slideFileNames = append(slideFileNames, slideFileName)
+
+ imageWriter, err := zipFile.Create(fmt.Sprintf("ppt/media/%s.png", imageId))
+ if err != nil {
+ return err
+ }
+ _, err = imageWriter.Write(slide.Image)
+ if err != nil {
+ return err
+ }
+
+ err = addFile(zipFile, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), getRelsSlideXml(imageId))
+ if err != nil {
+ return err
+ }
+
+ // TODO: center the image?
+ err = addFile(zipFile, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), getSlideXml(imageId, imageId, 0, 0, slide.Width, slide.Height))
+ if err != nil {
+ return err
+ }
+ }
+
+ err = addFile(zipFile, "[Content_Types].xml", getContentTypesXml(slideFileNames))
+ if err != nil {
+ return err
+ }
+
+ err = addFile(zipFile, "ppt/_rels/presentation.xml.rels", getPresentationXmlRels(slideFileNames))
+ if err != nil {
+ return err
+ }
+
+ err = addFile(zipFile, "ppt/presentation.xml", getPresentationXml(slideFileNames))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/lib/ppt/template.pptx b/lib/ppt/template.pptx
new file mode 100644
index 000000000..cdb16e97f
Binary files /dev/null and b/lib/ppt/template.pptx differ