Skip to content

Commit c516543

Browse files
committed
fix: render font previews from font files
1 parent 550e7e3 commit c516543

2 files changed

Lines changed: 93 additions & 71 deletions

File tree

desktop_qt_ui/ui/main_page/layout.py

Lines changed: 92 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import time
77

88
from PyQt6.QtCore import Qt
9-
from PyQt6.QtGui import QBrush, QColor, QFontDatabase, QRawFont
9+
from PyQt6.QtGui import QBrush, QColor, QImage, QPixmap
1010
from PyQt6.QtWidgets import (
1111
QButtonGroup,
1212
QFileDialog,
@@ -29,6 +29,7 @@
2929
QVBoxLayout,
3030
QWidget,
3131
)
32+
from PIL import Image, ImageDraw, ImageFont
3233
from utils.app_version import format_version_label
3334
from utils.resource_helper import resource_path
3435
from ui.widgets.wheel_filter import NoWheelComboBox as QComboBox
@@ -115,8 +116,7 @@ def _resolve_settings_tab_layout_file() -> str:
115116
_PROMPT_EXTENSIONS = (".yaml", ".yml", ".json")
116117
_FONT_EXTENSIONS = (".ttf", ".otf", ".ttc")
117118
_CURRENT_ASSET_PREFIX = "✓ "
118-
_FONT_PREVIEW_FACE_CACHE = {}
119-
_FONT_PREVIEW_REGISTRATION_CACHE = {}
119+
_FONT_PREVIEW_PIXMAP_CACHE = {}
120120

121121

122122
def _load_reclassify_settings_layout():
@@ -132,54 +132,81 @@ def _load_reclassify_settings_layout():
132132
return []
133133

134134

135-
def _font_preview_style(size: int, family_name: str | None = None) -> str:
136-
"""根据当前主题生成字体预览标签样式。"""
135+
def _font_preview_style(size: int) -> str:
136+
"""字体预览文本回退样式。正常预览会直接渲染为 pixmap。"""
137137
text_color = get_current_theme_colors()["text_primary"]
138-
parts = [f"font-size: {size}pt", f"color: {text_color}"]
139-
if family_name:
140-
parts.insert(0, f"font-family: '{family_name}'")
141-
return "; ".join(parts) + ";"
138+
return f"font-size: {size}pt; color: {text_color};"
142139

143140

144-
def _get_font_preview_face(font_path: str) -> tuple[str | None, str | None]:
145-
cached = _FONT_PREVIEW_FACE_CACHE.get(font_path)
146-
if cached is not None:
147-
return cached
141+
def _render_font_preview_pixmap(font_path: str | None, text: str, size: int) -> QPixmap | None:
142+
"""Render preview text directly from the font file without Qt family matching."""
143+
if not font_path or not os.path.isfile(font_path):
144+
return None
148145

149-
family_name = None
150-
style_name = None
146+
norm_path = os.path.normpath(font_path)
147+
text_color = get_current_theme_colors()["text_primary"]
148+
try:
149+
mtime = os.path.getmtime(norm_path)
150+
except OSError:
151+
mtime = 0.0
152+
cache_key = (norm_path, mtime, text, int(size), text_color)
153+
cached = _FONT_PREVIEW_PIXMAP_CACHE.get(cache_key)
154+
if cached is not None:
155+
return QPixmap(cached)
151156

152157
try:
153-
raw_font = QRawFont(font_path, 32)
154-
if raw_font.isValid():
155-
family_name = raw_font.familyName() or family_name
156-
style_name = raw_font.styleName() or None
158+
font = ImageFont.truetype(norm_path, int(max(size, 1)))
157159
except Exception:
158-
pass
159-
160-
result = (family_name, style_name)
161-
_FONT_PREVIEW_FACE_CACHE[font_path] = result
162-
return result
163-
160+
return None
164161

