Draw graphics and export with pixel precision with CoreGraphics - swift

I saw few questions here on stackoverflow but none of them is solving my problem. What I want to do is to subclass NSView and draw some shapes on it. Then I want to export/save created graphics to png file. And while drawing is quite simple, I want to be able to store image with pixel precision - I know that drawing is being done in points instead of pixels. So what I am doing is I override draw() method to draw any graphic like so:
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSColor.white.setFill()
dirtyRect.fill()
NSColor.green.setFill()
NSColor.green.setStroke()
currentContext?.beginPath()
currentContext?.setLineWidth(1.0)
currentContext?.setStrokeColor(NSColor.green.cgColor)
currentContext?.move(to: CGPoint(x: 0, y: 0))
currentContext?.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
currentContext?.closePath()
}
and since on screen it looks OK, after saving this to file is not what I expected. I set line width to 1 but in exported file it is 2 pixels wide. And to save image, I create NSImage from current view:
func getImage() -> NSImage? {
let size = self.bounds.size
let imageSize = NSMakeSize(size.width, size.height)
guard let imageRepresentation = self.bitmapImageRepForCachingDisplay(in: self.bounds) else {
return nil
}
imageRepresentation.size = imageSize
self.cacheDisplay(in: self.bounds, to: imageRepresentation)
let image = NSImage(size: imageSize)
image.addRepresentation(imageRepresentation)
return image
}
and this image is then save to file:
do {
guard let image = self.canvasView?.getImage() else {
return
}
let imageRep = image.representations.first as? NSBitmapImageRep
let data = imageRep?.representation(using: .png, properties: [:])
try data?.write(to: url, options: .atomic)
} catch {
print(error.localizedDescription)
}
Do you have any tips of what I am doing wrong?

Related

Swift PDFKit draw multiple images in a single PDFPage

I'm trying to draw multiple images into a single PDFPage.
Looking at the docs and over StackOverflow seems like the best I got is to use PDFPage with an UIImage initializer like so:
let pdfPage = PDFPage(image: image)
But it just creates a page with a full-page image. I tried to draw the images using CGContext but I don't understand how to use PDFPage within a drawing context for it to draw the images rapidly like in the example below.
let bounds = page.bounds(for: .cropBox)
// Create a `UIGraphicsImageRenderer` to use for drawing an image.
let renderer = UIGraphicsImageRenderer(bounds: bounds, format: UIGraphicsImageRendererFormat.default())
let image = renderer.image { (context) in
// How do I rapidly draw them here?
}
Any help will be highly appreciated!
The result I get with PDFPage(image: <UIImage>) vs expected result:
You probably need to improve the positioning logic for the image in the loop, but this should point you to the right direction.
import UIKit
import PDFKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let pdfView = PDFView()
pdfView.frame = CGRect(x: 0, y: 50, width: view.frame.width, height: view.frame.height - 50)
if let d = createPDFDocument() {
pdfView.document = d
}
view.addSubview(pdfView)
}
func createPDFDocument() -> PDFDocument? {
let pdfDocument = PDFDocument()
let page = PDFPage()
let bounds = page.bounds(for: .cropBox)
let imageRenderer = UIGraphicsImageRenderer(bounds: bounds, format: UIGraphicsImageRendererFormat.default())
let image = imageRenderer.image { (context) in
context.cgContext.saveGState()
context.cgContext.translateBy(x: 0, y: bounds.height)
context.cgContext.concatenate(CGAffineTransform.init(scaleX: 1, y: -1))
page.draw(with: .mediaBox, to: context.cgContext)
context.cgContext.restoreGState()
// Improve logic for image position
Range(1...4).forEach { value in
let image = UIImage(named: "YOUR_IMAGE_NAME")
let rect = CGRect(x: 50 * value, y: 0, width: 40, height: 100)
image?.draw(in: rect)
}
}
let newPage = PDFPage(image: image)!
pdfDocument.insert(newPage, at: 0)
return pdfDocument
}
}

