Convert NSAttributedString to RTFD and back in Swift

Viewed 390

I have an NSAttributedString that contains text and images with attributes associated to them. I'm trying to convert the NSAttributedString into an RTFD so that it can be uploaded to Cloud Firestore and then can be converted back from an RTFD into an NSAttributedString.

I'm currently using these extensions to convert from an NSAttributedString to an RTFD and back but they are returning nil.

extension NSAttributedString {
    var attributedString2rtfd: String? {
        do {
            let rtfdData = try self.data(from: NSRange(location: 0, length: self.length), documentAttributes:[.documentType: NSAttributedString.DocumentType.rtfd])
            return String.init(data: rtfdData, encoding: String.Encoding.utf8)
        } catch {
            print("error:", error)
            return nil
        }
    }
}
extension String {
    var rtfd2AttributedString: NSAttributedString? {
        do {
            guard let data = data(using: String.Encoding.utf8) else {
                return nil
            }
            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.rtfd,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }
}

These extensions work when I replace all of the "rtfd" with "rtf" and only have text in the attributed string. However, when I include images in the attributed string and use the extensions with "rtfd," as shown above, they both return nil. What am I doing wrong here?

2 Answers

Hope this will help you, it works for me by using fileWrapper.

To save data:

let directoryURL = appDelegate.directoryPathURL //Directory URL path, folder should be named as "name.rtfd"
    
let details = textView.attributedText 
let documentAttributes = [NSAttributedString.DocumentAttributeKey.documentType: NSAttributedString.DocumentType.rtfd]
let fileWrapper = try? details.fileWrapper(from: NSMakeRange(0, details.length), documentAttributes: documentAttributes)
do {
    try fileWrapper?.write(to: directoryURL!, options: .atomic, originalContentsURL: nil)
    print("saved")
} catch {
    print(error)
}
    

To Get data-

let rtfdDirectoryURL = appDelegate.directoryPathURL //Directory URL path, folder should be named as "name.rtfd"

let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtfd]
do {
    let attributedString = try NSAttributedString(url: rtfdDirectoryURL!, options: options, documentAttributes: nil)
    textView.attributedText = attributedString
} catch {
    print("\(error.localizedDescription)")
}

I met the same problem a few days ago. I solve it by removing String.Encoding.utf8 at both encoding and decoding part of the code. If you save the utf8 encoded rtfd data to a file and then open it by any utf8 compatible text editor, you can tell that whether you use utf8 to encode it or not, the text is not readable, which means it is not utf8 encoded anyway. For this reason, swift is not able to read from it using utf8 decoding.

After some searching, I also noticed that when people convert NSAttributedString to rtf, rtfd, HTML, and plain formatted Data, they don't use the String.Encoding.utf8 option.

Therefore, my solution is like this. And it is tested in my environment.

extension NSAttributedString {
    
    func rtf() throws -> Data {
        try data(.rtf)
    }
    
    func rtfd() throws -> Data {
        try data(.rtfd)
    }
    
    func html() throws -> Data {
        try data(.html)
    }
    
    func plain() throws -> Data {
        try data(.plain)
    }
    
    func data(_ documentType: NSAttributedString.DocumentType) throws -> Data {
        return try data(from: .init(location: 0, length: length),
                        documentAttributes: [.documentType: documentType])
    }
}

    func saveDocumentToFile(_ document: NSAttributedString,
                            documentType: NSAttributedString.DocumentType) {
        guard document.length > 0 else { return }
        
        
        do {
            if let fileURL = ... {
                switch documentType {
                case .rtf:
                    try document.rtf().write(to: fileURL)
                case .html:
                    try document.html().write(to: fileURL)
                case .rtfd:
                    try document.rtfd().write(to: fileURL)
                default:
                    try document.plain().write(to: fileURL)
                }
                
            }
        } catch {
            print("Error when save document to file: \(error.localizedDescription)")
        }
    }

    func loadDocumentFromFile(_ documentType: NSAttributedString.DocumentType) -> NSAttributedString? {
        let fileURL = ...
        if fileURL != nil, FileManager().fileExists(atPath: fileURL!.path) {
            do {
                let fileData = try Data(contentsOf: fileURL!)
                let contentString = try NSAttributedString(data: fileData,
                                                           options: [.documentType: documentType],
                                                           documentAttributes: nil)
                if contentString.length > 0 {
                    return contentString
                }
            } catch {
                print("Error when loading: \(error.localizedDescription)")
            }
        }
        return nil
    }
    
Related