Open-sourcing PINRemoteImage
This week we announced the open-sourcing of PINRemoteImage, a library we use to download more than three billion images to the Pinterest iOS app every day and quickly render them back to tens of millions of Pinners. So if you have an image-heavy app, instead of writing new code, this technology allows you to download, cache (on disk and memory) and prefetch images to reliably display them to your users. Unlike other similar technologies, PINRemoteImage maintains the following features while also avoiding deadlocks, and can even improve the experience on slower connections.
PINRemoteImage features include:
Download and processing priority
Image decompression off the main thread
Attractive blurred progressive JPEGs
Image post-processing
Automatically choose and download different images depending on recent network conditions
Optional support of FLAnimatedImage (Flipboard’s GIF library)
Optional support for WebP
Never blocks on the main thread
Motivation
Displaying remote images is a core part of Pinterest’s functionality, so the download and display of images need to be easy and efficient. There are many existing libraries that offer this type of functionality, but we found they often had limitations such as deadlocking when large numbers of images were requested at once or efficiencies that can only be gained if all images downloaded are the same size. PINRemoteImage was born out of the need for an efficient, thread-safe image downloading library.
How it works
To download an image and set it on a UIImageView, the easiest thing to do is use the built-in category method setImageFromURL:. Calling this takes care of everything from downloading the image to caching it and setting it on the UIImageView. There are several categories already included with PINRemoteImage, but writing a new category is made easy by implementing a defined protocol, PINRemoteImageCategory. Implementing this protocol is further simplified: almost all instance methods call matching class methods on PINRemoteImageCategory.
If you’d rather use the image directly you can call methods on a shared instance of PINRemoteImageManager, which is the workhorse class of PINRemoteImage. It will coalesce downloads, cache fetches and image processing operations so no matter how many times you request an image to be downloaded, the download and disk cache fetches will only happen once. It can also be used if you need to modify options which may not be exposed on the UIView categories.
All caching to disk and memory is handled by PINCache, our open-source object caching library.
Processing images
PINRemoteImage also supports processing images and storing the resulting image in the image cache. For example, the code below would download an image, resize it and apply a corner radius:
[imageView setImageFromURL:heroURL processorKey:@"rounded-hero" processor:^UIImage *(PIRemoteImageManagerResult *result, NSUInteger *cost) { UIImage *image = result.image; CGFloat radius = 7.0f; CGSize targetSize = CGSizeMake(100, 100); CGRect imageRect = CGRectMake(0, 0, targetSize.width, targetSize.height); UIGraphicsBeginImageContext(imageRect.size); [[UIBezierPath bezierPathWithRoundedRect:imageRect cornerRadius:radius] addClip]; CGFloat widthMultiplier = targetSize.width / image.size.width; CGFloat heightMultiplier = targetSize.height / image.size.height; CGFloat sizeMultiplier = MAX(widthMultiplier, heightMultiplier); CGRect drawRect = CGRectMake(0, 0, image.size.width * sizeMultiplier, image.size.height * sizeMultiplier); if (CGRectGetMaxX(drawRect) > CGRectGetMaxX(imageRect)) { drawRect.origin.x -= (CGRectGetMaxX(drawRect) - CGRectGetMaxX(imageRect)) / 2.0; } if (CGRectGetMaxY(drawRect) > CGRectGetMaxY(imageRect)) { drawRect.origin.y -= (CGRectGetMaxY(drawRect) - CGRectGetMaxY(imageRect)) / 2.0; } [image drawInRect:drawRect]; UIImage *processedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return processedImage; }];
This method could be called multiple times, and the image would only be processed once.
Combatting slow connections
PINRemoteImage has several methods for improving the experience of downloading images on slow connections, including support for Google’s WebP format. If WebP is available (by including the webp CocoaPod, for example), PINRemoteImage will detect WebP downloads and decode them appropriately with the WebP library:
[imageView setImageFromURL:[NSURL URLWithString:@”webPImage.webp”]];
Alternatively, if you’re downloading progressive JPEGs, PINRemoteImage supports displaying progressive scans of the image with an attractive blur applied (more attractive when not filtered through a GIF as below, as this feature is only available on iOS 8 and above). To support progressive scans, enable updates on a UIImageView by calling setUpdateWithProgress:YES, or use one of the progressive methods on PINRemoteImageManager:
[imageView setUpdateWithProgress:YES]; [imageView setImageFromURL[NSURLURLWithString:@”progressiveJPEG.jpg”]];
A final method for gracefully supporting slow connections is to provide the library with several URLs of varying quality.
[imageView setImageFromURLs:@[[NSURL URLWithString:@"lowQualityURL"], [NSURL URLWithString:@"mediumQualityURL"], [NSURL URLWithString:@"highQualityURL"]]];
PINRemoteImage will download one of the images based on most recently observed network  conditions. So if the last few images download slowly, it’ll choose a lower quality image to download. If you’d like to upgrade these images to a higher quality version, you can tell the image manager it should upgrade low quality images and set the image URLs on the image view again:
[[PINRemoteImageManager sharedManager] setShouldUpgradeLowQualityImages:YES completion:nil]; [imageView setImageFromURLs:@[[NSURL URLWithString:@"lowQualityURL"], [NSURL URLWithString:@"mediumQualityURL"], [NSURL URLWithString:@"highQualityURL"]]];
Support for FLAnimatedImage
PINRemoteImage also has native support for FLAnimatedImage, a memory efficient GIF decoder and player. If you have an instance of an FLAnimatedImageView, downloading and setting the GIF is as easy as on a UIImageView:
[flaAnimatedImageView setImageFromURL:[NSURLURLWithString:@”gifURL.gif”];
Contributing to PINRemoteImage
As with our other open-source contributions, we believe that the community can make PINRemoteImage even better. Whether it be through pull requests or bug reports, we’re excited to make PINRemoteImage better. To get started, check out our GitHub repository where you can see how to install and use PINRemoteImage, as well as play with a sample app (it’s full of kittens).
Garrett Moon is a software engineer on the iOS Team.
For Pinterest engineering news and updates, follow our engineering Pinterest, Facebook and Twitter. Interested in joining the team? Check out our Careers site.
// <![CDATA[ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); ga('create', 'UA-47193001-1', 'pinterest.com'); ga('send', 'pageview'); // ]]>














