Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/ImageEditor/ImageEditorViewController+Blur.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import Vision

// MARK: - Blur Tool

extension ImageEditorViewController {

    private func initializeBlurToolUIIfNecessary() {
        guard !blurToolUIInitialized else { return }

        view.addSubview(blurToolbar)
        blurToolbar.autoHCenterInSuperview()
        blurToolbar.autoPinEdge(.bottom, to: .top, of: bottomBar, withOffset: -36)

        view.addGestureRecognizer(blurToolGestureRecognizer)

        blurToolUIInitialized = true
    }

    func updateBlurToolControlsVisibility() {
        blurToolbar.alpha = topBar.alpha
        strokeWidthSliderContainer.alpha = topBar.alpha
    }

    func updateBlurToolUIVisibility() {
        let visible = mode == .blur

        if visible {
            initializeBlurToolUIIfNecessary()
        } else {
            guard blurToolUIInitialized else { return }
        }

        blurToolbar.isHidden = !visible
        blurToolGestureRecognizer.isEnabled = visible

        if visible {
            currentStrokeType = .blur
        }
    }

    @objc
    func didToggleAutoBlur(sender: UISwitch) {
        if let currentAutoBlurItem {
            model.remove(item: currentAutoBlurItem)
        }

        guard sender.isOn else { return }

        guard
            let srcImage = ImageEditorCanvasView.loadSrcImage(model: model),
            let srcCGImage = srcImage.cgImage
        else {
            return
        }

        let cgOrientation = CGImagePropertyOrientation(srcImage.imageOrientation)

        ModalActivityIndicatorViewController.present(
            fromViewController: self,
            canCancel: false,
            presentationDelay: 0.5,
        ) { modal in
            func showToast() {
                let toastController = ToastController(text: OWSLocalizedString(
                    "IMAGE_EDITOR_BLUR_TOAST",
                    comment: "A toast indicating that you can blur more faces after detection",
                ))
                let bottomInset = self.view.safeAreaInsets.bottom + 90
                toastController.presentToastView(from: .bottom, of: self.view, inset: bottomInset)
            }

            func faceDetectionFailed() {
                DispatchQueue.main.async {
                    sender.isOn = false
                    modal.dismiss { showToast() }
                }
            }

            let request = VNDetectFaceRectanglesRequest { request, error in
                if let error {
                    owsFailDebug("Face Detection Error \(error)")
                    return faceDetectionFailed()
                }
                // Perform drawing on the main thread.
                DispatchQueue.main.async {
                    guard let results = request.results as? [VNFaceObservation] else {
                        return faceDetectionFailed()
                    }

                    func unitBoundingBox(_ faceObservation: VNFaceObservation) -> CGRect {
                        var unitRect = faceObservation.boundingBox
                        unitRect.origin.y = 1 - unitRect.origin.y - unitRect.height
                        return unitRect
                    }

                    let autoBlurItem = ImageEditorBlurRegionsItem(
                        itemId: ImageEditorViewController.autoBlurItemIdentifier,
                        unitBoundingBoxes: results.map(unitBoundingBox),
                    )
                    self.model.append(item: autoBlurItem)

                    modal.dismiss { showToast() }
                }
            }

            let imageRequestHandler = VNImageRequestHandler(
                cgImage: srcCGImage,
                orientation: cgOrientation,
                options: [:],
            )

            // Send the requests to the request handler.
            do {
                try imageRequestHandler.perform([request])
            } catch let error as NSError {
                owsFailDebug("Failed to perform image request: \(error)")
                return faceDetectionFailed()
            }
        }
    }

    @objc
    func handleBlurToolGesture(_ gestureRecognizer: ImageEditorPanGestureRecognizer) {
        AssertIsOnMainThread()

        owsAssertDebug(mode == .blur, "Incorrect mode [\(mode)]")

        func removeCurrentBlur() {
            if let blur = self.currentStroke {
                self.model.remove(item: blur)
            }
            self.currentStroke = nil
            self.currentStrokeSamples.removeAll()
        }
        func tryToAppendBlurSample(_ locationInView: CGPoint) {
            let view = self.imageEditorView.gestureReferenceView
            let viewBounds = view.bounds
            let newSample = ImageEditorCanvasView.locationImageUnit(
                forLocationInView: locationInView,
                viewBounds: viewBounds,
                model: self.model,
                transform: self.model.currentTransform(),
            )

            if
                let prevSample = self.currentStrokeSamples.last,
                prevSample == newSample
            {
                // Ignore duplicate samples.
                return
            }
            self.currentStrokeSamples.append(newSample)
        }

        let unitBlurStrokeWidth = currentStrokeUnitWidth()

        switch gestureRecognizer.state {
        case .began:
            removeCurrentBlur()

            // Apply the location history of the gesture so that the blur reflects
            // the touch's movement before the gesture recognized.
            for location in gestureRecognizer.locationHistory {
                tryToAppendBlurSample(location)
            }

            let locationInView = gestureRecognizer.location(in: imageEditorView.gestureReferenceView)
            tryToAppendBlurSample(locationInView)

            let blur = ImageEditorStrokeItem(
                strokeType: .blur,
                unitSamples: currentStrokeSamples,
                unitStrokeWidth: unitBlurStrokeWidth,
            )
            model.append(item: blur)
            currentStroke = blur

        case .changed, .ended:
            let locationInView = gestureRecognizer.location(in: imageEditorView.gestureReferenceView)
            tryToAppendBlurSample(locationInView)

            guard let lastBlur = self.currentStroke else {
                owsFailDebug("Missing last blur.")
                removeCurrentBlur()
                return
            }

            // Model items are immutable; we _replace_ the
            // blur item rather than modify it.
            let blurStroke = ImageEditorStrokeItem(
                itemId: lastBlur.itemId,
                strokeType: .blur,
                unitSamples: currentStrokeSamples,
                unitStrokeWidth: unitBlurStrokeWidth,
            )
            model.replace(item: blurStroke, suppressUndo: true)

            if gestureRecognizer.state == .ended {
                currentStroke = nil
                currentStrokeSamples.removeAll()
            } else {
                currentStroke = blurStroke
            }

        default:
            removeCurrentBlur()
        }
    }
}

private extension CGImagePropertyOrientation {

    init(_ uiImageOrientation: UIImage.Orientation) {
        switch uiImageOrientation {
        case .up: self = .up
        case .down: self = .down
        case .left: self = .left
        case .right: self = .right
        case .upMirrored: self = .upMirrored
        case .downMirrored: self = .downMirrored
        case .leftMirrored: self = .leftMirrored
        case .rightMirrored: self = .rightMirrored
        default: self = .up
        }
    }
}