blob: dac41087176a5b50758725b198a064d3f6fedc20 [file] [view]
# Naïve Image Formats: NIE, NII, NIA
Status: Draft (as of December 2018). There is no compatibility guarantee yet.
## NIE: Still Images
NIE is an easily parsed, uncompressed, lossless format for still (single frame)
images. The 16 byte header:
- 4 bytes of 'magic': \[0x6E, 0xC3, 0xAF, 0x45\], the UTF-8 encoding of "nïE".
- 4 bytes of version-and-configuration: \[0xFF, 0x62 or 0x72, 0x6E or 0x70,
0x34 or 0x38\].
- The first byte denotes the overall NIE/NII/NIA format version. 0xFF (which
is not valid UTF-8) denotes version 1. There are no other valid versions at
this time.
- The second byte, either an ASCII 'b' or an ASCII 'r', denotes whether the
pixel's data is in BGRA or RGBA order (in memory order, independent of CPU
endianness).
- The third byte, either an ASCII 'n' or an ASCII 'p', denotes whether the
payload contains non-premultiplied or premultiplied alpha.
- The fourth byte, either an ASCII '4' or an ASCII '8', denotes whether there
are 4 or 8 bytes per pixel.
- Future format versions may allow other byte values, but in version 1, it
must be '\xFF', then 'b' or 'r', then 'n' or 'p', then '4' or '8'.
- 4 bytes little-endian `uint32` width.
- 4 bytes little-endian `uint32` height.
The payload:
- 4 or 8 bytes per pixel. W×H pixels in row-major order. Row-major means that
horizontally adjacent pixels are adjacent in memory. Values are
little-endian. For example, with BGRA order and 8 bytes per pixel, those
bytes are \[B₀, B₁, G₀, G₁, R₀, R₁, A₀, A₁\]. The ₀ and ₁ subscripts denote
the low and high bytes of the `uint16`.
That's it.
### Example NIE File
This still image is 3 pixels wide and 2 pixels high. It is a crude
approximation to the French flag, being three columns: blue, white and red.
00000000: 6ec3 af45 ff62 6e34 0300 0000 0200 0000 n..E.bn4........
00000010: ff00 00ff ffff ffff 0000 ffff ff00 00ff ................
00000020: ffff ffff 0000 ffff ........
## NII: Animated Images, Timing Index Only, Out-of-Band Frames
NII is an index for animated (multiple frame) images. In video compression
terminology, every NII frame is an I-frame, also known as a keyframe. A NII
file doesn't contain the images per se, only the duration that each frame
should be shown.
The per-frame images, not part of a NII file, may be NIE images, but they may
also be in other formats, such as PNG or WebP or a heterogenous mixture. They
may be static files (possibly with systematic filenames such as
`frame000000.png`, `frame000001.png`, etc.) or dynamically generated. That is
for each application to decide, and out of scope of this specification.
### NII Header
The 32 byte NII header:
- 4 bytes of 'magic': \[0x6E, 0xC3, 0xAF, 0x49\]. The final byte differs from
NIE: an ASCII 'I' instead of an ASCII 'E'.
- 4 bytes of version-and-padding, all 0xFF.
- 4 bytes little-endian `uint32` width.
- 4 bytes little-endian `uint32` height.
- 4 bytes little-endian `uint32` LCValue. See below.
- 4 bytes little-endian `uint32` NFValue. See below.
- 8 bytes little-endian `int64` Overall-TValue. See below.
A zero LCValue means that the animation loops forever. Non-zero means a loop
count: the animation is played LCValue times. This is the
[APNG](https://wiki.mozilla.org/APNG_Specification) meaning, not the
[GIF](https://www.w3.org/Graphics/GIF/spec-gif89a.txt) meaning (the number of
times to repeat the loop _after_ the first play). The two meanings differ by 1.
The Overall-TValue must not be positive. If it is zero, the NFValue must also
be zero. If it is negative, it must equal the final frame's TValue (the final 8
bytes of the NII file, see the NII Payload section below), or if there are no
frames, it must equal -1.
A negative Overall-TValue means the overall animation length, both in terms of
time and in terms of number of frames, is known at the time the 32 byte NII
header was written. The overall time is the OCDD (see the NII Payload section
below). The overall number of frames is the NFValue. Animations lasting longer
than `((1<<32)-1)` frames, more than 4 billion frames, are not representable in
the NII format.
A zero Overall-TValue means that the animation has at least one frame, but the
overall animation length is not known until the end of the NII file is reached.
For example, a one-pass GIF to NII converter might not know the total duration
or the total number of frames until the entire GIF file is processed, at which
point the NII header may already have been written.
To repeat, if an animation has no frames, its NFValue and Overall-TValue must
be 0 and -1 respectively. If an animation has at least one frame, its NFValue
and Overall-TValue pair must be either (zero, zero) or (positive, negative).
### NII Payload
The payload is a sequence of 0 or more frames, exactly 8 bytes per frame:
- 8 bytes little-endian `int64` timing value (TValue).
Every TValue must be non-negative, except for the final TValue, which must be
negative. A frame's CDD (cumulative display duration) equals its TValue for all
but the final frame, and equals the bitwise-complement (or equivalently, "xor
with -1") of the TValue for that final frame. CDD values must therefore be
non-negative int64 values.
The OCDD (overall cumulative display duration) is the final frame's CDD. If the
animation contains no frames, then the OCDD is 0 and the Overall-TValue is -1.
For example, if an animation has four frames, to be displayed for 1 second, 2
seconds, 0 seconds and finally 4.5 seconds, then the CDD's are 1s, 3s, 3s and
7.5s. NII's unit of time is [flicks](https://github.com/OculusVR/Flicks): one
flick (frame-tick) is 1 / 705\_600\_000 of a second. Continuing our example,
the CDDs (in decimal and then hexadecimal) are:
- 705\_600\_000 × 1.0 = 0\_705\_600\_000 = 0x0000\_0000\_2A0E\_9A00.
- 705\_600\_000 × 3.0 = 2\_116\_800\_000 = 0x0000\_0000\_7E2B\_CE00.
- 705\_600\_000 × 3.0 = 2\_116\_800\_000 = 0x0000\_0000\_7E2B\_CE00.
- 705\_600\_000 × 7.5 = 5\_292\_000\_000 = 0x0000\_0001\_3B6D\_8300.
The TValue bytes (little-endian `int64` encoded) are therefore:
- 0x00 0x9A 0x0E 0x2A 0x00 0x00 0x00 0x00
- 0x00 0xCE 0x2B 0x7E 0x00 0x00 0x00 0x00
- 0x00 0xCE 0x2B 0x7E 0x00 0x00 0x00 0x00
- 0xFF 0x7C 0x92 0xC4 0xFE 0xFF 0xFF 0xFF
It is invalid for one frame's CDD value to be less than the previous frame's
CDD value. Animations lasting longer than `((1<<63)-1)` flicks, more than 400
years, are not representable in the NII format.
### Example NII File
This animated image is 3 pixels wide and 2 pixels high. It consists of 20
frames, being 10 loops of 2 frames. The total animation time of a single loop
is 3 seconds, so the 10 loops will take 30 seconds. The first frame is shown
for 1 second. The next frame is shown for (3 - 1) seconds (i.e., 2 seconds).
The actual pixel data per frame is stored elsewhere.
00000000: 6ec3 af49 ffff ffff 0300 0000 0200 0000 n..I............
00000010: 0a00 0000 0200 0000 ff31 d481 ffff ffff .........1......
00000020: 009a 0e2a 0000 0000 ff31 d481 ffff ffff ...*.....1......
## NIA: Animated Images, In-band Frames
NIA is like an NII file where the per-frame still images are NIE files
interleaved between the NII TValues.
The NIA header is the same as the 32 byte NII header, except that the 4 byte
'magic' ends in an ASCII 'A' instead of an ASCII 'I', and the 5th to 8th bytes
are version-and-configuration (the same as for NIE), instead of NII's
version-and-padding. The range of valid version-and-configuration bytes is the
same for NIA as it is for NIE.
The payload is a sequence of 0 or more frames. Each frame is:
- A complete NIE image: header and payload. The outer NIA and inner NIE
must have the same 12 bytes of version-and-configuration, width and height.
- Either 0 or 4 bytes of padding. If present, it must be all zeroes. The
padding ensures that the size of the padded NIE image a multiple of 8 bytes,
so that every TValue field is 8 byte aligned. The padding size is 4 if and
only if there are 4 (not 8) bytes per pixel and both the width and height are
odd. A C programming language expression for its presence is
`((bytes_per_pixel == 4) && (width & height & 1))`.
- 8 bytes little-endian int64 TValue, the same meaning and constraints as NII.
### Example NIA File
This animated image is 3 pixels wide and 2 pixels high. It consists of 20
frames, being 10 loops of 2 frames. The total animation time of a single loop
is 3 seconds, so the 10 loops will take 30 seconds. The first frame is a crude
approximation to the French flag (blue, white and red) and is shown for 1
second. The next frame is a crude approximation to the Italian flag (green,
white and red) and is shown for (3 - 1) seconds (i.e., 2 seconds).
00000000: 6ec3 af41 ff62 6e34 0300 0000 0200 0000 n..A.bn4........
00000010: 0a00 0000 0200 0000 ff31 d481 ffff ffff .........1......
00000020: 6ec3 af45 ff62 6e34 0300 0000 0200 0000 n..E.bn4........
00000030: ff00 00ff ffff ffff 0000 ffff ff00 00ff ................
00000040: ffff ffff 0000 ffff 009a 0e2a 0000 0000 ...........*....
00000050: 6ec3 af45 ff62 6e34 0300 0000 0200 0000 n..E.bn4........
00000060: 00ff 00ff ffff ffff 0000 ffff 00ff 00ff ................
00000070: ffff ffff 0000 ffff ff31 d481 ffff ffff .........1......
# Commentary
## Motivation
One motivating example is securely decoding untrusted images, perhaps uploaded
from potentially malicious actors. Codec libraries have been a rich source of
software security vulnerabilities in the past. One response is to split off
such code into a separate, sandboxed process that reads the compressed image
and writes the equivalent NIE/NIA image, perhaps through pipes or shared
memory. The untrusted codec library processes the untrusted data within the
sandboxed worker process. The unsandboxed manager process only needs to handle
the much simpler NIE/NIA format. That format can be further simplified by the
manager mandating a fixed version-and-configuration, such as v1-"bn4".
Another example is connecting a series of independent image manipulation
programs, each component reading, transforming and then writing a NIE/NIA
image. Such filters can be written in simple programming languages and
connected with Unix-style pipes.
Another example is storing 'golden images' for codec development. Given a
corpus of test images in a compressed format (e.g. a corpus of PNG files), it
is useful to store their expected decodings for comparison, but those golden
test files should be encoded in an alternative format, such as NIE/NIA.
## Magic
The 4 bytes of 'magic' are the UTF-8 encoding of the non-ASCII strings "nïE",
"nïI" and "nïA". The unusual capitalization lessens the chance of plain text
data accidentally matching these magic bytes.
## Alpha Premultiplication
For premultiplied alpha, it is valid for a pixel's blue, green or red values to
be greater than its alpha value. Interpretation of such super-saturated colors
is out of scope of this specification.
A program that simply extracts a subset of a NIA's frames as a new NIA
animation is not required to examine or re-encode every payload byte in order
to always output valid NIA data.
## Random Access
Given a NIA animation's bytes per pixel, width and height _B_, _W_ and _H_, the
offset and length of the _i_'th frame's NIE data within that NIA is a simple
computation (but remember to check for overflow):
- length = roundup8(_B_ × _W_ × _H_) + 16
- offset = ((length + 8) × _i_) + 32
The roundup8 function rounds its argument up to the nearest multiple of 8.
This is random access by frame index (the "_i_" in "the _i_'th frame"), not by
time, as different frames can have different display durations.
## Cumulative Display Duration
Other animation formats, like
[APNG](https://wiki.mozilla.org/APNG_Specification) and
[GIF](https://www.w3.org/Graphics/GIF/spec-gif89a.txt), provide display
durations relative to the previous frame, not relative to the initial frame.
The two schemes are equivalent, in that from a complete stream, either one can
be derived from the other. NII / NIA frames report the cumulative number so
that random access by time can be implemented as a binary search, given random
access by frame index.
Suppose we are given a time _t_ ≥ 0 and want to find the frame to show at that
time. First, there may be no such frame, if the animation contains no frames.
Otherwise, let _o_ be the OCDD, so that _o_ ≥ 0. If _o_ is zero, the frame to
show is the final frame of the animation, and no further computation is
necessary.
Otherwise, calculate the number of loops that would complete by time t: _n_ =
_t_ / _o_, rounding down to the nearest integer. If the LCValue is non-zero (as
zero means loop forever) and _n_ ≥ LCValue then the frame to show is the final
frame.
Otherwise, calculate _t′_ = _t_ - (_n_ × _o_), the time 'modulo' _o_. Binary
search to find the smallest _i_ ≥ 0 such that both CDD(_i_) > _t′_ and the
_i_th frame is non-instantaneous. CDD(_i_) is the cumulative display duration
for frame _i_. The first frame is instantaneous if its CDD is zero. Any other
frame is instantaneous if its CDD equals its previous frame's CDD.
## Integer Arithmetic Overflow
Parsing NIE data is almost trivial, but care should be taken to avoid integer
arithmetic overflow when calculating the pixel buffer size from fields in the
NIE header. For example, a C programming language statement like `size_t
row_size = bytes_per_pixel * width;` is incorrect without additional prior
checks. A careful C implementation is:
```c
#include <stdbool.h>
#include <stdint.h>
// nie_payload_size calculates the size in bytes of a NIE payload, given the
// metadata from the NIE header: bytes_per_pixel, width and height. The max
// argument, not defined in the metadata, is the caller's maximum acceptable
// payload size. For example, pass SIZE_MAX for max, or pass a smaller value if
// you wish to limit the memory required to decode an arbitrary NIE file (and
// reject otherwise valid NIE files that would require more memory).
//
// That size is essentially (bytes_per_pixel * width * height), but this
// function checks for integer arithmetic overflow. It also checks that the
// result pointer is non-NULL, the result (the calculated payload size) is less
// than or equal to max, and that the bytes_per_pixel is either 4 or 8.
//
// The bool return value is whether all checks pass. On success, it sets
// *result to the payload size.
bool nie_payload_size(size_t* result,
size_t max,
uint32_t bytes_per_pixel,
uint32_t width,
uint32_t height) {
if ((result == NULL) || ((bytes_per_pixel != 4) && (bytes_per_pixel != 8))) {
return false;
}
uint64_t n = ((uint64_t)width) * ((uint64_t)height);
// bpp_shift is 2 or 3, depending on bytes_per_pixel being 4 or 8.
uint32_t bpp_shift = 2 + (bytes_per_pixel >> 3);
if (n > (max >> bpp_shift)) {
return false;
}
n <<= bpp_shift;
*result = (size_t)n;
return true;
}
```
## Color Management and Other Metadata
There is no facility for describing color spaces, gamma, palettes or other
metadata such as EXIF information. For example, when using a sandboxed worker
process to convert from a PNG image (with an embedded color profile) to a NIE
image, the target color space should be provided to the worker out-of-band.
## YUV Color
There is no facility for explicitly describing YUV or Y'CbCr color. Converting
between NIE/NIA and formats such as
[JPEG](http://www.w3.org/Graphics/JPEG/itu-t81.pdf) or [WebP
Lossy](https://developers.google.com/speed/webp/docs/riff_container) is a lossy
process, although JPEG and WebP Lossy are lossy formats to begin with.
## Bytes versus Octets
It was not always the case, historically, but in this specification, `byte` is
synonymous with `octet` and `uint8`. Similarly, signed integers are assumed to
use two's complement representation.
# Filename Extensions and MIME Types
The recommended filename extensions are `.nie`, `.nii` and `.nia`.
The recommended MIME types are `image/nie`, `image/nii` and `image/nia`.
## Why NIE and not NIF (for Naïve Image Format)?
The `.nif` filename extension is already used by the NetImmerse / Gamebryo game
engine. Instead, you can think of `.nie` as derived from the word "naïve".
## Pronunciation
I pronounce "NIE", "NII" and "NIA" as "naɪ'i", "naɪ'aɪ" and "naɪ'eɪ", ending
in a long "E", "I" or "A" sound. It's definitely a hard "N", not a soft one.
# Related Work
In general, being uncompressed, NIE/NIA is not intended for long term storage
or for delivery over a network. Alternative lossless image formats like
[PNG](https://www.w3.org/TR/2003/REC-PNG-20031110/) or [WebP
Lossless](https://developers.google.com/speed/webp/docs/riff_container) are far
more suitable.
Amongst uncompressed formats, [Farbfeld](https://tools.suckless.org/farbfeld/)
is a very similar design. The main difference is that NIE is little-endian (the
same as the vast majority of CPUs) where Farbfeld is big-endian. NIE/NII/NIA
also allows animated images, not just still images, and is also configurable
with respect to BGRA versus RGBA order, non-premultiplied versus premultiplied
alpha, and 8 versus 16 bit channels. Configurability is admittedly a trade-off:
one person's configuration parameter is another person's unnecessary bloat.
We're not saying that Farbfeld is a bad design, just a different design that
has chosen different trade-offs.
[PAM](http://netpbm.sourceforge.net/doc/pam.html) (Portable Arbitrary Map) and
its related formats (PBM, PGM, PPM) are also very similar. The main difference
is that their headers are ASCII text and of variable length. Such headers are
easier for humans to read but harder for computers to parse. Again, NIE/NII/NIA
also allows animated images. Some programs process concatenated streams of PAM
images as an animation, but that doesn't contain explicit timing information.
[VIPS](https://jcupitt.github.io/libvips/API/current/file-format.html) is the
intermediate format for the [libvips](https://libvips.github.io/libvips/) image
processing library. Pixel data is stored in a straightforward fashion, but
metadata is stored as XML, which is not simple, and is a potential security
concern.
Amongst flexible formats,
[BMP](https://www.fileformat.info/format/bmp/egff.htm),
[PNG](https://www.w3.org/TR/2003/REC-PNG-20031110/) and its animated variant
[APNG](https://wiki.mozilla.org/APNG_Specification),
[Targa](http://tfc.duke.free.fr/coding/tga_specs.pdf) and
[TIFF](http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf) all have
their built-in compression being optional. Some of those uncompressed forms
have been used as simple interchange formats. Compared to those, NIE has fewer
features, which is obviously unfavorable if you want such features, but can be
favorable if you prefer simplicity and code auditability. A NIE file is still
simpler than an uncompressed BMP file and a NIA file is still simpler than an
uncompressed APNG file.
For example, when using a sandboxed worker process to convert to an
intermediate format, we don't want to use "all possible valid TIFF images" as
the intermediate format, because a compromised worker process could generate a
malicious intermediate image that provoked a security bug in an unsandboxed
TIFF decoder. Instead, we would want only a very simple subset of TIFF, but it
can be easier (in terms of implementation and in terms of a code audit) to
write an (almost trivial) NIE parser from scratch instead of whittling down a
full-featured TIFF library and hoping that you've eliminated all the potential
security vulnerabilities.
---
Updated on February 2019.