377 lines
16 KiB
Go
377 lines
16 KiB
Go
package officekit
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Slide is one slide's content, as the model supplies it to
|
|
// create_presentation/edit_presentation: a title and a flat list of
|
|
// bullet lines. Deliberately this simple — no layouts, no inherited
|
|
// placeholders, no raw DrawingML — so the model never authors
|
|
// PresentationML either; every slide is rendered as two independent,
|
|
// absolutely-positioned text boxes (title, body) that don't need to
|
|
// resolve against the slide layout/master's placeholder scheme.
|
|
type Slide struct {
|
|
Title string
|
|
Bullets []string
|
|
}
|
|
|
|
// --- read side: typed XML for parsing a slide's shape tree back out
|
|
// (ReadPptxSlides). Every struct tag below is a plain, unprefixed
|
|
// local name with no namespace component — PresentationML genuinely
|
|
// needs several simultaneous namespaces (p: for presentation elements,
|
|
// a: for drawing elements), so unlike docx's single-namespace case we
|
|
// can't lean on one default namespace to cover everything. Instead we
|
|
// rely on a different, equally standard encoding/xml behavior: a field
|
|
// tag with no namespace component matches an element by local name
|
|
// only, regardless of which namespace it actually resolved to. So
|
|
// "cSld" matches <p:cSld> and "xfrm" matches <a:xfrm> alike, without
|
|
// our Go types needing to know or care which prefix/namespace a given
|
|
// element belongs to. (See the longer version of this note on
|
|
// wDocument in docx.go — same underlying mechanism, applied here to
|
|
// the harder, multi-namespace case.) ---
|
|
|
|
type pSld struct {
|
|
XMLName xml.Name `xml:"sld"`
|
|
CSld pCSld `xml:"cSld"`
|
|
}
|
|
|
|
type pCSld struct {
|
|
SpTree pSpTree `xml:"spTree"`
|
|
}
|
|
|
|
type pSpTree struct {
|
|
Sp []pSp `xml:"sp"`
|
|
}
|
|
|
|
type pCNvPr struct {
|
|
Id int `xml:"id,attr"`
|
|
Name string `xml:"name,attr"`
|
|
}
|
|
|
|
type pSp struct {
|
|
NvSpPr pNvSpPr `xml:"nvSpPr"`
|
|
TxBody pTxBody `xml:"txBody"`
|
|
}
|
|
|
|
type pNvSpPr struct {
|
|
CNvPr pCNvPr `xml:"cNvPr"`
|
|
}
|
|
|
|
type pTxBody struct {
|
|
Paras []pTxPara `xml:"p"`
|
|
}
|
|
|
|
type pTxPara struct {
|
|
Run pTxRun `xml:"r"`
|
|
}
|
|
|
|
type pTxRun struct {
|
|
Text string `xml:"t"`
|
|
}
|
|
|
|
// --- write side: deliberately NOT built with xml.Marshal. PresentationML's
|
|
// several simultaneous namespaces mean a struct-tag-driven Marshal would
|
|
// need Go to auto-assign namespace prefixes, which is both unpredictable
|
|
// and avoidable here — we fully control this template, so we write the
|
|
// real p:/a: prefixes directly and escape every piece of user-supplied
|
|
// text through encoding/xml's own escaper (xmlEscapeText), which is the
|
|
// same function Marshal itself uses internally for chardata. That keeps
|
|
// the "well-formed, correctly-escaped, never touched by the model"
|
|
// property from the package doc comment without fighting Go's namespace
|
|
// prefix allocation. ---
|
|
|
|
// xmlEscapeText XML-escapes s for use as element chardata (title and
|
|
// bullet text — the only user-supplied strings that end up in a slide).
|
|
func xmlEscapeText(s string) string {
|
|
var buf bytes.Buffer
|
|
_ = xml.EscapeText(&buf, []byte(s))
|
|
return buf.String()
|
|
}
|
|
|
|
const titleShapeTemplate = `<p:sp><p:nvSpPr><p:cNvPr id="%d" name="Title"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
|
|
<p:spPr><a:xfrm><a:off x="457200" y="274638"/><a:ext cx="8229600" cy="1143000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
|
|
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr lang="en-US" sz="3200" b="1"/><a:t>%s</a:t></a:r></a:p></p:txBody>
|
|
</p:sp>`
|
|
|
|
const bodyParaTemplate = `<a:p><a:pPr><a:buChar char="•"/></a:pPr><a:r><a:rPr lang="en-US" sz="2000"/><a:t>%s</a:t></a:r></a:p>`
|
|
|
|
const bodyShapeTemplate = `<p:sp><p:nvSpPr><p:cNvPr id="%d" name="Body"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
|
|
<p:spPr><a:xfrm><a:off x="457200" y="1600200"/><a:ext cx="8229600" cy="4525963"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
|
|
<p:txBody><a:bodyPr/><a:lstStyle/>%s</p:txBody>
|
|
</p:sp>`
|
|
|
|
func titleShape(id int, text string) string {
|
|
return fmt.Sprintf(titleShapeTemplate, id, xmlEscapeText(text))
|
|
}
|
|
|
|
func bodyShape(id int, bullets []string) string {
|
|
var paras strings.Builder
|
|
for _, b := range bullets {
|
|
fmt.Fprintf(¶s, bodyParaTemplate, xmlEscapeText(b))
|
|
}
|
|
return fmt.Sprintf(bodyShapeTemplate, id, paras.String())
|
|
}
|
|
|
|
func slideXML(s Slide) ([]byte, error) {
|
|
var shapes strings.Builder
|
|
if s.Title != "" {
|
|
shapes.WriteString(titleShape(2, s.Title))
|
|
}
|
|
if len(s.Bullets) > 0 {
|
|
shapes.WriteString(bodyShape(3, s.Bullets))
|
|
}
|
|
out := xml.Header + `<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/>` + shapes.String() + `</p:spTree></p:cSld>
|
|
<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
|
|
</p:sld>`
|
|
return []byte(out), nil
|
|
}
|
|
|
|
func slidePath(n int) string { return fmt.Sprintf("ppt/slides/slide%d.xml", n) }
|
|
func slideRelsPath(n int) string { return fmt.Sprintf("ppt/slides/_rels/slide%d.xml.rels", n) }
|
|
|
|
const slideRelsBody = xml.Header + `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
|
|
</Relationships>`
|
|
|
|
// BuildPptx renders slides into a complete, valid .pptx file.
|
|
func BuildPptx(slides []Slide) ([]byte, error) {
|
|
if len(slides) == 0 {
|
|
slides = []Slide{{}}
|
|
}
|
|
|
|
parts := []zipPart{
|
|
{Name: "[Content_Types].xml", Body: pptxContentTypes(len(slides))},
|
|
{Name: "_rels/.rels", Body: pptxRootRels},
|
|
{Name: "ppt/presentation.xml", Body: pptxPresentationXML(len(slides))},
|
|
{Name: "ppt/_rels/presentation.xml.rels", Body: pptxPresentationRels(len(slides))},
|
|
{Name: "ppt/slideMasters/slideMaster1.xml", Body: pptxSlideMaster},
|
|
{Name: "ppt/slideMasters/_rels/slideMaster1.xml.rels", Body: pptxSlideMasterRels},
|
|
{Name: "ppt/slideLayouts/slideLayout1.xml", Body: pptxSlideLayout},
|
|
{Name: "ppt/slideLayouts/_rels/slideLayout1.xml.rels", Body: pptxSlideLayoutRels},
|
|
{Name: "ppt/theme/theme1.xml", Body: pptxTheme},
|
|
{Name: "docProps/core.xml", Body: officeCoreProps},
|
|
}
|
|
for i, s := range slides {
|
|
n := i + 1
|
|
sx, err := slideXML(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parts = append(parts,
|
|
zipPart{Name: slidePath(n), Body: string(sx)},
|
|
zipPart{Name: slideRelsPath(n), Body: slideRelsBody},
|
|
)
|
|
}
|
|
return buildZip(parts)
|
|
}
|
|
|
|
// ReadPptxSlides returns every slide's title + bullets, in order, for
|
|
// the read_presentation tool. It re-derives slide order and count from
|
|
// ppt/presentation.xml's sldIdLst / presentation.xml.rels rather than
|
|
// assuming a fixed count, so a file this package wrote (or edited)
|
|
// round-trips exactly.
|
|
func ReadPptxSlides(data []byte) ([]Slide, error) {
|
|
n, err := countSlides(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []Slide
|
|
for i := 1; i <= n; i++ {
|
|
raw, ok, err := readZipPart(data, slidePath(i))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
var sld pSld
|
|
if err := xml.Unmarshal(raw, &sld); err != nil {
|
|
return nil, fmt.Errorf("officekit: parse %s: %w", slidePath(i), err)
|
|
}
|
|
s := Slide{}
|
|
for _, sp := range sld.CSld.SpTree.Sp {
|
|
switch sp.NvSpPr.CNvPr.Name {
|
|
case "Title":
|
|
if len(sp.TxBody.Paras) > 0 {
|
|
s.Title = sp.TxBody.Paras[0].Run.Text
|
|
}
|
|
case "Body":
|
|
for _, p := range sp.TxBody.Paras {
|
|
s.Bullets = append(s.Bullets, p.Run.Text)
|
|
}
|
|
}
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// countSlides counts how many ppt/slides/slideN.xml parts a .pptx
|
|
// actually contains by probing sequentially (simplest reliable
|
|
// approach given this package writes slides as a dense 1..n sequence
|
|
// and never deletes from the middle).
|
|
func countSlides(data []byte) (int, error) {
|
|
n := 0
|
|
for {
|
|
_, ok, err := readZipPart(data, slidePath(n+1))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !ok {
|
|
break
|
|
}
|
|
n++
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// AppendPptxSlide adds one new slide at the end and returns the new
|
|
// file bytes, keeping [Content_Types].xml, presentation.xml and
|
|
// presentation.xml.rels in sync with the new slide count.
|
|
func AppendPptxSlide(data []byte, s Slide) ([]byte, error) {
|
|
n, err := countSlides(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newN := n + 1
|
|
sx, err := slideXML(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out, err := addZipParts(data, []zipPart{
|
|
{Name: slidePath(newN), Body: string(sx)},
|
|
{Name: slideRelsPath(newN), Body: slideRelsBody},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, err = replaceZipPart(out, "[Content_Types].xml", []byte(pptxContentTypes(newN)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, err = replaceZipPart(out, "ppt/presentation.xml", []byte(pptxPresentationXML(newN)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return replaceZipPart(out, "ppt/_rels/presentation.xml.rels", []byte(pptxPresentationRels(newN)))
|
|
}
|
|
|
|
// UpdatePptxSlide replaces one existing slide's content (1-based
|
|
// index) in place.
|
|
func UpdatePptxSlide(data []byte, index int, s Slide) ([]byte, error) {
|
|
n, err := countSlides(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if index < 1 || index > n {
|
|
return nil, fmt.Errorf("officekit: slide %d out of range (presentation has %d slide(s))", index, n)
|
|
}
|
|
sx, err := slideXML(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return replaceZipPart(data, slidePath(index), []byte(sx))
|
|
}
|
|
|
|
func pptxContentTypes(n int) string {
|
|
var overrides strings.Builder
|
|
for i := 1; i <= n; i++ {
|
|
fmt.Fprintf(&overrides, `<Override PartName="/%s" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>`, slidePath(i))
|
|
}
|
|
return xml.Header + `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
<Default Extension="xml" ContentType="application/xml"/>
|
|
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
|
|
<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
|
|
<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>
|
|
<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
|
|
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
|
|
` + overrides.String() + `</Types>`
|
|
}
|
|
|
|
func pptxPresentationXML(n int) string {
|
|
var ids strings.Builder
|
|
for i := 1; i <= n; i++ {
|
|
fmt.Fprintf(&ids, `<p:sldId id="%d" r:id="rId%d"/>`, 255+i, i+1)
|
|
}
|
|
return xml.Header + `<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">
|
|
<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
|
|
<p:sldIdLst>` + ids.String() + `</p:sldIdLst>
|
|
<p:sldSz cx="9144000" cy="6858000"/>
|
|
<p:notesSz cx="6858000" cy="9144000"/>
|
|
</p:presentation>`
|
|
}
|
|
|
|
func pptxPresentationRels(n int) string {
|
|
var rels strings.Builder
|
|
for i := 1; i <= n; i++ {
|
|
fmt.Fprintf(&rels, `<Relationship Id="rId%d" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide%d.xml"/>`, i+1, i)
|
|
}
|
|
return xml.Header + `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>
|
|
` + rels.String() + `</Relationships>`
|
|
}
|
|
|
|
const pptxRootRels = xml.Header + `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
|
|
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
|
|
</Relationships>`
|
|
|
|
const pptxSlideMasterRels = xml.Header + `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
|
|
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>
|
|
</Relationships>`
|
|
|
|
const pptxSlideLayoutRels = xml.Header + `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>
|
|
</Relationships>`
|
|
|
|
const pptxSlideMaster = xml.Header + `<p:sldMaster 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">
|
|
<p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg>
|
|
<p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld>
|
|
<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>
|
|
<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>
|
|
</p:sldMaster>`
|
|
|
|
const pptxSlideLayout = xml.Header + `<p:sldLayout 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" type="blank" preserve="1">
|
|
<p:cSld name="Blank">
|
|
<p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree>
|
|
</p:cSld>
|
|
<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
|
|
</p:sldLayout>`
|
|
|
|
const pptxTheme = xml.Header + `<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="enkly">
|
|
<a:themeElements>
|
|
<a:clrScheme name="enkly">
|
|
<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
|
|
<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
|
|
<a:dk2><a:srgbClr val="44546A"/></a:dk2>
|
|
<a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>
|
|
<a:accent1><a:srgbClr val="4472C4"/></a:accent1>
|
|
<a:accent2><a:srgbClr val="ED7D31"/></a:accent2>
|
|
<a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>
|
|
<a:accent4><a:srgbClr val="FFC000"/></a:accent4>
|
|
<a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>
|
|
<a:accent6><a:srgbClr val="70AD47"/></a:accent6>
|
|
<a:hlink><a:srgbClr val="0563C1"/></a:hlink>
|
|
<a:folHlink><a:srgbClr val="954F72"/></a:folHlink>
|
|
</a:clrScheme>
|
|
<a:fontScheme name="enkly">
|
|
<a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
|
|
<a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
|
|
</a:fontScheme>
|
|
<a:fmtScheme name="enkly">
|
|
<a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst>
|
|
<a:lnStyleLst><a:ln><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst>
|
|
<a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>
|
|
<a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst>
|
|
</a:fmtScheme>
|
|
</a:themeElements>
|
|
</a:theme>`
|