Crop quadrilateral image from image to swiftUI [duplicate]

I would like to clip a bezier path from an image. For some reason, the image remains the unclipped. And how do I position the path so it would be properly cut?
extension UIImage {
func imageByApplyingMaskingBezierPath(_ path: UIBezierPath, _ pathFrame: CGFrame) -> UIImage {
UIGraphicsBeginImageContext(self.size)
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
path.addClip()
draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
let maskedImage = UIGraphicsGetImageFromCurrentImageContext()!
context.restoreGState()
UIGraphicsEndImageContext()
return maskedImage
}
}
You need to add your path.cgPath to your current context, also you need to remove context.saveGState() and context.restoreGState()
Use this code
func imageByApplyingMaskingBezierPath(_ path: UIBezierPath, _ pathFrame: CGRect) -> UIImage {
UIGraphicsBeginImageContext(self.size)
let context = UIGraphicsGetCurrentContext()!
context.addPath(path.cgPath)
context.clip()
draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
let maskedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return maskedImage
}
Using it
let testPath = UIBezierPath()
testPath.move(to: CGPoint(x: self.imageView.frame.width / 2, y: self.imageView.frame.height))
testPath.addLine(to: CGPoint(x: 0, y: 0))
testPath.addLine(to: CGPoint(x: self.imageView.frame.width, y: 0))
testPath.close()
self.imageView.image = UIImage(named:"Image")?.imageByApplyingMaskingBezierPath(testPath, self.imageView.frame)
Result
You can try like this.
var path = UIBezierPath()
var shapeLayer = CAShapeLayer()
var cropImage = UIImage()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch?{
let touchPoint = touch.location(in: self.YourimageView)
print("touch begin to : \(touchPoint)")
path.move(to: touchPoint)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch?{
let touchPoint = touch.location(in: self.YourimageView)
print("touch moved to : \(touchPoint)")
path.addLine(to: touchPoint)
addNewPathToImage()
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch?{
let touchPoint = touch.location(in: self.YourimageView)
print("touch ended at : \(touchPoint)")
path.addLine(to: touchPoint)
addNewPathToImage()
path.close()
}
}
func addNewPathToImage(){
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = lineWidth
YourimageView.layer.addSublayer(shapeLayer)
}
func cropImage(){
UIGraphicsBeginImageContextWithOptions(YourimageView.bounds.size, false, 1)
tempImageView.layer.render(in: UIGraphicsGetCurrentContext()!)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.cropImage = newImage!
}
#IBAction func btnCropImage(_ sender: Any) {
cropImage()
}
Once you draw a path on particular button action just call your imageByApplyingMaskingBezierPath
Here is Swift code to get clips from an image based on a UIBezierPath and it is very quickly implemented. This method works if the image is already being shown on the screen, which will most often be the case. The resultant image will have a transparent background, which is what most people want when they clip part of a photo image. You can use the resultant clipped image locally because you will have it in a UIImage object that I called imageWithTransparentBackground. This very simple code also shows you how to save the image to the camera roll, and how to also put it right into the pasteboard so a user can paste that image directly into a text message, paste it into Notes, an email, etc. Note that in order to write the image to the camera roll, you need to edit the info.plist and provide a reason for “Privacy - Photo Library Usage Description”
import Photos // Needed if you save to the camera roll
Provide a UIBezierPath for clipping. Here is my declaration for one.
let clipPath = UIBezierPath()
Populate the clipPath with some logic of your own using some combination of commands. Below are a few I used in my drawing logic. Provide CGPoint equivalents for aPointOnScreen, etc Build your path relative to the main screen as self.view is this apps ViewController (for this code), and self.view.layer is rendered through the clipPath.
clipPath.move(to: aPointOnScreen)
clipPath.addLine(to: otherPointOnScreen)
clipPath.addLine(to: someOtherPointOnScreen)
clipPath.close()
This logic uses all of the devices screen as the context size. A CGSize is declared for that. fullScreenX and fullScreenY are my variables where I have already captured the devices width and height. It is nice if the photo you are clipping from is already zoomed into and is an adequate size as shown on the whole of the screen. What you see, is what you get.
let mainScreenSize = CGSize(width: fullScreenX, height: fullScreenY)
// Get an empty context
UIGraphicsBeginImageContext(mainScreenSize)
// Specify the clip path
clipPath.addClip()
// Render through the clip path from the whole of the screen.
self.view.layer.render(in: UIGraphicsGetCurrentContext()!)
// Get the clipped image from the context
let image : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
// Done with the context, so end it.
UIGraphicsEndImageContext()
// The PNG data has the alpha channel for the transparent background
let imageData = image.pngData()
// Below is the local UIImage to use within your code
let imageWithTransparentBackground = UIImage.init(data: imageData!)
// Make the image available to the pasteboard.
UIPasteboard.general.image = imageWithTransparentBackground
// Save the image to the camera roll.
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: imageWithTransparentBackground!)
}, completionHandler: { success, error in
if success {
//
}
else if let error = error {
//
}
else {
//
}
})

