Image editor in C: Part I

· 1946 words · 10 minute read

Project Overview ##

I am currently majoring in applied computer science at DHBW. In the first Semester, we have to submit the first programming project, which is an image editor in C conforming to the PGM standard. It is split in three subtasks and is to be finished till late March.

Task ITask IITask III
methods in _pgm.hmethods in _image.hTUI

Grading ###

xPoints
Task I15
Task II15
Task III10
Robustness & compilablity5
Comments & structure5
Total50 Points

Project description ###

The PGM editor should implement the Portable GrayMap standard, which I will explain in the next chapter. It should also include methods to manipulate Images in the PGM-format. The following functions are required by the lecturer. :

README ###

pgmE - portable graymap Editor
==========
pgmE is an extremely fast, small and efficient editor which implements the pgm standard.

Requirements
------------
In order to build you need:
    - make
    - gcc

Running pgmE
------------
Start it by running:

    make run

This will compile pgmE.

Features
------------
- load pgm images
- save pgm images
- edit pgm images:
    - median filter
    - gauss operator
    - laplace operator
    - resize
    - rotate
    - threshold operato

Regarding Images ####

  1. create an image structure
  2. free the memory of an image structure
  3. deep copy an image
  4. load image from file system into the program
  5. save image to file system

Filters and Operators ####

  1. median filter
  2. gauss filter
  3. laplace operator
  4. threshold operator
  5. scale
  6. rotate

.pgm Standard ###

Differentiating between the three P* -image formats ####

TypeASCII (plain)Binary (raw)ExtensionColors
Portable BitMapP1P4.pbm0-1 (white & black)
Portable GrayMapP2P5.pgm0-255, 0-65535 (gray scale)
Portable PixMapP3P6.ppmetc.

PGM or in my case P2 is a image format coming from the Netpbm Project. It consists of the two characters P and 2 in the first line of the file to indicate the image format. The second line contains the Dimensions of the image, e.g.: 1920 1080 . The third line contains the max. brightness possible in the image. Line 3 till the End of the file specifies the pixel grey values. The specification allows for comments between the first line (format indicator) and the second line (dimension definition), prefixed with # .

Example: ####

P2
# Shows the word "FEEP" (example from Netpbm man page on PGM)
24 7
15
0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
0  3  3  3  3  0  0  7  7  7  7  0  0 11 11 11 11  0  0 15 15 15 15  0
0  3  0  0  0  0  0  7  0  0  0  0  0 11  0  0  0  0  0 15  0  0 15  0
0  3  3  3  0  0  0  7  7  7  0  0  0 11 11 11  0  0  0 15 15 15 15  0
0  3  0  0  0  0  0  7  0  0  0  0  0 11  0  0  0  0  0 15  0  0  0  0
0  3  0  0  0  0  0  7  7  7  7  0  0 11 11 11 11  0  0 15  0  0  0  0
0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0

Directory structure ##

After reading two dozen blogs and a lot of trial and error, i settled on the following dir structure:

.
├── build
│  └── main.out
├── makefile
├── README
└── src
   ├── libs
   │  ├── image
   │  │  ├── _image.c
   │  │  └── _image.h
   │  ├── pgm
   │  │  ├── _pgm.c
   │  │  └── _pgm.h
   │  └── util
   │     ├── _util.c
   │     └── _util.h
   └── main.c

Files: ###

In the following I will try to explain the file contents and what they do. The how to the what will follow in the next parts of this series.

_image.(c|h) ####

includes several image manipulation methods

 1/**
 2 * @file _image.h
 3 * @author xnacly
 4 * @brief includes several image manipulation methods
 5 * @date 2022-02-21
 6 */
 7
 8#ifndef _IMAGE_H_INCLUDED
 9#define _IMAGE_H_INCLUDED
