v0.7.2 71ebe61

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

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