"""图像管线测试 — 算法参数证据:analysis/web/timedjs.js(docs/protocol.md §5)。""" import pytest from PIL import Image from ppclock import image_pipeline as IP def make_img(w, h, pixels): """pixels: list[(r,g,b)] 长度 w*h,行主序。""" img = Image.new("RGB", (w, h)) img.putdata(pixels) return img class TestPack: def test_all_white_bw_plane(self): px = [(255, 255, 255)] * 16 assert IP.pack_plane(px, "bw") == bytes([0xFF, 0xFF]) def test_all_black_bw_plane(self): px = [(0, 0, 0)] * 16 assert IP.pack_plane(px, "bw") == bytes(2) def test_msb_first_order(self): """首像素=bit7:仅(0,0)白→0x80;仅第8像素白→0x01;第9像素→第二字节0x80""" px = [(255, 255, 255)] + [(0, 0, 0)] * 15 assert IP.pack_plane(px, "bw") == bytes([0x80, 0x00]) px = [(0, 0, 0)] * 7 + [(255, 255, 255)] + [(0, 0, 0)] * 8 assert IP.pack_plane(px, "bw") == bytes([0x01, 0x00]) px = [(0, 0, 0)] * 8 + [(255, 255, 255)] + [(0, 0, 0)] * 7 assert IP.pack_plane(px, "bw") == bytes([0x00, 0x80]) def test_red_pixel_planes(self): """红(255,0,0):bw 平面=0,红平面=1(timedjs.js:573-576,599-601)""" px = [(255, 0, 0)] * 8 assert IP.pack_plane(px, "bw") == bytes([0x00]) assert IP.pack_plane(px, "red") == bytes([0xFF]) def test_partial_channels_count_as_white(self): """bw 规则:R,G,B 全>0 → 1;(1,1,1) 也是白""" px = [(1, 1, 1)] * 8 assert IP.pack_plane(px, "bw") == bytes([0xFF]) def test_length_not_multiple_of_8_raises(self): with pytest.raises(ValueError): IP.pack_plane([(0, 0, 0)] * 7, "bw") class TestDither: def test_none_threshold(self): """none:grayg*1.5 且 r>b*1.5 且 r>threshold → 纯红且不扩散误差""" px = [(200, 50, 50)] * 8 out = IP.dither_pixels(px, 8, 1, "none", threshold=125, tricolor=True) assert out == [(255, 0, 0)] * 8 def test_not_red_when_threshold_high(self): px = [(200, 50, 50)] * 8 out = IP.dither_pixels(px, 8, 1, "none", threshold=250, tricolor=True) # 200>250 假 → 走红判定失败 → 灰度 0.299*200+0.587*50+0.114*50=94.9 <250 → 黑 assert out == [(0, 0, 0)] * 8 def test_floyd_steinberg_error_propagation(self): """2x1 灰100:px0→黑 err=100*7/16=43.75 → px1=143.75→白(阈值125,扩散1.0)""" px = [(100, 100, 100)] * 2 out = IP.dither_pixels(px, 2, 1, "floydsteinberg", threshold=125) assert out[0] == (0, 0, 0) assert out[1] == (255, 255, 255) def test_diffusion_zero_behaves_like_none(self): px = [(100, 100, 100)] * 2 out = IP.dither_pixels(px, 2, 1, "floydsteinberg", threshold=125, diffusion=0.0) assert out == [(0, 0, 0)] * 2 def test_atkinson_kernel_smaller_error(self): """Atkinson 只扩散 6/8:px0 黑 err=100*1/8=12.5 → px1=112.5<125 仍黑""" px = [(100, 100, 100)] * 2 out = IP.dither_pixels(px, 2, 1, "atkinson", threshold=125) assert out == [(0, 0, 0), (0, 0, 0)] def test_bayer_matrix_origin(self): """bayer (0,0) 阈值=15:灰10→黑,灰20→白(矩阵 timedjs.js:116-136)""" out = IP.dither_pixels([(10, 10, 10)] * 8, 8, 1, "bayer", threshold=125) assert out[0] == (0, 0, 0) out = IP.dither_pixels([(20, 20, 20)] * 8, 8, 1, "bayer", threshold=125) assert out[0] == (255, 255, 255) def test_stucki_and_jarvis_run(self): """大核算法冒烟:全灰输入输出只有黑白""" px = [(128, 128, 128)] * 64 for algo in ("stucki", "jarvis"): out = IP.dither_pixels(px, 8, 8, algo, threshold=125) assert set(out) <= {(0, 0, 0), (255, 255, 255)} def test_unknown_algo_raises(self): with pytest.raises(ValueError): IP.dither_pixels([(0, 0, 0)] * 8, 8, 1, "magic") class TestAdjust: def test_brightness(self): """亮度+50 → v+=127.5(timedjs.js:9-17)""" px = [(100, 100, 100)] out = IP.adjust_pixels(px, brightness=50, contrast=0, saturation=100) assert out == [(228, 228, 228)] # 100+127.5=227.5 → round=228 def test_contrast(self): """对比度+100 → factor=2, v=v*2-128(timedjs.js:18-27)""" out = IP.adjust_pixels([(200, 50, 128)], brightness=0, contrast=100, saturation=100) assert out == [(255, 0, 128)] def test_saturation_desaturate(self): """饱和度35 → v=gray+(v-gray)*0.35(timedjs.js:28-38)""" out = IP.adjust_pixels([(200, 50, 50)], brightness=0, contrast=0, saturation=35) gray = 0.299 * 200 + 0.587 * 50 + 0.114 * 50 exp = round(gray + (200 - gray) * 0.35) assert out[0][0] == exp class TestProcess: def test_full_size_output_lengths(self): """400x300 → bw 15000B;三色另有红平面 15000B""" img = Image.new("RGB", (400, 300), (255, 255, 255)) bw, red = IP.process_image(img, size=(400, 300), algorithm="none", tricolor=True) assert len(bw) == 15000 assert len(red) == 15000 assert bw == bytes([0xFF]) * 15000 assert red == bytes(15000) def test_mono_has_no_red_plane(self): img = Image.new("RGB", (400, 300), (0, 0, 0)) bw, red = IP.process_image(img, size=(400, 300), algorithm="none", tricolor=False) assert len(bw) == 15000 assert red is None def test_resize_to_fit(self): img = Image.new("RGB", (800, 600), (255, 255, 255)) bw, _ = IP.process_image(img, size=(400, 300), algorithm="none", tricolor=False) assert len(bw) == 15000 def test_rotate_180(self): """rotate=180:左上角白块移到右下""" img = Image.new("RGB", (8, 8), (0, 0, 0)) for x in range(8): img.putpixel((x, 0), (255, 255, 255)) bw, _ = IP.process_image(img, size=(8, 8), algorithm="none", tricolor=False, rotate=180) assert bw[0] == 0x00 assert bw[-1] == 0xFF