cimageppm

Creating a Checkerboard in a .ppm File Only Works for Specific Tile Dimensions


I'm trying to write a C program to generate a .ppm image file, of given length and height, that looks like a rectangular checkerboard pattern, consisting of tiles of a given length and height, and given the two colors of the checkerboard. I represent the pixels in a .ppm image with a contiguous one-dimensional array of integers (size_t). Each pixel in the image consists of three integer values in the range [0, 255] representing their RGB code, so the array contains three times the total number of pixels in the image. The arrays are in row-major order.

I've tried a few different approaches such as using a single for loop and modular arithmetic to fill in the pixels appropriately, and I've also tried trying to fill in the image one tile at a time. But what I eventually found worked the best for me was filling in the pixels one row at a time, from top to bottom. After a few days I finally thought I had gotten my program working, but for some reason the image works for any value of tile height (parameter size_y), but not tile width (parameter size_x). The program seems to generate my 500x500 image correctly only when the tile width is equal to 20 or 100. At first I thought this only worked when the tile width evenly divides the image width, but I found that this can't be the case, since using tile widths of 10, 25, 250, etc. still doesn't generate the image correctly, despite being factors of the image width of 500. It seems like when I change the tile width, it makes the tile height appear very small. The program is deterministic I'm almost certain, the output is always identical with given inputs, I don't do any strange pointer operations or uninitialized variables. I've tried completely restarting my system just in case. I haven't been able to find anything on Stack Overflow or elsewhere on the web that could help me with this. Here is my relevant C code below (with a tile width of 25):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define IMAGE_SIZE_X 500
#define IMAGE_SIZE_Y 500
#define IMAGE_ARRAY_SIZE 3*IMAGE_SIZE_X*IMAGE_SIZE_Y
#define MAX_PIXEL_VALUE 255


/*
    Creates a .ppm binary image file, given a file path (relative or absolute) with the file
    extension, and two positive integer dimensions (horizontal and vertical pixels), a positive
    maximum pixel color value (commonly 255), and a one-dimensional array of non-negative integers
    representing RGB (red, green blue) pixel values, in that order. Returns EXIT_SUCCESS on
    success.

    If a file with the given path already exists, the file is overwritten.
    If the file could not be opened for writing, errno is set, an error message is printed to
    stderr, and EXIT_FAILURE is returned. If the file could not be closed properly, errno is set,
    and EXIT_FAILURE is returned.
*/
int write_ppm(const char* filepath, size_t* pic, const size_t size_x, const size_t size_y, const size_t maxval) {
    //  open the file as a binary file, create the file first if it does not exist
    FILE *fp = fopen(filepath, "wb");
    if(!fp) {
        fprintf(stderr, "Failed to open file named \"%s\" for writing\n", filepath);
        return EXIT_FAILURE;
    }

    //  header
    fprintf(fp, "P6\n%zu %zu\n%zu\n", size_x, size_y, maxval);

    //  populate the RGB pixel values
    for(size_t i = 0; i < 3*size_x*size_y; i+=3)
        fprintf(fp, "%c%c%c", (unsigned char) pic[i], (unsigned char) pic[i+1], (unsigned char) pic[i+2]);

    //  close the file
    if(fclose(fp) != 0)
        return EXIT_FAILURE;
    else
        return EXIT_SUCCESS;
}


//  create a checkerboard pattern on an array representing a .ppm file,
//  given two colors and the two side lengths of the checkerboard tiles
void checkerboard_ppm(size_t* pic,
                      const size_t size_x,
                      const size_t size_y,
                      const size_t color0[3],
                      const size_t color1[3],
                      const size_t tileLength_x,
                      const size_t tileLength_y)
{
    int color = 0;

    for(size_t row = 0; row < size_y; row++) {
        for(size_t col = 3*size_x*row; col < 3*size_x*(row+1); col += 3) {
            //  swap colors when reaching the rightmost part of a tile,
            //  but not at the right edge of the image
            if(((col/3) % tileLength_x == 0) && ((col/3) % size_x != 0))
                color = 1 - color;
            if(color == 0) {
                pic[col]     = color0[0];
                pic[col + 1] = color0[1];
                pic[col + 2] = color0[2];
            }
            else {
                pic[col]     = color1[0];
                pic[col + 1] = color1[1];
                pic[col + 2] = color1[2];
            }
        }
        //  swap colors after writing a number of rows equal to the tile height
        if((row + 1) % tileLength_y == 0)
            color = 1 - color;
    }
}


int main() {
    const size_t RED  [3] = {255,   0,   0},
                 BLUE [3] = {  0,   0, 255};

    size_t image[IMAGE_ARRAY_SIZE];
    checkerboard_ppm(image, IMAGE_SIZE_X, IMAGE_SIZE_Y, RED, BLUE, 25, 20);
    write_ppm("output.ppm", image, IMAGE_SIZE_X, IMAGE_SIZE_Y, MAX_PIXEL_VALUE);
    return EXIT_SUCCESS;
}

Here is a convenient site to view .ppm files.
I'd appreciate any guidance, thanks!


Solution

  • Here's an example I put together that may help.

    I found it useful to make structures for Pixel and Image to keep all the data together, make the loop indexing and color assignment easier, and cut down on the number of parameters required.

    I also switched to writing the entire pixel array to the file with fwrite since the data is arranged correctly to make that possible.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <stdint.h>
    
    #define IMAGE_SIZE_X 500
    #define IMAGE_SIZE_Y 500
    
    typedef struct Pixel
    {
        uint8_t r;
        uint8_t g;
        uint8_t b;
    } Pixel;
    
    typedef struct Image
    {
        size_t width;
        size_t height;
        Pixel* pixels;
    } Image;
    
    int write_ppm(const char* filepath, Image* image) 
    {
        FILE* fp = fopen(filepath, "wb");
        if (!fp) {
            fprintf(stderr, "Failed to open file named \"%s\" for writing\n", filepath);
            return EXIT_FAILURE;
        }
    
        fprintf(fp, "P6\n%zu %zu\n255\n", image->width, image->height);
        fwrite(image->pixels, 1, image->width * image->height * sizeof(Pixel), fp);
        fclose(fp);
    
        return EXIT_SUCCESS;
    }
    
    void checkerboard_ppm(Image* image, const Pixel colors[2], const size_t tileLength_x, const size_t tileLength_y)
    {
        for (size_t row = 0; row < image->height; ++row) 
        {
            int colorIndex = (row / tileLength_y) % 2;
            for (size_t col = 0; col < image->width; ++col)
            {
                if (col && ((col % tileLength_x) == 0))
                {
                    colorIndex = 1 - colorIndex;
                }
                image->pixels[image->width * row + col] = colors[colorIndex];
            }
        }
    }
    
    Image* newImage(size_t width, size_t height)
    {
        Image *image = calloc(1, sizeof *image);
        image->width = width;
        image->height = height;
        image->pixels = calloc(width * height, sizeof *image->pixels);
    
        return image;
    }
    
    void freeImage(Image* image)
    {
        free(image->pixels);
        free(image);
    }
    
    int main() 
    {
        const Pixel colors[2] = { { 255,   0,   0 }, { 0,   0, 255 } };
        Image* image = newImage(IMAGE_SIZE_X, IMAGE_SIZE_Y);
    
        checkerboard_ppm(image, colors, 25, 20);
        write_ppm("output.ppm", image);
        
        freeImage(image);
    
        return EXIT_SUCCESS;
    }