10#include "../pgm/_pgm.h"
11// used for: copyImage, createImage
12#include "../util/_util.h"
13// used for: compare
14
15#define PI 3.141592
16
17/**
18 * @brief Applies the median filter to the given image
19 * @param img
20 */
21Image *median(Image *img);
22
23/**
24 * @brief Applies the gauss filter to the given image
25 * @param img
26 */
27Image *gauss(Image *img);
28
29/**
30 * @brief Modifies the image using the laplace-operator
31 * @param img
32 */
33Image *laplace(Image *img);
34
35/**
36 * @brief Modifies the Image using the thresholding method
37 * @param img
38 * @param threshold
39 */
40Image *threshold(Image *img, int threshold);
41
42/**
43 * @brief Scales the given Image to the width and height specified
44 * @param img
45 * @param width
46 * @param height
47 */
48Image *scale(Image *img, int width, int height);
49
50/**
51 * @brief Rotates the Image to the given angle
52 * @param img
53 * @param angle
54 * @param brigthness
55 */
56Image *rotate(Image *img, double angle, int brigthness);
57#endif

_pgm.(c|h) ####

handles everything regarding images in the .pgm standard

 1/**
 2 * @file _pgm.h
 3 * @brief handles everything regarding images in the .pgm standard
 4 * http://netpbm.sourceforge.net/doc/pgm.html (Plain PGM)
 5 * @author xnacly
 6 * @date 2022-02-21
 7 */
 8#ifndef _PGM_H_INCLUDED
 9#define _PGM_H_INCLUDED
10
11#define MAX_BRIGHT 255
12
13/**
14 * @brief struct to store PGM-image data in
15 */
16typedef struct {
17  int width;
18  int height;
19  int **data; // 2d pointer: Brightness values
20} Image;
21
22/**
23 * @brief creates an Image with given width and height, set every pixel to the
24 * default_brightness
25 * @param width
26 * @param height
27 * @param default_brightness
28 * @return *Image
29 */
30Image *createImage(int width, int height, int default_brightness);
31
32/**
33 * @brief frees the memory taken up by the given Image pointer
34 * @param img_pointer Image pointer created with createImage()
35 */
36void freeImage(Image *img_pointer);
37
38/**
39 * @brief  copys the image from the given pointer to a new pointer
40 * @param img_pointer Image pointer created with createImage()
41 * @return *Image
42 */
43Image *copyImage(Image *img_pointer);
44
45/**
46 * @brief loads image from filesystem with given file name (without extension)
47 * @param file_name
48 * @return *Image
49 */
50Image *loadImage(char file_name[]);
51
52/**
53 * @brief saves the given pointer in a .pgm file with the given name
54 * @param file_name
55 * @param img_pointer Image pointer created with createImage()
56 * @return 0 or 1
57 */
58int saveImage(char file_name[], Image *img_pointer);
59
60#endif

_util.(c|h) ####

provides utility methods and defines ANSI macros for colored output, as well as an enum for the main menu selection handling

 1/*
 2 * _util.h provides utility methods
 3 */
 4#ifndef _UTIL_H_INCLUDED
 5#define _UTIL_H_INCLUDED
 6
 7#include "../pgm/_pgm.h"
 8
 9#define ANSI_COLOR_RED "\x1b[91m"
10#define ANSI_COLOR_GREEN "\x1b[92m"
11#define ANSI_COLOR_YELLOW "\x1b[93m"
12#define ANSI_STYLE_BOLD "\x1b[1m"
13#define ANSI_RESET "\x1b[0m"
14
15enum {
16  SELECTION_LOAD = 0,
17  SELECTION_MEDIAN_FILTER,
18  SELECTION_GAUSS_FILTER,
19  SELECTION_LAPLACE_OPERATOR,
20  SELECTION_THRESHOLD,
21  SELECTION_SCALE,
22  SELECTION_ROTATE,
23  SELECTION_SAVE,
24  SELECTION_EXIT, // ALWAYS LATEST AVAILABLE OPTION ;)
25  SELECTION_INVALID =
26      9999 // MAKE SURE THAT THIS WILL RUN INTO THE INVALID SECTION ;)
27};
28
29/**
30 * used for qsort
31 * @param a
32 * @param b
33 * @return
34 */
35int compare(const void *a, const void *b);
36
37/**
38 * @brief converts string to integer
39 * @param text
40 * @return integer
41 */
42int toInt(const char *text);
43
44/**
45 * @brief exits the program and prints the given text highlighted red
46 * @param text
47 */
48void throw_error(char text[]);
49
50/**
51 * @brief prints the given text highlighted yellow, differs from throw_error by
52 * not exiting the program.
53 * @param text
54 */
55void throw_warning(char text[]);
56
57/**
58 * @brief checks if the selection meets certian criteria
59 * @param selection
60 * @param arr_size size of the array containing possible inputs
61 * @param edited_unsaved_image_in_memory
62 * @param image_in_memory
63 */
64int check_is_option_valid(int selection, int image_in_memory);
65#endif

