Out of idle curiosity, I wanted to know if it is possible to create bitmaps (BMPs) with pure PL/SQL alone. And it is! The following package is the result of this endeavor. 
The procedure init must be called first in order to use the package and create a bitmap with the specified width and height. The three other parameters r, g and b must be between 0 and 255 and specify the rgb-value of the bitmap's background. Obviously, these three parameters can be omitted in which case they default to 0 which makes a black background.
The three parameters r, g and b are present in the other procedures as well and have the same meaning as in init.
PixelAt plots a simple pixel at the coordinates x/y. Line draws a line from xFrom/yFrom to xTo/yTo. Circle draws a circle with the specified radius around x/y. Finally, the function AsBlob returns the drawn bitmap as a blob.
create package bmp as
procedure Init (width pls_integer,
height pls_integer,
r pls_integer := 0,
g pls_integer := 0,
b pls_integer := 0);
procedure PixelAt(x pls_integer,
y pls_integer,
r pls_integer,
g pls_integer,
b pls_integer);
procedure Line (xFrom pls_integer,
yFrom pls_integer,
xTo pls_integer,
yTo pls_integer,
r pls_integer,
g pls_integer,
b pls_integer);
procedure Circle (x pls_integer,
y pls_integer,
radius pls_integer,
r pls_integer,
g pls_integer,
b pls_integer);
function AsBlob return blob;
end bmp;
/
create package body bmp as headersize constant pls_integer := 14; infosize constant pls_integer := 40; offset constant pls_integer := infosize + headersize; bmpWidth pls_integer; bmpHeight pls_integer; line_len pls_integer; filesize pls_integer; output_file utl_file.file_type; the_bits blob; function unsigned_short(s in pls_integer) return raw is ret raw(2); v pls_integer; r pls_integer; begin v := trunc (s/256 ); r := s-v; ret := utl_raw.cast_to_raw(chr(v)); v := trunc (s ); r := s-v; ret := utl_raw.cast_to_raw(chr(v)) || ret; return ret; end unsigned_short; function unsigned_rgb(r in pls_integer, g in pls_integer, b in pls_integer) return raw is ret raw(3); begin ret := utl_raw.cast_to_raw(chr(r)); ret := utl_raw.cast_to_raw(chr(g)) || ret; ret := utl_raw.cast_to_raw(chr(b)) || ret; return ret; end unsigned_rgb; function unsigned_int(i in pls_integer) return raw is /* i = ret(4) * 256*256*256 + ret(3) * 256*256 + ret(2) * 256 + ret(1) */ ret raw(4); v pls_integer; r pls_integer; begin v := trunc (i/256/256/256); r := i-v; ret := utl_raw.cast_to_raw(chr(v)); v := trunc (i/256/256 ); r := i-v; ret := utl_raw.cast_to_raw(chr(v)) || ret; v := trunc (i/256 ); r := i-v; ret := utl_raw.cast_to_raw(chr(v)) || ret; v := trunc (i ); r := i-v; ret := utl_raw.cast_to_raw(chr(v)) || ret; return ret; end unsigned_int; procedure WriteHeader is imagesize pls_integer; begin imagesize := bmpHeight * line_len; filesize := imagesize + offset; -- Header dbms_lob.append(the_bits, utl_raw.cast_to_raw('BM')); -- Pos 0 dbms_lob.append(the_bits, unsigned_int(filesize)); -- Pos 2 dbms_lob.append(the_bits, unsigned_short(0)); -- Pos 6, reserved 1 dbms_lob.append(the_bits, unsigned_short(0)); -- Pos 8, reserved 2 dbms_lob.append(the_bits, unsigned_int(offset)); -- Pos 10, offset to image -- Information dbms_lob.append(the_bits, unsigned_int(infosize)); -- Pos 14 dbms_lob.append(the_bits, unsigned_int(bmpWidth)); -- Pos 18 dbms_lob.append(the_bits, unsigned_int(bmpHeight)); -- Pos 22 dbms_lob.append(the_bits, unsigned_short( 1)); -- Pos 26, planes dbms_lob.append(the_bits, unsigned_short(24)); -- Pos 28, bits per pixel dbms_lob.append(the_bits, unsigned_int ( 0)); -- Pos 30, no compression dbms_lob.append(the_bits, unsigned_int (imagesize)); -- Pos 34 dbms_lob.append(the_bits, unsigned_int (7874)); -- Pos 38, x pixels/meter (???) dbms_lob.append(the_bits, unsigned_int (7874)); -- Pos 42, y pixels/meter (???) dbms_lob.append(the_bits, unsigned_int (0)); -- Pos 46, Number of colors dbms_lob.append(the_bits, unsigned_int (0)); -- Pos 50, Important colors end WriteHeader; procedure Init(width pls_integer, height pls_integer, r in pls_integer, g in pls_integer, b in pls_integer) is bgColor raw(3); begin bmpWidth := width; bmpHeight := height; -- line_len must be divisible by 4 line_len := 4*ceil(3*bmpWidth/4); bgColor := unsigned_rgb(r,g,b); the_bits := empty_blob(); dbms_lob.createTemporary(the_bits, true); dbms_lob.open(the_bits, dbms_lob.lob_readwrite); WriteHeader; for x in 0 .. bmpWidth-1 loop for Y in 0 .. bmpHeight-1 loop dbms_lob.append(the_bits, bgColor); end loop; end loop; end Init; function AsBlob return blob is begin return the_bits; end AsBlob; procedure PixelAt(x in pls_integer, y in pls_integer, rgb in raw) is begin if x < 0 or y < 0 or x >= bmpWidth or y >= bmpHeight then return; end if; dbms_lob.write(the_bits, 3, 1+offset+ (bmpHeight-y-1)*line_len + x*3, rgb); end PixelAt; procedure PixelAt(x pls_integer, y pls_integer, r pls_integer, g pls_integer, b pls_integer) is rgb raw(3); begin rgb := unsigned_rgb(r,g,b); PixelAt(x, y, rgb); end; procedure Line (xFrom pls_integer, yFrom pls_integer, xTo pls_integer, yTo pls_integer, r pls_integer, g pls_integer, b pls_integer) is rgb raw(3); c pls_integer; m pls_integer; x pls_integer; y pls_integer; D pls_integer; HX pls_integer; HY pls_integer; xInc pls_integer; yInc pls_integer; begin rgb := unsigned_rgb(r,g,b); x := xFrom; y := yFrom; D := 0; HX := xTo - xfrom; HY := yTo - yfrom; xInc := 1; yInc := 1; if HX < 0 then xInc := -1; HX := -HX; end if; if HY < 0 then yInc := -1; HY := -HY; end if; if HY <= HX then c := 2*HX; M := 2*HY; loop PixelAt(x, y, rgb); exit when x = xTo; x := x + xInc; D := D + M; if D > HX then y := y+yInc; D := D-c; end if; end loop; else c := 2*HY; M := 2*HX; loop PixelAt(x, y, rgb); exit when y = yTo; y := y + yInc; D := D + M; if D > HY then x := x + xInc; D := D - c; end if; end loop; end if; end Line; procedure Circle_(x pls_integer, y pls_integer, xx pls_integer, yy pls_integer, rgb raw) is begin if xx = 0 then PixelAt(x , y + yy , rgb); PixelAt(x , y - yy , rgb); PixelAt(x + yy, y , rgb); PixelAt(x - yy, y , rgb); elsif xx = yy then PixelAt(x + xx , y + yy , rgb); PixelAt(x - xx , y + yy , rgb); PixelAt(x + xx , y - yy , rgb); PixelAt(x - xx , y - yy , rgb); elsif xx < yy then PixelAt(x + xx , y + yy , rgb); PixelAt(x - xx , y + yy , rgb); PixelAt(x + xx , y - yy , rgb); PixelAt(x - xx , y - yy , rgb); PixelAt(x + yy , y + xx , rgb); PixelAt(x - yy , y + xx , rgb); PixelAt(x + yy , y - xx , rgb); PixelAt(x - yy , y - xx , rgb); end if; end Circle_; procedure Circle (x pls_integer, y pls_integer, radius pls_integer, r pls_integer, g pls_integer, b pls_integer) is xx pls_integer := 0; yy pls_integer := radius; pp pls_integer := (5-radius*4)/4; rgb raw(3); begin rgb := unsigned_rgb(r,g,b); Circle_(x, y, xx, yy, rgb); while xx < yy loop xx := xx+1; if pp < 0 then pp := pp + 2*xx+1; else yy := yy - 1; pp := pp + 2*(xx-yy) + 1; end if; Circle_(x, y, xx, yy, rgb); end loop; end Circle; end bmp; /
Testing the package
I am going to save the bitmap returned from AsBlob into a file. Therefore, I need to create a directory object that points to the directory in which I want to save the bitmap:
create directory temp_dir as 'c:\temp';
This anonymous block actually creates the bitmap. First, I set the width, height and background color with Init, then I draw a rectangle around the bitmap followed by 36 lines and lastly a circle.
The blob I get with AsBlob is saved using blob_wrapper which is a package which I have presented two days ago.
begin
bmp.Init(300, 200, 238, 238, 204);
bmp.Line( 0, 0, 0, 199, 66, 166, 194);
bmp.Line( 0, 199, 299, 199, 66, 166, 194);
bmp.Line(299, 199, 299, 0, 66, 166, 194);
bmp.Line(299, 0, 0, 0, 66, 166, 194);
for i in 1 .. 36 loop
bmp.Line(150, 100, 150+sin(i/18*3.141)* 80, 100+cos(i/18*3.141)*80, 55, 0, 180);
end loop;
bmp.Circle(150, 100, 80, 255, 0, 0);
blob_wrapper.to_file('TEMP_DIR', 'test.bmp', bmp.AsBlob);
end;
/
This is the created bitmap:

Cleaning up:
drop directory temp_dir;
Update January 9, 2006
Maxim Demenko sends me an email regarding the function unsigned_int:
I got an ORA-06502 on a multibyte database by last assignment - the previous 3
filled ret variable with 3 bytes, so if your argument is more than 255, than
you get in multibyte database with utl_raw.cast_to_raw(chr(v)) 2 or more bytes,
for those where are no more enough space in the 4 byte variable.
If i may provide a suggestion, it can be also implemented as follows, to avoid
this error ( i used number for IN parameter to not lose the highest byte of an
pls_integer, for example pls_integer 256*256*256*256-1 gives me 'FFFFFF7F'
whereas number 256*256*256*256-1 gives me 'FFFFFFFF', but , maybe , in your
package it is irrelevant , if you not deal with such big numbers )
function unsigned_int(i number) return raw is
format constant varchar2(8) := lpad('X',8,'X');
ret raw(4);
begin
return utl_raw.reverse(lpad(trim(to_char(i, format)), 8, '0'));
end unsigned_int;
Once again, thank you for an excellent Oracle Resource and your work.
Best regards
Maxim Demenko
I want to thank Maxim for sharing his feedback here.