mirror of
https://github.com/aljazceru/njump.git
synced 2025-12-17 06:14:22 +01:00
various meaningless speedups to render_image.
This commit is contained in:
4
data.go
4
data.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]) {
|
||||
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,
|
||||
|
||||
8
utils.go
8
utils.go
@@ -264,12 +264,13 @@ 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 {
|
||||
input[i] = strings.TrimSpace(
|
||||
nostrNpubNprofileMatcher.ReplaceAllStringFunc(line, func(match string) string {
|
||||
submatch := nostrNpubNprofileMatcher.FindStringSubmatch(match)
|
||||
nip19code := submatch[1]
|
||||
name, ok := getNameFromNip19(ctx, nip19code)
|
||||
@@ -277,7 +278,8 @@ func replaceUserReferencesWithNames(ctx context.Context, input []string, prefix
|
||||
return prefix + strings.ReplaceAll(name, " ", string(THIN_SPACE))
|
||||
}
|
||||
return nip19code[0:10] + "…" + nip19code[len(nip19code)-5:]
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user