Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to merge UIImages while using transform (scale, rotation and translation)?

I'm trying to replicate a function of Instagram where you have your picture and you can add stickers (others images) and then save it.

So on the UIImageView that holds my picture, I add the sticker (another UIImageView) to it as a subview, positioned at the center of the parent UIImageView.

To move the sticker around the picture, I do it using a CGAffineTransform (I don't move the center of the UIImageView). I also apply a CGAffineTransform for rotating and scaling the sticker.

To save the picture with the stickers, I use a CGContext as following:

    extension UIImage {
        func merge2(in rect: CGRect, with imageTuples: [(image: UIImage, viewSize: CGSize, transform: CGAffineTransform)]) -> UIImage? {
            UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)

            guard let context = UIGraphicsGetCurrentContext() else { return nil }

            draw(in: CGRect(size: size), blendMode: .normal, alpha: 1)

            // Those multiplicators are used to properly scale the transform of each sub image as the parent image (self) might be bigger than its view bounds, same goes for the subviews
            let xMultiplicator = size.width / rect.width
            let yMultiplicator = size.height / rect.height

            for imageTuple in imageTuples {
                let size = CGSize(width: imageTuple.viewSize.width * xMultiplicator, height: imageTuple.viewSize.height * yMultiplicator)
                let center = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
                let areaRect = CGRect(center: center, size: size)

                context.saveGState()

                let transform = imageTuple.transform
                context.translateBy(x: center.x, y: center.y)
                context.concatenate(transform)
                context.translateBy(x: -center.x, y: -center.y)

// EDITED CODE
                context.setBlendMode(.color)
                UIColor.subPink.setFill()
                context.fill(areaRect)
// EDITED CODE
                imageTuple.image.draw(in: areaRect, blendMode: .normal, alpha: 1)

                context.restoreGState()
            }

            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()

            return image
        }
    }

The rotation inside the transform is taken into account. The scaling inside the transform is taken into account.

