various meaningless speedups to render_image.

This commit is contained in:
fiatjaf
2024-10-17 00:39:11 -03:00
parent d8efae7473
commit e4ac756648
6 changed files with 66 additions and 57 deletions

View File

@@ -38,9 +38,9 @@ type Data struct {
Kind30818Metadata Kind30818Metadata
}
func grabData(ctx context.Context, code string) (Data, error) {
func grabData(ctx context.Context, code string, withRelays bool) (Data, error) {
// code can be a nevent or naddr, in which case we try to fetch the associated event
event, relays, err := getEvent(ctx, code, true)
event, relays, err := getEvent(ctx, code, withRelays)
if err != nil {
return Data{}, fmt.Errorf("error fetching event: %w", err)
}

View File

@@ -29,6 +29,7 @@ import (
"github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/opentype/api"
"github.com/go-text/typesetting/shaping"
"github.com/golang/freetype/truetype"
"github.com/nbd-wtf/emoji"
"github.com/nfnt/resize"
"github.com/srwiley/rasterx"
@@ -72,6 +73,7 @@ var (
scriptRanges []ScriptRange
fontMap [nSupportedScripts]font.Face
emojiFace font.Face
dateFont *truetype.Font
defaultLanguageMap = [nSupportedScripts]language.Language{
"en-us",
@@ -163,6 +165,9 @@ func initializeImageDrawingStuff() error {
fontMap[12] = loadFont("fonts/NotoSansKR.ttf")
emojiFace = loadFont("fonts/NotoEmoji.ttf")
fontData, _ := fonts.ReadFile("fonts/NotoSans.ttf")
dateFont, _ = truetype.Parse(fontData)
// shaper stuff
emojiFont = harfbuzz.NewFont(emojiFace)
@@ -268,8 +273,11 @@ gotScriptIndex:
return lng, script, direction, face
}
func fetchImageFromURL(url string) (image.Image, error) {
response, err := http.Get(url)
func fetchImageFromURL(ctx context.Context, url string) (image.Image, error) {
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*350)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
response, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch image from %s: %w", url, err)
}
@@ -703,8 +711,8 @@ func drawShapedBlockAt(
return charsWritten, int(math.Ceil(float64(x)))
}
func drawImageAt(img draw.Image, imageUrl string, startY int) int {
srcImg, err := fetchImageFromURL(imageUrl)
func drawImageAt(ctx context.Context, img draw.Image, imageUrl string, startY int) int {
srcImg, err := fetchImageFromURL(ctx, imageUrl)
if err != nil {
return -1
}
@@ -740,7 +748,7 @@ func drawVideoAt(img draw.Image, videoUrl string, startY int) int {
width := img.Bounds().Dx()
resizedFrame := resize.Resize(uint(width), 0, imgData, resize.Lanczos3)
// Draw the play icon on the center of the frame
// draw the play icon on the center of the frame
videoFrame := image.NewRGBA(resizedFrame.Bounds())
draw.Draw(videoFrame, videoFrame.Bounds(), resizedFrame, image.Point{}, draw.Src)
iconFile, _ := static.ReadFile("static/play.png")
@@ -754,16 +762,16 @@ func drawVideoAt(img draw.Image, videoUrl string, startY int) int {
destRect := image.Rect(posX, posY, posX+iconWidth, posY+iconHeight)
draw.Draw(videoFrame, destRect, stampImg, image.Point{}, draw.Over)
// Draw the modified video frame onto the main canvas
// draw the modified video frame onto the main canvas
destRect = image.Rect(0, startY, img.Bounds().Dx(), startY+videoFrame.Bounds().Dy())
draw.Draw(img, destRect, videoFrame, image.Point{}, draw.Src)
return startY + videoFrame.Bounds().Dy()
}
func drawMediaAt(img draw.Image, mediaUrl string, startY int) int {
func drawMediaAt(ctx context.Context, img draw.Image, mediaUrl string, startY int) int {
if isImageURL(mediaUrl) {
return drawImageAt(img, mediaUrl, startY)
return drawImageAt(ctx, img, mediaUrl, startY)
} else if isVideoURL(mediaUrl) {
return drawVideoAt(img, mediaUrl, startY)
} else {
@@ -772,17 +780,12 @@ func drawMediaAt(img draw.Image, mediaUrl string, startY int) int {
}
func isImageURL(input string) bool {
trimmedURL := strings.TrimSpace(input)
if trimmedURL == "" {
return false
}
parsedURL, err := url.Parse(trimmedURL)
parsedURL, err := url.Parse(input)
if err != nil {
return false // Unable to parse URL, consider it non-image URL
return false // unable to parse URL, consider it non-image URL
}
// Extract the path (excluding query string and hash fragment)
// extract the path (excluding query string and hash fragment)
path := parsedURL.Path
imageExtensions := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp"}
for _, ext := range imageExtensions {
@@ -794,14 +797,9 @@ func isImageURL(input string) bool {
}
func isVideoURL(input string) bool {
trimmedURL := strings.TrimSpace(input)
if trimmedURL == "" {
return false
}
parsedURL, err := url.Parse(trimmedURL)
parsedURL, err := url.Parse(input)
if err != nil {
return false // Unable to parse URL, consider it non-image URL
return false // unable to parse URL, consider it non-video URL
}
// Extract the path (excluding query string and hash fragment)

View File

@@ -53,7 +53,7 @@ func renderOEmbed(w http.ResponseWriter, r *http.Request) {
host := r.Header.Get("X-Forwarded-Host")
data, err := grabData(ctx, code)
data, err := grabData(ctx, code, false)
if err != nil {
w.Header().Set("Cache-Control", "max-age=180")
log.Warn().Err(err).Str("code", code).Msg("event not found on oembed")

View File

@@ -63,7 +63,7 @@ func renderEvent(w http.ResponseWriter, r *http.Request) {
}
// get data for this event
data, err := grabData(ctx, code)
data, err := grabData(ctx, code, true)
if err != nil {
w.Header().Set("Cache-Control", "max-age=60")
log.Warn().Err(err).Str("code", code).Msg("event not found on render_event")

View File

@@ -2,6 +2,7 @@ package main
import (
"bytes"
"context"
"embed"
"fmt"
"image"
@@ -45,13 +46,13 @@ func renderImage(w http.ResponseWriter, r *http.Request) {
return
}
// Trim fake extensions
// trim fake extensions
extensions := []string{".png", ".jpg", ".jpeg"}
for _, ext := range extensions {
code = strings.TrimSuffix(code, ext)
}
data, err := grabData(ctx, code)
data, err := grabData(ctx, code, false)
if err != nil {
http.Error(w, "error fetching event: "+err.Error(), http.StatusNotFound)
log.Warn().Err(err).Str("code", code).Msg("event not found on render_image")
@@ -64,6 +65,9 @@ func renderImage(w http.ResponseWriter, r *http.Request) {
content = strings.Replace(content, "\t", " ", -1)
content = strings.Replace(content, "\r", "", -1)
content = shortenURLs(content, true)
if len(content) > 650 {
content = content[0:650]
}
// this turns the raw event.Content into a series of lines ready to drawn
paragraphs := replaceUserReferencesWithNames(ctx,
@@ -73,7 +77,7 @@ func renderImage(w http.ResponseWriter, r *http.Request) {
string(INVISIBLE_SPACE),
)
img, err := drawImage(paragraphs, getPreviewStyle(r), data.event.author, data.createdAt)
img, err := drawImage(ctx, paragraphs, getPreviewStyle(r), data.event.author, data.event.CreatedAt.Time())
if err != nil {
log.Warn().Err(err).Msg("failed to draw paragraphs as image")
http.Error(w, "error writing image!", 500)
@@ -90,10 +94,11 @@ func renderImage(w http.ResponseWriter, r *http.Request) {
}
func drawImage(
ctx context.Context,
paragraphs []string,
style Style,
metadata sdk.ProfileMetadata,
date string,
date time.Time,
) (image image.Image, err error) {
defer func() {
if r := recover(); r != nil {
@@ -146,13 +151,12 @@ func drawImage(
addedSize = int(200.0 / largeness * zoom)
textFontSize = int(float64(fontSize + addedSize))
}
textImg, overflowingText := drawParagraphs(paragraphs, textFontSize, width-paddingLeft*2, height-20-barHeight)
textImg, overflowingText := drawParagraphs(ctx,
paragraphs, textFontSize, width-paddingLeft*2, height-20-barHeight)
img.DrawImage(textImg, paddingLeft, 20)
// font for writing the date
fontData, _ := fonts.ReadFile("fonts/NotoSans.ttf")
ttf, _ := truetype.Parse(fontData)
img.SetFontFace(truetype.NewFace(ttf, &truetype.Options{
img.SetFontFace(truetype.NewFace(dateFont, &truetype.Options{
Size: (6 * barScale),
DPI: 260,
Hinting: xfont.HintingFull,
@@ -178,7 +182,7 @@ func drawImage(
authorTextX := paddingLeft
picHeight := barHeight - 20
if metadata.Picture != "" {
authorImage, err := fetchImageFromURL(metadata.Picture)
authorImage, err := fetchImageFromURL(ctx, metadata.Picture)
if err == nil {
resizedAuthorImage := resize.Resize(uint(barHeight-20), uint(picHeight), roundImage(cropToSquare(authorImage)), resize.Lanczos3)
img.DrawImage(resizedAuthorImage, paddingLeft, height-barHeight+10)
@@ -195,7 +199,7 @@ func drawImage(
}
img.SetColor(color.White)
textImg, _ = drawParagraphs([]string{metadata.ShortName()}, fontSize, width, barHeight)
textImg, _ = drawParagraphs(ctx, []string{metadata.ShortName()}, fontSize, width, barHeight)
img.DrawImage(textImg, authorTextX, authorTextY)
// a gradient to cover too long names
@@ -221,17 +225,15 @@ func drawImage(
stampY := height - barHeight + (barHeight-int(stampHeight))/2
img.DrawImage(resizedStampImg, stampX, stampY)
// Draw event date
layout := "2006-01-02 15:04:05"
parsedTime, _ := time.Parse(layout, date)
formattedDate := parsedTime.Format("Jan 02, 2006")
// draw event date
formattedDate := date.Format("Jan 02, 2006")
img.SetColor(color.RGBA{160, 160, 160, 255})
img.DrawStringWrapped(formattedDate, float64(width-paddingLeft-int(stampWidth)-250), float64(height-barHeight+(barHeight-int(stampHeight))/2)+3, 0, 0, float64(240), 1.5, gg.AlignRight)
return img.Image(), nil
}
func drawParagraphs(paragraphs []string, fontSize int, width, height int) (image.Image, bool) {
func drawParagraphs(ctx context.Context, paragraphs []string, fontSize int, width, height int) (image.Image, bool) {
img := image.NewNRGBA(image.Rect(0, 0, width, height))
lineNumber := 1
@@ -239,16 +241,23 @@ func drawParagraphs(paragraphs []string, fontSize int, width, height int) (image
for i := 0; i < len(paragraphs); i++ {
paragraph := paragraphs[i]
// Skip empty lines if the next element is an image
if paragraph == "" && len(paragraphs) > i+1 && isMediaURL(paragraphs[i+1]) {
continue
if paragraph == "" {
// do not draw lines if the next element is an image
if len(paragraphs) > i+1 && isMediaURL(paragraphs[i+1]) {
continue
} else {
// just move us down a little then jump to the next line
lineNumber++
yPos = yPos + fontSize*12/10
continue
}
}
if isMediaURL(paragraph) {
if i == 0 {
yPos = 0
}
next := drawMediaAt(img, paragraph, yPos)
next := drawMediaAt(ctx, img, paragraph, yPos)
if next != -1 {
yPos = next
// this means the media picture was successfully drawn
@@ -269,7 +278,7 @@ func drawParagraphs(paragraphs []string, fontSize int, width, height int) (image
totalCharsWritten := 0
for _, line := range lines {
for _, out := range line {
for _, out := range line { // this iteration is useless because there is always just one line
charsWritten, _ := drawShapedBlockAt(
img,
fontSize,

View File

@@ -264,20 +264,22 @@ func getNameFromNip19(ctx context.Context, nip19code string) (string, bool) {
// replaces an npub/nprofile with the name of the author, if possible.
// meant to be used when plaintext is expected, not formatted HTML.
func replaceUserReferencesWithNames(ctx context.Context, input []string, prefix string) []string {
// Match and replace npup1 or nprofile1
// match and replace npup1 or nprofile1
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
for i, line := range input {
input[i] = nostrNpubNprofileMatcher.ReplaceAllStringFunc(line, func(match string) string {
submatch := nostrNpubNprofileMatcher.FindStringSubmatch(match)
nip19code := submatch[1]
name, ok := getNameFromNip19(ctx, nip19code)
if ok {
return prefix + strings.ReplaceAll(name, " ", string(THIN_SPACE))
}
return nip19code[0:10] + "…" + nip19code[len(nip19code)-5:]
})
input[i] = strings.TrimSpace(
nostrNpubNprofileMatcher.ReplaceAllStringFunc(line, func(match string) string {
submatch := nostrNpubNprofileMatcher.FindStringSubmatch(match)
nip19code := submatch[1]
name, ok := getNameFromNip19(ctx, nip19code)
if ok {
return prefix + strings.ReplaceAll(name, " ", string(THIN_SPACE))
}
return nip19code[0:10] + "…" + nip19code[len(nip19code)-5:]
}),
)
}
return input
}