fix:提升图片像素到适应手机3x
This commit is contained in:
parent
b429b8e874
commit
fae6bb1b2a
4
app.py
4
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
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
Loading…
Reference in New Issue