Add OWSThumbnailService.

pull/1/head
Matthew Chen 6 years ago
parent 1831f0b1f8
commit ac4365e1c9

@ -148,7 +148,6 @@
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; };
347850551FD749C0007B8332 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
347850571FD86544007B8332 /* SAEFailedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850561FD86544007B8332 /* SAEFailedViewController.swift */; };
347850591FD9972E007B8332 /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850581FD9972E007B8332 /* SwiftSingletons.swift */; };
347850691FD9B78A007B8332 /* AppSetup.m in Sources */ = {isa = PBXBuildFile; fileRef = 347850651FD9B789007B8332 /* AppSetup.m */; };
3478506A1FD9B78A007B8332 /* AppSetup.h in Headers */ = {isa = PBXBuildFile; fileRef = 347850661FD9B789007B8332 /* AppSetup.h */; settings = {ATTRIBUTES = (Public, ); }; };
3478506B1FD9B78A007B8332 /* NoopCallMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850671FD9B78A007B8332 /* NoopCallMessageHandler.swift */; };
@ -783,7 +782,6 @@
34661FB720C1C0D60056EDD6 /* message_sent.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; name = message_sent.aiff; path = Signal/AudioFiles/message_sent.aiff; sourceTree = SOURCE_ROOT; };
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = "<group>"; };
347850561FD86544007B8332 /* SAEFailedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEFailedViewController.swift; sourceTree = "<group>"; };
347850581FD9972E007B8332 /* SwiftSingletons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSingletons.swift; sourceTree = "<group>"; };
3478505A1FD999D5007B8332 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = translations/et.lproj/Localizable.strings; sourceTree = "<group>"; };
3478505C1FD99A1F007B8332 /* zh_TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_TW; path = translations/zh_TW.lproj/Localizable.strings; sourceTree = "<group>"; };
347850651FD9B789007B8332 /* AppSetup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppSetup.m; sourceTree = "<group>"; };
@ -1559,7 +1557,6 @@
34641E1120878FB000E2EDE5 /* OWSWindowManager.h */,
34641E1020878FAF00E2EDE5 /* OWSWindowManager.m */,
45360B8C1F9521F800FA666C /* Searcher.swift */,
347850581FD9972E007B8332 /* SwiftSingletons.swift */,
346129BD1FD2068600532771 /* ThreadUtil.h */,
346129BE1FD2068600532771 /* ThreadUtil.m */,
B97940251832BD2400BD66CB /* UIUtil.h */,
@ -3208,7 +3205,6 @@
346129AB1FD1F0EE00532771 /* OWSFormat.m in Sources */,
34AC0A12211B39EA00997B47 /* ContactTableViewCell.m in Sources */,
451F8A461FD715BA005CB9DA /* OWSGroupAvatarBuilder.m in Sources */,
347850591FD9972E007B8332 /* SwiftSingletons.swift in Sources */,
346129961FD1E30000532771 /* OWSDatabaseMigration.m in Sources */,
346129FB1FD5F31400532771 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */,
34AC09EA211B39B100997B47 /* ModalActivityIndicatorViewController.swift in Sources */,

