Sunday, March 20, 2011

How to take control of Internet Explorer 9's text rendering



I installed Internet Explorer 9 earlier this week when I saw an article announcing it had been released. Aside from the rather disappointing first impression from a website called "Beauty of the Web" seen on the right, the install was quick and even offered the option to close processes that were locking some files it had to update, to avoid having to reboot. So far, so good.

It wasn't until the next day, when I got an instant message from a friend, that I noticed what the upgrade entailed: ClearType font rendering that had no "off" switch! That's right, the Always use ClearType for HTML option was, for all intents and purposes, removed and hard-coded to YES. And not just in Internet Explorer, but in all applications that embed its HTML rendering: Google Talk, HtmlHelp, Microsoft Document Explorer, you name it!

A frantic search on the web eventually lead me to Sub-pixel Fonts in IE9 where it was explained that the conversion from font point sizes to pixel sizes has been inaccurate for quite some time, which meant that if you were to zoom a web page, the text would sometimes wrap differently due to rounding errors during the conversion, as shown below with the word "pellentesque" on one line at 100% and on the previous at 125%:


To put this ahem "defect" into context, let's examine the following chart produced by the Internet Explorer UX team after gathering data on IE8 feature usage:


No "zoom". In fact, no mention of "zoom" in the accompanying blog post, either. I had to search the blog for "zoom" to eventually find out that “Select Preset Zoom” [was] used by 1.6% of people, although that's hardly indicative of feature use, since you can also Ctrl+scroll and use Ctrl+ & Ctrl-, nor is there any indication who these "people" are.

To summarize, someone thought the rounding errors while zooming looked silly and decided, as a result, to ignore the operating system's settings for ClearType. Well, some of us (Scott Hanselman thinks it is about a 60/40 split) will have none of that, thank you very much.

Anti-AliasingTunerForFirefox4A precedent

It seems the upcoming Firefox 4 has the same problem (it also uses DirectWrite), but an ingenious add-on developer called Nag. MATSUI has released the Anti-Aliasing Tuner extension which provides some familiar-looking options, seen on the right.

I don't run Firefox 4, but I still downloaded the add-on, since it looked like it did what I wanted and I was curious to know how it did so, in case it was similar to what I needed to do.

Aside from the parts that coordinate with Firefox to be an add-on that politely attaches and detaches on demand as well as loads and saves its settings, the crux of the technique (intercepting glyph drawing) turns out to be portable!

MATSUI was kind enough to provide their source code inside the add-on, released under the MIT license, so I was able to copy fork it to port the functionality for Internet Explorer!

An opportunity

I did not feel like creating an entire Internet Explorer add-on/extension (since I am stuck using C++) but because I knew that the interception technique was related to DirectWrite, maybe there was some way to wedge myself between IE and DirectWrite. That's when I discovered this, using ProcMon, near the beginning of IE's start-up:

Interception opportunity

The first line is the tell: iexplore.exe tries to load DWrite.dll from its folder before trying the SYSDIR folder. This is typical of the LoadLibrary() function's DLL search order for run-time dynamic linking. A quick look inside this library reveals it exports only one function:

Export Table:
 Name:                          DWrite.dll
 Time Date Stamp:               0x4D5F2AF7 (2011-02-18 22:29:11)
 Version:                       0.00
 Ordinal Base:                  1
 Number of Functions:           1
 Number of Names:               1

 Ordinal   Entry Point   Name
       1   0x0000AC70    DWriteCreateFactory

The idea is as follows: if it looks like DWrite.dll and quacks like DWrite.dll, it must be DWrite.dll!

The interception

A proof of concept DLL was created that exported the same entry point while delegating the work to the real DLL (loaded dynamically with an absolute path) and it "worked"! (ProcMon showed that my DLL was being loaded, then the original, and IE still worked) Now what? Would I be re-implementing the IDWriteFactory interface to tweak the values specified by the IDWriteRenderingParams interface? Implementing a .NET interface in C#, that's a piece of cake. Implementing a COM interface in C++, umm, no thanks.

A closer look at the original Firefox add-on revealed it was using the Detours library to figuratively slip the operating system a $20 bill in exchange for first dibs on any and all calls (in the currently-executing process) to the ID2D1RenderTarget::DrawGlyphRun() method so that a little text rendering override could be performed before calling the original function, whose pointer we just happen to have.

Some tinkering revealed that the detour can be installed when the DWriteCreateFactory() method is first called (since we're intercepting that due to being at the right place at the right time) and removed when the process detaches from the DLL. The whole business of loading and saving settings with Firefox was removed and instead we [temporarily] hardcode everything to be "aliased".

The result

The DWrite.dll wrapper was released as freeware. Fanfare did not ensue, but I've been copying the DLLs to various folders on my computer as I find applications that embed the Trident layout engine, thus restoring my sanity (and eyesight), one application at a time.

In theory, this wrapper DLL could be used to override any application that uses DirectWrite and hardcodes ClearType to be always on. More testing is required, as I have found at least one application that simply refuses to start when those DLLs are present in its folder, plus it [currently] only works with the 32-bit version of Internet Explorer, breaking the 64-bit version.

In the spirit of continuous improvement and helping out other curious programmers who might be in the same boat, the source code is publically available.