forked from tannerhelland/PhotoDemon
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pdFontCollection.cls
351 lines (278 loc) · 15.2 KB
/
pdFontCollection.cls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
Persistable = 0 'NotPersistable
DataBindingBehavior = 0 'vbNone
DataSourceBehavior = 0 'vbNone
MTSTransactionMode = 0 'NotAnMTSObject
END
Attribute VB_Name = "pdFontCollection"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
'***************************************************************************
'PhotoDemon Font Collection Manager
'Copyright 2014-2022 by Tanner Helland
'Created: 12/May/15
'Last updated: 29/October/15
'Last update: allow the caller to request their own font matching mode. Some font caches in the program use a uniform font size,
' but different font faces (e.g. font dropdowns). Others use the same font face, but different font sizes and styles
' (e.g. the central UI font cache). This class can now handle these different situations elegantly.
'
'PD's font selection dropdowns must manage a (potentially) huge gallery of fonts. It is not conducive to cache every font we use,
' so instead, we use a simple "round robin" approach where we keep a rotating cache of fonts, and free them in the order they
' were created if more fonts are required.
'
'This class wraps pdFont for specific font creation/destruction and rendering duties.
'
'Obviously, this class relies heavily on WAPI. Functions are documented to the best of my knowledge and ability.
'
'Unless otherwise noted, all source code in this file is shared under a simplified BSD license.
' Full license details are available in the LICENSE.md file, or at https://photodemon.org/license/
'
'***************************************************************************
Option Explicit
'Different parts of PD have different caching requirements. Sometimes, they need to cache fonts that differ only by name
' (but not by size). Other times, fonts may have matching names but different sizes or styles. To accelerate the
' cumbersome process of font-matching, the caller can specify a caching mode.
Public Enum FONT_CACHE_MODE
FCM_NameOnly = 0
FCM_SizeOnly = 1
FCM_NameAndSize = 2
FCM_SizeAndStyle = 3
FCM_NameAndSizeAndStyle = 4
End Enum
Private m_CurrentCacheMode As FONT_CACHE_MODE
'Size of the font collection. This is the maximum number of fonts this class is allowed to cache. For scrollable
' UI elements with variable font faces (e.g. font dropdowns), this should be at least 3x the size of the dropped list;
' that gives us some breathing room to cache new fonts during mousewheel events.
Private Const DEFAULT_CACHE_SIZE As Long = 24
Private m_CacheSize As Long
'Pointer to the current font index. This will rotate around to 0 after the cache is filled.
Private m_FontPointer As Long
'Once the cache has been completely filled, this will be set to TRUE. A full cache starts using font access times
' to determine which fonts are dumped from the cache.
Private m_CacheIsFull As Boolean
'Actual font collection
Private m_FontCollection() As pdFont
'If/when the font collection hits its maximum size, we clear out the oldest fonts first when making room for
' new fonts. (This is most relevant for font dropdowns, which consume an enormous amount of font objects.)
Private m_FontAccessTimes() As Currency
'This class can (optionally) manage extended font properties as well, like which scripts are supported by a given font.
' To activate this behavior, pass TRUE to the setExtendedPropertyCaching sub.
Private m_ExtendedPropertiesActive As Boolean
Private m_ExtendedProperties() As PD_FONT_PROPERTY
'Add a font to the cache.
' Returns: value >= 0, indicating the index of said font in the cache. (This index can be subsequently used to retrieve the
' actual handle or object.)
Friend Function AddFontToCache(ByRef fontName As String, ByVal FontSize As Single, Optional ByVal isBold As Boolean = False, Optional ByVal isItalic As Boolean = False, Optional ByVal isUnderline As Boolean = False) As Long
'First, see if this font already exists in the cache
Dim fontIndex As Long
fontIndex = DoesFontExist(fontName, FontSize, isBold, isItalic, isUnderline)
'If this font already exists in our collection, don't recreate it; instead, return its current index.
' IMPORTANT NOTE: the matching criteria used by DoesFontExist defaults to font-name matching only.
' Call SetCacheMode() if you want to match fonts by additional criteria.
If (fontIndex >= 0) Then
AddFontToCache = fontIndex
'Also, update the font's access time. (Oldest fonts are dumped from the cache first, on the assumption
' that they are needed less than new fonts.)
m_FontAccessTimes(fontIndex) = OS.GetSystemTimeAsCurrency()
Else
'This font does not already exist, meaning we need to create it anew.
'If the cache is full, we need to figure out which font to erase.
If m_CacheIsFull Then
'Search the font collection and look for the largest delta between the current time, and each font's creation time.
Dim curTime As Currency, thisDelta As Currency, maxDelta As Currency, maxIndex As Long
curTime = OS.GetSystemTimeAsCurrency()
Dim i As Long
For i = 0 To m_CacheSize - 1
thisDelta = curTime - m_FontAccessTimes(i)
If (thisDelta > maxDelta) Then
maxIndex = i
maxDelta = thisDelta
End If
Next i
'Kill the oldest font
m_FontPointer = maxIndex
'If the cache is not yet full, the font index pointer will already point at the best available spot.
End If
'Create a new font object as necessary
If (m_FontCollection(m_FontPointer) Is Nothing) Then
Set m_FontCollection(m_FontPointer) = New pdFont
Else
m_FontCollection(m_FontPointer).DeleteCurrentFont
End If
'Initialize all the required font properties
m_FontCollection(m_FontPointer).SetFontPropsAllAtOnce fontName, FontSize, isBold, isItalic, isUnderline
'Create the font object
m_FontCollection(m_FontPointer).CreateFontObject
'Update this font's access time
m_FontAccessTimes(m_FontPointer) = OS.GetSystemTimeAsCurrency()
'If extended font caching is active, retrieve those values now
If m_ExtendedPropertiesActive Then Uniscribe.GetScriptsSupportedByFont fontName, m_ExtendedProperties(m_FontPointer)
'Return this index
AddFontToCache = m_FontPointer
'Increment the font pointer, and cycle back to zero as necessary
If (Not m_CacheIsFull) Then
m_FontPointer = m_FontPointer + 1
If (m_FontPointer >= UBound(m_FontCollection)) Then
m_FontPointer = 0
m_CacheIsFull = True
End If
End If
End If
End Function
'See if a given set of font properties exists in the current cache. By default, only font face and size are currently matched.
' (In the future, it might be nice to expose an option for "comprehensive searching", which attempts to match all of a font's
' style attributes, too - bold/italic/underline, etc...?)
'
'Returns a value >= 0 if the font exists; the exact value is the index of the font in the collection.
'Returns -1 if the font does not exist.
Private Function DoesFontExist(ByRef fontName As String, Optional ByVal srcFontSize As Single = 0!, Optional ByVal isBold As Boolean = False, Optional ByVal isItalic As Boolean = False, Optional ByVal isUnderline As Boolean = False) As Long
'-1 means the requested font does not exist. If the requested font *does* exist, this will be set to a non-zero value.
DoesFontExist = -1
Dim i As Long
For i = 0 To m_CacheSize - 1
If (Not m_FontCollection(i) Is Nothing) Then
'How we match fonts depends on the current cache mode
Select Case m_CurrentCacheMode
'Size and style is the most common check in PD, because UI fonts all share the same name
' (so we can skip name checks completely, improving performance).
Case FCM_SizeAndStyle
If (srcFontSize = m_FontCollection(i).GetFontSize) Then
If (isBold = m_FontCollection(i).GetFontBold) Then
If (isItalic = m_FontCollection(i).GetFontItalic) Then
If (isUnderline = m_FontCollection(i).GetFontUnderline) Then
DoesFontExist = i
Exit For
End If
End If
End If
End If
Case FCM_NameOnly
If Strings.StringsEqual(fontName, m_FontCollection(i).GetFontFace, False) Then
DoesFontExist = i
Exit For
End If
Case FCM_SizeOnly
If (srcFontSize = m_FontCollection(i).GetFontSize) Then
DoesFontExist = i
Exit For
End If
Case FCM_NameAndSize
If Strings.StringsEqual(fontName, m_FontCollection(i).GetFontFace, False) Then
If (srcFontSize = m_FontCollection(i).GetFontSize) Then
DoesFontExist = i
Exit For
End If
End If
Case FCM_NameAndSizeAndStyle
If Strings.StringsEqual(fontName, m_FontCollection(i).GetFontFace, False) Then
If (srcFontSize = m_FontCollection(i).GetFontSize) Then
If (isBold = m_FontCollection(i).GetFontBold) Then
If (isItalic = m_FontCollection(i).GetFontItalic) Then
If (isUnderline = m_FontCollection(i).GetFontUnderline) Then
DoesFontExist = i
Exit For
End If
End If
End If
End If
End If
End Select
End If
Next i
End Function
'Given an index into the collection (returned by AddFontToCache, presumably), return the corresponding GDI font handle of that
' font object.
'
'IMPORTANT NOTE! This function does *not* bound check the passed fontIndex, for performance reasons. It is impossible to pass
' an invalid value if you use the results of AddFontToCache, above, so please do not modify that value after it's been returned.
Friend Function GetFontHandleByPosition(ByVal fontIndex As Long) As Long
GetFontHandleByPosition = m_FontCollection(fontIndex).GetFontHandle
End Function
'Given an index into the collection (returned by AddFontToCache, presumably), return the corresponding pdFont reference of that
' font object.
'
'IMPORTANT NOTE! This function does *not* bound check the passed fontIndex, for performance reasons. It is impossible to pass
' an invalid value if you use the results of AddFontToCache, above, so please do not modify that value after it's been returned.
Friend Function GetFontObjectByPosition(ByVal fontIndex As Long) As pdFont
Set GetFontObjectByPosition = m_FontCollection(fontIndex)
End Function
'Given an index into the collection (returned by AddFontToCache, presumably), return the corresponding extended font properties
' of that font object.
'
'IMPORTANT NOTE! This function does *not* bound check the passed fontIndex, for performance reasons. It is impossible to pass
' an invalid value if you use the results of AddFontToCache, above, so please do not modify that value after it's been returned.
Friend Function GetFontPropertiesByPosition(ByVal fontIndex As Long, ByRef dstProperties As PD_FONT_PROPERTY) As Boolean
If m_ExtendedPropertiesActive Then
dstProperties = m_ExtendedProperties(fontIndex)
GetFontPropertiesByPosition = True
Else
GetFontPropertiesByPosition = False
End If
End Function
'Manually reset the cache
Friend Sub ResetCache()
Dim i As Long
For i = LBound(m_FontCollection) To UBound(m_FontCollection)
If (Not m_FontCollection(i) Is Nothing) Then m_FontCollection(i).DeleteCurrentFont
Next i
m_FontPointer = 0
ReDim m_FontCollection(0 To m_CacheSize - 1) As pdFont
ReDim m_ExtendedProperties(0 To m_CacheSize - 1) As PD_FONT_PROPERTY
ReDim m_FontAccessTimes(0 To m_CacheSize - 1) As Currency
m_CacheIsFull = False
End Sub
Friend Sub SetCacheMode(Optional ByVal newMode As FONT_CACHE_MODE = FCM_NameOnly)
m_CurrentCacheMode = newMode
End Sub
'This function is completely optional, but if you have a known cache requirement, feel free to use of it.
' (Also, note that a large size doesn't hurt you unless you actually fill the cache completely. Fonts are only created as-needed.)
Friend Sub SetCacheSize(Optional ByVal newSize As Long = DEFAULT_CACHE_SIZE)
m_CacheSize = newSize
ReDim Preserve m_FontCollection(0 To newSize - 1) As pdFont
ReDim Preserve m_ExtendedProperties(0 To newSize - 1) As PD_FONT_PROPERTY
ReDim Preserve m_FontAccessTimes(0 To newSize - 1) As Currency
End Sub
'Activate (or deactivate) extended font property caching. Returns TRUE if successful; note that the function will fail on XP.
Friend Function SetExtendedPropertyCaching(ByVal newSetting As Boolean) As Boolean
'Extended properties are only available on Vista+
m_ExtendedPropertiesActive = newSetting
If OS.IsVistaOrLater And m_ExtendedPropertiesActive Then
'If any fonts are already loaded, cache them now
Dim i As Long
For i = 0 To UBound(m_FontCollection)
If Not m_FontCollection(i) Is Nothing Then
Uniscribe.GetScriptsSupportedByFont m_FontCollection(i).GetFontFace, m_ExtendedProperties(i)
End If
Next i
Else
If m_ExtendedPropertiesActive Then
Debug.Print "WARNING! Extended font properties are only available on Vista or later."
m_ExtendedPropertiesActive = False
End If
End If
End Function
Private Sub Class_Initialize()
'Initialize a default cache
Me.SetCacheSize
'By default, extended properties are not available
m_ExtendedPropertiesActive = False
'By default, fonts are matched only by name (and not by size or style)
m_CurrentCacheMode = FCM_NameOnly
'By default, the cache is not full (obviously!)
m_CacheIsFull = False
End Sub
Private Sub Class_Terminate()
If PDMain.IsProgramRunning() Then
Dim i As Long
For i = LBound(m_FontCollection) To UBound(m_FontCollection)
If (Not m_FontCollection(i) Is Nothing) Then
m_FontCollection(i).DeleteCurrentFont
Set m_FontCollection(i) = Nothing
End If
Next i
End If
End Sub