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