summaryrefslogtreecommitdiff
path: root/internal/export
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 16:56:07 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 16:56:07 +0200
commit378926240f34759116b19e078d2b2504965f5329 (patch)
treec8e8f2288869882ccc01845eaead8f3c11f067dd /internal/export
parent29e244c48222278c2f255ff2adf45e3af7c9f895 (diff)
downloadlectio-378926240f34759116b19e078d2b2504965f5329.tar.gz
lectio-378926240f34759116b19e078d2b2504965f5329.zip
export: month calendar PDF (A4 landscape, Monday-first grid, feast name + gospel ref + liturgical colour per day); cli --calendar YYYY-MM; web /calendar + link; readings.GospelCitation; v0.23.0
Diffstat (limited to 'internal/export')
-rw-r--r--internal/export/calendar.go140
1 files changed, 140 insertions, 0 deletions
diff --git a/internal/export/calendar.go b/internal/export/calendar.go
new file mode 100644
index 0000000..8828892
--- /dev/null
+++ b/internal/export/calendar.go
@@ -0,0 +1,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
+}