Next: Defining Types, Previous: Poking a SBM Image, Up: Structuring Data [Contents][Index]
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) defvar ppl = byte @ 3#B (poke) ppl 5UB (poke) defvar lines = byte @ 4#B 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) defvar 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 recutils6 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 Mode Size Name * #1 0x00001000#B *scratch* #0 rw 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) defvar ppl = byte @ 3#B (poke) defvar lines = byte @ 4#B (poke) defvar 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) defvar 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.
It follows that if we wanted to change the color of some SBM pixel stored at offset offset, we would do this:
(poke) defvar pixel = 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. 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 'ios
and 'offset
attributes. 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 that
later.
Well, at this point it should be clear how to paint pixels. First, let’s define our background and foreground pixels:
(poke) defvar bga = [255UB, 255UB, 255UB] (poke) defvar 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 all background pixels. We know 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) defvar 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) defvar 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:2] [[255UB,99UB,71UB],[255UB,99UB,71UB]]
Note how the limits of the interval specified in the trim reflect array indexes (hence 0 based) and are both inclusive. The result of an array trimming is always another array, even if it contains just one element:
(poke) l0[1:1] [[255UB,99UB,71UB]]
Armed with this new operation, we can very easily mount the sequence of pixels for our cropped image:
(poke) defvar l0 = image_data[0] (poke) defvar l1 = image_data[1] (poke) defvar l2 = image_data[2] (poke) defvar l3 = image_data[3] (poke) defvar l4 = image_data[4] (poke) defvar l5 = image_data[5]
And then update the lines in the mapped image data:
(poke) image_data[0] = l0[1:3] (poke) image_data[0] = l1[1:3] (poke) image_data[1] = l2[1:3] (poke) image_data[2] = l3[1:3] (poke) image_data[3] = l4[1:3] (poke) image_data[4] = l5[1:3]
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"
Next: Defining Types, Previous: Poking a SBM Image, Up: Structuring Data [Contents][Index]