OpenTTD Source 20251213-master-g1091fa6071
font_osx.cpp
Go to the documentation of this file.
1/*
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 <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
6 */
7
10#include "../../stdafx.h"
11#include "../../debug.h"
12#include "font_osx.h"
13#include "../../core/math_func.hpp"
14#include "../../blitter/factory.hpp"
15#include "../../error_func.h"
16#include "../../fileio_func.h"
17#include "../../string_func.h"
18#include "../../strings_func.h"
19#include "../../zoom_func.h"
20#include "macos.h"
21
22#include "../../table/control_codes.h"
23
24#include "../../safeguards.h"
25
26CoreTextFontCache::CoreTextFontCache(FontSize fs, CFAutoRelease<CTFontDescriptorRef> &&font, int pixels) : TrueTypeFontCache(fs, pixels), font_desc(std::move(font))
27{
28 this->SetFontSize(pixels);
29}
30
35{
36 /* GUI scaling might have changed, determine font size anew if it was automatically selected. */
37 if (this->font) this->SetFontSize(this->req_size);
38
40}
41
42void CoreTextFontCache::SetFontSize(int pixels)
43{
44 if (pixels == 0) {
45 /* Try to determine a good height based on the height recommended by the font. */
46 int scaled_height = ScaleGUITrad(FontCache::GetDefaultFontHeight(this->fs));
47 pixels = scaled_height;
48
49 CFAutoRelease<CTFontRef> font(CTFontCreateWithFontDescriptor(this->font_desc.get(), 0.0f, nullptr));
50 if (font) {
51 float min_size = 0.0f;
52
53 /* The 'head' TrueType table contains information about the
54 * 'smallest readable size in pixels'. Try to read it, if
55 * that doesn't work, we use the default OS font size instead.
56 *
57 * Reference: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html */
58 CFAutoRelease<CFDataRef> data(CTFontCopyTable(font.get(), kCTFontTableHead, kCTFontTableOptionNoOptions));
59 if (data) {
60 uint16_t lowestRecPPEM; // At offset 46 of the 'head' TrueType table.
61 CFDataGetBytes(data.get(), CFRangeMake(46, sizeof(lowestRecPPEM)), (UInt8 *)&lowestRecPPEM);
62 min_size = CFSwapInt16BigToHost(lowestRecPPEM); // TrueType data is always big-endian.
63 } else {
64 CFAutoRelease<CFNumberRef> size((CFNumberRef)CTFontCopyAttribute(font.get(), kCTFontSizeAttribute));
65 CFNumberGetValue(size.get(), kCFNumberFloatType, &min_size);
66 }
67
68 /* Font height is minimum height plus the difference between the default
69 * height for this font size and the small size. */
70 int diff = scaled_height - ScaleGUITrad(FontCache::GetDefaultFontHeight(FS_SMALL));
71 /* Clamp() is not used as scaled_height could be greater than MAX_FONT_SIZE, which is not permitted in Clamp(). */
72 pixels = std::min(std::max(std::min<int>(min_size, MAX_FONT_MIN_REC_SIZE) + diff, scaled_height), MAX_FONT_SIZE);
73 }
74 } else {
75 pixels = ScaleGUITrad(pixels);
76 }
77 this->used_size = pixels;
78
79 this->font.reset(CTFontCreateWithFontDescriptor(this->font_desc.get(), pixels, nullptr));
80
81 /* Query the font metrics we needed. We generally round all values up to
82 * make sure we don't inadvertently cut off a row or column of pixels,
83 * except when determining glyph to glyph advances. */
84 this->ascender = (int)std::ceil(CTFontGetAscent(this->font.get()));
85 this->descender = -(int)std::ceil(CTFontGetDescent(this->font.get()));
86 this->height = this->ascender - this->descender;
87
88 /* Get real font name. */
89 char name[128];
90 CFAutoRelease<CFStringRef> font_name((CFStringRef)CTFontCopyAttribute(this->font.get(), kCTFontDisplayNameAttribute));
91 CFStringGetCString(font_name.get(), name, lengthof(name), kCFStringEncodingUTF8);
92 this->font_name = name;
93
94 Debug(fontcache, 2, "Loaded font '{}' with size {}", this->font_name, pixels);
95}
96
98{
99 assert(IsPrintable(key));
100
101 /* Convert characters outside of the Basic Multilingual Plane into surrogate pairs. */
102 UniChar chars[2];
103 if (key >= 0x010000U) {
104 chars[0] = (UniChar)(((key - 0x010000U) >> 10) + 0xD800);
105 chars[1] = (UniChar)(((key - 0x010000U) & 0x3FF) + 0xDC00);
106 } else {
107 chars[0] = (UniChar)(key & 0xFFFF);
108 }
109
110 CGGlyph glyph[2] = {0, 0};
111 if (CTFontGetGlyphsForCharacters(this->font.get(), chars, glyph, key >= 0x010000U ? 2 : 1)) {
112 return glyph[0];
113 }
114
115 return 0;
116}
117
118const Sprite *CoreTextFontCache::InternalGetGlyph(GlyphID key, bool use_aa)
119{
120 /* Get glyph size. */
121 CGGlyph glyph = (CGGlyph)key;
122 CGRect bounds = CGRectNull;
123 if (MacOSVersionIsAtLeast(10, 8, 0)) {
124 bounds = CTFontGetOpticalBoundsForGlyphs(this->font.get(), &glyph, nullptr, 1, 0);
125 } else {
126 bounds = CTFontGetBoundingRectsForGlyphs(this->font.get(), kCTFontOrientationDefault, &glyph, nullptr, 1);
127 }
128 if (CGRectIsNull(bounds)) UserError("Unable to render font glyph");
129
130 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.
131 uint bb_height = (uint)std::ceil(bounds.size.height);
132
133 /* Add 1 scaled pixel for the shadow on the medium font. Our sprite must be at least 1x1 pixel. */
134 uint shadow = (this->fs == FS_NORMAL) ? ScaleGUITrad(1) : 0;
135 uint width = std::max(1U, bb_width + shadow);
136 uint height = std::max(1U, bb_height + shadow);
137
138 /* Limit glyph size to prevent overflows later on. */
139 if (width > MAX_GLYPH_DIM || height > MAX_GLYPH_DIM) UserError("Font glyph is too large");
140
141 SpriteLoader::SpriteCollection spritecollection;
142 SpriteLoader::Sprite &sprite = spritecollection[ZoomLevel::Min];
143 sprite.AllocateData(ZoomLevel::Min, width * height);
145 if (use_aa) sprite.colours.Set(SpriteComponent::Alpha);
146 sprite.width = width;
147 sprite.height = height;
148 sprite.x_offs = (int16_t)std::round(CGRectGetMinX(bounds));
149 sprite.y_offs = this->ascender - (int16_t)std::ceil(CGRectGetMaxY(bounds));
150
151 if (bounds.size.width > 0) {
152 /* Glyph is not a white-space glyph. Render it to a bitmap context. */
153
154 /* We only need the alpha channel, as we apply our own colour constants to the sprite. */
155 int pitch = Align(bb_width, 16);
156 CFAutoRelease<CGContextRef> context(CGBitmapContextCreate(nullptr, bb_width, bb_height, 8, pitch, nullptr, kCGImageAlphaOnly));
157 const uint8_t *bmp = static_cast<uint8_t *>(CGBitmapContextGetData(context.get()));
158 /* Set antialias according to requirements. */
159 CGContextSetAllowsAntialiasing(context.get(), use_aa);
160 CGContextSetAllowsFontSubpixelPositioning(context.get(), use_aa);
161 CGContextSetAllowsFontSubpixelQuantization(context.get(), !use_aa);
162 CGContextSetShouldSmoothFonts(context.get(), false);
163
164 CGPoint pos{-bounds.origin.x, -bounds.origin.y};
165 CTFontDrawGlyphs(this->font.get(), &glyph, &pos, 1, context.get());
166
167 /* Draw shadow for medium size. */
168 if (this->fs == FS_NORMAL && !use_aa) {
169 for (uint y = 0; y < bb_height; y++) {
170 for (uint x = 0; x < bb_width; x++) {
171 if (bmp[y * pitch + x] > 0) {
172 sprite.data[shadow + x + (shadow + y) * sprite.width].m = SHADOW_COLOUR;
173 sprite.data[shadow + x + (shadow + y) * sprite.width].a = use_aa ? bmp[x + y * pitch] : 0xFF;
174 }
175 }
176 }
177 }
178
179 /* Extract pixel data. */
180 for (uint y = 0; y < bb_height; y++) {
181 for (uint x = 0; x < bb_width; x++) {
182 if (bmp[y * pitch + x] > 0) {
183 sprite.data[x + y * sprite.width].m = FACE_COLOUR;
184 sprite.data[x + y * sprite.width].a = use_aa ? bmp[x + y * pitch] : 0xFF;
185 }
186 }
187 }
188 }
189
190 UniquePtrSpriteAllocator allocator;
191 BlitterFactory::GetCurrentBlitter()->Encode(SpriteType::Font, spritecollection, allocator);
192
193 GlyphEntry new_glyph;
194 new_glyph.data = std::move(allocator.data);
195 new_glyph.width = (uint8_t)std::round(CTFontGetAdvancesForGlyphs(this->font.get(), kCTFontOrientationDefault, &glyph, nullptr, 1));
196
197 return this->SetGlyphPtr(key, std::move(new_glyph)).GetSprite();
198}
199
201public:
202 CoreTextFontCacheFactory() : FontCacheFactory("coretext", "CoreText font loader") {}
203
210 std::unique_ptr<FontCache> LoadFont(FontSize fs, FontType fonttype, bool search, const std::string &font, std::span<const std::byte>) const override
211 {
212 if (fonttype != FontType::TrueType) return nullptr;
213
215
216 if (MacOSVersionIsAtLeast(10, 6, 0)) {
217 /* Might be a font file name, try load it. */
218 font_ref.reset(LoadFontFromFile(font));
219 if (!font_ref) ShowInfo("Unable to load file '{}' for {} font, using default OS font selection instead", font, FontSizeToName(fs));
220 }
221
222 if (!font_ref && search) {
223 CFAutoRelease<CFStringRef> name(CFStringCreateWithCString(kCFAllocatorDefault, font.c_str(), kCFStringEncodingUTF8));
224
225 /* Simply creating the font using CTFontCreateWithNameAndSize will *always* return
226 * something, no matter the name. As such, we can't use it to check for existence.
227 * We instead query the list of all font descriptors that match the given name which
228 * does not do this stupid name fallback. */
229 CFAutoRelease<CTFontDescriptorRef> name_desc(CTFontDescriptorCreateWithNameAndSize(name.get(), 0.0));
230 CFAutoRelease<CFSetRef> mandatory_attribs(CFSetCreate(kCFAllocatorDefault, const_cast<const void **>(reinterpret_cast<const void * const *>(&kCTFontNameAttribute)), 1, &kCFTypeSetCallBacks));
231 CFAutoRelease<CFArrayRef> descs(CTFontDescriptorCreateMatchingFontDescriptors(name_desc.get(), mandatory_attribs.get()));
232
233 /* Assume the first result is the one we want. */
234 if (descs && CFArrayGetCount(descs.get()) > 0) {
235 font_ref.reset((CTFontDescriptorRef)CFArrayGetValueAtIndex(descs.get(), 0));
236 CFRetain(font_ref.get());
237 }
238 }
239
240 if (!font_ref) {
241 ShowInfo("Unable to use '{}' for {} font, using sprite font instead", font, FontSizeToName(fs));
242 return nullptr;
243 }
244
245 return std::make_unique<CoreTextFontCache>(fs, std::move(font_ref), GetFontCacheFontSize(fs));
246 }
247
248 bool FindFallbackFont(const std::string &language_isocode, FontSizes fontsizes, MissingGlyphSearcher *callback) const override
249 {
250 /* Determine fallback font using CoreText. This uses the language isocode
251 * to find a suitable font. CoreText is available from 10.5 onwards. */
252 std::string lang;
253 if (language_isocode == "zh_TW") {
254 /* Traditional Chinese */
255 lang = "zh-Hant";
256 } else if (language_isocode == "zh_CN") {
257 /* Simplified Chinese */
258 lang = "zh-Hans";
259 } else {
260 /* Just copy the first part of the isocode. */
261 lang = language_isocode.substr(0, language_isocode.find('_'));
262 }
263
264 /* Create a font descriptor matching the wanted language and latin (english) glyphs.
265 * Can't use CFAutoRelease here for everything due to the way the dictionary has to be created. */
266 CFStringRef lang_codes[2];
267 lang_codes[0] = CFStringCreateWithCString(kCFAllocatorDefault, lang.c_str(), kCFStringEncodingUTF8);
268 lang_codes[1] = CFSTR("en");
269 CFArrayRef lang_arr = CFArrayCreate(kCFAllocatorDefault, (const void **)lang_codes, lengthof(lang_codes), &kCFTypeArrayCallBacks);
270 CFAutoRelease<CFDictionaryRef> lang_attribs(CFDictionaryCreate(kCFAllocatorDefault, const_cast<const void **>(reinterpret_cast<const void *const *>(&kCTFontLanguagesAttribute)), (const void **)&lang_arr, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
271 CFAutoRelease<CTFontDescriptorRef> lang_desc(CTFontDescriptorCreateWithAttributes(lang_attribs.get()));
272 CFRelease(lang_arr);
273 CFRelease(lang_codes[0]);
274
275 /* Get array of all font descriptors for the wanted language. */
276 CFAutoRelease<CFSetRef> mandatory_attribs(CFSetCreate(kCFAllocatorDefault, const_cast<const void **>(reinterpret_cast<const void *const *>(&kCTFontLanguagesAttribute)), 1, &kCFTypeSetCallBacks));
277 CFAutoRelease<CFArrayRef> descs(CTFontDescriptorCreateMatchingFontDescriptors(lang_desc.get(), mandatory_attribs.get()));
278
279 bool result = false;
280 for (int tries = 0; tries < 2; tries++) {
281 for (CFIndex i = 0; descs.get() != nullptr && i < CFArrayGetCount(descs.get()); i++) {
282 CTFontDescriptorRef font = (CTFontDescriptorRef)CFArrayGetValueAtIndex(descs.get(), i);
283
284 /* Get font traits. */
285 CFAutoRelease<CFDictionaryRef> traits((CFDictionaryRef)CTFontDescriptorCopyAttribute(font, kCTFontTraitsAttribute));
286 CTFontSymbolicTraits symbolic_traits;
287 CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(traits.get(), kCTFontSymbolicTrait), kCFNumberIntType, &symbolic_traits);
288
289 /* Skip symbol fonts and vertical fonts. */
290 if ((symbolic_traits & kCTFontClassMaskTrait) == (CTFontStylisticClass)kCTFontSymbolicClass || (symbolic_traits & kCTFontVerticalTrait)) continue;
291 /* Skip bold fonts (especially Arial Bold, which looks worse than regular Arial). */
292 if (symbolic_traits & kCTFontBoldTrait) continue;
293 /* Select monospaced fonts if asked for. */
294 if (((symbolic_traits & kCTFontMonoSpaceTrait) == kCTFontMonoSpaceTrait) != fontsizes.Test(FS_MONO)) continue;
295
296 /* Get font name. */
297 char buffer[128];
298 CFAutoRelease<CFStringRef> font_name((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontDisplayNameAttribute));
299 CFStringGetCString(font_name.get(), buffer, std::size(buffer), kCFStringEncodingUTF8);
300
301 /* Serif fonts usually look worse on-screen with only small
302 * font sizes. As such, we try for a sans-serif font first.
303 * If we can't find one in the first try, try all fonts. */
304 if (tries == 0 && (symbolic_traits & kCTFontClassMaskTrait) != (CTFontStylisticClass)kCTFontSansSerifClass) continue;
305
306 /* There are some special fonts starting with an '.' and the last
307 * resort font that aren't usable. Skip them. */
308 std::string_view name{buffer};
309 if (name.starts_with(".") || name.starts_with("LastResort")) continue;
310
311 /* Save result. */
312 FontCache::AddFallback(fontsizes, callback->GetLoadReason(), name);
313
314 if (callback->FindMissingGlyphs().None()) {
315 Debug(fontcache, 2, "CT-Font for {}: {}", language_isocode, name);
316 result = true;
317 break;
318 }
319 }
320 }
321
322 if (!result) {
323 /* For some OS versions, the font 'Arial Unicode MS' does not report all languages it
324 * supports. If we didn't find any other font, just try it, maybe we get lucky. */
325 FontCache::AddFallback(fontsizes, callback->GetLoadReason(), "Arial Unicode MS");
326 result = callback->FindMissingGlyphs().None();
327 }
328
329 callback->FindMissingGlyphs();
330 return result;
331 }
332
333private:
334 static CTFontDescriptorRef LoadFontFromFile(const std::string &font_name)
335 {
336 if (!MacOSVersionIsAtLeast(10, 6, 0)) return nullptr;
337
338 /* Might be a font file name, try load it. Direct font loading is
339 * only supported starting on OSX 10.6. */
341
342 /* See if this is an absolute path. */
343 if (FileExists(font_name)) {
344 path.reset(CFStringCreateWithCString(kCFAllocatorDefault, font_name.c_str(), kCFStringEncodingUTF8));
345 } else {
346 /* Scan the search-paths to see if it can be found. */
347 std::string full_font = FioFindFullPath(BASE_DIR, font_name);
348 if (!full_font.empty()) {
349 path.reset(CFStringCreateWithCString(kCFAllocatorDefault, full_font.c_str(), kCFStringEncodingUTF8));
350 }
351 }
352
353 if (path) {
354 /* Try getting a font descriptor to see if the system can use it. */
355 CFAutoRelease<CFURLRef> url(CFURLCreateWithFileSystemPath(kCFAllocatorDefault, path.get(), kCFURLPOSIXPathStyle, false));
356 CFAutoRelease<CFArrayRef> descs(CTFontManagerCreateFontDescriptorsFromURL(url.get()));
357
358 if (descs && CFArrayGetCount(descs.get()) > 0) {
359 CTFontDescriptorRef font_ref = (CTFontDescriptorRef)CFArrayGetValueAtIndex(descs.get(), 0);
360 CFRetain(font_ref);
361 return font_ref;
362 }
363 }
364
365 return nullptr;
366 }
367
368private:
369 static CoreTextFontCacheFactory instance;
370};
371
372/* static */ CoreTextFontCacheFactory CoreTextFontCacheFactory::instance;
constexpr bool Test(Tvalue_type value) const
Test if the value-th bit is set.
constexpr bool None() const
Test if none of the values are set.
constexpr Timpl & Set()
Set all bits.
static Blitter * GetCurrentBlitter()
Get the current active blitter (always set by calling SelectBlitter).
Definition factory.hpp:136
std::unique_ptr< FontCache > LoadFont(FontSize fs, FontType fonttype, bool search, const std::string &font, std::span< const std::byte >) const override
Loads the TrueType font.
Definition font_osx.cpp:210
CFAutoRelease< CTFontDescriptorRef > font_desc
Font descriptor excluding font size.
Definition font_osx.h:19
std::string font_name
Cached font name.
Definition font_osx.h:22
CFAutoRelease< CTFontRef > font
CoreText font handle.
Definition font_osx.h:20
GlyphID MapCharToGlyph(char32_t key) override
Map a character into a glyph.
Definition font_osx.cpp:97
void ClearFontCache() override
Reset cached glyphs.
Definition font_osx.cpp:34
Factory for FontCaches.
Definition fontcache.h:304
int height
The height of the font.
Definition fontcache.h:48
const FontSize fs
The size of the font.
Definition fontcache.h:45
int descender
The descender value of the font.
Definition fontcache.h:50
int ascender
The ascender value of the font.
Definition fontcache.h:49
static void AddFallback(FontSizes fontsizes, FontLoadReason load_reason, std::string_view name, std::span< const std::byte > os_data={})
Add a fallback font, with optional OS-specific handle.
A searcher for missing glyphs.
FontSizes FindMissingGlyphs()
Test if any glyphs are missing.
Definition strings.cpp:2279
Map zoom level to data.
virtual Sprite * Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator)=0
Convert a sprite from the loader to our own format.
Font cache for fonts that are based on a TrueType font.
static constexpr int MAX_GLYPH_DIM
Maximum glyph dimensions.
int used_size
Used font size.
int req_size
Requested font size.
void ClearFontCache() override
Reset cached glyphs.
static constexpr uint MAX_FONT_MIN_REC_SIZE
Upper limit for the recommended font size in case a font file contains nonsensical values.
SpriteAllocator that allocates memory via a unique_ptr array.
Definition spritecache.h:20
#define Debug(category, level, format_string,...)
Output a line of debugging information.
Definition debug.h:37
bool FileExists(std::string_view filename)
Test whether the given filename exists.
Definition fileio.cpp:132
std::string FioFindFullPath(Subdirectory subdir, std::string_view filename)
Find a path to the filename in one of the search directories.
Definition fileio.cpp:144
@ BASE_DIR
Base directory for all subdirectories.
Definition fileio_type.h:89
Functions related to font handling on MacOS.
uint GetFontCacheFontSize(FontSize fs)
Get the scalable font size to use for a FontSize.
FontType
Different types of font that can be loaded.
Definition fontcache.h:298
@ TrueType
Scalable TrueType fonts.
uint32_t GlyphID
Glyphs are characters from a font.
Definition fontcache.h:18
@ Font
A sprite used for fonts.
FontSize
Available font sizes.
Definition gfx_type.h:248
@ FS_MONO
Index of the monospaced font in the font tables.
Definition gfx_type.h:252
@ FS_SMALL
Index of the small font in the font tables.
Definition gfx_type.h:250
@ FS_NORMAL
Index of the normal font in the font tables.
Definition gfx_type.h:249
Functions related to MacOS support.
std::unique_ptr< typename std::remove_pointer< T >::type, CFDeleter< typename std::remove_pointer< T >::type > > CFAutoRelease
Specialisation of std::unique_ptr for CoreFoundation objects.
Definition macos.h:54
bool MacOSVersionIsAtLeast(long major, long minor, long bugfix)
Check if we are at least running on the specified version of Mac OS.
Definition macos.h:25
constexpr T Align(const T x, uint n)
Return the smallest multiple of n equal or greater than x.
Definition math_func.hpp:37
@ Palette
Sprite has palette data.
@ Alpha
Sprite has alpha.
#define lengthof(array)
Return the length of an fixed size array.
Definition stdafx.h:271
uint8_t m
Remap-channel.
uint8_t a
Alpha-channel.
Structure for passing information from the sprite loader to the blitter.
SpriteComponents colours
The colour components of the sprite with useful information.
void AllocateData(ZoomLevel zoom, size_t size)
Allocate the sprite data of this sprite.
uint16_t width
Width of the sprite.
int16_t x_offs
The x-offset of where the sprite will be drawn.
SpriteLoader::CommonPixel * data
The sprite itself.
uint16_t height
Height of the sprite.
int16_t y_offs
The y-offset of where the sprite will be drawn.
Data structure describing a sprite.
std::byte data[]
Sprite data.
static const int MAX_FONT_SIZE
Maximum font size.
static RectPadding ScaleGUITrad(const RectPadding &r)
Scale a RectPadding to GUI zoom level.
Definition widget.cpp:49
@ Min
Minimum zoom level.