165-
def _register_font_preview_face(font_path: str) -> tuple[str | None, str | None]:
166-
family_name, style_name = _get_font_preview_face(font_path)
167-
if font_path not in _FONT_PREVIEW_REGISTRATION_CACHE:
168-
try:
169-
font_id = QFontDatabase.addApplicationFont(font_path)
170-
except Exception:
171-
font_id = -1
172-
_FONT_PREVIEW_REGISTRATION_CACHE[font_path] = font_id
173-
font_id = _FONT_PREVIEW_REGISTRATION_CACHE.get(font_path, -1)
174-
if font_id >= 0:
162+
lines = str(text or " ").splitlines() or [" "]
163+
probe = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
164+
draw = ImageDraw.Draw(probe)
165+
try:
166+
ascent, descent = font.getmetrics()
167+
except Exception:
168+
ascent, descent = int(size), int(size * 0.25)
169+
line_gap = max(2, int(round(size * 0.18)))
170+
line_height = max(1, ascent + descent + line_gap)
171+
bboxes = []
172+
max_width = 1
173+
for line in lines:
174+
content = line or " "
175175
try:
176-
families = QFontDatabase.applicationFontFamilies(font_id)
177-
if families:
178-
family_name = families[0]
176+
bbox = draw.textbbox((0, 0), content, font=font)
179177
except Exception:
180-
pass
181-
_FONT_PREVIEW_FACE_CACHE[font_path] = (family_name, style_name)
182-
return family_name, style_name
178+
bbox = (0, 0, int(size * max(len(content), 1)), line_height)
179+
left, top, right, bottom = bbox
180+
max_width = max(max_width, right - left)
181+
bboxes.append((content, left, top, right, bottom))
182+
183+
margin = max(4, int(round(size * 0.2)))
184+
width = max(1, max_width + margin * 2)
185+
height = max(1, line_height * len(lines) + margin * 2)
186+
width = max(1, min(width, 4096))
187+
height = max(1, min(height, 2048))
188+
189+
qcolor = QColor(text_color)
190+
fill = (
191+
qcolor.red() if qcolor.isValid() else 31,
192+
qcolor.green() if qcolor.isValid() else 41,
193+
qcolor.blue() if qcolor.isValid() else 51,
194+
qcolor.alpha() if qcolor.isValid() else 255,
195+
)
196+
image = Image.new("RGBA", (width, height), (0, 0, 0, 0))
197+
draw = ImageDraw.Draw(image)
198+
y = margin
199+
for content, left, top, _right, _bottom in bboxes:
200+
draw.text((margin - left, y - top), content, font=font, fill=fill)
201+
y += line_height
202+
203+
raw_data = image.tobytes("raw", "RGBA")
204+
qimage = QImage(raw_data, width, height, QImage.Format.Format_RGBA8888).copy()
205+
pixmap = QPixmap.fromImage(qimage)
206+
if len(_FONT_PREVIEW_PIXMAP_CACHE) >= 128:
207+
_FONT_PREVIEW_PIXMAP_CACHE.clear()
208+
_FONT_PREVIEW_PIXMAP_CACHE[cache_key] = QPixmap(pixmap)
209+
return pixmap
183210

184211

185212
def refresh_font_preview_styles(self):
@@ -1137,17 +1164,16 @@ def create_font_page(self) -> QWidget:
11371164
for i in range(3):
11381165
lbl = QLabel()
11391166
lbl.setObjectName("font_preview_text")
1140-
lbl.setWordWrap(True)
1141-
lbl.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
1167+
lbl.setWordWrap(False)
1168+
lbl.setScaledContents(False)
11421169
self.scroll_content_layout.addWidget(lbl)
11431170
self.font_preview_labels.append(lbl)
11441171

11451172
self.scroll_content_layout.addStretch()
11461173
self.font_preview_scroll.setWidget(scroll_content)
11471174
preview_card_layout.addWidget(self.font_preview_scroll, 1)
11481175

1149-
self._current_preview_family = None
1150-
self._current_preview_style = None
1176+
self._current_preview_font_path = None
11511177
page_layout.addWidget(self.font_preview_card)
11521178

11531179
# --- Signals ---
@@ -1810,8 +1836,7 @@ def _on_font_selection_changed(self, current, previous):
18101836
return
18111837

