This page aims to give an introduction to how the library works and how to use it to perform some common tasks, including working code samples. Detailed information about the library can be found in the rest of the documentation.
All code samples are released to the public domain, just like the rest of the repository.
Note: all code samples use C, not C++, unless otherwise noted.
- Loading an image
- Storing an image
- Accessing pixel data
- Using pixel coordinates
- Palettes and indexed-color mode
- Metadata
- Animations
- Color and palette conversions
- Generating images from scratch
- Memory management
- Accessing images not in files
- Further resources
Most of the functionality of the library is provided by two functions, plum_load_image
and
plum_store_image
.
These two functions allow, respectively, loading image file data and writing it out.
In the program, image data is represented by a plum_image
struct.
This struct contains the image's pixel data, as well as some ancillary information like its dimensions; its full
contents can be found in the corresponding reference page.
(Note: all function, type, constant and macro names link to their corresponding reference pages; click on them for
a full description of them.)
The library's main design concept is to provide a unified interface for all image data.
Therefore, a plum_image
struct will contain image data for any format; when an image is loaded, the format
it used will be written to its type
member, and that same member will determine which format will be generated when
the image data is written out.
The first part of this tutorial will focus on loading and storing images to files. Loading an image from a file and checking some of its attributes is very simple:
#include <stdio.h>
#include "libplum.h"
int main (int argc, char ** argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s <image>\n", *argv);
return 2;
}
unsigned error;
struct plum_image * image = plum_load_image(argv[1], PLUM_MODE_FILENAME,
PLUM_COLOR_32, &error);
if (image) {
const char * format = plum_get_file_format_name(image -> type);
printf("%s: %s image, %lu x %lu px", argv[1], format,
(unsigned long) image -> width,
(unsigned long) image -> height);
if (image -> frames > 1)
printf(", %lu frames", (unsigned long) image -> frames);
putchar('\n');
plum_destroy_image(image);
} else
printf("%s: load error (%s)\n", argv[1], plum_get_error_text(error));
return !image; // 0 if it was valid, 1 if not
}
A few things can be noted in this example:
- When the program is done using an image, it can release all its resources with
plum_destroy_image
. This will free everything allocated by the library for that image — including, in this case, the image struct itself. - When loading an image, the program must specify the color format it will use.
The library supports four different color formats, as well as a fifth pixel format for images using
palettes (referred to as indexed-color mode in this documentation).
(For images using palettes, the color format indicates the format of the palette data, not the pixel data.)
The user must request which color format they want to operate with; the library will load the image and convert to
the chosen color format, regardless of the actual representation used internally by the image file.
The format chosen in this example,
PLUM_COLOR_32
, uses 32-bit (uint32_t
) color values, encoded as RGBA 8.8.8.8 (with the red channel in the least significant bits), and with the alpha channel inverted (i.e., 0 means opaque) for convenience when dealing with images without transparency. - Some functions can return an error constant describing why they failed.
The
plum_load_image
function does so via a return argument (which may be a null pointer if the caller doesn't care about the reason for failure). Error constants are always positive, and no error (i.e.,PLUM_OK
) is zero, so it is possible to test whether an error occurred by testing this value. - At no point does the user tell the library the type of file being loaded. Image formats are autodetected: all supported types have distinguishable headers, and thus the library can automatically determine which format is being loaded. Therefore, any user of the library will automatically be able to handle all supported image file formats out of the box.
- Images can have more than one frame. This is necessary for formats that contain animations (such as GIF), but also valid in some non-animated formats (such as the PNM family of formats). If an image contains more than one frame, all frames will have the same dimensions.
The counterpart to plum_load_image
is plum_store_image
, a function that converts the image data
from a plum_image
struct into actual image data in some file format and stores that data.
(Much like plum_load_image
, plum_store_image
can store image data in many different locations,
but the first part of this tutorial will focus on files.)
The file format will be determined by the image's type
member; the remaining members will determine the
characteristics of the image file that will be generated.
Not all file formats can contain all of the information that a struct plum_image
can hold.
The library will store as much as it can possibly store in that format.
For example:
- BMP files are limited to 32 bits per pixel; if an image's color depth is higher, it will be reduced.
- JPEG compression is lossy, and therefore, the generated image won't be an exact representation of the original.
- Images can contain a certain amount of metadata. However, only the metadata supported by the chosen format will be stored.
However, limitations that would completely prevent parts of the image from being stored properly will cause the conversion to fail. For example:
- GIF files may only contain up to 256 colors per frame.
An image with more will fail to convert with a
PLUM_ERR_TOO_MANY_COLORS
error. - JPEG files are limited to 65,535 pixels in each dimension.
Larger images will fail to convert with a
PLUM_ERR_IMAGE_TOO_LARGE
error. - Many file formats only support a single image (i.e., a single frame).
Images with two or more frames will fail to convert to those formats with a
PLUM_ERR_NO_MULTI_FRAME
error.
This sample program converts an image to PNG:
#include <stdio.h>
#include "libplum.h"
int main (int argc, char ** argv) {
if (argc != 3) {
fprintf(stderr, "usage: %s <input> <output.png>\n", *argv);
return 2;
}
unsigned error;
struct plum_image * image = plum_load_image(argv[1], PLUM_MODE_FILENAME,
PLUM_COLOR_32, &error);
if (!image) {
fprintf(stderr, "load error: %s\n", plum_get_error_text(error));
return 1;
}
image -> type = PLUM_IMAGE_PNG;
plum_store_image(image, argv[2], PLUM_MODE_FILENAME, &error);
plum_destroy_image(image);
if (error) {
fprintf(stderr, "store error: %s\n", plum_get_error_text(error));
return 1;
}
return 0;
}
As the example shows, no configuration is needed to generate an image file.
The image data is already contained in the plum_image
struct, and the library will automatically choose any
parameters that the chosen file format requires when generating the image file.
The image conversion may fail: for example, if the loaded image file is a GIF animation with multiple frames, it
cannot be encoded as a PNG.
(The library does support APNG, but it is treated as a separate format; the type
member would have to be set to
PLUM_IMAGE_APNG
to emit an APNG file.)
In that case, plum_store_image
will fail with an error.
This doesn't necessarily indicate that the image is invalid: the image data can be validated with the
plum_validate_image
function, and all images loaded by plum_load_image
will be reported as
valid by that function.
Instead, it indicates that the conversion to the chosen file format (in this case, PNG) failed for some reason.
Images' pixel data is stored in the data
member of the plum_image
struct.
This member is a three-dimensional array of color values, where the dimensions are the number of frames, the image's
height, and the image's width.
Pixel data is just a series of color values (or palette indexes for images using indexed-color mode; that
mode will be explained in a later section); color values can take one of four
different formats, chosen when the image is created/loaded.
(This is the meaning of the PLUM_COLOR_32
constant used previously as an argument to the
plum_load_image
function.)
The four available color formats are:
PLUM_COLOR_32
: RGBA 8.8.8.8,uint32_t
color valuesPLUM_COLOR_64
: RGBA 16.16.16.16,uint64_t
color valuesPLUM_COLOR_16
: RGBA 5.5.5.1,uint16_t
color valuesPLUM_COLOR_32X
: RGBA 10.10.10.2,uint32_t
color values
In all cases, the components are listed LSB first (i.e., the least significant bits always correspond to the red
component).
The suffixes 32
, 64
, 16
and 32X
are used consistently to represent these four formats throughout the library.
(Where only data width matters, not the actual bit layout, 32
is also used for the 32X
format.)
The alpha channel is inverted by default: 0 means opaque, and a maximum value means fully transparent.
This allows programs that don't use transparency to leave it at zero without accidentally creating a fully-transparent
image.
However, since programs that do use transparency might find this convention inconvenient, it is possible to combine
these color formats with the PLUM_ALPHA_INVERT
flag through a bitwise OR with the color format
constant.
(For example, the PLUM_COLOR_32 | PLUM_ALPHA_INVERT
color format uses 0 as fully transparent and 255 (highest 8-bit
value) as fully opaque.)
Everything explained for these color formats also works with their alpha-inverted variants; they merely use opposite
alpha values.
Given the descriptions above, it is perfectly possible to construct color values by hand.
For example, in the PLUM_COLOR_32
color format, a color value of 0x0000ffff
(maximum red and
green, zero blue and alpha) would represent solid yellow.
However, to make color calculations simpler, the library offers a number of macros.
These macros are also available as shorter, unprefixed macros, intended to make user code more readable;
in order to make unprefixed macros available, #define
the PLUM_UNPREFIXED_MACROS
constant before
including the library header.
Macros are available for each color format; they differ only in their suffix.
The macros for the PLUM_COLOR_32
color format are:
PLUM_COLOR_VALUE_32
: takes four components (red, green, blue, alpha) and generates a single color value from them; the resulting value is of the right type (uint32_t
in this case). Short form:COLOR32
.PLUM_RED_32
: takes a color value and extracts its red component; the resulting value will still be of the type expected by the color format (uint32_t
in this case). Similar macros exist for the other three components; replaceRED
withGREEN
,BLUE
orALPHA
. Short form:RED32
.
While the image's pixel data is accessible through its data
member, that member is of void *
type, making it
inconvenient to access the pixels directly.
Therefore, in sufficiently recent versions of C and C++ (C11 onwards and all standard versions of C++), the library
header defines aliases for this member, data16
, data32
and data64
, which are pointers to the correct integer
type.
(There is also a data8
alias for indexed-color mode, but that will be explained in
a later section.)
Using all of these elements, the following program will darken an image by reducing its color components by 10%:
#include <stdio.h>
#include <stddef.h>
#define PLUM_UNPREFIXED_MACROS
#include "libplum.h"
#define COEF 90 /* multiply all color values by 90% */
int main (int argc, char ** argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s <filename>\n", *argv);
return 2;
}
unsigned error;
struct plum_image * image = plum_load_image(argv[1], PLUM_MODE_FILENAME,
PLUM_COLOR_64, &error);
if (!image) {
fprintf(stderr, "load error: %s\n", plum_get_error_text(error));
return 1;
}
size_t size = (size_t) image -> width * image -> height * image -> frames;
for (size_t index = 0; index < size; index ++)
image -> data64[index] = COLOR64(
(RED64(image -> data64[index]) * COEF + 50) / 100,
(GREEN64(image -> data64[index]) * COEF + 50) / 100,
(BLUE64(image -> data64[index]) * COEF + 50) / 100,
ALPHA64(image -> data64[index])
);
plum_store_image(image, argv[1], PLUM_MODE_FILENAME, &error);
plum_destroy_image(image);
if (!error) return 0;
fprintf(stderr, "store error: %s\n", plum_get_error_text(error));
return 1;
}
It should be noted that the program above accesses the pixel data through image -> data64
as a linear array;
data64
is just a uint64_t *
member.
This is simple when a program merely needs to iterate through all pixels in an image.
However, it can be inconvenient when a program needs to know the location of the pixels it is accessing.
While pixel data is laid out in the obvious order (frame by frame, row by row, top to bottom, left to right, and without gaps), and the way to access it is well defined, accessing it through computed array indexes isn't always practical, which leads to the next section of this tutorial.
When working with image data, it is almost always desirable to be able to address that data using its coordinates. Since this library allows handling multi-frame images, a pixel is always identified by three coordinates: the column (i.e., X coordinate), the row (i.e., Y coordinate) and the frame number. All coordinates start counting from 0 at the top-left corner of the first frame of the image.
For all examples in this section, an image using the PLUM_COLOR_32
color format will be used.
To access data of a different bit width, replace all instances of 32
with 64
or 16
(or 8
for images using
indexed-color mode, as it will be shown in a later section).
Note that images using the PLUM_COLOR_32X
color format will use the same mechanisms described here,
since the underlying data type (uint32_t
) is the same.
It is entirely possible to access a pixel directly using its coordinates, since the pixels are laid out sequentially, one frame after the other, one row after the other. For example:
uint32_t get_color_at_coordinates (const struct plum_image * image,
uint32_t col, uint32_t row, uint32_t frame) {
assert((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32);
return image -> data32[((size_t) frame * image -> height + row)
* image -> width + col];
}
(Note that the cast from uint32_t
to size_t
may be needed to prevent overflow on particularly large images.)
However, having to do this every time is inconvenient. The library therefore offers several convenience mechanisms that address a single pixel from its coordinates. All of them result in an lvalue, i.e., a value that can also be used to assign a new color, not just read it.
The simplest method is to just use the PLUM_PIXEL_INDEX
macro, which is also available as PIXEL
if unprefixed macros are enabled.
This macro just takes the image and the three coordinates as arguments, and returns a size_t
value that can be used
as an array index, like so:
uint32_t get_color_at_coordinates (const struct plum_image * image,
uint32_t col, uint32_t row, uint32_t frame) {
assert((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32);
return image -> data32[PIXEL(image, col, row, frame)];
}
It is also possible to use a macro that expands to the pixel value directly; there is a different macro for each
possible bit width.
For the 32-bit data example, the corresponding macro is PLUM_PIXEL_32
, or PIXEL32
if
unprefixed macros are enabled.
This macro is good for both reading from a pixel and assigning a new value to it.
(It's also possible to take its address to obtain a pointer to a pixel, as in &PIXEL32(image, col, row, frame)
.)
For example, the following snippet will invert all the colors along an image's main diagonal:
void invert_diagonal (struct plum_image * image) {
assert((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32);
uint32_t limit = (image -> width < image -> height) ?
image -> width : image -> height;
uint32_t mask = PLUM_RED_MASK_32 | PLUM_GREEN_MASK_32 | PLUM_BLUE_MASK_32;
for (uint32_t frame = 0; frame < image -> frames; frame ++)
for (uint32_t p = 0; p < limit; p ++) PIXEL32(image, p, p, frame) ^= mask;
}
If the compiler supports VLAs (which requires using C99 or later, and isn't available in C++), it is possible to
declare a suitable array pointer to which the data
member of the image may be converted; this array can be used to
access the pixels directly.
The PLUM_PIXEL_ARRAY
macro (or PIXARRAY
if unprefixed macros are enabled) will expand
to a declarator for such an array; the first argument is the name of the variable being declared, and the second
argument is the image that the declarator is based on.
(Note: C declarations are comprised of three components: declaration specifiers (including the type names),
declarators (including the variable names), and optional initializers (self-explanatory).
This macro only expands to the declarator; it doesn't include the type name, because that depends on the color format
in use.
Therefore, the type name must be added manually, as in uint32_t PIXARRAY(array, image);
to declare array
.)
Since the above mechanism requires declaring a new variable, the library also provides macros to cast the image's
data
member to the correct array type directly.
For example, for the PLUM_COLOR_32
color format, the PLUM_PIXELS_32
macro (or
PIXELS32
if unprefixed macros are enabled; note the difference with PIXEL32
) will expand to the
image's data
member cast to the corresponding pointer to array of uint32_t
values.
Note: since arrays are indexed by their major dimension first, array indexes are backwards with respect to all other access methods: first the frame, then the row (Y coordinate), then the column (X coordinate).
The following example rewrites the functions above using both VLA-based array macros (one for each):
uint32_t get_color_at_coordinates (const struct plum_image * image,
uint32_t col, uint32_t row, uint32_t frame) {
assert((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32);
return PIXELS32(image)[frame][row][col];
}
void invert_diagonal (struct plum_image * image) {
assert((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32);
uint32_t limit = (image -> width < image -> height) ?
image -> width : image -> height;
uint32_t mask = PLUM_RED_MASK_32 | PLUM_GREEN_MASK_32 | PLUM_BLUE_MASK_32;
uint32_t PIXARRAY(data, image) = image -> data;
for (uint32_t frame = 0; frame < image -> frames; frame ++)
for (uint32_t p = 0; p < limit; p ++) data[frame][p][p] ^= mask;
}
Finally, in C++ mode, while the macros above will not be available, the plum_image
struct has instead some
helper methods that can be used to access the pixel data directly.
These methods all return lvalue references (e.g., uint32_t &
for 32-bit color formats) and exist in both const
and
non-const
variants.
There are separate methods for each bit width, since they return different data types; for example, for the
PLUM_COLOR_32
color format, the method is called pixel32
.
The following example rewrites the functions above using that method:
uint32_t get_color_at_coordinates (const struct plum_image * image,
uint32_t col, uint32_t row, uint32_t frame) {
assert((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32);
return image -> pixel32(col, row, frame);
}
void invert_diagonal (struct plum_image * image) {
assert((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32);
uint32_t limit = (image -> width < image -> height) ?
image -> width : image -> height;
uint32_t mask = PLUM_RED_MASK_32 | PLUM_GREEN_MASK_32 | PLUM_BLUE_MASK_32;
for (uint32_t frame = 0; frame < image -> frames; frame ++)
for (uint32_t p = 0; p < limit; p ++)
image -> pixel32(p, p, frame) ^= mask;
}
Some images may use a fixed palette of colors: instead of assigning each pixel a color, they assign them an index into a palette of colors used by the whole image. This is called indexed-color mode (because pixel values are indexes), and it may be used for many reasons: it ensures that images use a known number of colors, it makes them easier to replace globally, and it results in smaller image files as long as they restrict themselves to a known palette, amongst others.
The library natively supports indexed-color mode with palettes of up to 256 colors.
Images using this mode use 8-bit (uint8_t
) pixel data; each pixel contains an index into the image's palette.
The palette is defined by the palette
member of the plum_image
struct, and it contains an array of
colors, in the format defined by the image's color format.
(Like with the data
member, the struct has typed aliases (in C11+ and C++ mode) for this member: palette16
,
palette32
and palette64
.)
The struct's max_palette_index
member indicates the maximum valid palette index, and thus implicitly the size of the
palette (which is one greater than this value); pixel values (i.e., indexes) must not be larger than this value in
a valid image.
(The choice of representing the maximum valid index instead of the palette size might seem strange at first, but it
ensures that all possible values for this member are valid: the max_palette_index
member is declared as a uint8_t
,
and all values from 0 through 255 are valid, representing a palette with 1 to 256 colors.
The library imposes no limitation on the number of colors a palette may have other than this range: for example, it is
not required to be a power of two.)
Whether an image uses indexed-color mode is determined entirely by its palette
member: if this member is
not a null pointer, then the image has a palette and uses indexed-color mode; if it is a null pointer, the
image has no palette and it uses direct-color mode (i.e., pixel values are colors, as shown in the examples in
previous sections).
Note that the image's max_palette_index
member is meaningless if the image doesn't use a palette.
When loading an image, whether that image will use indexed-color mode or not is determined by a number of
flags that can be passed to plum_load_image
.
All of these flags are ORed into the third argument (the one that contains the color format flags);
omitting them (as it has been done so far) will use the defaults for each category.
The most important flags are the ones that control whether palettes will be loaded at all:
PLUM_PALETTE_NONE
(default): never load a palette. This is the default because most programs don't expect to deal with palettes. If the image has a palette, it will be removed on load, and the image will be converted to direct-color mode (i.e., the usual mode).PLUM_PALETTE_LOAD
: load a palette if the image has one. This mode will faithfully reproduce the image's contents.PLUM_PALETTE_GENERATE
: load a palette if the image has one, or try to generate one otherwise. Generating a palette will succeed if the image has 256 or fewer colors. This mode is intended for applications that prefer to deal with images with palettes.PLUM_PALETTE_FORCE
: always load a palette; this mode is the opposite toPLUM_PALETTE_NONE
. It works likePLUM_PALETTE_GENERATE
, but fails withPLUM_ERR_TOO_MANY_COLORS
if it cannot generate a palette. This mode is intended for applications that only intend to deal with indexed-color mode images.
When generating a palette, the colors in the palette will always be sorted from brightest to darkest, with ties being
broken by transparency.
Note that the library will never dither or approximate colors in any way when generating a palette: if it attempts to
generate a palette, but the image has too many colors, it will simply fail.
When loading an image that already has a palette using the PLUM_PALETTE_GENERATE
flag (or the
PLUM_PALETTE_FORCE
flag), if the image's palette has some unused colors at the end, the resulting
image's max_palette_index
member will be adjusted to reflect the highest color index that is actually in use.
(Those modes may already generate palette data that isn't present in the image, after all, so this small change is
made to account for image file formats that only support fixed palette sizes and will pad palettes with unused colors
up to those sizes.)
Some additional flags may be ORed into that argument to specify some additional behaviors:
PLUM_SORT_EXISTING
: indicates that, if the image already has a palette, that palette must also be sorted when loaded. (By default, only generated palettes are sorted.) Note that theplum_sort_palette
function can also do this for already-loaded images.PLUM_SORT_DARK_FIRST
: when sorting a palette (generated or existing), it indicates that it should sort it from darkest to brightest instead of the other way around.PLUM_PALETTE_REDUCE
: when loading an existing palette, it indicates that the palette should be reduced by removing all duplicate and unused colors. Note that theplum_reduce_palette
function can also do this for already-loaded images.
Putting all of this together, the following program will load an image with a palette and find its most common color:
#include <stdio.h>
#include <stddef.h>
#include "libplum.h"
#define TIE ((size_t) -1)
int main (int argc, char ** argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s <filename>\n", *argv);
return 2;
}
unsigned error;
/* use PLUM_PALETTE_FORCE to ensure that the image will always have a *
* palette, and PLUM_PALETTE_REDUCE to remove duplicates from it so that *
* they won't mess up the counts */
struct plum_image * image = plum_load_image(argv[1], PLUM_MODE_FILENAME,
PLUM_COLOR_64 | PLUM_PALETTE_FORCE | PLUM_PALETTE_REDUCE, &error);
if (!image) {
fprintf(stderr, "error: %s\n", plum_get_error_text(error));
return 1;
}
size_t pixels = (size_t) image -> width * image -> height * image -> frames;
size_t counts[256] = {0};
for (size_t current = 0; current < pixels; current ++)
counts[image -> data8[current]] ++;
size_t max = 0, maxcount = *counts; // assume the maximum is the first
for (size_t current = 1; current <= image -> max_palette_index; current ++)
if (counts[current] > maxcount) {
max = current;
maxcount = counts[current];
} else if (counts[current] == maxcount)
max = TIE;
if (max == TIE)
printf("Most common color: tied (%zu pixels)\n", maxcount);
else
printf("Most common color: 0x%016llx (%zu pixels)\n",
(unsigned long long) image -> palette64[max], maxcount);
plum_destroy_image(image);
return 0;
}
This library's goal is to provide a unified interface for all image file formats, and therefore, metadata is not the primary focus. However, a small amount of metadata is supported, since it represents data that is commonly available across several file formats.
Metadata is accessed through the metadata
member of the plum_image
struct.
This member points to a linked list of plum_metadata
nodes; the nodes are unordered, and new
metadata may be inserted anywhere in the list.
(The library uses a linked list for this data because the number of nodes is small (typically single-digit) and
because it facilitates insertion and removal.)
Each metadata node has a type
, which describes what kind of metadata that node represents, and a data
and a size
containing the data; the layout for each metadata node is described in the corresponding page.
(Since metadata is represented by a linked list, the next
member points to the following node; for the last node in
the list, it is a null pointer, as usual.)
It is possible for users to create their own metadata types, too.
Nodes with a negative value of type
are reserved for the user.
However, this tutorial will focus on metadata nodes with a positive type
, since those are the ones with semantics
defined by the library.
(A type
value of zero, represented by the PLUM_METADATA_NONE
constant, indicates an unused
node and may be used instead of removing the node entirely.)
An image may only contain one of each (positive) type of metadata node.
(PLUM_METADATA_NONE
nodes and user-defined nodes may be repeated.)
The library will load and store metadata nodes whenever possible, i.e., whenever they are meaningful for the
underlying file format (and present in the image file data, when loading).
Metadata nodes defined by the library are:
PLUM_METADATA_COLOR_DEPTH
: describes the actual bit depth for each color channel. The library will always load this node; when storing an image, this node (if present) will be used to determine the true color depth that should be stored. (If this node isn't present, the library will use the color depth determined by the color format instead.)PLUM_METADATA_BACKGROUND
: contains a single color value indicating the image's background color, i.e., the color against which it should be presented. (Note that this node, if present, will always contain a color, even for indexed-color mode images.)PLUM_METADATA_FRAME_AREA
: defines the effective area for each frame in a multi-frame image. This will be determined when loading such an image; when storing one, this node will allow the library to store reduced frames if appropriate.PLUM_METADATA_LOOP_COUNT
: indicates how many times an animation will loop; defaults to 1 if not present.PLUM_METADATA_FRAME_DURATION
: indicates the duration of each frame in an animation, in nanoseconds; defaults to 0 for all frames if not present.PLUM_METADATA_FRAME_DISPOSAL
: indicates how a frame will be removed from the canvas once its duration expires; defaults toPLUM_DISPOSAL_NONE
for all frames if not present.
The exact layout of the data for each node is described in the Metadata page. The last three types in that list are related to animation and will be analyzed in a later section.
The plum_find_metadata
function can be used to find a metadata node in an image; this is a simple
search through the linked list, but it is available as a convenience function so that users aren't required to
reimplement this functionality.
This function will return an image's background color, or a default value if none is defined:
uint32_t background_color (const struct plum_image * image, uint32_t dflt) {
assert(image -> color_format == PLUM_COLOR_32);
const struct plum_metadata * metadata =
plum_find_metadata(image, PLUM_METADATA_BACKGROUND);
if (!metadata) return dflt;
return *(const uint32_t *) (metadata -> data);
}
And this function will replace an image's background color with yellow before storing it:
size_t store_yellow_image (struct plum_image * image, void * buffer,
size_t size_mode, unsigned * restrict error) {
// note: the arguments replicate plum_store_image's arguments
if (!image) {
if (error) *error = PLUM_ERR_INVALID_ARGUMENTS;
return 0;
}
assert(image -> color_format == PLUM_COLOR_32);
// create a new metadata node, to be inserted at the head of the list
uint32_t yellow = 0xffff; // color value for yellow
struct plum_metadata new_metadata = {
.next = image -> metadata, // link to existing metadata nodes
.type = PLUM_METADATA_BACKGROUND,
.size = sizeof yellow,
.data = &yellow
};
// if there was a background, suppress it temporarily
struct plum_metadata * old_metadata =
plum_find_metadata(image, PLUM_METADATA_BACKGROUND);
if (old_metadata) old_metadata -> type = PLUM_METADATA_NONE;
image -> metadata = &new_metadata;
size_t result = plum_store_image(image, buffer, size_mode, error);
// restore the old metadata, and restore the old background if needed
image -> metadata = new_metadata.next; // restore the old metadata list
if (old_metadata) old_metadata -> type = PLUM_METADATA_BACKGROUND;
return result;
}
Since the library supports multi-frame images, it follows that it is capable of handling animated images. Image file formats such as GIF and APNG can specify an animation as a sequence of image frames, and the library can load and generate such files.
Animations are represented as multi-frame images, using metadata nodes to contain the animation parameters. If a file format supports animations, whenever an image contains animation metadata, that metadata will be used to generate the corresponding animation when generating the image file; otherwise, defaults will be used.
The metadata nodes containing animation parameters are:
PLUM_METADATA_LOOP_COUNT
: contains a singleuint32_t
value indicating how many times the animation will loop; if this value is 0, the animation loops forever. If this value is missing, the animation doesn't loop at all; this is equivalent to a loop count of 1.PLUM_METADATA_FRAME_DURATION
: array ofuint64_t
values containing the durations for each frame, in nanoseconds; 0 is a special value that indicates that a frame is not part of the animation at all. (For a frame to be displayed as briefly as possible, use 1 instead; the library follows this convention when loading an image.) If this value is missing, or if it is shorter than the number of frames in the image, remaining frames are assumed to have zero duration.PLUM_METADATA_FRAME_DISPOSAL
: array ofuint8_t
values containing the disposal methods for each frame, i.e., the actions that will be taken to remove the frame from the canvas once its duration expires. Possible values are any of the disposal constants. If this value is missing, or if it is shorter than the number of frames in the image, remaining frames are assumed to have a disposal method ofPLUM_DISPOSAL_NONE
.
Note that image file formats can impose limitations on the values of these parameters. The library will approximate them as best as possible when generating an image file.
The sample program for this chapter will create a slideshow out of a list of image files. As it is significantly longer than the examples shown before, this sample is located in a separate file.
(Note: there is a cleaner way of generating the metadata, using memory management functions. That version will be shown in a later section.)
All examples so far have used a single color format. This is the intended use of the library: the user will choose which color format they want to use, and always use that format throughout; the library supports multiple color formats to adapt to different use cases, but it is not expected (nor necessary) for user programs to support multiple color formats at once.
However, it is sometimes necessary to convert between color formats; for example, different parts of the same program may use different color formats. It might also be necessary to convert an image from direct-color pixel values into indexed-color mode or vice-versa.
To convert a single color value, use the plum_convert_color
function; this function simply takes a
color value and the source and destination color formats as arguments, and returns the converted color value.
For example, plum_convert_color(0x123456, PLUM_COLOR_32, PLUM_COLOR_64)
will return 0x121234345656
.
Converting more than one color can be done by simply calling this function in a loop.
However, different color formats have different data sizes, which makes it more complex to iterate
over an array of colors when either the source or the destination format aren't known in advance.
Therefore, the plum_convert_colors
function will convert an entire array of colors from one
color format to another.
This function takes as arguments the destination and source buffers, the number of colors to convert, and the
destination and source color formats (note that these last two arguments are flipped with respect to
plum_convert_color
, to match the order of the first two arguments).
For example, this function will convert an image's palette to the PLUM_COLOR_64
color format:
void make_palette_64 (struct plum_image * image) {
if (!image -> palette) return;
uint64_t * new_palette =
plum_malloc(image, (image -> max_palette_index + 1) * sizeof *new_palette);
plum_convert_colors(new_palette, image -> palette,
image -> max_palette_index + 1,
PLUM_COLOR_64, image -> color_format);
image -> palette = new_palette;
image -> color_format = PLUM_COLOR_64;
// if there was background color metadata, remove it (easier than converting)
struct plum_metadata * metadata =
plum_find_metadata(image, PLUM_METADATA_BACKGROUND);
if (metadata) metadata -> type = PLUM_METADATA_NONE;
}
(Note: the plum_malloc
function allocates a buffer, like malloc
does, but associated with an image, so
that plum_destroy_image
will later release it.
This will be explained in detail in a later section.)
A more interesting case is converting an image to or from indexed-color mode.
This is already done by plum_load_image
as needed, but it can also be done by the user manually whenever it
becomes necessary to operate in the other mode.
Converting an image to indexed-color mode requires generating a palette and assigning each pixel the index
of its color into the generated palette.
The plum_convert_colors_to_indexes
function handles both steps, but it requires the user
to preallocate the buffers where the data will be held.
The following function shows an example (although it doesn't handle releasing the memory buffers for the old pixel
data after conversion is finished):
unsigned make_image_indexed (struct plum_image * image) {
// will return an error constant if an error occurs
if (image -> palette) return 0; // nothing to do here
void * palette = plum_malloc(image,
plum_color_buffer_size(256, image -> color_format));
// plum_color_buffer_size returns the size of a memory buffer that fits the
// specified number of colors in the chosen color format
if (!palette) return PLUM_ERR_OUT_OF_MEMORY;
uint8_t * pixeldata = plum_malloc(image,
(size_t) image -> width * image -> height * image -> frames);
if (!pixeldata) {
plum_free(image, palette);
return PLUM_ERR_OUT_OF_MEMORY;
}
int result = plum_convert_colors_to_indexes(
pixeldata, image -> data, palette,
(size_t) image -> width * image -> height * image -> frames,
image -> color_format | PLUM_SORT_LIGHT_FIRST);
if (result < 0) {
plum_free(image, palette);
plum_free(image, pixeldata);
return -result;
}
image -> palette = palette;
image -> data = pixeldata;
image -> max_palette_index = result;
return 0;
}
Note that the plum_convert_colors_to_indexes
function may fail, in which case it returns a
negative error code.
(If it doesn't fail, it returns the new value for max_palette_index
.)
For example, if the pixel data contains more than 256 distinct colors, the function will fail with a
PLUM_ERR_TOO_MANY_COLORS
error (i.e., it will return -PLUM_ERR_TOO_MANY_COLORS
).
The opposite case is handled by the plum_convert_indexes_to_colors
function, which will
render an array of pixels from an array of indexes and its palette.
This is a much simpler case that cannot return an error code.
The equivalent example to the function above (also without releasing the original buffer) is:
unsigned make_image_direct (struct plum_image * image) {
if (!image -> palette) return 0; // nothing to do here
size_t pixels = (size_t) image -> width * image -> height * image -> frames;
void * pixeldata = plum_malloc(image,
plum_color_buffer_size(pixels, image -> color_format));
if (!pixeldata) return PLUM_ERR_OUT_OF_MEMORY;
plum_convert_indexes_to_colors(pixeldata, image -> data8, image -> palette,
pixels, image -> color_format);
image -> data = pixeldata;
image -> palette = NULL; // mark the image as not indexed
return 0;
}
So far, this tutorial has mostly been dealing with images loaded from a file.
However, it is obviously possible to generate image data directly, without loading it from anywhere, by writing the
corresponding data to a plum_image
struct.
The library doesn't require images to be allocated in any particular way, nor it makes any assumptions about their
layout in memory beyond what is implied by the data types in use.
Therefore, it is perfectly possible to allocate image data in any way: as local variables (i.e., on the stack), via
malloc
, etc.
Simple images are easy enough to generate. The following example will generate a red-blue gradient that fades to white towards the bottom:
#include <stdio.h>
#include <stdint.h>
#define PLUM_UNPREFIXED_MACROS
#include "libplum.h"
int main (int argc, char ** argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s <output>\n", *argv);
return 2;
}
uint32_t pixeldata[256][256];
struct plum_image image = {
.type = PLUM_IMAGE_PNG,
.width = 256,
.height = 256,
.frames = 1,
.color_format = PLUM_COLOR_32,
.data32 = (uint32_t *) pixeldata
};
for (uint32_t row = 0; row < 256; row ++)
for (uint32_t col = 0; col < 256; col ++) {
unsigned red = (row < col) ? 255 + row - col : 255;
unsigned blue = ((row + col) < 255) ? row + col : 255;
pixeldata[row][col] = COLOR32(red, row, blue, 0);
}
unsigned error;
plum_store_image(&image, argv[1], PLUM_MODE_FILENAME, &error);
if (error) fprintf(stderr, "error: %s\n", plum_get_error_text(error));
return !!error;
}
Note that the example above doesn't allocate anything on the heap: there are no calls to malloc
or any similar
functions.
This is completely valid, and the library can deal with image data laid out that way.
However, this can easily become inconvenient when images get large and complex.
The following section will explain how memory can be bound to an image and thus quickly released with
plum_destroy_image
after finishing working with the image, like with the examples above.
Throughout this tutorial, whenever an image was created by plum_load_image
, that image's memory was later
released by plum_destroy_image
.
This function releases all memory associated with an image that has been allocated by the library.
However, memory associated with an image isn't limited to what plum_load_image
allocates.
This facility is made available to the user as well through memory management functions, so that users can
allocate memory associated with an image that will later be released all at once by plum_destroy_image
.
Memory allocations are tracked through the allocator
member of the plum_image
struct.
This is a private-use member that the library uses for this purpose; it must not be modified by user code.
The plum_malloc
and plum_calloc
functions will allocate memory in the same way as their
standard library counterparts, but the memory they allocate will be associated with an image.
Their first argument is the image they will associate the memory with, and their second argument is the size of the
buffer that will be allocated.
The plum_realloc
function can be used to redimension a buffer allocated this way, just like realloc
does; its three arguments are the associated image (which must be the same as the one used to allocate the buffer),
the buffer to be redimensioned and its new size.
The plum_free
function can be used to release a buffer allocated this way; its arguments are the image that
the buffer is associated with and the buffer itself.
(It is not necessary to call plum_free
on buffers allocated this way, as plum_destroy_image
will deallocate all buffers allocated for an image.
However, it can be desirable in many circumstances to release a buffer once the user is done using it.)
Finally, the plum_allocate_metadata
function will allocate a
struct plum_metadata
node and its data as a single allocation, initializing the struct's data
and size
members (and zero-initializing the rest); while this can also be achieved using plum_malloc
,
this function allocates both the node itself and its associated data buffer in a single allocation (that can be
released with a single call to plum_free
if needed), simplifying its deallocation if necessary.
(Note that it is not advisable to call plum_realloc
on a buffer allocated this way.)
Note that plum_load_image
(and plum_copy_image
, which will be described later on) will allocate
the data
and palette
buffers as if by plum_malloc
and all of the image's metadata as if by
plum_allocate_metadata
, so it is possible to release these buffers with plum_free
if
the user intends to stop using them (perhaps to replace them).
This sample function will scale up an image by an integer factor. Doing so requires redimensioning the image, and thus it requires allocating a new buffer for the pixel data.
void scale_up (struct plum_image * image, unsigned factor) {
if (factor < 2) return; // 1 needs no scaling; 0 is erroneous
assert((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32);
assert(!image -> palette);
uint32_t PIXARRAY(in, image) = image -> data;
// assume that the new dimensions will be valid
image -> width *= factor;
image -> height *= factor;
// allocate a new array based on the new dimensions
uint32_t PIXARRAY(out, image) =
plum_malloc(image, plum_pixel_buffer_size(image));
if (!out) abort();
for (uint32_t frame = 0; frame < image -> frames; frame ++)
for (uint32_t row = 0; row < image -> height; row ++)
for (uint32_t col = 0; col < image -> width; col ++)
out[frame][row][col] = in[frame][row / factor][col / factor];
// assume that the previous image data was allocated with plum_malloc
// (true if it comes directly from plum_load_image, for example)
plum_free(image, image -> data);
image -> data = out;
}
This method of allocation can be used for all images, not only images created by the library.
For instance, it is accessible for images statically initialized by the user as well, like the example shown in the
previous section.
For those images, the allocator
member must be initialized to a null pointer (which will happen automatically if
that member is left out in an initializer, as in the example in that section); the library will manage it from that
point on.
Calling plum_destroy_image
on such an image works as expected: all memory associated with the image is
released (and its allocator
member is reinitialized to a null pointer).
However, the struct itself won't be released, as it hasn't been allocated by the library: only memory associated with
the image (by explicit library calls) is released by plum_destroy_image
.
Therefore, it is always safe to call this function when the user is finished working with an image, regardless of how
the image itself or any buffers it uses were allocated.
As an example, the slideshow program shown in the Animations section can be rewritten to take
advantage of memory management functions, even though the main struct plum_image
defined by that
program is statically initialized and not created by the library at all.
Due to its length, this example is shown in a separate file.
(Note: the plum_append_metadata
function will simply append a new metadata node to the image by
allocating it via plum_allocate_metadata
and inserting it at the head of the metadata list.)
It is often desirable to allocate a new image on the heap altogether, like so:
// not recommended (see below)
struct plum_image * image = calloc(1, sizeof *image);
An allocation like this, performed by a function that doesn't belong to the library, is not managed by the library,
and therefore, not associated with the image.
In particular, this means that plum_destroy_image
will not release that particular buffer if called
on that image, increasing the chances of an accidental memory leak.
It is also impossible to perform this allocation via plum_calloc
, as plum_calloc
requires a
pointer to the image with which the buffer will be associated, but since the image itself is being allocated, such an
image doesn't exist yet.
The plum_new_image
function solves this problem, allocating a zero-initialized image like calloc
would, but
associating its own memory with itself, so that it can be released by plum_destroy_image
.
(Note that plum_load_image
and plum_copy_image
also allocate the image itself as if by
plum_new_image
, which is why plum_destroy_image
can release that memory too.)
The above snippet would simply become:
struct plum_image * image = plum_new_image();
Finally, sometimes it is useful to create a full copy of an image, with its own independent pixel data, metadata, and
so on.
This requires allocating multiple buffers, determining their sizes, copying their contents, etc.
The plum_copy_image
function will handle this, creating a full (deep) copy of an image and copying all its
data, palette (if any) and metadata (in the same order as the original).
(Note that the plum_image
struct contains a member, userdata
, whose only purpose is to hold a pointer to
any data the user wants; it is initialized to a null pointer by plum_new_image
and plum_load_image
and ignored by all other functions in the library.
This member will simply be copied directly by plum_copy_image
, since the library has no way of knowing the
internal structure or size of any data placed there by the user.)
For example, the following function stores an image, but without transparency, by creating a temporary copy:
size_t store_without_transparency (const struct plum_image * image,
void * buffer, size_t size_mode,
unsigned * restrict error) {
// note: same arguments as plum_store_image
struct plum_image * copy = plum_copy_image(image);
if (!copy) {
if (error) *error = PLUM_ERR_OUT_OF_MEMORY;
return 0;
}
plum_remove_alpha(copy);
size_t result = plum_store_image(copy, buffer, size_mode, error);
plum_destroy_image(copy);
return result;
}
So far, all examples have loaded images directly from a file, or stored them directly to a file.
While this is most likely the most common use case, it is also possible to access image files in memory, or from
arbitrary locations using callbacks.
(For example, an OS-aware user may prefer to map a file to memory and point plum_load_image
at that memory
buffer for performance reasons.)
The plum_load_image
and plum_store_image
functions take two arguments that describe the location
of the image data, called buffer
and size_mode
.
The size_mode
argument also indicates what type of location buffer
will refer to; so far, all examples have used
PLUM_MODE_FILENAME
for that argument, indicating that buffer
points to a filename.
Other possibilities are described in the Loading and storing modes page. These are:
PLUM_MODE_BUFFER
: indicates thatbuffer
points to aplum_buffer
struct; this struct can be used to dynamically allocate memory when generating an image.PLUM_MODE_CALLBACK
: indicates thatbuffer
points to aplum_callback
struct that describes how to read or write data in terms of a callback function.- An actual size: indicates that
buffer
points to a memory buffer of that size; this possibility is what gives the argument its name.
(Note that PLUM_MODE_CALLBACK
, PLUM_MODE_BUFFER
and
PLUM_MODE_FILENAME
take up the highest possible size_t
values, and therefore won't collide in
practice with actual size values.)
The full semantics for each mode are explained in the page mentioned above. As a quick summary:
- A fixed-size memory buffer (i.e., where
size_mode
is the size) works as expected. PLUM_MODE_FILENAME
accesses a file;buffer
is aconst char *
containing a filename, as shown in the examples throughout this tutorial.PLUM_MODE_BUFFER
accesses a variable length buffer (through theplum_buffer
struct); itssize
anddata
members describe it. When generating an image, thedata
member will be set to an allocated memory buffer (throughmalloc
) containing the generated data, and thesize
member will be set to the size of the generated data; this data must be freed (usingfree
) by the user after using it.PLUM_MODE_CALLBACK
reads or writes data through a callback (from aplum_callback
struct), which receives a buffer (to write from or read into) and its size and returns the number of bytes written (or 0 for EOF, or a negative value for an error) repeatedly until it finishes or fails.
The following sample program converts image data to the PNG format, like the program shown near the beginning of the tutorial in an earlier section, but reading from standard input and writing to standard output using a callback:
#include <stdio.h>
#include "libplum.h"
int readcb(void *, void *, int);
int writecb(void *, void *, int);
int main (void) {
struct plum_callback callback = {.callback = &readcb, .userdata = stdin};
unsigned error;
struct plum_image * image = plum_load_image(&callback, PLUM_MODE_CALLBACK,
PLUM_COLOR_32, &error);
if (error) {
fprintf(stderr, "load error: %s\n", plum_get_error_text(error));
return 1;
}
image -> type = PLUM_IMAGE_PNG;
callback = (struct plum_callback) {.callback = &writecb, .userdata = stdout};
plum_store_image(image, &callback, PLUM_MODE_CALLBACK, &error);
plum_destroy_image(image);
if (error) fprintf(stderr, "store error: %s\n", plum_get_error_text(error));
return !!error;
}
int readcb (void * file, void * buffer, int size) {
size_t result = fread(buffer, 1, size, file);
if (ferror(file)) return -1; // negative result on error
return result; // return number of bytes read (0 on EOF)
}
int writecb (void * file, void * buffer, int size) {
size_t result = fwrite(buffer, 1, size, file);
if (ferror(file) || feof(file)) return -1; // negative result on error
return result;
}
And the following program will generate a red-blue gradient that fades to white, like a sample program from an earlier section, but instead of writing out the gradient to a file, it will write out a hexadecimal dump of the image to standard output:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#define PLUM_UNPREFIXED_MACROS
#include "libplum.h"
void print_safe_char (unsigned char c) {
putchar((c >= 0x20 && c <= 0x7e) ? c : '.');
}
int main (void) {
uint32_t pixeldata[256][256];
struct plum_image image = {
.type = PLUM_IMAGE_PNG,
.width = 256,
.height = 256,
.frames = 1,
.color_format = PLUM_COLOR_32,
.data32 = (uint32_t *) pixeldata
};
for (uint32_t row = 0; row < 256; row ++)
for (uint32_t col = 0; col < 256; col ++) {
unsigned red = (row < col) ? 255 + row - col : 255;
unsigned blue = ((row + col) < 255) ? row + col : 255;
pixeldata[row][col] = COLOR32(red, row, blue, 0);
}
unsigned error;
struct plum_buffer buffer;
plum_store_image(&image, &buffer, PLUM_MODE_BUFFER, &error);
if (error) {
fprintf(stderr, "error: %s\n", plum_get_error_text(error));
return 1;
}
const unsigned char * ptr = buffer.data;
// print rows of 16 bytes each
unsigned rows = buffer.size >> 4;
for (unsigned row = 0; row < rows; row ++) {
printf("%04x: ", (unsigned) row * 16);
for (uint_fast8_t col = 0; col < 16; col ++)
printf("%02x ", ptr[row * 16 + col]);
putchar(' ');
for (uint_fast8_t col = 0; col < 16; col ++)
print_safe_char(ptr[row * 16 + col]);
putchar('\n');
}
// print a last row with the remaining bytes if needed
uint_fast8_t remaining = buffer.size & 15;
if (remaining) {
printf("%04x: ", rows * 16);
for (uint_fast8_t col = 0; col < remaining; col ++)
printf("%02x ", ptr[rows * 16 + col]);
for (uint_fast8_t col = remaining; col < 16; col ++)
fputs(" ", stdout); // padding to complete the row of hex data
putchar(' ');
for (uint_fast8_t col = 0; col < remaining; col ++)
print_safe_char(ptr[rows * 16 + col]);
putchar('\n');
}
free(buffer.data);
return 0;
}
Previous chapters of this tutorial have focused on the most important functions in the library, key concepts, and the necessary code to use the library successfully. However, some more specific parts of it remain unexplored, as it would be impractical to write a tutorial to cover absolutely every corner case.
There are separate pages listing every function, struct, constant and macro in the library, as well as a list of supported file formats. Another starting point is the alphabetical list of declared identifiers.
Finally, there are additional pages for many other specific topics linked from the documentation main page that will contain further information about the library that users should check out.
Prev: Overview
Next: Building and including the library
Up: README