如何在使用变换(缩放,旋转和平移)时合并UIImages?

问题描述 投票:11回答:2

我正在尝试复制Instagram的功能,你有你的照片,你可以添加贴纸(其他图像),然后保存。

所以在持有我的照片的UIImageView上,我将贴纸(另一个UIImageView)添加到它作为子视图,位于父母UIImageView的中心。

要在图片周围移动贴纸,我使用CGAffineTransform(我不移动UIImageView的中心)。我还应用了CGAffineTransform来旋转和缩放贴纸。

为了用贴纸保存图片,我使用CGContext如下:

    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
        }
    }

转换内部的旋转被考虑在内。转换内部的缩放被考虑在内。

但是转换中的翻译似乎没有用(有一个很小的翻译,但它并不反映真实的翻译)。

我显然在这里遗漏了一些东西但却找不到什么。

任何的想法?

编辑:

以下是贴纸在应用程序中的外观以及库中保存的最终图像的截图。如您所见,最终图像的旋转和比例(宽度/高度比)与应用程序中的旋转和比例相同。

持有UIImageViewUIImage与其图像具有相同的比率。

我还在绘制贴纸时添加了背景,以清楚地看到实际图像的边界。

没有旋转或缩放:

enter image description here enter image description here

旋转和缩放:

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

编辑2:

Here是一个重现上述行为的测试项目。

ios swift core-graphics cgcontext cgaffinetransform
2个回答
6
投票

问题是你有多个几何(坐标系)和比例因子和参考点,并且很难保持它们。您具有根视图的几何图形,其中定义了图像视图和贴纸视图的框架,然后您具有图形上下文的几何图形,并且不会使它们匹配。图像视图的原点不在其超视图几何体的原点,因为您将其约束到安全区域,并且我不确定您是否正确地补偿了该偏移。您尝试通过在绘制贴纸时调整贴纸大小来处理图像视图中图像的缩放。你没有正确地补偿你有贴纸的center属性和它的transform影响其姿势(位置/比例/旋转)这一事实。

让我们简化一下。

首先,让我们引入一个“画布视图”作为图像视图的超视图。无论您想要哪个安全区域,都可以布置画布视图。我们将约束图像视图以填充画布视图,因此图像视图的原点将是.zero

new view hierarchy

接下来,我们将贴纸视图的layer.anchorPoint设置为.zero。这使得视图的transform相对于贴纸视图的左上角而不是其中心进行操作。它还使view.layer.position(与view.center相同)控制视图左上角的位置,而不是控制视图中心的位置。我们想要这些更改,因为它们与我们合并图像时Core Graphics在areaRect中绘制贴纸图像的方式相匹配。

我们还将view.layer.position设置为.zero。这简化了我们在合并图像时计算绘制贴纸图像的位置的方式。

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
}

这意味着我们需要使用transform完全定位贴纸,因此我们要初始化transform以使贴纸居中:

@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)
}

由于这些变化,我们必须处理捏合并以更复杂的方式旋转。我们将使用辅助方法来管理复杂性:

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
    }
}

然后我们处理捏和旋转,如下所示:

@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
}

这也使得缩放和旋转工作比以前更好。在您的代码中,缩放和旋转始终围绕视图的中心发生。使用此代码,它们发生在用户手指之间的中心点周围,感觉更自然。

最后,为了合并图像,我们将首先按照imageView缩放图像的方式缩放图形上下文的几何图形,因为贴纸变换相对于imageView尺寸,而不是图像尺寸。由于我们现在完全使用变换定位贴纸,并且由于我们已将图像视图和贴纸视图原点设置为.zero,因此我们无需对奇怪的起源进行任何调整。

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
    }
}

你可以找到我的测试项目here的固定版本。


0
投票

我相信CGAffineTransform属性(缩放,旋转和平移)每个只能在一个view上工作。所以,当你说你想要实现所有三个转换属性时,我相信你一直在尝试只有两个UIImageViews(一个用于旋转,另一个用于缩放)添加一个UIImageView用于翻译属性。

希望能帮助到你。

© www.soinside.com 2019 - 2024. All rights reserved.