|
@@ -0,0 +1,500 @@
|
|
|
+//
|
|
|
+// ALConfirmViewController.swift
|
|
|
+// ALCameraViewController
|
|
|
+//
|
|
|
+// Created by Alex Littlejohn on 2015/06/30.
|
|
|
+// Copyright (c) 2015 zero. All rights reserved.
|
|
|
+//
|
|
|
+//
|
|
|
+// Modified by Kevin Kieffer on 2019/08/06. Changes as follows: significantly updated the operation of this
|
|
|
+// class because as far as I could determine the subviews were not arranging themselves properly when the
|
|
|
+// device was rotated. Simplified this class by removing the centeringView and scrollView insets, and simply centering the
|
|
|
+// scrollView in the overall view, setting the scrollView content size = imageView frame size, and centering the cropOverlay
|
|
|
+// over the scrollView whenever the view finished laying out its subviews.
|
|
|
+//
|
|
|
+// Furthermore minimum scrollView zoom size was set based on the crop rectangle, but the initial view was set to fully
|
|
|
+// fit the image on the screen.
|
|
|
+//
|
|
|
+// A new aspectRatio constraint was created and all constraints were removed from the cropOverlay view in the .xib file.
|
|
|
+// The centeringView was also removed from the .xib file and the Confirm and Cancel buttons were moved closer to the edge.
|
|
|
+//
|
|
|
+// A center touch point is set on the CropOverlay if the CropParameters say its movable
|
|
|
+//
|
|
|
+// Lastly, the image cropping worked differently if the crop rectangle was out of the image bounds, depending on whether
|
|
|
+// the image came from a PHAsset or a UIImage. In the former, the crop maintained the aspect ratio of the crop rectangle
|
|
|
+// (possibly distorting the image), but in the latter, it truncated the crop rectangle to the bounds of the image, changing
|
|
|
+// the aspect ratio. Since maintaining the aspect ratio is preferred, a change to the UIImage extension was made to rescale the
|
|
|
+// cropped image back to the aspect ratio, which also possibly distorts the image but preserves the aspect ratio.
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+import UIKit
|
|
|
+import Photos
|
|
|
+
|
|
|
+public class ConfirmViewController: UIViewController, UIScrollViewDelegate {
|
|
|
+
|
|
|
+ var CROP_PADDING : CGFloat {
|
|
|
+ switch UIDevice.current.userInterfaceIdiom {
|
|
|
+ case .pad:
|
|
|
+ return CGFloat(120)
|
|
|
+ default:
|
|
|
+ return CGFloat(30)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let imageView = UIImageView()
|
|
|
+
|
|
|
+ @IBOutlet weak var scrollView: UIScrollView!
|
|
|
+ @IBOutlet weak var cropOverlay: CropOverlay!
|
|
|
+ @IBOutlet weak var confirmButton: UIButton!
|
|
|
+ @IBOutlet weak var cancelButton: UIButton!
|
|
|
+ @IBOutlet weak var rotateButton: UIButton!
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ var croppingParameters: CroppingParameters {
|
|
|
+ didSet {
|
|
|
+ cropOverlay.showsCenterPoint = croppingParameters.allowMoving
|
|
|
+ cropOverlay.isResizable = croppingParameters.allowResizing
|
|
|
+ cropOverlay.isMovable = croppingParameters.allowMoving
|
|
|
+ cropOverlay.minimumSize = croppingParameters.minimumSize
|
|
|
+ cropOverlay.showsButtons = croppingParameters.allowResizing
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public var onComplete: CameraViewCompletion?
|
|
|
+
|
|
|
+ let asset: PHAsset?
|
|
|
+ let image: UIImage?
|
|
|
+
|
|
|
+ var didInitialAdjustCropOverlay = false
|
|
|
+ var didInitialCenterCropOverlay = false
|
|
|
+
|
|
|
+ public init(image: UIImage, croppingParameters: CroppingParameters) {
|
|
|
+ self.croppingParameters = croppingParameters
|
|
|
+ self.asset = nil
|
|
|
+ self.image = image
|
|
|
+ super.init(nibName: "ConfirmViewController", bundle: CameraGlobals.shared.bundle)
|
|
|
+ }
|
|
|
+
|
|
|
+ public init(asset: PHAsset, croppingParameters: CroppingParameters) {
|
|
|
+ self.croppingParameters = croppingParameters
|
|
|
+ self.asset = asset
|
|
|
+ self.image = nil
|
|
|
+ super.init(nibName: "ConfirmViewController", bundle: CameraGlobals.shared.bundle)
|
|
|
+ }
|
|
|
+
|
|
|
+ public required init?(coder aDecoder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ deinit {
|
|
|
+ NotificationCenter.default.removeObserver(self)
|
|
|
+ }
|
|
|
+
|
|
|
+ public override var prefersStatusBarHidden: Bool {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ public override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
|
|
+ return UIStatusBarAnimation.slide
|
|
|
+ }
|
|
|
+
|
|
|
+ public override func viewDidLoad() {
|
|
|
+ super.viewDidLoad()
|
|
|
+
|
|
|
+ view.backgroundColor = UIColor.black
|
|
|
+
|
|
|
+ scrollView.addSubview(imageView)
|
|
|
+ scrollView.delegate = self
|
|
|
+ scrollView.maximumZoomScale = croppingParameters.maximumZoom
|
|
|
+
|
|
|
+ cropOverlay.showsCenterPoint = croppingParameters.allowMoving
|
|
|
+ cropOverlay.isHidden = true
|
|
|
+ cropOverlay.isResizable = croppingParameters.allowResizing
|
|
|
+ cropOverlay.isMovable = croppingParameters.allowMoving
|
|
|
+ cropOverlay.minimumSize = croppingParameters.minimumSize
|
|
|
+ cropOverlay.showsButtons = croppingParameters.allowResizing
|
|
|
+
|
|
|
+ if !croppingParameters.allowRotate {
|
|
|
+ rotateButton.isHidden = true
|
|
|
+ }
|
|
|
+
|
|
|
+ let spinner = showSpinner()
|
|
|
+
|
|
|
+ disable()
|
|
|
+
|
|
|
+ if let asset = asset { //load full resolution image size
|
|
|
+ _ = SingleImageFetcher()
|
|
|
+ .setAsset(asset)
|
|
|
+ .onSuccess { [weak self] image in
|
|
|
+ self?.configureWithImage(image)
|
|
|
+ self?.hideSpinner(spinner)
|
|
|
+ self?.enable()
|
|
|
+ }
|
|
|
+ .onFailure { [weak self] error in
|
|
|
+ self?.hideSpinner(spinner)
|
|
|
+ }
|
|
|
+ .fetch()
|
|
|
+ } else if let image = image {
|
|
|
+ configureWithImage(image)
|
|
|
+ hideSpinner(spinner)
|
|
|
+ enable()
|
|
|
+ }
|
|
|
+
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged), name: Notification.Name("UIDeviceOrientationDidChangeNotification"), object: nil)
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc func orientationChanged() {
|
|
|
+ centerCropFrame()
|
|
|
+ }
|
|
|
+
|
|
|
+ public override func viewWillLayoutSubviews() {
|
|
|
+
|
|
|
+ if !didInitialAdjustCropOverlay || !cropOverlay.isResizable {
|
|
|
+ adjustCropOverlay() //keep it centered and constrainted on orientation changes
|
|
|
+ didInitialAdjustCropOverlay = true
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public override func viewDidLayoutSubviews() {
|
|
|
+ super.viewDidLayoutSubviews()
|
|
|
+
|
|
|
+
|
|
|
+ let (minscale, initscale) = calculateMinimumAndInitialScale()
|
|
|
+
|
|
|
+ scrollView.contentSize = imageView.frame.size
|
|
|
+ scrollView.minimumZoomScale = minscale
|
|
|
+ scrollView.zoomScale = initscale
|
|
|
+
|
|
|
+ self.centerScrollViewContents()
|
|
|
+
|
|
|
+ if !cropOverlay.isResizable || !didInitialCenterCropOverlay {
|
|
|
+ self.centerCropFrame()
|
|
|
+ didInitialCenterCropOverlay = true
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ private func adjustCropOverlay() {
|
|
|
+ switch UIDevice.current.orientation {
|
|
|
+ case .landscapeLeft, .landscapeRight:
|
|
|
+ cropOverlay.frame.size.height = view.frame.height - CROP_PADDING //height is constrained in landscale
|
|
|
+ cropOverlay.frame.size.width = cropOverlay.frame.size.height / croppingParameters.aspectRatioHeightToWidth
|
|
|
+ default:
|
|
|
+ cropOverlay.frame.size.width = view.frame.width - CROP_PADDING //width is constrained in portrait
|
|
|
+ cropOverlay.frame.size.height = cropOverlay.frame.size.width * croppingParameters.aspectRatioHeightToWidth
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func configureWithImage(_ image: UIImage) {
|
|
|
+ cropOverlay.isHidden = !croppingParameters.isEnabled
|
|
|
+
|
|
|
+ buttonActions()
|
|
|
+
|
|
|
+ imageView.image = image
|
|
|
+ imageView.sizeToFit()
|
|
|
+ view.setNeedsLayout()
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ //Returns a tuple containing the minimum scale and the desired initial scale of the scroll view
|
|
|
+ private func calculateMinimumAndInitialScale() -> (CGFloat, CGFloat) {
|
|
|
+ guard let image = imageView.image else {
|
|
|
+ return (1,1)
|
|
|
+ }
|
|
|
+
|
|
|
+ //The initial scale will fit the entire image on the screen in either orientation
|
|
|
+ let size = view.bounds
|
|
|
+ let scaleWidth = size.width / image.size.width
|
|
|
+ let scaleHeight = size.height / image.size.height
|
|
|
+
|
|
|
+ let minSizeWithoutCrop = min(scaleWidth, scaleHeight)
|
|
|
+
|
|
|
+
|
|
|
+ //If cropping enabled, the minimum scale fits the image into the crop rectangle, otherwise
|
|
|
+ //its the same as in the initial scale
|
|
|
+ if croppingParameters.isEnabled {
|
|
|
+
|
|
|
+ let cropSize = cropOverlay.frame.size
|
|
|
+ let cropScaleWidth = (cropSize.width - CROP_PADDING) / image.size.width
|
|
|
+ let cropScaleHeight = (cropSize.height - CROP_PADDING) / image.size.height
|
|
|
+
|
|
|
+ let minSizeWithCrop = min(cropScaleWidth, cropScaleHeight)
|
|
|
+
|
|
|
+ return (minSizeWithCrop, minSizeWithoutCrop)
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ return (minSizeWithoutCrop, minSizeWithoutCrop)
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ private func calculateScrollViewInsets(_ frame: CGRect) -> UIEdgeInsets {
|
|
|
+ let bottom = view.frame.height - (frame.origin.y + frame.height)
|
|
|
+ let right = view.frame.width - (frame.origin.x + frame.width)
|
|
|
+ let insets = UIEdgeInsets(top: frame.origin.y, left: frame.origin.x, bottom: bottom, right: right)
|
|
|
+ return insets
|
|
|
+ }
|
|
|
+
|
|
|
+ private func centerImageViewOnRotate() {
|
|
|
+ if croppingParameters.isEnabled {
|
|
|
+ let size = cropOverlay.frame.size
|
|
|
+ let scrollInsets = scrollView.contentInset
|
|
|
+ let imageSize = imageView.frame.size
|
|
|
+ var contentOffset = CGPoint(x: -scrollInsets.left, y: -scrollInsets.top)
|
|
|
+ contentOffset.x -= (size.width - imageSize.width) / 2
|
|
|
+ contentOffset.y -= (size.height - imageSize.height) / 2
|
|
|
+ scrollView.contentOffset = contentOffset
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func centerCropFrame() {
|
|
|
+ let size = scrollView.frame.size
|
|
|
+ let cropSize = cropOverlay.frame.size
|
|
|
+ var origin = CGPoint.zero
|
|
|
+
|
|
|
+ if cropSize.width < size.width {
|
|
|
+ origin.x = (size.width - cropSize.width) / 2
|
|
|
+ }
|
|
|
+
|
|
|
+ if cropSize.height < size.height {
|
|
|
+ origin.y = (size.height - cropSize.height) / 2
|
|
|
+ }
|
|
|
+
|
|
|
+ cropOverlay.frame.origin = origin
|
|
|
+ }
|
|
|
+
|
|
|
+ private func centerScrollViewContents() {
|
|
|
+ let size = scrollView.frame.size
|
|
|
+ let imageSize = imageView.frame.size
|
|
|
+ var imageOrigin = CGPoint.zero
|
|
|
+
|
|
|
+ if imageSize.width < size.width {
|
|
|
+ imageOrigin.x = (size.width - imageSize.width) / 2
|
|
|
+ }
|
|
|
+
|
|
|
+ if imageSize.height < size.height {
|
|
|
+ imageOrigin.y = (size.height - imageSize.height) / 2
|
|
|
+ }
|
|
|
+
|
|
|
+ imageView.frame.origin = imageOrigin
|
|
|
+ }
|
|
|
+
|
|
|
+ private func buttonActions() {
|
|
|
+ confirmButton.action = { [weak self] in self?.confirmPhoto() }
|
|
|
+ cancelButton.action = { [weak self] in self?.cancel() }
|
|
|
+ rotateButton.action = { [weak self] in self?.rotateRight() }
|
|
|
+ }
|
|
|
+
|
|
|
+ internal func rotateRight() {
|
|
|
+ if let rotatedImage = imageView.image?.rotate() {
|
|
|
+ configureWithImage(rotatedImage)
|
|
|
+ centerScrollViewContents()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ internal func cancel() {
|
|
|
+ cropOverlay.removeFromSuperview() //remove overlay while processing
|
|
|
+ onComplete?(nil, nil)
|
|
|
+ }
|
|
|
+
|
|
|
+ internal func confirmPhoto() {
|
|
|
+
|
|
|
+ guard let image = imageView.image else {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ disable()
|
|
|
+
|
|
|
+ imageView.isHidden = true
|
|
|
+
|
|
|
+ let spinner = showSpinner()
|
|
|
+
|
|
|
+ if croppingParameters.isEnabled {
|
|
|
+ let cropRect = makeProportionalCropRect()
|
|
|
+ let resizedCropRect = CGRect(x: (image.size.width) * cropRect.origin.x,
|
|
|
+ y: (image.size.height) * cropRect.origin.y,
|
|
|
+ width: (image.size.width * cropRect.width),
|
|
|
+ height: (image.size.height * cropRect.height))
|
|
|
+
|
|
|
+ DispatchQueue.global(qos: .userInitiated).async {
|
|
|
+ let croppedImage = image.crop(rect: resizedCropRect) //This can take long time and block UI
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ self.onComplete?(croppedImage, self.asset)
|
|
|
+ self.hideSpinner(spinner)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ onComplete?(image, self.asset)
|
|
|
+ hideSpinner(spinner)
|
|
|
+ }
|
|
|
+
|
|
|
+ cropOverlay.removeFromSuperview() //remove overlay while processing
|
|
|
+ }
|
|
|
+
|
|
|
+ public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
|
+ return imageView
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
|
+ centerScrollViewContents()
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
|
+ view.setNeedsLayout()
|
|
|
+ }
|
|
|
+
|
|
|
+ func showSpinner() -> UIActivityIndicatorView {
|
|
|
+ let spinner = UIActivityIndicatorView()
|
|
|
+ spinner.style = .white
|
|
|
+ spinner.center = view.center
|
|
|
+ spinner.startAnimating()
|
|
|
+
|
|
|
+ view.addSubview(spinner)
|
|
|
+ view.bringSubviewToFront(spinner)
|
|
|
+
|
|
|
+ return spinner
|
|
|
+ }
|
|
|
+
|
|
|
+ func hideSpinner(_ spinner: UIActivityIndicatorView) {
|
|
|
+ spinner.stopAnimating()
|
|
|
+ spinner.removeFromSuperview()
|
|
|
+ }
|
|
|
+
|
|
|
+ func disable() {
|
|
|
+ confirmButton.isEnabled = false
|
|
|
+ cancelButton.isEnabled = false
|
|
|
+ }
|
|
|
+
|
|
|
+ func enable() {
|
|
|
+ confirmButton.isEnabled = true
|
|
|
+ cancelButton.isEnabled = true
|
|
|
+ }
|
|
|
+
|
|
|
+ func showNoImageScreen(_ error: NSError) {
|
|
|
+ let permissionsView = PermissionsView(frame: view.bounds)
|
|
|
+
|
|
|
+ let desc = localizedString("error.cant-fetch-photo.description")
|
|
|
+
|
|
|
+ permissionsView.configureInView(view, title: error.localizedDescription, description: desc, completion: { [weak self] in self?.cancel() })
|
|
|
+ }
|
|
|
+
|
|
|
+ private func makeProportionalCropRect() -> CGRect {
|
|
|
+ var cropRect = CGRect(x: cropOverlay.frame.origin.x + cropOverlay.outterGap,
|
|
|
+ y: cropOverlay.frame.origin.y + cropOverlay.outterGap,
|
|
|
+ width: cropOverlay.frame.size.width - 2 * cropOverlay.outterGap,
|
|
|
+ height: cropOverlay.frame.size.height - 2 * cropOverlay.outterGap)
|
|
|
+
|
|
|
+ cropRect.origin.x += scrollView.contentOffset.x - imageView.frame.origin.x
|
|
|
+ cropRect.origin.y += scrollView.contentOffset.y - imageView.frame.origin.y
|
|
|
+
|
|
|
+ let normalizedX = cropRect.origin.x / imageView.frame.width
|
|
|
+ let normalizedY = cropRect.origin.y / imageView.frame.height
|
|
|
+
|
|
|
+ let extraWidth = CGFloat(0) //fabs(cropRect.origin.x)
|
|
|
+ let extraHeight = CGFloat(0) //fabs(cropRect.origin.y)
|
|
|
+
|
|
|
+ let normalizedWidth = (cropRect.width + extraWidth) / imageView.frame.width
|
|
|
+ let normalizedHeight = (cropRect.height + extraHeight) / imageView.frame.height
|
|
|
+
|
|
|
+ return CGRect(x: normalizedX, y: normalizedY, width: normalizedWidth, height: normalizedHeight)
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+extension UIImage {
|
|
|
+
|
|
|
+ func crop(rect: CGRect) -> UIImage {
|
|
|
+
|
|
|
+ var rectTransform: CGAffineTransform
|
|
|
+ switch imageOrientation {
|
|
|
+ case .left:
|
|
|
+ rectTransform = CGAffineTransform(rotationAngle: radians(90)).translatedBy(x: 0, y: -size.height)
|
|
|
+ case .right:
|
|
|
+ rectTransform = CGAffineTransform(rotationAngle: radians(-90)).translatedBy(x: -size.width, y: 0)
|
|
|
+ case .down:
|
|
|
+ rectTransform = CGAffineTransform(rotationAngle: radians(-180)).translatedBy(x: -size.width, y: -size.height)
|
|
|
+ default:
|
|
|
+ rectTransform = CGAffineTransform.identity
|
|
|
+ }
|
|
|
+
|
|
|
+ rectTransform = rectTransform.scaledBy(x: scale, y: scale)
|
|
|
+
|
|
|
+ let cropAspect = rect.height / rect.width
|
|
|
+
|
|
|
+ if let cropped = cgImage?.cropping(to: rect.applying(rectTransform)) {
|
|
|
+
|
|
|
+ let cropImage = UIImage(cgImage: cropped, scale: scale, orientation: imageOrientation).fixOrientation()
|
|
|
+
|
|
|
+
|
|
|
+ //Rescale the cropped portion to maintain the crop aspect ratio
|
|
|
+ let currentAspect = cropImage.size.height / cropImage.size.width
|
|
|
+
|
|
|
+ return cropImage.scaledBy(size: CGSize(width: cropImage.size.width, height: cropImage.size.height * cropAspect / currentAspect)) ?? self
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return self
|
|
|
+ }
|
|
|
+
|
|
|
+ func fixOrientation() -> UIImage {
|
|
|
+ if imageOrientation == .up {
|
|
|
+ return self
|
|
|
+ }
|
|
|
+
|
|
|
+ UIGraphicsBeginImageContextWithOptions(size, false, scale)
|
|
|
+ draw(in: CGRect(origin: .zero, size: size))
|
|
|
+ let normalizedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
|
|
|
+ UIGraphicsEndImageContext()
|
|
|
+
|
|
|
+ return normalizedImage
|
|
|
+ }
|
|
|
+
|
|
|
+ func scaledBy(size: CGSize) -> UIImage? {
|
|
|
+ let hasAlpha = false
|
|
|
+ let scale: CGFloat = 0.0 // Automatically use scale factor of main screen
|
|
|
+
|
|
|
+ UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale)
|
|
|
+ self.draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: size))
|
|
|
+
|
|
|
+ let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
|
+ UIGraphicsEndImageContext()
|
|
|
+
|
|
|
+ return scaledImage
|
|
|
+ }
|
|
|
+
|
|
|
+ //Rotate by 90 degrees
|
|
|
+ func rotate() -> UIImage? {
|
|
|
+
|
|
|
+ let radians = Float.pi/2
|
|
|
+
|
|
|
+ var newSize = CGRect(origin: CGPoint.zero, size: CGSize(width: self.size.width, height: self.size.height)).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size
|
|
|
+
|
|
|
+ // Trim off the extremely small float value to prevent core graphics from rounding it up
|
|
|
+ newSize.width = floor(newSize.width)
|
|
|
+ newSize.height = floor(newSize.height)
|
|
|
+
|
|
|
+ UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
|
|
|
+ let context = UIGraphicsGetCurrentContext()!
|
|
|
+
|
|
|
+ // Move origin to middle
|
|
|
+ context.translateBy(x: newSize.width/2, y: newSize.height/2)
|
|
|
+ // Rotate around middle
|
|
|
+ context.rotate(by: CGFloat(radians))
|
|
|
+ // Draw the image at its center
|
|
|
+ self.draw(in: CGRect(x: -self.size.width/2, y: -self.size.height/2, width: self.size.width, height: self.size.height))
|
|
|
+
|
|
|
+ let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
|
+ UIGraphicsEndImageContext()
|
|
|
+
|
|
|
+ return newImage
|
|
|
+ }
|
|
|
+}
|