Let’s open with poke the cute image we created in the last section, p.sbm:
$ poke p.sbm […] (poke) dump 76543210 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789ABCDEF 00000000: 5342 4d05 07ff ffff ff63 47ff 6347 ffff SBM......cG.cG.. 00000010: ffff ffff ffff ffff 6347 ffff ffff 6347 ........cG....cG 00000020: ffff ffff ffff ff63 47ff ffff ff63 47ff .......cG....cG. 00000030: ffff ffff ffff 6347 ff63 47ff ffff ffff ......cG.cG..... 00000040: ffff ffff ff63 47ff ffff ffff ffff ffff .....cG......... 00000050: ffff ffff 6347 ffff ffff ffff ffff ffff ....cG.......... 00000060: ffff ff63 47ff ffff ffff ffff ffff ...cG.........
You can see the P
in the ASCII column, right? If it wasn’t for
the header, it would be pictured almost straight. This is because dump
shows 16 bytes per row, and our image has lines that are 15 bytes
long. This is a happy coincidence: you definitely shouldn’t expect to
see ASCII art in the dump output of SBM files in general! :)
Now let’s read the image’s metadata from the header: pixels per line and how many lines are contained in the image:
(poke) var ppl = byte @ 3#B (poke) ppl 5UB (poke) var lines = byte @ 4#B (poke) lines 7UB
All right, our image is 7x5. Knowing that each pixel occupies three
bytes, and that each line contains ppl
pixels, and that we have
lines
lines, we can map the entire image data using an array
type specifier:
(poke) var image_data = byte[3][ppl][lines] @ 5#B (poke) image_data [[[255UB,255UB,255UB],[255UB,99UB,71UB], …]…]
The “P is for poke” slogan was so successful and widely appraised
that the recutils7 chaps wanted
to do a similar campaign “R is for recutils”. For that purpose,
they asked us for a tomato-colored SBM image with an R
in it.
Our creative department got at it, and after a lot of work they came with the following design:
| 0 | 1 | 2 | 3 | 4 | +---+---+---+---+---+ 0 | | * | * | | | 1 | | * | | * | | 2 | | * | | * | | 3 | | * | * | | | 4 | | * | * | | | 5 | | * | | * | | 6 | | * | | * | |
Observe that this design really looks like our P
(so much for a
creative department). The bitmap has exactly the same dimensions, and
difference are just three pixels, that pass from being background
pixels to foreground pixels.
Therefore, it makes sense to read our p.sbm and use it as a base, completing the missing pixels. We saw in the last section how to read a SBM image. This time, however, we will copy the image first to a memory IO space to avoid overwriting p.sbm:
$ poke p.sbm […] (poke) .mem scratch The current IOS is now `*scratch*'. (poke) .info ios Id Type Mode Bias Size Name * 1 MEMORY 0x00000000#B 0x00001000#B *scratch* 0 FILE rw 0x00000000#B 0x0000006e#B ./p.sbm (poke) copy :from_ios 0 :from 0#B :to 0#B :size iosize (0) (poke) dump 76543210 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789ABCDEF 00000000: 5342 4d05 07ff ffff ff63 47ff 6347 ffff SBM......cG.cG.. 00000010: ffff ffff ffff ffff 6347 ffff ffff 6347 ........cG....cG 00000020: ffff ffff ffff ff63 47ff ffff ff63 47ff .......cG....cG. 00000030: ffff ffff ffff 6347 ff63 47ff ffff ffff ......cG.cG..... 00000040: ffff ffff ff63 47ff ffff ffff ffff ffff .....cG......... 00000050: ffff ffff 6347 ffff ffff ffff ffff ffff ....cG.......... 00000060: ffff ff63 47ff ffff ffff ffff ffff 0000 ...cG........... 00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
Good. Now let’s map the contents of the image, both header information and the sequence of pixels:
(poke) var ppl = byte @ 3#B (poke) var lines = byte @ 4#B (poke) var image_data = byte[3][ppl][lines] @ 5#B
Let’s modify the image. Since the dimensions of the new image are
exactly the same, the header remains the same. It is the pixel
sequence that is different. We basically need to turn the pixels at
coordinates (4,2)
, (5,3)
and (6,3)
from
background pixels to foreground pixels.
Remember how we would change the value of some integer in the IO space? First, we would map it into a variable, change the value, and then poke it back to the IO space. Something like this:
(poke) var n = int @ offset (poke) n = n + 1 (poke) int @ offset = n
This three-steps process is necessary because in the n = n + 1
above we are modifying the value of the variable n
, not the
integer actually stored at offset offset in the current IO
space. Therefore we have to explicitly poke it back if we want the IO
space to be updated as well.
Array values (and, as we shall see, other “composited” values) are different: when they are the result of the application of the map operator, the resulting values are mapped themselves.
When a Poke value is mapped, updating their elements have a side effect: the area corresponding to the updated element, in whatever IO space it is mapped on, is updated as well!
Why is this? The map operator, regardless of the kind of value it is mapping, always returns a copy of the value found stored in the IO space. We already saw how this worked with integers. However, in Poke values are copied around using a mechanism called “shared value”. This means that when a composite value like an array is copied, its elements are shared by both the original value and the new value.
We can depict this graphically for better understanding. A Poke array like:
(poke) var a = [1,2,3]
is stored in memory like this:
+---+---+---+ a | 1 | 2 | 3 | +---+---+---+
If we make a copy of the array in another variable b
:
(poke) var b = a
we get
+---+---+---+ +---+---+---+ a | 1 | 2 | 3 | b | 1 | 2 | 3 | +---+---+---+ +---+---+---+
Note how each of the integer elements has been copied to the new value. The resulting two arrays can then be modified independently:
(poke) a[1] = 5
resulting in:
+---+---+---+ +---+---+---+ a | 1 | 5 | 3 | b | 1 | 2 | 3 | +---+---+---+ +---+---+---+
However, consider the following array whose elements are also arrays:
(poke) var a = [[1,2],[3,4],[5,6]]
This array is stored in memory like this:
+---+---+---+ a | | | | +---+---+---+ | | | +---+---+ | | +---| 5 | 6 | | | +---+---+ | | | | +---+---+ | +-------| 3 | 4 | | +---+---+ | | +---+---+ +-----------| 1 | 2 | +---+---+
If now we make a copy of the same array into another variable
b
:
(poke) var b = a
The elements of the array are copied “by shared value”, i.e.
+---+---+---+ +---+---+---+ a | | | | b | | | | +---+---+---+ +---+---+---+ | | | +---+---+ | | | | | +---| 5 | 6 |---+ | | | | +---+---+ | | | | | | | | +---+---+ | | | +-------| 3 | 4 |-------+ | | +---+---+ | | | | +---+---+ | +-----------| 1 | 2 |-----------+ +---+---+
The elements are indeed shared between the original array and the copy! If now we modify any of the shared values, this will be reflected in both containing values:
(poke) a[1][1] = 9 (poke) a [[1,2],[3,9],[5,6]] (poke) b [[1,2],[3,9],[5,6]]
or graphically:
+---+---+---+ +---+---+---+ a | | | | b | | | | +---+---+---+ +---+---+---+ | | | +---+---+ | | | | | +---| 5 | 6 |---+ | | | | +---+---+ | | | | | | | | +---+---+ | | | +-------| 3 | 9 |-------+ | | +---+---+ | | | | +---+---+ | +-----------| 1 | 2 |-----------+ +---+---+
Back to our image, it follows that if we wanted to change the color of some SBM pixel stored at offset offset, we would do this:
(poke) var a = byte[3] @ offset (poke) a[1] = 10
There is no need to poke the array back explicitly: the side effect of
assigning 10 to a[1] is that the byte at offset offset+1
is poked.
Generally speaking, mapped values can be handled in exactly the same
way than non-mapped values. This is actually a very central concept
in poke. However, it is possible to check whether a given value is
mapped or not using the 'mapped
attribute.
As we said, simple values such as integers and strings are never
mapped, regardless of where they come from. Both ppl
and
lines
are integers, therefore:
(poke) ppl'mapped 0 (poke) lines'mapped 0
However, image_data
is an array that was the result of the
application of a map operator, so:
(poke) image_data'mapped 1
When a value is mapped, you can ask for the offset where it is mapped,
and the IO space where it is mapped, using the attributes
'offset
and 'ios
respectively. Therefore:
(poke) image_data'ios 1 (poke) image_data'offset 40UL#b
In other words, image_data
is mapped in the IO space with id 1
(the *scratch*
buffer) at offset 40 bits, or 5 bytes. We
already knew that, because we mapped the image data ourselves, but in
other situations these attributes are most useful. We shall see how
later.
Well, at this point it should be clear how to paint pixels. First, let’s define our background and foreground pixels:
(poke) var bga = [255UB, 255UB, 255UB] (poke) var fga = [255UB, 99UB, 71UB]
Then, we just update the pixels in the image data using the right coordinates:
(poke) image_data[4][2] = fga (poke) image_data[5][3] = fga (poke) image_data[6][3] = fga (poke) dump 76543210 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789ABCDEF 00000000: 5342 4d05 07ff ffff ff63 47ff 6347 ffff SBM......cG.cG.. 00000010: ffff ffff ffff ffff 6347 ffff ffff 6347 ........cG....cG 00000020: ffff ffff ffff ff63 47ff ffff ff63 47ff .......cG....cG. 00000030: ffff ffff ffff 6347 ff63 47ff ffff ffff ......cG.cG..... 00000040: ffff ffff ff63 47ff 6347 ffff ffff ffff .....cG.cG...... 00000050: ffff ffff 6347 ffff ffff 6347 ffff ffff ....cG....cG.... 00000060: ffff ff63 47ff ffff ff63 47ff ffff 0000 ...cG....cG..... 00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
Looking at our new image, we realize that the first and the last column are all background pixels. We are aware that the recutils project is always short of resources, so we would like to modify the image to remove these columns, cropping it so it looks like this:
| 0 | 1 | 2 | +---+---+---+ 0 | * | * | | 1 | * | | * | 2 | * | | * | 3 | * | * | | 4 | * | * | | 5 | * | | * | 6 | * | | * |
In order to perform this operation we need to rework the stream of pixels to reflect the desired result, and then to update the header metadata accordingly.
Let’s think in term of lines. In the original image, each line has 5 pixels, that we can enumerate as:
+----+----+----+----+----+ line | p0 | p1 | p2 | p3 | p4 | +----+----+----+----+----+
What we want is to crop out the first and the last column, so the resulting line would look like:
+----+----+----+ line | p1 | p2 | p3 | +----+----+----+
Let’s get the first line from the original image_data
:
(poke) var l0 = image_data[0] (poke) l0 [[255UB,255UB,255UB],[255UB,99UB,71UB],…]
We could create the corresponding cropped line, by doing something like this:
(poke) var cl0 = [l0[1],l0[2],l0[3]]
And the same for the other lines. However, Poke provides a better way
to easily obtain sub portions of arrays. It is called trimming.
Given an array like the line l0
, we can obtain the desired
portion of it by issuing:
(poke) l0[1:4] [[255UB,99UB,71UB],[255UB,99UB,71UB]]
Note how the limits of the semi-open interval specified in the trim reflect array indexes, and hence they are 0 based. Also, the left limit is closed and the right limit is open. The result of an array trimming is always another array, which may be empty:
(poke) l0[1:3] [[255UB,99UB,71UB]]
Armed with this new operation, we can very easily mount the sequence of pixels for our cropped image:
(poke) var l0 = image_data[0] (poke) var l1 = image_data[1] (poke) var l2 = image_data[2] (poke) var l3 = image_data[3] (poke) var l4 = image_data[4] (poke) var l5 = image_data[5]
And then update the lines in the mapped image data:
(poke) image_data[0] = l0[1:4] (poke) image_data[0] = l1[1:4] (poke) image_data[1] = l2[1:4] (poke) image_data[2] = l3[1:4] (poke) image_data[3] = l4[1:4] (poke) image_data[4] = l5[1:4]
The last step is to update the header to reflect the new dimensions of the image:
(poke) byte[] @ 0#B = ['S','B','M'] (poke) byte @ 3#B = 3 (poke) byte @ 4#B = 7
And we are done. Note how this time we wrote the magic bytes as an
array, to save some typing and silly manual offset arithmetic. You
may have noticed that the type specifier we used this time in the map
is byte[]
instead of byte[3]
. This type specifier
denotes an array of any number of bytes, which certainly includes
arrays of three bytes, like in the example.
And finally, let’s write out the new file as r.sbm:
(poke) save :from 0#B :size 5#B + image_data'size :file "r.sbm"