gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/text/gotext_test.go (about) 1 package text 2 3 import ( 4 "fmt" 5 "math" 6 "reflect" 7 "strconv" 8 "testing" 9 10 nsareg "eliasnaur.com/font/noto/sans/arabic/regular" 11 "github.com/go-text/typesetting/font" 12 "github.com/go-text/typesetting/shaping" 13 "golang.org/x/exp/slices" 14 "golang.org/x/image/font/gofont/goregular" 15 "golang.org/x/image/math/fixed" 16 17 giofont "gioui.org/font" 18 "gioui.org/font/opentype" 19 "gioui.org/io/system" 20 ) 21 22 var english = system.Locale{ 23 Language: "EN", 24 Direction: system.LTR, 25 } 26 27 var arabic = system.Locale{ 28 Language: "AR", 29 Direction: system.RTL, 30 } 31 32 func testShaper(faces ...giofont.Face) *shaperImpl { 33 ff := make([]FontFace, 0, len(faces)) 34 for _, face := range faces { 35 ff = append(ff, FontFace{Face: face}) 36 } 37 shaper := newShaperImpl(false, ff) 38 return shaper 39 } 40 41 func TestEmptyString(t *testing.T) { 42 ppem := fixed.I(200) 43 ltrFace, _ := opentype.Parse(goregular.TTF) 44 shaper := testShaper(ltrFace) 45 46 lines := shaper.LayoutRunes(Parameters{ 47 PxPerEm: ppem, 48 MaxWidth: 2000, 49 Locale: english, 50 }, []rune{}) 51 if len(lines.lines) == 0 { 52 t.Fatalf("Layout returned no lines for empty string; expected 1") 53 } 54 l := lines.lines[0] 55 if expected := fixed.Int26_6(12094); l.ascent != expected { 56 t.Errorf("unexpected ascent for empty string: %v, expected %v", l.ascent, expected) 57 } 58 if expected := fixed.Int26_6(2700); l.descent != expected { 59 t.Errorf("unexpected descent for empty string: %v, expected %v", l.descent, expected) 60 } 61 } 62 63 func TestNoFaces(t *testing.T) { 64 ppem := fixed.I(200) 65 shaper := testShaper() 66 67 // Ensure shaping text with no faces does not panic. 68 shaper.LayoutRunes(Parameters{ 69 PxPerEm: ppem, 70 MaxWidth: 2000, 71 Locale: english, 72 }, []rune("✨ⷽℎ↞⋇ⱜ⪫⢡⽛⣦␆Ⱨⳏ⳯⒛⭣╎⌞⟻⢇┃➡⬎⩱⸇ⷎ⟅▤⼶⇺⩳⎏⤬⬞ⴈ⋠⿶⢒₍☟⽂ⶦ⫰⭢⌹∼▀⾯⧂❽⩏ⓖ⟅⤔⍇␋⽓ₑ⢳⠑❂⊪⢘⽨⃯▴ⷿ")) 73 } 74 75 func TestAlignWidth(t *testing.T) { 76 lines := []line{ 77 {width: fixed.I(50)}, 78 {width: fixed.I(75)}, 79 {width: fixed.I(25)}, 80 } 81 for _, minWidth := range []int{0, 50, 100} { 82 width := alignWidth(minWidth, lines) 83 if width < minWidth { 84 t.Errorf("expected width >= %d, got %d", minWidth, width) 85 } 86 } 87 } 88 89 func TestShapingAlignWidth(t *testing.T) { 90 ppem := fixed.I(10) 91 ltrFace, _ := opentype.Parse(goregular.TTF) 92 shaper := testShaper(ltrFace) 93 94 type testcase struct { 95 name string 96 minWidth, maxWidth int 97 expected int 98 str string 99 } 100 for _, tc := range []testcase{ 101 { 102 name: "zero min", 103 maxWidth: 100, 104 str: "a\nb\nc", 105 expected: 22, 106 }, 107 { 108 name: "min == max", 109 minWidth: 100, 110 maxWidth: 100, 111 str: "a\nb\nc", 112 expected: 100, 113 }, 114 { 115 name: "min < max", 116 minWidth: 50, 117 maxWidth: 100, 118 str: "a\nb\nc", 119 expected: 50, 120 }, 121 { 122 name: "min < max, text > min", 123 minWidth: 50, 124 maxWidth: 100, 125 str: "aphabetic\nb\nc", 126 expected: 60, 127 }, 128 } { 129 t.Run(tc.name, func(t *testing.T) { 130 lines := shaper.LayoutString(Parameters{PxPerEm: ppem, 131 MinWidth: tc.minWidth, 132 MaxWidth: tc.maxWidth, 133 Locale: english, 134 }, tc.str) 135 if lines.alignWidth != tc.expected { 136 t.Errorf("expected line alignWidth to be %d, got %d", tc.expected, lines.alignWidth) 137 } 138 }) 139 } 140 } 141 142 // TestNewlineSynthesis ensures that the shaper correctly inserts synthetic glyphs 143 // representing newline runes. 144 func TestNewlineSynthesis(t *testing.T) { 145 ppem := fixed.I(10) 146 ltrFace, _ := opentype.Parse(goregular.TTF) 147 rtlFace, _ := opentype.Parse(nsareg.TTF) 148 shaper := testShaper(ltrFace, rtlFace) 149 150 type testcase struct { 151 name string 152 locale system.Locale 153 txt string 154 } 155 for _, tc := range []testcase{ 156 { 157 name: "ltr bidi newline in rtl segment", 158 locale: english, 159 txt: "The quick سماء שלום لا fox تمط שלום\n", 160 }, 161 { 162 name: "ltr bidi newline in ltr segment", 163 locale: english, 164 txt: "The quick سماء שלום لا fox\n", 165 }, 166 { 167 name: "rtl bidi newline in ltr segment", 168 locale: arabic, 169 txt: "الحب سماء brown привет fox تمط jumps\n", 170 }, 171 { 172 name: "rtl bidi newline in rtl segment", 173 locale: arabic, 174 txt: "الحب سماء brown привет fox تمط\n", 175 }, 176 } { 177 t.Run(tc.name, func(t *testing.T) { 178 179 doc := shaper.LayoutRunes(Parameters{ 180 PxPerEm: ppem, 181 MaxWidth: 200, 182 Locale: tc.locale, 183 }, []rune(tc.txt)) 184 for lineIdx, line := range doc.lines { 185 lastRunIdx := len(line.runs) - 1 186 lastRun := line.runs[lastRunIdx] 187 lastGlyphIdx := len(lastRun.Glyphs) - 1 188 if lastRun.Direction.Progression() == system.TowardOrigin { 189 lastGlyphIdx = 0 190 } 191 glyph := lastRun.Glyphs[lastGlyphIdx] 192 if glyph.glyphCount != 0 { 193 t.Errorf("expected synthetic newline on line %d, run %d, glyph %d", lineIdx, lastRunIdx, lastGlyphIdx) 194 } 195 for runIdx, run := range line.runs { 196 for glyphIdx, glyph := range run.Glyphs { 197 if runIdx == lastRunIdx && glyphIdx == lastGlyphIdx { 198 continue 199 } 200 if glyph.glyphCount == 0 { 201 t.Errorf("found invalid synthetic newline on line %d, run %d, glyph %d", lineIdx, runIdx, glyphIdx) 202 } 203 } 204 } 205 } 206 if t.Failed() { 207 printLinePositioning(t, doc.lines, nil) 208 } 209 }) 210 } 211 212 } 213 214 // simpleGlyph returns a simple square glyph with the provided cluster 215 // value. 216 func simpleGlyph(cluster int) shaping.Glyph { 217 return complexGlyph(cluster, 1, 1) 218 } 219 220 // ligatureGlyph returns a simple square glyph with the provided cluster 221 // value and number of runes. 222 func ligatureGlyph(cluster, runes int) shaping.Glyph { 223 return complexGlyph(cluster, runes, 1) 224 } 225 226 // expansionGlyph returns a simple square glyph with the provided cluster 227 // value and number of glyphs. 228 func expansionGlyph(cluster, glyphs int) shaping.Glyph { 229 return complexGlyph(cluster, 1, glyphs) 230 } 231 232 // complexGlyph returns a simple square glyph with the provided cluster 233 // value, number of associated runes, and number of glyphs in the cluster. 234 func complexGlyph(cluster, runes, glyphs int) shaping.Glyph { 235 return shaping.Glyph{ 236 Width: fixed.I(10), 237 Height: fixed.I(10), 238 XAdvance: fixed.I(10), 239 YAdvance: fixed.I(10), 240 YBearing: fixed.I(10), 241 ClusterIndex: cluster, 242 GlyphCount: glyphs, 243 RuneCount: runes, 244 } 245 } 246 247 // copyLines performs a deep copy of the provided lines. This is necessary if you 248 // want to use the line wrapper again while also using the lines. 249 func copyLines(lines []shaping.Line) []shaping.Line { 250 out := make([]shaping.Line, len(lines)) 251 for lineIdx, line := range lines { 252 lineCopy := make([]shaping.Output, len(line)) 253 for runIdx, run := range line { 254 lineCopy[runIdx] = run 255 lineCopy[runIdx].Glyphs = slices.Clone(run.Glyphs) 256 } 257 out[lineIdx] = lineCopy 258 } 259 return out 260 } 261 262 // makeTestText creates a simple and complex(bidi) sample of shaped text at the given 263 // font size and wrapped to the given line width. The runeLimit, if nonzero, 264 // truncates the sample text to ensure shorter output for expensive tests. 265 func makeTestText(shaper *shaperImpl, primaryDir system.TextDirection, fontSize, lineWidth, runeLimit int) (simpleSample, complexSample []shaping.Line) { 266 ltrFace, _ := opentype.Parse(goregular.TTF) 267 rtlFace, _ := opentype.Parse(nsareg.TTF) 268 if shaper == nil { 269 shaper = testShaper(ltrFace, rtlFace) 270 } 271 272 ltrSource := "The quick brown fox jumps over the lazy dog." 273 rtlSource := "الحب سماء لا تمط غير الأحلام" 274 // bidiSource is crafted to contain multiple consecutive RTL runs (by 275 // changing scripts within the RTL). 276 bidiSource := "The quick سماء שלום لا fox تمط שלום غير the lazy dog." 277 // bidi2Source is crafted to contain multiple consecutive LTR runs (by 278 // changing scripts within the LTR). 279 bidi2Source := "الحب سماء brown привет fox تمط jumps привет over غير الأحلام" 280 281 locale := english 282 simpleSource := ltrSource 283 complexSource := bidiSource 284 if primaryDir == system.RTL { 285 simpleSource = rtlSource 286 complexSource = bidi2Source 287 locale = arabic 288 } 289 if runeLimit != 0 { 290 simpleRunes := []rune(simpleSource) 291 complexRunes := []rune(complexSource) 292 if runeLimit < len(simpleRunes) { 293 ltrSource = string(simpleRunes[:runeLimit]) 294 } 295 if runeLimit < len(complexRunes) { 296 rtlSource = string(complexRunes[:runeLimit]) 297 } 298 } 299 simpleText, _ := shaper.shapeAndWrapText(Parameters{ 300 PxPerEm: fixed.I(fontSize), 301 MaxWidth: lineWidth, 302 Locale: locale, 303 }, []rune(simpleSource)) 304 simpleText = copyLines(simpleText) 305 complexText, _ := shaper.shapeAndWrapText(Parameters{ 306 PxPerEm: fixed.I(fontSize), 307 MaxWidth: lineWidth, 308 Locale: locale, 309 }, []rune(complexSource)) 310 complexText = copyLines(complexText) 311 testShaper(rtlFace, ltrFace) 312 return simpleText, complexText 313 } 314 315 func fixedAbs(a fixed.Int26_6) fixed.Int26_6 { 316 if a < 0 { 317 a = -a 318 } 319 return a 320 } 321 322 func TestToLine(t *testing.T) { 323 ltrFace, _ := opentype.Parse(goregular.TTF) 324 rtlFace, _ := opentype.Parse(nsareg.TTF) 325 shaper := testShaper(ltrFace, rtlFace) 326 ltr, bidi := makeTestText(shaper, system.LTR, 16, 100, 0) 327 rtl, bidi2 := makeTestText(shaper, system.RTL, 16, 100, 0) 328 _, bidiWide := makeTestText(shaper, system.LTR, 16, 200, 0) 329 _, bidi2Wide := makeTestText(shaper, system.RTL, 16, 200, 0) 330 type testcase struct { 331 name string 332 lines []shaping.Line 333 // Dominant text direction. 334 dir system.TextDirection 335 } 336 for _, tc := range []testcase{ 337 { 338 name: "ltr", 339 lines: ltr, 340 dir: system.LTR, 341 }, 342 { 343 name: "rtl", 344 lines: rtl, 345 dir: system.RTL, 346 }, 347 { 348 name: "bidi", 349 lines: bidi, 350 dir: system.LTR, 351 }, 352 { 353 name: "bidi2", 354 lines: bidi2, 355 dir: system.RTL, 356 }, 357 { 358 name: "bidi_wide", 359 lines: bidiWide, 360 dir: system.LTR, 361 }, 362 { 363 name: "bidi2_wide", 364 lines: bidi2Wide, 365 dir: system.RTL, 366 }, 367 } { 368 t.Run(tc.name, func(t *testing.T) { 369 // We expect: 370 // - Line dimensions to be populated. 371 // - Line direction to be populated. 372 // - Runs to be ordered from lowest runes first. 373 // - Runs to have widths matching the input. 374 // - Runs to have the same total number of glyphs/runes as the input. 375 runesSeen := Range{} 376 shaper := testShaper(ltrFace, rtlFace) 377 for i, input := range tc.lines { 378 seenRun := make([]bool, len(input)) 379 inputLowestRuneOffset := math.MaxInt 380 totalInputGlyphs := 0 381 totalInputRunes := 0 382 for _, run := range input { 383 if run.Runes.Offset < inputLowestRuneOffset { 384 inputLowestRuneOffset = run.Runes.Offset 385 } 386 totalInputGlyphs += len(run.Glyphs) 387 totalInputRunes += run.Runes.Count 388 } 389 output := toLine(shaper.faceToIndex, input, tc.dir) 390 if output.direction != tc.dir { 391 t.Errorf("line %d: expected direction %v, got %v", i, tc.dir, output.direction) 392 } 393 totalRunWidth := fixed.I(0) 394 totalLineGlyphs := 0 395 totalLineRunes := 0 396 for k, run := range output.runs { 397 seenRun[run.VisualPosition] = true 398 if output.visualOrder[run.VisualPosition] != k { 399 t.Errorf("line %d, run %d: run.VisualPosition=%d, but line.VisualOrder[%d]=%d(should be %d)", i, k, run.VisualPosition, run.VisualPosition, output.visualOrder[run.VisualPosition], k) 400 } 401 if run.Runes.Offset != totalLineRunes { 402 t.Errorf("line %d, run %d: expected Runes.Offset to be %d, got %d", i, k, totalLineRunes, run.Runes.Offset) 403 } 404 runGlyphCount := len(run.Glyphs) 405 if inputGlyphs := len(input[k].Glyphs); runGlyphCount != inputGlyphs { 406 t.Errorf("line %d, run %d: expected %d glyphs, found %d", i, k, inputGlyphs, runGlyphCount) 407 } 408 runRuneCount := 0 409 currentCluster := -1 410 for _, g := range run.Glyphs { 411 if g.clusterIndex != currentCluster { 412 runRuneCount += g.runeCount 413 currentCluster = g.clusterIndex 414 } 415 } 416 if run.Runes.Count != runRuneCount { 417 t.Errorf("line %d, run %d: expected %d runes, counted %d", i, k, run.Runes.Count, runRuneCount) 418 } 419 runesSeen.Count += run.Runes.Count 420 totalRunWidth += fixedAbs(run.Advance) 421 totalLineGlyphs += len(run.Glyphs) 422 totalLineRunes += run.Runes.Count 423 } 424 if output.runeCount != totalInputRunes { 425 t.Errorf("line %d: input had %d runes, only counted %d", i, totalInputRunes, output.runeCount) 426 } 427 if totalLineGlyphs != totalInputGlyphs { 428 t.Errorf("line %d: input had %d glyphs, only counted %d", i, totalInputRunes, totalLineGlyphs) 429 } 430 if totalRunWidth != output.width { 431 t.Errorf("line %d: expected width %d, got %d", i, totalRunWidth, output.width) 432 } 433 for runIndex, seen := range seenRun { 434 if !seen { 435 t.Errorf("line %d, run %d missing from runs VisualPosition fields", i, runIndex) 436 } 437 } 438 } 439 lastLine := tc.lines[len(tc.lines)-1] 440 maxRunes := 0 441 for _, run := range lastLine { 442 if run.Runes.Count+run.Runes.Offset > maxRunes { 443 maxRunes = run.Runes.Count + run.Runes.Offset 444 } 445 } 446 if runesSeen.Count != maxRunes { 447 t.Errorf("input covered %d runes, output only covers %d", maxRunes, runesSeen.Count) 448 } 449 }) 450 } 451 } 452 453 func TestComputeVisualOrder(t *testing.T) { 454 type testcase struct { 455 name string 456 input line 457 expectedVisualOrder []int 458 } 459 for _, tc := range []testcase{ 460 { 461 name: "ltr", 462 input: line{ 463 direction: system.LTR, 464 runs: []runLayout{ 465 {Direction: system.LTR}, 466 {Direction: system.LTR}, 467 {Direction: system.LTR}, 468 }, 469 }, 470 expectedVisualOrder: []int{0, 1, 2}, 471 }, 472 { 473 name: "rtl", 474 input: line{ 475 direction: system.RTL, 476 runs: []runLayout{ 477 {Direction: system.RTL}, 478 {Direction: system.RTL}, 479 {Direction: system.RTL}, 480 }, 481 }, 482 expectedVisualOrder: []int{2, 1, 0}, 483 }, 484 { 485 name: "bidi-ltr", 486 input: line{ 487 direction: system.LTR, 488 runs: []runLayout{ 489 {Direction: system.LTR}, 490 {Direction: system.RTL}, 491 {Direction: system.RTL}, 492 {Direction: system.RTL}, 493 {Direction: system.LTR}, 494 }, 495 }, 496 expectedVisualOrder: []int{0, 3, 2, 1, 4}, 497 }, 498 { 499 name: "bidi-ltr-complex", 500 input: line{ 501 direction: system.LTR, 502 runs: []runLayout{ 503 {Direction: system.RTL}, 504 {Direction: system.RTL}, 505 {Direction: system.LTR}, 506 {Direction: system.RTL}, 507 {Direction: system.RTL}, 508 {Direction: system.LTR}, 509 {Direction: system.RTL}, 510 {Direction: system.RTL}, 511 {Direction: system.LTR}, 512 {Direction: system.RTL}, 513 {Direction: system.RTL}, 514 }, 515 }, 516 expectedVisualOrder: []int{1, 0, 2, 4, 3, 5, 7, 6, 8, 10, 9}, 517 }, 518 { 519 name: "bidi-rtl", 520 input: line{ 521 direction: system.RTL, 522 runs: []runLayout{ 523 {Direction: system.RTL}, 524 {Direction: system.LTR}, 525 {Direction: system.LTR}, 526 {Direction: system.LTR}, 527 {Direction: system.RTL}, 528 }, 529 }, 530 expectedVisualOrder: []int{4, 1, 2, 3, 0}, 531 }, 532 { 533 name: "bidi-rtl-complex", 534 input: line{ 535 direction: system.RTL, 536 runs: []runLayout{ 537 {Direction: system.LTR}, 538 {Direction: system.LTR}, 539 {Direction: system.RTL}, 540 {Direction: system.LTR}, 541 {Direction: system.LTR}, 542 {Direction: system.RTL}, 543 {Direction: system.LTR}, 544 {Direction: system.LTR}, 545 {Direction: system.RTL}, 546 {Direction: system.LTR}, 547 {Direction: system.LTR}, 548 }, 549 }, 550 expectedVisualOrder: []int{9, 10, 8, 6, 7, 5, 3, 4, 2, 0, 1}, 551 }, 552 } { 553 t.Run(tc.name, func(t *testing.T) { 554 computeVisualOrder(&tc.input) 555 if !reflect.DeepEqual(tc.input.visualOrder, tc.expectedVisualOrder) { 556 t.Errorf("expected visual order %v, got %v", tc.expectedVisualOrder, tc.input.visualOrder) 557 } 558 for i, visualIndex := range tc.input.visualOrder { 559 if pos := tc.input.runs[visualIndex].VisualPosition; pos != i { 560 t.Errorf("line.VisualOrder[%d]=%d, but line.Runs[%d].VisualPosition=%d", i, visualIndex, visualIndex, pos) 561 } 562 } 563 }) 564 } 565 } 566 567 func FuzzLayout(f *testing.F) { 568 ltrFace, _ := opentype.Parse(goregular.TTF) 569 rtlFace, _ := opentype.Parse(nsareg.TTF) 570 f.Add("د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.", true, false, uint8(10), uint16(200)) 571 572 shaper := testShaper(ltrFace, rtlFace) 573 f.Fuzz(func(t *testing.T, txt string, rtl bool, truncate bool, fontSize uint8, width uint16) { 574 locale := system.Locale{ 575 Direction: system.LTR, 576 } 577 if rtl { 578 locale.Direction = system.RTL 579 } 580 if fontSize < 1 { 581 fontSize = 1 582 } 583 maxLines := 0 584 if truncate { 585 maxLines = 1 586 } 587 lines := shaper.LayoutRunes(Parameters{ 588 PxPerEm: fixed.I(int(fontSize)), 589 MaxWidth: int(width), 590 MaxLines: maxLines, 591 Locale: locale, 592 }, []rune(txt)) 593 validateLines(t, lines.lines, len([]rune(txt))) 594 }) 595 } 596 597 func validateLines(t *testing.T, lines []line, expectedRuneCount int) { 598 t.Helper() 599 runesSeen := 0 600 for i, line := range lines { 601 totalRunWidth := fixed.I(0) 602 totalLineGlyphs := 0 603 lineRunesSeen := 0 604 for k, run := range line.runs { 605 if line.visualOrder[run.VisualPosition] != k { 606 t.Errorf("line %d, run %d: run.VisualPosition=%d, but line.VisualOrder[%d]=%d(should be %d)", i, k, run.VisualPosition, run.VisualPosition, line.visualOrder[run.VisualPosition], k) 607 } 608 if run.Runes.Offset != lineRunesSeen { 609 t.Errorf("line %d, run %d: expected Runes.Offset to be %d, got %d", i, k, lineRunesSeen, run.Runes.Offset) 610 } 611 runRuneCount := 0 612 currentCluster := -1 613 for _, g := range run.Glyphs { 614 if g.clusterIndex != currentCluster { 615 runRuneCount += g.runeCount 616 currentCluster = g.clusterIndex 617 } 618 } 619 if run.Runes.Count != runRuneCount { 620 t.Errorf("line %d, run %d: expected %d runes, counted %d", i, k, run.Runes.Count, runRuneCount) 621 } 622 lineRunesSeen += run.Runes.Count 623 totalRunWidth += fixedAbs(run.Advance) 624 totalLineGlyphs += len(run.Glyphs) 625 } 626 if totalRunWidth != line.width { 627 t.Errorf("line %d: expected width %d, got %d", i, line.width, totalRunWidth) 628 } 629 runesSeen += lineRunesSeen 630 } 631 if runesSeen != expectedRuneCount { 632 t.Errorf("input covered %d runes, output only covers %d", expectedRuneCount, runesSeen) 633 } 634 } 635 636 // TestTextAppend ensures that appending two texts together correctly updates the new lines' 637 // y offsets. 638 func TestTextAppend(t *testing.T) { 639 ltrFace, _ := opentype.Parse(goregular.TTF) 640 rtlFace, _ := opentype.Parse(nsareg.TTF) 641 642 shaper := testShaper(ltrFace, rtlFace) 643 644 text1 := shaper.LayoutString(Parameters{ 645 PxPerEm: fixed.I(14), 646 MaxWidth: 200, 647 Locale: english, 648 }, "د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.") 649 text2 := shaper.LayoutString(Parameters{ 650 PxPerEm: fixed.I(14), 651 MaxWidth: 200, 652 Locale: english, 653 }, "د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.") 654 655 text1.append(text2) 656 curY := math.MinInt 657 for lineNum, line := range text1.lines { 658 yOff := line.yOffset 659 if yOff <= curY { 660 t.Errorf("lines[%d] has y offset %d, <= to previous %d", lineNum, yOff, curY) 661 } 662 curY = yOff 663 } 664 } 665 666 func TestGlyphIDPacking(t *testing.T) { 667 const maxPPem = fixed.Int26_6((1 << sizebits) - 1) 668 type testcase struct { 669 name string 670 ppem fixed.Int26_6 671 faceIndex int 672 gid font.GID 673 expected GlyphID 674 } 675 for _, tc := range []testcase{ 676 { 677 name: "zero value", 678 }, 679 { 680 name: "10 ppem faceIdx 1 GID 5", 681 ppem: fixed.I(10), 682 faceIndex: 1, 683 gid: 5, 684 expected: 284223755780101, 685 }, 686 { 687 name: maxPPem.String() + " ppem faceIdx " + strconv.Itoa(math.MaxUint16) + " GID " + fmt.Sprintf("%d", int64(math.MaxUint32)), 688 ppem: maxPPem, 689 faceIndex: math.MaxUint16, 690 gid: math.MaxUint32, 691 expected: 18446744073709551615, 692 }, 693 } { 694 t.Run(tc.name, func(t *testing.T) { 695 actual := newGlyphID(tc.ppem, tc.faceIndex, tc.gid) 696 if actual != tc.expected { 697 t.Errorf("expected %d, got %d", tc.expected, actual) 698 } 699 actualPPEM, actualFaceIdx, actualGID := splitGlyphID(actual) 700 if actualPPEM != tc.ppem { 701 t.Errorf("expected ppem %d, got %d", tc.ppem, actualPPEM) 702 } 703 if actualFaceIdx != tc.faceIndex { 704 t.Errorf("expected faceIdx %d, got %d", tc.faceIndex, actualFaceIdx) 705 } 706 if actualGID != tc.gid { 707 t.Errorf("expected gid %d, got %d", tc.gid, actualGID) 708 } 709 }) 710 } 711 }