Summary: In this homework, you will be implementing a program that loads an image file, applies a simple filter to it, and saves it back to disk. You will also learn how to read and write real-world binary file formats, specifically BMP and PPM.

Background

In this assignment you will write a program that applies a filter to an image. The image will be loaded from a local BMP or PPM file, specified by the user, and then be transformed using a color shift also specified by the user. The resulting file will be saved back to a BMP or PPM file that can be viewed on the host system.

This document is separated into four sections: Background, Requirements, Loading Binary Files, Include Files, and Submission. You have almost finished reading the Background section already. In Requirements, we will discuss what is expected of you in this homework. In Loading Binary Files, we will discuss the BMP and PPM file formats and how to interact with binary files.

Requirements

Your program needs to implement loading a binary BMP or PPM file, applying a color shift to its pixel data, and saving the modified image back into a BMP or PPM file. The width and height may be of any size, but you can assume that you will be capable of storing the image data in memory. Assume that all BMP files are 24-bit uncompressed files - chosen as it is the default for Windows 10 BMP format. That means compression method is BI_RGB), which have a 14-bit bmp header, a 40-bit dib header, and a variable length pixel array. This is shown in Figure 2. PPM files will also be in 24- bit. For testing, please only use the PPM and BMP files attached to the assignment.

As a base requirement, your program must compile and run under Xubuntu (or another variant of Ubuntu) 18.04. Sample output for this program is shown in Figure 1.

Specific Requirements:

  • BMP Headers IO: Create structures for the headers of a BMP file (BMP header struct and DIB header struct) and functions which read and write them.
  • PPM Headers IO: Create structures for the headers of a PPM file (PPM header struct) and functions which read and write them.
  • Pixel IO: Create a structure which represents a single pixel (24-bit pixel struct) and the functions for reading and writing all pixels in both PPM and BMP files.
  • Input and output file names: Read the input file name and output file name from the command line arguments. The user should be required to enter the input file name and this should be the first argument. The output file name is an "option," it is not required and it can be in any place in the option list. The output file option is specified by -o followed by the output file name. Validate that the input file exists and that both files are of the correct type (either bmp or ppm).
    • The input file is the file to read and copy.
    • The output file name is the file to write to and copy to (the new file created).
  • Output file format: The user optionally can set the output file type to either PPM or BMP by using the "-t" option. The only valid options for type should be PPM or BMP. If no format is chosen, then the default is BMP.
  • RGB Color Shift input: Accept options for red, green, or blue color shift. These are specified by"-r" -g or -b followed by an integer. Validate that these are integers. Like the -o option described above, these can come in any order in the option list and are optional for the user to enter.
    • Note: "-o" -r -g and -b -t are all options. All for of these can come in any order (-r -g -n -b, for example) and none of them are required.
  • Copy images: Correctly copy images from a BMP file to a new BMP file and from a PPM file to a new PPM file.
  • Convert images: Correctly copy images from a BMP file to a new PPM file and from a PPM file to a new BMP file.
  • Color Shift calculation: Shift the color of the new image before saving it, according to the options the user entered.
    • Color shift refers to increasing or decreasing the color in a pixel by the specified amount. So, if the user entered "-b -98" all of the blue values in a pixel would be decreased by 98.
    • If no color shift option was entered for a color, do not shift it.
    • After color shift, color should be clamped to 0 ~ 255. For example, color R = 100, shift = 200. The color R after shift should be 255 (300 clamped to 255).
  • Modular Programming: Use the provided header files correctly and create corresponding C files, along with a main file. You do not have to create any more header files or C files than this but you may if it helps you. Only modify the headers files where it says TODOs or to include other relevant header files.

Figure 1 shows an example of how the finished program should function. see image.

Loading Binary Files

The BMP File Format

