diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-24 16:56:07 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-24 16:56:07 +0200 |
| commit | 378926240f34759116b19e078d2b2504965f5329 (patch) | |
| tree | c8e8f2288869882ccc01845eaead8f3c11f067dd /internal/export/calendar.go | |
| parent | 29e244c48222278c2f255ff2adf45e3af7c9f895 (diff) | |
| download | lectio-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/calendar.go')
| -rw-r--r-- | internal/export/calendar.go | 140 |
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 +} |
