1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
package export
import (
"bytes"
"fmt"
"strconv"
"time"
"github.com/go-pdf/fpdf"
)
// CalendarDay is one day's entry in a month calendar: the day-of-month, the
// celebration name, the gospel reference, and the liturgical colour (a name
// like "white"/"green"/"violet"/"red"/"rose"; "" = uncoloured).
type CalendarDay struct {
Day int
Name string
Citation string
Colour string
}
// liturgicalRGB maps a normalized liturgical colour to a soft printable RGB for
// the coloured strip atop each calendar cell.
var liturgicalRGB = map[string][3]int{
"white": {235, 235, 230},
"green": {90, 160, 90},
"violet": {130, 90, 165},
"red": {200, 70, 70},
"rose": {235, 165, 195},
}
// weekdayAbbr are the Monday-first column headers per UI language.
var weekdayAbbr = map[string][7]string{
"en": {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"},
"pl": {"Pn", "Wt", "Śr", "Cz", "Pt", "So", "Nd"},
}
// truncate shortens s to at most n runes, appending "…" when it had to cut.
func truncate(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
if n < 1 {
return ""
}
return string(r[:n-1]) + "…"
}
// CalendarPDF renders a printable A4 landscape month calendar: a Monday-first
// grid where each day shows its number, a coloured liturgical strip, the
// celebration name and the gospel reference. lectionary/lang label the header;
// days is the month's CalendarDay entries (day 1..N).
func CalendarPDF(year, month int, days []CalendarDay, lectionary, lang string) ([]byte, error) {
if len(days) == 0 {
return nil, fmt.Errorf("export: empty month")
}
pdf := fpdf.New("L", "mm", "A4", "")
pdf.AddUTF8FontFromBytes(fontFamily, "", fontRegular)
pdf.AddUTF8FontFromBytes(fontFamily, "B", fontBold)
const margin = 10.0
pdf.SetMargins(margin, margin, margin)
pdf.SetAutoPageBreak(false, margin)
pdf.AddPage()
pw, ph := pdf.GetPageSize()
lect := lectionary
if lect == "new" {
lect = "modern"
}
pdf.SetFont(fontFamily, "B", 16)
pdf.CellFormat(0, 9, fmt.Sprintf("%s %d — %s", time.Month(month).String(), year, lect), "", 1, "L", false, 0, "")
const gridY0 = 22.0
const weekdayH = 6.0
const cols = 7
cellW := (pw - 2*margin) / float64(cols)
hdr := weekdayAbbr[lang]
if hdr[0] == "" {
hdr = weekdayAbbr["en"]
}
pdf.SetFont(fontFamily, "B", 9)
for i, h := range hdr {
pdf.SetXY(margin+float64(i)*cellW, gridY0)
pdf.CellFormat(cellW, weekdayH, h, "", 0, "C", false, 0, "")
}
gridY := gridY0 + weekdayH
byDay := map[int]CalendarDay{}
daysInMonth := 0
for _, d := range days {
byDay[d.Day] = d
if d.Day > daysInMonth {
daysInMonth = d.Day
}
}
first := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
firstCol := (int(first.Weekday()) + 6) % 7 // Monday = 0
rows := (firstCol + daysInMonth + 6) / 7
cellH := (ph - gridY - margin) / float64(rows)
// per-cell text budgets (approximate, keeps content inside the cell)
nameChars := int((cellW - 3) / 1.35)
for day := 1; day <= daysInMonth; day++ {
idx := firstCol + day - 1
x := margin + float64(idx%7)*cellW
y := gridY + float64(idx/7)*cellH
pdf.SetDrawColor(150, 150, 150)
pdf.Rect(x, y, cellW, cellH, "D")
cd := byDay[day]
if rgb, ok := liturgicalRGB[cd.Colour]; ok {
pdf.SetFillColor(rgb[0], rgb[1], rgb[2])
pdf.Rect(x, y, cellW, 2.4, "F")
}
pdf.SetTextColor(0, 0, 0)
pdf.SetFont(fontFamily, "B", 10)
pdf.SetXY(x+1.5, y+3)
pdf.CellFormat(cellW-3, 5, strconv.Itoa(day), "", 0, "L", false, 0, "")
pdf.SetFont(fontFamily, "", 7)
pdf.SetXY(x+1.5, y+8.5)
pdf.MultiCell(cellW-3, 3.1, truncate(cd.Name, nameChars*2), "", "L", false)
if cd.Citation != "" {
pdf.SetX(x + 1.5)
pdf.SetFont(fontFamily, "B", 7)
pdf.MultiCell(cellW-3, 3.1, truncate(cd.Citation, nameChars), "", "L", false)
}
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
|