CS50 PSet Filter(more) : filter.c
Tags: cs50, c, beginners, computerscience
Published: 2024-04-16 12:49:55

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 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._ 

<u>Example 1 </u>: input `./program -b infile outfile`, the code recognizes '-b' as a valid filter and confirms that there is only one filter provided.
<u>Example 2</u>: 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 outfile\n");
        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 the 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 `RGBTRIPLE`s (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 `RGBTRIPLE`s 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 |

Each row must be a multiple of 4 bytes in a bitmap file. 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 that 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 the 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.

My code :

{% embed https://github.com/vivekvohra/filter %}