| 1 | """ |
|---|
| 2 | GSUB, GPOS and GDEF table objects. |
|---|
| 3 | """ |
|---|
| 4 | |
|---|
| 5 | try: |
|---|
| 6 | set |
|---|
| 7 | except NameError: |
|---|
| 8 | from sets import Set as set |
|---|
| 9 | |
|---|
| 10 | try: |
|---|
| 11 | sorted |
|---|
| 12 | except NameError: |
|---|
| 13 | def sorted(iterable): |
|---|
| 14 | if not isinstance(iterable, list): |
|---|
| 15 | iterable = list(iterable) |
|---|
| 16 | iterable.sort() |
|---|
| 17 | return iterable |
|---|
| 18 | |
|---|
| 19 | import unicodedata |
|---|
| 20 | from cmap import reverseCMAP |
|---|
| 21 | from scriptList import ScriptList |
|---|
| 22 | from featureList import FeatureList |
|---|
| 23 | from lookupList import GSUBLookupList, GPOSLookupList |
|---|
| 24 | from classDefinitionTables import MarkAttachClassDef, GlyphClassDef |
|---|
| 25 | |
|---|
| 26 | |
|---|
| 27 | defaultOnFeatures = [ |
|---|
| 28 | # GSUB |
|---|
| 29 | "calt", |
|---|
| 30 | "ccmp", # this should always be the first feature processed |
|---|
| 31 | "clig", |
|---|
| 32 | "fina", |
|---|
| 33 | "half", # applies only to indic |
|---|
| 34 | "init", |
|---|
| 35 | "isol", |
|---|
| 36 | "liga", |
|---|
| 37 | "locl", |
|---|
| 38 | "med2", # applies only to syriac |
|---|
| 39 | "medi", |
|---|
| 40 | "nukt", # applies only to indic |
|---|
| 41 | "pref", # applies only to khmer and myanmar |
|---|
| 42 | "pres", # applies only to indic |
|---|
| 43 | "pstf", # applies only to indic |
|---|
| 44 | "psts", |
|---|
| 45 | "rand", |
|---|
| 46 | "rlig", # applies only to arabic and syriac |
|---|
| 47 | "rphf", # applies only to indic |
|---|
| 48 | "tjmo", # applies only to hangul |
|---|
| 49 | "vatu", # applies only to indic |
|---|
| 50 | "vjmo", # applies only to hangul |
|---|
| 51 | # GPOS |
|---|
| 52 | "abvm", # applies only to indic |
|---|
| 53 | "blwm", # applies only to indic |
|---|
| 54 | "kern", |
|---|
| 55 | "mark", |
|---|
| 56 | "mkmk", |
|---|
| 57 | "opbd", |
|---|
| 58 | "vkrn" |
|---|
| 59 | ] |
|---|
| 60 | |
|---|
| 61 | |
|---|
| 62 | class BaseTable(object): |
|---|
| 63 | |
|---|
| 64 | def __init__(self, table, reversedCMAP, gdef): |
|---|
| 65 | self.ScriptList = ScriptList(table.table.ScriptList) |
|---|
| 66 | self.FeatureList = FeatureList(table.table.FeatureList) |
|---|
| 67 | self.LookupList = self._LookupListClass(table.table.LookupList, self, gdef) |
|---|
| 68 | |
|---|
| 69 | self._cmap = reversedCMAP |
|---|
| 70 | |
|---|
| 71 | self._featureApplicationStates = {} |
|---|
| 72 | self._applicableFeatureCache = {} |
|---|
| 73 | self._featureTags = None |
|---|
| 74 | self.getFeatureList() |
|---|
| 75 | self._setDefaultFeatureApplicationStates() |
|---|
| 76 | |
|---|
| 77 | def process(self, glyphRecords, script="latn", langSys=None, logger=None): |
|---|
| 78 | """ |
|---|
| 79 | Pass the list of GlyphRecord objects through the features |
|---|
| 80 | applicable for the given script and langSys. This returns |
|---|
| 81 | a list of processed GlyphRecord objects. |
|---|
| 82 | """ |
|---|
| 83 | applicableLookups = self._preprocess(script, langSys) |
|---|
| 84 | if logger: |
|---|
| 85 | logger.logApplicableLookups(self, applicableLookups) |
|---|
| 86 | logger.logProcessingStart() |
|---|
| 87 | result = self._processLookups(glyphRecords, applicableLookups, logger=logger) |
|---|
| 88 | if logger: |
|---|
| 89 | logger.logProcessingEnd() |
|---|
| 90 | return result |
|---|
| 91 | |
|---|
| 92 | # ------------------ |
|---|
| 93 | # feature management |
|---|
| 94 | # ------------------ |
|---|
| 95 | |
|---|
| 96 | def _setDefaultFeatureApplicationStates(self): |
|---|
| 97 | """ |
|---|
| 98 | Activate all features defined as on by |
|---|
| 99 | default in the Layout Tag Registry. |
|---|
| 100 | """ |
|---|
| 101 | for tag in self._featureTags: |
|---|
| 102 | if tag in defaultOnFeatures: |
|---|
| 103 | state = True |
|---|
| 104 | else: |
|---|
| 105 | state = False |
|---|
| 106 | self._featureApplicationStates[tag] = state |
|---|
| 107 | |
|---|
| 108 | def __contains__(self, featureTag): |
|---|
| 109 | return featureTag in self._featureTags |
|---|
| 110 | |
|---|
| 111 | def getFeatureList(self): |
|---|
| 112 | """ |
|---|
| 113 | Get a list of all available features in the table. |
|---|
| 114 | """ |
|---|
| 115 | if self._featureTags is None: |
|---|
| 116 | featureList = self.FeatureList |
|---|
| 117 | featureRecords = featureList.FeatureRecord |
|---|
| 118 | self._featureTags = [] |
|---|
| 119 | for featureRecord in featureRecords: |
|---|
| 120 | tag = featureRecord.FeatureTag |
|---|
| 121 | if tag not in self._featureTags: |
|---|
| 122 | self._featureTags.append(tag) |
|---|
| 123 | return self._featureTags |
|---|
| 124 | |
|---|
| 125 | def getFeatureState(self, featureTag): |
|---|
| 126 | """ |
|---|
| 127 | Get a boolean representing if a feature is on or not. |
|---|
| 128 | """ |
|---|
| 129 | return self._featureApplicationStates[featureTag] |
|---|
| 130 | |
|---|
| 131 | def setFeatureState(self, featureTag, state): |
|---|
| 132 | """ |
|---|
| 133 | Set the application state of a feature. |
|---|
| 134 | """ |
|---|
| 135 | self._featureApplicationStates[featureTag] = state |
|---|
| 136 | |
|---|
| 137 | # ------------- |
|---|
| 138 | # preprocessing |
|---|
| 139 | # ------------- |
|---|
| 140 | |
|---|
| 141 | def _preprocess(self, script, langSys): |
|---|
| 142 | """ |
|---|
| 143 | Get a list of ordered (featureTag, lookupObject) |
|---|
| 144 | for the given script and langSys. |
|---|
| 145 | """ |
|---|
| 146 | # 1. get a list of applicable feature records |
|---|
| 147 | # based on the script and langSys |
|---|
| 148 | features = self._getApplicableFeatures(script, langSys) |
|---|
| 149 | # 2. get a list of applicable lookup tables based on the |
|---|
| 150 | # found features and the feature application states |
|---|
| 151 | lookupIndexes = set() |
|---|
| 152 | for feature in features: |
|---|
| 153 | featureTag = feature.FeatureTag |
|---|
| 154 | if not self._featureApplicationStates[featureTag]: |
|---|
| 155 | continue |
|---|
| 156 | featureRecord = feature.Feature |
|---|
| 157 | if featureRecord.LookupCount: |
|---|
| 158 | for lookupIndex in featureRecord.LookupListIndex: |
|---|
| 159 | lookupIndexes.add((lookupIndex, featureTag)) |
|---|
| 160 | # 3. get a list of ordered lookup records for each feature |
|---|
| 161 | lookupList = self.LookupList |
|---|
| 162 | lookupRecords = lookupList.Lookup |
|---|
| 163 | applicableLookups = [] |
|---|
| 164 | for lookupIndex, featureTag in sorted(lookupIndexes): |
|---|
| 165 | lookup = lookupRecords[lookupIndex] |
|---|
| 166 | applicableLookups.append((featureTag, lookup)) |
|---|
| 167 | return applicableLookups |
|---|
| 168 | |
|---|
| 169 | def _getApplicableFeatures(self, script, langSys): |
|---|
| 170 | """ |
|---|
| 171 | Get a list of features that apply to |
|---|
| 172 | a particular script and langSys. Both |
|---|
| 173 | script and langSys can be None. However, |
|---|
| 174 | if script is None and no script record |
|---|
| 175 | in the font is assigned to DFLT, no |
|---|
| 176 | features wil be found. |
|---|
| 177 | """ |
|---|
| 178 | # first check to see if this has already been found |
|---|
| 179 | if (script, langSys) in self._applicableFeatureCache: |
|---|
| 180 | return self._applicableFeatureCache[script, langSys] |
|---|
| 181 | scriptList = self.ScriptList |
|---|
| 182 | # 1. Find the appropriate script record |
|---|
| 183 | scriptRecords = scriptList.ScriptRecord |
|---|
| 184 | defaultScript = None |
|---|
| 185 | applicableScript = None |
|---|
| 186 | for scriptRecord in scriptRecords: |
|---|
| 187 | scriptTag = scriptRecord.ScriptTag |
|---|
| 188 | if scriptTag == "DFLT": |
|---|
| 189 | defaultScript = scriptRecord.Script |
|---|
| 190 | continue |
|---|
| 191 | if scriptTag == script: |
|---|
| 192 | applicableScript = scriptRecord.Script |
|---|
| 193 | break |
|---|
| 194 | # 2. if no suitable script record was found, return an empty list |
|---|
| 195 | if applicableScript is None: |
|---|
| 196 | applicableScript = defaultScript |
|---|
| 197 | if applicableScript is None: |
|---|
| 198 | return [] |
|---|
| 199 | # 3. get the applicable langSys records |
|---|
| 200 | defaultLangSys = applicableScript.DefaultLangSys |
|---|
| 201 | specificLangSys = None |
|---|
| 202 | # if we have a langSys and the table |
|---|
| 203 | # defines specific langSys behavior, |
|---|
| 204 | # try to find a matching langSys record |
|---|
| 205 | if langSys is not None and applicableScript.LangSysCount: |
|---|
| 206 | for langSysRecord in applicableScript.LangSysRecord: |
|---|
| 207 | langSysTag = langSysRecord.LangSysTag |
|---|
| 208 | if langSysTag == langSys: |
|---|
| 209 | specificLangSys = langSysRecord.LangSys |
|---|
| 210 | break |
|---|
| 211 | # 4. get the list of applicable features |
|---|
| 212 | applicableFeatures = set() |
|---|
| 213 | if defaultLangSys.FeatureCount: |
|---|
| 214 | applicableFeatures |= set(defaultLangSys.FeatureIndex) |
|---|
| 215 | if defaultLangSys.ReqFeatureIndex != 0xFFFF: |
|---|
| 216 | applicableFeatures.add(defaultLangSys.ReqFeatureIndex) |
|---|
| 217 | if specificLangSys is not None: |
|---|
| 218 | if specificLangSys.FeatureCount: |
|---|
| 219 | applicableFeatures |= set(specificLangSys.FeatureIndex) |
|---|
| 220 | if specificLangSys.ReqFeatureIndex != 0xFFFF: |
|---|
| 221 | applicableFeatures.add(specificLangSys.ReqFeatureIndex) |
|---|
| 222 | applicableFeatures = self._getFeatures(applicableFeatures) |
|---|
| 223 | # store the found features for potential use by this method |
|---|
| 224 | self._applicableFeatureCache[script, langSys] = applicableFeatures |
|---|
| 225 | return applicableFeatures |
|---|
| 226 | |
|---|
| 227 | def _getFeatures(self, indices): |
|---|
| 228 | """ |
|---|
| 229 | Get a list of ordered features located at indices. |
|---|
| 230 | """ |
|---|
| 231 | featureList = self.FeatureList |
|---|
| 232 | featureRecords = featureList.FeatureRecord |
|---|
| 233 | features = [featureRecords[index] for index in sorted(indices)] |
|---|
| 234 | return features |
|---|
| 235 | |
|---|
| 236 | def _getLookups(self, indices): |
|---|
| 237 | """ |
|---|
| 238 | Get a list of ordered lookups at indices |
|---|
| 239 | """ |
|---|
| 240 | lookupList = self.LookupList |
|---|
| 241 | lookupRecords = lookupList.Lookup |
|---|
| 242 | lookups = [lookupRecords[index] for index in sorted(indices)] |
|---|
| 243 | return lookups |
|---|
| 244 | |
|---|
| 245 | # ---------- |
|---|
| 246 | # processing |
|---|
| 247 | # ---------- |
|---|
| 248 | |
|---|
| 249 | def _processLookups(self, glyphRecords, lookups, processingAalt=False, logger=None): |
|---|
| 250 | aaltHolding = [] |
|---|
| 251 | whitespaceSensitive = set(["init", "medi", "fina", "isol"]) |
|---|
| 252 | for featureTag, lookup in lookups: |
|---|
| 253 | # store aalt for processing at the end |
|---|
| 254 | if not processingAalt and featureTag == "aalt": |
|---|
| 255 | aaltHolding.append((featureTag, lookup)) |
|---|
| 256 | continue |
|---|
| 257 | if logger: |
|---|
| 258 | logger.logLookupStart(self, featureTag, lookup) |
|---|
| 259 | processed = [] |
|---|
| 260 | # init, medi and fina need to be aware |
|---|
| 261 | # of word boundaries. determining the |
|---|
| 262 | # boundaries incurs some expense, so |
|---|
| 263 | # only do it when necessary. |
|---|
| 264 | testForWhitespace = featureTag in whitespaceSensitive |
|---|
| 265 | if testForWhitespace: |
|---|
| 266 | previousWasWhitespace = True |
|---|
| 267 | nextIsWhiteSpace = self._nextIsWhitespace(glyphRecords) |
|---|
| 268 | # loop through the glyph records |
|---|
| 269 | while glyphRecords: |
|---|
| 270 | skip = False |
|---|
| 271 | if testForWhitespace: |
|---|
| 272 | previousWasWhitespace = self._previousWasWhitespace(processed) |
|---|
| 273 | nextIsWhiteSpace = self._nextIsWhitespace(glyphRecords) |
|---|
| 274 | if featureTag == "init" and not previousWasWhitespace: |
|---|
| 275 | skip = True |
|---|
| 276 | elif featureTag == "fina" and not nextIsWhiteSpace: |
|---|
| 277 | skip = True |
|---|
| 278 | elif featureTag == "medi": |
|---|
| 279 | if previousWasWhitespace: |
|---|
| 280 | skip = True |
|---|
| 281 | if nextIsWhiteSpace: |
|---|
| 282 | skip = True |
|---|
| 283 | elif featureTag == "isol": |
|---|
| 284 | if not previousWasWhitespace: |
|---|
| 285 | skip = True |
|---|
| 286 | if not nextIsWhiteSpace: |
|---|
| 287 | skip = True |
|---|
| 288 | # loop through the lookups subtables |
|---|
| 289 | performedAction = False |
|---|
| 290 | if not skip: |
|---|
| 291 | processed, glyphRecords, performedAction = self._processLookup(processed, glyphRecords, lookup, featureTag, logger=logger) |
|---|
| 292 | if not performedAction: |
|---|
| 293 | processed.append(glyphRecords[0]) |
|---|
| 294 | glyphRecords = glyphRecords[1:] |
|---|
| 295 | glyphRecords = processed |
|---|
| 296 | if logger: |
|---|
| 297 | logger.logLookupEnd() |
|---|
| 298 | # process aalt for the final glyph records |
|---|
| 299 | if not processingAalt and aaltHolding: |
|---|
| 300 | glyphRecords = self._processLookups(glyphRecords, aaltHolding, processingAalt=True, logger=logger) |
|---|
| 301 | return glyphRecords |
|---|
| 302 | |
|---|
| 303 | def _processLookup(self, processed, glyphRecords, lookup, featureTag, logger=None): |
|---|
| 304 | performedAction = False |
|---|
| 305 | for subtable in lookup.SubTable: |
|---|
| 306 | if logger: |
|---|
| 307 | logger.logSubTableStart(lookup, subtable) |
|---|
| 308 | logger.logInput(processed, glyphRecords) |
|---|
| 309 | processed, glyphRecords, performedAction = subtable.process(processed, glyphRecords, featureTag) |
|---|
| 310 | if logger: |
|---|
| 311 | if performedAction: |
|---|
| 312 | logger.logOutput(processed, glyphRecords) |
|---|
| 313 | logger.logSubTableEnd() |
|---|
| 314 | if performedAction: |
|---|
| 315 | break |
|---|
| 316 | return processed, glyphRecords, performedAction |
|---|
| 317 | |
|---|
| 318 | # ------------------ |
|---|
| 319 | # whitespace testing |
|---|
| 320 | # ------------------ |
|---|
| 321 | |
|---|
| 322 | def _isWhitespace(self, glyphRecord): |
|---|
| 323 | glyphName = glyphRecord.glyphName |
|---|
| 324 | if glyphName not in self._cmap: |
|---|
| 325 | return False |
|---|
| 326 | for uniValue in self._cmap[glyphName]: |
|---|
| 327 | uniChr = unichr(uniValue) |
|---|
| 328 | if unicodedata.category(uniChr) == "Zs": |
|---|
| 329 | return True |
|---|
| 330 | return False |
|---|
| 331 | |
|---|
| 332 | def _previousWasWhitespace(self, processed): |
|---|
| 333 | if not processed: |
|---|
| 334 | return True |
|---|
| 335 | glyphRecord = processed[-1] |
|---|
| 336 | return self._isWhitespace(glyphRecord) |
|---|
| 337 | |
|---|
| 338 | def _nextIsWhitespace(self, glyphRecords): |
|---|
| 339 | if len(glyphRecords) < 2: |
|---|
| 340 | return True |
|---|
| 341 | glyphRecord = glyphRecords[1] |
|---|
| 342 | return self._isWhitespace(glyphRecord) |
|---|
| 343 | |
|---|
| 344 | |
|---|
| 345 | class GSUB(BaseTable): |
|---|
| 346 | |
|---|
| 347 | _LookupListClass = GSUBLookupList |
|---|
| 348 | |
|---|
| 349 | |
|---|
| 350 | class GPOS(BaseTable): |
|---|
| 351 | |
|---|
| 352 | _LookupListClass = GPOSLookupList |
|---|
| 353 | |
|---|
| 354 | |
|---|
| 355 | class GDEF(object): |
|---|
| 356 | |
|---|
| 357 | def __init__(self, table): |
|---|
| 358 | table = table.table |
|---|
| 359 | self.GlyphClassDef = GlyphClassDef(table.GlyphClassDef) |
|---|
| 360 | if table.AttachList is not None: |
|---|
| 361 | raise NotImplementedError("Need GDEF sample with AttachList") |
|---|
| 362 | if table.LigCaretList is not None: |
|---|
| 363 | raise NotImplementedError("Need GDEF sample with LigCaretList") |
|---|
| 364 | if table.MarkAttachClassDef is None: |
|---|
| 365 | self.MarkAttachClassDef = None |
|---|
| 366 | else: |
|---|
| 367 | self.MarkAttachClassDef = MarkAttachClassDef(table.MarkAttachClassDef) |
|---|
| 368 | |
|---|