Generate an image with KRL
Introduction
When a ruleset shares
a KRL function and a user agent (i.e. browser) calls for that function through a /sky/cloud
URL, the response is generally a string or a JSON object. The string option is useful when you want a person to see the result of the calculation directly, while the JSON option is best when it will be handled by Javascript code running within the user agent.
The pico engine performs content negotiation which allows us to add a file extension after the function name (but before arguments in a query string) to indicate how we want the user agent to interpret a string response. Shown here, from top to bottom, with no extension, we see the string, but with a .html extension, the browser interprets that string as a selection box widget, and with a .txt extension, shows it as plain text.
The results of the same KRL function can appear in these three different ways, just by specifying a mime type using a file extension. Notice that the third sample (with mime type text/plain
) is what we would see if we asked to view the source of the second sample (with mime type text/html
).
Using KRL to compute a binary response
Unlike text (whether plain or HTML), an image requires a binary response. The way to prepare a binary response in KRL is to use an Array containing octet values (as decimal numbers between 0 and 255 inclusive). This is supported by the pico engine as of version 0.45.5.
When the pico engine sees a .gif file extension, it will expect such an array, and will present the correct content type (image/gif
) and send the bytes computed by your function as the response body. The user agent can then display the image by interpreting those bytes.
Generating a GIF image
Our goal will be to use KRL to create an image like this one, shown here magnified, consisting of a sequence of digits.
It is called for by a URL like this one. Notice the file extension .gif added to the function name (See the page on Content negotiation with HTTP).
http://localhost:8080/sky/cloud/M8LoyVEVaCZenZvJD8GShG/net.sanbachs.counters/number.gif?n=007&bgcolor=[128,0,128]
The function arguments expected in the query string are: n
, a string of digits, an optional color
, and an optional background color, named bgcolor
. In this case we've let the color default to black, but have specified a background color.
We can ask to see the bytes of the image with a query like this one. Notice that it is exactly like the query for the image, except that there is no file extension, so we see the KRL object
http://localhost:8080/sky/cloud/M8LoyVEVaCZenZvJD8GShG/net.sanbachs.counters/number?n=007&bgcolor=[128,0,128]
with this response (it comes all on one line, but we've added whitespace, line breaks, and ellipses by hand for clarity)
[71,73,70,56,57,97, 30, 0,20,0,128,0,1, 0,0,0, 128,0,128, 44, 0, 0,0,0,10,0,20,0,0,8,47,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,161,64,0,16,...,196,134,1,255,0, 44, 10, 0,0,0,10,0,20,0,0,8,47,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,161,64,0,16,...,196,134,1,255,0, 44, 20, 0,0,0,10,0,20,0,0,8,43,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,97,0,0,16,...,20,233,48,224,0, 59]
The complete documentation of the GIF format can be found at http://www.w3.org/Graphics/GIF/spec-gif89a.txt but we'll spare you the heavy reading by describing the parts of the encoded image.
Line 1 is the file header, which starts with the six ASCII characters for "GIF89a" identifying this stream of bytes as a GIF image in the 1989a version. The next byte, 30, is the overall width of the image, and the last two triplets are the color (#000000
) and background color (#800080
), respectively, in decimal RGB. In this case, the color is black and the background is deep pink.
Line 2 is a separator character (actually a comma). Notice that each of the three digits in the image takes three lines and begins with a separator, then a postion, then the bytes that make up the glyph.
The first zero is inset zero pixels (line 3) and is the glyph for zero (line 4). The second zero is inset ten pixels (line 6). The seven is inset twenty pixels (line 9). The code for these three glyphs are followed by a terminator character (actually a semi-colon) in line 11.
Using KRL to produce an array of octets
We'll now show the KRL code to put together this array, taking a top-down approach. The complete ruleset is on GitHub.
ruleset net.sanbachs.counters { meta { shares __testing, number } global { ... // /sky/cloud/<ECI>/<RID>/number.gif?n=999&color=[R,G,B]&bgcolor=[R,G,B] number = function(n,color,bgcolor) { num = n.as("String").extract(re#(\d)#g).join(""); num_len = num.length() <= 25 => num.length() | 25; h(num_len,val(color),val(bgcolor)) .append(num.substr(0,num_len).split(re##).map(dgt).reduce(flt,[])) .append(T) } } }
We see the function, named number
, shared in line 3, and defined in lines 8-14. In line 8, we see that it expects three arguments named n, color, and bgcolor. The Sky Cloud API allows them to be specified in any order, and they are matched up by name. If we were to call this function from KRL code instead, they would be matched up by order.
In line 9, we ensure that the string of digits given is in fact a KRL String
using the operator as
. Of course it will be when called through the Sky Cloud API, but we are defensive for a possible call directly from KRL code. The operator extract
produces an array of single character strings which are digits, and the operator join
stitches them back together into a string. Any non-digit charcters sent in to us will be elided by this sequence of operators.
Line 10 uses the operator length
to determine the size of the string of (characters verified to be) digits, and we bind this value to the name num_len
, thus ensuring that it will be a number between zero and twenty-five.
Line 11 calls a function named h
(for "header") passing in the number of digits to display, a validated (using a function named val
) color, and a validated background color. This function will return an array of octets. Line 12 will append to this the digits (in a string named num
) limited in length to 25 by the operator substr
, split into an array of single characters (by the operator split
), mapped by the operator map
using a function named dgt
, and finally stitched together by the operator reduce
using a function named flt
. Finally, line 13 will append (again, using the operator append
) the terminator character to the array under construction, which will be returned as the value of the number
function, which ends on line 14.
Notice that we are now obligated to show the definitions of these functions:
h
which must return an array of octets for the header of the GIF89a streamval
which returns an array of three octets for a valid color (ornull
if invalid)dgt
which must return an array of array of octets for all of the digitsflt
which will flatten the outer array into a single array containing all the octets
Validating a user-supplied color
We'll show the val function first
oct = function(v) {0 <= v && v <= 255}; // valid octet val = function(color) { // validate color triplet c_val = color.decode(); c_ok = color && c_val.typeof()=="Array" && c_val.length()==3 && c_val.filter(oct).length()==3; c_ok => c_val | null }
Line 1 is a helper function, named oct
, which returns true
if and only if given a decimal number between 0 and 255 inclusive. Such numbers map easily to octets.
Line 2 begins the definition of the val function, which will be passed a String
of the form "[R, B, G]"
where R, B, and G are each decimal numbers between 0 and 255. Line 3 uses the operator decode
to produce a KRL array (if all goes well), bound to the name c_val. Line 4 begins computing a Boolean to be bound to the name c_ok, using the operator typeof
to ensure that it is indeed an Array
, line 5 ensures that it contains three items (using the operator length
), and line 6 ensures that all three of those items are decimal numbers which will be valid octets.
Line 7 returns either null
if the color is not valid, or it returns the decoded and validated Array which specifies an RGB color.
Flattening an array of arrays
The flt
function
flt = function(a,v){a.append(v)}; // flatten
is a one-liner that is used as the first argument to the operator reduce
(of line 12 of the number
function) to flatten an outer array into a single array containing all of the elements in the contained arrays.
Producing a GIF89a header
The function named h
is presented here
// header fragments given width and colors h = function(len,color,bgcolor) { c = color => color | [0,0,0]; b = bgcolor => bgcolor | [255,255,255]; "GIF89a".split(re##).map(function(v){v.ord()}) .append([len*10]) .append([0,20,0,128,0,1]) .append(c) .append(b) };
The function expects up to three arguments, as shown in line 2. In line 3, we bind the name c
to the second argument, if any, or black otherwise by default. Line 4 similarly binds a background color to the name b
, using white as a default.
Line 5 begins appending the octets which will be returned as a single array in line 10. The header begins with the string "GIF89a" which is split into an array of characters, each of which is mapped into its decimal value by the operator ord
.
To this, line 6 appends a single value, being the width in pixels of the entire image. Line 7 appends some other information (which happens to include a height of 20 pixels.
Finally, lines 8 and 9 append the color and background color triplets, respectively.
Putting together the pieces for one digit
The final function, dgt
, used in line 12 of the number
function, must compute a single array with all of the octest needed to encode the separator, position, and glyph of a single digit.
dgt = function(v,k) {S.append(p(k)).append(D[v])}; // digit fragment
This is also a one-liner, but quite involved. It appends the separator, a position octet for the index of the digit within the string of digits we wish to render, and finally the actual octets for the glyph of that digit. Since this function is used as the argument to a map operator, it will be passed first the value and then the index of each digit.
The pos function and a couple of missing pieces are shown here.
// separator fragment S = [",".ord()]; // position fragments p = function(pos) { [pos*10] }; // terminator fragment T = [";".ord()];
Finally, the glyphs for the ten digits are bound to names as shown here, culminating in the name D
which can be indexed by the digit value. The array of octets for each digit's glyph is not shown in its entirety for readability. You can pick out the constant width (10 pixels) and height (20 pixels) in the first few bytes octets. The remaining octets encode the pixels required to render the particular glyph.
// digit fragments D0 = [0,0,0,10,0,20,0,0,8,47,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,161,64,0,16,...,196,134,1,255,0]; D1 = [0,0,0,10,0,20,0,0,8,44,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,225,64,0,0,...,9,146,96,192,0]; D2 = [0,0,0,10,0,20,0,0,8,45,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,161,64,0,16,...,177,228,194,128,0]; D3 = [0,0,0,10,0,20,0,0,8,44,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,225,64,0,16,...,184,81,97,192,0]; D4 = [0,0,0,10,0,20,0,0,8,44,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,97,65,0,8,...,212,8,114,97,192,0]; D5 = [0,0,0,10,0,20,0,0,8,43,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,161,64,0,16,...,106,4,137,48,224,0]; D6 = [0,0,0,10,0,20,0,0,8,47,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,33,65,0,16,...,227,70,134,1,255,0]; D7 = [0,0,0,10,0,20,0,0,8,43,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,97,0,0,16,...,20,233,48,224,0]; D8 = [0,0,0,10,0,20,0,0,8,48,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,161,64,0,16,...,143,12,3,254,0]; D9 = [0,0,0,10,0,20,0,0,8,48,0,3,8,28,72,176,160,193,131,8,19,42,92,200,176,161,64,0,16,...,135,1,2,254,0]; D = [D0,D1,D2,D3,D4,D5,D6,D7,D8,D9];
The name D
is bound to an array of arrays.
Producing an image from an event
The GIF image in this case has been produced from a function, which the sample ruleset shares. What if we wanted to increment a counter and then return an image of the updated counter value? This involves a mutation of the pico state, and so can only be done in the postlude of a rule triggered by an event.
Experimentally, the engine has been modified to intercept a directive named _gif
and, if it is one of the directives produced during event evaluation, instead of returning the array of directives in a JSON object, the engine will produce a proper GIF image response.
experimental feature in version 0.45.6
This feature is recent, so you will need to do a git pull after updating to version 0.45.5, or wait until the feature has been included in version 0.45.6
Here is a rule which will do what is desribed above.
rule increment_and_display_counter { select when counters hit pre { new_count = ent:counter.defaultsTo(0) + 1; image = number(new_count); } send_directive("_gif",{"content": image}) fired { ent:counter := new_count; } }
In line 4, we precompute the new counter value, binding it to the name new_count
. No worries about a race condition or critical section, because each event arriving at a pico is evaluating in a single thread.
Line 5 calls the function number
to get the array of bytes encoding the new counter value as a GIF image.
The special directive is emitted in line 7, and the new counter value stored in the pico in line 9.
Copyright Picolabs | Licensed under Creative Commons.