From 4e5e4b8ce069eb3b5826fbf00c1ebb444767ac29 Mon Sep 17 00:00:00 2001 From: Josh Wren Date: Sun, 16 Aug 2026 11:15:56 -0500 Subject: [PATCH 1/2] Fix page counter visibility on pages with borders (issue #58) The page counter was using the average luminance of the entire page to determine text color, but the counter is displayed in the bottom-right corner. When a page has a white border around a dark image, the average luminance might be dark (so white text is chosen), but the bottom-right corner where the counter sits is white, making the white text invisible. This fix samples the luminance specifically in the area where the counter is displayed, ensuring the text color contrasts with the actual background behind it. Added tests to verify the fix works for both dark pages with white borders and light pages with dark borders. --- cdisplayagain.py | 52 ++++++++++++++++++++++++++++-- tests/test_page_counter.py | 66 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/cdisplayagain.py b/cdisplayagain.py index a755ea2..58f5466 100644 --- a/cdisplayagain.py +++ b/cdisplayagain.py @@ -1302,14 +1302,60 @@ def _page_counter_text(self) -> str | None: return f"{self._current_index + 1}/{total}" def _page_counter_color(self) -> str: - """Pick black or white text based on the mean luminance of the current page.""" + """Pick black or white text based on the luminance of the page area where the counter sits. + + The counter is drawn at the bottom-right of the canvas. Sample that region + (after accounting for image scaling/positioning) so the text contrasts with + whatever is actually behind it. + """ if self._current_pil is None: return "#ffffff" try: from PIL import ImageStat - thumb = self._current_pil.convert("L").resize((32, 32), Image.Resampling.BILINEAR) - mean = ImageStat.Stat(thumb).mean[0] + cw = max(1, self.canvas.winfo_width()) + ch = max(1, self.canvas.winfo_height()) + if self._scaled_size is None: + thumb = self._current_pil.convert("L").resize((32, 32), Image.Resampling.BILINEAR) + mean = ImageStat.Stat(thumb).mean[0] + return "#000000" if mean >= 128 else "#ffffff" + + iw, ih = self._scaled_size + scale = min(cw / iw, ch / ih) + dw = int(iw * scale) + dh = int(ih * scale) + + margin = 12 + cx = cw - margin + cy = ch - margin + + if dh <= ch: + img_y = (ch - dh) // 2 + cy + else: + img_y = cy + self._scroll_offset + + if dw <= cw: + img_x = (cw - dw) // 2 + cx + else: + img_x = cx + + img_x = max(0, min(iw - 1, int(img_x / scale))) + img_y = max(0, min(ih - 1, int(img_y / scale))) + + sample_size = min(32, iw // 4, ih // 4) + if sample_size < 4: + sample_size = min(4, iw, ih) + left = max(0, img_x - sample_size) + top = max(0, img_y - sample_size) + right = min(iw, img_x + sample_size) + bottom = min(ih, img_y + sample_size) + + if left >= right or top >= bottom: + thumb = self._current_pil.convert("L").resize((32, 32), Image.Resampling.BILINEAR) + mean = ImageStat.Stat(thumb).mean[0] + else: + region = self._current_pil.crop((left, top, right, bottom)).convert("L") + mean = ImageStat.Stat(region).mean[0] except Exception: return "#ffffff" return "#000000" if mean >= 128 else "#ffffff" diff --git a/tests/test_page_counter.py b/tests/test_page_counter.py index 27fc8b9..e2a6cf9 100644 --- a/tests/test_page_counter.py +++ b/tests/test_page_counter.py @@ -316,3 +316,69 @@ def test_render_current_sync_no_source_clears_counter(tk_root, tmp_path): viewer._render_current_sync() assert viewer._page_counter_id is None + + +def test_counter_color_sampling_logic(): + """Test the coordinate sampling logic directly.""" + from PIL import Image, ImageStat + + # Create a dark page (100x100) with a white border in the bottom-right + img = Image.new("RGB", (100, 100), color=(0, 0, 0)) + # Add white border in bottom-right 30x30 area + for x in range(70, 100): + for y in range(70, 100): + img.putpixel((x, y), (255, 255, 255)) + + # Simulate the logic from _page_counter_color with realistic canvas size + cw, ch = 800, 600 # canvas size + iw, ih = 100, 100 # image size + scale = min(cw / iw, ch / ih) + dw = int(iw * scale) + dh = int(ih * scale) + + margin = 12 + cx = cw - margin + cy = ch - margin + + if dh <= ch: + img_y = (ch - dh) // 2 + cy + else: + img_y = cy + 0 # scroll_offset = 0 + + if dw <= cw: + img_x = (cw - dw) // 2 + cx + else: + img_x = cx + + # Map back to image coordinates + img_x = max(0, min(iw - 1, int(img_x / scale))) + img_y = max(0, min(ih - 1, int(img_y / scale))) + + sample_size = min(32, iw // 4, ih // 4) + if sample_size < 4: + sample_size = min(4, iw, ih) + left = max(0, img_x - sample_size) + top = max(0, img_y - sample_size) + right = min(iw, img_x + sample_size) + bottom = min(ih, img_y + sample_size) + + # Sample the region + region = img.crop((left, top, right, bottom)).convert("L") + mean = ImageStat.Stat(region).mean[0] + + # The counter area should be white, so text should be black + assert mean >= 128 # white area + + # Create light page with dark border + img2 = Image.new("RGB", (100, 100), color=(255, 255, 255)) + # Add dark border in bottom-right 30x30 area + for x in range(70, 100): + for y in range(70, 100): + img2.putpixel((x, y), (0, 0, 0)) + + # Sample the region again + region2 = img2.crop((left, top, right, bottom)).convert("L") + mean2 = ImageStat.Stat(region2).mean[0] + + # The counter area should be dark, so text should be white + assert mean2 < 128 # dark area From edf7c12bc9ea5ef11294b4e8ecd79a62453c5fee Mon Sep 17 00:00:00 2001 From: Josh Wren Date: Sun, 16 Aug 2026 11:32:25 -0500 Subject: [PATCH 2/2] Improve page counter visibility fix based on CodeRabbit review The previous implementation was incorrectly scaling the _scaled_size a second time. _scaled_size already contains the displayed dimensions, so we should not scale them again. This improved implementation: 1. Calculates the image's top-left canvas position based on iw, ih, and _scroll_offset 2. Maps the counter position by subtracting the image position from the counter position 3. Added comprehensive tests including the tall-page case mentioned in the review --- cdisplayagain.py | 20 ++------ tests/test_page_counter.py | 96 +++++++++++++++++++++----------------- 2 files changed, 56 insertions(+), 60 deletions(-) diff --git a/cdisplayagain.py b/cdisplayagain.py index 58f5466..31c0bcd 100644 --- a/cdisplayagain.py +++ b/cdisplayagain.py @@ -1321,26 +1321,14 @@ def _page_counter_color(self) -> str: return "#000000" if mean >= 128 else "#ffffff" iw, ih = self._scaled_size - scale = min(cw / iw, ch / ih) - dw = int(iw * scale) - dh = int(ih * scale) - margin = 12 cx = cw - margin cy = ch - margin - if dh <= ch: - img_y = (ch - dh) // 2 + cy - else: - img_y = cy + self._scroll_offset - - if dw <= cw: - img_x = (cw - dw) // 2 + cx - else: - img_x = cx - - img_x = max(0, min(iw - 1, int(img_x / scale))) - img_y = max(0, min(ih - 1, int(img_y / scale))) + image_left = (cw - iw) // 2 + image_top = (ch - ih) // 2 if ih <= ch else -self._scroll_offset + img_x = max(0, min(iw - 1, cx - image_left)) + img_y = max(0, min(ih - 1, cy - image_top)) sample_size = min(32, iw // 4, ih // 4) if sample_size < 4: diff --git a/tests/test_page_counter.py b/tests/test_page_counter.py index e2a6cf9..e4a6136 100644 --- a/tests/test_page_counter.py +++ b/tests/test_page_counter.py @@ -322,63 +322,71 @@ def test_counter_color_sampling_logic(): """Test the coordinate sampling logic directly.""" from PIL import Image, ImageStat + def test_sampling(img, cw, ch, iw, ih, scroll_offset=0): + """Test sampling logic for given image and dimensions.""" + margin = 12 + cx = cw - margin + cy = ch - margin + + image_left = (cw - iw) // 2 + image_top = (ch - ih) // 2 if ih <= ch else -scroll_offset + img_x = max(0, min(iw - 1, cx - image_left)) + img_y = max(0, min(ih - 1, cy - image_top)) + + sample_size = min(32, iw // 4, ih // 4) + if sample_size < 4: + sample_size = min(4, iw, ih) + left = max(0, img_x - sample_size) + top = max(0, img_y - sample_size) + right = min(iw, img_x + sample_size) + bottom = min(ih, img_y + sample_size) + + region = img.crop((left, top, right, bottom)).convert("L") + return ImageStat.Stat(region).mean[0] + + # Test 1: Normal size image (fits in canvas) # Create a dark page (100x100) with a white border in the bottom-right - img = Image.new("RGB", (100, 100), color=(0, 0, 0)) + img1 = Image.new("RGB", (100, 100), color=(0, 0, 0)) # Add white border in bottom-right 30x30 area for x in range(70, 100): for y in range(70, 100): - img.putpixel((x, y), (255, 255, 255)) + img1.putpixel((x, y), (255, 255, 255)) - # Simulate the logic from _page_counter_color with realistic canvas size - cw, ch = 800, 600 # canvas size - iw, ih = 100, 100 # image size - scale = min(cw / iw, ch / ih) - dw = int(iw * scale) - dh = int(ih * scale) - - margin = 12 - cx = cw - margin - cy = ch - margin - - if dh <= ch: - img_y = (ch - dh) // 2 + cy - else: - img_y = cy + 0 # scroll_offset = 0 - - if dw <= cw: - img_x = (cw - dw) // 2 + cx - else: - img_x = cx - - # Map back to image coordinates - img_x = max(0, min(iw - 1, int(img_x / scale))) - img_y = max(0, min(ih - 1, int(img_y / scale))) - - sample_size = min(32, iw // 4, ih // 4) - if sample_size < 4: - sample_size = min(4, iw, ih) - left = max(0, img_x - sample_size) - top = max(0, img_y - sample_size) - right = min(iw, img_x + sample_size) - bottom = min(ih, img_y + sample_size) - - # Sample the region - region = img.crop((left, top, right, bottom)).convert("L") - mean = ImageStat.Stat(region).mean[0] + # Test with realistic canvas size + cw, ch = 800, 600 + iw, ih = 100, 100 + mean1 = test_sampling(img1, cw, ch, iw, ih) # The counter area should be white, so text should be black - assert mean >= 128 # white area + assert mean1 >= 128 # white area - # Create light page with dark border + # Test 2: Light page with dark border img2 = Image.new("RGB", (100, 100), color=(255, 255, 255)) # Add dark border in bottom-right 30x30 area for x in range(70, 100): for y in range(70, 100): img2.putpixel((x, y), (0, 0, 0)) - # Sample the region again - region2 = img2.crop((left, top, right, bottom)).convert("L") - mean2 = ImageStat.Stat(region2).mean[0] - + mean2 = test_sampling(img2, cw, ch, iw, ih) # The counter area should be dark, so text should be white assert mean2 < 128 # dark area + + # Test 3: Tall image (doesn't fit in canvas) - regression test for issue #58 + # Create a tall dark page (100x1200) with white border at the bottom + img3 = Image.new("RGB", (100, 1200), color=(0, 0, 0)) + # Add white border in bottom 30 rows + for x in range(0, 100): + for y in range(1170, 1200): + img3.putpixel((x, y), (255, 255, 255)) + + # Test with scroll offset = 0 (showing top of image) + cw, ch = 800, 600 + iw, ih = 100, 1200 + mean3 = test_sampling(img3, cw, ch, iw, ih, scroll_offset=0) + # Counter is at bottom of canvas, but we're showing top of image, so it should be dark + assert mean3 < 128 # dark area + + # Test with scroll offset = 600 (showing bottom of image) + mean4 = test_sampling(img3, cw, ch, iw, ih, scroll_offset=600) + # Counter is at bottom of canvas, showing bottom of image, so it should be white + assert mean4 >= 128 # white area