@ -0,0 +1,194 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
private struct OWSThumbnailRequest {
public typealias CompletionBlock = (UIImage) -> Void
let attachmentId: String
let thumbnailDimensionPoints: UInt
let completion: CompletionBlock
init(attachmentId: String, thumbnailDimensionPoints: UInt, completion: @escaping CompletionBlock) {
self.attachmentId = attachmentId
self.thumbnailDimensionPoints = thumbnailDimensionPoints
self.completion = completion
}
}
@objc public class OWSThumbnailService: NSObject {
// MARK: - Singleton class
@objc(shared)
public static let shared = OWSThumbnailService()
public typealias CompletionBlock = (UIImage) -> Void
private let serialQueue = DispatchQueue(label: "OWSThumbnailService")
private let dbConnection: YapDatabaseConnection
// This property should only be accessed on the serialQueue.
//
// We want to process requests in _reverse_ order in which they
// arrive so that we prioritize the most recent view state.
// This data structure is actually used like a stack.
private var thumbnailRequestQueue = [OWSThumbnailRequest]()
private override init() {
dbConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
super.init()
SwiftSingletons.register(self)
}
private func canThumbnailAttachment(attachment: TSAttachmentStream) -> Bool {
guard attachment.isImage() else {
return false
}
guard !attachment.isAnimated() else {
return false
}
guard attachment.isValidImage() else {
return false
}
return true
}
// completion will only be called on success.
// completion will be called async on the main thread.
@objc public func ensureThumbnailForAttachmentId(attachmentId: String,
thumbnailDimensionPoints: UInt,
completion:@escaping CompletionBlock) {
guard attachmentId.count > 0 else {
owsFail("Empty attachment id.")
return
}
serialQueue.async {
let thumbnailRequest = OWSThumbnailRequest(attachmentId: attachmentId, thumbnailDimensionPoints: thumbnailDimensionPoints, completion: completion)
self.thumbnailRequestQueue.append(thumbnailRequest)
self.processNextRequestSync()
}
}
private func processNextRequestAsync() {
serialQueue.async {
self.processNextRequestSync()
}
}
// This should only be called on the serialQueue.
private func processNextRequestSync() {
guard !thumbnailRequestQueue.isEmpty else {
return
}
let thumbnailRequest = thumbnailRequestQueue.removeLast()
if let image = process(thumbnailRequest: thumbnailRequest) {
DispatchQueue.main.async {
thumbnailRequest.completion(image)
}
}
}
// This should only be called on the serialQueue.
private func process(thumbnailRequest: OWSThumbnailRequest) -> UIImage? {
var possibleAttachment: TSAttachmentStream?
self.dbConnection.read({ (transaction) in
possibleAttachment = TSAttachmentStream.fetch(uniqueId: thumbnailRequest.attachmentId, transaction: transaction)
})
guard let attachment = possibleAttachment else {
Logger.warn("Could not load attachment for thumbnailing.")
return nil
}
guard canThumbnailAttachment(attachment: attachment) else {
Logger.warn("Cannot thumbnail attachment.")
return nil
}
if let thumbnails = attachment.thumbnails {
for thumbnail in thumbnails {
if thumbnail.thumbnailDimensionPoints == thumbnailRequest.thumbnailDimensionPoints {
guard let filePath = attachment.path(for: thumbnail) else {
owsFail("Could not determine thumbnail path.")
return nil
}
guard let image = UIImage(contentsOfFile: filePath) else {
owsFail("Could not load thumbnail.")
return nil
}
return image
}
}
}
guard let originalFilePath = attachment.originalFilePath() else {
owsFail("Could not determine thumbnail path.")
return nil
}
guard let originalImage = UIImage(contentsOfFile: originalFilePath) else {
owsFail("Could not load original image.")
return nil
}
let originalSize = originalImage.size
guard originalSize.width > 0 && originalSize.height > 0 else {
owsFail("Original image has invalid size.")
return nil
}
var thumbnailSize = CGSize.zero
if originalSize.width > originalSize.height {
thumbnailSize.width = CGFloat(thumbnailRequest.thumbnailDimensionPoints)
thumbnailSize.height = round(CGFloat(thumbnailRequest.thumbnailDimensionPoints) * thumbnailSize.height / thumbnailSize.width)
} else {
thumbnailSize.width = round(CGFloat(thumbnailRequest.thumbnailDimensionPoints) * thumbnailSize.width / thumbnailSize.height)
thumbnailSize.height = CGFloat(thumbnailRequest.thumbnailDimensionPoints)
}
guard thumbnailSize.width > 0 && thumbnailSize.height > 0 else {
owsFail("Thumbnail has invalid size.")
return nil
}
guard originalSize.width < thumbnailSize.width &&
originalSize.height < thumbnailSize.height else {
owsFail("Thumbnail isn't smaller than the original.")
return nil
}
// We use UIGraphicsBeginImageContextWithOptions() to scale.
// Core Image would provide better quality (e.g. Lanczos) but
// at perf cost we don't want to pay. We could also use
// CoreGraphics directly, but I'm not sure there's any benefit.
guard let thumbnailImage = originalImage.resizedImage(to: thumbnailSize) else {
owsFail("Could not thumbnail image.")
return nil
}
guard let thumbnailData = UIImageJPEGRepresentation(thumbnailImage, 0.85) else {
owsFail("Could not convert thumbnail to JPEG.")
return nil
}
let temporaryDirectory = NSTemporaryDirectory()
let thumbnailFilename = "\(NSUUID().uuidString).jpg"
let thumbnailFilePath = (temporaryDirectory as NSString).appendingPathComponent(thumbnailFilename)
do {
try thumbnailData.write(to: NSURL.fileURL(withPath: thumbnailFilePath), options: .atomicWrite)
} catch let error as NSError {
owsFail("File write failed: \(thumbnailFilePath), \(error)")
return nil
}
// It should be safe to assume that an attachment will never end up with two thumbnails of
// the same size since:
//
// * Thumbnails are only added by this method.
// * This method checks for an existing thumbnail using the same connection.
// * This method is performed on the serial queue.
self.dbConnection.readWrite({ (transaction) in
attachment.update(withNewThumbnail: thumbnailFilePath,
thumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints,
size: thumbnailSize,
transaction: transaction)
})
return thumbnailImage
}
}

