Home > CaseBook, CSS, Object Pascal, OP4JS, QTX, Smart Mobile Studio > SMS fonts, once and for all, part 2

SMS fonts, once and for all, part 2

Typographic measurements

Typographic measurements

In the previous article on this topic, we covered the ported “font detection” JS library which is now a part of QTXLibrary (just update your SVN repository and you have it. The class can be found in the file qtxutils.pas).

As mentioned yesterday, detecting if the browser supports a font doesn’t give us any real tangible information about which font has actually been selected by the browser for an element. A calculated style is, after all, calculated and managed by the browser – not the stylesheet. A stylesheet does nothing more than tell the browser what it would like to have, but it’s the browser that calls all the shots and will override your styles (naturally, if a font is not on your system the browser cant magically invent it) if it has to.

But as luck would have it, being able to detect if a font is installed just what we need to figure out if it’s applied to an element!

In short, here is what we do to figure out what font the browser has selected for an element:

  • Read the font-family string for the element
  • Split the string into an array, separated by “,”
  • Iterate through the array, detecting if each item is installed
  • Select the first valid font, since that’s the rule defined by WC3

We should also try to expand this in the future by adding support for:

  • Px vs. pt calculations (using body font as % basis for points)
  • Percent support (same as above, but percentage of defined document size)
  • Inherited keyword support
  • Initial keyword support

While I dont intend to deal with the latter list right now, we at least have a game plan! But.. we are also going to take this one step further and introduce something very valuable, namely content pre-calculation.

Content pre-calculation and measurement

Imagine you have to code a news-app for Android and iOS. Just like Facebook and twitter you have to display a piece of a message (or indeed, the whole message if it’s reasonable in size) in a scrollable list. Just like I do in the CaseBook demo app. It’s also a common task when working on e-books or anything which requires dynamic display of content.

Font metrics is more complex than you think

Font metrics is more complex than you think

The question of “how many pixels will my text occupy” will arise in your mind rather quickly. And like all problems dealing with content rendering (be it under Delphi or Smart Mobile), what you already know are the only factors you can use to truly pre-calculate the correct end result.

In our case we have only one fixed constant, namely the width of the display (depends only on how the user holds the mobile device), while the height factor is flexible (since we have a scrolling display). So the height is unknown and determined completely by the length of the text, the size of the font and ultimately how the browser breaks the content down within the boundaries of the width.

So once we are able to determine the font used by a HTML element and it’s size, being able to pre-calculate what a text message (or a compound message which may include images and other HTML content) looks like becomes the logical next step. And a very valuable and important one, since we are dealing with HTML and browser based app’s after all.

Well, I’m not going to hold you in suspense any longer, here is the final result (below). Again, this code is stored on Google Code, so just update your local SVN repository copy and that’s it. If you install for the first time, remember to check-out the SVN trunk under your SMS/Libraries folder – otherwise the Smart Mobile Studio IDE wont be able to find the files.

Well, here it is – the evolved version of Font Detector library (JS):

type

  TQTXFontInfo = Record
    fiName: String;
    fiSize: Integer;
    function  toString:String;
  End;

  TQTXFontDetector = Class(TObject)
  private
    FBaseFonts:     array of string;
    FtestString:    String = "mmmmmmmmmmlli";
    FtestSize:      String = '72px';
    Fh:             THandle;
    Fs:             THandle;
    FdefaultWidth:  Variant;
    FdefaultHeight: Variant;
  public
    function    Detect(aFont:String):Boolean;

    function    MeasureText(aFontInfo:TQTXFontInfo;
                aContent:String):TQTXTextMetric;overload;

    function    MeasureText(aFontInfo:TQTXFontInfo;
                aFixedWidth:Integer;
                aContent:String):TQTXTextMetric;overload;

    function    MeasureText(aFontName:String;aFontSize:Integer;
                aContent:String):TQTXTextMetric;overload;

    function    MeasureText(aFontName:String;aFontSize:Integer;
                aFixedWidth:Integer;
                aContent:String):TQTXTextMetric;overload;

    function    getFontInfo(const aHandle:THandle):TQTXFontInfo;

    Constructor Create;virtual;
  End;

//############################################################################
// TQTXFontInfo
//############################################################################

function TQTXFontInfo.toString:String;
begin
  result:=Format('%s %dpx',[fiName,fiSize]);
end;

//############################################################################
// TQTXFontDetector
//############################################################################

Constructor TQTXFontDetector.Create;
var
  x:  Integer;
begin
  inherited Create;
  FBaseFonts.add('monospace');
  FBaseFonts.add('sans-serif');
  FBaseFonts.add('serif');

  Fh:=browserApi.document.body;

  Fs:=browserApi.document.createElement("span");
  Fs.style.fontSize:=FtestSize;
  Fs.innerHTML := FtestString;
  FDefaultWidth:=TVariant.createObject;
  FDefaultHeight:=TVariant.createObject;

  if FBaseFonts.Count>0 then
  for x:=FBaseFonts.low to FBaseFonts.high do
  begin
    Fs.style.fontFamily := FbaseFonts[x];
    Fh.appendChild(Fs);
    FdefaultWidth[FbaseFonts[x]]  :=  Fs.offsetWidth;
    FdefaultHeight[FbaseFonts[x]] :=  Fs.offsetHeight;
    Fh.removeChild(Fs);
  end;
