Smaller Apps with Vector Images
3/26/2012: Added an update at the end of the post.
For the past couple of weeks, I’ve been working on new vector processing code for a future update of my Halftone app for iOS. As I’ve mentioned before, Halftone draws a lot of its graphics using vectors, and as a result, it automatically takes advantage of the new iPad Retina display. These recent experiments have forced me to take a closer look at vector handling in iOS, and I thought I’d share what I’ve learned.
By the end of this post, I hope to convince you that a vector version of your app may require only 14.8% of the space that a bitmap-based Retina version requires.
Before I get going, it’s important to mention that—while I love vectors—I love bitmaps too! Halftone contains a lot of bitmap images, and I spend a lot more time in Photoshop than I do in Illustrator. So don’t walk away thinking that I’m recommending vectors for everything.
That said, here’s how I generally break it down:
- For small images that aren’t photographic, I use PNG images. This includes icons, UI elements, and logos, to name a few. If every pixel and edge matters, a bitmap provides the most control.
- For photographic images, I use JPGs, no matter what the size.
- For large images that aren’t photographic, it depends…
Though the iOS SDK has great support for bitmap image formats like JPG and PNG, it doesn’t include any support for vector image formats like SVG or EPS. To be fair, the UIWebView control can be used in a pinch, but it doesn’t provide access to the raw image data (including its alpha channel), and it’s often a much heavier solution than necessary.
The current version of Halftone draws each vector with Objective-C code that’s been exported directly from Adobe Illustrator using a custom plug-in that I’ve built. This method works well, but it requires that the vector image code is compiled into the app itself before it’s submitted to the App Store. One downside of this method is that there’s no way to enable a Halftone user to add their own vector image files. Nor is there a way to include in-app purchase for vector images (apps are not allowed to download code after-the-fact). These limitations led me to consider a new workflow.
Ever since the advent of HTML5, SVG files have enjoyed a bit of a resurgence (the format was invented in 1999), and they’re supported by all modern web browsers. The two dominant vector editing tools, Adobe Illustrator and the Open Source Inkscape, have excellent support for creating and editing SVG documents. This means that any image that can be opened by those tools can easily be saved to an SVG file.
My goal was clear: I needed to find a SVG parser and renderer for iOS!
After some searching, I found a few projects that purported to handle simple SVG files but none that claimed robust support for the format (at least robust enough). So, I downloaded each project, and it only took a few quick tests to find the rough edges. The existing projects are good enough for basic support, and if that’s all you need, you’ll probably be fine with any of them.
Like any good developer, I have a healthy dose of naïveté. If I had realized what I was getting myself into, I probably wouldn’t have started. As it goes, I’ve spent the last couple of weeks nearly memorizing the SVG specification, and I’ve become more than familiar with the rules of the CSS cascade. Thankfully, I have no need for scripting, external style sheets, events, or any of the fancy text-based support, and I’ve been able to avoid those sections of the spec.
I’ve built a test app that allows me to open an SVG file and display the UIWebView/WebKit-rendered version by holding down a “Show Reference” button. There’s also an “onion skin” mode that fades the UIWebView to 50% so that I can see both renderings simultaneously, though it’s not as useful as flipping back-and-forth with the button. As an aside, does anyone remember those advertisements (perhaps from National Geographic?) where they’d show an elephant, then flip between a normal version and a version that didn’t include its trunk? If memory serves, they’d say something like: “did you see that?” Well, that’s me testing my renders against WebKit; I’m constantly looking for pixels that appear or disappear.
To test my parsing and rendering, I’ve used the SVG test suite. The suite is incredibly useful, as there are many complex rules that—no matter how many times you read the spec—can be ambiguous. Many of the tests use crafty or clever techniques, and they’re quick to expose issues. I’ve also collected a set of common SVG files, and as acid tests, I’ve exported some extremely complex vector images from Adobe Illustrator. Here’s a file from the SVG test suite rendered by my test app on the left and by UIWebView/WebKit on the right:
To test the extremes, here’s the Rise Together sample that ships with Adobe Illustrator CS5. The exported SVG file is a full 21MB (!), and it takes my non-optimized test app a full 30 seconds to parse and render it! As before, my version is on the left, and the UIWebView version is on the right. Not sure what UIWebView is up to here, but I’ll take the “win,” as I usually lose to WebKit.
A Rise Together close-up from a Retina display:
Here’s another acid test that uses the Yellowstone Map sample from Illustrator. It’s not as complex as Rise Together, but it includes a lot of text. While my needs don’t require robust text support, it’s good to see that the test app can cover the basics. As a side note, I’ve noticed that UIWebView appears to ignore some font kerning and ligature information while rendering.
A Yellowstone Map close-up from a Retina display:
Here’s a more reasonable example that illustrates a plant cell structure. It’s easy to imagine diagrams like this being featured in reference or magazine apps:
A Plant Cell Structure close-up from a Retina display:
Finally, here’s a popular lion sample that hits the sweet spot. It parses and renders quickly, and it’s easy to imagine the image (along with its alpha channel) being used as an element or character in an app:
Being vector-based, all of these images can be rendered at any size, and they can easily be transformed (positioned, scaled, rotated, skewed, etc.) without any loss of quality.
Another major benefit of vector-based formats is file size. SVG documents are XML documents, and they’re very compressible. On average, my sample files ended up being around 33% of their original size when packaged in an IPA file (the compressed format that’s used to package iOS apps).
To compare format sizes, I used four versions of eight different SVG test files:
- The original, untouched SVG file.
- A PNG export of the SVG file from Adobe Illustrator (including an alpha channel) that fills the 2,048 x 1,536 pixel dimensions of the new iPad Retina display and preserves its aspect ratio.
- A PNG that fills the 1,024 x 768 pixel dimensions of the standard display on an iPad/iPad 2.
- A compiled stream of Objective-C code that uses Quartz 2D commands to draw the SVG file. The code is exported by my test app after the parsing phase and it represents the steps that would be required by almost any SVG renderer. It’s not completely optimized, but it’s still useful for comparison. Other than loading a bitmap, it’s about as fast as you can get.
To accurately measure the payload of each image as it would be distributed in an app, I started by creating an image-free reference IPA file. Then, one by laborious one, I built individual IPA files for each sample image using the four different data types (that’s a total of 1 + (8 x 4) = 33 IPA files!). Finally, I calculated the difference between the reference IPA file and each IPA + image file to get a measure of its payload in bytes.
Here are the results for the eight sample files (in bytes):
|Giza Pyramid Complex||934,950||281,182||320,211||309,018||327,854||355,424||411,178|
|Plant Cell Structure||189,161||46,420||436,100||432,548||316,653||333,397||184,332|
As you can see from the table, images that are comprised of strokes, fills, and gradients can be represented very efficiently as SVG files and compiled Objective-C. The more complex an image becomes (and therefore, the more instructions it takes to describe the image), the more PNG files start to show their strength. Sometimes, it’s just quicker and easier to store the result of a parse/render than it is to draw an image when you need it. If you’re wondering, yes, I double-checked the Giza PNG numbers.
Remember that if you’re shipping an iPad Retina-capable app, you need to include at least two PNG files for each image: one at standard resolution, and one at Retina resolution. With vector images, you only ever have to store one, and it’ll never change in size. This can be a huge payload win. For apps that share artwork with desktop apps, using vectors could also help to future-proof assets in anticipation of possible Retina support in Mountain Lion.
Here’s a look at the relative payload sizes for each of the sample images:
|Giza Pyramid Complex||281,182||664,442||42.3%|
|Plant Cell Structure||46,420||765,945||6.1%|
The last big issue to consider is performance. Bitmap images are pre-baked and ready to go…just decompress the data and pump pixels to the screen. Vector images require additional parsing, computation, and drawing, and they often take much longer to render. For simple images that don’t need to be available instantly, this can be done in real-time (as examples, both the Butterfly and Lion sample images take about 0.28 seconds to load, parse, and render on a 3rd generation iPad using my non-optimized test app). For images that need to be available instantly, this processing overhead may be unacceptable. One strategy is to parse and render the SVG image the first time, then cache the final bitmap to disk. This way, you get the benefit of shipping a much smaller app, the quality of near-infinite-resolution vector images, and the raw performance of a bitmap.
The compiled Objective-C code method saves the payload of the parser/renderer but is just a bit heavier than an embedded SVG file. However, as there is no parsing/cascading step to perform, the code is very fast. It’s an interesting compromise for images that can be baked-in to the app.
Imagine the characters and backgrounds for a game like Angry Birds. These relatively simple line-art images could be drawn off-screen using vectors between levels, then sent to the screen as bitmaps (this is probably what they’re doing anyway, but with OpenGL). Or, they could be rendered and cached to disk the first time the app runs. There are lots of apps that use vector-based art but ship with PNG versions.
To summarize, if we throw out the Rise Together acid test where SVG clearly loses to PNG, on average, a vector-based app that embeds SVG files requires around 14.8% of the space that a Retina-capable app requires with its dual PNG files. Put another way, you can include more than six times the graphical content in the same payload size without losing any quality, all while future-proofing your assets.
Just remember that the right format depends on the nature of your content and the appropriate balance of payload and performance. Only you can know what’s best based on your needs, and as always, test, test, test.
Update on 3/26/2012: I’ve received a lot of questions about the technical nature of the SVG parser and what I intend to do with it. The technical details could probably fill a whole article, but I’ll try to summarize them here.
It wasn’t clear in the original post, but I’ve written a brand new SVG parser and renderer from scratch that isn’t based on any of the referenced projects. It offers near full support for the following SVG elements: circle, clipPath, defs, ellipse, fill, font-face, g, image (including embedded base64-encoded images), line, linearGradient, path, pattern, polygon, polyline, radialGradient, rect, stroke, svg, symbol, text, tspan, and use.
The parser performs an abbreviated CSS cascade that respects both style and presentation attributes (no support for external style sheets, as I have no need for them). It supports all length units and their associated computations, including percentages, ems, exs, points, etc. It has full viewBox support, including preserveAspectRatio and objectBoundingBox units, and support for an arbitrary combination of transforms. IRI/xlink references are also correctly resolved and used.
Additionally, it supports fills and strokes that use solid colors, gradients, and patterns.
The Objective-C generation is based on a “Quartz 2D emulator” that understands contexts and states and outputs the minimum set of commands to render its output.
As far as release plans, I hadn’t given it much thought. My experiments and work are currently focused on Halftone and future Juicy Bits apps, but if there’s enough interest, I’d consider releasing it in some form.
For what it’s worth, I’ve exchanged a few e-mails with the SVGKit folks, and it sounds like they’re pushing for much more robust support in future releases.