| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- package cover
- import (
- "bytes"
- "image"
- "image/color"
- "image/jpeg"
- "strings"
- "testing"
- "git.monoexp.com/bajdev/rekopub/internal/device"
- )
- func decode(t *testing.T, data []byte) *image.Gray {
- t.Helper()
- img, err := jpeg.Decode(bytes.NewReader(data))
- if err != nil {
- t.Fatal(err)
- }
- gray, ok := img.(*image.Gray)
- if !ok {
- t.Fatalf("cover decodes as %T, want a grayscale JPEG", img)
- }
- if b := gray.Bounds(); b.Dx() != 1404 || b.Dy() != 1872 {
- t.Fatalf("cover is %dx%d, want 1404x1872", b.Dx(), b.Dy())
- }
- return gray
- }
- // darkShare is the share of pixels in r darker than mid-gray.
- func darkShare(img *image.Gray, r image.Rectangle) float64 {
- dark, total := 0, 0
- for y := r.Min.Y; y < r.Max.Y; y++ {
- for x := r.Min.X; x < r.Max.X; x++ {
- if img.GrayAt(x, y).Y < 128 {
- dark++
- }
- total++
- }
- }
- return float64(dark) / float64(total)
- }
- func TestPlainCover(t *testing.T) {
- data, err := Render(Details{Title: "A Short Title", SiteName: "Example News", Date: "14 March 2025"}, nil, device.EInk)
- if err != nil {
- t.Fatal(err)
- }
- img := decode(t, data)
- if s := darkShare(img, image.Rect(0, 0, 1404, 150)); s < 0.8 {
- t.Errorf("top band is %.0f%% dark, want a black band", s*100)
- }
- if s := darkShare(img, image.Rect(100, 300, 1300, 700)); s < 0.05 {
- t.Errorf("title area is %.1f%% dark, want visible text", s*100)
- }
- if s := darkShare(img, image.Rect(0, 1700, 1404, 1872)); s > 0.01 {
- t.Errorf("bottom edge is %.1f%% dark, want it empty", s*100)
- }
- }
- func TestPhotoCover(t *testing.T) {
- // A wide black photo with a white stripe in the middle: cropping to the
- // cover's shape must keep the centre.
- photo := image.NewRGBA(image.Rect(0, 0, 3000, 1000))
- for y := 0; y < 1000; y++ {
- for x := 0; x < 3000; x++ {
- c := color.RGBA{0, 0, 0, 255}
- if x > 1400 && x < 1600 {
- c = color.RGBA{255, 255, 255, 255}
- }
- photo.Set(x, y, c)
- }
- }
- data, err := Render(Details{Title: "Photo Story", SiteName: "Example", Date: "1 May 2025"}, photo, device.EInk)
- if err != nil {
- t.Fatal(err)
- }
- img := decode(t, data)
- if v := img.GrayAt(702, 500).Y; v < 200 {
- t.Errorf("photo centre is %d, want the white stripe", v)
- }
- if v := img.GrayAt(100, 500).Y; v > 50 {
- t.Errorf("photo edge is %d, want black", v)
- }
- if s := darkShare(img, image.Rect(100, 1150, 1300, 1450)); s < 0.02 {
- t.Errorf("title area is %.1f%% dark, want visible text", s*100)
- }
- }
- func TestTransparentPhotoIsOnWhite(t *testing.T) {
- photo := image.NewNRGBA(image.Rect(0, 0, 800, 600)) // fully transparent
- data, err := Render(Details{Title: "T"}, photo, device.EInk)
- if err != nil {
- t.Fatal(err)
- }
- if v := decode(t, data).GrayAt(702, 500).Y; v < 240 {
- t.Errorf("transparent photo became %d, want white", v)
- }
- }
- func TestLongTitlesFit(t *testing.T) {
- long := strings.Repeat("Remarkably long headline words ", 20)
- for _, title := range []string{
- long,
- "Supercalifragilisticexpialidociousandthensomemorelettersontop",
- "",
- "日本語のタイトル",
- } {
- for _, photo := range []image.Image{nil, image.NewGray(image.Rect(0, 0, 600, 400))} {
- if _, err := Render(Details{Title: title, SiteName: strings.Repeat("Site ", 30)}, photo, device.EInk); err != nil {
- t.Errorf("Render(%q): %v", title, err)
- }
- }
- }
- }
- func TestFitTitle(t *testing.T) {
- // Short titles get the largest size; long ones shrink, and lines never
- // exceed the width or the box.
- face, lines := fitTitle("Hi", 1200, 800, 140, 64)
- if face.Metrics().Height.Ceil() < 140 || len(lines) != 1 {
- t.Errorf("short title: height %d, lines %q", face.Metrics().Height.Ceil(), lines)
- }
- title := strings.Repeat("word ", 200)
- face, lines = fitTitle(title, 1200, 800, 140, 64)
- if len(lines)*int(float64(face.Metrics().Height.Ceil())*1.0) > 800*1.2 {
- t.Errorf("long title overflows: %d lines", len(lines))
- }
- if !strings.HasSuffix(lines[len(lines)-1], "…") {
- t.Errorf("truncated title should end with an ellipsis: %q", lines[len(lines)-1])
- }
- if fitted, _ := fitTitle("", 1200, 800, 140, 64); fitted == nil {
- t.Error("empty title should still render")
- }
- }
- func TestSubtitle(t *testing.T) {
- tests := map[Details]string{
- {SiteName: "Site", Date: "1 May 2025"}: "Site · 1 May 2025",
- {SiteName: "Site"}: "Site",
- {Date: "1 May 2025"}: "1 May 2025",
- {}: "",
- }
- for d, want := range tests {
- if got := subtitle(d); got != want {
- t.Errorf("subtitle(%+v) = %q, want %q", d, got, want)
- }
- }
- }
|