July 2012

Dev diary: Creation of PNG animation renderer

2 Implementation of APNG renderer

APNG renderer is extension of Bitmap renderer. The extension is located in Cleanup method which is called at the end of processing of all iterations. APNG renderer just takes all images (outputs) generated by its base Bitmap renderer and composes an animation from them.

To be able to create animated PNG it is necessary to be able to decode and encode PNGs. Since PNG format is quite simple I decided to write own decoder and encoder classes (available for download).

2.1 PNG format

PNG image is composed from PNG signature (special 8 bytes) and blocks called chunks. Chunks carry all the necessary information about the PNG image. Each chunk have its length, type (4 chars), data and checksum. Figure 1 shows PNG format visually.

There are around 20 different chunk types, but for a minimal PNG, only 3 are required: IHDR, IDAT, and IEND. The IHDR chunk which specifies image size and other necessary information, the IDAT chunk which contains compressed image data, and the IEND chunk which signalize end of PNG image. For detail see Official Portable Network Graphics (PNG) Specification (Second Edition).

Minimal PNG image structure —
(a) Minimal PNG image structure
PNG chunk structure —
(b) PNG chunk structure

As mentioned in the first chapter, the Mozilla Foundation created simple format for animated PNG called APNG. Big advantage of this format is backwards compatibility with PNG format. Decoders that do not support APNG format will show the first frame (which may or may not be included in the animation itself). This is because specification of PNG says that unknown chunks should be ignored.

In my case, another advantage of APNG is that there is no need for decoding compressed PNG data from IDAT chunk. IDAT chunks canbe just copied over from PNG. APNG composition is shown in Figure 2.

Composition of animated PNG from PNG images

The Image header (IHDR) chunk of animation specifies size of animation, color depth, compression method and other information for all frames. The Animation Control Chunk (acTL) chunk contains number of frames and number of loops of the animation (zero means forever). The Frame Control Chunk (fcTL) is before each frame and specifies dimension and offset of the frame as well as its delay (time to be shown) and blend operation. For details see the APNG specification APNG Specification).

One last note to PNG format. The IDAT chunk is usually "divided" into more smaller IDAT chunks. PNG standard allows more consecutive IDAT chunks which should be considered as one "data stream". For example standard .NET Bitmap class divides IDAT chunks to 64 kB pieces.

2.2 Implementation of PNG reader/writer

To be able to assemble APNG as shows Figure 2 we need PNG reader and writer. As already mentioned, we do not need to touch IDAT chunks which are encoded (compressed) so implementation of PNG reader is then relatively simple. The reader is designed to work as stream reader to minimize memory consumption, thus, improve performance. Its interface is shown in Code listing 1 (unimportant members are omitted).

Code listing 1: Interface of PngReader class (unimportant members are omitted).
1
2
3
4
5
6
7
8
9
10
11
public class PngReader {
public void ReadPngHeader();
public ChunkReader ReadChunk();
 
public class ChunkReader {
public readonly string Name;
public readonly uint Length;
 
public int Read(byte[] buffer, int offset, int count);
}
}

PNG writer class has similar design as reader. Only difference is that it needs to be able to compute checksum of chunks. Code listing 3 shows interface of PNG writer (unimportant members omitted).

Code listing 3: Interface of PngWriter class (unimportant members are omitted).
1
2
3
4
5
6
7
8
9
10
11
public class PngWriter {
public void WritePngHeader();
public ChunkWriter StartChunk(string name, uint length);
 
public class ChunkWriter {
public readonly string Name;
public readonly uint Length;
 
public void Write(byte[] buffer, int offset, int count);
}
}

The usage of PngReader class is demonstrated in Code listing 2. Shown code prints chunk names and lengths to the console.

Note: described simple PNG reader/writer can be downloaded in Section 4.1 Downloads at the end of this article.

Code listing 2: usage of PngReader class to print chunk names and sizes to console.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using (var stream = FileStream("test.png", FileMode.Open, FileAccess.Read)) {
var buffer = new byte[1024];
var pngReader = new PngReader(stream);
pngReader.ReadPngHeader();
 
PngReader.ChunkReader chunkReader;
while ((chunkReader = pngReader.ReadChunk()) != null) {
Console.WriteLine("{0} [{1}]", chunkReader.Name, chunkReader.Length);
 
int readedLength;
while ((readedLength = chunkReader.Read(buffer, 0, buffer.Length)) > 0) {
Console.WriteLine("Readed {0} B", readedLength);
}
}
}

2.3 Results

Now we have everything what is needed for creation of animated PNG renderer. I will not be describing it in details here, it jsut reads all PNGs and takes theirs IDAT chunk and writes new PNG with correct chunks around. If you are interested in reading more about this process, let me know in the comments below!

One of the limitations is that every frame of the animation has to have the same size. Fortunately, property canvasOriginSize do just that. L-system in Code listing 4 creates a "growing" Pythagoras tree and rendered result can be seen in Figure 3.

Code listing 4: L-system that produces a simple animation of Pythagoras tree.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lsystem SimpleAnimation extends Branches {
 
set symbols axiom = F(128, 16);
set iterations = 11;
set initialAngle = 90;
 
set interpretEveryIteration = true; // to render all iterations as frames
set canvasOriginSize = {-251, 0, 502, 382}; // to ensure same sizes of frames
 
interpret E as DrawForward;
interpret + as TurnLeft(45);
interpret - as TurnLeft(-45);
 
let sq2 = sqrt(2);
 
rewrite F(step, w) to E(step, w)
[ + F(step / sq2, w / sq2) ] - F(step / sq2, w / sq2);
}
process all with AnimationRenderer;
Simple animation of Pythagoras tree.

2.4 Preview image for unsupported viewers

Because APNG is not well supported it is important to provide reasonable default image with some information that the file is an animation. This image is also used in previews so it is not good idea to put there things like "Your browser sucks!".

The easiest approach is to leave the first image as default fallback. This solution however do not allow to place some warning that file is animation because it would be also in animation loop. Next disadvantage is that animations of L-systems tend to be "empty" at beginning so preview would be white in the most cases.

Hopefully, APNG allows to separate first (default) fallback image from animation so it is simple to put there some warning. I've decided to take middle frame as representative preview of animation and put some warning text there. The fallback image of previous animationis shown in Figure 4.

Fallback image for unsupported viewers.