pdf_slides.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. package office
  2. /*
  3. pdf_slides.go - Build a PDF from a Slides Presentation with REAL text.
  4. One landscape page per slide, page size matching the deck's pixel
  5. canvas (960x540 by default). Text boxes, shapes with captions and
  6. tables are written as selectable text; images and charts are embedded
  7. from their client-inlined data URLs; video/audio placeholders are
  8. drawn with the shared poster frame.
  9. */
  10. import (
  11. "bytes"
  12. "sort"
  13. "strings"
  14. "github.com/go-pdf/fpdf"
  15. )
  16. // BuildSlidesPdf renders a Presentation into PDF bytes
  17. func BuildSlidesPdf(p *Presentation) ([]byte, error) {
  18. wPx, hPx := 960.0, 540.0
  19. if len(p.Size) == 2 && p.Size[0] > 0 && p.Size[1] > 0 {
  20. wPx, hPx = float64(p.Size[0]), float64(p.Size[1])
  21. }
  22. wMM, hMM := wPx*pxToMM, hPx*pxToMM
  23. // pass "P" with explicit dims: fpdf swaps Wd/Ht itself on "L"
  24. pdf := fpdf.NewCustom(&fpdf.InitType{
  25. OrientationStr: "P", UnitStr: "mm",
  26. Size: fpdf.SizeType{Wd: wMM, Ht: hMM},
  27. })
  28. pdf.SetMargins(0, 0, 0)
  29. pdf.SetAutoPageBreak(false, 0)
  30. tr := pdfTr(pdf)
  31. imgSeq := 0
  32. posterRegistered := false
  33. for _, slide := range p.Slides {
  34. pdf.AddPage()
  35. bg := slide.Bg
  36. if bg == "" {
  37. bg = "#" + themeBgColor(p.Theme)
  38. }
  39. if pdfSetFillHex(pdf, bg) {
  40. pdf.Rect(0, 0, wMM, hMM, "F")
  41. }
  42. objs := append([]*Object(nil), slide.Objects...)
  43. sort.SliceStable(objs, func(i, j int) bool { return objs[i].Z < objs[j].Z })
  44. for _, o := range objs {
  45. x, y := o.X*pxToMM, o.Y*pxToMM
  46. w, h := o.W*pxToMM, o.H*pxToMM
  47. switch o.Type {
  48. case "text":
  49. slidePdfText(pdf, tr, o, p.Theme, x, y, w)
  50. case "image", "chart":
  51. src := o.Props.Src
  52. if o.Type == "chart" {
  53. src = o.Props.Png
  54. }
  55. if name, _, ok := pdfImageFromDataURL(pdf, src, &imgSeq); ok {
  56. pdf.ImageOptions(name, x, y, w, h, false, fpdf.ImageOptions{}, 0, "")
  57. }
  58. case "shape":
  59. slidePdfShape(pdf, tr, o, x, y, w, h)
  60. case "line":
  61. slidePdfLine(pdf, o, x, y, w, h)
  62. case "table":
  63. slidePdfTable(pdf, tr, o, x, y, w, h)
  64. case "video", "audio":
  65. // prefer the client-captured video frame (props.png)
  66. if name, _, ok := pdfImageFromDataURL(pdf, o.Props.Png, &imgSeq); ok {
  67. pdf.ImageOptions(name, x, y, w, h, false, fpdf.ImageOptions{}, 0, "")
  68. continue
  69. }
  70. if !posterRegistered {
  71. pdf.RegisterImageOptionsReader("of-media-poster",
  72. fpdf.ImageOptions{ImageType: "PNG"}, bytes.NewReader(mediaPosterPNG()))
  73. posterRegistered = true
  74. }
  75. pdf.ImageOptions("of-media-poster", x, y, w, h, false, fpdf.ImageOptions{}, 0, "")
  76. }
  77. }
  78. }
  79. if pdf.Err() {
  80. return nil, pdf.Error()
  81. }
  82. return pdfOutput(pdf)
  83. }
  84. func slidePdfText(pdf *fpdf.Fpdf, tr func(string) string, o *Object, theme string, x, y, w float64) {
  85. p := o.Props
  86. fs := p.FontSize
  87. if fs <= 0 {
  88. fs = 18
  89. }
  90. sizePt := fs * 0.75 // css px -> pt
  91. color := p.Color
  92. if color == "" {
  93. color = "#" + themeTextColor(theme)
  94. }
  95. align := map[string]string{"center": "C", "right": "R"}[p.Align]
  96. if align == "" {
  97. align = "L"
  98. }
  99. pdf.SetFont("Arial", pdfStyleStr(p.Bold, p.Italic, p.Underline), sizePt)
  100. pdfSetTextHex(pdf, color, 32, 33, 36)
  101. lineH := fs * 1.3 * pxToMM
  102. pdf.SetXY(x, y)
  103. pdf.MultiCell(w, lineH, tr(strings.Join(htmlToLines(p.HTML), "\n")), "", align, false)
  104. }
  105. func slidePdfShape(pdf *fpdf.Fpdf, tr func(string) string, o *Object, x, y, w, h float64) {
  106. p := o.Props
  107. fill := pdfSetFillHex(pdf, p.Fill)
  108. stroke := p.StrokeW > 0
  109. if stroke {
  110. pdfSetDrawHex(pdf, p.Stroke)
  111. pdf.SetLineWidth(p.StrokeW * pxToMM)
  112. }
  113. mode := "F"
  114. if stroke && fill {
  115. mode = "FD"
  116. } else if stroke {
  117. mode = "D"
  118. } else if !fill {
  119. mode = ""
  120. }
  121. if mode != "" {
  122. switch p.Kind {
  123. case "ellipse", "circle":
  124. pdf.Ellipse(x+w/2, y+h/2, w/2, h/2, 0, mode)
  125. case "round", "rounded":
  126. r := minFloat(w, h) * 0.12
  127. pdf.RoundedRect(x, y, w, h, r, "1234", mode)
  128. default:
  129. pdf.Rect(x, y, w, h, mode)
  130. }
  131. }
  132. if txt := strings.TrimSpace(p.Text); txt != "" {
  133. fs := p.FontSize
  134. if fs <= 0 {
  135. fs = 18
  136. }
  137. tc := p.TextColor
  138. if tc == "" {
  139. tc = "#FFFFFF"
  140. }
  141. pdf.SetFont("Arial", pdfStyleStr(p.Bold, false, false), fs*0.75)
  142. pdfSetTextHex(pdf, tc, 255, 255, 255)
  143. lineH := fs * 1.3 * pxToMM
  144. lines := strings.Split(txt, "\n")
  145. totalH := float64(len(lines)) * lineH
  146. pdf.SetXY(x, y+(h-totalH)/2)
  147. pdf.MultiCell(w, lineH, tr(txt), "", "C", false)
  148. }
  149. }
  150. func slidePdfLine(pdf *fpdf.Fpdf, o *Object, x, y, w, h float64) {
  151. p := o.Props
  152. sw := p.StrokeW
  153. if sw <= 0 {
  154. sw = 2
  155. }
  156. pdfSetDrawHex(pdf, p.Stroke)
  157. pdf.SetLineWidth(sw * pxToMM)
  158. if p.Dash {
  159. pdf.SetDashPattern([]float64{2, 1.5}, 0)
  160. }
  161. pdf.Line(x, y, x+w, y+h)
  162. if p.Dash {
  163. pdf.SetDashPattern([]float64{}, 0)
  164. }
  165. }
  166. func slidePdfTable(pdf *fpdf.Fpdf, tr func(string) string, o *Object, x, y, w, h float64) {
  167. p := o.Props
  168. if len(p.Rows) == 0 {
  169. return
  170. }
  171. cols := len(p.Rows[0])
  172. if cols == 0 {
  173. return
  174. }
  175. colW := make([]float64, cols)
  176. if len(p.ColW) == cols {
  177. for i, pct := range p.ColW {
  178. colW[i] = w * pct / 100
  179. }
  180. } else {
  181. for i := range colW {
  182. colW[i] = w / float64(cols)
  183. }
  184. }
  185. rowH := make([]float64, len(p.Rows))
  186. if len(p.RowH) == len(p.Rows) {
  187. for i, pct := range p.RowH {
  188. rowH[i] = h * pct / 100
  189. }
  190. } else {
  191. for i := range rowH {
  192. rowH[i] = h / float64(len(p.Rows))
  193. }
  194. }
  195. pdf.SetDrawColor(160, 160, 160)
  196. pdf.SetLineWidth(0.25)
  197. cy := y
  198. for r, row := range p.Rows {
  199. cx := x
  200. head := p.HeaderRow && r == 0
  201. for c := 0; c < cols && c < len(row); c++ {
  202. if head {
  203. pdf.SetFillColor(52, 86, 138)
  204. pdf.Rect(cx, cy, colW[c], rowH[r], "FD")
  205. pdf.SetTextColor(255, 255, 255)
  206. pdf.SetFont("Arial", "B", 10.5)
  207. } else {
  208. pdf.SetFillColor(255, 255, 255)
  209. pdf.Rect(cx, cy, colW[c], rowH[r], "FD")
  210. pdf.SetTextColor(32, 33, 36)
  211. pdf.SetFont("Arial", "", 10.5)
  212. }
  213. txt := tr(strings.TrimSpace(row[c]))
  214. if txt != "" {
  215. pdf.SetXY(cx+1.2, cy+(rowH[r]-4.6)/2)
  216. pdf.CellFormat(colW[c]-2.4, 4.6, txt, "", 0, "L", false, 0, "")
  217. }
  218. cx += colW[c]
  219. }
  220. cy += rowH[r]
  221. }
  222. }
  223. func minFloat(a, b float64) float64 {
  224. if a < b {
  225. return a
  226. }
  227. return b
  228. }