Search code examples
iosswiftcropcifilterciimage

CIAffineClamp filter does not clamp


I am trying to do simple thing. I have image view with image in it. I want to translate image by half width of image size and clamp the pixels at the edge of the transformed image, extending them outwards with help of CIAffineClamp. But it's no working. It's do nothing. I have attached sample project.

My code is:

func translateAndClamp(image: UIImage) -> UIImage {
        guard let cgImage = image.cgImage else {
            return image
        }
        let ciImage = CIImage(cgImage: cgImage)

        var transform = CGAffineTransform.identity
        transform = transform.translatedBy(x: image.size.width / 2, y: 0)

        let filter = CIFilter(name: "CIAffineClamp")!
        filter.setValue(ciImage, forKey: kCIInputImageKey)
        filter.setValue(NSValue(cgAffineTransform: transform), forKey: "inputTransform")

        let context = CIContext(options: [CIContextOption.useSoftwareRenderer: true])

        guard let outputImage = filter.outputImage else {
            return image
        }

        let extent = ciImage.extent.applying(transform)

        guard let result = context.createCGImage(outputImage, from: extent) else {
            return image
        }

        return UIImage(cgImage: result, scale: image.scale, orientation: image.imageOrientation)
    }

Sample project

Updates


    private func makeResultImageCroppedAndRotated(from image: UIImage) -> UIImage {
        let angle = CGFloat(gestureHandler.rotation(from: imgView.transform))

        guard let cgImage = image.cgImage else {
            return image
        }

        let rotatedCGImage = rotateImage(image: cgImage, angle: angle)
        let rotated = UIImage(cgImage: rotatedCGImage, scale: image.scale, orientation: image.imageOrientation)
        let cropArea = makeCropArea(for: rotated, in: imgView, anchor: layoutContentView)
        let cropped = cropImage(image: rotatedCGImage, to: cropArea)

        return UIImage(cgImage: cropped, scale: image.scale, orientation: image.imageOrientation)
    }


    private func makeCropArea(for image: UIImage, in imageView: UIImageView, anchor: UIView) -> CGRect {
        let boundingRect = LayoutCorrectionUtility.boundingAspectRect(for: image, inside: imageView)
        let factor = LayoutCorrectionUtility.factor(for: image, insideBoundingRect: boundingRect)

        let x = (anchor.bounds.origin.x - boundingRect.origin.x) * factor
        let y = (anchor.bounds.origin.y - boundingRect.origin.y) * factor
        let width = anchor.frame.width * factor
        let height = anchor.frame.height * factor

        return CGRect(x: x, y: y, width: width, height: height)
    }


    func rotateImage(image: CGImage, angle: CGFloat) -> CGImage {
        let ciImage = CIImage(cgImage: image)

        let newAngle = angle * CGFloat(-1)

        var transform = CATransform3DIdentity
        transform = CATransform3DRotate(transform, CGFloat(newAngle), 0, 0, 1)
        let affineTransform = CATransform3DGetAffineTransform(transform)

        let filter = CIFilter(name: "CIAffineClamp")
        filter?.setValue(ciImage, forKey: kCIInputImageKey)
        filter?.setDefaults()
        filter?.setValue(NSValue(cgAffineTransform: affineTransform), forKey: "inputTransform")

        let contex = CIContext(options: [CIContextOption.useSoftwareRenderer: true])

        guard let outputImage = filter?.outputImage else {
            return image
        }

        let extent = ciImage.extent.applying(affineTransform)
        guard let result = contex.createCGImage(outputImage, from: extent) else {
            return image
        }

        return result
    }


    func cropImage(image: CGImage, to rect: CGRect) -> CGImage {
        let ciImage = CIImage(cgImage: image).clampedToExtent()

        var transform = CGAffineTransform(scaleX: 1, y: -1)
        transform = transform.translatedBy(x: 0, y: -CGFloat(image.height))

        let contex = CIContext(options: [CIContextOption.useSoftwareRenderer: true])

        guard let result = contex.createCGImage(ciImage, from: rect.applying(transform)) else {
            return image
        }

        return result
    }

Solution

  • The problem is that you also applied the transform to the output extent. Here is the code that works for me with some comments:

    func translateAndClamp(image: UIImage) -> UIImage {
        // no need to use use the internal CGImage
        guard let ciImage = CIImage(image: image) else {
            return image
        }
    
        // Important: use the CIImage's width here since the UIImage's size
        // is in points, not pixels.
        let transform = CGAffineTransform(translationX: ciImage.extent.width / 2, y: 0)
    
        // This does the same as CIAffineClamp, but is shorter and doesn't return an Optional.
        let outputImage = ciImage.transformed(by: transform).clampedToExtent()
    
        // I would advise against using the software renderer, but maybe you have your reasons.
        // Also, if you want to call this method more then once, it's better to create the
        // context on object initialization and re-use it every time since it's expensive to create.
        let context = CIContext(options: [CIContextOption.useSoftwareRenderer: true])
    
        // Important: don't apply the transform to the output extent!
        // Think of this as the "window" on the image you want to draw.
        // Since you moved the image "inside" the window with the transform,
        // the window needs to be the original image's extent.
        let extent = ciImage.extent
    
        guard let result = context.createCGImage(outputImage, from: extent) else {
            return image
        }
    
        return UIImage(cgImage: result, scale: image.scale, orientation: image.imageOrientation)
    }