github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/renderer_test.go (about) 1 //go:build gtxt 2 3 package etxt 4 5 import "os" 6 import "image" 7 import "image/color" 8 import "image/png" 9 import "log" 10 11 import "testing" 12 13 import "golang.org/x/image/font/sfnt" 14 import "golang.org/x/image/math/fixed" 15 16 import "github.com/Kintar/etxt/emask" 17 import "github.com/Kintar/etxt/esizer" 18 import "github.com/Kintar/etxt/efixed" 19 20 func TestSetGet(t *testing.T) { 21 // mostly tests the renderer default values 22 rast := emask.FauxRasterizer{} 23 renderer := NewRenderer(&rast) 24 vAlign, hAlign := renderer.GetAlign() 25 if vAlign != Baseline { 26 t.Fatalf("expected Baseline, got %d", vAlign) 27 } 28 if hAlign != Left { 29 t.Fatalf("expected Left, got %d", hAlign) 30 } 31 32 handler := renderer.GetCacheHandler() 33 if handler != nil { 34 t.Fatalf("expected nil cache handler") 35 } 36 37 rgba, isRgba := renderer.GetColor().(color.RGBA) 38 if !isRgba { 39 t.Fatal("expected rgba color") 40 } 41 if rgba.R != 255 || rgba.G != 255 || rgba.B != 255 || rgba.A != 255 { 42 t.Fatalf("expected white") 43 } 44 45 font := renderer.GetFont() 46 if font != nil { 47 t.Fatal("expected nil font") 48 } 49 50 renderer.SetLineHeight(10) 51 renderer.SetLineSpacing(2) 52 advance := renderer.GetLineAdvance() 53 if advance != (20 << 6) { 54 t.Fatalf("expected advance = 20, got %f", float64(advance)/64) 55 } 56 renderer.SetLineHeightAuto() 57 58 if renderer.GetRasterizer() != &rast { 59 t.Fatal("what") 60 } 61 62 sizePx := renderer.GetSizePxFract() 63 if sizePx != 16<<6 { 64 t.Fatalf("expected size = 16, got %f", float64(sizePx)/64) 65 } 66 renderer.SetSizePxFract(17 << 6) 67 sizePx = renderer.GetSizePxFract() 68 if sizePx != 17<<6 { 69 t.Fatalf("expected size = 17, got %f", float64(sizePx)/64) 70 } 71 72 sizer := renderer.GetSizer() 73 _, isDefaultSizer := sizer.(*esizer.DefaultSizer) 74 if !isDefaultSizer { 75 t.Fatal("expected DefaultSizer") 76 } 77 78 renderer.SetVertAlign(YCenter) 79 renderer.SetHorzAlign(XCenter) 80 vAlign, hAlign = renderer.GetAlign() 81 if vAlign != YCenter { 82 t.Fatalf("expected YCenter, got %d", vAlign) 83 } 84 if hAlign != XCenter { 85 t.Fatalf("expected XCenter, got %d", hAlign) 86 } 87 } 88 89 func TestSelectionRect(t *testing.T) { 90 renderer := NewStdRenderer() 91 renderer.SetFont(testFont) 92 renderer.SetCacheHandler(NewDefaultCache(1024).NewHandler()) 93 renderer.SetDirection(RightToLeft) 94 95 rect := renderer.SelectionRect("hey ho") 96 if rect.Width.Ceil() < 32 { 97 t.Fatalf("expected Width.Ceil to be at least 32, but got %d", rect.Width.Ceil()) 98 } 99 if rect.Width.Ceil() > 128 { 100 t.Fatalf("expected Width.Ceil to be below 128, but got %d", rect.Width.Ceil()) 101 } 102 if rect.Height.Ceil() < 8 { 103 t.Fatalf("expected Height.Ceil to be at least 8, but got %d", rect.Height.Ceil()) 104 } 105 imgRect := rect.ImageRect() 106 rect2 := renderer.SelectionRect("hey ho hey ho") 107 if !imgRect.In(rect2.ImageRect()) { 108 t.Fatal("inconsistent rects") 109 } 110 111 testGlyphs := make([]GlyphIndex, 0, len("hey ho")) 112 var buffer sfnt.Buffer 113 for _, codePoint := range "hey ho" { 114 index, err := testFont.GlyphIndex(&buffer, codePoint) 115 if err != nil { 116 panic(err) 117 } 118 if index == 0 { 119 panic(err) 120 } 121 testGlyphs = append(testGlyphs, index) 122 } 123 124 renderer.SetLineSpacing(0) 125 imgRect3 := renderer.SelectionRect("hey ho\nhey ho").ImageRect() 126 if !imgRect3.Eq(imgRect) { 127 t.Fatalf("line spacing 0 failed (%v vs %v)", imgRect, imgRect3) 128 } 129 130 // test line breaks and other edge cases 131 renderer.SetLineSpacing(1) // restore spacing 132 renderer.SetQuantizerStep(1, 1) // prevent vertical quantization adjustments 133 renderer.SetDirection(LeftToRight) 134 rect = renderer.SelectionRect("") 135 if rect.Width != 0 || rect.Height != 0 { 136 t.Fatalf("expected Width and Height to be 0, but got ~%d", rect.Width.Ceil()) 137 } 138 rect = renderer.SelectionRect("MMMMM") 139 baseHeight := efixed.ToFloat64(rect.Height) 140 rect = renderer.SelectionRect(" ") 141 checkHeight := efixed.ToFloat64(rect.Height) 142 if checkHeight != baseHeight { 143 t.Fatalf("expected Height to be %f, but got %f", baseHeight, checkHeight) 144 } 145 146 rect = renderer.SelectionRect("MMM\n") 147 heightA := efixed.ToFloat64(rect.Height) 148 rect = renderer.SelectionRect("\nMMM") 149 heightB := efixed.ToFloat64(rect.Height) 150 if heightA != heightB { 151 t.Fatalf("expected heightA (%f) == heightB (%f)", heightA, heightB) 152 } 153 if heightA == baseHeight { 154 t.Fatalf("expected heightA to be different from baseHeight (%f), but got %f", baseHeight, heightA) 155 } 156 if heightA != baseHeight*2 { 157 t.Fatalf("baseHeight = %f, heightA = %f", baseHeight, heightA) 158 t.Fatalf("expected heightA to be baseHeight*2 (%f), but got %f", baseHeight*2, heightA) 159 } 160 rect = renderer.SelectionRect("\n") 161 checkHeight = efixed.ToFloat64(rect.Height) 162 if checkHeight != baseHeight { 163 t.Fatalf("expected \\n Height to be %f, but got %f", baseHeight, checkHeight) 164 } 165 rect = renderer.SelectionRect("\n\n") 166 checkHeight = efixed.ToFloat64(rect.Height) 167 if checkHeight != baseHeight*2 { 168 t.Fatalf("expected \\n\\n Height to be %f, but got %f", baseHeight*2, checkHeight) 169 } 170 171 rect = renderer.SelectionRect("\n\n\n") 172 heightC := efixed.ToFloat64(rect.Height) 173 rect = renderer.SelectionRect("\n \n") 174 heightD := efixed.ToFloat64(rect.Height) 175 if heightC != heightD { 176 t.Fatalf("expected heightC (%f) == heightD (%f)", heightC, heightD) 177 } 178 } 179 180 // the real consistency test 181 func TestStringVsGlyph(t *testing.T) { 182 renderer := NewStdRenderer() 183 renderer.SetSizePx(16) 184 renderer.SetFont(testFont) 185 renderer.SetQuantizerStep(64, 64) 186 renderer.SetColor(color.RGBA{0, 0, 0, 255}) // black 187 188 alignPairs := []struct { 189 vert VertAlign 190 horz HorzAlign 191 }{ 192 {vert: Baseline, horz: Left}, {vert: YCenter, horz: XCenter}, 193 {vert: Top, horz: Right}, {vert: Bottom, horz: Left}, 194 } 195 type quantMode struct { 196 HorzStep fixed.Int26_6 197 VertStep fixed.Int26_6 198 } 199 quantModes := []quantMode{ 200 quantMode{HorzStep: 64, VertStep: 64}, // full quantization 201 quantMode{HorzStep: 1, VertStep: 64}, // vert quantization 202 quantMode{HorzStep: 1, VertStep: 1}, // no quantization 203 } 204 205 testText := "for lack of better words" 206 var buffer sfnt.Buffer 207 208 missing, err := GetMissingRunes(testFont, testText) 209 if err != nil { 210 panic(err) 211 } 212 if len(missing) > 0 { 213 panic("missing runes to test") 214 } 215 216 // get text as glyphs 217 testGlyphs := make([]GlyphIndex, 0, len(testText)) 218 for _, codePoint := range testText { 219 index, err := testFont.GlyphIndex(&buffer, codePoint) 220 if err != nil { 221 t.Fatalf("Unexpected error on testFont.GlyphIndex: " + err.Error()) 222 } 223 if index == 0 { 224 t.Fatalf("testFont.GlyphIndex missing rune '" + string(codePoint) + "'") 225 } 226 testGlyphs = append(testGlyphs, index) 227 } 228 229 // compute text size 230 rect := renderer.SelectionRect(testText) // fully quantized 231 for _, textDir := range []Direction{LeftToRight, RightToLeft} { 232 renderer.SetDirection(textDir) 233 for _, quantMode := range quantModes { 234 renderer.SetQuantizerStep(quantMode.HorzStep, quantMode.VertStep) 235 testRect := renderer.SelectionRect(testText) 236 for _, alignPair := range alignPairs { 237 renderer.SetAlign(alignPair.vert, alignPair.horz) 238 txtRect := renderer.SelectionRect(testText) 239 if txtRect.Width != testRect.Width || txtRect.Height != testRect.Height { 240 t.Fatalf("selection rect mismatch between aligns") 241 } 242 glyphsRect := renderer.SelectionRectGlyphs(testGlyphs) 243 if glyphsRect.Width != testRect.Width || glyphsRect.Height != testRect.Height { 244 t.Fatalf("selection rect mismatch between glyphs and text") 245 } 246 } 247 } 248 } 249 250 // create target image and fill it with white 251 w, h := rect.Width.Ceil()*2+8, rect.Height.Ceil()*2+8 252 outImageA := image.NewRGBA(image.Rect(0, 0, w, h)) 253 outImageB := image.NewRGBA(image.Rect(0, 0, w, h)) 254 for i := 0; i < w*h*4; i++ { 255 outImageA.Pix[i] = 255 256 } 257 for i := 0; i < w*h*4; i++ { 258 outImageB.Pix[i] = 255 259 } 260 261 // draw and compare results between glyphs and text 262 for _, textDir := range []Direction{LeftToRight, RightToLeft} { 263 renderer.SetDirection(textDir) 264 for _, quantMode := range quantModes { 265 renderer.SetQuantizerStep(quantMode.HorzStep, quantMode.VertStep) 266 for _, alignPair := range alignPairs { 267 renderer.SetAlign(alignPair.vert, alignPair.horz) 268 renderer.SetTarget(outImageA) 269 dotA := renderer.Draw(testText, w/2, h/2) 270 renderer.SetTarget(outImageB) 271 dotB := drawGlyphs(renderer, testGlyphs, w/2, h/2) 272 for i := 0; i < w*h*4; i++ { 273 if outImageA.Pix[i] != outImageB.Pix[i] { 274 what := "drawing mismatch between glyphs and text (quantMode " 275 what += "= %d, align pair = %d / %d)" 276 t.Fatalf(what, quantMode, alignPair.vert, alignPair.horz) 277 } 278 } 279 280 // compare returned dots 281 if dotA.X != dotB.X || dotA.Y != dotB.Y { 282 what := "mismatch in the dots returned by Draw/DrawGlyphs (quantMode " 283 what += "= %d, align pair = %d / %d): %v vs %v" 284 t.Fatalf(what, quantMode, alignPair.vert, alignPair.horz, dotA, dotB) 285 } 286 287 // clear images 288 for i := 0; i < w*h*4; i++ { 289 outImageA.Pix[i] = 255 290 } 291 for i := 0; i < w*h*4; i++ { 292 outImageB.Pix[i] = 255 293 } 294 } 295 } 296 } 297 } 298 299 func drawGlyphs(renderer *Renderer, glyphIndices []GlyphIndex, x, y int) fixed.Point26_6 { 300 return renderer.TraverseGlyphs(glyphIndices, fixed.P(x, y), 301 func(dot fixed.Point26_6, glyphIndex GlyphIndex) { 302 mask := renderer.LoadGlyphMask(glyphIndex, dot) 303 renderer.DefaultDrawFunc(dot, mask, glyphIndex) 304 }) 305 } 306 307 func TestDrawCached(t *testing.T) { 308 renderer := NewStdRenderer() 309 renderer.SetFont(testFont) 310 renderer.SetCacheHandler(NewDefaultCache(1024).NewHandler()) 311 target := image.NewAlpha(image.Rect(0, 0, 64, 64)) 312 renderer.SetTarget(target) 313 renderer.Draw("dumb test", 0, 0) 314 renderer.Draw("dumb test", 0, 0) 315 renderer.SetSizePx(18) 316 renderer.Draw("dumb test", 0, 0) 317 } 318 319 func TestGtxtMixModes(t *testing.T) { 320 target := image.NewRGBA(image.Rect(0, 0, 64, 64)) 321 renderer := NewStdRenderer() 322 renderer.SetFont(testFont) 323 renderer.SetSizePx(24) 324 renderer.SetTarget(target) 325 326 // replace mode 327 for i, _ := range target.Pix { 328 target.Pix[i] = 255 329 } 330 renderer.SetMixMode(MixReplace) 331 renderer.Draw("O", 32, 32) 332 333 ok := false 334 for i := 0; i < len(target.Pix); i += 4 { 335 alpha := target.Pix[i+3] 336 if alpha == 0 { 337 ok = true 338 } 339 if target.Pix[i+0] != alpha { 340 t.Fatalf("%d, %d, %d", i, alpha, target.Pix[i+0]) 341 } 342 if target.Pix[i+1] != alpha { 343 t.Fatalf("%d, %d, %d", i, alpha, target.Pix[i+1]) 344 } 345 if target.Pix[i+2] != alpha { 346 t.Fatalf("%d, %d, %d", i, alpha, target.Pix[i+2]) 347 } 348 } 349 if !ok { 350 t.Fatal("expected some transparent region, but didn't find it") 351 } 352 353 // mix cut mode 354 renderer.SetMixMode(MixCut) 355 renderer.Draw("O", 32, 32) 356 for i := 0; i < len(target.Pix); i += 4 { 357 alpha := target.Pix[i+3] 358 if alpha != 0 && alpha != 255 { 359 t.Fatalf("unexpected alpha %d at %d", alpha, i) 360 } 361 } 362 363 // sub mode 364 for i, _ := range target.Pix { 365 target.Pix[i] = 255 366 } 367 renderer.SetMixMode(MixSub) 368 renderer.SetColor(color.RGBA{255, 0, 255, 255}) 369 renderer.Draw("O", 32, 32) 370 ok = false 371 for i := 0; i < len(target.Pix); i += 4 { 372 if target.Pix[i+1] == 255 && target.Pix[i+3] == 255 && 373 target.Pix[i+0] == 0 && target.Pix[i+2] == 0 { 374 ok = true // pure green found 375 } 376 } 377 if !ok { 378 t.Fatal("failed to find green") 379 } 380 381 renderer.SetMixMode(MixMultiply) 382 renderer.SetColor(color.RGBA{0, 0, 0, 255}) 383 renderer.Draw("O", 32, 32) 384 for i := 0; i < len(target.Pix); i += 4 { 385 alpha := target.Pix[i+3] 386 if alpha != 255 { 387 t.Fatalf("unexpected alpha %d at %d", alpha, i) 388 } 389 if target.Pix[i+0] != target.Pix[i+2] || target.Pix[i+1] < target.Pix[i+2] { 390 t.Fatalf("bad color") 391 } 392 } 393 394 // add mode 395 for i := 0; i < len(target.Pix); i += 4 { 396 target.Pix[i+0] = 255 397 target.Pix[i+1] = 0 398 target.Pix[i+2] = 0 399 target.Pix[i+3] = 255 400 } 401 renderer.SetMixMode(MixAdd) 402 renderer.SetColor(color.RGBA{0, 0, 255, 255}) 403 renderer.Draw("O", 32, 32) 404 ok = false 405 for i := 0; i < len(target.Pix); i += 4 { 406 if target.Pix[i+1] != 0 { 407 t.Fatal("green must be 0") 408 } 409 if target.Pix[i+3] != 255 { 410 t.Fatal("alpha must be 255") 411 } 412 if target.Pix[i] == 255 && target.Pix[i+2] == 255 { 413 ok = true 414 } 415 } 416 if !ok { 417 t.Fatal("failed to find pure magenta") 418 } 419 420 // fifty-fifty mode 421 for i := 0; i < len(target.Pix); i += 4 { 422 target.Pix[i+0] = 255 423 target.Pix[i+1] = 0 424 target.Pix[i+2] = 0 425 target.Pix[i+3] = 255 426 } 427 renderer.SetMixMode(MixFiftyFifty) 428 renderer.SetColor(color.RGBA{255, 0, 255, 255}) 429 renderer.Draw("O", 32, 32) 430 for i := 0; i < len(target.Pix); i += 4 { 431 if target.Pix[i+1] != 0 { 432 t.Fatal("green must be 0") 433 } 434 if target.Pix[i+3] != 255 { 435 t.Fatal("alpha must be 255") 436 } 437 if target.Pix[i+0] != 255 { 438 t.Fatal("red must be 255") 439 } 440 if target.Pix[i+2] > 128 { 441 t.Fatalf("blue over 128 %d", target.Pix[i+2]) 442 } 443 } 444 } 445 446 func TestAlignBound(t *testing.T) { 447 renderer := NewStdRenderer() 448 renderer.SetFont(testFont) 449 renderer.SetAlign(YCenter, XCenter) 450 horzPadder := &esizer.HorzPaddingSizer{} 451 renderer.SetSizer(horzPadder) 452 453 for size := 7; size < 73; size += 3 { 454 renderer.SetSizePx(size) 455 prevWidth := fixed.Int26_6(0) 456 for i := 0; i < 64; i += 3 { 457 const text = "abcdefghijkl - mnopq0123456789" 458 horzPadder.SetHorzPaddingFract(fixed.Int26_6(i)) 459 460 renderer.SetQuantizerStep(64, 64) // full quantization 461 rect1 := renderer.SelectionRect(text) 462 renderer.SetQuantizerStep(1, 64) // vertical quantization 463 rect2 := renderer.SelectionRect(text) 464 renderer.SetQuantizerStep(1, 1) // no quantization 465 rect3 := renderer.SelectionRect(text) 466 467 if rect1.Height != rect2.Height { 468 t.Fatalf("SelectionRect.Height different for QuantizeFull and QuantizeVert (%d vs %d)", rect1.Height, rect2.Height) 469 } 470 if rect2.Width != rect3.Width { 471 t.Fatalf("SelectionRect.Width different for QuantizeVert and QuantizeNone (%d vs %d)", rect2.Width, rect3.Width) 472 } 473 if rect3.Width <= prevWidth { 474 t.Fatalf("SelectionRect.Width didn't increase") 475 } 476 prevWidth = rect3.Width 477 478 // if rect1.Width == rect2.Width { // uncommon but this can happen legitimately 479 // t.Fatalf("SelectionRect.Width uncommon match for QuantizeFull and QuantizeVert (%d vs %d)", rect1.Width, rect2.Width) 480 // } 481 } 482 } 483 } 484 485 func debugExport(name string, img image.Image) { 486 file, err := os.Create(name) 487 if err != nil { 488 log.Fatal(err) 489 } 490 err = png.Encode(file, img) 491 if err != nil { 492 log.Fatal(err) 493 } 494 err = file.Close() 495 if err != nil { 496 log.Fatal(err) 497 } 498 }