Adding support for animated gifs

Implemented using corbett's suggestion in issue #525. Uses
Flipboard/FLAnimatedImage in an AttachmentAdapter. Detects gifs using
new Animated category in MIMETypeUtil.

Backwards compatible with previous versions of Signal on iOS for both
sending and receiving Gifs, though they are sent/received in older
versions as UIImage and won't animate. Gifs also animate on both ends of
conversations with TextSecure users on Android.

//FREEBIE
pull/1/head
Michael Okner 9 years ago committed by Frederic Jacobs
parent 25293fd40b
commit 37b582beda

@ -0,0 +1,156 @@
//
// FLAnimatedImage.h
// Flipboard
//
// Created by Raphael Schaad on 7/8/13.
// Copyright (c) 2013-2015 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
// Allow user classes conveniently just importing one header.
#import "FLAnimatedImageView.h"
#if defined(DEBUG) && DEBUG
@protocol FLAnimatedImageDebugDelegate;
#endif
// Logging
// If set to 0, disables integration with CocoaLumberjack Logger (only matters if CocoaLumberjack is installed).
#ifndef FLLumberjackIntegrationEnabled
#define FLLumberjackIntegrationEnabled 1
#endif
// If set to 1, enables NSLog logging (only matters #if DEBUG -- never for release builds).
#ifndef FLDebugLoggingEnabled
#define FLDebugLoggingEnabled 0
#endif
#ifndef NS_DESIGNATED_INITIALIZER
#if __has_attribute(objc_designated_initializer)
#define NS_DESIGNATED_INITIALIZER __attribute((objc_designated_initializer))
#else
#define NS_DESIGNATED_INITIALIZER
#endif
#endif
//
// An `FLAnimatedImage`'s job is to deliver frames in a highly performant way and works in conjunction with `FLAnimatedImageView`.
// It subclasses `NSObject` and not `UIImage` because it's only an "image" in the sense that a sea lion is a lion.
// It tries to intelligently choose the frame cache size depending on the image and memory situation with the goal to lower CPU usage for smaller ones, lower memory usage for larger ones and always deliver frames for high performant play-back.
// Note: `posterImage`, `size`, `loopCount`, `delayTimes` and `frameCount` don't change after successful initialization.
//
@interface FLAnimatedImage : NSObject
@property (nonatomic, strong, readonly) UIImage *posterImage; // Guaranteed to be loaded; usually equivalent to `-imageLazilyCachedAtIndex:0`
@property (nonatomic, assign, readonly) CGSize size; // The `.posterImage`'s `.size`
@property (nonatomic, assign, readonly) NSUInteger loopCount; // 0 means repeating the animation indefinitely
@property (nonatomic, strong, readonly) NSDictionary *delayTimesForIndexes; // Of type `NSTimeInterval` boxed in `NSNumber`s
@property (nonatomic, assign, readonly) NSUInteger frameCount; // Number of valid frames; equal to `[.delayTimes count]`
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeCurrent; // Current size of intelligently chosen buffer window; can range in the interval [1..frameCount]
@property (nonatomic, assign) NSUInteger frameCacheSizeMax; // Allow to cap the cache size; 0 means no specific limit (default)
// Intended to be called from main thread synchronously; will return immediately.
// If the result isn't cached, will return `nil`; the caller should then pause playback, not increment frame counter and keep polling.
// After an initial loading time, depending on `frameCacheSize`, frames should be available immediately from the cache.
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index;
// Pass either a `UIImage` or an `FLAnimatedImage` and get back its size
+ (CGSize)sizeForImage:(id)image;
// On success, the initializers return an `FLAnimatedImage` with all fields initialized, on failure they return `nil` and an error will be logged.
- (instancetype)initWithAnimatedGIFData:(NSData *)data NS_DESIGNATED_INITIALIZER;
+ (instancetype)animatedImageWithGIFData:(NSData *)data;
@property (nonatomic, strong, readonly) NSData *data; // The data the receiver was initialized with; read-only
#if defined(DEBUG) && DEBUG
// Only intended to report internal state for debugging
@property (nonatomic, weak) id<FLAnimatedImageDebugDelegate> debug_delegate;
@property (nonatomic, strong) NSMutableDictionary *debug_info; // To track arbitrary data (e.g. original URL, loading durations, cache hits, etc.)
#endif
@end
@interface FLWeakProxy : NSProxy
+ (instancetype)weakProxyForObject:(id)targetObject;
@end
#if defined(DEBUG) && DEBUG
@protocol FLAnimatedImageDebugDelegate <NSObject>
@optional
- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didUpdateCachedFrames:(NSIndexSet *)indexesOfFramesInCache;
- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didRequestCachedFrame:(NSUInteger)index;
- (CGFloat)debug_animatedImagePredrawingSlowdownFactor:(FLAnimatedImage *)animatedImage;
@end
#endif
// Try to detect and import CocoaLumberjack in all scenarious (library versions, way of including it, CocoaPods versions, etc.).
#if FLLumberjackIntegrationEnabled
#if defined(__has_include)
#if __has_include("<CocoaLumberjack/CocoaLumberjack.h>")
#import <CocoaLumberjack/CocoaLumberjack.h>
#elif __has_include("CocoaLumberjack.h")
#import "CocoaLumberjack.h"
#elif __has_include("<CocoaLumberjack/DDLog.h>")
#import <CocoaLumberjack/DDLog.h>
#elif __has_include("DDLog.h")
#import "DDLog.h"
#endif
#elif defined(COCOAPODS_POD_AVAILABLE_CocoaLumberjack) || defined(__POD_CocoaLumberjack)
#if COCOAPODS_VERSION_MAJOR_CocoaLumberjack == 2
#import <CocoaLumberjack/CocoaLumberjack.h>
#else
#import <CocoaLumberjack/DDLog.h>
#endif
#endif
#if defined(DDLogError) && defined(DDLogWarn) && defined(DDLogInfo) && defined(DDLogDebug) && defined(DDLogVerbose)
#define FLLumberjackAvailable
#endif
#endif
#if FLLumberjackIntegrationEnabled && defined(FLLumberjackAvailable)
// Use a custom, global (not per-file) log level for this library.
extern int flAnimatedImageLogLevel;
#if defined(LOG_OBJC_MAYBE) // CocoaLumberjack 1.x
#define FLLogError(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_ERROR, flAnimatedImageLogLevel, LOG_FLAG_ERROR, 0, frmt, ##__VA_ARGS__)
#define FLLogWarn(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_WARN, flAnimatedImageLogLevel, LOG_FLAG_WARN, 0, frmt, ##__VA_ARGS__)
#define FLLogInfo(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_INFO, flAnimatedImageLogLevel, LOG_FLAG_INFO, 0, frmt, ##__VA_ARGS__)
#define FLLogDebug(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_DEBUG, flAnimatedImageLogLevel, LOG_FLAG_DEBUG, 0, frmt, ##__VA_ARGS__)
#define FLLogVerbose(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_VERBOSE, flAnimatedImageLogLevel, LOG_FLAG_VERBOSE, 0, frmt, ##__VA_ARGS__)
#else // CocoaLumberjack 2.x
#define FLLogError(frmt, ...) LOG_MAYBE(NO, flAnimatedImageLogLevel, DDLogFlagError, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
#define FLLogWarn(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, flAnimatedImageLogLevel, DDLogFlagWarning, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
#define FLLogInfo(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, flAnimatedImageLogLevel, DDLogFlagInfo, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
#define FLLogDebug(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, flAnimatedImageLogLevel, DDLogFlagDebug, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
#define FLLogVerbose(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, flAnimatedImageLogLevel, DDLogFlagVerbose, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
#endif
#else
#if FLDebugLoggingEnabled && DEBUG
// CocoaLumberjack is disabled or not available, but we want to fallback to regular logging (debug builds only).
#define FLLog(...) NSLog(__VA_ARGS__)
#else
// No logging at all.
#define FLLog(...) ((void)0)
#endif
#define FLLogError(...) FLLog(__VA_ARGS__)
#define FLLogWarn(...) FLLog(__VA_ARGS__)
#define FLLogInfo(...) FLLog(__VA_ARGS__)
#define FLLogDebug(...) FLLog(__VA_ARGS__)
#define FLLogVerbose(...) FLLog(__VA_ARGS__)
#endif

