For this project, you will be making a small command-line image editing tool. It will be capable of reading and writing some BMP image files, an uncompressed image format that’s easy to read and create. It will be able to:

Please note: I have every past submission for this project. I will know if you turn in someone else’s code.

But as I said at the beginning of the term, if you took this class with me before, and you have your old code, I don’t really mind if you reuse it. But maybe make it even better this time? ;o


Grading Rubric

The indented bullet points are subcategories.


0. Starting off

  1. On thoth, you should really make a directory for this project since it’s going to involve a lot of files. Make a directory like ~/private/cs0449/projects/proj1/ and cd into it.
  2. Like you did on lab 3, wget this file into that directory and unzip it.
  3. Rename abc123_proj1.c to your username like you’ve been doing on the labs.
  4. Run this: chmod -w *.bmp
    • This changes the file modes of all the images (*.bmp) to make them un-writeable.
    • This will prevent you from overwriting these files and having to get fresh ones from the zip file. Now, if you accidentally try to open these files for writing, fopen will fail.

In _proj1.c, there’s already some stuff:

fatal("it accepts formatting arguments: %d %d %d", x, y, z);

What is this OpenBMP struct about?

In this project you’ll be using an object-oriented programming style on the OpenBMP struct. Object orientation is a vibe, not a strict set of language features. In C, you can “do” object orientation by:

The OpenBMP struct represents an… open BMP file. Or two, if there is an input file and an output file. In my implementation, I have the following “methods” for it (notice the first argument on all of them is the same - an OpenBMP* which acts like this in Java):

Structuring your program

This is a somewhat sizeable program. My implementation is ~330 lines, about 210 lines longer than what you’re starting with in _proj1.c.

In this program there are many common, repeated operations that you will need in multiple functions. You need to structure your program properly. You need to break things up into functions instead of doing everything in the print_info, invert_image etc. functions!

You are graded on doing this. We are not expecting you to get it perfect, but you do have to make an effort. If you just copy and paste the same code four times into the four top-level functions, you will get 0 for that category, and you will be making programming much harder on yourself.

I’ve already given you some information about the OpenBMP methods above. It would probably be a very good idea for you to follow my lead on those. You don’t have to break those up any further, but you could if they get too big for your liking. But please try to work on these habits:

Code is primarily for humans to read,
and only secondarily for computers to execute!
Write code for you, not the computer!

Compiling

Use this line to compile your project:

gcc -Wall -Werror --std=c99 -g -o proj1 abc123_proj1.c -lm

The -g includes debug info so it’ll be easier to use gdb when your program breaks, and the -lm at the end links in the math library, needed for the pow() function later in the assignment. (We’ll talk about linking eventually.)

Also try this on thoth: use the file command on the .bmp files, like

$ file noodle.bmp

and see what it prints. file is a really useful command that looks much deeper than the file extension by looking at the file contents to figure out what kind of file something is. It recognizes lots of different file types, and can even display some simple info about common ones. Try it out on other files like your proj1 executable too!


1. ./proj1 info

The first thing you’ll implement is print_info. This command will check that a given file really is a valid BMP file, and then prints out some basic information (which will help you check that you did things right!).

The BMP file format

BMP (short for “bitmap”) was created by Microsoft for Windows 2.0 in 1987 to hold bitmap images. It has been extended over the years with more and more features, but there is a version that is extremely common that was introduced in Windows 3.1 that we will be dealing with.

No matter what version of BMP you look at, they have the same basic structure (numbers are byte positions):

The BMP Header is 14 bytes long and identifies this file as a BMP. It only has a few useful values in it.

The DIB (“device-independent bitmap”) Header is 40 bytes long in the version we’re using. It contains all the information about the image itself - its dimensions, color depth, and so on.

“The primary colors are red, YELLOW, and blue!” For paint, yes, because it is subtractive color; it absorbs light. Screens use additive color because they produce light. The additive primaries are red, green, and blue.

The Pixel Data is variable length and is the data for the pixels themselves. Pixels are the little colored squares that every image is made of. Each pixel has three color components: red, green, and blue. Varying amounts of these three primary colors gives us the entire color spectrum that our screens can produce.

