mirror of
https://github.com/aljazceru/njump.git
synced 2025-12-18 06:44:22 +01:00
bring in HarfbuzzShaper into code as a function so we can modify it.
This commit is contained in:
136
image_utils.go
136
image_utils.go
@@ -10,16 +10,24 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fogleman/gg"
|
"github.com/fogleman/gg"
|
||||||
"github.com/go-text/typesetting/di"
|
"github.com/go-text/typesetting/di"
|
||||||
"github.com/go-text/typesetting/font"
|
"github.com/go-text/typesetting/font"
|
||||||
|
"github.com/go-text/typesetting/harfbuzz"
|
||||||
"github.com/go-text/typesetting/language"
|
"github.com/go-text/typesetting/language"
|
||||||
|
"github.com/go-text/typesetting/shaping"
|
||||||
"github.com/pemistahl/lingua-go"
|
"github.com/pemistahl/lingua-go"
|
||||||
|
"github.com/puzpuzpuz/xsync/v2"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
const nSupportedScripts = 13
|
const (
|
||||||
|
nSupportedScripts = 13
|
||||||
|
scaleShift = 6
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
supportedScripts = [nSupportedScripts]language.Script{
|
supportedScripts = [nSupportedScripts]language.Script{
|
||||||
@@ -41,7 +49,7 @@ var (
|
|||||||
detector lingua.LanguageDetector
|
detector lingua.LanguageDetector
|
||||||
scriptRanges []ScriptRange
|
scriptRanges []ScriptRange
|
||||||
fontMap [nSupportedScripts]font.Face
|
fontMap [nSupportedScripts]font.Face
|
||||||
emojiFont font.Face
|
emojiFace font.Face
|
||||||
|
|
||||||
defaultLanguageMap = [nSupportedScripts]language.Language{
|
defaultLanguageMap = [nSupportedScripts]language.Language{
|
||||||
"en-us",
|
"en-us",
|
||||||
@@ -74,6 +82,11 @@ var (
|
|||||||
di.DirectionLTR,
|
di.DirectionLTR,
|
||||||
di.DirectionRTL,
|
di.DirectionRTL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shaperLock sync.Mutex
|
||||||
|
shaperBuffer = harfbuzz.NewBuffer()
|
||||||
|
fontCache = xsync.NewTypedMapOf[font.Face, *harfbuzz.Font](pointerHasher)
|
||||||
|
emojiFont *harfbuzz.Font
|
||||||
)
|
)
|
||||||
|
|
||||||
type ScriptRange struct {
|
type ScriptRange struct {
|
||||||
@@ -133,7 +146,10 @@ func initializeImageDrawingStuff() error {
|
|||||||
fontMap[9] = loadFont("fonts/NotoSansJavanese.ttf")
|
fontMap[9] = loadFont("fonts/NotoSansJavanese.ttf")
|
||||||
fontMap[10] = loadFont("fonts/NotoSansSC.ttf")
|
fontMap[10] = loadFont("fonts/NotoSansSC.ttf")
|
||||||
fontMap[11] = loadFont("fonts/NotoSansKR.ttf")
|
fontMap[11] = loadFont("fonts/NotoSansKR.ttf")
|
||||||
emojiFont = loadFont("fonts/NotoEmoji.ttf")
|
emojiFace = loadFont("fonts/NotoEmoji.ttf")
|
||||||
|
|
||||||
|
// shaper stuff
|
||||||
|
emojiFont = harfbuzz.NewFont(emojiFace)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -325,3 +341,117 @@ func shortenURLs(text string) string {
|
|||||||
return strings.Replace(urlStr, "/////", strings.Join(pathParts, "/"), 1)
|
return strings.Replace(urlStr, "/////", strings.Join(pathParts, "/"), 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shapeText(input shaping.Input) shaping.Output {
|
||||||
|
shaperLock.Lock()
|
||||||
|
defer shaperLock.Unlock()
|
||||||
|
shaperBuffer.Clear()
|
||||||
|
|
||||||
|
runes, start, end := input.Text, input.RunStart, input.RunEnd
|
||||||
|
if end < start {
|
||||||
|
panic("end < start")
|
||||||
|
}
|
||||||
|
start = clamp(start, 0, len(runes))
|
||||||
|
end = clamp(end, 0, len(runes))
|
||||||
|
shaperBuffer.AddRunes(runes, start, end-start)
|
||||||
|
|
||||||
|
shaperBuffer.Props.Direction = input.Direction.Harfbuzz()
|
||||||
|
shaperBuffer.Props.Language = input.Language
|
||||||
|
shaperBuffer.Props.Script = input.Script
|
||||||
|
|
||||||
|
// load or get font from cache
|
||||||
|
hfont, _ := fontCache.LoadOrCompute(input.Face, func() *harfbuzz.Font {
|
||||||
|
return harfbuzz.NewFont(input.Face)
|
||||||
|
})
|
||||||
|
|
||||||
|
// adjust the user provided fields
|
||||||
|
hfont.XScale = int32(input.Size.Ceil()) << scaleShift
|
||||||
|
hfont.YScale = hfont.XScale
|
||||||
|
|
||||||
|
// actually use harfbuzz to shape the text.
|
||||||
|
shaperBuffer.Shape(hfont, nil)
|
||||||
|
|
||||||
|
// convert the shaped text into an output
|
||||||
|
glyphs := make([]shaping.Glyph, len(shaperBuffer.Info))
|
||||||
|
for i := range glyphs {
|
||||||
|
g := shaperBuffer.Info[i].Glyph
|
||||||
|
glyphs[i] = shaping.Glyph{
|
||||||
|
ClusterIndex: shaperBuffer.Info[i].Cluster,
|
||||||
|
GlyphID: g,
|
||||||
|
Mask: shaperBuffer.Info[i].Mask,
|
||||||
|
}
|
||||||
|
extents, ok := hfont.GlyphExtents(g)
|
||||||
|
if !ok {
|
||||||
|
// leave the glyph having zero size if it isn't in the font. There
|
||||||
|
// isn't really anything we can do to recover from such an error.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
glyphs[i].Width = fixed.I(int(extents.Width)) >> scaleShift
|
||||||
|
glyphs[i].Height = fixed.I(int(extents.Height)) >> scaleShift
|
||||||
|
glyphs[i].XBearing = fixed.I(int(extents.XBearing)) >> scaleShift
|
||||||
|
glyphs[i].YBearing = fixed.I(int(extents.YBearing)) >> scaleShift
|
||||||
|
glyphs[i].XAdvance = fixed.I(int(shaperBuffer.Pos[i].XAdvance)) >> scaleShift
|
||||||
|
glyphs[i].YAdvance = fixed.I(int(shaperBuffer.Pos[i].YAdvance)) >> scaleShift
|
||||||
|
glyphs[i].XOffset = fixed.I(int(shaperBuffer.Pos[i].XOffset)) >> scaleShift
|
||||||
|
glyphs[i].YOffset = fixed.I(int(shaperBuffer.Pos[i].YOffset)) >> scaleShift
|
||||||
|
}
|
||||||
|
countClusters(glyphs, input.RunEnd, input.Direction.Progression())
|
||||||
|
out := shaping.Output{
|
||||||
|
Glyphs: glyphs,
|
||||||
|
Direction: input.Direction,
|
||||||
|
Face: input.Face,
|
||||||
|
Size: input.Size,
|
||||||
|
}
|
||||||
|
out.Runes.Offset = input.RunStart
|
||||||
|
out.Runes.Count = input.RunEnd - input.RunStart
|
||||||
|
|
||||||
|
fontExtents := hfont.ExtentsForDirection(out.Direction.Harfbuzz())
|
||||||
|
out.LineBounds = shaping.Bounds{
|
||||||
|
Ascent: fixed.I(int(fontExtents.Ascender)) >> scaleShift,
|
||||||
|
Descent: fixed.I(int(fontExtents.Descender)) >> scaleShift,
|
||||||
|
Gap: fixed.I(int(fontExtents.LineGap)) >> scaleShift,
|
||||||
|
}
|
||||||
|
out.RecalculateAll()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// countClusters tallies the number of runes and glyphs in each cluster
|
||||||
|
// and updates the relevant fields on the provided glyph slice.
|
||||||
|
func countClusters(glyphs []shaping.Glyph, textLen int, dir di.Progression) {
|
||||||
|
currentCluster := -1
|
||||||
|
runesInCluster := 0
|
||||||
|
glyphsInCluster := 0
|
||||||
|
previousCluster := textLen
|
||||||
|
for i := range glyphs {
|
||||||
|
g := glyphs[i].ClusterIndex
|
||||||
|
if g != currentCluster {
|
||||||
|
// If we're processing a new cluster, count the runes and glyphs
|
||||||
|
// that compose it.
|
||||||
|
runesInCluster = 0
|
||||||
|
glyphsInCluster = 1
|
||||||
|
currentCluster = g
|
||||||
|
nextCluster := -1
|
||||||
|
glyphCountLoop:
|
||||||
|
for k := i + 1; k < len(glyphs); k++ {
|
||||||
|
if glyphs[k].ClusterIndex == g {
|
||||||
|
glyphsInCluster++
|
||||||
|
} else {
|
||||||
|
nextCluster = glyphs[k].ClusterIndex
|
||||||
|
break glyphCountLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nextCluster == -1 {
|
||||||
|
nextCluster = textLen
|
||||||
|
}
|
||||||
|
switch dir {
|
||||||
|
case di.FromTopLeft:
|
||||||
|
runesInCluster = nextCluster - currentCluster
|
||||||
|
case di.TowardTopLeft:
|
||||||
|
runesInCluster = previousCluster - currentCluster
|
||||||
|
}
|
||||||
|
previousCluster = g
|
||||||
|
}
|
||||||
|
glyphs[i].GlyphCount = glyphsInCluster
|
||||||
|
glyphs[i].RuneCount = runesInCluster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -186,9 +186,8 @@ func drawText(paragraphs []string, width, height int) image.Image {
|
|||||||
rawText := []rune(paragraph)
|
rawText := []rune(paragraph)
|
||||||
|
|
||||||
lang, script, dir, face := getLanguageAndScriptAndDirectionAndFont(rawText)
|
lang, script, dir, face := getLanguageAndScriptAndDirectionAndFont(rawText)
|
||||||
shaper := &shaping.HarfbuzzShaper{}
|
|
||||||
|
|
||||||
shapedRunes := shaper.Shape(shaping.Input{
|
shapedRunes := shapeText(shaping.Input{
|
||||||
Text: rawText,
|
Text: rawText,
|
||||||
RunStart: 0,
|
RunStart: 0,
|
||||||
RunEnd: len(rawText),
|
RunEnd: len(rawText),
|
||||||
|
|||||||
17
utils.go
17
utils.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/maphash"
|
||||||
"html"
|
"html"
|
||||||
"html/template"
|
"html/template"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
@@ -516,3 +518,18 @@ func maxIndex(slice []int) int {
|
|||||||
}
|
}
|
||||||
return maxIndex
|
return maxIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clamp ensures val is in the inclusive range [low,high].
|
||||||
|
func clamp(val, low, high int) int {
|
||||||
|
if val < low {
|
||||||
|
return low
|
||||||
|
}
|
||||||
|
if val > high {
|
||||||
|
return high
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func pointerHasher[V any](_ maphash.Seed, k *V) uint64 {
|
||||||
|
return uint64(uintptr(unsafe.Pointer(k)))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user