807 lines
33 KiB
Python
807 lines
33 KiB
Python
# coding: utf-8
|
||
from __future__ import print_function, division, absolute_import, unicode_literals
|
||
from fontTools.misc.py23 import *
|
||
from fontTools.misc.testTools import FakeFont, getXML, parseXML
|
||
from fontTools.misc.textTools import deHexStr, hexStr
|
||
from fontTools.ttLib import newTable
|
||
import unittest
|
||
|
||
|
||
# A simple 'morx' table with non-contextual glyph substitution.
|
||
# Unfortunately, the Apple spec for 'morx' does not contain a complete example.
|
||
# The test case has therefore been adapted from the example 'mort' table in
|
||
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6mort.html
|
||
MORX_NONCONTEXTUAL_DATA = deHexStr(
|
||
'0002 0000 ' # 0: Version=2, Reserved=0
|
||
'0000 0001 ' # 4: MorphChainCount=1
|
||
'0000 0001 ' # 8: DefaultFlags=1
|
||
'0000 0058 ' # 12: StructLength=88
|
||
'0000 0003 ' # 16: MorphFeatureCount=3
|
||
'0000 0001 ' # 20: MorphSubtableCount=1
|
||
'0004 0000 ' # 24: Feature[0].FeatureType=4/VertSubst, .FeatureSetting=on
|
||
'0000 0001 ' # 28: Feature[0].EnableFlags=0x00000001
|
||
'FFFF FFFF ' # 32: Feature[0].DisableFlags=0xFFFFFFFF
|
||
'0004 0001 ' # 36: Feature[1].FeatureType=4/VertSubst, .FeatureSetting=off
|
||
'0000 0000 ' # 40: Feature[1].EnableFlags=0x00000000
|
||
'FFFF FFFE ' # 44: Feature[1].DisableFlags=0xFFFFFFFE
|
||
'0000 0001 ' # 48: Feature[2].FeatureType=0/GlyphEffects, .FeatSetting=off
|
||
'0000 0000 ' # 52: Feature[2].EnableFlags=0 (required for last feature)
|
||
'0000 0000 ' # 56: Feature[2].EnableFlags=0 (required for last feature)
|
||
'0000 0024 ' # 60: Subtable[0].StructLength=36
|
||
'80 ' # 64: Subtable[0].CoverageFlags=0x80
|
||
'00 00 ' # 65: Subtable[0].Reserved=0
|
||
'04 ' # 67: Subtable[0].MorphType=4/NoncontextualMorph
|
||
'0000 0001 ' # 68: Subtable[0].SubFeatureFlags=0x1
|
||
'0006 0004 ' # 72: LookupFormat=6, UnitSize=4
|
||
'0002 0008 ' # 76: NUnits=2, SearchRange=8
|
||
'0001 0000 ' # 80: EntrySelector=1, RangeShift=0
|
||
'000B 0087 ' # 84: Glyph=11 (parenleft); Value=135 (parenleft.vertical)
|
||
'000D 0088 ' # 88: Glyph=13 (parenright); Value=136 (parenright.vertical)
|
||
'FFFF 0000 ' # 92: Glyph=<end>; Value=0
|
||
) # 96: <end>
|
||
assert len(MORX_NONCONTEXTUAL_DATA) == 96
|
||
|
||
|
||
MORX_NONCONTEXTUAL_XML = [
|
||
'<Version value="2"/>',
|
||
'<Reserved value="0"/>',
|
||
'<!-- MorphChainCount=1 -->',
|
||
'<MorphChain index="0">',
|
||
' <DefaultFlags value="0x00000001"/>',
|
||
' <!-- StructLength=88 -->',
|
||
' <!-- MorphFeatureCount=3 -->',
|
||
' <!-- MorphSubtableCount=1 -->',
|
||
' <MorphFeature index="0">',
|
||
' <FeatureType value="4"/>',
|
||
' <FeatureSetting value="0"/>',
|
||
' <EnableFlags value="0x00000001"/>',
|
||
' <DisableFlags value="0xFFFFFFFF"/>',
|
||
' </MorphFeature>',
|
||
' <MorphFeature index="1">',
|
||
' <FeatureType value="4"/>',
|
||
' <FeatureSetting value="1"/>',
|
||
' <EnableFlags value="0x00000000"/>',
|
||
' <DisableFlags value="0xFFFFFFFE"/>',
|
||
' </MorphFeature>',
|
||
' <MorphFeature index="2">',
|
||
' <FeatureType value="0"/>',
|
||
' <FeatureSetting value="1"/>',
|
||
' <EnableFlags value="0x00000000"/>',
|
||
' <DisableFlags value="0x00000000"/>',
|
||
' </MorphFeature>',
|
||
' <MorphSubtable index="0">',
|
||
' <!-- StructLength=36 -->',
|
||
' <CoverageFlags value="128"/>',
|
||
' <Reserved value="0"/>',
|
||
' <!-- MorphType=4 -->',
|
||
' <SubFeatureFlags value="0x00000001"/>',
|
||
' <NoncontextualMorph>',
|
||
' <Substitution>',
|
||
' <Lookup glyph="parenleft" value="parenleft.vertical"/>',
|
||
' <Lookup glyph="parenright" value="parenright.vertical"/>',
|
||
' </Substitution>',
|
||
' </NoncontextualMorph>',
|
||
' </MorphSubtable>',
|
||
'</MorphChain>',
|
||
]
|
||
|
||
|
||
MORX_REARRANGEMENT_DATA = deHexStr(
|
||
'0002 0000 ' # 0: Version=2, Reserved=0
|
||
'0000 0001 ' # 4: MorphChainCount=1
|
||
'0000 0001 ' # 8: DefaultFlags=1
|
||
'0000 0078 ' # 12: StructLength=120 (+8=128)
|
||
'0000 0000 ' # 16: MorphFeatureCount=0
|
||
'0000 0001 ' # 20: MorphSubtableCount=1
|
||
'0000 0068 ' # 24: Subtable[0].StructLength=104 (+24=128)
|
||
'80 ' # 28: Subtable[0].CoverageFlags=0x80
|
||
'00 00 ' # 29: Subtable[0].Reserved=0
|
||
'00 ' # 31: Subtable[0].MorphType=0/RearrangementMorph
|
||
'0000 0001 ' # 32: Subtable[0].SubFeatureFlags=0x1
|
||
'0000 0006 ' # 36: STXHeader.ClassCount=6
|
||
'0000 0010 ' # 40: STXHeader.ClassTableOffset=16 (+36=52)
|
||
'0000 0028 ' # 44: STXHeader.StateArrayOffset=40 (+36=76)
|
||
'0000 004C ' # 48: STXHeader.EntryTableOffset=76 (+36=112)
|
||
'0006 0004 ' # 52: ClassTable.LookupFormat=6, .UnitSize=4
|
||
'0002 0008 ' # 56: .NUnits=2, .SearchRange=8
|
||
'0001 0000 ' # 60: .EntrySelector=1, .RangeShift=0
|
||
'0001 0005 ' # 64: Glyph=A; Class=5
|
||
'0003 0004 ' # 68: Glyph=C; Class=4
|
||
'FFFF 0000 ' # 72: Glyph=<end>; Value=0
|
||
'0000 0001 0002 0003 0002 0001 ' # 76: State[0][0..5]
|
||
'0003 0003 0003 0003 0003 0003 ' # 88: State[1][0..5]
|
||
'0001 0003 0003 0003 0002 0002 ' # 100: State[2][0..5]
|
||
'0002 FFFF ' # 112: Entries[0].NewState=2, .Flags=0xFFFF
|
||
'0001 A00D ' # 116: Entries[1].NewState=1, .Flags=0xA00D
|
||
'0000 8006 ' # 120: Entries[2].NewState=0, .Flags=0x8006
|
||
'0002 0000 ' # 124: Entries[3].NewState=2, .Flags=0x0000
|
||
) # 128: <end>
|
||
assert len(MORX_REARRANGEMENT_DATA) == 128, len(MORX_REARRANGEMENT_DATA)
|
||
|
||
|
||
MORX_REARRANGEMENT_XML = [
|
||
'<Version value="2"/>',
|
||
'<Reserved value="0"/>',
|
||
'<!-- MorphChainCount=1 -->',
|
||
'<MorphChain index="0">',
|
||
' <DefaultFlags value="0x00000001"/>',
|
||
' <!-- StructLength=120 -->',
|
||
' <!-- MorphFeatureCount=0 -->',
|
||
' <!-- MorphSubtableCount=1 -->',
|
||
' <MorphSubtable index="0">',
|
||
' <!-- StructLength=104 -->',
|
||
' <CoverageFlags value="128"/>',
|
||
' <Reserved value="0"/>',
|
||
' <!-- MorphType=0 -->',
|
||
' <SubFeatureFlags value="0x00000001"/>',
|
||
' <RearrangementMorph>',
|
||
' <StateTable>',
|
||
' <!-- GlyphClassCount=6 -->',
|
||
' <GlyphClass glyph="A" value="5"/>',
|
||
' <GlyphClass glyph="C" value="4"/>',
|
||
' <State index="0">',
|
||
' <Transition onGlyphClass="0">',
|
||
' <NewState value="2"/>',
|
||
' <Flags value="MarkFirst,DontAdvance,MarkLast"/>',
|
||
' <ReservedFlags value="0x1FF0"/>',
|
||
' <Verb value="15"/><!-- ABxCD ⇒ DCxBA -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="1">',
|
||
' <NewState value="1"/>',
|
||
' <Flags value="MarkFirst,MarkLast"/>',
|
||
' <Verb value="13"/><!-- ABxCD ⇒ CDxBA -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="2">',
|
||
' <NewState value="0"/>',
|
||
' <Flags value="MarkFirst"/>',
|
||
' <Verb value="6"/><!-- xCD ⇒ CDx -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="3">',
|
||
' <NewState value="2"/>',
|
||
' <Verb value="0"/><!-- no change -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="4">',
|
||
' <NewState value="0"/>',
|
||
' <Flags value="MarkFirst"/>',
|
||
' <Verb value="6"/><!-- xCD ⇒ CDx -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="5">',
|
||
' <NewState value="1"/>',
|
||
' <Flags value="MarkFirst,MarkLast"/>',
|
||
' <Verb value="13"/><!-- ABxCD ⇒ CDxBA -->',
|
||
' </Transition>',
|
||
' </State>',
|
||
' <State index="1">',
|
||
' <Transition onGlyphClass="0">',
|
||
' <NewState value="2"/>',
|
||
' <Verb value="0"/><!-- no change -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="1">',
|
||
' <NewState value="2"/>',
|
||
' <Verb value="0"/><!-- no change -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="2">',
|
||
' <NewState value="2"/>',
|
||
' <Verb value="0"/><!-- no change -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="3">',
|
||
' <NewState value="2"/>',
|
||
' <Verb value="0"/><!-- no change -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="4">',
|
||
' <NewState value="2"/>',
|
||
' <Verb value="0"/><!-- no change -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="5">',
|
||
' <NewState value="2"/>',
|
||
' <Verb value="0"/><!-- no change -->',
|
||
' </Transition>',
|
||
' </State>',
|
||
' <State index="2">',
|
||
' <Transition onGlyphClass="0">',
|
||
' <NewState value="1"/>',
|
||
' <Flags value="MarkFirst,MarkLast"/>',
|
||
' <Verb value="13"/><!-- ABxCD ⇒ CDxBA -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="1">',
|
||
' <NewState value="2"/>',
|
||
' <Verb value="0"/><!-- no change -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="2">',
|
||
' <NewState value="2"/>',
|
||
' <Verb value="0"/><!-- no change -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="3">',
|
||
' <NewState value="2"/>',
|
||
' <Verb value="0"/><!-- no change -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="4">',
|
||
' <NewState value="0"/>',
|
||
' <Flags value="MarkFirst"/>',
|
||
' <Verb value="6"/><!-- xCD ⇒ CDx -->',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="5">',
|
||
' <NewState value="0"/>',
|
||
' <Flags value="MarkFirst"/>',
|
||
' <Verb value="6"/><!-- xCD ⇒ CDx -->',
|
||
' </Transition>',
|
||
' </State>',
|
||
' </StateTable>',
|
||
' </RearrangementMorph>',
|
||
' </MorphSubtable>',
|
||
'</MorphChain>',
|
||
]
|
||
|
||
|
||
# Taken from “Example 1: A contextal substituation table” in
|
||
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html
|
||
# as retrieved on 2017-09-05.
|
||
#
|
||
# Compared to the example table in Apple’s specification, we’ve
|
||
# made the following changes:
|
||
#
|
||
# * at offsets 0..35, we’ve prepended 36 bytes of boilerplate
|
||
# to make the data a structurally valid ‘morx’ table;
|
||
#
|
||
# * at offset 36 (offset 0 in Apple’s document), we’ve changed
|
||
# the number of glyph classes from 5 to 6 because the encoded
|
||
# finite-state machine has transitions for six different glyph
|
||
# classes (0..5);
|
||
#
|
||
# * at offset 52 (offset 16 in Apple’s document), we’ve replaced
|
||
# the presumably leftover ‘XXX’ mark by an actual data offset;
|
||
#
|
||
# * at offset 72 (offset 36 in Apple’s document), we’ve changed
|
||
# the input GlyphID from 51 to 52. With the original value of 51,
|
||
# the glyph class lookup table can be encoded with equally many
|
||
# bytes in either format 2 or 6; after changing the GlyphID to 52,
|
||
# the most compact encoding is lookup format 6, as used in Apple’s
|
||
# example;
|
||
#
|
||
# * at offset 90 (offset 54 in Apple’s document), we’ve changed
|
||
# the value for the lookup end-of-table marker from 1 to 0.
|
||
# Fonttools always uses zero for this value, whereas Apple’s
|
||
# spec examples are inconsistently using one of {0, 1, 0xFFFF}
|
||
# for this filler value;
|
||
#
|
||
# * at offset 172 (offset 136 in Apple’s document), we’ve again changed
|
||
# the input GlyphID from 51 to 52, for the same reason as above.
|
||
#
|
||
# TODO: Ask Apple to fix “Example 1” in the ‘morx’ specification.
|
||
MORX_CONTEXTUAL_DATA = deHexStr(
|
||
'0002 0000 ' # 0: Version=2, Reserved=0
|
||
'0000 0001 ' # 4: MorphChainCount=1
|
||
'0000 0001 ' # 8: DefaultFlags=1
|
||
'0000 00B4 ' # 12: StructLength=180 (+8=188)
|
||
'0000 0000 ' # 16: MorphFeatureCount=0
|
||
'0000 0001 ' # 20: MorphSubtableCount=1
|
||
'0000 00A4 ' # 24: Subtable[0].StructLength=164 (+24=188)
|
||
'80 ' # 28: Subtable[0].CoverageFlags=0x80
|
||
'00 00 ' # 29: Subtable[0].Reserved=0
|
||
'01 ' # 31: Subtable[0].MorphType=1/ContextualMorph
|
||
'0000 0001 ' # 32: Subtable[0].SubFeatureFlags=0x1
|
||
'0000 0006 ' # 36: STXHeader.ClassCount=6
|
||
'0000 0014 ' # 40: STXHeader.ClassTableOffset=20 (+36=56)
|
||
'0000 0038 ' # 44: STXHeader.StateArrayOffset=56 (+36=92)
|
||
'0000 005C ' # 48: STXHeader.EntryTableOffset=92 (+36=128)
|
||
'0000 0074 ' # 52: STXHeader.PerGlyphTableOffset=116 (+36=152)
|
||
|
||
# Glyph class table.
|
||
'0006 0004 ' # 56: ClassTable.LookupFormat=6, .UnitSize=4
|
||
'0005 0010 ' # 60: .NUnits=5, .SearchRange=16
|
||
'0002 0004 ' # 64: .EntrySelector=2, .RangeShift=4
|
||
'0032 0004 ' # 68: Glyph=50; Class=4
|
||
'0034 0004 ' # 72: Glyph=52; Class=4
|
||
'0050 0005 ' # 76: Glyph=80; Class=5
|
||
'00C9 0004 ' # 80: Glyph=201; Class=4
|
||
'00CA 0004 ' # 84: Glyph=202; Class=4
|
||
'FFFF 0000 ' # 88: Glyph=<end>; Value=<filler>
|
||
|
||
# State array.
|
||
'0000 0000 0000 0000 0000 0001 ' # 92: State[0][0..5]
|
||
'0000 0000 0000 0000 0000 0001 ' # 104: State[1][0..5]
|
||
'0000 0000 0000 0000 0002 0001 ' # 116: State[2][0..5]
|
||
|
||
# Entry table.
|
||
'0000 0000 ' # 128: Entries[0].NewState=0, .Flags=0
|
||
'FFFF FFFF ' # 132: Entries[0].MarkSubst=None, .CurSubst=None
|
||
'0002 0000 ' # 136: Entries[1].NewState=2, .Flags=0
|
||
'FFFF FFFF ' # 140: Entries[1].MarkSubst=None, .CurSubst=None
|
||
'0000 0000 ' # 144: Entries[2].NewState=0, .Flags=0
|
||
'FFFF 0000 ' # 148: Entries[2].MarkSubst=None, .CurSubst=PerGlyph #0
|
||
# 152: <no padding needed for 4-byte alignment>
|
||
|
||
# Per-glyph lookup tables.
|
||
'0000 0004 ' # 152: Offset from this point to per-glyph lookup #0.
|
||
|
||
# Per-glyph lookup #0.
|
||
'0006 0004 ' # 156: ClassTable.LookupFormat=6, .UnitSize=4
|
||
'0004 0010 ' # 160: .NUnits=4, .SearchRange=16
|
||
'0002 0000 ' # 164: .EntrySelector=2, .RangeShift=0
|
||
'0032 0258 ' # 168: Glyph=50; ReplacementGlyph=600
|
||
'0034 0259 ' # 172: Glyph=52; ReplacementGlyph=601
|
||
'00C9 025A ' # 176: Glyph=201; ReplacementGlyph=602
|
||
'00CA 0384 ' # 180: Glyph=202; ReplacementGlyph=900
|
||
'FFFF 0000 ' # 184: Glyph=<end>; Value=<filler>
|
||
|
||
) # 188: <end>
|
||
assert len(MORX_CONTEXTUAL_DATA) == 188, len(MORX_CONTEXTUAL_DATA)
|
||
|
||
|
||
MORX_CONTEXTUAL_XML = [
|
||
'<Version value="2"/>',
|
||
'<Reserved value="0"/>',
|
||
'<!-- MorphChainCount=1 -->',
|
||
'<MorphChain index="0">',
|
||
' <DefaultFlags value="0x00000001"/>',
|
||
' <!-- StructLength=180 -->',
|
||
' <!-- MorphFeatureCount=0 -->',
|
||
' <!-- MorphSubtableCount=1 -->',
|
||
' <MorphSubtable index="0">',
|
||
' <!-- StructLength=164 -->',
|
||
' <CoverageFlags value="128"/>',
|
||
' <Reserved value="0"/>',
|
||
' <!-- MorphType=1 -->',
|
||
' <SubFeatureFlags value="0x00000001"/>',
|
||
' <ContextualMorph>',
|
||
' <StateTable>',
|
||
' <!-- GlyphClassCount=6 -->',
|
||
' <GlyphClass glyph="A" value="4"/>',
|
||
' <GlyphClass glyph="B" value="4"/>',
|
||
' <GlyphClass glyph="C" value="5"/>',
|
||
' <GlyphClass glyph="X" value="4"/>',
|
||
' <GlyphClass glyph="Y" value="4"/>',
|
||
' <State index="0">',
|
||
' <Transition onGlyphClass="0">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="1">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="2">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="3">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="4">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="5">',
|
||
' <NewState value="2"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' </State>',
|
||
' <State index="1">',
|
||
' <Transition onGlyphClass="0">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="1">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="2">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="3">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="4">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="5">',
|
||
' <NewState value="2"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' </State>',
|
||
' <State index="2">',
|
||
' <Transition onGlyphClass="0">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="1">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="2">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="3">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="4">',
|
||
' <NewState value="0"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="5">',
|
||
' <NewState value="2"/>',
|
||
' <MarkIndex value="65535"/>',
|
||
' <CurrentIndex value="65535"/>',
|
||
' </Transition>',
|
||
' </State>',
|
||
' <PerGlyphLookup index="0">',
|
||
' <Lookup glyph="A" value="A.swash"/>',
|
||
' <Lookup glyph="B" value="B.swash"/>',
|
||
' <Lookup glyph="X" value="X.swash"/>',
|
||
' <Lookup glyph="Y" value="Y.swash"/>',
|
||
' </PerGlyphLookup>',
|
||
' </StateTable>',
|
||
' </ContextualMorph>',
|
||
' </MorphSubtable>',
|
||
'</MorphChain>',
|
||
]
|
||
|
||
|
||
# Taken from “Example 2: A ligature table” in
|
||
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html
|
||
# as retrieved on 2017-09-11.
|
||
#
|
||
# Compared to the example table in Apple’s specification, we’ve
|
||
# made the following changes:
|
||
#
|
||
# * at offsets 0..35, we’ve prepended 36 bytes of boilerplate
|
||
# to make the data a structurally valid ‘morx’ table;
|
||
#
|
||
# * at offsets 88..91 (offsets 52..55 in Apple’s document), we’ve
|
||
# changed the range of the third segment from 23..24 to 26..28.
|
||
# The hexdump values in Apple’s specification are completely wrong;
|
||
# the values from the comments would work, but they can be encoded
|
||
# more compactly than in the specification example. For round-trip
|
||
# testing, we omit the ‘f’ glyph, which makes AAT lookup format 2
|
||
# the most compact encoding;
|
||
#
|
||
# * at offsets 92..93 (offsets 56..57 in Apple’s document), we’ve
|
||
# changed the glyph class of the third segment from 5 to 6, which
|
||
# matches the values from the comments to the spec (but not the
|
||
# Apple’s hexdump).
|
||
#
|
||
# TODO: Ask Apple to fix “Example 2” in the ‘morx’ specification.
|
||
MORX_LIGATURE_DATA = deHexStr(
|
||
'0002 0000 ' # 0: Version=2, Reserved=0
|
||
'0000 0001 ' # 4: MorphChainCount=1
|
||
'0000 0001 ' # 8: DefaultFlags=1
|
||
'0000 00DA ' # 12: StructLength=218 (+8=226)
|
||
'0000 0000 ' # 16: MorphFeatureCount=0
|
||
'0000 0001 ' # 20: MorphSubtableCount=1
|
||
'0000 00CA ' # 24: Subtable[0].StructLength=202 (+24=226)
|
||
'80 ' # 28: Subtable[0].CoverageFlags=0x80
|
||
'00 00 ' # 29: Subtable[0].Reserved=0
|
||
'02 ' # 31: Subtable[0].MorphType=2/LigatureMorph
|
||
'0000 0001 ' # 32: Subtable[0].SubFeatureFlags=0x1
|
||
|
||
# State table header.
|
||
'0000 0007 ' # 36: STXHeader.ClassCount=7
|
||
'0000 001C ' # 40: STXHeader.ClassTableOffset=28 (+36=64)
|
||
'0000 0040 ' # 44: STXHeader.StateArrayOffset=64 (+36=100)
|
||
'0000 0078 ' # 48: STXHeader.EntryTableOffset=120 (+36=156)
|
||
'0000 0090 ' # 52: STXHeader.LigActionsOffset=144 (+36=180)
|
||
'0000 009C ' # 56: STXHeader.LigComponentsOffset=156 (+36=192)
|
||
'0000 00AE ' # 60: STXHeader.LigListOffset=174 (+36=210)
|
||
|
||
# Glyph class table.
|
||
'0002 0006 ' # 64: ClassTable.LookupFormat=2, .UnitSize=6
|
||
'0003 000C ' # 68: .NUnits=3, .SearchRange=12
|
||
'0001 0006 ' # 72: .EntrySelector=1, .RangeShift=6
|
||
'0016 0014 0004 ' # 76: GlyphID 20..22 [a..c] -> GlyphClass 4
|
||
'0018 0017 0005 ' # 82: GlyphID 23..24 [d..e] -> GlyphClass 5
|
||
'001C 001A 0006 ' # 88: GlyphID 26..28 [g..i] -> GlyphClass 6
|
||
'FFFF FFFF 0000 ' # 94: <end of lookup>
|
||
|
||
# State array.
|
||
'0000 0000 0000 0000 0001 0000 0000 ' # 100: State[0][0..6]
|
||
'0000 0000 0000 0000 0001 0000 0000 ' # 114: State[1][0..6]
|
||
'0000 0000 0000 0000 0001 0002 0000 ' # 128: State[2][0..6]
|
||
'0000 0000 0000 0000 0001 0002 0003 ' # 142: State[3][0..6]
|
||
|
||
# Entry table.
|
||
'0000 0000 ' # 156: Entries[0].NewState=0, .Flags=0
|
||
'0000 ' # 160: Entries[0].ActionIndex=<n/a> because no 0x2000 flag
|
||
'0002 8000 ' # 162: Entries[1].NewState=2, .Flags=0x8000 (SetComponent)
|
||
'0000 ' # 166: Entries[1].ActionIndex=<n/a> because no 0x2000 flag
|
||
'0003 8000 ' # 168: Entries[2].NewState=3, .Flags=0x8000 (SetComponent)
|
||
'0000 ' # 172: Entries[2].ActionIndex=<n/a> because no 0x2000 flag
|
||
'0000 A000 ' # 174: Entries[3].NewState=0, .Flags=0xA000 (SetComponent,Act)
|
||
'0000 ' # 178: Entries[3].ActionIndex=0 (start at Action[0])
|
||
|
||
# Ligature actions table.
|
||
'3FFF FFE7 ' # 180: Action[0].Flags=0, .GlyphIndexDelta=-25
|
||
'3FFF FFED ' # 184: Action[1].Flags=0, .GlyphIndexDelta=-19
|
||
'BFFF FFF2 ' # 188: Action[2].Flags=<end of list>, .GlyphIndexDelta=-14
|
||
|
||
# Ligature component table.
|
||
'0000 0001 ' # 192: LigComponent[0]=0, LigComponent[1]=1
|
||
'0002 0003 ' # 196: LigComponent[2]=2, LigComponent[3]=3
|
||
'0000 0004 ' # 200: LigComponent[4]=0, LigComponent[5]=4
|
||
'0000 0008 ' # 204: LigComponent[6]=0, LigComponent[7]=8
|
||
'0010 ' # 208: LigComponent[8]=16
|
||
|
||
# Ligature list.
|
||
'03E8 03E9 ' # 210: LigList[0]=1000, LigList[1]=1001
|
||
'03EA 03EB ' # 214: LigList[2]=1002, LigList[3]=1003
|
||
'03EC 03ED ' # 218: LigList[4]=1004, LigList[3]=1005
|
||
'03EE 03EF ' # 222: LigList[5]=1006, LigList[6]=1007
|
||
) # 226: <end>
|
||
assert len(MORX_LIGATURE_DATA) == 226, len(MORX_LIGATURE_DATA)
|
||
|
||
|
||
MORX_LIGATURE_XML = [
|
||
'<Version value="2"/>',
|
||
'<Reserved value="0"/>',
|
||
'<!-- MorphChainCount=1 -->',
|
||
'<MorphChain index="0">',
|
||
' <DefaultFlags value="0x00000001"/>',
|
||
' <!-- StructLength=218 -->',
|
||
' <!-- MorphFeatureCount=0 -->',
|
||
' <!-- MorphSubtableCount=1 -->',
|
||
' <MorphSubtable index="0">',
|
||
' <!-- StructLength=202 -->',
|
||
' <CoverageFlags value="128"/>',
|
||
' <Reserved value="0"/>',
|
||
' <!-- MorphType=2 -->',
|
||
' <SubFeatureFlags value="0x00000001"/>',
|
||
' <LigatureMorph>',
|
||
' <StateTable>',
|
||
' <!-- GlyphClassCount=7 -->',
|
||
' <GlyphClass glyph="a" value="4"/>',
|
||
' <GlyphClass glyph="b" value="4"/>',
|
||
' <GlyphClass glyph="c" value="4"/>',
|
||
' <GlyphClass glyph="d" value="5"/>',
|
||
' <GlyphClass glyph="e" value="5"/>',
|
||
' <GlyphClass glyph="g" value="6"/>',
|
||
' <GlyphClass glyph="h" value="6"/>',
|
||
' <GlyphClass glyph="i" value="6"/>',
|
||
' <State index="0">',
|
||
' <Transition onGlyphClass="0">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="1">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="2">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="3">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="4">',
|
||
' <NewState value="2"/>',
|
||
' <Flags value="SetComponent"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="5">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="6">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' </State>',
|
||
' <State index="1">',
|
||
' <Transition onGlyphClass="0">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="1">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="2">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="3">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="4">',
|
||
' <NewState value="2"/>',
|
||
' <Flags value="SetComponent"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="5">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="6">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' </State>',
|
||
' <State index="2">',
|
||
' <Transition onGlyphClass="0">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="1">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="2">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="3">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="4">',
|
||
' <NewState value="2"/>',
|
||
' <Flags value="SetComponent"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="5">',
|
||
' <NewState value="3"/>',
|
||
' <Flags value="SetComponent"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="6">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' </State>',
|
||
' <State index="3">',
|
||
' <Transition onGlyphClass="0">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="1">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="2">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="3">',
|
||
' <NewState value="0"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="4">',
|
||
' <NewState value="2"/>',
|
||
' <Flags value="SetComponent"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="5">',
|
||
' <NewState value="3"/>',
|
||
' <Flags value="SetComponent"/>',
|
||
' </Transition>',
|
||
' <Transition onGlyphClass="6">',
|
||
' <NewState value="0"/>',
|
||
' <Flags value="SetComponent"/>',
|
||
' <Action GlyphIndexDelta="-25"/>',
|
||
' <Action GlyphIndexDelta="-19"/>',
|
||
' <Action GlyphIndexDelta="-14"/>',
|
||
' </Transition>',
|
||
' </State>',
|
||
' <LigComponents>',
|
||
' <LigComponent index="0" value="0"/>',
|
||
' <LigComponent index="1" value="1"/>',
|
||
' <LigComponent index="2" value="2"/>',
|
||
' <LigComponent index="3" value="3"/>',
|
||
' <LigComponent index="4" value="0"/>',
|
||
' <LigComponent index="5" value="4"/>',
|
||
' <LigComponent index="6" value="0"/>',
|
||
' <LigComponent index="7" value="8"/>',
|
||
' <LigComponent index="8" value="16"/>',
|
||
' </LigComponents>',
|
||
' <Ligatures>',
|
||
' <Ligature glyph="adf" index="0"/>',
|
||
' <Ligature glyph="adg" index="1"/>',
|
||
' <Ligature glyph="adh" index="2"/>',
|
||
' <Ligature glyph="adi" index="3"/>',
|
||
' <Ligature glyph="aef" index="4"/>',
|
||
' <Ligature glyph="aeg" index="5"/>',
|
||
' <Ligature glyph="aeh" index="6"/>',
|
||
' <Ligature glyph="aei" index="7"/>',
|
||
' </Ligatures>',
|
||
' </StateTable>',
|
||
' </LigatureMorph>',
|
||
' </MorphSubtable>',
|
||
'</MorphChain>',
|
||
]
|
||
|
||
|
||
class MORXNoncontextualGlyphSubstitutionTest(unittest.TestCase):
|
||
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
cls.maxDiff = None
|
||
glyphs = ['.notdef'] + ['g.%d' % i for i in range (1, 140)]
|
||
glyphs[11], glyphs[13] = 'parenleft', 'parenright'
|
||
glyphs[135], glyphs[136] = 'parenleft.vertical', 'parenright.vertical'
|
||
cls.font = FakeFont(glyphs)
|
||
|
||
def test_decompile_toXML(self):
|
||
table = newTable('morx')
|
||
table.decompile(MORX_NONCONTEXTUAL_DATA, self.font)
|
||
self.assertEqual(getXML(table.toXML), MORX_NONCONTEXTUAL_XML)
|
||
|
||
def test_compile_fromXML(self):
|
||
table = newTable('morx')
|
||
for name, attrs, content in parseXML(MORX_NONCONTEXTUAL_XML):
|
||
table.fromXML(name, attrs, content, font=self.font)
|
||
self.assertEqual(hexStr(table.compile(self.font)),
|
||
hexStr(MORX_NONCONTEXTUAL_DATA))
|
||
|
||
|
||
class MORXRearrangementTest(unittest.TestCase):
|
||
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
cls.maxDiff = None
|
||
cls.font = FakeFont(['.nodef', 'A', 'B', 'C'])
|
||
|
||
def test_decompile_toXML(self):
|
||
table = newTable('morx')
|
||
table.decompile(MORX_REARRANGEMENT_DATA, self.font)
|
||
self.assertEqual(getXML(table.toXML), MORX_REARRANGEMENT_XML)
|
||
|
||
def test_compile_fromXML(self):
|
||
table = newTable('morx')
|
||
for name, attrs, content in parseXML(MORX_REARRANGEMENT_XML):
|
||
table.fromXML(name, attrs, content, font=self.font)
|
||
self.assertEqual(hexStr(table.compile(self.font)),
|
||
hexStr(MORX_REARRANGEMENT_DATA))
|
||
|
||
|
||
class MORXContextualSubstitutionTest(unittest.TestCase):
|
||
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
cls.maxDiff = None
|
||
g = ['.notdef'] + ['g.%d' % i for i in range (1, 910)]
|
||
g[80] = 'C'
|
||
g[50], g[52], g[201], g[202] = 'A', 'B', 'X', 'Y'
|
||
g[600], g[601], g[602], g[900] = (
|
||
'A.swash', 'B.swash', 'X.swash', 'Y.swash')
|
||
cls.font = FakeFont(g)
|
||
|
||
def test_decompile_toXML(self):
|
||
table = newTable('morx')
|
||
table.decompile(MORX_CONTEXTUAL_DATA, self.font)
|
||
self.assertEqual(getXML(table.toXML), MORX_CONTEXTUAL_XML)
|
||
|
||
def test_compile_fromXML(self):
|
||
table = newTable('morx')
|
||
for name, attrs, content in parseXML(MORX_CONTEXTUAL_XML):
|
||
table.fromXML(name, attrs, content, font=self.font)
|
||
self.assertEqual(hexStr(table.compile(self.font)),
|
||
hexStr(MORX_CONTEXTUAL_DATA))
|
||
|
||
|
||
class MORXLigatureSubstitutionTest(unittest.TestCase):
|
||
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
cls.maxDiff = None
|
||
g = ['.notdef'] + ['g.%d' % i for i in range (1, 1515)]
|
||
g[20:29] = 'a b c d e f g h i'.split()
|
||
g[1000:1008] = 'adf adg adh adi aef aeg aeh aei'.split()
|
||
g[1008:1016] = 'bdf bdg bdh bdi bef beg beh bei'.split()
|
||
g[1500:1507] = 'cdf cdg cdh cdi cef ceg ceh'.split()
|
||
g[1511] = 'cei'
|
||
cls.font = FakeFont(g)
|
||
|
||
def test_decompile_toXML(self):
|
||
table = newTable('morx')
|
||
table.decompile(MORX_LIGATURE_DATA, self.font)
|
||
self.assertEqual(getXML(table.toXML), MORX_LIGATURE_XML)
|
||
|
||
def test_compile_fromXML(self):
|
||
table = newTable('morx')
|
||
for name, attrs, content in parseXML(MORX_LIGATURE_XML):
|
||
table.fromXML(name, attrs, content, font=self.font)
|
||
self.assertEqual(hexStr(table.compile(self.font)),
|
||
hexStr(MORX_LIGATURE_DATA))
|
||
|
||
if __name__ == '__main__':
|
||
import sys
|
||
sys.exit(unittest.main())
|