fix:提升图片像素到适应手机3x

This commit is contained in:
fuzhongyun 2025-12-30 17:21:43 +08:00
parent b429b8e874
commit fae6bb1b2a
3 changed files with 51 additions and 13 deletions

4
app.py
View File

@ -18,6 +18,7 @@ app = FastAPI(
async def convert_excel( async def convert_excel(
file: UploadFile = File(..., description="The Excel file to convert"), 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)"), 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. Convert an uploaded Excel file to a PNG image.
@ -35,7 +36,8 @@ async def convert_excel(
renderer = ExcelRenderer(contents) renderer = ExcelRenderer(contents)
# Render # 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 # Return as streaming response
# Handle Chinese filenames in Content-Disposition # Handle Chinese filenames in Content-Disposition

View File

@ -51,18 +51,19 @@ class ExcelRenderer:
self.font_cache[key] = font self.font_cache[key] = font
return 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. 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() output = io.BytesIO()
img.save(output, format='PNG', dpi=(dpi, dpi)) img.save(output, format='PNG', dpi=(dpi, dpi))
output.seek(0) output.seek(0)
return output.getvalue() 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. Internal method to draw the Excel sheet onto a PIL Image.
""" """
@ -76,10 +77,11 @@ class ExcelRenderer:
else: else:
raise ValueError(f"Sheet '{sheet_name}' not found. Available sheets: {wb.sheetnames}") 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: def _draw_sheet(self, sheet: Worksheet, padding: int, scale: int) -> Image.Image:
cell_height = 40 # Default cell height cell_height = 40 * scale # Scaled cell height
padding = padding * scale # Scaled padding
max_row = sheet.max_row max_row = sheet.max_row
max_col = sheet.max_column max_col = sheet.max_column
@ -95,7 +97,8 @@ class ExcelRenderer:
col_width_excel = col_dim.width if col_dim.width else 10 col_width_excel = col_dim.width if col_dim.width else 10
# Excel width to pixels (approximate factor ~7 + padding) # 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) col_widths_pixels.append(width_px)
img_width += width_px img_width += width_px
@ -134,7 +137,7 @@ class ExcelRenderer:
# Use the max_row of the merged range to determine y2 # Use the max_row of the merged range to determine y2
y2 = padding + max_r * cell_height 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) # Skip if it is a merged cell but NOT the top-left (already handled above or by MergedCell check)
elif isinstance(cell, MergedCell): elif isinstance(cell, MergedCell):
@ -147,11 +150,11 @@ class ExcelRenderer:
x2 = col_x_positions[col] x2 = col_x_positions[col]
y2 = y1 + cell_height 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 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 # Skip MergedCells that are not the top-left cell
if isinstance(cell, MergedCell): if isinstance(cell, MergedCell):
return return
@ -176,7 +179,9 @@ class ExcelRenderer:
is_bold = cell.font and cell.font.bold is_bold = cell.font and cell.font.bold
# Excel font size is in points. 12 is default. # Excel font size is in points. 12 is default.
font_size = int(cell.font.sz) if (cell.font and cell.font.sz) else 12 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 # Font color
# Excel's Color object can be complex. We pass the whole object to _parse_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' v_align = cell.alignment.vertical if (cell.alignment and cell.alignment.vertical) else 'center'
# Text rendering with simple truncation # 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]: def _parse_color(self, color_obj, default=(0, 0, 0)) -> Tuple[int, int, int]:
""" """

31
tests/test_high_dpi.py Normal file
View File

@ -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}")