0

I have the following code to draw a text over an NSImage. But the resulting image is getting resized to smaller one when I save it to disk.

What i'm i doing wrong? Please advice

func drawText(image :NSImage) ->NSImage
{
    let text = "Sample Text"
    let font = NSFont.boldSystemFont(ofSize: 18)
    let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)

    let textRect = CGRect(x: 5, y: 5, width: image.size.width - 5, height: image.size.height - 5)
    let textStyle = NSMutableParagraphStyle.default().mutableCopy() as! NSMutableParagraphStyle
    let textFontAttributes = [
        NSFontAttributeName: font,
        NSForegroundColorAttributeName: NSColor.white,
        NSParagraphStyleAttributeName: textStyle
    ]

    let im:NSImage = NSImage(size: image.size)

    let rep:NSBitmapImageRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(image.size.width), pixelsHigh: Int(image.size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace, bytesPerRow: 0, bitsPerPixel: 0)!

    im.addRepresentation(rep)

    im.lockFocus()

    image.draw(in: imageRect)
    text.draw(in: textRect, withAttributes: textFontAttributes)

    im.unlockFocus()

    return im
}
techno
  • 6,100
  • 16
  • 86
  • 192
  • 1
    "I have the following code to draw a text over an NSImage. But the resulting image is getting resized to smaller one when I save it to disk." Yeah, we already heard it several hours ago. Why are you deleting and then re-submitting the same topic over and over? – El Tomato Oct 18 '17 at 06:42
  • @ElTomato In that question I mentioned image was getting cut.. but actually the image is getting resized. – techno Oct 18 '17 at 06:43
  • 1. I would create an attributed string separetely so that you can concentrate on drawTest. 2. Last time, i suggested that you use DRAW AT. So why are you still using draw in? But somebody else said yesterday you should use draw in? Why should I care? – El Tomato Oct 18 '17 at 07:03
  • @ElTomato Sorry... I tried changing .. but I keep getting this compilation error `Command failed due to signal: Segmentation fault: 11` – techno Oct 18 '17 at 07:06
  • @ElTomato Also im just a beginner .. I don't how to create attributed string and stuff... it would be helpful if you point me to some resources or add an example. – techno Oct 18 '17 at 07:08
  • If you get `Segmentation fault: 11` this is a compiler issue and the bug should be reported to Apple. – Eric Aya Oct 18 '17 at 08:39
  • @ElTomato I followed your advice and used `image.draw(at: NSMakePoint(0, 0), from: imageRect, operation:NSCompositeSourceOver ,fraction:1)`,still the same issue. – techno Oct 19 '17 at 06:51
  • @ElTomato It seems the issue is with `bounds.insectBy` – techno Oct 20 '17 at 10:19
  • What issue? Initially, you wanted to know how to make an image out of an attributed string. You seem to have then added totally new lines code. – El Tomato Oct 20 '17 at 10:26
  • @ElTomato Actually i needed to draw a string on an image.My old code for writing the string works fine.But it seems the issue is with this line `bounds.insectBy` in the CImage. – techno Oct 20 '17 at 10:29

2 Answers2

1

This is a different approach using a temporary NSView to draw the image and the text and cache the result in a new image (code is Swift 4). The benefit it you don't need to deal with pixels

class ImageView : NSView  {

    var image : NSImage
    var text : String

    init(image: NSImage, text: String)
    {
        self.image = image
        self.text = text
        super.init(frame: NSRect(origin: NSZeroPoint, size: image.size))
    }

    required init?(coder decoder: NSCoder) { fatalError() }

    override func draw(_ dirtyRect: NSRect) {
        let font = NSFont.boldSystemFont(ofSize: 18)
        let textRect = CGRect(x: 5, y: 5, width: image.size.width - 5, height: image.size.height - 5)
        image.draw(in: dirtyRect)
        text.draw(in: textRect, withAttributes: [.font: font, .foregroundColor: NSColor.white])
    }

    var outputImage : NSImage  {
        let imageRep = bitmapImageRepForCachingDisplay(in: frame)!
        cacheDisplay(in: frame, to:imageRep)
        let tiffData = imageRep.tiffRepresentation!
        return NSImage(data : tiffData)!
    }
}

To use it, initialize a view

let image = ... // get some image
let view = ImageView(image: image, text: "Sample Text")

and get the new image

let imageWithText = view.outputImage

Note:

The paragraph style is not used at all, but if you want to create a mutable paragraph style just write

let textStyle = NSMutableParagraphStyle()
vadian
  • 274,689
  • 30
  • 353
  • 361
  • Thanks..Should I put the class in a separate swift file.Im getting segmentation fault....Also I'm using SWIFT 3 (Xcode 8) – techno Oct 20 '17 at 08:24
  • A separate file is not necessary. The class must on the top level of the file. The reason of the segmented fault is somewhere else. The main Swift 4 difference are the text attributes. In Swift 3 it's `..., withAttributes: [NSFontAttributeName: font, NSForegroundColorAttributeName: NSColor.white]` – vadian Oct 20 '17 at 08:26
  • Thanks..now the code runs but the same issue persists(cropping and white space).Have you seen my update.I think the issue is with `bounds.insectBy`.Please advice. – techno Oct 20 '17 at 08:38
  • My code doesn't crop anything. It uses exactly the size of the source image. The cropping is most likely related to `CIDetector`. I just answered the question ( ... getting resized ...). The code doesn't resize the image. – vadian Oct 20 '17 at 08:40
  • Yeah... I'm doing the cropping here 'let ciimage = ciImage.cropping' When I remove 'bounds.insectBy' my old code to draw text works too.This the core problem I'm facing.Could you please tell me what's going wrong? – techno Oct 20 '17 at 08:50
  • Sorry, I'm not familiar with `CIDetector`. Once again my answer is related to the original resizing problem. – vadian Oct 20 '17 at 08:54
  • Thanks for your answer.. I will have to wait for 3 hours to award the bounty... I have one more query.. how I can adjust the text size to match the image size and make it appear in the centre.. Please advice. – techno Oct 20 '17 at 18:06
  • I don't understand what *text size to match the image size* means. Positioning the text is just math. The center point is `width/2`, `height/2` – vadian Oct 20 '17 at 18:13
  • yeah.. I get the math..What I intent to achieve is to make sure the Text Don't look too small in large resolution images.. so I need to scale the text accordingly ...Please see this https://stackoverflow.com/a/39754866/848968 – techno Oct 20 '17 at 18:16
  • Actually it's math, too. You can define a relation between image width and text size and you can calculate the text width for a specific text length with the linked code. But this is beyond the scope of this question. I would ask a new one. – vadian Oct 20 '17 at 18:26
  • I think I should drop the textrectangle and use `draw(at:` like this `text.draw(at: NSZeroPoint, withAttributes: [NSFontAttributeName: font, NSForegroundColorAttributeName: NSColor.white])` – techno Oct 21 '17 at 03:14
  • and it seems the coordinate system starts from the bottom left corner. – techno Oct 21 '17 at 03:15
  • Yes, indeed the origin of the coordinate system in macOS is bottom left. See https://developer.apple.com/library/content/documentation/General/Conceptual/Devpedia-CocoaApp/CoordinateSystem.html – vadian Oct 21 '17 at 03:56
  • I'm noticing the image is shifted to the top right corner when I use your code when running on Swift 5 in Xcode 13. Does it have something to do with the update? – techno Apr 27 '22 at 04:17
0

Mixed pixel vs point? Depending on your screen 2x or 3x image is smaller 2 times or 3 times?

Here is more detailed info (scroll down to "Converting between pixels and points") http://blog.fluidui.com/designing-for-mobile-101-pixels-points-and-resolutions/

But keep in mind that:

NSImage is resolution aware and uses a HiDPI graphics context when you lockFocus on a system with retina screen. The image dimensions you pass to your NSBitmapImageRep initializer are in points (not pixels). An 150.0 point-wide image therefore uses 300 horizontal pixels in a @2x context.

Source: How to save PNG file from NSImage (retina issues)

Following simple app works for me. Enjoy ;)

import Cocoa

class ViewController: NSViewController {

    func save(image:NSImage, imageURL:String, format:String) -> Bool
    {
        let bMImg = NSBitmapImageRep(data: (image.tiffRepresentation)!)
        switch format {
        case ".png":
            let filepath = URL(fileURLWithPath: imageURL+".png")
            let dataToSave = bMImg?.representation(using: NSBitmapImageRep.FileType.png, properties: [NSBitmapImageRep.PropertyKey.compressionFactor : 1])
            do
            {
                try  dataToSave?.write(to: filepath)
                return true

            } catch {
                return false
            }
        default:
            return false
        }
    }

