CS50 PSet Filter(more) : filter.c

RMAG news

In this post, we continue our series with a more detailed look at the filter.c file,while breaking down its key components and functionality.

The filter.c file is part of the “Filter” problem set in CS50. It’s a program that applies filters to bitmap images. The file contains a main function that handles the command-line arguments and calls the appropriate filter function. The actual implementation of the filters is done in the helpers.c file.

Now, when we open up filter.c. We see that this file has already been written for us, but it’s still important to know the inner workings of this code.

Now this code roughly performs these functions:

Firstly it handles the command-line arguments, checks for correct usage, and opens the input and output image files.

Then it reads the bitmap file into a 2D array of RGBTRIPLEs, which is a data structure defined in the bmp.h header file. Each RGBTRIPLE represents a pixel in the image.

Then, it calls the appropriate filter function based on the command-line arguments. Which is written in helpers.c file, which we write it as part of the problem set.

After the filter has been applied to the image, it writes the 2D array back to a bitmap file, creating the output image.

Now let us discuss the code in detail :

Ensure Proper Syntax of CLA.

Command-Line Filter Flag Validation

// Define allowable filters
char *filters = “begr”;

// Get filter flag and check validity
char filter = getopt(argc, argv, filters);
if (filter == ‘?’)
{
printf(“Invalid filter.n”);
return 1;
}

char *filters = “begr”; This line defines the filters that are allowed in the command line.

char filter = getopt(argc, argv, filters); This line calls the getopt function, which analyzes the command-line arguments. The arguments for this function are argc, argv, and the string of allowable filters.

if (filter == ‘?’) This line checks if the returned value from getopt is ‘?’. If getopt encounters a character that is not included in the allowable options, it returns ‘?’.

Example:

The arguments are filters that can be ‘b’, ‘e’, ‘g’, or ‘r’.

Let the command be: ./program -b

In this case, argc would be 2 (the program name and the ‘-b’ argument), and argv would be an array containing “./program” and “-b”.

When getopt(argc, argv, filters) is called, it will** return the character ‘b’** because ‘-b’ is a valid command-line argument according to the filters string. So, filter would be ‘b’.

If the command is: ./program -x, getopt would return ‘?’ because ‘-x’ is not a valid argument according to filters, and filter would be ‘?’.

Checking for Multiple Filter Flags

if (getopt(argc, argv, filters) != -1)
{
printf(“Only one filter allowed.n”);
return 1;
}

The getopt function is called again. If it returns anything other than -1, it means there’s another command-line argument present. -1 is returned by getopt when it has finished processing all the command-line options. 

Example 1 : input ./program -b infile outfile, the code recognizes ‘-b’ as a valid filter and confirms that there is only one filter provided.
Example 2: if we run your program: ./program -b -g, getopt will not return -1 after processing -b because there’s another argument -g. The ‘if’ condition will be true, and the program will print “Only one filter allowed.” and return 1, indicating an error.

This code ensures that only one filter can be used at a time. If more than one is provided, it’s considered an error.

Ensure infile, outfile in syntax

// Ensure proper usage
if (argc != optind + 2)
{
printf(“Usage: ./filter [flag] infile outfilen”);
return 1;
}

argc is the count of command-line arguments, including the program name. optind is a variable from the getopt library that represents the index of the next argument to be processed.

The condition argc != optind + 2 checks if the number of arguments is not equal to the number of already processed arguments (optind) plus 2. The ‘+2’ accounts for the ‘infile’ and ‘outfile’ that should follow the filter flag.

For example, if we run program: ./program -b infile outfile, the value of  argc would be 4, and of  optind would be 2 (after processing ‘-b’), therefore value of  optind + 2 would be 4. Thus the condition would be false, and the program would skip.

If we run program: ./program -b infile, the value of argc would be 3, and value of optind would still be 2, but the value of optind + 2 would be 4. Thus the condition would be true, the program would print “Usage: ./filter [flag] infile outfile”, and return 1, indicating an error.

Loading Files

Assigning Input and Output File Names

// Assigning Input and Output File Names from Command-Line Arguments
char *infile = argv[optind];
char *outfile = argv[optind + 1];

./program -b infile outfile, argc :  optind is the index of the next argument to be processed by getopt.argv[optind] would be the argument right after the last processed option, which should be the input file name (‘infile’) in this case.

Opening Input and Output File

// Open input file
FILE *inptr = fopen(infile, “r”);
—–

// Open output file
FILE *outptr = fopen(outfile, “w”);
—–

// Read infile’s BITMAPFILEHEADER
BITMAPFILEHEADER bf;
fread(&bf, sizeof(BITMAPFILEHEADER), 1, inptr);

// Read infile’s BITMAPINFOHEADER
BITMAPINFOHEADER bi;
fread(&bi, sizeof(BITMAPINFOHEADER), 1, inptr);

The above code opens the input/output file; and returns 1 if the file is not found. Else it reads the header files from bmp.h.

Validating BMP File Format

// Ensure infile is (likely) a 24-bit uncompressed BMP 4.0
if (bf.bfType != 0x4d42 || bf.bfOffBits != 54 || bi.biSize != 40 ||
bi.biBitCount != 24 || bi.biCompression != 0)
{
fclose(outptr);
fclose(inptr);
printf(“Unsupported file format.n”);
return 6;
}

bf.bfType != 0x4d42 checks if the file type is not ‘BM’ (0x4d42 is the hexadecimal representation of ‘BM’).

bf.bfOffBits != 54 checks if the offset to the bitmap data is not 54 bytes.

bi.biSize != 40 checks if the size of the info header is not 40 bytes.

bi.biBitCount != 24 checks if the bitmap is not 24-bit.

bi.biCompression != 0 checks if the bitmap is not uncompressed.

