v0.1.0 47aaa0f

raster.go RawHistoryFolder

1package text
2
3import (
4	"crypto/sha256"
5	"encoding/binary"
6	"errors"
7	"image"
8	"image/draw"
9	"math"
10
11	"github.com/go-text/typesetting/font"
12	"golang.org/x/image/font/sfnt"
13	"golang.org/x/image/math/fixed"
14	"golang.org/x/image/vector"
15)
16
17// Key identifies a grayscale glyph mask. Size is physical px in 26.6 units;
18// Phase is one of four horizontal quarter-pixel origins (vertical phase is zero).
19type Key struct {
20	FaceID     string
21	Glyph      font.GID
22	Size       fixed.Int26_6
23	Variations [32]byte
24	Phase      uint8
25}
26type Mask struct {
27	Alpha  *image.Alpha
28	Origin image.Point
29}
30
31func VariationHash(v []font.Variation) [32]byte {
32	h := sha256.New()
33	var b [8]byte
34	for _, coord := range v {
35		binary.BigEndian.PutUint32(b[:4], uint32(coord.Tag))
36		binary.BigEndian.PutUint32(b[4:], math.Float32bits(coord.Value))
37		_, _ = h.Write(b[:])
38	}
39	var out [32]byte
40	copy(out[:], h.Sum(nil))
41	return out
42}
43func GlyphKey(face *Face, id font.GID, physicalSize, originX float64, variations []font.Variation) (Key, error) {
44	if face == nil || VariationHash(variations) != VariationHash(face.Variations) || physicalSize <= 0 || physicalSize > 4096 || math.IsNaN(physicalSize) || math.IsInf(physicalSize, 0) || math.IsNaN(originX) || math.IsInf(originX, 0) {
45		return Key{}, errors.New("invalid glyph key")
46	}
47	frac := originX - math.Floor(originX)
48	return Key{FaceID: face.ID, Glyph: id, Size: fixed.Int26_6(math.Round(physicalSize * 64)), Phase: uint8(math.Floor(frac * 4)), Variations: VariationHash(variations)}, nil
49}
50
51// Rasterize uses sfnt for static faces and go-text outlines for variable faces.
52// Neither path hints glyphs. Masks have one pixel of transparent padding for filtering.
53func Rasterize(face *Face, key Key) (Mask, error) {
54	if face == nil || key.FaceID != face.ID || key.Size <= 0 || key.Size > 4096*64 || key.Phase > 3 {
55		return Mask{}, errors.New("invalid raster request")
56	}
57	if VariationHash(face.Variations) != key.Variations {
58		return Mask{}, errors.New("variation key does not match face")
59	}
60	if key.Glyph == font.EmptyGlyph {
61		return Mask{Alpha: image.NewAlpha(image.Rect(0, 0, 0, 0))}, nil
62	}
63	var segments []outlineSegment
64	if face.Variable {
65		outline, ok := face.Shape.GlyphData(key.Glyph).(font.GlyphOutline)
66		if !ok {
67			return Mask{}, errors.New("outline unavailable")
68		}
69		scale := float64(key.Size) / 64 / float64(face.Shape.Upem())
70		for _, s := range outline.Segments {
71			seg := outlineSegment{op: int(s.Op)}
72			for i, p := range s.ArgsSlice() {
73				seg.points[i] = point{float64(p.X) * scale, -float64(p.Y) * scale}
74			}
75			segments = append(segments, seg)
76		}
77	} else {
78		if key.Glyph > 65535 {
79			return Mask{}, errors.New("glyph id out of sfnt range")
80		}
81		var buf sfnt.Buffer
82		segs, err := face.Raster.LoadGlyph(&buf, sfnt.GlyphIndex(key.Glyph), key.Size, nil)
83		if err != nil {
84			return Mask{}, err
85		}
86		for _, s := range segs {
87			seg := outlineSegment{op: int(s.Op)}
88			n := 1
89			if s.Op == sfnt.SegmentOpQuadTo {
90				n = 2
91			} else if s.Op == sfnt.SegmentOpCubeTo {
92				n = 3
93			}
94			for i, p := range s.Args[:n] {
95				seg.points[i] = point{float64(p.X) / 64, float64(p.Y) / 64}
96			}
97			segments = append(segments, seg)
98		}
99	}
100	if len(segments) == 0 {
101		return Mask{Alpha: image.NewAlpha(image.Rect(0, 0, 0, 0))}, nil
102	}
103	phase := float64(key.Phase) / 4
104	loX, loY := math.Inf(1), math.Inf(1)
105	hiX, hiY := math.Inf(-1), math.Inf(-1)
106	for _, s := range segments {
107		n := 1
108		if s.op == 2 {
109			n = 2
110		}
111		if s.op == 3 {
112			n = 3
113		}
114		for _, p := range s.points[:n] {
115			loX = math.Min(loX, p.x+phase)
116			hiX = math.Max(hiX, p.x+phase)
117			loY = math.Min(loY, p.y)
118			hiY = math.Max(hiY, p.y)
119		}
120	}
121	x0, y0 := int(math.Floor(loX))-1, int(math.Floor(loY))-1
122	x1, y1 := int(math.Ceil(hiX))+1, int(math.Ceil(hiY))+1
123	if x1-x0 > 8192 || y1-y0 > 8192 || x1 <= x0 || y1 <= y0 {
124		return Mask{}, errors.New("invalid glyph bounds")
125	}
126	dst := image.NewAlpha(image.Rect(0, 0, x1-x0, y1-y0))
127	v := vector.NewRasterizer(dst.Rect.Dx(), dst.Rect.Dy())
128	v.DrawOp = draw.Src
129	for _, s := range segments {
130		p := s.points
131		a := func(i int) (float32, float32) {
132			return float32(p[i].x + phase - float64(x0)), float32(p[i].y - float64(y0))
133		}
134		switch s.op {
135		case 0:
136			x, y := a(0)
137			v.MoveTo(x, y)
138		case 1:
139			x, y := a(0)
140			v.LineTo(x, y)
141		case 2:
142			x, y := a(0)
143			xx, yy := a(1)
144			v.QuadTo(x, y, xx, yy)
145		case 3:
146			x, y := a(0)
147			xx, yy := a(1)
148			xxx, yyy := a(2)
149			v.CubeTo(x, y, xx, yy, xxx, yyy)
150		}
151	}
152	v.Draw(dst, dst.Bounds(), image.Opaque, image.Point{})
153	return Mask{Alpha: dst, Origin: image.Pt(x0, y0)}, nil
154}
155
156type point struct{ x, y float64 }
157type outlineSegment struct {
158	op     int
159	points [3]point
160}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close