How can I convert the following code about Core Graphics into Swift? - objective-c

- (void) drawTextLink:(NSString *) text inFrame:(CGRect) frameRect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform ctm = CGContextGetCTM(context);
// Translate the origin to the bottom left.
// Notice that 842 is the size of the PDF page.
CGAffineTransformTranslate(ctm, 0.0, 842);
// Flip the handedness of the coordinate system back to right handed.
CGAffineTransformScale(ctm, 1.0, -1.0);
// Convert the update rectangle to the new coordiante system.
CGRect xformRect = CGRectApplyAffineTransform(frameRect, ctm);
NSURL *url = [NSURL URLWithString:text];
UIGraphicsSetPDFContextURLForRect( url, xformRect );
CGContextSaveGState(context);
NSDictionary *attributesDict;
NSMutableAttributedString *attString;
NSNumber *underline = [NSNumber numberWithInt:NSUnderlineStyleSingle];
attributesDict = #{NSUnderlineStyleAttributeName : underline,
NSForegroundColorAttributeName : [UIColor blueColor]};
attString = [[NSMutableAttributedString alloc] initWithString:url.absoluteString attributes:attributesDict];
[attString drawInRect:frameRect];
CGContextRestoreGState(context);
}
It's code I found to make url's in a pdf clickable.
I'm still getting the hang of objective-c so your help is greatly apprecaited. Thank you very much in advance!
Right now here is what I have translated, and I'm pretty sure there are a couple mistakes:
func drawTextLink(text: NSString, frameRect: CGRect) {
let context: CGContext = UIGraphicsGetCurrentContext()!
var c = context.ctm
let cc = CGAffineTransform.translatedBy(c)
cc.(0.0, 842)
}

Above method in Swift 4.0 should be like this.
func drawTextLink(text: String, frame: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
let ctm = context.ctm
ctm.translatedBy(x: 0.0, y: 842)
ctm.scaledBy(x: 1.0, y: -1.0)
let newFrame = frame.applying(ctm)
guard let url = URL(string: text) else {return}
UIGraphicsSetPDFContextURLForRect(url, newFrame)
context.saveGState()
let dict = [NSAttributedStringKey.underlineStyle: 1, .foregroundColor: UIColor.red] as [NSAttributedStringKey : Any]
let attrString = NSAttributedString(string: text, attributes: dict)
attrString.draw(in: frame)
context.restoreGState()
}

Related

How to draw NSAttributedString with .background attributes and insets?

I need to draw in CALayer subclass NSAttributedString with background for every line of text and insets(horizontal and vertical). How to do that?
I am trying this code. But, I can't set insets text insets.
override func draw(in ctx: CGContext) {
if let attributedString = nsAttributedString {
ctx.translateBy(x: 0, y: (bounds.size.height * 1.5) - (stringSize.height / 2))
ctx.scaleBy(x: 1.0, y: -1.0)
let path = CGMutablePath()
path.addRect(bounds)
let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
let frame = CTFramesetterCreateFrame(framesetter,
CFRangeMake(0, attributedString.length),
path, nil)
ctx.textMatrix = .identity
ctx.textPosition = .zero
CTFrameDraw(frame, ctx)
}
}
Its should looks like this

