I'm trying to generate a pdf of my UIView. In the view I can simply erase lines using CGContextSetBlendMode(context, CGBlendMode.Clear). While this works properly on the context of the UIView when I draw to the pdf context (as generated using UIGraphicsBeginPDFContextToData) the same code results in a line with the same color as set in CGContextSetStrokeColorWithColor. Does anybody know of an alternative way to achieve erasing in a pdf context?
For instance, when I run the code below a context from drawrect it results in a faint red line (alpha = 0.5) with a small gap running through the red line. However, if I supply a pdfcontext it becomes a faint redline with a hard red line running through it (as another thin red line with alpha 0.5 is superimposed)
let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, 10, 10)
CGPathAddLineToPoint(path, nil, 10,100)
CGContextAddPath(context, path)
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetStrokeColorWithColor(context, UIColor.redColor().colorWithAlphaComponent(0.5).CGColor)
CGContextSetLineWidth(context, 10.0)
CGContextStrokePath(context)
CGPathMoveToPoint(path, nil, 10, 10)
CGPathAddLineToPoint(path, nil, 10,100)
CGContextAddPath(context, path)
CGContextSetBlendMode(context, CGBlendMode.Clear)
CGContextSetLineWidth(context, 2.0)
CGContextStrokePath(context)
I ended up solving my problem with a workaround. For the benefit of anyone else with this issue, here goes.
I draw subviews individually in the PDFContext. For each I check if it contains erasing. If this is the case I open a separate ImageContext and generate an image of the view as such.
UIGraphicsBeginImageContext(objectView.frame.size)
if let imageContext = UIGraphicsGetCurrentContext() {
objectView.drawObjectInContext(imageContext,rect: objectView.bounds)
self.applyEraser(imageContext, eraserShape: objectView.eraserLayer.eraseShape)
let image = UIGraphicsGetImageFromCurrentImageContext()
CGContextTranslateCTM(pdfContext, 0, objectView.bounds.height)
CGContextScaleCTM(pdfContext, 1.0, -1.0)
CGContextDrawImage(pdfContext, objectView.bounds, image.CGImage)
}
UIGraphicsEndImageContext()
Note that the image is upside compared to regular drawing in CGContext, which is why I invoke CGContextTranslate en CGContextScale.
The downside is that you get images of subviews in your PDF rather than perhaps simple text. However, if you do it for each view separately you can draw the views without erasing properly as text or whatever you want.
Related
I am working on drawing app. I want to implement eraser functionality that erases the path drawn using UIBezierPath.Is it possible??
I have tried below code but its not working
UIGraphicsBeginImageContextWithOptions(self.tempImageView.frame.size, false, 0.0)
if let context = UIGraphicsGetCurrentContext()
{
tempImageView.image?.draw(in: CGRect(x:
0, y: 0, width: self.tempImageView.frame.size.width, height: self.tempImageView.frame.size.height))
let sublayer = CAShapeLayer()
self.tempImageView.layer.addSublayer(sublayer)
context.setBlendMode(CGBlendMode.clear)
sublayer.strokeColor = editorPanelView.drawingColor.cgColor
sublayer.lineJoin = kCALineJoinRound
sublayer.lineCap = kCALineCapRound
sublayer.lineWidth = editorPanelView.brushWidth
let path = UIBezierPath()
path.move(to: CGPoint(x: mid1.x, y: mid1.y))
path.addQuadCurve(to:CGPoint(x:mid2.x, y:mid2.y) , controlPoint: CGPoint(x: prevPoint1.x, y: prevPoint1.y))
sublayer.path = path.cgPath
layerArray.append(sublayer)
UIGraphicsEndImageContext()
}
I am not able to find any solution for this. Any help (even if it's just a kick in the right direction) would be appreciated.
A CAShapeLayer is a vector drawing. It's shapes. The usual eraser tool works with raster (pixel-based) drawings. The two are different, and there isn't a straightforward way to do an eraser tool on vector drawings.
One way you could do it is to have the eraser tool edit a mask layer which is applied to your shape layer. It would have the effect of erasing the parts of the shape layer where the mask is opaque.
One tricky part to this is that once you've drawn into the mask, you'd have to erase that part of the mask in order for the user to be able to draw new content into the erased area, and that might cause the erased parts of the old drawing to show through.
Getting what you want to do to work well would be quite tricky.
Using Swift 4, I'm drawing a NSMutableAttributedString using CGContext to a PDF file. The string contains an attachment with an UIImage. But the image gets lost when using the UIGraphicsGetCurrentContext.
Creation of NSMutableAttributedString with attachment:
let aString = NSMutableAttributedString()
let attachment = NSTextAttachment()
attachment.image = UIImage(named: "miniatureIcon")!
aString.append( attachmentString )
Creation of the context.
UIGraphicsBeginPDFPageWithInfo....
print("attributedString \(attributedString)") // attachement still here
// Get current graphics context
let currentContext = UIGraphicsGetCurrentContext()!
// Create a core text frame setter
let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
// Save context pre manipulation
currentContext.saveGState()
// Reset text matrix, so no text scaling is affected
currentContext.textMatrix = CGAffineTransform.identity
// Create the frame and a rectangular path of the text frame
let frameRect = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
let framePath = UIBezierPath(rect: frameRect).cgPath
// Create core text frame for the given attributed string
// The whole text should fit the frame, as calculations were already done
let frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedString.length), framePath, nil)
// Translate by 100% graphics height up and reverse scale, as core text does draw from bottom up and not from top down
currentContext.translateBy(x: 0, y: UIGraphicsGetPDFContextBounds().height)
currentContext.scaleBy(x: 1.0, y: -1.0)
// Translate context to actual position of text
currentContext.translateBy(x: frame.minX, y: UIGraphicsGetPDFContextBounds().height - frame.maxY)
// Draw text into context
CTFrameDraw(frameRef, currentContext)
// Restore context to pre manipulation
currentContext.restoreGState()
UIGraphicsEndPDFContext()
Since this is the last time the text gets manipulated before it gets drawn on the PDF file and since I have debugged it down to this part where the attachment is available for the last time, I assume that the error is as part of the CGContext.
Other attributes like bold or center remain on the final file.
But the attachment is gone.
What am I missing? Help is very appreciated.
I have a custom class that manages a custom view that has a horizontal center on screen (which will represent a solid line). And I've drawn the line like this:
override func drawRect(rect: CGRect)
{
let line = UIGraphicsGetCurrentContext()
CGContextSetLineWidth(line, 3.0)
CGContextSetStrokeColorWithColor(line, UIColor.redColor().CGColor)
CGContextMoveToPoint(line, 0, self.bounds.midY)
CGContextAddLineToPoint(line, self.bounds.width, self.bounds.midY)
CGContextStrokePath(line)
}
However, I need to draw a multiple solid circles on this line and try to look it like a point in a chart. I'm trying to make a mini chart representation in fact. How can I draw the circles? With a nested 'for in' loop or? Is there any official chart API from Apple?
UIGraphicsGetCurrentContext() gives you a context that you can draw multiple things into. Calling it line is not the right idea, because it can contain multiple lines, or circles, or all sorts of other things.
Think of the context as an artist sitting in front of a blank canvas. You give the artist instructions, like "draw a red line, then draw a blue circle". The artist follows the instructions, and afterwards, you look at the canvas.
Here's how you might draw a line and then a circle.
let context = UIGraphicsGetCurrentContext()
// Tell the context what stroked paths should look like
CGContextSetLineWidth(context, 3.0)
CGContextSetStrokeColorWithColor(context, UIColor.redColor().CGColor)
// Draw a single line
CGContextMoveToPoint(context, 0, self.bounds.midY)
CGContextAddLineToPoint(context, self.bounds.width, self.bounds.midY)
CGContextStrokePath(context)
// Now draw a circle by filling a path.
// First, set the fill color:
CGContextSetFillColorWithColor(context, UIColor.blueColor().CGColor)
// Specify how big the circle is, and where its center is:
let circleRadius = CGFloat(5.0)
let circleCenter = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
// Then add a circle to the context, by specifying the rectangle that surrounds it:
let circleRect = CGRect(x: circleCenter.x - circleRadius,
y: circleCenter.y - circleRadius,
width: circleRadius * 2,
height: circleRadius * 2)
CGContextAddEllipseInRect(context, circleRect)
// And fill that circle:
CGContextFillPath(context)
If you want to draw more circles but in different places, just call CGContextAddEllipseInRect and CGContextFillPath again, but with different values for circleRect. Depending on what you want, a for loop might be appropriate. It's entirely up to you.
If you don't want to write it yourself, there are lots of 3rd-party chart libraries available, just do a search. Apple does not provide an "official" one.
Any suggestion on using SpriteKit rendering engine, but have the ability to draw game graphic programmatically. Those graphics are simple but slightly complex than just rectangular and oval.
I recently found out the SKShapeNode have a cap on the complexity when using custom path. I basically have a function that take a number to generate random CGPath shape, The larger the number the more complex and details about the shape. When I use a small number, there is no problem, but when I use a larger number to generate more complex path, SKShapeNode give me this error. (I tested on real device with iOS9.2.1 as well)
Assertion failed: (length + offset <= _length)
Related Question
Now I don't think using SKShapeNode is a good idea, so any suggestion on programmatically draw graphics that able to work nicely with SpriteKit?
I read somewhere else which is draw using the any graphics API available as long as it can convert to a format that SKTexture can initialize, is the right way to go?
If you will use SKTexture you can draw your chart with CGContext.
First of all you need create array of chart's points like:
let pathPoints: [CGPoint] = [...]
Then you need to call UIGraphicsBeginImageContext(rect.size) where rect.size is CGSize object which determines the size of the region in which will be inscribed the chart and rect is CGRect object. Next step is creation of CGContextRef and it setting:
UIGraphicsBeginImageContext(size)
let context: CGContextRef = UIGraphicsGetCurrentContext()!
CGContextSetStrokeColorWithColor(context, UIColor.redColor().CGColor);
CGContextSetLineWidth(context, lineWidth)
where lineWidth is CGFloat type value.
After this you can create your chart in context like this:
if pathPoints.count > 1 {
CGContextMoveToPoint(context, pathPoints.first!.x, pathPoints.first!.y)
for var i = 1; i < pathPoints.count; ++i {
CGContextAddLineToPoint(context, pathPoints[i].x, pathPoints[i].y)
}
}
CGContextStrokePath(context)
Then you need to create image form context like this:
let image = UIGraphicsGetImageFromCurrentImageContext()
Then create texture form image and use it for SKSpriteNode object that will be added to the scene:
let pathTexture = SKTexture(image: image)
let pathNode = SKSpriteNode(texture: pathTexture)
pathNode.position = CGPointMake(rect.origin.x + rect.width/2 - lineWidth/2, rect.origin.y + rect.height/2 - lineWidth/2)
pathNode.zPosition = 0
someParrentNodeThatOnScene.addChild(pathNode)
UIGraphicsEndImageContext()
That's all you need to create chart.
I'd like to create a rounded rect similar to the one around the labels (while editing) in AddressBook. I am trying to use a bezierPath with a rounded rect and then stroking it to do this. But, the end result doesn't have very smooth edges.
Address Book label
Address Book label zoomed in pixie
My label
My label zoomed in pixie
It looks like the curves in the addressbook version are more aggressively mixed towards the background white color. In Pixie, the pixel I have my cursor on has a sRGB value of (0.98, 0.98, 0.98), wheras in my version it is (0.86, 0.86, 0.86) resulting in a bit of jagged edge.
My code to draw the rect is
override func drawWithFrame(cellFrame: NSRect, inView controlView: NSView) {
if let context = NSGraphicsContext.currentContext() {
context.saveGraphicsState()
let borderColor = NSColor.init(SRGBRed: 0.75, green: 0.75, blue: 0.75, alpha: 1)
let outline = NSBezierPath.init(roundedRect: cellFrame, xRadius: 4, yRadius: 4)
outline.lineWidth = 4
borderColor.setStroke()
outline.stroke()
drawInteriorWithFrame(cellFrame, inView: controlView)
context.restoreGraphicsState()
}
}
I have tried playing around with different compositing types, line widths and xy radiuses for the round rect - without much success. I would appreciate some guidance on this. Thanks
I believe this article here explains the issue. When we stroke a rect, Coregraphics draws the outline along the middle of the edge of the rectangle we provide. If the rect is 1 pixel wide, then half the stroke line will lie outside the rectangle, while the other half will lie on the inside. As we can't draw half a pixel, Coregraphics mixes the two colors (on the inside and outside) to draw the line. So, to draw a single pixel line, we need to modify the outline rect
CGRect rectFor1PxStroke(CGRect rect)
{
return CGRectMake(rect.origin.x + 0.5, rect.origin.y + 0.5, rect.size.width - 1, rect.size.height - 1);
}
A couple of other options are also provided in the referenced article. But, with my initial testing, the above solution seems to work fine - along with setting the linewidth = 1 and x-y radiuses of the curve to 2