Verified Commit 91af9dcf authored by Geoff Pado's avatar Geoff Pado
Browse files

Merge branch '193-manual-redaction-draws-outside-image' into 180-magic-lasso

parents c109a6a9 eda16436
......@@ -98,6 +98,11 @@ open class BasePhotoEditingViewController: UIViewController, UIScrollViewDelegat
updateToolbarItems()
}
@objc public func selectLasso() {
photoEditingView.highlighterTool = .lasso
updateToolbarItems()
}
private func updateToolbarItems(animated: Bool = true) {
let undoToolItem = UIBarButtonItem(image: Icons.undo, style: .plain, target: self, action: #selector(BasePhotoEditingViewController.undo))
undoToolItem.isEnabled = undoManager?.canUndo ?? false
......
......@@ -10,18 +10,25 @@ public class PhotoEditingView: UIView, UIScrollViewDelegate {
photoScrollView.delegate = self
addSubview(photoScrollView)
addSubview(brushStrokeView)
NSLayoutConstraint.activate([
photoScrollView.widthAnchor.constraint(equalTo: safeAreaLayoutGuide.widthAnchor),
photoScrollView.heightAnchor.constraint(equalTo: safeAreaLayoutGuide.heightAnchor),
photoScrollView.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
photoScrollView.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor)
photoScrollView.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor),
brushStrokeView.centerXAnchor.constraint(equalTo: centerXAnchor),
brushStrokeView.centerYAnchor.constraint(equalTo: centerYAnchor),
brushStrokeView.widthAnchor.constraint(equalTo: widthAnchor),
brushStrokeView.heightAnchor.constraint(equalTo: heightAnchor)
])
brushStrokeView.addTarget(self, action: #selector(handleStrokeCompletion), for: .touchUpInside)
}
public var color: UIColor {
get { return workspaceView.color }
set(newColor) { workspaceView.color = newColor }
get { return brushStrokeView.color }
set(newColor) { brushStrokeView.color = newColor }
}
public var image: UIImage? {
......@@ -46,6 +53,7 @@ public class PhotoEditingView: UIView, UIScrollViewDelegate {
public var highlighterTool: HighlighterTool {
get { return workspaceView.highlighterTool }
set(newTool) {
brushStrokeView.highlighterTool = newTool
workspaceView.highlighterTool = newTool
}
}
......@@ -59,7 +67,7 @@ public class PhotoEditingView: UIView, UIScrollViewDelegate {
}
func redact<ObservationType: TextObservation>(_ observations: [ObservationType]) {
observations.forEach { [unowned self] in self.workspaceView.redact($0) }
observations.forEach { [unowned self] in self.workspaceView.redact($0, color: self.color) }
}
// MARK: UIScrollViewDelegate
......@@ -73,8 +81,82 @@ public class PhotoEditingView: UIView, UIScrollViewDelegate {
workspaceView.scrollViewDidZoom(to: scrollView.zoomScale)
}
// MARK: Actions
private var scaledPath: UIBezierPath? {
guard let currentPath = brushStrokeView.currentPath else { return nil }
let scaledPath = UIBezierPath(cgPath: currentPath.cgPath)
let scale = pow(photoScrollView.zoomScale, -1.0)
scaledPath.lineWidth = 10.0 * scale
let convertedOrigin = convert(workspaceView.bounds.origin, from: workspaceView)
let scaleTransform = CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: convertedOrigin.x * -1, y: convertedOrigin.y * -1)
scaledPath.apply(scaleTransform)
return scaledPath
}
@objc func handleStrokeCompletion() {
switch highlighterTool {
case .magic: handleMagicStrokeCompletion()
case .manual: handleManualStrokeCompletion()
case .eraser: handleEraserCompletion()
case .lasso: handleLassoCompletion()
}
workspaceView.sendAction(#selector(BasePhotoEditingViewController.markHasMadeEdits), to: nil, for: nil)
}
private func handleMagicStrokeCompletion() {
guard let strokePath = scaledPath, let textObservations = textObservations else { return }
let strokeBorderPath = strokePath.strokeBorderPath
let redactedCharacterObservations = textObservations
.compactMap { $0.characterObservations }
.flatMap { $0 }
.filter { strokeBorderPath.contains($0.bounds.center) }
workspaceView.addDebugPath(strokeBorderPath)
if let newRedaction = Redaction(redactedCharacterObservations, color: color) {
workspaceView.add(newRedaction)
}
}
private func handleManualStrokeCompletion() {
guard let strokePath = scaledPath else { return }
workspaceView.add(Redaction(path: strokePath, color: color))
}
private func handleEraserCompletion() {
guard let strokePath = scaledPath else { return }
let strokeBorderPath = strokePath.strokeBorderPath
let intersectedRedactions = redactions.filter { redaction in
redaction.paths.contains(where: { path in
path.strokeBorderPath.intersection(with: strokeBorderPath)?.count ?? 0 > 0
})
}
workspaceView.remove(intersectedRedactions)
}
private func handleLassoCompletion() {
guard let strokePath = scaledPath, let textObservations = textObservations else { return }
strokePath.close()
workspaceView.addDebugPath(strokePath)
let redactedCharacterObservations = textObservations
.compactMap { $0.characterObservations }
.flatMap { $0 }
.filter { strokePath.contains($0.bounds.center) }
if let newRedaction = Redaction(redactedCharacterObservations, color: color) {
workspaceView.add(newRedaction)
}
}
// MARK: Boilerplate
private let brushStrokeView = PhotoEditingCanvasBrushStrokeView()
private let photoScrollView = PhotoEditingScrollView()
private var workspaceView: PhotoEditingWorkspaceView { return photoScrollView.workspaceView }
......
......@@ -27,6 +27,7 @@ class HighlighterToolBarButtonItem: UIBarButtonItem {
case .magic: return NSLocalizedString("ToolPickerItem.magicToolItem", comment: "Menu item for the magic highlighter tool")
case .manual: return NSLocalizedString("ToolPickerItem.manualToolItem", comment: "Menu item for the manual highlighter tool")
case .eraser: return NSLocalizedString("ToolPickerItem.eraserToolItem", comment: "Menu item for the eraser tool")
case .lasso: return NSLocalizedString("ToolPickerItem.lassoToolItem", comment: "Menu item for the lasso tool")
}
}
......@@ -35,6 +36,7 @@ class HighlighterToolBarButtonItem: UIBarButtonItem {
case .magic: return #selector(BasePhotoEditingViewController.selectMagicHighlighter)
case .manual: return #selector(BasePhotoEditingViewController.selectManualHighlighter)
case .eraser: return #selector(BasePhotoEditingViewController.selectEraser)
case .lasso: return #selector(BasePhotoEditingViewController.selectLasso)
}
}
}
......@@ -9,7 +9,6 @@ NS_ASSUME_NONNULL_BEGIN
@property (readwrite, nonnull) UIColor *color;
@property (readonly, nullable) UIBezierPath *currentPath;
- (void)updateToolWithCurrentZoomScale:(CGFloat)currentZoomScale NS_SWIFT_NAME(updateTool(currentZoomScale:));
@end
......
// Created by Geoff Pado on 8/27/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
#import <Editing/PhotoEditingBrushStrokeView.h>
#import <PencilKit/PencilKit.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface PhotoEditingCanvasBrushStrokeView : UIControl <PhotoEditingBrushStrokeView, PKCanvasViewDelegate>
@property (nonatomic, nullable, readonly) UIBezierPath *currentPath;
@end
NS_ASSUME_NONNULL_END
// Created by Geoff Pado on 8/27/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
#import "PhotoEditingCanvasBrushStrokeView.h"
#import "PhotoEditingCanvasView.h"
@interface PhotoEditingCanvasBrushStrokeView ()
@property (nonatomic, strong) PhotoEditingCanvasView *canvasView;
@property (nonatomic) BOOL canvasViewIsResetting;
@property (nonatomic, nullable, strong) UIBezierPath *currentPath;
@end
@implementation PhotoEditingCanvasBrushStrokeView
@synthesize currentPath;
- (instancetype)init {
if ((self = [super initWithFrame:CGRectZero])) {
self.translatesAutoresizingMaskIntoConstraints = NO;
self.canvasView = [[PhotoEditingCanvasView alloc] init];
self.canvasView.delegate = self;
[self addSubview:self.canvasView];
[NSLayoutConstraint activateConstraints:@[
[self.canvasView.widthAnchor constraintEqualToAnchor:self.widthAnchor multiplier:1.0],
[self.canvasView.heightAnchor constraintEqualToAnchor:self.heightAnchor multiplier:1.0],
[self.canvasView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[self.canvasView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor]
]];
}
return self;
}
- (UIColor *)color {
return self.canvasView.color;
}
- (void)setColor:(UIColor *)color {
self.canvasView.color = color;
}
- (void)updateToolWithCurrentZoomScale:(CGFloat)currentZoomScale {
[self.canvasView updateToolWithCurrentZoomScale:currentZoomScale];
}
#pragma mark PKCanvasViewDelegate
- (void)canvasViewDrawingDidChange:(PKCanvasView *)canvasView {
if (self.canvasViewIsResetting) { return; }
if (@available(iOS 14.0, *)) {
self.currentPath = [self pathFromDrawing:self.canvasView.drawing];
}
[self sendActionsForControlEvents:UIControlEventTouchUpInside];
self.canvasViewIsResetting = YES;
self.canvasView.drawing = [PKDrawing new];
self.canvasViewIsResetting = NO;
}
#pragma mark Path Manipulation
- (UIBezierPath *)newPath {
UIBezierPath *newPath = [UIBezierPath bezierPath];
newPath.lineCapStyle = kCGLineCapRound;
newPath.lineJoinStyle = kCGLineJoinRound;
newPath.lineWidth = self.canvasView.currentLineWidth;
return newPath;
}
- (UIBezierPath *)pathFromDrawing:(PKDrawing *)drawing {
if (@available(iOS 14.0, *)) {
NSArray<PKStroke *> *strokes = [drawing strokes];
PKStroke *currentStroke = [strokes lastObject];
PKStrokePath *strokePath = [currentStroke path];
UIBezierPath *bezierPath = [self newPath];
[bezierPath moveToPoint:[strokePath interpolatedLocationAt:0]];
for (NSUInteger i = 1; i < [strokePath count]; i++) {
[bezierPath addLineToPoint:[strokePath interpolatedLocationAt:i]];
}
return bezierPath;
} else {
return [self newPath];
}
}
#pragma mark Touch Handling
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (@available(iOS 14.0, *)) { return; }
UITouch *touch = [touches anyObject];
if (touch == nil) { return; }
self.currentPath = [self newPath];
[self.currentPath moveToPoint:[touch locationInView:self]];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (@available(iOS 14.0, *)) { return; }
UITouch *touch = [touches anyObject];
if (touch == nil) { return; }
[self.currentPath addLineToPoint:[touch locationInView:self]];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (@available(iOS 14.0, *)) { return; }
[self sendActionsForControlEvents:UIControlEventTouchUpInside];
self.currentPath = nil;
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (@available(iOS 14.0, *)) { return; }
self.currentPath = nil;
}
@end
//
// PhotoEditingCanvasView.h
// Canvas
//
// Created by Geoff Pado on 8/27/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
//
#import <PencilKit/PencilKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface PhotoEditingCanvasView : PKCanvasView
@property (nonatomic, nonnull) UIColor *color;
@property (nonatomic) CGFloat currentLineWidth;
- (void)updateToolWithCurrentZoomScale:(CGFloat)currentZoomScale;
@end
NS_ASSUME_NONNULL_END
// Created by Geoff Pado on 8/27/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
#import "PhotoEditingCanvasView.h"
#import "PhotoEditingCanvasBrushStrokeView.h"
#define STANDARD_LINE_WIDTH 10.0
@implementation PhotoEditingCanvasView
- (instancetype)init
{
if ((self = [super initWithFrame:CGRectZero])) {
self.accessibilityIgnoresInvertColors = YES;
#if TARGET_OS_MACCATALYST
self.drawingPolicy = PKCanvasViewDrawingPolicyAnyInput;
#else
self.allowsFingerDrawing = YES;
#endif
self.backgroundColor = [UIColor clearColor];
self.color = [UIColor blackColor];
self.opaque = NO;
self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
self.tool = [self toolForZoomScale:1.0];
self.translatesAutoresizingMaskIntoConstraints = NO;
}
return self;
}
- (void)setColor:(UIColor *)color {
_color = color;
PKInkingTool *currentTool = (PKInkingTool *)[self tool];
PKInkingTool *newTool = [[PKInkingTool alloc] initWithInkType:currentTool.inkType color:color width:currentTool.width];
self.tool = newTool;
}
// MARK: Tool Creation
- (CGFloat)currentLineWidth {
return [((PKInkingTool *)[self tool]) width];
}
- (PKTool *)toolForZoomScale:(CGFloat)zoomScale {
return [[PKInkingTool alloc] initWithInkType:PKInkTypeMarker color:self.color width:[self adjustedLineWidthForZoomScale:zoomScale]];
}
// MARK: Zoom Handling
- (void)updateToolWithCurrentZoomScale:(CGFloat)currentZoomScale {
self.tool = [self toolForZoomScale:currentZoomScale];
}
- (CGFloat)adjustedLineWidthForZoomScale:(CGFloat)zoomScale {
return STANDARD_LINE_WIDTH * pow(zoomScale, -1.0);
}
// MARK: Touch Handling
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self.brushStrokeView touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
[self.brushStrokeView touchesMoved:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
[self.brushStrokeView touchesCancelled:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[self.brushStrokeView touchesEnded:touches withEvent:event];
}
- (PhotoEditingCanvasBrushStrokeView *)brushStrokeView {
return (PhotoEditingCanvasBrushStrokeView *)[self superview];
}
@end
// Created by Geoff Pado on 8/27/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
import PencilKit
class PhotoEditingCanvasView: PKCanvasView {
static let standardLineWidth = CGFloat(10)
var color = UIColor.black {
didSet {
updateTool()
}
}
var highlighterTool = HighlighterTool.magic {
didSet {
updateTool()
}
}
init() {
super.init(frame: .zero)
accessibilityIgnoresInvertColors = true
#if targetEnvironment(macCatalyst)
drawingPolicy = .anyInput
#else
allowsFingerDrawing = true
#endif
backgroundColor = .clear
isOpaque = false
overrideUserInterfaceStyle = .light
translatesAutoresizingMaskIntoConstraints = false
updateTool()
}
private func updateTool() {
switch highlighterTool {
case .magic, .manual:
tool = PKInkingTool(.marker, color: color, width: Self.standardLineWidth)
case .eraser:
tool = PKEraserTool(.bitmap)
case .lasso:
tool = PKLassoTool()
}
}
// MARK: Touch Handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
brushStrokeView?.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
brushStrokeView?.touchesMoved(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
brushStrokeView?.touchesCancelled(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
brushStrokeView?.touchesEnded(touches, with: event)
}
private var brushStrokeView: PhotoEditingCanvasBrushStrokeView? {
superview as? PhotoEditingCanvasBrushStrokeView
}
// MARK: Boilerplate
@available(*, unavailable)
required init(coder: NSCoder) {
let className = String(describing: type(of: self))
fatalError("\(className) does not implement init(coder:)")
}
}
class PhotoEditingCanvasBrushStrokeView: UIControl, PhotoEditingBrushStrokeView, PKCanvasViewDelegate {
init() {
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
canvasView.delegate = self
addSubview(canvasView)
NSLayoutConstraint.activate([
canvasView.widthAnchor.constraint(equalTo: widthAnchor),
canvasView.heightAnchor.constraint(equalTo: heightAnchor),
canvasView.centerXAnchor.constraint(equalTo: centerXAnchor),
canvasView.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
var color: UIColor {
get { canvasView.color }
set { canvasView.color = newValue }
}
var highlighterTool: HighlighterTool {
get { canvasView.highlighterTool }
set { canvasView.highlighterTool = newValue }
}
// MARK: PKCanvasViewDelegate
// func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
// guard canvasViewIsResetting == false else { return }
// if #available(iOS 14, *) {
// currentPath = path(from: canvasView.drawing)
// }
//
// sendActions(for: .touchUpInside)
//
// canvasViewIsResetting = true
// canvasView.drawing = PKDrawing()
// canvasViewIsResetting = false
// }
// MARK: Path Manipulation
func newPath() -> UIBezierPath {
let newPath = UIBezierPath()
newPath.lineCapStyle = .round
newPath.lineJoinStyle = .round
newPath.lineWidth = PhotoEditingCanvasView.standardLineWidth
return newPath
}
// func path(from drawing: PKDrawing) -> UIBezierPath {
// guard #available(iOS 14, *) else { return newPath() }
//
// let strokes = drawing.strokes
// let currentStroke = strokes.last
// let strokePath = currentStroke?.path
// let bezierPath = newPath()
//
// if let strokePath = strokePath {
// bezierPath.move(to: strokePath.interpolatedLocation(at: 0))
// for index in 1..<strokePath.count {
// bezierPath.addLine(to: strokePath.interpolatedLocation(at: CGFloat(index)))
// }
// }
//
// return bezierPath
// }
// MARK: Touch Handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// if #available(iOS 14, *) { return }
guard let touch = touches.first else { return }
currentPath = newPath()
currentPath?.move(to: touch.location(in: self))
}