保存带图片的NSAttributedString到RTF文件时出现问题

5

我有一些非常简单的RTF文件输出。当我生成这个文档时,用户可以通过电子邮件发送它。所有这些都很好用。文档看起来很好。一旦我有了NSAttributedString,我就会制作一个NSData块,并将其写入文件中,就像这样:

NSData* rtfData = [attrString dataFromRange:NSMakeRange(0, [attrString length]) documentAttributes:@{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType} error:&error];

这个文件可以通过电子邮件发送。当我检查邮件时一切正常。
现在,我的任务是在文档顶部添加一个UIImage。很好,所以我正在创建一个类似于这样的属性字符串:
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"]];

在Xcode中,此时我可以对imageAttrString进行QuickLook,并且它可以正常绘制。

一旦构建了此字符串,我会执行以下操作:

[attrString appendAttributedString:imageAttrString];

然后加入我最初生成的所有其他属性文本。

现在查看文件时,没有图像。QuickLook在调试器中看起来很好,但最终输出中没有图像。

提前感谢任何对此的帮助。


由于这是一个有趣的问题,我尝试了一些方法。我遇到了同样的问题。但是,当我尝试使用.rtfd(而不是.rtf)进行操作,并使用TextWrangler或TextEdit打开它们时,我发现附件比rtf文件中的“更多”:{{\NeXTGraphic Attachment.png \width400 \height400}¨},以及一堆数据在结尾处,在十六进制转储中,我看到标记:89 50 4E 47 0D 0A 1A 0A。也许这是值得研究的东西。我还看到你尝试了Apple Dev论坛,也许可以报告一个错误。 - Larme
2个回答

7
如Andris所提到的,苹果公司的RTF实现不支持嵌入式图片。
RTFD并不是一个真正的替代方案,因为只有少数OS X应用程序可以打开RTFD文件。例如,微软办公室无法打开。
在某些情况下,创建一个带有嵌入式图像的HTML文件可能会有所帮助,但是大多数电子邮件客户端不支持带有嵌入式图像的HTML(Apple Mail可行,Outlook不可行)。
但是幸运的是,有一种解决方案可以创建具有嵌入式图像的真正的RTF文件!
由于RTF格式当然支持嵌入式图像(只有苹果公司的实现不支持),因此NSTextAttachments中的图像可以(手动)编码到RTF流中。
以下类别完成了所有必要的工作:
/**
 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

这个基本想法源自 这篇文章


6
尽管RTF支持Windows上的嵌入式图像,但显然在OS X上不支持。 RTF是由Microsoft开发的,在1.5版中添加了嵌入式图像。我认为苹果采用了该格式的早期版本,并且他们在文档中解决图像的方法是RTFD。以下是苹果文档对RTF的说明:富文本格式(RTF)是由Microsoft Corporation设计的文本格式语言。您可以使用普通文本表示字符,段落和文档格式属性,并插入RTF命令,组和转义序列。 RTF被广泛用作文档交换格式,以在应用程序和计算平台之间传输带有其格式信息的文档。苹果已使用自定义命令扩展了RTF,这些命令在本章中进行了描述。因此,未提及任何图像。最后,为了证明RTF在mac上不支持图像,请下载此RTF文档-它将在Windows WordPad中显示照片,并在OS X TextEdit中不显示。因此,正如Larme所提到的-添加附件时应选择RTFD文件类型。来自维基百科:
富文本格式目录,也被称为RTFD(由于其扩展名.rtfd),或带有附件的富文本格式。
虽然您可以通过dataFromRange:documentAttributes:@{NSDocumentTypeDocumentAttribute:NSRTFDTextDocumentType} error:]获取包含文本和图像的NSData对象(根据其大小判断),但您可能无法保存它以便成功打开。至少我不能做到这一点。
这可能是因为实际上RTFD不是文件格式-它是捆绑格式。要检查它,您可以在mac上使用TextEdit创建一个新文档,添加图像和文本,并将其保存为文件。然后右键单击该文件并选择“显示包内容”,您会注意到该目录包含您的图像和RTF格式的文本。
但是,您将能够使用此代码成功保存此文档:
NSFileWrapper *fileWrapper = [imageAttrString fileWrapperFromRange:NSMakeRange(0, [imageAttrString length]) documentAttributes:@{NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType} error:&error];
[fileWrapper writeToURL:yourFileURL options:NSFileWrapperWritingAtomic originalContentsURL:nil error:&error];

因为NSFileWrapper知道如何处理RTFD文档,而NSData不知道它包含的内容,所以显然NSFileWrapper可以处理RTFD文档。
但是主要问题仍然存在 - 如何通过电子邮件发送它?因为RTFD文档是一个目录而不是文件,我认为它不太适合通过电子邮件发送,然而你可以将其压缩并使用扩展名.rtfd.zip发送。这里的扩展名非常关键,因为它会告诉邮件应用程序如何在用户点击附件时显示附件的内容。实际上,它也适用于Gmail和iOS上的其他电子邮件应用程序,因为UIWebView知道如何显示.rtfd.zip。这里有一个技术说明:https://developer.apple.com/library/ios/qa/qa1630/_index.html#//apple_ref/doc/uid/DTS40008749 因此,底线是 - 它可以完成,但RTFD文档将作为电子邮件的附件而不是电子邮件内容本身。如果你想将其作为电子邮件内容,请考虑将图像嵌入HTML并将邮件作为HTML发送。

顺便提一下,可以将图像嵌入到HTML文件中 - 因此,如果您需要发送一个包含文本和图像的单个文件,请考虑使用HTML而不是RTF。 - Thomas Tempelmann
嗨,Andris。我考虑过使用RTFD,但是RTF规范说它可以包含图像,如此描述:链接。而NSTextAttachment有一个属性,可以接受UIImage。因此,考虑到这一点,我应该能够做到这一点。如果有一些文档告诉我这是完全不支持的,我很想阅读它。但是看起来这就是这个API的设计初衷。对我来说,这不合理。 - Dan Morrow
我更新了答案,包括关于RTF在Mac上不支持嵌入式图像的信息。此外,NSTextAttachment具有一个属性,可以使用UIImage,但这并不意味着生成的格式将是RTF。正如我在答案中提到的那样 - 您可以使用您的代码并将其保存为RTFD。因此,您将使用NSTextAttachment将图像添加到您的RTFD文档中。 - Andris Zalitis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接