Skip to main content
added 117 characters in body
Source Link
Madagascar
  • 10.1k
  • 1
  • 16
  • 52
#ifdef _POSIX_C_SOURCE
#undef _POSIX_C_SOURCE
#endif

#ifdef _XOPEN_SOURCE
#undef _XOPEN_SOURCE
#endif

#define _POSIX_C_SOURCE 200819L
#define _XOPEN_SOURCE   700

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>

#include <getopt.h>

#include "helpers.h"

/* TODO: Support BMP images stored as bottom-up. */

/* ARRAY_CARDINALITY(x) calculates the number of elements in the array 'x'.
 * If 'x' is a pointer, it will trigger an assertion.
 */
#define ARRAY_CARDINALITY(x) \
        (assert((void *)&(x) == (void *)(x)), sizeof (x) / sizeof *(x))

#define BMP_SCANLINE_PADDING 4
#define BF_UNPADDED_REGION_SIZE 12

struct flags {
    bool sflag;                 /* Sepia flag. */
    bool rflag;                 /* Reverse flag. */
    bool gflag;                 /* Greyscale flag. */
    bool bflag;                 /* Blur flag. */
    FILE *out_file;             /* Output to file. */
};

static void help(void)
{
    puts("Usage: filter [OPTIONS] <infile> <outfile>\n"
         "\n\tTransform your BMP images with powerful filters.\n\n"
         "Options:\n"
         "    -s, --sepia           Apply a sepia filter for a warm, vintage look.\n"
         "    -r, --reverse         Create a horizontal reflection for a mirror effect.\n"
         "    -g, --grayscale       Convert the image to classic greyscale.\n"
         "    -b, --blur            Add a soft blur to the image.\n"
         "    -h, --help            displays this message and exit.\n");
    exit(EXIT_SUCCESS);
}

static void err_msg(void)
{
    fputs("Usage: filter [OPTIONS] <infile> <outfile>\n"
          "Try filter -h for help.\n", stderr);
    exit(EXIT_FAILURE);
}

