2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8 /** @file font_osx.cpp Functions related to font handling on MacOS. */
10 #include "../../stdafx.h"
11 #include "../../debug.h"
13 #include "../../core/math_func.hpp"
14 #include "../../blitter/factory.hpp"
15 #include "../../error_func.h"
16 #include "../../fileio_func.h"
17 #include "../../fontdetection.h"
18 #include "../../string_func.h"
19 #include "../../strings_func.h"
20 #include "../../zoom_func.h"
23 #include "../../table/control_codes.h"
25 #include "safeguards.h"
27 bool SetFallbackFont(FontCacheSettings
*settings
, const std::string
&language_isocode
, int, MissingGlyphSearcher
*callback
)
29 /* Determine fallback font using CoreText. This uses the language isocode
30 * to find a suitable font. CoreText is available from 10.5 onwards. */
32 if (language_isocode
== "zh_TW") {
33 /* Traditional Chinese */
35 } else if (language_isocode
== "zh_CN") {
36 /* Simplified Chinese */
39 /* Just copy the first part of the isocode. */
40 lang
= language_isocode
.substr(0, language_isocode
.find('_'));
43 /* Create a font descriptor matching the wanted language and latin (english) glyphs.
44 * Can't use CFAutoRelease here for everything due to the way the dictionary has to be created. */
45 CFStringRef lang_codes
[2];
46 lang_codes
[0] = CFStringCreateWithCString(kCFAllocatorDefault
, lang
.c_str(), kCFStringEncodingUTF8
);
47 lang_codes
[1] = CFSTR("en");
48 CFArrayRef lang_arr
= CFArrayCreate(kCFAllocatorDefault
, (const void **)lang_codes
, lengthof(lang_codes
), &kCFTypeArrayCallBacks
);
49 CFAutoRelease
<CFDictionaryRef
> lang_attribs(CFDictionaryCreate(kCFAllocatorDefault
, const_cast<const void **>(reinterpret_cast<const void *const *>(&kCTFontLanguagesAttribute
)), (const void **)&lang_arr
, 1, &kCFTypeDictionaryKeyCallBacks
, &kCFTypeDictionaryValueCallBacks
));
50 CFAutoRelease
<CTFontDescriptorRef
> lang_desc(CTFontDescriptorCreateWithAttributes(lang_attribs
.get()));
52 CFRelease(lang_codes
[0]);
54 /* Get array of all font descriptors for the wanted language. */
55 CFAutoRelease
<CFSetRef
> mandatory_attribs(CFSetCreate(kCFAllocatorDefault
, const_cast<const void **>(reinterpret_cast<const void *const *>(&kCTFontLanguagesAttribute
)), 1, &kCFTypeSetCallBacks
));
56 CFAutoRelease
<CFArrayRef
> descs(CTFontDescriptorCreateMatchingFontDescriptors(lang_desc
.get(), mandatory_attribs
.get()));
59 for (int tries
= 0; tries
< 2; tries
++) {
60 for (CFIndex i
= 0; descs
.get() != nullptr && i
< CFArrayGetCount(descs
.get()); i
++) {
61 CTFontDescriptorRef font
= (CTFontDescriptorRef
)CFArrayGetValueAtIndex(descs
.get(), i
);
63 /* Get font traits. */
64 CFAutoRelease
<CFDictionaryRef
> traits((CFDictionaryRef
)CTFontDescriptorCopyAttribute(font
, kCTFontTraitsAttribute
));
65 CTFontSymbolicTraits symbolic_traits
;
66 CFNumberGetValue((CFNumberRef
)CFDictionaryGetValue(traits
.get(), kCTFontSymbolicTrait
), kCFNumberIntType
, &symbolic_traits
);
68 /* Skip symbol fonts and vertical fonts. */
69 if ((symbolic_traits
& kCTFontClassMaskTrait
) == (CTFontStylisticClass
)kCTFontSymbolicClass
|| (symbolic_traits
& kCTFontVerticalTrait
)) continue;
70 /* Skip bold fonts (especially Arial Bold, which looks worse than regular Arial). */
71 if (symbolic_traits
& kCTFontBoldTrait
) continue;
72 /* Select monospaced fonts if asked for. */
73 if (((symbolic_traits
& kCTFontMonoSpaceTrait
) == kCTFontMonoSpaceTrait
) != callback
->Monospace()) continue;
77 CFAutoRelease
<CFStringRef
> font_name((CFStringRef
)CTFontDescriptorCopyAttribute(font
, kCTFontDisplayNameAttribute
));
78 CFStringGetCString(font_name
.get(), name
, lengthof(name
), kCFStringEncodingUTF8
);
80 /* Serif fonts usually look worse on-screen with only small
81 * font sizes. As such, we try for a sans-serif font first.
82 * If we can't find one in the first try, try all fonts. */
83 if (tries
== 0 && (symbolic_traits
& kCTFontClassMaskTrait
) != (CTFontStylisticClass
)kCTFontSansSerifClass
) continue;
85 /* There are some special fonts starting with an '.' and the last
86 * resort font that aren't usable. Skip them. */
87 if (name
[0] == '.' || strncmp(name
, "LastResort", 10) == 0) continue;
90 callback
->SetFontNames(settings
, name
);
91 if (!callback
->FindMissingGlyphs()) {
92 Debug(fontcache
, 2, "CT-Font for {}: {}", language_isocode
, name
);
100 /* For some OS versions, the font 'Arial Unicode MS' does not report all languages it
101 * supports. If we didn't find any other font, just try it, maybe we get lucky. */
102 callback
->SetFontNames(settings
, "Arial Unicode MS");
103 result
= !callback
->FindMissingGlyphs();
106 callback
->FindMissingGlyphs();
111 CoreTextFontCache::CoreTextFontCache(FontSize fs
, CFAutoRelease
<CTFontDescriptorRef
> &&font
, int pixels
) : TrueTypeFontCache(fs
, pixels
), font_desc(std::move(font
))
113 this->SetFontSize(pixels
);
117 * Reset cached glyphs.
119 void CoreTextFontCache::ClearFontCache()
121 /* GUI scaling might have changed, determine font size anew if it was automatically selected. */
122 if (this->font
) this->SetFontSize(this->req_size
);
124 this->TrueTypeFontCache::ClearFontCache();
127 void CoreTextFontCache::SetFontSize(int pixels
)
130 /* Try to determine a good height based on the height recommended by the font. */
131 int scaled_height
= ScaleGUITrad(FontCache::GetDefaultFontHeight(this->fs
));
132 pixels
= scaled_height
;
134 CFAutoRelease
<CTFontRef
> font(CTFontCreateWithFontDescriptor(this->font_desc
.get(), 0.0f
, nullptr));
136 float min_size
= 0.0f
;
138 /* The 'head' TrueType table contains information about the
139 * 'smallest readable size in pixels'. Try to read it, if
140 * that doesn't work, we use the default OS font size instead.
142 * Reference: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html */
143 CFAutoRelease
<CFDataRef
> data(CTFontCopyTable(font
.get(), kCTFontTableHead
, kCTFontTableOptionNoOptions
));
145 uint16_t lowestRecPPEM
; // At offset 46 of the 'head' TrueType table.
146 CFDataGetBytes(data
.get(), CFRangeMake(46, sizeof(lowestRecPPEM
)), (UInt8
*)&lowestRecPPEM
);
147 min_size
= CFSwapInt16BigToHost(lowestRecPPEM
); // TrueType data is always big-endian.
149 CFAutoRelease
<CFNumberRef
> size((CFNumberRef
)CTFontCopyAttribute(font
.get(), kCTFontSizeAttribute
));
150 CFNumberGetValue(size
.get(), kCFNumberFloatType
, &min_size
);
153 /* Font height is minimum height plus the difference between the default
154 * height for this font size and the small size. */
155 int diff
= scaled_height
- ScaleGUITrad(FontCache::GetDefaultFontHeight(FS_SMALL
));
156 /* Clamp() is not used as scaled_height could be greater than MAX_FONT_SIZE, which is not permitted in Clamp(). */
157 pixels
= std::min(std::max(std::min
<int>(min_size
, MAX_FONT_MIN_REC_SIZE
) + diff
, scaled_height
), MAX_FONT_SIZE
);
160 pixels
= ScaleGUITrad(pixels
);
162 this->used_size
= pixels
;
164 this->font
.reset(CTFontCreateWithFontDescriptor(this->font_desc
.get(), pixels
, nullptr));
166 /* Query the font metrics we needed. We generally round all values up to
167 * make sure we don't inadvertently cut off a row or column of pixels,
168 * except when determining glyph to glyph advances. */
169 this->ascender
= (int)std::ceil(CTFontGetAscent(this->font
.get()));
170 this->descender
= -(int)std::ceil(CTFontGetDescent(this->font
.get()));
171 this->height
= this->ascender
- this->descender
;
173 /* Get real font name. */
175 CFAutoRelease
<CFStringRef
> font_name((CFStringRef
)CTFontCopyAttribute(this->font
.get(), kCTFontDisplayNameAttribute
));
176 CFStringGetCString(font_name
.get(), name
, lengthof(name
), kCFStringEncodingUTF8
);
177 this->font_name
= name
;
179 Debug(fontcache
, 2, "Loaded font '{}' with size {}", this->font_name
, pixels
);
182 GlyphID
CoreTextFontCache::MapCharToGlyph(char32_t key
, bool allow_fallback
)
184 assert(IsPrintable(key
));
186 /* Convert characters outside of the Basic Multilingual Plane into surrogate pairs. */
188 if (key
>= 0x010000U
) {
189 chars
[0] = (UniChar
)(((key
- 0x010000U
) >> 10) + 0xD800);
190 chars
[1] = (UniChar
)(((key
- 0x010000U
) & 0x3FF) + 0xDC00);
192 chars
[0] = (UniChar
)(key
& 0xFFFF);
195 CGGlyph glyph
[2] = {0, 0};
196 if (CTFontGetGlyphsForCharacters(this->font
.get(), chars
, glyph
, key
>= 0x010000U
? 2 : 1)) {
200 if (allow_fallback
&& key
>= SCC_SPRITE_START
&& key
<= SCC_SPRITE_END
) {
201 return this->parent
->MapCharToGlyph(key
);
207 const Sprite
*CoreTextFontCache::InternalGetGlyph(GlyphID key
, bool use_aa
)
209 /* Get glyph size. */
210 CGGlyph glyph
= (CGGlyph
)key
;
211 CGRect bounds
= CGRectNull
;
212 if (MacOSVersionIsAtLeast(10, 8, 0)) {
213 bounds
= CTFontGetOpticalBoundsForGlyphs(this->font
.get(), &glyph
, nullptr, 1, 0);
215 bounds
= CTFontGetBoundingRectsForGlyphs(this->font
.get(), kCTFontOrientationDefault
, &glyph
, nullptr, 1);
217 if (CGRectIsNull(bounds
)) UserError("Unable to render font glyph");
219 uint bb_width
= (uint
)std::ceil(bounds
.size
.width
) + 1; // Sometimes the glyph bounds are too tight and cut of the last pixel after rounding.
220 uint bb_height
= (uint
)std::ceil(bounds
.size
.height
);
222 /* Add 1 scaled pixel for the shadow on the medium font. Our sprite must be at least 1x1 pixel. */
223 uint shadow
= (this->fs
== FS_NORMAL
) ? ScaleGUITrad(1) : 0;
224 uint width
= std::max(1U, bb_width
+ shadow
);
225 uint height
= std::max(1U, bb_height
+ shadow
);
227 /* Limit glyph size to prevent overflows later on. */
228 if (width
> MAX_GLYPH_DIM
|| height
> MAX_GLYPH_DIM
) UserError("Font glyph is too large");
230 SpriteLoader::SpriteCollection spritecollection
;
231 SpriteLoader::Sprite
&sprite
= spritecollection
[ZOOM_LVL_MIN
];
232 sprite
.AllocateData(ZOOM_LVL_MIN
, width
* height
);
233 sprite
.type
= SpriteType::Font
;
234 sprite
.colours
= (use_aa
? SCC_PAL
| SCC_ALPHA
: SCC_PAL
);
235 sprite
.width
= width
;
236 sprite
.height
= height
;
237 sprite
.x_offs
= (int16_t)std::round(CGRectGetMinX(bounds
));
238 sprite
.y_offs
= this->ascender
- (int16_t)std::ceil(CGRectGetMaxY(bounds
));
240 if (bounds
.size
.width
> 0) {
241 /* Glyph is not a white-space glyph. Render it to a bitmap context. */
243 /* We only need the alpha channel, as we apply our own colour constants to the sprite. */
244 int pitch
= Align(bb_width
, 16);
245 CFAutoRelease
<CGContextRef
> context(CGBitmapContextCreate(nullptr, bb_width
, bb_height
, 8, pitch
, nullptr, kCGImageAlphaOnly
));
246 const uint8_t *bmp
= static_cast<uint8_t *>(CGBitmapContextGetData(context
.get()));
247 /* Set antialias according to requirements. */
248 CGContextSetAllowsAntialiasing(context
.get(), use_aa
);
249 CGContextSetAllowsFontSubpixelPositioning(context
.get(), use_aa
);
250 CGContextSetAllowsFontSubpixelQuantization(context
.get(), !use_aa
);
251 CGContextSetShouldSmoothFonts(context
.get(), false);
253 CGPoint pos
{-bounds
.origin
.x
, -bounds
.origin
.y
};
254 CTFontDrawGlyphs(this->font
.get(), &glyph
, &pos
, 1, context
.get());
256 /* Draw shadow for medium size. */
257 if (this->fs
== FS_NORMAL
&& !use_aa
) {
258 for (uint y
= 0; y
< bb_height
; y
++) {
259 for (uint x
= 0; x
< bb_width
; x
++) {
260 if (bmp
[y
* pitch
+ x
] > 0) {
261 sprite
.data
[shadow
+ x
+ (shadow
+ y
) * sprite
.width
].m
= SHADOW_COLOUR
;
262 sprite
.data
[shadow
+ x
+ (shadow
+ y
) * sprite
.width
].a
= use_aa
? bmp
[x
+ y
* pitch
] : 0xFF;
268 /* Extract pixel data. */
269 for (uint y
= 0; y
< bb_height
; y
++) {
270 for (uint x
= 0; x
< bb_width
; x
++) {
271 if (bmp
[y
* pitch
+ x
] > 0) {
272 sprite
.data
[x
+ y
* sprite
.width
].m
= FACE_COLOUR
;
273 sprite
.data
[x
+ y
* sprite
.width
].a
= use_aa
? bmp
[x
+ y
* pitch
] : 0xFF;
279 UniquePtrSpriteAllocator allocator
;
280 BlitterFactory::GetCurrentBlitter()->Encode(spritecollection
, allocator
);
282 GlyphEntry new_glyph
;
283 new_glyph
.data
= std::move(allocator
.data
);
284 new_glyph
.width
= (uint8_t)std::round(CTFontGetAdvancesForGlyphs(this->font
.get(), kCTFontOrientationDefault
, &glyph
, nullptr, 1));
286 return this->SetGlyphPtr(key
, std::move(new_glyph
)).GetSprite();
289 static CTFontDescriptorRef
LoadFontFromFile(const std::string
&font_name
)
291 if (!MacOSVersionIsAtLeast(10, 6, 0)) return nullptr;
293 /* Might be a font file name, try load it. Direct font loading is
294 * only supported starting on OSX 10.6. */
295 CFAutoRelease
<CFStringRef
> path
;
297 /* See if this is an absolute path. */
298 if (FileExists(font_name
)) {
299 path
.reset(CFStringCreateWithCString(kCFAllocatorDefault
, font_name
.c_str(), kCFStringEncodingUTF8
));
301 /* Scan the search-paths to see if it can be found. */
302 std::string full_font
= FioFindFullPath(BASE_DIR
, font_name
);
303 if (!full_font
.empty()) {
304 path
.reset(CFStringCreateWithCString(kCFAllocatorDefault
, full_font
.c_str(), kCFStringEncodingUTF8
));
309 /* Try getting a font descriptor to see if the system can use it. */
310 CFAutoRelease
<CFURLRef
> url(CFURLCreateWithFileSystemPath(kCFAllocatorDefault
, path
.get(), kCFURLPOSIXPathStyle
, false));
311 CFAutoRelease
<CFArrayRef
> descs(CTFontManagerCreateFontDescriptorsFromURL(url
.get()));
313 if (descs
&& CFArrayGetCount(descs
.get()) > 0) {
314 CTFontDescriptorRef font_ref
= (CTFontDescriptorRef
)CFArrayGetValueAtIndex(descs
.get(), 0);
324 * Loads the TrueType font.
325 * If a CoreText font description is present, e.g. from the automatic font
326 * fallback search, use it. Otherwise, try to resolve it by font name.
327 * @param fs The font size to load.
329 void LoadCoreTextFont(FontSize fs
)
331 FontCacheSubSetting
*settings
= GetFontCacheSubSetting(fs
);
333 std::string font
= GetFontCacheFontName(fs
);
334 if (font
.empty()) return;
336 CFAutoRelease
<CTFontDescriptorRef
> font_ref
;
338 if (settings
->os_handle
!= nullptr) {
339 font_ref
.reset(static_cast<CTFontDescriptorRef
>(const_cast<void *>(settings
->os_handle
)));
340 CFRetain(font_ref
.get()); // Increase ref count to match a later release.
343 if (!font_ref
&& MacOSVersionIsAtLeast(10, 6, 0)) {
344 /* Might be a font file name, try load it. */
345 font_ref
.reset(LoadFontFromFile(font
));
346 if (!font_ref
) ShowInfo("Unable to load file '{}' for {} font, using default OS font selection instead", font
, FontSizeToName(fs
));
350 CFAutoRelease
<CFStringRef
> name(CFStringCreateWithCString(kCFAllocatorDefault
, font
.c_str(), kCFStringEncodingUTF8
));
352 /* Simply creating the font using CTFontCreateWithNameAndSize will *always* return
353 * something, no matter the name. As such, we can't use it to check for existence.
354 * We instead query the list of all font descriptors that match the given name which
355 * does not do this stupid name fallback. */
356 CFAutoRelease
<CTFontDescriptorRef
> name_desc(CTFontDescriptorCreateWithNameAndSize(name
.get(), 0.0));
357 CFAutoRelease
<CFSetRef
> mandatory_attribs(CFSetCreate(kCFAllocatorDefault
, const_cast<const void **>(reinterpret_cast<const void * const *>(&kCTFontNameAttribute
)), 1, &kCFTypeSetCallBacks
));
358 CFAutoRelease
<CFArrayRef
> descs(CTFontDescriptorCreateMatchingFontDescriptors(name_desc
.get(), mandatory_attribs
.get()));
360 /* Assume the first result is the one we want. */
361 if (descs
&& CFArrayGetCount(descs
.get()) > 0) {
362 font_ref
.reset((CTFontDescriptorRef
)CFArrayGetValueAtIndex(descs
.get(), 0));
363 CFRetain(font_ref
.get());
368 ShowInfo("Unable to use '{}' for {} font, using sprite font instead", font
, FontSizeToName(fs
));
372 new CoreTextFontCache(fs
, std::move(font_ref
), GetFontCacheFontSize(fs
));