How to put text in images in Swift? [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 6 years ago.
Improve this question
I'm making an app, so I need to know how can I put some text in images.
I'm working with Xcode 7 and Swift 2
I already have this code, but it's in Objective-C and I don't know if it works...
- (UIImage *)burnTextIntoImage:(NSString *)text :(UIImage *)img {
UIGraphicsBeginImageContext(img.size);
CGRect aRectangle = CGRectMake(0,0, img.size.width, img.size.height);
[img drawInRect:aRectangle];
[[UIColor redColor] set];
NSInteger fontSize = 14;
if ( [text length] > 200 ) {
fontSize = 10;
}
UIFont *font = [UIFont boldSystemFontOfSize: fontSize];
[text drawInRect : aRectangle
withFont : font
lineBreakMode : UILineBreakModeTailTruncation
alignment : UITextAlignmentCenter ];
UIImage *theImage=UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return theImage;
}
I tried translating the code to Swift 2. This is as close as I got:
extension UIImage {
func withText(text: String) -> UIImage {
UIGraphicsBeginImageContext(size)
let rectangle = CGRect(x: 0, y: 0, width: size.width, height: size.height)
drawInRect(rectangle)
UIColor.redColor().set()
let fontSize = text.characters.count > 200 ? 10 : 14
let font = UIFont.boldSystemFontOfSize(CGFloat(fontSize))
let attributes: [String: AnyObject] = [
NSFontAttributeName: font
]
(text as NSString).drawInRect(rectangle, withAttributes: attributes)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
As you can see NSString's drawInRect now takes a [String: AnyObject]? for its attributes. I was not able to find the corresponding keys for lineBreakMode and alignment.
As the documentation states:
These are the same attributes that can be applied to an NSAttributedString object...
Update:
I have added the other specifications:
extension UIImage {
func withText(text: String) -> UIImage {
UIGraphicsBeginImageContext(size)
let rectangle = CGRect(x: 0, y: 0, width: size.width, height: size.height)
drawInRect(rectangle)
let fontSize = text.characters.count > 200 ? 10 : 14
let font = UIFont.boldSystemFontOfSize(CGFloat(fontSize))
let paragraphAttributes = NSMutableParagraphStyle()
paragraphAttributes.lineBreakMode = NSLineBreakMode.ByTruncatingTail
paragraphAttributes.alignment = NSTextAlignment.Center
let attributes: [String: AnyObject] = [
NSFontAttributeName: font,
NSForegroundColorAttributeName: UIColor.redColor(),
NSParagraphStyleAttributeName: paragraphAttributes
]
(text as NSString).drawInRect(rectangle, withAttributes: attributes)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
And just incase this was unclear before... This is how you would use the method:
// `someImage` is an optional `UIImage` (`UIImage?`), as the initializer is failable
let someImage = UIImage(named: "something.png")
// `imageWithText` is of type `UIImage` here
if let image = someImage {
let imageWithText = someImage.withText("Hello World")
}
// `imageWithText` is of type `UIImage?` here
let imageWithText = someImage?.withText("Hello World")

Rendering PDF in Apple TV tvOS

I am working on an addition to my tvOS app that would allow viewing of PDFs stored in the app. However, without UIWebView, I'm at a loss on how to do this. I've asked question in other places, and get greeted with a link to a wordy and helpless document from Apple about the APIs that can be used, and even here it has been referenced (CGPDFPage) but no real guide on how to implement this. Has anyone successfully done this on tvOS, and if so, would you help me get started in this process?
Below is some code that I wrote and tested in tvOS. Note that this is in Objective-c.
I've created two functions to do the job, and one helper function to display the PDF images in a UIScrollView. The first one will open the PDF document from a URL. A web URL was used. A local file could also be used in this sample.
There is also a helper function to open a document from a local file.
The second function renders the PDF document to a context. I chose to display the context by creating an image from it. There are other ways of handling the context too.
Opening the document is fairly straight forward, so there are no comments in the code for that. Rendering the document is slightly more involved, and so there are comments explaining that function.
The complete application is below.
- (CGPDFDocumentRef)openPDFLocal:(NSString *)pdfURL {
NSURL* NSUrl = [NSURL fileURLWithPath:pdfURL];
return [self openPDF:NSUrl];
}
- (CGPDFDocumentRef)openPDFURL:(NSString *)pdfURL {
NSURL* NSUrl= [NSURL URLWithString:pdfURL];
return [self openPDF:NSUrl];
}
- (CGPDFDocumentRef)openPDF:(NSURL*)NSUrl {
CFURLRef url = (CFURLRef)CFBridgingRetain(NSUrl);
CGPDFDocumentRef myDocument;
myDocument = CGPDFDocumentCreateWithURL(url);
if (myDocument == NULL) {
NSLog(#"can't open %#", NSUrl);
CFRelease (url);
return nil;
}
CFRelease (url);
if (CGPDFDocumentGetNumberOfPages(myDocument) == 0) {
CGPDFDocumentRelease(myDocument);
return nil;
}
return myDocument;
}
- (void)drawDocument:(CGPDFDocumentRef)pdfDocument
{
// Get the total number of pages for the whole PDF document
int totalPages= (int)CGPDFDocumentGetNumberOfPages(pdfDocument);
NSMutableArray *pageImages = [[NSMutableArray alloc] init];
// Iterate through the pages and add each page image to an array
for (int i = 1; i <= totalPages; i++) {
// Get the first page of the PDF document
CGPDFPageRef page = CGPDFDocumentGetPage(pdfDocument, i);
CGRect pageRect = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
// Begin the image context with the page size
// Also get the grapgics context that we will draw to
UIGraphicsBeginImageContext(pageRect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
// Rotate the page, so it displays correctly
CGContextTranslateCTM(context, 0.0, pageRect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextConcatCTM(context, CGPDFPageGetDrawingTransform(page, kCGPDFMediaBox, pageRect, 0, true));
// Draw to the graphics context
CGContextDrawPDFPage(context, page);
// Get an image of the graphics context
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[pageImages addObject:image];
}
// Set the image of the PDF to the current view
[self addImagesToScrollView:pageImages];
}
-(void)addImagesToScrollView:(NSMutableArray*)imageArray {
int heigth = 0;
for (UIImage *image in imageArray) {
UIImageView *imgView = [[UIImageView alloc] initWithImage:image];
imgView.frame=CGRectMake(0, heigth, imgView.frame.size.width, imgView.frame.size.height);
[_scrollView addSubview:imgView];
heigth += imgView.frame.size.height;
}
}
And to tie it all together, you can do this:
CGPDFDocumentRef pdfDocument = [self openPDFURL:#"http://www.guardiansuk.com/uploads/accreditation/10testing.pdf"];
[self drawDocument:pdfDocument];
Note that I'm using a random PDF that was available for free on the web. I ran into some problems with https URLs, but I'm sure this can be resolved, and it's not actually related to the PDF opening question.
The tvOS documentation contains a section on creating, viewing and transforming PDF documents so I think it contains the functionality you need.
There’s lots of example code on that page, but here’s some code I use on iOS for the same purpose. It should work on tvOS, but I don’t have a way to test it:
func imageForPDF(URL: NSURL, pageNumber: Int, imageWidth: CGFloat) -> UIImage {
let document = CGPDFDocumentCreateWithURL(URL)
let page = CGPDFDocumentGetPage(document, pageNumber)
var pageRect = CGPDFPageGetBoxRect(page, .MediaBox)
let scale = imageWidth / pageRect.size.width
pageRect.size = CGSizeMake(pageRect.size.width * scale, pageRect.size.height * scale)
pageRect.origin = CGPointZero
UIGraphicsBeginImageContext(pageRect.size)
let ctx = UIGraphicsGetCurrentContext()
CGContextSetRGBFillColor(ctx, 1.0, 1.0, 1.0, 1.0) // White background
CGContextFillRect(ctx, pageRect)
CGContextSaveGState(ctx)
// Rotate the PDF so that it’s the right way around
CGContextTranslateCTM(ctx, 0.0, pageRect.size.height)
CGContextScaleCTM(ctx, 1.0, -1.0)
CGContextConcatCTM(ctx, CGPDFPageGetDrawingTransform(page, .MediaBox, pageRect, 0, true))
CGContextDrawPDFPage(ctx, page)
CGContextRestoreGState(ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
This is #jbg's answer in swift 5
func imageForPDF(URL: NSURL, pageNumber: Int, imageWidth: CGFloat) -> UIImage? {
guard let document = CGPDFDocument(URL) else { return nil }
guard let page = document.page(at: pageNumber) else { return nil }
var pageRect = page.getBoxRect(.mediaBox)
let scale = imageWidth / pageRect.size.width
pageRect.size = CGSize(width: pageRect.size.width * scale,
height: pageRect.size.height * scale)
pageRect.origin = CGPoint.zero
UIGraphicsBeginImageContext(pageRect.size)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.setFillColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
context.fill(pageRect)
context.saveGState()
// Rotate the PDF so that it’s the right way around
context.translateBy(x: 0.0, y: pageRect.size.height)
context.scaleBy(x: 1.0, y: -1.0)
context.concatenate(page.getDrawingTransform(.mediaBox, rect: pageRect, rotate: 0, preserveAspectRatio: true))
context.drawPDFPage(page)
context.restoreGState()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
`

How to add a gradient tint color to a UISlider in XCode 6?

I'm working on a design application that has a section for selecting colors by three sliders for RGB.
As we can see in xcode, where we want to select a color by RGB values, the slider tint color is a gradient color that changes when we change the sliders. I want to use this in my application. but I have no idea about how to do this?
I've found this code in a blog. but didn't work for me.
- (void)setGradientToSlider:(UISlider *)Slider WithColors:(NSArray *)Colors{
UIView * view = (UIView *)[[Slider subviews]objectAtIndex:0];
UIImageView * maxTrackImageView = (UIImageView *)[[view subviews]objectAtIndex:0];
CAGradientLayer * maxTrackGradient = [CAGradientLayer layer];
CGRect rect = maxTrackImageView.frame;
rect.origin.x = view.frame.origin.x;
maxTrackGradient.frame = rect;
maxTrackGradient.colors = Colors;
[maxTrackGradient setStartPoint:CGPointMake(0.0, 0.5)];
[maxTrackGradient setEndPoint:CGPointMake(1.0, 0.5)];
[[maxTrackImageView layer] insertSublayer:maxTrackGradient atIndex:0];
/////////////////////////////////////////////////////
UIImageView * minTrackImageView = (UIImageView *)[[view subviews]objectAtIndex:1];
CAGradientLayer * minTrackGradient = [CAGradientLayer layer];
rect = minTrackImageView.frame;
rect.size.width = maxTrackImageView.frame.size.width;
rect.origin.x = 0;
rect.origin.y = 0;
minTrackGradient.frame = rect;
minTrackGradient.colors = Colors;
[minTrackGradient setStartPoint:CGPointMake(0.0, 0.5)];
[minTrackGradient setEndPoint:CGPointMake(1.0, 0.5)];
[minTrackImageView.layer insertSublayer:minTrackGradient atIndex:0];
}
I would appreciate any helps. Thanks.
While it didnt give me the desired results here is a down and dirty Swift version of the answer above for those that want to try it.
func setSlider(slider:UISlider) {
let tgl = CAGradientLayer()
let frame = CGRectMake(0, 0, slider.frame.size.width, 5)
tgl.frame = frame
tgl.colors = [UIColor.blueColor().CGColor, UIColor.greenColor().CGColor, UIColor.yellowColor().CGColor, UIColor.orangeColor().CGColor, UIColor.redColor().CGColor]
tgl.startPoint = CGPointMake(0.0, 0.5)
tgl.endPoint = CGPointMake(1.0, 0.5)
UIGraphicsBeginImageContextWithOptions(tgl.frame.size, tgl.opaque, 0.0);
tgl.renderInContext(UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
image.resizableImageWithCapInsets(UIEdgeInsetsZero)
slider.setMinimumTrackImage(image, forState: .Normal)
//slider.setMaximumTrackImage(image, forState: .Normal)
}
UPDATE for Swift 4.0
func setSlider(slider:UISlider) {
let tgl = CAGradientLayer()
let frame = CGRect.init(x:0, y:0, width:slider.frame.size.width, height:5)
tgl.frame = frame
tgl.colors = [UIColor.blue.cgColor, UIColor.green.cgColor, UIColor.yellow.cgColor, UIColor.orange.cgColor, UIColor.red.cgColor]
tgl.startPoint = CGPoint.init(x:0.0, y:0.5)
tgl.endPoint = CGPoint.init(x:1.0, y:0.5)
UIGraphicsBeginImageContextWithOptions(tgl.frame.size, tgl.isOpaque, 0.0);
tgl.render(in: UIGraphicsGetCurrentContext()!)
if let image = UIGraphicsGetImageFromCurrentImageContext() {
UIGraphicsEndImageContext()
image.resizableImage(withCapInsets: UIEdgeInsets.zero)
slider.setMinimumTrackImage(image, for: .normal)
}
}
Here is possible solution:
Usage:
//array of CGColor objects, color1 and color2 are UIColor objects
NSArray *colors = [NSArray arrayWithObjects:(id)color1.CGColor, (id)color2.CGColor, nil];
//your UISlider
[slider setGradientBackgroundWithColors:colors];
Implementation:
Create category on UISlider:
- (void)setGradientBackgroundWithColors:(NSArray *)colors
{
CAGradientLayer *trackGradientLayer = [CAGradientLayer layer];
CGRect frame = self.frame;
frame.size.height = 5.0; //set the height of slider
trackGradientLayer.frame = frame;
trackGradientLayer.colors = colors;
//setting gradient as horizontal
trackGradientLayer.startPoint = CGPointMake(0.0, 0.5);
trackGradientLayer.endPoint = CGPointMake(1.0, 0.5);
UIImage *trackImage = [[UIImage imageFromLayer:trackGradientLayer] resizableImageWithCapInsets:UIEdgeInsetsZero];
[self setMinimumTrackImage:trackImage forState:UIControlStateNormal];
[self setMaximumTrackImage:trackImage forState:UIControlStateNormal];
}
Where colors is array of CGColor.
I have also created a category on UIImage which creates image from layer as you need an UIImage for setting gradient on slider.
+ (UIImage *)imageFromLayer:(CALayer *)layer
{
UIGraphicsBeginImageContextWithOptions(layer.frame.size, layer.opaque, 0.0);
[layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return outputImage;
}
For Swift 3 and to prevent the slider from scaling the Min image, apply this when setting the its image. Recalculating the slider's left side is not necessary. Only recalc if you can changing the color of the gradient. The Max image does not seem to scale, but you should probably apply the same setting for consistency. There is a slight difference on the Max image when not applying its insets.
slider.setMinimumTrackImage(image?.resizableImage(withCapInsets:.zero), for: .normal)
For some reason it only works properly when resizableImage(withCapInsets:.zero) is all done at the same time. Running that part separate does not allow the image to work and gets scaled.
Here is the entire routine in Swift 3:
func setSlider(slider:UISlider) {
let tgl = CAGradientLayer()
let frame = CGRect(x: 0.0, y: 0.0, width: slider.bounds.width, height: 5.0 )
tgl.frame = frame
tgl.colors = [ UIColor.yellow.cgColor,UIColor.black.cgColor]
tgl.endPoint = CGPoint(x: 1.0, y: 1.0)
tgl.startPoint = CGPoint(x: 0.0, y: 1.0)
UIGraphicsBeginImageContextWithOptions(tgl.frame.size, false, 0.0)
tgl.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
slider.setMaximumTrackImage(image?.resizableImage(withCapInsets:.zero), for: .normal)
slider.setMinimumTrackImage(image?.resizableImage(withCapInsets:.zero), for: .normal)
}
This is a really effective approach that I've found after a lot of web search. So it's better to share it here as a complete answer. The following code is a Swift Class That you can use to create and use gradients as UIView or UIImage.
import Foundation
import UIKit
class Gradient: UIView{
// Gradient Color Array
private var Colors: [UIColor] = []
// Start And End Points Of Linear Gradient
private var SP: CGPoint = CGPoint.zeroPoint
private var EP: CGPoint = CGPoint.zeroPoint
// Start And End Center Of Radial Gradient
private var SC: CGPoint = CGPoint.zeroPoint
private var EC: CGPoint = CGPoint.zeroPoint
// Start And End Radius Of Radial Gradient
private var SR: CGFloat = 0.0
private var ER: CGFloat = 0.0
// Flag To Specify If The Gradient Is Radial Or Linear
private var flag: Bool = false
// Some Overrided Init Methods
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
// Draw Rect Method To Draw The Graphics On The Context
override func drawRect(rect: CGRect) {
// Get Context
let context = UIGraphicsGetCurrentContext()
// Get Color Space
let colorSpace = CGColorSpaceCreateDeviceRGB()
// Create Arrays To Convert The UIColor to CG Color
var colorComponent: [CGColor] = []
var colorLocations: [CGFloat] = []
var i: CGFloat = 0.0
// Add Colors Into The Color Components And Use An Index Variable For Their Location In The Array [The Location Is From 0.0 To 1.0]
for color in Colors {
colorComponent.append(color.CGColor)
colorLocations.append(i)
i += CGFloat(1.0) / CGFloat(self.Colors.count - 1)
}
// Create The Gradient With The Colors And Locations
let gradient: CGGradientRef = CGGradientCreateWithColors(colorSpace, colorComponent, colorLocations)
// Create The Suitable Gradient Based On Desired Type
if flag {
CGContextDrawRadialGradient(context, gradient, SC, SR, EC, ER, 0)
} else {
CGContextDrawLinearGradient(context, gradient, SP, EP, 0)
}
}
// Get The Input Data For Linear Gradient
func CreateLinearGradient(startPoint: CGPoint, endPoint: CGPoint, colors: UIColor...) {
self.Colors = colors
self.SP = startPoint
self.EP = endPoint
self.flag = false
}
// Get The Input Data For Radial Gradient
func CreateRadialGradient(startCenter: CGPoint, startRadius: CGFloat, endCenter: CGPoint, endRadius: CGFloat, colors: UIColor...) {
self.Colors = colors
self.SC = startCenter
self.EC = endCenter
self.SR = startRadius
self.ER = endRadius
self.flag = true
}
// Function To Convert Gradient To UIImage And Return It
func getImage() -> UIImage {
// Begin Image Context
UIGraphicsBeginImageContext(self.bounds.size)
// Draw The Gradient
self.drawRect(self.frame)
// Get Image From The Current Context
let image = UIGraphicsGetImageFromCurrentImageContext()
// End Image Context
UIGraphicsEndImageContext()
// Return The Result Gradient As UIImage
return image
}
}

Mac OS X: Drawing into an offscreen NSGraphicsContext using CGContextRef C functions has no effect. Why?

Mac OS X 10.7.4
I am drawing into an offscreen graphics context created via +[NSGraphicsContext graphicsContextWithBitmapImageRep:].
When I draw into this graphics context using the NSBezierPath class, everything works as expected.
However, when I draw into this graphics context using the CGContextRef C functions, I see no results of my drawing. Nothing works.
For reasons I won't get into, I really need to draw using the CGContextRef functions (rather than the Cocoa NSBezierPath class).
My code sample is listed below. I am attempting to draw a simple "X". One stroke using NSBezierPath, one stroke using CGContextRef C functions. The first stroke works, the second does not. What am I doing wrong?
NSRect imgRect = NSMakeRect(0.0, 0.0, 100.0, 100.0);
NSSize imgSize = imgRect.size;
NSBitmapImageRep *offscreenRep = [[[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:imgSize.width
pixelsHigh:imgSize.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bitmapFormat:NSAlphaFirstBitmapFormat
bytesPerRow:0
bitsPerPixel:0] autorelease];
// set offscreen context
NSGraphicsContext *g = [NSGraphicsContext graphicsContextWithBitmapImageRep:offscreenRep];
[NSGraphicsContext setCurrentContext:g];
NSImage *img = [[[NSImage alloc] initWithSize:imgSize] autorelease];
CGContextRef ctx = [g graphicsPort];
// lock and draw
[img lockFocus];
// draw first stroke with Cocoa. this works!
NSPoint p1 = NSMakePoint(NSMaxX(imgRect), NSMinY(imgRect));
NSPoint p2 = NSMakePoint(NSMinX(imgRect), NSMaxY(imgRect));
[NSBezierPath strokeLineFromPoint:p1 toPoint:p2];
// draw second stroke with Core Graphics. This doesn't work!
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 0.0, 0.0);
CGContextAddLineToPoint(ctx, imgSize.width, imgSize.height);
CGContextClosePath(ctx);
CGContextStrokePath(ctx);
[img unlockFocus];
You don't specify how you are looking at the results. I assume you are looking at the NSImage img and not the NSBitmapImageRep offscreenRep.
When you call [img lockFocus], you are changing the current NSGraphicsContext to be a context to draw into img. So, the NSBezierPath drawing goes into img and that's what you see. The CG drawing goes into offscreenRep which you aren't looking at.
Instead of locking focus onto an NSImage and drawing into it, create an NSImage and add the offscreenRep as one of its reps.
NSRect imgRect = NSMakeRect(0.0, 0.0, 100.0, 100.0);
NSSize imgSize = imgRect.size;
NSBitmapImageRep *offscreenRep = [[[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:imgSize.width
pixelsHigh:imgSize.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bitmapFormat:NSAlphaFirstBitmapFormat
bytesPerRow:0
bitsPerPixel:0] autorelease];
// set offscreen context
NSGraphicsContext *g = [NSGraphicsContext graphicsContextWithBitmapImageRep:offscreenRep];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:g];
// draw first stroke with Cocoa
NSPoint p1 = NSMakePoint(NSMaxX(imgRect), NSMinY(imgRect));
NSPoint p2 = NSMakePoint(NSMinX(imgRect), NSMaxY(imgRect));
[NSBezierPath strokeLineFromPoint:p1 toPoint:p2];
// draw second stroke with Core Graphics
CGContextRef ctx = [g graphicsPort];
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 0.0, 0.0);
CGContextAddLineToPoint(ctx, imgSize.width, imgSize.height);
CGContextClosePath(ctx);
CGContextStrokePath(ctx);
// done drawing, so set the current context back to what it was
[NSGraphicsContext restoreGraphicsState];
// create an NSImage and add the rep to it
NSImage *img = [[[NSImage alloc] initWithSize:imgSize] autorelease];
[img addRepresentation:offscreenRep];
// then go on to save or view the NSImage
The solution by #Robin Stewart worked well for me. I was able to condense it to an NSImage extension.
extension NSImage {
convenience init(size: CGSize, actions: (CGContext) -> Void) {
self.init(size: size)
lockFocusFlipped(false)
actions(NSGraphicsContext.current!.cgContext)
unlockFocus()
}
}
Usage:
let image = NSImage(size: CGSize(width: 100, height: 100), actions: { ctx in
// Drawing commands here for example:
// ctx.setFillColor(.white)
// ctx.fill(pageRect)
})
I wonder why everyone writes such complicated code for drawing to an image. Unless you care for the exact bitmap representation of an image (and usually you don't!), there is no need to create one. You can just create a blank image and directly draw to it. In that case the system will create an appropriate bitmap representation (or maybe a PDF representation or whatever the system believes to be more suitable for drawing).
The documentation of the init method
- (instancetype)initWithSize:(NSSize)aSize
which exists since MacOS 10.0 and still isn't deprecated, clearly says:
After using this method to initialize an image object, you are
expected to provide the image contents before trying to draw the
image. You might lock focus on the image and draw to the image or you
might explicitly add an image representation that you created.
So here's how I would have written that code:
NSRect imgRect = NSMakeRect(0.0, 0.0, 100.0, 100.0);
NSImage * image = [[NSImage alloc] initWithSize:imgRect.size];
[image lockFocus];
// draw first stroke with Cocoa
NSPoint p1 = NSMakePoint(NSMaxX(imgRect), NSMinY(imgRect));
NSPoint p2 = NSMakePoint(NSMinX(imgRect), NSMaxY(imgRect));
[NSBezierPath strokeLineFromPoint:p1 toPoint:p2];
// draw second stroke with Core Graphics
CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 0.0, 0.0);
CGContextAddLineToPoint(ctx, imgRect.size.width, imgRect.size.height);
CGContextClosePath(ctx);
CGContextStrokePath(ctx);
[image unlockFocus];
That's all folks.
graphicsPort is actually void *:
#property (readonly) void * graphicsPort
and documented as
The low-level, platform-specific graphics context represented
by the graphic port.
Which may be pretty much everything, but the final note says
In OS X, this is the Core Graphics context,
a CGContextRef object (opaque type).
This property has been deprecated in 10.10 in favor of the new property
#property (readonly) CGContextRef CGContext
which is only available in 10.10 and later. If you have to support older systems, it's fine to still use graphicsPort.
Swift 4: I use this code, which replicates the convenient API from UIKit (but runs on macOS):
public class UIGraphicsImageRenderer {
let size: CGSize
init(size: CGSize) {
self.size = size
}
func image(actions: (CGContext) -> Void) -> NSImage {
let image = NSImage(size: size)
image.lockFocusFlipped(true)
actions(NSGraphicsContext.current!.cgContext)
image.unlockFocus()
return image
}
}
Usage:
let renderer = UIGraphicsImageRenderer(size: imageSize)
let image = renderer.image { ctx in
// Drawing commands here
}
Here are 3 ways of drawing same image (Swift 4).
The method suggested by #Mecki produces an image without blurring artefacts (like blurred curves). But this can be fixed by adjusting CGContext settings (not included in this example).
public struct ImageFactory {
public static func image(size: CGSize, fillColor: NSColor, rounded: Bool = false) -> NSImage? {
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
return drawImage(size: size) { context in
if rounded {
let radius = min(size.height, size.width)
let path = NSBezierPath(roundedRect: rect, xRadius: 0.5 * radius, yRadius: 0.5 * radius).cgPath
context.addPath(path)
context.clip()
}
context.setFillColor(fillColor.cgColor)
context.fill(rect)
}
}
}
extension ImageFactory {
private static func drawImage(size: CGSize, drawingCalls: (CGContext) -> Void) -> NSImage? {
return drawImageInLockedImageContext(size: size, drawingCalls: drawingCalls)
}
private static func drawImageInLockedImageContext(size: CGSize, drawingCalls: (CGContext) -> Void) -> NSImage? {
let image = NSImage(size: size)
image.lockFocus()
guard let context = NSGraphicsContext.current else {
image.unlockFocus()
return nil
}
drawingCalls(context.cgContext)
image.unlockFocus()
return image
}
// Has scalling or antialiasing issues, like blurred curves.
private static func drawImageInBitmapImageContext(size: CGSize, drawingCalls: (CGContext) -> Void) -> NSImage? {
guard let offscreenRep = NSBitmapImageRep(pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true,
isPlanar: false, colorSpaceName: .deviceRGB) else {
return nil
}
guard let context = NSGraphicsContext(bitmapImageRep: offscreenRep) else {
return nil
}
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = context
drawingCalls(context.cgContext)
NSGraphicsContext.restoreGraphicsState()
let img = NSImage(size: size)
img.addRepresentation(offscreenRep)
return img
}
// Has scalling or antialiasing issues, like blurred curves.
private static func drawImageInCGContext(size: CGSize, drawingCalls: (CGContext) -> Void) -> NSImage? {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
guard let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8,
bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else {
return nil
}
drawingCalls(context)
guard let image = context.makeImage() else {
return nil
}
return NSImage(cgImage: image, size: size)
}
}