TM-SGNL-iOS/SignalUI/ImageEditor/ImageEditorViewController+Blur.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

220 lines
7.8 KiB
Swift

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