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

标签 ios swift core-graphics cgcontext cgaffinetransform

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

所以在保存我的图片的 UIImageView 上,我将标签(另一个 UIImageView)添加到它作为 subview ,位于父 的中心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
        }
    }

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

但是转换中的翻译似乎不起作用(有一个很小的翻译,但不能反射(reflect)真实的翻译)。

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

有什么想法吗?

编辑:

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

持有 UIImageUIImageView 与其图像的比例相同。

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

无旋转或缩放:

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是一个重现上述行为的测试项目。

最佳答案

问题是您有多个几何体(坐标系)、比例因子和引用点在起作用,并且很难使它们保持直线。你有 Root View 的几何图形,其中定义了 ImageView 和贴纸 View 的框架,然后你有图形上下文的几何图形,但你没有使它们匹配。 ImageView 的原点不在其父 View 几何图形的原点,因为您将它限制在安全区域,而且我不确定您是否正确补偿了该偏移量。您尝试通过在绘制贴纸时调整贴纸大小来处理 ImageView 中图像的缩放。您没有正确补偿贴纸的 center 属性及其影响其姿势(位置/比例/旋转)的 transform 这一事实。

让我们简化一下。

首先,让我们介绍一个“ Canvas View ”作为 ImageView 的父 View 。 Canvas View 可以按照您希望的安全区域进行布局。我们将约束 ImageView 以填充 Canvas View ,因此 ImageView 的原点将为 .zero

new view hierarchy

接下来,我们将设置贴纸 View 的 layer.anchorPoint.zero。这使得 View 的 transform 操作相对于标签 View 的左上角,而不是它的中心。它还使view.layer.position(与view.center相同)控制 View 左上角的位置,而不是控制位置 View 的中心。我们需要这些更改,因为它们与合并图像时 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
}

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

最后,为了合并图像,我们将从缩放图形上下文的几何形状开始,就像 imageView 缩放其图像一样,因为贴纸变换是相对于 imageView 大小,而不是图像大小。由于我们现在完全使用变换来定位贴纸,并且由于我们已将 ImageView 和贴纸 View 原点设置为 .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 .

关于ios - 如何在使用变换(缩放、旋转和平移)时合并 UIImages?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47746713/

相关文章:

ios - 如何根据后者的进度向已经运行的动画添加动画?

ios - Swift Collcetionview didSelectItemAtIndexPath 不起作用

ios - 如何使用 UIBezierPath 绘制弧线

ios - 如何缩小 iOS 中的图像,消除锯齿但不柔和?

ios - 你如何设置 UILayoutPriority?

ios - 如何使用 Storyboard或 xibs 在 Swift 中使用 iOS 自定义键盘扩展作为可编程自定义输入 View

objective-c - 如何使用 iOS 钥匙串(keychain)存储多个用户名和密码?

swift - 将快照解析为模型 - FirebaseUI

swift - 如何为 iOS 7 和 iOS 8 实现搜索 Controller

ios - 如何自动裁剪 UIImage?