use NotoSans for all text except when hiragana or katakana is detected, then use NotoSansJP -- and break japanese characters even without whitespace.

This commit is contained in:
fiatjaf
2023-10-19 15:00:58 -03:00
parent 9882be5cb6
commit 3e762a8eea
2 changed files with 111 additions and 12 deletions

BIN
fonts/NotoSans.ttf Normal file

Binary file not shown.

View File

@@ -2,17 +2,19 @@ package main
import ( import (
"context" "context"
"embed"
"fmt" "fmt"
"image" "image"
"image/draw" "image/draw"
"image/png" "image/png"
"net/http" "net/http"
"os"
"strings" "strings"
"time" "time"
"unicode"
"github.com/apatters/go-wordwrap" "github.com/apatters/go-wordwrap"
"github.com/lukevers/freetype-go/freetype" "github.com/lukevers/freetype-go/freetype"
"github.com/lukevers/freetype-go/freetype/truetype"
) )
const ( const (
@@ -24,6 +26,9 @@ const (
BLOCK = "▒" BLOCK = "▒"
) )
//go:embed fonts/*
var fonts embed.FS
func renderImage(w http.ResponseWriter, r *http.Request) { func renderImage(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.URL.Path, ":~", r.Header.Get("user-agent")) fmt.Println(r.URL.Path, ":~", r.Header.Get("user-agent"))
@@ -39,15 +44,24 @@ func renderImage(w http.ResponseWriter, r *http.Request) {
return return
} }
// get the font and language specifics based on the characters used
font, breakWords, err := getLanguage(event.Content)
if err != nil {
http.Error(w, "error getting font: "+err.Error(), 500)
return
}
// this turns the raw event.Content into a series of lines ready to drawn
lines := normalizeText( lines := normalizeText(
replaceUserReferencesWithNames(r.Context(), replaceUserReferencesWithNames(r.Context(),
renderQuotesAsBlockPrefixedText(r.Context(), renderQuotesAsBlockPrefixedText(r.Context(),
event.Content, event.Content,
), ),
), ),
breakWords,
) )
img, err := drawImage(lines, getPreviewStyle(r)) img, err := drawImage(lines, font, getPreviewStyle(r))
if err != nil { if err != nil {
log.Printf("error writing image: %s", err) log.Printf("error writing image: %s", err)
http.Error(w, "error writing image!", 500) http.Error(w, "error writing image!", 500)
@@ -64,7 +78,7 @@ func renderImage(w http.ResponseWriter, r *http.Request) {
} }
} }
func normalizeText(input []string) []string { func normalizeText(input []string, breakWords bool) []string {
lines := make([]string, 0, MAX_LINES) lines := make([]string, 0, MAX_LINES)
l := 0 // global line counter l := 0 // global line counter
@@ -84,12 +98,47 @@ func normalizeText(input []string) []string {
return lines return lines
} }
line = wordwrap.Wrap(maxChars, strings.TrimSpace(line)) // turn a single line into multiple if it is long enough -- carefully splitting on word ends
for _, subline := range strings.Split(line, "\n") { wrappedLines := strings.Split(wordwrap.Wrap(maxChars, strings.TrimSpace(line)), "\n")
// now we go over all these lines and further split them if necessary
// in japanese, for example, we must break the words otherwise nothing works
var sublines []string
if breakWords {
sublines = make([]string, 0, len(wrappedLines))
for _, wline := range wrappedLines {
// split until we have a bunch of lines all under maxChars
for {
if len(wline) > maxChars {
// we can't split exactly at maxChars because that would break utf-8 runes
// so we do this range mess to try to grab where the last rune in the line ends
subline := make([]rune, 0, maxChars)
var i int
var r rune
for i, r = range wline {
if i > maxChars {
break
}
subline = append(subline, r)
}
sublines = append(sublines, string(subline))
wline = wline[i:]
} else {
sublines = append(sublines, wline)
break
}
}
}
} else {
sublines = wrappedLines
}
for _, subline := range sublines {
// if a line has a word so big that it would overflow (like a nevent), hide it with an ellipsis // if a line has a word so big that it would overflow (like a nevent), hide it with an ellipsis
if len(subline) > maxChars { if len([]rune(subline)) > maxChars {
subline = subline[0:maxChars-1] + "…" subline = subline[0:maxChars-1] + "…"
} }
if quoting { if quoting {
subline = BLOCK + " " + subline subline = BLOCK + " " + subline
} }
@@ -102,7 +151,7 @@ func normalizeText(input []string) []string {
return lines return lines
} }
func drawImage(lines []string, style string) (image.Image, error) { func drawImage(lines []string, font *truetype.Font, style string) (image.Image, error) {
width := 700 width := 700
height := 525 height := 525
paddingLeft := 0 paddingLeft := 0
@@ -120,11 +169,7 @@ func drawImage(lines []string, style string) (image.Image, error) {
// draw the empty image // draw the empty image
draw.Draw(img, img.Bounds(), bg, image.Point{}, draw.Src) draw.Draw(img, img.Bounds(), bg, image.Point{}, draw.Src)
// create new freetype context to get ready for // create new freetype context to get ready for adding text.
// adding text.
fontData, _ := os.ReadFile("fonts/NotoSansJP.ttf")
font, _ := freetype.ParseFont(fontData)
c := freetype.NewContext() c := freetype.NewContext()
c.SetDPI(300) c.SetDPI(300)
c.SetFont(font) c.SetFont(font)
@@ -203,3 +248,57 @@ func renderQuotesAsBlockPrefixedText(ctx context.Context, input string) []string
return blocks return blocks
} }
func getLanguage(text string) (*truetype.Font, bool, error) {
fontName := "fonts/NotoSans.ttf"
shouldBreakWords := false
for _, group := range []struct {
lang *unicode.RangeTable
fontName string
breakWords bool
}{
{
unicode.Katakana,
"fonts/NotoSansJP.ttf",
true,
},
{
unicode.Hiragana,
"fonts/NotoSansJP.ttf",
true,
},
} {
for _, rune := range text {
rune16 := uint16(rune)
for _, r16 := range group.lang.R16 {
if rune16 >= r16.Lo && rune16 <= r16.Hi {
fontName = group.fontName
shouldBreakWords = group.breakWords
goto gotLang
}
}
rune32 := uint32(rune)
for _, r32 := range group.lang.R32 {
if rune32 >= r32.Lo && rune32 <= r32.Hi {
fontName = group.fontName
shouldBreakWords = group.breakWords
goto gotLang
}
}
}
}
gotLang:
fontData, err := fonts.ReadFile(fontName)
if err != nil {
return nil, false, err
}
font, err := freetype.ParseFont(fontData)
if err != nil {
return nil, false, err
}
return font, shouldBreakWords, nil
}