Archive

Archive for August 31, 2014

SMS fonts, once and for all, part 2

August 31, 2014 5 comments
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!

SMS fonts, once and for all, part 1

August 31, 2014 Leave a comment

While Smart Mobile Studio makes HTML5 app development fun and enjoyable, there are aspects of HTML5 and CSS3 which quite frankly cannot be simplified without a lot of research. This has nothing to with the top-level programming language, be it Smart Pascal or Typescript, but rather how browsers are designed, their abhorrent need for legacy support for all manner of whimsical ideas over the past 15 years – and last but not least, shortcomings of CSS and Javascript combined.

One of these topics is font management, which (at the present moment is driving me insane) you could be mistaken for being an issue of the 90’s and not 2014. But you will quickly realize that despite all the advancements of HTML5 there are still huge holes in the .. eh, whole WC3 scheme of things.

Now there used to be a time when you could, anywhere in a HTML segment, simply get away with:

<font name="verdana" size="14px">Some text</font>

But alas those days are gone (and we should be happy about that), and the free-standing font tag has been marked as obsolete for quite some time.

For Smart Pascal, how you style your components is really not the issue, but rather – getting concurrent and reliable information from the browser is. You should think that something as trivial as asking what font is used for an element would be simple and straight-forward, but sadly that is not the case.

For instance, you should imagine that you could do something like this:

var mSize = element.style.fontSize; //works
var mName = element.style.fontName; //nope

The above wont even work, not even the first. In order to read a calculated style you have to access the browser’s internal workings like this:

   mObj := BrowserAPI.Document.defaultView.getComputedStyle(tagRef, Null);
   if (mObj) then
   Result := (mObj).getPropertyValue(aStyleName);

But all this aside. The point of this article is to make more sense of fonts under HTML5. As the designer of SMS up to version 1.0 I know perfectly well how much work has gone into this, and also how many aspects of SMS that still needs a polish. And font management is absolutely one of them. Not because we did anything wrong, but because coding clever font detection and/or analysis requires more work than anyone could be expected to anticipate. Smart gives you a huge advantage over raw HTML5 authoring in plain JavaScript – but it can be done better and more accurate. Which is what I am going to do now once and for all (insert sound of trumpet’s going off in the distance here).

Before we dive into some code, here are a few things you can do to alleviate the problem straight away.

Clean up the default stylesheet

At the very top of the stylesheet for your app, is a strange segment which looks like this:

* {
  //content here
}

Just like under SQL, the “*” (star) character means “all”. So whatever you define within those curly-brackets’s is automatically applied to ALL elements (read: all controls) in your app.
So this is the perfect place to clearly define the font-name you want to use in your app, and it’s size.

So if you do like this:

* {
  font-family: "Segoe UI", "Helvetica neue";
  font-color: Transparent;
}

Then the default font for the document and all the elements created by Smart will be (and this is important) either “Segoe UI” (Windows 7 and 8) or, if that is not available, “Helvetica neue” (which is the font for iPhone and iPad). You may want to add Helvetica to that list, just in case your app is running under android.

The next step is to completely remove all “font-family” declarations besides that one, from the entire CSS file. This will ensure that unless you have hard-coded a font into your app, you will be able to control font handling from your stylesheet completely. Which is very handy!

Getting the name and size of a font

Let me explain the problem clearly. When you define what font should be used, there is no guarantee that this font is indeed installed on the system running you app. This is why the CSS “font-family” accepts more than one font name. And the rule is, that if “Segoe UI” (first in list) is not installed, then the browser tries to use the next one – and so fourth.

The problem? Whenever you try to read-back what font was actually selected, well.. there are no such function (!) The only thing you can do is to read back the whole “font-family” string which contains exactly what you already know. I was hoping the computed style contained just the applied font and not the whole list, but no such luck.

So what is the solution? Well, there are JS libraries for checking if a font is installed on the system – and the only thing you can actually do about this, is to iterate through the font-family string and use the first one which is valid.

One such solution is FontUnStack, which sadly has a dependency on jQuery. Which is not something I want to ship with Smart Mobile Studio to be perfectly frank. A second solution is Font Detector which, since it has no dependencies, is the one I have re-implemented in Smart Pascal and which we will be using.

But we are still not out of the woods, because while detecting if a font is installed is all nice and dandy, we still have to make a little snippet which uses the font detector to compare against the font-family list we know. Then, and only then, will we have fixed the problem of being able to determine what font is used for an element (phew!).

But let us begin with the font detection class, which turned out very small (based on the 0.3 branch):

type

  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;
    Constructor Create;virtual;
  End;

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

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

  Fh:=browserApi.Document.getElementsByTagName("body")[0];

  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.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;

You can try this out yourself, and you will find that it absolutely works and easily detects if a font is installed on your system. So great, that’s the first part of the equation finished.

More to follow soon..