Compare commits

...

40 Commits

Author SHA1 Message Date
fe23b63070 Improve doc comment for TypeSetter.AtPosition 2024-09-20 00:47:03 -04:00
436686961b Remove old files 2024-09-20 00:39:43 -04:00
f386684454 Add AtPosition to TypeSetter 2024-09-20 00:38:23 -04:00
76bd2837d3 Add debug package 2024-09-20 00:13:01 -04:00
de12ee6bcd Move some drawing functionality into internal package 2024-09-20 00:07:32 -04:00
4be5154df0 Add RecommendedHeight to TypeSetter 2024-09-19 23:57:23 -04:00
197346d730 Add algorithm for figuring out the recommended height of text 2024-09-19 23:54:08 -04:00
ef8944f2a6 Measure tokens in the benchmarks 2024-09-19 22:08:37 -04:00
4c42b12cc1 Have the test example stress the flow algorithm a bit more 2024-09-19 21:22:21 -04:00
6efe40efc2 Sort methods of TypeSetter alphabetically 2024-09-19 21:10:02 -04:00
17385c4c9a Add RunesWithNull iterator 2024-09-19 21:08:14 -04:00
e38cac8e3b Benchmark text flow 2024-09-19 21:05:18 -04:00
90b2e49664 Fix doc comment on TypeSetter.Runes 2024-09-19 10:43:50 -04:00
2ae07af710 Add little TODO 2024-09-19 10:43:13 -04:00
ce21b34f86 Fix LayoutBoundsSpace 2024-09-19 10:42:48 -04:00
ff8f86e034 5 is the appears to be the sweet spot for average token len 2024-09-19 09:40:00 -04:00
288a1fb9ef Fix token slice size estimation to massively reduce waste 2024-09-19 09:36:01 -04:00
a91816df6c Benchmarks report token waste 2024-09-19 09:33:14 -04:00
aa486fe660 Report waste as a fraction 2024-09-19 09:20:44 -04:00
cde84b8756 Benchmarks report len, cap, and waste of runes slice 2024-09-19 09:18:11 -04:00
013b121d46 Made Chinese lorem ipsum same # of runes as Latin lorem ipsum 2024-09-19 09:06:29 -04:00
85c48461c7 Add parsing benchmarks for latin and chinese text 2024-09-19 09:03:09 -04:00
0342e25456 LayoutBounds can have a negative start
This causes the LayoutBounds of center-aligned, left-aligned text
to be more accurate
2024-09-19 08:49:49 -04:00
f0adca5c37 Minimum size is calculated from extents instead of vice versa 2024-09-19 08:31:21 -04:00
56024caaf5 Fix LayoutBounds calculation
More work is needed for LayoutBoundsSpace
2024-09-19 08:07:39 -04:00
5171cbac16 Fix the memory problem 2024-09-18 23:48:21 -04:00
300c28853d Add another memory test just to be sure 2024-09-18 23:37:57 -04:00
6fabfd9fd0 Test whether tokens reference the same memory as runes 2024-09-18 23:28:27 -04:00
6b6e485aca Add documentation on what the DrawBounds colors mean 2024-09-18 22:54:25 -04:00
89c23a8947 Add test example 2024-09-10 15:52:56 -04:00
0cb6e28542 Add drawing functions 2024-09-10 15:52:46 -04:00
5dee53b8a9 Add incomplete TypeSetter struct 2024-09-10 15:52:33 -04:00
9c7732c95b Add incomplete reflow stage 2024-09-10 15:52:18 -04:00
37554dd719 Add measurement stage 2024-09-10 11:20:08 -04:00
569defdb36 Add parsing stage 2024-09-10 11:19:57 -04:00
0c9d50ebcd Remove setter.go 2024-09-10 11:19:31 -04:00
943fc57080 Remove old files from repository root 2024-09-10 11:18:13 -04:00
0592fe32b6 Add .editorconfig because why not 2024-09-10 11:17:52 -04:00
56cf7e3fb8 Update README.md 2024-09-10 11:17:29 -04:00
650ecf0c2e Back up old files 2024-09-10 11:17:10 -04:00
18 changed files with 1332 additions and 760 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 8
charset = utf-8
[*.md]
indent_style = space
indent_size = 2

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
output.png

View File

