github.com/Seikaijyu/gio@v0.0.1/text/gotext.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package text 4 5 import ( 6 "bytes" 7 "image" 8 "io" 9 "log" 10 "os" 11 12 "github.com/go-text/typesetting/di" 13 "github.com/go-text/typesetting/font" 14 "github.com/go-text/typesetting/fontscan" 15 "github.com/go-text/typesetting/language" 16 "github.com/go-text/typesetting/opentype/api" 17 "github.com/go-text/typesetting/opentype/api/metadata" 18 "github.com/go-text/typesetting/shaping" 19 "golang.org/x/exp/slices" 20 "golang.org/x/image/math/fixed" 21 "golang.org/x/text/unicode/bidi" 22 23 "github.com/Seikaijyu/gio/f32" 24 giofont "github.com/Seikaijyu/gio/font" 25 "github.com/Seikaijyu/gio/font/opentype" 26 "github.com/Seikaijyu/gio/internal/debug" 27 "github.com/Seikaijyu/gio/io/system" 28 "github.com/Seikaijyu/gio/op" 29 "github.com/Seikaijyu/gio/op/clip" 30 "github.com/Seikaijyu/gio/op/paint" 31 ) 32 33 // document holds a collection of shaped lines and alignment information for 34 // those lines. 35 type document struct { 36 lines []line 37 alignment Alignment 38 // alignWidth is the width used when aligning text. 39 alignWidth int 40 unreadRuneCount int 41 } 42 43 // append adds the lines of other to the end of l and ensures they 44 // are aligned to the same width. 45 func (l *document) append(other document) { 46 l.lines = append(l.lines, other.lines...) 47 l.alignWidth = max(l.alignWidth, other.alignWidth) 48 calculateYOffsets(l.lines) 49 } 50 51 // reset empties the document in preparation to reuse its memory. 52 func (l *document) reset() { 53 l.lines = l.lines[:0] 54 l.alignment = Start 55 l.alignWidth = 0 56 l.unreadRuneCount = 0 57 } 58 59 func max(a, b int) int { 60 if a > b { 61 return a 62 } 63 return b 64 } 65 66 // A line contains the measurements of a line of text. 67 type line struct { 68 // runs contains sequences of shaped glyphs with common attributes. The order 69 // of runs is logical, meaning that the first run will contain the glyphs 70 // corresponding to the first runes of data in the original text. 71 runs []runLayout 72 // visualOrder is a slice of indices into Runs that describes the visual positions 73 // of each run of text. Iterating this slice and accessing Runs at each 74 // of the values stored in this slice traverses the runs in proper visual 75 // order from left to right. 76 visualOrder []int 77 // width is the width of the line. 78 width fixed.Int26_6 79 // ascent is the height above the baseline. 80 ascent fixed.Int26_6 81 // descent is the height below the baseline, including 82 // the line gap. 83 descent fixed.Int26_6 84 // lineHeight captures the gap that should exist between the baseline of this 85 // line and the previous (if any). 86 lineHeight fixed.Int26_6 87 // direction is the dominant direction of the line. This direction will be 88 // used to align the text content of the line, but may not match the actual 89 // direction of the runs of text within the line (such as an RTL sentence 90 // within an LTR paragraph). 91 direction system.TextDirection 92 // runeCount is the number of text runes represented by this line's runs. 93 runeCount int 94 95 yOffset int 96 } 97 98 // insertTrailingSyntheticNewline adds a synthetic newline to the final logical run of the line 99 // with the given shaping cluster index. 100 func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) { 101 // If there was a newline at the end of this paragraph, insert a synthetic glyph representing it. 102 finalContentRun := len(l.runs) - 1 103 // If there was a trailing newline update the rune counts to include 104 // it on the last line of the paragraph. 105 l.runeCount += 1 106 l.runs[finalContentRun].Runes.Count += 1 107 108 syntheticGlyph := glyph{ 109 id: 0, 110 clusterIndex: newLineClusterIdx, 111 glyphCount: 0, 112 runeCount: 1, 113 xAdvance: 0, 114 yAdvance: 0, 115 xOffset: 0, 116 yOffset: 0, 117 } 118 // Inset the synthetic newline glyph on the proper end of the run. 119 if l.runs[finalContentRun].Direction.Progression() == system.FromOrigin { 120 l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, syntheticGlyph) 121 } else { 122 // Ensure capacity. 123 l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, glyph{}) 124 copy(l.runs[finalContentRun].Glyphs[1:], l.runs[finalContentRun].Glyphs) 125 l.runs[finalContentRun].Glyphs[0] = syntheticGlyph 126 } 127 } 128 129 func (l *line) setTruncatedCount(truncatedCount int) { 130 // If we've truncated the text with a truncator, adjust the rune counts within the 131 // truncator to make it represent the truncated text. 132 finalRunIdx := len(l.runs) - 1 133 l.runs[finalRunIdx].truncator = true 134 finalGlyphIdx := len(l.runs[finalRunIdx].Glyphs) - 1 135 // The run represents all of the truncated text. 136 l.runs[finalRunIdx].Runes.Count = truncatedCount 137 // Only the final glyph represents any runes, and it represents all truncated text. 138 for i := range l.runs[finalRunIdx].Glyphs { 139 if i == finalGlyphIdx { 140 l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncatedCount 141 } else { 142 l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0 143 } 144 } 145 } 146 147 // Range describes the position and quantity of a range of text elements 148 // within a larger slice. The unit is usually runes of unicode data or 149 // glyphs of shaped font data. 150 type Range struct { 151 // Count describes the number of items represented by the Range. 152 Count int 153 // Offset describes the start position of the represented 154 // items within a larger list. 155 Offset int 156 } 157 158 // glyph contains the metadata needed to render a glyph. 159 type glyph struct { 160 // id is this glyph's identifier within the font it was shaped with. 161 id GlyphID 162 // clusterIndex is the identifier for the text shaping cluster that 163 // this glyph is part of. 164 clusterIndex int 165 // glyphCount is the number of glyphs in the same cluster as this glyph. 166 glyphCount int 167 // runeCount is the quantity of runes in the source text that this glyph 168 // corresponds to. 169 runeCount int 170 // xAdvance and yAdvance describe the distance the dot moves when 171 // laying out the glyph on the X or Y axis. 172 xAdvance, yAdvance fixed.Int26_6 173 // xOffset and yOffset describe offsets from the dot that should be 174 // applied when rendering the glyph. 175 xOffset, yOffset fixed.Int26_6 176 // bounds describes the visual bounding box of the glyph relative to 177 // its dot. 178 bounds fixed.Rectangle26_6 179 } 180 181 type runLayout struct { 182 // VisualPosition describes the relative position of this run of text within 183 // its line. It should be a valid index into the containing line's VisualOrder 184 // slice. 185 VisualPosition int 186 // X is the visual offset of the dot for the first glyph in this run 187 // relative to the beginning of the line. 188 X fixed.Int26_6 189 // Glyphs are the actual font characters for the text. They are ordered 190 // from left to right regardless of the text direction of the underlying 191 // text. 192 Glyphs []glyph 193 // Runes describes the position of the text data this layout represents 194 // within the containing text.Line. 195 Runes Range 196 // Advance is the sum of the advances of all clusters in the Layout. 197 Advance fixed.Int26_6 198 // PPEM is the pixels-per-em scale used to shape this run. 199 PPEM fixed.Int26_6 200 // Direction is the layout direction of the glyphs. 201 Direction system.TextDirection 202 // face is the font face that the ID of each Glyph in the Layout refers to. 203 face font.Face 204 // truncator indicates that this run is a text truncator standing in for remaining 205 // text. 206 truncator bool 207 } 208 209 // shaperImpl implements the shaping and line-wrapping of opentype fonts. 210 type shaperImpl struct { 211 // Fields for tracking fonts/faces. 212 fontMap *fontscan.FontMap 213 faces []font.Face 214 faceToIndex map[font.Font]int 215 faceMeta []giofont.Font 216 defaultFaces []string 217 logger interface { 218 Printf(format string, args ...any) 219 } 220 parser parser 221 222 // Shaping and wrapping state. 223 shaper shaping.HarfbuzzShaper 224 wrapper shaping.LineWrapper 225 bidiParagraph bidi.Paragraph 226 227 // Scratch buffers used to avoid re-allocating slices during routine internal 228 // shaping operations. 229 splitScratch1, splitScratch2 []shaping.Input 230 outScratchBuf []shaping.Output 231 scratchRunes []rune 232 233 // bitmapGlyphCache caches extracted bitmap glyph images. 234 bitmapGlyphCache bitmapCache 235 } 236 237 // debugLogger only logs messages if debug.Text is true. 238 type debugLogger struct { 239 *log.Logger 240 } 241 242 func newDebugLogger() debugLogger { 243 return debugLogger{Logger: log.New(log.Writer(), "[text] ", log.Default().Flags())} 244 } 245 246 func (d debugLogger) Printf(format string, args ...any) { 247 if debug.Text.Load() { 248 d.Logger.Printf(format, args...) 249 } 250 } 251 252 func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl { 253 var shaper shaperImpl 254 shaper.logger = newDebugLogger() 255 shaper.fontMap = fontscan.NewFontMap(shaper.logger) 256 shaper.faceToIndex = make(map[font.Font]int) 257 if systemFonts { 258 str, err := os.UserCacheDir() 259 if err != nil { 260 shaper.logger.Printf("failed resolving font cache dir: %v", err) 261 shaper.logger.Printf("skipping system font load") 262 } 263 if err := shaper.fontMap.UseSystemFonts(str); err != nil { 264 shaper.logger.Printf("failed loading system fonts: %v", err) 265 } 266 } 267 for _, f := range collection { 268 shaper.Load(f) 269 shaper.defaultFaces = append(shaper.defaultFaces, string(f.Font.Typeface)) 270 } 271 shaper.shaper.SetFontCacheSize(32) 272 return &shaper 273 } 274 275 // Load registers the provided FontFace with the shaper, if it is compatible. 276 // It returns whether the face is now available for use. FontFaces are prioritized 277 // in the order in which they are loaded, with the first face being the default. 278 func (s *shaperImpl) Load(f FontFace) { 279 s.fontMap.AddFace(f.Face.Face(), opentype.FontToDescription(f.Font)) 280 s.addFace(f.Face.Face(), f.Font) 281 } 282 283 func (s *shaperImpl) addFace(f font.Face, md giofont.Font) { 284 if _, ok := s.faceToIndex[f.Font]; ok { 285 return 286 } 287 s.logger.Printf("loaded face %s(style:%s, weight:%d)", md.Typeface, md.Style, md.Weight) 288 idx := len(s.faces) 289 s.faceToIndex[f.Font] = idx 290 s.faces = append(s.faces, f) 291 s.faceMeta = append(s.faceMeta, md) 292 } 293 294 // splitByScript divides the inputs into new, smaller inputs on script boundaries 295 // and correctly sets the text direction per-script. It will 296 // use buf as the backing memory for the returned slice if buf is non-nil. 297 func splitByScript(inputs []shaping.Input, documentDir di.Direction, buf []shaping.Input) []shaping.Input { 298 var splitInputs []shaping.Input 299 if buf == nil { 300 splitInputs = make([]shaping.Input, 0, len(inputs)) 301 } else { 302 splitInputs = buf 303 } 304 for _, input := range inputs { 305 currentInput := input 306 if input.RunStart == input.RunEnd { 307 return []shaping.Input{input} 308 } 309 firstNonCommonRune := input.RunStart 310 for i := firstNonCommonRune; i < input.RunEnd; i++ { 311 if language.LookupScript(input.Text[i]) != language.Common { 312 firstNonCommonRune = i 313 break 314 } 315 } 316 currentInput.Script = language.LookupScript(input.Text[firstNonCommonRune]) 317 for i := firstNonCommonRune + 1; i < input.RunEnd; i++ { 318 r := input.Text[i] 319 runeScript := language.LookupScript(r) 320 321 if runeScript == language.Common || runeScript == currentInput.Script { 322 continue 323 } 324 325 if i != input.RunStart { 326 currentInput.RunEnd = i 327 splitInputs = append(splitInputs, currentInput) 328 } 329 330 currentInput = input 331 currentInput.RunStart = i 332 currentInput.Script = runeScript 333 // In the future, it may make sense to try to guess the language of the text here as well, 334 // but this is a complex process. 335 } 336 // close and add the last input 337 currentInput.RunEnd = input.RunEnd 338 splitInputs = append(splitInputs, currentInput) 339 } 340 341 return splitInputs 342 } 343 344 func (s *shaperImpl) splitBidi(input shaping.Input) []shaping.Input { 345 var splitInputs []shaping.Input 346 if input.Direction.Axis() != di.Horizontal || input.RunStart == input.RunEnd { 347 return []shaping.Input{input} 348 } 349 def := bidi.LeftToRight 350 if input.Direction.Progression() == di.TowardTopLeft { 351 def = bidi.RightToLeft 352 } 353 s.bidiParagraph.SetString(string(input.Text), bidi.DefaultDirection(def)) 354 out, err := s.bidiParagraph.Order() 355 if err != nil { 356 return []shaping.Input{input} 357 } 358 for i := 0; i < out.NumRuns(); i++ { 359 currentInput := input 360 run := out.Run(i) 361 dir := run.Direction() 362 _, endRune := run.Pos() 363 currentInput.RunEnd = endRune + 1 364 if dir == bidi.RightToLeft { 365 currentInput.Direction = di.DirectionRTL 366 } else { 367 currentInput.Direction = di.DirectionLTR 368 } 369 splitInputs = append(splitInputs, currentInput) 370 input.RunStart = currentInput.RunEnd 371 } 372 return splitInputs 373 } 374 375 // ResolveFace allows shaperImpl to implement shaping.FontMap, wrapping its fontMap 376 // field and ensuring that any faces loaded as part of the search are registered with 377 // ids so that they can be referred to by a GlyphID. 378 func (s *shaperImpl) ResolveFace(r rune) font.Face { 379 face := s.fontMap.ResolveFace(r) 380 if face != nil { 381 family, aspect := s.fontMap.FontMetadata(face.Font) 382 md := opentype.DescriptionToFont(metadata.Description{ 383 Family: family, 384 Aspect: aspect, 385 }) 386 s.addFace(face, md) 387 return face 388 } 389 return nil 390 } 391 392 // splitByFaces divides the inputs by font coverage in the provided faces. It will use the slice provided in buf 393 // as the backing storage of the returned slice if buf is non-nil. 394 func (s *shaperImpl) splitByFaces(inputs []shaping.Input, buf []shaping.Input) []shaping.Input { 395 var split []shaping.Input 396 if buf == nil { 397 split = make([]shaping.Input, 0, len(inputs)) 398 } else { 399 split = buf 400 } 401 for _, input := range inputs { 402 split = append(split, shaping.SplitByFace(input, s)...) 403 } 404 return split 405 } 406 407 // shapeText invokes the text shaper and returns the raw text data in the shaper's native 408 // format. It does not wrap lines. 409 func (s *shaperImpl) shapeText(ppem fixed.Int26_6, lc system.Locale, txt []rune) []shaping.Output { 410 lcfg := langConfig{ 411 Language: language.NewLanguage(lc.Language), 412 Direction: mapDirection(lc.Direction), 413 } 414 // Create an initial input. 415 input := toInput(nil, ppem, lcfg, txt) 416 if input.RunStart == input.RunEnd && len(s.faces) > 0 { 417 // Give the empty string a face. This is a necessary special case because 418 // the face splitting process works by resolving faces for each rune, and 419 // the empty string contains no runes. 420 input.Face = s.faces[0] 421 } 422 // Break input on font glyph coverage. 423 inputs := s.splitBidi(input) 424 inputs = s.splitByFaces(inputs, s.splitScratch1[:0]) 425 inputs = splitByScript(inputs, lcfg.Direction, s.splitScratch2[:0]) 426 // Shape all inputs. 427 if needed := len(inputs) - len(s.outScratchBuf); needed > 0 { 428 s.outScratchBuf = slices.Grow(s.outScratchBuf, needed) 429 } 430 s.outScratchBuf = s.outScratchBuf[:0] 431 for _, input := range inputs { 432 if input.Face != nil { 433 s.outScratchBuf = append(s.outScratchBuf, s.shaper.Shape(input)) 434 } else { 435 s.outScratchBuf = append(s.outScratchBuf, shaping.Output{ 436 // Use the text size as the advance of the entire fake run so that 437 // it doesn't occupy zero space. 438 Advance: input.Size, 439 Size: input.Size, 440 Glyphs: []shaping.Glyph{ 441 { 442 Width: input.Size, 443 Height: input.Size, 444 XBearing: 0, 445 YBearing: 0, 446 XAdvance: input.Size, 447 YAdvance: input.Size, 448 XOffset: 0, 449 YOffset: 0, 450 ClusterIndex: input.RunStart, 451 RuneCount: input.RunEnd - input.RunStart, 452 GlyphCount: 1, 453 GlyphID: 0, 454 Mask: 0, 455 }, 456 }, 457 LineBounds: shaping.Bounds{ 458 Ascent: input.Size, 459 Descent: 0, 460 Gap: 0, 461 }, 462 GlyphBounds: shaping.Bounds{ 463 Ascent: input.Size, 464 Descent: 0, 465 Gap: 0, 466 }, 467 Direction: input.Direction, 468 Runes: shaping.Range{ 469 Offset: input.RunStart, 470 Count: input.RunEnd - input.RunStart, 471 }, 472 }) 473 } 474 } 475 return s.outScratchBuf 476 } 477 478 func wrapPolicyToGoText(p WrapPolicy) shaping.LineBreakPolicy { 479 switch p { 480 case WrapGraphemes: 481 return shaping.Always 482 case WrapWords: 483 return shaping.Never 484 default: 485 return shaping.WhenNecessary 486 } 487 } 488 489 // shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format. 490 func (s *shaperImpl) shapeAndWrapText(params Parameters, txt []rune) (_ []shaping.Line, truncated int) { 491 wc := shaping.WrapConfig{ 492 TruncateAfterLines: params.MaxLines, 493 TextContinues: params.forceTruncate, 494 BreakPolicy: wrapPolicyToGoText(params.WrapPolicy), 495 } 496 families := s.defaultFaces 497 if params.Font.Typeface != "" { 498 parsed, err := s.parser.parse(string(params.Font.Typeface)) 499 if err != nil { 500 s.logger.Printf("Unable to parse typeface %q: %v", params.Font.Typeface, err) 501 } else { 502 families = parsed 503 } 504 } 505 s.fontMap.SetQuery(fontscan.Query{ 506 Families: families, 507 Aspect: opentype.FontToDescription(params.Font).Aspect, 508 }) 509 if wc.TruncateAfterLines > 0 { 510 if len(params.Truncator) == 0 { 511 params.Truncator = "…" 512 } 513 // We only permit a single run as the truncator, regardless of whether more were generated. 514 // Just use the first one. 515 wc.Truncator = s.shapeText(params.PxPerEm, params.Locale, []rune(params.Truncator))[0] 516 } 517 // Wrap outputs into lines. 518 return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(params.PxPerEm, params.Locale, txt))) 519 } 520 521 // replaceControlCharacters replaces problematic unicode 522 // code points with spaces to ensure proper rune accounting. 523 func replaceControlCharacters(in []rune) []rune { 524 for i, r := range in { 525 switch r { 526 // ASCII File separator. 527 case '\u001C': 528 // ASCII Group separator. 529 case '\u001D': 530 // ASCII Record separator. 531 case '\u001E': 532 case '\r': 533 case '\n': 534 // Unicode "next line" character. 535 case '\u0085': 536 // Unicode "paragraph separator". 537 case '\u2029': 538 default: 539 continue 540 } 541 in[i] = ' ' 542 } 543 return in 544 } 545 546 // Layout shapes and wraps the text, and returns the result in Gio's shaped text format. 547 func (s *shaperImpl) LayoutString(params Parameters, txt string) document { 548 return s.LayoutRunes(params, []rune(txt)) 549 } 550 551 // Layout shapes and wraps the text, and returns the result in Gio's shaped text format. 552 func (s *shaperImpl) Layout(params Parameters, txt io.RuneReader) document { 553 s.scratchRunes = s.scratchRunes[:0] 554 for r, _, err := txt.ReadRune(); err != nil; r, _, err = txt.ReadRune() { 555 s.scratchRunes = append(s.scratchRunes, r) 556 } 557 return s.LayoutRunes(params, s.scratchRunes) 558 } 559 560 func calculateYOffsets(lines []line) { 561 if len(lines) < 1 { 562 return 563 } 564 // Ceil the first value to ensure that we don't baseline it too close to the top of the 565 // viewport and cut off the top pixel. 566 currentY := lines[0].ascent.Ceil() 567 for i := range lines { 568 if i > 0 { 569 currentY += lines[i].lineHeight.Round() 570 } 571 lines[i].yOffset = currentY 572 } 573 } 574 575 // LayoutRunes shapes and wraps the text, and returns the result in Gio's shaped text format. 576 func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document { 577 hasNewline := len(txt) > 0 && txt[len(txt)-1] == '\n' 578 var ls []shaping.Line 579 var truncated int 580 if hasNewline { 581 txt = txt[:len(txt)-1] 582 } 583 if params.MaxLines != 0 && hasNewline { 584 // If we might end up truncating a trailing newline, we must insert the truncator symbol 585 // on the final line (if we hit the limit). 586 params.forceTruncate = true 587 } 588 ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt)) 589 590 hasTruncator := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls)) 591 if hasTruncator && hasNewline { 592 // We have a truncator at the end of the line, so the newline is logically 593 // truncated as well. 594 truncated++ 595 hasNewline = false 596 } 597 598 // Convert to Lines. 599 textLines := make([]line, len(ls)) 600 maxHeight := fixed.Int26_6(0) 601 for i := range ls { 602 otLine := toLine(s.faceToIndex, ls[i], params.Locale.Direction) 603 if otLine.lineHeight > maxHeight { 604 maxHeight = otLine.lineHeight 605 } 606 if isFinalLine := i == len(ls)-1; isFinalLine { 607 if hasNewline { 608 otLine.insertTrailingSyntheticNewline(len(txt)) 609 } 610 if hasTruncator { 611 otLine.setTruncatedCount(truncated) 612 } 613 } 614 textLines[i] = otLine 615 } 616 if params.LineHeight != 0 { 617 maxHeight = params.LineHeight 618 } 619 if params.LineHeightScale == 0 { 620 params.LineHeightScale = 1.2 621 } 622 623 maxHeight = floatToFixed(fixedToFloat(maxHeight) * params.LineHeightScale) 624 for i := range textLines { 625 textLines[i].lineHeight = maxHeight 626 } 627 calculateYOffsets(textLines) 628 return document{ 629 lines: textLines, 630 alignment: params.Alignment, 631 alignWidth: alignWidth(params.MinWidth, textLines), 632 } 633 } 634 635 func alignWidth(minWidth int, lines []line) int { 636 for _, l := range lines { 637 minWidth = max(minWidth, l.width.Ceil()) 638 } 639 return minWidth 640 } 641 642 // Shape converts the provided glyphs into a path. The path will enclose the forms 643 // of all vector glyphs. 644 func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec { 645 var lastPos f32.Point 646 var x fixed.Int26_6 647 var builder clip.Path 648 builder.Begin(pathOps) 649 for i, g := range gs { 650 if i == 0 { 651 x = g.X 652 } 653 ppem, faceIdx, gid := splitGlyphID(g.ID) 654 if faceIdx >= len(s.faces) { 655 continue 656 } 657 face := s.faces[faceIdx] 658 if face == nil { 659 continue 660 } 661 scaleFactor := fixedToFloat(ppem) / float32(face.Upem()) 662 glyphData := face.GlyphData(gid) 663 switch glyphData := glyphData.(type) { 664 case api.GlyphOutline: 665 outline := glyphData 666 // Move to glyph position. 667 pos := f32.Point{ 668 X: fixedToFloat((g.X - x) - g.Offset.X), 669 Y: -fixedToFloat(g.Offset.Y), 670 } 671 builder.Move(pos.Sub(lastPos)) 672 lastPos = pos 673 var lastArg f32.Point 674 675 // Convert fonts.Segments to relative segments. 676 for _, fseg := range outline.Segments { 677 nargs := 1 678 switch fseg.Op { 679 case api.SegmentOpQuadTo: 680 nargs = 2 681 case api.SegmentOpCubeTo: 682 nargs = 3 683 } 684 var args [3]f32.Point 685 for i := 0; i < nargs; i++ { 686 a := f32.Point{ 687 X: fseg.Args[i].X * scaleFactor, 688 Y: -fseg.Args[i].Y * scaleFactor, 689 } 690 args[i] = a.Sub(lastArg) 691 if i == nargs-1 { 692 lastArg = a 693 } 694 } 695 switch fseg.Op { 696 case api.SegmentOpMoveTo: 697 builder.Move(args[0]) 698 case api.SegmentOpLineTo: 699 builder.Line(args[0]) 700 case api.SegmentOpQuadTo: 701 builder.Quad(args[0], args[1]) 702 case api.SegmentOpCubeTo: 703 builder.Cube(args[0], args[1], args[2]) 704 default: 705 panic("unsupported segment op") 706 } 707 } 708 lastPos = lastPos.Add(lastArg) 709 } 710 } 711 return builder.End() 712 } 713 714 func fixedToFloat(i fixed.Int26_6) float32 { 715 return float32(i) / 64.0 716 } 717 718 func floatToFixed(f float32) fixed.Int26_6 { 719 return fixed.Int26_6(f * 64) 720 } 721 722 // Bitmaps returns an op.CallOp that will display all bitmap glyphs within gs. 723 // The positioning of the bitmaps uses the same logic as Shape(), so the returned 724 // CallOp can be added at the same offset as the path data returned by Shape() 725 // and will align correctly. 726 func (s *shaperImpl) Bitmaps(ops *op.Ops, gs []Glyph) op.CallOp { 727 var x fixed.Int26_6 728 bitmapMacro := op.Record(ops) 729 for i, g := range gs { 730 if i == 0 { 731 x = g.X 732 } 733 _, faceIdx, gid := splitGlyphID(g.ID) 734 if faceIdx >= len(s.faces) { 735 continue 736 } 737 face := s.faces[faceIdx] 738 if face == nil { 739 continue 740 } 741 glyphData := face.GlyphData(gid) 742 switch glyphData := glyphData.(type) { 743 case api.GlyphBitmap: 744 var imgOp paint.ImageOp 745 var imgSize image.Point 746 bitmapData, ok := s.bitmapGlyphCache.Get(g.ID) 747 if !ok { 748 var img image.Image 749 switch glyphData.Format { 750 case api.PNG, api.JPG, api.TIFF: 751 img, _, _ = image.Decode(bytes.NewReader(glyphData.Data)) 752 case api.BlackAndWhite: 753 // This is a complex family of uncompressed bitmaps that don't seem to be 754 // very common in practice. We can try adding support later if needed. 755 fallthrough 756 default: 757 // Unknown format. 758 continue 759 } 760 imgOp = paint.NewImageOp(img) 761 imgSize = img.Bounds().Size() 762 s.bitmapGlyphCache.Put(g.ID, bitmap{img: imgOp, size: imgSize}) 763 } else { 764 imgOp = bitmapData.img 765 imgSize = bitmapData.size 766 } 767 off := op.Affine(f32.Affine2D{}.Offset(f32.Point{ 768 X: fixedToFloat((g.X - x) - g.Offset.X), 769 Y: fixedToFloat(g.Offset.Y + g.Bounds.Min.Y), 770 })).Push(ops) 771 cl := clip.Rect{Max: imgSize}.Push(ops) 772 773 glyphSize := image.Rectangle{ 774 Min: image.Point{ 775 X: g.Bounds.Min.X.Round(), 776 Y: g.Bounds.Min.Y.Round(), 777 }, 778 Max: image.Point{ 779 X: g.Bounds.Max.X.Round(), 780 Y: g.Bounds.Max.Y.Round(), 781 }, 782 }.Size() 783 aff := op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Point{ 784 X: float32(glyphSize.X) / float32(imgSize.X), 785 Y: float32(glyphSize.Y) / float32(imgSize.Y), 786 })).Push(ops) 787 imgOp.Add(ops) 788 paint.PaintOp{}.Add(ops) 789 aff.Pop() 790 cl.Pop() 791 off.Pop() 792 } 793 } 794 return bitmapMacro.Stop() 795 } 796 797 // langConfig describes the language and writing system of a body of text. 798 type langConfig struct { 799 // Language the text is written in. 800 language.Language 801 // Writing system used to represent the text. 802 language.Script 803 // Direction of the text, usually driven by the writing system. 804 di.Direction 805 } 806 807 // toInput converts its parameters into a shaping.Input. 808 func toInput(face font.Face, ppem fixed.Int26_6, lc langConfig, runes []rune) shaping.Input { 809 var input shaping.Input 810 input.Direction = lc.Direction 811 input.Text = runes 812 input.Size = ppem 813 input.Face = face 814 input.Language = lc.Language 815 input.Script = lc.Script 816 input.RunStart = 0 817 input.RunEnd = len(runes) 818 return input 819 } 820 821 func mapDirection(d system.TextDirection) di.Direction { 822 switch d { 823 case system.LTR: 824 return di.DirectionLTR 825 case system.RTL: 826 return di.DirectionRTL 827 } 828 return di.DirectionLTR 829 } 830 831 func unmapDirection(d di.Direction) system.TextDirection { 832 switch d { 833 case di.DirectionLTR: 834 return system.LTR 835 case di.DirectionRTL: 836 return system.RTL 837 } 838 return system.LTR 839 } 840 841 // toGioGlyphs converts text shaper glyphs into the minimal representation 842 // that Gio needs. 843 func toGioGlyphs(in []shaping.Glyph, ppem fixed.Int26_6, faceIdx int) []glyph { 844 out := make([]glyph, 0, len(in)) 845 for _, g := range in { 846 // To better understand how to calculate the bounding box, see here: 847 // https://freetype.org/freetype2/docs/glyphs/glyph-metrics-3.svg 848 var bounds fixed.Rectangle26_6 849 bounds.Min.X = g.XBearing 850 bounds.Min.Y = -g.YBearing 851 bounds.Max = bounds.Min.Add(fixed.Point26_6{X: g.Width, Y: -g.Height}) 852 out = append(out, glyph{ 853 id: newGlyphID(ppem, faceIdx, g.GlyphID), 854 clusterIndex: g.ClusterIndex, 855 runeCount: g.RuneCount, 856 glyphCount: g.GlyphCount, 857 xAdvance: g.XAdvance, 858 yAdvance: g.YAdvance, 859 xOffset: g.XOffset, 860 yOffset: g.YOffset, 861 bounds: bounds, 862 }) 863 } 864 return out 865 } 866 867 // toLine converts the output into a Line with the provided dominant text direction. 868 func toLine(faceToIndex map[font.Font]int, o shaping.Line, dir system.TextDirection) line { 869 if len(o) < 1 { 870 return line{} 871 } 872 line := line{ 873 runs: make([]runLayout, len(o)), 874 direction: dir, 875 } 876 maxSize := fixed.Int26_6(0) 877 for i := range o { 878 run := o[i] 879 if run.Size > maxSize { 880 maxSize = run.Size 881 } 882 var font font.Font 883 if run.Face != nil { 884 font = run.Face.Font 885 } 886 line.runs[i] = runLayout{ 887 Glyphs: toGioGlyphs(run.Glyphs, run.Size, faceToIndex[font]), 888 Runes: Range{ 889 Count: run.Runes.Count, 890 Offset: line.runeCount, 891 }, 892 Direction: unmapDirection(run.Direction), 893 face: run.Face, 894 Advance: run.Advance, 895 PPEM: run.Size, 896 } 897 line.runeCount += run.Runes.Count 898 line.width += run.Advance 899 if line.ascent < run.LineBounds.Ascent { 900 line.ascent = run.LineBounds.Ascent 901 } 902 if line.descent < -run.LineBounds.Descent+run.LineBounds.Gap { 903 line.descent = -run.LineBounds.Descent + run.LineBounds.Gap 904 } 905 } 906 line.lineHeight = maxSize 907 computeVisualOrder(&line) 908 return line 909 } 910 911 // computeVisualOrder will populate the Line's VisualOrder field and the 912 // VisualPosition field of each element in Runs. 913 func computeVisualOrder(l *line) { 914 l.visualOrder = make([]int, len(l.runs)) 915 const none = -1 916 bidiRangeStart := none 917 918 // visPos returns the visual position for an individual logically-indexed 919 // run in this line, taking only the line's overall text direction into 920 // account. 921 visPos := func(logicalIndex int) int { 922 if l.direction.Progression() == system.TowardOrigin { 923 return len(l.runs) - 1 - logicalIndex 924 } 925 return logicalIndex 926 } 927 928 // resolveBidi populated the line's VisualOrder fields for the elements in the 929 // half-open range [bidiRangeStart:bidiRangeEnd) indicating that those elements 930 // should be displayed in reverse-visual order. 931 resolveBidi := func(bidiRangeStart, bidiRangeEnd int) { 932 firstVisual := bidiRangeEnd - 1 933 // Just found the end of a bidi range. 934 for startIdx := bidiRangeStart; startIdx < bidiRangeEnd; startIdx++ { 935 pos := visPos(firstVisual) 936 l.runs[startIdx].VisualPosition = pos 937 l.visualOrder[pos] = startIdx 938 firstVisual-- 939 } 940 bidiRangeStart = none 941 } 942 for runIdx, run := range l.runs { 943 if run.Direction.Progression() != l.direction.Progression() { 944 if bidiRangeStart == none { 945 bidiRangeStart = runIdx 946 } 947 continue 948 } else if bidiRangeStart != none { 949 // Just found the end of a bidi range. 950 resolveBidi(bidiRangeStart, runIdx) 951 bidiRangeStart = none 952 } 953 pos := visPos(runIdx) 954 l.runs[runIdx].VisualPosition = pos 955 l.visualOrder[pos] = runIdx 956 } 957 if bidiRangeStart != none { 958 // We ended iteration within a bidi segment, resolve it. 959 resolveBidi(bidiRangeStart, len(l.runs)) 960 } 961 // Iterate and resolve the X of each run. 962 x := fixed.Int26_6(0) 963 for _, runIdx := range l.visualOrder { 964 l.runs[runIdx].X = x 965 x += l.runs[runIdx].Advance 966 } 967 }