node-lwip
Version:
Comprehensive, fast, and simple image processing and manipulation
186 lines (139 loc) • 7.92 kB
Markdown
# Development Notes
## Getting started
0. Read README.md to get a sense of the public API and usage conventions.
0. Read the rest of this document (you can skip parts that are not relevant
to you).
0. Go quickly over the JS code, but don't try to understand each and every line.
0. Try to understand how the JS code uses the native part.
0. __Ask questions.__
## Intro
An image is just a matrix of pixels. Pixels have channels. Grayscale images have
one channel, color images have 3 or more channels (RGB. CMYK, etc.) and
transparent images have an extra alpha channel (RGBA, etc.), which defines how
transparent the respective pixel is.
Pixel color can be represented in various ways. It can be broken down to base
color components such as in RGB and CMYK. It can be encoded as an indexed color
palette, where the palette itself can be RGB or CMYK.
Storing the image's pixels as they are is wasteful. There are methods to encode
and compress the pixels such that the image takes less space. Some of those
methods loose data in the process of compression ("lossy compressions"), and
thus the quality of the image is reduced. Some methods don't loose data in the
compressions process ("lossless compression"), and thus the quality of the image
is preserved. We call those methods "image formats".
For example:
- JPEG does lossy compression (first it discards image data, then it compresses
it).
- PNG does lossless compression (just compresses the data, without loosing any
of it).
Different formats support different color representations and compressions
methods.
## Opening an image
Images exist in the wild in various formats with various color representations.
Before working on an image we need to obtain the decoded and uncompressed pixels
data. Each format has its own methods of decoding and decompression.
By "opening an image", we mean - going through the process of having the pixels
in memory. Once we have that, we can start manipulating the image.
We need to decide how to represent uncompressed pixels data in memory.
- What if we open a grayscale image?
- What if we open a 3 channels colored image?
- What if we open a 4 channels colored image?
- What if we open an image with an alpha channel?
In order to reduce complexity, __we will always represent pixels as 4 channels
RGBA values__. If the image is originally grayscale, we will convert it to RGBA
when opening it. If the image has no transparency, the alpha channel will be
100% for all pixels.
In order to decode an image, we have to identify its format. Once we do that,
we can use the appropriate decoding library.
Images are constructed as `CImg<unsigned char>`. Notice the type of a pixel
channel is `unsigned char`, which makes our images 8bit.
Once an image is opened, the underlying raw pixels buffer will always have a
size of `4*W*H` bytes (4 channels, each with `W*H`) pixels, where `W` and `H`
are the width and height, respectively. The channels in the buffer are ordered
sequentially. I.e. first all values of the Red channel, then the Green, etc.
## Structure of the native part
Basically, there's nothing inherent in image handling that says it can't be
done in pure JavaScript. So why have a native part?
0. Existing de facto official image decoding / encoding libraries are written
in C/C++ (libjpeg, libpng, giflib, etc.).
0. Implementing image encoding / decoding libraries (such as JPEG) in JS is
_a lot_ of work.
0. ...and includes lots of memory management and handling, which may not be
efficient in JS.
0. Image processing algorithms, though obviously can be implemented in JS, are
much faster in native code with direct memory access.
Disclaimer: I would love to get rid of the native part in favor of pure JS
implementations. The day comparable image encoding / decoding libraries will be
available in JS, I'll seriously consider switching.
There are three native parts to lwip, each a standalone module:
### Decoder
Sources are under `src/decoder`
The decoder will expose a module with a decoding method for each supported
image format. Each of these methods will decode an image buffer of the
respective format. E.g. `decoder.jpeg( ... )` will decode JPEG buffers.
Each of these functions receives 2 arguments - A Buffer object and a callback
function.
The image Buffer object and the callback function are handed to the decoders
from the JS side. We don't care how the buffer was created (most probably with
`fs.readFile`), as long as it's a valid image buffer of the correct format.
The purpose of the decoder is, well, to decode the image buffer, and generate
a new buffer of raw pixels data. Each decoder function will use the respective
library (libjpeg, libpng, etc.) to do its thing.
The decoding function takes a callback as the second argument. Decoding an image
buffer is done asynchronously. When the decoding is finished, the callback
function is called with __6 parameters__:
0. An Error object, or `null` if no error
0. Raw pixels Buffer object (the decoded image)
0. Width of the image
0. Height of the image
0. Number of channels in the raw pixels buffer. __Currently always 4__ (all
images are converted to RGBA when decoding. See the `toRGBA` function in
`util.cpp`).
0. Whether the image has transparency (`true` / `false`).
Calling the callback with these parameters means effectively giving control back
to the JS side.
**Note** that the decoder is not doing _any_ arguments validation. If called
with invalid arguments, __it will crash__. Arguments validation in C++ is a
pain, which is why we never interact with the decoder directly; but rather we
wrap it with JS code which makes sure it's used correctly.
### Encoder
Sources are under `src/encoder`
The encoder, much like the decoder, exposes a module with an encoding method for
each supported image format.
Unlike the decoder, which receives an encoded image buffer; the encoder will
receive a raw pixels buffer to encode. E.g. `encoder.jpeg( ... )` will receive
a raw pixels buffer and encode it into a jpeg image buffer.
The arguments for the encoding methods differ for each encoding format.
We list them here:
0. JPEG: `encoder.jpeg(pixbuff, width, height, quality, callback)`
0. `pixbuff` - Raw RGBA pixels buffer (a Buffer object)
0. `width` - The with of the image
0. `height` - The height of the image
0. `quality` - JPEG quality (0-100)
0. `callback` - callback function
0. PNG: `encoder.png(pixbuff, width, height, compression, interlaced, trans, callback)`
0. `pixbuff` - Raw RGBA pixels buffer (a Buffer object)
0. `width` - The with of the image
0. `height` - The height of the image
0. `compression` - Level of zlib compression:
- `0` for `Z_NO_COMPRESSION`
- `1` for `Z_BEST_SPEED`
- `2` for `Z_BEST_COMPRESSION`
0. `interlaced` - should the created image be interlaced (`true` / `false`).
0. `trans` - should the alpha channel be considered when created the PNG?
- `false`: The alpha channel will be ignored.
- `true`: The image will be encoded with the alpha channel.
0. `callback` - callback function
In all cases encoding is done asynchronously. `callback` is a function which
will be called with __2 parameters__:
0. An Error object, or `null` if no error
0. Buffer object of the encoded image
Calling the callback with these parameters means effectively giving control back
to the JS side; in which, for example, the Buffer object can be written to disk
as a file, sent over the network, etc.
**Note** that the encoder is not doing _any_ arguments validation. If called
with invalid arguments, __it will crash__. Arguments validation in C++ is a
pain, which is why we never interact with the encoder directly; but rather we
wrap it with JS code which makes sure it's used correctly.
### Image processor
Sources are under `src/image`
## Structure of the JS part