Как объединить UIImages при использовании преобразования (масштаб, вращение и перевод)?

Я пытаюсь реплицировать функцию 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
        }
    }

Учитывается поворот внутри преобразования. Масштабирование внутри преобразования принимается во внимание.

Но перевод внутри преобразования, кажется, не работает (есть крошечный перевод, но он не отражает реального).

Мне явно что-то не хватает, но не могу понять, что.

Любая идея?

EDIT:

Вот несколько скриншотов о том, как выглядит наклейка в приложении, и каково окончательное изображение, сохраненное в библиотеке. Как вы можете видеть, вращение и масштаб (соотношение ширина/высота) конечного изображения такие же, как и в приложении.

UIImageView, содержащее UIImage, имеет такое же отношение, что и его изображение.

Я также добавил фон при рисовании наклейки, чтобы четко видеть границы фактического изображения.

Нет вращения или масштабирования:

введите описание изображения здесь введите описание изображения здесь

Поворот и масштабирование:

введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь

ИЗМЕНИТЬ 2:

Здесь - тестовый проект, который воспроизводит описанное выше поведение.

Ответ 1

Проблема заключается в том, что у вас есть несколько геометрий (системы координат), а также масштабные коэффициенты и контрольные точки в игре, и трудно держать их прямолинейными. У вас есть геометрия корневого представления, в которой определены кадры изображения и вид наклейки, а затем у вас есть геометрия графического контекста, и вы не делаете их совпадающими. Происхождение изображения не является источником геометрии супервизора, потому что вы ограничили его безопасными областями, и я не уверен, что вы должным образом компенсируете это смещение. Вы пытаетесь справиться с масштабированием изображения в представлении изображения, отрегулировав размер наклейки при рисовании наклейки. Вы не должным образом компенсируете тот факт, что у вас есть свойство стикера center и его transform, влияющие на его позу (местоположение/масштаб/вращение).

Пусть упрощается.

Во-первых, введем "представление холста" в качестве представления вида изображения. Вид холста может быть выложен, однако вы хотите, чтобы в отношении безопасных областей. Мы ограничим представление изображения, чтобы заполнить представление холста, поэтому начало изображения будет .zero.

новая иерархия представлений

Затем мы установим представление наклейки 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 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
    }
}

Вы можете найти мою фиксированную версию тестового проекта здесь.

Ответ 2

Я считаю, что свойства CGAffineTransform (масштабирование, поворот и перевод) работают только на одном view за раз. Итак, когда вы говорите, что пытаетесь достичь всех трех свойств преобразования, я считаю, что вы пытались использовать только два UIImageViews (один для вращения и другой для масштабирования) Добавьте еще одно свойство UIImageView для перевода.

Надеюсь, что это поможет.