pdf_sheet.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. package office
  2. /*
  3. pdf_sheet.go - Build a PDF from a Sheets workbook with REAL text.
  4. Formula evaluation and number formatting live in the web client, so
  5. the client posts a "print model": per sheet, the used-range grid of
  6. formatted display strings plus the visual styles that affect print
  7. (bold/italic/underline, font and fill colors, alignment) and the
  8. column widths in CSS pixels. The server lays that grid out on A4
  9. landscape pages, scaling the columns down when the sheet is wider
  10. than the printable area and splitting rows across pages.
  11. */
  12. import (
  13. "encoding/json"
  14. "errors"
  15. "github.com/go-pdf/fpdf"
  16. )
  17. // SheetPrintCell is one formatted cell in the print model
  18. type SheetPrintCell struct {
  19. T string `json:"t"` // display text
  20. B bool `json:"b,omitempty"` // bold
  21. I bool `json:"i,omitempty"` // italic
  22. U bool `json:"u,omitempty"` // underline
  23. Fc string `json:"fc,omitempty"` // font color (#rrggbb)
  24. Bg string `json:"bg,omitempty"` // fill color (#rrggbb)
  25. Al string `json:"al,omitempty"` // l | c | r
  26. }
  27. // SheetPrintSheet is one sheet's used-range grid
  28. type SheetPrintSheet struct {
  29. Name string `json:"name"`
  30. ColW []float64 `json:"colW"` // column widths, css px
  31. RowH []float64 `json:"rowH,omitempty"` // row heights, css px
  32. Rows [][]*SheetPrintCell `json:"rows"`
  33. }
  34. // SheetPrintModel is the whole workbook print model
  35. type SheetPrintModel struct {
  36. Sheets []*SheetPrintSheet `json:"sheets"`
  37. }
  38. // ParseSheetPrintJSON decodes the print model posted by sheets.js
  39. func ParseSheetPrintJSON(jsonStr string) (*SheetPrintModel, error) {
  40. m := SheetPrintModel{}
  41. if err := json.Unmarshal([]byte(jsonStr), &m); err != nil {
  42. return nil, errors.New("invalid print model JSON: " + err.Error())
  43. }
  44. if len(m.Sheets) == 0 {
  45. return nil, errors.New("print model has no sheets")
  46. }
  47. return &m, nil
  48. }
  49. // BuildSheetPdf renders the print model into PDF bytes
  50. func BuildSheetPdf(m *SheetPrintModel) ([]byte, error) {
  51. const marginMM = 12.0
  52. pdf := fpdf.New("L", "mm", "A4", "")
  53. pdf.SetMargins(marginMM, marginMM, marginMM)
  54. pdf.SetAutoPageBreak(false, marginMM)
  55. tr := pdfTr(pdf)
  56. pageW, pageH := pdf.GetPageSize()
  57. usableW := pageW - 2*marginMM
  58. for _, sheet := range m.Sheets {
  59. pdf.AddPage()
  60. // sheet name heading
  61. pdf.SetFont("Arial", "B", 12)
  62. pdf.SetTextColor(31, 35, 40)
  63. pdf.SetXY(marginMM, marginMM)
  64. pdf.CellFormat(usableW, 6, tr(sheet.Name), "", 1, "L", false, 0, "")
  65. pdf.SetY(pdf.GetY() + 2)
  66. if len(sheet.Rows) == 0 {
  67. continue
  68. }
  69. cols := 0
  70. for _, row := range sheet.Rows {
  71. if len(row) > cols {
  72. cols = len(row)
  73. }
  74. }
  75. if cols == 0 {
  76. continue
  77. }
  78. // column widths: px -> mm, scaled to fit the page when too wide
  79. widths := make([]float64, cols)
  80. total := 0.0
  81. for i := 0; i < cols; i++ {
  82. w := 100.0 * pxToMM
  83. if i < len(sheet.ColW) && sheet.ColW[i] > 0 {
  84. w = sheet.ColW[i] * pxToMM
  85. }
  86. widths[i] = w
  87. total += w
  88. }
  89. scale := 1.0
  90. if total > usableW {
  91. scale = usableW / total
  92. for i := range widths {
  93. widths[i] *= scale
  94. }
  95. }
  96. fontPt := 9.0 * scale
  97. if fontPt < 5 {
  98. fontPt = 5
  99. }
  100. for r, row := range sheet.Rows {
  101. rowH := 24.0 * pxToMM * scale
  102. if r < len(sheet.RowH) && sheet.RowH[r] > 0 {
  103. rowH = sheet.RowH[r] * pxToMM * scale
  104. }
  105. minH := fontPt*0.3528 + 1.6
  106. if rowH < minH {
  107. rowH = minH
  108. }
  109. if pdf.GetY()+rowH > pageH-marginMM {
  110. pdf.AddPage()
  111. pdf.SetY(marginMM)
  112. }
  113. x := marginMM
  114. y := pdf.GetY()
  115. pdf.SetDrawColor(190, 194, 200)
  116. pdf.SetLineWidth(0.15)
  117. for c := 0; c < cols; c++ {
  118. var cell *SheetPrintCell
  119. if c < len(row) {
  120. cell = row[c]
  121. }
  122. fill := false
  123. if cell != nil && cell.Bg != "" {
  124. fill = pdfSetFillHex(pdf, cell.Bg)
  125. }
  126. pdf.Rect(x, y, widths[c], rowH, map[bool]string{true: "FD", false: "D"}[fill])
  127. if cell != nil && cell.T != "" {
  128. pdf.SetFont("Arial", pdfStyleStr(cell.B, cell.I, cell.U), fontPt)
  129. pdfSetTextHex(pdf, cell.Fc, 31, 35, 40)
  130. align := map[string]string{"c": "C", "r": "R"}[cell.Al]
  131. if align == "" {
  132. align = "L"
  133. }
  134. pdf.SetXY(x+0.8, y)
  135. pdf.CellFormat(widths[c]-1.6, rowH, tr(cell.T), "", 0, align, false, 0, "")
  136. }
  137. x += widths[c]
  138. }
  139. pdf.SetY(y + rowH)
  140. }
  141. }
  142. if pdf.Err() {
  143. return nil, pdf.Error()
  144. }
  145. return pdfOutput(pdf)
  146. }