18121838
if not current:
1813-
self._current_preview_family = None
1814-
self._current_preview_style = None
1839+
self._current_preview_font_path = None
18151840
if hasattr(self, "font_preview_name_label"):
18161841
self.font_preview_name_label.setText(self._t("Select a font to preview"))
18171842
self._update_font_preview()
@@ -1825,20 +1850,18 @@ def _on_font_selection_changed(self, current, previous):
18251850
if hasattr(self, "font_preview_name_label"):
18261851
self.font_preview_name_label.setText(font_filename)
18271852

1828-
# 读取字体 family/style,并按具体样式创建预览字体
1829-
family_name = None
1830-
style_name = None
1853+
# 后端渲染按真实字体文件路径区分字体,预览也保存文件路径直接渲染 glyph。
18311854
font_path = None
18321855
try:
18331856
fonts_dir = resource_path("fonts")
18341857
font_path = os.path.join(fonts_dir, font_filename)
1835-
if os.path.isfile(font_path):
1836-
family_name, style_name = _register_font_preview_face(font_path)
1858+
if not os.path.isfile(font_path):
1859+
font_path = None
18371860
except Exception:
1861+
font_path = None
18381862
pass
18391863

1840-
self._current_preview_family = family_name
1841-
self._current_preview_style = style_name
1864+
self._current_preview_font_path = font_path
18421865
self._update_font_preview()
18431866

18441867

@@ -1861,9 +1884,7 @@ def _update_font_preview(self):
18611884
if hasattr(self, "font_preview_input"):
18621885
custom_text = self.font_preview_input.text().strip()
18631886

1864-
# 获取缓存的字体信息
1865-
family_name = getattr(self, "_current_preview_family", None)
1866-
style_name = getattr(self, "_current_preview_style", None)
1887+
font_path = getattr(self, "_current_preview_font_path", None)
18671888

18681889
# 预设的预览文本与缩放比例
18691890
if custom_text:
@@ -1883,18 +1904,18 @@ def _update_font_preview(self):
18831904
text = preview_texts[i]
18841905
size = max(8, int(round(base_size * size_multipliers[i])))
18851906

1886-
# 更新文本
1907+
pixmap = _render_font_preview_pixmap(font_path, text, size)
1908+
if pixmap is not None and not pixmap.isNull():
1909+
lbl.setStyleSheet("background: transparent;")
1910+
lbl.setText("")
1911+
lbl.setPixmap(pixmap)
1912+
lbl.setMinimumSize(pixmap.size())
1913+
continue
1914+
1915+
lbl.clear()
1916+
lbl.setMinimumSize(0, 0)
18871917
lbl.setText(text)
1888-
1889-
# 更新样式 (颜色与大小)
1890-
lbl.setStyleSheet(_font_preview_style(size, family_name))
1891-
1892-
# 应用字体
1893-
if family_name:
1894-
preview_font = QFontDatabase.font(family_name, style_name or "", size)
1895-
if style_name:
1896-
preview_font.setStyleName(style_name)
1897-
if preview_font.family():
1898-
lbl.setFont(preview_font)
1899-
continue
1900-
lbl.setFont(self.font())
1918+
lbl.setStyleSheet(_font_preview_style(size))
1919+
fallback_font = self.font()
1920+
fallback_font.setPointSize(size)
1921+
lbl.setFont(fallback_font)

doc/CHANGELOG_v2.2.10.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
- 修复行距倍率小于 1.0 时文本渲染行距计算不准确的问题。
5050
- 修复加入 API 轮询后 Sakura 在 API 管理页不再显示地址和词典路径配置的问题;Sakura 保持固定兼容密钥,不暴露自定义 Key 或轮询槽。
5151
- 修复 PaddleOCR-VL 偶发重复符号幻觉输出的问题:检测到大量重复字符/符号串时会自动使用更严格的生成参数重新推理,多次重试仍异常则丢弃该结果。
52+
- 修复字体管理预览在同一字体族的不同字重之间无法区分的问题;预览现在直接按字体文件路径生成图片,避免 Qt 字体族合并导致 Medium/Bold/Heavy 切换无效,并降低切换卡顿。
5253

5354
## 📝 说明
5455

0 commit comments

Comments
 (0)