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