SMS fonts, once and for all, part 2
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.
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!
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?
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?
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.
* 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
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!