Replace edited attachments when sending.

pull/1/head
Matthew Chen 6 years ago
parent 2f7e99de46
commit b0e0c6e8c2

@ -581,7 +581,63 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}
var attachments: [SignalAttachment] {
return attachmentItems.map { $0.attachment }
return attachmentItems.map { self.attachment(forAttachmentItem: $0) }
}
// For any attachments edited with the image editor, returns a
// new SignalAttachment that reflects those changes. Otherwise,
// returns the original attachment.
//
// If any errors occurs in the export process, we fail over to
// sending the original attachment. This seems better than trying
// to involve the user in resolving the issue.
func attachment(forAttachmentItem attachmentItem: SignalAttachmentItem) -> SignalAttachment {
guard let imageEditorModel = attachmentItem.imageEditorModel else {
// Image was not edited.
return attachmentItem.attachment
}
guard imageEditorModel.itemCount() > 0 else {
// Image editor has no changes.
return attachmentItem.attachment
}
guard let dstImage = ImageEditorView.renderForOutput(model: imageEditorModel) else {
owsFailDebug("Could not render for output.")
return attachmentItem.attachment
}
var dataUTI = kUTTypeImage as String
guard let dstData: Data = {
let isLossy: Bool = attachmentItem.attachment.mimeType.caseInsensitiveCompare(OWSMimeTypeImageJpeg) == .orderedSame
if isLossy {
dataUTI = kUTTypeJPEG as String
return UIImageJPEGRepresentation(dstImage, 0.9)
} else {
dataUTI = kUTTypePNG as String
return UIImagePNGRepresentation(dstImage)
}
}() else {
owsFailDebug("Could not export for output.")
return attachmentItem.attachment
}
guard let dataSource = DataSourceValue.dataSource(with: dstData, utiType: dataUTI) else {
owsFailDebug("Could not prepare data source for output.")
return attachmentItem.attachment
}
// Rewrite the filename's extension to reflect the output file format.
var filename: String? = attachmentItem.attachment.sourceFilename
if let sourceFilename = attachmentItem.attachment.sourceFilename {
if let fileExtension: String = MIMETypeUtil.fileExtension(forUTIType: dataUTI) {
filename = (sourceFilename as NSString).deletingPathExtension.appendingFileExtension(fileExtension)
}
}
dataSource.sourceFilename = filename
let dstAttachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .original)
if let attachmentError = dstAttachment.error {
owsFailDebug("Could not prepare attachment for output: \(attachmentError).")
return attachmentItem.attachment
}
return dstAttachment
}
func attachmentItem(before currentItem: SignalAttachmentItem) -> SignalAttachmentItem? {

@ -246,7 +246,7 @@ public class ImageEditorModel: NSObject {
public let srcImagePath: String
@objc
public let srcImageSize: CGSize
public let srcImageSizePixels: CGSize
private var contents = ImageEditorContents()
@ -273,12 +273,12 @@ public class ImageEditorModel: NSObject {
throw ImageEditorError.invalidInput
}
let srcImageSize = NSData.imageSize(forFilePath: srcImagePath, mimeType: mimeType)
guard srcImageSize.width > 0, srcImageSize.height > 0 else {
let srcImageSizePixels = NSData.imageSize(forFilePath: srcImagePath, mimeType: mimeType)
guard srcImageSizePixels.width > 0, srcImageSizePixels.height > 0 else {
Logger.error("Couldn't determine image size.")
throw ImageEditorError.invalidInput
}
self.srcImageSize = srcImageSize
self.srcImageSizePixels = srcImageSizePixels
super.init()
}

@ -98,7 +98,8 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
CATransaction.setDisableActions(true)
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.")
continue
}
@ -109,7 +110,8 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
CATransaction.commit()
}
private func layerForItem(item: ImageEditorItem) -> CALayer? {
private class func layerForItem(item: ImageEditorItem,
viewSize: CGSize) -> CALayer? {
AssertIsOnMainThread()
switch item.itemType {
@ -121,14 +123,14 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
owsFailDebug("Item has unexpected type: \(type(of: item)).")
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()
let viewSize = bounds.size
let strokeWidth = ImageEditorStrokeItem.strokeWidth(forUnitStrokeWidth: item.unitStrokeWidth,
dstSize: viewSize)
let unitSamples = item.unitSamples
@ -139,7 +141,7 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = strokeWidth
shapeLayer.strokeColor = item.color.cgColor
shapeLayer.frame = self.bounds
shapeLayer.frame = CGRect(origin: .zero, size: viewSize)
let transformSampleToPoint = { (unitSample: CGPoint) -> CGPoint in
return CGPoint(x: viewSize.width * unitSample.x,
@ -209,4 +211,59 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate {
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
}
}

@ -11,7 +11,11 @@
- (BOOL)ows_isValidImage;
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType;
// Returns the image size in pixels.
//
// Returns CGSizeZero on error.
+ (CGSize)imageSizeForFilePath:(NSString *)filePath mimeType:(NSString *)mimeType;
+ (BOOL)hasAlphaForValidImageFilePath:(NSString *)filePath;
@end

@ -2,8 +2,8 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "MIMETypeUtil.h"
#import "NSData+Image.h"
#import "MIMETypeUtil.h"
#import "OWSFileSystem.h"
#import <AVFoundation/AVFoundation.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
@ -370,4 +370,33 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
}
}
+ (BOOL)hasAlphaForValidImageFilePath:(NSString *)filePath
{
NSURL *url = [NSURL fileURLWithPath:filePath];
// With CGImageSource we avoid loading the whole image into memory.
CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, NULL);
if (!source) {
OWSFailDebug(@"Could not load image: %@", url);
return NO;
}
NSDictionary *options = @{
(NSString *)kCGImageSourceShouldCache : @(NO),
};
NSDictionary *properties
= (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, (CFDictionaryRef)options);
BOOL result = NO;
if (properties) {
NSNumber *hasAlpha = properties[(NSString *)kCGImagePropertyHasAlpha];
if (hasAlpha) {
result = hasAlpha.boolValue;
} else {
OWSFailDebug(@"Could not determine transparency of image: %@", url);
}
}
CFRelease(source);
return result;
}
@end

Loading…
Cancel
Save