main.c ####

takes care of printing the main menu and handling user input, runs the functions implemented in the ’libs'

Compiler setup ##

Compiler choice ###

The lecturer gave us very specific instructions on how and where to compile our project. The project has to be compiled with gcc on a Unix system. I’m using Unix for around 2 years now, this makes the requirement easily fulfillable.

Compiler flags ###

Some warning compiler flags are mandatory:

  • -fdiagnostics-color=always (Use color in diagnostic)
  • -Wall (enables all warnings about constructions that are easy to avoid)
  • -Wpedantic (Issue all the warnings demanded by strict ISO C)

Some were added by me:

  • -lm (link the math library)
  • -g (tell gcc to generate debugging and symbol information, necessary for debugging using gdb)

After me asking, the lecturer told us to use the C99 standard (-std=c99).

Makefile ###

After learning about makefiles (before I knew about them I wrote a shell script to compile my project) and gdb I promptly used both in this project:

 1cc := -fdiagnostics-color=always \
 2			-Wall -Wpedantic -std=c99 \
 3			src/main.c src/libs/util/_util.c \
 4			src/libs/pgm/_pgm.c \
 5			src/libs/image/_image.c \
 6			-lm -o build/main.out
 7main:
 8	gcc ${cc}
 9	build/main.out
10debug:
11	gcc -g ${cc}
12	gdb build/main.out
13clean:
14	rm -r build/; rm test.pgm
15pre:
16	mkdir build/
  • make main

    compiles and runs the executable

  • make debug

    compiles with the -g flag and starts gdb on the executable

  • make clean

    removes generated images and the build folder recursive

  • make pre

    creates the build dir, due to me being lazy

Updated, more complicated Makefile ###

I won’t explain too much here, due to the extensive comments and the explaining I’ve done before. If you are interested in learning more about Makefiles, take a look here.

New features:

  • more compiler flags
  • better structure
  • comments
  • dynamic (can compile even if i add new source files)
  • made build steps dependent on the pre target
 1# compiler flags
 2MANDATORY_FLAGS := -fdiagnostics-color=always  \
 3									-Wall \
 4									-Wpedantic \
 5									-std=c99
 6
 7									# use color in diagnostics
 8									# enables all construction warnings
 9									# enable all warnings demanded by ISO C
10									# follow ANSI C99
11
12MY_FLAGS := -Wextra \
13						-Werror \
14						-Wshadow \
15						-Wundef \
16						-fno-common \
17
18						# set of warnings and errors not covered by -Wall
19						# all warnings cause errors
20						# warnings for shadowing variables
21						# warnings for undefined macros
22						# warnings for global variables
23
24BUILD_DIR := ./build
25SRC_DIR := ./src
26
27# finds all source files in /src/*
28FILES := $(shell find $(SRC_DIR) -name "*.c")
29
30COMPILE := $(MANDATORY_FLAGS) $(MY_FLAGS) $(FILES) -lm -o $(BUILD_DIR)/main.out
31
32# run the previously built executable
33run: main
34	$(BUILD_DIR)/main.out
35
36# compile the executable
37main: pre
38	gcc -O2 $(COMPILE)
39
40# compiles executable with debugging info and runs it with the GNU-debugger (gdb)
41debug: pre
42	gcc -g3 $(COMPILE)
43	gdb $(BUILD_DIR)/main.out
44
45# creates build dir, only if its not created yet
46pre:
47	mkdir -p $(BUILD_DIR)
48
49# removes build and test files/dirs
50.PHONY: clean
51clean:
52	rm -rf $(BUILD_DIR)
53	rm -f test.pgm
PS: #####

The next part will focus on the methods found in _pgm.(c|h) and some general issues I found myself having while working on this project.