internal/site/og.go
Ref: Size: 4.0 KiB History
// og.go draws the link-preview card — the image a chat client or a timeline
// shows when someone pastes an eitri.sh link. It is drawn from the same
// GohuFont pixels as the mark, at build time, so a card can never disagree
// with the site it came from.
package site
import (
"bytes"
"fmt"
"image"
"image/color"
"image/png"
)
// Card geometry, in image pixels. 1200x630 is what every scraper crops to.
const (
cardW, cardH = 1200, 630
cardInset = 110 // left edge of the block
cardRuleW = 260
cardRuleH = 4
// Scale factors: how many image pixels one font pixel becomes.
scaleMark = 12
scaleWordmark = 6
scaleTagline = 3
scaleLabel = 4
)
// The card's fixed copy. The label under the rule is the page's own.
const (
cardWordmark = "eitri.sh"
cardTagline = "the cloud you already own"
)
// Palette indices: a three-colour card encodes to a few KB.
const (
cardBG = iota
cardFG
cardDim
)
var cardPalette = color.Palette{
mustParseHex(inkLight), // background: the ink colour, inverted card
mustParseHex(inkDark), // foreground
color.RGBA{0x8a, 0x8a, 0x8a, 0xff},
}
func mustParseHex(s string) color.RGBA {
var r, g, b uint8
if _, err := fmt.Sscanf(s, "#%02x%02x%02x", &r, &g, &b); err != nil {
panic("site: bad colour " + s)
}
return color.RGBA{r, g, b, 0xff}
}
// renderCard draws the card for one page. label names the page under the
// rule; empty leaves the block at mark, wordmark and tagline — the landing
// page needs no label, it is the site.
func renderCard(label string) ([]byte, error) {
font, err := parseBDF(gohu14b)
if err != nil {
return nil, err
}
mark, err := markBitmap(font)
if err != nil {
return nil, err
}
// Lay the block out first so it can be centred as a whole.
type row struct {
gap int // space above this row
h int
draw func(img *image.Paletted, y int) error
}
line := func(gap int, s string, scale, ink int) row {
return row{gap: gap, h: font.lineHeight() * scale, draw: func(img *image.Paletted, y int) error {
runs, err := font.runs(s)
if err != nil {
return err
}
w, err := font.width(s)
if err != nil {
return err
}
// Text that overruns the card would be cropped by the scraper,
// silently: fail the build instead.
if room := cardW - 2*cardInset; w*scale > room {
return fmt.Errorf("%q is %dpx wide at scale %d, past the %dpx the card leaves", s, w*scale, scale, room)
}
drawRuns(img, runs, cardInset, y, scale, uint8(ink))
return nil
}}
}
rows := []row{
{h: len(mark) * scaleMark, draw: func(img *image.Paletted, y int) error {
drawRuns(img, bitmapRuns(mark), cardInset, y, scaleMark, cardFG)
return nil
}},
line(44, cardWordmark, scaleWordmark, cardFG),
line(20, cardTagline, scaleTagline, cardDim),
}
if label != "" {
rows = append(rows,
row{gap: 44, h: cardRuleH, draw: func(img *image.Paletted, y int) error {
fillRect(img, cardInset, y, cardRuleW, cardRuleH, cardDim)
return nil
}},
line(28, label, scaleLabel, cardFG))
}
total := 0
for _, r := range rows {
total += r.gap + r.h
}
if total > cardH {
return nil, fmt.Errorf("card block is %dpx tall, taller than the %dpx card", total, cardH)
}
img := image.NewPaletted(image.Rect(0, 0, cardW, cardH), cardPalette)
y := (cardH - total) / 2
for _, r := range rows {
y += r.gap
if err := r.draw(img, y); err != nil {
return nil, err
}
y += r.h
}
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// drawRuns paints font-pixel runs at scale, with the run's own origin taken
// as (ox, oy) in image pixels.
func drawRuns(img *image.Paletted, runs []textRun, ox, oy, scale int, ink uint8) {
for _, r := range runs {
fillRect(img, ox+r.X*scale, oy+r.Y*scale, r.W*scale, scale, ink)
}
}
func fillRect(img *image.Paletted, x, y, w, h int, ink uint8) {
for row := y; row < y+h; row++ {
for col := x; col < x+w; col++ {
if (image.Point{col, row}).In(img.Rect) {
img.SetColorIndex(col, row, ink)
}
}
}
}