Hello again! In this video, we are going to have a practical on binary files.
We are going to use a bitmap as an example of how to work with a binary
file. Bitmap is a simple format for images. It actually goes back to MS-DOS, before Windows.
So it is quite basic.
In fact, it even assumes you have a 16-bit computer.
So we need to take account of that.
As with all these file formats, there is a standard.
In fact, there are actually several standards! We need to make sure that we write the data to the binary
file in the correct format.
If we make any mistakes, even ones which seem trivial, then the file is going to be invalid and we
cannot display it.
There are three parts to the bitmap formats. There is a file header, which is information for the operating
system,
I think. The info header, which has the properties of the image. And then the actual data, the 1's
and 0's that will make the vector appear on the screen.
The bitmap file header is designed for 16 bits, so we need to have everything with a 2-byte alignment.
So we need to use this hash pragma pack instruction. And then use 2 for 2-byte alignment. 16
bit intervals.
We are using a struct because this is just a compound type for storing data.
We are not doing anything object oriented. So there is no encapsulation, no member functions.
So it is easier to make everything public.
There are two characters which say what kind of file it is.
There is an integer with the size of the file. So this is a fixed width integer, to make sure we get
the same data, regardless of which system we are opening it with.
This is going to contain the total size of the file. So that is the file header, plus the info header, plus
all the data.
Then there is this reserved field, which we do not use.
And then there is the data offset.
So this will say how far from the beginning of the file to where the actual image data starts.
We are using these in-place initializers, so this means that "reserved" will have an initial value of 0.
Unless there is a constructor which initialize it with something else.
And then once we have declared this struct, we need to set the alignments back to the default. So we perform
this pop operation that we saw in the last video.
For the info header, these are practically all fields that we can default.
The only ones that we might want to change are the width and the height of the image.
For the actual image itself, this is made up of so-called "pixels" or vector elements.
And each of these vector elements corresponds to a single dot on your computer screen. In the bitmap
format,
each of these pixels consists of three bytes.
There is one byte for each of the primary colours: red, green and blue.
And these can have values from 0 to 255.
The value of each of these colour bytes will represent the contribution that that primary colour makes
towards the final colour of the pixel.
For example, if the red and green bytes are 0, and the blue byte is not 0, then you are going
to get a blue of some shade. And the intensity of that blue will depend on the value of the byte.
If blue is 0 and red and green are non-0, then you get some kind of yellow or orange dot on the
screen.
If all the bytes are 0 you get black; if they are all 255, you will get white.
And if they all have the same value, and it is something in between, you will get a shade of grey.
The normal convention in computer graphics is to have red first, then green, then blue. But
this standard was developed by Microsoft, so they have blue and green, then red.
How do we know where the pixel is going to appear on your screen?
We have a system of coordinates.
And you may recall this from doing maths at school.
So we have Cartesian coordinates. We have this variable x, which measures how far across the screen
the dot is. And this variable y will measure how far up or down the screen is.
Again, this is not the normal convention.
We are starting with 0 at the bottom and increasing y upwards.
Normally you start with y equals 0 at the top and y increases as it goes downwards.
The reason for that, is that that is how the electronics works. And that goes back to the old days of
computer sets with cathode ray tubes, and they electron beams, which were scanning left to right,
top to bottom.
One thing we need to be able to work out is the offset the Pixel. So, to get from the start of the
screen, in the bottom left hand corner, to the actual position x and y, how many pixels do we need to
go past?
Well, we need to go past all these rows.
So that is 'y' rows. And each of these rows will have a number of pixels in it, which is equal to the
width of the screen.
So to get from there to there,
we need to go 'y' times the width of the screen. And then to get from there to there,
we need to go x pixels further.
So the number of pixels, between the corner of the screen and that dot, is equal to y multiplied by
the width of the screen plus x. All expressed in pixels.
And this is the image that we are going to draw. And you can perhaps guess why I did not become a graphic
designer! Bitmap is a 1980's format, so why not have a nice, blocky 1980's image?
We're going to display C++ using blocks to represent the letters, the characters.
All the blocks have the same thickness.
So that is 'x' units across and 'y' units up.
We also have these coordinates of the middle of the screen. So x_mid and y_mid will be the middle.
So you can see that these all
line up with the middle. And that these are all symmetric about the middle of the screen, vertically
speaking.
We start off by drawing this column.
So this is going to be x units thick.
It is going to go from y equals 0 all the way up to the top.
So that is going to be up to the height of the screen.
Then we have these two cross pieces, which start at the edge of this column. And they go all the way
to the middle.
This one goes from y equals 0.
It is one thickness [high], so it goes up to y_unit.
This one ends
at the [top] of the screen, so it starts from height minus Y units.
And then the pluses are just 2x1 rectangles superimposed on each other.
So this is 2 units high.
And 1 unit wide. This one is 2 units wide and 1 unit high.
And this one is exactly the same, but it is offset by 3 thicknesses.
So there is 1 unit of thickness there and then another 2 units for the thickness of the plus sign.
If you are not too sure about how to work this out, you can always use trial and error. Some trial and
error may have been involved when I created this program :)
We are going to write a class to represents the bitmap in memory - or the image data part of it anyway.
We are going to use a vector of pixels to store the image data.
So this is going to be a vector of the vector elements, the pixel objects we defined earlier.
And we also have the width and height of the screen, and the name of the file that we are going to write
the bitmap file to.
So this is going to store all the pixels.
The first pixel in the vector will be the one in the bottom left of the screen.
The next one will be the first pixel after that. And so on.
So the pixels will be in the same order in this vector as they are on the screen.
So this means that the index of the element is also going to be the offset of the pixel.
So if we have a pixel at x and y and we want to retrieve it from the vector, we work out the offset.
y times the width of the screen plus x. And then we look up the elements which has that index.
And finally, we have some public member functions. We are going to have a constructor; a member function
for writing out the file, which is the point of the exercise; and perhaps some member functions for
writing blocks of pixels.
And then for actually writing out the file, we need to write out the file header, the info header
and the image data. For the file header and the info header,
we can just do the same trick that we had in the last video.
We take the address of the object, convert it to a pointer to char, and pass the size of the object
as the second argument.
With the image data, we need to convert the vector elements to an array of pixels.
And if you remember the video, there is the data() member function, which will return the array.
And then we cast that to char.
The second argument will be the number of pixels in the vector multiplied by the size of each pixel.
So here's our bitmap header, bitmap dot h.
We have the bitmap file header, with its unusual alignment.
We have our info header, with all these default initial values.
We have the pixel struct to store the colours for each pixel, and then we have the bitmap class.
With our data members and the member functions. For the constructor, we set the name of the file.
And then we want to initialize the vector, so that it has the right size to store all the elements.
The vector class has a constructor which takes the number of elements.
So if we do width times height, that will give us the total number of pixels in the image.
And then, if we pass that as the argument, then that will initialize the vector with enough elements
to hold all the pixels.
And then we do not need to do anything else. Because we have all these default initializers,
We do not need to worry about initializing those
headers.
Then we have a member function for setting the colour of a pixel.
So we give it a pixel object, which has the colours we want. Then we say x and y for the position of the
pixel we want to adjust.
And then this will find the pixel at those coordinates, and set it equal to the colours in that pixel.
Then we have one for setting all the pixels in a row to the same values. So it will take the row
number.
So that will be the y coordinate, and it will set all the pixels in that row to have that value.
And then, finally, one for setting all the pixels in the image to a single colour.
And of course, we have the write function, which is the whole reason for doing this exercise.
Here is this write function. So we need to write out the file header, the info header and the data.
First of all, we need to fill in a couple of fields in these headers. In the file
header, we need to provide the file size and the data offset. The size
of the file is going to be equal to the size of the header,
plus the size of the info header, plus the total memory used by the pixels.
So that is going to be the number of pixels,
the width times the height, Multiplied by the memory used by a single pixel.
The offset of the data.
To get to the data, we first have to go past the file header and then the info header.
So once we have gone past that many bytes, we will be at the start of the data.
So that is the value of the data offset in the file header.
In the info header, we need to set the width and height of the image.
Then we can open the file where we are going to write the bitmap. Using binary
mode, of course! And we check that the file is successfully opened.
If it is not, the caller will inform the user.
And then we write out the data into the file, as we saw on the slides.
And then we return true or false, and of course, we close the file if it was open.
For the member functions. The set_pixel(). So this will take a pixel and set the pixel, which
is at x and y, to be the same as that pixel.
So we need to calculate the offset of the pixel at x and y, and then that will give us the index into
the vector. And then we can just assign
that elements in the vector. So that pixel will now have the same value as the one that we passed
to the function.
And then, for setting all the pixels in a row.
So this will take a pixel and then we iterate over all the pixels in a single rwo, so with the same
y coordinate.
And then we call set_pixel() for each of those.
And, for setting all the pixels in the image, we iterate over all the rows in the image and then for each
row, we set all the pixels in the row. Which will call this member function, and then that will set
all the pixels.
Finally, the main program, which is going to run all this code, we calculate the midpoint of the
screen and also the thicknesses.
I just used one 8th of the screen height and width.
You can change that if you like.
We create a bitmap object.
We pass the name of the file that we are going to use as the argument.
We create a pixel in which all the primary colours are 127. So that is 50 per cent.
So that is going to be a mid-grey shade.
And then we set all the pixels in the image to have this.
So now, the representation of the image in memory is of a completely grey image.
And then we create a pixel with the colour we are going to use for the blocks.
So blue is 255, green is 255, which are both the maximum. Red is just 25,
so that's just a little hint of red, maybe!
This is going to produce a bluish green colour, which is cyan, or perhaps aqua, or maybe
some other colour.
To draw the stem of the 'C', we start at the bottom left hand corner: x equals 0, y equals 0.
We go across, 1 unit of thickness, and we go all the way up the screen. And then we set all the pixels
in that column to be this
cyan colour.
And then, for the cross pieces, we start one thickness in, from where we left off. The one for the bottom goes from y equals
0, one thickness high.
And then the one at the top of the screen goes up to the top of the screen, starting from one unit
below.
And then the rest is just a bit of algebra. Arithmetic, if you like.
You have the source code as a downloadable resource, so you can just copy this. Or you could try to
work it out for yourself, if you like.
And then we have created this vector full of pixels with all the colours that we want. And we are now
ready to write the file out.
So we write it to disk. And then we can find out if it successfully wrote the file, or if something
went wrong.
So, does it work?
So "Succeeded". So that means we managed to write the file.
It does not necessarily mean that the bitmap is valid.
And if you do have any problems, it means that you have made it a small mistake somewhere.
I have seen cases where there was a loop with "less then" and someone put "less than equals" by mistake, and that
made the image invalid.
So it can be really trivial.
So you do need to check everything very closely and double check it.
So do we have a valid image?
Yes, we do.
So there it is, C++. Maybe not a great masterpiece, but it is a start! And in theory, now that you
know how to work with binary files, you can go on to other formats, which are probably quite a bit more
involved.
But the principle is the same.
So for multimedia files, saving the state of the game, creating your own document format, and so on.
Okay, so that is all for this video.
I will see you next time, but until then, keep coding!