aboutsummaryrefslogtreecommitdiff
path: root/internal/export/calendar.go
blob: 1d3b043ad85685e148bcb0d7c1d0a193ef592109 (plain) (blame)
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
141
142
143
144
145
146
147
package export

import (
	"bytes"
	"fmt"
	"strconv"
	"time"

	"github.com/go-pdf/fpdf"

	"github.com/lukaszkasprzak/lectio/internal/i18n"
)

// 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()

	ui := i18n.Get(lang)
	lect := ui.CalModern
	if lectionary == "traditional" {
		lect = ui.CalTraditional
	}
	monthName := time.Month(month).String()
	if month >= 1 && month <= 12 && ui.Months[month-1] != "" {
		monthName = ui.Months[month-1]
	}
	pdf.SetFont(fontFamily, "B", 16)
	pdf.CellFormat(0, 9, fmt.Sprintf("%s %d — %s", monthName, 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
}