aboutsummaryrefslogtreecommitdiff
path: root/tests/test_cover.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_cover.py')
-rw-r--r--tests/test_cover.py189
1 files changed, 0 insertions, 189 deletions
diff --git a/tests/test_cover.py b/tests/test_cover.py
deleted file mode 100644
index f19db5a..0000000
--- a/tests/test_cover.py
+++ /dev/null
@@ -1,189 +0,0 @@
-"""Tests for stdlib-only cover generation: PNG structure, gradient, text."""
-
-import random
-import struct
-import tempfile
-import unittest
-import zlib
-from pathlib import Path
-
-from converter.cover import (
- _random_light_color,
- _text_width,
- _wrap_title,
- generate_cover,
-)
-
-
-def _decode_png(data: bytes):
- """Parse a PNG into (width, height, rows of RGB tuples)."""
- assert data[:8] == b"\x89PNG\r\n\x1a\n", "bad PNG signature"
- pos = 8
- idat = b""
- width = height = None
- while pos < len(data):
- length, chunk_type = struct.unpack(">I4s", data[pos:pos + 8])
- chunk_data = data[pos + 8:pos + 8 + length]
- crc = struct.unpack(">I", data[pos + 8 + length:pos + 12 + length])[0]
- assert crc == zlib.crc32(chunk_type + chunk_data) & 0xFFFFFFFF, "bad CRC"
- if chunk_type == b"IHDR":
- width, height, depth, color_type = struct.unpack(">IIBB", chunk_data[:10])
- assert depth == 8 and color_type == 2 # 8-bit RGB
- elif chunk_type == b"IDAT":
- idat += chunk_data
- pos += 12 + length
- raw = zlib.decompress(idat)
- stride = 1 + width * 3
- assert len(raw) == height * stride, "unexpected decompressed size"
- rows = []
- for y in range(height):
- row = raw[y * stride + 1:(y + 1) * stride]
- rows.append([tuple(row[x * 3:x * 3 + 3]) for x in range(width)])
- return width, height, rows
-
-
-def _black_pixels(rows):
- return sum(1 for row in rows for pixel in row if pixel == (0, 0, 0))
-
-
-class GenerateCoverTests(unittest.TestCase):
- def _write(self, title, width=120, height=180, seed=7):
- with tempfile.TemporaryDirectory() as tmp:
- path = Path(tmp) / "cover.png"
- result = generate_cover(title, path, width=width, height=height, seed=seed)
- data = path.read_bytes()
- return result, data
-
- def test_valid_png_with_requested_dimensions(self):
- _, data = self._write("Hello")
- width, height, rows = _decode_png(data)
- self.assertEqual((width, height), (120, 180))
- self.assertEqual(len(rows), 180)
-
- def test_gradient_matches_seeded_colors(self):
- _, data = self._write("Hello", seed=42)
- width, height, rows = _decode_png(data)
- rng = random.Random(42)
- top = _random_light_color(rng)
- bottom = _random_light_color(rng)
- # Corners of the text-free top/bottom rows match the endpoints
- self.assertEqual(rows[0][0], top)
- self.assertEqual(rows[0][-1], top)
- self.assertEqual(rows[height - 1][0], bottom)
- self.assertEqual(rows[height - 1][-1], bottom)
-
- def test_gradient_colors_are_light(self):
- # Text-free bottom row: every channel must stay in pastel territory
- _, data = self._write("Hello", seed=1)
- _, height, rows = _decode_png(data)
- for channel in rows[height - 1][0]:
- self.assertGreaterEqual(channel, 90)
-
- def test_title_renders_black_pixels(self):
- _, data = self._write("Hello")
- _, _, rows = _decode_png(data)
- self.assertGreater(_black_pixels(rows), 50)
-
- def test_title_renders_white_pixels(self):
- _, data = self._write("Hello")
- _, _, rows = _decode_png(data)
- white = sum(1 for row in rows for pixel in row if pixel == (255, 255, 255))
- self.assertGreater(white, 50)
-
- def test_white_text_sits_on_black_stroke(self):
- # Directly above a white pixel row there must be a black stroke row:
- # sample white pixels and confirm black neighbors within stroke width.
- _, data = self._write("Hi", width=200, height=100, seed=3)
- _, _, rows = _decode_png(data)
- whites = [(x, y) for y, row in enumerate(rows)
- for x, pixel in enumerate(row) if pixel == (255, 255, 255)]
- self.assertTrue(whites)
- checked = near_stroke = 0
- for x, y in whites[::5]:
- neighborhood = []
- for dy in range(-3, 4):
- for dx in range(-3, 4):
- if 0 <= y + dy < len(rows) and 0 <= x + dx < len(rows[0]):
- neighborhood.append(rows[y + dy][x + dx])
- checked += 1
- if (0, 0, 0) in neighborhood:
- near_stroke += 1
- # Interior white pixels are surrounded by white; every sampled pixel
- # should still see stroke black within 3px (font strokes are 5-6 px thick)
- self.assertEqual(near_stroke, checked)
-
- def test_empty_title_renders_gradient_only(self):
- _, data = self._write("")
- _, _, rows = _decode_png(data)
- self.assertEqual(_black_pixels(rows), 0)
-
- def test_unrenderable_title_degrades_to_gradient(self):
- # CJK glyphs are not in the bitmap font; no crash, no text pixels
- _, data = self._write("书名")
- _, _, rows = _decode_png(data)
- self.assertEqual(_black_pixels(rows), 0)
-
- def test_write_failure_returns_none(self):
- result = generate_cover("Hello", Path("/nonexistent_dir/cover.png"))
- self.assertIsNone(result)
-
-
-class WrapTitleTests(unittest.TestCase):
- def test_short_title_one_line(self):
- self.assertEqual(len(_wrap_title("Dune", 500)), 1)
-
- def test_long_title_wraps(self):
- lines = _wrap_title("The Extremely Long Windy Title of a Very Long Book", 600)
- self.assertGreater(len(lines), 1)
- for line in lines:
- self.assertLessEqual(_text_width(line), 600)
-
- def test_single_long_word_kept_intact(self):
- lines = _wrap_title("Antidisestablishmentarianism", 10)
- self.assertEqual(lines, ["Antidisestablishmentarianism"])
-
- def test_empty_title_no_lines(self):
- self.assertEqual(_wrap_title("", 500), [])
-
-
-class TextWidthTests(unittest.TestCase):
- def test_empty(self):
- self.assertEqual(_text_width(""), 0)
-
- def test_single_char_is_scaled_glyph(self):
- self.assertEqual(_text_width("A"), 30) # 5 px * scale 6
-
- def test_chars_include_spacing(self):
- self.assertEqual(_text_width("AB"), 66) # (2 glyphs * 6 - 1) * 6
-
-
-class DropShadowTests(unittest.TestCase):
- def _cover_rows(self, title, seed=7):
- with tempfile.TemporaryDirectory() as tmp:
- path = Path(tmp) / "cover.png"
- generate_cover(title, path, width=200, height=200, seed=seed)
- return _decode_png(path.read_bytes())[2]
-
- def test_shadow_pixels_survive_next_to_text(self):
- rows = self._cover_rows("Hi")
- # The shadow lives down-right of the glyphs: there must be darkened
- # (but not pure black, not full-brightness) pixels beyond the text
- # block's bottom edge.
- blacks = {(x, y) for y, row in enumerate(rows)
- for x, pixel in enumerate(row) if pixel == (0, 0, 0)}
- self.assertTrue(blacks, "no text rendered")
- text_bottom = max(y for _, y in blacks)
- darkened = [pixel for y, row in enumerate(rows)
- if y > text_bottom for pixel in row
- if pixel != (0, 0, 0) and max(pixel) < 130]
- self.assertTrue(darkened, "no shadow pixels below the text")
-
- def test_empty_title_has_no_shadow(self):
- rows = self._cover_rows("")
- for row in rows:
- for pixel in row:
- self.assertNotEqual(pixel, (0, 0, 0))
-
-
-if __name__ == "__main__":
- unittest.main()