Today I will lay out the structure and functions of the util
and pgm
libraries. Generally speaking, you should take
all the following code and explanations with a grain of salt due to me not being very proficient in and with C.
The PGM handler
Located in src/libs/pgm/
the file contains all the functions showcased in the header file
here.
We will get started with the easiest methods, working our way up to the harder functions.
The very first thing however will be me explaining the mandatory data structure we had to use.
Image data structure
1typedef struct {
2 int width;
3 int height;
4 int **data;
5} Image;
Even though this is pretty self-explanatory, we should talk about the third value of the structure. The int **data
defines a double pointer containing each and every pixel from an image.
For instance, one can access the value of a pixel at the coordinates x=0
and y=0
by using the following snippet:
1Image *img = createImage(25,25,255);
2
3int x = 0;
4int y = 0;
5
6int pixel_value_at_coords = img->data[x][y];
7printf("pixel at (%d,%d): %d", x,y,pixel_value_at_coords);
8
9// Result: pixel at (0,0): 255
As one can see, there is a function used in line 1 which wasn’t introduced yet: createImage()
The createImage
method
This method is crucial for several other methods, such as copyImage()
and loadImage()
.
Create Image contains the following code, which we will walk through step by step:
1Image *createImage(int width, int height, int default_brightness) {
2 Image *img;
3 img = malloc(sizeof *img);
4
5 img->width = width;
6 img->height = height;
7
8 img->data = (int **)malloc(height * sizeof(int *));
9
10 for (int i = 0; i < height; i++) {
11 img->data[i] = (int *)malloc(width * sizeof(int));
12
13 for (int ii = 0; ii < width; ii++) {
14 img->data[i][ii] = default_brightness;
15 }
16 }
17
18 return img;
19}
Foremost in Line one, we define the return type to be of type Image
. Remember, we defined this structure at the
beginning. We define three parameters in the method head, which all share the type int
.
Line two declares the variable *img
of type Image
-pointer, which we will return later. Line three allocates memory
for this variable, allowing us to store data in this variable.
The Lines four and five are assigning the values of our function parameters to the variables in the structure using the
arrow operator (->
).
Right in the next line, we have a rather complicated line of code which allocates the double pointer to, again, allow us to store data in it. Let’s take a closer look at this line of code by splitting it in three parts:
img->data =
: assigns everything right of=
to thedata
variable in theimg
pointer(int **)
:malloc
returnsvoid*
, therefore we need to cast this pointer into a doubleint
pointermalloc(height * sizeof(int *))
The last one is a bit tricky:
We know we use malloc
to allocate memory, but what does height * sizeof(int *)
do, and why do we need to use it
here? The answer is simpler than you think.:
Basically, the data field is an array of pointers, each of them is filled with exactly as many pixels as the height is big. Let’s visualize this concept:
Image | Width: 0 | Width: 1 | Width: 2 |
---|---|---|---|
Height: 0 | 0,0 | 0,1 | 0,2 |
Height: 1 | 1,0 | 1,1 | 1,2 |
Height: 2 | 2,0 | 2,1 | 2,2 |
You should notice our point of origin being in the top right, learn why
here.
This means you can mentally visualize this concept by simply flipping the known coordinate system upside down, meaning
0,0 is in the top left corner instead of the bottom left corner. As you can see, in this example the array contains
three arrays with height, therefore we allocate the field with the size of height*sizeof(int)
. To explain in simple
terms, we tell the compiler to expect three fields of height, which we will later each fill with values.
After explaining this line in depth, we will now take a closer look at the for loops of this function. The first one loops over the columns of the field we just allocated and allocates each one of these rows with:
1img->data[i] = (int *)malloc(width * sizeof(int));
2// cast *void into *int, allocate as many items as there are pixels for each height, e.g.: 0-1080, 1-1080, 2-1080
The second loop assigns the default value specified in the function parameter default_brightness
to every pixel in the
data field.
The freeImage
method
This method enables memory management for the main menu.
1void freeImage(Image **img_pointer) {
2 if(*img_pointer == NULL) return;
3 free(*img_pointer);
4 *img_pointer = NULL;
5}
Pointers in C are passed by value not by reference, this means editing a pointer inside a function is only possible by passing the address of the pointer to the function.
Not much to explain here, the first line makes the function do nothing if the pointer is already NULL
. The second line
frees the given pointer, and the third line assigns NULL
to the just freed pointer. This is necessary due to the fact
that pointers can still point to random memory after being assigned.
The copyImage
method
This method performs a deep copy of the given Image pointer and returns it.
1Image *copyImage(Image *img_pointer) {
2 int width = img_pointer->width;
3 int height = img_pointer->height;
4 Image *cpImage = createImage(width, height, 1);
5
6 for (int i = 0; i < height; i++) {
7 for (int ii = 0; ii < width; ii++) {
8 cpImage->data[i][ii] = img_pointer->data[i][ii];
9 }
10 }
11
12 return cpImage;
13}
Line one and two assign the height and width of the passed image to temporary variables. Line 3 creates a new image. The
two for
-loops loop over every item in every column and every row, allowing for a deep copy to be made.
The loadImage
method
This method was by far the hardest of any code to implement in the whole project. Wrapping my head around reading files and parsing their contents was such a struggle and cost me a lot of time.
The method’s head is defined as follows:
1Image *loadImage(char file_name[]) {
The method can be called by passing a file name with path to the file_name
parameter.
In line 1 we open the file, by passing the given file name and the “r”
parameter to indicate the mode we intend to
use: See fopen
man pages here.
1FILE *file = fopen(file_name, "r");
Next we check if opening the file failed, if this is the case we return NULL
to indicate failure.:
1if (file == NULL)
2 return NULL;
Now we check if the file confirms to the PGM/P2
standard, which I explained
here.
To do this, we first need to get the first character of the file, which should be a P
:
1char pgm_prefix = getc(file);
Directly after, we declare some variables for later usage:
1int pgm_version = 0;
2int width = 0;
3int height = 0;
4int brightness = 0;
Now we get the first integer of the file, which should, if the file is PGM conform, be a 2 or a 5. But in our case only needing to support P2, we can simply test for that:
1// get first int of the file
2fscanf(file, "%d\n", &pgm_version);
3// check if first line of the file conforms to the pgm standard
4if (pgm_prefix != 'P' || pgm_version != 2) {
5 return NULL;
6}
Remembering the explanation from here, we know the
second and third integers are the width and height and the fourth integer is the max brightness. To get these values, we
use fscanf
again. We will need to check if the scanned variables are 0 or smaller than 0.:
1fscanf(file, "%d", &width);
2fscanf(file, "%d", &height);
3fscanf(file, "%d", &brightness);
4
5if (width <= 0 || height <= 0 || brightness <= 0) {
6 return NULL;
7}
After establishing this solid foundation, we can now get started with creating the image and assigning all the values at their respecting coordinates:
1Image *img = createImage(width, height, 255);
2for (int i = 0; i < height; i++) {
3 for (int ii = 0; ii < width; ii++) {
4 fscanf(file, "%d", &img->data[i][ii]);
5 }
6}
Finally, we close the file and return the created Image.
1fclose(file);
2return img;
The saveImage
method
This function saves the image to a file.
Opening file with mode “w”
(write) and return 0 if opening failed.:
1FILE *file = fopen(file_name, "w");
2if (file == NULL) {
3 return 0;
4}
Now we print the first three lines, like the standard dictates.:
P2
width height
brightness
1fprintf(file, "P2\n%d %d\n%d\n", img_pointer->width, img_pointer->height, MAX_BRIGHT);
*MAX_BRIGHT
is a macro in _util.h
which I defined to be 255
.
To write all the pixel data into the file, we just loop over every pixel again and write the data to the file.:
1// loops over every pixel and appends the value to the file
2for (int i = 0; i < img_pointer->height; i++) {
3 for (int ii = 0; ii < img_pointer->width; ii++) {
4 fprintf(file, "%d\n", img_pointer->data[i][ii]);
5 }
6}
The function ends with closing the file and returning one as the indicator for success.
1fclose(file);
2return 1;
The Utility methods
This source file contains functions not fitting into the other modules. We have for example the compare function, which
is needed for a qsort
call in the median
function in _image.c
.:
1int compare(const void *a, const void *b) {
2 int int_a = *((int *)a);
3 int int_b = *((int *)b);
4
5 if (int_a == int_b)
6 return 0;
7 else if (int_a < int_b)
8 return -1;
9 else
10 return 1;
11}
There is also the toInt
method, which converts a string to an integer. I wrote this function after Clang told me
multiple times to refrain from using scanf
and I should use strtol
instead, so I did.:
strtol
documentation here.
1int toInt(const char *text) {
2 char *ptr;
3 long l;
4
5 l = strtol(text, &ptr, 10);
6
7 return (int)l;
8}
We also have two feedback methods to communicate errors and warnings to the user:
1void throw_error(char text[]) {
2 printf("%s%s%s\n", ANSI_COLOR_RED, text, ANSI_RESET);
3 exit(1);
4}
5
6void throw_warning(char text[]) {
7 printf("%s%s%s\n", ANSI_COLOR_YELLOW, text, ANSI_RESET);
8}
The last utility function is a check if the made selection in the main menu is a valid input:
Basically, if the selection is not in the defined range in the enum or there is currently no image in memory, disallow the user to make edits to an image.
1int check_is_option_valid(int selection, int image_in_memory) {
2 if (selection > SELECTION_EXIT) {
3 return SELECTION_INVALID;
4 }
5 // disallow editing and saving if there is no file in mem
6 if (selection != 0 && selection != SELECTION_EXIT && !image_in_memory) {
7 throw_warning("No Image loaded into the program.");
8 return SELECTION_INVALID;
9 }
10 return selection;
11}
Conclusion so far
For me personally, my lack of a sophisticated knowledge of c made the beginning and some parts of this project so far
very hard. I have some experience as a somewhat full-stack web-dev, but having to learn and use c in such a small amount
of time was very challenging. I had a hard time understanding pointers at first and made easily avoidable mistakes.
Whenever I searched online, I was feeling like there weren’t that many resources. Linux man pages really helped me out,
but they don’t introduce you to methods you could use for specific tasks, except of the see also
section.
A big mistake I made from the beginning was to not read the PGM specification correctly, I just assumed I could access all pixels by looping from 0 to width and in that loop, loop from 0 to height. This did in fact not work and gave me headaches.
I also made the mistake of not knowing that pointers were passed by value not by reference, meaning the freeImage
method did in fact not free the memory taken up by the structures.
To conclude, I’d say it was smart to start working on the project this early, as I it is about to be finished.