Next: Modifying SBM Images, Previous: The SBM Format, Up: Structuring Data [Contents][Index]
Let’s compose our first SBM image, using poke. The image we want to
encode is the very simple rendering of the letter P
shown in
the figure below.
| 0 | 1 | 2 | 3 | 4 | +---+---+---+---+---+ 0 | | * | * | | | 1 | | * | | * | | 2 | | * | | * | | 3 | | * | * | | | 4 | | * | | | | 5 | | * | | | | 6 | | * | | | |
The image has seven lines, and there are five pixels per line, i.e. the dimension of the image in pixels is 5x7. Also, the pixels denoted by asterisks are red, whereas the pixels denoted with empty spaces are white. In other words, our image uses a red foreground over a write background. The “painted” pixels are called foreground pixels, the non painted pixels are called background pixels.
The first thing we need is some suitable IO space where to encode the image. Let’s fire up poke and create a memory buffer:
$ poke […] (poke) .mem image The current IOS is now `*image*'. (poke) dump 76543210 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789ABCDEF 00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
Freshly created memory IO spaces are 4096 bytes long, and that’s big enough for our little image. If we wanted to work with more data, remember that memory IO spaces will grow automagically when poked past their size.
The first three bytes of the header of a SBM file contains the magic number that identifies the file as a SBM bitmap. We can poke these bytes very easily:
(poke) byte @ 0#B = 'S' (poke) byte @ 1#B = 'B' (poke) byte @ 2#B = 'M'
The next couple of bytes encode the dimensions of the bitmap, in this case 5x7:
(poke) byte @ 3#B = 5 (poke) byte @ 4#B = 7
There is something worth noting in this last mapping. Even tough we
were poking bytes (passing the byte
type specifier to the map
operators) we specified the 32-bit signed integers 5
and
7
instead of 5UB
and 7UB
. When poke finds a
situation like this, where certain kind of integers are expected but
other kind are provided, it converts the value from the provided type
to the expected type. This conversion may result in truncation (think
about converting, say 0xfff to an unsigned byte, whose maximum
possible value is 0xff) but certainly not in the case at hands.
The final header looks like:
(poke) dump :size 16#B 76543210 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789ABCDEF 00000000: 5342 4d05 0800 0000 0000 0000 0000 0000 SBM.............
Now that we have written a SBM header, we have to encode the sequence of pixels composing the image.
Recall that every pixel is encoded using three bytes, that conform a
RGB24 color. We have two kinds of pixels in our image: white pixels,
and red pixels. In RGB24 white is encoded as (255,255,255)
.
Pure red is encoded as (255,0,0)
, but to make things more
interesting we will be using a nicer tomato-like red
(255,99,71)
.
Therefore, poking a white pixel at some offset offset would involve the following operations:
(poke) byte @ offset = 255 (poke) byte @ offset+1#B = 255 (poke) byte @ offset+2#B = 255
Likewise, the operations to poke a tomato pixel would look like:
(poke) byte @ offset = 255 (poke) byte @ offset+1#B = 99 (poke) byte @ offset+2#B = 71
To ease things a bit, we can define variables with the color components for both foreground and background pixels:
(poke) var bg1 = 255 (poke) var bg2 = 255 (poke) var bg3 = 255 (poke) var fg1 = 255 (poke) var fg2 = 99 (poke) var fg3 = 71
Then to poke a foreground pixel would involve doing:
(poke) byte @ offset = fg1 (poke) byte @ offset+1#B = fg2 (poke) byte @ offset+2#B = fg3
At this point, you may feel that the perspective of mapping the pixels of our image is not very appealing, considering we have 5x7 = 35 pixels in our image. We will need to poke 35 * 3 = 105 bytes. We may feel tempted to, somehow, use a bigger integer to “encapsulate” the bytes. Using the bit-concatenation operator, we could do something like:
(poke) var bg = 255UB::255UB::255UB (poke) var fg = 255UB::99UB::71UB (poke) bg (uint<24>) 0xffffff (poke) fg (uint<24>) 0xff6347
This encodes each color with a 24-bit unsigned integer. When looking
at the hexadecimal values of bg
and fg
above, note that
0xff = 255, 0x63 = 99 and 0x47 = 71. Each byte seems to be in the
right position in the 24-bit containing number. Now, poking a pixel
at some given offset should be as easy as issuing just
one map operation, right? Let’s see, using some arbitrary offset 10#B:
(poke) uint<24> @ 10#B = fg (poke) dump :from 10#B :size 4#B 76543210 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789ABCDEF 0000000a: 4763 ff00 Gc..
If your current endianness is little (i.e. you are running on a x86 system or similar) you will get the dump above. The bytes are reversed, and consequently the resulting pixel has the wrong color. Our little trick didn’t work :(
So are we doomed to poke three bytes for each pixel we want to poke in our image? No, not really. The Poke language provides a construction oriented to alleviate cases like this, where several similar elements are to be “encapsulated” in a container. These constructions are called arrays.
Using array values, we can define the foreground and background colors like this:
(poke) var bga = [255UB, 255UB, 255UB] (poke) var fga = [255UB, 99UB, 71UB]
All the elements on an array should be of the same kind, i.e. of the same type. Therefore, this is not allowed:
(poke) [1,"foo"] <stdin>:1:1: error: array initializers should be of the same type [1,"foo"]; ^~~~~~~~~
Given an array value, it is possible to query for the number of values
contained in it (called elements) by using the 'length
value attribute. For example:
(poke) bga'length 3UL
Tells us that the array value stored in the variable bga
has
three elements.
How can we poke an array value? We know that the map operator accepts
two operands: a type specifier and the value to map. The type
specifier of an array of three bytes is denoted as byte[3]
.
Therefore, we can again try to poke a foreground pixel at offset 10#B,
this time using fga
:
(poke) byte[3] @ 10#B = fga (poke) dump :from 10#B :size 4#B 76543210 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789ABCDEF 0000000a: ff63 4700 .cG.
This time, the bytes were written in the right order. This is because array elements are always written using their “written” ordering, with no mind to endianness. We can also map a pixel from a given offset:
(poke) byte[3] @ 10#B [255UB,99UB,71UB]
At this point, we could encode the 40 pixels composing the image, by
issuing the same number of pokes of byte[3]
arrays. However,
we can simplify the task even further.
Our pixels are arrays of bytes, denoted by the type specifier
byte[3]
. Similarly, we could conceive arrays of 32-bit signed
integers, denoted by int[3]
, or arrays of bits, denoted by
uint<1>[3]
. But, is it possible to have arrays of other
arrays? Yes, it is:
(poke) [[1,2],[3,4]]
The value above is an array of two arrays of two integers each. If we
wanted to map such an array, what would be the type specifier we would
need to use? It would be int[2][2]
, which should be read from
right-to-left as “array of two arrays of two integers”. Let’s map
one from an arbitrary offset in our IO space:
(poke) int[2][2] @ 100#B [[0,0],[0,0]]
Consider again the sequence of pixels composing the image. Using the information we have in the SBM header, we can group the pixels in the sequence into “lines”. In our example image, each line contains 5 pixels. It would be natural to express each line as a sequence of pixels. The first line in our image would be:
(poke) var l0 = [bga,fga,fga,bga,bga] (poke) l0 [[255UB,255UB,255UB],[255UB,99UB,71UB],…]
Let’s complete the image lines:
(poke) var l0 = [bga,fga,bga,fga,bga] (poke) var l1 = [bga,fga,bga,fga,bga] (poke) var l2 = [bga,fga,fga,bga,bga] (poke) var l3 = [bga,fga,bga,bga,bga] (poke) var l4 = l3 (poke) var l5 = l4
Note how we exploited the fact that the three last lines of our image are identical, to avoid to write the same array thrice. Array values can be assigned, and in general manipulated, like any other kind of value, such as integers or strings.
At this point, we could poke the pixels line-by-line. What would be
the type specifier for a line? A line is an array of five arrays of
3 bytes each, so the type specifier would be byte[3][5]
. Let’s
do that:
(poke) byte[3][5] @ 5#B = l0 (poke) byte[3][5] @ 10#B = l1 (poke) byte[3][5] @ 15#B = l2 (poke) byte[3][5] @ 20#B = l3 (poke) byte[3][5] @ 25#B = l4 (poke) byte[3][5] @ 30#B = l5 (poke) byte[3][5] @ 35#B = l6
Not bad, we went from poking 105 bytes in the IO space to poking six lines. But we can still do better…
When we poked the lines at the end of the previous section, we had to increase the offset in every map operation. This is inconvenient.
In the same way that a sequence of bytes can be abstracted in a line, a sequence of lines can be abstracted in an image. It follows that we can look at the image data as an array of lines. But lines are themselves arrays of arrays… no matter, there is no limit on the number of arrays-of levels that you can nest.
So, let’s define our image as an array of the lines defined above:
(poke) var image_data = [l0,l1,l2,l3,l4,l5] (poke) image_data [[[255UB,255UB,255UB],[255UB,99UB,71UB],[255UB,99UB,71UB]…]…]
What would be the type specifier for an image? It would be an array
of seven arrays of five arrays of three bytes each, in other words
byte[3][5][7]
. Let’s poke the pixels:
(poke) byte[3][5][6] @ 5#B = image_data
This is an example of how abstraction can simplify the handling of binary data: we switched from manipulating bytes to manipulate higher abstractions such as colors, lines and images. We achieved that by structuring the data in a way that reflects these abstractions. That’s the way of the Poker.
Now that we have completed the SBM image in our buffer *image*
,
it is time to save it to disk. For that, we can use the
save
command we are already familiar with.
We know that the SBM image starts at offset 0#B, but what is the size of its entire binary representation? The header is easy: it spans for 5 bytes. The size of the sequence of pixels can be derived from the pixels per line byte, and the number of lines byte. We know that each pixel occupies 3 bytes, so calculating…
(poke) var ppl = byte @ 3#B (poke) var lines = byte @ 4#B (poke) save :from 0#B :size 5#B + ppl#B * lines#B :file "p.sbm"
Note how we expressed “ppl bytes” as ppl#B
, and “lines
bytes” as lines#B
. This is the same than expressing “10
bytes” as 10#B
. We will talk more about these united values
later.
There is another way of getting the size of the stream of pixels.
Recall that we have the entire set of pixels, structured as lines,
stored in the variable image_data
. Given an array, it is
possible to query for its size using the 'size
attribute:
(poke) .set obase 10 (poke) [1,2,3]'size 96UL#b
The above indicates that the size of the array of the three integers 1, 2 and 3 is 96 bits. Using that attribute, we can also obtain the size of the pixels in the image:
(poke) image_data'size 720UL#b
And we can use it in the save command:
(poke) save :from 0#B :size 5#B + image_data'size :file "p.sbm"
Using either strategy, at this point a file named p.sbm should have been written in the current working directory, containing our “P is for poke” image. Keep that file around, because we will be poking it further!
Next: Modifying SBM Images, Previous: The SBM Format, Up: Structuring Data [Contents][Index]