1
0
mirror of https://github.com/danog/Telegram.git synced 2024-12-02 09:27:55 +01:00
Telegram/Telegraph/TGSharedFileSignals.m
2016-02-25 01:03:51 +01:00

404 lines
18 KiB
Objective-C

#import "TGSharedFileSignals.h"
#import "TGImageInfo+Telegraph.h"
#import "TGDocumentMediaAttachment.h"
#import "TGMemoryImageCache.h"
#import "TGImageUtils.h"
#import "TGImageBlur.h"
#import "TGListThumbnailSignals.h"
#import "TGSharedMediaSignals.h"
#import "TGAppDelegate.h"
#import <AVFoundation/AVFoundation.h>
#import "ActionStage.h"
#import "TGPreparedLocalDocumentMessage.h"
@interface TGDownloadDocumentHelper : NSObject <ASWatcher> {
void (^_completion)(id);
void (^_error)();
void (^_progress)(float);
bool _path;
TGDocumentMediaAttachment *_document;
}
@property (nonatomic, strong) ASHandle *actionHandle;
@end
@implementation TGDownloadDocumentHelper
- (instancetype)initWithDocument:(TGDocumentMediaAttachment *)document priority:(bool)priority path:(bool)path completion:(void (^)(id))completion error:(void (^)())error progress:(void (^)(float))progress {
self = [super init];
if (self != nil) {
_completion = [completion copy];
_error = [error copy];
_progress = [progress copy];
_path = path;
_document = document;
_actionHandle = [[ASHandle alloc] initWithDelegate:self];
if (document.documentId != 0) {
NSString *filePath = [[TGPreparedLocalDocumentMessage localDocumentDirectoryForDocumentId:document.documentId] stringByAppendingPathComponent:[document safeFileName]];
bool download = false;
if (path) {
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
completion(filePath);
} else {
download = true;
}
} else {
NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:nil];
if (data != nil) {
completion(data);
} else {
download = true;
}
}
if (download) {
NSString *path = [NSString stringWithFormat:@"/tg/media/document/(%d:%" PRId64 ":%@)", document.datacenterId, document.documentId, document.documentUri.length != 0 ? document.documentUri : @""];
[ActionStageInstance() requestActor:path options:@{@"documentAttachment": document} flags:priority ? TGActorRequestChangePriority : 0 watcher:self];
}
} else {
error();
}
}
return self;
}
- (void)dealloc {
[_actionHandle reset];
[ActionStageInstance() removeWatcher:self];
}
- (void)actorMessageReceived:(NSString *)__unused path messageType:(NSString *)messageType message:(id)message {
if ([messageType isEqualToString:@"progress"]) {
_progress([message floatValue]);
}
}
- (void)actorCompleted:(int)status path:(NSString *)__unused path result:(id)__unused result {
if (status == ASStatusSuccess) {
NSString *filePath = [[TGPreparedLocalDocumentMessage localDocumentDirectoryForDocumentId:_document.documentId] stringByAppendingPathComponent:_document.safeFileName];
if (_path) {
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
_completion(filePath);
} else {
_error();
}
} else {
NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:nil];
if (data != nil) {
_completion(data);
} else {
_error();
}
}
}
}
@end
@implementation TGSharedFileSignals
+ (NSString *)pathForFileDirectory:(TGDocumentMediaAttachment *)documentAttachment
{
static NSString *filesDirectory = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
filesDirectory = [[TGAppDelegate documentsPath] stringByAppendingPathComponent:@"files"];
});
NSString *fileDirectoryName = nil;
if (documentAttachment.documentId != 0)
fileDirectoryName = [[NSString alloc] initWithFormat:@"%" PRIx64 "", documentAttachment.documentId];
else
fileDirectoryName = [[NSString alloc] initWithFormat:@"local%" PRIx64 "", documentAttachment.localDocumentId];
return [filesDirectory stringByAppendingPathComponent:fileDirectoryName];
}
+ (UIImage *)_localCachedImageForFileThumbnail:(TGDocumentMediaAttachment *)documentAttachment ofSize:(CGSize)size renderSize:(CGSize)renderSize lowQuality:(bool)lowQuality
{
NSString *photoDirectoryPath = [self pathForFileDirectory:documentAttachment];
NSString *cachedSizePath = [photoDirectoryPath stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"thumbnail-%dx%d-%dx%d%@.jpg", (int)size.width, (int)size.height, (int)renderSize.width, (int)renderSize.height, lowQuality ? @"-low" : @""]];
UIImage *cachedSizeImage = [[UIImage alloc] initWithContentsOfFile:cachedSizePath];
return cachedSizeImage;
}
+ (SSignal *)localCachedImageForFileThumbnail:(TGDocumentMediaAttachment *)documentAttachment ofSize:(CGSize)size renderSize:(CGSize)renderSize lowQuality:(bool)lowQuality
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
UIImage *cachedSizeImage = [self _localCachedImageForFileThumbnail:documentAttachment ofSize:size renderSize:renderSize lowQuality:lowQuality];
if (cachedSizeImage != nil)
{
[subscriber putNext:cachedSizeImage];
[subscriber putCompletion];
}
else
[subscriber putError:nil];
return nil;
}];
}
+ (SSignal *)localImageForLowQualityFileThumbnail:(TGDocumentMediaAttachment *)imageAttachment
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
NSString *fileDirectoryPath = [self pathForFileDirectory:imageAttachment];
NSString *genericThumbnailPath = [fileDirectoryPath stringByAppendingPathComponent:@"thumbnail"];
UIImage *genericThumbnailImage = [[UIImage alloc] initWithContentsOfFile:genericThumbnailPath];
if (genericThumbnailImage != nil)
{
[subscriber putNext:genericThumbnailImage];
[subscriber putCompletion];
}
else
[subscriber putError:nil];
return nil;
}];
}
+ (SSignal *)localImageForFullSizeFile:(TGDocumentMediaAttachment *)documentAttachment
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
NSString *fileDirectoryPath = [self pathForFileDirectory:documentAttachment];
NSString *fullImagePath = [fileDirectoryPath stringByAppendingPathComponent:[documentAttachment safeFileName]];
UIImage *fullImage = [[UIImage alloc] initWithContentsOfFile:fullImagePath];
if (fullImage != nil && fullImage.size.width * fullImage.size.height < 4096 * 4096)
{
[subscriber putNext:fullImage];
[subscriber putCompletion];
}
else {
if ([documentAttachment.mimeType isEqualToString:@"video/mp4"]) {
NSString *videoPath = [fileDirectoryPath stringByAppendingPathComponent:documentAttachment.safeFileName];
if ([videoPath pathExtension].length == 0) {
[[NSFileManager defaultManager] createSymbolicLinkAtPath:[videoPath stringByAppendingPathExtension:@"mov"] withDestinationPath:[videoPath lastPathComponent] error:nil];
videoPath = [videoPath stringByAppendingPathExtension:@"mov"];
}
AVAsset *asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:videoPath]];
AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
imageGenerator.maximumSize = CGSizeMake(800, 800);
imageGenerator.appliesPreferredTrackTransform = true;
NSError *imageError = nil;
CGImageRef imageRef = [imageGenerator copyCGImageAtTime:CMTimeMake(0, asset.duration.timescale) actualTime:NULL error:&imageError];
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
if (imageRef != NULL) {
CGImageRelease(imageRef);
}
if (image != nil) {
[subscriber putNext:image];
[subscriber putCompletion];
return nil;
}
}
[subscriber putError:nil];
}
return nil;
}];
}
+ (SSignal *)squareFileThumbnail:(TGDocumentMediaAttachment *)documentAttachment ofSize:(CGSize)size threadPool:(SThreadPool *)threadPool memoryCache:(TGMemoryImageCache *)memoryCache pixelProcessingBlock:(void (^)(void *, int, int, int))pixelProcessingBlock
{
CGSize imageSize = CGSizeZero;
NSString *thumbnailUrl = [documentAttachment.thumbnailInfo imageUrlForLargestSize:&imageSize];
for (id attribute in documentAttachment.attributes)
{
if ([attribute isKindOfClass:[TGDocumentAttributeImageSize class]])
{
imageSize = ((TGDocumentAttributeImageSize *)attribute).size;
break;
}
else if ([attribute isKindOfClass:[TGDocumentAttributeVideo class]]) {
imageSize = ((TGDocumentAttributeVideo *)attribute).size;
break;
}
}
CGSize renderSize = TGScaleToFill(imageSize, size);
NSString *photoDirectoryPath = [self pathForFileDirectory:documentAttachment];
NSString *cachedSizeLowPath = [photoDirectoryPath stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"thumbnail-%dx%d-%dx%d%@.jpg", (int)size.width, (int)size.height, (int)renderSize.width, (int)renderSize.height, @"-low"]];
NSString *cachedSizePath = [photoDirectoryPath stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"thumbnail-%dx%d-%dx%d%@.jpg", (int)size.width, (int)size.height, (int)renderSize.width, (int)renderSize.height, @""]];
__block bool lowQuality = false;
__block bool alreadyBlurred = false;
__block bool cacheResult = false;
UIImage *cachedImage = [memoryCache imageForKey:cachedSizePath attributes:NULL];
if (cachedImage != nil)
return [SSignal single:cachedImage];
cachedImage = [memoryCache imageForKey:cachedSizeLowPath attributes:NULL];
if (cachedImage != nil)
{
SSignal *fetchFullSizeImageInBackground = [[[self localImageForFullSizeFile:documentAttachment]
catch:^SSignal *(__unused id error)
{
return [SSignal complete];
}] startOnThreadPool:threadPool];
return [[SSignal single:cachedImage] then:[[fetchFullSizeImageInBackground mapToSignal:^SSignal *(UIImage *image)
{
cacheResult = true;
CGSize optimizedSize = size;
return [[TGListThumbnailSignals signalForListThumbnail:optimizedSize image:image blurImage:false pixelProcessingBlock:pixelProcessingBlock calculateAverageColor:false] filter:^bool (id next)
{
return [next isKindOfClass:[UIImage class]];
}];
}] map:^UIImage *(UIImage *image)
{
if (cacheResult) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
NSString *fileDirectoryPath = [self pathForFileDirectory:documentAttachment];
NSString *cachedSizePath = [fileDirectoryPath stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"thumbnail-%dx%d-%dx%d%@.jpg", (int)size.width, (int)size.height, (int)renderSize.width, (int)renderSize.height, lowQuality ? @"-low" : @""]];
[UIImageJPEGRepresentation(image, 0.8f) writeToFile:cachedSizePath atomically:true];
});
}
[memoryCache setImage:image forKey:cachedSizePath attributes:nil];
return image;
}]];
}
bool averageColorCalculated = false;
uint32_t averageColor = 0;
averageColorCalculated = [memoryCache averageColorForKey:cachedSizePath color:&averageColor];
SSignal *signal = [[[[[[[[self localCachedImageForFileThumbnail:documentAttachment ofSize:size renderSize:renderSize lowQuality:false] catch:^SSignal *(__unused id error)
{
cacheResult = true;
return [self localImageForFullSizeFile:documentAttachment];
}] catch:^SSignal *(__unused id error)
{
lowQuality = true;
return [[self localCachedImageForFileThumbnail:documentAttachment ofSize:size renderSize:renderSize lowQuality:true] map:^id (UIImage *image)
{
alreadyBlurred = true;
lowQuality = true;
cacheResult = false;
return image;
}];
}] catch:^SSignal *(__unused id error)
{
return [self localImageForLowQualityFileThumbnail:documentAttachment];
}] catch:^SSignal *(__unused id error)
{
NSInteger datacenterId = 0;
TLInputFileLocation *location = [TGSharedMediaSignals inputFileLocationForImageUrl:thumbnailUrl datacenterId:&datacenterId];
if (location == nil)
return [SSignal fail:nil];
else
{
return [[TGSharedMediaSignals memoizedDataSignalForRemoteLocation:location datacenterId:datacenterId reportProgress:false] mapToSignal:^SSignal *(NSData *data)
{
NSString *photoDirectoryPath = [self pathForFileDirectory:documentAttachment];
NSString *genericThumbnailPath = [photoDirectoryPath stringByAppendingPathComponent:@"thumbnail"];
[[NSFileManager defaultManager] createDirectoryAtPath:photoDirectoryPath withIntermediateDirectories:true attributes:nil error:nil];
[data writeToFile:genericThumbnailPath atomically:true];
UIImage *image = [[UIImage alloc] initWithData:data];
if (image != nil)
return [SSignal single:image];
else
return [SSignal fail:nil];
}];
}
}] mapToSignal:^SSignal *(UIImage *image)
{
CGSize optimizedSize = size;
if (lowQuality && pixelProcessingBlock == nil)
optimizedSize = TGFitSize(size, CGSizeMake(25.0f, 25.0f));
return [[TGListThumbnailSignals signalForListThumbnail:optimizedSize image:image blurImage:lowQuality && !alreadyBlurred pixelProcessingBlock:pixelProcessingBlock calculateAverageColor:!averageColorCalculated] filter:^bool(id next)
{
if ([next isKindOfClass:[UIImage class]])
return true;
else
[memoryCache setAverageColor:(uint32_t)[next unsignedIntValue] forKey:cachedSizePath];
return false;
}];
}] map:^id (UIImage *image)
{
if (cacheResult)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
NSString *fileDirectoryPath = [self pathForFileDirectory:documentAttachment];
NSString *cachedSizePath = [fileDirectoryPath stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"thumbnail-%dx%d-%dx%d%@.jpg", (int)size.width, (int)size.height, (int)renderSize.width, (int)renderSize.height, lowQuality ? @"-low" : @""]];
[UIImageJPEGRepresentation(image, 0.8f) writeToFile:cachedSizePath atomically:true];
});
}
[memoryCache setImage:image forKey:lowQuality ? cachedSizeLowPath : cachedSizePath attributes:nil];
return image;
}] startOnThreadPool:threadPool];
if (averageColorCalculated)
return [[SSignal single:TGAverageColorImage(UIColorRGB(averageColor))] then:signal];
return signal;
}
+ (SSignal *)documentData:(TGDocumentMediaAttachment *)document priority:(bool)priority {
return [SSignal defer:^SSignal *{
SPipe *progressPipe = [[SPipe alloc] init];
return [SSignal single:@[[[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber) {
TGDownloadDocumentHelper *helper = [[TGDownloadDocumentHelper alloc] initWithDocument:document priority:priority path:false completion:^(NSData *data) {
[subscriber putNext:data];
[subscriber putCompletion];
} error:^{
[subscriber putError:nil];
} progress:^(float progress) {
progressPipe.sink(@(progress));
}];
return [[SBlockDisposable alloc] initWithBlock:^{
[helper description]; // keep reference
}];
}], progressPipe.signalProducer()]];
}];
}
+ (SSignal *)documentPath:(TGDocumentMediaAttachment *)document priority:(bool)priority {
return [SSignal defer:^SSignal *{
SPipe *progressPipe = [[SPipe alloc] init];
return [SSignal single:@[[[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber) {
TGDownloadDocumentHelper *helper = [[TGDownloadDocumentHelper alloc] initWithDocument:document priority:priority path:true completion:^(NSString *path) {
[subscriber putNext:path];
[subscriber putCompletion];
} error:^{
[subscriber putError:nil];
} progress:^(float progress) {
progressPipe.sink(@(progress));
}];
return [[SBlockDisposable alloc] initWithBlock:^{
[helper description]; // keep reference
}];
}], progressPipe.signalProducer()]];
}];
}
@end