The main "benefit" of this memory-mapping approach is that you can draw the pixels in a completely arbitrary order, and during calculation, you can use e.g. eog (Eye of Gnome) or any other program to view the progress.
Welp, this is not production code, but hopefully shows the principles. I need to write better comments, though... No guarantees, no warranties, and you get to keep the bugs that almost certainly lurk in the code, but it is Creative Commons Zero 1.0 -licensed, so do with it whatever you want.
First, ppm.h:
/* SPDX-License-Identifier: CC0-1.0 */
#ifndef PPM_H
#define PPM_H
#if defined(__GNUC__) || defined(__clang__)
#define HELPER_FUNCTION __attribute__((unused)) static inline
#else
#define HELPER_FUNCTION static inline
#endif
typedef struct ppm ppm;
struct ppm {
unsigned char *rgb;
void *map;
size_t xstride;
size_t ystride;
size_t size;
int width;
int height;
};
/* Create and open a new PPM image file. */
int ppm_create(ppm *const img, const char *filename, const int width, const int height, const int color);
/* Open an existing PPM image file. */
int ppm_open(ppm *const img, const char *filename);
/* Close an open PPM image file. */
int ppm_close(ppm *const img);
/* Helper functions:
ppm_width(img)
Return the width of the PPM image in pixels, or -1 if no image.
ppm_height(img)
Return the height of the PPM image in pixels, or -1 if no image.
ppm_setpixel(img, x, y, color)
Given a hexadecimal color 0xRRGGBB, set pixel at column x, row y to 'color'.
It is safe to call this with x and/or y outside the image.
ppm_getpixel(ppm *img, int x, int y, int outside)
Return the color at column x, row y; or outside, if x and/or y outside the image.
*/
HELPER_FUNCTION void ppm_setpixel(ppm *const img, const int x, const int y, const int color)
{
if (img && x >= 0 && x < img->width && y >= 0 && y <= img->height) {
unsigned char *const rgb = img->rgb + (size_t)x * img->xstride + (size_t)y * img->ystride;
rgb[2] = color & 0xFF;
rgb[1] = (color >> 8) & 0xFF;
rgb[0] = (color >> 16) & 0xFF;
}
}
HELPER_FUNCTION int ppm_getpixel(ppm *const img, const int x, const int y, const int outside)
{
if (img && x >= 0 && x < img->width && y >= 0 && y <= img->height) {
const unsigned char *const rgb = img->rgb + (size_t)x * img->xstride + (size_t)y * img->ystride;
return ((unsigned int)(rgb[0]) << 24) | ((unsigned int)(rgb[1]) << 16) | rgb[2];
} else {
return outside;
}
}
HELPER_FUNCTION int ppm_width(ppm *const img)
{
return (img) ? img->width : -1;
}
HELPER_FUNCTION int ppm_height(ppm *const img)
{
return (img) ? img->height : -1;
}
#endif
The ppm structure is a basic memory-mapped image structure that allows you to access the RGB data either directly, or via the ppm_getpixel() and ppm_setpixel() helper functions. The red component (0..255) is at ppm.rgb+(size_t)(x)*ppm.xstride + (size_t)(y)*ppm.ystride , the green component in the following byte (offset +1), and the blue component in the byte following that (offset +2).
The implementation is in ppm.c:
/* SPDX-License-Identifier: CC0-1.0 */
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include "ppm.h"
static inline unsigned char *parse_field(unsigned char *src, unsigned char *const end, size_t *to)
{
/* Verify we're still within the buffer. */
if (!src || src >= end)
return NULL;
/* Remember old starting point, since at least one whitespace character or comment is required. */
unsigned char *const start = src;
/* Skip comments and whitespace. */
while (src < end)
if (*src == '#')
while (src < end && *src != '\n' && *src != '\r')
src++;
else
if (*src == '\t' || *src == '\n' || *src == '\v' || *src == '\f' || *src == '\r' || *src == ' ')
src++;
else
break;
/* No whitespace nor comments? Fell off the buffer? */
if (start == src || src >= end)
return NULL;
/* Not a decimal digit? */
if (*src < '0' || *src > '9')
return NULL;
/* Parse decimal number. */
size_t val = 0;
while (src < end && *src >= '0' && *src <= '9') {
const size_t old = val;
val = (10 * val) + (*(src++) - '0');
if ((size_t)(val / 10) != old)
return NULL; /* Overflow */
}
/* Save parsed number. */
if (to)
*to = val;
return src;
}
int ppm_close(ppm *const img)
{
int err = 0;
if (!img)
return errno = EINVAL;
void *const map = img->map;
const size_t size = img->size;
img->rgb = NULL;
img->map = MAP_FAILED;
img->xstride = 0;
img->ystride = 0;
img->size = 0;
img->width = -1;
img->height = -1;
if (size > 0 && map != MAP_FAILED) {
/* Flush all changes back to the filesystem. */
if (msync(map, size, MS_SYNC) == -1)
err = errno;
/* Undo the mapping. */
if (munmap(map, size) == -1)
if (!err)
err = errno;
}
return errno = err;
}
int ppm_create(ppm *const img, const char *filename, const int width, const int height, const int color)
{
unsigned char *map;
char header[32];
int header_len, fd, rc;
struct flock lock;
if (!img)
return errno = EINVAL;
/* Initialize the image descriptor to safe/invalid values. */
img->rgb = NULL;
img->map = MAP_FAILED;
img->xstride = 0;
img->ystride = 0;
img->size = 0;
img->width = -1;
img->height = -1;
if (!filename || !*filename)
return errno = ENOENT;
if (width < 1 || height < 1)
return errno = EINVAL;
/* Construct the PPM image header. */
header_len = snprintf(header, sizeof header, "P6\n%d %d\n255\n", width, height);
if (header_len < 0 || (size_t)header_len >= sizeof header)
return errno = ENOMEM;
/* Calculate the PPM image size and file size, being careful about overflow. */
const size_t xsize = width;
const size_t ysize = height;
const size_t pixels = xsize * ysize;
const size_t bytes = 3 * pixels;
const size_t total = bytes + (size_t)header_len;
if ((size_t)(pixels / ysize) != xsize || (size_t)(pixels / xsize) != ysize ||
(size_t)(bytes / 3) != pixels || total <= bytes || (size_t)((off_t)total) != total)
return errno = ENOMEM;
/* Create the target file. */
do {
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, 0666);
} while (fd == -1 && errno == EINTR);
if (fd == -1)
return errno;
/* Place an advisory exclusive lock on the header part of the file */
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = header_len;
do {
rc = fcntl(fd, F_SETLKW, &lock);
} while (rc == -1 && errno == EINTR);
if (rc == -1) {
const int saved_errno = errno;
unlink(filename);
close(fd);
return errno = saved_errno;
}
/* Resize to final size. */
do {
rc = ftruncate(fd, (off_t)total);
} while (rc == -1 && errno == EINTR);
if (rc == -1) {
const int saved_errno = errno;
unlink(filename);
close(fd);
return errno = saved_errno;
}
/* Reserve storage for the file contents. */
do {
rc = posix_fallocate(fd, (off_t)0, (off_t)total);
} while (rc == -1 && errno == EINTR);
if (rc == -1) {
const int saved_errno = errno;
unlink(filename);
close(fd);
return errno = saved_errno;
}
/* Memory-map the file. Because we've reserved its storage already,
we do not need to reserve swap for it. */
map = mmap(NULL, total, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_NORESERVE, fd, (off_t)0);
if (map == MAP_FAILED) {
const int saved_errno = errno;
unlink(filename);
close(fd);
return errno = saved_errno;
}
/* Copy the file header. */
memcpy(map, header, header_len);
/* Initialize the pixel data to the given values. */
{
unsigned char *rgb = map + header_len;
unsigned char *const end = map + total;
const unsigned char r = (color >> 16) & 255;
const unsigned char g = (color >> 8) & 255;
const unsigned char b = color & 255;
while (rgb < end) {
*(rgb++) = r;
*(rgb++) = g;
*(rgb++) = b;
}
}
/* Tell the OS the entire mapping should be, at some point, be written to a file.
In Linux, this doesn't do anything, but for portability, let's keep the call. */
msync(map, total, MS_ASYNC);
/* Unlock the file. */
lock.l_type = F_UNLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = header_len;
do {
rc = fcntl(fd, F_SETLKW, &lock);
} while (rc == -1 && errno == EINTR);
if (rc == -1) {
const int saved_errno = errno;
unlink(filename);
munmap(map, total);
return errno = saved_errno;
}
/* We can now close the file descriptor, as it is not needed while using the mapping. */
if (close(fd) == -1) {
const int saved_errno = errno;
unlink(filename);
munmap(map, total);
return errno = saved_errno;
}
/* Update the image structure. */
img->rgb = map + header_len;
img->map = map;
img->xstride = 3;
img->ystride = 3 * xsize;
img->size = total;
img->width = width;
img->height = height;
/* Normally, errno is not zeroed. However, this is a complex, long operation,
with multiple errno manipulations during its run, so here doing so is okay. */
return errno = 0;
}
int ppm_open(ppm *const img, const char *filename)
{
unsigned char *map;
int fd, rc;
struct flock lock;
struct stat info;
if (!img)
return errno = EINVAL;
/* Initialize the image descriptor to safe/invalid values. */
img->rgb = NULL;
img->map = MAP_FAILED;
img->xstride = 0;
img->ystride = 0;
img->size = 0;
img->width = -1;
img->height = -1;
if (!filename || !*filename)
return errno = ENOENT;
/* Open the existing PPM image. */
do {
fd = open(filename, O_RDWR);
} while (fd == -1 && errno == EINTR);
if (fd == -1)
return errno;
/* Obtain an advisory read lock on the entire file. */
lock.l_type = F_RDLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; /* Entire file */
do {
rc = fcntl(fd, F_SETLKW, &lock);
} while (rc == -1 && errno == EINTR);
if (rc == -1) {
const int saved_errno = errno;
close(fd);
return errno = saved_errno;
}
/* Obtain the file information. */
if (fstat(fd, &info) == -1) {
const int saved_errno = errno;
close(fd);
return errno = saved_errno;
}
/* Check for size overflow. Minimum PPM image size is ten bytes. */
const size_t total = (size_t)info.st_size;
if (info.st_size < 10 || (off_t)total != info.st_size) {
close(fd);
return errno = ENOMEM;
}
/* Map the file read-write. Don't reserve swap for the mapping. */
map = mmap(NULL, total, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_NORESERVE, fd, (off_t)0);
if (map == MAP_FAILED) {
const int saved_errno = errno;
close(fd);
return errno = saved_errno;
}
/* Verify signature. */
if (map[0] != 'P' || map[1] != '6') {
munmap(map, total);
close(fd);
return errno = EDOM; /* Unsupported format */
}
unsigned char *const end = map + total;
unsigned char *rgb = map + 2;
size_t w = 0, h = 0, n = 0;
rgb = parse_field(rgb, end, &w);
rgb = parse_field(rgb, end, &h);
rgb = parse_field(rgb, end, &n);
/* Invalid header? */
if (!rgb || rgb >= end || w < 1 || h < 1 || n != 255) {
munmap(map, total);
close(fd);
return errno = EDOM; /* Unsupported format */
}
/* Header must end with a single whitespace character. */
if (*rgb == '\t' || *rgb == '\n' || *rgb == '\v' || *rgb == '\f' || *rgb == '\r' || *rgb == ' ') {
rgb++;
} else {
munmap(map, total);
close(fd);
return errno = EDOM; /* Unsupported format */
}
const size_t header_len = (size_t)(rgb - map);
const size_t bytes = 3*w*h;
const size_t check = bytes + header_len;
if ((size_t)((bytes / w) / 3) != h || (size_t)((bytes / h) / 3) != w || check > total) {
munmap(map, total);
close(fd);
return errno = EDOM; /* Unsupported format, or header error */
}
/* Check for size overflow. */
if ((int)w <= 0 || (size_t)((int)w) != w ||
(int)h <= 0 || (size_t)((int)h) != h) {
munmap(map, total);
close(fd);
return errno = ENOMEM; /* Image is too large */
}
/* File seems okay. Unlock advisory lock, */
lock.l_type = F_UNLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; /* Entire file */
do {
rc = fcntl(fd, F_SETLKW, &lock);
} while (rc == -1 && errno == EINTR);
if (rc == -1) {
const int saved_errno = errno;
munmap(map, total);
close(fd);
return errno = saved_errno;
}
/* and close the file. */
if (close(fd) == -1) {
const int saved_errno = errno;
munmap(map, total);
return errno = saved_errno;
}
/* Update image structure. */
img->rgb = rgb;
img->map = map;
img->xstride = 3;
img->ystride = 3*w;
img->size = 0;
img->width = (int)w; /* Verified not to overflow */
img->height = (int)h; /* Verified not to overflow */
/* Normally, errno is not zeroed. However, this is a complex, long operation,
with multiple errno manipulations during its run, so here doing so is okay. */
return errno = 0;
}
Here is a silly example program, that when given a nonexistent file name, will make it into a 16×16-pixel PPM image filled with blue color; and otherwise will replace one blue pixel in the existing image with white. test.c:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include "ppm.h"
static int parse_int(const char *src, int *to)
{
const char *end = src;
long val;
errno = 0;
val = strtol(src, (char **)&end, 0);
if (errno || end == src || (long)((int)val) != val)
return -1;
while (*end == '\t' || *end == '\n' || *end == '\v' ||
*end == '\f' || *end == '\r' || *end == ' ')
end++;
if (*end)
return -1;
if (to)
*to = val;
return 0;
}
static int parse_double(const char *src, double *to)
{
const char *end = src;
double val;
errno = 0;
val = strtod(src, (char **)&end);
if (errno || end == src)
return -1;
while (*end == '\t' || *end == '\n' || *end == '\v' ||
*end == '\f' || *end == '\r' || *end == ' ')
end++;
if (*end)
return -1;
if (to)
*to = val;
return 0;
}
int main(int argc, char *argv[])
{
ppm image;
if (argc != 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", arg0);
fprintf(stderr, " %s FILENAME.PPM\n", arg0);
fprintf(stderr, "\n");
fprintf(stderr, "This is an example of \"ppm.h\" memory-mapped PPM image file format support.\n");
fprintf(stderr, "If FILENAME.PPM does not exist, it will be created, and filled blue.\n");
fprintf(stderr, "Otherwise, one of the blue pixels in it will be turned white.\n");
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
/* Try creating the image file first. */
if (!ppm_create(&image, argv[1], 16, 16, 0x0000FF)) {
fprintf(stderr, "%s: Created a 16x16-pixel blue PPM image.\n", argv[1]);
if (ppm_close(&image)) {
fprintf(stderr, "%s: Write error: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
/* Couldn't create the file, so try opening it. */
if (ppm_open(&image, argv[1])) {
fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
/* Find a pixel that is blue. */
int found_x = -1, found_y = -1;
int x, y;
for (y = 0; found_x == -1 && y < image.height; y++) {
for (x = 0; x < image.width; x++) {
if (ppm_getpixel(&image, x, y, -1) == 0x0000FF) {
ppm_setpixel(&image, x, y, 0xFFFFFF);
found_x = x;
found_y = y;
break;
}
}
}
if (found_x != -1) {
fprintf(stderr, "%s: Painted pixel (%d,%d) white.\n", argv[1], found_x, found_y);
} else {
fprintf(stderr, "%s: No blue pixels found.\n", argv[1]);
}
if (ppm_close(&image)) {
fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
The parse_int() and parse_double() functions are included to make parsing command-line arguments easier. You can use them with getopt(), or with positional arguments. For example, if the second command-line parameter (argv[2]) is the desired image width, you could use
int width;
if (parse_int(argv[2], &width) || width < 1) {
fprintf(stderr, "%s: Invalid image width.\n", argv[2]);
return EXIT_FAILURE;
}
Integers are parsed in decimal, octal, and hexadecimal; and doubles in decimal or hexadecimal format. Unlike atoi()/sscanf() parsing, these have pretty nice error checking, and won't just blindly return a best-guess value.
Note that if multiple processes open the same PPM file, they see the changes in real time, but because there is no locking, that means they might observe the changes after only one or two components of a pixel have been changed, yielding temporary color "flicker". (You cannot even assume that those only take "a short time", because the kernel may interrupt a process in the middle of writing a pixel value; this is because these are triplets of bytes, and may even cross a page boundary, so definitely not atomic.)
The Linux kernel is pretty efficient about keeping the data in page cache, so as long as there is not too much memory pressure, the data will be in RAM, and only written back to disk per kernel dirty page rules. Any other process opening the file on the same machine will get the page-cached contents, even if you say cat or copy the file.
The ppm_close() uses msync(map, size, MS_SYNC) to ensure the mapping contents match the file at that point.
Note that ppm_open() should be able to memory-map any PPM (P6, binary pixmap format) images with full 24bpp color, i.e. maximum color component value 255. For example, any PPM image you export from Gimp (RGB format, in RAW and not ASCII).