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!
You must be logged in to post a comment.