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(
|
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
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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