Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I balance script-oriented OpenType features with other OpenType features using DirectWrite?

Full disclosure: I'm working on my libui GUI framework's text API. This wraps DirectWrite on Windows, Core Text on OS X, and Pango (which uses HarfBuzz for OpenType shaping) on other Unixes. One of the text formatting attributes I want to specify is a collection of OpenType features to use, which all three provide; DirectWrite's is IDWriteTypography.

Now, when you draw some text with these libraries, by default you'll get a few useful OpenType features enabled, such as the standard ligatures (liga) like the f+i ligature. I thought this was font-specific, but it turns out this is specific to the script of the text being shaped. Microsoft provides guidelines for all the scripts supported by OpenType (under "Script-specific Development"), and I can see rather complex logic for doing it all in HarfBuzz itself to confirm it.

On Core Text and Pango, if I enable other attributes, they'll be added on top of these defaults. But with DirectWrite, in particular IDWriteTextLayout::SetTypography(), doing so removes the defaults:

DirectWrite removes default OpenType features if you explicitly specify a IDWriteTypography object

The program that produces this output is can be found here.

Obviously my first option would be to ask how to get the default features on DirectWrite. Someone did so already on this site, though, and the answer seems to be "no".

I am guessing that DirectWrite is allowing me to be in complete control of the list of features to apply to some text. This is nice, except that I can't do this with the other APIs unless I explicitly disable the default features somehow! Of course, I don't know if this list will ever change, so hardcoding it might not be the best idea.

Even if hardcoding is an option, I could just grab HarfBuzz's list for each script, but a) it's rather complicated b) there are multiple possible shapers for a script, depending on (I think) version compatibility (for instance, Myanmar).

So why not use HarfBuzz's lists to recreate the default list of features for DirectWrite anyway? It seems to want to be accurate to other shapers anyway, so this should work, right? Well I would need to do two things: figure out what script to use, and figure out which attributes to use on which characters for script where the position of a character in the word matters.

DirectWrite provides an interface IDWriteTextAnalyzer that provides facilities to perform shaping. I could use this, but it seems the script data is returned in a DWRITE_SCRIPT_ANALYSIS structure, and the description for the script ID says "The zero-based index representation of writing system script.".

This doesn't help, so I wrote a program to just dump the script numbers for text I type in. Running it on the input string

لللللللللللللاااااااااالا abcd محمد ابن بطوطة‎‎ Отложения датского яруса

yields the output

0 - 26 script 3 shapes 0
26 - 5 script 49 shapes 0
31 - 14 script 3 shapes 0
45 - 2 script 1 shapes 1
47 - 25 script 22 shapes 0

I cannot match these script numbers to anything in any of the Windows headers: if there is a defined number for Arabic, Latin, or Cyrillic in any API, they don't match these. And even if I did get a mapping between script and script number, that still doesn't give me the data to apply intra-word features.

What about Uniscribe? Well, the documentation for the equivalent SCRIPT_ANALYSIS type says that its script ID is an "[opaque] value" whose "value for this member is undefined and applications should not rely on its value being the same from one release to the next". And while I can get a language code to identify the script by, there's still no defined value other than LANG_ENGLISH for "Western" (Latin?) scripts. Are the DirectWrite values the same as the Uniscribe ones? And it seems like I can at least figure the initial and final states of words by looking at the fLinkBefore and fLinkAfter fields, but is this enough to properly apply attributes per-script?

HarfBuzz does have an experimental DirectWrite backend that isn't intended to be used by real programs; I'm not yet sure whether it has the same feature-clobbering I specified above. If I find out, I'll update this part here.

Finally, if I enter the following equivalent test case to the first one above in something like kaxaml:

<Page
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Grid>  
  <FlowDocumentPageViewer>
  <FlowDocument FontFamily="Constantia" FontSize="48">
  <Paragraph>
  afford afire aflight 1/4<LineBreak/>
  <Run Typography.Fraction="1">afford afire aflight 1/4</Run>
  </Paragraph>
  </FlowDocument>
  </FlowDocumentPageViewer>
  </Grid>
</Page>

I see the ligatures being applied properly, even in the latter case:

kaxaml shows what I want works fine

(The fraction at the end is just to prove that that attribute is being applied.) If I assume XAML uses DirectWrite, then that proves my first option (simply overlaying my custom attributes on top of the defaults) should be possible... (I make this assumption based on the idea that XAML provides a strikingly similar API to Direct2D for drawing 2D graphics, and has a lot of holes filled in where I had to manually write a lot of glue code to do the same things with vanilla Direct2D, so I assume whatever is possible in XAML is possible with Direct2D, and by extension DirectWrite since they were technically introduced together...)

At this point I'm completely lost. I want to at least be predictable across platforms, and I'm not sure how programs are even supposed to, let alone going to, use OpenType features directly or not anyway. Am I making bad expectations of text layout APIs? Will I have to drop IDWriteTextLayout and do all the text shaping and layout myself if I want this?

Or do I have to drop vanilla Windows 7 support and upgrade to the Platform Update DirectWrite feature set? Or even Windows 7 entirely?

like image 855
andlabs Avatar asked Jun 18 '17 03:06

andlabs


People also ask

Should I use OpenType features all the time?

Not all OpenType features are appropriate to use all of the time, but some features are critical for great typography. To use a font's OpenType features in your project, you need to include them in the web project and then style your text with the required CSS.

What is OpenType in Adobe Creative Cloud?

某些 Creative Cloud 应用程序、服务和功能在中国不可用。 OpenType features are like secret compartments in fonts. Unlock them and you’ll find ways to make fonts look and behave differently in subtle and dramatic ways. Not all OpenType features are appropriate to use all of the time, but some features are critical for great typography.

What is an OpenType layout?

Explicit script and language information contained in font, so a text-processing application can adjust its behavior accordingly. The OpenType Layout tables are described in more detail in the "Font File Tables" section of the OpenType specification.

How do I include a font's OpenType features in a project?

To include a font’s OpenType features in a web project, visit the Web Projects page and click the "edit" link for the project. In the Character Set section, check the OpenType Features box.


1 Answers

After some discussions with Peter Sikking and Ebrahim Byagowi, I went and debugged a more general-purpose program I built quickly to test things, and I figured out what's going on internally.

First, however, I will say this applies to Uniscribe and DirectWrite equally.

As it turns out, DirectWrite is always providing a set of default OpenType features, regardless of what feature set I use! The situation is that the list of default features provided differs depending on whether I load my own features or not, and depending on the shaping engine. For the latn script in horizontal writing mode and for English, this is done with the "generic engine".

If I don't provide any features, the generic engine will load script-specific features. For horizontal latn, this list is

locl
ccmp
rlig
rclt
calt
liga
clig

If I do provide features, the generic engine will use the same default list for all scripts:

locl
ccmp
rclt
rlig
mark
mkmk
dist

So I don't know what to do about this. I could probably just provide liga and a few others myself in libui code (marked as a HACK of course), but this is still weird. I'm not sure what the motivation is either. Either way, this explains the behavior I'm seeing.

like image 162
andlabs Avatar answered Oct 02 '22 17:10

andlabs