René Nyffenegger's collection of things on the web
René Nyffenegger on Oracle - Most wanted - Feedback -
 

November 22, 2005: On creating bitmaps with pure PL/SQL only

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:
/blog/2005/november/test.bmp
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.

Links

The following links proved useful to create this package:

More on Oracle

This is an on Oracle article. The most current articles of this series can be found here.