@ -0,0 +1,774 @@
//
// FLAnimatedImage.m
// Flipboard
//
// Created by Raphael Schaad on 7/8/13.
// Copyright (c) 2013-2015 Flipboard. All rights reserved.
//
#import "FLAnimatedImage.h"
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/MobileCoreServices.h>
// From vm_param.h, define for iOS 8.0 or higher to build on device.
#ifndef BYTE_SIZE
#define BYTE_SIZE 8 // byte size in bits
#endif
#define MEGABYTE (1024 * 1024)
#if FLLumberjackIntegrationEnabled && defined(FLLumberjackAvailable)
#if defined(DEBUG) && DEBUG
#if defined(LOG_LEVEL_DEBUG) // CocoaLumberjack 1.x
int flAnimatedImageLogLevel = LOG_LEVEL_DEBUG;
#else // CocoaLumberjack 2.x
int flAnimatedImageLogLevel = DDLogFlagDebug;
#endif
#else
#if defined(LOG_LEVEL_WARN) // CocoaLumberjack 1.x
int flAnimatedImageLogLevel = LOG_LEVEL_WARN;
#else // CocoaLumberjack 2.x
int flAnimatedImageLogLevel = DDLogFlagWarning;
#endif
#endif
#endif
// An animated image's data size (dimensions * frameCount) category; its value is the max allowed memory (in MB).
// E.g.: A 100x200px GIF with 30 frames is ~2.3MB in our pixel format and would fall into the `FLAnimatedImageDataSizeCategoryAll` category.
typedef NS_ENUM(NSUInteger, FLAnimatedImageDataSizeCategory) {
FLAnimatedImageDataSizeCategoryAll = 10, // All frames permanently in memory (be nice to the CPU)
FLAnimatedImageDataSizeCategoryDefault = 75, // A frame cache of default size in memory (usually real-time performance and keeping low memory profile)
FLAnimatedImageDataSizeCategoryOnDemand = 250, // Only keep one frame at the time in memory (easier on memory, slowest performance)
FLAnimatedImageDataSizeCategoryUnsupported // Even for one frame too large, computer says no.
};
typedef NS_ENUM(NSUInteger, FLAnimatedImageFrameCacheSize) {
FLAnimatedImageFrameCacheSizeNoLimit = 0, // 0 means no specific limit
FLAnimatedImageFrameCacheSizeLowMemory = 1, // The minimum frame cache size; this will produce frames on-demand.
FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning = 2, // If we can produce the frames faster than we consume, one frame ahead will already result in a stutter-free playback.
FLAnimatedImageFrameCacheSizeDefault = 5 // Build up a comfy buffer window to cope with CPU hiccups etc.
};
@interface FLAnimatedImage ()
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeOptimal; // The optimal number of frames to cache based on image size & number of frames; never changes
@property (nonatomic, assign) NSUInteger frameCacheSizeMaxInternal; // Allow to cap the cache size e.g. when memory warnings occur; 0 means no specific limit (default)
@property (nonatomic, assign) NSUInteger requestedFrameIndex; // Most recently requested frame index
@property (nonatomic, assign, readonly) NSUInteger posterImageFrameIndex; // Index of non-purgable poster image; never changes
@property (nonatomic, strong, readonly) NSMutableDictionary *cachedFramesForIndexes;
@property (nonatomic, strong, readonly) NSMutableIndexSet *cachedFrameIndexes; // Indexes of cached frames
@property (nonatomic, strong, readonly) NSMutableIndexSet *requestedFrameIndexes; // Indexes of frames that are currently produced in the background
@property (nonatomic, strong, readonly) NSIndexSet *allFramesIndexSet; // Default index set with the full range of indexes; never changes
@property (nonatomic, assign) NSUInteger memoryWarningCount;
@property (nonatomic, strong, readonly) dispatch_queue_t serialQueue;
@property (nonatomic, strong, readonly) __attribute__((NSObject)) CGImageSourceRef imageSource;
// The weak proxy is used to break retain cycles with delayed actions from memory warnings.
// We are lying about the actual type here to gain static type checking and eliminate casts.
// The actual type of the object is `FLWeakProxy`.
@property (nonatomic, strong, readonly) FLAnimatedImage *weakProxy;
@end
// For custom dispatching of memory warnings to avoid deallocation races since NSNotificationCenter doesn't retain objects it is notifying.
static NSHashTable *allAnimatedImagesWeak;
@implementation FLAnimatedImage
#pragma mark - Accessors
#pragma mark Public
// This is the definite value the frame cache needs to size itself to.
- (NSUInteger)frameCacheSizeCurrent
{
NSUInteger frameCacheSizeCurrent = self.frameCacheSizeOptimal;
// If set, respect the caps.
if (self.frameCacheSizeMax > FLAnimatedImageFrameCacheSizeNoLimit) {
frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMax);
}
if (self.frameCacheSizeMaxInternal > FLAnimatedImageFrameCacheSizeNoLimit) {
frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMaxInternal);
}
return frameCacheSizeCurrent;
}
- (void)setFrameCacheSizeMax:(NSUInteger)frameCacheSizeMax
{
if (_frameCacheSizeMax != frameCacheSizeMax) {
// Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed.
BOOL willFrameCacheSizeShrink = (frameCacheSizeMax < self.frameCacheSizeCurrent);
// Update the value
_frameCacheSizeMax = frameCacheSizeMax;
if (willFrameCacheSizeShrink) {
[self purgeFrameCacheIfNeeded];
}
}
}
#pragma mark Private
- (void)setFrameCacheSizeMaxInternal:(NSUInteger)frameCacheSizeMaxInternal
{
if (_frameCacheSizeMaxInternal != frameCacheSizeMaxInternal) {
// Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed.
BOOL willFrameCacheSizeShrink = (frameCacheSizeMaxInternal < self.frameCacheSizeCurrent);
// Update the value
_frameCacheSizeMaxInternal = frameCacheSizeMaxInternal;
if (willFrameCacheSizeShrink) {
[self purgeFrameCacheIfNeeded];
}
}
}
#pragma mark - Life Cycle
+ (void)initialize
{
if (self == [FLAnimatedImage class]) {
// UIKit memory warning notification handler shared by all of the instances
allAnimatedImagesWeak = [NSHashTable weakObjectsHashTable];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
// UIKit notifications are posted on the main thread. didReceiveMemoryWarning: is expecting the main run loop, and we don't lock on allAnimatedImagesWeak
NSAssert([NSThread isMainThread], @"Received memory warning on non-main thread");
// Get a strong reference to all of the images. If an instance is returned in this array, it is still live and has not entered dealloc.
// Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
NSArray *images = nil;
@synchronized(allAnimatedImagesWeak) {
images = [[allAnimatedImagesWeak allObjects] copy];
}
// Now issue notifications to all of the images while holding a strong reference to them
[images makeObjectsPerformSelector:@selector(didReceiveMemoryWarning:) withObject:note];
}];
}
}
- (instancetype)init
{
FLAnimatedImage *animatedImage = [self initWithAnimatedGIFData:nil];
if (!animatedImage) {
FLLogError(@"Use `-initWithAnimatedGIFData:` and supply the animated GIF data as an argument to initialize an object of type `FLAnimatedImage`.");
}
return animatedImage;
}
- (instancetype)initWithAnimatedGIFData:(NSData *)data
{
// Early return if no data supplied!
BOOL hasData = ([data length] > 0);
if (!hasData) {
FLLogError(@"No animated GIF data supplied.");
return nil;
}
self = [super init];
if (self) {
// Do one-time initializations of `readonly` properties directly to ivar to prevent implicit actions and avoid need for private `readwrite` property overrides.
// Keep a strong reference to `data` and expose it read-only publicly.
// However, we will use the `_imageSource` as handler to the image data throughout our life cycle.
_data = data;
// Initialize internal data structures
_cachedFramesForIndexes = [[NSMutableDictionary alloc] init];
_cachedFrameIndexes = [[NSMutableIndexSet alloc] init];
_requestedFrameIndexes = [[NSMutableIndexSet alloc] init];
// Note: We could leverage `CGImageSourceCreateWithURL` too to add a second initializer `-initWithAnimatedGIFContentsOfURL:`.
_imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
// Early return on failure!
if (!_imageSource) {
FLLogError(@"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data);
return nil;
}
// Early return if not GIF!
CFStringRef imageSourceContainerType = CGImageSourceGetType(_imageSource);
BOOL isGIFData = UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF);
if (!isGIFData) {
FLLogError(@"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data);
return nil;
}
// Get `LoopCount`
// Note: 0 means repeating the animation indefinitely.
// Image properties example:
// {
// FileSize = 314446;
// "{GIF}" = {
// HasGlobalColorMap = 1;
// LoopCount = 0;
// };
// }
NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
_loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
// Iterate through frame images
size_t imageCount = CGImageSourceGetCount(_imageSource);
NSUInteger skippedFrameCount = 0;
NSMutableDictionary *delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];
for (size_t i = 0; i < imageCount; i++) {
CGImageRef frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL);
if (frameImageRef) {
UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef];
// Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid).
if (frameImage) {
// Set poster image
if (!self.posterImage) {
_posterImage = frameImage;
// Set its size to proxy our size.
_size = _posterImage.size;
// Remember index of poster image so we never purge it; also add it to the cache.
_posterImageFrameIndex = i;
[self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)];
[self.cachedFrameIndexes addIndex:self.posterImageFrameIndex];
}
// Get `DelayTime`
// Note: It's not in (1/100) of a second like still falsely described in the documentation as per iOS 8 (rdar://19507384) but in seconds stored as `kCFNumberFloat32Type`.
// Frame properties example:
// {
// ColorModel = RGB;
// Depth = 8;
// PixelHeight = 960;
// PixelWidth = 640;
// "{GIF}" = {
// DelayTime = "0.4";
// UnclampedDelayTime = "0.4";
// };
// }
NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL);
NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
// Try to use the unclamped delay time; fall back to the normal delay time.
NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
if (!delayTime) {
delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
}
// If we don't get a delay time from the properties, fall back to `kDelayTimeIntervalDefault` or carry over the preceding frame's value.
const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
if (!delayTime) {
if (i == 0) {
FLLogInfo(@"Falling back to default delay time for first frame %@ because none found in GIF properties %@", frameImage, frameProperties);
delayTime = @(kDelayTimeIntervalDefault);
} else {
FLLogInfo(@"Falling back to preceding delay time for frame %zu %@ because none found in GIF properties %@", i, frameImage, frameProperties);
delayTime = delayTimesForIndexesMutable[@(i - 1)];
}
}
// Support frame delays as low as `kDelayTimeIntervalMinimum`, with anything below being rounded up to `kDelayTimeIntervalDefault` for legacy compatibility.
// This is how the fastest browsers do it as per 2012: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility
const NSTimeInterval kDelayTimeIntervalMinimum = 0.02;
// To support the minimum even when rounding errors occur, use an epsilon when comparing. We downcast to float because that's what we get for delayTime from ImageIO.
if ([delayTime floatValue] < ((float)kDelayTimeIntervalMinimum - FLT_EPSILON)) {
FLLogInfo(@"Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kDelayTimeIntervalMinimum);
delayTime = @(kDelayTimeIntervalDefault);
}
delayTimesForIndexesMutable[@(i)] = delayTime;
} else {
skippedFrameCount++;
FLLogInfo(@"Dropping frame %zu because valid `CGImageRef` %@ did result in `nil`-`UIImage`.", i, frameImageRef);
}
CFRelease(frameImageRef);
} else {
skippedFrameCount++;
FLLogInfo(@"Dropping frame %zu because failed to `CGImageSourceCreateImageAtIndex` with image source %@", i, _imageSource);
}
}
_delayTimesForIndexes = [delayTimesForIndexesMutable copy];
_frameCount = imageCount;
if (self.frameCount == 0) {
FLLogInfo(@"Failed to create any valid frames for GIF with properties %@", imageProperties);
return nil;
} else if (self.frameCount == 1) {
// Warn when we only have a single frame but return a valid GIF.
FLLogInfo(@"Created valid GIF but with only a single frame. Image properties: %@", imageProperties);
} else {
// We have multiple frames, rock on!
}
// Calculate the optimal frame cache size: try choosing a larger buffer window depending on the predicted image size.
// It's only dependent on the image size & number of frames and never changes.
CGFloat animatedImageDataSize = CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * (self.frameCount - skippedFrameCount) / MEGABYTE;
if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) {
_frameCacheSizeOptimal = self.frameCount;
} else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) {
// This value doesn't depend on device memory much because if we're not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames.
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault;
} else {
// The predicted size exceeds the limits to build up a cache and we go into low memory mode from the beginning.
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory;
}
// In any case, cap the optimal cache size at the frame count.
_frameCacheSizeOptimal = MIN(_frameCacheSizeOptimal, self.frameCount);
// Convenience/minor performance optimization; keep an index set handy with the full range to return in `-frameIndexesToCache`.
_allFramesIndexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, self.frameCount)];
// See the property declarations for descriptions.
_weakProxy = (id)[FLWeakProxy weakProxyForObject:self];
// Register this instance in the weak table for memory notifications. The NSHashTable will clean up after itself when we're gone.
// Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
@synchronized(allAnimatedImagesWeak) {
[allAnimatedImagesWeak addObject:self];
}
}
return self;
}
+ (instancetype)animatedImageWithGIFData:(NSData *)data
{
FLAnimatedImage *animatedImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:data];
return animatedImage;
}
- (void)dealloc
{
if (_weakProxy) {
[NSObject cancelPreviousPerformRequestsWithTarget:_weakProxy];
}
if (_imageSource) {
CFRelease(_imageSource);
}
}
#pragma mark - Public Methods
// See header for more details.
// Note: both consumer and producer are throttled: consumer by frame timings and producer by the available memory (max buffer window size).
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index
{
// Early return if the requested index is beyond bounds.
// Note: We're comparing an index with a count and need to bail on greater than or equal to.
if (index >= self.frameCount) {
FLLogWarn(@"Skipping requested frame %lu beyond bounds (total frame count: %lu) for animated image: %@", (unsigned long)index, (unsigned long)self.frameCount, self);
return nil;
}
// Remember requested frame index, this influences what we should cache next.
self.requestedFrameIndex = index;
#if defined(DEBUG) && DEBUG
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didRequestCachedFrame:)]) {
[self.debug_delegate debug_animatedImage:self didRequestCachedFrame:index];
}
#endif
// Quick check to avoid doing any work if we already have all possible frames cached, a common case.
if ([self.cachedFrameIndexes count] < self.frameCount) {
// If we have frames that should be cached but aren't and aren't requested yet, request them.
// Exclude existing cached frames, frames already requested, and specially cached poster image.
NSMutableIndexSet *frameIndexesToAddToCacheMutable = [[self frameIndexesToCache] mutableCopy];
[frameIndexesToAddToCacheMutable removeIndexes:self.cachedFrameIndexes];
[frameIndexesToAddToCacheMutable removeIndexes:self.requestedFrameIndexes];
[frameIndexesToAddToCacheMutable removeIndex:self.posterImageFrameIndex];
NSIndexSet *frameIndexesToAddToCache = [frameIndexesToAddToCacheMutable copy];
// Asynchronously add frames to our cache.
if ([frameIndexesToAddToCache count] > 0) {
[self addFrameIndexesToCache:frameIndexesToAddToCache];
}
}
// Get the specified image.
UIImage *image = self.cachedFramesForIndexes[@(index)];
// Purge if needed based on the current playhead position.
[self purgeFrameCacheIfNeeded];
return image;
}
// Only called once from `-imageLazilyCachedAtIndex` but factored into its own method for logical grouping.
- (void)addFrameIndexesToCache:(NSIndexSet *)frameIndexesToAddToCache
{
// Order matters. First, iterate over the indexes starting from the requested frame index.
// Then, if there are any indexes before the requested frame index, do those.
NSRange firstRange = NSMakeRange(self.requestedFrameIndex, self.frameCount - self.requestedFrameIndex);
NSRange secondRange = NSMakeRange(0, self.requestedFrameIndex);
if (firstRange.length + secondRange.length != self.frameCount) {
FLLogWarn(@"Two-part frame cache range doesn't equal full range.");
}
// Add to the requested list before we actually kick them off, so they don't get into the queue twice.
[self.requestedFrameIndexes addIndexes:frameIndexesToAddToCache];
// Lazily create dedicated isolation queue.
if (!self.serialQueue) {
_serialQueue = dispatch_queue_create("com.flipboard.framecachingqueue", DISPATCH_QUEUE_SERIAL);
}
// Start streaming requested frames in the background into the cache.
// Avoid capturing self in the block as there's no reason to keep doing work if the animated image went away.
FLAnimatedImage * __weak weakSelf = self;
dispatch_async(self.serialQueue, ^{
// Produce and cache next needed frame.
void (^frameRangeBlock)(NSRange, BOOL *) = ^(NSRange range, BOOL *stop) {
// Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`.
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
#if defined(DEBUG) && DEBUG
CFTimeInterval predrawBeginTime = CACurrentMediaTime();
#endif
UIImage *image = [weakSelf predrawnImageAtIndex:i];
#if defined(DEBUG) && DEBUG
CFTimeInterval predrawDuration = CACurrentMediaTime() - predrawBeginTime;
CFTimeInterval slowdownDuration = 0.0;
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImagePredrawingSlowdownFactor:)]) {
CGFloat predrawingSlowdownFactor = [self.debug_delegate debug_animatedImagePredrawingSlowdownFactor:self];
slowdownDuration = predrawDuration * predrawingSlowdownFactor - predrawDuration;
[NSThread sleepForTimeInterval:slowdownDuration];
}
FLLogVerbose(@"Predrew frame %lu in %f ms for animated image: %@", (unsigned long)i, (predrawDuration + slowdownDuration) * 1000, self);
#endif
// The results get returned one by one as soon as they're ready (and not in batch).
// The benefits of having the first frames as quick as possible outweigh building up a buffer to cope with potential hiccups when the CPU suddenly gets busy.
if (image && weakSelf) {
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.cachedFramesForIndexes[@(i)] = image;
[weakSelf.cachedFrameIndexes addIndex:i];
[weakSelf.requestedFrameIndexes removeIndex:i];
#if defined(DEBUG) && DEBUG
if ([weakSelf.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) {
[weakSelf.debug_delegate debug_animatedImage:weakSelf didUpdateCachedFrames:weakSelf.cachedFrameIndexes];
}
#endif
});
}
}
};
[frameIndexesToAddToCache enumerateRangesInRange:firstRange options:0 usingBlock:frameRangeBlock];
[frameIndexesToAddToCache enumerateRangesInRange:secondRange options:0 usingBlock:frameRangeBlock];
});
}
+ (CGSize)sizeForImage:(id)image
{
CGSize imageSize = CGSizeZero;
// Early return for nil
if (!image) {
return imageSize;
}
if ([image isKindOfClass:[UIImage class]]) {
UIImage *uiImage = (UIImage *)image;
imageSize = uiImage.size;
} else if ([image isKindOfClass:[FLAnimatedImage class]]) {
FLAnimatedImage *animatedImage = (FLAnimatedImage *)image;
imageSize = animatedImage.size;
} else {
// Bear trap to capture bad images; we have seen crashers cropping up on iOS 7.
FLLogError(@"`image` isn't of expected types `UIImage` or `FLAnimatedImage`: %@", image);
}
return imageSize;
}
#pragma mark - Private Methods
#pragma mark Frame Loading
- (UIImage *)predrawnImageAtIndex:(NSUInteger)index
{
// It's very important to use the cached `_imageSource` since the random access to a frame with `CGImageSourceCreateImageAtIndex` turns from an O(1) into an O(n) operation when re-initializing the image source every time.
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CFRelease(imageRef);
// Loading in the image object is only half the work, the displaying image view would still have to synchronosly wait and decode the image, so we go ahead and do that here on the background thread.
image = [[self class] predrawnImageFromImage:image];
return image;
}
#pragma mark Frame Caching
- (NSIndexSet *)frameIndexesToCache
{
NSIndexSet *indexesToCache = nil;
// Quick check to avoid building the index set if the number of frames to cache equals the total frame count.
if (self.frameCacheSizeCurrent == self.frameCount) {
indexesToCache = self.allFramesIndexSet;
} else {
NSMutableIndexSet *indexesToCacheMutable = [[NSMutableIndexSet alloc] init];
// Add indexes to the set in two separate blocks- the first starting from the requested frame index, up to the limit or the end.
// The second, if needed, the remaining number of frames beginning at index zero.
NSUInteger firstLength = MIN(self.frameCacheSizeCurrent, self.frameCount - self.requestedFrameIndex);
NSRange firstRange = NSMakeRange(self.requestedFrameIndex, firstLength);
[indexesToCacheMutable addIndexesInRange:firstRange];
NSUInteger secondLength = self.frameCacheSizeCurrent - firstLength;
if (secondLength > 0) {
NSRange secondRange = NSMakeRange(0, secondLength);
[indexesToCacheMutable addIndexesInRange:secondRange];
}
// Double check our math, before we add the poster image index which may increase it by one.
if ([indexesToCacheMutable count] != self.frameCacheSizeCurrent) {
FLLogWarn(@"Number of frames to cache doesn't equal expected cache size.");
}
[indexesToCacheMutable addIndex:self.posterImageFrameIndex];
indexesToCache = [indexesToCacheMutable copy];
}
return indexesToCache;
}
- (void)purgeFrameCacheIfNeeded
{
// Purge frames that are currently cached but don't need to be.
// But not if we're still under the number of frames to cache.
// This way, if all frames are allowed to be cached (the common case), we can skip all the `NSIndexSet` math below.
if ([self.cachedFrameIndexes count] > self.frameCacheSizeCurrent) {
NSMutableIndexSet *indexesToPurge = [self.cachedFrameIndexes mutableCopy];
[indexesToPurge removeIndexes:[self frameIndexesToCache]];
[indexesToPurge enumerateRangesUsingBlock:^(NSRange range, BOOL *stop) {
// Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`.
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
[self.cachedFrameIndexes removeIndex:i];
[self.cachedFramesForIndexes removeObjectForKey:@(i)];
// Note: Don't `CGImageSourceRemoveCacheAtIndex` on the image source for frames that we don't want cached any longer to maintain O(1) time access.
#if defined(DEBUG) && DEBUG
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.debug_delegate debug_animatedImage:self didUpdateCachedFrames:self.cachedFrameIndexes];
});
}
#endif
}
}];
}
}
- (void)growFrameCacheSizeAfterMemoryWarning:(NSNumber *)frameCacheSize
{
self.frameCacheSizeMaxInternal = [frameCacheSize unsignedIntegerValue];
FLLogDebug(@"Grew frame cache size max to %lu after memory warning for animated image: %@", (unsigned long)self.frameCacheSizeMaxInternal, self);
// Schedule resetting the frame cache size max completely after a while.
const NSTimeInterval kResetDelay = 3.0;
[self.weakProxy performSelector:@selector(resetFrameCacheSizeMaxInternal) withObject:nil afterDelay:kResetDelay];
}
- (void)resetFrameCacheSizeMaxInternal
{
self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeNoLimit;
FLLogDebug(@"Reset frame cache size max (current frame cache size: %lu) for animated image: %@", (unsigned long)self.frameCacheSizeCurrent, self);
}
#pragma mark System Memory Warnings Notification Handler
- (void)didReceiveMemoryWarning:(NSNotification *)notification
{
self.memoryWarningCount++;
// If we were about to grow larger, but got rapped on our knuckles by the system again, cancel.
[NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(growFrameCacheSizeAfterMemoryWarning:) object:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning)];
[NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(resetFrameCacheSizeMaxInternal) object:nil];
// Go down to the minimum and by that implicitly immediately purge from the cache if needed to not get jettisoned by the system and start producing frames on-demand.
FLLogDebug(@"Attempt setting frame cache size max to %lu (previous was %lu) after memory warning #%lu for animated image: %@", (unsigned long)FLAnimatedImageFrameCacheSizeLowMemory, (unsigned long)self.frameCacheSizeMaxInternal, (unsigned long)self.memoryWarningCount, self);
self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeLowMemory;
// Schedule growing larger again after a while, but cap our attempts to prevent a periodic sawtooth wave (ramps upward and then sharply drops) of memory usage.
//
// [mem]^ (2) (5) (6) 1) Loading frames for the first time
// (*)| , , , 2) Mem warning #1; purge cache
// | /| (4)/| /| 3) Grow cache size a bit after a while, if no mem warning occurs
// | / | _/ | _/ | 4) Try to grow cache size back to optimum after a while, if no mem warning occurs
// |(1)/ |_/ |/ |__(7) 5) Mem warning #2; purge cache
// |__/ (3) 6) After repetition of (3) and (4), mem warning #3; purge cache
// +----------------------> 7) After 3 mem warnings, stay at minimum cache size
// [t]
// *) The mem high water mark before we get warned might change for every cycle.
//
const NSUInteger kGrowAttemptsMax = 2;
const NSTimeInterval kGrowDelay = 2.0;
if ((self.memoryWarningCount - 1) <= kGrowAttemptsMax) {
[self.weakProxy performSelector:@selector(growFrameCacheSizeAfterMemoryWarning:) withObject:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning) afterDelay:kGrowDelay];
}
// Note: It's not possible to get the level of a memory warning with a public API: http://stackoverflow.com/questions/2915247/iphone-os-memory-warnings-what-do-the-different-levels-mean/2915477#2915477
}
#pragma mark Image Decoding
// Decodes the image's data and draws it off-screen fully in memory; it's thread-safe and hence can be called on a background thread.
// On success, the returned object is a new `UIImage` instance with the same content as the one passed in.
// On failure, the returned object is the unchanged passed in one; the data will not be predrawn in memory though and an error will be logged.
// First inspired by & good Karma to: https://gist.github.com/steipete/1144242
+ (UIImage *)predrawnImageFromImage:(UIImage *)imageToPredraw
{
// Always use a device RGB color space for simplicity and predictability what will be going on.
CGColorSpaceRef colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB();
// Early return on failure!
if (!colorSpaceDeviceRGBRef) {
FLLogError(@"Failed to `CGColorSpaceCreateDeviceRGB` for image %@", imageToPredraw);
return imageToPredraw;
}
// Even when the image doesn't have transparency, we have to add the extra channel because Quartz doesn't support other pixel formats than 32 bpp/8 bpc for RGB:
// kCGImageAlphaNoneSkipFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaPremultipliedFirst, kCGImageAlphaPremultipliedLast
// (source: docs "Quartz 2D Programming Guide > Graphics Contexts > Table 2-1 Pixel formats supported for bitmap graphics contexts")
size_t numberOfComponents = CGColorSpaceGetNumberOfComponents(colorSpaceDeviceRGBRef) + 1; // 4: RGB + A
// "In iOS 4.0 and later, and OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap." (source: docs)
void *data = NULL;
size_t width = imageToPredraw.size.width;
size_t height = imageToPredraw.size.height;
size_t bitsPerComponent = CHAR_BIT;
size_t bitsPerPixel = (bitsPerComponent * numberOfComponents);
size_t bytesPerPixel = (bitsPerPixel / BYTE_SIZE);
size_t bytesPerRow = (bytesPerPixel * width);
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage);
// If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one.
// "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs)
if (alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly) {
alphaInfo = kCGImageAlphaNoneSkipFirst;
} else if (alphaInfo == kCGImageAlphaFirst) {
alphaInfo = kCGImageAlphaPremultipliedFirst;
} else if (alphaInfo == kCGImageAlphaLast) {
alphaInfo = kCGImageAlphaPremultipliedLast;
}
// "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo` type but can be passed to this parameter safely." (source: docs)
bitmapInfo |= alphaInfo;
// Create our own graphics context to draw to; `UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context but returns the current one which isn't thread-safe (e.g. main thread could use it at the same time).
// Note: It's not worth caching the bitmap context for multiple frames ("unique key" would be `width`, `height` and `hasAlpha`), it's ~50% slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why.
CGContextRef bitmapContextRef = CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, colorSpaceDeviceRGBRef, bitmapInfo);
CGColorSpaceRelease(colorSpaceDeviceRGBRef);
// Early return on failure!
if (!bitmapContextRef) {
FLLogError(@"Failed to `CGBitmapContextCreate` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, bitsPerComponent, bytesPerRow, imageToPredraw);
return imageToPredraw;
}
// Draw image in bitmap context and create image by preserving receiver's properties.
CGContextDrawImage(bitmapContextRef, CGRectMake(0.0, 0.0, imageToPredraw.size.width, imageToPredraw.size.height), imageToPredraw.CGImage);
CGImageRef predrawnImageRef = CGBitmapContextCreateImage(bitmapContextRef);
UIImage *predrawnImage = [UIImage imageWithCGImage:predrawnImageRef scale:imageToPredraw.scale orientation:imageToPredraw.imageOrientation];
CGImageRelease(predrawnImageRef);
CGContextRelease(bitmapContextRef);
// Early return on failure!
if (!predrawnImage) {
FLLogError(@"Failed to `imageWithCGImage:scale:orientation:` with image ref %@ created with color space %@ and bitmap context %@ and properties and properties (scale: %f orientation: %ld) for image %@", predrawnImageRef, colorSpaceDeviceRGBRef, bitmapContextRef, imageToPredraw.scale, (long)imageToPredraw.imageOrientation, imageToPredraw);
return imageToPredraw;
}
return predrawnImage;
}
#pragma mark - Description
- (NSString *)description
{
NSString *description = [super description];
description = [description stringByAppendingFormat:@" size=%@", NSStringFromCGSize(self.size)];
description = [description stringByAppendingFormat:@" frameCount=%lu", (unsigned long)self.frameCount];
return description;
}
@end
#pragma mark - FLWeakProxy
@interface FLWeakProxy ()
@property (nonatomic, weak) id target;
@end
@implementation FLWeakProxy
#pragma mark Life Cycle
// This is the designated creation method of an `FLWeakProxy` and
// as a subclass of `NSProxy` it doesn't respond to or need `-init`.
+ (instancetype)weakProxyForObject:(id)targetObject
{
FLWeakProxy *weakProxy = [FLWeakProxy alloc];
weakProxy.target = targetObject;
return weakProxy;
}
#pragma mark Forwarding Messages
- (id)forwardingTargetForSelector:(SEL)selector
{
// Keep it lightweight: access the ivar directly
return _target;
}
#pragma mark - NSWeakProxy Method Overrides
#pragma mark Handling Unimplemented Methods
- (void)forwardInvocation:(NSInvocation *)invocation
{
// Fallback for when target is nil. Don't do anything, just return 0/NULL/nil.
// The method signature we've received to get here is just a dummy to keep `doesNotRecognizeSelector:` from firing.
// We can't really handle struct return types here because we don't know the length.
void *nullPointer = NULL;
[invocation setReturnValue:&nullPointer];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
// We only get here if `forwardingTargetForSelector:` returns nil.
// In that case, our weak target has been reclaimed. Return a dummy method signature to keep `doesNotRecognizeSelector:` from firing.
// We'll emulate the Obj-c messaging nil behavior by setting the return value to nil in `forwardInvocation:`, but we'll assume that the return value is `sizeof(void *)`.
// Other libraries handle this situation by making use of a global method signature cache, but that seems heavier than necessary and has issues as well.
// See https://www.mikeash.com/pyblog/friday-qa-2010-02-26-futures.html and https://github.com/steipete/PSTDelegateProxy/issues/1 for examples of using a method signature cache.
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
@end

@ -0,0 +1,47 @@
//
// FLAnimatedImageView.h
// Flipboard
//
// Created by Raphael Schaad on 7/8/13.
// Copyright (c) 2013-2015 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLAnimatedImage;
@protocol FLAnimatedImageViewDebugDelegate;
//
// An `FLAnimatedImageView` can take an `FLAnimatedImage` and plays it automatically when in view hierarchy and stops when removed.
// The animation can also be controlled with the `UIImageView` methods `-start/stop/isAnimating`.
// It is a fully compatible `UIImageView` subclass and can be used as a drop-in component to work with existing code paths expecting to display a `UIImage`.
// Under the hood it uses a `CADisplayLink` for playback, which can be inspected with `currentFrame` & `currentFrameIndex`.
//
@interface FLAnimatedImageView : UIImageView
// Setting `[UIImageView.image]` to a non-`nil` value clears out existing `animatedImage`.
// And vice versa, setting `animatedImage` will initially populate the `[UIImageView.image]` to its `posterImage` and then start animating and hold `currentFrame`.
@property (nonatomic, strong) FLAnimatedImage *animatedImage;
@property (nonatomic, strong, readonly) UIImage *currentFrame;
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
#if defined(DEBUG) && DEBUG
// Only intended to report internal state for debugging
@property (nonatomic, weak) id<FLAnimatedImageViewDebugDelegate> debug_delegate;
#endif
@end
#if defined(DEBUG) && DEBUG
@protocol FLAnimatedImageViewDebugDelegate <NSObject>
@optional
- (void)debug_animatedImageView:(FLAnimatedImageView *)animatedImageView waitingForFrame:(NSUInteger)index duration:(NSTimeInterval)duration;
@end
#endif

@ -0,0 +1,299 @@
//
// FLAnimatedImageView.h
// Flipboard
//
// Created by Raphael Schaad on 7/8/13.
// Copyright (c) 2013-2015 Flipboard. All rights reserved.
//
#import "FLAnimatedImageView.h"
#import "FLAnimatedImage.h"
#import <QuartzCore/QuartzCore.h>
@interface FLAnimatedImageView ()
// Override of public `readonly` properties as private `readwrite`
@property (nonatomic, strong, readwrite) UIImage *currentFrame;
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
@property (nonatomic, assign) NSUInteger loopCountdown;
@property (nonatomic, assign) NSTimeInterval accumulator;
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, assign) BOOL shouldAnimate; // Before checking this value, call `-updateShouldAnimate` whenever the animated image, window or superview has changed.
@property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
@end
@implementation FLAnimatedImageView
#pragma mark - Accessors
#pragma mark Public
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
if (![_animatedImage isEqual:animatedImage]) {
if (animatedImage) {
// Clear out the image.
super.image = nil;
// Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
super.highlighted = NO;
// UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
[self invalidateIntrinsicContentSize];
} else {
// Stop animating before the animated image gets cleared out.
[self stopAnimating];
}
_animatedImage = animatedImage;
self.currentFrame = animatedImage.posterImage;
self.currentFrameIndex = 0;
if (animatedImage.loopCount > 0) {
self.loopCountdown = animatedImage.loopCount;
} else {
self.loopCountdown = NSUIntegerMax;
}
self.accumulator = 0.0;
// Start animating after the new animated image has been set.
[self updateShouldAnimate];
if (self.shouldAnimate) {
[self startAnimating];
}
[self.layer setNeedsDisplay];
}
}
#pragma mark - Life Cycle
- (void)dealloc
{
// Removes the display link from all run loop modes.
[_displayLink invalidate];
}
#pragma mark - UIView Method Overrides
#pragma mark Observing View-Related Changes
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
[self updateShouldAnimate];
if (self.shouldAnimate) {
[self startAnimating];
} else {
[self stopAnimating];
}
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
[self updateShouldAnimate];
if (self.shouldAnimate) {
[self startAnimating];
} else {
[self stopAnimating];
}
}
#pragma mark Auto Layout
- (CGSize)intrinsicContentSize
{
// Default to let UIImageView handle the sizing of its image, and anything else it might consider.
CGSize intrinsicContentSize = [super intrinsicContentSize];
// If we have have an animated image, use its image size.
// UIImageView's intrinsic content size seems to be the size of its image. The obvious approach, simply calling `-invalidateIntrinsicContentSize` when setting an animated image, results in UIImageView steadfastly returning `{UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric}` for its intrinsicContentSize.
// (Perhaps UIImageView bypasses its `-image` getter in its implementation of `-intrinsicContentSize`, as `-image` is not called after calling `-invalidateIntrinsicContentSize`.)
if (self.animatedImage) {
intrinsicContentSize = self.image.size;
}
return intrinsicContentSize;
}
#pragma mark - UIImageView Method Overrides
#pragma mark Image Data
- (UIImage *)image
{
UIImage *image = nil;
if (self.animatedImage) {
// Initially set to the poster image.
image = self.currentFrame;
} else {
image = super.image;
}
return image;
}
- (void)setImage:(UIImage *)image
{
if (image) {
// Clear out the animated image and implicitly pause animation playback.
self.animatedImage = nil;
}
super.image = image;
}
#pragma mark Animating Images
- (void)startAnimating
{
if (self.animatedImage) {
// Lazily create the display link.
if (!self.displayLink) {
// It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
// will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
// independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
// link which will lead to the deallocation of both the display link and the weak proxy.
FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
NSString *mode = NSDefaultRunLoopMode;
// Enable playback during scrolling by allowing timer events (i.e. animation) with `NSRunLoopCommonModes`.
// But too keep scrolling smooth, only do this for hardware with more than one core and otherwise keep it at the default `NSDefaultRunLoopMode`.
// The only devices with single-core chips (supporting iOS 6+) are iPhone 3GS/4 and iPod Touch 4th gen.
// Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
if ([NSProcessInfo processInfo].activeProcessorCount > 1) {
mode = NSRunLoopCommonModes;
}
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:mode];
// Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
// Setting it to 2 divides the frame rate by 2 and hence calls back at every other frame.
}
self.displayLink.paused = NO;
} else {
[super startAnimating];
}
}
- (void)stopAnimating
{
if (self.animatedImage) {
self.displayLink.paused = YES;
} else {
[super stopAnimating];
}
}
- (BOOL)isAnimating
{
BOOL isAnimating = NO;
if (self.animatedImage) {
isAnimating = self.displayLink && !self.displayLink.isPaused;
} else {
isAnimating = [super isAnimating];
}
return isAnimating;
}
#pragma mark Highlighted Image Unsupport
- (void)setHighlighted:(BOOL)highlighted
{
// Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell.
if (!self.animatedImage) {
[super setHighlighted:highlighted];
}
}
#pragma mark - Private Methods
#pragma mark Animation
// Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons.
// Just update our cached value whenever the animated image, window or superview is changed.
- (void)updateShouldAnimate
{
self.shouldAnimate = self.animatedImage && self.window && self.superview;
}
- (void)displayDidRefresh:(CADisplayLink *)displayLink
{
// If for some reason a wild call makes it through when we shouldn't be animating, bail.
// Early return!
if (!self.shouldAnimate) {
FLLogWarn(@"Trying to animate image when we shouldn't: %@", self);
return;
}
NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
// If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block).
if (delayTimeNumber) {
NSTimeInterval delayTime = [delayTimeNumber floatValue];
// If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
if (image) {
FLLogVerbose(@"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
self.currentFrame = image;
if (self.needsDisplayWhenImageBecomesAvailable) {
[self.layer setNeedsDisplay];
self.needsDisplayWhenImageBecomesAvailable = NO;
}
self.accumulator += displayLink.duration;
// While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
while (self.accumulator >= delayTime) {
self.accumulator -= delayTime;
self.currentFrameIndex++;
if (self.currentFrameIndex >= self.animatedImage.frameCount) {
// If we've looped the number of times that this animated image describes, stop looping.
self.loopCountdown--;
if (self.loopCountdown == 0) {
[self stopAnimating];
return;
}
self.currentFrameIndex = 0;
}
// Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
// Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
self.needsDisplayWhenImageBecomesAvailable = YES;
}
} else {
FLLogDebug(@"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
#if defined(DEBUG) && DEBUG
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) {
[self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:(NSTimeInterval)self.displayLink.duration];
}
#endif
}
} else {
self.currentFrameIndex++;
}
}
#pragma mark - CALayerDelegate (Informal)
#pragma mark Providing the Layer's Content
- (void)displayLayer:(CALayer *)layer
{
layer.contents = (__bridge id)self.image.CGImage;
}
@end

@ -1242,6 +1242,30 @@
<key>FooterText</key>
<string>SCWaveformView34</string>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>FooterText</key>
<string>SQLCipher</string>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>FooterText</key>
<string>SQLCipher2</string>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>FooterText</key>
<string>SQLCipher3</string>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>FooterText</key>
<string>SQLCipher4</string>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>

@ -1,4 +1,4 @@
"25519" = "Curve25519-donna by Adam Langley https://github.com/agl/curve25519-donna =====";
"25519" = "Curve25519-donna by Adam Langley https://github.com/agl/curve25519-donna";
"255192" = "GNU GENERAL PUBLIC LICENSE Version 2, June 1991";
"255193" = " Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.";
"255194" = " Preamble";
@ -58,18 +58,18 @@
"2551958" = " Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.";
"2551959" = " {signature of Ty Coon}, 1 April 1989 Ty Coon, President of Vice";
"2551960" = "This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License.";
"AFNetworking" = "AFNetworking https://github.com/AFNetworking/AFNetworking =====";
"AFNetworking" = "AFNetworking https://github.com/AFNetworking/AFNetworking";
"AFNetworking2" = "Copyright (c) 2013-2015 AFNetworking (http://afnetworking.com/)";
"AFNetworking3" = "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:";
"AFNetworking4" = "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.";
"AFNetworking5" = "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ";
"APDropDownNavToolbar" = "APDropDownNavToolbar https://github.com/ankurp/APDropDownNavToolbar =====";
"APDropDownNavToolbar" = "APDropDownNavToolbar https://github.com/ankurp/APDropDownNavToolbar";
"APDropDownNavToolbar2" = "The MIT License (MIT)";
"APDropDownNavToolbar3" = "Copyright (c) 2013 Ankur Patel";
"APDropDownNavToolbar4" = "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:";
"APDropDownNavToolbar5" = "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.";
"APDropDownNavToolbar6" = "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ";
"AxolotlKit" = "AxolotlKit https://github.com/WhisperSystems/AxolotlKit =====";
"AxolotlKit" = "AxolotlKit https://github.com/WhisperSystems/AxolotlKit";
"AxolotlKit2" = "GNU GENERAL PUBLIC LICENSE Version 2, June 1991";
"AxolotlKit3" = " Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.";
"AxolotlKit4" = " Preamble";
@ -129,31 +129,31 @@
"AxolotlKit58" = " Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.";
"AxolotlKit59" = " {signature of Ty Coon}, 1 April 1989 Ty Coon, President of Vice";
"AxolotlKit60" = "This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License.";
"CocoaLumberjack" = "CocoaLumberjack https://github.com/CocoaLumberjack/CocoaLumberjack ==================================================";
"CocoaLumberjack" = "CocoaLumberjack https://github.com/CocoaLumberjack/CocoaLumberjack";
"CocoaLumberjack2" = "Software License Agreement (BSD License)";
"CocoaLumberjack3" = "Copyright (c) 2010, Deusty, LLC All rights reserved.";
"CocoaLumberjack4" = "Redistribution and use of this software in source and binary forms, with or without modification, are permitted provided that the following conditions are met:";
"CocoaLumberjack5" = "* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.";
"CocoaLumberjack6" = "* Neither the name of Deusty nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission of Deusty, LLC.";
"CocoaLumberjack7" = "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
"DJWActionSheet" = "DJWActionSheet by DanWilliams64 https://github.com/danwilliams64/DJWActionSheet =====";
"DJWActionSheet" = "DJWActionSheet by DanWilliams64 https://github.com/danwilliams64/DJWActionSheet";
"DJWActionSheet2" = "The MIT License (MIT)";
"DJWActionSheet3" = "Copyright (c) 2014 Dan Williams";
"DJWActionSheet4" = "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:";
"DJWActionSheet5" = "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.";
"DJWActionSheet6" = "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ";
"FFCircularProgressView" = "FFCircularProgressView https://github.com/elbryan/FFCircularProgressView =====";
"FFCircularProgressView" = "FFCircularProgressView https://github.com/elbryan/FFCircularProgressView";
"FFCircularProgressView2" = "Copyright (c) 2013 Fabiano Francesconi";
"FFCircularProgressView3" = "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:";
"FFCircularProgressView4" = "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.";
"FFCircularProgressView5" = "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.";
"JSQMessagesViewController" = "JSQMessagesViewController https://github.com/jessesquires/JSQMessagesViewController =====";
"JSQMessagesViewController" = "JSQMessagesViewController https://github.com/jessesquires/JSQMessagesViewController";
"JSQMessagesViewController2" = "MIT License Copyright (c) 2014 Jesse Squires";
"JSQMessagesViewController3" = "http://www.hexedbits.com";
"JSQMessagesViewController4" = "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:";
"JSQMessagesViewController5" = "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.";
"JSQMessagesViewController6" = "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ";
"Mantle" = "Mantle https://github.com/Mantle/Mantle ================================";
"Mantle" = "Mantle https://github.com/Mantle/Mantle";
"Mantle2" = "**Copyright (c) 2012 - 2014, GitHub, Inc.** **All rights reserved.**";
"Mantle3" = "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:";
"Mantle4" = "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.";
@ -163,14 +163,14 @@
"Mantle8" = "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:";
"Mantle9" = " * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Neither the name of the Bitswift, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.";
"Mantle10" = "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ";
"OpenSSL" = "OpenSSL https://www.openssl.org/ =====";
"OpenSSL" = "OpenSSL https://www.openssl.org/";
"OpenSSL2" = " LICENSE ISSUES ==============";
"OpenSSL3" = " The OpenSSL toolkit stays under a dual license, i.e. both the conditions of the OpenSSL License and the original SSLeay license apply to the toolkit. See below for the actual license texts. Actually both licenses are BSD-style Open Source licenses. In case of any license issues related to OpenSSL please contact openssl-core@openssl.org.";
"OpenSSL4" = " OpenSSL License ---------------";
"OpenSSL5" = "/* ==================================================================== * Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. All advertising materials mentioning features or use of this * software must display the following acknowledgment: * 'This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit. (http://www.openssl.org/)' * * 4. The names 'OpenSSL Toolkit' and 'OpenSSL Project' must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please contact * openssl-core@openssl.org. * * 5. Products derived from this software may not be called 'OpenSSL' * nor may 'OpenSSL' appear in their names without prior written * permission of the OpenSSL Project. * * 6. Redistributions of any form whatsoever must retain the following * acknowledgment: * 'This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit (http://www.openssl.org/)' * * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * ==================================================================== * * This product includes cryptographic software written by Eric Young * (eay@cryptsoft.com). This product includes software written by Tim * Hudson (tjh@cryptsoft.com). * */";
"OpenSSL6" = " Original SSLeay License -----------------------";
"OpenSSL7" = "/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) * All rights reserved. * * This package is an SSL implementation written * by Eric Young (eay@cryptsoft.com). * The implementation was written so as to conform with Netscapes SSL. * * This library is free for commercial and non-commercial use as long as * the following conditions are aheared to. The following conditions * apply to all code found in this distribution, be it the RC4, RSA, * lhash, DES, etc., code; not just the SSL code. The SSL documentation * included with this distribution is covered by the same copyright terms * except that the holder is Tim Hudson (tjh@cryptsoft.com). * * Copyright remains Eric Young's, and as such any Copyright notices in * the code are not to be removed. * If this package is used in a product, Eric Young should be given attribution * as the author of the parts of the library used. * This can be in the form of a textual message at program startup or * in documentation (online or textual) provided with the package. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * 'This product includes cryptographic software written by * Eric Young (eay@cryptsoft.com)' * The word 'cryptographic' can be left out if the rouines from the library * being used are not cryptographic related :-). * 4. If you include any Windows specific code (or a derivative thereof) from * the apps directory (application code) you must include an acknowledgement: * 'This product includes software written by Tim Hudson (tjh@cryptsoft.com)' * * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * The licence and distribution terms for any publically available version or * derivative of this code cannot be changed. i.e. this code cannot simply be * copied and put under another distribution licence * [including the GNU Public Licence.] */";
"SCWaveformView" = "SCWaveformView https://github.com/rFlex/SCWaveformView =====";
"SCWaveformView" = "SCWaveformView https://github.com/rFlex/SCWaveformView";
"SCWaveformView2" = " Apache License Version 2.0, January 2004 http://www.apache.org/licenses/";
"SCWaveformView3" = " TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION";
"SCWaveformView4" = " 1. Definitions.";
@ -204,17 +204,21 @@
"SCWaveformView32" = " Licensed under the Apache License, Version 2.0 (the 'License'); you may not use this file except in compliance with the License. You may obtain a copy of the License at";
"SCWaveformView33" = " http://www.apache.org/licenses/LICENSE-2.0";
"SCWaveformView34" = " Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.";
"SSKeychain" = "SSKeyChain =====";
"SQLCipher" = "SQLCipher";
"SQLCipher2" = "Copyright (c) 2008, ZETETIC LLC All rights reserved.";
"SQLCipher3" = "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the ZETETIC LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.";
"SQLCipher4" = "THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ";
"SSKeychain" = "SSKeyChain";
"SSKeychain2" = "Copyright (c) 2010-2014 Sam Soffes, http://soff.es";
"SSKeychain3" = "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:";
"SSKeychain4" = "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.";
"SSKeychain5" = "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ";
"SocketRocket" = "SocketRocket https://github.com/square/SocketRocket =====";
"SocketRocket" = "SocketRocket https://github.com/square/SocketRocket";
"SocketRocket2" = " Copyright 2012 Square Inc.";
"SocketRocket3" = " Licensed under the Apache License, Version 2.0 (the 'License'); you may not use this file except in compliance with the License. You may obtain a copy of the License at";
"SocketRocket4" = " http://www.apache.org/licenses/LICENSE-2.0";
"SocketRocket5" = " Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.";
"Spandsp" = "SpandSP =====";
"Spandsp" = "SpandSP";
"Spandsp2" = " GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999";
"Spandsp3" = " Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.";
"Spandsp4" = "[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.]";
@ -280,17 +284,17 @@
"Spandsp64" = " 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY 'AS IS' WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.";
"Spandsp65" = " 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.";
"Spandsp66" = " END OF TERMS AND CONDITIONS ";
"Speex" = "Speex: A Free Codec For Free Speech http://www.speex.org/ =====";
"Speex" = "Speex: A Free Codec For Free Speech http://www.speex.org/";
"Speex2" = "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:";
"Speex3" = "Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the Xiph.org Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ";
"YapDatabase" = "YapDatabase https://github.com/yapstudios/YapDatabase =========================================";
"YapDatabase" = "YapDatabase https://github.com/yapstudios/YapDatabase";
"YapDatabase2" = "Software License Agreement (BSD License)";
"YapDatabase3" = "Copyright (c) 2013, yap.TV Inc. All rights reserved.";
"YapDatabase4" = "Redistribution and use of this software in source and binary forms, with or without modification, are permitted provided that the following conditions are met:";
"YapDatabase5" = "* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.";
"YapDatabase6" = "* Neither the name of yap.TV nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission of yap.TV Inc.";
"YapDatabase7" = "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
"libPhoneNumber-iOS" = "libPhoneNumber https://github.com/iziz/libPhoneNumber-iOS ==========================================";
"libPhoneNumber-iOS" = "libPhoneNumber https://github.com/iziz/libPhoneNumber-iOS";
"libPhoneNumber-iOS2" = " Apache License Version 2.0, January 2004 http://www.apache.org/licenses/";
"libPhoneNumber-iOS3" = " TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION";
"libPhoneNumber-iOS4" = " 1. Definitions.";

@ -0,0 +1,24 @@
FLAnimatedImage
https://github.com/Flipboard/FLAnimatedImage
The MIT License (MIT)
Copyright (c) 2014-2015 Flipboard
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
057B54FA6208D8269CFE6146 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D488327B732CFBE8349C7024 /* libPods.a */; };
28508611F3DF531CEDF5103D /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D488327B732CFBE8349C7024 /* libPods.a */; };
4CE0E3771B954546007210CF /* TSAnimatedAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E3761B954546007210CF /* TSAnimatedAdapter.m */; };
53EF5134D8FB5FCBDEDE2A35 /* (null) in Frameworks */ = {isa = PBXBuildFile; settings = {ATTRIBUTES = (Weak, ); }; };
701231B518ECAA4500D456C4 /* EvpMessageDigest.m in Sources */ = {isa = PBXBuildFile; fileRef = 701231B418ECAA4500D456C4 /* EvpMessageDigest.m */; };
70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; };
@ -341,6 +342,8 @@
B68112EA1A4D9EC400BA82FF /* UIImage+normalizeImage.m in Sources */ = {isa = PBXBuildFile; fileRef = B68112E91A4D9EC400BA82FF /* UIImage+normalizeImage.m */; };
B684A46D19C3446200B11029 /* PushManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B684A46C19C3446200B11029 /* PushManagerTest.m */; };
B6850E5A1995A4710068E715 /* whisperFake.cer in Resources */ = {isa = PBXBuildFile; fileRef = B6850E591995A4710068E715 /* whisperFake.cer */; };
B68EF9BA1C0B1EBD009C3DCD /* FLAnimatedImage.m in Sources */ = {isa = PBXBuildFile; fileRef = B68EF9B71C0B1EBD009C3DCD /* FLAnimatedImage.m */; };
B68EF9BB1C0B1EBD009C3DCD /* FLAnimatedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = B68EF9B91C0B1EBD009C3DCD /* FLAnimatedImageView.m */; };
B692BF071A76EF0F002786DA /* TSDatabaseSecondaryIndexes.m in Sources */ = {isa = PBXBuildFile; fileRef = B692BF061A76EF0F002786DA /* TSDatabaseSecondaryIndexes.m */; };
B69CD25119773E79005CE69A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B69CD25019773E79005CE69A /* XCTest.framework */; };
B6A3EB4B1A423B3800B2236B /* TSPhotoAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A3EB4A1A423B3800B2236B /* TSPhotoAdapter.m */; };
@ -532,6 +535,8 @@
/* Begin PBXFileReference section */
14DDBCE302E19644A773D119 /* Pods.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods.app store release.xcconfig"; path = "Pods/Target Support Files/Pods/Pods.app store release.xcconfig"; sourceTree = "<group>"; };
4CE0E3751B95453C007210CF /* TSAnimatedAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSAnimatedAdapter.h; sourceTree = "<group>"; };
4CE0E3761B954546007210CF /* TSAnimatedAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAnimatedAdapter.m; sourceTree = "<group>"; };
701231B318ECAA4500D456C4 /* EvpMessageDigest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EvpMessageDigest.h; sourceTree = "<group>"; };
701231B418ECAA4500D456C4 /* EvpMessageDigest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EvpMessageDigest.m; sourceTree = "<group>"; };
70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
@ -957,6 +962,10 @@
B68CB7E31AA548660065AC3F /* th_TH */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th_TH; path = translations/th_TH.lproj/Localizable.strings; sourceTree = "<group>"; };
B68CB7E41AA548700065AC3F /* tr_TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr_TR; path = translations/tr_TR.lproj/Localizable.strings; sourceTree = "<group>"; };
B68CB7E61AA548870065AC3F /* zh_CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_CN; path = translations/zh_CN.lproj/Localizable.strings; sourceTree = "<group>"; };
B68EF9B61C0B1EBD009C3DCD /* FLAnimatedImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FLAnimatedImage.h; path = Libraries/FLAnimatedImage/FLAnimatedImage.h; sourceTree = SOURCE_ROOT; };
B68EF9B71C0B1EBD009C3DCD /* FLAnimatedImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLAnimatedImage.m; path = Libraries/FLAnimatedImage/FLAnimatedImage.m; sourceTree = SOURCE_ROOT; };
B68EF9B81C0B1EBD009C3DCD /* FLAnimatedImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FLAnimatedImageView.h; path = Libraries/FLAnimatedImage/FLAnimatedImageView.h; sourceTree = SOURCE_ROOT; };
B68EF9B91C0B1EBD009C3DCD /* FLAnimatedImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLAnimatedImageView.m; path = Libraries/FLAnimatedImage/FLAnimatedImageView.m; sourceTree = SOURCE_ROOT; };
B692BF051A76EF0F002786DA /* TSDatabaseSecondaryIndexes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSDatabaseSecondaryIndexes.h; sourceTree = "<group>"; };
B692BF061A76EF0F002786DA /* TSDatabaseSecondaryIndexes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSDatabaseSecondaryIndexes.m; sourceTree = "<group>"; };
B69C2D171AA5445000A640C2 /* az_AZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az_AZ; path = translations/az_AZ.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -1734,6 +1743,7 @@
76EB04C818170B33006006FC /* util */ = {
isa = PBXGroup;
children = (
B68EF9B51C0B1E7D009C3DCD /* Animated GIFS */,
B6DA6B051B8A2F9A00CA6F98 /* AppStoreRating.h */,
B6DA6B061B8A2F9A00CA6F98 /* AppStoreRating.m */,
B6FE7EB81ADD63AE00A6D22F /* NSData+ows_StripToken.h */,
@ -2074,6 +2084,8 @@
A5E9D4B91A65FAD800E4481C /* TSVideoAttachmentAdapter.m */,
B6A3EB491A423B3800B2236B /* TSPhotoAdapter.h */,
B6A3EB4A1A423B3800B2236B /* TSPhotoAdapter.m */,
4CE0E3751B95453C007210CF /* TSAnimatedAdapter.h */,
4CE0E3761B954546007210CF /* TSAnimatedAdapter.m */,
B62D53F51A23CCAD009AAF82 /* TSMessageAdapter.h */,
B62D53F61A23CCAD009AAF82 /* TSMessageAdapter.m */,
);
@ -2196,6 +2208,17 @@
name = Requests;
sourceTree = "<group>";
};
B68EF9B51C0B1E7D009C3DCD /* Animated GIFS */ = {
isa = PBXGroup;
children = (
B68EF9B61C0B1EBD009C3DCD /* FLAnimatedImage.h */,
B68EF9B71C0B1EBD009C3DCD /* FLAnimatedImage.m */,
B68EF9B81C0B1EBD009C3DCD /* FLAnimatedImageView.h */,
B68EF9B91C0B1EBD009C3DCD /* FLAnimatedImageView.m */,
);
name = "Animated GIFS";
sourceTree = "<group>";
};
B6B095DD1A1D25C5008BFAA6 /* textsecure */ = {
isa = PBXGroup;
children = (
@ -2987,6 +3010,7 @@
76EB062A18170B33006006FC /* BadState.m in Sources */,
B97940271832BD2400BD66CB /* UIUtil.m in Sources */,
B62EFBEC1A91352F0072ADD3 /* TSInvalidIdentityKeyErrorMessage.m in Sources */,
4CE0E3771B954546007210CF /* TSAnimatedAdapter.m in Sources */,
B6B096861A1D25ED008BFAA6 /* SecurityUtils.m in Sources */,
76EB05BE18170B33006006FC /* ConfirmPacket.m in Sources */,
76EB058618170B33006006FC /* PreferencesUtil.m in Sources */,
@ -3089,6 +3113,7 @@
FCAC965119FF0A6E0046DFC5 /* MessagesViewController.m in Sources */,
B63AF5D01A1F757900D01AAD /* TSSubmitMessageRequest.m in Sources */,
B6A5D0631A7850180043D837 /* TSCurrentSignedPreKeyRequest.m in Sources */,
B68EF9BB1C0B1EBD009C3DCD /* FLAnimatedImageView.m in Sources */,
B6B0966B1A1D25ED008BFAA6 /* TSAttachment.m in Sources */,
76EB057618170B33006006FC /* Contact.m in Sources */,
B6B0968F1A1D25ED008BFAA6 /* TSYapDatabaseObject.m in Sources */,
@ -3165,6 +3190,7 @@
B6B0968C1A1D25ED008BFAA6 /* TSDatabaseView.m in Sources */,
B6B0966A1A1D25ED008BFAA6 /* IncomingPushMessageSignal.pb.m in Sources */,
BFB074C919A5611000F2947C /* ObservableValue.m in Sources */,
B68EF9BA1C0B1EBD009C3DCD /* FLAnimatedImage.m in Sources */,
B68112EA1A4D9EC400BA82FF /* UIImage+normalizeImage.m in Sources */,
B6B0968E1A1D25ED008BFAA6 /* TSStorageManager.m in Sources */,
FCB11D8A1A1284BB002F93FB /* SettingsTableViewCell.m in Sources */,

@ -19,6 +19,7 @@
- (UIImage *)image;
- (BOOL)isAnimated;
- (BOOL)isImage;
- (BOOL)isVideo;
-(NSURL*)mediaURL;

@ -59,6 +59,10 @@ NSString * const TSAttachementFileRelationshipEdge = @"TSAttachementFileEdge";
return [NSURL fileURLWithPath:[self filePath]];
}
- (BOOL)isAnimated {
return [MIMETypeUtil isAnimated:self.contentType];
}
- (BOOL)isImage {
return [MIMETypeUtil isImage:self.contentType];
}
@ -72,11 +76,13 @@ NSString * const TSAttachementFileRelationshipEdge = @"TSAttachementFileEdge";
}
- (UIImage*)image {
if (![self isImage]) {
if ([self isVideo] || [self isAudio]) {
return [self videoThumbnail];
}
return [UIImage imageWithContentsOfFile:self.filePath];
else {
// [self isAnimated] || [self isImage]
return [UIImage imageWithData:[NSData dataWithContentsOfURL:[self mediaURL]]];
}
}

@ -6,22 +6,27 @@
+(BOOL)isSupportedVideoMIMEType:(NSString*)contentType;
+(BOOL)isSupportedAudioMIMEType:(NSString*)contentType;
+(BOOL)isSupportedImageMIMEType:(NSString*)contentType;
+(BOOL)isSupportedAnimatedMIMEType:(NSString*)contentType;
+(BOOL)isSupportedVideoFile:(NSString*)filePath;
+(BOOL)isSupportedAudioFile:(NSString*)filePath;
+(BOOL)isSupportedImageFile:(NSString*)filePath;
+(BOOL)isSupportedAnimatedFile:(NSString*)filePath;
+(NSString*)getSupportedExtensionFromVideoMIMEType:(NSString*)supportedMIMEType;
+(NSString*)getSupportedExtensionFromAudioMIMEType:(NSString*)supportedMIMEType;
+(NSString*)getSupportedExtensionFromImageMIMEType:(NSString*)supportedMIMEType;
+(NSString*)getSupportedExtensionFromAnimatedMIMEType:(NSString*)supportedMIMEType;
+(NSString*)getSupportedMIMETypeFromVideoFile:(NSString*)supportedVideoFile;
+(NSString*)getSupportedMIMETypeFromAudioFile:(NSString*)supportedAudioFile;
+(NSString*)getSupportedMIMETypeFromImageFile:(NSString*)supportedImageFile;
+(NSString*)getSupportedMIMETypeFromAnimatedFile:(NSString*)supportedImageFile;
+(NSString*)getSupportedImageMIMETypeFromImage:(UIImage*)image;
+(BOOL)getIsSupportedTypeFromImage:(UIImage*)image;
+(BOOL)isAnimated:(NSString*)contentType;
+(BOOL)isImage:(NSString*)contentType;
+(BOOL)isVideo:(NSString*)contentType;
+(BOOL)isAudio:(NSString*)contentType;
@ -30,6 +35,7 @@
+(NSString*)filePathForImage:(NSString*)uniqueId ofMIMEType:(NSString*)contentType inFolder:(NSString*)folder;
+(NSString*)filePathForVideo:(NSString*)uniqueId ofMIMEType:(NSString*)contentType inFolder:(NSString*)folder;
+(NSString*)filePathForAudio:(NSString*)uniqueId ofMIMEType:(NSString*)contentType inFolder:(NSString*)folder;
+(NSString*)filePathForAnimated:(NSString*)uniqueId ofMIMEType:(NSString*)contentType inFolder:(NSString*)folder;
+(NSURL*)simLinkCorrectExtensionOfFile:(NSURL*)mediaURL ofMIMEType:(NSString*)contentType;

@ -37,7 +37,6 @@
return @{@"image/jpeg":@"jpeg",
@"image/pjpeg":@"jpeg",
@"image/png":@"png",
@"image/gif":@"gif",
@"image/tiff":@"tif",
@"image/x-tiff":@"tif",
@"image/bmp":@"bmp",
@ -45,6 +44,11 @@
};
}
+ (NSDictionary*)supportedAnimatedMIMETypesToExtensionTypes{
return @{@"image/gif":@"gif",
};
}
+ (NSDictionary*)supportedVideoExtensionTypesToMIMETypes{
return @{@"3gp":@"video/3gpp",
@"3gpp":@"video/3gpp",
@ -89,12 +93,16 @@
@"jpe":@"image/pjpeg",
@"jpeg":@"image/jpeg",
@"jpg":@"image/jpeg",
@"gif":@"image/gif",
@"tif":@"image/tiff",
@"tiff":@"image/tiff"
};
}
+ (NSDictionary*)supportedAnimatedExtensionTypesToMIMETypes{
return @{@"gif":@"image/gif",
};
}
+(BOOL) isSupportedVideoMIMEType:(NSString*)contentType {
return [[self supportedVideoMIMETypesToExtensionTypes] objectForKey:contentType]!=nil;
}
@ -107,8 +115,12 @@
return [[self supportedImageMIMETypesToExtensionTypes] objectForKey:contentType]!=nil;
}
+(BOOL) isSupportedAnimatedMIMEType:(NSString*)contentType {
return [[self supportedAnimatedMIMETypesToExtensionTypes] objectForKey:contentType]!=nil;
}
+(BOOL) isSupportedMIMEType:(NSString*)contentType {
return [self isSupportedImageMIMEType:contentType] || [self isSupportedAudioMIMEType:contentType] || [self isSupportedVideoMIMEType:contentType];
return [self isSupportedImageMIMEType:contentType] || [self isSupportedAudioMIMEType:contentType] || [self isSupportedVideoMIMEType:contentType] || [self isSupportedAnimatedMIMEType:contentType];
}
+(BOOL) isSupportedVideoFile:(NSString*) filePath {
@ -123,6 +135,10 @@
return [[self supportedImageExtensionTypesToMIMETypes] objectForKey:[filePath pathExtension]]!=nil;
}
+(BOOL) isSupportedAnimatedFile:(NSString*) filePath {
return [[self supportedAnimatedExtensionTypesToMIMETypes] objectForKey:[filePath pathExtension]]!=nil;
}
+(NSString*) getSupportedExtensionFromVideoMIMEType:(NSString*)supportedMIMEType {
return [[self supportedVideoMIMETypesToExtensionTypes] objectForKey:supportedMIMEType];
}
@ -135,6 +151,10 @@
return [[self supportedImageMIMETypesToExtensionTypes] objectForKey:supportedMIMEType];
}
+(NSString*) getSupportedExtensionFromAnimatedMIMEType:(NSString*)supportedMIMEType {
return [[self supportedAnimatedMIMETypesToExtensionTypes] objectForKey:supportedMIMEType];
}
+(NSString*) getSupportedMIMETypeFromVideoFile:(NSString*)supportedVideoFile {
return [[self supportedVideoExtensionTypesToMIMETypes] objectForKey:[supportedVideoFile pathExtension]];
}
@ -147,6 +167,10 @@
return [[self supportedImageExtensionTypesToMIMETypes] objectForKey:[supportedImageFile pathExtension]];
}
+(NSString*) getSupportedMIMETypeFromAnimatedFile:(NSString*)supportedAnimatedFile {
return [[self supportedAnimatedExtensionTypesToMIMETypes] objectForKey:[supportedAnimatedFile pathExtension]];
}
#pragma mark uses bytes
+(NSString*) getSupportedImageMIMETypeFromImage:(UIImage*)image {
return [image contentType];
@ -157,6 +181,9 @@
}
#pragma mark full attachment utilities
+ (BOOL)isAnimated:(NSString *)contentType {
return [MIMETypeUtil isSupportedAnimatedMIMEType:contentType];
}
+ (BOOL)isImage:(NSString*)contentType {
return [MIMETypeUtil isSupportedImageMIMEType:contentType];
}
@ -179,7 +206,10 @@
else if([self isImage:contentType]){
return [MIMETypeUtil filePathForImage:uniqueId ofMIMEType:contentType inFolder:folder];
}
else if([self isAnimated:contentType]){
return [MIMETypeUtil filePathForAnimated:uniqueId ofMIMEType:contentType inFolder:folder];
}
DDLogError(@"Got asked for path of file %@ which is unsupported", contentType);
return nil;
}
@ -215,4 +245,8 @@
return [[folder stringByAppendingFormat:@"/%@",uniqueId] stringByAppendingPathExtension:[self getSupportedExtensionFromAudioMIMEType:contentType]];
}
+ (NSString*)filePathForAnimated:(NSString*)uniqueId ofMIMEType:(NSString*)contentType inFolder:(NSString*)folder{
return [[folder stringByAppendingFormat:@"/%@",uniqueId] stringByAppendingPathExtension:[self getSupportedExtensionFromAnimatedMIMEType:contentType]];
}
@end

