package main import ( "bytes" "image" "image/color" "image/draw" "image/png" "net/http" "github.com/go-chi/chi/v5" ) var ( ogCream = color.RGBA{0xf5, 0xf0, 0xe8, 0xff} ogInk = color.RGBA{0x1a, 0x1a, 0x1a, 0xff} ogYellow = color.RGBA{0xf5, 0xe6, 0x42, 0xff} ogSage = color.RGBA{0xa8, 0xc5, 0xa0, 0xff} ogPeach = color.RGBA{0xf2, 0xa4, 0x82, 0xff} ogWhite = color.RGBA{0xff, 0xff, 0xff, 0xff} ) func fillRect(img *image.RGBA, x1, y1, x2, y2 int, c color.RGBA) { draw.Draw(img, image.Rect(x1, y1, x2, y2), &image.Uniform{c}, image.Point{}, draw.Src) } // 5x7 pixel font for uppercase letters, digits, and a few symbols var glyphs = map[rune][7]uint8{ 'A': {0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001}, 'B': {0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110}, 'C': {0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110}, 'D': {0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110}, 'E': {0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111}, 'F': {0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000}, 'G': {0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01110}, 'H': {0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001}, 'I': {0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b11111}, 'J': {0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100}, 'K': {0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001}, 'L': {0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111}, 'M': {0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001}, 'N': {0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001}, 'O': {0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110}, 'P': {0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000}, 'Q': {0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101}, 'R': {0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001}, 'S': {0b01110, 0b10001, 0b10000, 0b01110, 0b00001, 0b10001, 0b01110}, 'T': {0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100}, 'U': {0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110}, 'V': {0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100}, 'W': {0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001}, 'X': {0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001}, 'Y': {0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100}, 'Z': {0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111}, '0': {0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110}, '1': {0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b11111}, '2': {0b01110, 0b10001, 0b00001, 0b00110, 0b01000, 0b10000, 0b11111}, '3': {0b01110, 0b10001, 0b00001, 0b00110, 0b00001, 0b10001, 0b01110}, '4': {0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010}, '5': {0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110}, '6': {0b01110, 0b10000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110}, '7': {0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000}, '8': {0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110}, '9': {0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001, 0b01110}, ' ': {0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000}, '\'': {0b00100, 0b00100, 0b01000, 0b00000, 0b00000, 0b00000, 0b00000}, '.': {0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100}, ',': {0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b01000}, '!': {0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00100}, '?': {0b01110, 0b10001, 0b00001, 0b00110, 0b00100, 0b00000, 0b00100}, '-': {0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000}, '#': {0b01010, 0b01010, 0b11111, 0b01010, 0b11111, 0b01010, 0b01010}, '/': {0b00001, 0b00010, 0b00010, 0b00100, 0b01000, 0b01000, 0b10000}, ':': {0b00000, 0b00100, 0b00000, 0b00000, 0b00000, 0b00100, 0b00000}, '@': {0b01110, 0b10001, 0b10111, 0b10101, 0b10110, 0b10000, 0b01110}, '+': {0b00000, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0b00000}, '&': {0b01100, 0b10010, 0b10100, 0b01000, 0b10101, 0b10010, 0b01101}, } func drawText(img *image.RGBA, text string, x, y, scale int, c color.RGBA) int { cx := x for _, ch := range text { if ch >= 'a' && ch <= 'z' { ch = ch - 'a' + 'A' } glyph, ok := glyphs[ch] if !ok { cx += 4 * scale // skip unknown chars continue } for row := 0; row < 7; row++ { for col := 0; col < 5; col++ { if glyph[row]&(1<<(4-col)) != 0 { fillRect(img, cx+col*scale, y+row*scale, cx+(col+1)*scale, y+(row+1)*scale, c) } } } cx += 6 * scale // 5 pixels + 1 spacing } return cx - x } func textWidth(text string, scale int) int { n := 0 for range text { n++ } if n == 0 { return 0 } return n*6*scale - scale } func (s *Server) handleOGImage(w http.ResponseWriter, r *http.Request) { slug := chi.URLParam(r, "slug") event, err := s.q.GetEventBySlug(r.Context(), slug) if err != nil { http.NotFound(w, r) return } imgW, imgH := 1200, 630 img := image.NewRGBA(image.Rect(0, 0, imgW, imgH)) // Background fillRect(img, 0, 0, imgW, imgH, ogCream) // Yellow header bar fillRect(img, 0, 0, imgW, 80, ogYellow) fillRect(img, 0, 78, imgW, 83, ogInk) // Logo in header: dark box with emoji-colored inner fillRect(img, 40, 16, 88, 64, ogInk) fillRect(img, 44, 20, 84, 60, ogYellow) drawText(img, "BBQ", 100, 26, 4, ogInk) // Center card cardX, cardY := 120, 140 cardW, cardH := imgW-240, 400 // Shadow fillRect(img, cardX+8, cardY+8, cardX+cardW+8, cardY+cardH+8, ogInk) // Border fillRect(img, cardX-3, cardY-3, cardX+cardW+3, cardY+cardH+3, ogInk) // Card fillRect(img, cardX, cardY, cardX+cardW, cardY+cardH, ogWhite) // Tag pill tagText := "POTLUCK" tagW := textWidth(tagText, 3) + 24 fillRect(img, cardX+30-2, cardY+28-2, cardX+30+tagW+2, cardY+28+28+2, ogInk) fillRect(img, cardX+30, cardY+28, cardX+30+tagW, cardY+28+28, ogSage) drawText(img, tagText, cardX+42, cardY+32, 3, ogInk) // Event title - scale to fit title := event.Title scale := 7 for scale > 3 { if textWidth(title, scale) <= cardW-80 { break } scale-- } // Truncate if still too wide for len(title) > 0 && textWidth(title, scale) > cardW-80 { title = title[:len(title)-1] } titleY := cardY + 90 drawText(img, title, cardX+38, titleY, scale, ogInk) // Date/time/location line meta := "" if event.Date != "" { meta = event.Date } if event.Time != "" { if meta != "" { meta += " " } meta += event.Time } if event.Location != "" { if meta != "" { meta += " - " } meta += event.Location } if meta != "" { metaScale := 3 for len(meta) > 0 && textWidth(meta, metaScale) > cardW-80 { meta = meta[:len(meta)-1] } metaY := titleY + 7*scale + 24 drawText(img, meta, cardX+38, metaY, metaScale, color.RGBA{0x66, 0x66, 0x66, 0xff}) } // Decorative slot-like bars at the bottom of the card barY := cardY + cardH - 100 bars := []struct { w int c color.RGBA }{ {320, ogYellow}, {240, ogSage}, {280, ogPeach}, {200, ogYellow}, } for i, b := range bars { y := barY + i*22 fillRect(img, cardX+38, y, cardX+38+b.w, y+14, b.c) fillRect(img, cardX+38, y, cardX+38+b.w, y+14, b.c) } // Bottom accent stripe fillRect(img, 0, imgH-14, imgW, imgH-10, ogInk) fillRect(img, 0, imgH-10, imgW, imgH, ogYellow) var buf bytes.Buffer png.Encode(&buf, img) w.Header().Set("Content-Type", "image/png") w.Header().Set("Cache-Control", "public, max-age=3600") w.Write(buf.Bytes()) }