static void parse_options(const struct option *restrict long_options,
                          const char *restrict short_options,
                          struct flags *restrict opt_ptr, int argc,
                          char *const argv[])
{
    int c;

    while ((c =
            getopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
        switch (c) {
            case 's':
                opt_ptr->sflag = true;
                break;
            case 'r':
                opt_ptr->rflag = true;
                break;
            case 'g':
                opt_ptr->gflag = true;
                break;
            case 'b':
                opt_ptr->bflag = true;
                break;
            case 'h':
                help();
                break;
            case 'o':
                /* We'll seek to the beginning once we've read input,
                 * in case it's the same file. 
                 */
                opt_ptr->out_file = (errno = 0, fopen(optarg, "ab"));

                if (!opt_ptr->out_file) {
                    errno ? perror(optarg) : (void)
                        fputs("Error - failed to open output file.", stderr);
                }
                break;

                /* case '?' */
            default:
                err_msg();
                break;
        }
    }
}

static void apply_filter(const struct flags *options, size_t height,
                         size_t width, RGBTRIPLE image[height][width])
{
    struct {
        bool flag;
        void (* const func)(size_t height, size_t width, RGBTRIPLE image[height][width]);
    } group[] = {
        { options->sflag, sepia },
        { options->rflag, reflect },
        { options->gflag, grayscale },
        { options->bflag, blur }, 
    };
    
    for (size_t i = 0; i < ARRAY_CARDINALITY(group); ++i) {
        if (group[i].flag) {
            group[i].func(height, width, image);
        }
    }
}

static size_t determine_padding(size_t width)
{
    /* In BMP images, each scanline (a row of pixels) must be a multiple of
     * BMP_SCANLINE_PADDING bytes in size. If the width of the image in pixels 
     * multipled by the size of each pixel (in bytes) is not a multiple of 
     * BMP_SCANLINE_PADDING, padding is added to make it so. 
     */
    return (BMP_SCANLINE_PADDING - (width * sizeof (RGBTRIPLE)) % BMP_SCANLINE_PADDING) % BMP_SCANLINE_PADDING;
}

static int write_scanlines(FILE * out_file, size_t height, size_t width,
                           const RGBTRIPLE image[][width], size_t padding)
{
    const size_t pad_byte = 0x00;

    /* Write new pixels to outfile */
    for (size_t i = 0; i < height; ++i) {
        /* Write row to outfile, with padding at the end. */
        if (fwrite(image[i], sizeof image[i][0], width, out_file) != width
            || fwrite(&pad_byte, 1, padding, out_file) != padding) {
            return -1;
        }
    }

    return 0;
}

static int write_image(const BITMAPFILEHEADER * restrict bf,
                       const BITMAPINFOHEADER * restrict bi,
                       FILE * restrict out_file, size_t height,
                       size_t width, const RGBTRIPLE image[height][width])
{
    if (out_file != stdout && (errno = 0, ftruncate(fileno(out_file), 0))) {
        errno ? perror("seek()") : 
                     (void) fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    if (fwrite(&bf->bf_type, sizeof bf->bf_type, 1, out_file) != 1
        || fwrite(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, out_file) != 1
        || fwrite(bi, sizeof *bi, 1, out_file) != 1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    const size_t padding = determine_padding(width);

    if (write_scanlines(out_file, height, width, image, padding) == -1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }
    return out_file == stdout || !fclose(out_file);
}

static int read_scanlines(FILE * in_file, size_t height, size_t width,
                          RGBTRIPLE image[][width], size_t padding)
{
    /* Iterate over infile's scanlines */
    for (size_t i = 0; i < height; i++) {
        /* Read row into pixel array */
        if (fread(image[i], sizeof image[i][0], width, in_file) != width) {
            return -1;
        }

        /* Temporary buffer to read and discard padding. */
        uint8_t padding_buffer[BMP_SCANLINE_PADDING];

        if (fread(padding_buffer, 1, padding, in_file) != padding) {
            return -1;
        }
    }
    return 0;
}

static void *read_image(BITMAPFILEHEADER * restrict bf,
                        BITMAPINFOHEADER * restrict bi,
                        size_t *restrict height_ptr,
                        size_t *restrict width_ptr, FILE * restrict in_file)
{
    /* Read infile's BITMAPFILEHEADER and BITMAPINFOHEADER. */
    if (fread(&bf->bf_type, sizeof bf->bf_type, 1, in_file) != 1
        || fread(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, in_file) != 1
        || fread(bi, sizeof *bi, 1, in_file) != 1) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    /* Ensure infile is (likely) a 24-bit uncompressed BMP 4.0 */
    if (!bmp_check_header(bf, bi)) {
        fputs("Error - unsupported file format.\n", stderr);
        return NULL;
    }
    
    /* There seems to be no need to treating the data differently. 
     * The code handles both top-down and bottom-up just fine. Or does it?
     */
#if 0    
    /* If bi_height is positive, the bitmap is a bottom-up DIB with the origin 
     * at the lower left corner. It bi_height is negative, the bitmap is a top-
     * down DIB with the origin at the upper left corner. 
     * We currenly only support images stored as top-down, so bail if the format
     * is elsewise.
     */
    if (bi->bi_height > 0) {
        fputs("Error - Bottom-up BMP image format is not yet supported.\n", stderr);
        return NULL;
    }
 #endif

    /* Get image's dimensions. */
    uint32_t abs_height = bi->bi_height < 0 ? 0u - (uint32_t) bi->bi_height :
        (uint32_t) bi->bi_height;

    /* If we are on a too small a machine, there is not much hope, so bail. */
    if (abs_height > SIZE_MAX) {
        fputs
            ("Error - Image dimensions are too large for this system to process.\n",
             stderr);
        return NULL;
    }
    
    size_t height = (size_t) abs_height;
    size_t width = (size_t) bi->bi_width;

    if (!height || !width) {
        fputs("Error - corrupted BMP file: width or height is zero.\n", stderr);
        return NULL;
    }

    if (width > (SIZE_MAX - sizeof (RGBTRIPLE)) / sizeof (RGBTRIPLE)) {
        fputs("Error - image width is too large for this system to process.\n",
              stderr);
        return NULL;
    }

    /* Allocate memory for image */
    RGBTRIPLE(*image)[width] = calloc(height, sizeof *image);

    if (!image) {
        fputs("Error - not enough memory to store image.\n", stderr);
        return NULL;
    }

    const size_t padding = determine_padding(width);

    if (read_scanlines(in_file, height, width, image, padding)) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    *height_ptr = height;
    *width_ptr = width;
    return image;
}

static int process_image(const struct flags *restrict options,
                         FILE * restrict in_file, FILE * restrict out_file)
{
    BITMAPFILEHEADER bf;
    BITMAPINFOHEADER bi;

    size_t height = 0;
    size_t width = 0;

    void *const image = read_image(&bf, &bi, &height, &width, in_file);

    if (!image) {
        return -1;
    }

    apply_filter(options, height, width, image);

    if (write_image(&bf, &bi, out_file, height, width, image) == -1) {
        return -1;
    }

    free(image);
    return 0;
}

int main(int argc, char *argv[])
{
    /* Sanity check. POSIX requires the invoking process to pass a non-NULL 
     * argv[0].
     */
    if (!argv) {
        fputs("A NULL argv[0] was passed through an exec system call.\n",
              stderr);
        return EXIT_FAILURE;
    }

    /* Define allowable filters */
    static const struct option long_options[] = {
        { "grayscale", no_argument, NULL, 'g' },
        { "reverse", no_argument, NULL, 'r' },
        { "sepia", no_argument, NULL, 's' },
        { "blur", no_argument, NULL, 'b' },
        { "help", no_argument, NULL, 'h' },
        { "output", required_argument, NULL, 'o' },
        { NULL, 0, NULL, 0 }
    };

    FILE *in_file = stdin;
    struct flags options = { false, false, false, false, stdout };
    int result = EXIT_SUCCESS;

    parse_options(long_options, "grsbho:", &options, argc, argv);

    if ((optind + 1) == argc) {
        in_file = (errno = 0, fopen(argv[optind], "rb"));

        if (!in_file) {
            errno ? perror(argv[optind]) : (void)
                fputs("Error - failed to open input file.", stderr);
            return EXIT_FAILURE;
        }
    } else if (optind > argc) {
        err_msg();
    }

    if (process_image(&options, in_file, options.out_file) == -1) {
        result = EXIT_FAILURE;
    }

    if (in_file != stdin) {
        fclose(in_file);
    }

    return result;
}
#ifdef _POSIX_C_SOURCE
#undef _POSIX_C_SOURCE
#endif

#ifdef _XOPEN_SOURCE
#undef _XOPEN_SOURCE
#endif

#define _POSIX_C_SOURCE 200819L
#define _XOPEN_SOURCE   700

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>

#include <getopt.h>

#include "helpers.h"

/* TODO: Support BMP images stored as bottom-up. */

/* ARRAY_CARDINALITY(x) calculates the number of elements in the array 'x'.
 * If 'x' is a pointer, it will trigger an assertion.
 */
#define ARRAY_CARDINALITY(x) \
        (assert((void *)&(x) == (void *)(x)), sizeof (x) / sizeof *(x))

#define BMP_SCANLINE_PADDING 4
#define BF_UNPADDED_REGION_SIZE 12

struct flags {
    bool sflag;                 /* Sepia flag. */
    bool rflag;                 /* Reverse flag. */
    bool gflag;                 /* Greyscale flag. */
    bool bflag;                 /* Blur flag. */
    FILE *out_file;             /* Output to file. */
};

static void help(void)
{
    puts("Usage: filter [OPTIONS] <infile> <outfile>\n"
         "\n\tTransform your BMP images with powerful filters.\n\n"
         "Options:\n"
         "    -s, --sepia           Apply a sepia filter for a warm, vintage look.\n"
         "    -r, --reverse         Create a horizontal reflection for a mirror effect.\n"
         "    -g, --grayscale       Convert the image to classic greyscale.\n"
         "    -b, --blur            Add a soft blur to the image.\n"
         "    -h, --help            displays this message and exit.\n");
    exit(EXIT_SUCCESS);
}

static void err_msg(void)
{
    fputs("Usage: filter [OPTIONS] <infile> <outfile>\n"
          "Try filter -h for help.\n", stderr);
    exit(EXIT_FAILURE);
}

static void parse_options(const struct option *restrict long_options,
                          const char *restrict short_options,
                          struct flags *restrict opt_ptr, int argc,
                          char *const argv[])
{
    int c;

    while ((c =
            getopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
        switch (c) {
            case 's':
                opt_ptr->sflag = true;
                break;
            case 'r':
                opt_ptr->rflag = true;
                break;
            case 'g':
                opt_ptr->gflag = true;
                break;
            case 'b':
                opt_ptr->bflag = true;
                break;
            case 'h':
                help();
                break;
            case 'o':
                /* We'll seek to the beginning once we've read input,
                 * in case it's the same file. 
                 */
                opt_ptr->out_file = (errno = 0, fopen(optarg, "ab"));

                if (!opt_ptr->out_file) {
                    errno ? perror(optarg) : (void)
                        fputs("Error - failed to open output file.", stderr);
                }
                break;

                /* case '?' */
            default:
                err_msg();
                break;
        }
    }
}

static void apply_filter(const struct flags *options, size_t height,
                         size_t width, RGBTRIPLE image[height][width])
{
    struct {
        bool flag;
        void (* const func)(size_t height, size_t width, RGBTRIPLE image[height][width]);
    } group[] = {
        { options->sflag, sepia },
        { options->rflag, reflect },
        { options->gflag, grayscale },
        { options->bflag, blur }, 
    };
    
    for (size_t i = 0; i < ARRAY_CARDINALITY(group); ++i) {
        if (group[i].flag) {
            group[i].func(height, width, image);
        }
    }
}

static size_t determine_padding(size_t width)
{
    /* In BMP images, each scanline (a row of pixels) must be a multiple of
     * BMP_SCANLINE_PADDING bytes in size. If the width of the image in pixels 
     * multipled by the size of each pixel (in bytes) is not a multiple of 
     * BMP_SCANLINE_PADDING, padding is added to make it so. 
     */
    return (BMP_SCANLINE_PADDING - (width * sizeof (RGBTRIPLE)) % BMP_SCANLINE_PADDING) % BMP_SCANLINE_PADDING;
}

static int write_scanlines(FILE * out_file, size_t height, size_t width,
                           const RGBTRIPLE image[][width], size_t padding)
{
    const size_t pad_byte = 0x00;

    /* Write new pixels to outfile */
    for (size_t i = 0; i < height; ++i) {
        /* Write row to outfile, with padding at the end. */
        if (fwrite(image[i], sizeof image[i][0], width, out_file) != width
            || fwrite(&pad_byte, 1, padding, out_file) != padding) {
            return -1;
        }
    }

    return 0;
}

static int write_image(const BITMAPFILEHEADER * restrict bf,
                       const BITMAPINFOHEADER * restrict bi,
                       FILE * restrict out_file, size_t height,
                       size_t width, const RGBTRIPLE image[height][width])
{
    if (out_file != stdout && (errno = 0, ftruncate(fileno(out_file), 0))) {
        errno ? perror("seek()") : 
                     (void) fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    if (fwrite(&bf->bf_type, sizeof bf->bf_type, 1, out_file) != 1
        || fwrite(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, out_file) != 1
        || fwrite(bi, sizeof *bi, 1, out_file) != 1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    const size_t padding = determine_padding(width);

    if (write_scanlines(out_file, height, width, image, padding) == -1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }
    return out_file == stdout || !fclose(out_file);
}

static int read_scanlines(FILE * in_file, size_t height, size_t width,
                          RGBTRIPLE image[][width], size_t padding)
{
    /* Iterate over infile's scanlines */
    for (size_t i = 0; i < height; i++) {
        /* Read row into pixel array */
        if (fread(image[i], sizeof image[i][0], width, in_file) != width) {
            return -1;
        }

        /* Temporary buffer to read and discard padding. */
        uint8_t padding_buffer[BMP_SCANLINE_PADDING];

        if (fread(padding_buffer, 1, padding, in_file) != padding) {
            return -1;
        }
    }
    return 0;
}

static void *read_image(BITMAPFILEHEADER * restrict bf,
                        BITMAPINFOHEADER * restrict bi,
                        size_t *restrict height_ptr,
                        size_t *restrict width_ptr, FILE * restrict in_file)
{
    /* Read infile's BITMAPFILEHEADER and BITMAPINFOHEADER. */
    if (fread(&bf->bf_type, sizeof bf->bf_type, 1, in_file) != 1
        || fread(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, in_file) != 1
        || fread(bi, sizeof *bi, 1, in_file) != 1) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    /* Ensure infile is (likely) a 24-bit uncompressed BMP 4.0 */
    if (!bmp_check_header(bf, bi)) {
        fputs("Error - unsupported file format.\n", stderr);
        return NULL;
    }
    
    /* If bi_height is positive, the bitmap is a bottom-up DIB with the origin 
     * at the lower left corner. It bi_height is negative, the bitmap is a top-
     * down DIB with the origin at the upper left corner. 
     * We currenly only support images stored as top-down, so bail if the format
     * is elsewise.
     */
    if (bi->bi_height > 0) {
        fputs("Error - Bottom-up BMP image format is not yet supported.\n", stderr);
        return NULL;
    }
 
    /* Get image's dimensions. */
    uint32_t abs_height = bi->bi_height < 0 ? 0u - (uint32_t) bi->bi_height :
        (uint32_t) bi->bi_height;

    /* If we are on a too small a machine, there is not much hope, so bail. */
    if (abs_height > SIZE_MAX) {
        fputs
            ("Error - Image dimensions are too large for this system to process.\n",
             stderr);
        return NULL;
    }
    
    size_t height = (size_t) abs_height;
    size_t width = (size_t) bi->bi_width;

    if (!height || !width) {
        fputs("Error - corrupted BMP file: width or height is zero.\n", stderr);
        return NULL;
    }

    if (width > (SIZE_MAX - sizeof (RGBTRIPLE)) / sizeof (RGBTRIPLE)) {
        fputs("Error - image width is too large for this system to process.\n",
              stderr);
        return NULL;
    }

    /* Allocate memory for image */
    RGBTRIPLE(*image)[width] = calloc(height, sizeof *image);

    if (!image) {
        fputs("Error - not enough memory to store image.\n", stderr);
        return NULL;
    }

    const size_t padding = determine_padding(width);

    if (read_scanlines(in_file, height, width, image, padding)) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    *height_ptr = height;
    *width_ptr = width;
    return image;
}

static int process_image(const struct flags *restrict options,
                         FILE * restrict in_file, FILE * restrict out_file)
{
    BITMAPFILEHEADER bf;
    BITMAPINFOHEADER bi;

    size_t height = 0;
    size_t width = 0;

    void *const image = read_image(&bf, &bi, &height, &width, in_file);

    if (!image) {
        return -1;
    }

    apply_filter(options, height, width, image);

    if (write_image(&bf, &bi, out_file, height, width, image) == -1) {
        return -1;
    }

    free(image);
    return 0;
}

int main(int argc, char *argv[])
{
    /* Sanity check. POSIX requires the invoking process to pass a non-NULL 
     * argv[0].
     */
    if (!argv) {
        fputs("A NULL argv[0] was passed through an exec system call.\n",
              stderr);
        return EXIT_FAILURE;
    }

    /* Define allowable filters */
    static const struct option long_options[] = {
        { "grayscale", no_argument, NULL, 'g' },
        { "reverse", no_argument, NULL, 'r' },
        { "sepia", no_argument, NULL, 's' },
        { "blur", no_argument, NULL, 'b' },
        { "help", no_argument, NULL, 'h' },
        { "output", required_argument, NULL, 'o' },
        { NULL, 0, NULL, 0 }
    };

    FILE *in_file = stdin;
    struct flags options = { false, false, false, false, stdout };
    int result = EXIT_SUCCESS;

    parse_options(long_options, "grsbho:", &options, argc, argv);

    if ((optind + 1) == argc) {
        in_file = (errno = 0, fopen(argv[optind], "rb"));

        if (!in_file) {
            errno ? perror(argv[optind]) : (void)
                fputs("Error - failed to open input file.", stderr);
            return EXIT_FAILURE;
        }
    } else if (optind > argc) {
        err_msg();
    }

    if (process_image(&options, in_file, options.out_file) == -1) {
        result = EXIT_FAILURE;
    }

    if (in_file != stdin) {
        fclose(in_file);
    }

    return result;
}
#ifdef _POSIX_C_SOURCE
#undef _POSIX_C_SOURCE
#endif

#ifdef _XOPEN_SOURCE
#undef _XOPEN_SOURCE
#endif

#define _POSIX_C_SOURCE 200819L
#define _XOPEN_SOURCE   700

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>

#include <getopt.h>

#include "helpers.h"

/* ARRAY_CARDINALITY(x) calculates the number of elements in the array 'x'.
 * If 'x' is a pointer, it will trigger an assertion.
 */
#define ARRAY_CARDINALITY(x) \
        (assert((void *)&(x) == (void *)(x)), sizeof (x) / sizeof *(x))

#define BMP_SCANLINE_PADDING 4
#define BF_UNPADDED_REGION_SIZE 12

struct flags {
    bool sflag;                 /* Sepia flag. */
    bool rflag;                 /* Reverse flag. */
    bool gflag;                 /* Greyscale flag. */
    bool bflag;                 /* Blur flag. */
    FILE *out_file;             /* Output to file. */
};

static void help(void)
{
    puts("Usage: filter [OPTIONS] <infile> <outfile>\n"
         "\n\tTransform your BMP images with powerful filters.\n\n"
         "Options:\n"
         "    -s, --sepia           Apply a sepia filter for a warm, vintage look.\n"
         "    -r, --reverse         Create a horizontal reflection for a mirror effect.\n"
         "    -g, --grayscale       Convert the image to classic greyscale.\n"
         "    -b, --blur            Add a soft blur to the image.\n"
         "    -h, --help            displays this message and exit.\n");
    exit(EXIT_SUCCESS);
}

static void err_msg(void)
{
    fputs("Usage: filter [OPTIONS] <infile> <outfile>\n"
          "Try filter -h for help.\n", stderr);
    exit(EXIT_FAILURE);
}

static void parse_options(const struct option *restrict long_options,
                          const char *restrict short_options,
                          struct flags *restrict opt_ptr, int argc,
                          char *const argv[])
{
    int c;

    while ((c =
            getopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
        switch (c) {
            case 's':
                opt_ptr->sflag = true;
                break;
            case 'r':
                opt_ptr->rflag = true;
                break;
            case 'g':
                opt_ptr->gflag = true;
                break;
            case 'b':
                opt_ptr->bflag = true;
                break;
            case 'h':
                help();
                break;
            case 'o':
                /* We'll seek to the beginning once we've read input,
                 * in case it's the same file. 
                 */
                opt_ptr->out_file = (errno = 0, fopen(optarg, "ab"));

                if (!opt_ptr->out_file) {
                    errno ? perror(optarg) : (void)
                        fputs("Error - failed to open output file.", stderr);
                }
                break;

                /* case '?' */
            default:
                err_msg();
                break;
        }
    }
}

static void apply_filter(const struct flags *options, size_t height,
                         size_t width, RGBTRIPLE image[height][width])
{
    struct {
        bool flag;
        void (* const func)(size_t height, size_t width, RGBTRIPLE image[height][width]);
    } group[] = {
        { options->sflag, sepia },
        { options->rflag, reflect },
        { options->gflag, grayscale },
        { options->bflag, blur }, 
    };
    
    for (size_t i = 0; i < ARRAY_CARDINALITY(group); ++i) {
        if (group[i].flag) {
            group[i].func(height, width, image);
        }
    }
}

static size_t determine_padding(size_t width)
{
    /* In BMP images, each scanline (a row of pixels) must be a multiple of
     * BMP_SCANLINE_PADDING bytes in size. If the width of the image in pixels 
     * multipled by the size of each pixel (in bytes) is not a multiple of 
     * BMP_SCANLINE_PADDING, padding is added to make it so. 
     */
    return (BMP_SCANLINE_PADDING - (width * sizeof (RGBTRIPLE)) % BMP_SCANLINE_PADDING) % BMP_SCANLINE_PADDING;
}

static int write_scanlines(FILE * out_file, size_t height, size_t width,
                           const RGBTRIPLE image[][width], size_t padding)
{
    const size_t pad_byte = 0x00;

    /* Write new pixels to outfile */
    for (size_t i = 0; i < height; ++i) {
        /* Write row to outfile, with padding at the end. */
        if (fwrite(image[i], sizeof image[i][0], width, out_file) != width
            || fwrite(&pad_byte, 1, padding, out_file) != padding) {
            return -1;
        }
    }

    return 0;
}

static int write_image(const BITMAPFILEHEADER * restrict bf,
                       const BITMAPINFOHEADER * restrict bi,
                       FILE * restrict out_file, size_t height,
                       size_t width, const RGBTRIPLE image[height][width])
{
    if (out_file != stdout && (errno = 0, ftruncate(fileno(out_file), 0))) {
        errno ? perror("seek()") : 
                     (void) fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    if (fwrite(&bf->bf_type, sizeof bf->bf_type, 1, out_file) != 1
        || fwrite(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, out_file) != 1
        || fwrite(bi, sizeof *bi, 1, out_file) != 1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    const size_t padding = determine_padding(width);

    if (write_scanlines(out_file, height, width, image, padding) == -1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }
    return out_file == stdout || !fclose(out_file);
}

static int read_scanlines(FILE * in_file, size_t height, size_t width,
                          RGBTRIPLE image[][width], size_t padding)
{
    /* Iterate over infile's scanlines */
    for (size_t i = 0; i < height; i++) {
        /* Read row into pixel array */
        if (fread(image[i], sizeof image[i][0], width, in_file) != width) {
            return -1;
        }

        /* Temporary buffer to read and discard padding. */
        uint8_t padding_buffer[BMP_SCANLINE_PADDING];

        if (fread(padding_buffer, 1, padding, in_file) != padding) {
            return -1;
        }
    }
    return 0;
}

static void *read_image(BITMAPFILEHEADER * restrict bf,
                        BITMAPINFOHEADER * restrict bi,
                        size_t *restrict height_ptr,
                        size_t *restrict width_ptr, FILE * restrict in_file)
{
    /* Read infile's BITMAPFILEHEADER and BITMAPINFOHEADER. */
    if (fread(&bf->bf_type, sizeof bf->bf_type, 1, in_file) != 1
        || fread(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, in_file) != 1
        || fread(bi, sizeof *bi, 1, in_file) != 1) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    /* Ensure infile is (likely) a 24-bit uncompressed BMP 4.0 */
    if (!bmp_check_header(bf, bi)) {
        fputs("Error - unsupported file format.\n", stderr);
        return NULL;
    }
    
    /* There seems to be no need to treating the data differently. 
     * The code handles both top-down and bottom-up just fine. Or does it?
     */
#if 0    
    /* If bi_height is positive, the bitmap is a bottom-up DIB with the origin 
     * at the lower left corner. It bi_height is negative, the bitmap is a top-
     * down DIB with the origin at the upper left corner. 
     * We currenly only support images stored as top-down, so bail if the format
     * is elsewise.
     */
    if (bi->bi_height > 0) {
        fputs("Error - Bottom-up BMP image format is not yet supported.\n", stderr);
        return NULL;
    }
#endif

    /* Get image's dimensions. */
    uint32_t abs_height = bi->bi_height < 0 ? 0u - (uint32_t) bi->bi_height :
        (uint32_t) bi->bi_height;

    /* If we are on a too small a machine, there is not much hope, so bail. */
    if (abs_height > SIZE_MAX) {
        fputs
            ("Error - Image dimensions are too large for this system to process.\n",
             stderr);
        return NULL;
    }
    
    size_t height = (size_t) abs_height;
    size_t width = (size_t) bi->bi_width;

    if (!height || !width) {
        fputs("Error - corrupted BMP file: width or height is zero.\n", stderr);
        return NULL;
    }

    if (width > (SIZE_MAX - sizeof (RGBTRIPLE)) / sizeof (RGBTRIPLE)) {
        fputs("Error - image width is too large for this system to process.\n",
              stderr);
        return NULL;
    }

    /* Allocate memory for image */
    RGBTRIPLE(*image)[width] = calloc(height, sizeof *image);

    if (!image) {
        fputs("Error - not enough memory to store image.\n", stderr);
        return NULL;
    }

    const size_t padding = determine_padding(width);

    if (read_scanlines(in_file, height, width, image, padding)) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    *height_ptr = height;
    *width_ptr = width;
    return image;
}

static int process_image(const struct flags *restrict options,
                         FILE * restrict in_file, FILE * restrict out_file)
{
    BITMAPFILEHEADER bf;
    BITMAPINFOHEADER bi;

    size_t height = 0;
    size_t width = 0;

    void *const image = read_image(&bf, &bi, &height, &width, in_file);

    if (!image) {
        return -1;
    }

    apply_filter(options, height, width, image);

    if (write_image(&bf, &bi, out_file, height, width, image) == -1) {
        return -1;
    }

    free(image);
    return 0;
}

int main(int argc, char *argv[])
{
    /* Sanity check. POSIX requires the invoking process to pass a non-NULL 
     * argv[0].
     */
    if (!argv) {
        fputs("A NULL argv[0] was passed through an exec system call.\n",
              stderr);
        return EXIT_FAILURE;
    }

    /* Define allowable filters */
    static const struct option long_options[] = {
        { "grayscale", no_argument, NULL, 'g' },
        { "reverse", no_argument, NULL, 'r' },
        { "sepia", no_argument, NULL, 's' },
        { "blur", no_argument, NULL, 'b' },
        { "help", no_argument, NULL, 'h' },
        { "output", required_argument, NULL, 'o' },
        { NULL, 0, NULL, 0 }
    };

    FILE *in_file = stdin;
    struct flags options = { false, false, false, false, stdout };
    int result = EXIT_SUCCESS;

    parse_options(long_options, "grsbho:", &options, argc, argv);

    if ((optind + 1) == argc) {
        in_file = (errno = 0, fopen(argv[optind], "rb"));

        if (!in_file) {
            errno ? perror(argv[optind]) : (void)
                fputs("Error - failed to open input file.", stderr);
            return EXIT_FAILURE;
        }
    } else if (optind > argc) {
        err_msg();
    }

    if (process_image(&options, in_file, options.out_file) == -1) {
        result = EXIT_FAILURE;
    }

    if (in_file != stdin) {
        fclose(in_file);
    }

    return result;
}
added 99 characters in body
Source Link
Madagascar
  • 10.1k
  • 1
  • 16
  • 52
#ifdef _POSIX_C_SOURCE
#undef _POSIX_C_SOURCE
#endif

#ifdef _XOPEN_SOURCE
#undef _XOPEN_SOURCE
#endif

#define _POSIX_C_SOURCE 200819L
#define _XOPEN_SOURCE   700

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>

#include <getopt.h>

#include "helpers.h"

/* TODO: Support BMP images stored as bottom-up. */

/* ARRAY_CARDINALITY(x) calculates the number of elements in the array 'x'.
 * If 'x' is a pointer, it will trigger an assertion.
 */
#define ARRAY_CARDINALITY(x) \
        (assert((void *)&(x) == (void *)(x)), sizeof (x) / sizeof *(x))

#define BMP_SCANLINE_PADDING 4
#define BF_UNPADDED_REGION_SIZE 12

struct flags {
    bool sflag;                 /* Sepia flag. */
    bool rflag;                 /* Reverse flag. */
    bool gflag;                 /* Greyscale flag. */
    bool bflag;                 /* Blur flag. */
    FILE *out_file;             /* Output to file. */
};

static void help(void)
{
    puts("Usage: filter [OPTIONS] <infile> <outfile>\n"
         "\n\tTransform your BMP images with powerful filters.\n\n"
         "Options:\n"
         "    -s, --sepia           Apply a sepia filter for a warm, vintage look.\n"
         "    -r, --reverse         Create a horizontal reflection for a mirror effect.\n"
         "    -g, --grayscale       Convert the image to classic greyscale.\n"
         "    -b, --blur            Add a soft blur to the image.\n"
         "    -h, --help            displays this message and exit.\n");
    exit(EXIT_SUCCESS);
}

static void err_msg(void)
{
    fputs("Usage: filter [OPTIONS] <infile> <outfile>\n"
          "Try filter -h for help.\n", stderr);
    exit(EXIT_FAILURE);
}

static void parse_options(const struct option *restrict long_options,
                          const char *restrict short_options,
                          struct flags *restrict opt_ptr, int argc,
                          char *const argv[])
{
    int c;

    while ((c =
            getopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
        switch (c) {
            case 's':
                opt_ptr->sflag = true;
                break;
            case 'r':
                opt_ptr->rflag = true;
                break;
            case 'g':
                opt_ptr->gflag = true;
                break;
            case 'b':
                opt_ptr->bflag = true;
                break;
            case 'h':
                help();
                break;
            case 'o':
                /* We'll seek to the beginning once we've read input,
                 * in case it's the same file. 
                 */
                opt_ptr->out_file = (errno = 0, fopen(optarg, "ab"));

                if (!opt_ptr->out_file) {
                    errno ? perror(optarg) : (void)
                        fputs("Error - failed to open output file.", stderr);
                }
                break;

                /* case '?' */
            default:
                err_msg();
                break;
        }
    }
}

static void apply_filter(const struct flags *options, size_t height,
                         size_t width, RGBTRIPLE image[height][width])
{
    struct {
        bool flag;
        void (* const func)(size_t height, size_t width, RGBTRIPLE image[height][width]);
    } group[] = {
        { options->sflag, sepia },
        { options->rflag, reflect },
        { options->gflag, grayscale },
        { options->bflag, blur }, 
    };
    
    for (size_t i = 0; i < ARRAY_CARDINALITY(group); ++i) {
        if (group[i].flag) {
            group[i].func(height, width, image);
        }
    }
}

static size_t determine_padding(size_t width)
{
    /* In BMP images, each scanline (a row of pixels) must be a multiple of
     * BMP_SCANLINE_PADDING bytes in size. If the width of the image in pixels 
     * multipled by the size of each pixel (in bytes) is not a multiple of 
     * BMP_SCANLINE_PADDING, padding is added to make it so. 
     */
    return (BMP_SCANLINE_PADDING - (width * sizeof (RGBTRIPLE)) % BMP_SCANLINE_PADDING) % BMP_SCANLINE_PADDING;
}

static int write_scanlines(FILE * out_file, size_t height, size_t width,
                           const RGBTRIPLE image[][width], size_t padding)
{
    const size_t pad_byte = 0x00;

    /* Write new pixels to outfile */
    for (size_t i = 0; i < height; ++i) {
        /* Write row to outfile, with padding at the end. */
        if (fwrite(image[i], sizeof image[i][0], width, out_file) != width
            || fwrite(&pad_byte, 1, padding, out_file) != padding) {
            return -1;
        }
    }

    return 0;
}

static int write_image(const BITMAPFILEHEADER * restrict bf,
                       const BITMAPINFOHEADER * restrict bi,
                       FILE * restrict out_file, size_t height,
                       size_t width, const RGBTRIPLE image[height][width])
{
    if (out_file != stdout && (errno = 0, ftruncate(fileno(out_file), 0))) {
        errno ? perror("seek()") : 
                     (void) fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    if (fwrite(&bf->bf_type, sizeof bf->bf_type, 1, out_file) != 1
        || fwrite(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, out_file) != 1
        || fwrite(bi, sizeof *bi, 1, out_file) != 1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    const size_t padding = determine_padding(width);

    if (write_scanlines(out_file, height, width, image, padding) == -1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }
    return out_file == stdout || !fclose(out_file);
}

static int read_scanlines(FILE * in_file, size_t height, size_t width,
                          RGBTRIPLE image[][width], size_t padding)
{
    /* Iterate over infile's scanlines */
    for (size_t i = 0; i < height; i++) {
        /* Read row into pixel array */
        if (fread(image[i], sizeof image[i][0], width, in_file) != width) {
            return -1;
        }

        /* Temporary buffer to read and discard padding. */
        uint8_t padding_buffer[BMP_SCANLINE_PADDING];

        if (fread(padding_buffer, 1, padding, in_file) != padding) {
            return -1;
        }
    }
    return 0;
}

static void *read_image(BITMAPFILEHEADER * restrict bf,
                        BITMAPINFOHEADER * restrict bi,
                        size_t *restrict height_ptr,
                        size_t *restrict width_ptr, FILE * restrict in_file)
{
    /* Read infile's BITMAPFILEHEADER and BITMAPINFOHEADER. */
    if (fread(&bf->bf_type, sizeof bf->bf_type, 1, in_file) != 1
        || fread(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, in_file) != 1
        || fread(bi, sizeof *bi, 1, in_file) != 1) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    /* Ensure infile is (likely) a 24-bit uncompressed BMP 4.0 */
    if (!bmp_check_header(bf, bi)) {
        fputs("Error - unsupported file format.\n", stderr);
        return NULL;
    }
    
    /* If bi_height is positive, the bitmap is a bottom-up DIB with the origin 
     * at the lower left corner. It bi_height is negative, the bitmap is a top-
     * down DIB with the origin at the upper left corner. 
     * We currenly only support images stored as top-down, so bail if the format
     * is elsewise.
     */
    if (bi->bi_height > 0) {
        fputs("Error - Bottom-up BMP image format is not yet supported.\n", stderr);
        return NULL;
    }
 
    /* Get image's dimensions. */
    uint32_t abs_height = bi->bi_height < 0 ? 0u - (uint32_t) bi->bi_height :
        (uint32_t) bi->bi_height;

    /* If we are on a too small a machine, there is not much hope, so bail. */
    if (abs_height > SIZE_MAX) {
        fputs
            ("Error - Image dimensions are too large for this system to process.\n",
             stderr);
        return NULL;
    }
    
    size_t height = (size_t) abs_height;
    size_t width = (size_t) bi->bi_width;

    if (!height || !width) {
        fputs("Error - corrupted BMP file: width or height is zero.\n", stderr);
        return NULL;
    }

    if (width > (SIZE_MAX - sizeof (RGBTRIPLE)) / sizeof (RGBTRIPLE)) {
        fputs("Error - image width is too large for this system to process.\n",
              stderr);
        return NULL;
    }

    /* Allocate memory for image */
    RGBTRIPLE(*image)[width] = calloc(height, sizeof *image);

    if (!image) {
        fputs("Error - not enough memory to store image.\n", stderr);
        return NULL;
    }

    const size_t padding = determine_padding(width);

    if (read_scanlines(in_file, height, width, image, padding)) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    *height_ptr = height;
    *width_ptr = width;
    return image;
}

static int process_image(const struct flags *restrict options,
                         FILE * restrict in_file, FILE * restrict out_file)
{
    BITMAPFILEHEADER bf;
    BITMAPINFOHEADER bi;

    size_t height = 0;
    size_t width = 0;

    void *const image = read_image(&bf, &bi, &height, &width, in_file);

    if (!image) {
        return -1;
    }

    apply_filter(options, height, width, image);

    if (write_image(&bf, &bi, out_file, height, width, image) == -1) {
        return -1;
    }

    free(image);
    return 0;
}

int main(int argc, char *argv[])
{
    /* Sanity check. POSIX requires the invoking process to pass a non-NULL 
     * argv[0].
     */
    if (!argv) {
        fputs("A NULL argv[0] was passed through an exec system call.\n",
              stderr);
        return EXIT_FAILURE;
    }

    /* Define allowable filters */
    static const struct option long_options[] = {
        { "grayscale", no_argument, NULL, 'g' },
        { "reverse", no_argument, NULL, 'r' },
        { "sepia", no_argument, NULL, 's' },
        { "blur", no_argument, NULL, 'b' },
        { "help", no_argument, NULL, 'h' },
        { "output", required_argument, NULL, 'o' },
        { NULL, 0, NULL, 0 }
    };

    FILE *in_file = stdin;
    struct flags options = { false, false, false, false, stdout };
    int result = EXIT_SUCCESS;

    parse_options(long_options, "grsbho:", &options, argc, argv);

    if ((optind + 1) == argc) {
        in_file = (errno = 0, fopen(argv[optind], "rb"));

        if (!in_file) {
            errno ? perror(argv[optind]) : (void)
                fputs("Error - failed to open input file.", stderr);
            return EXIT_FAILURE;
        }
    } else if (optind > argc) {
        err_msg();
    }

    if (process_image(&options, in_file, options.out_file) == -1) {
        result = EXIT_FAILURE;
    }

    if (in_file != stdin) {
        fclose(in_file);
    }

    return result;
}
#ifdef _POSIX_C_SOURCE
#undef _POSIX_C_SOURCE
#endif

#ifdef _XOPEN_SOURCE
#undef _XOPEN_SOURCE
#endif

#define _POSIX_C_SOURCE 200819L
#define _XOPEN_SOURCE   700

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>

#include <getopt.h>

#include "helpers.h"

/* TODO: Support BMP images stored as bottom-up. */

/* ARRAY_CARDINALITY(x) calculates the number of elements in the array 'x'.
 * If 'x' is a pointer, it will trigger an assertion.
 */
#define ARRAY_CARDINALITY(x) \
        (assert((void *)&(x) == (void *)(x)), sizeof (x) / sizeof *(x))

#define BMP_SCANLINE_PADDING 4
#define BF_UNPADDED_REGION_SIZE 12

struct flags {
    bool sflag;                 /* Sepia flag. */
    bool rflag;                 /* Reverse flag. */
    bool gflag;                 /* Greyscale flag. */
    bool bflag;                 /* Blur flag. */
    FILE *out_file;             /* Output to file. */
};

static void help(void)
{
    puts("Usage: filter [OPTIONS] <infile> <outfile>\n"
         "\n\tTransform your BMP images with powerful filters.\n\n"
         "Options:\n"
         "    -s, --sepia           Apply a sepia filter for a warm, vintage look.\n"
         "    -r, --reverse         Create a horizontal reflection for a mirror effect.\n"
         "    -g, --grayscale       Convert the image to classic greyscale.\n"
         "    -b, --blur            Add a soft blur to the image.\n"
         "    -h, --help            displays this message and exit.\n");
    exit(EXIT_SUCCESS);
}

static void err_msg(void)
{
    fputs("Usage: filter [OPTIONS] <infile> <outfile>\n"
          "Try filter -h for help.\n", stderr);
    exit(EXIT_FAILURE);
}

static void parse_options(const struct option *restrict long_options,
                          const char *restrict short_options,
                          struct flags *restrict opt_ptr, int argc,
                          char *const argv[])
{
    int c;

    while ((c =
            getopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
        switch (c) {
            case 's':
                opt_ptr->sflag = true;
                break;
            case 'r':
                opt_ptr->rflag = true;
                break;
            case 'g':
                opt_ptr->gflag = true;
                break;
            case 'b':
                opt_ptr->bflag = true;
                break;
            case 'h':
                help();
                break;
            case 'o':
                /* We'll seek to the beginning once we've read input,
                 * in case it's the same file. 
                 */
                opt_ptr->out_file = (errno = 0, fopen(optarg, "ab"));

                if (!opt_ptr->out_file) {
                    errno ? perror(optarg) : (void)
                        fputs("Error - failed to open output file.", stderr);
                }
                break;

                /* case '?' */
            default:
                err_msg();
                break;
        }
    }
}

static void apply_filter(const struct flags *options, size_t height,
                         size_t width, RGBTRIPLE image[height][width])
{
    struct {
        bool flag;
        void (* const func)(size_t height, size_t width, RGBTRIPLE image[height][width]);
    } group[] = {
        { options->sflag, sepia },
        { options->rflag, reflect },
        { options->gflag, grayscale },
        { options->bflag, blur }, 
    };
    
    for (size_t i = 0; i < ARRAY_CARDINALITY(group); ++i) {
        if (group[i].flag) {
            group[i].func(height, width, image);
        }
    }
}

static size_t determine_padding(size_t width)
{
    /* In BMP images, each scanline (a row of pixels) must be a multiple of
     * BMP_SCANLINE_PADDING bytes in size. If the width of the image in pixels 
     * multipled by the size of each pixel (in bytes) is not a multiple of 
     * BMP_SCANLINE_PADDING, padding is added to make it so. 
     */
    return (BMP_SCANLINE_PADDING - (width * sizeof (RGBTRIPLE)) % BMP_SCANLINE_PADDING) % BMP_SCANLINE_PADDING;
}

static int write_scanlines(FILE * out_file, size_t height, size_t width,
                           const RGBTRIPLE image[][width], size_t padding)
{
    const size_t pad_byte = 0x00;

    /* Write new pixels to outfile */
    for (size_t i = 0; i < height; ++i) {
        /* Write row to outfile, with padding at the end. */
        if (fwrite(image[i], sizeof image[i][0], width, out_file) != width
            || fwrite(&pad_byte, 1, padding, out_file) != padding) {
            return -1;
        }
    }

    return 0;
}

static int write_image(const BITMAPFILEHEADER * restrict bf,
                       const BITMAPINFOHEADER * restrict bi,
                       FILE * restrict out_file, size_t height,
                       size_t width, const RGBTRIPLE image[height][width])
{
    if (out_file != stdout && (errno = 0, ftruncate(fileno(out_file), 0))) {
        perror("seek()");
        return -1;
    }

    if (fwrite(&bf->bf_type, sizeof bf->bf_type, 1, out_file) != 1
        || fwrite(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, out_file) != 1
        || fwrite(bi, sizeof *bi, 1, out_file) != 1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    const size_t padding = determine_padding(width);

    if (write_scanlines(out_file, height, width, image, padding) == -1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }
    return out_file == stdout || !fclose(out_file);
}

static int read_scanlines(FILE * in_file, size_t height, size_t width,
                          RGBTRIPLE image[][width], size_t padding)
{
    /* Iterate over infile's scanlines */
    for (size_t i = 0; i < height; i++) {
        /* Read row into pixel array */
        if (fread(image[i], sizeof image[i][0], width, in_file) != width) {
            return -1;
        }

        /* Temporary buffer to read and discard padding. */
        uint8_t padding_buffer[BMP_SCANLINE_PADDING];

        if (fread(padding_buffer, 1, padding, in_file) != padding) {
            return -1;
        }
    }
    return 0;
}

static void *read_image(BITMAPFILEHEADER * restrict bf,
                        BITMAPINFOHEADER * restrict bi,
                        size_t *restrict height_ptr,
                        size_t *restrict width_ptr, FILE * restrict in_file)
{
    /* Read infile's BITMAPFILEHEADER and BITMAPINFOHEADER. */
    if (fread(&bf->bf_type, sizeof bf->bf_type, 1, in_file) != 1
        || fread(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, in_file) != 1
        || fread(bi, sizeof *bi, 1, in_file) != 1) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    /* Ensure infile is (likely) a 24-bit uncompressed BMP 4.0 */
    if (!bmp_check_header(bf, bi)) {
        fputs("Error - unsupported file format.\n", stderr);
        return NULL;
    }
    
    /* If bi_height is positive, the bitmap is a bottom-up DIB with the origin 
     * at the lower left corner. It bi_height is negative, the bitmap is a top-
     * down DIB with the origin at the upper left corner. 
     * We currenly only support images stored as top-down, so bail if the format
     * is elsewise.
     */
    if (bi->bi_height > 0) {
        fputs("Error - Bottom-up BMP image format is not yet supported.\n", stderr);
        return NULL;
    }
 
    /* Get image's dimensions. */
    uint32_t abs_height = bi->bi_height < 0 ? 0u - (uint32_t) bi->bi_height :
        (uint32_t) bi->bi_height;

    /* If we are on a too small a machine, there is not much hope, so bail. */
    if (abs_height > SIZE_MAX) {
        fputs
            ("Error - Image dimensions are too large for this system to process.\n",
             stderr);
        return NULL;
    }
    
    size_t height = (size_t) abs_height;
    size_t width = (size_t) bi->bi_width;

    if (!height || !width) {
        fputs("Error - corrupted BMP file: width or height is zero.\n", stderr);
        return NULL;
    }

    if (width > (SIZE_MAX - sizeof (RGBTRIPLE)) / sizeof (RGBTRIPLE)) {
        fputs("Error - image width is too large for this system to process.\n",
              stderr);
        return NULL;
    }

    /* Allocate memory for image */
    RGBTRIPLE(*image)[width] = calloc(height, sizeof *image);

    if (!image) {
        fputs("Error - not enough memory to store image.\n", stderr);
        return NULL;
    }

    const size_t padding = determine_padding(width);

    if (read_scanlines(in_file, height, width, image, padding)) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    *height_ptr = height;
    *width_ptr = width;
    return image;
}

static int process_image(const struct flags *restrict options,
                         FILE * restrict in_file, FILE * restrict out_file)
{
    BITMAPFILEHEADER bf;
    BITMAPINFOHEADER bi;

    size_t height = 0;
    size_t width = 0;

    void *const image = read_image(&bf, &bi, &height, &width, in_file);

    if (!image) {
        return -1;
    }

    apply_filter(options, height, width, image);

    if (write_image(&bf, &bi, out_file, height, width, image) == -1) {
        return -1;
    }

    free(image);
    return 0;
}

int main(int argc, char *argv[])
{
    /* Sanity check. POSIX requires the invoking process to pass a non-NULL 
     * argv[0].
     */
    if (!argv) {
        fputs("A NULL argv[0] was passed through an exec system call.\n",
              stderr);
        return EXIT_FAILURE;
    }

    /* Define allowable filters */
    static const struct option long_options[] = {
        { "grayscale", no_argument, NULL, 'g' },
        { "reverse", no_argument, NULL, 'r' },
        { "sepia", no_argument, NULL, 's' },
        { "blur", no_argument, NULL, 'b' },
        { "help", no_argument, NULL, 'h' },
        { "output", required_argument, NULL, 'o' },
        { NULL, 0, NULL, 0 }
    };

    FILE *in_file = stdin;
    struct flags options = { false, false, false, false, stdout };
    int result = EXIT_SUCCESS;

    parse_options(long_options, "grsbho:", &options, argc, argv);

    if ((optind + 1) == argc) {
        in_file = (errno = 0, fopen(argv[optind], "rb"));

        if (!in_file) {
            errno ? perror(argv[optind]) : (void)
                fputs("Error - failed to open input file.", stderr);
            return EXIT_FAILURE;
        }
    } else if (optind > argc) {
        err_msg();
    }

    if (process_image(&options, in_file, options.out_file) == -1) {
        result = EXIT_FAILURE;
    }

    if (in_file != stdin) {
        fclose(in_file);
    }

    return result;
}
#ifdef _POSIX_C_SOURCE
#undef _POSIX_C_SOURCE
#endif

#ifdef _XOPEN_SOURCE
#undef _XOPEN_SOURCE
#endif

#define _POSIX_C_SOURCE 200819L
#define _XOPEN_SOURCE   700

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>

#include <getopt.h>

#include "helpers.h"

/* TODO: Support BMP images stored as bottom-up. */

/* ARRAY_CARDINALITY(x) calculates the number of elements in the array 'x'.
 * If 'x' is a pointer, it will trigger an assertion.
 */
#define ARRAY_CARDINALITY(x) \
        (assert((void *)&(x) == (void *)(x)), sizeof (x) / sizeof *(x))

#define BMP_SCANLINE_PADDING 4
#define BF_UNPADDED_REGION_SIZE 12

struct flags {
    bool sflag;                 /* Sepia flag. */
    bool rflag;                 /* Reverse flag. */
    bool gflag;                 /* Greyscale flag. */
    bool bflag;                 /* Blur flag. */
    FILE *out_file;             /* Output to file. */
};

static void help(void)
{
    puts("Usage: filter [OPTIONS] <infile> <outfile>\n"
         "\n\tTransform your BMP images with powerful filters.\n\n"
         "Options:\n"
         "    -s, --sepia           Apply a sepia filter for a warm, vintage look.\n"
         "    -r, --reverse         Create a horizontal reflection for a mirror effect.\n"
         "    -g, --grayscale       Convert the image to classic greyscale.\n"
         "    -b, --blur            Add a soft blur to the image.\n"
         "    -h, --help            displays this message and exit.\n");
    exit(EXIT_SUCCESS);
}

static void err_msg(void)
{
    fputs("Usage: filter [OPTIONS] <infile> <outfile>\n"
          "Try filter -h for help.\n", stderr);
    exit(EXIT_FAILURE);
}

static void parse_options(const struct option *restrict long_options,
                          const char *restrict short_options,
                          struct flags *restrict opt_ptr, int argc,
                          char *const argv[])
{
    int c;

    while ((c =
            getopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
        switch (c) {
            case 's':
                opt_ptr->sflag = true;
                break;
            case 'r':
                opt_ptr->rflag = true;
                break;
            case 'g':
                opt_ptr->gflag = true;
                break;
            case 'b':
                opt_ptr->bflag = true;
                break;
            case 'h':
                help();
                break;
            case 'o':
                /* We'll seek to the beginning once we've read input,
                 * in case it's the same file. 
                 */
                opt_ptr->out_file = (errno = 0, fopen(optarg, "ab"));

                if (!opt_ptr->out_file) {
                    errno ? perror(optarg) : (void)
                        fputs("Error - failed to open output file.", stderr);
                }
                break;

                /* case '?' */
            default:
                err_msg();
                break;
        }
    }
}

static void apply_filter(const struct flags *options, size_t height,
                         size_t width, RGBTRIPLE image[height][width])
{
    struct {
        bool flag;
        void (* const func)(size_t height, size_t width, RGBTRIPLE image[height][width]);
    } group[] = {
        { options->sflag, sepia },
        { options->rflag, reflect },
        { options->gflag, grayscale },
        { options->bflag, blur }, 
    };
    
    for (size_t i = 0; i < ARRAY_CARDINALITY(group); ++i) {
        if (group[i].flag) {
            group[i].func(height, width, image);
        }
    }
}

static size_t determine_padding(size_t width)
{
    /* In BMP images, each scanline (a row of pixels) must be a multiple of
     * BMP_SCANLINE_PADDING bytes in size. If the width of the image in pixels 
     * multipled by the size of each pixel (in bytes) is not a multiple of 
     * BMP_SCANLINE_PADDING, padding is added to make it so. 
     */
    return (BMP_SCANLINE_PADDING - (width * sizeof (RGBTRIPLE)) % BMP_SCANLINE_PADDING) % BMP_SCANLINE_PADDING;
}

static int write_scanlines(FILE * out_file, size_t height, size_t width,
                           const RGBTRIPLE image[][width], size_t padding)
{
    const size_t pad_byte = 0x00;

    /* Write new pixels to outfile */
    for (size_t i = 0; i < height; ++i) {
        /* Write row to outfile, with padding at the end. */
        if (fwrite(image[i], sizeof image[i][0], width, out_file) != width
            || fwrite(&pad_byte, 1, padding, out_file) != padding) {
            return -1;
        }
    }

    return 0;
}

static int write_image(const BITMAPFILEHEADER * restrict bf,
                       const BITMAPINFOHEADER * restrict bi,
                       FILE * restrict out_file, size_t height,
                       size_t width, const RGBTRIPLE image[height][width])
{
    if (out_file != stdout && (errno = 0, ftruncate(fileno(out_file), 0))) {
        errno ? perror("seek()") : 
                     (void) fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    if (fwrite(&bf->bf_type, sizeof bf->bf_type, 1, out_file) != 1
        || fwrite(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, out_file) != 1
        || fwrite(bi, sizeof *bi, 1, out_file) != 1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    const size_t padding = determine_padding(width);

    if (write_scanlines(out_file, height, width, image, padding) == -1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }
    return out_file == stdout || !fclose(out_file);
}

static int read_scanlines(FILE * in_file, size_t height, size_t width,
                          RGBTRIPLE image[][width], size_t padding)
{
    /* Iterate over infile's scanlines */
    for (size_t i = 0; i < height; i++) {
        /* Read row into pixel array */
        if (fread(image[i], sizeof image[i][0], width, in_file) != width) {
            return -1;
        }

        /* Temporary buffer to read and discard padding. */
        uint8_t padding_buffer[BMP_SCANLINE_PADDING];

        if (fread(padding_buffer, 1, padding, in_file) != padding) {
            return -1;
        }
    }
    return 0;
}

static void *read_image(BITMAPFILEHEADER * restrict bf,
                        BITMAPINFOHEADER * restrict bi,
                        size_t *restrict height_ptr,
                        size_t *restrict width_ptr, FILE * restrict in_file)
{
    /* Read infile's BITMAPFILEHEADER and BITMAPINFOHEADER. */
    if (fread(&bf->bf_type, sizeof bf->bf_type, 1, in_file) != 1
        || fread(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, in_file) != 1
        || fread(bi, sizeof *bi, 1, in_file) != 1) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    /* Ensure infile is (likely) a 24-bit uncompressed BMP 4.0 */
    if (!bmp_check_header(bf, bi)) {
        fputs("Error - unsupported file format.\n", stderr);
        return NULL;
    }
    
    /* If bi_height is positive, the bitmap is a bottom-up DIB with the origin 
     * at the lower left corner. It bi_height is negative, the bitmap is a top-
     * down DIB with the origin at the upper left corner. 
     * We currenly only support images stored as top-down, so bail if the format
     * is elsewise.
     */
    if (bi->bi_height > 0) {
        fputs("Error - Bottom-up BMP image format is not yet supported.\n", stderr);
        return NULL;
    }
 
    /* Get image's dimensions. */
    uint32_t abs_height = bi->bi_height < 0 ? 0u - (uint32_t) bi->bi_height :
        (uint32_t) bi->bi_height;

    /* If we are on a too small a machine, there is not much hope, so bail. */
    if (abs_height > SIZE_MAX) {
        fputs
            ("Error - Image dimensions are too large for this system to process.\n",
             stderr);
        return NULL;
    }
    
    size_t height = (size_t) abs_height;
    size_t width = (size_t) bi->bi_width;

    if (!height || !width) {
        fputs("Error - corrupted BMP file: width or height is zero.\n", stderr);
        return NULL;
    }

    if (width > (SIZE_MAX - sizeof (RGBTRIPLE)) / sizeof (RGBTRIPLE)) {
        fputs("Error - image width is too large for this system to process.\n",
              stderr);
        return NULL;
    }

    /* Allocate memory for image */
    RGBTRIPLE(*image)[width] = calloc(height, sizeof *image);

    if (!image) {
        fputs("Error - not enough memory to store image.\n", stderr);
        return NULL;
    }

    const size_t padding = determine_padding(width);

    if (read_scanlines(in_file, height, width, image, padding)) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    *height_ptr = height;
    *width_ptr = width;
    return image;
}

static int process_image(const struct flags *restrict options,
                         FILE * restrict in_file, FILE * restrict out_file)
{
    BITMAPFILEHEADER bf;
    BITMAPINFOHEADER bi;

    size_t height = 0;
    size_t width = 0;

    void *const image = read_image(&bf, &bi, &height, &width, in_file);

    if (!image) {
        return -1;
    }

    apply_filter(options, height, width, image);

    if (write_image(&bf, &bi, out_file, height, width, image) == -1) {
        return -1;
    }

    free(image);
    return 0;
}

int main(int argc, char *argv[])
{
    /* Sanity check. POSIX requires the invoking process to pass a non-NULL 
     * argv[0].
     */
    if (!argv) {
        fputs("A NULL argv[0] was passed through an exec system call.\n",
              stderr);
        return EXIT_FAILURE;
    }

    /* Define allowable filters */
    static const struct option long_options[] = {
        { "grayscale", no_argument, NULL, 'g' },
        { "reverse", no_argument, NULL, 'r' },
        { "sepia", no_argument, NULL, 's' },
        { "blur", no_argument, NULL, 'b' },
        { "help", no_argument, NULL, 'h' },
        { "output", required_argument, NULL, 'o' },
        { NULL, 0, NULL, 0 }
    };

    FILE *in_file = stdin;
    struct flags options = { false, false, false, false, stdout };
    int result = EXIT_SUCCESS;

    parse_options(long_options, "grsbho:", &options, argc, argv);

    if ((optind + 1) == argc) {
        in_file = (errno = 0, fopen(argv[optind], "rb"));

        if (!in_file) {
            errno ? perror(argv[optind]) : (void)
                fputs("Error - failed to open input file.", stderr);
            return EXIT_FAILURE;
        }
    } else if (optind > argc) {
        err_msg();
    }

    if (process_image(&options, in_file, options.out_file) == -1) {
        result = EXIT_FAILURE;
    }

    if (in_file != stdin) {
        fclose(in_file);
    }

    return result;
}
Source Link
Madagascar
  • 10.1k
  • 1
  • 16
  • 52

Filter: BMP Image Filtering Tool - follow-up

This is a follow-up of this quesiton: Filter: BMP Image Filtering Tool

Changes:

  • Added comments where necessary.
  • Added checks for integer overflow.
  • Added support for input and output redirection.
  • Added support for more than one filter in a single invocation.
  • Eliminated casts, magic numbers, and Windows typedefs for standard types.
  • Changed the algorithm for blurring the image.
  • Some other points raised by the reviewers.

Review Goals:

  • Should the functions doing the input/output be moved to bmp.c?
  • Style, potential undefined behavior, et cetera.

Code:

bmp.h:

#ifndef BMP_H
#define BMP_H

/* BMP-related data types based on Microsoft's own. */

#include <stdbool.h>
#include <stdint.h>

/* The BITMAPFILEHEADER structure contains information about the type, size,
 * and layout of a file that contains a DIB [device-independent bitmap].
 * Adapted from http://msdn.microsoft.com/en-us/library/dd183374(VS.85).aspx.
 */
typedef struct {
    uint16_t bf_type;
    uint32_t bf_size;
    uint16_t bf_reserved1;
    uint16_t bf_reserved2;
    uint32_t bf_offbits;
} BITMAPFILEHEADER;

/* The BITMAPINFOHEADER structure contains information about the
 * dimensions and color format of a DIB [device-independent bitmap].
 * Adapted from http://msdn.microsoft.com/en-us/library/dd183376(VS.85).aspx.
 */
typedef struct {
    uint32_t bi_size;
    int32_t bi_width;
    int32_t bi_height;
    uint16_t bi_planes;
    uint16_t bi_bitcount;
    uint32_t bi_compression;
    uint32_t bi_size_image;
    int32_t bi_x_resolution_ppm;
    int32_t bi_y_resolution_ppm;
    uint32_t bi_clr_used;
    uint32_t bi_clr_important;
} BITMAPINFOHEADER;

/* The RGBTRIPLE structure describes a color consisting of relative intensities of
 * red, green, and blue. Adapted from http://msdn.microsoft.com/en-us/library/aa922590.aspx.
 */
typedef struct {
    uint8_t rgbt_blue;
    uint8_t rgbt_green;
    uint8_t rgbt_red;
} RGBTRIPLE;

bool bmp_check_header(const BITMAPFILEHEADER * restrict bf,
                      const BITMAPINFOHEADER * restrict bi);

#endif      /* BMP_H */

bmp.c:

#include "bmp.h"

#include <stdbool.h>

#define SUPPORTED_BF_TYPE           0x4d42
#define SUPPORTED_BF_OFF_BITS       54
#define SUPPORTED_BI_SIZE           40
#define SUPPORTED_BI_BIT_COUNT      24
#define SUPPORTED_BI_COMPRESSION    0

bool bmp_check_header(const BITMAPFILEHEADER * restrict bf,
                      const BITMAPINFOHEADER * restrict bi)
{
    return bf->bf_type == SUPPORTED_BF_TYPE
        && bf->bf_offbits == SUPPORTED_BF_OFF_BITS
        && bi->bi_size == SUPPORTED_BI_SIZE
        && bi->bi_bitcount == SUPPORTED_BI_BIT_COUNT
        && bi->bi_compression == SUPPORTED_BI_COMPRESSION;
}

helpers.c:

#include "helpers.h"

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

#define NEIGHBORHOOD_SIZE   9
#define SCALE               8192
#define SCALE_UP(x)         ((uint_fast32_t) ((x) * SCALE + 0.5))

static inline int min(int x, int y)
{
    return x < y ? x : y;
}

void grayscale(size_t height, size_t width, RGBTRIPLE image[height][width])
{
    for (size_t i = 0; i < height; ++i) {
        for (size_t j = 0; j < width; ++j) {
            const int average = (image[i][j].rgbt_blue + image[i][j].rgbt_red +
                                 image[i][j].rgbt_green + 1) / 3;
            image[i][j].rgbt_red = image[i][j].rgbt_green = image[i][j].rgbt_blue =
                (uint8_t) average;
        }
    }
}

void sepia(size_t height, size_t width, RGBTRIPLE image[height][width])
{
    for (size_t i = 0; i < height; ++i) {
        for (size_t j = 0; j < width; ++j) {
            const int sepia_red = ((SCALE_UP(0.393) * image[i][j].rgbt_red +
                                    SCALE_UP(0.769) * image[i][j].rgbt_green +
                                    SCALE_UP(0.189) * image[i][j].rgbt_blue) +
                                   SCALE / 2) / SCALE;
            const int sepia_green =
                ((SCALE_UP(0.349) * image[i][j].rgbt_red +
                  SCALE_UP(0.686) * image[i][j].rgbt_green +
                  SCALE_UP(0.168) * image[i][j].rgbt_blue) + SCALE / 2) / SCALE;
            const int sepia_blue =
                ((SCALE_UP(0.272) * image[i][j].rgbt_red +
                  SCALE_UP(0.534) * image[i][j].rgbt_green +
                  SCALE_UP(0.131) * image[i][j].rgbt_blue) + SCALE / 2) / SCALE;

            image[i][j].rgbt_red = (uint8_t) min(255, sepia_red);
            image[i][j].rgbt_blue = (uint8_t) min(255, sepia_blue);
            image[i][j].rgbt_green = (uint8_t) min(255, sepia_green);
        }
    }
}

static inline void swap(RGBTRIPLE *restrict lhs, RGBTRIPLE *restrict rhs)
{
    RGBTRIPLE tmp = *lhs;

    *lhs = *rhs;
    *rhs = tmp;
}

void reflect(size_t height, size_t width, RGBTRIPLE image[height][width])
{
    for (size_t i = 0; i < height; ++i) {
        size_t start = 0;
        size_t end = width - 1;

        while (start < end) {
            swap(&image[i][start], &image[i][end]);
            --end;
            ++start;
        }
    }
}

void box_blur(size_t height, size_t width, RGBTRIPLE image[height][width])
{
    RGBTRIPLE(*temp)[width + 2] = (errno = 0, calloc(height + 2, sizeof *temp));

    if (!temp) {
        errno ? perror("calloc()") : (void)
            fputs("Error - failed to allocate memory for the image.", stderr);
        exit(EXIT_FAILURE);
    }

    for (size_t i = 0; i < height; ++i) {
        for (size_t j = 0; j < width; ++j) {
            temp[i + 1][j + 1] = image[i][j];
        }
    }
    
    for (size_t i = 0; i < height + 2; ++i) {
        temp[i][0] = temp[i][1];                     /* Copy left edge. */
        temp[i][width + 1] = temp[i][width];         /* Copy right edge. */
    }

    for (size_t j = 0; j < width + 2; ++j) {
        temp[0][j] = temp[1][j];                     /* Copy top edge. */
        temp[height + 1][j] = temp[height][j];       /* Copy bottom edge. */
    }

    for (size_t i = 1; i < height + 1; ++i) {
        for (size_t j = 1; j < width + 1; ++j) {
            size_t blue = 0, red = 0, green = 0;

            for (size_t k = i - 1; k < i + 2; ++k) {
                for (size_t l = j - 1; l < j + 2; ++l) {
                    red += temp[k][l].rgbt_red;
                    green += temp[k][l].rgbt_green;
                    blue += temp[k][l].rgbt_blue;
                }
            }

            image[i - 1][j - 1].rgbt_red = (uint8_t) ((red + NEIGHBORHOOD_SIZE/ 2) / NEIGHBORHOOD_SIZE);
            image[i - 1][j - 1].rgbt_blue = (uint8_t) ((blue + NEIGHBORHOOD_SIZE / 2) / NEIGHBORHOOD_SIZE);
            image[i - 1][j - 1].rgbt_green = (uint8_t) ((green + NEIGHBORHOOD_SIZE / 2) / NEIGHBORHOOD_SIZE);
        }
    }
    
    free(temp);
}

void blur(size_t height, size_t width, RGBTRIPLE image[height][width])
{
    /* We try to approximate a Gaussian blur. */
    for (size_t i = 0; i < 3; ++i) {
        box_blur(height, width, image);
    }
}

#undef NEIGHBORHOOD_SIZE  
#undef SCALE            
#undef SCALE_UP    

helpers.h:

#ifndef HELPERS_H
#define HELPERS_H

#include "bmp.h"

#include <stddef.h>

/* Convert image to grayscale. */
void grayscale(size_t height, size_t width, RGBTRIPLE image[height][width]);

/* Convert image to sepia. */
void sepia(size_t height, size_t width, RGBTRIPLE image[height][width]);

/* Reflect image horizontally. */
void reflect(size_t height, size_t width, RGBTRIPLE image[height][width]);

/* Blur image. */
void blur(size_t height, size_t width, RGBTRIPLE image[height][width]);

#endif      /* HELPERS_H */

filter.c:

#ifdef _POSIX_C_SOURCE
#undef _POSIX_C_SOURCE
#endif

#ifdef _XOPEN_SOURCE
#undef _XOPEN_SOURCE
#endif

#define _POSIX_C_SOURCE 200819L
#define _XOPEN_SOURCE   700

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>

#include <getopt.h>

#include "helpers.h"

/* TODO: Support BMP images stored as bottom-up. */

/* ARRAY_CARDINALITY(x) calculates the number of elements in the array 'x'.
 * If 'x' is a pointer, it will trigger an assertion.
 */
#define ARRAY_CARDINALITY(x) \
        (assert((void *)&(x) == (void *)(x)), sizeof (x) / sizeof *(x))

#define BMP_SCANLINE_PADDING 4
#define BF_UNPADDED_REGION_SIZE 12

struct flags {
    bool sflag;                 /* Sepia flag. */
    bool rflag;                 /* Reverse flag. */
    bool gflag;                 /* Greyscale flag. */
    bool bflag;                 /* Blur flag. */
    FILE *out_file;             /* Output to file. */
};

static void help(void)
{
    puts("Usage: filter [OPTIONS] <infile> <outfile>\n"
         "\n\tTransform your BMP images with powerful filters.\n\n"
         "Options:\n"
         "    -s, --sepia           Apply a sepia filter for a warm, vintage look.\n"
         "    -r, --reverse         Create a horizontal reflection for a mirror effect.\n"
         "    -g, --grayscale       Convert the image to classic greyscale.\n"
         "    -b, --blur            Add a soft blur to the image.\n"
         "    -h, --help            displays this message and exit.\n");
    exit(EXIT_SUCCESS);
}

static void err_msg(void)
{
    fputs("Usage: filter [OPTIONS] <infile> <outfile>\n"
          "Try filter -h for help.\n", stderr);
    exit(EXIT_FAILURE);
}

static void parse_options(const struct option *restrict long_options,
                          const char *restrict short_options,
                          struct flags *restrict opt_ptr, int argc,
                          char *const argv[])
{
    int c;

    while ((c =
            getopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
        switch (c) {
            case 's':
                opt_ptr->sflag = true;
                break;
            case 'r':
                opt_ptr->rflag = true;
                break;
            case 'g':
                opt_ptr->gflag = true;
                break;
            case 'b':
                opt_ptr->bflag = true;
                break;
            case 'h':
                help();
                break;
            case 'o':
                /* We'll seek to the beginning once we've read input,
                 * in case it's the same file. 
                 */
                opt_ptr->out_file = (errno = 0, fopen(optarg, "ab"));

                if (!opt_ptr->out_file) {
                    errno ? perror(optarg) : (void)
                        fputs("Error - failed to open output file.", stderr);
                }
                break;

                /* case '?' */
            default:
                err_msg();
                break;
        }
    }
}

static void apply_filter(const struct flags *options, size_t height,
                         size_t width, RGBTRIPLE image[height][width])
{
    struct {
        bool flag;
        void (* const func)(size_t height, size_t width, RGBTRIPLE image[height][width]);
    } group[] = {
        { options->sflag, sepia },
        { options->rflag, reflect },
        { options->gflag, grayscale },
        { options->bflag, blur }, 
    };
    
    for (size_t i = 0; i < ARRAY_CARDINALITY(group); ++i) {
        if (group[i].flag) {
            group[i].func(height, width, image);
        }
    }
}

static size_t determine_padding(size_t width)
{
    /* In BMP images, each scanline (a row of pixels) must be a multiple of
     * BMP_SCANLINE_PADDING bytes in size. If the width of the image in pixels 
     * multipled by the size of each pixel (in bytes) is not a multiple of 
     * BMP_SCANLINE_PADDING, padding is added to make it so. 
     */
    return (BMP_SCANLINE_PADDING - (width * sizeof (RGBTRIPLE)) % BMP_SCANLINE_PADDING) % BMP_SCANLINE_PADDING;
}

static int write_scanlines(FILE * out_file, size_t height, size_t width,
                           const RGBTRIPLE image[][width], size_t padding)
{
    const size_t pad_byte = 0x00;

    /* Write new pixels to outfile */
    for (size_t i = 0; i < height; ++i) {
        /* Write row to outfile, with padding at the end. */
        if (fwrite(image[i], sizeof image[i][0], width, out_file) != width
            || fwrite(&pad_byte, 1, padding, out_file) != padding) {
            return -1;
        }
    }

    return 0;
}

static int write_image(const BITMAPFILEHEADER * restrict bf,
                       const BITMAPINFOHEADER * restrict bi,
                       FILE * restrict out_file, size_t height,
                       size_t width, const RGBTRIPLE image[height][width])
{
    if (out_file != stdout && (errno = 0, ftruncate(fileno(out_file), 0))) {
        perror("seek()");
        return -1;
    }

    if (fwrite(&bf->bf_type, sizeof bf->bf_type, 1, out_file) != 1
        || fwrite(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, out_file) != 1
        || fwrite(bi, sizeof *bi, 1, out_file) != 1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }

    const size_t padding = determine_padding(width);

    if (write_scanlines(out_file, height, width, image, padding) == -1) {
        fputs("Error - failed to write to output file.\n", stderr);
        return -1;
    }
    return out_file == stdout || !fclose(out_file);
}

static int read_scanlines(FILE * in_file, size_t height, size_t width,
                          RGBTRIPLE image[][width], size_t padding)
{
    /* Iterate over infile's scanlines */
    for (size_t i = 0; i < height; i++) {
        /* Read row into pixel array */
        if (fread(image[i], sizeof image[i][0], width, in_file) != width) {
            return -1;
        }

        /* Temporary buffer to read and discard padding. */
        uint8_t padding_buffer[BMP_SCANLINE_PADDING];

        if (fread(padding_buffer, 1, padding, in_file) != padding) {
            return -1;
        }
    }
    return 0;
}

static void *read_image(BITMAPFILEHEADER * restrict bf,
                        BITMAPINFOHEADER * restrict bi,
                        size_t *restrict height_ptr,
                        size_t *restrict width_ptr, FILE * restrict in_file)
{
    /* Read infile's BITMAPFILEHEADER and BITMAPINFOHEADER. */
    if (fread(&bf->bf_type, sizeof bf->bf_type, 1, in_file) != 1
        || fread(&bf->bf_size, BF_UNPADDED_REGION_SIZE, 1, in_file) != 1
        || fread(bi, sizeof *bi, 1, in_file) != 1) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    /* Ensure infile is (likely) a 24-bit uncompressed BMP 4.0 */
    if (!bmp_check_header(bf, bi)) {
        fputs("Error - unsupported file format.\n", stderr);
        return NULL;
    }
    
    /* If bi_height is positive, the bitmap is a bottom-up DIB with the origin 
     * at the lower left corner. It bi_height is negative, the bitmap is a top-
     * down DIB with the origin at the upper left corner. 
     * We currenly only support images stored as top-down, so bail if the format
     * is elsewise.
     */
    if (bi->bi_height > 0) {
        fputs("Error - Bottom-up BMP image format is not yet supported.\n", stderr);
        return NULL;
    }
 
    /* Get image's dimensions. */
    uint32_t abs_height = bi->bi_height < 0 ? 0u - (uint32_t) bi->bi_height :
        (uint32_t) bi->bi_height;

    /* If we are on a too small a machine, there is not much hope, so bail. */
    if (abs_height > SIZE_MAX) {
        fputs
            ("Error - Image dimensions are too large for this system to process.\n",
             stderr);
        return NULL;
    }
    
    size_t height = (size_t) abs_height;
    size_t width = (size_t) bi->bi_width;

    if (!height || !width) {
        fputs("Error - corrupted BMP file: width or height is zero.\n", stderr);
        return NULL;
    }

    if (width > (SIZE_MAX - sizeof (RGBTRIPLE)) / sizeof (RGBTRIPLE)) {
        fputs("Error - image width is too large for this system to process.\n",
              stderr);
        return NULL;
    }

    /* Allocate memory for image */
    RGBTRIPLE(*image)[width] = calloc(height, sizeof *image);

    if (!image) {
        fputs("Error - not enough memory to store image.\n", stderr);
        return NULL;
    }

    const size_t padding = determine_padding(width);

    if (read_scanlines(in_file, height, width, image, padding)) {
        fputs("Error - failed to read input file.\n", stderr);
        return NULL;
    }

    *height_ptr = height;
    *width_ptr = width;
    return image;
}

static int process_image(const struct flags *restrict options,
                         FILE * restrict in_file, FILE * restrict out_file)
{
    BITMAPFILEHEADER bf;
    BITMAPINFOHEADER bi;

    size_t height = 0;
    size_t width = 0;

    void *const image = read_image(&bf, &bi, &height, &width, in_file);

    if (!image) {
        return -1;
    }

    apply_filter(options, height, width, image);

    if (write_image(&bf, &bi, out_file, height, width, image) == -1) {
        return -1;
    }

    free(image);
    return 0;
}

int main(int argc, char *argv[])
{
    /* Sanity check. POSIX requires the invoking process to pass a non-NULL 
     * argv[0].
     */
    if (!argv) {
        fputs("A NULL argv[0] was passed through an exec system call.\n",
              stderr);
        return EXIT_FAILURE;
    }

    /* Define allowable filters */
    static const struct option long_options[] = {
        { "grayscale", no_argument, NULL, 'g' },
        { "reverse", no_argument, NULL, 'r' },
        { "sepia", no_argument, NULL, 's' },
        { "blur", no_argument, NULL, 'b' },
        { "help", no_argument, NULL, 'h' },
        { "output", required_argument, NULL, 'o' },
        { NULL, 0, NULL, 0 }
    };

    FILE *in_file = stdin;
    struct flags options = { false, false, false, false, stdout };
    int result = EXIT_SUCCESS;

    parse_options(long_options, "grsbho:", &options, argc, argv);

    if ((optind + 1) == argc) {
        in_file = (errno = 0, fopen(argv[optind], "rb"));

        if (!in_file) {
            errno ? perror(argv[optind]) : (void)
                fputs("Error - failed to open input file.", stderr);
            return EXIT_FAILURE;
        }
    } else if (optind > argc) {
        err_msg();
    }

    if (process_image(&options, in_file, options.out_file) == -1) {
        result = EXIT_FAILURE;
    }

    if (in_file != stdin) {
        fclose(in_file);
    }

    return result;
}