@ -12,7 +12,10 @@
@interface FullImageViewController : UIViewController
- (instancetype)initWithAttachment:(TSAttachmentStream*)attachment fromRect:(CGRect)rect forInteraction:(TSInteraction*)interaction;
- (instancetype)initWithAttachment:(TSAttachmentStream*)attachment
fromRect:(CGRect)rect
forInteraction:(TSInteraction*)interaction
isAnimated:(BOOL)animated;
- (void)presentFromViewController:(UIViewController*)viewController;

@ -3,12 +3,15 @@
// Signal
//
// Created by Dylan Bourgeois on 11/11/14.
// Animated GIF support added by Mike Okner (@mikeokner) on 11/27/15.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import "FullImageViewController.h"
#import "DJWActionSheet+OWS.h"
#import "UIUtil.h"
#import <AssetsLibrary/AssetsLibrary.h>
#import "FLAnimatedImage.h"
#define kImageViewCornerRadius 5.0f
@ -33,6 +36,8 @@
@property CGRect originRect;
@property BOOL isPresenting;
@property BOOL isAnimated;
@property NSData *fileData;
@property TSAttachmentStream *attachment;
@property TSInteraction *interaction;
@ -42,14 +47,19 @@
@implementation FullImageViewController
- (instancetype)initWithAttachment:(TSAttachmentStream*)attachment fromRect:(CGRect)rect forInteraction:(TSInteraction*)interaction {
- (instancetype)initWithAttachment:(TSAttachmentStream*)attachment
fromRect:(CGRect)rect
forInteraction:(TSInteraction*)interaction
isAnimated:(BOOL)animated
{
self = [super initWithNibName:nil bundle:nil];
if (self) {
self.attachment = attachment;
self.imageView.image = self.image;
self.originRect = rect;
self.interaction = interaction;
self.attachment = attachment;
self.originRect = rect;
self.interaction = interaction;
self.isAnimated = animated;
self.fileData = [NSData dataWithContentsOfURL:[attachment mediaURL]];
}
return self;
@ -101,19 +111,32 @@
- (void)initializeImageView
{
self.imageView = [[UIImageView alloc]initWithFrame:self.originRect];
self.imageView.layer.cornerRadius = kImageViewCornerRadius;
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
self.imageView.userInteractionEnabled = YES;
self.imageView.clipsToBounds = YES;
self.imageView.layer.allowsEdgeAntialiasing = YES;
[self.scrollView addSubview:self.imageView];
if (self.isAnimated) {
// Present the animated image using Flipboard/FLAnimatedImage
FLAnimatedImage *animatedGif = [FLAnimatedImage animatedImageWithGIFData:self.fileData];
FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init];
imageView.animatedImage = animatedGif;
imageView.frame = self.originRect;
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.clipsToBounds = YES;
self.imageView = imageView;
}
else {
// Present the static image using standard UIImageView
self.imageView = [[UIImageView alloc]initWithFrame:self.originRect];
self.imageView.layer.cornerRadius = kImageViewCornerRadius;
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
self.imageView.userInteractionEnabled = YES;
self.imageView.clipsToBounds = YES;
self.imageView.layer.allowsEdgeAntialiasing = YES;
}
[self.scrollView addSubview:self.imageView];
}
- (void)populateImageView:(UIImage*)image
{
if (image) {
if (image && !self.isAnimated) {
self.imageView.image = image;
}
}
@ -371,9 +394,15 @@
} else {
switch (tappedButtonIndex) {
case 0:
UIImageWriteToSavedPhotosAlbum(self.image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
case 0: {
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library writeImageDataToSavedPhotosAlbum:self.fileData metadata:nil completionBlock:^(NSURL *assetURL, NSError *error) {
if (error) {
DDLogWarn(@"Error Saving image to photo album: %@", error);
}
}];
break;
}
case 1:
[[UIPasteboard generalPasteboard] setImage:self.image];
break;

@ -35,7 +35,9 @@
#import "TSInvalidIdentityKeyErrorMessage.h"
#import "TSIncomingMessage.h"
#import "TSAttachmentPointer.h"
#import "TSAnimatedAdapter.h"
#import "TSVideoAttachmentAdapter.h"
#import <AssetsLibrary/AssetsLibrary.h>
#import "TSMessagesManager+sendMessages.h"
#import "TSMessagesManager+attachments.h"
@ -899,7 +901,10 @@ typedef enum : NSUInteger {
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
TSAttachmentStream *attStream = (TSAttachmentStream*)attachment;
FullImageViewController * vc = [[FullImageViewController alloc] initWithAttachment:attStream fromRect:convertedRect forInteraction:[self interactionAtIndexPath:indexPath]];
FullImageViewController * vc = [[FullImageViewController alloc] initWithAttachment:attStream
fromRect:convertedRect
forInteraction:[self interactionAtIndexPath:indexPath]
isAnimated:NO];
[vc presentFromViewController:self.navigationController];
}
@ -907,6 +912,25 @@ typedef enum : NSUInteger {
DDLogWarn(@"Currently unsupported");
}
}
else if ([[messageItem media] isKindOfClass:[TSAnimatedAdapter class]]) {
// Show animated image full-screen
TSAnimatedAdapter* messageMedia = (TSAnimatedAdapter*)[messageItem media];
tappedImage = ((UIImageView*)[messageMedia mediaView]).image;
CGRect convertedRect = [self.collectionView convertRect:[collectionView cellForItemAtIndexPath:indexPath].frame toView:nil];
__block TSAttachment* attachment = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction* transaction) {
attachment = [TSAttachment fetchObjectWithUniqueID:messageMedia.attachmentId transaction:transaction];
}];
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
TSAttachmentStream* attStream = (TSAttachmentStream*)attachment;
FullImageViewController* vc = [[FullImageViewController alloc] initWithAttachment:attStream
fromRect:convertedRect
forInteraction:[self interactionAtIndexPath:indexPath]
isAnimated:YES];
[vc presentFromViewController:self.navigationController];
}
}
else if([[messageItem media] isKindOfClass:[TSVideoAttachmentAdapter class]]){
// fileurl disappeared should look up in db as before. will do refactor
// full screen, check this setup with a .mov
@ -1272,11 +1296,44 @@ typedef enum : NSUInteger {
[self sendQualityAdjustedAttachment:videoURL];
}
else {
UIImage *picture_camera = [[info objectForKey:UIImagePickerControllerOriginalImage] normalizedImage];
if(picture_camera) {
DDLogVerbose(@"Sending picture attachement ...");
[self sendMessageAttachment:[self qualityAdjustedAttachmentForImage:picture_camera] ofType:@"image/jpeg"];
}
// Send image as NSData to accommodate both static and animated images
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library assetForURL:[info objectForKey:UIImagePickerControllerReferenceURL]
resultBlock:^(ALAsset *asset)
{
ALAssetRepresentation *representation = [asset defaultRepresentation];
Byte *img_buffer = (Byte*)malloc((unsigned long)representation.size);
NSUInteger length_buffered = [representation getBytes:img_buffer fromOffset:0 length:(unsigned long)representation.size error:nil];
NSData *img_data = [NSData dataWithBytesNoCopy:img_buffer length:length_buffered];
NSString *file_type;
switch (img_buffer[0])
{
case 0x89:
file_type = @"image/png";
break;
case 0x47:
file_type = @"image/gif";
break;
case 0x49:
case 0x4D:
file_type = @"image/tiff";
break;
case 0x42:
file_type = @"@image/bmp";
break;
case 0xFF:
default:
file_type = @"image/jpeg";
break;
}
DDLogVerbose(@"Sending image. Size in bytes: %lu; first byte: %02x (%c); detected filetype: %@", (unsigned long)length_buffered, img_buffer[0], img_buffer[0], file_type);
[self sendMessageAttachment:img_data ofType:file_type];
}
failureBlock:^(NSError *error)
{
DDLogVerbose(@"Couldn't get image asset: %@", error);
}
];
}
}

