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