diff --git a/core/renderer.py b/core/renderer.py index 1638d1a..6538e49 100644 --- a/core/renderer.py +++ b/core/renderer.py @@ -179,8 +179,9 @@ class ExcelRenderer: current_font = self._get_font(is_bold, font_size) # Font color - font_color_hex = cell.font.color.rgb if (cell.font and cell.font.color) else None - text_color = self._parse_color(font_color_hex, default=(0, 0, 0)) + # Excel's Color object can be complex. We pass the whole object to _parse_color. + font_color_obj = cell.font.color if (cell.font and cell.font.color) else None + text_color = self._parse_color(font_color_obj, default=(0, 0, 0)) # Alignment h_align = cell.alignment.horizontal if (cell.alignment and cell.alignment.horizontal) else 'left' @@ -189,15 +190,44 @@ class ExcelRenderer: # Text rendering with simple truncation self._draw_text(draw, text, x1, y1, x2, y2, current_font, text_color, h_align, v_align, font_size) - def _parse_color(self, color_code, default=(0, 0, 0)) -> Tuple[int, int, int]: - if not color_code or color_code == '00000000' or not isinstance(color_code, str): + def _parse_color(self, color_obj, default=(0, 0, 0)) -> Tuple[int, int, int]: + """ + Parse Excel color object to RGB tuple. + """ + if not color_obj: return default + + color_code = None - # Handle ARGB (Excel often uses this) + # If it's a string, treat as hex + if isinstance(color_obj, str): + color_code = color_obj + # If it's a Color object + elif hasattr(color_obj, 'type'): + if color_obj.type == 'rgb': + color_code = color_obj.rgb + elif color_obj.type == 'theme': + # Use hardcoded common theme colors as a fallback for MVP + # Theme 0: Light 1 (White) - FFFFFF + # Theme 1: Dark 1 (Black) - 000000 + # Theme 2: Light 2 (EEECE1) + # Theme 3: Dark 2 (1F497D) + if color_obj.theme == 0: + color_code = "FFFFFFFF" # White + elif color_obj.theme == 1: + color_code = "FF000000" # Black + else: + # Attempt to check if rgb is populated even for theme + if hasattr(color_obj, 'rgb') and color_obj.rgb: + color_code = color_obj.rgb + elif hasattr(color_obj, 'rgb'): + color_code = color_obj.rgb + + if not color_code or color_code == '00000000': + return default + + # Handle ARGB if len(color_code) > 6: - # Strip alpha if present (usually first 2 chars for ARGB) - # Example: FF000000 -> 000000 (Black), FFFFFFFF -> FFFFFF (White) - # Note: This is a simplification. if color_code.startswith('FF') or len(color_code) == 8: color_code = color_code[2:] diff --git a/tests/kshj_gt1767064690.xlsx b/tests/kshj_gt1767064690.xlsx deleted file mode 100755 index 9343eeb..0000000 Binary files a/tests/kshj_gt1767064690.xlsx and /dev/null differ diff --git a/tests/kshj_gt1767081783800.xlsx b/tests/kshj_gt1767081783800.xlsx new file mode 100755 index 0000000..960a8c9 Binary files /dev/null and b/tests/kshj_gt1767081783800.xlsx differ diff --git a/tests/test_font_color.py b/tests/test_font_color.py new file mode 100644 index 0000000..15eb6e5 --- /dev/null +++ b/tests/test_font_color.py @@ -0,0 +1,36 @@ +import pytest +import io +import os +from core.renderer import ExcelRenderer +from PIL import Image + +# Use the file provided by the user for reproduction +TEST_FILE_PATH = "tests/kshj_gt1767081783800.xlsx" + +@pytest.mark.skipif(not os.path.exists(TEST_FILE_PATH), reason="Test file not found") +def test_font_color_rendering_real_file(): + """ + Test rendering with a real file that has font color issues. + This test will generate an output image for visual inspection. + """ + with open(TEST_FILE_PATH, "rb") as f: + content = f.read() + + renderer = ExcelRenderer(content) + + try: + # Render the first sheet (or specific sheet if known, default to active) + img_bytes = renderer.render_to_bytes() + + # Save for visual inspection + output_path = "tests/test_output_font_color.png" + with open(output_path, "wb") as f_out: + f_out.write(img_bytes) + + print(f"Generated test image at: {os.path.abspath(output_path)}") + + assert isinstance(img_bytes, bytes) + assert len(img_bytes) > 0 + + except Exception as e: + pytest.fail(f"Rendering failed: {e}")