@ -2,7 +2,20 @@
[![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/typeset.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/typeset)
Typeset provides utilities for text layout, wrapping, and rendering.
Typeset provides utilities for text layout, wrapping, and rendering. It is
designed to avoid redundant work and minimize memory allocations wherever
posible in situations where the bounds of a section of text may change
frequently and its content semi-frequently. Text layout is performed by the
TypeSetter struct, which operates in a three-phase process:
The state of a text layout is stored in a TypeSetter, and it can be drawn to any
draw.Image using a Drawer which "extends" TypeSetter.
1. Tokenization
2. Measurement
3. Layout, alignment
The results of these phases are memoized. When the state of the TypeSetter is
queried, it will run through only the required phases before returning a value.
The contents of a TypeSetter can be drawn onto any draw.Image using the Draw
function included within this package, but it is entirely possible to create a
custom draw function that iterates over TypeSetter.Runes that uses some other
method of drawing that's faster than five gazillion virtual method calls.

28
debug/debug.go Normal file
View File

@ -0,0 +1,28 @@
package debug
import "image/draw"
import "image/color"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/tomo/typeset"
import "git.tebibyte.media/tomo/typeset/internal"
// DrawBounds draws the LayoutBounds, MinimumSize, and LayoutBoundsSpace of a
// TypeSetter to the given image using these colors:
// - Red: LayoutBounds
// - Green: MinimumSize
// - Blue: LayoutBoundsSpace
func DrawBounds (destination draw.Image, setter *typeset.TypeSetter, offset fixed.Point26_6) {
blue := color.RGBA { B: 255, A: 255 }
red := color.RGBA { R: 255, A: 255 }
green := color.RGBA { G: 255, A: 255 }
layoutBoundsSpace := setter.LayoutBoundsSpace()
layoutBounds := setter.LayoutBounds()
minimum := setter.MinimumSize()
minimumRect := internal.RoundRect(fixed.Rectangle26_6 { Max: minimum }.Add(offset).Add(layoutBounds.Min))
internal.DrawRectangleOutline(destination, minimumRect, green)
internal.DrawRectangleOutline(destination, internal.RoundRect(layoutBoundsSpace.Add(offset)), blue)
internal.DrawRectangleOutline(destination, internal.RoundRect(layoutBounds.Add(offset)), red)
}

65
draw.go Normal file
View File

@ -0,0 +1,65 @@
package typeset
import "image"
import "unicode"
import "image/draw"
import "image/color"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/tomo/typeset/internal"
// Draw draws the contents of a TypeSetter to an image at the given offset. It
// returns a rectangle containing all pixels in the image that were updated.
func Draw (destination draw.Image, setter *TypeSetter, offset fixed.Point26_6, col color.Color) image.Rectangle {
source := image.NewUniform(col)
face := setter.Face()
var updatedRegion image.Rectangle
bounds := destination.Bounds()
setter.Runes()(func (position fixed.Point26_6, run rune) bool {
// leave empty space for space characters
if unicode.IsSpace(run) {
return true
}
dot := offset.Add(position)
destinationRectangle, mask, maskPoint, _, ok := face.Glyph(dot, run)
if ok {
// don't bother drawing runes that are out of bounds
if destinationRectangle.Min.Y > bounds.Max.Y { return false }
if destinationRectangle.Intersect(bounds).Empty() { return true }
// draw rune
draw.DrawMask (
destination,
destinationRectangle,
source, image.Point { },
mask, maskPoint,
draw.Over)
} else {
// draw tofu
drawTofu(run, destination, dot, face, col)
}
updatedRegion = updatedRegion.Union(destinationRectangle)
return true
})
return updatedRegion
}
func drawTofu (
char rune,
destination draw.Image,
position fixed.Point26_6,
face font.Face,
col color.Color,
) {
bounds, _ := tofuBounds(face)
rectBounds := internal.RoundRect(bounds).Add(image.Pt (
position.X.Round(),
position.Y.Round()))
internal.DrawRectangleOutline(destination, rectBounds, col)
}

View File

@ -1,84 +0,0 @@
package typeset
import "image"
import "unicode"
import "image/draw"
import "image/color"
import "golang.org/x/image/math/fixed"
// Drawer is an extended TypeSetter that is able to draw text. Much like
// TypeSetter, It has no constructor and its zero value can be used safely.
type Drawer struct { TypeSetter }
// Draw draws the drawer's text onto the specified canvas at the given offset.
func (drawer Drawer) Draw (
destination draw.Image,
col color.Color,
offset image.Point,
) (
updatedRegion image.Rectangle,
) {
source := image.NewUniform(col)
drawer.ForRunes (func (
index int,
char rune,
position fixed.Point26_6,
) bool {
// leave empty space for space characters
if unicode.IsSpace(char) {
return true
}
dot := fixed.P (
offset.X + position.X.Round(),
offset.Y + position.Y.Round())
destinationRectangle,
mask, maskPoint, _, ok := drawer.face.Glyph(dot, char)
// tofu
if !ok {
drawer.drawTofu(char, destination, col, dot)
return true
}
// FIXME:? clip destination rectangle if we are on the cusp of
// the maximum height.
draw.DrawMask (
destination,
destinationRectangle,
source, image.Point { },
mask, maskPoint,
draw.Over)
updatedRegion = updatedRegion.Union(destinationRectangle)
return true
})
return
}
func (drawer Drawer) drawTofu (
char rune,
destination draw.Image,
col color.Color,
position fixed.Point26_6,
) {
bounds, _ := tofuBounds(drawer.face)
rectBounds := image.Rect (
bounds.Min.X.Round(),
bounds.Min.Y.Round(),
bounds.Max.X.Round(),
bounds.Max.Y.Round()).Add(image.Pt(
position.X.Round(),
position.Y.Round()))
for x := rectBounds.Min.X; x < rectBounds.Max.X; x ++ {
destination.Set(x, rectBounds.Min.Y, col)
}
for y := rectBounds.Min.Y; y < rectBounds.Max.Y; y ++ {
destination.Set(rectBounds.Min.X, y, col)
destination.Set(rectBounds.Max.X - 1, y, col)
}
for x := rectBounds.Min.X; x < rectBounds.Max.X; x ++ {
destination.Set(x, rectBounds.Max.Y - 1, col)
}
}

165
examples/test/main.go Normal file
View File

@ -0,0 +1,165 @@
// Example test demonstrates a variety of ways that TypeSetter can arrange text.
package main
import "os"
import "image"
import "image/png"
import "image/draw"
import "image/color"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/tomo/typeset"
import "golang.org/x/image/font/basicfont"
import "git.tebibyte.media/tomo/typeset/debug"
func main () {
img := image.NewRGBA(image.Rect(0, 0, 2048, 1024))
setter := typeset.TypeSetter { }
setter.SetWrap(true)
setter.SetFace(basicfont.Face7x13)
setter.SetText(lipsum)
setter.SetAlign(typeset.AlignStart, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Inset(4))
setter.SetText(dimple)
setter.SetAlign(typeset.AlignStart, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(512, 0)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(512, 0)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(512, 0)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(512, 0)).Inset(4))
setter.SetAlign(typeset.AlignStart, typeset.AlignMiddle)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(0, 512)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(0, 512)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignMiddle)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(0, 512)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(0, 512)).Inset(4))
setter.SetAlign(typeset.AlignStart, typeset.AlignEnd)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(512, 512)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignEnd)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(512, 512)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignEnd)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(512, 512)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignEnd)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(512, 512)).Inset(4))
setter.SetText(haiku)
setter.SetWrap(false)
setter.SetAlign(typeset.AlignStart, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1024, 0)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1024, 0)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1024, 0)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(1024, 0)).Inset(4))
setter.SetAlign(typeset.AlignStart, typeset.AlignMiddle)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1536, 0)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1536, 0)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignMiddle)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1536, 0)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(1536, 0)).Inset(4))
setter.SetAlign(typeset.AlignStart, typeset.AlignEnd)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1024, 512)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignEnd)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1024, 512)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignEnd)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1024, 512)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignEnd)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(1024, 512)).Inset(4))
setter.SetText(haikuAlt)
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1536, 512)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1536, 512)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignEnd)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1536, 512)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(1536, 512)).Inset(4))
file, err := os.Create("output.png")
if err != nil { panic(err) }
defer file.Close()
err = png.Encode(file, img)
if err != nil { panic(err) }
}
type subDrawImage interface {
draw.Image
SubImage (image.Rectangle) image.Image
}
func drawText (destination subDrawImage, setter *typeset.TypeSetter, bounds image.Rectangle) {
whiteRectangle(destination, bounds)
subImage := destination.SubImage(bounds).(draw.Image)
metrics := setter.Face().Metrics()
bounds = bounds.Inset(16)
grayRectangle(destination, bounds)
size := fixed.P(bounds.Dx(), bounds.Dy())
setter.SetSize(size)
offset := fixed.Point26_6 {
X: fixed.I(bounds.Min.X),
Y: metrics.Ascent + fixed.I(bounds.Min.Y),
}
// typeset.Draw(destination, setter, offset, color.Black)
debug.DrawBounds(subImage, setter, offset)
typeset.Draw(subImage, setter, offset, color.Black)
}
func whiteRectangle (destination draw.Image, rect image.Rectangle) {
draw.Over.Draw(destination, rect, image.NewUniform(color.RGBA { R: 150, G: 150, B: 150, A: 255 }), image.Pt(0, 0))
}
func grayRectangle (destination draw.Image, rect image.Rectangle) {
draw.Over.Draw(destination, rect, image.NewUniform(color.RGBA { R: 200, G: 200, B: 200, A: 255 }), image.Pt(0, 0))
}
const lipsum = `Eum officia beatae harum. Rem aut praesentium possimus dignissimos ea sed. Recusandae sint rerum ut. Qui delectus rerum ut ut. Nobis non veritatis consequatur quia explicabo id. Et aut qui reiciendis esse voluptatem.
Eaque rem incidunt porro unde quia expedita quia. Deleniti repellat modi placeat. Et beatae aut voluptatem. Veritatis perspiciatis et aperiam sit modi sequi.
Accusantium et fugit expedita consequatur incidunt explicabo ea voluptatibus. Debitis consectetur veniam ut et esse aspernatur. Quas occaecati explicabo consequuntur. Quae dolorem ea harum ut tempora. Corporis ducimus et voluptatem. Corporis distinctio quia velit accusantium sunt omnis.
Libero blanditiis aut aut exercitationem modi. Eum corporis quam facere. Perferendis sit nulla et qui repellat eaque neque in. Expedita quidem similique sunt delectus similique non assumenda.
Hic rerum earum sapiente et itaque harum. Itaque amet voluptatem aliquid. Et qui excepturi animi voluptatem debitis necessitatibus atque animi. Nemo voluptates delectus quisquam non. Ipsam error voluptas similique dolores odit quos.`
const dimple = `I have been trying to remember this for a while
I could never place it
My brain kept saying Dimple when I tried to remember. Ever closer yet unobtainable. Censored, even. It did not want me to remember the dumple...`
const haiku = `An ocean voyage.
As waves break over the bow,
the sea welcomes me.
This is a very long line that will probably get cut off.`
const haikuAlt = `An ocean voyage.
As waves break over the bow,
the sea welcomes me.
This is a short ending`

239
flow.go Normal file
View File

