text_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. // Copyright 2023 The Ebitengine Authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package text_test
  15. import (
  16. "bytes"
  17. "image"
  18. "image/color"
  19. "math"
  20. "os"
  21. "path/filepath"
  22. "regexp"
  23. "strings"
  24. "testing"
  25. "github.com/hajimehoshi/bitmapfont/v3"
  26. "golang.org/x/image/font"
  27. "golang.org/x/image/font/opentype"
  28. "golang.org/x/image/math/fixed"
  29. "github.com/hajimehoshi/ebiten/v2"
  30. t "github.com/hajimehoshi/ebiten/v2/internal/testing"
  31. "github.com/hajimehoshi/ebiten/v2/text/v2"
  32. )
  33. func TestMain(m *testing.M) {
  34. t.MainWithRunLoop(m)
  35. }
  36. func TestGlyphIndex(t *testing.T) {
  37. const sampleText = `The quick brown fox jumps
  38. over the lazy dog.`
  39. f := text.NewGoXFace(bitmapfont.Face)
  40. got := sampleText
  41. for _, g := range text.AppendGlyphs(nil, sampleText, f, nil) {
  42. got = got[:g.StartIndexInBytes] + strings.Repeat(" ", g.EndIndexInBytes-g.StartIndexInBytes) + got[g.EndIndexInBytes:]
  43. }
  44. want := regexp.MustCompile(`\S`).ReplaceAllString(sampleText, " ")
  45. if got != want {
  46. t.Errorf("got: %q, want: %q", got, want)
  47. }
  48. }
  49. func TestTextColor(t *testing.T) {
  50. clr := color.RGBA{R: 0x80, G: 0x80, B: 0x80, A: 0x80}
  51. img := ebiten.NewImage(30, 30)
  52. op := &text.DrawOptions{}
  53. op.GeoM.Translate(0, 0)
  54. op.ColorScale.ScaleWithColor(clr)
  55. text.Draw(img, "Hello", text.NewGoXFace(bitmapfont.Face), op)
  56. w, h := img.Bounds().Dx(), img.Bounds().Dy()
  57. allTransparent := true
  58. for j := 0; j < h; j++ {
  59. for i := 0; i < w; i++ {
  60. got := img.At(i, j)
  61. want1 := color.RGBA{R: 0x80, G: 0x80, B: 0x80, A: 0x80}
  62. want2 := color.RGBA{}
  63. if got != want1 && got != want2 {
  64. t.Errorf("img At(%d, %d): got %v; want %v or %v", i, j, got, want1, want2)
  65. }
  66. if got == want1 {
  67. allTransparent = false
  68. }
  69. }
  70. }
  71. if allTransparent {
  72. t.Fail()
  73. }
  74. }
  75. const testGoXFaceSize = 6
  76. type testGoXFace struct{}
  77. func (f *testGoXFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
  78. dr = image.Rect(0, 0, testGoXFaceSize, testGoXFaceSize)
  79. a := image.NewAlpha(dr)
  80. switch r {
  81. case 'a':
  82. for j := dr.Min.Y; j < dr.Max.Y; j++ {
  83. for i := dr.Min.X; i < dr.Max.X; i++ {
  84. a.SetAlpha(i, j, color.Alpha{A: 0x80})
  85. }
  86. }
  87. case 'b':
  88. for j := dr.Min.Y; j < dr.Max.Y; j++ {
  89. for i := dr.Min.X; i < dr.Max.X; i++ {
  90. a.SetAlpha(i, j, color.Alpha{A: 0xff})
  91. }
  92. }
  93. }
  94. mask = a
  95. advance = fixed.I(testGoXFaceSize)
  96. ok = true
  97. return
  98. }
  99. func (f *testGoXFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
  100. bounds = fixed.R(0, 0, testGoXFaceSize, testGoXFaceSize)
  101. advance = fixed.I(testGoXFaceSize)
  102. ok = true
  103. return
  104. }
  105. func (f *testGoXFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
  106. return fixed.I(testGoXFaceSize), true
  107. }
  108. func (f *testGoXFace) Kern(r0, r1 rune) fixed.Int26_6 {
  109. if r1 == 'b' {
  110. return fixed.I(-testGoXFaceSize)
  111. }
  112. return 0
  113. }
  114. func (f *testGoXFace) Close() error {
  115. return nil
  116. }
  117. func (f *testGoXFace) Metrics() font.Metrics {
  118. return font.Metrics{
  119. Height: fixed.I(testGoXFaceSize),
  120. Ascent: 0,
  121. Descent: fixed.I(testGoXFaceSize),
  122. XHeight: 0,
  123. CapHeight: fixed.I(testGoXFaceSize),
  124. CaretSlope: image.Pt(0, 1),
  125. }
  126. }
  127. // Issue #1378
  128. func TestNegativeKern(t *testing.T) {
  129. f := text.NewGoXFace(&testGoXFace{})
  130. dst := ebiten.NewImage(testGoXFaceSize*2, testGoXFaceSize)
  131. // With testGoXFace, 'b' is rendered at the previous position as 0xff.
  132. // 'a' is rendered at the current position as 0x80.
  133. op := &text.DrawOptions{}
  134. op.GeoM.Translate(0, 0)
  135. text.Draw(dst, "ab", f, op)
  136. for j := 0; j < testGoXFaceSize; j++ {
  137. for i := 0; i < testGoXFaceSize; i++ {
  138. got := dst.At(i, j)
  139. want := color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
  140. if got != want {
  141. t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want)
  142. }
  143. }
  144. }
  145. // The glyph 'a' should be treated correctly.
  146. op = &text.DrawOptions{}
  147. op.GeoM.Translate(testGoXFaceSize, 0)
  148. text.Draw(dst, "a", f, op)
  149. for j := 0; j < testGoXFaceSize; j++ {
  150. for i := testGoXFaceSize; i < testGoXFaceSize*2; i++ {
  151. got := dst.At(i, j)
  152. want := color.RGBA{R: 0x80, G: 0x80, B: 0x80, A: 0x80}
  153. if got != want {
  154. t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want)
  155. }
  156. }
  157. }
  158. }
  159. type unhashableGoXFace func()
  160. const unhashableGoXFaceSize = 10
  161. func (u *unhashableGoXFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
  162. dr = image.Rect(0, 0, unhashableGoXFaceSize, unhashableGoXFaceSize)
  163. a := image.NewAlpha(dr)
  164. for j := dr.Min.Y; j < dr.Max.Y; j++ {
  165. for i := dr.Min.X; i < dr.Max.X; i++ {
  166. a.SetAlpha(i, j, color.Alpha{A: 0xff})
  167. }
  168. }
  169. mask = a
  170. advance = fixed.I(unhashableGoXFaceSize)
  171. ok = true
  172. return
  173. }
  174. func (u *unhashableGoXFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
  175. bounds = fixed.R(0, 0, unhashableGoXFaceSize, unhashableGoXFaceSize)
  176. advance = fixed.I(unhashableGoXFaceSize)
  177. ok = true
  178. return
  179. }
  180. func (u *unhashableGoXFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
  181. return fixed.I(unhashableGoXFaceSize), true
  182. }
  183. func (u *unhashableGoXFace) Kern(r0, r1 rune) fixed.Int26_6 {
  184. return 0
  185. }
  186. func (u *unhashableGoXFace) Close() error {
  187. return nil
  188. }
  189. func (u *unhashableGoXFace) Metrics() font.Metrics {
  190. return font.Metrics{
  191. Height: fixed.I(unhashableGoXFaceSize),
  192. Ascent: 0,
  193. Descent: fixed.I(unhashableGoXFaceSize),
  194. XHeight: 0,
  195. CapHeight: fixed.I(unhashableGoXFaceSize),
  196. CaretSlope: image.Pt(0, 1),
  197. }
  198. }
  199. // Issue #2669
  200. func TestUnhashableFace(t *testing.T) {
  201. var face unhashableGoXFace
  202. f := text.NewGoXFace(&face)
  203. dst := ebiten.NewImage(unhashableGoXFaceSize*2, unhashableGoXFaceSize*2)
  204. text.Draw(dst, "a", f, nil)
  205. for j := 0; j < unhashableGoXFaceSize*2; j++ {
  206. for i := 0; i < unhashableGoXFaceSize*2; i++ {
  207. got := dst.At(i, j)
  208. var want color.RGBA
  209. if i < unhashableGoXFaceSize && j < unhashableGoXFaceSize {
  210. want = color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
  211. }
  212. if got != want {
  213. t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want)
  214. }
  215. }
  216. }
  217. }
  218. func TestConvertToFixed26_6(t *testing.T) {
  219. testCases := []struct {
  220. In float64
  221. Out fixed.Int26_6
  222. }{
  223. {
  224. In: 0,
  225. Out: 0,
  226. },
  227. {
  228. In: 0.25,
  229. Out: fixed.I(1) / 4,
  230. },
  231. {
  232. In: 0.5,
  233. Out: fixed.I(1) / 2,
  234. },
  235. {
  236. In: 1.25,
  237. Out: fixed.I(1) * 5 / 4,
  238. },
  239. {
  240. In: 1,
  241. Out: fixed.I(1),
  242. },
  243. {
  244. In: -0.25,
  245. Out: fixed.I(-1) / 4,
  246. },
  247. {
  248. In: -0.5,
  249. Out: fixed.I(-1) / 2,
  250. },
  251. {
  252. In: -1,
  253. Out: fixed.I(-1),
  254. },
  255. {
  256. In: -1.25,
  257. Out: fixed.I(-1) * 5 / 4,
  258. },
  259. }
  260. for _, tc := range testCases {
  261. got := text.Float32ToFixed26_6(float32(tc.In))
  262. want := tc.Out
  263. if got != want {
  264. t.Errorf("Float32ToFixed26_6(%v): got: %v, want: %v", tc.In, got, want)
  265. }
  266. got = text.Float64ToFixed26_6(tc.In)
  267. want = tc.Out
  268. if got != want {
  269. t.Errorf("Float32ToFixed26_6(%v): got: %v, want: %v", tc.In, got, want)
  270. }
  271. }
  272. }
  273. func TestConvertToFloat(t *testing.T) {
  274. testCases := []struct {
  275. In fixed.Int26_6
  276. Out float64
  277. }{
  278. {
  279. In: 0,
  280. Out: 0,
  281. },
  282. {
  283. In: fixed.I(1) / 4,
  284. Out: 0.25,
  285. },
  286. {
  287. In: fixed.I(1) / 2,
  288. Out: 0.5,
  289. },
  290. {
  291. In: fixed.I(1) * 5 / 4,
  292. Out: 1.25,
  293. },
  294. {
  295. In: fixed.I(1),
  296. Out: 1,
  297. },
  298. {
  299. In: fixed.I(-1) / 4,
  300. Out: -0.25,
  301. },
  302. {
  303. In: fixed.I(-1) / 2,
  304. Out: -0.5,
  305. },
  306. {
  307. In: fixed.I(-1),
  308. Out: -1,
  309. },
  310. {
  311. In: fixed.I(-1) * 5 / 4,
  312. Out: -1.25,
  313. },
  314. }
  315. for _, tc := range testCases {
  316. got := text.Fixed26_6ToFloat32(tc.In)
  317. want := float32(tc.Out)
  318. if got != want {
  319. t.Errorf("Fixed26_6ToFloat32(%v): got: %v, want: %v", tc.In, got, want)
  320. }
  321. got64 := text.Fixed26_6ToFloat64(tc.In)
  322. want64 := tc.Out
  323. if got64 != want64 {
  324. t.Errorf("Fixed26_6ToFloat64(%v): got: %v, want: %v", tc.In, got64, want64)
  325. }
  326. }
  327. }
  328. // Issue #2954
  329. func TestDrawOptionsNotModified(t *testing.T) {
  330. img := ebiten.NewImage(30, 30)
  331. op := &text.DrawOptions{}
  332. text.Draw(img, "Hello", text.NewGoXFace(bitmapfont.Face), op)
  333. if got, want := op.GeoM, (ebiten.GeoM{}); got != want {
  334. t.Errorf("got: %v, want: %v", got, want)
  335. }
  336. if got, want := op.ColorScale, (ebiten.ColorScale{}); got != want {
  337. t.Errorf("got: %v, want: %v", got, want)
  338. }
  339. }
  340. func TestGoXFaceMetrics(t *testing.T) {
  341. const size = 100
  342. fontFiles := []string{
  343. // MPLUS1p-Regular.ttf is an old version of M+ 1p font, and this doesn't have metadata.
  344. "MPLUS1p-Regular.ttf",
  345. "Roboto-Regular.ttf",
  346. }
  347. for _, fontFile := range fontFiles {
  348. fontFile := fontFile
  349. t.Run(fontFile, func(t *testing.T) {
  350. fontdata, err := os.ReadFile(filepath.Join("testdata", fontFile))
  351. if err != nil {
  352. t.Fatal(err)
  353. }
  354. sfntFont, err := opentype.Parse(fontdata)
  355. if err != nil {
  356. t.Fatal(err)
  357. }
  358. opentypeFace, err := opentype.NewFace(sfntFont, &opentype.FaceOptions{
  359. Size: size,
  360. DPI: 72,
  361. })
  362. if err != nil {
  363. t.Fatal(err)
  364. }
  365. goXFace := text.NewGoXFace(opentypeFace)
  366. goXMetrics := goXFace.Metrics()
  367. if goXMetrics.XHeight <= 0 {
  368. t.Errorf("GoXFace's XHeight must be positive but not: %f", goXMetrics.XHeight)
  369. }
  370. if goXMetrics.CapHeight <= 0 {
  371. t.Errorf("GoXFace's CapHeight must be positive but not: %f", goXMetrics.CapHeight)
  372. }
  373. goTextFaceSource, err := text.NewGoTextFaceSource(bytes.NewBuffer(fontdata))
  374. if err != nil {
  375. t.Fatal(err)
  376. }
  377. goTextFace := &text.GoTextFace{
  378. Source: goTextFaceSource,
  379. Size: size,
  380. }
  381. goTextMetrics := goTextFace.Metrics()
  382. if goTextMetrics.XHeight <= 0 {
  383. t.Errorf("GoTextFace's XHeight must be positive but not: %f", goTextMetrics.XHeight)
  384. }
  385. if goTextMetrics.CapHeight <= 0 {
  386. t.Errorf("GoTextFace's CapHeight must be positive but not: %f", goTextMetrics.CapHeight)
  387. }
  388. if math.Abs(goXMetrics.XHeight-goTextMetrics.XHeight) >= 0.1 {
  389. t.Errorf("XHeight values don't match: %f (GoXFace) vs %f (GoTextFace)", goXMetrics.XHeight, goTextMetrics.XHeight)
  390. }
  391. if math.Abs(goXMetrics.CapHeight-goTextMetrics.CapHeight) >= 0.1 {
  392. t.Errorf("CapHeight values don't match: %f (GoXFace) vs %f (GoTextFace)", goXMetrics.CapHeight, goTextMetrics.CapHeight)
  393. }
  394. // Check that a MultiFace should have the same metrics.
  395. multiFace, err := text.NewMultiFace(goTextFace)
  396. if err != nil {
  397. t.Fatal(err)
  398. }
  399. if got := multiFace.Metrics(); got != goTextMetrics {
  400. t.Errorf("got: %v, want: %v", got, goTextMetrics)
  401. }
  402. })
  403. }
  404. }