end;

function TQTXFontDetector.getFontInfo(const aHandle:THandle):TQTXFontInfo;
var
  mName:  String;
  mSize:  Integer;
  mData:  Array of string;
  x:  Integer;
Begin
  result.fiSize:=-1;
  if aHandle.valid then
  begin
    mName:=w3_getStyleAsStr(aHandle,'font-family');
    mSize:=w3_getStyleAsInt(aHandle,'font-size');

    if length(mName)>0 then
    begin
      asm
        @mData = (@mName).split(",");
      end;
      if mData.Length>0 then
      Begin
        for x:=mData.low to mData.high do
        begin
          if Detect(mData[x]) then
          begin
            result.fiName:=mData[x];
            result.fiSize:=mSize;
            break;
          end;
        end;
      end;
    end;
  end;
end;

function TQTXFontDetector.MeasureText(aFontInfo:TQTXFontInfo;
         aFixedWidth:Integer;
         aContent:String):TQTXTextMetric;
Begin
  result:=MeasureText(aFontInfo.fiName,aFontInfo.fiSize,aFixedWidth,aContent);
end;

function TQTXFontDetector.MeasureText(aFontInfo:TQTXFontInfo;
         aContent:String):TQTXTextMetric;
Begin
  result:=MeasureText(aFontInfo.fiName,aFontInfo.fiSize,aContent);
end;

function TQTXFontDetector.MeasureText(aFontName:String;aFontSize:Integer;
         aFixedWidth:Integer;
         aContent:String):TQTXTextMetric;
var
  mElement: THandle;
Begin
  if Detect(aFontName) then
  begin
    aContent:=trim(aContent);
    if length(aContent)>0 then
    begin
      mElement:=BrowserAPi.document.createElement("div");
      if (mElement) then
      begin
        mElement.style['font-family']:=aFontName;
        mElement.style['font-size']:=TInteger.toPxStr(aFontSize);
        mElement.style['overflow']:='scroll';

        mElement.style.maxWidth:=TInteger.toPxStr(aFixedWidth);
        mElement.style.width:=TInteger.toPxStr(aFixedWidth);
        mElement.style.height:='10000px';

        mElement.innerHTML := aContent;
        Fh.appendChild(mElement);

        mElement.style.width:="4px";
        mElement.style.height:="4px";

        result.tmWidth:=mElement.scrollWidth;
        result.tmHeight:=mElement.scrollHeight;
        Fh.removeChild(mElement);

      end;
    end;
  end;
end;

function TQTXFontDetector.MeasureText(aFontName:String;aFontSize:Integer;
         aContent:String):TQTXTextMetric;
var
  mElement: THandle;
Begin
  if Detect(aFontName) then
  begin
    aContent:=trim(aContent);
    if length(aContent)>0 then
    begin
      mElement:=BrowserAPi.document.createElement("div");
      if (mElement) then
      begin
        mElement.style['font-family']:=aFontName;
        mElement.style['font-size']:=TInteger.toPxStr(aFontSize);
        mElement.style['overflow']:='scroll';

        mElement.style['display']:='inline-block';
        mElement.style['white-space']:='nowrap';

        mElement.style.width:='10000px';
        mElement.style.height:='10000px';

        mElement.innerHTML := aContent;
        Fh.appendChild(mElement);

        mElement.style.width:="4px";
        mElement.style.height:="4px";

        result.tmWidth:=mElement.scrollWidth;
        result.tmHeight:=mElement.scrollHeight;
        Fh.removeChild(mElement);

      end;
    end;
  end;
end;

function TQTXFontDetector.Detect(aFont:String):Boolean;
var
  x:  Integer;
Begin
  aFont:=trim(aFont);
  if aFont.Length>0 then
  Begin
    if FBaseFonts.Count>0 then
    for x:=FBaseFonts.low to FBaseFonts.high do
    begin
      Fs.style.fontFamily:=aFont + ',' + FbaseFonts[x];
      Fh.appendChild(Fs);
      result:= (Fs.offsetWidth  <> FdefaultWidth[FBaseFonts[x]])
          and  (Fs.offsetHeight <> FdefaultHeight[FBaseFonts[x]]);
      Fh.removeChild(Fs);
      if result then
      break;
    end;
  end;
end;

Now things are much easier to work with! In fact, we can now measure the width and height of a message by simply doing this:

Procedure TForm1.Test;
var
  mObj:TQTXFontDetector;
  mInfo: TQTXFontInfo;
Begin
  mObj:=TQTXFontDetector.Create;
  mInfo:=mObj.MeasureText(mObj.getFontInfo(self.handle),
         'this is cool!<br>And this is even cooler');
  showmessage(mInfo.toString);
