I'm trying to in an [string] array for a PDF. Below is what I have so far. I am guess I need to do a foreach somewhere, but I'm not entirely sure.
I thought something like this might work, but it does not.
for entry in body {
let attributedText = NSAttributedString(
string: entry,
attributes: textAttributes
)
}
private func addBody(body: [String], pageRect: CGRect, textTop: CGFloat) {
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
let bodyCG = addInstructor(instructor: "", pageRect: pageRect)
let textFont = UIFont.systemFont(ofSize: 12.0, weight: .regular)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont
]
let attributedText = NSAttributedString(
string: body,
attributes: textAttributes
)
let textRect = CGRect(
x: 15,
y: bodyCG + 30,
width: pageRect.width - 20,
height: pageRect.height - textTop - pageRect.height / 5.0
)
attributedText.draw(in: textRect)
}
Adding some additional details. If I don't use [String] and just use String everything works fine. The PDF is generated, The concept I'm struggling to understand is how can I pass an array for the PDF.
var courseAttendees : [String] = ["name", "name", "name", "name"]
For Example, I want to pass courseAttendees and then loop through the array the names are just overlapped and shown below.
Final code.
private func addBody(body: [String], textTop: CGFloat) {
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
let bodyCG = addInstructor(instructor: "", pageRect: pageRect)
let textFont = UIFont.systemFont(ofSize: 12.0, weight: .regular)
let textAttributes: [NSAttributedString.Key: Any] =
[NSAttributedString.Key.font: textFont]
// keep track of the y position on the page. You might need
// to set this globally as you have multiple pages
var currentYPos: CGFloat = bodyCG
// Loop through the array
for entry in body {
let attributedText = NSAttributedString(
string: "\(entry)",
attributes: textAttributes
)
// Update the currentYPos
currentYPos += 15
// Use the currentYPos in the textRect
let textRect = CGRect(
x: 15,
y: currentYPos,
width: pageRect.width - 20,
height: pageRect.height - textTop - pageRect.height / 5.0
)
attributedText.draw(in: textRect)
}
}
Based on your question and image, I am assuming the PDF creation works fine but the data is not rendered as desired.
I think the two questions to answer here are:
Where to loop through your array
How to keep track of the current y coordinate in the page which is responsible for the vertical positioning
Here are some additions I made to try and fix your issue, I have added comments to what I have changed
// Somewhere appropriate in your code
var courseAttendees : [String] = ["name1", "name2", "name3", "name4"]
// Call the function, 15 is just a random number for textTop,
// give it what you feel is appropriate
addBody(body: courseAttendees, textTop: 15)
// I have removed the pageRect parameter since you create it
// in the function
private func addBody(body: [String], textTop: CGFloat) {
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
let bodyCG = addInstructor(instructor: "", pageRect: pageRect)
let textFont = UIFont.systemFont(ofSize: 12.0, weight: .regular)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont
]
// keep track of the y position on the page. You might need
// to set this globally as you have multiple pages
var currentYPos: CGFloat = 0.0
// Loop through the array
for entry in body {
let attributedText = NSAttributedString(
string: entry,
attributes: textAttributes
)
// Update the currentYPos
currentYPos += bodyCG + 30
// Use the currentYPos in the textRect
let textRect = CGRect(
x: 15,
y: currentYPos,
width: pageRect.width - 20,
height: pageRect.height - textTop - pageRect.height / 5.0
)
attributedText.draw(in: textRect)
}
}
I have not tested the above so please give this a try and check if it solves your issue.
If not, comment and I will update this accordingly.
Related
I am having an issue with text not wrapping correctly if there is a single quote, or macOS ASCII Extended Character #213 (shift+opt.+]) in a string.
Apple does not escape the media item title string when it is retrieved through the iTunesLibrary framework.
As you can see in the example below, the first string is exactly how it come from the iTunesLibrary using the framework API call. The second string is is the single quote is escaped, the third string is if I use macOS Extended ASCII Character code 213, and the fourth string is if I use a tilde. The tilde is not the right character to use in this situation, but it is the only one that correctly wraps the text in the cell.
I've been working on this for the past 6-8 hours to figure it out and I'm just throwing it out there to see if someone can help me.
ViewController.swift
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.frame.size = NSSize(width: 616, height: 184)
// Strings
let string1 = "I Keep Forgettin' (Every Time You're Near)"
let string2 = "I Keep Forgettin\' (Every Time You're Near)"
let string3 = "I Keep Forgettin’ (Every Time You're Near)"
let string4 = "I Keep Forgettin` (Every Time You're Near)"
// Formatting
let foreground = NSColor.purple.cgColor
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.tabStops = .none
paragraphStyle.baseWritingDirection = .leftToRight
guard let font = NSFont(name: "Helvetica", size: 28.0) else { return }
// Labels
let label1 = NSTextField(frame: NSRect(x: 20, y: self.view.frame.minY+20, width: 144, height: 144))
label1.cell = VerticallyCenteredTextFieldCell()
label1.wantsLayer = true
label1.layer?.borderColor = NSColor.purple.cgColor
label1.layer?.borderWidth = 0.5
label1.layer?.backgroundColor = NSColor.lightGray.cgColor
label1.alphaValue = 1
var fontSize = bestFontSize(attributedString: NSAttributedString(string: string1, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label1.attributedStringValue = NSAttributedString(string: string1, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label1)
let label2 = NSTextField(frame: NSRect(x: 164, y: self.view.frame.minY+20, width: 144, height: 144))
label2.cell = VerticallyCenteredTextFieldCell()
label2.wantsLayer = true
label2.layer?.borderColor = NSColor.purple.cgColor
label2.layer?.borderWidth = 0.5
label2.layer?.backgroundColor = NSColor.lightGray.cgColor
label2.alphaValue = 1
fontSize = bestFontSize(attributedString: NSAttributedString(string: string2, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label2.attributedStringValue = NSAttributedString(string: string2, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label2)
let label3 = NSTextField(frame: NSRect(x: 308, y: self.view.frame.minY+20, width: 144, height: 144))
label3.cell = VerticallyCenteredTextFieldCell()
label3.wantsLayer = true
label3.layer?.borderColor = NSColor.purple.cgColor
label3.layer?.borderWidth = 0.5
label3.layer?.backgroundColor = NSColor.lightGray.cgColor
label3.alphaValue = 1
fontSize = bestFontSize(attributedString: NSAttributedString(string: string3, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label3.attributedStringValue = NSAttributedString(string: string3, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label3)
let label4 = NSTextField(frame: NSRect(x: 452, y: self.view.frame.minY+20, width: 144, height: 144))
label4.cell = VerticallyCenteredTextFieldCell()
label4.wantsLayer = true
label4.layer?.borderColor = NSColor.purple.cgColor
label4.layer?.borderWidth = 0.5
label4.layer?.backgroundColor = NSColor.lightGray.cgColor
label4.alphaValue = 1
fontSize = bestFontSize(attributedString: NSAttributedString(string: string4, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label4.attributedStringValue = NSAttributedString(string: string4, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label4)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func bestFontSize(attributedString: NSAttributedString, size: CGSize) -> CGFloat {
// Create a property to hold the font and size
var font: NSFont?
// Get the font information from the string attibutes
attributedString.enumerateAttribute(.font, in: NSRange(0..<attributedString.length)) { value, range, stop in
if let attrFont = value as? NSFont {
font = attrFont
}
}
if font == nil {
return 0
}
// Get any paragraph styling attributes
var paragraphStyle: NSMutableParagraphStyle?
attributedString.enumerateAttribute(.paragraphStyle, in: NSMakeRange(0, attributedString.length)) { value, range, stop in
if let style = value as? NSMutableParagraphStyle {
paragraphStyle = style
}
}
if paragraphStyle == nil {
return 0
}
// Create a sorted list of words from the string in descending order of length (chars) of the word
let fragment = attributedString.string.split(separator: " ").sorted() { $0.count > $1.count }
// Create a bounding box size that will be used to check the width of the largest word in the string
var width = String(fragment[0]).boundingRect(with: CGSize(width: .greatestFiniteMagnitude, height: size.height), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).width.rounded(.up)
// Create a bounding box size that will be used to check the height of the string
var height = attributedString.string.boundingRect(with: CGSize(width: size.width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).height.rounded(.up)
while height >= size.height || width >= size.width {
guard let pointSize = font?.pointSize else {
return 0
}
font = font?.withSize(pointSize-0.25)
width = String(fragment[0]).boundingRect(with: CGSize(width: .greatestFiniteMagnitude, height: size.height), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).width.rounded(.up)
height = attributedString.string.boundingRect(with: CGSize(width: size.width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).height.rounded(.up)
}
return font!.pointSize
}
}
VerticallyCenteredTextFieldCell.swift
import Cocoa
class VerticallyCenteredTextFieldCell: NSTextFieldCell {
// https://stackoverflow.com/questions/11775128/set-text-vertical-center-in-nstextfield/33788973 - Sayanti Mondal
func adjustedFrame(toVerticallyCenterText rect: NSRect) -> NSRect {
// super would normally draw from the top of the cell
var titleRect = super.titleRect(forBounds: rect)
let minimumHeight = self.cellSize(forBounds: rect).height
titleRect.origin.y += (titleRect.height - minimumHeight) / 2
titleRect.size.height = minimumHeight
return titleRect
}
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
super.drawInterior(withFrame: adjustedFrame(toVerticallyCenterText: cellFrame), in: controlView)
}
}
This is the result I get:
Anyone else get the same result running this?
I'm trying to draw a string in a UIImageView in Swift's CoreGraphics for a dynamic geometry software app. I'm using context graphics for graphing points and lines, but when I try to draw a text string (for example, the length of a line segment), I either get no text (such as in these two examples), or everything else I'm drawing is erased except the text string (sorry, I didn't save examples of that). In the latter case, I could draw other stuff on top of the string, but that isn't really helpful either, since I could only draw a single string (trying to draw a second string erased the first).
Here are two attempts, both of which result in no text string drawn:
UIGraphicsBeginImageContext(canvas.frame.size)
canvas.draw(CGRect(x: 0, y: 0, width: canvas.frame.width, height: canvas.frame.height))
let context = UIGraphicsGetCurrentContext()
let text="\(value)"
let font=UIFont(name: "Helvetica", size: 12)!
let text_style=NSMutableParagraphStyle()
text_style.alignment=NSTextAlignment.center
let text_color=UIColor.black
let attributes=[NSAttributedString.Key.font:font, NSAttributedString.Key.paragraphStyle:text_style, NSAttributedString.Key.foregroundColor:text_color]
let text_x=coordinates.x
let text_y=coordinates.y
let text_rect=CGRect(x: text_x, y: text_y, width: 100, height: font.lineHeight)
text.draw(in: text_rect.integral, withAttributes: attributes)
UIGraphicsEndImageContext()
}```
```override func draw(_ canvas: UIImageView, _ isRed: Bool) {
UIGraphicsBeginImageContext(canvas.frame.size)
canvas.draw(CGRect(x: 0, y: 0, width: canvas.frame.width, height: canvas.frame.height))
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512))
let img = renderer.image { ctx in
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attrs = [NSAttributedString.Key.font: UIFont(name: "HelveticaNeue-Thin", size: 12)!, NSAttributedString.Key.paragraphStyle: paragraphStyle]
let string = "\(value)"
string.draw(with: CGRect(x: coordinates.x, y: coordinates.y, width: 100, height: 16), options: .usesLineFragmentOrigin, attributes: attrs, context: nil)
}
}```
I have also found a page that says something to the effect that it is impossible to draw text in a UIImageView. However, it is hard to believe that a graphics system as mature as core graphics wouldn't allow drawing text.
I am working on a barChart in MacOS Cocoa, using core Graphic. The program embed some usefull link for SwiftUI. You should be able to adapt my code to your program.
// ViewController.swift
// Chart_test
//
// Created by slareau on 2022-10-10.
//
//https://stackoverflow.com/questions/38079917/drawing-in-cocoa-swift
//https://www.raywenderlich.com/1101-core-graphics-on-macos-tutorial
//https://stackoverflow.com/questions/10627557/mac-os-x-drawing-into-an-offscreen-nsgraphicscontext-using-cgcontextref-c-funct
//https://nshipster.com/cggeometry/
//https://www.hackingwithswift.com/example-code/core-graphics/how-to-draw-a-text-string-using-core-graphics
import Cocoa
import CoreGraphics
class ChartsVC: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("ChartsVC")
let dr = Drawing(frame: NSRect(x: 50, y: 50, width: 550, height: 500))
self.view.addSubview(dr)
}
}
class Drawing: NSView {
//var facts_list: [myfact] = [] //complete database
//var fact_total: [Double] = [0.0, 0.0, 0.0, 0.0, 0.0]
var fact_total: [Double] = [12454, 22365,18989, 14656, 16777]
//var comm_list: [myComm] = []
// var comm_total: [Double] = [0.0, 0.0, 0.0, 0.0, 0.0]
var comm_total: [Double] = [8976, 14989, 13989, 12065, 15040]
var Current_Year : Int = 0
var Current_Month : Int = 0
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let todays_date = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy"
Current_Year = Int(dateFormatter.string(from: todays_date))!
// print ("VC60:", dateFormatter.string(from: todays_date), Current_Year!) OK
dateFormatter.dateFormat = "MM"
Current_Month = Int(dateFormatter.string(from: todays_date))!
// Load_CommDB()
// Load_FactDB()
var j = 0
while j < 5 { // last 5 years
print ( "VCH165: ", Current_Year - j, fact_total[j], comm_total[j] )
j += 1
}
// frame background
myRect(x: 0, y: 0, wd: 500, ht: 400, color: NSColor.lightGray)
for i in 0...4 {
let factT = String(fact_total[4 - i])
let commT = String(comm_total[4 - i])
let diffT = String(fact_total[4 - i] - comm_total[4 - i])
myRect(x: Double(50 + 80 * i) , y: 80, wd: 50, ht: Double(fact_total[4 - i]/100), color: NSColor.blue)
myRect(x: Double(60 + 80 * i) , y: 80, wd: 50, ht: Double(comm_total[4 - i]/100), color: NSColor.orange)
myRect(x: Double(70 + 80 * i) , y: 80, wd: 50, ht: Double((fact_total[4 - i] - comm_total[4 - i])/100), color: NSColor.green)
mytext(txt: factT.toCurrencyFormat(), x: Double(45 + 80 * i), y: 60)
mytext(txt: commT.toCurrencyFormat(), x: Double(45 + 80 * i), y: 40)
mytext(txt: diffT.toCurrencyFormat(), x: Double(45 + 80 * i), y: 20)
mytext(txt: String(Current_Year - (4 - i)), x: Double(45 + 80 * i), y: 3)
}
let context = NSGraphicsContext.current!.cgContext;
context.beginPath()
context.move(to: CGPoint(x: 10.0, y: 78.0))
context.addLine(to: CGPoint(x: 420.0, y: 78.0))
//context.setStrokeColor(red: 1, green: 0, blue: 0, alpha: 1)
context.setStrokeColor(NSColor.black.cgColor)
context.setLineWidth(2.0)
context.strokePath()
}
func myRect( x: Double, y: Double, wd: Double, ht :Double, color: NSColor) {
let context = NSGraphicsContext.current!.cgContext;
let rectangle = CGRect(x: x, y: y, width: wd, height: ht) // NSRect or CGRect work
//context.setFillColor(NSColor.yellow.cgColor)
context.setFillColor(color.cgColor)
context.setStrokeColor(NSColor.black.cgColor)
context.setLineWidth(2)
context.addRect(rectangle)
context.drawPath(using: .fillStroke)
}
func mytext ( txt: String, x: Double, y: Double) {
// let myfont = CTFontCreateWithName("Papyrus" as CFString, 12, nil)
let myfont = CTFontCreateWithName("Arial" as CFString, 12, nil)
let attributedString = NSAttributedString(string: txt, attributes: [NSAttributedString.Key.font: myfont])
// let attributedString = NSAttributedString(string: "allo le monde")
let line = CTLineCreateWithAttributedString(attributedString)
// Draw text
if let context = NSGraphicsContext.current?.cgContext {
context.textPosition = .init(x: x , y: y)
CTLineDraw(line, context)
}
//https://blog.krzyzanowskim.com/2020/07/09/coretext-academy-part-1/
//https://blog.krzyzanowskim.com/2020/07/10/coretext-swift-academy-part-2/
//https://blog.krzyzanowskim.com/2020/07/13/coretext-swift-academy-part-3/
}
}
Looking for a solution to create a pdf programatically with the contents of my UITabelView with a label on top and allow the users to print on standard A4 paper.
This is my not working code:
func createPdfFromView(aView: UIView, saveToDocumentsWithFileName fileName: String) {
let pdfTitle = "\(pilotName)'s Past Duty"
let pageSize = CGRect(x: 0, y: 0, width: 11 * 70, height: 8.5 * 70)
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, pageSize, nil)
UIGraphicsBeginPDFPage()
let font = UIFont.preferredFont(forTextStyle: .largeTitle)
let attributedPDFTitle = NSAttributedString(string: pdfTitle, attributes: [NSAttributedString.Key.font: font])
let stringSize = attributedPDFTitle.size()
let stringRect = CGRect(x: (pageSize.width / 2 - stringSize.width / 2), y: 20, width: stringSize.width, height: stringSize.height)
attributedPDFTitle.draw(in: stringRect)
aView.layer.render(in: UIGraphicsGetCurrentContext()!)
UIGraphicsGetCurrentContext()!.translateBy(x: 0, y: 40)
UIGraphicsEndPDFContext()
if let documentDirectories = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
let documentsFileName = documentDirectories + "/" + fileName
debugPrint(documentsFileName)
pdfData.write(toFile: documentsFileName, atomically: true)
}
}
I am trying to display a fraction as something like 1/2 but I want to line to be horizontal so the fraction looks more fraction like, some thing like this:
1
_
2
However with no massive space and in a way that I can use it inside one UILabel (which can have multiple lines). I also need to be able to put a fraction as the numerator/denominator of a fraction and so on. Any suggestions? (Fonts, characters...)
Thank you,
Gleb Koval
Here's a UIView subclass that draws a fraction:
class FractionView: UIView {
var font: UIFont = UIFont.systemFont(ofSize: 16) {
didSet { self.setNeedsDisplay() }
}
var numerator: Int = 1 {
didSet { self.setNeedsDisplay() }
}
var denominator: Int = 2{
didSet { self.setNeedsDisplay() }
}
var spacing: CGFloat = 5{
didSet { self.setNeedsDisplay() }
}
override func draw(_ rect: CGRect) {
let numString = "\(numerator)" as NSString
let numWidth = numString.size(withAttributes: [.font: font]).width
let denomString = "\(denominator)" as NSString
let denomWidth = denomString.size(withAttributes: [.font: font]).width
let numX: CGFloat
let denomX: CGFloat
if numWidth <= denomWidth {
denomX = 0
numX = (denomWidth - numWidth) / 2
} else {
denomX = (numWidth - denomWidth) / 2
numX = 0
}
numString.draw(at: CGPoint(x: numX, y: 0), withAttributes: [.font : font])
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: font.lineHeight + spacing))
path.addLine(to: CGPoint(x: self.frame.maxX, y: font.lineHeight + spacing))
UIColor.black.setStroke()
path.lineWidth = 1
path.stroke()
denomString.draw(at: CGPoint(x: denomX, y: font.lineHeight + spacing * 2), withAttributes: [.font: font])
}
}
// usage:
let width = ("12" as NSString).size(withAttributes: [.font: UIFont.systemFont(ofSize: 16)]).width
let view = FractionView(frame: CGRect(x: 0, y: 0, width: width, height: 48))
view.backgroundColor = .white
view.denominator = 12
As Sulthan in comments has suggested, using an NSAttributedString would be an easier approach:
let attrString = NSMutableAttributedString(string: "1", attributes: [NSAttributedStringKey.underlineStyle : 1])
attrString.append(NSAttributedString(string: "\n2"))
attrString
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
label.attributedText = attrString
label.numberOfLines = 2
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
}
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()
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
}
}
}