From fae6bb1b2a839b5b55b9e070aa212c9cb0418ccc Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Tue, 30 Dec 2025 17:21:43 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E6=8F=90=E5=8D=87=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=83=8F=E7=B4=A0=E5=88=B0=E9=80=82=E5=BA=94=E6=89=8B?= =?UTF-8?q?=E6=9C=BA3x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 4 +++- core/renderer.py | 29 +++++++++++++++++------------ tests/test_high_dpi.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 tests/test_high_dpi.py diff --git a/app.py b/app.py index 5d764c5..df6ff3a 100644 --- a/app.py +++ b/app.py @@ -18,6 +18,7 @@ app = FastAPI( async def convert_excel( file: UploadFile = File(..., description="The Excel file to convert"), sheet_name: str = Form(None, description="Name of the sheet to convert (optional, defaults to active sheet)"), + scale: int = Form(3, description="Scaling factor for high-DPI rendering (default 3, best for mobile/retina screens)"), ): """ Convert an uploaded Excel file to a PNG image. @@ -35,7 +36,8 @@ async def convert_excel( renderer = ExcelRenderer(contents) # Render - image_bytes = renderer.render_to_bytes(sheet_name=sheet_name) + # Use scale parameter + image_bytes = renderer.render_to_bytes(sheet_name=sheet_name, scale=scale) # Return as streaming response # Handle Chinese filenames in Content-Disposition diff --git a/core/renderer.py b/core/renderer.py index 6538e49..4948e4c 100644 --- a/core/renderer.py +++ b/core/renderer.py @@ -51,18 +51,19 @@ class ExcelRenderer: self.font_cache[key] = font return font - def render_to_bytes(self, sheet_name: Optional[str] = None, dpi: int = 200, padding: int = 20) -> bytes: + def render_to_bytes(self, sheet_name: Optional[str] = None, dpi: int = 300, padding: int = 20, scale: int = 2) -> bytes: """ Render the specified sheet to a PNG image and return bytes. + :param scale: Internal scaling factor for high-DPI rendering (default 2x). """ - img = self._render_image(sheet_name, padding) + img = self._render_image(sheet_name, padding, scale) output = io.BytesIO() img.save(output, format='PNG', dpi=(dpi, dpi)) output.seek(0) return output.getvalue() - def _render_image(self, sheet_name: Optional[str], padding: int) -> Image.Image: + def _render_image(self, sheet_name: Optional[str], padding: int, scale: int) -> Image.Image: """ Internal method to draw the Excel sheet onto a PIL Image. """ @@ -76,10 +77,11 @@ class ExcelRenderer: else: raise ValueError(f"Sheet '{sheet_name}' not found. Available sheets: {wb.sheetnames}") - return self._draw_sheet(sheet, padding) + return self._draw_sheet(sheet, padding, scale) - def _draw_sheet(self, sheet: Worksheet, padding: int) -> Image.Image: - cell_height = 40 # Default cell height + def _draw_sheet(self, sheet: Worksheet, padding: int, scale: int) -> Image.Image: + cell_height = 40 * scale # Scaled cell height + padding = padding * scale # Scaled padding max_row = sheet.max_row max_col = sheet.max_column @@ -95,7 +97,8 @@ class ExcelRenderer: col_width_excel = col_dim.width if col_dim.width else 10 # Excel width to pixels (approximate factor ~7 + padding) - width_px = int(col_width_excel * 7) + 5 + # Apply scale factor + width_px = int((col_width_excel * 7 + 5) * scale) col_widths_pixels.append(width_px) img_width += width_px @@ -134,7 +137,7 @@ class ExcelRenderer: # Use the max_row of the merged range to determine y2 y2 = padding + max_r * cell_height - self._draw_cell(draw, cell, x1, y1, x2, y2) + self._draw_cell(draw, cell, x1, y1, x2, y2, scale) # Skip if it is a merged cell but NOT the top-left (already handled above or by MergedCell check) elif isinstance(cell, MergedCell): @@ -147,11 +150,11 @@ class ExcelRenderer: x2 = col_x_positions[col] y2 = y1 + cell_height - self._draw_cell(draw, cell, x1, y1, x2, y2) + self._draw_cell(draw, cell, x1, y1, x2, y2, scale) return img - def _draw_cell(self, draw: ImageDraw.ImageDraw, cell, x1, y1, x2, y2): + def _draw_cell(self, draw: ImageDraw.ImageDraw, cell, x1, y1, x2, y2, scale: int): # Skip MergedCells that are not the top-left cell if isinstance(cell, MergedCell): return @@ -176,7 +179,9 @@ class ExcelRenderer: is_bold = cell.font and cell.font.bold # Excel font size is in points. 12 is default. font_size = int(cell.font.sz) if (cell.font and cell.font.sz) else 12 - current_font = self._get_font(is_bold, font_size) + # Scale the font size + scaled_font_size = int(font_size * scale) + current_font = self._get_font(is_bold, scaled_font_size) # Font color # Excel's Color object can be complex. We pass the whole object to _parse_color. @@ -188,7 +193,7 @@ class ExcelRenderer: v_align = cell.alignment.vertical if (cell.alignment and cell.alignment.vertical) else 'center' # Text rendering with simple truncation - self._draw_text(draw, text, x1, y1, x2, y2, current_font, text_color, h_align, v_align, font_size) + self._draw_text(draw, text, x1, y1, x2, y2, current_font, text_color, h_align, v_align, scaled_font_size) def _parse_color(self, color_obj, default=(0, 0, 0)) -> Tuple[int, int, int]: """ diff --git a/tests/test_high_dpi.py b/tests/test_high_dpi.py new file mode 100644 index 0000000..44f74c4 --- /dev/null +++ b/tests/test_high_dpi.py @@ -0,0 +1,31 @@ +import pytest +import io +import os +from core.renderer import ExcelRenderer +from PIL import Image + +TEST_FILE_PATH = "tests/kshj_gt1767081783800.xlsx" + +@pytest.mark.skipif(not os.path.exists(TEST_FILE_PATH), reason="Test file not found") +def test_high_dpi_rendering(): + """ + Test rendering with higher scale factor. + """ + with open(TEST_FILE_PATH, "rb") as f: + content = f.read() + + renderer = ExcelRenderer(content) + + # Render with scale=3 (High Quality) + img_bytes = renderer.render_to_bytes(scale=3, dpi=300) + + assert isinstance(img_bytes, bytes) + assert len(img_bytes) > 0 + + img = Image.open(io.BytesIO(img_bytes)) + + # Save for visual inspection + output_path = "tests/test_output_high_dpi.png" + img.save(output_path) + print(f"Generated high DPI test image at: {os.path.abspath(output_path)}") + print(f"Image Size: {img.size}")