Skip to content

Commit 5ac4bc2

Browse files
authored
V.2.24.2 (#60)
* Update WebP codec * fix doc/spec versions
1 parent 75e76a2 commit 5ac4bc2

36 files changed

+281
-156
lines changed

BuildConfiguration/TwitterImagePipeline.Test.xcconfig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ MACOSX_DEPLOYMENT_TARGET = 10.15
2525
// Linking
2626
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks
2727

28+
// 1. CoreServices is not available until iOS 12
29+
// 2. in Xcode11 going forward, including MobileCoreServices in the Build Phase
30+
// `Link Binary With Libraries` results in a warning.
31+
//
32+
// workaround: remove from that Build Phase and link using this -framework MobileCoreServices arg
33+
// TODO: when min deployment target >= 12.0: delete this & place CoreServices in 'Link Binary With Libraries`
34+
OTHER_LDFLAGS = $(inherited) -framework MobileCoreServices
35+
2836
// Packaging
2937
INFOPLIST_FILE = $(TARGET_NAME)/Info.plist
3038
PRODUCT_BUNDLE_IDENTIFIER = com.twitter.${PRODUCT_NAME:rfc1034identifier}

BuildConfiguration/TwitterImagePipeline.framework.xcconfig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ DYLIB_CURRENT_VERSION = $(CURRENT_PROJECT_VERSION)
2626
DYLIB_INSTALL_NAME_BASE = @rpath
2727
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks
2828

29+
// 1. CoreServices is not available until iOS 12
30+
// 2. in Xcode11 going forward, including MobileCoreServices in the Build Phase
31+
// `Link Binary With Libraries` results in a warning.
32+
//
33+
// workaround: remove from that Build Phase and link using this -framework MobileCoreServices arg
34+
// TODO: when min deployment target >= 12.0: delete this & place CoreServices in 'Link Binary With Libraries`
35+
OTHER_LDFLAGS = $(inherited) -framework MobileCoreServices
36+
2937
// Packaging
3038
DEFINES_MODULE = YES
3139
INFOPLIST_FILE = $(PROJECT_NAME)/Info.plist

BuildConfiguration/TwitterImagePipeline.xcconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES
7979
CLANG_WARN_INFINITE_RECURSION = YES
8080
CLANG_WARN_INT_CONVERSION = YES
8181
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES
82-
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES
82+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES_ERROR
8383
CLANG_WARN_STRICT_PROTOTYPES = YES
8484
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES
8585
CLANG_WARN_UNREACHABLE_CODE = YES

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22

33
## Info
44

5-
**Document version:** 2.24.1
5+
**Document version:** 2.24.2
66

7-
**Last updated:** 10/20/2020
7+
**Last updated:** 10/22/2020
88

99
**Author:** Nolan O'Brien
1010

1111
## History
1212

13+
### 2.24.2
14+
15+
- Fix WebP decoder for animations
16+
- Complex animations were not properly being decoded
17+
1318
### 2.24.1 - Liam Nichols
1419

1520
- Add MP4 and WebP subspecs for CocoaPods

COCOAPODS.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ To integrate TIP into your iOS project using CocoaPods, simply add the following
66

77
```ruby
88
target 'MyApp' do
9-
pod 'TwitterImagePipeline', '~> 2.24.1'
9+
pod 'TwitterImagePipeline', '~> 2.24.2'
1010
end
1111
```
1212

@@ -23,13 +23,13 @@ If you wish to include these codecs, modify your **Podfile** to define the appro
2323

2424
```ruby
2525
target 'MyApp' do
26-
pod 'TwitterImagePipeline', '~> 2.24.1', :subspecs => ['WebPCodec/Default']
26+
pod 'TwitterImagePipeline', '~> 2.24.2', :subspecs => ['WebPCodec/Default']
2727

28-
pod 'TwitterImagePipeline', '~> 2.24.1', :subspecs => ['WebPCodec/Animated']
28+
pod 'TwitterImagePipeline', '~> 2.24.2', :subspecs => ['WebPCodec/Animated']
2929

30-
pod 'TwitterImagePipeline', '~> 2.24.1', :subspecs => ['MP4Codec']
30+
pod 'TwitterImagePipeline', '~> 2.24.2', :subspecs => ['MP4Codec']
3131

32-
pod 'TwitterImagePipeline', '~> 2.24.1', :subspecs => ['WebPCodec/Animated', 'MP4']
32+
pod 'TwitterImagePipeline', '~> 2.24.2', :subspecs => ['WebPCodec/Animated', 'MP4']
3333
end
3434
```
3535

Extended/TIPXWebPCodec.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ NS_ASSUME_NONNULL_BEGIN
2828

2929
/**
3030
Initializer
31-
@param preserve Pass `YES` to preserve the default system encoder and/or decoder if possible. Pass `NO` to use the `TIPXWebPCodec` implementations for both encoder and decoder.
31+
@param preferredCodec Pass the default system encoder and/or decoder if possible. If they are not provided (including if a nil `tip_decoder` or `tip_encoder` are found), use the `TIPXWebPCodec` implementations.
3232
@return a new `TIPXWebPCodec` instance
3333
*/
34-
- (instancetype)initPreservingDefaultCodecsIfPresent:(BOOL)preserve NS_DESIGNATED_INITIALIZER;
34+
- (instancetype)initWithPreferredCodec:(nullable id<TIPImageCodec>)preferredCodec NS_DESIGNATED_INITIALIZER;
3535
- (instancetype)init NS_UNAVAILABLE;
3636

3737
/** Convenience check to see if animation decoding was compiled */

Extended/TIPXWebPCodec.m

Lines changed: 112 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
#pragma mark WebP includes
1818

19+
// TODO: remove this pushed diagnostic "ignored" when the modular closure of libwebp framework headers is completely fixed
20+
#pragma clang diagnostic push
21+
#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header"
22+
1923
#if TARGET_OS_MACCATALYST
2024
#import <webp/webp.h>
2125
#if TIPX_WEBP_ANIMATION_DECODING_ENABLED
@@ -30,14 +34,19 @@
3034
#endif
3135
#endif
3236

37+
#pragma clang diagnostic pop
38+
39+
3340
NS_ASSUME_NONNULL_BEGIN
3441

3542
#pragma mark - Declarations
3643

3744
static UIImage * __nullable TIPXWebPRenderImage(NSData *dataBuffer,
3845
CGSize sourceDimensions,
3946
CGSize targetDimensions,
40-
UIViewContentMode targetContentMode);
47+
UIViewContentMode targetContentMode,
48+
CGRect framingRect,
49+
CGContextRef __nullable canvas);
4150
static UIImage * __nullable TIPXWebPConstructImage(CGDataProviderRef dataProvider,
4251
const size_t width,
4352
const size_t height,
@@ -84,20 +93,14 @@ @implementation TIPXWebPCodec
8493
- (instancetype)init
8594
{
8695
// Shouldn't be called, but will permit in case of type erasure
87-
return [self initPreservingDefaultCodecsIfPresent:NO];
96+
return [self initWithPreferredCodec:nil];
8897
}
8998

90-
- (instancetype)initPreservingDefaultCodecsIfPresent:(BOOL)preserve
99+
- (instancetype)initWithPreferredCodec:(nullable id<TIPImageCodec>)preferredCodec
91100
{
92101
if (self = [super init]) {
93-
if (preserve) {
94-
id<TIPImageCodec> webpCodec = [TIPImageCodecCatalogue defaultCodecs][TIPImageTypeWEBP];
95-
_tip_decoder = webpCodec.tip_decoder ?: [[TIPXWebPDecoder alloc] init];
96-
_tip_encoder = webpCodec.tip_encoder ?: [[TIPXWebPEncoder alloc] init];
97-
} else {
98-
_tip_decoder = [[TIPXWebPDecoder alloc] init];
99-
_tip_encoder = [[TIPXWebPEncoder alloc] init];
100-
}
102+
_tip_decoder = preferredCodec.tip_decoder ?: [[TIPXWebPDecoder alloc] init];
103+
_tip_encoder = preferredCodec.tip_encoder ?: [[TIPXWebPEncoder alloc] init];
101104
}
102105
return self;
103106
}
@@ -392,7 +395,9 @@ - (nullable TIPImageContainer *)_renderStaticImage:(TIPImageDecoderRenderMode)re
392395
UIImage *image = TIPXWebPRenderImage(_dataBuffer,
393396
_tip_dimensions,
394397
targetDimensions,
395-
targetContentMode);
398+
targetContentMode,
399+
CGRectZero,
400+
NULL);
396401
if (image) {
397402
_cachedImageContainer = [[TIPImageContainer alloc] initWithImage:image];
398403
[self _cleanup];
@@ -463,17 +468,57 @@ - (nullable TIPImageContainer *)_renderAnimatedImage:(TIPImageDecoderRenderMode)
463468
// Go through the animation and pull out the frames (stopping after the first frame if `justFirstFrame` is `YES`)
464469
NSTimeInterval totalDuration = 0;
465470
const NSUInteger loopCount = (NSUInteger)WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT);
471+
#if DEBUG
472+
const CGSize canvasDimensions = CGSizeMake(WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH), WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT));
473+
NSCParameterAssert(canvasDimensions.width == _tip_dimensions.width);
474+
NSCParameterAssert(canvasDimensions.height == _tip_dimensions.height);
475+
#endif
476+
const CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast;
477+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
478+
TIPXDeferRelease(colorSpace);
479+
CGContextRef canvas = CGBitmapContextCreate(NULL /*data*/,
480+
(size_t)_tip_dimensions.width,
481+
(size_t)_tip_dimensions.height,
482+
8,
483+
4 * (size_t)_tip_dimensions.width,
484+
colorSpace,
485+
bitmapInfo);
486+
TIPXDeferRelease(canvas);
487+
CGContextClearRect(canvas, (CGRect){ .origin = CGPointZero, .size = _tip_dimensions });
466488
NSMutableArray<UIImage *> *frames = [[NSMutableArray alloc] initWithCapacity:(NSUInteger)iter->num_frames];
467489
NSMutableArray<NSNumber *> *frameDurations = [[NSMutableArray alloc] initWithCapacity:(NSUInteger)iter->num_frames];
468490
do {
469491

470492
NSData *fragment = [[NSData alloc] initWithBytesNoCopy:(void*)iter->fragment.bytes
471493
length:iter->fragment.size
472494
freeWhenDone:NO];
495+
const CGRect framingRect = CGRectMake(iter->x_offset,
496+
iter->y_offset,
497+
iter->width,
498+
iter->height);
499+
// CGBitmapContext is bottem-left aligned instead of top-left aligned, so adjust the origin for that use case
500+
const CGRect canvasFramingRect = CGRectMake(framingRect.origin.x,
501+
_tip_dimensions.height - framingRect.size.height - framingRect.origin.y,
502+
framingRect.size.width,
503+
framingRect.size.height);
504+
505+
const BOOL isSizedToCanvas = CGSizeEqualToSize(framingRect.size, _tip_dimensions) && CGPointEqualToPoint(framingRect.origin, CGPointZero);
506+
507+
if (iter->blend_method == WEBP_MUX_NO_BLEND) {
508+
// clear the area we are about to draw to
509+
CGContextClearRect(canvas, canvasFramingRect);
510+
}
473511
UIImage *frame = TIPXWebPRenderImage(fragment,
474512
_tip_dimensions,
475-
justFirstFrame ? targetDimensions : CGSizeZero,
476-
targetContentMode);
513+
(justFirstFrame && isSizedToCanvas) ? targetDimensions : CGSizeZero,
514+
targetContentMode,
515+
framingRect,
516+
canvas);
517+
if (iter->dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
518+
// clear the area we just finished drawing to
519+
CGContextClearRect(canvas, canvasFramingRect);
520+
}
521+
477522
if (!frame) {
478523
return nil;
479524
}
@@ -546,7 +591,9 @@ - (void)_cleanup TIPX_OBJC_DIRECT
546591
static UIImage *TIPXWebPRenderImage(NSData *dataBuffer,
547592
CGSize sourceDimensions,
548593
CGSize targetDimensions,
549-
UIViewContentMode targetContentMode)
594+
UIViewContentMode targetContentMode,
595+
CGRect framingRect,
596+
CGContextRef __nullable canvas)
550597
{
551598
__block WebPDecoderConfig* config = (WebPDecoderConfig*)WebPMalloc(sizeof(WebPDecoderConfig));
552599
tipx_defer(^{
@@ -558,15 +605,32 @@ - (void)_cleanup TIPX_OBJC_DIRECT
558605
return nil;
559606
}
560607

561-
const CGSize scaledDimensions = TIPDimensionsScaledToTargetSizing(sourceDimensions,
562-
targetDimensions,
563-
targetContentMode);
564-
if (!CGSizeEqualToSize(scaledDimensions, sourceDimensions)) {
565-
config->options.scaled_width = (int)scaledDimensions.width;
566-
config->options.scaled_height = (int)scaledDimensions.height;
567-
config->options.use_scaling = 1;
568-
// should we stop fancy upscaling? config.options.no_fancy_upsampling = 1;
608+
BOOL isAligned = canvas != NULL; // having a canvas indicates an animation, always treat fragment as being "aligned"
609+
if (!isAligned) {
610+
// is aligned if the origin is offset
611+
isAligned = !CGPointEqualToPoint(framingRect.origin, CGPointZero);
612+
if (!isAligned) {
613+
// is aligned if the size is not the canvas size (nor zero size)
614+
isAligned = !CGSizeEqualToSize(framingRect.size, sourceDimensions)
615+
&& !CGSizeEqualToSize(framingRect.size, CGSizeZero);
616+
}
569617
}
618+
619+
if (!isAligned) {
620+
const CGSize scaledDimensions = TIPDimensionsScaledToTargetSizing(sourceDimensions,
621+
targetDimensions,
622+
targetContentMode);
623+
if (!CGSizeEqualToSize(scaledDimensions, sourceDimensions)) {
624+
config->options.scaled_width = (int)scaledDimensions.width;
625+
config->options.scaled_height = (int)scaledDimensions.height;
626+
config->options.use_scaling = 1;
627+
// should we stop fancy upscaling? config.options.no_fancy_upsampling = 1;
628+
}
629+
framingRect.origin = CGPointZero;
630+
framingRect.size = scaledDimensions;
631+
}
632+
633+
// Set the output colorspace as RGB (TODO: there might be a device optimization using BGRA or ABGR...)
570634
config->output.colorspace = MODE_RGBA;
571635

572636
if (VP8_STATUS_OK != WebPDecode(dataBuffer.bytes, dataBuffer.length, config)) {
@@ -593,11 +657,31 @@ - (void)_cleanup TIPX_OBJC_DIRECT
593657
return nil;
594658
}
595659

596-
return TIPXWebPConstructImage(provider,
597-
(size_t)configLongLived->output.width,
598-
(size_t)configLongLived->output.height,
599-
componentsPerPixel,
600-
bytesPerPixel);
660+
UIImage *image = TIPXWebPConstructImage(provider,
661+
(size_t)configLongLived->output.width,
662+
(size_t)configLongLived->output.height,
663+
componentsPerPixel,
664+
bytesPerPixel);
665+
if (image && isAligned) {
666+
667+
if (canvas) {
668+
framingRect.origin.y = sourceDimensions.height - framingRect.size.height - framingRect.origin.y;
669+
CGContextDrawImage(canvas, framingRect, image.CGImage);
670+
CGImageRef imageRef = CGBitmapContextCreateImage(canvas);
671+
TIPXDeferRelease(imageRef);
672+
image = [UIImage imageWithCGImage:imageRef];
673+
} else {
674+
UIGraphicsBeginImageContextWithOptions(sourceDimensions, !configLongLived->input.has_alpha, 1.0);
675+
tipx_defer(^{
676+
UIGraphicsEndImageContext();
677+
});
678+
CGContextClearRect(UIGraphicsGetCurrentContext(), (CGRect){ .origin = CGPointZero, .size = sourceDimensions });
679+
[image drawInRect:framingRect];
680+
image = UIGraphicsGetImageFromCurrentImageContext();
681+
}
682+
683+
}
684+
return image;
601685
}
602686

603687
@end

ImageSpeedComparison/AppDelegate.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ @implementation AppDelegate
2222
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
2323
{
2424
[TIPGlobalConfiguration sharedInstance].logger = self;
25-
[TIPImageCodecCatalogue sharedInstance][TIPImageTypeWEBP] = [[TIPXWebPCodec alloc] initPreservingDefaultCodecsIfPresent:YES];
25+
[[TIPImageCodecCatalogue sharedInstance] replaceCodecForImageType:TIPImageTypeWEBP usingBlock:^id<TIPImageCodec> _Nonnull(id<TIPImageCodec> _Nullable existingCodec) {
26+
return [[TIPXWebPCodec alloc] initWithPreferredCodec:nil];//existingCodec];
27+
}];
2628
return YES;
2729
}
2830

ImageSpeedComparison/ViewController.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
{ @"public.tiff", "TIFF", "twitterfied.tiff", NO, NO },
3030
{ @"com.compuserve.gif", "GIF", "fireworks_original.gif", NO, YES },
3131
{ @"org.webmproject.webp", "WEBP", "twitterfied.webp", NO, NO },
32-
{ @"org.webmproject.webp", "Ani-WEBP", "fireworks_original.webp", NO, YES },
32+
//{ @"org.webmproject.webp", "Ani-WEBP", "fireworks_original.webp", NO, YES },
33+
{ @"org.webmproject.webp", "Ani-WEBP", "tenor_test.webp", NO, YES },
3334
// { @"com.compuserve.gif", "Static GIF", "https://media3.giphy.com/media/d3F2Dj8zECyDLFpm/v1.Y2lkPWU4MjZjOWZjOGViZWNhZmJmMjk0NDIyZGQzZjM2ZjhkMzhlNGRhZTk5OTYzZjliMQ/200_s.gif", NO, NO },
3435
{ @"public.heic", "HEIC", "twitterfied.heic", NO, NO },
3536
// { @"public.heic", "Ani-HEIC", "starfield_animation.heic", NO, YES },

ImageSpeedComparison/tenor_test.webp

24.3 KB
Binary file not shown.

0 commit comments

Comments
 (0)