For reference, use the BMP specification on Wikipedia: https://en.wikipedia.org/wiki/BMP_file_format. In Figure 2, a graphical overview of a BMP file's layout is shown. The layout is literally the meaning of the bits/bytes in the file starting at 0 and going to the end of the file. The first (green) region shows the BMP header information in 14 bytes: 2 for the signature, 4 for the file size, 2 for reserved1, 2 for reserved2, and 4 for file offset. Details on each of these (such as their data format, and contents) can be found on the Wikipedia page. For example, the area labeled "Signature" should contain the characters BM to confirm that the file is in BMP format. The second (blue region) shows the DIP header information in 40 bytes: 4 for header size, 4 for width, 4 for height, and so on. The last region (yellow) forms a 2D array of pixels. It stores columns from left to right, but rows are inverted to be bottom to top. Note that each row of pixels in this section is padded to be a multiple of four bytes. For example, if a line contains two pixels (24-bits or 3-bytes each), then an addition 2-bytes of blank data will be appended.

Figure 2: BMP file format structure for 24-bit files without compression. Image modified from https://en.wikipedia.org/wiki/File:BMPfileFormat.png. see image.

Review the following struct called BMP_Header which holds the bmp header information. (You should also consider creating structs to hold the dib header, and pixel data.) Notice that the entries in BMP_Header correspond to the pieces of data listed in the file format. In general, a chunk of 8-bits should be represented as a char, a chunk of 16-bits as a short, and 32-bits as a int. (Optionally: consider using unsigned types.)

struct BMP_Header {
char signature[2]; //ID field
int size;
//Size of the BMP file
short reserved1; //Application specific
short reserved2; //Application specific
int offset_pixel_array; //Offset where the pixel array can be found
};

Plan to review "Example 1" on the Wikipedia page to get a feel for the contents of each of these regions. A recreated version of that image is provided as the attached test2.bmp file. A good exercise would be to view the file in a hex editor like HxD.

The PPM File Format

The PPM file format is a much simpler format. Xubuntu supports opening PPM by default, but to open it in Windows or MacOS you may use GIMP: https://www.gimp.org/. For reference, please visit the official specification here: http://netpbm.sourceforge.net/doc/ppm.html. The header of a PPM file consists of the signature "P6", follow by white space, the width of the file (in ASCII characters, not binary), white space, and the height of the image (in ASCII characters, not binary). After the header, there is a single white space character followed by the pixel array. The pixels are 3 bytes each, with each color being represented by a single byte. The columns are stored left to right and the rows top to bottom. Do not use the Plain PPM format that is described in the spec, use the regular one. From top to bottom, each PPM image text file consists of the following:

  • A "magic number" for identifying the file type. A ppm image's magic number is the two characters "P6". Whitespace (blanks, TABs, CRs, LFs).
  • A width, formatted as ASCII characters in decimal. Whitespace.
  • A height, again in ASCII decimal. Whitespace.
  • The maximum color value (maxval), again in ASCII decimal. Must be less than 65536 and more than zero. A single whitespace character (usually a newline).
  • A raster of height rows, in order from top to bottom. Each row consists of width pixels, in order from left to right. Each pixel is a triplet of red, green, and blue samples, in that order. Each sample is represented in pure binary by either 1 or 2 bytes. If the maxval is less than 256, it is 1 byte. Otherwise, it is 2 bytes. The most significant byte is first.

Plan to review examples of PPM on PPM Wikipedia page (https://en.wikipedia.org/wiki/Netpbm_format#PPM_example) to get a feel for the contents of each of these regions. Some PPM header examples are available on http://paulbourke.net/dataformats/ppm/. You should include all necessary information of the header in the PPM struct.

Loading the BMP header

This is example of code to load the BMP file header (the first 14 bits). Figure 3 shows the output.

Error! Filename not specified.

Figure 3: Output of base file on test2.bmp.

//sample code to read first 14 bytes of BMP file format
FILE* file = fopen("test2.bmp", "rb");
struct BMP_Header header;

//read bitmap file header (14 bytes)
fread(&header.signature, sizeof(char)*2, 1, file);
fread(&header.size, sizeof(int), 1, file);
fread(&header.reserved1, sizeof(short), 1, file);
fread(&header.reserved2, sizeof(short), 1, file);
fread(&header.offset_pixel_array, sizeof(int), 1, file);

printf("signature: %c%c\n", header.signature[0], header.signature[1]);
printf("size: %d\n", header.size);
printf("reserved1: %d\n", header.reserved1);
printf("reserved2: %d\n", header.reserved2);
printf("offset_pixel_array: %d\n", header.offset_pixel_array);

fclose(file);

The key functions here are:

FILE *fopen(const char *filename, const char *mode)

This creates a new file stream. fopen is used with the "rb" mode to indicate we are reading a file in binary mode. To read a file, use the mode wb instead of rb.

size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream)

