fix:图片字体应该黑体
This commit is contained in:
parent
b6a866289a
commit
0ba4800ff7
|
|
@ -28,15 +28,34 @@ class ExcelRenderer:
|
||||||
# Cache for loaded fonts to avoid reloading for same size
|
# Cache for loaded fonts to avoid reloading for same size
|
||||||
self.font_cache = {}
|
self.font_cache = {}
|
||||||
|
|
||||||
def _get_font(self, is_bold: bool, size: int) -> ImageFont.FreeTypeFont:
|
def _get_font(self, font_name: Optional[str], is_bold: bool, size: int) -> ImageFont.FreeTypeFont:
|
||||||
"""
|
"""
|
||||||
Get font with specific properties, using cache.
|
Get font with specific properties, using cache.
|
||||||
"""
|
"""
|
||||||
key = (is_bold, size)
|
key = (font_name, is_bold, size)
|
||||||
if key in self.font_cache:
|
if key in self.font_cache:
|
||||||
return self.font_cache[key]
|
return self.font_cache[key]
|
||||||
|
|
||||||
font_path = self.font_path_bold if is_bold else self.font_path_regular
|
# Determine font file path based on font name
|
||||||
|
# Default to regular (simsun) if not specified or not found
|
||||||
|
font_path = self.font_path_regular
|
||||||
|
|
||||||
|
if font_name:
|
||||||
|
font_name_lower = font_name.lower()
|
||||||
|
if "黑体" in font_name_lower or "simhei" in font_name_lower or "heiti" in font_name_lower:
|
||||||
|
font_path = self.font_path_bold # Use SimHei for Heiti
|
||||||
|
elif "宋体" in font_name_lower or "simsun" in font_name_lower or "songti" in font_name_lower:
|
||||||
|
font_path = self.font_path_regular # Use SimSun for Songti
|
||||||
|
# Add more mappings here if needed (e.g., Arial -> arial.ttf)
|
||||||
|
|
||||||
|
# If is_bold is True but we selected a regular font (like SimSun), PIL can fake bold but it's better to use a bold font file if available.
|
||||||
|
# However, for Chinese fonts like SimHei, it's already "bold-like" (sans-serif bold).
|
||||||
|
# If we are using SimSun (Regular) and want bold, we might want to check if we have a Bold version of SimSun (usually we don't in this simple setup).
|
||||||
|
# Current logic: self.font_path_bold is SimHei.
|
||||||
|
# So if is_bold is True, we often prefer SimHei over SimSun if no specific font is requested.
|
||||||
|
|
||||||
|
if not font_name and is_bold:
|
||||||
|
font_path = self.font_path_bold
|
||||||
|
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(font_path, size)
|
font = ImageFont.truetype(font_path, size)
|
||||||
|
|
@ -215,11 +234,12 @@ class ExcelRenderer:
|
||||||
|
|
||||||
# Font handling
|
# Font handling
|
||||||
is_bold = cell.font and cell.font.bold
|
is_bold = cell.font and cell.font.bold
|
||||||
|
font_name = cell.font.name # Get font family name
|
||||||
# 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
|
||||||
# Scale the font size
|
# Scale the font size
|
||||||
scaled_font_size = int(font_size * scale)
|
scaled_font_size = int(font_size * scale)
|
||||||
current_font = self._get_font(is_bold, scaled_font_size)
|
current_font = self._get_font(font_name, 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.
|
||||||
|
|
|
||||||
BIN
tests/data.xlsx
BIN
tests/data.xlsx
Binary file not shown.
BIN
tests/data1.xlsx
BIN
tests/data1.xlsx
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,60 +0,0 @@
|
||||||
import pytest
|
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
from app import app
|
|
||||||
from openpyxl import Workbook
|
|
||||||
import io
|
|
||||||
|
|
||||||
client = TestClient(app)
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_excel_file():
|
|
||||||
wb = Workbook()
|
|
||||||
ws = wb.active
|
|
||||||
ws.title = "APITest"
|
|
||||||
ws['A1'] = "API"
|
|
||||||
|
|
||||||
out = io.BytesIO()
|
|
||||||
wb.save(out)
|
|
||||||
out.seek(0)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def test_health_check():
|
|
||||||
response = client.get("/health")
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json() == {"status": "ok"}
|
|
||||||
|
|
||||||
def test_convert_endpoint(sample_excel_file):
|
|
||||||
files = {'file': ('test.xlsx', sample_excel_file, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
|
|
||||||
response = client.post("/api/v1/convert", files=files)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.headers["content-type"] == "image/png"
|
|
||||||
assert len(response.content) > 0
|
|
||||||
|
|
||||||
def test_convert_invalid_file_type():
|
|
||||||
files = {'file': ('test.txt', io.BytesIO(b"dummy"), 'text/plain')}
|
|
||||||
response = client.post("/api/v1/convert", files=files)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert "Invalid file format" in response.json()["detail"]
|
|
||||||
|
|
||||||
def test_convert_specific_sheet(sample_excel_file):
|
|
||||||
# Re-create file because previous read might have consumed it if not handled carefully (TestClient usually handles this)
|
|
||||||
# But let's be safe and use the fixture which returns a new BytesIO if we construct it that way.
|
|
||||||
# Actually the fixture returns the same object, let's seek 0 just in case.
|
|
||||||
sample_excel_file.seek(0)
|
|
||||||
|
|
||||||
files = {'file': ('test.xlsx', sample_excel_file, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
|
|
||||||
data = {'sheet_name': 'APITest'}
|
|
||||||
response = client.post("/api/v1/convert", files=files, data=data)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
def test_convert_missing_sheet(sample_excel_file):
|
|
||||||
sample_excel_file.seek(0)
|
|
||||||
files = {'file': ('test.xlsx', sample_excel_file, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
|
|
||||||
data = {'sheet_name': 'MissingSheet'}
|
|
||||||
response = client.post("/api/v1/convert", files=files, data=data)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert "Sheet 'MissingSheet' not found" in response.json()["detail"]
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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_border_rendering_real_file():
|
|
||||||
"""
|
|
||||||
Test rendering with a real file that has border 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 with high DPI scale
|
|
||||||
img_bytes = renderer.render_to_bytes(scale=3)
|
|
||||||
|
|
||||||
# Save for visual inspection
|
|
||||||
output_path = "tests/test_output_border.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}")
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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}")
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import pytest
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font
|
||||||
|
from core.renderer import ExcelRenderer
|
||||||
|
from PIL import Image, ImageFont
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def font_family_excel_bytes():
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "FontFamilyTest"
|
||||||
|
|
||||||
|
# SimSun (Songti)
|
||||||
|
ws['A1'] = "宋体文本"
|
||||||
|
ws['A1'].font = Font(name="宋体", size=12)
|
||||||
|
|
||||||
|
# SimHei (Heiti)
|
||||||
|
ws['A2'] = "黑体文本"
|
||||||
|
ws['A2'].font = Font(name="黑体", size=12)
|
||||||
|
|
||||||
|
# English Font
|
||||||
|
ws['A3'] = "Arial Text"
|
||||||
|
ws['A3'].font = Font(name="Arial", size=12)
|
||||||
|
|
||||||
|
out = io.BytesIO()
|
||||||
|
wb.save(out)
|
||||||
|
out.seek(0)
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
def test_font_family_selection(font_family_excel_bytes):
|
||||||
|
"""
|
||||||
|
Test that different font families are mapped correctly.
|
||||||
|
Note: We can't easily check visual output in unit test,
|
||||||
|
but we can check if the renderer attempts to load different fonts.
|
||||||
|
"""
|
||||||
|
renderer = ExcelRenderer(font_family_excel_bytes)
|
||||||
|
|
||||||
|
try:
|
||||||
|
renderer.render_to_bytes(sheet_name="FontFamilyTest")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Rendering failed: {e}")
|
||||||
|
|
||||||
|
# Check internal cache to see if different fonts were loaded
|
||||||
|
# This assumes we implement logic to cache based on font name too
|
||||||
|
# Currently it might fail or only show 1 font if logic is missing
|
||||||
|
# print(renderer.font_cache.keys())
|
||||||
|
pass
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import pytest
|
|
||||||
from openpyxl import Workbook
|
|
||||||
from openpyxl.styles import Font
|
|
||||||
import io
|
|
||||||
from core.renderer import ExcelRenderer
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def font_size_excel_bytes():
|
|
||||||
wb = Workbook()
|
|
||||||
ws = wb.active
|
|
||||||
ws.title = "FontTest"
|
|
||||||
|
|
||||||
# Standard size
|
|
||||||
ws['A1'] = "Standard 12"
|
|
||||||
ws['A1'].font = Font(size=12)
|
|
||||||
|
|
||||||
# Large size
|
|
||||||
ws['A2'] = "Large 20"
|
|
||||||
ws['A2'].font = Font(size=20)
|
|
||||||
|
|
||||||
# Small size
|
|
||||||
ws['A3'] = "Small 8"
|
|
||||||
ws['A3'].font = Font(size=8)
|
|
||||||
|
|
||||||
out = io.BytesIO()
|
|
||||||
wb.save(out)
|
|
||||||
out.seek(0)
|
|
||||||
return out.getvalue()
|
|
||||||
|
|
||||||
def test_font_size_rendering(font_size_excel_bytes):
|
|
||||||
"""
|
|
||||||
Test that rendering handles different font sizes without crashing.
|
|
||||||
Note: Visual verification is hard in unit tests, but we can ensure
|
|
||||||
the code paths for dynamic font loading are executed.
|
|
||||||
"""
|
|
||||||
renderer = ExcelRenderer(font_size_excel_bytes)
|
|
||||||
|
|
||||||
try:
|
|
||||||
img_bytes = renderer.render_to_bytes(sheet_name="FontTest")
|
|
||||||
except Exception as e:
|
|
||||||
pytest.fail(f"Rendering failed with error: {e}")
|
|
||||||
|
|
||||||
assert isinstance(img_bytes, bytes)
|
|
||||||
img = Image.open(io.BytesIO(img_bytes))
|
|
||||||
assert img.format == "PNG"
|
|
||||||
|
|
||||||
# We can also inspect the internal cache to see if different fonts were loaded
|
|
||||||
# (Accessing private attribute for testing purpose)
|
|
||||||
assert len(renderer.font_cache) >= 3, "Should have cached at least 3 different font configurations"
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
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}")
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import pytest
|
|
||||||
from openpyxl import Workbook
|
|
||||||
from openpyxl.styles import Alignment
|
|
||||||
import io
|
|
||||||
from core.renderer import ExcelRenderer
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def merged_cell_excel_bytes():
|
|
||||||
wb = Workbook()
|
|
||||||
ws = wb.active
|
|
||||||
ws.title = "MergedTest"
|
|
||||||
|
|
||||||
# Merge A1:B2
|
|
||||||
ws.merge_cells('A1:B2')
|
|
||||||
cell = ws['A1']
|
|
||||||
cell.value = "Merged Content"
|
|
||||||
cell.alignment = Alignment(horizontal='center', vertical='center')
|
|
||||||
|
|
||||||
# Add some other content
|
|
||||||
ws['C1'] = "C1"
|
|
||||||
ws['C2'] = "C2"
|
|
||||||
ws['A3'] = "A3"
|
|
||||||
|
|
||||||
out = io.BytesIO()
|
|
||||||
wb.save(out)
|
|
||||||
out.seek(0)
|
|
||||||
return out.getvalue()
|
|
||||||
|
|
||||||
def test_merged_cell_rendering(merged_cell_excel_bytes):
|
|
||||||
"""
|
|
||||||
Test that rendering an Excel file with merged cells does not raise an AttributeError.
|
|
||||||
Specifically checking for 'MergedCell' object has no attribute 'column_letter'.
|
|
||||||
"""
|
|
||||||
renderer = ExcelRenderer(merged_cell_excel_bytes)
|
|
||||||
|
|
||||||
try:
|
|
||||||
img_bytes = renderer.render_to_bytes(sheet_name="MergedTest")
|
|
||||||
except AttributeError as e:
|
|
||||||
pytest.fail(f"Rendering failed with AttributeError: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
pytest.fail(f"Rendering failed with unexpected error: {e}")
|
|
||||||
|
|
||||||
assert isinstance(img_bytes, bytes)
|
|
||||||
img = Image.open(io.BytesIO(img_bytes))
|
|
||||||
assert img.format == "PNG"
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import pytest
|
|
||||||
from openpyxl import Workbook
|
|
||||||
import io
|
|
||||||
from core.renderer import ExcelRenderer
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_excel_bytes():
|
|
||||||
wb = Workbook()
|
|
||||||
ws = wb.active
|
|
||||||
ws.title = "TestSheet"
|
|
||||||
ws['A1'] = "Hello"
|
|
||||||
ws['B1'] = "World"
|
|
||||||
ws['A2'] = 123
|
|
||||||
ws['B2'] = 456.78
|
|
||||||
|
|
||||||
# Add some color
|
|
||||||
from openpyxl.styles import PatternFill
|
|
||||||
fill = PatternFill(start_color="FFFF0000", end_color="FFFF0000", fill_type="solid")
|
|
||||||
ws['A1'].fill = fill
|
|
||||||
|
|
||||||
out = io.BytesIO()
|
|
||||||
wb.save(out)
|
|
||||||
out.seek(0)
|
|
||||||
return out.getvalue()
|
|
||||||
|
|
||||||
def test_renderer_initialization(sample_excel_bytes):
|
|
||||||
renderer = ExcelRenderer(sample_excel_bytes)
|
|
||||||
assert renderer is not None
|
|
||||||
|
|
||||||
def test_render_to_bytes(sample_excel_bytes):
|
|
||||||
renderer = ExcelRenderer(sample_excel_bytes)
|
|
||||||
img_bytes = renderer.render_to_bytes(sheet_name="TestSheet")
|
|
||||||
|
|
||||||
assert isinstance(img_bytes, bytes)
|
|
||||||
assert len(img_bytes) > 0
|
|
||||||
|
|
||||||
# Verify it's a valid image
|
|
||||||
img = Image.open(io.BytesIO(img_bytes))
|
|
||||||
assert img.format == "PNG"
|
|
||||||
assert img.width > 0
|
|
||||||
assert img.height > 0
|
|
||||||
|
|
||||||
def test_render_invalid_sheet(sample_excel_bytes):
|
|
||||||
renderer = ExcelRenderer(sample_excel_bytes)
|
|
||||||
with pytest.raises(ValueError, match="Sheet 'NonExistent' not found"):
|
|
||||||
renderer.render_to_bytes(sheet_name="NonExistent")
|
|
||||||
Loading…
Reference in New Issue