move to a single file
This commit is contained in:
parent
0fad458858
commit
442f61331e
2 changed files with 198 additions and 207 deletions
198
lib/pptx/pptx.go
198
lib/pptx/pptx.go
|
|
@ -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
|
package pptx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -5,10 +14,199 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"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
|
// 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).
|
// 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
|
// Office Open XML (OOXML) http://officeopenxml.com/prPresentation.php
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue