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 }