For each call to fread, we give it first a pointer to an element of struct containing the BMP header, then the number of bytes to read, the number of times to read (typically 1), and the file stream to use. Note that order of the calls to fread define the order in which we read data so the order must match the file layout. (There is also a function called fwrite which works in exactly the opposite manner. The first won't be a pointer though.)

One function not used here but which may be useful, is fseek:

int fseek ( FILE * stream, long int offset, int origin )

The purpose of fseek is to move the "reading head" of the FILE object by some number of bytes. It can used to skip a number of bytes. The first parameter to fseek is the file pointer, followed by a number of bytes to move, and an origin. For origin, SEEK_CUR is a relative repositioning while SEEK_SET is global repositioning.

The basic idea to support loading a BMP file will be to parse it by byte-byte (using fread) into a set of structs. Later, you can use fwrite to write out the contents of those structs to a file stream to save the filtered image.

Creating file headers

When copying from BMP to BMP or PPM to PPM, you could easily copy the original file's header into the new one. But when copying from BMP to PPM, or PPM to BMP, you will need to fill in the header of the new file yourself. For example, you will need to fill in the compression type, the number of color planes, etc for BMP files. The following defines default values you should use when creating an image. All values that are not defined here, you should calculate based on the input image.

  • BMP Header:
    • Signature: "BM"
    • Reserved 1: 0
    • Reserved 2: 0
  • DIB Header
    • Planes: 1
    • Compression: 0
    • Horizontal resolution: 3780
    • Vertical resolution: 3780
    • Color number: 0
    • Important color number: 0
  • PPM Header
    • Magic Number: "P6"
    • Maximum color value: "255"

Include Files

To complete this assignment, you may find the following include files and functions useful:

  • stdio.h: Defines standard IO functions.
  • stdlib.h: Defines memory allocation functions.
  • string.h: Defines string manipulation functions.
    • char* strcat(char* dest, const char* src): The strcat() function appends the string pointed to by src to the end of the string pointed to by dest.
    • char* strcpy(char* dest, const char* src): The strcpy() function copies the string pointed to, by src to dest.
    • size_t strlen(const char* str): The strlen() function computes the length of the string str up to, but not including the terminating null character.
    • char* strtok_r(char* str, const char* delim, char** saveptr): The strtok_r function parses a string into a sequence of tokens.
  • unistd.h: Defines POSIX operating system API functions.
    • int getopt(int argc, char *const argv[], const char *optstring): The getopt() function is a builtin function in C and is used to parse command line arguments.

Your solution may not include all of these include files or functions, they are only suggestions. If you want to include any other files, please check with the instructor or TA before doing so. Note: Since PPM files are in ASCII (not binary), you should write and read them differently. fprint and fscanf would be a good choice to write and read PPM files.

Academic Honesty!
It is not our intention to break the school's academic policy. Posted solutions are meant to be used as a reference and should not be submitted as is. We are not held liable for any misuse of the solutions. Please see the frequently asked questions page for further questions and inquiries.
Kindly complete the form. Please provide a valid email address and we will get back to you within 24 hours. Payment is through PayPal, Buy me a Coffee or Cryptocurrency. We are a nonprofit organization however we need funds to keep this organization operating and to be able to complete our research and development projects.