@ -17,6 +17,21 @@ NS_ASSUME_NONNULL_BEGIN
@class TSAttachmentPointer;
@class YapDatabaseReadWriteTransaction;
typedef void (^OWSThumbnailCompletion)(UIImage *image);
@interface TSAttachmentThumbnail : MTLModel
@property (nonatomic, readonly) NSString *filename;
@property (nonatomic, readonly) CGSize size;
// The length of the longer side.
@property (nonatomic, readonly) NSUInteger thumbnailDimensionPoints;
- (instancetype)init NS_UNAVAILABLE;
@end
#pragma mark -
@interface TSAttachmentStream : TSAttachment
- (instancetype)init NS_UNAVAILABLE;
@ -36,6 +51,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) NSDate *creationTimestamp;
@property (nonatomic, nullable, readonly) NSArray<TSAttachmentThumbnail *> *thumbnails;
#if TARGET_OS_IPHONE
- (nullable NSData *)validStillImageData;
#endif
@ -49,6 +66,7 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable NSString *)originalFilePath;
- (nullable NSURL *)originalMediaURL;
// TODO: Rename to legacy...
- (nullable UIImage *)thumbnailImage;
- (nullable NSData *)thumbnailData;
- (nullable NSString *)thumbnailPath;
@ -78,6 +96,20 @@ NS_ASSUME_NONNULL_BEGIN
// Non-nil for attachments which need "lazy backup restore."
- (nullable OWSBackupFragment *)lazyRestoreFragment;
#pragma mark - Thumbnails
// On cache hit, the thumbnail will be returned synchronously and completion will never be invoked.
// On cache miss, nil will be returned and the completion will be invoked async on main if
// thumbnail can be generated.
- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint completion:(OWSThumbnailCompletion)completion;
- (nullable UIImage *)thumbnailImageSmallWithCompletion:(OWSThumbnailCompletion)completion;
- (nullable UIImage *)thumbnailImageMediumWithCompletion:(OWSThumbnailCompletion)completion;
- (nullable UIImage *)thumbnailImageLargeWithCompletion:(OWSThumbnailCompletion)completion;
// This method should only be invoked by OWSThumbnailService.
- (nullable NSString *)pathForThumbnail:(TSAttachmentThumbnail *)thumbnail;
#pragma mark - Validation
- (BOOL)isValidImage;
@ -91,8 +123,14 @@ NS_ASSUME_NONNULL_BEGIN
// Marks attachment as having completed "lazy backup restore."
- (void)updateWithLazyRestoreComplete;
// TODO: Review.
- (nullable TSAttachmentStream *)cloneAsThumbnail;
- (void)updateWithNewThumbnail:(NSString *)tempFilePath
thumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
size:(CGSize)size
transaction:(YapDatabaseReadWriteTransaction *)transaction;
#pragma mark - Protobuf
+ (nullable SSKProtoAttachmentPointer *)buildProtoForAttachmentId:(nullable NSString *)attachmentId;

