Is it Possible to save AttributedString with image to RTF doc?

Hello,

I am trying to implement an RTF document export feature on a small app. But after days of trying and failing with dozens of approaches I’m beginning to wonder if it is even possible to create an .rtf file with embedded text and images in Swift/SwiftUI.

I’ve tried many variations of (A) building the text and image content with HTML, converting it to AttributedString, then exporting to RTF, and (B) building the text and image content directly with AttributedString attributes and attachments for the image — and in both cases, the images are not saved in the RTF file.

I am able to create a preview of the AttributedString with formatted text and image, and able to create an RTF file with formatted text that opens with TextEdit, Pages and Word without issue; but cannot get the image to appear in the saved RTF file. I’m hoping someone here can shed some light on if this is possible and if yes, how to save the combined text and image data to an RTF file.

Here is the latest variation of the code I’m using — any ideas/suggestions are appreciated 🙏🏽:

import SwiftUI

struct ContentView: View {
    @State private var showExportSheet = false
    @State private var rtfData: Data?
    @State private var isLoading = false
    @State private var previewAttributedString: NSAttributedString?
    
    var body: some View {
        VStack {
            Button("Export RTF with Image") {
                isLoading = true
                createRTFWithEmbeddedImage()
            }
            .disabled(isLoading)
            
            if isLoading {
                ProgressView()
            }
            
            if let previewAttributedString = previewAttributedString {
                VStack {
                    Text("Preview:")
                        .font(.headline)
                    TextView(attributedString: previewAttributedString)
                        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
                        .background(Color.gray.opacity(0.1))
                }
                .padding()
            }
        }
        .sheet(isPresented: $showExportSheet) {
            DocumentPicker(rtfData: $rtfData)
        }
    }
    
    func createRTFWithEmbeddedImage() {
        let text = "This is a sample RTF document with an embedded image:"
        
        // Load the image (star.fill as a fallback)
        guard let image = UIImage(systemName: "star.fill") else {
            print("Failed to load image")
            isLoading = false
            return
        }
        
        // Resize the image to 100x100 pixels
        let resizedImage = resizeImage(image: image, targetSize: CGSize(width: 100, height: 100))
        
        // Convert image to NSTextAttachment
        let attachment = NSTextAttachment()
        attachment.image = resizedImage
        
        // Set bounds for the image
        attachment.bounds = CGRect(x: 0, y: 0, width: 100, height: 100)
        
        // Create attributed string with the attachment
        let attributedString = NSMutableAttributedString(string: text)
        let attachmentString = NSAttributedString(attachment: attachment)
        attributedString.append(attachmentString)
        
        // Add red border around the image
        attributedString.addAttribute(.strokeColor, value: UIColor.red, range: NSRange(location: attributedString.length - attachmentString.length, length: attachmentString.length))
        attributedString.addAttribute(.strokeWidth, value: -2.0, range: NSRange(location: attributedString.length - attachmentString.length, length: attachmentString.length))
        
        // Set previewAttributedString for preview
        self.previewAttributedString = attributedString
        
        // Convert attributed string to RTF data
        guard let rtfData = try? attributedString.data(from: NSRange(location: 0, length: attributedString.length),
                                                       documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf]) else {
            print("Failed to create RTF data")
            isLoading = false
            return
        }
        
        self.rtfData = rtfData
        isLoading = false
        showExportSheet = true
        
        // Debug: Save RTF to a file in the Documents directory
        saveRTFToDocuments(rtfData)
    }
    
    func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
        let size = image.size
        let widthRatio = targetSize.width / size.width
        let heightRatio = targetSize.height / size.height
        let newSize: CGSize
        if widthRatio > heightRatio {
            newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio)
        } else {
            newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio)
        }
        let rect = CGRect(origin: .zero, size: newSize)
        
        UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
        image.draw(in: rect)
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        return newImage ?? UIImage()
    }
    
    func saveRTFToDocuments(_ data: Data) {
        let fileManager = FileManager.default
        guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
            print("Unable to access Documents directory")
            return
        }
        
        let fileURL = documentsDirectory.appendingPathComponent("debug_output.rtf")
        
        do {
            try data.write(to: fileURL)
            print("Debug RTF file saved to: \(fileURL.path)")
        } catch {
            print("Error saving debug RTF file: \(error)")
        }
    }
}

struct DocumentPicker: UIViewControllerRepresentable {
    @Binding var rtfData: Data?
    
    func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("document_with_image.rtf")
        do {
            try rtfData?.write(to: tempURL)
        } catch {
            print("Error writing RTF file: \(error)")
        }
        
        let picker = UIDocumentPickerViewController(forExporting: [tempURL], asCopy: true)
        return picker
    }
    
    func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {}
}

struct TextView: UIViewRepresentable {
    let attributedString: NSAttributedString
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.isEditable = false
        textView.attributedText = attributedString
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.attributedText = attributedString
    }
}

Thank in advance!

Answered by szymczyk in 794162022

To save images in a rich text document, you need to save the file as an RTFD file. An RTFD file is an RTF file with support for attachments, such as image files.

You set the document type to RTF in the createRTFWithEmbeddedImage function.

[.documentType: NSAttributedString.DocumentType.rtf])

If you change the document type to NSAttributedString.DocumentType.rtfd, the file should save as an RTFD file.

I have not tried saving an attributed string with an image to a file so I can't guarantee there isn't more you have to do to get the image to save. But saving the file as RTFD is a start.

Accepted Answer

To save images in a rich text document, you need to save the file as an RTFD file. An RTFD file is an RTF file with support for attachments, such as image files.

You set the document type to RTF in the createRTFWithEmbeddedImage function.

[.documentType: NSAttributedString.DocumentType.rtf])

If you change the document type to NSAttributedString.DocumentType.rtfd, the file should save as an RTFD file.

I have not tried saving an attributed string with an image to a file so I can't guarantee there isn't more you have to do to get the image to save. But saving the file as RTFD is a start.

@szmczyk, thank you very much — that’s exactly what I needed to do. Much appreciated. 🙏🏽

Is it Possible to save AttributedString with image to RTF doc?
 
 
Q