Home > JavaScript, Object Pascal, OP4JS, Smart Mobile Studio > ByteRage converted to Smart Pascal and JavaScript

ByteRage converted to Smart Pascal and JavaScript

For a while now I’ve been thinking about re-creating one of my most used and low-level Delphi libraries, ByteRage, to Smart Pascal and JavaScript. But until now it’s not really been possible since JavaScript lacks any notion of memory, bytes or heaven forbid: pointers.

Well the other day I sat down and decided to sort this out, because in Smart Mobile Studio we have managed to re-create, implement and pretty much provide a full infrastructure for cutting edge application development — so why not bytes, steams, buffers and memory allocations? Well keep on reading and I’ll explain why. In detail.

But first; ByteRage has been successfully ported to Smart Mobile Studio ! If you want to know what all the fuzz is about and what ByteRage can do for you – then you can download the Delphi version here: https://code.google.com/p/byterage/source/browse/trunk/brage.pas

Byterage for Smart Pascal

You are probably wondering what this “byterage” library brings to the table. I mean, this is HTML5 after all. Well, first of all the basic concept of a buffer is what allows us to implement streams. But unlike our JavaScript friends we don’t cheat and just stuff your data into a string; pretending to support streams when we in fact just stuff things into strings. No I took on the task from the ground up and did it the proper way, using the browser API for buffers and typed arrays. And for all functions which have no browser-support, I implemented them using loop expansion for maximum speed.

As a result of this endeavour the Smart Mobile Studio RTL has gained a few units these past weeks, most notably System.Interop and SmartCL.Legacy. Which means you don’t have to download byterage for Smart Pascal, It’s now a part of Smart Mobile Studio and will ship with the next update.

Let’s have a peek at what System.Interop and SmartCL.legacy contains. I’ll also talk a bit about a few other important features I am adding to the next update (but rest assured, there is much much more in the update than this!).

System.Interop

This unit (short for system inter-operability) provides the fundamental byterage class: TBRBuffer (another name has been selected for the Smart RTL, but it’s essentially the same code). This is a class that fully manages a JavaScript UInt8Array, which is a typed array typically used for loading or storing binary data. For some odd reason JavaScript programmers seem to shun typed arrays so they are not yet widely used. Which is a bonus for us 🙂

What is unique about byterage buffers is:

  • It can scale without losing it’s content
  • Even large buffers can be serialized safely
  • Has a large number of export methods
  • Encodes and decodes all JavaScript datatypes to and from bytes
  • Provides bit manipulation for entire allocated buffer
  • Can be stored and loaded as JSON
  • .. and much more

First, the scaling: JavaScript UInt8Array’s don’t scale. They are allocated with a fixed size and once they are created their size remains. The only way to thus provide the ability to grow and shrink is to create a new UInt8Array, copy over the data from the previous and switch handle. Which is what my code does, very very quickly using loop expansion.

Next, Serialization. As you may know there is a time-limit involved in JavaScript execution. The idea is that if a JavaScript function call takes longer than X seconds, the JS VM aborts the call with an error. This means that JavaScript is hopeless at processing large heaps of data. To solve this, we process and serialize the buffer in chunks of 0x8000 bytes, allowing you to serialize megabytes of data if you so wish non-stop.

Exporting data from a buffer is imperative. As of writing the following export methods are available:

  • Array of Byte
  • Stream
  • Buffer clone
  • Buffer segment
  • UInt8Array clone
  • UInt8Array segment
  • JSON
  • Raw string

This should more than cover the export needs of getting data out of the buffer on byte-level. And naturally all export methods have a corresponding import function. So getting data back into a buffer is equally simple.

Next, encoding and decoding datatypes. Now this is a tricky one, and if you google the topic for JavaScript you will find many different solutions to the problem. Most of these solutions are either extremely complex, like breaking down a floating-point using math and bit-shifts alone. But they all have one thing in common – namely that they typically export as strings.

My code follows the JavaScript API and simply writes the data to a secondary memory buffer (the size of the datatype itself), then creates two different views into that buffer. One for byte access and one for the datatype in question (for instance a float view or an integer view). This is not only faster than any of the other experiments out there (read: “hacks”), it’s also fully valid and the way JavaScript buffers were designed to operate.

