Merge branch 'charlesmchen/imageEditor3'

pull/1/head
Matthew Chen 6 years ago
commit a1dbb2f04e

@ -1080,6 +1080,12 @@
/* Title for the home view's default mode. */
"HOME_VIEW_TITLE_INBOX" = "Signal";
/* Label for brush button in image editor. */
"IMAGE_EDITOR_BRUSH_BUTTON" = "Brush";
/* Label for crop button in image editor. */
"IMAGE_EDITOR_CROP_BUTTON" = "Crop";
/* Call setup status label */
"IN_CALL_CONNECTING" = "Connecting…";

@ -947,13 +947,19 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
#if DEBUG
if let imageEditorModel = attachmentItem.imageEditorModel,
let imageMediaView = self.mediaMessageView.contentView {
let imageMediaView = mediaMessageView.contentView {
let imageEditorView = ImageEditorView(model: imageEditorModel)
imageMediaView.isUserInteractionEnabled = true
imageMediaView.addSubview(imageEditorView)
imageEditorView.autoPinEdgesToSuperviewEdges()
if imageEditorView.createImageView() {
mediaMessageView.isHidden = true
imageMediaView.isUserInteractionEnabled = true
mediaMessageView.superview?.addSubview(imageEditorView)
imageEditorView.autoPin(toEdgesOf: mediaMessageView)
imageEditorView.addRedBorder()
imageEditorView.addControls(to: self.mediaMessageView)
imageEditorView.addControls(to: imageEditorView)
}
}
#endif

@ -43,6 +43,12 @@ public class ImageEditorItem: NSObject {
super.init()
}
public typealias PointConversionFunction = (CGPoint) -> CGPoint
public func clone(withPointConversionFunction conversion: PointConversionFunction) -> ImageEditorItem {
return ImageEditorItem(itemId: itemId, itemType: itemType)
}
}
// MARK: -
@ -110,6 +116,17 @@ public class ImageEditorStrokeItem: ImageEditorItem {
dstSize: CGSize) -> CGFloat {
return CGFloatClamp01(unitStrokeWidth) * min(dstSize.width, dstSize.height)
}
public override func clone(withPointConversionFunction conversion: PointConversionFunction) -> ImageEditorItem {
// TODO: We might want to convert the unitStrokeWidth too.
let convertedUnitSamples = unitSamples.map { (sample) in
conversion(sample)
}
return ImageEditorStrokeItem(itemId: itemId,
color: color,
unitSamples: convertedUnitSamples,
unitStrokeWidth: unitStrokeWidth)
}
}
// MARK: -
@ -222,6 +239,12 @@ public class OrderedDictionary<ValueType>: NSObject {
// as immutable, once configured.
public class ImageEditorContents: NSObject {
@objc
public let imagePath: String
@objc
public let imageSizePixels: CGSize
public typealias ItemMapType = OrderedDictionary<ImageEditorItem>
// This represents the current state of each item,
@ -229,18 +252,27 @@ public class ImageEditorContents: NSObject {
var itemMap = ItemMapType()
// Used to create an initial, empty instances of this class.
public override init() {
public init(imagePath: String,
imageSizePixels: CGSize) {
self.imagePath = imagePath
self.imageSizePixels = imageSizePixels
}
// Used to clone copies of instances of this class.
public init(itemMap: ItemMapType) {
public init(imagePath: String,
imageSizePixels: CGSize,
itemMap: ItemMapType) {
self.imagePath = imagePath
self.imageSizePixels = imageSizePixels
self.itemMap = itemMap
}
// Since the contents are immutable, we only modify copies
// made with this method.
public func clone() -> ImageEditorContents {
return ImageEditorContents(itemMap: itemMap.clone())
return ImageEditorContents(imagePath: imagePath,
imageSizePixels: imageSizePixels,
itemMap: itemMap.clone())
}
@objc
@ -308,7 +340,13 @@ private class ImageEditorOperation: NSObject {
@objc
public protocol ImageEditorModelDelegate: class {
func imageEditorModelDidChange()
// Used for large changes to the model, when the entire
// model should be reloaded.
func imageEditorModelDidChange(before: ImageEditorContents,
after: ImageEditorContents)
// Used for small narrow changes to the model, usually
// to a single item.
func imageEditorModelDidChange(changedItemIds: [String])
}
@ -325,7 +363,7 @@ public class ImageEditorModel: NSObject {
@objc
public let srcImageSizePixels: CGSize
private var contents = ImageEditorContents()
private var contents: ImageEditorContents
private var undoStack = [ImageEditorOperation]()
private var redoStack = [ImageEditorOperation]()
@ -357,9 +395,17 @@ public class ImageEditorModel: NSObject {
}
self.srcImageSizePixels = srcImageSizePixels
self.contents = ImageEditorContents(imagePath: srcImagePath,
imageSizePixels: srcImageSizePixels)
super.init()
}
@objc
public var currentImagePath: String {
return contents.imagePath
}
@objc
public func itemCount() -> Int {
return contents.itemCount()
@ -395,10 +441,12 @@ public class ImageEditorModel: NSObject {
let redoOperation = ImageEditorOperation(contents: contents)
redoStack.append(redoOperation)
let oldContents = self.contents
self.contents = undoOperation.contents
// We could diff here and yield a more narrow change event.
delegate?.imageEditorModelDidChange()
delegate?.imageEditorModelDidChange(before: oldContents,
after: self.contents)
}
@objc
@ -411,37 +459,98 @@ public class ImageEditorModel: NSObject {
let undoOperation = ImageEditorOperation(contents: contents)
undoStack.append(undoOperation)
let oldContents = self.contents
self.contents = redoOperation.contents
// We could diff here and yield a more narrow change event.
delegate?.imageEditorModelDidChange()
delegate?.imageEditorModelDidChange(before: oldContents,
after: self.contents)
}
@objc
public func append(item: ImageEditorItem) {
performAction({ (newContents) in
performAction({ (oldContents) in
let newContents = oldContents.clone()
newContents.append(item: item)
return newContents
}, changedItemIds: [item.itemId])
}
@objc
public func replace(item: ImageEditorItem,
suppressUndo: Bool = false) {
performAction({ (newContents) in
performAction({ (oldContents) in
let newContents = oldContents.clone()
newContents.replace(item: item)
return newContents
}, changedItemIds: [item.itemId],
suppressUndo: suppressUndo)
}
@objc
public func remove(item: ImageEditorItem) {
performAction({ (newContents) in
performAction({ (oldContents) in
let newContents = oldContents.clone()
newContents.remove(item: item)
return newContents
}, changedItemIds: [item.itemId])
}
private func performAction(_ action: (ImageEditorContents) -> Void,
changedItemIds: [String],
@objc
public func crop(unitCropRect: CGRect) {
guard let croppedImage = ImageEditorModel.crop(imagePath: contents.imagePath,
unitCropRect: unitCropRect) else {
owsFailDebug("Could not crop image.")
return
}
// Use PNG for temp files; PNG is lossless.
guard let croppedImageData = UIImagePNGRepresentation(croppedImage) else {
owsFailDebug("Could not convert cropped image to PNG.")
return
}
let croppedImagePath = OWSFileSystem.temporaryFilePath(withFileExtension: "png")
do {
try croppedImageData.write(to: NSURL.fileURL(withPath: croppedImagePath), options: .atomicWrite)
} catch let error as NSError {
owsFailDebug("File write failed: \(error)")
return
}
let croppedImageSizePixels = CGSizeScale(croppedImage.size, croppedImage.scale)
let left = unitCropRect.origin.x
let right = unitCropRect.origin.x + unitCropRect.size.width
let top = unitCropRect.origin.y
let bottom = unitCropRect.origin.y + unitCropRect.size.height
let conversion: ImageEditorItem.PointConversionFunction = { (point) in
// Convert from the pre-crop unit coordinate system
// to post-crop unit coordinate system using inverse
// lerp.
//
// NOTE: Some post-conversion unit values will _NOT_
// be clamped. e.g. strokes outside the crop
// are that < 0 or > 1. This is fine.
// We could hypothethically discard any items
// whose bounding box is entirely outside the
// new unit rectangle (e.g. have been completely
// cropped) but it doesn't seem worthwhile.
let converted = CGPoint(x: CGFloatInverseLerp(point.x, left, right),
y: CGFloatInverseLerp(point.y, top, bottom))
return converted
}
performAction({ (oldContents) in
let newContents = ImageEditorContents(imagePath: croppedImagePath,
imageSizePixels: croppedImageSizePixels)
for oldItem in oldContents.items() {
let newItem = oldItem.clone(withPointConversionFunction: conversion)
newContents.append(item: newItem)
}
return newContents
}, changedItemIds: nil)
}
private func performAction(_ action: (ImageEditorContents) -> ImageEditorContents,
changedItemIds: [String]?,
suppressUndo: Bool = false) {
if !suppressUndo {
let undoOperation = ImageEditorOperation(contents: contents)
@ -449,10 +558,69 @@ public class ImageEditorModel: NSObject {
redoStack.removeAll()
}
let newContents = contents.clone()
action(newContents)
let oldContents = self.contents
let newContents = action(oldContents)
contents = newContents
delegate?.imageEditorModelDidChange(changedItemIds: changedItemIds)
if let changedItemIds = changedItemIds {
delegate?.imageEditorModelDidChange(changedItemIds: changedItemIds)
} else {
delegate?.imageEditorModelDidChange(before: oldContents,
after: self.contents)
}
}
// MARK: - Utilities
// Returns nil on error.
private class func crop(imagePath: String,
unitCropRect: CGRect) -> UIImage? {
// TODO: Do we want to render off the main thread?
AssertIsOnMainThread()
guard let srcImage = UIImage(contentsOfFile: imagePath) else {
owsFailDebug("Could not load image")
return nil
}
let srcImageSize = srcImage.size
// Convert from unit coordinates to src image coordinates.
let cropRect = CGRect(x: unitCropRect.origin.x * srcImageSize.width,
y: unitCropRect.origin.y * srcImageSize.height,
width: unitCropRect.size.width * srcImageSize.width,
height: unitCropRect.size.height * srcImageSize.height)
guard cropRect.origin.x >= 0,
cropRect.origin.y >= 0,
cropRect.origin.x + cropRect.size.width <= srcImageSize.width,
cropRect.origin.y + cropRect.size.height <= srcImageSize.height else {
owsFailDebug("Invalid crop rectangle.")
return nil
}
guard cropRect.size.width > 0,
cropRect.size.height > 0 else {
owsFailDebug("Empty crop rectangle.")
return nil
}
let hasAlpha = NSData.hasAlpha(forValidImageFilePath: imagePath)
UIGraphicsBeginImageContextWithOptions(cropRect.size, !hasAlpha, srcImage.scale)
defer { UIGraphicsEndImageContext() }
guard let context = UIGraphicsGetCurrentContext() else {
owsFailDebug("Could not create output context.")
return nil
}
context.interpolationQuality = .high
// Draw source image.
let dstFrame = CGRect(origin: CGPointInvert(cropRect.origin), size: srcImageSize)
srcImage.draw(in: dstFrame)
let dstImage = UIGraphicsGetImageFromCurrentImageContext()
if dstImage == nil {
owsFailDebug("could not generate dst image.")
}
return dstImage
}
}

@ -10,6 +10,13 @@ import UIKit
public class ImageEditorView: UIView, ImageEditorModelDelegate {
private let model: ImageEditorModel
enum EditorMode: String {
case brush
case crop
}
private var editorMode = EditorMode.brush
@objc
public required init(model: ImageEditorModel) {
self.model = model
@ -17,11 +24,6 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
super.init(frame: .zero)
model.delegate = self
self.isUserInteractionEnabled = true
let anyTouchGesture = ImageEditorGestureRecognizer(target: self, action: #selector(handleTouchGesture(_:)))
self.addGestureRecognizer(anyTouchGesture)
}
@available(*, unavailable, message: "use other init() instead.")
@ -29,10 +31,78 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
notImplemented()
}
// MARK: - Buttons
// MARK: - Views
private let imageView = UIImageView()
private var imageViewConstraints = [NSLayoutConstraint]()
private let layersView = OWSLayerView()
@objc
public func createImageView() -> Bool {
self.addSubview(imageView)
guard updateImageView() else {
return false
}
layersView.clipsToBounds = true
layersView.layoutCallback = { [weak self] (_) in
self?.updateAllContent()
}
self.addSubview(layersView)
layersView.autoPin(toEdgesOf: imageView)
self.isUserInteractionEnabled = true
layersView.isUserInteractionEnabled = true
let anyTouchGesture = ImageEditorGestureRecognizer(target: self, action: #selector(handleTouchGesture(_:)))
layersView.addGestureRecognizer(anyTouchGesture)
return true
}
@objc
public func updateImageView() -> Bool {
Logger.verbose("")
guard let image = UIImage(contentsOfFile: model.currentImagePath) else {
owsFailDebug("Could not load image")
return false
}
guard image.size.width > 0 && image.size.height > 0 else {
owsFailDebug("Could not load image")
return false
}
imageView.image = image
imageView.layer.minificationFilter = kCAFilterTrilinear
imageView.layer.magnificationFilter = kCAFilterTrilinear
let aspectRatio = image.size.width / image.size.height
for constraint in imageViewConstraints {
constraint.autoRemove()
}
imageViewConstraints = applyScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio)
return true
}
private func applyScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) -> [NSLayoutConstraint] {
// This emulates the behavior of contentMode = .scaleAspectFit using
// iOS auto layout constraints.
//
// This allows ConversationInputToolbar to place the "cancel" button
// in the upper-right hand corner of the preview content.
var constraints = [NSLayoutConstraint]()
constraints.append(contentsOf: view.autoCenterInSuperview())
constraints.append(view.autoPin(toAspectRatio: aspectRatio))
constraints.append(view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual))
constraints.append(view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual))
return constraints
}
private let undoButton = UIButton(type: .custom)
private let redoButton = UIButton(type: .custom)
private let brushButton = UIButton(type: .custom)
private let cropButton = UIButton(type: .custom)
@objc
public func addControls(to containerView: UIView) {
@ -44,7 +114,15 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
label: NSLocalizedString("BUTTON_REDO", comment: "Label for redo button."),
selector: #selector(didTapRedo(sender:)))
let stackView = UIStackView(arrangedSubviews: [undoButton, redoButton])
configure(button: brushButton,
label: NSLocalizedString("IMAGE_EDITOR_BRUSH_BUTTON", comment: "Label for brush button in image editor."),
selector: #selector(didTapBrush(sender:)))
configure(button: cropButton,
label: NSLocalizedString("IMAGE_EDITOR_CROP_BUTTON", comment: "Label for crop button in image editor."),
selector: #selector(didTapCrop(sender:)))
let stackView = UIStackView(arrangedSubviews: [brushButton, cropButton, undoButton, redoButton])
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = 10
@ -60,10 +138,9 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
label: String,
selector: Selector) {
button.setTitle(label, for: .normal)
button.setTitleColor(.white,
for: .normal)
button.setTitleColor(.gray,
for: .disabled)
button.setTitleColor(.white, for: .normal)
button.setTitleColor(.gray, for: .disabled)
button.setTitleColor(UIColor.ows_materialBlue, for: .selected)
button.titleLabel?.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight()
button.addTarget(self, action: selector, for: .touchUpInside)
}
@ -71,6 +148,10 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
private func updateButtons() {
undoButton.isEnabled = model.canUndo()
redoButton.isEnabled = model.canRedo()
// brushButton.isSelected = editorMode == .brush
brushButton.isEnabled = editorMode != .brush
// cropButton.isSelected = editorMode == .crop
cropButton.isEnabled = editorMode != .crop
}
// MARK: - Actions
@ -93,12 +174,40 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
model.redo()
}
@objc func didTapBrush(sender: UIButton) {
Logger.verbose("")
editorMode = .brush
updateButtons()
}
@objc func didTapCrop(sender: UIButton) {
Logger.verbose("")
editorMode = .crop
updateButtons()
}
@objc
public func handleTouchGesture(_ gestureRecognizer: UIGestureRecognizer) {
AssertIsOnMainThread()
switch editorMode {
case .brush:
handleBrushGesture(gestureRecognizer)
case .crop:
handleCropGesture(gestureRecognizer)
}
}
// MARK: - Brush
// These properties are non-empty while drawing a stroke.
private var currentStroke: ImageEditorStrokeItem?
private var currentStrokeSamples = [ImageEditorStrokeItem.StrokeSample]()
@objc
public func handleTouchGesture(_ gestureRecognizer: UIGestureRecognizer) {
public func handleBrushGesture(_ gestureRecognizer: UIGestureRecognizer) {
AssertIsOnMainThread()
let removeCurrentStroke = {
@ -109,15 +218,6 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
self.currentStrokeSamples.removeAll()
}
let referenceView = self
let unitSampleForGestureLocation = { () -> CGPoint in
// TODO: Smooth touch samples before converting into stroke samples.
let location = gestureRecognizer.location(in: referenceView)
let x = CGFloatClamp01(CGFloatInverseLerp(location.x, 0, referenceView.bounds.width))
let y = CGFloatClamp01(CGFloatInverseLerp(location.y, 0, referenceView.bounds.height))
return CGPoint(x: x, y: y)
}
// TODO: Color picker.
let strokeColor = UIColor.blue
// TODO: Tune stroke width.
@ -127,14 +227,14 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
case .began:
removeCurrentStroke()
currentStrokeSamples.append(unitSampleForGestureLocation())
currentStrokeSamples.append(unitSampleForGestureLocation(gestureRecognizer))
let stroke = ImageEditorStrokeItem(color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth)
model.append(item: stroke)
currentStroke = stroke
case .changed, .ended:
currentStrokeSamples.append(unitSampleForGestureLocation())
currentStrokeSamples.append(unitSampleForGestureLocation(gestureRecognizer))
guard let lastStroke = self.currentStroke else {
owsFailDebug("Missing last stroke.")
@ -158,9 +258,125 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
}
}
private func unitSampleForGestureLocation(_ gestureRecognizer: UIGestureRecognizer) -> CGPoint {
let referenceView = layersView
// TODO: Smooth touch samples before converting into stroke samples.
let location = gestureRecognizer.location(in: referenceView)
let x = CGFloatClamp01(CGFloatInverseLerp(location.x, 0, referenceView.bounds.width))
let y = CGFloatClamp01(CGFloatInverseLerp(location.y, 0, referenceView.bounds.height))
return CGPoint(x: x, y: y)
}
// MARK: - Crop
private var cropStartUnit = CGPoint.zero
private var cropEndUnit = CGPoint.zero
private var cropLayer1 = CAShapeLayer()
private var cropLayer2 = CAShapeLayer()
private var cropLayers: [CAShapeLayer] {
return [cropLayer1, cropLayer2]
}
@objc
public func handleCropGesture(_ gestureRecognizer: UIGestureRecognizer) {
AssertIsOnMainThread()
let kCropDashLength: CGFloat = 3
let cancelCrop = {
for cropLayer in self.cropLayers {
cropLayer.removeFromSuperlayer()
cropLayer.removeAllAnimations()
}
}
let updateCropLayer = { (cropLayer: CAShapeLayer) in
cropLayer.fillColor = nil
cropLayer.lineWidth = 1.0
cropLayer.lineDashPattern = [NSNumber(value: Double(kCropDashLength)), NSNumber(value: Double(kCropDashLength))]
let viewSize = self.layersView.bounds.size
cropLayer.frame = CGRect(origin: .zero, size: viewSize)
// Find the upper-left and bottom-right corners of the
// crop rectangle, in unit coordinates.
let unitMin = CGPointMin(self.cropStartUnit, self.cropEndUnit)
let unitMax = CGPointMax(self.cropStartUnit, self.cropEndUnit)
let transformSampleToPoint = { (unitSample: CGPoint) -> CGPoint in
return CGPoint(x: viewSize.width * unitSample.x,
y: viewSize.height * unitSample.y)
}
// Convert from unit coordinates to view coordinates.
let pointMin = transformSampleToPoint(unitMin)
let pointMax = transformSampleToPoint(unitMax)
let cropRect = CGRect(x: pointMin.x,
y: pointMin.y,
width: pointMax.x - pointMin.x,
height: pointMax.y - pointMin.y)
let bezierPath = UIBezierPath(rect: cropRect)
cropLayer.path = bezierPath.cgPath
}
let updateCrop = {
updateCropLayer(self.cropLayer1)
updateCropLayer(self.cropLayer2)
self.cropLayer1.strokeColor = UIColor.white.cgColor
self.cropLayer2.strokeColor = UIColor.black.cgColor
self.cropLayer1.lineDashPhase = 0
self.cropLayer2.lineDashPhase = self.cropLayer1.lineDashPhase + kCropDashLength
}
let startCrop = {
for cropLayer in self.cropLayers {
self.layersView.layer.addSublayer(cropLayer)
}
updateCrop()
}
let endCrop = {
updateCrop()
for cropLayer in self.cropLayers {
cropLayer.removeFromSuperlayer()
cropLayer.removeAllAnimations()
}
// Find the upper-left and bottom-right corners of the
// crop rectangle, in unit coordinates.
let unitMin = CGPointClamp01(CGPointMin(self.cropStartUnit, self.cropEndUnit))
let unitMax = CGPointClamp01(CGPointMax(self.cropStartUnit, self.cropEndUnit))
let unitCropRect = CGRect(x: unitMin.x,
y: unitMin.y,
width: unitMax.x - unitMin.x,
height: unitMax.y - unitMin.y)
self.model.crop(unitCropRect: unitCropRect)
}
switch gestureRecognizer.state {
case .began:
let unitSample = unitSampleForGestureLocation(gestureRecognizer)
cropStartUnit = unitSample
cropEndUnit = unitSample
startCrop()
case .changed:
cropEndUnit = unitSampleForGestureLocation(gestureRecognizer)
updateCrop()
case .ended:
cropEndUnit = unitSampleForGestureLocation(gestureRecognizer)
endCrop()
default:
cancelCrop()
}
}
// MARK: - ImageEditorModelDelegate
public func imageEditorModelDidChange() {
public func imageEditorModelDidChange(before: ImageEditorContents,
after: ImageEditorContents) {
if before.imagePath != after.imagePath {
_ = updateImageView()
}
updateAllContent()
updateButtons()
@ -210,12 +426,13 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
bounds.height > 0 {
for item in model.items() {
let viewSize = layersView.bounds.size
guard let layer = ImageEditorView.layerForItem(item: item,
viewSize: bounds.size) else {
viewSize: viewSize) else {
continue
}
self.layer.addSublayer(layer)
layersView.layer.addSublayer(layer)
contentLayerMap[item.itemId] = layer
}
}
@ -249,12 +466,13 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
}
// Item was inserted or updated.
let viewSize = layersView.bounds.size
guard let layer = ImageEditorView.layerForItem(item: item,
viewSize: bounds.size) else {
viewSize: viewSize) else {
continue
}
self.layer.addSublayer(layer)
layersView.layer.addSublayer(layer)
contentLayerMap[item.itemId] = layer
}
}
@ -283,7 +501,7 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
viewSize: CGSize) -> CALayer? {
AssertIsOnMainThread()
Logger.verbose("\(item.itemId)")
Logger.verbose("\(item.itemId), viewSize: \(viewSize)")
let strokeWidth = ImageEditorStrokeItem.strokeWidth(forUnitStrokeWidth: item.unitStrokeWidth,
dstSize: viewSize)
@ -359,6 +577,7 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
shapeLayer.path = bezierPath.cgPath
shapeLayer.fillColor = nil
shapeLayer.lineCap = kCALineCapRound
shapeLayer.lineJoin = kCALineJoinRound
return shapeLayer
}
@ -406,9 +625,9 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
// Render output at same size as source image.
let dstSizePixels = model.srcImageSizePixels
let hasAlpha = NSData.hasAlpha(forValidImageFilePath: model.srcImagePath)
let hasAlpha = NSData.hasAlpha(forValidImageFilePath: model.currentImagePath)
guard let srcImage = UIImage(contentsOfFile: model.srcImagePath) else {
guard let srcImage = UIImage(contentsOfFile: model.currentImagePath) else {
owsFailDebug("Could not load src image.")
return nil
}

@ -6,10 +6,17 @@ import Foundation
@objc
public class OWSLayerView: UIView {
let layoutCallback: ((UIView) -> Void)
public var layoutCallback: ((UIView) -> Void)
@objc
public required init(frame: CGRect, layoutCallback : @escaping (UIView) -> Void) {
public init() {
self.layoutCallback = { (_) in
}
super.init(frame: .zero)
}
@objc
public init(frame: CGRect, layoutCallback : @escaping (UIView) -> Void) {
self.layoutCallback = layoutCallback
super.init(frame: frame)
}

@ -2,6 +2,7 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSMath.h"
#import <PureLayout/PureLayout.h>
#import <UIKit/UIKit.h>
@ -153,57 +154,6 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value);
#pragma mark - Macros
CG_INLINE CGSize CGSizeCeil(CGSize size)
{
return CGSizeMake((CGFloat)ceil(size.width), (CGFloat)ceil(size.height));
}
CG_INLINE CGSize CGSizeFloor(CGSize size)
{
return CGSizeMake((CGFloat)floor(size.width), (CGFloat)floor(size.height));
}
CG_INLINE CGSize CGSizeRound(CGSize size)
{
return CGSizeMake((CGFloat)round(size.width), (CGFloat)round(size.height));
}
CG_INLINE CGSize CGSizeMax(CGSize size1, CGSize size2)
{
return CGSizeMake(MAX(size1.width, size2.width), MAX(size1.height, size2.height));
}
CG_INLINE CGPoint CGPointAdd(CGPoint left, CGPoint right)
{
return CGPointMake(left.x + right.x, left.y + right.y);
}
CG_INLINE CGPoint CGPointSubtract(CGPoint left, CGPoint right)
{
return CGPointMake(left.x - right.x, left.y - right.y);
}
CG_INLINE CGPoint CGPointScale(CGPoint point, CGFloat factor)
{
return CGPointMake(point.x * factor, point.y * factor);
}
CG_INLINE CGFloat CGPointDistance(CGPoint left, CGPoint right)
{
CGPoint delta = CGPointSubtract(left, right);
return sqrt(delta.x * delta.x + delta.y * delta.y);
}
CG_INLINE CGSize CGSizeScale(CGSize size, CGFloat factor)
{
return CGSizeMake(size.width * factor, size.height * factor);
}
CG_INLINE CGSize CGSizeAdd(CGSize left, CGSize right)
{
return CGSizeMake(left.width + right.width, left.height + right.height);
}
CGFloat CGHairlineWidth(void);
NS_ASSUME_NONNULL_END

@ -4,33 +4,111 @@
NS_ASSUME_NONNULL_BEGIN
// TODO: We'll eventually want to promote these into an OWSMath.h header.
static inline CGFloat CGFloatClamp(CGFloat value, CGFloat minValue, CGFloat maxValue)
CG_INLINE CGFloat CGFloatClamp(CGFloat value, CGFloat minValue, CGFloat maxValue)
{
return MAX(minValue, MIN(maxValue, value));
}
static inline CGFloat CGFloatClamp01(CGFloat value)
CG_INLINE CGFloat CGFloatClamp01(CGFloat value)
{
return CGFloatClamp(value, 0.f, 1.f);
}
static inline CGFloat CGFloatLerp(CGFloat left, CGFloat right, CGFloat alpha)
CG_INLINE CGFloat CGFloatLerp(CGFloat left, CGFloat right, CGFloat alpha)
{
alpha = CGFloatClamp01(alpha);
return (left * (1.f - alpha)) + (right * alpha);
}
static inline CGFloat CGFloatInverseLerp(CGFloat value, CGFloat minValue, CGFloat maxValue)
CG_INLINE CGFloat CGFloatInverseLerp(CGFloat value, CGFloat minValue, CGFloat maxValue)
{
return (value - minValue) / (maxValue - minValue);
}
// Ceil to an even number
static inline CGFloat CeilEven(CGFloat value)
CG_INLINE CGFloat CeilEven(CGFloat value)
{
return 2.f * (CGFloat)ceil(value * 0.5f);
}
CG_INLINE CGSize CGSizeCeil(CGSize size)
{
return CGSizeMake((CGFloat)ceil(size.width), (CGFloat)ceil(size.height));
}
CG_INLINE CGSize CGSizeFloor(CGSize size)
{
return CGSizeMake((CGFloat)floor(size.width), (CGFloat)floor(size.height));
}
CG_INLINE CGSize CGSizeRound(CGSize size)
{
return CGSizeMake((CGFloat)round(size.width), (CGFloat)round(size.height));
}
CG_INLINE CGSize CGSizeMax(CGSize size1, CGSize size2)
{
return CGSizeMake(MAX(size1.width, size2.width), MAX(size1.height, size2.height));
}
CG_INLINE CGPoint CGPointAdd(CGPoint left, CGPoint right)
{
return CGPointMake(left.x + right.x, left.y + right.y);
}
CG_INLINE CGPoint CGPointSubtract(CGPoint left, CGPoint right)
{
return CGPointMake(left.x - right.x, left.y - right.y);
}
CG_INLINE CGPoint CGPointScale(CGPoint point, CGFloat factor)
{
return CGPointMake(point.x * factor, point.y * factor);
}
CG_INLINE CGFloat CGPointDistance(CGPoint left, CGPoint right)
{
CGPoint delta = CGPointSubtract(left, right);
return sqrt(delta.x * delta.x + delta.y * delta.y);
}
CG_INLINE CGPoint CGPointMin(CGPoint left, CGPoint right)
{
return CGPointMake(MIN(left.x, right.x), MIN(left.y, right.y));
}
CG_INLINE CGPoint CGPointMax(CGPoint left, CGPoint right)
{
return CGPointMake(MAX(left.x, right.x), MAX(left.y, right.y));
}
CG_INLINE CGPoint CGPointClamp01(CGPoint point)
{
return CGPointMake(CGFloatClamp01(point.x), CGFloatClamp01(point.y));
}
CG_INLINE CGPoint CGPointInvert(CGPoint point)
{
return CGPointMake(-point.x, -point.y);
}
CG_INLINE CGSize CGSizeScale(CGSize size, CGFloat factor)
{
return CGSizeMake(size.width * factor, size.height * factor);
}
CG_INLINE CGSize CGSizeAdd(CGSize left, CGSize right)
{
return CGSizeMake(left.width + right.width, left.height + right.height);
}
CG_INLINE CGRect CGRectScale(CGRect rect, CGFloat factor)
{
CGRect result;
result.origin = CGPointScale(rect.origin, factor);
result.size = CGSizeScale(rect.size, factor);
return result;
}
NS_ASSUME_NONNULL_END

Loading…
Cancel
Save