We’ll come back to the pixel data for the invert command. For now, we’ll focus on the two header sections.


The print_info function

print_info is called by main with the filename to open as its argument. Here is how you will write print_info (yes I am giving you the code, it’s only a few lines but this is a Learning Moment):

OpenBMP bmp = {};

the = {} is technically a gcc feature but it’ll be part of C23

This creates an OpenBMP struct variable, and importantly initializes its fields to 0/NULL. That’s what the = {} does. Remember that if you don’t initialize a variable, it will contain garbage.

bmp_open(&bmp, in_filename);

Here we are calling a function that doesn’t exist yet. That’s fine! We are starting high level, and will implement the lower level functions after this one. We’re just trying to get our thoughts down in code at this point. This says, “open the BMP file from in_filename using bmp as the object.”

We don’t have to worry about details right now, so let’s just assume that bmp_open succeeded and that bmp has been filled in with information about the BMP file.

Please copy and paste the following code exactly because the autograder will want the info in this format:

printf("Size: %d x %d\n", bmp.width, bmp.height);
printf("Padding between rows: %d\n", bmp.padding);
printf("Pixel data start offset: %d\n", bmp.pixelStart);

We print out the fields of the bmp struct. This will help you ensure that your bmp_open function is working correctly.

Finally,

bmp_close(&bmp);

We close the BMP file.

Okay. Read the code. Isn’t it nice? It says exactly what it does. There’s no confusing long-winded code, there’s no weird math or array indexing or anything, there’s no error handling, it just reads like English. Open the BMP, print some info, close the BMP. This level of readability is what all true programmers strive for.


The bmp_close function (super easy)