@ -16,6 +16,36 @@ NS_ASSUME_NONNULL_BEGIN
const CGFloat kMaxVideoStillSize = 1 * 1024;
const NSUInteger kThumbnailDimensionPointsSmall = 200;
const NSUInteger kThumbnailDimensionPointsMedium = 800;
// This size is large enough to render full screen.
const NSUInteger ThumbnailDimensionPointsLarge() {
CGSize screenSizePoints = UIScreen.mainScreen.bounds.size;
return MAX(screenSizePoints.width, screenSizePoints.height);
}
@implementation TSAttachmentThumbnail
- (instancetype)initWithFilename:(NSString *)filename
size:(CGSize)size
thumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
{
self = [super init];
if (!self) {
return self;
}
_filename = filename;
_size = size;
_thumbnailDimensionPoints = thumbnailDimensionPoints;
return self;
}
@end
#pragma mark -
@interface TSAttachmentStream ()
// We only want to generate the file path for this attachment once, so that
@ -32,6 +62,8 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
// Optional property. Only set for attachments which need "lazy backup restore."
@property (nonatomic, nullable) NSString *lazyRestoreFragmentId;
@property (nonatomic, nullable) NSArray<TSAttachmentThumbnail *> *thumbnails;
@end
#pragma mark -
@ -258,6 +290,18 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return [[containingDir stringByAppendingPathComponent:newFilename] stringByAppendingPathExtension:@"jpg"];
}
- (nullable NSString *)pathForThumbnail:(TSAttachmentThumbnail *)thumbnail
{
NSString *filePath = self.originalFilePath;
if (!filePath) {
OWSFail(@"%@ Attachment missing local file path.", self.logTag);
return nil;
}
NSString *containingDir = filePath.stringByDeletingLastPathComponent;
return [containingDir stringByAppendingPathComponent:thumbnail.filename];
}
- (nullable NSURL *)originalMediaURL
{
NSString *_Nullable filePath = self.originalFilePath;
@ -272,12 +316,22 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
{
NSError *error;
for (TSAttachmentThumbnail *thumbnail in self.thumbnails) {
NSString *_Nullable thumbnailPath = [self pathForThumbnail:thumbnail];
if (thumbnailPath) {
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:thumbnailPath error:&error];
if (error || !success) {
DDLogError(@"%@ remove thumbnail failed with: %@", self.logTag, error);
}
}
}
NSString *_Nullable thumbnailPath = self.thumbnailPath;
if (thumbnailPath) {
[[NSFileManager defaultManager] removeItemAtPath:thumbnailPath error:&error];
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:thumbnailPath error:&error];
if (error) {
DDLogError(@"%@ remove thumbnail errored with: %@", self.logTag, error);
if (error || !success) {
DDLogError(@"%@ remove legacy thumbnail failed with: %@", self.logTag, error);
}
}
@ -286,10 +340,9 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
OWSFail(@"%@ Missing path for attachment.", self.logTag);
return;
}
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (error) {
DDLogError(@"%@ remove file errored with: %@", self.logTag, error);
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (error || !success) {
DDLogError(@"%@ remove file failed with: %@", self.logTag, error);
}
}
@ -728,6 +781,70 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return string;
}
#pragma mark - Thumbnails
- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint completion:(OWSThumbnailCompletion)completion
{
CGFloat maxDimensionHint = MAX(sizeHint.width, sizeHint.height);
NSUInteger thumbnailDimensionPoints;
if (maxDimensionHint <= kThumbnailDimensionPointsSmall) {
thumbnailDimensionPoints = kThumbnailDimensionPointsSmall;
} else if (maxDimensionHint <= kThumbnailDimensionPointsMedium) {
thumbnailDimensionPoints = kThumbnailDimensionPointsMedium;
} else {
thumbnailDimensionPoints = ThumbnailDimensionPointsLarge();
}
return [self thumbnailImageWithThumbnailDimensionPoints:thumbnailDimensionPoints completion:completion];
}
- (nullable UIImage *)thumbnailImageSmallWithCompletion:(OWSThumbnailCompletion)completion
{
return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall completion:completion];
}
- (nullable UIImage *)thumbnailImageMediumWithCompletion:(OWSThumbnailCompletion)completion
{
return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsMedium completion:completion];
}
- (nullable UIImage *)thumbnailImageLargeWithCompletion:(OWSThumbnailCompletion)completion
{
return [self thumbnailImageWithThumbnailDimensionPoints:ThumbnailDimensionPointsLarge() completion:completion];
}
- (nullable UIImage *)thumbnailImageWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
completion:(OWSThumbnailCompletion)completion
{
CGSize originalSize = self.imageSize;
if (originalSize.width < 1 || originalSize.height < 1) {
return nil;
}
if (originalSize.width <= thumbnailDimensionPoints || originalSize.height <= thumbnailDimensionPoints) {
// There's no point in generating a thumbnail if the original is smaller than the
// thumbnail size.
return self.originalImage;
}
for (TSAttachmentThumbnail *thumbnail in self.thumbnails) {
if (thumbnail.thumbnailDimensionPoints != thumbnailDimensionPoints) {
continue;
}
NSString *_Nullable thumbnailPath = [self pathForThumbnail:thumbnail];
if (!thumbnailPath) {
OWSFail(@"Missing thumbnail path.");
continue;
}
UIImage *_Nullable image = [UIImage imageWithContentsOfFile:thumbnailPath];
return image;
}
[OWSThumbnailService.shared ensureThumbnailForAttachmentIdWithAttachmentId:self.uniqueId
thumbnailDimensionPoints:thumbnailDimensionPoints
completion:completion];
return nil;
}
#pragma mark - Update With... Methods
- (void)markForLazyRestoreWithFragment:(OWSBackupFragment *)lazyRestoreFragment
@ -783,6 +900,40 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return thumbnailAttachment;
}
- (void)updateWithNewThumbnail:(NSString *)tempFilePath
thumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
size:(CGSize)size
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(tempFilePath.length > 0);
OWSAssert(thumbnailDimensionPoints > 0);
OWSAssert(size.width > 0 && size.height);
OWSAssert(transaction);
NSString *filename = tempFilePath.lastPathComponent;
NSString *containingDir = self.originalFilePath.stringByDeletingLastPathComponent;
NSString *filePath = [containingDir stringByAppendingPathComponent:filename];
NSError *_Nullable error;
BOOL success = [[NSFileManager defaultManager] moveItemAtPath:tempFilePath toPath:filePath error:&error];
if (error || !success) {
OWSFail(@"Could not move new thumbnail image: %@.", error);
return;
}
TSAttachmentThumbnail *newThumbnail = [[TSAttachmentThumbnail alloc] initWithFilename:filename
size:size
thumbnailDimensionPoints:thumbnailDimensionPoints];
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSAttachmentStream *attachment) {
NSMutableArray<TSAttachmentThumbnail *> *thumbnails
= (attachment.thumbnails ? [attachment.thumbnails mutableCopy]
: [NSMutableArray new]);
[thumbnails addObject:newThumbnail];
[attachment setThumbnails:thumbnails];
}];
}
// MARK: Protobuf serialization
+ (nullable SSKProtoAttachmentPointer *)buildProtoForAttachmentId:(nullable NSString *)attachmentId