@ -0,0 +1,239 @@
package typeset
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
// TODO perhaps follow https://unicode.org/reports/tr14/
func reflow (
tokens []token,
face font.Face, size fixed.Point26_6,
wrap bool, xAlign, yAlign Align,
) (
extents, extentsSpace fixed.Rectangle26_6,
minimumSize fixed.Point26_6,
) {
if len(tokens) == 0 { return }
metrics := face.Metrics()
var dot fixed.Point26_6
const notSeen = -1
firstWord := notSeen
lineStart := 0
lineEnd := 0
lastWord := 0
lastNonLineBreak := notSeen
nLines := 0
firstLine := true
newline := func (wrapped bool) {
// if the line isn't empty
if lineStart != lineEnd {
// align line
alignLine (
tokens[lineStart:lineEnd],
size.X, xAlign, lineEnd == len(tokens))
// calculate extents
var lineMin, lineMinSpace, lineMax, lineMaxSpace fixed.Int26_6
lineMinSpace = tokens[lineStart].position.X
if firstWord == notSeen {
lineMin = lineMinSpace
} else {
lineMin = tokens[firstWord].position.X
}
lastWordTok := tokens[lastWord]
if lastWordTok.kind == tokenKindWord {
// the line had a word in it
lineMax = lastWordTok.position.X + lastWordTok.width
}
if wrapped || lastNonLineBreak == notSeen {
lineMaxSpace = lineMax
} else {
lastTokenTok := tokens[lastNonLineBreak]
lineMaxSpace = lastTokenTok.position.X + lastTokenTok.width
}
// println(lineMax.String(), lineMaxSpace.String())
if lineMin < extents.Min.X || firstLine { extents.Min.X = lineMin }
if lineMinSpace < extentsSpace.Min.X || firstLine { extentsSpace.Min.X = lineMinSpace }
if lineMax > extents.Max.X { extents.Max.X = lineMax }
if lineMaxSpace > extentsSpace.Max.X { extentsSpace.Max.X = lineMaxSpace }
firstLine = false
}
// update dot
dot.Y += metrics.Height
dot.X = 0
// update indices, counts
lineStart = lineEnd
lastWord = lineEnd
lastNonLineBreak = notSeen
firstWord = notSeen
nLines ++
}
// for each line, arrange and align while calculating effective
// bounds/extents
sawLineBreak := false
for index, token := range tokens {
lineEnd = index
updateIndices := func () {
if token.kind != tokenKindLineBreak {
lastNonLineBreak = index
}
if token.kind == tokenKindWord {
lastWord = index
if firstWord == notSeen {
firstWord = index
}
}
}
// demarcate lines
if sawLineBreak {
newline(false)
sawLineBreak = false
}
if token.kind == tokenKindLineBreak {
updateIndices()
tokens[index].position = dot
sawLineBreak = true
} else {
needWrap :=
wrap &&
token.kind == tokenKindWord &&
dot.X + token.width > size.X
if needWrap {
newline(true)
}
updateIndices()
tokens[index].position = dot
dot.X += token.width
}
}
lineEnd ++ // make lineEnd equal to len(tokens)
newline(false)
minimumSize.Y = metrics.Height * fixed.Int26_6(nLines) + metrics.Descent
// second, vertical alignment pass
alignLinesVertically(tokens, size.Y, minimumSize.Y, yAlign)
// calculate extents
extentsVerticalOffset := fixed.Point26_6 { Y: metrics.Ascent - tokens[0].position.Y }
extents.Max.Y = dot.Y + metrics.Descent
extentsSpace.Max.Y = dot.Y + metrics.Descent
minimumSize.X = fixedRectDx(extents)
minimumSize.Y = fixedRectDy(extents)
extents = extents.Sub(extentsVerticalOffset)
extentsSpace = extentsSpace.Sub(extentsVerticalOffset)
return
}
func fixedRectDx (rect fixed.Rectangle26_6) fixed.Int26_6 {
return rect.Max.X - rect.Min.X
}
func fixedRectDy (rect fixed.Rectangle26_6) fixed.Int26_6 {
return rect.Max.Y - rect.Min.Y
}
func calculateLineExtents (
firstWord, firstToken, lastWord, lastToken token,
) (
lineMin, lineMinSpace, lineMax, lineMaxSpace fixed.Int26_6,
) {
return
}
func alignLinesVertically (tokens []token, height, contentHeight fixed.Int26_6, align Align) {
if len(tokens) == 0 { return }
if align == AlignStart { return }
var topOffset fixed.Int26_6
switch align {
case AlignMiddle: topOffset = (height - contentHeight) / 2
case AlignEnd, AlignEven: topOffset = height - contentHeight
}
for index := range tokens {
tokens[index].position.Y += topOffset
}
}
func alignLine (tokens []token, width fixed.Int26_6, align Align, atEnd bool) {
if len(tokens) == 0 { return }
if align == AlignStart { return }
if align == AlignEven {
alignLineJustify(tokens, width, atEnd)
return
}
var leftOffset fixed.Int26_6
contentWidth := lineContentWidth(tokens)
switch align {
case AlignMiddle: leftOffset = (width - contentWidth) / 2
case AlignEnd: leftOffset = width - contentWidth
}
for index := range tokens {
tokens[index].position.X += leftOffset
}
}
func alignLineJustify (tokens []token, width fixed.Int26_6, atEnd bool) {
cantJustify :=
len(tokens) < 2 ||
atEnd ||
tokens[len(tokens) - 1].kind == tokenKindLineBreak
if cantJustify {
alignLine(tokens, width, AlignStart, atEnd)
return
}
contentWidth, wordCount := lineContentWordWidth(tokens)
spaceCount := wordCount - 1
if spaceCount == 0 { return }
spacePerWord := (width - contentWidth) / fixed.Int26_6(spaceCount)
var x fixed.Int26_6
for index, token := range tokens {
if token.kind == tokenKindWord {
tokens[index].position.X = x
x += spacePerWord + token.width
} else {
tokens[index].position.X = x
}
}
}
func lineContentWordWidth (tokens []token) (fixed.Int26_6, int) {
var width fixed.Int26_6
var count int
for _, token := range tokens {
if token.kind == tokenKindWord {
width += token.width
count ++
}
}
return width, count
}
func lineContentWidth (tokens []token) fixed.Int26_6 {
var width, spaceWidth fixed.Int26_6
for _, token := range tokens {
if token.kind == tokenKindWord {
width += spaceWidth + token.width
spaceWidth = 0
} else {
spaceWidth = token.width
}
}
return width
}

39
flow_test.go Normal file
View File

@ -0,0 +1,39 @@
package typeset
import "testing"
import "golang.org/x/image/math/fixed"
import "golang.org/x/image/font/basicfont"
func BenchmarkFlowLipsumLeftTop (benchmark *testing.B) {
benchmarkFlow(benchmark, false, AlignStart, AlignStart)
}
func BenchmarkFlowLipsumWrapLeftTop (benchmark *testing.B) {
benchmarkFlow(benchmark, true, AlignStart, AlignStart)
}
func BenchmarkFlowLipsumCenterTop (benchmark *testing.B) {
benchmarkFlow(benchmark, false, AlignMiddle, AlignStart)
}
func BenchmarkFlowLipsumWrapCenterTop (benchmark *testing.B) {
benchmarkFlow(benchmark, true, AlignMiddle, AlignStart)
}
func BenchmarkFlowLipsumWrapJustifyTop (benchmark *testing.B) {
benchmarkFlow(benchmark, true, AlignEven, AlignStart)
}
func benchmarkFlow (benchmark *testing.B, wrap bool, xAlign, yAlign Align) {
_, tokens := parseString(lipsumLt)
measure(tokens, basicfont.Face7x13)
benchmark.ReportAllocs()
benchmark.ResetTimer()
for i := 0; i < benchmark.N; i ++ {
reflow (
tokens,
basicfont.Face7x13,
fixed.P(256, 256),
wrap, xAlign, yAlign)
}
}

27
internal/draw.go Normal file
View File

@ -0,0 +1,27 @@
package internal
import "image"
import "image/draw"
import "image/color"
import "golang.org/x/image/math/fixed"
func DrawRectangleOutline (destination draw.Image, bounds image.Rectangle, col color.Color) {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
destination.Set(x, bounds.Min.Y, col)
}
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
destination.Set(bounds.Min.X, y, col)
destination.Set(bounds.Max.X - 1, y, col)
}
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
destination.Set(x, bounds.Max.Y - 1, col)
}
}
func RoundRect (rectangle fixed.Rectangle26_6) image.Rectangle {
return image.Rect (
rectangle.Min.X.Round(),
rectangle.Min.Y.Round(),
rectangle.Max.X.Round(),
rectangle.Max.Y.Round())
}

245
layout.go
View File

