Vuo  2.3.2
VuoImageText.cc
Go to the documentation of this file.
1 
10 #include "module.h"
11 #include "VuoImageText.h"
12 
13 #include <OpenGL/CGLMacro.h>
14 
15 #include <ApplicationServices/ApplicationServices.h>
16 
17 extern "C" {
18 #ifdef VUO_COMPILER
20 "title" : "VuoImageText",
21 "dependencies" : [
22  "VuoImage",
23  "VuoFont",
24  "ApplicationServices.framework"
25  ]
26  });
27 #endif
28 }
29 
30 #include <map>
31 #include <string>
32 #include <vector>
33 
38 {
39 public:
42  float verticalScale;
43  float rotation;
44  float wrapWidth;
45 
51  {
53  }
57  VuoFontClass(const VuoFontClass &font) :
59  {
61  }
66  {
68  }
69 };
70 
74 bool operator<(const VuoFontClass &a, const VuoFontClass &b)
75 {
81  return false;
82 }
83 
88 {
89  VuoImageTextData i = (VuoImageTextData)malloc(sizeof(struct _VuoImageTextData));
90  i->lineCounts = nullptr;
91  i->lineBounds = nullptr;
92  i->lineWidthsExcludingTrailingWhitespace = nullptr;
93  i->lineXOrigins = nullptr;
94  i->charAdvance = nullptr;
96  return i;
97 }
98 
102 void VuoImageTextData_free(void* data)
103 {
104  VuoImageTextData value = (VuoImageTextData)data;
105  if (value->lineCounts)
106  free(value->lineCounts);
107  if (value->lineBounds)
108  free(value->lineBounds);
109  if (value->lineWidthsExcludingTrailingWhitespace)
110  free(value->lineWidthsExcludingTrailingWhitespace);
111  if (value->lineXOrigins)
112  free(value->lineXOrigins);
113  if (value->charAdvance)
114  free(value->charAdvance);
115  free(value);
116 }
117 
118 typedef std::vector<VuoPoint2d> VuoCorners;
119 typedef std::pair<std::string, VuoFontClass> VuoImageTextCacheDescriptor;
120 typedef std::pair<std::pair<VuoImage, VuoCorners>, double> VuoImageTextCacheEntry;
121 typedef std::map<VuoImageTextCacheDescriptor, VuoImageTextCacheEntry> VuoImageTextCacheType;
123 static dispatch_semaphore_t VuoImageTextCache_semaphore;
124 static dispatch_semaphore_t VuoImageTextCache_canceledAndCompleted;
125 static volatile dispatch_source_t VuoImageTextCache_timer = NULL;
126 static double VuoImageTextCache_timeout = 1.0;
127 
131 static void VuoImageTextCache_cleanup(void *blah)
132 {
133  std::vector<VuoImage> imagesToRelease;
134 
135  dispatch_semaphore_wait(VuoImageTextCache_semaphore, DISPATCH_TIME_FOREVER);
136  {
137  double now = VuoLogGetTime();
138  // VLog("cache:");
139  for (VuoImageTextCacheType::iterator item = VuoImageTextCache->begin(); item != VuoImageTextCache->end(); )
140  {
141  double lastUsed = item->second.second;
142  // VLog("\t\"%s\" %s backingScaleFactor=%g (last used %gs ago)", item->first.first.c_str(), item->first.second.first.f.fontName, item->first.second.second, now - lastUsed);
143  if (now - lastUsed > VuoImageTextCache_timeout)
144  {
145  // VLog("\t\tpurging");
146  imagesToRelease.push_back(item->second.first.first);
147  VuoImageTextCache->erase(item++);
148  }
149  else
150  ++item;
151  }
152  }
153  dispatch_semaphore_signal(VuoImageTextCache_semaphore);
154 
155  for (std::vector<VuoImage>::iterator item = imagesToRelease.begin(); item != imagesToRelease.end(); ++item)
156  VuoRelease(*item);
157 }
158 
167 extern "C" void __attribute__((constructor)) VuoImageTextCache_preinit(void)
168 {
169  VuoImageTextCache_timer = nullptr;
170 }
171 
175 static void VuoImageTextCache_init(void)
176 {
177  VuoImageTextCache_semaphore = dispatch_semaphore_create(1);
178  VuoImageTextCache_canceledAndCompleted = dispatch_semaphore_create(0);
180 
181  VuoImageTextCache_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
182  dispatch_source_set_timer(VuoImageTextCache_timer, dispatch_walltime(NULL, 0), NSEC_PER_SEC * VuoImageTextCache_timeout, NSEC_PER_SEC * VuoImageTextCache_timeout);
183  dispatch_source_set_event_handler_f(VuoImageTextCache_timer, VuoImageTextCache_cleanup);
184  dispatch_source_set_cancel_handler(VuoImageTextCache_timer, ^ {
185  dispatch_semaphore_signal(VuoImageTextCache_canceledAndCompleted);
186  });
187  dispatch_resume(VuoImageTextCache_timer);
188 }
189 
195 extern "C" void __attribute__((destructor)) VuoImageTextCache_fini(void)
196 {
198  return;
199 
200  dispatch_source_cancel(VuoImageTextCache_timer);
201 
202  // Wait for the last cleanup to complete.
203  dispatch_semaphore_wait(VuoImageTextCache_canceledAndCompleted, DISPATCH_TIME_FOREVER);
204 
205  // Clean up anything that still remains.
206  // VLog("cache:");
207  for (VuoImageTextCacheType::iterator item = VuoImageTextCache->begin(); item != VuoImageTextCache->end(); ++item)
208  {
209  // VLog("\t\"%s\" %s backingScaleFactor=%g", item->first.first.c_str(), item->first.second.first.f.fontName, item->first.second.second);
210  // VLog("\t\tpurging");
211  VuoRelease(item->second.first.first);
212  }
213 
214  delete VuoImageTextCache;
215  dispatch_release(VuoImageTextCache_timer);
217 }
218 
223 VuoReal VuoImageText_getVerticalScale(VuoReal screenWidth, VuoReal backingScaleFactor)
224 {
225  return (screenWidth / backingScaleFactor) / VuoGraphicsWindowDefaultWidth;
226 }
227 
231 static CFArrayRef VuoImageText_createCTLines(
232  VuoText text,
233  VuoFont font,
234  VuoReal backingScaleFactor,
235  VuoReal verticalScale,
236  VuoReal rotation,
237  VuoReal wrapWidth,
238  bool includeTrailingWhiteSpace,
239  CTFontRef *ctFont,
240  CGColorRef *cgColor,
241  CGColorSpaceRef *colorspace,
242  VuoImageTextData textImageData,
243  CGAffineTransform *outTransform)
244 {
245  CFStringRef fontNameCF = NULL;
246 
247  if (font.fontName)
248  fontNameCF = CFStringCreateWithCString(NULL, font.fontName, kCFStringEncodingUTF8);
249 
250  *ctFont = CTFontCreateWithName(fontNameCF ? fontNameCF : CFSTR(""), font.pointSize * backingScaleFactor, NULL);
251 
252  if (fontNameCF)
253  CFRelease(fontNameCF);
254 
255  CGFloat colorComponents[4] = {font.color.r, font.color.g, font.color.b, font.color.a};
256  *colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
257  *cgColor = CGColorCreate(*colorspace, colorComponents);
258 
259  unsigned long underline = font.underline ? kCTUnderlineStyleSingle : kCTUnderlineStyleNone;
260  CFNumberRef underlineNumber = CFNumberCreate(NULL, kCFNumberCFIndexType, &underline);
261 
262  float kern = (font.characterSpacing - 1) * font.pointSize * backingScaleFactor;
263  CFNumberRef kernNumber = CFNumberCreate(NULL, kCFNumberFloatType, &kern);
264 
265  // Create a temporary context to get the bounds.
266  CGContextRef cgContext = CGBitmapContextCreate(NULL, 1, 1, 8, 4, *colorspace, kCGImageAlphaPremultipliedLast);
267 
268  *outTransform = CGAffineTransformScale(CGAffineTransformMakeRotation(-rotation), 1., verticalScale);
269 
270  // Create a rectangle to optionally limit the text width.
271  double wrapWidthPixels = fmax(1, wrapWidth * VuoGraphicsWindowDefaultWidth/2. * backingScaleFactor);
272  CGMutablePathRef path = CGPathCreateMutable();
273  CGPathAddRect(path, NULL, CGRectMake(0, 0, wrapWidthPixels, INFINITY));
274 
275  // Split the user's text into lines, both on manually-added linebreaks and automatically word-wrapped at `wrapWidth`.
276  CFStringRef cfText = CFStringCreateWithCStringNoCopy(NULL, text, kCFStringEncodingUTF8, kCFAllocatorNull);
277  CFIndex characterCount = CFStringGetLength(cfText);
278  CFStringRef keys[] = { kCTFontAttributeName, kCTForegroundColorAttributeName, kCTUnderlineStyleAttributeName, kCTKernAttributeName };
279  CFTypeRef values[] = { *ctFont, *cgColor, underlineNumber, kernNumber };
280  CFDictionaryRef attr = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, sizeof(keys) / sizeof(keys[0]), NULL, NULL);
281  CFAttributedStringRef attrString = CFAttributedStringCreate(NULL, cfText, attr);
282  CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
283  CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0,0), path, NULL);
284  CFArrayRef ctLines = CTFrameGetLines(frame);
285  CFRetain(ctLines);
286  CFIndex lineCount = CFArrayGetCount(ctLines);
287  if (text[strlen(text) - 1] == '\n')
288  ++lineCount;
289 
290  // Get the bounds of each line of text, and union them into bounds for the entire block of text.
291  double ascent = CTFontGetAscent(*ctFont);
292  double descent = CTFontGetDescent(*ctFont);
293  double leading = CTFontGetLeading(*ctFont);
294 
295  double lineHeight = ascent + descent + leading;
296  CGRect bounds = CGRectMake(0, 0, 0, 0);
297  CGRect* lineBounds = (CGRect *)malloc(sizeof(CGRect) * lineCount);
298  unsigned int* lineCounts = (unsigned int*) malloc(sizeof(unsigned int) * lineCount);
299  textImageData->lineWidthsExcludingTrailingWhitespace = (VuoReal *)malloc(sizeof(VuoReal) * lineCount);
300  textImageData->lineXOrigins = (VuoReal *)malloc(sizeof(VuoReal) * lineCount);
301  VuoReal* charAdvance = (VuoReal*) calloc(characterCount, sizeof(VuoReal));
302 
303  for (CFIndex i = 0; i < lineCount; ++i)
304  {
305  // Run through the loop body for the empty trailing newline (if any), adding normal lineheight with zero width.
306  CTLineRef ctLine = nullptr;
307  if (i < CFArrayGetCount(ctLines))
308  ctLine = (CTLineRef) CFArrayGetValueAtIndex(ctLines, i);
309 
310  CFRange stringRange = ctLine ? CTLineGetStringRange(ctLine) : CFRangeMake(0,0);
311  lineCounts[i] = stringRange.length;
312 
313  // get each individual character offset
314  CGFloat secondaryOffset;
315  CGFloat previousOffset = ctLine ? CTLineGetOffsetForStringIndex(ctLine, stringRange.location, &secondaryOffset ) : 0;
316 
317  for(CFIndex index = stringRange.location; index < stringRange.location + stringRange.length; index++)
318  {
319  CGFloat offset = CTLineGetOffsetForStringIndex(ctLine, MIN(stringRange.location + stringRange.length, index + 1), &secondaryOffset );
320  charAdvance[index] = (VuoReal) (offset - previousOffset);
321  previousOffset = offset;
322  }
323 
324  CGRect lineImageBounds = ctLine ? CTLineGetImageBounds(ctLine, cgContext) : CGRectZero;
325  double width = CGRectGetWidth(lineImageBounds);
326  textImageData->lineWidthsExcludingTrailingWhitespace[i] = lineImageBounds.size.width;
327  textImageData->lineXOrigins[i] = lineImageBounds.origin.x;
328  if (includeTrailingWhiteSpace && ctLine)
329  width += CTLineGetTrailingWhitespaceWidth(ctLine);
330  lineBounds[i] = CGRectMake(CGRectGetMinX(lineImageBounds), lineHeight * i - ascent, width, lineHeight);
331 
332  // Can't use CGRectUnion since it shifts the origin to (0,0), cutting off the glyph's ascent and strokes left of the origin (e.g., Zapfino's "g").
333  if (CGRectGetMinX(lineBounds[i]) < CGRectGetMinX(bounds))
334  bounds.origin.x = CGRectGetMinX(lineBounds[i]);
335  if (CGRectGetMinY(lineBounds[i]) < CGRectGetMinY(bounds))
336  bounds.origin.y = CGRectGetMinY(lineBounds[i]);
337  if (CGRectGetMaxX(lineBounds[i]) > CGRectGetMaxX(bounds))
338  bounds.size.width += CGRectGetMaxX(lineBounds[i]) - CGRectGetMaxX(bounds);
339 
340  // Final bounds should always include the full first line's height.
341  if (i == 0)
342  bounds.size.height += lineHeight;
343  else
344  bounds.size.height += lineHeight * font.lineSpacing;
345  }
346 
347  // The 2 extra pixels are to account for the antialiasing on strokes that touch the edge of the glyph bounds — without those pixels, some edge strokes are slightly cut off.
348  const unsigned int AA_STROKE_PAD = 2;
349 
350  CGRect transformedBounds = CGRectApplyAffineTransform(bounds, *outTransform);
351  CGPoint transformedTopLeft = CGPointApplyAffineTransform(bounds.origin, *outTransform);
352  CGPoint transformedTopRight = CGPointApplyAffineTransform(CGPointMake(bounds.origin.x+bounds.size.width, bounds.origin.y), *outTransform);
353  CGPoint transformedBottomRight = CGPointApplyAffineTransform(CGPointMake(bounds.origin.x+bounds.size.width, bounds.origin.y+bounds.size.height), *outTransform);
354  CGPoint transformedBottomLeft = CGPointApplyAffineTransform(CGPointMake(bounds.origin.x, bounds.origin.y+bounds.size.height), *outTransform);
355 
356  unsigned int width = ceil(CGRectGetWidth(transformedBounds)) + AA_STROKE_PAD;
357  unsigned int height = ceil(CGRectGetHeight(transformedBounds)) + AA_STROKE_PAD;
358 
359  textImageData->width = width;
360  textImageData->height = height;
361  textImageData->lineHeight = lineHeight;
362  textImageData->bounds = VuoRectangle_make(CGRectGetMidX(bounds), CGRectGetMidY(bounds), CGRectGetWidth(bounds), CGRectGetHeight(bounds));
363  textImageData->transformedBounds = VuoRectangle_make(CGRectGetMidX(transformedBounds), CGRectGetMidY(transformedBounds), CGRectGetWidth(transformedBounds), CGRectGetHeight(transformedBounds));
364  textImageData->transformedCorners[0] = (VuoPoint2d){(float)(transformedTopLeft.x - transformedBounds.origin.x), (float)(transformedTopLeft.y - transformedBounds.origin.y)};
365  textImageData->transformedCorners[1] = (VuoPoint2d){(float)(transformedTopRight.x - transformedBounds.origin.x), (float)(transformedTopRight.y - transformedBounds.origin.y)};
366  textImageData->transformedCorners[2] = (VuoPoint2d){(float)(transformedBottomRight.x - transformedBounds.origin.x), (float)(transformedBottomRight.y - transformedBounds.origin.y)};
367  textImageData->transformedCorners[3] = (VuoPoint2d){(float)(transformedBottomLeft.x - transformedBounds.origin.x), (float)(transformedBottomLeft.y - transformedBounds.origin.y)};
368  textImageData->lineCount = lineCount;
369  textImageData->lineCounts = lineCounts;
370  textImageData->lineBounds = (VuoRectangle*) malloc(sizeof(VuoRectangle) * lineCount);
371  textImageData->charAdvance = charAdvance;
372  textImageData->charCount = characterCount;
373  textImageData->horizontalAlignment = font.alignment;
374 
375  for (int i = 0; i < lineCount; i++)
376  {
377  CGRect bb = CGRectApplyAffineTransform(lineBounds[i], *outTransform);
378  textImageData->lineBounds[i] = VuoRectangle_make(CGRectGetMidX(bb), CGRectGetMidY(bb), CGRectGetWidth(bb), CGRectGetHeight(bb));
379  }
380 
381  free(lineBounds);
382 
383  // Release the temporary context.
384  CGContextRelease(cgContext);
385 
386  CFRelease(attr);
387  CFRelease(attrString);
388  CFRelease(framesetter);
389  CFRelease(path);
390  CFRelease(frame);
391  CFRelease(cfText);
392  CFRelease(kernNumber);
393  CFRelease(underlineNumber);
394 
395  return ctLines;
396 }
397 
405 VuoRectangle VuoImage_getTextRectangle(VuoText text, VuoFont font, VuoReal backingScaleFactor, VuoReal verticalScale, VuoReal rotation, float wrapWidth, bool includeTrailingWhiteSpace)
406 {
407  VuoPoint2d corners[4];
408  // This should be faster than VuoImage_getTextImageData(), since we should be hitting VuoImage_makeText()'s cache.
409  VuoImage_makeText(text, font, backingScaleFactor, verticalScale, rotation, wrapWidth, corners);
410 
411  double minX = MIN(corners[0].x, MIN(corners[1].x, MIN(corners[2].x, corners[3].x)));
412  double minY = MIN(corners[0].y, MIN(corners[1].y, MIN(corners[2].y, corners[3].y)));
413  double maxX = MAX(corners[0].x, MAX(corners[1].x, MAX(corners[2].x, corners[3].x)));
414  double maxY = MAX(corners[0].y, MAX(corners[1].y, MAX(corners[2].y, corners[3].y)));
415 
416  return VuoRectangle_make(0, 0, maxX-minX, maxY-minY);
417 }
418 
426 VuoImageTextData VuoImage_getTextImageData(VuoText text, VuoFont font, VuoReal backingScaleFactor, VuoReal verticalScale, VuoReal rotation, bool includeTrailingWhiteSpace)
427 {
428  if (!VuoText_length(text))
429  return NULL;
430 
431  CTFontRef ctFont;
432  CGColorRef cgColor;
433  CGColorSpaceRef colorspace;
435  CGAffineTransform transform;
436  CFArrayRef ctLines = VuoImageText_createCTLines(text, font, backingScaleFactor, verticalScale, rotation, INFINITY, includeTrailingWhiteSpace, &ctFont, &cgColor, &colorspace, textData, &transform);
437  CFRelease(ctLines);
438  CGColorRelease(cgColor);
439  CGColorSpaceRelease(colorspace);
440  CFRelease(ctFont);
441 
442  return textData;
443 }
444 
449 VuoPoint2d VuoImageText_getTextSize(VuoText text, VuoFont font, VuoPoint2d windowSize, VuoReal backingScaleFactor, bool includeTrailingWhiteSpace)
450 {
451  VuoRectangle textBounds = VuoImage_getTextRectangle(text, font, backingScaleFactor, 1, 0, INFINITY, includeTrailingWhiteSpace);
452 
453  double s = VuoImageText_getVerticalScale(windowSize.x, backingScaleFactor);
454  double w = textBounds.size.x * s;
455  double h = textBounds.size.y * s;
456 
457  VuoPoint2d size;
458  size.x = (w / windowSize.x) * 2;
459  size.y = size.x * (h / w);
460 
461  return size;
462 }
463 
464 #define ScreenToGL(x) (((x) / screenWidthInPixels) * 2.)
465 
471 VuoReal VuoImageText_getLineHeight(VuoFont font, VuoReal screenWidthInPixels, VuoReal backingScaleFactor)
472 {
473  CFStringRef fontNameCF = NULL;
474 
475  if (font.fontName)
476  fontNameCF = CFStringCreateWithCString(NULL, font.fontName, kCFStringEncodingUTF8);
477 
478  CTFontRef ctFont = CTFontCreateWithName(fontNameCF ? fontNameCF : CFSTR(""), font.pointSize * backingScaleFactor, NULL);
479 
480  if (fontNameCF)
481  CFRelease(fontNameCF);
482 
483  double ascent = CTFontGetAscent(ctFont);
484  double descent = CTFontGetDescent(ctFont);
485  double leading = CTFontGetLeading(ctFont);
486  VuoReal lineHeight = ascent + descent + leading;
487 
488  CFRelease(ctFont);
489 
490  double scale = VuoImageText_getVerticalScale(screenWidthInPixels, backingScaleFactor);
491  return ScreenToGL(lineHeight * scale);
492 }
493 
497 void VuoImageTextData_convertToVuoCoordinates(VuoImageTextData textData, VuoReal screenWidthInPixels, VuoReal backingScaleFactor)
498 {
499  // @todo convert the other values too
500 
501  double scale = VuoImageText_getVerticalScale(screenWidthInPixels, backingScaleFactor);
502  double w = textData->width * scale;
503  double h = textData->height * scale;
504 
505  textData->width = (w / screenWidthInPixels) * 2;
506  textData->height = textData->width * (h / w);
507 
508  textData->lineHeight = ScreenToGL(textData->lineHeight * scale);
509 
510  textData->bounds.center = VuoPoint2d_make(ScreenToGL(textData->bounds.center.x * scale), ScreenToGL(textData->bounds.center.y * scale));
511  textData->bounds.size = VuoPoint2d_make(ScreenToGL(textData->bounds.size.x * scale), ScreenToGL(textData->bounds.size.y * scale));
512 
513  for (int i = 0; i < textData->lineCount; i++)
514  {
515  textData->lineBounds[i].center = VuoPoint2d_make(ScreenToGL(textData->lineBounds[i].center.x * scale), ScreenToGL(textData->lineBounds[i].center.y * scale));
516  textData->lineBounds[i].size = VuoPoint2d_make(ScreenToGL(textData->lineBounds[i].size.x * scale), ScreenToGL(textData->lineBounds[i].size.y * scale));
517  }
518 
519  for (int n = 0; n < textData->charCount; n++)
520  {
521  float advance = textData->charAdvance[n] * scale;
522  textData->charAdvance[n] = ScreenToGL(advance);
523  }
524 }
525 
526 #undef ScreenToGL
527 
532 VuoPoint2d VuoImageTextData_getPositionForLineIndex(VuoImageTextData textData, unsigned int lineIndex)
533 {
534  double x = 0;
535 
536  // if horizontal alignment is `left` the origin is just image bounds left.
537  // if center or right, the start position is line width dependent
538  if(textData->horizontalAlignment == VuoHorizontalAlignment_Left)
539  {
540  x = -textData->width * .5;
541  }
542  else
543  {
544  if(textData->horizontalAlignment == VuoHorizontalAlignment_Center)
545  x = textData->lineBounds[lineIndex].size.x * -.5;
546  else
547  x = textData->width * .5 - textData->lineBounds[lineIndex].size.x;
548  }
549 
550  // position is bottom left of rect (includes descent)
551  return VuoPoint2d_make(x, ((textData->lineCount - lineIndex) * textData->lineHeight) - textData->lineHeight - (textData->height * .5));
552 }
553 
561 unsigned int VuoImageTextData_getLineWithCharIndex(VuoImageTextData textData, unsigned int charIndex, unsigned int* lineStartCharIndex)
562 {
563  // figure out which row of text the char index is on
564  unsigned int lineIndex = 0;
565  // the char index that the starting line begins with
566  unsigned int lineStart = 0;
567 
568  while (lineIndex < textData->lineCount && charIndex >= lineStart + textData->lineCounts[lineIndex])
569  {
570  lineStart += textData->lineCounts[lineIndex];
571  lineIndex++;
572  }
573 
574  if(lineIndex >= textData->lineCount)
575  {
576  lineIndex = textData->lineCount - 1;
577  lineStart -= textData->lineCounts[lineIndex];
578  }
579 
580  if(lineStartCharIndex != NULL)
581  *lineStartCharIndex = lineStart;
582 
583  return lineIndex;
584 }
585 
589 VuoPoint2d VuoImageTextData_getPositionForCharIndex(VuoImageTextData textData, unsigned int charIndex, unsigned int* lineIndex)
590 {
591  charIndex = MAX(0, MIN(textData->charCount, charIndex));
592 
593  // figure out which row of text the char index is on
594  unsigned int lineStart = 0;
595  unsigned int lineIdx = VuoImageTextData_getLineWithCharIndex(textData, charIndex, &lineStart);
596  VuoPoint2d pos = VuoImageTextData_getPositionForLineIndex(textData, lineIdx);
597 
598  // now that origin x and y are set, step through line to actual index
599  for(int n = lineStart; n < charIndex; n++)
600  pos.x += textData->charAdvance[n];
601 
602  if( lineIndex != NULL )
603  *lineIndex = lineIdx;
604 
605  return pos;
606 }
607 
612 VuoRectangle* VuoImageTextData_getRectsForHighlight(VuoImageTextData textData, unsigned int selectionStartIndex, unsigned int selectionLength, unsigned int* lineCount)
613 {
614  std::vector<VuoRectangle> rects;
615 
616  unsigned int lineStart = 0;
617  unsigned int lineIndex = VuoImageTextData_getLineWithCharIndex(textData, selectionStartIndex, &lineStart);
618  VuoPoint2d origin = VuoImageTextData_getPositionForLineIndex(textData, lineIndex);
619 
620  // now that origin x and y are set for the starting line, step through line to actual character start index
621  for(int n = lineStart; n < selectionStartIndex; n++)
622  origin.x += textData->charAdvance[n];
623 
624  float width = 0;
625 
626  unsigned int curIndex = selectionStartIndex;
627  unsigned int textLength = textData->charCount;
628 
629  while(curIndex < selectionStartIndex + selectionLength)
630  {
631  width += curIndex < textLength ? textData->charAdvance[curIndex] : 0;
632  curIndex++;
633 
634  if( curIndex >= lineStart + textData->lineCounts[lineIndex] || curIndex >= selectionStartIndex + selectionLength )
635  {
636  float w = fmax(.01, width);
637  float h = textData->lineHeight;
638  rects.push_back( VuoRectangle_make(origin.x + (w * .5), origin.y + (h * .5), w, h) );
639 
640  lineStart += textData->lineCounts[lineIndex];
641  lineIndex++;
642 
643  if(lineIndex >= textData->lineCount)
644  break;
645 
646  origin = VuoImageTextData_getPositionForLineIndex(textData, lineIndex);
647  width = 0;
648  }
649  }
650 
651  *lineCount = rects.size();
652  VuoRectangle* copy = (VuoRectangle*) malloc(sizeof(VuoRectangle) * (*lineCount));
653  std::copy(rects.begin(), rects.end(), copy);
654  return copy;
655 }
656 
661 unsigned int VuoImageTextData_getCharIndexForLine(VuoImageTextData textData, unsigned int lineIndex)
662 {
663  unsigned int charIndex = 0;
664  unsigned int index = MIN(textData->lineCount, lineIndex);
665  for(unsigned int i = 0; i < index; i++)
666  charIndex += textData->lineCounts[i];
667  return charIndex;
668 }
669 
674 VuoRectangle VuoImageTextData_layoutRowAtIndex(VuoImageTextData textData, unsigned int index, unsigned int* charactersRemaining)
675 {
676  unsigned int lineIndex;
677  VuoPoint2d begin = VuoImageTextData_getPositionForCharIndex(textData, index, &lineIndex);
678  unsigned int lineBegin = VuoImageTextData_getCharIndexForLine(textData, lineIndex);
679  *charactersRemaining = (textData->lineCounts[lineIndex] - (index - lineBegin));
680  float width = textData->lineBounds[lineIndex].size.x - begin.x;
681  return VuoRectangle_makeTopLeft(begin.x, textData->lineHeight, width, textData->lineHeight);
682 }
683 
688 {
689  int index = 0;
690 
691  // point is already in model space, but it needs to be relative to image where (0,0) is the
692  // center of the image. if the text billboard is not centered we need to translate the point
693  // to suit.
694  VuoHorizontalAlignment h = VuoAnchor_getHorizontal(textData->billboardAnchor);
695  VuoVerticalAlignment v = VuoAnchor_getVertical(textData->billboardAnchor);
696 
697  if(h == VuoHorizontalAlignment_Left)
698  point.x -= textData->width * .5;
699  else if(h == VuoHorizontalAlignment_Right)
700  point.x += textData->width * .5;
701 
702  if(v == VuoVerticalAlignment_Bottom)
703  point.y -= textData->height * .5;
704  else if(v == VuoVerticalAlignment_Top)
705  point.y += textData->height * .5;
706 
707  for(int r = 0; r < textData->lineCount; r++)
708  {
709  bool lastLine = r == textData->lineCount - 1;
710  VuoRectangle lineBounds = textData->lineBounds[r];
711  VuoPoint2d lineOrigin = VuoImageTextData_getPositionForLineIndex(textData, r);
712 
713  // if the point is above this line, or it's the last line, that means
714  // this row is the nearest to the cursor
715  if(point.y > lineOrigin.y || lastLine)
716  {
717  // left of the start of this line, so first index it is
718  if(point.x < lineOrigin.x)
719  return index;
720 
721  // right of the end of this line, so last index (before new line char) unless it's the last line
722  else if(point.x > (lineOrigin.x + lineBounds.size.x))
723  return index + textData->lineCounts[r] - (lastLine ? 0 : 1);
724 
725  for(int i = 0; i < textData->lineCounts[r]; i++)
726  {
727  float advance = textData->charAdvance[index + i];
728 
729  if(point.x < lineOrigin.x + advance * .5)
730  return index + i;
731 
732  lineOrigin.x += advance;
733  }
734 
735  return index + textData->lineCounts[r] + (lastLine ? 1 : 0);
736  }
737 
738  index += textData->lineCounts[r];
739  }
740 
741  // never should get here, but just in case
742  return textData->charCount;
743 }
744 
752 VuoImage VuoImage_makeText(VuoText text, VuoFont font, float backingScaleFactor, float verticalScale, float rotation, float wrapWidth, VuoPoint2d *outCorners)
753 {
754  if (VuoText_isEmpty(text))
755  return NULL;
756 
757  if (font.pointSize < 0.00001
758  || verticalScale < 0.00001)
759  return NULL;
760 
761  // Is there an image ready in the cache?
762  static dispatch_once_t initCache = 0;
763  dispatch_once(&initCache, ^ {
765  });
766  VuoFontClass fc(font, backingScaleFactor, verticalScale, rotation, wrapWidth);
767  VuoImageTextCacheDescriptor descriptor(text, fc);
768  dispatch_semaphore_wait(VuoImageTextCache_semaphore, DISPATCH_TIME_FOREVER);
769  {
770  VuoImageTextCacheType::iterator e = VuoImageTextCache->find(descriptor);
771  if (e != VuoImageTextCache->end())
772  {
773  // VLog("found in cache");
774  VuoImage image = e->second.first.first;
775  if (outCorners)
776  memcpy(outCorners, &e->second.first.second[0], sizeof(VuoPoint2d)*4);
777  e->second.second = VuoLogGetTime();
778  dispatch_semaphore_signal(VuoImageTextCache_semaphore);
779  return image;
780  }
781  }
782  dispatch_semaphore_signal(VuoImageTextCache_semaphore);
783 
784  // …if not, render it.
785 
786  CTFontRef ctFont;
787  CGColorRef cgColor;
788  CGColorSpaceRef colorspace;
789 
791  VuoLocal(textData);
792  CGAffineTransform transform;
793  CFArrayRef ctLines = VuoImageText_createCTLines(text, font, backingScaleFactor, verticalScale, rotation, wrapWidth, false, &ctFont, &cgColor, &colorspace, textData, &transform);
794 
795 
796  // Create the rendering context.
797  // VuoImage_makeFromBuffer() expects a premultiplied buffer.
798  CGContextRef cgContext = CGBitmapContextCreate(NULL, textData->width, textData->height, 8, textData->width * 4, colorspace, kCGImageAlphaPremultipliedLast);
799 
800  // Draw a red box at the border of the entire image.
801  // CGContextSetRGBStrokeColor(cgContext, 1.0, 0.0, 0.0, 1.0); /// nocommit
802  // CGContextStrokeRect(cgContext, CGRectMake(0,0,textData->width,textData->height)); /// nocommit
803 
804  // Antialiasing is enabled by default.
805  // Disable it to test sharpness.
806 // CGContextSetShouldAntialias(cgContext, false);
807 // CGContextSetAllowsAntialiasing(cgContext, false);
808 // CGContextSetShouldSmoothFonts(cgContext, false);
809 // CGContextSetAllowsFontSmoothing(cgContext, false);
810 
811  // Subpixel positioning is enabled by default.
812  // Disabling it makes some letters sharper,
813  // but makes spacing less consistent.
814 // CGContextSetShouldSubpixelPositionFonts(cgContext, false);
815 // CGContextSetAllowsFontSubpixelPositioning(cgContext, false);
816 // CGContextSetShouldSubpixelQuantizeFonts(cgContext, false);
817 // CGContextSetAllowsFontSubpixelQuantization(cgContext, false);
818 
819  // Vertically flip, since VuoImage_makeFromBuffer() expects a flipped buffer.
820  CGContextSetTextMatrix(cgContext, CGAffineTransformMakeScale(1.0, -1.0));
821 
822  if (outCorners)
823  memcpy(outCorners, textData->transformedCorners, sizeof(VuoPoint2d)*4);
824 
825  // Move the origin so the entire transformed text rectangle is within the image area.
826  CGContextTranslateCTM(cgContext, textData->transformedCorners[0].x, textData->transformedCorners[0].y);
827 
828  // Offset by AA_STROKE_PAD/2, depending on rotation, to keep the edges in bounds.
829  CGContextTranslateCTM(cgContext, fabs(cos(rotation)), fabs(sin(rotation)));
830 
831  // Apply verticalScale and rotation.
832  CGContextConcatCTM(cgContext, transform);
833 
834  VuoRectangle bounds = textData->bounds;
835 
836  // Draw each line of text.
837  CFIndex lineCount = CFArrayGetCount(ctLines);
838  for (CFIndex i = 0; i < lineCount; ++i)
839  {
840  CTLineRef ctLine = (CTLineRef)CFArrayGetValueAtIndex(ctLines, i);
841 
842  float textXPosition = -(bounds.center.x - (bounds.size.x * .5));
843  if (font.alignment == VuoHorizontalAlignment_Center)
844  textXPosition += (bounds.size.x - textData->lineWidthsExcludingTrailingWhitespace[i] - textData->lineXOrigins[i]) / 2.;
845  else if (font.alignment == VuoHorizontalAlignment_Right)
846  textXPosition += bounds.size.x - textData->lineWidthsExcludingTrailingWhitespace[i] - textData->lineXOrigins[i];
847 
848  float textYPosition = -(bounds.center.y - (bounds.size.y * .5));
849  textYPosition += textData->lineHeight * i * font.lineSpacing;
850 
851  CGContextSetTextPosition(cgContext, textXPosition, textYPosition);
852  CTLineDraw(ctLine, cgContext);
853 
854 
855  // textData->lineBounds is already scaled/rotated, so temporarily un-apply that matrix.
856 // CGContextSaveGState(cgContext);
857 // CGContextConcatCTM(cgContext, CGAffineTransformInvert(transform));
858 // CGContextSetRGBStrokeColor(cgContext, 0, 1, 0, 1); // NOCOMMIT
859 // VuoRectangle r = textData->lineBounds[i]; // NOCOMMIT
860 // CGContextStrokeRect(cgContext, CGRectMake(r.center.x - r.size.x/2, r.center.y + r.size.y/2, r.size.x, r.size.y)); // NOCOMMIT
861 // CGContextRestoreGState(cgContext);
862 
863 // CGContextSetRGBStrokeColor(cgContext, 0, 0, 1, 1); // NOCOMMIT
864 // CGContextStrokeRect(cgContext, CGRectMake(textXPosition-1,textYPosition-1,2,2)); // NOCOMMIT
865  }
866 
867  // Make a VuoImage from the CGContext.
868  VuoImage image = VuoImage_makeFromBuffer(CGBitmapContextGetData(cgContext), GL_RGBA, textData->width, textData->height, VuoImageColorDepth_8, ^(void *buffer) { CGContextRelease(cgContext); });
869 
870  CFRelease(ctLines);
871  CGColorSpaceRelease(colorspace);
872  CGColorRelease(cgColor);
873  CFRelease(ctFont);
874 
875  // …and store it in the cache.
876  if (image)
877  {
878  dispatch_semaphore_wait(VuoImageTextCache_semaphore, DISPATCH_TIME_FOREVER);
879 
880  // Another thread might have added it to the cache since the check at the beginning of this function.
881  VuoImageTextCacheType::iterator it = VuoImageTextCache->find(descriptor);
882  if (it != VuoImageTextCache->end())
883  {
884 // VLog("This image is already in the cache; I'm destroying mine and returning the cached image.");
885  VuoLocal(image);
886 
887  VuoImage cachedImage = it->second.first.first;
888  if (outCorners)
889  memcpy(outCorners, &it->second.first.second[0], sizeof(VuoPoint2d)*4);
890  it->second.second = VuoLogGetTime();
891  dispatch_semaphore_signal(VuoImageTextCache_semaphore);
892  return cachedImage;
893  }
894 
895  VuoRetain(image);
896  VuoCorners c(4);
897  memcpy(&c[0], textData->transformedCorners, sizeof(VuoPoint2d)*4);
898  VuoImageTextCacheEntry e(std::make_pair(image, c), VuoLogGetTime());
899  (*VuoImageTextCache)[descriptor] = e;
900  // VLog("stored in cache");
901 
902  dispatch_semaphore_signal(VuoImageTextCache_semaphore);
903  }
904 
905 
906  return image;
907 }