|
|
@ -98,7 +98,8 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
|
|
|
|
CATransaction.setDisableActions(true)
|
|
|
|
CATransaction.setDisableActions(true)
|
|
|
|
|
|
|
|
|
|
|
|
for item in model.items() {
|
|
|
|
for item in model.items() {
|
|
|
|
guard let layer = layerForItem(item: item) else {
|
|
|
|
guard let layer = ImageEditorView.layerForItem(item: item,
|
|
|
|
|
|
|
|
viewSize: bounds.size) else {
|
|
|
|
Logger.error("Couldn't create layer for item.")
|
|
|
|
Logger.error("Couldn't create layer for item.")
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -109,7 +110,8 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
|
|
|
|
CATransaction.commit()
|
|
|
|
CATransaction.commit()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func layerForItem(item: ImageEditorItem) -> CALayer? {
|
|
|
|
private class func layerForItem(item: ImageEditorItem,
|
|
|
|
|
|
|
|
viewSize: CGSize) -> CALayer? {
|
|
|
|
AssertIsOnMainThread()
|
|
|
|
AssertIsOnMainThread()
|
|
|
|
|
|
|
|
|
|
|
|
switch item.itemType {
|
|
|
|
switch item.itemType {
|
|
|
@ -121,14 +123,14 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
|
|
|
|
owsFailDebug("Item has unexpected type: \(type(of: item)).")
|
|
|
|
owsFailDebug("Item has unexpected type: \(type(of: item)).")
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return strokeLayerForItem(item: strokeItem)
|
|
|
|
return strokeLayerForItem(item: strokeItem, viewSize: viewSize)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func strokeLayerForItem(item: ImageEditorStrokeItem) -> CALayer? {
|
|
|
|
private class func strokeLayerForItem(item: ImageEditorStrokeItem,
|
|
|
|
|
|
|
|
viewSize: CGSize) -> CALayer? {
|
|
|
|
AssertIsOnMainThread()
|
|
|
|
AssertIsOnMainThread()
|
|
|
|
|
|
|
|
|
|
|
|
let viewSize = bounds.size
|
|
|
|
|
|
|
|
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
|
|
|
@ -139,7 +141,7 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
|
|
|
|
let shapeLayer = CAShapeLayer()
|
|
|
|
let shapeLayer = CAShapeLayer()
|
|
|
|
shapeLayer.lineWidth = strokeWidth
|
|
|
|
shapeLayer.lineWidth = strokeWidth
|
|
|
|
shapeLayer.strokeColor = item.color.cgColor
|
|
|
|
shapeLayer.strokeColor = item.color.cgColor
|
|
|
|
shapeLayer.frame = self.bounds
|
|
|
|
shapeLayer.frame = CGRect(origin: .zero, size: viewSize)
|
|
|
|
|
|
|
|
|
|
|
|
let transformSampleToPoint = { (unitSample: CGPoint) -> CGPoint in
|
|
|
|
let transformSampleToPoint = { (unitSample: CGPoint) -> CGPoint in
|
|
|
|
return CGPoint(x: viewSize.width * unitSample.x,
|
|
|
|
return CGPoint(x: viewSize.width * unitSample.x,
|
|
|
@ -209,4 +211,59 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
|
|
|
|
|
|
|
|
|
|
|
|
return shapeLayer
|
|
|
|
return shapeLayer
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Actions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Returns nil on error.
|
|
|
|
|
|
|
|
@objc
|
|
|
|
|
|
|
|
public class func renderForOutput(model: ImageEditorModel) -> UIImage? {
|
|
|
|
|
|
|
|
// TODO: Do we want to render off the main thread?
|
|
|
|
|
|
|
|
AssertIsOnMainThread()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Render output at same size as source image.
|
|
|
|
|
|
|
|
let dstSizePixels = model.srcImageSizePixels
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let hasAlpha = NSData.hasAlpha(forValidImageFilePath: model.srcImagePath)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
guard let srcImage = UIImage(contentsOfFile: model.srcImagePath) else {
|
|
|
|
|
|
|
|
owsFailDebug("Could not load src image.")
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let dstScale: CGFloat = 1.0 // The size is specified in pixels, not in points.
|
|
|
|
|
|
|
|
UIGraphicsBeginImageContextWithOptions(dstSizePixels, !hasAlpha, dstScale)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
guard let context = UIGraphicsGetCurrentContext() else {
|
|
|
|
|
|
|
|
owsFailDebug("Could not create output context.")
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
context.interpolationQuality = .high
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Draw source image.
|
|
|
|
|
|
|
|
let dstFrame = CGRect(origin: .zero, size: model.srcImageSizePixels)
|
|
|
|
|
|
|
|
srcImage.draw(in: dstFrame)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for item in model.items() {
|
|
|
|
|
|
|
|
guard let layer = layerForItem(item: item,
|
|
|
|
|
|
|
|
viewSize: dstSizePixels) else {
|
|
|
|
|
|
|
|
Logger.error("Couldn't create layer for item.")
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// This might be superfluous, but ensure that the layer renders
|
|
|
|
|
|
|
|
// at "point=pixel" scale.
|
|
|
|
|
|
|
|
layer.contentsScale = 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO:
|
|
|
|
|
|
|
|
Logger.verbose("layer.contentsScale: \(layer.contentsScale)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
layer.render(in: context)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
|
|
|
|
|
|
if scaledImage == nil {
|
|
|
|
|
|
|
|
owsFailDebug("could not generate dst image.")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
UIGraphicsEndImageContext()
|
|
|
|
|
|
|
|
return scaledImage
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|