If you are curious about buffers and views you can have a peek at w3buffers.pas in the Smart RTL right now, it’s been a part of Smart Mobile Studio since the beginning. There you will find wrapper classes for the buffer-view mechanisms I have described above.

Then there is a novelty, namely bit manipulation. Now being able to set or get a bit inside a single integer or byte is handy, but it’s rare that it serves any real purpose. It’s only when you can treat a whole buffer as a collection of bits that things get interesting. Typically such “bit-maps” are used to keep track of open slots or re-cycle the use of a fixed set of elements. It can be a sprite-engine for a game which only can display 32 elements on-screen at once, or a database which needs to keep track of what pages are available inside a data-file. Either way, the buffer class provides full bit access on a global (buffer) scale.

Finally there is JSON, which is the de-facto storage format of the web. Buffers can both be stored and loaded in JSON format, and as mentioned I process the buffer in chunks, ensuring that JavaScript doesnt abort the call on large buffers.

Streaming

As you have seen the fundamental buffer class is extremely versatile and provides pretty much everything you can ask for. You can read and write into a buffer at any offset, export it in a myriad of ways and even scale it. The only thing missing from the original Delphi byterage library, is the ability to insert data at any point in the buffer (as opposed to over-writing data). This is very handy for database work where you want to shrink (compact, compress) the database-file on occasion. Microsoft Access is a typical example, where calling “compress” removes empty pages from the file and re-organize records so pages are stored in-sequence inside the file.

So buffers are pretty cool!

On top of the buffer architecture I have implemented streams. Good old Delphi like streams, with TStreamReader and TStreamWriter classes to boot – allowing you to write into an advancing buffer (which is essentially the only difference between a buffer and a stream) just like you would do under FreePascal or Delphi. And the bonus is that the content of the streams on binary level, are identical to those produced with a “real” language. This means (as implied by the name “inter-operation”) that we can now share files between Delphi and Smart. If you have a custom file-format you want to bundle with your HTML5 applications you no longer need to pre-convert them to JSON. This is especially good news for RPC communication, like RemObjects SDK binary protocol which can finally be supported. Although I need to get LZH compression in here first.

Streams in combination with buffers opens up for a whole wealth of interesting communication between components as well. TW3Image can now load images directly from a stream. TW3Canvas now has a ToStream() function, allowing for 1:1 population of an on-screen image.

Streams makes us less dependent on the rules of the browser. For instance, implementing a PNG or JPG reader can now be done more or less exactly as it would be done in Delphi or C#. Although I would not recommend it for speed reasons – it’s now safely within the bounds of what can be achieved.

Perhaps you have a huge 3d mesh file you want to use with OpenGL? Stored in binary format you say? 64bit floating point? Well that’s no longer an issue. Just load the data into a stream and start reading the values, just like you would any other native language.

SmartCL.Legacy

One of the mistakes I did when writing the original Smart Pascal RTL, was to sculpt TW3Canvas according to HTML5. I should in fact have re-created good old TCanvas from Delphi, juiced it up with all the cool new functions HTML5 provides – and made that the de-facto standard canvas.

I hardly think there has been a question so frequently asked as “how to I draw graphics in Smart Mobile Studio”. People coming straight from Delphi or FreePascal/Lazarus are so adjusted to TCanvas that the new HTML5 canvas comes across as alien. So in a perfect world I could go back in time 4 years and do that part over again, but sadly that is not possible.

The legacy unit intends to change all that. It provides a re-implementation of good old TCanvas. So if you find yourself porting over Delphi code or just want to play around with HTML5 and see what Smart Pascal can do, then you will enjoy this unit wholeheartedly. Whenever you need to draw graphics you can now use TBitmap and TCanvas just like under Delphi.

So you get these old gems out of the box:

  • TCanvas
  • TPen
  • TBrush
  • TPaintObject
  • TBitmap