how to draw images on top of circles in UIView swift

I'm trying to draw circles and in the middle of each circle, I want to draw an image.
my circles work fine but I'm not getting along with the images.
I don't understand why I can't just draw an UIImage directly.
The code below //draw PNGs is what my question is about but I posted the whole code.
thanks in advance for any help
import UIKit
import CoreGraphics
enum Shape2 {
case circle
case Rectangle
case Line
}
class Canvas: UIView {
let viewModel = ViewModel(set: set1)
var currentShape: Shape2?
override func draw(_ rect: CGRect) {
guard let currentContext = UIGraphicsGetCurrentContext() else {
print("Could not get Context")
return
}
drawIcons (user: currentContext)
}
private func drawIcons(user context: CGContext){
for i in 0...viewModel.iconsList.count-1 {
let centerPoint = CGPoint(x: viewModel.icons_coord_x[i], y: viewModel.icons_coord_y[i])
context.addArc(center: centerPoint, radius: CGFloat(viewModel.Diameters[i]), startAngle: CGFloat(0).degreesToRadians, endAngle: CGFloat(360).degreesToRadians, clockwise: true)
context.setFillColor(UIColor.blue.cgColor)
//context.setFillColor(viewModel.iconsbackground_colors[i].cgColor)
context.fillPath()
context.setLineWidth(4.0)
//draw PNGs:
let image = UIImage(named: "rocket")!
let ciImage = image.ciImage
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let context2 = CIContext(options: nil)
let cgImage = context2.createCGImage(ciImage ?? <#default value#>, from: ciImage?.extent ?? <#default value#>)
context.draw(CGImage() as! CGLayer, in: imageRect)
}
}
func drawShape(selectedShape: Shape2){
currentShape = selectedShape
setNeedsDisplay()
}
} ```
I don't understand why I can't just draw an UIImage directly.
You can.
https://developer.apple.com/documentation/uikit/uiimage/1624132-draw
https://developer.apple.com/documentation/uikit/uiimage/1624092-draw

How do I position an image correctly in MTKView?

I am trying to implement an image editing view using MTKView and Core Image filters and have the basics working and can see the filter applied in realtime. However the image is not positioned correctly in the view - can someone point me in the right direction for what needs to be done to get the image to render correctly in the view. It needs to fit the view and retain its original aspect ratio.
Here is the metal draw function - and the empty drawableSizeWillChange!? - go figure. its probably also worth mentioning that the MTKView is a subview of another view in a ScrollView and can be resized by the user. It's not clear to me how Metals handles resizing the view but it seems that doesn't come for free.
I am also trying to call the draw() function from a background thread and this appears to sort of work. I can see the filter effects as they are applied to the image using a slider. As I understand it this should be possible.
It also seems that the coordinate space for rendering is in the images coordinate space - so if the image is smaller than the MTKView then to position the image in the centre the X and Y coordinates will be negative.
When the view is resized then everything gets crazy with the image suddenly becoming way too big and parts of the background not being cleared.
Also when resting the view the image gets stretched rather than redrawing smoothly.
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
public func draw(in view: MTKView) {
if let ciImage = self.ciImage {
if let currentDrawable = view.currentDrawable { // 1
let commandBuffer = commandQueue.makeCommandBuffer()
let inputImage = ciImage // 2
exposureFilter.setValue(inputImage, forKey: kCIInputImageKey)
exposureFilter.setValue(ev, forKey: kCIInputEVKey)
context.render(exposureFilter.outputImage!,
to: currentDrawable.texture,
commandBuffer: commandBuffer,
bounds: CGRect(origin: .zero, size: view.drawableSize),
colorSpace: colorSpace)
commandBuffer?.present(currentDrawable)
commandBuffer?.commit()
}
}
}
As you can see the image is on the bottom left
let scaleFilter = CIFilter(name: "CILanczosScaleTransform")
That should help you out. The issue is that your CIImage, wherever it might come from, is not the same size as the view you are rendering it in.
So what you could opt to do is calculate the scale, and apply it as a filter:
let scaleFilter = CIFilter(name: "CILanczosScaleTransform")
scaleFilter?.setValue(ciImage, forKey: kCIInputImageKey)
scaleFilter?.setValue(scale, forKey: kCIInputScaleKey)
This resolves your scale issue; I currently do not know what the most efficient approach would be to actually reposition the image
Further reference: https://nshipster.com/image-resizing/
The problem is your call to context.render — you are calling render with bounds: origin .zero. That’s the lower left.
Placing the drawing in the correct spot is up to you. You need to work out where the right bounds origin should be, based on the image dimensions and your drawable size, and render there. If the size is wrong, you also need to apply a scale transform first.
Thanks to Tristan Hume's MetalTest2 I now have it working nicely in two synchronised scrollViews. The basics are in the subclass below - the renderer and shaders can be found at Tristan's MetalTest2 project. This class is managed by a viewController and is a subview of the scrollView's documentView. See image of the final result.
//
// MetalLayerView.swift
// MetalTest2
//
// Created by Tristan Hume on 2019-06-19.
// Copyright © 2019 Tristan Hume. All rights reserved.
//
import Cocoa
// Thanks to https://stackoverflow.com/questions/45375548/resizing-mtkview-scales-old-content-before-redraw
// for the recipe behind this, although I had to add presentsWithTransaction and the wait to make it glitch-free
class ImageMetalView: NSView, CALayerDelegate {
var renderer : Renderer
var metalLayer : CAMetalLayer!
var commandQueue: MTLCommandQueue!
var sourceTexture: MTLTexture!
let colorSpace = CGColorSpaceCreateDeviceRGB()
var context: CIContext!
var ciMgr: CIManager?
var showEdits: Bool = false
var ciImage: CIImage? {
didSet {
self.metalLayer.setNeedsDisplay()
}
}
#objc dynamic var fileUrl: URL? {
didSet {
if let url = fileUrl {
self.ciImage = CIImage(contentsOf: url)
}
}
}
/// Bind to this property from the viewController to receive notifications of changes to CI filter parameters
#objc dynamic var adjustmentsChanged: Bool = false {
didSet {
self.metalLayer.setNeedsDisplay()
}
}
override init(frame: NSRect) {
let _device = MTLCreateSystemDefaultDevice()!
renderer = Renderer(pixelFormat: .bgra8Unorm, device: _device)
self.commandQueue = _device.makeCommandQueue()
self.context = CIContext()
self.ciMgr = CIManager(context: self.context)
super.init(frame: frame)
self.wantsLayer = true
self.layerContentsRedrawPolicy = .duringViewResize
// This property only matters in the case of a rendering glitch, which shouldn't happen anymore
// The .topLeft version makes glitches less noticeable for normal UIs,
// while .scaleAxesIndependently matches what MTKView does and makes them very noticeable
// self.layerContentsPlacement = .topLeft
self.layerContentsPlacement = .scaleAxesIndependently
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func makeBackingLayer() -> CALayer {
metalLayer = CAMetalLayer()
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.device = renderer.device
metalLayer.delegate = self
// If you're using the strategy of .topLeft placement and not presenting with transaction
// to just make the glitches less visible instead of eliminating them, it can help to make
// the background color the same as the background of your app, so the glitch artifacts
// (solid color bands at the edge of the window) are less visible.
// metalLayer.backgroundColor = CGColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
metalLayer.allowsNextDrawableTimeout = false
// these properties are crucial to resizing working
metalLayer.autoresizingMask = CAAutoresizingMask(arrayLiteral: [.layerHeightSizable, .layerWidthSizable])
metalLayer.needsDisplayOnBoundsChange = true
metalLayer.presentsWithTransaction = true
metalLayer.framebufferOnly = false
return metalLayer
}
override func setFrameSize(_ newSize: NSSize) {
super.setFrameSize(newSize)
self.size = newSize
renderer.viewportSize.x = UInt32(newSize.width)
renderer.viewportSize.y = UInt32(newSize.height)
// the conversion below is necessary for high DPI drawing
metalLayer.drawableSize = convertToBacking(newSize)
self.viewDidChangeBackingProperties()
}
var size: CGSize = .zero
// This will hopefully be called if the window moves between monitors of
// different DPIs but I haven't tested this part
override func viewDidChangeBackingProperties() {
guard let window = self.window else { return }
// This is necessary to render correctly on retina displays with the topLeft placement policy
metalLayer.contentsScale = window.backingScaleFactor
}
func display(_ layer: CALayer) {
if let drawable = metalLayer.nextDrawable(),
let commandBuffer = commandQueue.makeCommandBuffer() {
let passDescriptor = MTLRenderPassDescriptor()
let colorAttachment = passDescriptor.colorAttachments[0]!
colorAttachment.texture = drawable.texture
colorAttachment.loadAction = .clear
colorAttachment.storeAction = .store
colorAttachment.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
if let outputImage = self.ciImage {
let xscale = self.size.width / outputImage.extent.width
let yscale = self.size.height / outputImage.extent.height
let scale = min(xscale, yscale)
if let scaledImage = self.ciMgr!.scaleTransformFilter(outputImage, scale: scale, aspectRatio: 1),
let processed = self.showEdits ? self.ciMgr!.processImage(inputImage: scaledImage) : scaledImage {
let x = self.size.width/2 - processed.extent.width/2
let y = self.size.height/2 - processed.extent.height/2
context.render(processed,
to: drawable.texture,
commandBuffer: commandBuffer,
bounds: CGRect(x:-x, y:-y, width: self.size.width, height: self.size.height),
colorSpace: colorSpace)
}
} else {
print("Image is nil")
}
commandBuffer.commit()
commandBuffer.waitUntilScheduled()
drawable.present()
}
}
}

How to draw NSImage to CGContext

I have an object NSImage, which I want to draw to CGContext.
I found that there is no corresponding method for CGContext, but there seems to be a CGImage method, clip
func clip(to: CGRect, mask: CGImage)
How do I need to do it, please?
The easiest way is to create an NSGraphicsContext wrapping your CGContext, and draw into that:
let gc: CGContext = ...
let nsImage: NSImage = ...
let rect: CGRect = ...
let priorNsgc = NSGraphicsContext.current
defer { NSGraphicsContext.current = priorNsgc }
NSGraphicsContext.current = NSGraphicsContext(cgContext: gc, flipped: false)
nsImage.draw(in: rect)