Earlier I gave you the signature for bmp_close. Use that signature to write your own bmp_close function. Don’t just throw it anywhere you want in the file. Part of good code style is keeping related code next to each other - so go put it with the OpenBMP struct. (That’s why I have those big // ------ lines to separate parts of the file.)

Here’s what bmp_close does:

That’s it. Easy! (Setting bmp->in and bmp->out to NULL is not strictly necessary but it’s good insurance so someone doesn’t accidentally use an OpenBMP after calling bmp_close on it.)


The bmp_open function (considerably harder)

Here’s the first real meaty function. bmp_open has to open the file, check that it’s a valid BMP file, and fill in most of the fields of the bmp structure.

First is the BMP header, which looks like this:

File Offset Byte Length C type Description
0 2 char[2] “Magic Number” (format identifier)
2 4 uint32_t Size of the file in bytes - should match real size
6 2 uint16_t A reserved value (“reserved” means “not used”)
8 2 uint16_t Another reserved value
10 4 uint32_t Offset from beginning of file to start of the pixel data

uint32_t and uint16_t are types that come from stdint.h - they are unsigned 32-bit and 16-bit ints. This avoids the weirdness about the size of C’s short vs int, which can vary from platform to platform.

So here’s what bmp_open should do:

  1. Open in_filename for reading binary, and assign the result of fopen into bmp->in
  2. If bmp->in is NULL then fopen failed so give this error:
    • fatal("could not open %s for reading.", in_filename);
    • Please use the exact error messages I give you! It will make making the autograder so much easier!
  3. Read the “magic number.” This is 2 chars that identify this as a BMP file. You can do it like this:
     char magic[2] = "";
     CHECKED_FREAD(bmp->in, &magic, in_filename);
    

    Notice I initialized magic so that I know it’s full of '\0', and I used CHECKED_FREAD instead of fread. CHECKED_FREAD is a little easier to use, and it will also give an error message if fread failed to read anything.

  4. If magic is not BM (you can’t use streq for this because it’s not a zero-terminated string):
    • fatal("%s does not appear to be a valid BMP file (bad magic).", in_filename);
  5. Check the length of the file:
    • Create a uint32_t and CHECKED_FREAD it in.
    • Using the technique showed on the file slides, get the actual length of the file with fseek/ftell.
    • Compare the length given in the header with the length that you found. If they’re not equal,
      • fatal("%s does not appear to be a valid BMP file (bad length).", in_filename);
  6. Seek to offset 10 and then read bmp->pixelStart. (We don’t care about the reserved values.)

Okay, that’s the BMP header out of the way. Then comes the DIB header, which looks like this (we won’t be looking at most of these fields, so don’t worry about what they all mean:)

File Offset Byte Length C type Description
14 4 uint32_t Size of this DIB header in bytes
18 4 uint32_t Width of the image in pixels
22 4 uint32_t Height of the image in pixels
26 2 uint16_t Number of color planes
28 2 uint16_t Number of bits per pixel
30 4 uint32_t Compression scheme used
34 4 uint32_t Image size in bytes
38 4 uint32_t Horizontal resolution
42 4 uint32_t Vertical resolution
46 4 uint32_t Number of colors in the palette
50 4 uint32_t Number of important colors

So let’s continue writing open_bmp (bmp->in’s position currently at the start of the DIB header):

  1. Read the DIB header size. If it’s not 40,
    • fatal("%s is an unsupported version of BMP.", in_filename);
  2. Read bmp->width and then bmp->height.
  3. Set bmp->padding to bmp->width % 4.
  4. Seek to offset 28, and read in the bits per pixel (careful which type you use for this variable!). If it’s not 24,
    • fatal("%s is %dbpp which is unsupported.", in_filename, bpp);
  5. Seek to bmp->pixelStart.

And you’re done!!!!!!! Whew. Now to test it. Here is the output of my program on the test images. Yours should match exactly with no crashes, segfaults, etc.


Debugging

I mentioned this on lab 3 but just in case you didn’t have to use it and don’t remember it: if you want to debug a program that takes command-line arguments, you have to run gdb like this:

$ gdb --args ./proj1 info abstract.bmp

That is, you add the --args before the program name and arguments.

Now when you run, it will pass those arguments to your program.


2. ./proj1 invert

Once ./proj1 info is working correctly, you now have a solid foundation on which to build the image manipulation commands.

The OpenBMP struct has a second FILE*, out. All the image manipulation commands work like this:

The first two bullet points will be handled by bmp_open_output, and the last by invert_image.


Implementing bmp_open_output

This function takes an OpenBMP that was already opened with bmp_open, creates a new file, and copies the header data over, leaving both file positions at their respective pixel data offsets.

So here’s what you have to do:

  1. Create a char array for the output filename that is PATH_BUFFER_SIZE long.
    • This is a #define constant at the top of the file. I just picked this number for fun.
    • The length of filenames/file paths varies from system to system and there is no easy way to know the maximum length, so this will be insecure as hell! Whatever.
  2. Set that output filename to the equivalent of the Java out_prefix + "_" + in_filename.
    • That is, if the input filename is noodle.bmp and out_prefix is inv, then the output filename should be inv_noodle.bmp
    • You could use strcpy() and strcat() for this, or snprintf - a variant of printf that prints into a string instead of to the console.
      • If you google for snprintf, the CPlusPlus site is a pretty good reference. It shows what arguments it takes.
  3. Open that filename for binary writing, and assign the result of fopen into bmp->out
  4. If bmp->out is NULL,
    • fatal("could not open %s for writing.", out_filename); (or whatever variable name you used for the output filename string)
  5. Seek bmp->in to the start of the file.
  6. Create a char array that is bmp->pixelStart items long (!, see below)
  7. CHECKED_FREAD from bmp->in into that array…
  8. …then CHECKED_FWRITE that array to bmp->out.

Wait, we created an array whose length is a variable? Yes. This is called a variable-length array or VLA. VLAs were introduced in the 1999 C standard (C99). They let you create a local array variable whose size is not known until runtime. There are two important things to know about VLAs:


Starting invert_image and testing bmp_open_output

Now in invert_image, just like in print_info you need to:

But this time, after bmp_opening it, call bmp_open_output on it with "inv" as the last argument.

Now to test that bmp_open_output is working right:

  1. compile, and run ./proj1 invert noodle.bmp
  2. ls -l, and you should have a file named inv_noodle.bmp that is 54 bytes
  3. cmp -n 54 noodle.bmp inv_noodle.bmp will compare the first 54 bytes of the two files. If it prints nothing at all, they are identical and you did it right.
    • It’ll look like:
       $ cmp -n 54 filename.bmp inv_filename.bmp
       $
      

      No output at all. No news is good news.


The pixel data and row padding

The pixel data is mostly straightforward. We are working with 24-bit-per-pixel BMP images. 24 bits == 3 Bytes. That means for each pixel, each color component is a value between 0 (that color is fully off) and 255 (that color is fully on). E.g. (0, 0, 0) is black and (255, 255, 255) is white.

The pixel data itself is stored row-by-row, weirdly enough bottom-to-top (I’m sure there’s some historical reason for that). But there is one major quirk with the rows: the BMP format requires that the data for each row of pixels must be a multiple of 4 bytes.

Since each pixel is 3 bytes, 3 × width is not guaranteed to be a multiple of 4. In that case, extra empty padding bytes are used to make the row a multiple of 4 bytes. This is what you computed in bmp_open for bmp->padding - the number of extra bytes that will appear at the end of each row.

Below are examples of the data for one row of pixels (shown as arrays of bytes) for images of 1, 2, 3, and 4 pixels wide. The “total length” of each row is always a multiple of 4.

So here is what invert_image will do:

  1. After opening the output file, make a nested loop: the outer loop loops over rows of the image (so bmp.height), and the inner loop loops over columns of pixels (so bmp.width).
  2. For each pixel (inside the inner loop):
    • Make a Pixel variable
    • CHECKED_FREAD it from bmp->in (see below)
    • “Invert” it (see below)
    • then CHECKED_FWRITE it out to bmp->out, using the string "output" as the last argument.
  3. After each row, if bmp->padding is not 0:
    • seek bmp->in ahead by bmp->padding; and then
    • write bmp->padding bytes of zeros to bmp->out (see below)
    • (My bmp_skip_padding method does both of these things. You’ll need to do this in multiple functions, so make this method too!)

Using fread/fwrite on structs is normally not okay. But since Pixel is made of uint8_t - single bytes - it is guaranteed to not have any padding or alignment holes, so it’s safe here. (C is full of this kind of thing you should Never Ever Do (except it’s okay to do it in this one case!). but we say “you should Never Ever Do It” because it’s easier than explaining all the little exceptions to the rules.)

For the padding: read the instructions carefully. e.g. if bmp->padding is 3, you would write 3 bytes with the value 0. You do not write bmp->padding to the file.

Inverting a pixel means setting each component to the bitwise complement of its value. That is, p.x = ~p.x and so on. Hmm. Maybe you should make a method for this? pixel_invert or something? (And if you do, should it take the Pixel by value or by reference?)

Testing it

First, run the program like:

$ ./proj1 invert noodle.bmp
$

It should print nothing, and not crash or anything. But ls -l should show:

Use your own program to test the validity of the output file:

$ ./proj1 info inv_noodle.bmp
Size: 600 x 600
Padding between rows: 0
Pixel data start offset: 54
$

Finally, use your SFTP client to download and visually inspect the input and output images. (If you already had it open, you may have to refresh its view to see the newly created image.) This is what you should see:

This is one of my cats, Noodle, with his favorite toy and friend, Worm on a Stick.

Try inverting the other good images (abstract.bmp, 202px.bmp, and 203px.bmp). The properly-inverted versions are shown below.


3. ./proj1 grayscale

Now that you have a bunch of functions created, the project starts to get easier, because you can reuse them!

The grayscale_image function converts an image to… well, grayscale. There’s a lot of color theory and perceptual.. biological… blah blah blah it’s a whole bunch of math, but if you can convert the math into code, it’ll work just fine.

This function will look almost exactly like invert_image. The only differences are:

These are the steps to convert a color to grayscale (details follow):

  1. Normalize the three color components from [0, 255] to the range [0, 1] (as a double)
  2. Convert each color component from sRGB to linear using some weird math
  3. Mix the three linear color components into a single linear luminance component
  4. Convert linear luminance to sRGB luminance
  5. Denormalize(?) sRGB luminance to the range [0, 255] and put it into all 3 color components

Yeah, I don’t really understand it either. Just plug and chug. Details on each step (formulas from this Wikipedia article):

  1. This means you need to divide the red, green, and blue components each by 255.0 and put the results into double variables. Name them like r_srgb, g_srgb, b_srgb.
  2. Convert each of those 3 variables to linear values using the formula below, putting the results into 3 new variables.
    • In the formula below, Csrgb stands for any of the 3 color components, and Clinear is the output:

      \[C_{linear}= \begin{cases} \frac{C_{srgb}}{12.92}, & \text{if } C_{srgb} \leq 0.04045\\ \left(\frac{C_{srgb} + 0.055}{1.055}\right)^{2.4}, & \text{otherwise} \end{cases}\]
    • Make a function for this. It saves A LOT of typing cause you have to do this 3 times.
    • The ^ operator in C does not perform exponentiation, it performs exclusive-OR (XOR).
      • pow() from <math.h> performs exponentiation.
      • It’s really only calculators that allow ^ for exponentiation.
  3. The mixing formula is wonderfully simple:
    • Y is the name for “luminance”. I have no idea Y it’s named that.

      \[Y_{linear} = 0.2126R_{linear} + 0.7152G_{linear} + 0.0722B_{linear}\]
    • The coefficients here are kind of how “bright” we humans perceive each color component. We see greens the best and blues the worst!

  4. Converting Ylinear back to Ysrgb is the inverse of step 2 (and you should make another function):

    \[Y_{srgb}= \begin{cases} 12.92Y_{linear}, & \text{if } Y_{linear} \leq 0.0031308\\ 1.055Y_{linear}^{1/2.4} - 0.055, & \text{otherwise} \end{cases}\]
    • Important: remember your order of operations… when do you multiply by 1.055?
  5. This means you multiply Ysrgb by 255 and then cast to uint8_t and put it back into the pixel’s r, g, and b components.

Whew. That was a lot of math. I was able to do this in about 25 lines over 3 functions.

Don’t forget to use ./proj1 info to test that the gray_whatever.bmp files are valid!

Here’s what the output of ./proj1 grayscale should look like on the 4 test images. If your images look really light/bright/washed-out compared to this, it may be that you mixed together the srgb colors instead of the linear colors. Check your formulas!

Also, here is a zip file of the expected outputs of the grayscale command. You can wget it into your project directory on thoth and unzip it, then compare your outputs to the correct outputs like

cmp gray_noodle.bmp correct_gray_noodle.bmp

If they’re identical, it’ll print nothing; if they’re not, it’ll say which offset is different. If the offset is >= 54, then something is wrong with your formulas.


4. ./proj1 hflip

Last one. This one is actually pretty easy, no scary math. But there’s a complication: in order to flip the image horizontally, we could either:

We’ll be doing the latter. It’s way easier, with one complication: you don’t know how wide the image is until runtime. So you need a VLA again, this time an array of Pixel that is bmp.width items long.

Create this VLA once, before the nested loop in hflip_image. You’ll just reuse the same array for every iteration of the outer loop.

The outer loop will read the entire row of pixels into that VLA. Then, the inner loop will swap the pixels in that row end-for-end (swap the pixel at x with the one at width - 1 - x). But be careful… the upper bound of the inner loop is going to be half the image width. Otherwise it will horizontally flip the pixels twice in one loop, giving the same pixels you started with!

After the inner loop, write the entire row of pixels out to the output, and then skip the padding as usual. That’s it!

I’d really recommend making a pixel_swap function that swaps the values at two Pixel*s. It makes this super simple.

And that’s it. This is what your program should output. Pay close attention to the center of abstract.bmp. I put those thin vertical lines there so you can check if your loop bounds are right. If they’re not right, it’ll look weird in the center!


Submission

The autograder will open up about a week after the project is released. This time, the autograder will have a rate limit on it, preventing you from submitting more than once per hour (except when it’s close to the deadline). So be sure to debug your program yourself and get help from the TAs and me instead of running the autograder after every three-character change in your source code!