Just to underline: one of the cool things about the HTML5 canvas, is that you can use any image as your pencil. This is also possible under Win32 using Delphi’s TCanvas, but you have to create a custom brush, select that into the device context, then put back the old system brush after your operation (as well as many more steps). So understandably very few programmers bother doing this and opt for a third-party graphics library instead (like Graphics32 or ImageEN).

Well in my TCanvas implementation such technique is not quite so horrendous. Simply assign a picture to Canvas.Brush or Canvas.Pen, both objects exposes a property called BitmapPattern for this exact purpose. Voila! When you use one of the drawing operations your image will be used as the brush. This can be augmented for spectacular effects. Just look at some of those JavaScript 10Kb demo’s and be amazed!

SmartCL.Legacy will no doubt grow, especially now that we have streams under wrap. I will try to port over as much of classical object pascal as I can, one class at a time.

Font measurement

You may not believe this but HTML5, neither the canvas nor the DOM, comes with proper font measurement! You have one rudimentary function, but having to create a graphics context and a canvas object just to measure the size of a text is ridicules. So I decided to do something about that as well.

For those of you that follow my blog I’m sure you remember the “Fonts once and for all” post a while back? Well in short I created the means of measuring fonts, which was one hell of a challenge. But now that the QTX library is being merged with the Smart RTL you will be able to enjoy that as well.

SmartCL.Fonts have been given a full overhaul and now includes a class named TW3FontDetector which takes care of business.

Note: You don’t create an instance of this class to measure text. The class indexes the browser’s fonts and families which takes a few milliseconds, so you don’t want to create an instance every time you need this. Instead, you simply access the unit-function “W3FontDetector” which returns an instance which is automatically created when your application starts.

So for measuring a piece of text you would write something like:

 var mSize:TW3TextMetric;
 mSize:=W3FontDetector.MeasureText("verdana",12,"this is my text here");

Now here comes the cool part: Access to font measurement has now been built into TW3CustomControl! When you use the methods in TW3CustomControl (which means the font measurement stuff is available almost anywhere) you don’t have to populate the font-name and pixel-size, the component will find out what font is selected into the element and it’s size automatically.
So finally calculating content size, element height and so on for your custom-controls is a walk in the park.

Which is another thing TW3FontDetector does. JavaScript and the DOM doesn’t have a clear-cut way of figuring out what font is selected into a DIV. In fact, several fonts can be selected into the same element – and the browser picks one of them when it builds its calculated-style (which is the stylesheet the browser actually uses when rendering). Well, if you need to figure out just what font is selected into a control, just call W3FontDetector.getFontInfo() and you get the info you need.

Peek-a-code

Here is a peek-preview of the interop unit’s interface:

