4.2 Poking a SBM Image

4.2.1 P is for poke

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 white background. The “painted” pixels are called foreground pixels, the non painted pixels are called background pixels.

4.2.2 Preparing the Canvas

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.

4.2.3 Poking the Header

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 though 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 0700 0000 0000 0000 0000 0000  SBM.............

4.2.4 Poking the Pixels

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 prospect 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
0xffffff as uint<24>
(poke) fg
0xff6347 as uint<24>

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]

4.2.5 Poking Lines

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…

4.2.6 Poking Images

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.

4.2.7 Saving the Image

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 :file "p.sbm"

Note how we expressed “ppl bytes” as ppl#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!