// see https://developer.apple.com/fonts/TrueType-Reference-Manual/RM09/AppendixF.html // and /System/Library/Frameworks/CoreText.framework/Versions/A/Headers/SFNTLayoutTypes.h on a Mac const features = { allTypographicFeatures: { code: 0, exclusive: false, allTypeFeatures: 0 }, ligatures: { code: 1, exclusive: false, requiredLigatures: 0, commonLigatures: 2, rareLigatures: 4, // logos: 6 rebusPictures: 8, diphthongLigatures: 10, squaredLigatures: 12, abbrevSquaredLigatures: 14, symbolLigatures: 16, contextualLigatures: 18, historicalLigatures: 20 }, cursiveConnection: { code: 2, exclusive: true, unconnected: 0, partiallyConnected: 1, cursive: 2 }, letterCase: { code: 3, exclusive: true }, // upperAndLowerCase: 0 # deprecated // allCaps: 1 # deprecated // allLowerCase: 2 # deprecated // smallCaps: 3 # deprecated // initialCaps: 4 # deprecated // initialCapsAndSmallCaps: 5 # deprecated verticalSubstitution: { code: 4, exclusive: false, substituteVerticalForms: 0 }, linguisticRearrangement: { code: 5, exclusive: false, linguisticRearrangement: 0 }, numberSpacing: { code: 6, exclusive: true, monospacedNumbers: 0, proportionalNumbers: 1, thirdWidthNumbers: 2, quarterWidthNumbers: 3 }, smartSwash: { code: 8, exclusive: false, wordInitialSwashes: 0, wordFinalSwashes: 2, // lineInitialSwashes: 4 // lineFinalSwashes: 6 nonFinalSwashes: 8 }, diacritics: { code: 9, exclusive: true, showDiacritics: 0, hideDiacritics: 1, decomposeDiacritics: 2 }, verticalPosition: { code: 10, exclusive: true, normalPosition: 0, superiors: 1, inferiors: 2, ordinals: 3, scientificInferiors: 4 }, fractions: { code: 11, exclusive: true, noFractions: 0, verticalFractions: 1, diagonalFractions: 2 }, overlappingCharacters: { code: 13, exclusive: false, preventOverlap: 0 }, typographicExtras: { code: 14, exclusive: false, // hyphensToEmDash: 0 // hyphenToEnDash: 2 slashedZero: 4 }, // formInterrobang: 6 // smartQuotes: 8 // periodsToEllipsis: 10 mathematicalExtras: { code: 15, exclusive: false, // hyphenToMinus: 0 // asteristoMultiply: 2 // slashToDivide: 4 // inequalityLigatures: 6 // exponents: 8 mathematicalGreek: 10 }, ornamentSets: { code: 16, exclusive: true, noOrnaments: 0, dingbats: 1, piCharacters: 2, fleurons: 3, decorativeBorders: 4, internationalSymbols: 5, mathSymbols: 6 }, characterAlternatives: { code: 17, exclusive: true, noAlternates: 0 }, // user defined options designComplexity: { code: 18, exclusive: true, designLevel1: 0, designLevel2: 1, designLevel3: 2, designLevel4: 3, designLevel5: 4 }, styleOptions: { code: 19, exclusive: true, noStyleOptions: 0, displayText: 1, engravedText: 2, illuminatedCaps: 3, titlingCaps: 4, tallCaps: 5 }, characterShape: { code: 20, exclusive: true, traditionalCharacters: 0, simplifiedCharacters: 1, JIS1978Characters: 2, JIS1983Characters: 3, JIS1990Characters: 4, traditionalAltOne: 5, traditionalAltTwo: 6, traditionalAltThree: 7, traditionalAltFour: 8, traditionalAltFive: 9, expertCharacters: 10, JIS2004Characters: 11, hojoCharacters: 12, NLCCharacters: 13, traditionalNamesCharacters: 14 }, numberCase: { code: 21, exclusive: true, lowerCaseNumbers: 0, upperCaseNumbers: 1 }, textSpacing: { code: 22, exclusive: true, proportionalText: 0, monospacedText: 1, halfWidthText: 2, thirdWidthText: 3, quarterWidthText: 4, altProportionalText: 5, altHalfWidthText: 6 }, transliteration: { code: 23, exclusive: true, noTransliteration: 0 }, // hanjaToHangul: 1 // hiraganaToKatakana: 2 // katakanaToHiragana: 3 // kanaToRomanization: 4 // romanizationToHiragana: 5 // romanizationToKatakana: 6 // hanjaToHangulAltOne: 7 // hanjaToHangulAltTwo: 8 // hanjaToHangulAltThree: 9 annotation: { code: 24, exclusive: true, noAnnotation: 0, boxAnnotation: 1, roundedBoxAnnotation: 2, circleAnnotation: 3, invertedCircleAnnotation: 4, parenthesisAnnotation: 5, periodAnnotation: 6, romanNumeralAnnotation: 7, diamondAnnotation: 8, invertedBoxAnnotation: 9, invertedRoundedBoxAnnotation: 10 }, kanaSpacing: { code: 25, exclusive: true, fullWidthKana: 0, proportionalKana: 1 }, ideographicSpacing: { code: 26, exclusive: true, fullWidthIdeographs: 0, proportionalIdeographs: 1, halfWidthIdeographs: 2 }, unicodeDecomposition: { code: 27, exclusive: false, canonicalComposition: 0, compatibilityComposition: 2, transcodingComposition: 4 }, rubyKana: { code: 28, exclusive: false, // noRubyKana: 0 # deprecated - use rubyKanaOff instead // rubyKana: 1 # deprecated - use rubyKanaOn instead rubyKana: 2 }, CJKSymbolAlternatives: { code: 29, exclusive: true, noCJKSymbolAlternatives: 0, CJKSymbolAltOne: 1, CJKSymbolAltTwo: 2, CJKSymbolAltThree: 3, CJKSymbolAltFour: 4, CJKSymbolAltFive: 5 }, ideographicAlternatives: { code: 30, exclusive: true, noIdeographicAlternatives: 0, ideographicAltOne: 1, ideographicAltTwo: 2, ideographicAltThree: 3, ideographicAltFour: 4, ideographicAltFive: 5 }, CJKVerticalRomanPlacement: { code: 31, exclusive: true, CJKVerticalRomanCentered: 0, CJKVerticalRomanHBaseline: 1 }, italicCJKRoman: { code: 32, exclusive: false, // noCJKItalicRoman: 0 # deprecated - use CJKItalicRomanOff instead // CJKItalicRoman: 1 # deprecated - use CJKItalicRomanOn instead CJKItalicRoman: 2 }, caseSensitiveLayout: { code: 33, exclusive: false, caseSensitiveLayout: 0, caseSensitiveSpacing: 2 }, alternateKana: { code: 34, exclusive: false, alternateHorizKana: 0, alternateVertKana: 2 }, stylisticAlternatives: { code: 35, exclusive: false, noStylisticAlternates: 0, stylisticAltOne: 2, stylisticAltTwo: 4, stylisticAltThree: 6, stylisticAltFour: 8, stylisticAltFive: 10, stylisticAltSix: 12, stylisticAltSeven: 14, stylisticAltEight: 16, stylisticAltNine: 18, stylisticAltTen: 20, stylisticAltEleven: 22, stylisticAltTwelve: 24, stylisticAltThirteen: 26, stylisticAltFourteen: 28, stylisticAltFifteen: 30, stylisticAltSixteen: 32, stylisticAltSeventeen: 34, stylisticAltEighteen: 36, stylisticAltNineteen: 38, stylisticAltTwenty: 40 }, contextualAlternates: { code: 36, exclusive: false, contextualAlternates: 0, swashAlternates: 2, contextualSwashAlternates: 4 }, lowerCase: { code: 37, exclusive: true, defaultLowerCase: 0, lowerCaseSmallCaps: 1, lowerCasePetiteCaps: 2 }, upperCase: { code: 38, exclusive: true, defaultUpperCase: 0, upperCaseSmallCaps: 1, upperCasePetiteCaps: 2 }, languageTag: { // indices into ltag table code: 39, exclusive: true }, CJKRomanSpacing: { code: 103, exclusive: true, halfWidthCJKRoman: 0, proportionalCJKRoman: 1, defaultCJKRoman: 2, fullWidthCJKRoman: 3 } }; const feature = (name, selector) => [features[name].code, features[name][selector]]; const OTMapping = { rlig: feature('ligatures', 'requiredLigatures'), clig: feature('ligatures', 'contextualLigatures'), dlig: feature('ligatures', 'rareLigatures'), hlig: feature('ligatures', 'historicalLigatures'), liga: feature('ligatures', 'commonLigatures'), hist: feature('ligatures', 'historicalLigatures'), // ?? smcp: feature('lowerCase', 'lowerCaseSmallCaps'), pcap: feature('lowerCase', 'lowerCasePetiteCaps'), frac: feature('fractions', 'diagonalFractions'), dnom: feature('fractions', 'diagonalFractions'), // ?? numr: feature('fractions', 'diagonalFractions'), // ?? afrc: feature('fractions', 'verticalFractions'), // aalt // abvf, abvm, abvs, akhn, blwf, blwm, blws, cfar, cjct, cpsp, falt, isol, jalt, ljmo, mset? // ltra, ltrm, nukt, pref, pres, pstf, psts, rand, rkrf, rphf, rtla, rtlm, size, tjmo, tnum? // unic, vatu, vhal, vjmo, vpal, vrt2 // dist -> trak table? // kern, vkrn -> kern table // lfbd + opbd + rtbd -> opbd table? // mark, mkmk -> acnt table? // locl -> languageTag + ltag table case: feature('caseSensitiveLayout', 'caseSensitiveLayout'), // also caseSensitiveSpacing ccmp: feature('unicodeDecomposition', 'canonicalComposition'), // compatibilityComposition? cpct: feature('CJKVerticalRomanPlacement', 'CJKVerticalRomanCentered'), // guess..., probably not given below valt: feature('CJKVerticalRomanPlacement', 'CJKVerticalRomanCentered'), swsh: feature('contextualAlternates', 'swashAlternates'), cswh: feature('contextualAlternates', 'contextualSwashAlternates'), curs: feature('cursiveConnection', 'cursive'), // ?? c2pc: feature('upperCase', 'upperCasePetiteCaps'), c2sc: feature('upperCase', 'upperCaseSmallCaps'), init: feature('smartSwash', 'wordInitialSwashes'), // ?? fin2: feature('smartSwash', 'wordFinalSwashes'), // ?? medi: feature('smartSwash', 'nonFinalSwashes'), // ?? med2: feature('smartSwash', 'nonFinalSwashes'), // ?? fin3: feature('smartSwash', 'wordFinalSwashes'), // ?? fina: feature('smartSwash', 'wordFinalSwashes'), // ?? pkna: feature('kanaSpacing', 'proportionalKana'), half: feature('textSpacing', 'halfWidthText'), // also HalfWidthCJKRoman, HalfWidthIdeographs? halt: feature('textSpacing', 'altHalfWidthText'), hkna: feature('alternateKana', 'alternateHorizKana'), vkna: feature('alternateKana', 'alternateVertKana'), // hngl: feature 'transliteration', 'hanjaToHangulSelector' # deprecated ital: feature('italicCJKRoman', 'CJKItalicRoman'), lnum: feature('numberCase', 'upperCaseNumbers'), onum: feature('numberCase', 'lowerCaseNumbers'), mgrk: feature('mathematicalExtras', 'mathematicalGreek'), // nalt: not enough info. what type of annotation? // ornm: ditto, which ornament style? calt: feature('contextualAlternates', 'contextualAlternates'), // or more? vrt2: feature('verticalSubstitution', 'substituteVerticalForms'), // oh... below? vert: feature('verticalSubstitution', 'substituteVerticalForms'), tnum: feature('numberSpacing', 'monospacedNumbers'), pnum: feature('numberSpacing', 'proportionalNumbers'), sups: feature('verticalPosition', 'superiors'), subs: feature('verticalPosition', 'inferiors'), ordn: feature('verticalPosition', 'ordinals'), pwid: feature('textSpacing', 'proportionalText'), hwid: feature('textSpacing', 'halfWidthText'), qwid: feature('textSpacing', 'quarterWidthText'), // also QuarterWidthNumbers? twid: feature('textSpacing', 'thirdWidthText'), // also ThirdWidthNumbers? fwid: feature('textSpacing', 'proportionalText'), //?? palt: feature('textSpacing', 'altProportionalText'), trad: feature('characterShape', 'traditionalCharacters'), smpl: feature('characterShape', 'simplifiedCharacters'), jp78: feature('characterShape', 'JIS1978Characters'), jp83: feature('characterShape', 'JIS1983Characters'), jp90: feature('characterShape', 'JIS1990Characters'), jp04: feature('characterShape', 'JIS2004Characters'), expt: feature('characterShape', 'expertCharacters'), hojo: feature('characterShape', 'hojoCharacters'), nlck: feature('characterShape', 'NLCCharacters'), tnam: feature('characterShape', 'traditionalNamesCharacters'), ruby: feature('rubyKana', 'rubyKana'), titl: feature('styleOptions', 'titlingCaps'), zero: feature('typographicExtras', 'slashedZero'), ss01: feature('stylisticAlternatives', 'stylisticAltOne'), ss02: feature('stylisticAlternatives', 'stylisticAltTwo'), ss03: feature('stylisticAlternatives', 'stylisticAltThree'), ss04: feature('stylisticAlternatives', 'stylisticAltFour'), ss05: feature('stylisticAlternatives', 'stylisticAltFive'), ss06: feature('stylisticAlternatives', 'stylisticAltSix'), ss07: feature('stylisticAlternatives', 'stylisticAltSeven'), ss08: feature('stylisticAlternatives', 'stylisticAltEight'), ss09: feature('stylisticAlternatives', 'stylisticAltNine'), ss10: feature('stylisticAlternatives', 'stylisticAltTen'), ss11: feature('stylisticAlternatives', 'stylisticAltEleven'), ss12: feature('stylisticAlternatives', 'stylisticAltTwelve'), ss13: feature('stylisticAlternatives', 'stylisticAltThirteen'), ss14: feature('stylisticAlternatives', 'stylisticAltFourteen'), ss15: feature('stylisticAlternatives', 'stylisticAltFifteen'), ss16: feature('stylisticAlternatives', 'stylisticAltSixteen'), ss17: feature('stylisticAlternatives', 'stylisticAltSeventeen'), ss18: feature('stylisticAlternatives', 'stylisticAltEighteen'), ss19: feature('stylisticAlternatives', 'stylisticAltNineteen'), ss20: feature('stylisticAlternatives', 'stylisticAltTwenty') }; // salt: feature 'stylisticAlternatives', 'stylisticAltOne' # hmm, which one to choose // Add cv01-cv99 features for (let i = 1; i <= 99; i++) { OTMapping[`cv${`00${i}`.slice(-2)}`] = [features.characterAlternatives.code, i]; } // create inverse mapping let AATMapping = {}; for (let ot in OTMapping) { let aat = OTMapping[ot]; if (AATMapping[aat[0]] == null) { AATMapping[aat[0]] = {}; } AATMapping[aat[0]][aat[1]] = ot; } // Maps an array of OpenType features to AAT features // in the form of {featureType:{featureSetting:true}} export function mapOTToAAT(features) { let res = {}; for (let k in features) { let r; if (r = OTMapping[k]) { if (res[r[0]] == null) { res[r[0]] = {}; } res[r[0]][r[1]] = features[k]; } } return res; } // Maps strings in a [featureType, featureSetting] // to their equivalent number codes function mapFeatureStrings(f) { let [type, setting] = f; if (isNaN(type)) { var typeCode = features[type] && features[type].code; } else { var typeCode = type; } if (isNaN(setting)) { var settingCode = features[type] && features[type][setting]; } else { var settingCode = setting; } return [typeCode, settingCode]; } // Maps AAT features to an array of OpenType features // Supports both arrays in the form of [[featureType, featureSetting]] // and objects in the form of {featureType:{featureSetting:true}} // featureTypes and featureSettings can be either strings or number codes export function mapAATToOT(features) { let res = {}; if (Array.isArray(features)) { for (let k = 0; k < features.length; k++) { let r; let f = mapFeatureStrings(features[k]); if (r = AATMapping[f[0]] && AATMapping[f[0]][f[1]]) { res[r] = true; } } } else if (typeof features === 'object') { for (let type in features) { let feature = features[type]; for (let setting in feature) { let r; let f = mapFeatureStrings([type, setting]); if (feature[setting] && (r = AATMapping[f[0]] && AATMapping[f[0]][f[1]])) { res[r] = true; } } } } return Object.keys(res); }