3

I am trying to replace an image in an NSAttributedString with a scaled copy of itself (to fit a UITextView's width), however, in many (not all) cases, the replaced image is a mask of the original. I assume that there is something in the other attributes of the string which is doing this as both the image extraction and the creation of a new attributed string from the image work perfectly.

Note: answers like this how to resize an image or done as a NSAttributedString NSTextAttachment (or set its initital size) show how to construct a string with images (which I can do) or how to modify the size of a text attachment (again, I can do that) prior to creating the string, but not how to modify in place.

Update Thanks to suggestions by commentator @Larme I have now solved my original problem of resizing an image in-place by changing the original NSTextAttachment.bounds - note, not replacing it, changing it in situ. I'm not going to post that as an answer to my own question, as it doesn't answer the wider question of replacing an image.

Any tips as to how to proceed gratefully accepted.

Here's the basis:

extension NSAttributedString {
    func resizeAttachments(maxWidth: CGFloat) -> NSAttributedString {

        var replacement = NSMutableAttributedString(attributedString: self)

        func replace(_ image: UIImage, in range: NSRange, dict: [NSAttributedString.Key : Any]) {
            // ... see below for variants of this
        }

        self.enumerateAttributes(in: NSMakeRange(0, self.length),
                                 options: [],
                                 using: {dict, range, stop in
            for (key, value) in dict {
                if key == .attachment, let attachment = value as? NSTextAttachment {
                    if
                        let fw = attachment.fileWrapper, fw.isRegularFile,
                        let d = fw.regularFileContents,
                        let image = UIImage(data: d)?.resized(toWidth: maxWidth) { 
                        // removing the resize above makes no difference to the issue
                        // i.e. replacing the image with itself still causes the same problem
                        replace(image, in: range, dict: dict)
                        return // Exit the block. Can't have 2 .attachments.
                    }
                }
            }
        })
        return replacement
    }
}

So far, a straight forward enumeration on self, modifying the mutable copy string replacement when an image is encountered. Here's what I get:

enter image description here

From this unscaled original:

enter image description here

So, what have I tried in the replace function?

    func replace(_ image: UIImage, in range: NSRange, dict: [NSAttributedString.Key : Any]) {
        let newAttachment = NSTextAttachment(image: image)
        let newCharacter = NSMutableAttributedString(attachment: newAttachment)
        // NB if I replace the entire string with `newCharacter` the image is perfect (but obviously the rest of the string has gone)

        // Option 1
        replacement.replaceCharacters(in: range, with: newCharacter)

        // Option 2
        var newDict: [NSAttributedString.Key : Any] = dict
        newDict[.attachment] = newAttachment
        replacement.addAttributes(newDict, range: range)

        // Option 3
        replacement.removeAttribute(.attachment, range: range)
        replacement.addAttribute(.attachment, value: newAttachment, range: range)

    }

I can't find anything anywhere about actually replacing images. As I said, the extraction of the image works fine - if I just return a new attributed string with only the extracted image then it's perfect. If I append the image to the end, it works. It's modifying it in place that seems to be the issue. Has anyone ever done this successfully?

Grimxn
  • 22,115
  • 10
  • 72
  • 85
  • 1
    Does this answer your question? [how to resize an image or done as a NSAttributedString NSTextAttachment (or set its initital size)](https://stackoverflow.com/questions/22357171/how-to-resize-an-image-or-done-as-a-nsattributedstring-nstextattachment-or-set) – Willeke Jan 06 '20 at 10:05
  • No I don't think it does... I saw this question - the accepted answer says "subclass NSTextAttachment" (not very usefully), and the other answers are about appending the image (which I can do), not modifying it "in place" i.e. not at the start or end. It's not the resizing that's the issue, it's (I think) the effect of surrounding attributes. – Grimxn Jan 06 '20 at 10:32
  • What about trying changing the underlying images? And then force the attributedString to redraw? – Filip Jan 06 '20 at 13:43
  • I'll try that, thanks... – Grimxn Jan 06 '20 at 14:32
  • Forcing the newly created attachment bounds (`newAttachment.bounds = CGRect(origin: .zero, size: image.size)`) doesn't work? Also, a needLayout might work? – Larme Jan 06 '20 at 17:16
  • Unrelated but instead of enumerating all the attributes with `enumerateAttributes()`, you can call `enumerateAttribute(_:in:options:using:)` focusing only in attachments. – Larme Jan 06 '20 at 17:20
  • @Larme - many thanks. I have fixed my immediate problem by starting with your suggestion on `NSTextAttachment.bounds` - but not, as you suggest on the `newAttachment` but simply modifying the original attachment's bounds (d'oh! should have tried that to start with!). So I can resize the attachment, but I still can't *replace* an image... not quite sure what to do about the question, now. :) – Grimxn Jan 06 '20 at 18:59

0 Answers0