Skip to content

Commit

Permalink
Merge branch '255-fix-redaction-overlay-size' into 'release/22.7'
Browse files Browse the repository at this point in the history
Resolve "Fix redaction overlay size"

Closes #255

See merge request highlighter/app!225
  • Loading branch information
Arclite committed Aug 8, 2022
2 parents fda6588 + 3b92a93 commit 203028c
Show file tree
Hide file tree
Showing 29 changed files with 459 additions and 371 deletions.
18 changes: 13 additions & 5 deletions Automator/BrushStampFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ import os.log
import Redacting

class BrushStampFactory: NSObject {
static func brushStamp(scaledToHeight height: CGFloat, color: NSColor) -> NSImage {
guard let standardImage = Bundle(for: Self.self).image(forResource: "Brush") else { fatalError("Unable to load brush stamp image") }
static func brushStart(scaledToHeight height: CGFloat, color: NSColor) -> NSImage {
guard let standardImage = Bundle(for: Self.self).image(forResource: "Brush Start") else { fatalError("Unable to load brush start image") }
return scaledImage(from: standardImage, toHeight: height, color: color)
}

static func brushEnd(scaledToHeight height: CGFloat, color: NSColor) -> NSImage {
guard let standardImage = Bundle(for: Self.self).image(forResource: "Brush End") else { fatalError("Unable to load brush end image") }
return scaledImage(from: standardImage, toHeight: height, color: color)
}

let brushScale = height / standardImage.size.height
let scaledBrushSize = standardImage.size * brushScale
private static func scaledImage(from image: NSImage, toHeight height: CGFloat, color: NSColor) -> NSImage {
let brushScale = height / image.size.height
let scaledBrushSize = image.size * brushScale

return NSImage(size: scaledBrushSize, flipped: false) { drawRect -> Bool in
color.setFill()
Expand All @@ -20,7 +28,7 @@ class BrushStampFactory: NSObject {
guard let context = NSGraphicsContext.current?.cgContext else { return false }
context.scaleBy(x: brushScale, y: brushScale)

standardImage.draw(at: .zero, from: CGRect(origin: .zero, size: standardImage.size), operation: .destinationIn, fraction: 1)
image.draw(at: .zero, from: CGRect(origin: .zero, size: image.size), operation: .destinationIn, fraction: 1)

return true
}
Expand Down
30 changes: 16 additions & 14 deletions Automator/RedactActionExportOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,27 @@ class RedactActionExportOperation: Operation {
// draw redactions
let drawings = redactions.flatMap { redaction -> [(path: NSBezierPath, color: NSColor)] in
return redaction.paths
.map(\.dashedPath)
.map { (path: $0, color: redaction.color) }
}

drawings.forEach { drawing in
let (path, color) = drawing
let stampImage = BrushStampFactory.brushStamp(scaledToHeight: path.lineWidth, color: color)
path.forEachPoint { point in
context.saveGState()
defer { context.restoreGState() }

context.translateBy(x: stampImage.size.width * -0.5, y: stampImage.size.height * -0.5)

let drawContext = NSGraphicsContext(cgContext: context, flipped: false)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = drawContext
stampImage.draw(at: point, from: CGRect(origin: .zero, size: stampImage.size), operation: .sourceOver, fraction: 1)
NSGraphicsContext.restoreGraphicsState()
}
let borderBounds = path.strokeBorderPath.bounds
let startImage = BrushStampFactory.brushStart(scaledToHeight: borderBounds.height, color: color)
let endImage = BrushStampFactory.brushEnd(scaledToHeight: borderBounds.height, color: color)

color.setFill()
NSBezierPath(rect: borderBounds).fill()

let drawContext = NSGraphicsContext(cgContext: context, flipped: false)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = drawContext
let startRect = CGRect(origin: borderBounds.origin, size: startImage.size).offsetBy(dx: -startImage.size.width, dy: 0)
startImage.draw(in: startRect, from: CGRect(origin: .zero, size: startImage.size), operation: .sourceOver, fraction: 1)

let endRect = CGRect(origin: borderBounds.origin, size: endImage.size).offsetBy(dx: borderBounds.width, dy: 0)
endImage.draw(in: endRect, from: CGRect(origin: .zero, size: endImage.size), operation: .sourceOver, fraction: 1)
NSGraphicsContext.restoreGraphicsState()
}

// export image
Expand Down
42 changes: 38 additions & 4 deletions Editing/BrushStampFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,45 @@ import ErrorHandling
import UIKit

public class BrushStampFactory: NSObject {
public static func brushStart(scaledToHeight height: CGFloat, color: UIColor) -> UIImage {
guard let startImage = UIImage(named: "Brush Start") else { ErrorHandling.crash("Unable to load brush start image") }

let brushScale = height / startImage.size.height
let scaledBrushSize = startImage.size * brushScale

return UIGraphicsImageRenderer(size: scaledBrushSize).image { context in
color.setFill()
context.fill(CGRect(origin: .zero, size: scaledBrushSize))

let cgContext = context.cgContext
cgContext.scaleBy(x: brushScale, y: brushScale)

startImage.draw(at: .zero, blendMode: .destinationIn, alpha: 1)
}
}

public static func brushEnd(scaledToHeight height: CGFloat, color: UIColor) -> UIImage {
guard let endImage = UIImage(named: "Brush End") else { ErrorHandling.crash("Unable to load brush end image") }

let brushScale = height / endImage.size.height
let scaledBrushSize = endImage.size * brushScale

return UIGraphicsImageRenderer(size: scaledBrushSize).image { context in
color.setFill()
context.fill(CGRect(origin: .zero, size: scaledBrushSize))

let cgContext = context.cgContext
cgContext.scaleBy(x: brushScale, y: brushScale)

endImage.draw(at: .zero, blendMode: .destinationIn, alpha: 1)
}
}

public static func brushStamp(scaledToHeight height: CGFloat, color: UIColor) -> UIImage {
guard let standardImage = UIImage(named: "Brush") else { ErrorHandling.crash("Unable to load brush stamp image") }
guard let stampImage = UIImage(named: "Brush") else { ErrorHandling.crash("Unable to load brush stamp image") }

let brushScale = height / standardImage.size.height
let scaledBrushSize = standardImage.size * brushScale
let brushScale = height / stampImage.size.height
let scaledBrushSize = stampImage.size * brushScale

return UIGraphicsImageRenderer(size: scaledBrushSize).image { context in
color.setFill()
Expand All @@ -18,7 +52,7 @@ public class BrushStampFactory: NSObject {
let cgContext = context.cgContext
cgContext.scaleBy(x: brushScale, y: brushScale)

standardImage.draw(at: .zero, blendMode: .destinationIn, alpha: 1)
stampImage.draw(at: .zero, blendMode: .destinationIn, alpha: 1)
}
}
}
43 changes: 43 additions & 0 deletions Editing/Editing View/PhotoEditingObservationDebugView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Created by Geoff Pado on 7/8/22.
// Copyright © 2022 Cocoatype, LLC. All rights reserved.

import UIKit

class PhotoEditingObservationDebugView: PhotoEditingRedactionView {
override init() {
super.init()
isUserInteractionEnabled = false
}

// MARK: Text Observations

var textObservations: [TextRectangleObservation]? {
didSet {
updateDebugLayers()
setNeedsDisplay()
}
}

private func updateDebugLayers() {
layer.sublayers = debugLayers
}

private var debugLayers: [CALayer] {
guard FeatureFlag.shouldShowDebugOverlay, let textObservations = textObservations else { return [] }
return textObservations.flatMap { textObservation -> [CALayer] in
guard let characterObservations = textObservation.characterObservations else { return [] }
let characterLayers = characterObservations.map { observation -> CALayer in
let layer = CALayer()
layer.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.3).cgColor
layer.frame = observation.bounds
return layer
}

let textLayer = CALayer()
textLayer.backgroundColor = UIColor.systemRed.withAlphaComponent(0.3).cgColor
textLayer.frame = textObservation.bounds

return characterLayers + [textLayer]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class PhotoEditingObservationVisualizationView: PhotoEditingRedactionView {
didSet {
setNeedsDisplay()
animateFullVisualization()
addDebugLayers()
}
}

Expand All @@ -131,6 +132,10 @@ class PhotoEditingObservationVisualizationView: PhotoEditingRedactionView {
}
}

private func addDebugLayers() {

}

// MARK: Boilerplate

private var reduceMotionObserver: Any?
Expand Down
8 changes: 3 additions & 5 deletions Editing/Editing View/PhotoEditingViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,9 @@ public class PhotoEditingViewController: UIViewController, UIScrollViewDelegate,

public func exportImage(completionHandler: @escaping ((UIImage?) -> Void)) {
guard let image = photoEditingView.image else { return completionHandler(nil) }
PhotoExporter.export(image, redactions: photoEditingView.redactions) { result in
switch result {
case .success(let image): completionHandler(image)
case .failure: completionHandler(nil)
}
Task {
let image = await PhotoExporter.export(image, redactions: photoEditingView.redactions)
completionHandler(image)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ public class PhotoEditingRedactionView: UIView {
private func updateDisplay() {
layer.sublayers = redactions.flatMap { redaction -> [RedactionPathLayer] in
return redaction.paths
.map(\.dashedPath)
.map { RedactionPathLayer(path: $0, color: redaction.color)}
}
}
Expand Down
20 changes: 11 additions & 9 deletions Editing/Editing View/Workspace/PhotoEditingWorkspaceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,14 @@ import UIKit

class PhotoEditingWorkspaceView: UIControl, UIGestureRecognizerDelegate {
init() {
imageView = PhotoEditingImageView()
visualizationView = PhotoEditingObservationVisualizationView()
redactionView = PhotoEditingRedactionView()
brushStrokeView = PhotoEditingCanvasBrushStrokeView()

super.init(frame: .zero)
isAccessibilityElement = false
backgroundColor = .appBackground
translatesAutoresizingMaskIntoConstraints = false

addSubview(imageView)
addSubview(visualizationView)
addSubview(debugView)
addSubview(redactionView)
addSubview(brushStrokeView)

Expand All @@ -30,6 +26,10 @@ class PhotoEditingWorkspaceView: UIControl, UIGestureRecognizerDelegate {
visualizationView.centerYAnchor.constraint(equalTo: centerYAnchor),
visualizationView.widthAnchor.constraint(equalTo: widthAnchor),
visualizationView.heightAnchor.constraint(equalTo: heightAnchor),
debugView.centerXAnchor.constraint(equalTo: centerXAnchor),
debugView.centerYAnchor.constraint(equalTo: centerYAnchor),
debugView.widthAnchor.constraint(equalTo: widthAnchor),
debugView.heightAnchor.constraint(equalTo: heightAnchor),
redactionView.centerXAnchor.constraint(equalTo: centerXAnchor),
redactionView.centerYAnchor.constraint(equalTo: centerYAnchor),
redactionView.widthAnchor.constraint(equalTo: widthAnchor),
Expand Down Expand Up @@ -97,6 +97,7 @@ class PhotoEditingWorkspaceView: UIControl, UIGestureRecognizerDelegate {
get { return visualizationView.textObservations }
set(newTextObservations) {
visualizationView.textObservations = newTextObservations
debugView.textObservations = newTextObservations
}
}

Expand Down Expand Up @@ -198,10 +199,11 @@ class PhotoEditingWorkspaceView: UIControl, UIGestureRecognizerDelegate {

// MARK: Boilerplate

private let imageView: PhotoEditingImageView
private let visualizationView: PhotoEditingObservationVisualizationView
private let redactionView: PhotoEditingRedactionView
private let brushStrokeView: UIControl & PhotoEditingBrushStrokeView
private let imageView: PhotoEditingImageView = PhotoEditingImageView()
private let visualizationView = PhotoEditingObservationVisualizationView()
private let debugView = PhotoEditingObservationDebugView()
private let redactionView = PhotoEditingRedactionView()
private let brushStrokeView: (UIControl & PhotoEditingBrushStrokeView) = PhotoEditingCanvasBrushStrokeView()

@available(*, unavailable)
required init(coder: NSCoder) {
Expand Down
61 changes: 43 additions & 18 deletions Editing/Editing View/Workspace/RedactionPathLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@ import Foundation

class RedactionPathLayer: CALayer {
init(path: UIBezierPath, color: UIColor) {
let brushWidth = path.lineWidth
let brushStampImage = BrushStampFactory.brushStamp(scaledToHeight: brushWidth, color: color)
let pathBounds = path.strokeBorderPath.bounds.insetBy(dx: brushStampImage.size.width * -0.5, dy: 0)
path.apply(CGAffineTransform(translationX: -pathBounds.origin.x, y: -pathBounds.origin.y))
let borderBounds = path.strokeBorderPath.bounds
let startImage = BrushStampFactory.brushStart(scaledToHeight: borderBounds.height, color: color)
let endImage = BrushStampFactory.brushEnd(scaledToHeight: borderBounds.height, color: color)
let dikembeMutombo = BrushStampFactory.brushStamp(scaledToHeight: path.lineWidth, color: color)

let pathBounds: CGRect
if path.isRect {
pathBounds = borderBounds.inset(by: UIEdgeInsets(top: 0, left: startImage.size.width * -1, bottom: 0, right: endImage.size.width * -1))
} else {
pathBounds = borderBounds.inset(by: UIEdgeInsets(top: dikembeMutombo.size.height * -0.5,
left: dikembeMutombo.size.width * -0.5,
bottom: dikembeMutombo.size.height * -0.5,
right: dikembeMutombo.size.width * -0.5))
}

self.color = color
self.dikembeMutombo = dikembeMutombo
self.startImage = startImage
self.endImage = endImage
self.path = path
self.brushWidth = brushWidth
super.init()

backgroundColor = UIColor.clear.cgColor
drawsAsynchronously = true
frame = pathBounds
masksToBounds = false
Expand All @@ -24,36 +37,48 @@ class RedactionPathLayer: CALayer {

override init(layer: Any) {
let pathLayer = layer as? RedactionPathLayer
self.brushWidth = pathLayer?.brushWidth ?? 0
self.color = pathLayer?.color ?? .black
self.startImage = pathLayer?.startImage ?? UIImage()
self.endImage = pathLayer?.endImage ?? UIImage()
self.path = pathLayer?.path ?? UIBezierPath()
self.dikembeMutombo = pathLayer?.dikembeMutombo ?? UIImage()
super.init(layer: layer)
}

override func draw(in context: CGContext) {
let stampImage = BrushStampFactory.brushStamp(scaledToHeight: path.lineWidth, color: color)

UIGraphicsPushContext(context)
defer { UIGraphicsPopContext() }

path.forEachPoint { point in
context.saveGState()
defer { context.restoreGState() }
if path.isRect {
color.setFill()
UIBezierPath(rect: bounds.inset(by: UIEdgeInsets(top: 0, left: startImage.size.width, bottom: 0, right: endImage.size.width))).fill()

context.draw(startImage.cgImage!, in: CGRect(origin: .zero, size: startImage.size))
context.draw(endImage.cgImage!, in: CGRect(origin: CGPoint(x: bounds.maxX - endImage.size.width, y: bounds.minY), size: endImage.size))
} else {
let stampImage = BrushStampFactory.brushStamp(scaledToHeight: path.lineWidth, color: color)
let dashedPath = path.dashedPath
dashedPath.apply(CGAffineTransform(translationX: -path.bounds.origin.x, y: -path.bounds.origin.y))
dashedPath.forEachPoint { point in
context.saveGState()
defer { context.restoreGState() }

context.translateBy(x: stampImage.size.width * -0.5, y: stampImage.size.height * -0.5)
stampImage.draw(at: point)
context.translateBy(x: stampImage.size.width * 0.5, y: stampImage.size.height * 0.5)
stampImage.draw(at: point)
}
}
}

private func brushStamp(scaledToHeight height: CGFloat) -> UIImage {
BrushStampFactory.brushStamp(scaledToHeight: height, color: color)
}
// dikembeMutombo by @KaenAitch on 8/1/22
// the brush stamp image
private let dikembeMutombo: UIImage
private let startImage: UIImage
private let endImage: UIImage
private let path: UIBezierPath

// MARK: Boilerplate

private let brushWidth: CGFloat
private let color: UIColor
private let path: UIBezierPath

@available(*, unavailable)
required init(coder: NSCoder) {
Expand Down
7 changes: 7 additions & 0 deletions Editing/Export/PhotoExportError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Created by Geoff Pado on 7/18/22.
// Copyright © 2022 Cocoatype, LLC. All rights reserved.

enum PhotoExportError: Error {
case imageGenerationFailed
case operationReturnedNoResult
}
Loading

0 comments on commit 203028c

Please sign in to comment.