@ -1,5 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
@ -20,7 +20,7 @@ public class SwiftSingletons: NSObject {
guard _isDebugAssertConfiguration() else {
return
}
let singletonClassName = String(describing:type(of:singleton))
let singletonClassName = String(describing: type(of: singleton))
guard !classSet.contains(singletonClassName) else {
owsFail("\(self.logTag) in \(#function) Duplicate singleton: \(singletonClassName).")
return

@ -9,7 +9,7 @@ NS_ASSUME_NONNULL_BEGIN
- (UIImage *)normalizedImage;
- (UIImage *)resizedWithQuality:(CGInterpolationQuality)quality rate:(CGFloat)rate;
- (UIImage *)resizedImageToSize:(CGSize)dstSize;
- (nullable UIImage *)resizedImageToSize:(CGSize)dstSize;
- (UIImage *)resizedImageToFillPixelSize:(CGSize)boundingSize;
+ (UIImage *)imageWithColor:(UIColor *)color;

@ -37,7 +37,7 @@
// Source: https://github.com/AliSoftware/UIImage-Resize
- (UIImage *)resizedImageToSize:(CGSize)dstSize
- (nullable UIImage *)resizedImageToSize:(CGSize)dstSize
{
CGImageRef imgRef = self.CGImage;
// the below values are regardless of orientation : for UIImages from Camera, width>height (landscape)
@ -106,10 +106,10 @@
UIGraphicsBeginImageContextWithOptions(dstSize, NO, self.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
if (!context) {
return nil;
}
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
if (orient == UIImageOrientationRight || orient == UIImageOrientationLeft) {
CGContextScaleCTM(context, -scaleRatio, scaleRatio);
@ -124,7 +124,7 @@
// we use srcSize (and not dstSize) as the size to specify is in user space (and we use the CTM to apply a
// scaleRatio)
CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, srcSize.width, srcSize.height), imgRef);
UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIImage *_Nullable resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resizedImage;

Loading…
Cancel
Save