@ -1,245 +0,0 @@
package typeset
import "unicode"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
// Align specifies a text alignment method.
type Align int
const (
// X | Y
AlignStart Align = iota // left | top
AlignMiddle // center | center
AlignEnd // right | bottom
AlignEven // justified | evenly spaced
)
// RuneLayout contains layout information for a single rune relative to its
// word.
type RuneLayout struct {
X fixed.Int26_6
Width fixed.Int26_6
Rune rune
}
// WordLayout contains layout information for a single word relative to its
// line.
type WordLayout struct {
X fixed.Int26_6
Width fixed.Int26_6
SpaceAfter fixed.Int26_6
Runes []RuneLayout
}
// DoWord consumes exactly one word from the given string, and produces a word
// layout according to the given font. It returns the remaining text as well.
func DoWord (text []rune, face font.Face) (word WordLayout, remaining []rune) {
remaining = text
gettingSpace := false
x := fixed.Int26_6(0)
lastRune := rune(-1)
for _, char := range text {
// if we run into a line break, we must break out immediately
// because it is not DoWord's job to handle that.
if char == '\n' { break }
// if we suddenly run into spaces, and then run into a word
// again, we must break out immediately.
if unicode.IsSpace(char) {
gettingSpace = true
} else if gettingSpace {
break
}
// apply kerning
if lastRune >= 0 { x += face.Kern(lastRune, char) }
lastRune = char
// consume and process the rune
remaining = remaining[1:]
advance, ok := face.GlyphAdvance(char)
if !ok {
advance = tofuAdvance(face)
}
runeLayout := RuneLayout {
X: x,
Width: advance,
Rune: char,
}
word.Runes = append(word.Runes, runeLayout)
// advance
if gettingSpace {
word.SpaceAfter += advance
} else {
word.Width += advance
}
x += advance
}
return
}
// LastRune returns the last rune in the word.
func (word WordLayout) LastRune () rune {
if word.Runes == nil {
return -1
} else {
return word.Runes[len(word.Runes) - 1].Rune
}
}
// FirstRune returns the last rune in the word.
func (word WordLayout) FirstRune () rune {
if word.Runes == nil {
return -1
} else {
return word.Runes[0].Rune
}
}
// LineLayout contains layout information for a single line.
type LineLayout struct {
Y fixed.Int26_6
Width fixed.Int26_6
ContentWidth fixed.Int26_6
SpaceAfter fixed.Int26_6
Words []WordLayout
BreakAfter bool
}
// DoLine consumes exactly one line from the given string, and produces a line
// layout according to the given font. It returns the remaining text as well. If
// wrap is set to true, this function will stop processing words once maxWidth
// is crossed. The word which would have crossed over the limit will not be
// processed.
func DoLine (text []rune, face font.Face, wrap bool, width fixed.Int26_6) (line LineLayout, remaining []rune) {
remaining = text
x := fixed.Int26_6(0)
isFirstWord := true
for {
// process one word
word, remainingFromWord := DoWord(remaining, face)
x += word.Width
// if we have gone over the preferred width, stop processing
// words (if wrap is enabled)
if !isFirstWord && wrap && x > width {
break
}
x += word.SpaceAfter
remaining = remainingFromWord
// if the word actually has contents, add it
if word.Runes != nil {
line.Words = append(line.Words, word)
}
// if we have hit the end of the line, stop processing words
if len(remaining) == 0 { break }
if remaining[0] == '\n' {
line.BreakAfter = true
remaining = remaining[1:]
break
}
isFirstWord = false
}
// set the width of the line's content.
line.Width = width
if len(line.Words) > 0 {
lastWord := line.Words[len(line.Words) - 1]
line.ContentWidth = x - lastWord.SpaceAfter
line.SpaceAfter = lastWord.SpaceAfter
}
return
}
// Length returns the amount of runes within the line, including the trailing
// line break if it exists.
func (line *LineLayout) Length () int {
lineSize := 0
for _, word := range line.Words {
lineSize += len(word.Runes)
}
if line.BreakAfter { lineSize ++ }
return lineSize
}
// Align aligns the text in the line according to the specified alignment
// method.
func (line *LineLayout) Align (align Align, tabWidth fixed.Int26_6) {
if len(line.Words) == 0 { return }
if align == AlignEven {
line.justify(tabWidth)
} else {
line.contract(tabWidth)
var leftOffset fixed.Int26_6
if align == AlignMiddle {
leftOffset = (line.Width - line.ContentWidth) / 2
} else if align == AlignEnd {
leftOffset = line.Width - line.ContentWidth
}
for index := range line.Words {
line.Words[index].X += leftOffset
}
}
}
// assume line has content > 0
func (line *LineLayout) contract (tabWidth fixed.Int26_6) {
x := fixed.Int26_6(0)
for index, word := range line.Words {
word.X = x
x += word.Width
x += word.SpaceAfter
line.Words[index] = word
}
lastWord := line.Words[len(line.Words) - 1]
line.ContentWidth = lastWord.X + lastWord.Width
line.SpaceAfter = lastWord.SpaceAfter
}
// assume line has content > 0
func (line *LineLayout) justify (tabWidth fixed.Int26_6) {
if len(line.Words) <= 1 {
line.Align(AlignStart, tabWidth)
return
}
// We are going to be moving the words, so we can't take SpaceAfter into
// account.
trueContentWidth := fixed.Int26_6(0)
for _, word := range line.Words {
trueContentWidth += word.Width
}
spaceCount := len(line.Words) - 1
spacePerWord := (line.Width - trueContentWidth) / fixed.Int26_6(spaceCount)
x := fixed.Int26_6(0)
for index, word := range line.Words {
line.Words[index].X = x
x += spacePerWord + word.Width
}
}
func tofuAdvance (face font.Face) fixed.Int26_6 {
if advance, ok := face.GlyphAdvance('M'); ok {
return advance
} else {
return 16
}
}
func tofuBounds (face font.Face) (fixed.Rectangle26_6, fixed.Int26_6) {
if bounds, advance, ok := face.GlyphBounds('M'); ok {
return bounds, advance
} else {
return fixed.R(0, -16, 14, 0), 16
}
}

45
measure.go Normal file
View File

@ -0,0 +1,45 @@
package typeset
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
func measure (tokens []token, face font.Face) {
var lastRune rune
for index, token := range tokens {
var x fixed.Int26_6
for index, runl := range token.runes {
advance, ok := face.GlyphAdvance(runl.run)
if !ok { advance = tofuAdvance(face) }
advance += face.Kern(lastRune, runl.run)
runl.x = x
x += advance
lastRune = runl.run
token.runes[index] = runl
}
token.width = x
tokens[index] = token
}
}
const tofuStandinRune = 'M'
const fallbackTofuAdvance = 16
const fallbackTofuWidth = 14
const fallbackTofuAscend = 16
func tofuAdvance (face font.Face) fixed.Int26_6 {
if advance, ok := face.GlyphAdvance(tofuStandinRune); ok {
return advance
} else {
return fallbackTofuAdvance
}
}
func tofuBounds (face font.Face) (fixed.Rectangle26_6, fixed.Int26_6) {
if bounds, advance, ok := face.GlyphBounds(tofuStandinRune); ok {
return bounds, advance
} else {
return fixed.R(0, -fallbackTofuAscend, fallbackTofuWidth, 0),
fallbackTofuAdvance
}
}

70
measure_test.go Normal file
View File

@ -0,0 +1,70 @@
package typeset
import "testing"
import "golang.org/x/image/math/fixed"
import "golang.org/x/image/font/basicfont"
const basicfontFace7x13advance = 7
func tkw (kind tokenKind, value string, width fixed.Int26_6) token {
tok := tk(kind, value)
tok.width = width
for index, runl := range tok.runes {
runl.x = fixed.I(basicfontFace7x13advance * index)
tok.runes[index] = runl
}
return tok
}
func TestMeasure (test *testing.T) {
// ---- processing ----
tokens := []token {
tk(tokenKindWord, "hello"),
tk(tokenKindSpace, " "),
tk(tokenKindWord, "\rworld!"),
tk(tokenKindLineBreak, "\n"),
tk(tokenKindWord, "foo"),
tk(tokenKindLineBreak, "\n"),
tk(tokenKindLineBreak, "\r\n"),
tk(tokenKindWord, "bar"),
tk(tokenKindTab, "\t"),
tk(tokenKindWord, "baz"),
tk(tokenKindTab, "\t\t"),
tk(tokenKindWord, "something"),
}
measure(tokens, basicfont.Face7x13)
// ---- correct data ----
correctTokens := []token {
tkw(tokenKindWord, "hello", fixed.I(35)),
tkw(tokenKindSpace, " ", fixed.I( 7)),
tkw(tokenKindWord, "\rworld!", fixed.I(49)),
tkw(tokenKindLineBreak, "\n", fixed.I( 7)),
tkw(tokenKindWord, "foo", fixed.I(21)),
tkw(tokenKindLineBreak, "\n", fixed.I( 7)),
tkw(tokenKindLineBreak, "\r\n", fixed.I(14)),
tkw(tokenKindWord, "bar", fixed.I(21)),
tkw(tokenKindTab, "\t", fixed.I( 7)),
tkw(tokenKindWord, "baz", fixed.I(21)),
tkw(tokenKindTab, "\t\t", fixed.I(14)),
tkw(tokenKindWord, "something", fixed.I(63)),
}
// ---- testing ----
if len(tokens) != len(correctTokens) {
test.Logf("len(tokens) != len(correctTokens): %d, %d", len(tokens), len(correctTokens))
test.Log("GOT")
logTokens(test, tokens)
test.Log("CORRECT")
logTokens(test, correctTokens)
test.FailNow()
}
if !compareTokens(tokens, correctTokens) {
test.Log("tokens != correctTokens:")
test.Log("GOT")
logTokens(test, tokens)
test.Log("CORRECT")
logTokens(test, correctTokens)
test.FailNow()
}
}

