Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Saving custom attributes in NSAttributedString

I need to add a custom attribute to the selected text in an NSTextView. So I can do that by getting the attributed string for the selection, adding a custom attribute to it, and then replacing the selection with my new attributed string.

So now I get the text view's attributed string as NSData and write it to a file. Later when I open that file and restore it to the text view my custom attributes are gone! After working out the entire scheme for my custom attribute I find that custom attributes are not saved for you. Look at the IMPORTANT note here: http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/AttributedStrings/Tasks/RTFAndAttrStrings.html

So I have no idea how to save and restore my documents with this custom attribute. Any help?

like image 393
regulus6633 Avatar asked Apr 13 '10 02:04

regulus6633


2 Answers

The normal way of saving an NSAttributedString is to use RTF, and RTF data is what the -dataFromRange:documentAttributes:error: method of NSAttributedString generates.

However, the RTF format has no support for custom attributes. Instead, you should use the NSCoding protocol to archive your attributed string, which will preserve the custom attributes:

//asssume attributedString is your NSAttributedString
//encode the string as NSData
NSData* stringData = [NSKeyedArchiver archivedDataWithRootObject:attributedString];
[stringData writeToFile:pathToFile atomically:YES];

//read the data back in and decode the string
NSData* newStringData = [NSData dataWithContentsOfFile:pathToFile];
NSAttributedString* newString = [NSKeyedUnarchiver unarchiveObjectWithData:newStringData];
like image 183
Rob Keniger Avatar answered Nov 26 '22 20:11

Rob Keniger


There is a way to save custom attributes to RTF using Cocoa. It relies on the fact that RTF is a text format, and so can be manipulated as a string even if you don't know all the rules of RTF and don't have a custom RTF reader/writer. The procedure I outline below post-processes the RTF both when writing and reading, and I have used this technique personally. One thing to be very careful of is that the text you insert into the RTF uses only 7-bit ASCII and no unescaped control characters, which include "\ { }".

Here's how you would encode your data:

NSData *GetRtfFromAttributedString(NSAttributedString *text)
{
    NSData *rtfData = nil;
    NSMutableString *rtfString = nil;
    NSString *customData = nil, *encodedData = nil;
    NSRange range;
    NSUInteger dataLocation;

// Convert the attributed string to RTF
    if ((rtfData = [text RTFFromRange:NSMakeRange(0, [text length]) documentAttributes:nil]) == nil)
        return(nil);

// Find and encode your custom attributes here. In this example the data is a string and there's at most one of them
    if ((customData = [text attribute:@"MyCustomData" atIndex:0 effectiveRange:&range]) == nil)
        return(rtfData); // No custom data, return RTF as is
    dataLocation = range.location;

// Get a string representation of the RTF
    rtfString = [[NSMutableString alloc] initWithData:rtfData encoding:NSASCIIStringEncoding];

// Find the anchor where we'll put our data, namely just before the first paragraph property reset
    range = [rtfString rangeOfString:@"\\pard" options:NSLiteralSearch];
    if (range.location == NSNotFound)
        {
        NSLog(@"Custom data dropped; RTF has no paragraph properties");
        [rtfString release];
        return(rtfData);
        }

// Insert the starred group containing the custom data and its location
    encodedData = [NSString stringWithFormat:@"{\\*\\my_custom_keyword %d,%@}\n", dataLocation, customData];
    [rtfString insertString:encodedData atIndex:range.location];

// Convert the amended RTF back to a data object    
    rtfData = [rtfString dataUsingEncoding:NSASCIIStringEncoding];
    [rtfString release];
    return(rtfData);
}

This technique works because all compliant RTF readers will ignore "starred groups" whose keyword they don't recognize. Therefore you want to be sure your control word will not be recognized by any other reader, so use something likely to be unique, such as a prefix with your company or product name. If your data is complex, or binary, or may contain illegal RTF characters that you don't want to escape, encode it in base64. Be sure you put a space after your keyword.

Similarly, when reading the RTF, you search for your control word, extract the data, and restore the attribute. This routine takes as arguments the attributed string and the RTF it was created from.

void RestoreCustomAttributes(NSMutableAttributedString *text, NSData *rtfData)
{
    NSString *rtfString = [[NSString alloc] initWithData:rtfData encoding:NSASCIIStringEncoding];
    NSArray *components = nil;
    NSRange range, endRange;

// Find the custom data and its end
    range = [rtfString rangeOfString:@"{\\*\\my_custom_keyword " options:NSLiteralSearch];
    if (range.location == NSNotFound)
        {
        [rtfString release];
        return;
        }
    range.location += range.length;

    endRange = [rtfString rangeOfString:@"}" options:NSLiteralSearch
        range:NSMakeRange(range.location, [rtfString length] - endRange.location)];
    if (endRange.location == NSNotFound)
        {
        [rtfString release];
        return;
        }

// Get the location and the string data, which are separated by a comma
    range.length = endRange.location - range.location;
    components = [[rtfString substringWithRange:range] componentsSeparatedByString:@","];
    [rtfString release];

// Assign the custom data back to the attributed string. You should do range checking here (omitted for clarity)
    [text addAttribute:@"MyCustomData" value:[components objectAtIndex:1]
        range:NSMakeRange([[components objectAtIndex:0] integerValue], 1)];
}
like image 45
Chris Mason Avatar answered Nov 26 '22 20:11

Chris Mason