66import time
77
88from PyQt6 .QtCore import Qt
9- from PyQt6 .QtGui import QBrush , QColor , QFontDatabase , QRawFont
9+ from PyQt6 .QtGui import QBrush , QColor , QImage , QPixmap
1010from PyQt6 .QtWidgets import (
1111 QButtonGroup ,
1212 QFileDialog ,
2929 QVBoxLayout ,
3030 QWidget ,
3131)
32+ from PIL import Image , ImageDraw , ImageFont
3233from utils .app_version import format_version_label
3334from utils .resource_helper import resource_path
3435from 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
122122def _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
185212def 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 )
0 commit comments