End;

Now we can measure any HTML segment no matter what it is, be it a compound string which contains images and text combined – even including fancy css fonts like Font Awesome. This gives us a huge advantage over those poor guys writing JavaScript by hand.

Isolating use

Like all great snippets, automating and isolating it’s use is a must. At the moment this piece of gold is safely tucked away in the QTXLibrary – but I plan to include this after more rigourous testing and, if time permits for another hotfix, support for inherited and initial keywords – which is much more complex than it sounds.

The initial and inherited CSS keywords means that we have to recursively query the parent and keep on going until we find a valid font declaration. This also means that I have to carefully sculpt the code to minimize expensive round-trips (so you dont want to put inherited font use on an element under a time critical segment of your app) and slow performance.

Hopefully I will be able to marry TQTXFontDetector with TW3Fonts (the latter name will be kept) to provide the best, easiest to use and most powerful HTML5 font manager out there. And considering that not even jQuery have functions like I have provided here now, that should not be to hard 🙂

Well, enjoy!

Advertisements
  1. abouchez
    September 1, 2014 at 7:36 am

    All those articles are indeed very interesting. It is difficult to a “desktop only” developer like me to switch to the CSS3/HTML5 platform, for all those kind of details. Thanks a lot for putting the information here!

    Some questions/remarks:
    * What about the performance of using scrollWidth?
    * You set overflow=scroll, but what does it mean exactly? What about other options, like overflow=hidden;
    * Be aware that, like in any other platforms, measuring words before displaying works well in English and most western languages, but not if you mix languages: e.g. writing some hebrew or arabic R2L text with some English words in the middle… logical order (i.e. the order of words in the string) may not match the visual order: complex scripting is handled by the browser, but there is no simple and easy “measure-and-fit” algorithm – Do you know any mean of knowing how much text would fit in a box, from CSS?

    • abouchez
      September 1, 2014 at 7:40 am

      About performance, TQTXFontDetector.Detect() may benefit of having a cache dictionnary of already searched font names.

      BTW, will all this (your qtxutils) eventually end up in the official SMS RTL?

      • Jon Lennart Aasenden
        September 1, 2014 at 9:10 am

        Yes a cache will probably be added. You must remember that i ported this “on the fly” between article 1 and article 2, so i havent really had time to performance test it. Also, a bugfix is already in transit.

        As for the rtl stuff… Eventually, yes – but probably not in the shape and form it has now (which is the benefit of a library). At the moment i am using QTXLib to collect what i regard as “bonus RTL features or missing features” for the RTL. The SMS project is massive, both delphi side and JS side, so the RTL has plenty of room for improvements and expansion.

        With the advent of samsung’s new OS, i think it’s safe to say that the more well written, stable and smart code we can get into the RTL – the more potential our customers will have.

        But I can promise you that the effects indeed will be added to the RTL, as will all the dependencies.

        When this will happen is another case alltogether, we probably wont include it in the next hotfix – since it has so little time in use and is not as tested as the other RTL code.

    • Jon Lennart Aasenden
      September 1, 2014 at 9:19 am

      * What about the performance of using scrollWidth?

      The alternative is to create a graphics context + a canvas context and then do pixel measurements — which in my experience is extremely taxing for the browser. The canvas/context maps to a device-context and gdi+ canvas behind the scenes (actually, the browser is implemented by apple under webkit, which is not exactly known for fast code, but rather stable code).

      * You set overflow=scroll, but what does it mean exactly? What about other options, like overflow=hidden;

      You must refer to the docs whenever you see CSS values. Just google “css overflow” and you will find a full description of the css style and it’s possible values.

      In short, the overflow=scroll informs the browser that if the content is larger than the defined width/height of the element – then it should show scrollbars, and thus we can use scrollWidth/Height to read the complete width/height of a piece of html.
      This would be the same as a TScrollBox under Delphi 🙂

      Has we used “hidden” it would have clipped the content and we would be unable to read the actual size of the html.

      * Be aware that, like in any other platforms, measuring words before displaying works well in English and most western languages

      Hm, yes that is a good point. At the moment I dont have a clue if jewish, arabic or asian languages work at all with SMS (which may sound sloppy, but i write that because I quite frankly have never tried it. It has not been a high priority with all the technical challenges we have had to face). But point taken!

      Here is a link to w3Schools — always have the w3schools main website at hand, it will save you a lot of time and frustration:
      http://www.w3schools.com/cssref/pr_class_display.asp

      • abouchez
        September 1, 2014 at 5:53 pm

        Thanks for the answer.
        For Arabic and Hebrew content within a Smart application, you can count on the app we are currently preparing: it should be multi-lingual (we need world wide customers), and as a biblical software, it will show Hebrew and Greek, in additional to other languages. 🙂
        Do not think that bidirectional abilities are not a big point: this is IMHO the biggest weakness of FireMonkey, not to support R2L languages, and complex text script rendering.
        Thanks for the links and answers!

  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: