move to a single file

This commit is contained in:
Júlio César Batista 2023-04-10 15:47:37 -03:00
parent 0fad458858
commit 442f61331e
No known key found for this signature in database
GPG key ID: 10C4B861BF314878
2 changed files with 198 additions and 207 deletions

View file

@ -1,3 +1,12 @@
// 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 (
@ -5,10 +14,199 @@ import (
"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

View file

@ -1,207 +0,0 @@
// 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"
"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
}