78
parse.go Normal file
View File

@ -0,0 +1,78 @@
package typeset
import "unicode"
// TODO perhaps follow https://unicode.org/reports/tr14/
func parseString (text string) ([]runeLayout, []token) {
// alloc initial rune slice
runes := make([]runeLayout, 0, len(text) * 2 / 3)
// build the rune slice
// we need to do this before parsing into tokens, because otherwise
// a realloc will occur in the middle of it and the tokens at the start
// will be referencing old memory
for _, run := range text {
runes = append(runes, runeLayout {
run: run,
})
}
// alloc initial token slice
tokens := make([]token, 0, len(runes) / 5)
var index int
var startingIndex int
var runl runeLayout
var lastRune rune
var tok token
tokenBoundary := func () {
if startingIndex != index {
tok.runes = runes[startingIndex:index]
startingIndex = index
tokens = append(tokens, tok)
}
tok = token { }
}
mustBeInToken := func (kind tokenKind) {
if tok.kind != kind {
tokenBoundary()
tok.kind = kind
}
}
// parse tokens
for index, runl = range runes {
switch {
case runl.run == '\r':
tokenBoundary()
// we don't know the token type yet. if next rune is a
// \n then this is a CRLF line break. if not, this is
// just a word.
case runl.run == '\n':
if lastRune == '\r' {
// continue the \r to make a CRLF line break
tok.kind = tokenKindLineBreak
} else {
tokenBoundary()
tok.kind = tokenKindLineBreak
}
case runl.run == '\t':
mustBeInToken(tokenKindTab)
case unicode.IsSpace(runl.run):
mustBeInToken(tokenKindSpace)
default:
mustBeInToken(tokenKindWord)
}
lastRune = runl.run
}
index ++ // make index equal to len([]rune(text))
tokenBoundary()
return runes, tokens
}

224
parse_test.go Normal file
View File

@ -0,0 +1,224 @@
package typeset
import "slices"
import "testing"
func rl (run rune) runeLayout { return runeLayout { run: run } }
func tk (kind tokenKind, value string) token {
tok := token {
kind: kind,
}
runeValue := []rune(value)
tok.runes = make([]runeLayout, len(runeValue))
for index, run := range runeValue {
tok.runes[index] = rl(run)
}
return tok
}
func compareTokens (got, correct []token) bool {
for index, tok := range got {
correctTok := correct[index]
isCorrect :=
correctTok.kind == tok.kind &&
correctTok.width == tok.width &&
slices.Equal(correctTok.runes, tok.runes)
if !isCorrect { return false }
}
return true
}
func logTokens (test *testing.T, tokens []token) {
for _, token := range tokens {
test.Logf("- %-40v | %v", token, token.runes)
}
}
func TestParseString (test *testing.T) {
// ---- processing ----
runes, tokens := parseString("hello \rworld!\nfoo\n\r\nbar\tbaz\t\tsomething")
// ---- correct data ----
correctRunes := []runeLayout {
rl('h'),
rl('e'),
rl('l'),
rl('l'),
rl('o'),
rl(' '),
rl('\r'),
rl('w'),
rl('o'),
rl('r'),
rl('l'),
rl('d'),
rl('!'),
rl('\n'),
rl('f'),
rl('o'),
rl('o'),
rl('\n'),
rl('\r'),
rl('\n'),
rl('b'),
rl('a'),
rl('r'),
rl('\t'),
rl('b'),
rl('a'),
rl('z'),
rl('\t'),
rl('\t'),
rl('s'),
rl('o'),
rl('m'),
rl('e'),
rl('t'),
rl('h'),
rl('i'),
rl('n'),
rl('g'),
}
correctTokens := []token {
tk(tokenKindWord, "hello"),
tk(tokenKindSpace, " "),
tk(tokenKindWord, "\rworld!"),
tk(tokenKindLineBreak, "\n"),
tk(tokenKindWord, "foo"),
tk(tokenKindLineBreak, "\n"),
tk(tokenKindLineBreak, "\r\n"),
tk(tokenKindWord, "bar"),
tk(tokenKindTab, "\t"),
tk(tokenKindWord, "baz"),
tk(tokenKindTab, "\t\t"),
tk(tokenKindWord, "something"),
}
// ---- testing ----
if len(runes) != len(correctRunes) {
test.Logf("len(runes) != len(correctRunes): %d, %d", len(runes), len(correctRunes))
test.Log(runes)
test.Log(correctRunes)
test.FailNow()
}
if !slices.Equal(runes, correctRunes) {
test.Log("runes != correctRunes:")
test.Log(runes)
test.Log(correctRunes)
test.FailNow()
}
if len(tokens) != len(correctTokens) {
test.Logf("len(tokens) != len(correctTokens): %d, %d", len(tokens), len(correctTokens))
test.Log("GOT")
logTokens(test, tokens)
test.Log("CORRECT")
logTokens(test, correctTokens)
test.FailNow()
}
if !compareTokens(tokens, correctTokens) {
test.Log("tokens != correctTokens:")
test.Log("GOT")
logTokens(test, tokens)
test.Log("CORRECT")
logTokens(test, correctTokens)
test.FailNow()
}
test.Logf("changing first rune from %c to x", runes[0].run)
runes[0].run = 'x'
test.Logf("first rune is now %c", runes[0].run)
tokenRune := tokens[0].runes[0].run
if tokenRune != 'x' {
test.Fatalf (
"tokens does not reference the same memory as runes after changing runes: %c, %c",
runes[0].run, tokenRune)
}
runeIndex := 0
for tokenIndex, token := range tokens {
tokenRunePtr := &token.runes[0]
runePtr := &runes[runeIndex]
if runePtr != tokenRunePtr {
test.Fatalf (
"tokens[%d] does not reference runes[%d]: %p, %p",
tokenIndex, runeIndex, tokenRunePtr, runePtr)
}
runeIndex += len(token.runes)
}
}
func BenchmarkParseStringLatin (benchmark *testing.B) {
benchmark.ReportAllocs()
var rmeanLen, rmeanCap int
var tmeanLen, tmeanCap int
for i := 0; i < benchmark.N; i ++ {
runes, tokens := parseString(lipsumLt)
rmeanLen += len(runes)
rmeanCap += cap(runes)
tmeanLen += len(tokens)
tmeanCap += cap(tokens)
}
rmeanLen /= benchmark.N
rmeanCap /= benchmark.N
tmeanLen /= benchmark.N
tmeanCap /= benchmark.N
benchmark.ReportMetric(float64(rmeanCap) / float64(rmeanLen), "rune-waste")
benchmark.ReportMetric(float64(tmeanCap) / float64(tmeanLen), "token-waste")
}
func BenchmarkParseStringChinese (benchmark *testing.B) {
benchmark.ReportAllocs()
var rmeanLen, rmeanCap int
var tmeanLen, tmeanCap int
for i := 0; i < benchmark.N; i ++ {
runes, tokens := parseString(lipsumCn)
rmeanLen += len(runes)
rmeanCap += cap(runes)
tmeanLen += len(tokens)
tmeanCap += cap(tokens)
}
rmeanLen /= benchmark.N
rmeanCap /= benchmark.N
tmeanLen /= benchmark.N
tmeanCap /= benchmark.N
benchmark.ReportMetric(float64(rmeanCap) / float64(rmeanLen), "rune-waste")
benchmark.ReportMetric(float64(tmeanCap) / float64(tmeanLen), "token-waste")
}
const lipsumLt =
`Voluptatem impedit id id facilis et. Sit eligendi aspernatur dicta vitae ipsa officia enim harum. Occaecati quod harum quos temporibus officiis provident enim neque. Odio totam ducimus commodi quis minima ea.
Ut delectus quis a rem consectetur laudantium hic sequi. Vel sunt neque nisi excepturi id sit id ut. Dolores expedita et odio. Quibusdam sed et quam nostrum. Sed perspiciatis voluptatibus et.
Omnis qui tempore corrupti alias ut repellendus est. A officiis molestias perspiciatis ut dolores nihil. Ut officiis hic quo aut aut dolorum. Modi at molestiae praesentium ea eveniet aut porro.
Similique facere cum amet nesciunt dolorem nemo. Rerum temporibus iure maiores. Facere quam nihil quia debitis nihil est officia aliquam. Magnam aut alias consectetur. Velit cumque eligendi assumenda magni ratione. Est dolorem modi a unde.
Illo reprehenderit est sunt quaerat cum nihil non. Quia nihil placeat qui ex hic molestiae eligendi. Asperiores optio et nobis et.`
const lipsumCn =
`这很容易并且妨碍快乐让我们很难为这些人选择上述生活的职责他们被这样的事实蒙蔽了双眼他们现在不提供办公室我讨厌我们给小孩子们带来所有的好处
作为被选中的人他将跟随这里的赞美或者除非他们被排除在外否则他们不是意想不到的痛苦和仇恨但对某些人来说以及我们自己但让我们看看其中的乐趣和
当时所有腐败的人都必须被击退办公室的麻烦被视为无痛至于这里的服务无论是哪里还是让人心疼但目前的麻烦将会发生或持续下去
当没有人知道其中的痛苦时也要做同样的事情事物的时代确实更加伟大无所作为因为你不欠任何东西这是一些责任这将是伟大的或其他方面无论他选择什么他都必须非常小心有一种痛从何而来
他责怪他们在什么都没有的情况下才问因为没有人喜欢从这里选择麻烦对我们来说这是一个更艰难的选择
这很容易并且妨碍快乐让我们很难为这些人选择上述生活的职责他们被这样的事实蒙蔽了双眼他们现在不提供办公室我讨厌我们给小孩子们带来所有的好处
作为被选中的人他将跟随这里的赞美或者除非他们被排除在外否则他们不是意想不到的痛苦和仇恨但对某些人来说以及我们自己但让我们看看其中的乐趣和
当时所有腐败的人都必须被击退办公室的麻烦被视为无痛至于这里的服务无论是哪里还是让人心疼但目前的麻烦将会发生或持续下去
当没有人知道其中的痛苦时也要做同样的事情事物的时代确实更加伟大无所作为因为你不欠任何东西这是一些责任这将是伟大的或其他方面无论他选择什么他都必须非常小心有一种痛从何而来
他责怪他们在什么都没有的情况下才问因为没有人喜欢从这里选择麻烦对我们来说这是一个更艰难的选择
这很容易并且妨碍快乐让我们很难为这些人选择上述生活的职责他们被这样的事实蒙蔽了双眼他们现在不提供办公室我讨厌我们给小孩子们带来所有的好处
作为被选中的人他将跟随这里的赞美或者除非他们被排除在外否则他们不是意想不到的痛苦和仇恨但对某些人来说以及我们自己但让我们看看其中的乐趣和
当时所有腐败的人都必须被击退办公室的麻烦被视为无痛至于这里的服务无论是哪里还是让人心疼但目前的麻烦将会发生或持续下去
当没有人知道其中的痛苦时也要做同样的事情事物的`

37
recommend.go Normal file
View File

@ -0,0 +1,37 @@
package typeset
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
func recommendHeight (tokens []token, face font.Face, width fixed.Int26_6) fixed.Int26_6 {
metrics := face.Metrics()
var dot fixed.Point26_6
newline := func () {
dot.Y += metrics.Height
dot.X = 0
}
sawLineBreak := false
for _, token := range tokens {
// demarcate lines
if sawLineBreak {
newline()
sawLineBreak = false
}
if token.kind == tokenKindLineBreak {
sawLineBreak = true
} else {
needWrap :=
token.kind == tokenKindWord &&
dot.X + token.width > width
if needWrap {
newline()
}
dot.X += token.width
}
}
newline()
return dot.Y
}

428
setter.go
View File

@ -1,428 +0,0 @@
package typeset
import "image"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
// TypeSetter manages several lines of text, and can perform layout operations
// on them. It automatically avoids performing redundant work. It has no
// constructor and its zero value can be used safely.
type TypeSetter struct {
lines []LineLayout
text []rune
layoutClean bool
alignClean bool
hAlign, vAlign Align
face font.Face
width, height int
wrap bool
tabWidth fixed.Int26_6
minWidth fixed.Int26_6
layoutBounds image.Rectangle
layoutBoundsSpace image.Rectangle
}
func (setter *TypeSetter) needLayout () {
if setter.layoutClean { return }
setter.layoutClean = true
setter.alignClean = false
setter.lines = nil
setter.layoutBounds = image.Rectangle { }
setter.layoutBoundsSpace = image.Rectangle { }
setter.minWidth = 0
if setter.face == nil { return }
horizontalExtent := fixed.Int26_6(0)
horizontalExtentSpace := fixed.Int26_6(0)
metrics := setter.face.Metrics()
remaining := setter.text
y := fixed.Int26_6(0)
// function to add line and update bounds statistics
addLine := func (line LineLayout) {
line.Y = y
y += metrics.Height
if line.ContentWidth > horizontalExtent {
horizontalExtent = line.ContentWidth
}
lineWidthSpace := line.ContentWidth + line.SpaceAfter
if lineWidthSpace > horizontalExtentSpace {
horizontalExtentSpace = lineWidthSpace
}
setter.lines = append(setter.lines, line)
}
// process every line until there are no more remaining runes
for len(remaining) > 0 {
line, remainingFromLine := DoLine (
remaining, setter.face, setter.wrap,
fixed.I(setter.width))
remaining = remainingFromLine
addLine(line)
}
// if there were no lines processed or the last line has a break after
// it, add a blank line at the end
needBlankLine :=
len(setter.lines) == 0 ||
setter.lines[len(setter.lines) - 1].BreakAfter
if needBlankLine { addLine(LineLayout { }) }
// if we are wrapping text, the width must be the user-set width
if setter.wrap {
horizontalExtent = fixed.I(setter.width)
horizontalExtentSpace = fixed.I(setter.width)
}
// calculate layout boundaries
setter.minWidth = horizontalExtentSpace
setter.layoutBounds.Max.X = horizontalExtent.Round()
setter.layoutBoundsSpace.Max.X = horizontalExtentSpace.Round()
y -= metrics.Height
setter.layoutBounds.Min.Y = -metrics.Ascent.Round()
setter.layoutBounds.Max.Y =
y.Round() +
metrics.Descent.Round()
setter.layoutBoundsSpace.Min.Y = setter.layoutBounds.Min.Y
setter.layoutBoundsSpace.Max.Y = setter.layoutBounds.Max.Y
}
func (setter *TypeSetter) needAlignedLayout () {
if setter.alignClean && setter.layoutClean { return }
setter.needLayout()
setter.alignClean = true
setter.alignHorizontally()
setter.alignVertically()
}
// should only be called from within setter.needAlignedLayout
func (setter *TypeSetter) alignHorizontally () {
if len(setter.lines) == 0 { return }
for index := range setter.lines {
align := setter.hAlign
// if the horizontal align is even, align lines with breaks
// after them to the left anyways
if align == AlignEven {
except :=
index == len(setter.lines) - 1 ||
setter.lines[index].BreakAfter
if except { align = AlignStart }
}
// align line
setter.lines[index].Align(align, setter.tabWidth)
}
}
// should only be called from within setter.needAlignedLayout
func (setter *TypeSetter) alignVertically () {
if setter.height == 0 { return }
if len(setter.lines) == 0 { return }
if setter.vAlign == AlignEven {
setter.justifyVertically()
return
}
// determine how much to shift lines
topOffset := fixed.I(0)
contentHeight := setter.layoutBoundsSpace.Dy()
if setter.vAlign == AlignMiddle {
topOffset += fixed.I((setter.height - contentHeight) / 2)
} else if setter.vAlign == AlignEnd {
topOffset += fixed.I(setter.height - contentHeight)
}
// we may be re-aligning already aligned text. if the text is shifted
// away from the origin, account for that.
if len(setter.lines) > 0 {
topOffset -= setter.lines[0].Y
}
// shift lines
for index := range setter.lines {
setter.lines[index].Y += topOffset
}
}
// should only be called from within setter.alignVertically
func (setter *TypeSetter) justifyVertically () {
spaceCount := len(setter.lines) - 1
contentHeight := setter.layoutBoundsSpace.Dy()
spacePerLine :=
fixed.Int26_6(setter.height - contentHeight) /
fixed.Int26_6(spaceCount)
y := fixed.Int26_6(0)
for index := range setter.lines {
setter.lines[index].Y = y
y += spacePerLine + setter.LineHeight()
}
}
// SetWrap sets whether or not the text wraps around and forms new lines.
func (setter *TypeSetter) SetWrap (wrap bool) {
if setter.wrap == wrap { return }
setter.layoutClean = false
setter.wrap = wrap
}
// SetAlign sets the alignment method of the typesetter.
func (setter *TypeSetter) SetAlign (horizontal, vertical Align) {
if setter.hAlign == horizontal && setter.vAlign == vertical { return }
setter.alignClean = false
setter.hAlign = horizontal
setter.vAlign = vertical
}
// SetText sets the text content of the typesetter.
func (setter *TypeSetter) SetText (text []rune) {
setter.layoutClean = false
setter.alignClean = false
setter.text = text
}
// SetFace sets the font face of the typesetter.
func (setter *TypeSetter) SetFace (face font.Face) {
if setter.face == face { return }
setter.layoutClean = false
setter.alignClean = false
setter.face = face
}
// SetWidth sets the width of the typesetter. Text will still be able
// to overflow outside of this width if wrapping is disabled.
func (setter *TypeSetter) SetWidth (width int) {
if setter.width == width { return }
setter.layoutClean = false
setter.alignClean = false
setter.width = width
}
// SetHeight sets the height of the typesetter. If the height is greater than
// zero, no lines will be laid out past it. If the height is zero, the text's
// maximum height will not be constrained.
func (setter *TypeSetter) SetHeight (heignt int) {
if setter.height == heignt { return }
setter.layoutClean = false
setter.alignClean = false
setter.height = heignt
}
// SetTabWidth sets the distance between tab stops.
func (setter *TypeSetter) SetTabWidth (tabWidth fixed.Int26_6) {
if setter.tabWidth == tabWidth { return }
setter.layoutClean = false
setter.alignClean = false
setter.tabWidth = tabWidth
}
// Em returns the width of one emspace according to the typesetter's font, which
// is the width of the capital letter 'M'.
func (setter *TypeSetter) Em () (width fixed.Int26_6) {
if setter.face == nil { return 0 }
width, _ = setter.face.GlyphAdvance('M')
return
}
// LineHeight returns the height of one line according to the typesetter's font.
func (setter *TypeSetter) LineHeight () fixed.Int26_6 {
if setter.face == nil { return 0 }
return setter.face.Metrics().Height
}
// Width returns the height of the typesetter as set by SetWidth.
func (setter *TypeSetter) Width () int {
return setter.width
}
// Height returns the height of the typesetter as set by SetHeight.
func (setter *TypeSetter) Height () int {
return setter.height
}
// Face returns the TypeSetter's font face as set by SetFace.
func (setter *TypeSetter) Face () font.Face {
return setter.face
}
// Length returns the amount of runes in the typesetter.
func (setter *TypeSetter) Length () int {
return len(setter.text)
}
// RuneIterator is a function that can iterate accross a typesetter's runes.
type RuneIterator func (
index int,
char rune,
position fixed.Point26_6,
) (
keepGoing bool,
)
// For calls the specified iterator for every rune in the typesetter. If the
// iterator returns false, the loop will immediately stop. This method will
// insert a fake null rune at the end.
func (setter *TypeSetter) For (iterator RuneIterator) {
setter.forInternal(iterator, true)
}
// ForRunes is like For, but leaves out the fake null rune.
func (setter *TypeSetter) ForRunes (iterator RuneIterator) {
setter.forInternal(iterator, false)
}
func (setter *TypeSetter) forInternal (iterator RuneIterator, fakeNull bool) {
setter.needAlignedLayout()
index := 0
lastLineY := fixed.Int26_6(0)
lastCharRightBound := fixed.Int26_6(0)
for _, line := range setter.lines {
lastLineY = line.Y
for _, word := range line.Words {
for _, char := range word.Runes {
lastCharRightBound = word.X + char.X + char.Width
keepGoing := iterator(index, char.Rune, fixed.Point26_6 {
X: word.X + char.X,
Y: line.Y,
})
if !keepGoing { return }
index ++
}}
if line.BreakAfter {
keepGoing := iterator(index, '\n', fixed.Point26_6 {
X: lastCharRightBound,
Y: line.Y,
})
if !keepGoing { return }
index ++
lastCharRightBound = fixed.Int26_6(0)
}
}
if fakeNull {
keepGoing := iterator (index, '\000', fixed.Point26_6 {
X: lastCharRightBound,
Y: lastLineY,
})
if !keepGoing { return }
index ++
}
}
// AtPosition returns the index of the rune at the specified position.
func (setter *TypeSetter) AtPosition (position fixed.Point26_6) (index int) {
setter.needAlignedLayout()
if setter.lines == nil { return }
if setter.face == nil { return }
// find the first line who's bottom bound is greater than position.Y. if
// we haven't found it, then dont set the line variable (defaults to the
// last line)
metrics := setter.face.Metrics()
lastLine := setter.lines[len(setter.lines) - 1]
for _, curLine := range setter.lines {
if curLine.Y + metrics.Descent > position.Y {
lastLine = curLine
break
}
index += curLine.Length()
}
if lastLine.Words == nil { return }
// find the first rune who's right bound is greater than position.X.
for _, curWord := range lastLine.Words {
for _, curChar := range curWord.Runes {
x := curWord.X + curChar.X + curChar.Width
if x > position.X { goto foundRune }
index ++
}
}
foundRune:
return
}
// PositionAt returns the position of the rune at the specified index.
func (setter *TypeSetter) PositionAt (index int) (position fixed.Point26_6) {
setter.needAlignedLayout()
setter.For (func (i int, r rune, p fixed.Point26_6) bool {
position = p
return i < index
})
return
}
// LayoutBounds returns the semantic bounding box of the text. The origin point
// (0, 0) of the rectangle corresponds to the origin of the first line's
// baseline.
func (setter *TypeSetter) LayoutBounds () (image.Rectangle) {
setter.needLayout()
return setter.layoutBounds
}
// LayoutBoundsSpace is like LayoutBounds, but it also takes into account the
// trailing whitespace at the end of each line (if it exists).
func (setter *TypeSetter) LayoutBoundsSpace () (image.Rectangle) {
setter.needLayout()
return setter.layoutBoundsSpace
}
// MinimumSize returns the minimum width and height needed to display text. If
// wrapping is enabled, this method will return (Em(), 0)
func (setter *TypeSetter) MinimumSize () image.Point {
setter.needLayout()
if setter.wrap {
return image.Pt(setter.Em().Round(), 0)
}
width := setter.minWidth
height := fixed.Int26_6(len(setter.lines)) * setter.LineHeight()
return image.Pt(width.Round(), height.Round())
}
// RecommendedHeight returns the reccomended max height if the text were to have
// its maximum width set to the given width. This does not alter the
// typesetter's state.
func (setter *TypeSetter) RecommendedHeight (width int) (height int) {
setter.needLayout()
if setter.lines == nil { return }
if setter.face == nil { return }
metrics := setter.face.Metrics()
dot := fixed.Point26_6 { X: 0, Y: metrics.Height }
firstWord := true
for _, line := range setter.lines {
for _, word := range line.Words {
if word.Width + dot.X > fixed.I(width) && !firstWord {
dot.Y += metrics.Height
dot.X = 0
firstWord = true
}
dot.X += word.Width + word.SpaceAfter
firstWord = false
}
if line.BreakAfter {
dot.Y += metrics.Height
dot.X = 0
firstWord = true
}
}
return dot.Y.Round()
}

286
typesetter.go Normal file
View File

@ -0,0 +1,286 @@
package typeset
import "fmt"
import "strconv"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
type validationLevel int; const (
validationLevelNone validationLevel = iota
validationLevelTokens
validationLevelMeasurement
validationLevelFlow
)
type tokenKind int; const (
tokenKindWord tokenKind = iota // contains everything that isn't:
tokenKindSpace // only unicode space runes, except \r or \n
tokenKindTab // only \t runes
tokenKindLineBreak // either "\n", or "\r\n"
)
func (kind tokenKind) String () string {
switch kind {
case tokenKindWord: return "Word"
case tokenKindSpace: return "Space"
case tokenKindTab: return "Tab"
case tokenKindLineBreak: return "LineBreak"
}
return fmt.Sprintf("typeset.tokenKind(%d)", kind)
}
type token struct {
kind tokenKind
width fixed.Int26_6
position fixed.Point26_6
runes []runeLayout
}
func (tok token) String () string {
str := ""
for _, runl := range tok.runes {
str += string(runl.run)
}
return fmt.Sprintf (
"%v:%v{%v,%v-%v}",
tok.kind, strconv.Quote(str),
tok.position.X, tok.position.Y, tok.width)
}
// TODO: perhaps rename this to just "glyph"
type runeLayout struct {
x fixed.Int26_6
run rune
}
func (run runeLayout) String () string {
return fmt.Sprintf("%s-{%v}", strconv.Quote(string([]rune { run.run })), run.x)
}
// RuneIter is an iterator that iterates over positioned runes.
type RuneIter func (yield func(fixed.Point26_6, rune) bool)
// Align specifies a text alignment method.
type Align int; const (
// X | Y
AlignStart Align = iota // left | top
AlignMiddle // center | center
AlignEnd // right | bottom
AlignEven // justified | (unsupported)
)
// TypeSetter manages text, and can perform layout operations on it. It
// automatically avoids performing redundant work. It has no constructor and its
// zero value can be used safely, but it must not be copied after first use.
type TypeSetter struct {
text string
runes []runeLayout
tokens []token
validationLevel validationLevel
xAlign, yAlign Align
face font.Face
size fixed.Point26_6 // width, height
wrap bool
minimumSize fixed.Point26_6
layoutBounds fixed.Rectangle26_6
layoutBoundsSpace fixed.Rectangle26_6
}
// AtPosition returns the index of the rune at the specified position. The
// returned index may be greater than the length of runes in the TypeSetter.
func (this *TypeSetter) AtPosition (position fixed.Point26_6) int {
metrics := this.face.Metrics()
lastValidIndex := 0
index := 0
for _, tok := range this.tokens {
pos := tok.position
yValid :=
position.Y >= pos.Y - metrics.Ascent &&
position.Y <= pos.Y + metrics.Descent
if !yValid { index += len(tok.runes); continue }
for _, runl := range tok.runes {
x := pos.X + runl.x
xValid := position.X >= pos.X + runl.x
if xValid {
lastValidIndex = index
} else if x > position.X {
return lastValidIndex
}
index ++
}
}
index ++
return lastValidIndex
}
// Runes returns an iterator for all runes in the TypeSetter, and their positions.
func (this *TypeSetter) Runes () RuneIter {
this.needFlow()
return func (yield func (fixed.Point26_6, rune) bool) {
for _, tok := range this.tokens {
for _, runl := range tok.runes {
pos := tok.position
pos.X += runl.x
if !yield(pos, runl.run) { return }
}
}
}
}
// RunesWithNull returns an iterator for all runes in the TypeSetter, plus an
// additional null rune at the end. This is useful for calculating the positions
// of things.
func (this *TypeSetter) RunesWithNull () RuneIter {
this.needFlow()
return func (yield func (fixed.Point26_6, rune) bool) {
var tok token
for _, tok = range this.tokens {
for _, runl := range tok.runes {
pos := tok.position
pos.X += runl.x
if !yield(pos, runl.run) { return }
}
}
pos := tok.position
pos.X += tok.width
yield(pos, 0)
}
}
// Em returns the width of one emspace according to the typesetter's typeface,
// which is the width of the capital letter 'M'.
func (this *TypeSetter) Em () fixed.Int26_6 {
if this.face == nil { return 0 }
width, _ := this.face.GlyphAdvance('M')
return width
}
// Face returns the font face as set by SetFace.
func (this *TypeSetter) Face () font.Face {
return this.face
}
// LayoutBounds returns the semantic bounding box of the text. The origin point
// (0, 0) of the rectangle corresponds to the origin of the first line's
// baseline.
func (this *TypeSetter) LayoutBounds () fixed.Rectangle26_6 {
this.needFlow()
return this.layoutBounds
}
// LayoutBoundsSpace is like LayoutBounds, but it also takes into account the
// trailing whitespace at the end of each line (if it exists).
func (this *TypeSetter) LayoutBoundsSpace () fixed.Rectangle26_6 {
this.needFlow()
return this.layoutBoundsSpace
}
// MinimumSize returns the minimum width and height needed to display text. If
// wrapping is enabled, this method will return { X: Em(), Y: 0 }.
func (this *TypeSetter) MinimumSize () fixed.Point26_6 {
if this.wrap { return fixed.Point26_6{ X: this.Em(), Y: 0 } }
this.needFlow()
return this.minimumSize
}
// PositionAt returns the position of the rune at the specified index.
func (this *TypeSetter) PositionAt (index int) fixed.Point26_6 {
idx := 0
var position fixed.Point26_6
this.RunesWithNull()(func (pos fixed.Point26_6, run rune) bool {
if index == idx {
position = pos
return false
}
idx ++
return true
})
return position
}
// ReccomendedHeightFor returns the reccomended max height if the text were to
// have its maximum width set to the given width. This does not actually move
// any text, it only simulates it.
func (this *TypeSetter) RecommendedHeight (width fixed.Int26_6) fixed.Int26_6 {
this.needMeasurement()
return recommendHeight(this.tokens, this.face, width)
}
// SetAlign sets the horizontal and vertical alignment of the text.
func (this *TypeSetter) SetAlign (x, y Align) {
if this.xAlign == x && this.yAlign == y { return }
this.xAlign = x
this.yAlign = y
this.invalidate(validationLevelFlow)
}
// SetFace sets the font face the text will be laid out according to.
func (this *TypeSetter) SetFace (face font.Face) {
if this.face == face { return }
this.face = face
this.invalidate(validationLevelMeasurement)
}
// SetSize sets the width and height of the TypeSetter.
func (this *TypeSetter) SetSize (size fixed.Point26_6) {
if this.size == size { return }
this.size = size
this.invalidate(validationLevelFlow)
}
// SetText sets the text of the TypeSetter.
func (this *TypeSetter) SetText (text string) {
if this.text == text { return }
this.text = text
this.invalidate(validationLevelTokens)
}
// SetWrap sets whether the text will wrap to the width specified by SetSize.
func (this *TypeSetter) SetWrap (wrap bool) {
if this.wrap == wrap { return }
this.wrap = wrap
this.invalidate(validationLevelFlow)
}
func (this *TypeSetter) needTokens () {
if this.valid(validationLevelTokens) { return }
this.runes, this.tokens = parseString(this.text)
this.validate(validationLevelTokens)
}
func (this *TypeSetter) needMeasurement () {
if this.valid(validationLevelMeasurement) { return }
this.needTokens()
measure(this.tokens, this.face)
this.validate(validationLevelMeasurement)
}
func (this *TypeSetter) needFlow () {
if this.valid(validationLevelFlow) { return }
this.needMeasurement()
this.layoutBounds, this.layoutBoundsSpace, this.minimumSize = reflow (
this.tokens,
this.face, this.size,
this.wrap, this.xAlign, this.yAlign)
this.validate(validationLevelFlow)
}
func (this *TypeSetter) validate (level validationLevel) {
this.validationLevel = level
}
func (this *TypeSetter) invalidate (level validationLevel) {
if this.valid(level) {
this.validationLevel = level - 1
}
}
func (this *TypeSetter) valid (level validationLevel) bool {
return this.validationLevel >= level
}