    func draw(text:String, image:NSImage) -> NSImage
    {
        let font = NSFont.boldSystemFont(ofSize: 18)
        let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)

        let textRect = CGRect(x: 5, y: 5, width: image.size.width - 5, height: image.size.height - 5)
        let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
        let textFontAttributes = [
            NSAttributedStringKey.font: font,
            NSAttributedStringKey.foregroundColor: NSColor.white,
            NSAttributedStringKey.paragraphStyle: textStyle
        ]

        let im:NSImage = NSImage(size: image.size)

        let rep:NSBitmapImageRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(image.size.width), pixelsHigh: Int(image.size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSColorSpaceName.calibratedRGB, bytesPerRow: 0, bitsPerPixel: 0)!

        im.addRepresentation(rep)

        im.lockFocus()

        image.draw(in: imageRect)
        text.draw(in: textRect, withAttributes: textFontAttributes)

        im.unlockFocus()

        return im
    }

    @IBAction func action(_ sender: NSButton) {
        let dialog = NSOpenPanel();

        dialog.title                   = "Choose a image...";
        dialog.showsResizeIndicator    = true;
        dialog.showsHiddenFiles        = false;
        dialog.canChooseDirectories    = true;
        dialog.canCreateDirectories    = true;
        dialog.allowsMultipleSelection = false;
        dialog.allowedFileTypes        = ["png", "jpg"];

        if (dialog.runModal() == NSApplication.ModalResponse.OK) {
            guard let url = dialog.url,
                let imageCIImage = CIImage(contentsOf: url) else {
                    return
            }
            let rep: NSCIImageRep = NSCIImageRep(ciImage: imageCIImage)
            let nsImage = NSImage(size: rep.size)
            nsImage.addRepresentation(rep)

            let imageWithText = draw(text:"ABC", image: nsImage)

            if (save(image: imageWithText, imageURL: "imageWithText", format: ".png")) {
                print("Success")
            } else {
                print("ERROR:Failed to save image")
            }
        } else {
            // User clicked on "Cancel"
            return
        }
    }
}
Tomasz Czyżak
  • 1,118
  • 12
  • 13
  • Thanks.. but I'm developing for OSX.I don't need the image to be screen size dependant.I just need to put some text on the image..How can I modify the code? – techno Oct 18 '17 at 07:01
  • Look here: https://stackoverflow.com/questions/27790315/macos-programmatically-add-some-text-to-an-image – Tomasz Czyżak Oct 18 '17 at 07:08
  • I have translated that objective C Code.But the same issue occurs again .. please see https://paste.ofcode.org/9ZbP74YPYHYsvgTea367 – techno Oct 18 '17 at 07:32
  • I just verified your code and if I pass 100x100 image do drawRect method then it returns the same size of image (with text inside) so please post your code which handle save? – Tomasz Czyżak Oct 18 '17 at 10:47
  • I could reproduce the issue with this image https://peopledotcom.files.wordpress.com/2016/11/arnold-schwarzenegger.jpg?w=1514 – techno Oct 18 '17 at 11:57
  • This is the code I use to save the image https://paste.ofcode.org/rUB9qjGeiiweHV3diaS5DD – techno Oct 18 '17 at 11:57
  • I'm assuming that source image is added thru assets? can you show me .assets screenshot? – Tomasz Czyżak Oct 18 '17 at 12:12
  • Its opened by user using open file dialog..Could you reproduce the issue on your side, with the image I have sent? – techno Oct 18 '17 at 12:13
  • what is size of image after it is being set thru open dialog? – Tomasz Czyżak Oct 18 '17 at 12:36
  • Please see this https://stackoverflow.com/q/46824301/848968 .. I think the issue is not with the draw image method.. its with the code `bounds.insetBy` – techno Oct 19 '17 at 07:14
  • Please see https://imgur.com/a/aaC8P there are some errors . On removing `bounds.insetBy` my old code works... but I need to change the bounds. – techno Oct 19 '17 at 07:20