Scalable iOS graphics (PDFImage.framework)

Guides | Thoughts By 4 years ago

Over the Christmas holidays I was building an app in my own time and came across a bit of a common problem I wanted to solve – multiple PNG exports all at different sizes for graphics in an iOS app.

My app had a menu with nice icons on it. In portrait on the iPhone the icons were a certain size, then in landscape on the iPhone they were a different size again. On the iPad in portrait they were another size, and as you could have guessed another size for iPad in landscape. This meant to make sure my images were nice and clean with no stretched scaling I would need 4 versions of the same PNG all at different sizes. Of course I also support non retina devices, so that’s now 8 versions of the same PNG all for just one menu item. I have roughly 10 menu items, that’s 80 image cuts, not a fun job… not to mention the file size of all these images.

So how to fix this? The answer is obviously using vector graphics, which if you don’t know, don’t contain actual pixel information like a PNG, but instead a set of mathematical curves and points so the object can be dynamically drawn at any size with no quality loss. If I could use vector graphics in my app then that would mean just one vector graphic per menu item, and I could scale it to any size, and even change sizes in the future without needing to revisit Photoshop.

PaintCode

A while back somebody had told me about PaintCode, an app that let’s you import a PSD and will give you the Objective-C code required to draw that object on screen. While I found this worked okay, it had some drawbacks.

For one, the code it generated wasn’t perfect. I often needed to change it a tad to have it work in my situation. PaintCode loves to use absolute values for things like frame sizes, positions and font sizes, making it not so dynamic when scaling up and down. The on screen result is pretty good, but can be slightly off if your designers do something a bit out of the ordinary, as it is limited to CoreGraphics API calls.

Also I don’t really like the idea of having graphics stuff in code. It makes for a lightweight app, but any time you want to change the graphics you need to reopen your IDE and recompile the source – and vector files are mere kilobytes anyway.

Vector files

I wanted to make a drop in framework that would easily handle a vector file format. I needed to pick which file format to support.

There were two popular formats I considered, PDF and SVG. Now PDFs are primarily for documents, but internally unless you explicitly use bitmaps everything is vector anyway.

SVGs are nice and lightweight, however when they start getting complicated there can be drawing anomalies. What I like about PDFs is they seem to be very strict in terms of drawing standards – especially on the Mac and iOS, they draw perfectly. I ended up choosing PDF as my supported vector file format, they are also quite easy to create from Photoshop and Illustrator.

PDFImage.framework

After a night I cooked up the majority of my PDFImage.framework. It’s open source, you can download it on GitHub and use it freely in any project.

I said this on my GitHub page but I’ll reinforce it: this is not a full PDF rendering library. If you want to render full document pages then look for something else. This is simply for small UI graphics (and doesn’t even give the option to render anything past page 1 if you tried).

First some familiar syntax, showing a PNG file on screen named email.png.

UIImage* image = [UIImage imageNamed:@"email"];

UIImageView* imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[imageView setImage:image];
[imageView setContentMode:UIViewContentModeScaleAspectFit];
[view addSubview:imageView];

When choosing PDF, I also considered how NSImage on the Mac loads and displays PDF files for scalable graphics. Eventually I think iOS may follow the same and UIImage will be able to load up PDF files. Because of this I designed my API to be extremely similar to UIImage and UIImageView so that when that time comes you can potentially strip out my framework with minimal effort and throw it away.

To do the exact same thing as above but with a PDF file using PDFImage.framework, simply replace UIImage for PDFImage and UIImageView for PDFImageView and you will display the email.pdf file on screen.

PDFImage* image = [PDFImage imageNamed:@"email"];

PDFImageView* imageView = [[PDFImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[imageView setImage:image];
[imageView setContentMode:UIViewContentModeScaleAspectFit];
[view addSubview:imageView];

In a nutshell, if you use the PDFImageView, it will ensure you are always drawing at a pixel perfect level.

See the GitHub page for more code examples, or download and run the demo for visual examples of the result.

PDFImage.framework features

PDFBarButtonItem for navigation buttons
NSCaching (in memory caching) for low overhead when loading and redrawing the same content
– Localized PDF loading support
– Tint color support for a solid overlay onto the PDF, for reusing the same colorized shape
– Automatic smooth animations when animating the PDFImageView, content is redrawn with low overhead while maintaining a smooth animation
PDFImage class can easily generate a usable UIImage instance
– All UIViewContentModes supported by PDFImageView

Integrating the framework

To use the framework in your code, first download PDFImage.framework. Unzip the package and drag the PDFImage.framework package to your Xcode project. When presented with linking options, check every target you wish to use the framework in.

  

 

Now put #import <PDFImage/PDFImage.h> in your headers wherever you want to use the framework. That’s it, you’re good to go.

Saving a PDF from Photoshop

When designing your icons and graphics, make sure everything is vector and made with paths or shape layers.

In the Photoshop menu, choose File > Save As…, then choose “Photoshop PDF” from the drop down. Proceed to save.

If a layer is an embedded smart object (like from an Illustrator file) then using the method above, Photoshop will make it a bitmap, not what you want. If this is the case then right click on the smart object layer and choose “Export Contents…”, this will save as a vector PDF file.

Smaller PDF file sizes

Photoshop will likely include a lot of metadata and other information inside the PDF file, a lot of information that isn’t actually needed to draw the thing.

I was looking into PDF file size reducer tools. I found one by Panic Software called ShrinkIt, however this sometimes messed up the cropping of the document. I set out to make my own, and I even found a little PDF saving trick that made my files even smaller than what ShrinkIt could do, and even better than a $5 app I found on the Mac AppStore.

This one is also open source on GitHub, download PDFShaver.app and drag your PDFs in to make them a fraction of the size.

  • Adam ✈ Kaplan

    Great write-up, Thanks Tom. This all sounds pretty good and you saved me from going down the same ugly paths that led you to this point. I’m going to play with your project a bit. The code looks tidy.

    Not sure about the separate alloc & inits for “strict typing”, but that’s beside the point 😛

    • Tom

      If I do the [[self alloc] init…] in two parts and I change my init method without changing the [[self alloc] init…] part then I get a compile time error about the missing method.

      If it was done in one part then [self alloc] has a return type of id, and something common such as initWithContentsOfFile: which exists in other classes would only error at runtime when it can’t find that init method on the PDFImage class. Two parts is simply just to add some compile time checks 🙂

      I guess the same could be done with [(PDFImage*)[self alloc] initWithContentsOfFile:…]

      • Adam ✈ Kaplan

        Yes, that is true as written & coded. What I meant was to specify the actual class.

        Instead of:
        + (PDFImage*) imageWithContentsOfFile:(NSString*) path {
        PDFImage* alloced = [self alloc];
        return [alloced initWithContentsOfFile:path];
        }

        Try using:
        + (PDFImage*) imageWithContentsOfFile:(NSString*) path {
        return [[PDFImage alloc] initWithContentsOfFile:path];
        }

        Despite the return type of -(id)[NSProxy alloc], it isn’t really a polymorphic method (alloc promises to return self’s class) and the compiler is able deduce the expected instance type. Seems to work fine for me in a quick test. I have only used [self alloc] from within a class cluster.

        Regardless, I was able to get some awesome scalable PDF graphics to render and animate without much effort. Thanks for that!