type

  (* Byte should really be implemented on compiler level, but until we
     have that under wraps, we introduce it here as an integer type *)
  Byte = Integer;
  TByteArray = Array of Byte;

  (* TW3ByteHelper provides some handy helper functions for working with
     bytes. Turning a byte-value into a character is a nice method,
     and also Ensure() which claps the byte-value within 0..255. *)
  TW3ByteHelper = Helper for Byte
    function  ToBitMask:String;
    function  ToHex:String;
    function  Ensure:Byte;
  end;

  (* Forward declarations *)
  TStreamWriter   = Class;
  TStreamReader   = Class;
  TMemoryStream   = Class;
  TStream         = Class;
  TW3MemoryBuffer = Class;

  (* TW3MemoryBuffer:
     This class introduces a dynamically allocated UInt8Array which is
     completely maintained.

     * All operations are optimized with loop-expansion for maximum speed.
     * Buffer can grow without losing content (in-place duplication)
     * Buffer can shrink without loss of content (in-place duplication)
     * Append methods, also from another buffer or a free-standing THandle

     The bytebuffer is Smart Mobile Studio's absolute lowest level of
     data, as it represents an array of "real" bytes.
     Such data-buffers are common throughout HTML5, from the canvas pixel-data
     to the content of an image-tag.

     Being able to work with low-level data under JavaScript gives us an
     edge, since very few programmers tend to venture into this region
     of JavaScript. It opens up for many posebilities:
      * Zip support
      * Encryption
      * Binary transport of data between objects
      * .. Much, much more *)

  EW3MemoryBuffer = Class(EW3Exception);

  TW3BufferHexDumpOptions = set of (doSign,doZeroPad);

  TW3MemoryBuffer = Class(TObject)
  private
    Fhandle:  THandle;
  protected
    function  getHandle:THandle;
    function  getSize:Integer;virtual;
    function  getByte(const Index:Integer):Byte;
    procedure setByte(const Index:Integer;const Value:Byte);
    function  OffsetInRange(Offset:Integer):Boolean;
  public
    property  Handle:THandle read getHandle;
    Property  Size:Integer read getSize;
    Property  BitCount:Integer read (getSize * 8);

    Property  Bytes[const index:Integer]:Byte
              read getByte write setByte;default;

    Procedure Grow(const ByAmount:Integer);
    procedure Shrink(const ByAmount:Integer);

    Procedure Append(const Bytes:Array of Byte);overload;
    Procedure Append(const Text:String);overload;
    Procedure Append(Const Buffer:TW3MemoryBuffer;
              ReleaseBuffer:Boolean);overload;
    Procedure Append(Const Value:Float);overload;
    Procedure Append(Const Raw:THandle);overload;

    Procedure CopyFrom(Buffer:TW3MemoryBuffer;
              Offset:Integer;ByteLen:Integer);overload;

    Procedure CopyFrom(Raw:THandle;
              Offset:Integer;ByteLen:Integer);overload;

    function  &ExportBuffer(Offset:Integer;
              ByteLen:Integer):TW3MemoryBuffer;overload;

    function  &ExportStream(Offset:Integer;
              ByteLen:Integer):TStream;overload;

    Procedure Write(Offset:Integer;
              Data:TW3MemoryBuffer);

    Procedure Write(Offset:Integer;Data:THandle);overload;
    Procedure Write(Offset:Integer;Data:String);overload;
    Procedure Write(Offset:Integer;Data:Integer);overload;
    procedure Write(Offset:Integer;Data:Float);Overload;
    procedure Write(Offset:Integer;Data:TByteArray);Overload;
    procedure Write(Offset:Integer;Data:Boolean);overload;

    function  ReadBool(Offset:Integer):Boolean;Overload;
    function  ReadFloat(Offset:Integer):Float;overload;
    function  ReadInt(Offset:Integer):Integer;overload;
    function  ReadStr(Offset:Integer;ByteLen:Integer):String;overload;
    function  ReadBytes(Offset:Integer;ByteLen:Integer):TByteArray;overload;

    function  Clone:TW3MemoryBuffer;

    function  ToBase64:String;
    function  ToString:String;
    function  ToRaw:THandle;
    function  ToBytes:TByteArray;
    function  ToHexDump(BytesPerRow:Integer;
              Options:TW3BufferHexDumpOptions):String;
    function  ToStream:TStream;

    procedure Allocate(const ByteSize:Integer);overload;
    Procedure Allocate(const Values:Array of Byte);overload;
    procedure Release;

    procedure setBit(bitIndex:Integer;const value:Boolean);
    function  getBit(bitIndex:Integer):Boolean;

    class function  StrToBase64(Value:String):String;
    class function  Base64ToStr(Value:String):String;

    class function  ByteToChar(Const Value:Byte):String;
    class function  CharToByte(const Value:String):Byte;


    Constructor Create(aHandle:THandle);virtual;
    Destructor  Destroy;Override;
  end;

  TW3StreamOrientation = (soFromStart,soFromCurrent);

  IStreamAccess = Interface
    function  getBuffer:TW3MemoryBuffer;
    function  getPosition:Integer;
    procedure setPosition(Offset:Integer);
    procedure Advance(Bytes:Integer);
  end;

  TStream = Class(TObject,IStreamAccess)
  protected
    function  getBuffer:TW3MemoryBuffer;virtual;abstract;
    function  getHandle:THandle;virtual;abstract;
    function  getSize:Integer;virtual;abstract;
    function  getPosition:Integer;virtual;abstract;
    procedure setPosition(Offset:Integer);virtual;abstract;
    procedure Advance(Bytes:Integer);virtual;abstract;
  public
    Property  Handle:THandle read getHandle;
    Property  Size:Integer read getSize;
    Property  Position:Integer read getPosition write setPosition;

    function  CreateReader:TStreamReader;
    function  CreateWriter:TStreamWriter;

    function  ToBuffer:TW3MemoryBuffer;

    function  Read(ByteLen:Integer):TByteArray;virtual;abstract;
    procedure Write(Const Data:TByteArray);virtual;abstract;
    function  CopyFrom(source:TStream;ByteLen:Integer):Integer;
    procedure Seek(aDistance:Integer;Orientation:TW3StreamOrientation);
  end;

  TMemoryStream = Class(TStream)
  private
    FBuffer:      TW3memoryBuffer;
    FPosition:    Integer;
  protected
    procedure     Advance(Bytes:Integer);override;
    function      getBuffer:TW3MemoryBuffer;override;
    function      getHandle:THandle;override;
    function      getSize:Integer;override;
    function      getPosition:Integer;override;
    procedure     setPosition(Offset:Integer);override;
  public
    function      Read(ByteLen:Integer):TByteArray;override;
    procedure     Write(Const Data:TByteArray);override;
    Constructor   Create;virtual;
    Destructor    Destroy;Override;
  end;

  TStreamWriter = Class(TObject)
  private
    FStream:    TStream;
    FAccess:    IStreamAccess;
  protected
    property    Stream:TStream read FStream;
    Property    Access:IStreamAccess read FAccess;
  public
    procedure   WriteInteger(Const Value:Integer);virtual;
    procedure   WriteString(Const Value:String);virtual;
    procedure   WriteBoolean(Const Value:Boolean);virtual;
    Procedure   WriteFloat(Const Value:Float);virtual;
    Constructor Create(AStream:TStream);virtual;
  end;

  TStreamReader = Class(TObject)
  private
    FStream:    TStream;
    FAccess:    IStreamAccess;
  protected
    property    Stream:TStream read FStream;
    Property    Access:IStreamAccess read FAccess;
  public
    function    ReadInteger:Integer;virtual;
    function    ReadString(aLength:Integer):String;virtual;
    function    ReadBoolean:Boolean;virtual;
    function    ReadFloat:Float;virtual;
    Constructor Create(AStream:TStream);virtual;
  end;

