1 /** @file coretextnativefont_macx.cpp
2 *
3 * @authors Copyright (c) 2014-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4 *
5 * @par License
6 * LGPL: http://www.gnu.org/licenses/lgpl.html
7 *
8 * <small>This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU Lesser General Public License as published by
10 * the Free Software Foundation; either version 3 of the License, or (at your
11 * option) any later version. This program is distributed in the hope that it
12 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
14 * General Public License for more details. You should have received a copy of
15 * the GNU Lesser General Public License along with this program; if not, see:
16 * http://www.gnu.org/licenses</small>
17 */
18
19 #include "../src/text/coretextnativefont_macx.h"
20 #include <de/Log>
21
22 #include <QFont>
23 #include <QColor>
24 #include <QThread>
25 #include <CoreGraphics/CoreGraphics.h>
26 #include <CoreText/CoreText.h>
27 #include <atomic>
28
29 namespace de {
30
31 struct CoreTextFontCache : public Lockable
32 {
33 struct Key {
34 String name;
35 dfloat size;
36
Keyde::CoreTextFontCache::Key37 Key(String const &n = "", dfloat s = 12.f) : name(n), size(s) {}
operator <de::CoreTextFontCache::Key38 bool operator < (Key const &other) const {
39 if (name == other.name) {
40 return size < other.size && !fequal(size, other.size);
41 }
42 return name < other.name;
43 }
44 };
45
46 typedef QMap<Key, CTFontRef> Fonts;
47 Fonts fonts;
48
49 CGColorSpaceRef _colorspace; ///< Shared by all fonts.
50
CoreTextFontCachede::CoreTextFontCache51 CoreTextFontCache() : _colorspace(0)
52 {}
53
~CoreTextFontCachede::CoreTextFontCache54 ~CoreTextFontCache()
55 {
56 clear();
57 if (_colorspace)
58 {
59 CGColorSpaceRelease(_colorspace);
60 }
61 }
62
colorspacede::CoreTextFontCache63 CGColorSpaceRef colorspace()
64 {
65 if (!_colorspace)
66 {
67 _colorspace = CGColorSpaceCreateDeviceRGB();
68 }
69 return _colorspace;
70 }
71
clearde::CoreTextFontCache72 void clear()
73 {
74 DENG2_GUARD(this);
75
76 foreach (CTFontRef ref, fonts.values())
77 {
78 CFRelease(ref);
79 }
80 }
81
getFontde::CoreTextFontCache82 CTFontRef getFont(String const &postScriptName, dfloat pointSize)
83 {
84 CTFontRef font;
85
86 // Only lock the cache while accessing the fonts database. If we keep the
87 // lock while printing log output, a flush might occur, which might in turn
88 // lead to text rendering and need for font information -- causing a hang.
89 {
90 DENG2_GUARD(this);
91
92 Key const key(postScriptName, pointSize);
93 if (fonts.contains(key))
94 {
95 // Already got it.
96 return fonts[key];
97 }
98
99 // Get a reference to the font.
100 CFStringRef name = CFStringCreateWithCharacters(nil, (UniChar *) postScriptName.data(),
101 postScriptName.size());
102 font = CTFontCreateWithName(name, pointSize, nil);
103 CFRelease(name);
104
105 fonts.insert(key, font);
106 }
107
108 LOG_GL_VERBOSE("Cached native font '%s' size %.1f") << postScriptName << pointSize;
109
110 return font;
111 }
112
113 #if 0
114 float fontSize(CTFontRef font) const
115 {
116 DENG2_FOR_EACH_CONST(Fonts, i, fonts)
117 {
118 if (i.value() == font) return i.key().size;
119 }
120 DENG2_ASSERT(!"Font not in cache");
121 return 0;
122 }
123
124 int fontWeight(CTFontRef font) const
125 {
126 DENG2_FOR_EACH_CONST(Fonts, i, fonts)
127 {
128 if (i.value() == font)
129 {
130 if (i.key().name.contains("Light")) return 25;
131 if (i.key().name.contains("Bold")) return 75;
132 return 50;
133 }
134 }
135 DENG2_ASSERT(!"Font not in cache");
136 return 0;
137 }
138 #endif
139 };
140
141 static CoreTextFontCache fontCache;
142
DENG2_PIMPL(CoreTextNativeFont)143 DENG2_PIMPL(CoreTextNativeFont)
144 {
145 CTFontRef font;
146 float ascent;
147 float descent;
148 float height;
149 float lineSpacing;
150
151 struct CachedLine
152 {
153 String lineText;
154 CTLineRef line = nullptr;
155
156 ~CachedLine()
157 {
158 release();
159 }
160
161 void release()
162 {
163 if (line)
164 {
165 CFRelease(line);
166 line = nullptr;
167 }
168 lineText.clear();
169 }
170 };
171 CachedLine cache;
172
173 Impl(Public *i)
174 : Base(i)
175 , font(0)
176 , ascent(0)
177 , descent(0)
178 , height(0)
179 , lineSpacing(0)
180 {}
181
182 Impl(Public *i, Impl const &other)
183 : Base(i)
184 , font(other.font)
185 , ascent(other.ascent)
186 , descent(other.descent)
187 , height(other.height)
188 , lineSpacing(other.lineSpacing)
189 {}
190
191 ~Impl()
192 {
193 release();
194 }
195
196 String applyTransformation(String const &str) const
197 {
198 switch (self().transform())
199 {
200 case Uppercase:
201 return str.toUpper();
202
203 case Lowercase:
204 return str.toLower();
205
206 default:
207 break;
208 }
209 return str;
210 }
211
212 void release()
213 {
214 font = 0;
215 cache.release();
216 }
217
218 void updateFontAndMetrics()
219 {
220 release();
221
222 // Get a reference to the font.
223 font = fontCache.getFont(self().nativeFontName(), self().size());
224
225 // Get basic metrics about the font.
226 ascent = ceil(CTFontGetAscent(font));
227 descent = ceil(CTFontGetDescent(font));
228 height = ascent + descent;
229 lineSpacing = height + CTFontGetLeading(font);
230 }
231
232 CachedLine &makeLine(String const &text, CGColorRef color = 0)
233 {
234 if (cache.lineText == text)
235 {
236 return cache; // Already got it.
237 }
238
239 cache.release();
240 cache.lineText = text;
241
242 void const *keys[] = { kCTFontAttributeName, kCTForegroundColorAttributeName };
243 void const *values[] = { font, color };
244 CFDictionaryRef attribs = CFDictionaryCreate(nil, keys, values,
245 color? 2 : 1, nil, nil);
246
247 CFStringRef textStr = CFStringCreateWithCharacters(nil, (UniChar *) text.data(), text.size());
248 CFAttributedStringRef as = CFAttributedStringCreate(0, textStr, attribs);
249 cache.line = CTLineCreateWithAttributedString(as);
250
251 CFRelease(attribs);
252 CFRelease(textStr);
253 CFRelease(as);
254 return cache;
255 }
256 };
257
258 } // namespace de
259
260 namespace de {
261
CoreTextNativeFont(String const & family)262 CoreTextNativeFont::CoreTextNativeFont(String const &family)
263 : NativeFont(family), d(new Impl(this))
264 {}
265
CoreTextNativeFont(QFont const & font)266 CoreTextNativeFont::CoreTextNativeFont(QFont const &font)
267 : NativeFont(font.family()), d(new Impl(this))
268 {
269 setSize (font.pointSizeF());
270 setWeight (font.weight());
271 setStyle (font.italic()? Italic : Regular);
272 setTransform(font.capitalization() == QFont::AllUppercase? Uppercase :
273 font.capitalization() == QFont::AllLowercase? Lowercase : NoTransform);
274 }
275
CoreTextNativeFont(CoreTextNativeFont const & other)276 CoreTextNativeFont::CoreTextNativeFont(CoreTextNativeFont const &other)
277 : NativeFont(other), d(new Impl(this, *other.d))
278 {
279 // If the other is ready, this will be too.
280 setState(other.state());
281 }
282
operator =(CoreTextNativeFont const & other)283 CoreTextNativeFont &CoreTextNativeFont::operator = (CoreTextNativeFont const &other)
284 {
285 NativeFont::operator = (other);
286 d.reset(new Impl(this, *other.d));
287 // If the other is ready, this will be too.
288 setState(other.state());
289 return *this;
290 }
291
commit() const292 void CoreTextNativeFont::commit() const
293 {
294 d->updateFontAndMetrics();
295 }
296
nativeFontAscent() const297 int CoreTextNativeFont::nativeFontAscent() const
298 {
299 return roundi(d->ascent);
300 }
301
nativeFontDescent() const302 int CoreTextNativeFont::nativeFontDescent() const
303 {
304 return roundi(d->descent);
305 }
306
nativeFontHeight() const307 int CoreTextNativeFont::nativeFontHeight() const
308 {
309 return roundi(d->height);
310 }
311
nativeFontLineSpacing() const312 int CoreTextNativeFont::nativeFontLineSpacing() const
313 {
314 return roundi(d->lineSpacing);
315 }
316
nativeFontMeasure(String const & text) const317 Rectanglei CoreTextNativeFont::nativeFontMeasure(String const &text) const
318 {
319 d->makeLine(d->applyTransformation(text));
320
321 //CGLineGetImageBounds(d->line, d->gc); // more accurate but slow
322
323 Rectanglei rect(Vector2i(0, -d->ascent),
324 Vector2i(roundi(CTLineGetTypographicBounds(d->cache.line, NULL, NULL, NULL)),
325 d->descent));
326
327 return rect;
328 }
329
nativeFontWidth(String const & text) const330 int CoreTextNativeFont::nativeFontWidth(String const &text) const
331 {
332 auto &cachedLine = d->makeLine(d->applyTransformation(text));
333 return roundi(CTLineGetTypographicBounds(cachedLine.line, NULL, NULL, NULL));
334 }
335
nativeFontRasterize(String const & text,Vector4ub const & foreground,Vector4ub const & background) const336 QImage CoreTextNativeFont::nativeFontRasterize(String const &text,
337 Vector4ub const &foreground,
338 Vector4ub const &background) const
339 {
340 #if 0
341 DENG2_ASSERT(fequal(fontCache.fontSize(d->font), size()));
342 DENG2_ASSERT(fontCache.fontWeight(d->font) == weight());
343 #endif
344
345 // Text color.
346 Vector4d const fg = foreground.zyxw().toVector4f() / 255.f;
347 CGFloat fgValues[4] = { fg.x, fg.y, fg.z, fg.w };
348 CGColorRef fgColor = CGColorCreate(fontCache.colorspace(), fgValues);
349
350 // Ensure the color is used by recreating the attributed line string.
351 d->cache.release();
352 d->makeLine(d->applyTransformation(text), fgColor);
353
354 // Set up the bitmap for drawing into.
355 Rectanglei const bounds = measure(d->cache.lineText);
356 QImage backbuffer(QSize(bounds.width(), bounds.height()), QImage::Format_ARGB32);
357 backbuffer.fill(QColor(background.x, background.y, background.z, background.w).rgba());
358
359 CGContextRef gc = CGBitmapContextCreate(backbuffer.bits(),
360 backbuffer.width(),
361 backbuffer.height(),
362 8, 4 * backbuffer.width(),
363 fontCache.colorspace(),
364 kCGImageAlphaPremultipliedLast);
365
366 CGContextSetTextPosition(gc, 0, d->descent);
367 CTLineDraw(d->cache.line, gc);
368
369 CGColorRelease(fgColor);
370 CGContextRelease(gc);
371 d->cache.release();
372
373 return backbuffer;
374 }
375
376 } // namespace de
377