trouble saving NSAttributedString, with image, to an RTF file - ios

I have some output that is a very simple RTF file. When I generate this document, the user can email it. All this works fine. The document looks good. Once I have the NSAttributedString, I make an NSData block, and write it to a file, like this:
NSData* rtfData = [attrString dataFromRange:NSMakeRange(0, [attrString length]) documentAttributes:#{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType} error:&error];
This file can be emailed. When I check the email all is good.
Now, I'm tasked with adding a UIImage at the top of the document. Great, so I'm creating an attributed string like this:
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
UIImage* image = [UIImage imageNamed:#"logo"];
attachment.image = image;
attachment.bounds = CGRectMake(0.0f, 0.0f, image.size.width, image.size.height);
NSMutableAttributedString *imageAttrString = [[NSAttributedString attributedStringWithAttachment:attachment] mutableCopy];
// sets the paragraph styling of the text attachment
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init] ;
[paragraphStyle setAlignment:NSTextAlignmentCenter]; // centers image horizontally
[paragraphStyle setParagraphSpacing:10.0f]; // adds some padding between the image and the following section
[imageAttrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [imageAttrString length])];
[imageAttrString appendAttributedString:[[NSAttributedString alloc] initWithString:#"\n\n"]];
At this point, in Xcode, I can do a QuickLook on imageAttrString, and it draws just fine.
Once this string is built, I'm doing this:
[attrString appendAttributedString:imageAttrString];
And then adding in all the rest of the attributed text that I originally generated.
When I look at the file now, there is no image. QuickLook looks good in the debugger, but no image in the final output.
Thanks in advance for any help with this.

Although, RTF does support embedded images on Windows, apparently it doesn't on OS X. RTF was developed by Microsoft and they added embedded images in version 1.5 (http://en.wikipedia.org/wiki/Rich_Text_Format#Version_changes). I think that Apple took earlier version of the format and their solution to images in documents was RTFD. Here is what Apple documentation says about RTF:
Rich Text Format (RTF) is a text formatting language devised by Microsoft Corporation. You can represent character, paragraph, and document format attributes using plain text with interspersed RTF commands, groups, and escape sequences. RTF is widely used as a document interchange format to transfer documents with their formatting information across applications and computing platforms. Apple has extended RTF with custom commands, which are described in this chapter.
So no images are mentioned. Finally to prove that RTF doesn't support images on mac, download this RTF document - it will show photo in Windows WordPad and won't show it in OS X TextEdit.
So as Larme mentioned - you should choose RTFD file type when adding attachments. From Wikipedia:
Rich Text Format Directory, also known as RTFD (due to its extension .rtfd), or Rich Text Format with Attachments
Although you will be able to get NSData object that contains both the text and the image (judging by its size), via dataFromRange:documentAttributes:#{NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType} error:] you probably won't be able to save it so that it could be opened successfully. At least - I wasn't able to do that.
That's probably because actually RTFD is not a file format - it's a format of a bundle. To check it, you could use TextEdit on your mac to create a new document, add image and a text to it and save it as a file. Then right click on that file and choose Show Package Contents and you'll notice that the directory contains both your image and the text in RTF format.
However you will be able to save this document successfully with this code:
NSFileWrapper *fileWrapper = [imageAttrString fileWrapperFromRange:NSMakeRange(0, [imageAttrString length]) documentAttributes:#{NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType} error:&error];
[fileWrapper writeToURL:yourFileURL options:NSFileWrapperWritingAtomic originalContentsURL:nil error:&error];
Because apparently NSFileWrapper knows how to deal with RTFD documents while NSData has no clue of what it contains.
However the main problem still remains - how to send it in email? Because RTFD document is a directory not a file, I'd say it's not very well suited for sending by email, however you can zip it and send with an extension .rtfd.zip. The extension here is the crucial because it will tell Mail app how to display contents of the attachment when user taps on it. Actually it will work also in Gmail and probably other email apps on iOS because it's the UIWebView that knows how to display .rtfd.zip. Here is a technical note about it: https://developer.apple.com/library/ios/qa/qa1630/_index.html#//apple_ref/doc/uid/DTS40008749
So the bottom line is - it can be done but the RTFD document will be an attachment to the email not the email content itself. If you want to have it as an email content you should probably look into embedding your image into HTML and sending the mail as HTML.

As Andris mentioned, Apple RTF implementation does not support embedded images.
RTFD isn't a real alternative, as only a few OS X apps can open RTFD files. For example MS Office can't.
Creating a HTML file with embedded images might help in some cases, but - for example - most email clients don't support HTML with embedded images (Apple Mail does, Outlook however doesn't).
But fortunately there is a solution to create real RTF files with embedded images!
As the RTF format of course supports embedded images (only Apples implementation doesn't), images in a NSAttributedStrings (NSTextAttachments) can be (hand) coded into the RTF stream.
The following category does all the work needed:
/**
NSAttributedString (MMRTFWithImages)
*/
#interface NSAttributedString (MMRTFWithImages)
- (NSString *)encodeRTFWithImages;
#end
/**
NSAttributedString (MMRTFWithImages)
*/
#implementation NSAttributedString (MMRTFWithImages)
/*
encodeRTFWithImages
*/
- (NSString *)encodeRTFWithImages {
NSMutableAttributedString* stringToEncode = [[NSMutableAttributedString alloc] initWithAttributedString:self];
NSRange strRange = NSMakeRange(0, stringToEncode.length);
//
// Prepare the attributed string by removing the text attachments (images) and replacing them by
// references to the images dictionary
NSMutableDictionary* attachmentDictionary = [NSMutableDictionary dictionary];
while (strRange.length) {
// Get the next text attachment
NSRange effectiveRange;
NSTextAttachment* textAttachment = [stringToEncode attribute:NSAttachmentAttributeName
atIndex:strRange.location
effectiveRange:&effectiveRange];
strRange = NSMakeRange(NSMaxRange(effectiveRange), NSMaxRange(strRange) - NSMaxRange(effectiveRange));
if (textAttachment) {
// Text attachment found -> store image to image dictionary and remove the attachment
NSFileWrapper* fileWrapper = [textAttachment fileWrapper];
UIImage* image = [[UIImage alloc] initWithData:[fileWrapper regularFileContents]];
// Kepp image size
UIImage* scaledImage = [self imageFromImage:image
withSize:textAttachment.bounds.size];
NSString* imageKey = [NSString stringWithFormat:#"_MM_Encoded_Image#%zi_", [scaledImage hash]];
[attachmentDictionary setObject:scaledImage
forKey:imageKey];
[stringToEncode removeAttribute:NSAttachmentAttributeName
range:effectiveRange];
[stringToEncode replaceCharactersInRange:effectiveRange
withString:imageKey];
strRange.length += [imageKey length] - 1;
} // if
} // while
//
// Create the RTF stream; without images but including our references
NSData* rtfData = [stringToEncode dataFromRange:NSMakeRange(0, stringToEncode.length)
documentAttributes:#{
NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType
}
error:NULL];
NSMutableString* rtfString = [[NSMutableString alloc] initWithData:rtfData
encoding:NSASCIIStringEncoding];
//
// Replace the image references with hex encoded image data
for (id key in attachmentDictionary) {
NSRange keyRange = [rtfString rangeOfString:(NSString*)key];
if (NSNotFound != keyRange.location) {
// Reference found -> replace with hex coded image data
UIImage* image = [attachmentDictionary objectForKey:key];
NSData* pngData = UIImagePNGRepresentation(image);
NSString* hexCodedString = [self hexadecimalRepresentation:pngData];
NSString* encodedImage = [NSString stringWithFormat:#"{\\*\\shppict {\\pict \\pngblip %#}}", hexCodedString];
[rtfString replaceCharactersInRange:keyRange withString:encodedImage];
}
}
return rtfString;
}
/*
imageFromImage:withSize:
Scales the input image to pSize
*/
- (UIImage *)imageFromImage:(UIImage *)pImage
withSize:(CGSize)pSize {
UIGraphicsBeginImageContextWithOptions(pSize, NO, 0.0);
[pImage drawInRect:CGRectMake(0, 0, pSize.width, pSize.height)];
UIImage* resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resultImage;
}
/*
hexadecimalRepresentation:
Returns a hex codes string for all bytes in a NSData object
*/
- (NSString *) hexadecimalRepresentation:(NSData *)pData {
static const char* hexDigits = "0123456789ABCDEF";
NSString* result = nil;
size_t length = pData.length;
if (length) {
NSMutableData* tempData = [NSMutableData dataWithLength:(length << 1)]; // double length
if (tempData) {
const unsigned char* src = [pData bytes];
unsigned char* dst = [tempData mutableBytes];
if ((src) &&
(dst)) {
// encode nibbles
while (length--) {
*dst++ = hexDigits[(*src >> 4) & 0x0F];
*dst++ = hexDigits[(*src++ & 0x0F)];
} // while
result = [[NSString alloc] initWithData:tempData
encoding:NSASCIIStringEncoding];
} // if
} // if
} // if
return result;
}
#end
The basic idea was taken from this article.

Related

Rich Text Format Directory File to NSAttributedString

Is it possible to convert an RFTD (Rich Text Format Directory) package to an NSAttributedString in iOS? This is a package that includes an RTF (Rich Text Format) file plus other files like images that are included in the rich text file.
I can convert a normal RTF file like this but I don't know how to convert an RFTD package to an NSData object. I also don't know if it's then possible to convert that NSData object to an NSAttributedString object.
NSString *path = [[NSBundle mainBundle] pathForResource:#"Name" ofType:#"rtf"];
NSData *data = [NSData dataWithContentsOfFile:path];
NSError *error = nil;
NSAttributedString *string = [[NSAttributedString alloc] initWithData:data options:#{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType} documentAttributes:nil error:&error];
It looks like all RTFD-related functions are deliberately cut off from iOS, but since RTFD is just a directory with a normal RTF file, you could try accessing it as such.
If you need attachments, based on the RTF docs here it seems that you can find a marker "NeXTGraphic" inside the RTF file string
{{\NeXTGraphic attachment \widthN \heightN} string}
where "attachment" will be a file name.
Similar question here: Read RTFD data in IOS

Formatted text on UITextView

I struck with a small thing i think so, in my app UITextView plays important role. So i like to add formatting feature (Bold, Italics, underline) to it.
Once i tried using,
[NotesTxtView setAllowsEditingTextAttributes:YES];
it works fine but when i save the data to db the formatted texts change to normal. What can i do for that?
Is there any solution for my problem?
Helpers are appreciated,..
You need to save style information also. NSAttributedString's method dataFromRange:documentAttributes:error: will help:
Returns an data object that contains a text stream corresponding to the characters and attributes within the given range.
So you save and restore NSData object from db.
NSDictionary *attrs = #{NSDocumentTypeDocumentAttribute:NSRTFTextDocumentType};
// export data
NSData *data =
[self.textView.attributedText
dataFromRange:NSMakeRange(0, self.textView.text.length)
documentAttributes:attrs
error:nil];
...
// save data to db, fetch later
...
// restore
self.textView.attributedText =
[[NSAttributedString alloc]
initWithData:data
options:nil
documentAttributes:&attrs
error:nil];
Consider using other document types (all available from iOS 7):
NSString *NSPlainTextDocumentType;
NSString *NSRTFTextDocumentType;
NSString *NSRTFDTextDocumentType;
NSString *NSHTMLTextDocumentType;

Creating a PDF From UITextView

I'm currently creating and saving a PDF of a UIView. It is working great, but now I'm trying to create a PDF from text data saved to the disk rather than from the UIView. Ideally, I would like to create a PDF based on the text in the UITextView. The below code writes the first part of the text view just fine, but it doesn't create the PDF from the entire text view. It is quite a bit of text, so it only created the PDF from the amount of text that would fit on the view. The code that I'm currently using is this:
- (void)createPDFfromUIView:(UITextView*)view saveToDocumentsWithFileName:(NSString*)aFilename
{
NSMutableDictionary *myDictionary = CFBridgingRelease(CFDictionaryCreateMutable(NULL, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks));
// Creates a mutable data object for updating with binary data, like a byte array
NSMutableData *pdfData = [NSMutableData data];
// Points the pdf converter to the mutable data object and to the UIView to be converted
UIGraphicsBeginPDFContextToData(pdfData, textview.bounds, myDictionary);
UIGraphicsBeginPDFPage();
CGContextRef pdfContext = UIGraphicsGetCurrentContext();
// draws rect to the view and thus this is captured by UIGraphicsBeginPDFContextToData
[textview.layer renderInContext:pdfContext]; // this line
// remove PDF rendering context
UIGraphicsEndPDFContext();
// Retrieves the document directories from the iOS device
NSArray* documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES);
NSString* documentDirectory = [documentDirectories objectAtIndex:0];
NSString* documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent:#"filename"];
// instructs the mutable data object to write its context to a file on disk
[pdfData writeToFile:documentDirectoryFilename atomically:YES];
}
And to call it / save it, I put this message send in an IBAction: [self createPDFfromUIView:textview saveToDocumentsWithFileName:#"pdflocation"];
Now, I've been parsing through this code and the docs for hours, and cannot figure out how to create a PDF based upon a saved text file rather than the view. I'm still new to programming, so a lot of this is going over my head. Does anyone know how ot change the above code to have it create a PDF based upon saved text rather than the view?
Options: I could create a PDF based on output of an NSString. I could also throw the saved text into a UITextView and create the PDF based upon that (original plan) I however don't know how to do either.
This article from Apple explains how to create Pdf files and draw text on Pdf pages.
For those still looking for some sample code, this works:
- (NSData *)createPDFforText: (NSString *)text
{
NSMutableData *pdfData = [NSMutableData data];
NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
UIGraphicsBeginPDFContextToData(pdfData, CGRectZero, nil);
UIGraphicsBeginPDFPage();
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
paragraphStyle.alignment = NSTextAlignmentLeft;
[attributes setObject: paragraphStyle forKey: NSParagraphStyleAttributeName];
[attributes setObject: [UIFont fontWithName: LATO_REGULAR size: 20.f] forKey: NSFontAttributeName];
[attributes setObject: [UIColor blackColor] forKey: NSForegroundColorAttributeName];
[text drawAtPoint: CGPointMake(20.f, 44.f) withAttributes: attributes];
UIGraphicsEndPDFContext();
return pdfData;
}
The CGRectZero bounds value sets the PDF document to the default page size of 8.5 by 11 inches (612 by 792 points).

How to convert formatted content of NSTextView to string

I need transfer content of NSTextView from Mac app to iOS app. I'm using XML as transfered file format.
So I need to save content of NSTextView (text, fonts, colors atd.) as a string. Is there any way how to do that?
One way to do this is to archive the NSAttributedString value. Outline sample code typed directly into answer:
NSTextView *myTextView;
NSString *myFilename;
...
[NSKeyedarchiver archiveRootObject:myTextStorage.textStorage
toFile:myFilename];
To read it back:
myTextView.textStorage.attributedString = [NSKeyedUnarchiver unarchiveObjectWithFile:myFilename];
That's all that is needed to create and read back a file. There are matching methods which create an NSData rather than a file, and you can convert an NSData into an NSString or just insert one into an NSDictionary and serialise that as a plist (XML), etc.
Your best bet is probably to store the text as RFTD and load it as such in the other text view via an NSAttributedString.
// Load
NSFileWrapper* filewrapper = [[NSFileWrapper alloc] initWithPath: path];
NSTextAttachment *attachment = [[NSTextAttachment alloc] initWithFileWrapper: filewrapper];
NSAttributedString* origFile = [NSAttributedString attributedStringWithAttachment: attachment];
// Save
NSData *data = [origFile RTFDFromRange: NSMakeRange(0, [origFile length]) documentAttributes: nil];
[[NSFileManager defaultManager] createFileAtPath: path contents: data attributes:nil];

Copy NSAttributedString in UIPasteBoard

How do you copy an NSAttributedString in the pasteboard, to allow the user to paste, or to paste programmatically (with - (void)paste:(id)sender, from UIResponderStandardEditActions protocol).
I tried:
UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard];
[pasteBoard setValue:attributedString forPasteboardType:(NSString *)kUTTypeRTF];
but this crash with:
-[UIPasteboard setValue:forPasteboardType:]: value is not a valid property list type'
which is to be expected, because NSAttributedString is not a property list value.
If the user paste the content of the pasteboard in my app, I would like to keep all the standards and custom attributes of the attributed string.
I have found that when I (as a user of the application) copy rich text from a UITextView into the pasteboard, the pasteboard contains two types:
"public.text",
"Apple Web Archive pasteboard type
Based on that, I created a convenient category on UIPasteboard.
(With heavy use of code from this answer).
It works, but:
The conversion to html format means I will lose custom attributes. Any clean solution will be gladly accepted.
File UIPasteboard+AttributedString.h:
#interface UIPasteboard (AttributedString)
- (void) setAttributedString:(NSAttributedString *)attributedString;
#end
File UIPasteboard+AttributedString.m:
#import <MobileCoreServices/UTCoreTypes.h>
#import "UIPasteboard+AttributedString.h"
#implementation UIPasteboard (AttributedString)
- (void) setAttributedString:(NSAttributedString *)attributedString {
NSString *htmlString = [attributedString htmlString]; // This uses DTCoreText category NSAttributedString+HTML - https://github.com/Cocoanetics/DTCoreText
NSDictionary *resourceDictionary = #{ #"WebResourceData" : [htmlString dataUsingEncoding:NSUTF8StringEncoding],
#"WebResourceFrameName": #"",
#"WebResourceMIMEType" : #"text/html",
#"WebResourceTextEncodingName" : #"UTF-8",
#"WebResourceURL" : #"about:blank" };
NSDictionary *htmlItem = #{ (NSString *)kUTTypeText : [attributedString string],
#"Apple Web Archive pasteboard type" : #{ #"WebMainResource" : resourceDictionary } };
[self setItems:#[ htmlItem ]];
}
#end
Only implemented setter. If you want to write the getter, and/or put it on GitHub, be my guest :)
Instead of involving HTML, the clean solution is to insert NSAttributedString as RTF (plus plaintext fallback) into the paste board:
- (void)setAttributedString:(NSAttributedString *)attributedString {
NSData *rtf = [attributedString dataFromRange:NSMakeRange(0, attributedString.length)
documentAttributes:#{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType}
error:nil];
self.items = #[#{(id)kUTTypeRTF: [[NSString alloc] initWithData:rtf encoding:NSUTF8StringEncoding],
(id)kUTTypeUTF8PlainText: attributedString.string}];
}
Swift 5
import MobileCoreServices
public extension UIPasteboard {
func set(attributedString: NSAttributedString) {
do {
let rtf = try attributedString.data(from: NSMakeRange(0, attributedString.length), documentAttributes: [NSAttributedString.DocumentAttributeKey.documentType: NSAttributedString.DocumentType.rtf])
items = [[kUTTypeRTF as String: NSString(data: rtf, encoding: String.Encoding.utf8.rawValue)!, kUTTypeUTF8PlainText as String: attributedString.string]]
} catch {
}
}
}
It is quite simple:
#import <MobileCoreServices/UTCoreTypes.h>
NSMutableDictionary *item = [[NSMutableDictionary alloc] init];
NSData *rtf = [attributedString dataFromRange:NSMakeRange(0, attributedString.length)
documentAttributes:#{NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType}
error:nil];
if (rtf) {
[item setObject:rtf forKey:(id)kUTTypeFlatRTFD];
}
[item setObject:attributedString.string forKey:(id)kUTTypeUTF8PlainText];
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.items = #[item];
The pasteboard manager in OSX can auto convert between a lot of textual and image types.
For rich text types, you'd usually place RTF into the pasteboard. You can create RTF representation from an attributed string, and vice versa. See the "NSAttributedString Application Kit Additions Reference".
If you have images included as well, then use the RTFd instead of RTF flavor.
I don't know the MIME types for these (I'm used to the Carbon Pasteboard API, not the Cocoa one), but you can convert between UTIs, Pboard and MIME Types using the UTType API.
UTI for RTF is "public.rtf", for RTFd it's "com.apple.flat-rtfd".

Resources