And what would the world be without a preview of the legacy unit:

  (* This unit provides legacy support for Delphi/FreePascal TCanvas
     class. Although highly extended.


     Notable-Changes:
      -- Both Brush and Pen support bitmap patters for drawing
      -- Stroke must be called to realize graphics operations
  *)

  TBrushStyle = (
    bsSolid,
    bsClear
  );

  TPenStyle = (
    psSolid,
    psClear
  );

  TCanvas = Class;

  TCanvasPatternRepeatMode = (prRepeat,prRepeatX,prRepeatY,prNoRepeat);

  TPaintObject = Class(TW3OwnedObject)
  private
    FBitmap:    TW3Image;
    FPattern:   JCanvasPattern;
    FRepeat:    TCanvasPatternRepeatMode;
  protected
    function    getFillStyle:THandle;virtual;
    procedure   setRepeat(Value:TCanvasPatternRepeatMode);virtual;
  public
    Property    BitmapPattern:TCanvasPatternRepeatMode
                read FRepeat write setRepeat;
    Property    Bitmap:TW3Image read FBitmap;
    property    Color:TColor;
    Procedure   Clear;virtual;
    Constructor Create(AOwner:TCanvas);reintroduce;virtual;
    Destructor  Destroy;Override;
  end;

  TBrush  = Class(TPaintObject)
  public
    Property    Style:TBrushStyle;
    Constructor Create(AOwner:TCanvas);override;
  end;

  TPen = Class(TPaintObject)
  public
    Property    Mode:TPenStyle;
    Property    Width:Integer;
    Constructor Create(AOwner:TCanvas);override;
  end;

  TCanvas = Class(TObject)
  private
    FContext:   TW3CustomGraphicContext;
    FBrush:     TBrush;
    FPen:       TPen;
    FDC:        JCanvasRenderingContext2D;
    FTempPxl:   TW3ImageData;
    FPos:       TPoint;
    FBounds:    TRect;
    FWidth:     Integer;
    FHeight:    Integer;
  protected
    function    getPixelDataBuffer:TW3ImageData;
    function    getValidPoint(const dx,dy:Integer):Boolean;

    function    getPixel(const x,y:Integer):TColor;
    procedure   setPixel(const x,y:Integer;const value:TColor);
    function    getPixelData(const x,y:Integer):TW3RGBA;
    procedure   setPixelData(const x,y:Integer;const value:TW3RGBA);
    function    getR(const x,y:Integer):Byte;
    function    getG(const x,y:Integer):Byte;
    function    getB(const x,y:Integer):Byte;
    function    getA(const x,y:Integer):Byte;
  public
    Property    Width:Integer read FWidth;
    property    Height:Integer read FHeight;
    Property    BoundsRect:TRect read FBounds;
    Property    Context:TW3CustomGraphicContext read FContext;
    Property    Brush:TBrush read FBrush;
    property    Pen:TPen read FPen;

    Property    Pixels[const x,y:Integer]:TColor
                read getPixel write setPixel;

    Property    R[const x,y:Integer]:Byte read getR;
    Property    G[const x,y:Integer]:Byte read getG;
    property    B[const x,y:Integer]:Byte read getB;
    property    A[const x,y:Integer]:Byte read getA;

    Property    PixelData[const x,y:Integer]:TW3RGBA
                read getPixelData write setPixelData;

    Procedure   MoveTo(const dx,dy:Integer);
    procedure   LineTo(const dx,dy:Integer);

    procedure   Line(const x1,y1,x2,y2:Integer);overload;
    procedure   Line(const x1,y1,x2,y2:Float);overload;

    Procedure   Hline(const x1,y1,width:Integer);
    Procedure   VLine(const x1,y1,Height:Integer);

    Procedure   Rectangle(const aRect:TRect);overload;
    procedure   Rectangle(const x1,y1,x2,y2:Integer);overload;

    Procedure   FillRect(const aRect:TRect);overload;
    procedure   FillRect(const x1,y1,x2,y2:Integer);overload;

    function    ToDataURL(aMimeType: String): String;
    function    ToImageData: TW3ImageData;
    function    ToBytes: JUint8Array;
    function    ToBuffer:TW3MemoryBuffer;
    function    ToStream:TStream;

    Procedure   Stroke;

    Constructor Create(const aContext:TW3CustomGraphicContext);
    Destructor  Destroy;Override;
  end;

  TPixelFormat =(pf32Bit);

  TBitmap = Class(TObject)
  private
    FCanvas:    TCanvas;
    FContext:   TW3GraphicContext;
    FWidth:     Integer;
    FHeight:    Integer;
    FOnLost:    TNotifyEvent;
    FOnGained:  TNotifyEvent;
  protected
    function    getWidth:Integer;
    procedure   setWidth(Value:Integer);
    function    getHeight:Integer;
    procedure   setHeight(Value:Integer);
    Procedure   ReCreateBitmap;
    function    getDC:JCanvasRenderingContext2D;
  public
    Property    Empty:Boolean read (FCanvas<>NIL);
    Property    DC:JCanvasRenderingContext2D read getDC;
    Property    Canvas:TCanvas read FCanvas;
    Procedure   Allocate(aWidth,aHeight:Integer);
    Procedure   Release;
    Constructor Create;virtual;
    Destructor  Destroy;Override;
  published
    Property    PixelFormat:TPixelFormat;
    Property    Width:Integer read getWidth write setWidth;
    property    Height:Integer read getHeight write setHeight;
    Property    OnBitmapAllocated:TNotifyEvent read FOnGained write FOnGained;
    Property    OnBitmapReleased:TNotifyEvent read FOnLost write FOnLost;
  end;

Enjoy!

Advertisements
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: