1
2
3
4
5
6
7
8
9
10 package pptx
11
12 import (
13 "archive/zip"
14 "bytes"
15 _ "embed"
16 "fmt"
17 "image/png"
18 "os"
19 "text/template"
20 "time"
21 )
22
23 type BoardTitle struct {
24 LinkID string
25 Name string
26 BoardID string
27 LinkToSlide int
28 }
29
30 type Presentation struct {
31 Title string
32 Description string
33 Subject string
34 Creator string
35
36
37 D2Version string
38 includeNav bool
39
40 Slides []*Slide
41 }
42
43 type Slide struct {
44 BoardTitle []BoardTitle
45 Links []*Link
46 Image []byte
47 ImageId string
48 ImageWidth int
49 ImageHeight int
50 ImageTop int
51 ImageLeft int
52 ImageScaleFactor float64
53 }
54
55 func (s *Slide) AddLink(link *Link) {
56 link.Index = len(s.Links)
57 s.Links = append(s.Links, link)
58 link.ID = fmt.Sprintf("link%d", len(s.Links))
59 link.Height *= int(s.ImageScaleFactor)
60 link.Width *= int(s.ImageScaleFactor)
61 link.Top = s.ImageTop + int(float64(link.Top)*s.ImageScaleFactor)
62 link.Left = s.ImageLeft + int(float64(link.Left)*s.ImageScaleFactor)
63 }
64
65 type Link struct {
66 ID string
67 Index int
68 Top int
69 Left int
70 Width int
71 Height int
72 SlideIndex int
73 ExternalUrl string
74 Tooltip string
75 }
76
77 func NewPresentation(title, description, subject, creator, d2Version string, includeNav bool) *Presentation {
78 return &Presentation{
79 Title: title,
80 Description: description,
81 Subject: subject,
82 Creator: creator,
83 D2Version: d2Version,
84 includeNav: includeNav,
85 }
86 }
87
88 func (p *Presentation) headerHeight() int {
89 if p.includeNav {
90 return HEADER_HEIGHT
91 }
92 return 0
93 }
94
95 func (p *Presentation) height() int {
96 return SLIDE_HEIGHT - p.headerHeight()
97 }
98
99 func (p *Presentation) aspectRatio() float64 {
100 return float64(IMAGE_WIDTH) / float64(p.height())
101 }
102
103 func (p *Presentation) AddSlide(pngContent []byte, titlePath []BoardTitle) (*Slide, error) {
104 src, err := png.Decode(bytes.NewReader(pngContent))
105 if err != nil {
106 return nil, fmt.Errorf("error decoding PNG image: %v", err)
107 }
108
109 var width, height int
110 srcSize := src.Bounds().Size()
111 srcWidth, srcHeight := float64(srcSize.X), float64(srcSize.Y)
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131 if srcWidth/srcHeight >= p.aspectRatio() {
132
133
134 width = SLIDE_WIDTH
135 height = int(float64(width) * (srcHeight / srcWidth))
136
137
138
139 if height > p.height() {
140 width = IMAGE_WIDTH
141 height = int(float64(width) * (srcHeight / srcWidth))
142 }
143 } else {
144
145
146
147 height = p.height()
148 width = int(float64(height) * (srcWidth / srcHeight))
149 }
150 top := p.headerHeight() + ((p.height() - height) / 2)
151 left := (SLIDE_WIDTH - width) / 2
152
153 slide := &Slide{
154 BoardTitle: make([]BoardTitle, len(titlePath)),
155 ImageId: fmt.Sprintf("slide%dImage", len(p.Slides)+1),
156 Image: pngContent,
157 ImageWidth: width,
158 ImageHeight: height,
159 ImageTop: top,
160 ImageLeft: left,
161 ImageScaleFactor: float64(width) / srcWidth,
162 }
163
164 for i := 0; i < len(titlePath); i++ {
165 titlePath[i].LinkID = fmt.Sprintf("navLink%d", i)
166 slide.BoardTitle[i] = titlePath[i]
167 }
168
169 p.Slides = append(p.Slides, slide)
170 return slide, nil
171 }
172
173 func (p *Presentation) SaveTo(filePath string) error {
174 f, err := os.Create(filePath)
175 if err != nil {
176 return err
177 }
178 defer f.Close()
179 zipWriter := zip.NewWriter(f)
180 defer zipWriter.Close()
181
182 if err = copyPptxTemplateTo(zipWriter); err != nil {
183 return err
184 }
185
186 var slideFileNames []string
187 for i, slide := range p.Slides {
188 imageID := fmt.Sprintf("slide%dImage", i+1)
189 slideFileName := fmt.Sprintf("slide%d", i+1)
190 slideFileNames = append(slideFileNames, slideFileName)
191
192 imageWriter, err := zipWriter.Create(fmt.Sprintf("ppt/media/%s.png", imageID))
193 if err != nil {
194 return err
195 }
196 _, err = imageWriter.Write(slide.Image)
197 if err != nil {
198 return err
199 }
200
201 err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), RELS_SLIDE_XML, getSlideXmlRelsContent(imageID, slide))
202 if err != nil {
203 return err
204 }
205
206 err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/%s.xml", slideFileName), SLIDE_XML, p.getSlideXmlContent(imageID, slide))
207 if err != nil {
208 return err
209 }
210 }
211
212 err = addFileFromTemplate(zipWriter, "[Content_Types].xml", CONTENT_TYPES_XML, ContentTypesXmlContent{
213 FileNames: slideFileNames,
214 })
215 if err != nil {
216 return err
217 }
218
219 err = addFileFromTemplate(zipWriter, "ppt/_rels/presentation.xml.rels", RELS_PRESENTATION_XML, getRelsPresentationXmlContent(slideFileNames))
220 if err != nil {
221 return err
222 }
223
224 err = addFileFromTemplate(zipWriter, "ppt/presentation.xml", PRESENTATION_XML, getPresentationXmlContent(slideFileNames))
225 if err != nil {
226 return err
227 }
228
229 dateTime := time.Now().Format(time.RFC3339)
230 err = addFileFromTemplate(zipWriter, "docProps/core.xml", CORE_XML, CoreXmlContent{
231 Creator: p.Creator,
232 Subject: p.Subject,
233 Description: p.Description,
234 LastModifiedBy: p.Creator,
235 Title: p.Title,
236 Created: dateTime,
237 Modified: dateTime,
238 })
239 if err != nil {
240 return err
241 }
242
243 titles := make([]string, 0, len(p.Slides))
244 for _, slide := range p.Slides {
245 titles = append(titles, slide.BoardTitle[len(slide.BoardTitle)-1].BoardID)
246 }
247 err = addFileFromTemplate(zipWriter, "docProps/app.xml", APP_XML, AppXmlContent{
248 SlideCount: len(p.Slides),
249 TitlesOfPartsCount: len(p.Slides) + 3,
250 D2Version: p.D2Version,
251 Titles: titles,
252 })
253 if err != nil {
254 return err
255 }
256
257 return nil
258 }
259
260
261
262
263
264 const SLIDE_WIDTH = 9_144_000
265 const SLIDE_HEIGHT = 5_143_500
266 const HEADER_HEIGHT = 392_471
267
268
269 const IMAGE_WIDTH = 8_446_273
270
271
272 var PPTX_TEMPLATE []byte
273
274 func copyPptxTemplateTo(w *zip.Writer) error {
275 reader := bytes.NewReader(PPTX_TEMPLATE)
276 zipReader, err := zip.NewReader(reader, reader.Size())
277 if err != nil {
278 fmt.Printf("error creating zip reader: %v", err)
279 }
280
281 for _, f := range zipReader.File {
282 if err := w.Copy(f); err != nil {
283 return fmt.Errorf("error copying %s: %v", f.Name, err)
284 }
285 }
286 return nil
287 }
288
289
290 var RELS_SLIDE_XML string
291
292 type RelsSlideXmlLinkContent struct {
293 RelationshipID string
294 ExternalUrl string
295 SlideIndex int
296 }
297
298 type RelsSlideXmlContent struct {
299 FileName string
300 RelationshipID string
301 Links []RelsSlideXmlLinkContent
302 }
303
304 func getSlideXmlRelsContent(imageID string, slide *Slide) RelsSlideXmlContent {
305 content := RelsSlideXmlContent{
306 FileName: imageID,
307 RelationshipID: imageID,
308 }
309
310 for _, link := range slide.Links {
311 content.Links = append(content.Links, RelsSlideXmlLinkContent{
312 RelationshipID: link.ID,
313 ExternalUrl: link.ExternalUrl,
314 SlideIndex: link.SlideIndex,
315 })
316 }
317
318 for _, t := range slide.BoardTitle {
319 content.Links = append(content.Links, RelsSlideXmlLinkContent{
320 RelationshipID: t.LinkID,
321 SlideIndex: t.LinkToSlide,
322 })
323 }
324
325 return content
326 }
327
328
329 var SLIDE_XML string
330
331 type SlideLinkXmlContent struct {
332 ID int
333 RelationshipID string
334 Name string
335 Action string
336 Left int
337 Top int
338 Width int
339 Height int
340 }
341
342 type SlideXmlTitlePathContent struct {
343 Name string
344 RelationshipID string
345 }
346
347 type SlideXmlContent struct {
348 Title string
349 TitlePrefix []SlideXmlTitlePathContent
350 Description string
351 HeaderHeight int
352 ImageID string
353 ImageLeft int
354 ImageTop int
355 ImageWidth int
356 ImageHeight int
357
358 Links []SlideLinkXmlContent
359 }
360
361 func (p *Presentation) getSlideXmlContent(imageID string, slide *Slide) SlideXmlContent {
362 title := make([]SlideXmlTitlePathContent, len(slide.BoardTitle)-1)
363 for i := 0; i < len(slide.BoardTitle)-1; i++ {
364 t := slide.BoardTitle[i]
365 title[i] = SlideXmlTitlePathContent{
366 Name: t.Name,
367 RelationshipID: t.LinkID,
368 }
369 }
370 content := SlideXmlContent{
371 Description: slide.BoardTitle[len(slide.BoardTitle)-1].BoardID,
372 HeaderHeight: p.headerHeight(),
373 ImageID: imageID,
374 ImageLeft: slide.ImageLeft,
375 ImageTop: slide.ImageTop,
376 ImageWidth: slide.ImageWidth,
377 ImageHeight: slide.ImageHeight,
378 }
379 if p.includeNav {
380 content.Title = slide.BoardTitle[len(slide.BoardTitle)-1].Name
381 content.TitlePrefix = title
382 }
383
384 for _, link := range slide.Links {
385 var action string
386 if link.ExternalUrl == "" {
387 action = "ppaction://hlinksldjump"
388 }
389 content.Links = append(content.Links, SlideLinkXmlContent{
390 ID: link.Index,
391 RelationshipID: link.ID,
392 Name: link.Tooltip,
393 Action: action,
394 Left: link.Left,
395 Top: link.Top,
396 Width: link.Width,
397 Height: link.Height,
398 })
399 }
400
401 return content
402 }
403
404
405 var RELS_PRESENTATION_XML string
406
407 type RelsPresentationSlideXmlContent struct {
408 RelationshipID string
409 FileName string
410 }
411
412 type RelsPresentationXmlContent struct {
413 Slides []RelsPresentationSlideXmlContent
414 }
415
416 func getRelsPresentationXmlContent(slideFileNames []string) RelsPresentationXmlContent {
417 var content RelsPresentationXmlContent
418 for _, name := range slideFileNames {
419 content.Slides = append(content.Slides, RelsPresentationSlideXmlContent{
420 RelationshipID: name,
421 FileName: name,
422 })
423 }
424
425 return content
426 }
427
428
429 var CONTENT_TYPES_XML string
430
431 type ContentTypesXmlContent struct {
432 FileNames []string
433 }
434
435
436 var PRESENTATION_XML string
437
438 type PresentationSlideXmlContent struct {
439 ID int
440 RelationshipID string
441 }
442
443 type PresentationXmlContent struct {
444 SlideWidth int
445 SlideHeight int
446 Slides []PresentationSlideXmlContent
447 }
448
449 func getPresentationXmlContent(slideFileNames []string) PresentationXmlContent {
450 content := PresentationXmlContent{
451 SlideWidth: SLIDE_WIDTH,
452 SlideHeight: SLIDE_HEIGHT,
453 }
454 for i, name := range slideFileNames {
455 content.Slides = append(content.Slides, PresentationSlideXmlContent{
456
457 ID: 256 + i,
458 RelationshipID: name,
459 })
460 }
461 return content
462 }
463
464
465 var CORE_XML string
466
467 type CoreXmlContent struct {
468 Title string
469 Subject string
470 Creator string
471 Description string
472 LastModifiedBy string
473 Created string
474 Modified string
475 }
476
477
478 var APP_XML string
479
480 type AppXmlContent struct {
481 SlideCount int
482 TitlesOfPartsCount int
483 Titles []string
484 D2Version string
485 }
486
487 func addFileFromTemplate(zipFile *zip.Writer, filePath, templateContent string, templateData interface{}) error {
488 w, err := zipFile.Create(filePath)
489 if err != nil {
490 return err
491 }
492
493 tmpl, err := template.New(filePath).Parse(templateContent)
494 if err != nil {
495 return err
496 }
497 return tmpl.Execute(w, templateData)
498 }
499
View as plain text