@ -0,0 +1,23 @@
//
// TSAnimatedAdapter.h
// Signal
//
// Created by Mike Okner (@mikeokner) on 2015-09-01.
// Copyright (c) 2015 Open Whisper Systems. All rights reserved.
//
#import <JSQMessagesViewController/JSQPhotoMediaItem.h>
#import "TSAttachmentStream.h"
#import <Foundation/Foundation.h>
@interface TSAnimatedAdapter : JSQMediaItem
- (instancetype)initWithAttachment:(TSAttachmentStream*)attachment;
- (BOOL)isImage;
- (BOOL)isAudio;
- (BOOL)isVideo;
@property NSString *attachmentId;
@end

@ -0,0 +1,144 @@
//
// TSAnimatedAdapter.m
// Signal
//
// Created by Mike Okner (@mikeokner) on 2015-09-01.
// Copyright (c) 2015 Open Whisper Systems. All rights reserved.
//
#import "TSAnimatedAdapter.h"
#import "UIDevice+TSHardwareVersion.h"
#import "JSQMessagesMediaViewBubbleImageMasker.h"
#import "FLAnimatedImage.h"
@interface TSAnimatedAdapter ()
@property (strong, nonatomic) UIImageView *cachedImageView;
@property (strong, nonatomic) NSData *fileData;
@property (strong, nonatomic) UIImage *image;
@property (strong, nonatomic) TSAttachmentStream *attachment;
@end
@implementation TSAnimatedAdapter
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment {
self = [super init];
if (self) {
_cachedImageView = nil;
_attachment = attachment;
_attachmentId = attachment.uniqueId;
_image = [attachment image];
_fileData = [NSData dataWithContentsOfURL:[attachment mediaURL]];
}
return self;
}
- (void)dealloc
{
_cachedImageView = nil;
_attachment = nil;
_attachmentId = nil;
_image = nil;
_fileData = nil;
}
- (void)clearCachedMediaViews
{
[super clearCachedMediaViews];
_cachedImageView = nil;
}
- (void)setAppliesMediaViewMaskAsOutgoing:(BOOL)appliesMediaViewMaskAsOutgoing
{
[super setAppliesMediaViewMaskAsOutgoing:appliesMediaViewMaskAsOutgoing];
_cachedImageView = nil;
}
#pragma mark - JSQMessageMediaData protocol
- (UIView *)mediaView
{
if (self.cachedImageView == nil) {
// Use Flipboard FLAnimatedImage library to display gifs
FLAnimatedImage *animatedGif = [FLAnimatedImage animatedImageWithGIFData:self.fileData];
FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init];
imageView.animatedImage = animatedGif;
CGSize size = [self mediaViewDisplaySize];
imageView.frame = CGRectMake(0.0, 0.0, size.width, size.height);
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.clipsToBounds = YES;
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:imageView isOutgoing:self.appliesMediaViewMaskAsOutgoing];
self.cachedImageView = imageView;
}
return self.cachedImageView;
}
- (CGSize)mediaViewDisplaySize
{
return [self getBubbleSizeForImage:self.image];
}
-(BOOL)isImage {
return YES;
}
-(BOOL) isAudio {
return NO;
}
-(BOOL) isVideo {
return NO;
}
#pragma mark - Utility
-(CGSize)getBubbleSizeForImage:(UIImage*)image
{
CGFloat aspectRatio = image.size.height / image.size.width ;
if ([[UIDevice currentDevice] isiPhoneVersionSixOrMore])
{
return [self getLargeSizeForAspectRatio:aspectRatio];
} else {
return [self getSmallSizeForAspectRatio:aspectRatio];
}
}
-(CGSize)getLargeSizeForAspectRatio:(CGFloat)ratio
{
return ratio > 1.0f ? [self largePortraitSize] : [self largeLandscapeSize];
}
-(CGSize)getSmallSizeForAspectRatio:(CGFloat)ratio
{
return ratio > 1.0f ? [self smallPortraitSize] : [self smallLandscapeSize];
}
- (CGSize)largePortraitSize
{
return CGSizeMake(220.0f, 310.0f);
}
- (CGSize)smallPortraitSize
{
return CGSizeMake(150.0f, 210.0f);
}
- (CGSize)largeLandscapeSize
{
return CGSizeMake(310.0f, 220.0f);
}
- (CGSize)smallLandscapeSize
{
return CGSizeMake(210.0f, 150.0f);
}
@end

@ -16,6 +16,7 @@
#import "TSErrorMessage.h"
#import "TSAttachmentPointer.h"
#import "TSVideoAttachmentAdapter.h"
#import "TSAnimatedAdapter.h"
@interface TSMessageAdapter ()
@ -99,7 +100,12 @@
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
TSAttachmentStream *stream = (TSAttachmentStream*)attachment;
if ([stream isImage]) {
if ([stream isAnimated]) {
adapter.mediaItem = [[TSAnimatedAdapter alloc] initWithAttachment:stream];
adapter.mediaItem.appliesMediaViewMaskAsOutgoing = [interaction isKindOfClass:[TSOutgoingMessage class]];
break;
}
else if ([stream isImage]) {
adapter.mediaItem = [[TSPhotoAdapter alloc] initWithAttachment:stream];
adapter.mediaItem.appliesMediaViewMaskAsOutgoing = [interaction isKindOfClass:[TSOutgoingMessage class]];
break;

Loading…
Cancel
Save