Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: fix greek rendering in opengraph embeds

+87 -30
+2 -1
.gitignore
··· 43 43 # Nix 44 44 result 45 45 result-* 46 - arabica 46 + /arabica 47 + /ogcard 47 48 48 49 # Other 49 50 *.bak
+11
cmd/ogtest/main.go tests/ogcard/main.go
··· 156 156 }) 157 157 }}, 158 158 159 + // Bean - Greek text (non-ASCII unicode) 160 + {"/tmp/og-bean-greek.png", func() (*ogcard.Card, error) { 161 + rating := 7 162 + return ogcard.DrawBeanCard(&models.Bean{ 163 + Name: "Λουμίδης Παπαγάλος Παραδοσιακός Ελληνικός Καφές 100 gr", Origin: "Unspecified Latin American", 164 + RoastLevel: "Medium", Process: "Unknown", Rating: &rating, Variety: "Unspecified Arabica", 165 + Roaster: &models.Roaster{Name: "Λουμίδης Παπαγάλος"}, 166 + Description: "A tasty blend with a balanced roast and earthy notes.", 167 + }) 168 + }}, 169 + 159 170 // Site card 160 171 {"/tmp/og-site.png", func() (*ogcard.Card, error) { 161 172 return ogcard.DrawSiteCard()
+8 -7
internal/ogcard/brew.go
··· 79 79 return y + 36 80 80 } 81 81 82 - // truncate shortens s to maxLen, appending "..." if truncated. 82 + // truncate shortens s to maxLen runes, appending "..." if truncated. 83 83 func truncate(s string, maxLen int) string { 84 - if len(s) > maxLen { 85 - return s[:maxLen-3] + "..." 84 + r := []rune(s) 85 + if len(r) > maxLen { 86 + return string(r[:maxLen-3]) + "..." 86 87 } 87 88 return s 88 89 } ··· 92 93 if card.MeasureText(s, sizePt, bold) <= maxWidth { 93 94 return s 94 95 } 95 - for len(s) > 0 { 96 - candidate := s + "..." 96 + runes := []rune(s) 97 + for len(runes) > 0 { 98 + candidate := string(runes) + "..." 97 99 if card.MeasureText(candidate, sizePt, bold) <= maxWidth { 98 100 return candidate 99 101 } 100 - // trim one rune at a time 101 - s = s[:len(s)-1] 102 + runes = runes[:len(runes)-1] 102 103 } 103 104 return "..." 104 105 }
+43
internal/ogcard/card.go
··· 163 163 return (m.Ascent + m.Descent).Ceil() 164 164 } 165 165 166 + // LineSpacing returns the line spacing (height + gap) for the given point size. 167 + func (c *Card) LineSpacing(sizePt float64) int { 168 + fc, err := getFace(false, sizePt) 169 + if err != nil { 170 + return int(sizePt * 5 / 4) 171 + } 172 + m := fc.Metrics() 173 + lineHeight := (m.Ascent + m.Descent).Ceil() 174 + return lineHeight + lineHeight/4 175 + } 176 + 177 + // CountWrappedLines returns how many lines text would occupy when wrapped, 178 + // capped at maxLines. 179 + func (c *Card) CountWrappedLines(text string, maxWidth int, sizePt float64, bold bool, maxLines int) int { 180 + fc, err := getFace(bold, sizePt) 181 + if err != nil { 182 + return 1 183 + } 184 + words := strings.Fields(text) 185 + if len(words) == 0 { 186 + return 1 187 + } 188 + lines := 1 189 + currentLine := "" 190 + for _, word := range words { 191 + proposed := currentLine 192 + if proposed != "" { 193 + proposed += " " 194 + } 195 + proposed += word 196 + if font.MeasureString(fc, proposed).Ceil() > maxWidth && currentLine != "" { 197 + lines++ 198 + if lines >= maxLines { 199 + return maxLines 200 + } 201 + currentLine = word 202 + } else { 203 + currentLine = proposed 204 + } 205 + } 206 + return lines 207 + } 208 + 166 209 // DrawWrappedText draws word-wrapped text within maxWidth pixels. 167 210 // Returns the Y position after the last line. 168 211 func (c *Card) DrawWrappedText(text string, x, y, maxWidth int, clr color.Color, sizePt float64, bold bool) int {
+23 -22
internal/ogcard/entities.go
··· 29 29 x := leftPad 30 30 31 31 // Calculate content height for centering 32 - h := 58 // name 32 + nameLines := card.CountWrappedLines(bean.Name, maxTextWidth, 44, true, 2) 33 + nameH := card.LineSpacing(44) * nameLines 34 + h := nameH 33 35 hasDetails := bean.Origin != "" || bean.RoastLevel != "" || bean.Process != "" 34 36 if hasDetails { 35 37 h += 42 ··· 50 52 51 53 y := entityStartY(h) 52 54 53 - // Bean name 54 - card.DrawBoldText(truncate(bean.Name, 50), x, y, ColorDark, 44) 55 - y += 58 55 + // Bean name (wrap up to 2 lines) 56 + y = card.DrawWrappedTextCapped(bean.Name, x, y, maxTextWidth, ColorDark, 44, true, 2) 56 57 57 58 // Origin / roast / process 58 59 var details []string ··· 66 67 details = append(details, bean.Process) 67 68 } 68 69 if len(details) > 0 { 69 - card.DrawText(strings.Join(details, dot), x, y, ColorBody, 26) 70 + card.DrawText(truncateLine(card, strings.Join(details, dot), maxTextWidth, 26, false), x, y, ColorBody, 26) 70 71 y += 42 71 72 } 72 73 73 74 // Roaster 74 75 if bean.Roaster != nil && bean.Roaster.Name != "" { 75 - card.DrawText("by "+bean.Roaster.Name, x, y, ColorBody, 26) 76 + card.DrawText(truncateLine(card, "by "+bean.Roaster.Name, maxTextWidth, 26, false), x, y, ColorBody, 26) 76 77 y += 42 77 78 } 78 79 ··· 112 113 113 114 x := leftPad 114 115 115 - h := 58 // name 116 + nameLines := card.CountWrappedLines(roaster.Name, maxTextWidth, 44, true, 2) 117 + h := card.LineSpacing(44) * nameLines 116 118 if roaster.Location != "" { 117 119 h += 44 118 120 } ··· 123 125 124 126 y := entityStartY(h) 125 127 126 - // Roaster name 127 - card.DrawBoldText(truncate(roaster.Name, 50), x, y, ColorDark, 44) 128 - y += 58 128 + // Roaster name (wrap up to 2 lines) 129 + y = card.DrawWrappedTextCapped(roaster.Name, x, y, maxTextWidth, ColorDark, 44, true, 2) 129 130 130 131 // Location 131 132 if roaster.Location != "" { ··· 155 156 156 157 x := leftPad 157 158 158 - h := 58 // name 159 + nameLines := card.CountWrappedLines(grinder.Name, maxTextWidth, 44, true, 2) 160 + h := card.LineSpacing(44) * nameLines 159 161 hasDetails := grinder.GrinderType != "" || grinder.BurrType != "" 160 162 if hasDetails { 161 163 h += 44 ··· 167 169 168 170 y := entityStartY(h) 169 171 170 - // Grinder name 171 - card.DrawBoldText(truncate(grinder.Name, 50), x, y, ColorDark, 44) 172 - y += 58 172 + // Grinder name (wrap up to 2 lines) 173 + y = card.DrawWrappedTextCapped(grinder.Name, x, y, maxTextWidth, ColorDark, 44, true, 2) 173 174 174 175 // Type and burr type 175 176 var details []string ··· 206 207 207 208 x := leftPad 208 209 209 - h := 58 // name 210 + nameLines := card.CountWrappedLines(brewer.Name, maxTextWidth, 44, true, 2) 211 + h := card.LineSpacing(44) * nameLines 210 212 if brewer.BrewerType != "" { 211 213 h += 44 212 214 } ··· 217 219 218 220 y := entityStartY(h) 219 221 220 - // Brewer name 221 - card.DrawBoldText(truncate(brewer.Name, 50), x, y, ColorDark, 44) 222 - y += 58 222 + // Brewer name (wrap up to 2 lines) 223 + y = card.DrawWrappedTextCapped(brewer.Name, x, y, maxTextWidth, ColorDark, 44, true, 2) 223 224 224 225 // Type label 225 226 if brewer.BrewerType != "" { ··· 260 261 261 262 x := leftPad 262 263 263 - h := 58 // name 264 + nameLines := card.CountWrappedLines(recipe.Name, maxTextWidth, 44, true, 2) 265 + h := card.LineSpacing(44) * nameLines 264 266 hasMethod := recipe.BrewerType != "" || (recipe.BrewerObj != nil && recipe.BrewerObj.Name != "") 265 267 if hasMethod { 266 268 h += 44 ··· 278 280 279 281 y := entityStartY(h) 280 282 281 - // Recipe name 282 - card.DrawBoldText(truncate(recipe.Name, 50), x, y, ColorDark, 44) 283 - y += 58 283 + // Recipe name (wrap up to 2 lines) 284 + y = card.DrawWrappedTextCapped(recipe.Name, x, y, maxTextWidth, ColorDark, 44, true, 2) 284 285 285 286 // Brewer type + brewer name 286 287 var methodParts []string
internal/ogcard/fonts/patricks-iosevka-regular.ttf

This is a binary file and will not be displayed.

internal/ogcard/fonts/patricks-iosevka-semibold.ttf

This is a binary file and will not be displayed.