1
0

cover_test.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. package cover
  2. import (
  3. "bytes"
  4. "image"
  5. "image/color"
  6. "image/jpeg"
  7. "strings"
  8. "testing"
  9. "git.monoexp.com/bajdev/rekopub/internal/device"
  10. )
  11. func decode(t *testing.T, data []byte) *image.Gray {
  12. t.Helper()
  13. img, err := jpeg.Decode(bytes.NewReader(data))
  14. if err != nil {
  15. t.Fatal(err)
  16. }
  17. gray, ok := img.(*image.Gray)
  18. if !ok {
  19. t.Fatalf("cover decodes as %T, want a grayscale JPEG", img)
  20. }
  21. if b := gray.Bounds(); b.Dx() != 1404 || b.Dy() != 1872 {
  22. t.Fatalf("cover is %dx%d, want 1404x1872", b.Dx(), b.Dy())
  23. }
  24. return gray
  25. }
  26. // darkShare is the share of pixels in r darker than mid-gray.
  27. func darkShare(img *image.Gray, r image.Rectangle) float64 {
  28. dark, total := 0, 0
  29. for y := r.Min.Y; y < r.Max.Y; y++ {
  30. for x := r.Min.X; x < r.Max.X; x++ {
  31. if img.GrayAt(x, y).Y < 128 {
  32. dark++
  33. }
  34. total++
  35. }
  36. }
  37. return float64(dark) / float64(total)
  38. }
  39. func TestPlainCover(t *testing.T) {
  40. data, err := Render(Details{Title: "A Short Title", SiteName: "Example News", Date: "14 March 2025"}, nil, device.EInk)
  41. if err != nil {
  42. t.Fatal(err)
  43. }
  44. img := decode(t, data)
  45. if s := darkShare(img, image.Rect(0, 0, 1404, 150)); s < 0.8 {
  46. t.Errorf("top band is %.0f%% dark, want a black band", s*100)
  47. }
  48. if s := darkShare(img, image.Rect(100, 300, 1300, 700)); s < 0.05 {
  49. t.Errorf("title area is %.1f%% dark, want visible text", s*100)
  50. }
  51. if s := darkShare(img, image.Rect(0, 1700, 1404, 1872)); s > 0.01 {
  52. t.Errorf("bottom edge is %.1f%% dark, want it empty", s*100)
  53. }
  54. }
  55. func TestPhotoCover(t *testing.T) {
  56. // A wide black photo with a white stripe in the middle: cropping to the
  57. // cover's shape must keep the centre.
  58. photo := image.NewRGBA(image.Rect(0, 0, 3000, 1000))
  59. for y := 0; y < 1000; y++ {
  60. for x := 0; x < 3000; x++ {
  61. c := color.RGBA{0, 0, 0, 255}
  62. if x > 1400 && x < 1600 {
  63. c = color.RGBA{255, 255, 255, 255}
  64. }
  65. photo.Set(x, y, c)
  66. }
  67. }
  68. data, err := Render(Details{Title: "Photo Story", SiteName: "Example", Date: "1 May 2025"}, photo, device.EInk)
  69. if err != nil {
  70. t.Fatal(err)
  71. }
  72. img := decode(t, data)
  73. if v := img.GrayAt(702, 500).Y; v < 200 {
  74. t.Errorf("photo centre is %d, want the white stripe", v)
  75. }
  76. if v := img.GrayAt(100, 500).Y; v > 50 {
  77. t.Errorf("photo edge is %d, want black", v)
  78. }
  79. if s := darkShare(img, image.Rect(100, 1150, 1300, 1450)); s < 0.02 {
  80. t.Errorf("title area is %.1f%% dark, want visible text", s*100)
  81. }
  82. }
  83. func TestTransparentPhotoIsOnWhite(t *testing.T) {
  84. photo := image.NewNRGBA(image.Rect(0, 0, 800, 600)) // fully transparent
  85. data, err := Render(Details{Title: "T"}, photo, device.EInk)
  86. if err != nil {
  87. t.Fatal(err)
  88. }
  89. if v := decode(t, data).GrayAt(702, 500).Y; v < 240 {
  90. t.Errorf("transparent photo became %d, want white", v)
  91. }
  92. }
  93. func TestLongTitlesFit(t *testing.T) {
  94. long := strings.Repeat("Remarkably long headline words ", 20)
  95. for _, title := range []string{
  96. long,
  97. "Supercalifragilisticexpialidociousandthensomemorelettersontop",
  98. "",
  99. "日本語のタイトル",
  100. } {
  101. for _, photo := range []image.Image{nil, image.NewGray(image.Rect(0, 0, 600, 400))} {
  102. if _, err := Render(Details{Title: title, SiteName: strings.Repeat("Site ", 30)}, photo, device.EInk); err != nil {
  103. t.Errorf("Render(%q): %v", title, err)
  104. }
  105. }
  106. }
  107. }
  108. func TestFitTitle(t *testing.T) {
  109. // Short titles get the largest size; long ones shrink, and lines never
  110. // exceed the width or the box.
  111. face, lines := fitTitle("Hi", 1200, 800, 140, 64)
  112. if face.Metrics().Height.Ceil() < 140 || len(lines) != 1 {
  113. t.Errorf("short title: height %d, lines %q", face.Metrics().Height.Ceil(), lines)
  114. }
  115. title := strings.Repeat("word ", 200)
  116. face, lines = fitTitle(title, 1200, 800, 140, 64)
  117. if len(lines)*int(float64(face.Metrics().Height.Ceil())*1.0) > 800*1.2 {
  118. t.Errorf("long title overflows: %d lines", len(lines))
  119. }
  120. if !strings.HasSuffix(lines[len(lines)-1], "…") {
  121. t.Errorf("truncated title should end with an ellipsis: %q", lines[len(lines)-1])
  122. }
  123. if fitted, _ := fitTitle("", 1200, 800, 140, 64); fitted == nil {
  124. t.Error("empty title should still render")
  125. }
  126. }
  127. func TestSubtitle(t *testing.T) {
  128. tests := map[Details]string{
  129. {SiteName: "Site", Date: "1 May 2025"}: "Site · 1 May 2025",
  130. {SiteName: "Site"}: "Site",
  131. {Date: "1 May 2025"}: "1 May 2025",
  132. {}: "",
  133. }
  134. for d, want := range tests {
  135. if got := subtitle(d); got != want {
  136. t.Errorf("subtitle(%+v) = %q, want %q", d, got, want)
  137. }
  138. }
  139. }