If any of these conditions are true, it means the file is not a 24-bit uncompressed BMP 4.0 file

Get image’s dimensions

// Get image’s dimensions
int height = abs(bi.biHeight);
int width = bi.biWidth;

This code retrieves the dimensions of the image from the bitmap info header.

bi.biHeight gives the height of the image in pixels. It can be negative, depending on the orientation of the image. Therefore we
take the absolute value to ensure the height is always positive.

bi.biWidth (defined in bmp.h file )gives the width of the image in pixels.

For example, if the bitmap image is 800 pixels wide and 600 pixels high, bi.biWidth would be 800 and bi.biHeight would be 600 (or -600, depending on orientation), so height would be 600 and width would be 800.

Memory allocation for the image

// Allocate memory for image
RGBTRIPLE(*image)[width] = calloc(height, width * sizeof(RGBTRIPLE));
if (image == NULL)
{
printf(“Not enough memory to store image.n”);
fclose(outptr);
fclose(inptr);
return 1;
}

This code allocates memory to store the image data.

RGBTRIPLE(*image)[width] = calloc(height, width * sizeof(RGBTRIPLE)); is declaring a pointer to an array of RGBTRIPLEs (which represent pixels) and allocating memory for height rows of width pixels each. calloc initializes the allocated memory to zero.

If calloc returns NULL.It means there was not enough memory to store the image, so the program prints an error message, closes the input and output files, and returns 1.

For example, if your image is 800 pixels wide and 600 pixels high, this code would allocate memory for 600 rows of 800 RGBTRIPLEs each. If there is not enough memory to do this, the program would print “Not enough memory to store image.” and terminate.

Padding

// Allocate memory for image
RGBTRIPLE(*image)[width] = calloc(height, width * sizeof(RGBTRIPLE));
if (image == NULL)
{
printf(“Not enough memory to store image.n”);
fclose(outptr);
fclose(inptr);
return 1;
}

This line of code is calculating the amount of padding needed for each row (scanline) of the image.

For example : If the Image is 5 pixels (15 bytes) wide(it would need 1 byte of padding in each row).So that it can be 16 bytes wide.

BGR
BGR
BGR
BGR
BGR
0

BGR
BGR
BGR
BGR
BGR
0

BGR
BGR
BGR
BGR
BGR
0

BGR
BGR
BGR
BGR
BGR
0

BGR
BGR
BGR
BGR
BGR
0

In a bitmap file, each row must be a multiple of 4 bytes. If the width of the image in bytes (width * sizeof(RGBTRIPLE)) is not a multiple of 4, some padding bytes (extra bytes of value 0) need to be added to the end of each row to make it a multiple of 4.

(width * sizeof(RGBTRIPLE)) % 4 calculates the remainder when the width of the image in bytes is divided by 4. If this is 0, no padding is needed.Else 4 – (width * sizeof(RGBTRIPLE)) % 4 calculates how many bytes of padding are needed. The % 4 at the end is to handle the case where no padding is needed (it ensures the padding is 0 in this case).

For example, if the image is 5 pixels (15 bytes) wide, (width * sizeof(RGBTRIPLE)) % 4 would be 3, so 4 – (width * sizeof(RGBTRIPLE)) % 4 would be 1, meaning 1 byte of padding is needed for each row. If your image is 4 pixels (12 bytes) wide, (width * sizeof(RGBTRIPLE)) % 4 would be 0, so 4 – (width * sizeof(RGBTRIPLE)) % 4 would be 4, but the % 4 at the end would make the padding 0 .

Why do padding?

The requirement for BMP rows to be a multiple of 4 bytes is due to the way computers handle data.

Computers often read data from memory in chunks of 4 bytes (32 bits) at a time. By ensuring each row aligns with these 4-byte boundaries, the computer can read the image data more efficiently.

This is known as “byte alignment” or “data structure alignment”.

If the rows weren’t padded to be a multiple of 4 bytes, the computer might need extra read operations, which could slow down the process of reading the image data.

Reading Pixels from BMP File

// Iterate over infile’s scanlines
for (int i = 0; i < height; i++)
{
// Read row into pixel array
fread(image[i], sizeof(RGBTRIPLE), width, inptr);

// Skip over padding
fseek(inptr, padding, SEEK_CUR);
}

This code reads the image data from a BMP file.

The for loop iterates over each row of the image. For each row, fread(image[i], sizeof(RGBTRIPLE), width, inptr); reads width pixels from the input file into the image[i] array.

fseek is a function that is used to change the position of the file pointer in a file. It takes three arguments: a file pointer, an offset (number of bytes), and a position from where the offset is added.

In fseek(inptr, padding, SEEK_CUR);, inptr is the file pointer, padding is the offset (is the number of bytes that the file pointer should move from its current position.), and SEEK_CUR is the position from where the offset is added.

SEEK_CUR is a constant defined in the library which specifies that the offset provided should be added to the current position of the file pointer.

So, fseek(inptr, padding, SEEK_CUR); moves the file pointer padding bytes forward from its current position. This is used to skip over the padding bytes at the end of each row of the image. If there is no padding, the file pointer doesn’t move.

Filter

switch (filter):

The above code calls the appropriate filter function based on the command-line arguments. The actual implementation of the filters is done in the helpers.c file.

save

After the filter has been applied to the image, the rest of code writes the 2D array back to a bitmap file, creating the output image using fwrite function.

In the end, we free up the memory that was previously allocated to the image variable and close the input and output files that were previously opened.

In the next blog post we will discuss about helper.c file. See you there! In the meantime continue to code with passion. 🚀

As a novice writer, I’m eager to learn and improve. Should you spot any errors, I warmly welcome your insights in the comments below. Your feedback is invaluable to me.

Leave a Reply

Your email address will not be published. Required fields are marked *