Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Go

on:
push:
branches: [ "master", "main" ]
pull_request:
branches: [ "master", "main" ]

permissions:
contents: write
pull-requests: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Install dependencies
run: go mod download

- name: Run go fix
run: go fix ./...

- name: Run go fmt
run: go fmt ./...

- name: Generate screenshot
run: go run cmd/example/main.go

- name: Check for changes
id: git-check
run: |
git diff --exit-code || echo "changes=true" >> $GITHUB_OUTPUT

- name: Create Pull Request
if: steps.git-check.outputs.changes == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Apply go fix, fmt and update screenshot
title: Apply go fix, fmt and update screenshot
body: |
This PR applies changes from `go fix`, `go fmt`, and updates the screenshot.
Auto-generated by GitHub Actions.
branch: auto-fix-fmt-screenshot
delete-branch: true

- name: Fail if changes detected
if: steps.git-check.outputs.changes == 'true'
run: |
echo "Changes detected. PR created."
exit 1

- name: Run go vet
run: go vet ./...

- name: Run go test
run: go test -v ./...

- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
2 changes: 1 addition & 1 deletion cmd/example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (r *ImageRenderer) Draw(verts []fontstash.Vertex) {

c := v0.Color
col := color.RGBA{
R: uint8(c), // R is LSB in C code usually for LE?
R: uint8(c), // R is LSB in C code usually for LE?
G: uint8(c >> 8),
B: uint8(c >> 16),
A: uint8(c >> 24),
Expand Down
278 changes: 146 additions & 132 deletions fontstash/fontstash.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,16 @@

// Internal limits and defaults
const (
maxStates = 20
maxBlur = 20
minFontSize = 2
blurPadding = 2
initAtlasNodes = 256
initFonts = 4
maxVertices = 1024
whiteRectSize = 2
sizeScale = 10.0
vertsPerQuad = 6
maxStates = 20
maxBlur = 20
minFontSize = 2
blurPadding = 2
initAtlasNodes = 256
initFonts = 4
maxVertices = 1024
whiteRectSize = 2
sizeScale = 10.0
vertsPerQuad = 6
)

// Common errors
Expand Down Expand Up @@ -253,128 +253,142 @@
}

func (fs *FontStash) getGlyph(f *Font, codepoint rune, isize, iblur int16) (*Glyph, error) {
if isize < minFontSize { return nil, nil }
if iblur > maxBlur { iblur = maxBlur }
pad := int(iblur) + blurPadding

h := hashInt(int(codepoint)) & (len(f.Lut) - 1)
i := f.Lut[h]
for i != -1 {
g := &f.Glyphs[i]
if g.Codepoint == codepoint && g.Size == isize && g.Blur == iblur {
return g, nil
}
i = g.Next
}

// Create glyph
gIndex := fs.getGlyphIndex(f, codepoint)
renderFont := f
if gIndex == 0 {
for _, fb := range f.Fallbacks {
fallbackFont := fs.Fonts[fb]
fallbackIndex := fs.getGlyphIndex(fallbackFont, codepoint)
if fallbackIndex != 0 {
gIndex = fallbackIndex
renderFont = fallbackFont
break
}
}
}

size := float64(isize) / sizeScale

// Get glyph metrics and bitmap
face, err := opentype.NewFace(renderFont.sfnt, &opentype.FaceOptions{
Size: size,
DPI: 72,
Hinting: font.HintingFull,
})
if err != nil { return nil, err }
defer face.Close()

// Bounds check skipped
_, advance, ok := face.GlyphBounds(codepoint)
if !ok {
// Continue but with empty bounds/image?
// Fallthrough
}

dr, mask, _, _, ok := face.Glyph(fixed.P(0, 0), codepoint)
if !ok {
// Fallthrough
}

gw := dr.Dx() + pad*2
gh := dr.Dy() + pad*2

// Find free spot
gx, gy, ok := fs.Atlas.addRect(gw, gh)
if !ok {
// Atlas full
if fs.Params.ErrorCallback != nil {
fs.Params.ErrorCallback(ErrAtlasFull)
}
// Try again? The C code calls handler and tries again.
// User might resize in callback.
gx, gy, ok = fs.Atlas.addRect(gw, gh)
if !ok {
return nil, ErrAtlasFull
}
}

// Init glyph
glyph := Glyph{
Codepoint: codepoint,
Size: isize,
Blur: iblur,
Index: gIndex,
X0: int16(gx),
Y0: int16(gy),
X1: int16(gx + gw),
Y1: int16(gy + gh),
XAdv: int16(int32(advance) * sizeScale / 64),
XOff: int16(dr.Min.X - pad),
YOff: int16(dr.Min.Y - pad),
}

// Copy bitmap to texture
dst := fs.TexData
width := fs.Params.Width

if mask != nil {
b := mask.Bounds()
for y := 0; y < b.Dy(); y++ {
for x := 0; x < b.Dx(); x++ {
_, _, _, a := mask.At(x + b.Min.X, y + b.Min.Y).RGBA()
val := uint8(a >> 8)

targetX := gx + pad + x
targetY := gy + pad + y
if targetX < width && targetY < fs.Params.Height {
dst[targetY * width + targetX] = val
}
}
}
}

// Blur if needed
if iblur > 0 {
fs.blur(gx, gy, gw, gh, width, int(iblur))
}

// Update dirty rect
if gx < fs.Dirty.Min.X { fs.Dirty.Min.X = gx }
if gy < fs.Dirty.Min.Y { fs.Dirty.Min.Y = gy }
if gx+gw > fs.Dirty.Max.X { fs.Dirty.Max.X = gx+gw }
if gy+gh > fs.Dirty.Max.Y { fs.Dirty.Max.Y = gy+gh }

// Add to cache
f.Glyphs = append(f.Glyphs, glyph)
f.Glyphs[len(f.Glyphs)-1].Next = f.Lut[h]
f.Lut[h] = len(f.Glyphs) - 1

return &f.Glyphs[len(f.Glyphs)-1], nil
if isize < minFontSize {
return nil, nil
}
if iblur > maxBlur {
iblur = maxBlur
}
pad := int(iblur) + blurPadding

h := hashInt(int(codepoint)) & (len(f.Lut) - 1)
i := f.Lut[h]
for i != -1 {
g := &f.Glyphs[i]
if g.Codepoint == codepoint && g.Size == isize && g.Blur == iblur {
return g, nil
}
i = g.Next
}

// Create glyph
gIndex := fs.getGlyphIndex(f, codepoint)
renderFont := f
if gIndex == 0 {
for _, fb := range f.Fallbacks {
fallbackFont := fs.Fonts[fb]
fallbackIndex := fs.getGlyphIndex(fallbackFont, codepoint)
if fallbackIndex != 0 {
gIndex = fallbackIndex
renderFont = fallbackFont
break
}
}
}

size := float64(isize) / sizeScale

// Get glyph metrics and bitmap
face, err := opentype.NewFace(renderFont.sfnt, &opentype.FaceOptions{
Size: size,
DPI: 72,
Hinting: font.HintingFull,
})
if err != nil {
return nil, err
}
defer face.Close()

// Bounds check skipped
_, advance, ok := face.GlyphBounds(codepoint)

Check failure on line 303 in fontstash/fontstash.go

View workflow job for this annotation

GitHub Actions / build

SA4006: this value of `ok` is never used (staticcheck)
if !ok {

Check failure on line 304 in fontstash/fontstash.go

View workflow job for this annotation

GitHub Actions / build

SA9003: empty branch (staticcheck)
// Continue but with empty bounds/image?
// Fallthrough
}

dr, mask, _, _, ok := face.Glyph(fixed.P(0, 0), codepoint)

Check failure on line 309 in fontstash/fontstash.go

View workflow job for this annotation

GitHub Actions / build

SA4006: this value of `ok` is never used (staticcheck)
if !ok {

Check failure on line 310 in fontstash/fontstash.go

View workflow job for this annotation

GitHub Actions / build

SA9003: empty branch (staticcheck)
// Fallthrough
}

gw := dr.Dx() + pad*2
gh := dr.Dy() + pad*2

// Find free spot
gx, gy, ok := fs.Atlas.addRect(gw, gh)
if !ok {
// Atlas full
if fs.Params.ErrorCallback != nil {
fs.Params.ErrorCallback(ErrAtlasFull)
}
// Try again? The C code calls handler and tries again.
// User might resize in callback.
gx, gy, ok = fs.Atlas.addRect(gw, gh)
if !ok {
return nil, ErrAtlasFull
}
}

// Init glyph
glyph := Glyph{
Codepoint: codepoint,
Size: isize,
Blur: iblur,
Index: gIndex,
X0: int16(gx),
Y0: int16(gy),
X1: int16(gx + gw),
Y1: int16(gy + gh),
XAdv: int16(int32(advance) * sizeScale / 64),
XOff: int16(dr.Min.X - pad),
YOff: int16(dr.Min.Y - pad),
}

// Copy bitmap to texture
dst := fs.TexData
width := fs.Params.Width

if mask != nil {
b := mask.Bounds()
for y := 0; y < b.Dy(); y++ {
for x := 0; x < b.Dx(); x++ {
_, _, _, a := mask.At(x+b.Min.X, y+b.Min.Y).RGBA()
val := uint8(a >> 8)

targetX := gx + pad + x
targetY := gy + pad + y
if targetX < width && targetY < fs.Params.Height {
dst[targetY*width+targetX] = val
}
}
}
}

// Blur if needed
if iblur > 0 {
fs.blur(gx, gy, gw, gh, width, int(iblur))
}

// Update dirty rect
if gx < fs.Dirty.Min.X {
fs.Dirty.Min.X = gx
}
if gy < fs.Dirty.Min.Y {
fs.Dirty.Min.Y = gy
}
if gx+gw > fs.Dirty.Max.X {
fs.Dirty.Max.X = gx + gw
}
if gy+gh > fs.Dirty.Max.Y {
fs.Dirty.Max.Y = gy + gh
}

// Add to cache
f.Glyphs = append(f.Glyphs, glyph)
f.Glyphs[len(f.Glyphs)-1].Next = f.Lut[h]
f.Lut[h] = len(f.Glyphs) - 1

return &f.Glyphs[len(f.Glyphs)-1], nil
}

func (fs *FontStash) blur(x, y, w, h, stride, blur int) {
Expand Down
Loading