github.com/kintar/etxt@v0.0.9/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  }