Now: tricorn - Optimized SIMD main implementation tricorn_V1 - Optimized SISD comparison implementation tricorn_V0 - Unoptimized SIMD implementation tricorn_V5 - highly optimized SIMD + threading implementation
560 lines
21 KiB
C
560 lines
21 KiB
C
#define _POSIX_C_SOURCE 199309L // for CLOCK_MONOTONIC
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h> // for getopt
|
|
#include <getopt.h> // for getopt_long
|
|
#include <string.h> // for strcpy
|
|
#include <time.h> // for benchmarking
|
|
|
|
// All implementations
|
|
#include "tricorn.h" // Final implmementation
|
|
#include "tricorn_V1.h"
|
|
#include "tricorn_V2.h"
|
|
#include "tricorn_V3.h"
|
|
|
|
|
|
|
|
void save_bmp(const char *filename, unsigned char *img, size_t width, ssize_t height,_Bool color);
|
|
|
|
void benchmarking(size_t benchmarking_amount, int version_to_use, float complex complexA, size_t width, ssize_t height, float resolution, unsigned n, _Bool colored, unsigned char *tempArr) {
|
|
struct timespec start;
|
|
struct timespec end;
|
|
void (*tricorn_implementation)(float complex, size_t, ssize_t, float, unsigned, _Bool, unsigned char*);
|
|
|
|
switch (version_to_use)
|
|
{
|
|
case 0: // Uses highly optimized SIMD and threaded version / main implementation
|
|
printf("Benchmarking V0 - highly optimized SIMD\n");
|
|
tricorn_implementation = tricorn;
|
|
break;
|
|
case 1: // Uses unoptimised scalar comparison implementation
|
|
printf("Benchmarking V1 - scalar comparison implementation\n");
|
|
tricorn_implementation = tricorn_V1;
|
|
break;
|
|
case 2: // Uses optimized SISD implementation
|
|
printf("Benchmarking V2 - unoptimized SIMD implementation\n");
|
|
tricorn_implementation = tricorn_V2;
|
|
break;
|
|
case 3: // Uses unoptimized SIMD implementation
|
|
printf("Benchmarking V3 - highly optimized SIMD + Threading implementation\n");
|
|
tricorn_implementation = tricorn_V3;
|
|
break;
|
|
default:
|
|
fprintf(stderr,"Invalid version to use: V%d. Available versions are : V0,V1,V2,V3,V4,V5\n",version_to_use);
|
|
return;
|
|
}
|
|
|
|
printf("\n");
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &start);
|
|
for (size_t i = 0; i < benchmarking_amount; i++)
|
|
{
|
|
tricorn_implementation(complexA,width,height,resolution,n,colored,tempArr);
|
|
}
|
|
clock_gettime(CLOCK_MONOTONIC, &end);
|
|
|
|
double time = end.tv_sec- start.tv_sec + 1e-9 * (end.tv_nsec- start.tv_nsec);
|
|
printf("The total amount of time spent for %zu function calls: %lf seconds\n", benchmarking_amount,time);
|
|
double average = time / benchmarking_amount;
|
|
printf("Average time per function: %lf seconds\n", average);
|
|
}
|
|
|
|
unsigned tests(void) {
|
|
|
|
printf("Doing testing\n\n");
|
|
size_t width = 1920;
|
|
size_t height = 1080;
|
|
unsigned char *tempArrColored = malloc(width*height*3);
|
|
unsigned char *tempArrNotColored = malloc(width*height*1);
|
|
if (tempArrColored == NULL || tempArrNotColored == NULL)
|
|
{
|
|
fprintf(stderr,"Failed to allocate space for image\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
printf("\nTest 1: Default image, not coloured\n");
|
|
benchmarking(3,0,-2.0f - 0.5f*I, width, height, 0.001f, 200, false, tempArrNotColored);
|
|
printf("\nTest 2: Default image, coloured\n");
|
|
benchmarking(3,0,-2.0f - 0.5f*I, width, height, 0.001f, 200, true, tempArrColored);
|
|
printf("\nTest 3: High iteration count\n");
|
|
benchmarking(3,0,-2.0f - 0.5f*I, width, height, 0.001f, 5000, false, tempArrNotColored);
|
|
printf("\nTest 4: Zoomed image\n");
|
|
benchmarking(3,0,-0.5f - 0.5f*I, 1024, 1024, 0.0005f, 500, false, tempArrNotColored);
|
|
|
|
free(tempArrColored);
|
|
free(tempArrNotColored);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void checkInputs(_Bool n,_Bool r,_Bool o,_Bool d,_Bool s) {
|
|
if (!n)
|
|
{
|
|
fprintf(stderr, "ERROR: Input n not given.\n");
|
|
}
|
|
if (!r)
|
|
{
|
|
fprintf(stderr, "ERROR: Input r not given.\n");
|
|
}
|
|
if (!o)
|
|
{
|
|
fprintf(stderr, "ERROR: Input o not given.\n");
|
|
}
|
|
if (!d)
|
|
{
|
|
fprintf(stderr, "ERROR: Input d not given.\n");
|
|
}
|
|
if (!s)
|
|
{
|
|
fprintf(stderr, "ERROR: Input s not given.\n");
|
|
}
|
|
|
|
}
|
|
|
|
// THESE WORK FOR - in calculation
|
|
// REMOVE LATER: interesting test command ./tricorn -s -2.0,0.6 -r 0.0010 -n 200 -d 1920,1080 -o image.bmp -C
|
|
// AND FOR NEGATIVE HEIGHT: ./tricorn -s -2,-0.5 -r 0.0010 -n 200 -d 1920,-1080 -o image.bmp -C
|
|
// THIS IS FOR + in calculation
|
|
// ./tricorn -s -2.0,-1.5 -d 1200,900 -r 0.003 -n 256 -o tricorn.bmp -C
|
|
const char* help_messsage = "Help Message\n\n"
|
|
"Usage:\n"
|
|
" ./tricorn [OPTIONS]\n\n"
|
|
"Options:\n"
|
|
"-V <number>\n"
|
|
" Selects the implementation to use.\n"
|
|
" -V 0 uses the main implementation.\n"
|
|
" If no argument is given, -V 0 is used.\n"
|
|
" Example: -V 1\n\n"
|
|
"-B <number>\n"
|
|
" Benchmarks the selected implementation.\n"
|
|
" The argument chooses how many times the function is called.\n"
|
|
" Example: -B 5\n\n"
|
|
"-s <real>,<imaginary>\n"
|
|
" Sets the starting point of the function.\n"
|
|
" Example: -s -2.0,1.5\n\n"
|
|
"-d <width>,<height>\n"
|
|
" Sets the width and the height of the generated image.\n"
|
|
" Example: -d 800,600\n\n"
|
|
"-n <number>\n"
|
|
" Sets the maximum number of iterations per pixel.\n"
|
|
" Example: -n 255\n\n"
|
|
"-r <floating-point number>\n"
|
|
" Sets the resolution per pixel.\n"
|
|
" Example: -r 0.01\n\n"
|
|
"-o <filename>\n"
|
|
" Sets the output file name.\n"
|
|
" The file name must end with .bmp\n"
|
|
" Example: -o image.bmp\n\n"
|
|
"-C, --color\n"
|
|
" Enables colored output.\n\n"
|
|
"-h, --help\n"
|
|
" Displays the help message and exits.\n\n"
|
|
"-t, --test\n"
|
|
" Runs the tests, prints the results, and exists.\n";
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
int opt_long;
|
|
// will be the returned optional character,
|
|
// example: ./main -c -> opt_long here will be the optional 'c'
|
|
|
|
const char* optstring = "V:B:s:d:n:r:o:Cht" ;
|
|
// valid options that can be given to the programme. options with ":" after them expect an argument, which we'll later store in <char *optarg>
|
|
// example: B: <number> -> optional B expects an argument after it, and is valid -> ./main -B 67 (optarg is now "67")
|
|
// example: h -> optional h doesn't expect an argument -> ./main -h
|
|
|
|
// optind -> keeps track of the next index. initialised as 1 in the beginning. calling opt_long updates it every time
|
|
|
|
// optarg -> is a charptr. stores the arguments given to the optionals
|
|
|
|
static struct option long_options[] = {
|
|
{"color",0,0,'C'},
|
|
{"help",0,0,'h'},
|
|
{"test",0,0,'t'},
|
|
{0,0,0,0}
|
|
};
|
|
// these are the long optionals, which we can call with double "--". "--help" or "--test". they are converted to the optionals "h" and "t" respectively
|
|
// first argument is "name" -> name of the long. "--<name>"
|
|
// second is "has_arg" -> 0 if no arg, 1 if arg, 2 if optional arg
|
|
// third is "*flag" -> sets a flag so that "--help" simply returns "h" and does nothing else
|
|
// fourth is "val" -> what to return when "--help" is given, returns "h"
|
|
|
|
|
|
int version_to_use = 0; // actual initial value
|
|
_Bool colored = 0; // actual initial value
|
|
_Bool will_benchmark = false; // actual initial value
|
|
_Bool will_test = false;
|
|
|
|
|
|
_Bool nGiven = false;
|
|
_Bool rGiven = false;
|
|
_Bool oGiven = false;
|
|
_Bool dGiven = false;
|
|
_Bool sGiven = false;
|
|
size_t amount_function_called = 0; // initial values, so that the compiler doesn't complain
|
|
unsigned n = 0;
|
|
float starting_point_real = -1;
|
|
float starting_point_imaginary = -1;
|
|
size_t width = 0;
|
|
ssize_t height = -1;
|
|
float resolution = -1;
|
|
char* output_data = NULL;
|
|
|
|
|
|
opt_long = getopt_long(argc,argv,optstring, long_options, NULL);
|
|
// first four arguments are self-explanatory, fifth argument is the index of which long_option we return in the struct "long_options", but it's irrelevant so we set it to NULL
|
|
while (opt_long != -1)
|
|
{
|
|
switch (opt_long)
|
|
{
|
|
case 'V': {// done, function needed
|
|
char *end;
|
|
version_to_use = strtol(optarg,&end,10);
|
|
|
|
|
|
if (version_to_use < 0 || version_to_use > 5 || end == optarg || *end != '\0')
|
|
{
|
|
fprintf(stderr,"Invalid version to use: V%s. Available versions are : V0,V1,V2,V3,V4,V5\n", optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
printf("version is %d\n", version_to_use);
|
|
// call different version functions
|
|
|
|
// Die Implementierung, die verwendet werden soll. Hierbei soll mit
|
|
// -V 0 Ihre Hauptimplementierung verwendet werden. Wenn diese Option nicht
|
|
// gesetzt wird, soll ebenfalls die Hauptimplementierung ausgeführt werden
|
|
break;
|
|
}
|
|
|
|
case 'B': { // done, function needed
|
|
// wird die Laufzeit der angegebenen Implementierung gemessen und ausgegeben (do this later)
|
|
char *end;
|
|
long value = strtol(optarg,&end,10);
|
|
|
|
if (value <= 0 || end == optarg || *end != '\0') {
|
|
fprintf(stderr, "Invalid benchmarking input: %s\n", optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
amount_function_called = (size_t)value;
|
|
will_benchmark = true;
|
|
printf("amount function called is %zu\n", amount_function_called);
|
|
// call benchmarking function
|
|
|
|
// Das Argument dieser Option gibt die Anzahl an Wiederholungen des Funktionsaufrufs an, z. B.-B 5 .
|
|
break;
|
|
}
|
|
|
|
case 's': {
|
|
sGiven = true;
|
|
char *imaginary_string;
|
|
char *end;
|
|
|
|
starting_point_real = strtof(optarg,&imaginary_string); // split into two
|
|
if (imaginary_string == optarg || *imaginary_string != ',')
|
|
{
|
|
fprintf(stderr, "Invalid first number or missing comma: %s\n",optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
starting_point_imaginary = strtof(imaginary_string + 1, &end); // second part, the imaginary part
|
|
if (end == imaginary_string + 1 || *end != '\0')
|
|
{
|
|
fprintf(stderr, "Invalid second number or extra characters: %s\n",optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
printf("real %f, img %f\n", starting_point_real,starting_point_imaginary);
|
|
|
|
|
|
// s <Realteil>,<Imaginärteil> — Startpunkt der Berechnung
|
|
break;
|
|
}
|
|
|
|
case 'd': {
|
|
dGiven = true;
|
|
char* height_string;
|
|
char *end;
|
|
long widthValue = strtol(optarg,&height_string,10);
|
|
|
|
if (height_string == optarg || *height_string != ',')
|
|
{
|
|
fprintf(stderr, "Invalid width or missing comma: %s\n",optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
long heightValue = strtol(height_string + 1,&end,10);
|
|
|
|
if (end == height_string + 1 || *end != '\0')
|
|
{
|
|
fprintf(stderr, "Invalid height or extra characters: %s\n",optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if (widthValue <= 0 || heightValue == 0)
|
|
{
|
|
fprintf(stderr, "Width must be > 0 and height must not be 0: %s\n", optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
width = (size_t)widthValue;
|
|
height = (ssize_t)heightValue;
|
|
|
|
printf("width %zu, height %zd\n", width,height);
|
|
// -d <Zahl>,<Zahl> —Breite und Höhe des Bildausschnitts
|
|
break;
|
|
}
|
|
|
|
case 'n': {
|
|
nGiven = true;
|
|
char* end;
|
|
long nValue = strtol(optarg,&end,10);
|
|
|
|
if (end == optarg || *end != '\0' || nValue <= 0)
|
|
{
|
|
fprintf(stderr, "Invalid n value: %s\n", optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
n = (unsigned) nValue;
|
|
printf("n:%u\n",n);
|
|
// -n <Zahl> —MaximaleAnzahlanIterationen pro Bildpixel
|
|
break;
|
|
}
|
|
|
|
case 'r': {
|
|
rGiven = true;
|
|
char *end;
|
|
resolution = strtof(optarg,&end);
|
|
if (end == optarg || *end != '\0' || resolution <= 0.0f)
|
|
{
|
|
fprintf(stderr, "Invalid floating point number: %s\n",optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
printf("r:%f\n", resolution);
|
|
// r <Floating Point Zahl> —Schrittweite pro Bildpixel
|
|
break;
|
|
}
|
|
|
|
case 'o':
|
|
oGiven = true;
|
|
if (optarg[0] == '-')
|
|
{
|
|
fprintf(stderr, "Invalid output filename: %s\n",optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
output_data = malloc(strlen(optarg) + 1);
|
|
if (output_data == NULL)
|
|
{
|
|
fprintf(stderr,"Memory allocation failed.\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
strcpy(output_data,optarg);
|
|
printf("output data: %s\n", output_data); // test
|
|
// -o <Dateiname> —Ausgabedatei
|
|
break;
|
|
|
|
case 'C': // done
|
|
colored = true;
|
|
printf("colored:%d\n",colored);
|
|
// C|--color —Farbausgabe
|
|
break;
|
|
|
|
case 'h': // done
|
|
printf("%s",help_messsage);
|
|
// print help message
|
|
// Eine Beschreibung aller Optionen des Programms und Verwendungsbeispiele werden ausgegeben und das Programm danach beendet.
|
|
// Die Beschreibung soll mit "Help Message" anfangen.
|
|
return EXIT_SUCCESS;
|
|
|
|
case 't': // done, function needed
|
|
will_test = true;
|
|
// Führt eine vordefinierte Testsuite aus, welche sämtliche von Ihnen
|
|
// verwendete Benchmarking-Szenarien gesammelt durchläuft. Das Programm gibt
|
|
// die jeweiligen Parameter, eine kurze Beschreibungen sowie die Ergebnisse in einer
|
|
// übersichtlichen und gut lesbaren Form aus und terminiert anschließend.
|
|
break;
|
|
|
|
default: // idk
|
|
// case '?' -> when the argument is not valid
|
|
fprintf(stderr,"ERROR: Invalid argument -%c:\n",optopt);
|
|
// print error msg
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
opt_long = getopt_long(argc,argv,optstring, long_options, NULL);
|
|
}
|
|
|
|
float complex complexA = (starting_point_real + starting_point_imaginary*I);
|
|
int bytesPerColour = colored ? 3 : 1;
|
|
|
|
ssize_t absolute_height = height >= 0 ? height : height * - 1;
|
|
unsigned char *tempArr = malloc(width*absolute_height*bytesPerColour);// allocate image array on the heap to support large images as the stack is usually only 8 MB big
|
|
if(tempArr == NULL) {
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
if (will_test)
|
|
{
|
|
return tests() ? EXIT_FAILURE : EXIT_SUCCESS;
|
|
}
|
|
|
|
if (!(nGiven && rGiven && oGiven && dGiven && sGiven))
|
|
{
|
|
checkInputs(nGiven,rGiven,oGiven,dGiven,sGiven);
|
|
free(tempArr);
|
|
free(output_data);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if (will_benchmark)
|
|
{
|
|
printf("\nDoing benchmarking. No images will be created\n");
|
|
benchmarking(amount_function_called,version_to_use,complexA,width,height,resolution,n,colored,tempArr);
|
|
free(tempArr);
|
|
free(output_data);
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
switch (version_to_use)
|
|
{
|
|
case 0:
|
|
// Uses highly optimized SIMD - main implementation
|
|
tricorn(complexA,width,height,resolution,n,colored,tempArr);
|
|
break;
|
|
case 1:
|
|
// Uses scalar comparison implementation
|
|
tricorn_V1(complexA,width,height,resolution,n,colored,tempArr);
|
|
break;
|
|
case 2:
|
|
// Uses unoptimized SIMD implementation
|
|
tricorn_V2(complexA,width,height,resolution,n,colored,tempArr);
|
|
break;
|
|
case 3:
|
|
// Uses highly optimized SIMD + Threading implementation
|
|
tricorn_V3(complexA,width,height,resolution,n,colored,tempArr);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
save_bmp(output_data,tempArr,width,height,colored);
|
|
|
|
free(output_data);
|
|
free(tempArr);
|
|
return 0;
|
|
}
|
|
|
|
|
|
void save_bmp(const char *filename, unsigned char *img, size_t width, ssize_t height,_Bool color) {
|
|
// Open a file at the filepath given as a stream in the program
|
|
FILE *fptr = fopen(filename,"wb");
|
|
|
|
if(fptr == NULL) { // Chech for errors with opening the file stream
|
|
fprintf(stderr,"ERROR: Could not open output file at %s\n.",filename);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
size_t bytesPerColor = color ? 3 : 1;
|
|
|
|
|
|
int actual_height = (height < 0) ? -height : height; // Calculate absolute height for iteration and calculation purposes
|
|
size_t row_padding = (4 - (width * bytesPerColor) % 4) % 4; // Calculate padding for each row as .bmp is 4 bytes aligned
|
|
size_t row_width = width * bytesPerColor + row_padding; // Calculate row width with padding
|
|
|
|
size_t image_size = actual_height * row_width;
|
|
size_t file_size = image_size + 54; // File size = Image Size + Headers
|
|
size_t header_offset = color ? 54 : 1078; // If colored then offset = 54 if not we add offset = 54 + 1024; 1024 Bytes for color pallete
|
|
|
|
|
|
// File header - contains information about the file
|
|
unsigned char file_header[14] = {
|
|
'B', 'M', // BM - identifies file as a .bmp file
|
|
0, 0, 0, 0, // 4 bytes for file size - will be set later
|
|
0, 0, 0, 0, // Reserved
|
|
0, 0, 0, 0 // 4 bytes that specify the offset where the image array begins
|
|
};
|
|
|
|
// Serialize the size into 4 bytes - (unsigned char) takes only bottom 4 bytes 1 2 3 4
|
|
file_header[2] = (unsigned char)(file_size);
|
|
file_header[3] = (unsigned char)(file_size >> 8);
|
|
file_header[4] = (unsigned char)(file_size >> 16);
|
|
file_header[5] = (unsigned char)(file_size >> 24);
|
|
|
|
file_header[10] = (unsigned char)(header_offset);
|
|
file_header[11] = (unsigned char)(header_offset >> 8);
|
|
|
|
// Info header - contains information about the image itself
|
|
unsigned char info_header[40] = {
|
|
40, 0, 0, 0, // Size of this header
|
|
0, 0, 0, 0, // Width in pixels - set later
|
|
0, 0, 0, 0, // Height in pixels - set later
|
|
1, 0, // Number of color planes - always 1
|
|
bytesPerColor * 8, 0, // Number of bits per pixel or color depth
|
|
0, 0, 0, 0, // Compression method - 0 for no compression
|
|
0, 0, 0, 0, // Image size - set later
|
|
0x13, 0x0B, 0, 0, // Horizontal resolution in pixels per metre - Set to standart 72 pixels per inch
|
|
0x13, 0x0B, 0, 0, // Vertical resolution in pixels per metre - Set to standart 72 pixels per inch
|
|
0, 0, 0, 0, // Number of colors in the color pallete - 0 for default of 2^n where n is bits per pixel
|
|
0, 0, 0, 0 // Number of important colors - 0 for all colors important
|
|
};
|
|
|
|
// Setting width
|
|
info_header[4] = (unsigned char)(width);
|
|
info_header[5] = (unsigned char)(width >> 8);
|
|
info_header[6] = (unsigned char)(width >> 16);
|
|
info_header[7] = (unsigned char)(width >> 24);
|
|
|
|
// Setting height
|
|
uint32_t h = (uint32_t)height;
|
|
info_header[8] = (unsigned char)(h);
|
|
info_header[9] = (unsigned char)(h >> 8);
|
|
info_header[10] = (unsigned char)(h >> 16);
|
|
info_header[11] = (unsigned char)(h >> 24);
|
|
|
|
// Setting image size
|
|
info_header[20] = (unsigned char)(image_size);
|
|
info_header[21] = (unsigned char)(image_size >> 8);
|
|
info_header[22] = (unsigned char)(image_size >> 16);
|
|
info_header[23] = (unsigned char)(image_size >> 24);
|
|
|
|
// Write headers into file
|
|
//size_t fwrite(const void ptr[restrict .size * .n], size_t size, size_t n, FILE *restrict stream);
|
|
fwrite(file_header,1,14,fptr);
|
|
fwrite(info_header,1,40,fptr);
|
|
|
|
|
|
//If image is grayscale write color dictionary to tell reader what color to visualise for each value
|
|
// Row 0 - Value 0: [R=0;B=0;G=0]
|
|
// ...
|
|
//Row i - Value i: [R=i;B=i;G=i]
|
|
if (!color) {
|
|
unsigned char palette[1024];
|
|
for (int i = 0; i < 256; i++) {
|
|
palette[i * 4 + 0] = i; // B
|
|
palette[i * 4 + 1] = i; // G
|
|
palette[i * 4 + 2] = i; // R
|
|
palette[i * 4 + 3] = 0; // Reserved (Padding)
|
|
}
|
|
fwrite(palette, 1, 1024, fptr);
|
|
}
|
|
|
|
// Save padding bytes as array so we can write from it easily to the file later
|
|
unsigned char padding_bytes[3] = {0, 0, 0};
|
|
|
|
for(int i = 0; i < actual_height; i++) {
|
|
// Write whole row of pixels - img + (i * width * 3) is the position of the row in the image array
|
|
// size = 3 as we are interpreting every pixel as an object
|
|
fwrite(img + (i * width * bytesPerColor), bytesPerColor, width, fptr);
|
|
if(row_padding > 0) {
|
|
fwrite(padding_bytes,1,row_padding,fptr); // Write row_padding number of padding bytes to the file stream
|
|
}
|
|
}
|
|
|
|
fclose(fptr);
|
|
} |