But the translation inside the transform seems to no work (there is a tiny translation but it doesn't reflect the real one).

I'm obviously missing something here but can't find out what.

Any idea?

EDIT:

Here are some screenshots of how the sticker looks like on the app and what is the final image saved in the library. As you can see, the rotation and scale (the width/height ratio) of the final image are the same than the one in the app.

The UIImageView holding the UIImage has the same ratio than its image.

I also added a background when drawing the sticker to clearly see the bounds of the actual image.

No rotation or scaling:

enter image description here enter image description here

Rotated and scaled:

enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here

EDIT 2:

Here is a test project that reproduces the behaviour described above.

like image 201
Nico Avatar asked Dec 11 '17 05:12

Nico


People also ask

How to rotate image with CSS?

Rotating an image using CSS Once the CSS code is applied to your . css file, stylesheet, or <style> tags, you can use the CSS class name in any of your image tags. To rotate an image by another measure of degrees, change the "180" in the CSS code and <img> tag to the degree you desire.

How do you rotate an image in JavaScript?

To rotate an image with JavaScript, access the image element with a method like getElementById() , then set the style. transform property to a string in the format rotate({value}deg) , where {value} is the clockwise angle of rotation in degrees. rotated. style.


1 Answers

The problem is you have multiple geometries (coordinate systems) and scale factors and reference points in play, and it's hard to keep them straight. You have the root view's geometry, in which the frames of the image view and the sticker view are defined, and then you have the geometry of the graphics context, and you don't make them match. The image view's origin is not at the origin of its superview's geometry, because you constrained it to the safe areas, and I'm not sure you're properly compensating for that offset. You try to deal with the scaling of the image in the image view by adjusting the sticker size when drawing the sticker. You don't properly compensate for the fact that you have both the sticker's center property and its transform affecting its pose (location / scale /rotation).

Let's simplify.

First, let's introduce a “canvas view” as the superview of the image view. The canvas view can be laid out however you want with respect to the safe areas. We'll constrain the image view to fill the canvas view, so the image view's origin will be .zero.

new view hierarchy

Next, we'll set the sticker view's layer.anchorPoint to .zero. This makes the view's transform operate relative to the top-left corner of the sticker view, instead of its center. It also makes view.layer.position (which is the same as view.center) control the position of the top-left corner of the view, instead of controlling the position of the center of the view. We want these changes because they match how Core Graphics draws the sticker image in areaRect when we merge the images.

We'll also set view.layer.position to .zero. This simplifies how we compute where to draw the sticker image when we merge the images.

private func makeStickerView(with image: UIImage, center: CGPoint) -> UIImageView {
    let heightOnWidthRatio = image.size.height / image.size.width
    let imageWidth: CGFloat = 150

    //      let newStickerImageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: imageWidth, height: imageWidth * heightOnWidthRatio)))
    let view = UIImageView(frame: CGRect(x: 0, y: 0, width: imageWidth, height: imageWidth * heightOnWidthRatio))
    view.image = image
    view.clipsToBounds = true
    view.contentMode = .scaleAspectFit
    view.isUserInteractionEnabled = true
    view.backgroundColor = UIColor.red.withAlphaComponent(0.7)
    view.layer.anchorPoint = .zero
    view.layer.position = .zero
    return view
}

This means we need to position the sticker entirely using its transform, so we want to initialize the transform to center the sticker:

@IBAction func resetPose(_ sender: Any) {
    let center = CGPoint(x: canvasView.bounds.midX, y: canvasView.bounds.midY)
    let size = stickerView.bounds.size
    stickerView.transform = .init(translationX: center.x - size.width / 2, y: center.y - size.height / 2)
}

Because of these changes, we have to handle pinches and rotates in a more complex way. We'll use a helper method to manage the complexity:

extension CGAffineTransform {
    func around(_ locus: CGPoint, do body: (CGAffineTransform) -> (CGAffineTransform)) -> CGAffineTransform {
        var transform = self.translatedBy(x: locus.x, y: locus.y)
        transform = body(transform)
        transform = transform.translatedBy(x: -locus.x, y: -locus.y)
        return transform
    }
}

Then we handle pinch and rotate like this:

@objc private func stickerDidPinch(pincher: UIPinchGestureRecognizer) {
    guard let stickerView = pincher.view else { return }
    stickerView.transform = stickerView.transform.around(pincher.location(in: stickerView), do: { $0.scaledBy(x: pincher.scale, y: pincher.scale) })
    pincher.scale = 1
}

@objc private func stickerDidRotate(rotater: UIRotationGestureRecognizer) {
    guard let stickerView = rotater.view else { return }
    stickerView.transform = stickerView.transform.around(rotater.location(in: stickerView), do: { $0.rotated(by: rotater.rotation) })
    rotater.rotation = 0
}

This also makes scaling and rotating work better than before. In your code, scaling and rotating always happen around the center of the view. With this code, they happen around the center point between the user's fingers, which feels more natural.

Finally, to merge the images, we'll start by scaling the graphics context's geometry the same way imageView scaled its image, because the sticker transform is relative to the imageView size, not the image size. Since we position the sticker entirely using the transform now, and since we've set the image view and sticker view origins to .zero, we don't have to make any adjustments for weird origins.

extension UIImage {

    func merge(in viewSize: CGSize, with imageTuples: [(image: UIImage, viewSize: CGSize, transform: CGAffineTransform)]) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)

        print("scale : \(UIScreen.main.scale)")
        print("size : \(size)")
        print("--------------------------------------")

        guard let context = UIGraphicsGetCurrentContext() else { return nil }

        // Scale the context geometry to match the size of the image view that displayed me, because that's what all the transforms are relative to.
        context.scaleBy(x: size.width / viewSize.width, y: size.height / viewSize.height)

        draw(in: CGRect(origin: .zero, size: viewSize), blendMode: .normal, alpha: 1)

        for imageTuple in imageTuples {
            let areaRect = CGRect(origin: .zero, size: imageTuple.viewSize)

            context.saveGState()
            context.concatenate(imageTuple.transform)

            context.setBlendMode(.color)
            UIColor.purple.withAlphaComponent(0.5).setFill()
            context.fill(areaRect)

            imageTuple.image.draw(in: areaRect, blendMode: .normal, alpha: 1)

            context.restoreGState()
        }

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return image
    }
}

You can find my fixed version of your test project here.

like image 140
rob mayoff Avatar answered Sep 27 '22 19:09

rob mayoff