WebCoreStringTruncator.mm [plain text]
/*
* Copyright (C) 2005 Apple Computer, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "WebCoreStringTruncator.h"
#import <wtf/Assertions.h>
#import "Font.h"
#define STRING_BUFFER_SIZE 2048
#define ELLIPSIS_CHARACTER 0x2026
#define SPACE_CHARACTER 0x0020
using namespace WebCore;
static GSFontRef currentFont;
static Font* currentRenderer = 0;
static float currentEllipsisWidth;
typedef unsigned TruncationFunction(NSString *string, unsigned length, unsigned keepCount, unichar *buffer);
static unsigned centerTruncateToBuffer(NSString *string, unsigned length, unsigned keepCount, unichar *buffer)
{
ASSERT(keepCount < length);
ASSERT(keepCount < STRING_BUFFER_SIZE);
unsigned omitStart = (keepCount + 1) / 2;
unsigned omitEnd = NSMaxRange([string rangeOfComposedCharacterSequenceAtIndex:omitStart + (length - keepCount) - 1]);
omitStart = [string rangeOfComposedCharacterSequenceAtIndex:omitStart].location;
// Strip single character before ellipsis character, when that character is preceded by a space
if (omitStart > 1 && [string characterAtIndex:omitStart - 1] != SPACE_CHARACTER &&
omitStart > 2 && [string characterAtIndex:omitStart - 2] == SPACE_CHARACTER)
omitStart--;
// Strip whitespace before and after the ellipsis character
while (omitStart > 1 && [string characterAtIndex:omitStart - 1] == SPACE_CHARACTER)
omitStart--;
// Strip single character after ellipsis character, when that character is followed by a space
if ((length - omitEnd) > 1 && [string characterAtIndex:omitEnd] != SPACE_CHARACTER &&
(length - omitEnd) > 2 && [string characterAtIndex:omitEnd + 1] == SPACE_CHARACTER)
omitEnd++;
while ((length - omitEnd) > 1 && [string characterAtIndex:omitEnd] == SPACE_CHARACTER)
omitEnd++;
NSRange beforeRange = NSMakeRange(0, omitStart);
NSRange afterRange = NSMakeRange(omitEnd, length - omitEnd);
unsigned truncatedLength = beforeRange.length + 1 + afterRange.length;
ASSERT(truncatedLength <= length);
[string getCharacters:buffer range:beforeRange];
buffer[beforeRange.length] = ELLIPSIS_CHARACTER;
[string getCharacters:&buffer[beforeRange.length + 1] range:afterRange];
return truncatedLength;
}
static unsigned rightTruncateToBuffer(NSString *string, unsigned length, unsigned keepCount, unichar *buffer)
{
ASSERT(keepCount < length);
ASSERT(keepCount < STRING_BUFFER_SIZE);
// Strip single character before ellipsis character, when that character is preceded by a space
if (keepCount > 1 && [string characterAtIndex:keepCount - 1] != SPACE_CHARACTER &&
keepCount > 2 && [string characterAtIndex:keepCount - 2] == SPACE_CHARACTER)
keepCount--;
// Strip whitespace before the ellipsis character
while (keepCount > 1 && [string characterAtIndex:keepCount - 1] == SPACE_CHARACTER)
keepCount--;
NSRange keepRange = NSMakeRange(0, [string rangeOfComposedCharacterSequenceAtIndex:keepCount].location);
[string getCharacters:buffer range:keepRange];
buffer[keepRange.length] = ELLIPSIS_CHARACTER;
return keepRange.length + 1;
}
static unsigned leftTruncateToBuffer(NSString *string, unsigned length, unsigned keepCount, unichar *buffer)
{
ASSERT(keepCount < length);
ASSERT(keepCount < STRING_BUFFER_SIZE);
unsigned startIndex = length - keepCount;
NSRange startComposedRange = [string rangeOfComposedCharacterSequenceAtIndex:startIndex];
if (startComposedRange.location != startIndex) startIndex = NSMaxRange(startComposedRange);
unsigned adjustedStartIndex = startIndex;
// Strip single character after ellipsis character, when that character is preceded by a space
if (adjustedStartIndex < length && [string characterAtIndex:adjustedStartIndex] != SPACE_CHARACTER &&
adjustedStartIndex < length - 1 && [string characterAtIndex:adjustedStartIndex + 1] == SPACE_CHARACTER)
adjustedStartIndex++;
// Strip whitespace after the ellipsis character
while (adjustedStartIndex < length && [string characterAtIndex:adjustedStartIndex] == SPACE_CHARACTER)
adjustedStartIndex++;
if (adjustedStartIndex != startIndex) {
startComposedRange = [string rangeOfComposedCharacterSequenceAtIndex:adjustedStartIndex];
if (startComposedRange.location != startIndex) startIndex = NSMaxRange(startComposedRange);
}
NSRange keepRange = NSMakeRange(startIndex, length - startIndex);
[string getCharacters:&(buffer[1]) range:keepRange];
buffer[0] = ELLIPSIS_CHARACTER;
return keepRange.length + 1;
}
static float stringWidth(Font* renderer, const unichar *characters, unsigned length)
{
TextRun run(characters, length);
TextStyle style;
style.disableRoundingHacks();
return renderer->floatWidth(run, style);
}
static NSString *truncateString(NSString *string, float maxWidth, GSFontRef font, TruncationFunction truncateToBuffer, float *resultWidth)
{
unsigned length = [string length];
if (length == 0) {
return string;
}
if (resultWidth)
*resultWidth = 0;
unichar stringBuffer[STRING_BUFFER_SIZE];
unsigned keepCount;
unsigned truncatedLength;
float width;
unichar ellipsis;
unsigned keepCountForLargestKnownToFit, keepCountForSmallestKnownToNotFit;
float widthForLargestKnownToFit, widthForSmallestKnownToNotFit;
float ratio;
ASSERT_ARG(font, font);
ASSERT_ARG(maxWidth, maxWidth >= 0);
if (currentFont != font) {
if (currentFont)
CFRelease (currentFont);
currentFont = (GSFontRef)CFRetain (font);
FontPlatformData f(font);
delete currentRenderer;
currentRenderer = new Font(f);
ellipsis = ELLIPSIS_CHARACTER;
currentEllipsisWidth = stringWidth(currentRenderer, &ellipsis, 1);
}
ASSERT(currentRenderer);
if (length > STRING_BUFFER_SIZE) {
keepCount = STRING_BUFFER_SIZE - 1; // need 1 character for the ellipsis
truncatedLength = centerTruncateToBuffer(string, length, keepCount, stringBuffer);
} else {
keepCount = length;
[string getCharacters:stringBuffer];
truncatedLength = length;
}
width = stringWidth(currentRenderer, stringBuffer, truncatedLength);
if (width <= maxWidth) {
if (resultWidth)
*resultWidth = width;
return string;
}
keepCountForLargestKnownToFit = 0;
widthForLargestKnownToFit = currentEllipsisWidth;
keepCountForSmallestKnownToNotFit = keepCount;
widthForSmallestKnownToNotFit = width;
if (currentEllipsisWidth >= maxWidth) {
keepCountForLargestKnownToFit = 1;
keepCountForSmallestKnownToNotFit = 2;
}
while (keepCountForLargestKnownToFit + 1 < keepCountForSmallestKnownToNotFit) {
ASSERT(widthForLargestKnownToFit <= maxWidth);
ASSERT(widthForSmallestKnownToNotFit > maxWidth);
ratio = (keepCountForSmallestKnownToNotFit - keepCountForLargestKnownToFit)
/ (widthForSmallestKnownToNotFit - widthForLargestKnownToFit);
keepCount = static_cast<unsigned>(maxWidth * ratio);
if (keepCount <= keepCountForLargestKnownToFit) {
keepCount = keepCountForLargestKnownToFit + 1;
} else if (keepCount >= keepCountForSmallestKnownToNotFit) {
keepCount = keepCountForSmallestKnownToNotFit - 1;
}
ASSERT(keepCount < length);
ASSERT(keepCount > 0);
ASSERT(keepCount < keepCountForSmallestKnownToNotFit);
ASSERT(keepCount > keepCountForLargestKnownToFit);
truncatedLength = truncateToBuffer(string, length, keepCount, stringBuffer);
width = stringWidth(currentRenderer, stringBuffer, truncatedLength);
if (width <= maxWidth) {
keepCountForLargestKnownToFit = keepCount;
widthForLargestKnownToFit = width;
if (resultWidth)
*resultWidth = width;
} else {
keepCountForSmallestKnownToNotFit = keepCount;
widthForSmallestKnownToNotFit = width;
}
}
if (keepCountForLargestKnownToFit == 0) {
keepCountForLargestKnownToFit = 1;
}
if (keepCount != keepCountForLargestKnownToFit) {
keepCount = keepCountForLargestKnownToFit;
truncatedLength = truncateToBuffer(string, length, keepCount, stringBuffer);
}
return [NSString stringWithCharacters:stringBuffer length:truncatedLength];
}
@implementation WebCoreStringTruncator
+ (NSString *)centerTruncateString:(NSString *)string toWidth:(float)maxWidth withFont:(GSFontRef)font
{
return truncateString(string, maxWidth, font, centerTruncateToBuffer, 0);
}
+ (NSString *)centerTruncateString:(NSString *)string toWidth:(float)maxWidth withFont:(GSFontRef)font resultWidth:(float *)resultWidth
{
return truncateString(string, maxWidth, font, centerTruncateToBuffer, resultWidth);
}
+ (NSString *)rightTruncateString:(NSString *)string toWidth:(float)maxWidth withFont:(GSFontRef)font
{
return truncateString(string, maxWidth, font, rightTruncateToBuffer, 0);
}
+ (NSString *)rightTruncateString:(NSString *)string toWidth:(float)maxWidth withFont:(GSFontRef)font resultWidth:(float *)resultWidth
{
return truncateString(string, maxWidth, font, rightTruncateToBuffer, resultWidth);
}
+ (NSString *)leftTruncateString:(NSString *)string toWidth:(float)maxWidth withFont:(GSFontRef)font resultWidth:(float *)resultWidth
{
return truncateString(string, maxWidth, font, leftTruncateToBuffer, resultWidth);
}
+ (float)widthOfString:(NSString *)string font:(GSFontRef)font
{
unsigned length = [string length];
unichar *s = static_cast<unichar*>(malloc(sizeof(unichar) * length));
[string getCharacters:s];
FontPlatformData f(font);
Font fontRenderer(f);
float width = stringWidth(&fontRenderer, s, length);
free(s);
return width;
}
+ (void)clear
{
delete currentRenderer;
currentRenderer = 0;
CFRelease (currentFont);
currentFont = nil;
}
@end