Image editor fixes.

pull/1/head
Matthew Chen 6 years ago
parent a6bc328779
commit 17c3ba0580

@ -950,7 +950,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
let imageMediaView = mediaMessageView.contentView { let imageMediaView = mediaMessageView.contentView {
let imageEditorView = ImageEditorView(model: imageEditorModel) let imageEditorView = ImageEditorView(model: imageEditorModel)
if imageEditorView.createImageView() { if imageEditorView.configureSubviews() {
mediaMessageView.isHidden = true mediaMessageView.isHidden = true
imageMediaView.isUserInteractionEnabled = true imageMediaView.isUserInteractionEnabled = true

@ -6,6 +6,12 @@ import UIKit
class ImageEditorGestureRecognizer: UIGestureRecognizer { class ImageEditorGestureRecognizer: UIGestureRecognizer {
@objc
public var shouldAllowOutsideView = true
@objc
public weak var referenceView: UIView?
@objc @objc
override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool { override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
return false return false
@ -95,8 +101,12 @@ class ImageEditorGestureRecognizer: UIGestureRecognizer {
} }
private func touchType(for touches: Set<UITouch>, with event: UIEvent) -> TouchType { private func touchType(for touches: Set<UITouch>, with event: UIEvent) -> TouchType {
guard let view = self.view else { guard let gestureView = self.view else {
owsFailDebug("Missing view") owsFailDebug("Missing gestureView")
return .invalid
}
guard let referenceView = referenceView else {
owsFailDebug("Missing referenceView")
return .invalid return .invalid
} }
guard let allTouches = event.allTouches else { guard let allTouches = event.allTouches else {
@ -112,25 +122,31 @@ class ImageEditorGestureRecognizer: UIGestureRecognizer {
guard let firstTouch: UITouch = touches.first else { guard let firstTouch: UITouch = touches.first else {
return .invalid return .invalid
} }
let location = firstTouch.location(in: view)
let isNewTouch = firstTouch.phase == .began let isNewTouch = firstTouch.phase == .began
if isNewTouch { if isNewTouch {
// Reject new touches that are inside a control subview. // Reject new touches that are inside a control subview.
if subviewControl(ofView: view, contains: firstTouch) { if subviewControl(ofView: gestureView, contains: firstTouch) {
return .invalid return .invalid
} }
} }
// Reject new touches outside this GR's view's bounds. // Reject new touches outside this GR's view's bounds.
guard view.bounds.contains(location) else { let location = firstTouch.location(in: referenceView)
return isNewTouch ? .invalid : .outside if !referenceView.bounds.contains(location) {
if shouldAllowOutsideView {
// Do nothing
} else if isNewTouch {
return .invalid
} else {
return .outside
}
} }
if isNewTouch { if isNewTouch {
// Ignore touches that start near the top or bottom edge of the screen; // Ignore touches that start near the top or bottom edge of the screen;
// they may be a system edge swipe gesture. // they may be a system edge swipe gesture.
let rootView = self.rootView(of: view) let rootView = self.rootView(of: gestureView)
let rootLocation = firstTouch.location(in: rootView) let rootLocation = firstTouch.location(in: rootView)
let distanceToTopEdge = max(0, rootLocation.y) let distanceToTopEdge = max(0, rootLocation.y)
let distanceToBottomEdge = max(0, rootView.bounds.size.height - rootLocation.y) let distanceToBottomEdge = max(0, rootView.bounds.size.height - rootLocation.y)

@ -500,7 +500,7 @@ public class ImageEditorModel: NSObject {
public func crop(unitCropRect: CGRect) { public func crop(unitCropRect: CGRect) {
guard let croppedImage = ImageEditorModel.crop(imagePath: contents.imagePath, guard let croppedImage = ImageEditorModel.crop(imagePath: contents.imagePath,
unitCropRect: unitCropRect) else { unitCropRect: unitCropRect) else {
owsFailDebug("Could not crop image.") Logger.warn("Could not crop image.")
return return
} }
// Use PNG for temp files; PNG is lossless. // Use PNG for temp files; PNG is lossless.
@ -584,10 +584,10 @@ public class ImageEditorModel: NSObject {
} }
let srcImageSize = srcImage.size let srcImageSize = srcImage.size
// Convert from unit coordinates to src image coordinates. // Convert from unit coordinates to src image coordinates.
let cropRect = CGRect(x: unitCropRect.origin.x * srcImageSize.width, let cropRect = CGRect(x: round(unitCropRect.origin.x * srcImageSize.width),
y: unitCropRect.origin.y * srcImageSize.height, y: round(unitCropRect.origin.y * srcImageSize.height),
width: unitCropRect.size.width * srcImageSize.width, width: round(unitCropRect.size.width * srcImageSize.width),
height: unitCropRect.size.height * srcImageSize.height) height: round(unitCropRect.size.height * srcImageSize.height))
guard cropRect.origin.x >= 0, guard cropRect.origin.x >= 0,
cropRect.origin.y >= 0, cropRect.origin.y >= 0,
@ -598,7 +598,9 @@ public class ImageEditorModel: NSObject {
} }
guard cropRect.size.width > 0, guard cropRect.size.width > 0,
cropRect.size.height > 0 else { cropRect.size.height > 0 else {
owsFailDebug("Empty crop rectangle.") // Not an error; indicates that the user tapped rather
// than dragged.
Logger.warn("Empty crop rectangle.")
return nil return nil
} }

@ -15,7 +15,20 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
case crop case crop
} }
private var editorMode = EditorMode.brush private var editorMode = EditorMode.brush {
didSet {
AssertIsOnMainThread()
switch editorMode {
case .brush:
// Brush strokes can start and end (and return from) outside the view.
editorGestureRecognizer?.shouldAllowOutsideView = true
case .crop:
// Crop gestures can start and end (and return from) outside the view.
editorGestureRecognizer?.shouldAllowOutsideView = true
}
}
}
@objc @objc
public required init(model: ImageEditorModel) { public required init(model: ImageEditorModel) {
@ -36,9 +49,10 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
private let imageView = UIImageView() private let imageView = UIImageView()
private var imageViewConstraints = [NSLayoutConstraint]() private var imageViewConstraints = [NSLayoutConstraint]()
private let layersView = OWSLayerView() private let layersView = OWSLayerView()
private var editorGestureRecognizer: ImageEditorGestureRecognizer?
@objc @objc
public func createImageView() -> Bool { public func configureSubviews() -> Bool {
self.addSubview(imageView) self.addSubview(imageView)
guard updateImageView() else { guard updateImageView() else {
@ -54,8 +68,10 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
self.isUserInteractionEnabled = true self.isUserInteractionEnabled = true
layersView.isUserInteractionEnabled = true layersView.isUserInteractionEnabled = true
let anyTouchGesture = ImageEditorGestureRecognizer(target: self, action: #selector(handleTouchGesture(_:))) let editorGestureRecognizer = ImageEditorGestureRecognizer(target: self, action: #selector(handleTouchGesture(_:)))
layersView.addGestureRecognizer(anyTouchGesture) editorGestureRecognizer.referenceView = layersView
self.addGestureRecognizer(editorGestureRecognizer)
self.editorGestureRecognizer = editorGestureRecognizer
return true return true
} }
@ -217,6 +233,15 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
self.currentStroke = nil self.currentStroke = nil
self.currentStrokeSamples.removeAll() self.currentStrokeSamples.removeAll()
} }
let tryToAppendStrokeSample = {
let newSample = self.unitSampleForGestureLocation(gestureRecognizer, shouldClamp: false)
if let prevSample = self.currentStrokeSamples.last,
prevSample == newSample {
// Ignore duplicate samples.
return
}
self.currentStrokeSamples.append(newSample)
}
// TODO: Color picker. // TODO: Color picker.
let strokeColor = UIColor.blue let strokeColor = UIColor.blue
@ -227,14 +252,14 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
case .began: case .began:
removeCurrentStroke() removeCurrentStroke()
currentStrokeSamples.append(unitSampleForGestureLocation(gestureRecognizer)) tryToAppendStrokeSample()
let stroke = ImageEditorStrokeItem(color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth) let stroke = ImageEditorStrokeItem(color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth)
model.append(item: stroke) model.append(item: stroke)
currentStroke = stroke currentStroke = stroke
case .changed, .ended: case .changed, .ended:
currentStrokeSamples.append(unitSampleForGestureLocation(gestureRecognizer)) tryToAppendStrokeSample()
guard let lastStroke = self.currentStroke else { guard let lastStroke = self.currentStroke else {
owsFailDebug("Missing last stroke.") owsFailDebug("Missing last stroke.")
@ -258,12 +283,17 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
} }
} }
private func unitSampleForGestureLocation(_ gestureRecognizer: UIGestureRecognizer) -> CGPoint { private func unitSampleForGestureLocation(_ gestureRecognizer: UIGestureRecognizer,
shouldClamp: Bool) -> CGPoint {
let referenceView = layersView let referenceView = layersView
// TODO: Smooth touch samples before converting into stroke samples. // TODO: Smooth touch samples before converting into stroke samples.
let location = gestureRecognizer.location(in: referenceView) let location = gestureRecognizer.location(in: referenceView)
let x = CGFloatClamp01(CGFloatInverseLerp(location.x, 0, referenceView.bounds.width)) var x = CGFloatInverseLerp(location.x, 0, referenceView.bounds.width)
let y = CGFloatClamp01(CGFloatInverseLerp(location.y, 0, referenceView.bounds.height)) var y = CGFloatInverseLerp(location.y, 0, referenceView.bounds.height)
if shouldClamp {
x = CGFloatClamp01(x)
y = CGFloatClamp01(y)
}
return CGPoint(x: x, y: y) return CGPoint(x: x, y: y)
} }
@ -350,18 +380,22 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
self.model.crop(unitCropRect: unitCropRect) self.model.crop(unitCropRect: unitCropRect)
} }
let currentUnitSample = {
self.unitSampleForGestureLocation(gestureRecognizer, shouldClamp: true)
}
switch gestureRecognizer.state { switch gestureRecognizer.state {
case .began: case .began:
let unitSample = unitSampleForGestureLocation(gestureRecognizer) let unitSample = currentUnitSample()
cropStartUnit = unitSample cropStartUnit = unitSample
cropEndUnit = unitSample cropEndUnit = unitSample
startCrop() startCrop()
case .changed: case .changed:
cropEndUnit = unitSampleForGestureLocation(gestureRecognizer) cropEndUnit = currentUnitSample()
updateCrop() updateCrop()
case .ended: case .ended:
cropEndUnit = unitSampleForGestureLocation(gestureRecognizer) cropEndUnit = currentUnitSample()
endCrop() endCrop()
default: default:
cancelCrop() cancelCrop()
@ -501,12 +535,10 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
viewSize: CGSize) -> CALayer? { viewSize: CGSize) -> CALayer? {
AssertIsOnMainThread() AssertIsOnMainThread()
Logger.verbose("\(item.itemId), viewSize: \(viewSize)")
let strokeWidth = ImageEditorStrokeItem.strokeWidth(forUnitStrokeWidth: item.unitStrokeWidth, let strokeWidth = ImageEditorStrokeItem.strokeWidth(forUnitStrokeWidth: item.unitStrokeWidth,
dstSize: viewSize) dstSize: viewSize)
let unitSamples = item.unitSamples let unitSamples = item.unitSamples
guard unitSamples.count > 1 else { guard unitSamples.count > 0 else {
// Not an error; the stroke doesn't have enough samples to render yet. // Not an error; the stroke doesn't have enough samples to render yet.
return nil return nil
} }
@ -532,7 +564,10 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
let point = points[index] let point = points[index]
let forwardVector: CGPoint let forwardVector: CGPoint
if index == 0 { if points.count <= 1 {
// Skip forward vectors.
forwardVector = .zero
} else if index == 0 {
// First sample. // First sample.
let nextPoint = points[index + 1] let nextPoint = points[index + 1]
forwardVector = CGPointSubtract(nextPoint, point) forwardVector = CGPointSubtract(nextPoint, point)
@ -552,6 +587,10 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
if index == 0 { if index == 0 {
// First sample. // First sample.
bezierPath.move(to: point) bezierPath.move(to: point)
if points.count == 1 {
bezierPath.addLine(to: point)
}
} else { } else {
let previousPoint = points[index - 1] let previousPoint = points[index - 1]
// We apply more than one kind of smoothing. // We apply more than one kind of smoothing.

@ -388,11 +388,14 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
= (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, (CFDictionaryRef)options); = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, (CFDictionaryRef)options);
BOOL result = NO; BOOL result = NO;
if (properties) { if (properties) {
NSNumber *hasAlpha = properties[(NSString *)kCGImagePropertyHasAlpha]; NSNumber *_Nullable hasAlpha = properties[(NSString *)kCGImagePropertyHasAlpha];
if (hasAlpha) { if (hasAlpha) {
result = hasAlpha.boolValue; result = hasAlpha.boolValue;
} else { } else {
OWSFailDebug(@"Could not determine transparency of image: %@", url); // This is not an error; kCGImagePropertyHasAlpha is an optional
// property.
OWSLogWarn(@"Could not determine transparency of image: %@", url);
result = NO;
} }
} }
CFRelease(source); CFRelease(source);

Loading…
Cancel
Save