if not modules then modules = { } end modules ['font-otf'] = { version = 1.001, comment = "companion to font-ini.tex", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- once we have all features working, i will redo this module .. caching lookups and such -- the flattening code is a prelude to a more compact table format (so, we're now -- at the fourth version); maybe we will go unicode, although that will mean that we -- miss some glyphs (unicode -1) -- todo: featuredata is now indexed by kind,lookup but probably lookup is okay too -- todo: now that we pack ... resolve strings to unicode points -- todo: unpack already in tmc file, i.e. save tables and return ref''d version -- todo: dependents etc resolve too, maybe even reorder glyphs to unicode -- todo: pack ignoreflags -- abvf abvs blwf blws dist falt half halt jalt lfbd ljmo -- mset opbd palt pwid qwid rand rtbd rtla ruby size tjmo twid valt vatu vert -- vhal vjmo vkna vkrn vpal vrt2 -- The specification of OpenType is vague, very vague. Apart from a lack of proper -- specifications (a free one) there's also the problem that Microsoft and Adobe -- may have their own rules. Anyhow, the following is from Adobe's feature file -- specification: -- -- http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html#6.h -- -- The following is a reference summary of the algorithm used by an OpenType layout -- (OTL) engine to perform substitutions and positionings. The important aspect of -- this for a feature file editor is that each lookup corresponds to one "pass" over -- the glyph run (see step 4 below). Thus, each lookup has as input the accumulated -- result of all previous lookups in the LookupList (whether in the same feature or -- in other features). -- 1. All glyphs in the client's glyph run must belong to the same language -- system. Glyph sequence matching may not occur across language -- systems. Do the following first for the GSUB and then for the GPOS: -- 2. Assemble all features (including any required feature) for the glyph -- run's language system. -- 3. Assemble all lookups in these features, in LookupList order, removing -- any duplicates. All features and thus all lookups needn't be applied to -- every glyph in the run. -- 4. For each lookup: -- 5. For each glyph in the glyph run: -- 6. If the lookup is applied to that glyph and the lookupflag doesn't -- indicate that that glyph is to be ignored: -- 7. For each subtable in the lookup: -- 8. If the subtable's target context is matched: -- 9. Do the glyph substitution or positioning, -- --- OR: --- -- If this is a (chain) contextual lookup do the following -- [(10)-(11)] in the subtable's Subst/PosLookupRecord order: -- 10. For each (sequenceIndex, lookupListIndex) pair: -- 11. Apply lookup[lookupListIndex] at input sequence[sequenceIndex] -- [steps (7)-(11)] -- 12. Goto the glyph after the input sequence matched in (8) -- (i.e. skip any remaining subtables in the lookup). -- The "target context" in step 8 above comprises the input sequence and any -- backtrack and lookahead sequences. -- The input sequence must be matched entirely within the lookup's "application -- range" at that glyph (that contiguous subrun of glyphs including and around -- the current glyph on which the lookup is applied). There is no such restriction -- on the backtrack and lookahead sequences. -- "Matching" includes matching any glyphs designated to be skipped in the -- lookup's LookupFlag. --[[ldx--

This module is sparsely documented because it is a moving target. The table format of the reader changes and we experiment a lot with different methods for supporting features.

As with the code, we may decide to store more information in the table.

Incrementing the version number will force a re-cache. We jump the number by one when there's a fix in the library or code that results in different tables.

--ldx]]-- --~ The node based processing functions look quite complex which is mainly due to --~ the fact that we need to share data and cache resolved issues (saves much memory and --~ is also faster). A further complication is that we support static as well as dynamic --~ features. fonts = fonts or { } fonts.otf = fonts.otf or { } fonts.otf.version = 2.09 fonts.otf.pack = true fonts.otf.tables = fonts.otf.tables or { } fonts.otf.meanings = fonts.otf.meanings or { } fonts.otf.enhance_data = false fonts.otf.syncspace = true fonts.otf.features = { } fonts.otf.features.aux = { } fonts.otf.features.data = { } fonts.otf.features.list = { } -- not (yet) used, oft fonts have gpos/gsub lists fonts.otf.features.default = { } fonts.otf.trace_features = false fonts.otf.trace_set_features = false fonts.otf.trace_replacements = false fonts.otf.trace_contexts = false fonts.otf.trace_anchors = false fonts.otf.trace_ligatures = false fonts.otf.trace_kerns = false fonts.otf.trace_cursive = false fonts.otf.notdef = false fonts.otf.cache = containers.define("fonts", "otf", fonts.otf.version, true) function fonts.otf.trace_process() fonts.otf.trace_replacements = true fonts.otf.trace_contexts = true fonts.otf.trace_anchors = true fonts.otf.trace_ligatures = true fonts.otf.trace_kerns = true fonts.otf.trace_cursive = true end --[[ldx--

We start with a lot of tables and related functions.

--ldx]]-- fonts.otf.tables.scripts = { ['dflt'] = 'Default', ['arab'] = 'Arabic', ['armn'] = 'Armenian', ['bali'] = 'Balinese', ['beng'] = 'Bengali', ['bopo'] = 'Bopomofo', ['brai'] = 'Braille', ['bugi'] = 'Buginese', ['buhd'] = 'Buhid', ['byzm'] = 'Byzantine Music', ['cans'] = 'Canadian Syllabics', ['cher'] = 'Cherokee', ['copt'] = 'Coptic', ['cprt'] = 'Cypriot Syllabary', ['cyrl'] = 'Cyrillic', ['deva'] = 'Devanagari', ['dsrt'] = 'Deseret', ['ethi'] = 'Ethiopic', ['geor'] = 'Georgian', ['glag'] = 'Glagolitic', ['goth'] = 'Gothic', ['grek'] = 'Greek', ['gujr'] = 'Gujarati', ['guru'] = 'Gurmukhi', ['hang'] = 'Hangul', ['hani'] = 'CJK Ideographic', ['hano'] = 'Hanunoo', ['hebr'] = 'Hebrew', ['ital'] = 'Old Italic', ['jamo'] = 'Hangul Jamo', ['java'] = 'Javanese', ['kana'] = 'Hiragana and Katakana', ['khar'] = 'Kharosthi', ['khmr'] = 'Khmer', ['knda'] = 'Kannada', ['lao' ] = 'Lao', ['latn'] = 'Latin', ['limb'] = 'Limbu', ['linb'] = 'Linear B', ['math'] = 'Mathematical Alphanumeric Symbols', ['mlym'] = 'Malayalam', ['mong'] = 'Mongolian', ['musc'] = 'Musical Symbols', ['mymr'] = 'Myanmar', ['nko' ] = "N'ko", ['ogam'] = 'Ogham', ['orya'] = 'Oriya', ['osma'] = 'Osmanya', ['phag'] = 'Phags-pa', ['phnx'] = 'Phoenician', ['runr'] = 'Runic', ['shaw'] = 'Shavian', ['sinh'] = 'Sinhala', ['sylo'] = 'Syloti Nagri', ['syrc'] = 'Syriac', ['tagb'] = 'Tagbanwa', ['tale'] = 'Tai Le', ['talu'] = 'Tai Lu', ['taml'] = 'Tamil', ['telu'] = 'Telugu', ['tfng'] = 'Tifinagh', ['tglg'] = 'Tagalog', ['thaa'] = 'Thaana', ['thai'] = 'Thai', ['tibt'] = 'Tibetan', ['ugar'] = 'Ugaritic Cuneiform', ['xpeo'] = 'Old Persian Cuneiform', ['xsux'] = 'Sumero-Akkadian Cuneiform', ['yi' ] = 'Yi' } fonts.otf.tables.languages = { ['dflt'] = 'Default', ['aba'] = 'Abaza', ['abk'] = 'Abkhazian', ['ady'] = 'Adyghe', ['afk'] = 'Afrikaans', ['afr'] = 'Afar', ['agw'] = 'Agaw', ['als'] = 'Alsatian', ['alt'] = 'Altai', ['amh'] = 'Amharic', ['ara'] = 'Arabic', ['ari'] = 'Aari', ['ark'] = 'Arakanese', ['asm'] = 'Assamese', ['ath'] = 'Athapaskan', ['avr'] = 'Avar', ['awa'] = 'Awadhi', ['aym'] = 'Aymara', ['aze'] = 'Azeri', ['bad'] = 'Badaga', ['bag'] = 'Baghelkhandi', ['bal'] = 'Balkar', ['bau'] = 'Baule', ['bbr'] = 'Berber', ['bch'] = 'Bench', ['bcr'] = 'Bible Cree', ['bel'] = 'Belarussian', ['bem'] = 'Bemba', ['ben'] = 'Bengali', ['bgr'] = 'Bulgarian', ['bhi'] = 'Bhili', ['bho'] = 'Bhojpuri', ['bik'] = 'Bikol', ['bil'] = 'Bilen', ['bkf'] = 'Blackfoot', ['bli'] = 'Balochi', ['bln'] = 'Balante', ['blt'] = 'Balti', ['bmb'] = 'Bambara', ['bml'] = 'Bamileke', ['bos'] = 'Bosnian', ['bre'] = 'Breton', ['brh'] = 'Brahui', ['bri'] = 'Braj Bhasha', ['brm'] = 'Burmese', ['bsh'] = 'Bashkir', ['bti'] = 'Beti', ['cat'] = 'Catalan', ['ceb'] = 'Cebuano', ['che'] = 'Chechen', ['chg'] = 'Chaha Gurage', ['chh'] = 'Chattisgarhi', ['chi'] = 'Chichewa', ['chk'] = 'Chukchi', ['chp'] = 'Chipewyan', ['chr'] = 'Cherokee', ['chu'] = 'Chuvash', ['cmr'] = 'Comorian', ['cop'] = 'Coptic', ['cos'] = 'Corsican', ['cre'] = 'Cree', ['crr'] = 'Carrier', ['crt'] = 'Crimean Tatar', ['csl'] = 'Church Slavonic', ['csy'] = 'Czech', ['dan'] = 'Danish', ['dar'] = 'Dargwa', ['dcr'] = 'Woods Cree', ['deu'] = 'German', ['dgr'] = 'Dogri', ['div'] = 'Divehi', ['djr'] = 'Djerma', ['dng'] = 'Dangme', ['dnk'] = 'Dinka', ['dri'] = 'Dari', ['dun'] = 'Dungan', ['dzn'] = 'Dzongkha', ['ebi'] = 'Ebira', ['ecr'] = 'Eastern Cree', ['edo'] = 'Edo', ['efi'] = 'Efik', ['ell'] = 'Greek', ['eng'] = 'English', ['erz'] = 'Erzya', ['esp'] = 'Spanish', ['eti'] = 'Estonian', ['euq'] = 'Basque', ['evk'] = 'Evenki', ['evn'] = 'Even', ['ewe'] = 'Ewe', ['fan'] = 'French Antillean', ['far'] = 'Farsi', ['fin'] = 'Finnish', ['fji'] = 'Fijian', ['fle'] = 'Flemish', ['fne'] = 'Forest Nenets', ['fon'] = 'Fon', ['fos'] = 'Faroese', ['fra'] = 'French', ['fri'] = 'Frisian', ['frl'] = 'Friulian', ['fta'] = 'Futa', ['ful'] = 'Fulani', ['gad'] = 'Ga', ['gae'] = 'Gaelic', ['gag'] = 'Gagauz', ['gal'] = 'Galician', ['gar'] = 'Garshuni', ['gaw'] = 'Garhwali', ['gez'] = "Ge'ez", ['gil'] = 'Gilyak', ['gmz'] = 'Gumuz', ['gon'] = 'Gondi', ['grn'] = 'Greenlandic', ['gro'] = 'Garo', ['gua'] = 'Guarani', ['guj'] = 'Gujarati', ['hai'] = 'Haitian', ['hal'] = 'Halam', ['har'] = 'Harauti', ['hau'] = 'Hausa', ['haw'] = 'Hawaiin', ['hbn'] = 'Hammer-Banna', ['hil'] = 'Hiligaynon', ['hin'] = 'Hindi', ['hma'] = 'High Mari', ['hnd'] = 'Hindko', ['ho'] = 'Ho', ['hri'] = 'Harari', ['hrv'] = 'Croatian', ['hun'] = 'Hungarian', ['hye'] = 'Armenian', ['ibo'] = 'Igbo', ['ijo'] = 'Ijo', ['ilo'] = 'Ilokano', ['ind'] = 'Indonesian', ['ing'] = 'Ingush', ['inu'] = 'Inuktitut', ['iri'] = 'Irish', ['irt'] = 'Irish Traditional', ['isl'] = 'Icelandic', ['ism'] = 'Inari Sami', ['ita'] = 'Italian', ['iwr'] = 'Hebrew', ['jan'] = 'Japanese', ['jav'] = 'Javanese', ['jii'] = 'Yiddish', ['jud'] = 'Judezmo', ['jul'] = 'Jula', ['kab'] = 'Kabardian', ['kac'] = 'Kachchi', ['kal'] = 'Kalenjin', ['kan'] = 'Kannada', ['kar'] = 'Karachay', ['kat'] = 'Georgian', ['kaz'] = 'Kazakh', ['keb'] = 'Kebena', ['kge'] = 'Khutsuri Georgian', ['kha'] = 'Khakass', ['khk'] = 'Khanty-Kazim', ['khm'] = 'Khmer', ['khs'] = 'Khanty-Shurishkar', ['khv'] = 'Khanty-Vakhi', ['khw'] = 'Khowar', ['kik'] = 'Kikuyu', ['kir'] = 'Kirghiz', ['kis'] = 'Kisii', ['kkn'] = 'Kokni', ['klm'] = 'Kalmyk', ['kmb'] = 'Kamba', ['kmn'] = 'Kumaoni', ['kmo'] = 'Komo', ['kms'] = 'Komso', ['knr'] = 'Kanuri', ['kod'] = 'Kodagu', ['koh'] = 'Korean Old Hangul', ['kok'] = 'Konkani', ['kon'] = 'Kikongo', ['kop'] = 'Komi-Permyak', ['kor'] = 'Korean', ['koz'] = 'Komi-Zyrian', ['kpl'] = 'Kpelle', ['kri'] = 'Krio', ['krk'] = 'Karakalpak', ['krl'] = 'Karelian', ['krm'] = 'Karaim', ['krn'] = 'Karen', ['krt'] = 'Koorete', ['ksh'] = 'Kashmiri', ['ksi'] = 'Khasi', ['ksm'] = 'Kildin Sami', ['kui'] = 'Kui', ['kul'] = 'Kulvi', ['kum'] = 'Kumyk', ['kur'] = 'Kurdish', ['kuu'] = 'Kurukh', ['kuy'] = 'Kuy', ['kyk'] = 'Koryak', ['lad'] = 'Ladin', ['lah'] = 'Lahuli', ['lak'] = 'Lak', ['lam'] = 'Lambani', ['lao'] = 'Lao', ['lat'] = 'Latin', ['laz'] = 'Laz', ['lcr'] = 'L-Cree', ['ldk'] = 'Ladakhi', ['lez'] = 'Lezgi', ['lin'] = 'Lingala', ['lma'] = 'Low Mari', ['lmb'] = 'Limbu', ['lmw'] = 'Lomwe', ['lsb'] = 'Lower Sorbian', ['lsm'] = 'Lule Sami', ['lth'] = 'Lithuanian', ['ltz'] = 'Luxembourgish', ['lub'] = 'Luba', ['lug'] = 'Luganda', ['luh'] = 'Luhya', ['luo'] = 'Luo', ['lvi'] = 'Latvian', ['maj'] = 'Majang', ['mak'] = 'Makua', ['mal'] = 'Malayalam Traditional', ['man'] = 'Mansi', ['map'] = 'Mapudungun', ['mar'] = 'Marathi', ['maw'] = 'Marwari', ['mbn'] = 'Mbundu', ['mch'] = 'Manchu', ['mcr'] = 'Moose Cree', ['mde'] = 'Mende', ['men'] = "Me'en", ['miz'] = 'Mizo', ['mkd'] = 'Macedonian', ['mle'] = 'Male', ['mlg'] = 'Malagasy', ['mln'] = 'Malinke', ['mlr'] = 'Malayalam Reformed', ['mly'] = 'Malay', ['mnd'] = 'Mandinka', ['mng'] = 'Mongolian', ['mni'] = 'Manipuri', ['mnk'] = 'Maninka', ['mnx'] = 'Manx Gaelic', ['moh'] = 'Mohawk', ['mok'] = 'Moksha', ['mol'] = 'Moldavian', ['mon'] = 'Mon', ['mor'] = 'Moroccan', ['mri'] = 'Maori', ['mth'] = 'Maithili', ['mts'] = 'Maltese', ['mun'] = 'Mundari', ['nag'] = 'Naga-Assamese', ['nan'] = 'Nanai', ['nas'] = 'Naskapi', ['ncr'] = 'N-Cree', ['ndb'] = 'Ndebele', ['ndg'] = 'Ndonga', ['nep'] = 'Nepali', ['new'] = 'Newari', ['ngr'] = 'Nagari', ['nhc'] = 'Norway House Cree', ['nis'] = 'Nisi', ['niu'] = 'Niuean', ['nkl'] = 'Nkole', ['nko'] = "N'ko", ['nld'] = 'Dutch', ['nog'] = 'Nogai', ['nor'] = 'Norwegian', ['nsm'] = 'Northern Sami', ['nta'] = 'Northern Tai', ['nto'] = 'Esperanto', ['nyn'] = 'Nynorsk', ['oci'] = 'Occitan', ['ocr'] = 'Oji-Cree', ['ojb'] = 'Ojibway', ['ori'] = 'Oriya', ['oro'] = 'Oromo', ['oss'] = 'Ossetian', ['paa'] = 'Palestinian Aramaic', ['pal'] = 'Pali', ['pan'] = 'Punjabi', ['pap'] = 'Palpa', ['pas'] = 'Pashto', ['pgr'] = 'Polytonic Greek', ['pil'] = 'Pilipino', ['plg'] = 'Palaung', ['plk'] = 'Polish', ['pro'] = 'Provencal', ['ptg'] = 'Portuguese', ['qin'] = 'Chin', ['raj'] = 'Rajasthani', ['rbu'] = 'Russian Buriat', ['rcr'] = 'R-Cree', ['ria'] = 'Riang', ['rms'] = 'Rhaeto-Romanic', ['rom'] = 'Romanian', ['roy'] = 'Romany', ['rsy'] = 'Rusyn', ['rua'] = 'Ruanda', ['rus'] = 'Russian', ['sad'] = 'Sadri', ['san'] = 'Sanskrit', ['sat'] = 'Santali', ['say'] = 'Sayisi', ['sek'] = 'Sekota', ['sel'] = 'Selkup', ['sgo'] = 'Sango', ['shn'] = 'Shan', ['sib'] = 'Sibe', ['sid'] = 'Sidamo', ['sig'] = 'Silte Gurage', ['sks'] = 'Skolt Sami', ['sky'] = 'Slovak', ['sla'] = 'Slavey', ['slv'] = 'Slovenian', ['sml'] = 'Somali', ['smo'] = 'Samoan', ['sna'] = 'Sena', ['snd'] = 'Sindhi', ['snh'] = 'Sinhalese', ['snk'] = 'Soninke', ['sog'] = 'Sodo Gurage', ['sot'] = 'Sotho', ['sqi'] = 'Albanian', ['srb'] = 'Serbian', ['srk'] = 'Saraiki', ['srr'] = 'Serer', ['ssl'] = 'South Slavey', ['ssm'] = 'Southern Sami', ['sur'] = 'Suri', ['sva'] = 'Svan', ['sve'] = 'Swedish', ['swa'] = 'Swadaya Aramaic', ['swk'] = 'Swahili', ['swz'] = 'Swazi', ['sxt'] = 'Sutu', ['syr'] = 'Syriac', ['tab'] = 'Tabasaran', ['taj'] = 'Tajiki', ['tam'] = 'Tamil', ['tat'] = 'Tatar', ['tcr'] = 'TH-Cree', ['tel'] = 'Telugu', ['tgn'] = 'Tongan', ['tgr'] = 'Tigre', ['tgy'] = 'Tigrinya', ['tha'] = 'Thai', ['tht'] = 'Tahitian', ['tib'] = 'Tibetan', ['tkm'] = 'Turkmen', ['tmn'] = 'Temne', ['tna'] = 'Tswana', ['tne'] = 'Tundra Nenets', ['tng'] = 'Tonga', ['tod'] = 'Todo', ['trk'] = 'Turkish', ['tsg'] = 'Tsonga', ['tua'] = 'Turoyo Aramaic', ['tul'] = 'Tulu', ['tuv'] = 'Tuvin', ['twi'] = 'Twi', ['udm'] = 'Udmurt', ['ukr'] = 'Ukrainian', ['urd'] = 'Urdu', ['usb'] = 'Upper Sorbian', ['uyg'] = 'Uyghur', ['uzb'] = 'Uzbek', ['ven'] = 'Venda', ['vit'] = 'Vietnamese', ['wa' ] = 'Wa', ['wag'] = 'Wagdi', ['wcr'] = 'West-Cree', ['wel'] = 'Welsh', ['wlf'] = 'Wolof', ['xbd'] = 'Tai Lue', ['xhs'] = 'Xhosa', ['yak'] = 'Yakut', ['yba'] = 'Yoruba', ['ycr'] = 'Y-Cree', ['yic'] = 'Yi Classic', ['yim'] = 'Yi Modern', ['zhh'] = 'Chinese Hong Kong', ['zhp'] = 'Chinese Phonetic', ['zhs'] = 'Chinese Simplified', ['zht'] = 'Chinese Traditional', ['znd'] = 'Zande', ['zul'] = 'Zulu' } fonts.otf.tables.features = { ['aalt'] = 'Access All Alternates', ['abvf'] = 'Above-Base Forms', ['abvm'] = 'Above-Base Mark Positioning', ['abvs'] = 'Above-Base Substitutions', ['afrc'] = 'Alternative Fractions', ['akhn'] = 'Akhands', ['blwf'] = 'Below-Base Forms', ['blwm'] = 'Below-Base Mark Positioning', ['blws'] = 'Below-Base Substitutions', ['c2pc'] = 'Petite Capitals From Capitals', ['c2sc'] = 'Small Capitals From Capitals', ['calt'] = 'Contextual Alternates', ['case'] = 'Case-Sensitive Forms', ['ccmp'] = 'Glyph Composition/Decomposition', ['cjct'] = 'Conjunct Forms', ['clig'] = 'Contextual Ligatures', ['cpsp'] = 'Capital Spacing', ['cswh'] = 'Contextual Swash', ['curs'] = 'Cursive Positioning', ['dflt'] = 'Default Processing', ['dist'] = 'Distances', ['dlig'] = 'Discretionary Ligatures', ['dnom'] = 'Denominators', ['expt'] = 'Expert Forms', ['falt'] = 'Final glyph Alternates', ['fin2'] = 'Terminal Forms #2', ['fin3'] = 'Terminal Forms #3', ['fina'] = 'Terminal Forms', ['frac'] = 'Fractions', ['fwid'] = 'Full Width', ['half'] = 'Half Forms', ['haln'] = 'Halant Forms', ['halt'] = 'Alternate Half Width', ['hist'] = 'Historical Forms', ['hkna'] = 'Horizontal Kana Alternates', ['hlig'] = 'Historical Ligatures', ['hngl'] = 'Hangul', ['hojo'] = 'Hojo Kanji Forms', ['hwid'] = 'Half Width', ['init'] = 'Initial Forms', ['isol'] = 'Isolated Forms', ['ital'] = 'Italics', ['jalt'] = 'Justification Alternatives', ['jp04'] = 'JIS2004 Forms', ['jp78'] = 'JIS78 Forms', ['jp83'] = 'JIS83 Forms', ['jp90'] = 'JIS90 Forms', ['kern'] = 'Kerning', ['lfbd'] = 'Left Bounds', ['liga'] = 'Standard Ligatures', ['ljmo'] = 'Leading Jamo Forms', ['lnum'] = 'Lining Figures', ['locl'] = 'Localized Forms', ['mark'] = 'Mark Positioning', ['med2'] = 'Medial Forms #2', ['medi'] = 'Medial Forms', ['mgrk'] = 'Mathematical Greek', ['mkmk'] = 'Mark to Mark Positioning', ['mset'] = 'Mark Positioning via Substitution', ['nalt'] = 'Alternate Annotation Forms', ['nlck'] = 'NLC Kanji Forms', ['nukt'] = 'Nukta Forms', ['numr'] = 'Numerators', ['onum'] = 'Old Style Figures', ['opbd'] = 'Optical Bounds', ['ordn'] = 'Ordinals', ['ornm'] = 'Ornaments', ['palt'] = 'Proportional Alternate Width', ['pcap'] = 'Petite Capitals', ['pnum'] = 'Proportional Figures', ['pref'] = 'Pre-base Forms', ['pres'] = 'Pre-base Substitutions', ['pstf'] = 'Post-base Forms', ['psts'] = 'Post-base Substitutions', ['pwid'] = 'Proportional Widths', ['qwid'] = 'Quarter Widths', ['rand'] = 'Randomize', ['rkrf'] = 'Rakar Forms', ['rlig'] = 'Required Ligatures', ['rphf'] = 'Reph Form', ['rtbd'] = 'Right Bounds', ['rtla'] = 'Right-To-Left Alternates', ['ruby'] = 'Ruby Notation Forms', ['salt'] = 'Stylistic Alternates', ['sinf'] = 'Scientific Inferiors', ['size'] = 'Optical Size', ['smcp'] = 'Small Capitals', ['smpl'] = 'Simplified Forms', ['ss01'] = 'Stylistic Set 1', ['ss02'] = 'Stylistic Set 2', ['ss03'] = 'Stylistic Set 3', ['ss04'] = 'Stylistic Set 4', ['ss05'] = 'Stylistic Set 5', ['ss06'] = 'Stylistic Set 6', ['ss07'] = 'Stylistic Set 7', ['ss08'] = 'Stylistic Set 8', ['ss09'] = 'Stylistic Set 9', ['ss10'] = 'Stylistic Set 10', ['ss11'] = 'Stylistic Set 11', ['ss12'] = 'Stylistic Set 12', ['ss13'] = 'Stylistic Set 13', ['ss14'] = 'Stylistic Set 14', ['ss15'] = 'Stylistic Set 15', ['ss16'] = 'Stylistic Set 16', ['ss17'] = 'Stylistic Set 17', ['ss18'] = 'Stylistic Set 18', ['ss19'] = 'Stylistic Set 19', ['ss20'] = 'Stylistic Set 20', ['subs'] = 'Subscript', ['sups'] = 'Superscript', ['swsh'] = 'Swash', ['titl'] = 'Titling', ['tjmo'] = 'Trailing Jamo Forms', ['tnam'] = 'Traditional Name Forms', ['tnum'] = 'Tabular Figures', ['trad'] = 'Traditional Forms', ['twid'] = 'Third Widths', ['unic'] = 'Unicase', ['valt'] = 'Alternate Vertical Metrics', ['vatu'] = 'Vattu Variants', ['vert'] = 'Vertical Writing', ['vhal'] = 'Alternate Vertical Half Metrics', ['vjmo'] = 'Vowel Jamo Forms', ['vkna'] = 'Vertical Kana Alternates', ['vkrn'] = 'Vertical Kerning', ['vpal'] = 'Proportional Alternate Vertical Metrics', ['vrt2'] = 'Vertical Rotation', ['zero'] = 'Slashed Zero' } fonts.otf.tables.baselines = { ['hang'] = 'Hanging baseline', ['icfb'] = 'Ideographic character face bottom edge baseline', ['icft'] = 'Ideographic character face tope edige baseline', ['ideo'] = 'Ideographic em-box bottom edge baseline', ['idtp'] = 'Ideographic em-box top edge baseline', ['math'] = 'Mathmatical centered baseline', ['romn'] = 'Roman baseline' } function fonts.otf.tables.to_tag(id) return stringformat("%4s",id:lower()) end function fonts.otf.meanings.resolve(tab,id) if tab and id then id = id:lower() return tab[id] or tab[id:gsub(" ","")] or tab['dflt'] or '' else return "unknown" end end function fonts.otf.meanings.script(id) return fonts.otf.meanings.resolve(fonts.otf.tables.scripts,id) end function fonts.otf.meanings.language(id) return fonts.otf.meanings.resolve(fonts.otf.tables.languages,id) end function fonts.otf.meanings.feature(id) return fonts.otf.meanings.resolve(fonts.otf.tables.features,id) end function fonts.otf.meanings.baseline(id) return fonts.otf.meanings.resolve(fonts.otf.tables.baselines,id) end fonts.otf.tables.to_scripts = table.reverse_hash(fonts.otf.tables.scripts ) fonts.otf.tables.to_languages = table.reverse_hash(fonts.otf.tables.languages) fonts.otf.tables.to_features = table.reverse_hash(fonts.otf.tables.features ) do local scripts = fonts.otf.tables.scripts local languages = fonts.otf.tables.languages local features = fonts.otf.tables.features local to_scripts = fonts.otf.tables.to_scripts local to_languages = fonts.otf.tables.to_languages local to_features = fonts.otf.tables.to_features function fonts.otf.meanings.normalize(features) local h = { } for k,v in pairs(features) do k = k:lower() -- :gsub("[^a-z0-9%-%.]" -- not needed if k == "language" or k == "lang" then v = (v:lower()):gsub("[^a-z0-9%-]","") k = language if not languages[v] then h.language = to_languages[v] or "dflt" else h.language = v end elseif k == "script" then v = (v:lower()):gsub("[^a-z0-9%-]","") if not scripts[v] then h.script = to_scripts[v] or "dflt" else h.script = v end else if type(v) == "string" then local b = v:is_boolean() if type(b) == "nil" then v = tonumber(v) or v:lower() -- gsub("[^a-z0-9%-]") -- too dangerous, e.g. featurefiles else v = b end end h[to_features[k] or k] = v end end return h end end --[[ldx--

Here we go.

--ldx]]-- fonts.otf.enhance = fonts.otf.enhance or { } fonts.otf.enhance.add_kerns = true fonts.otf.featurefiles = { --~ "texhistoric.fea" } function fonts.otf.load(filename,format,sub,featurefile) local name = file.basename(file.removesuffix(filename)) if featurefile then name = name .. "@" .. file.removesuffix(file.basename(featurefile)) end if sub == "" then sub = false end local hash = name if sub then -- name cleanup will move to cache code hash = hash .. "-" .. sub hash = hash:lower() hash = hash:gsub("[^%w%d]+","-") end local data = containers.read(fonts.otf.cache(), hash) local size = lfs.attributes(filename,"size") or 0 if data and data.size ~= size then data = nil end if not data then logs.report("load otf","loading: " .. filename) local ff, messages if sub then ff, messages = fontforge.open(filename,sub) else ff, messages = fontforge.open(filename) end if messages and #messages > 0 then for _, m in ipairs(messages) do logs.report("load otf","warning: " .. m) end end if ff then local function load_featurefile(featurefile) if featurefile then featurefile = input.find_file(texmf.instance,file.addsuffix(featurefile,'fea')) -- "FONTFEATURES" if featurefile and featurefile ~= "" then logs.report("load otf", "featurefile: " .. featurefile) fontforge.apply_featurefile(ff, featurefile) end end end -- for _, featurefile in pairs(fonts.otf.featurefiles) do -- load_featurefile(featurefile) -- end load_featurefile(featurefile) data = fontforge.to_table(ff) fontforge.close(ff) if data then logs.report("load otf","enhance: before") fonts.otf.enhance.before(data,filename) logs.report("load otf","enhance: enrich") fonts.otf.enhance.enrich(data,filename) logs.report("load otf","enhance: flatten") fonts.otf.enhance.flatten(data,filename) logs.report("load otf","enhance: analyze") fonts.otf.enhance.analyze(data,filename) logs.report("load otf","enhance: after") fonts.otf.enhance.after(data,filename) logs.report("load otf","enhance: patch") fonts.otf.enhance.patch(data,filename) logs.report("load otf","enhance: strip") fonts.otf.enhance.strip(data,filename) if fonts.otf.pack then logs.report("load otf","enhance: pack") fonts.otf.enhance.pack(data) end logs.report("load otf","file size: " .. size) data.size = size logs.report("load otf","saving: in cache") data = containers.write(fonts.otf.cache(), hash, data) else logs.error("load otf","loading failed (table conversion error)") end else logs.error("load otf","loading failed (file read error)") end end fonts.otf.enhance.unpack(data) return data end -- memory saver .. function fonts.otf.enhance.pack(data) if data then local h, t = { }, { } local concat = table.concat local function tabstr(t) for i=1,#t do if type(t[i]) == "boolean" then local s = tostring(t[1]) for i=2,#t do s = s .. ",".. tostring(t[i]) end return s end end return concat(t,",") end local function pack(v) local tag = tabstr(v,",") if not h[tag] then t[#t+1] = v h[tag] = #t end return h[tag] end for k, v in pairs(data.glyphs) do v.boundingbox = pack(v.boundingbox) if v.lookups then for k,v in pairs(v.lookups) do for kk, vv in ipairs(v) do v[kk] = pack(vv) end end end end if data.lookups then for k, v in pairs(data.lookups) do if v.rules then for kk, vv in pairs(v.rules) do local l = vv.lookups if l then vv.lookups = pack(l) end local c = vv.coverage if c then c.before = c.before and pack(c.before ) c.after = c.after and pack(c.after ) c.current = c.current and pack(c.current) end end end end end if data.luatex then local li = data.luatex.ignore_flags if li then for k, v in pairs(li) do li[k] = pack(v) end end end if #t > 0 then data.tables = t end end end function fonts.otf.enhance.unpack(data) if data then local t = data.tables if t then for k, v in pairs(data.glyphs) do v.boundingbox = t[v.boundingbox] local l = v.lookups if l then for k,v in pairs(l) do for i=1,#v do v[i] = t[v[i]] end end end end if data.lookups then for k, v in pairs(data.lookups) do local r = v.rules if r then for kk, vv in pairs(r) do local l = vv.lookups if l then vv.lookups = t[l] end local c = vv.coverage if c then local cc = c.before if cc then c.before = t[cc] end cc = c.after if cc then c.after = t[cc] end cc = c.current if cc then c.current = t[cc] end end end end end end if data.luatex then local li = data.luatex.ignore_flags if li then for k, v in pairs(li) do li[k] = t[v] end end end data.tables = nil end end end -- todo: normalize, design_size => designsize function fonts.otf.enhance.analyze(data,filename) local t = { filename = file.basename(filename), version = fonts.otf.version, creator = "context mkiv", unicodes = fonts.otf.analyze_unicodes(data), gposfeatures = fonts.otf.analyze_features(data.gpos), gsubfeatures = fonts.otf.analyze_features(data.gsub), marks = fonts.otf.analyze_class(data,'mark'), } t.subtables, t.name_to_type, t.internals, t.always_valid, t.ignore_flags, t.ctx_always = fonts.otf.analyze_subtables(data) data.luatex = t end do -- original string parser: 0.109, lpeg parser: 0.036 seconds for Adobe-CNS1-4.cidmap -- -- 18964 18964 (leader) -- 0 /.notdef -- 1..95 0020 -- 99 3000 local number = lpeg.C(lpeg.R("09","af","AF")^1) local space = lpeg.S(" \n\r\t") local spaces = space^0 local period = lpeg.P(".") local periods = period * period local name = lpeg.P("/") * lpeg.C((1-space)^1) local unicodes, names = { }, {} local tonumber = tonumber local function do_one(a,b) unicodes[tonumber(a)] = tonumber(b,16) end local function do_range(a,b,c) c = tonumber(c,16) for i=tonumber(a),tonumber(b) do unicodes[i] = c c = c + 1 end end local function do_name(a,b) names[tonumber(a)] = b end local grammar = lpeg.P { "start", start = number * spaces * number * lpeg.V("series"), series = (spaces * (lpeg.V("one") + lpeg.V("range") + lpeg.V("named")) )^1, one = (number * spaces * number) / do_one, range = (number * periods * number * spaces * number) / do_range, named = (number * spaces * name) / do_name } function fonts.otf.load_cidmap(filename) -- lpeg local data = io.loaddata(filename) if data then unicodes, names = { }, { } grammar:match(data) local supplement, registry, ordering = filename:match("^(.-)%-(.-)%-()%.(.-)$") return { supplement = supplement, registry = registry, ordering = ordering, filename = filename, unicodes = unicodes, names = names } else return nil end end end fonts.otf.cidmaps = { } fonts.otf.cidmax = 10 function fonts.otf.cidmap(registry,ordering,supplement) -- cf Arthur R. we can safely scan upwards since cids are downward compatible local template = "%s-%s-%s.cidmap" local supplement = tonumber(supplement) logs.report("load otf",string.format("needed cidmap, registry: %s, ordering: %s, supplement: %s",registry,ordering,supplement)) local function locate(registry,ordering,supplement) local filename = string.format(template,registry,ordering,supplement) local cidmap = fonts.otf.cidmaps[filename] if not cidmap then logs.report("load otf",string.format("checking cidmap, registry: %s, ordering: %s, supplement: %s, filename: %s",registry,ordering,supplement,filename)) local fullname = input.find_file(texmf.instance,filename,'cid') or "" if fullname ~= "" then cidmap = fonts.otf.load_cidmap(fullname) if cidmap then logs.report("load otf",string.format("using cidmap file %s",filename)) fonts.otf.cidmaps[filename] = cidmap return cidmap end end end return cidmap end local cidmap = locate(registry,ordering,supplement) if not cidmap then local cidnum = nil -- next highest (alternatively we could start high) if supplement < fonts.otf.cidmax then for supplement=supplement+1,fonts.otf.cidmax do local c = locate(registry,ordering,supplement) if c then cidmap, cidnum = c, supplement break end end end -- next lowest (least worse fit) if not cidmap and supplement > 0 then for supplement=supplement-1,0,-1 do local c = locate(registry,ordering,supplement) if c then cidmap, cidnum = c, supplement break end end end -- prevent further lookups if cidmap and cidnum > 0 then for s=0,cidnum-1 do filename = string.format(template,registry,ordering,s) if not fonts.otf.cidmaps[filename] then fonts.otf.cidmaps[filename] = cidmap -- copy of ref end end end end return cidmap end --~ ["cidinfo"]={ --~ ["ordering"]="Japan1", --~ ["registry"]="Adobe", --~ ["supplement"]=6, --~ ["version"]=6, --~ }, function fonts.otf.enhance.before(data,filename) local private = fonts.private if data.subfonts and table.is_empty(data.glyphs) then local cidinfo = data.cidinfo if cidinfo.registry then local cidmap = fonts.otf.cidmap(cidinfo.registry,cidinfo.ordering,cidinfo.supplement) if cidmap then local glyphs, uni_to_int, int_to_uni, nofnames, nofunicodes = { }, { }, { }, 0, 0 local unicodes, names = cidmap.unicodes, cidmap.names for n, subfont in pairs(data.subfonts) do for index, g in pairs(subfont.glyphs) do if not next(g) then -- dummy entry else local unicode, name = unicodes[index], names[index] g.cidindex = n g.boundingbox = g.boundingbox -- or zerobox g.name = g.name or name or "unknown" if unicode then g.unicode = unicode uni_to_int[unicode] = index int_to_uni[index] = unicode nofunicodes = nofunicodes + 1 elseif name then g.unicode = -1 nofnames = nofnames + 1 end glyphs[index] = g end end subfont.glyphs = nil end logs.report("load otf",string.format("cid font remapped, %s unicode points, %s symbolic names, %s glyphs",nofunicodes, nofnames, nofunicodes+nofnames)) data.glyphs = glyphs data.map = data.map or { } data.map.map = uni_to_int data.map.backmap = int_to_uni else logs.report("load otf",string.format("unable to remap cid font, missing cid file for %s",filename)) end else logs.report("load otf",string.format("font %s has no glyphs",filename)) end end if data.map then local uni_to_int = data.map.map -- [unic] = slot local int_to_uni = data.map.backmap -- { [0|1] = unic, ... } for index, glyph in pairs(data.glyphs) do if glyph.name then local unic = glyph.unicode or glyph.unicodeenc or -1 glyph.unicodeenc = nil -- older luatex version if index > 0 and (unic == -1 or unic >= 0x110000) then while uni_to_int[private] do private = private + 1 end uni_to_int[private] = index int_to_uni[index] = private glyph.unicode = private if fonts.trace then logs.report("load otf",string.format("enhance: glyph %s at index %s is moved to private unicode slot %s",glyph.name,index,private)) end else glyph.unicode = unic -- safeguard for older version end end end local n = 0 for k,v in pairs(int_to_uni) do if v == -1 or v >= 0x110000 then int_to_uni[k], n = nil, n+1 end end if fonts.trace then logs.report("load otf",string.format("enhance: %s entries removed from map.backmap",n)) end local n = 0 for k,v in pairs(uni_to_int) do if k == -1 or k >= 0x110000 then uni_to_int[k], n = nil, n+1 end end if fonts.trace then logs.report("load otf",string.format("enhance: %s entries removed from map.mapmap",n)) end else data.map = { map = {}, backmap = {} } end if data.ttf_tables then for _, v in ipairs(data.ttf_tables) do if v.data then v.data = "deleted" end --~ if v.data then v.data = v.data:gsub("\026","\\026") end -- does not work out well end end table.compact(data.glyphs) if data.subfonts then for _, subfont in pairs(data.subfonts) do table.compact(subfont.glyphs) end end -- we prefer the before lookups in a normal order if data.lookups then for _, v in pairs(data.lookups) do if v.rules then for _, vv in pairs(v.rules) do local c = vv.coverage if c and c.before then c.before = table.reverse(c.before) end end end end end --~ for index, glyph in pairs(data.glyphs) do --~ for k,v in pairs(glyph) do --~ if v == 0 then glyph[k] = nil end --~ end --~ end end function fonts.otf.enhance.after(data,filename) -- to be split if fonts.otf.enhance.add_kerns then local glyphs, mapmap, unicodes = data.glyphs, data.map.map, data.luatex.unicodes local mkdone = false for index, glyph in pairs(data.glyphs) do if glyph.kerns then local mykerns = { } -- unicode indexed ! for k,v in pairs(glyph.kerns) do local vc, vo, vl = v.char, v.off, v.lookup if vc and vo and vl then -- brrr, wrong! we miss the non unicode ones local uvc = unicodes[vc] if uvc then local mkl = mykerns[vl] if not mkl then mkl = { [unicodes[vc]] = vo } mykerns[v.lookup] = mkl else mkl[unicodes[vc]] = vo end else logs.report("load otf", string.format("problems with unicode %s of kern %s at glyph %s",vc,k,index)) end end end glyph.mykerns = mykerns glyph.kerns = nil -- saves space and time mkdone = true end end if mkdone then logs.report("load otf", "replacing 'kerns' tables by 'mykerns' tables") end if data.gpos then for _, gpos in ipairs(data.gpos) do if gpos.subtables then for _, subtable in ipairs(gpos.subtables) do local kernclass = subtable.kernclass if kernclass then for _, kcl in ipairs(kernclass) do local firsts, seconds, offsets, lookup = kcl.firsts, kcl.seconds, kcl.offsets, kcl.lookup local maxfirsts, maxseconds = table.getn(firsts), table.getn(seconds) logs.report("load otf", string.format("adding kernclass %s with %s times %s pairs)",lookup, maxfirsts, maxseconds)) for fk, fv in pairs(firsts) do for first in fv:gmatch("[^ ]+") do local glyph = glyphs[mapmap[unicodes[first]]] local mykerns = glyph.mykerns if not mykerns then mykerns = { } -- unicode indexed ! glyph.mykerns = mykerns end local lookupkerns = mykerns[lookup] if not lookupkerns then lookupkerns = { } mykerns[lookup] = lookupkerns end for sk, sv in pairs(seconds) do for second in sv:gmatch("[^ ]+") do lookupkerns[unicodes[second]] = offsets[(fk-1) * maxseconds + sk] end end end end end subtable.comment = "The kernclass table is merged into mykerns in the indexed glyph tables." subtable.kernclass = { } end end end end end end end function fonts.otf.enhance.strip(data) for k, v in pairs(data.glyphs) do local d = v.dependents if d then v.dependents = nil end end data.map = nil data.names = nil data.luatex.comment = "Glyph tables have their original index. When present, mykern tables are indexed by unicode." end function fonts.otf.enhance.flatten(data,filename) -- to be split logs.report("load otf", "flattening 'specifications' tables") for k, v in pairs(data.glyphs) do if v.lookups then for kk, vv in pairs(v.lookups) do for kkk, vvv in ipairs(vv) do local s = vvv.specification if s then local t = vvv.type if t == "ligature" then vv[kkk] = { "ligature", s.components, s.char } elseif t == "alternate" then vv[kkk] = { "alternate", s.components } elseif t == "substitution" then vv[kkk] = { "substitution", s.variant } elseif t == "multiple" then vv[kkk] = { "multiple", s.components } elseif t == "position" then vv[kkk] = { "position", s.x or 0, s.y or 0, s.h or 0, s.v or 0 } elseif t == "pair" then local one, two, paired = s.offsets[1], s.offsets[2], s.paired or "" if one then if two then vv[kkk] = { "pair", paired, one.x or 0, one.y or 0, one.h or 0, one.v or 0, two.x or 0, two.y or 0, two.h or 0, two.v or 0 } else vv[kkk] = { "pair", paired, one.x or 0, one.y or 0, one.h or 0 } end else if two then vv[kkk] = { "pair", paired, 0, 0, 0, 0, two.x or 0, two.y or 0, two.h or 0, two.v or 0 } else vv[kkk] = { "pair", paired } end end else logs.report("load otf", "flattening needed, warn Hans and/or Taco") for a, b in pairs(s) do if vvv[a] then logs.report("load otf", "flattening conflict, warn Hans and/or Taco") end vvv[a] = b end vvv.specification = nil end end end end end end logs.report("load otf", "flattening 'anchor' tables") for k, v in pairs(data.glyphs) do if v.anchors then for kk, vv in pairs(v.anchors) do for kkk, vvv in pairs(vv) do if vvv.x or vvv.y then -- kkk == "centry" vv[kkk] = { vvv.x or 0, vvv.y or 0 } else for kkkk, vvvv in ipairs(vvv) do vvv[kkkk] = { vvvv.x or 0, vvvv.y or 0 } end end end end end end for _, tag in pairs({"gpos","gsub"}) do if data[tag] then logs.report("load otf", "flattening '" .. tag.. "' tables") for k, v in pairs(data[tag]) do if v.features then for kk, vv in ipairs(v.features) do local t = { } for kkk, vvv in ipairs(vv.scripts) do t[vvv.script] = vvv.langs end vv.scripts = t end end end end end end fonts.otf.enhance.patches = { } function fonts.otf.enhance.patch(data,filename) local basename = file.basename(filename) for pattern, action in pairs(fonts.otf.enhance.patches) do if basename:find(pattern) then action(data,filename) end end end -- tex features function fonts.otf.enhance.enrich(data,filename) -- later end -- patching do -- will move to a typescript local function patch(data,filename) if data.design_size == 0 then local ds = (file.basename(filename)):match("(%d+)") if ds then logs.report("load otf",string.format("patching design size (%s)",ds)) data.design_size = tonumber(ds) * 10 end end end fonts.otf.enhance.patches["^lmroman"] = patch fonts.otf.enhance.patches["^lmsans"] = patch fonts.otf.enhance.patches["^lmtypewriter"] = patch end function fonts.otf.analyze_class(data,class) local classes = { } for index, glyph in pairs(data.glyphs) do if glyph.class == class then classes[glyph.unicode] = true end end return classes end function fonts.otf.analyze_subtables(data) local subtables, name_to_type, internals, always_valid, ignore_flags, ctx_always = { }, { }, { }, { }, { }, { } local function collect(g) if g then for k,v in ipairs(g) do if v.features then local ignored = { false, false, false } if v.flags.ignorecombiningmarks then ignored[1] = 'mark' end if v.flags.ignorebasechars then ignored[2] = 'base' end if v.flags.ignoreligatures then ignored[3] = 'ligature' end if v.subtables then local type = v.type for _, feature in ipairs(v.features) do local ft = feature.tag:lower() subtables[ft] = subtables[ft] or { } ctx_always[ft] = v.always for script, languages in pairs(feature.scripts) do script = script:lower() script = script:strip() sft = subtables[ft] local sfts = sft[script] if not sfts then sfts = { } sft[script] = sfts end for _, language in ipairs(languages) do language = language:lower() language = language:strip() local sftsl = sfts[language] if not sftsl then sftsl = sfts[language] or { } sfts[language] = sftsl end local lookups, valid = sftsl.lookups or { }, sftsl.valid or { } for n, subtable in ipairs(v.subtables) do local stl = subtable.name if stl then lookups[#lookups+1] = stl valid[stl] = true name_to_type[stl] = type ignore_flags[stl] = ignored end end sftsl.lookups, sftsl.valid = lookups, valid end end end end else -- we have an internal feature, say ss_l_83 that resolves to -- subfeatures like ss_l_83_s which we find in the glyphs name_to_type[v.name] = v.type local lookups, valid = { }, { } for n, subtable in ipairs(v.subtables) do local stl = subtable.name if stl then lookups[#lookups+1] = stl valid[stl] = true always_valid[stl] = true end end internals[v.name] = { lookups = lookups, valid = valid } always_valid[v.name] = true -- bonus end end end end collect(data.gsub) collect(data.gpos) return subtables, name_to_type, internals, always_valid, ignore_flags, ctx_always end function fonts.otf.analyze_unicodes(data) local unicodes = { } for _, blob in pairs(data.glyphs) do if blob.name then unicodes[blob.name] = blob.unicode or 0 end end unicodes['space'] = unicodes['space'] or 32 -- handly later on return unicodes end function fonts.otf.analyze_features(g, features) if g then local t, done = { }, { } for k=1,#g do local f = features or g[k].features if f then for k=1,#f do -- scripts and tag local tag = f[k].tag if not done[tag] then t[#t+1] = tag done[tag] = true end end end end if #t > 0 then return t end end return nil end function fonts.otf.valid_subtable(otfdata,kind,script,language) local tk = otfdata.luatex.subtables[kind] if tk then local tks = tk[script] or tk.dflt if tks then local tksl = tks[language] or tks.dflt if tksl then return tksl.lookups end end end return false end function fonts.otf.features.register(name,default) fonts.otf.features.list[#fonts.otf.features.list+1] = name fonts.otf.features.default[name] = default end function fonts.otf.set_features(tfmdata) -- node and base, simple mapping local shared = tfmdata.shared local otfdata = shared.otfdata shared.features = fonts.define.check(shared.features,fonts.otf.features.default) local features = shared.features local trace = fonts.otf.trace_features or fonts.otf.trace_set_features if not tfmdata.language then tfmdata.language = 'dflt' end if not tfmdata.script then tfmdata.script = 'dflt' end if not table.is_empty(features) then local gposlist = otfdata.luatex.gposfeatures local gsublist = otfdata.luatex.gsubfeatures local mode = tfmdata.mode or fonts.mode local initializers = fonts.initializers local fi = initializers[mode] if fi then -- todo: delay initilization for mode 'node' local fiotf = fi.otf if fiotf then local done = { } local function initialize(list) -- using tex lig and kerning if list then for i=1,#list do local f = list[i] local value = features[f] if value and fiotf[f] then -- brr if not done[f] then -- so, we can move some to triggers if trace then logs.report("define otf",string.format("initializing feature %s to %s for mode %s for font %s",f,tostring(value),mode or 'unknown', tfmdata.fullname or 'unknown')) end fiotf[f](tfmdata,value) -- can set mode (no need to pass otf) mode = tfmdata.mode or fonts.mode -- keep this, mode can be set local ! local fi = initializers[mode] fiotf = fi.otf done[f] = true end end end end end initialize(fonts.triggers) initialize(gsublist) initialize(gposlist) initialize(fonts.manipulators) end end local fm = fonts.methods[mode] if fm then local fmotf = fm.otf local sp = shared.processors if fmotf then local function register(list) -- node manipulations if list then for i=1,#list do local f = list[i] if features[f] and fmotf[f] then -- brr if trace then logs.report("define otf",string.format("installing feature handler %s for mode %s for font %s",f,mode or 'unknown', tfmdata.fullname or 'unknown')) end sp[#sp+1] = fmotf[f] end end end end register(fonts.triggers) register(gsublist) register(gposlist) register(fonts.manipulators) end end end end function fonts.otf.otf_to_tfm(specification) local name = specification.name local sub = specification.sub local filename = specification.filename local format = specification.format local features = specification.features.normal local cache_id = specification.hash local tfmdata = containers.read(fonts.tfm.cache(),cache_id) if not tfmdata then local otfdata = fonts.otf.load(filename,format,sub,features and features.featurefile) if not table.is_empty(otfdata) then fonts.otf.add_dimensions(otfdata) if true then otfdata._shared_ = otfdata._shared_ or { -- aggressive sharing processes = { }, lookuptable = { }, featuredata = { }, featurecache = { }, } end tfmdata = fonts.otf.copy_to_tfm(otfdata) if not table.is_empty(tfmdata) then tfmdata.unique = tfmdata.unique or { } tfmdata.shared = tfmdata.shared or { } -- combine local shared = tfmdata.shared shared.otfdata = otfdata shared.features = features shared.processors = { } shared.dynamics = { } shared.processes = { } shared.lookuptable = { } shared.featuredata = { } shared.featurecache = { } if otfdata._shared_ then shared.processes = otfdata._shared_.processes shared.lookuptable = otfdata._shared_.lookuptable shared.featuredata = otfdata._shared_.featuredata shared.featurecache = otfdata._shared_.featurecache end fonts.otf.set_features(tfmdata) end end containers.write(fonts.tfm.cache(),cache_id,tfmdata) end return tfmdata end function fonts.otf.features.prepare_base_kerns(tfmdata,kind,value) -- todo what kind of kerns, currently all if value then local otfdata = tfmdata.shared.otfdata local charlist = otfdata.glyphs local unicodes = otfdata.luatex.unicodes local somevalid = fonts.otf.some_valid_feature(otfdata,kind,tfmdata.script,tfmdata.language) for _, chr in pairs(tfmdata.characters) do local d = charlist[chr.description.index] if d then local dk = d.mykerns if dk then local t, done = chr.kerns or { }, false for lookup,kerns in pairs(dk) do if somevalid[lookup] then for k, v in pairs(kerns) do if v ~= 0 then t[k], done = v, true end end end end if done then chr.kerns = t -- no empty assignments end else dk = d.kerns if dk then local t, done = chr.kerns or { }, false for _, v in pairs(dk) do if somevalid[v.lookup] then local k = unicodes[v.char] if k > 0 then t[k], done = v.off, true end end end if done then chr.kerns = t -- no empty assignments end end end end end end end function fonts.otf.add_dimensions(data) if data then local force = fonts.otf.notdef for k, d in pairs(data.glyphs) do local bb, wd = d.boundingbox, d.width or 0 if force and not d.name then d.name = ".notdef" end if wd ~= 0 and d.class == "mark" then d.width = -wd end if bb then local ht, dp = bb[4], -bb[2] if ht ~= 0 then d.height = ht end if dp ~= 0 then d.depth = dp end end d.index = k end end end function fonts.otf.copy_to_tfm(data) -- we can save a copy when we reorder the tma to unicode if data then local tfm = { characters = { }, parameters = { } } local unicodes = data.luatex.unicodes local characters = tfm.characters local parameters = tfm.parameters local glyphs = data.glyphs for k, d in pairs(glyphs) do if d.name then characters[d.unicode] = { description = d } end end local designsize = data.designsize or data.design_size or 100 if designsize == 0 then designsize = 100 end local spaceunits = 500 tfm.units = data.units_per_em or 1000 -- we need a runtime lookup because of running from cdrom or zip, brrr tfm.filename = input.findbinfile(texmf.instance,data.luatex.filename,"") or data.luatex.filename tfm.fullname = data.fontname or data.fullname tfm.encodingbytes = 2 tfm.cidinfo = data.cidinfo tfm.cidinfo.registry = tfm.cidinfo.registry or "" tfm.type = "real" tfm.stretch = 0 -- stretch tfm.slant = 0 -- slant tfm.direction = 0 tfm.boundarychar_label = 0 tfm.boundarychar = 65536 tfm.designsize = (designsize/10)*65536 tfm.spacer = "500 units" data.isfixedpitch = data.pfminfo and data.pfminfo.panose and data.pfminfo.panose["proportion"] == "Monospaced" data.charwidth = nil if data.pfminfo then data.charwidth = data.pfminfo.avgwidth end local endash, emdash = unicodes['space'], unicodes['emdash'] if data.isfixedpitch then if characters[endash] then spaceunits, tfm.spacer = characters[endash].description.width, "space" end if not spaceunits and characters[emdash] then spaceunits, tfm.spacer = characters[emdash].description.width, "emdash" end if not spaceunits and data.charwidth then spaceunits, tfm.spacer = data.charwidth, "charwidth" end else if characters[endash] then spaceunits, tfm.spacer = characters[endash].description.width, "space" end if not spaceunits and characters[emdash] then spaceunits, tfm.spacer = characters[emdash].description.width/2, "emdash/2" end if not spaceunits and data.charwidth then spaceunits, tfm.spacer = data.charwidth, "charwidth" end end spaceunits = tonumber(spaceunits) or tfm.units/2 -- 500 -- brrr parameters.slant = 0 parameters.space = spaceunits parameters.space_stretch = tfm.units/2 -- 500 parameters.space_shrink = 2*tfm.units/3 -- 333 parameters.x_height = 4*tfm.units/5 -- 400 parameters.quad = tfm.units -- 1000 parameters.extra_space = 0 if spaceunits < 2*tfm.units/5 then -- todo: warning end tfm.italicangle = data.italicangle tfm.ascender = math.abs(data.ascent or 0) tfm.descender = math.abs(data.descent or 0) if data.italicangle then -- maybe also in afm _ parameters.slant = parameters.slant - math.round(math.tan(data.italicangle*math.pi/180)) end if data.isfixedpitch then parameters.space_stretch = 0 parameters.space_shrink = 0 elseif fonts.otf.syncspace then -- parameters.space_stretch = spaceunits/2 parameters.space_shrink = spaceunits/3 end if data.pfminfo and data.pfminfo.os2_xheight and data.pfminfo.os2_xheight > 0 then parameters.x_height = data.pfminfo.os2_xheight else local x = characters[unicodes['x']] if x then parameters.x_height = x.description.height end end -- [6] return tfm else return nil end end function fonts.tfm.read_from_open_type(specification) local tfmtable = fonts.otf.otf_to_tfm(specification) if tfmtable then tfmtable.name = specification.name tfmtable.sub = specification.sub tfmtable = fonts.tfm.scale(tfmtable, specification.size) -- here we resolve the name; file can be relocated, so this info is not in the cache local otfdata = tfmtable.shared.otfdata local filename = (otfdata and otfdata.luatex and otfdata.luatex.filename) or specification.filename if not filename then -- try to locate anyway and set otfdata.luatex.filename end if filename then tfmtable.encodingbytes = 2 tfmtable.filename = input.findbinfile(texmf.instance,filename,"") or filename tfmtable.fullname = otfdata.fontname or otfdata.fullname local order = otfdata and otfdata.order2 if order == 0 then tfmtable.format = 'opentype' elseif order == 1 then tfmtable.format = 'truetype' else tfmtable.format = specification.format end tfmtable.name = tfmtable.filename or tfmtable.fullname end fonts.logger.save(tfmtable,file.extname(specification.filename),specification) end return tfmtable end function fonts.otf.analyze_only(otfdata) local analyze = fonts.otf.analyze_features return analyze(otfdata.gpos), analyze(otfdata.gsub) end local a_to_script = { } local a_to_language = { } do local context_setups = fonts.define.specify.context_setups local context_numbers = fonts.define.specify.context_numbers function fonts.otf.set_dynamics(tfmdata,attribute,features) --currently experimental and slow / hackery local shared = tfmdata.shared if shared then local dynamics = shared.dynamics if dynamics then features = features or context_setups[context_numbers[attribute]] if features then local script = features.script or 'dflt' local language = features.language or 'dflt' local ds = dynamics[script] if not ds then ds = { } dynamics[script] = ds end local dsl = ds[language] if not dsl then dsl = { } ds[language] = dsl end local dsla = dsl[attribute] if dsla then return dsla else a_to_script [attribute] = script a_to_language[attribute] = language dsla = { } local otfdata = shared.otfdata local methods = fonts.methods.node.otf local initializers = fonts.initializers.node.otf local gposfeatures, gsubfeatures = fonts.otf.analyze_only(otfdata,features) local default = fonts.otf.features.default local function register(list) if list then for i=1,#list do local f = list[i] local value = features[f] or default[f] if value then local i, m = initializers[f], methods[f] if i then i(tfmdata,value) end if m then dsla[#dsla+1] = m end end end end end register(fonts.triggers) register(gsubfeatures) register(gposfeatures) dynamics[script][language][attribute] = dsla return dsla end end end end return { } -- todo: false end end -- scripts fonts.otf.default_language = 'latn' fonts.otf.default_script = 'dflt' function fonts.otf.valid_feature(otfdata,kind,script,language) -- return hash is faster if otfdata.luatex.ctx_always[kind] then script, language = 'dflt', 'dflt' else script = script or fonts.otf.default_script language = language or fonts.otf.default_language end script, language = script:lower(), language:lower() -- will go away, we will lowercase values local ft = otfdata.luatex.subtables[kind] local st = ft[script] or ft.dflt local lt = st and (st[language] or st.dflt) return false, otfdata.luatex.always_valid, lt.valid end function fonts.otf.some_valid_feature(otfdata,kind,script,language) if otfdata.luatex.ctx_always[kind] then script, language = 'dflt', 'dflt' else script = script or fonts.otf.default_script language = language or fonts.otf.default_language script, language = script:lower(), language:lower() -- will go away, we will lowercase values end local t = otfdata.luatex.subtables[kind] if t then local ts = t[script] or t.dflt if ts then local tsl = ts[language] or ts.dflt return (tsl and tsl.valid) or { } end end return { } end function fonts.otf.features.aux.resolve_ligatures(tfmdata,ligatures,kind) local otfdata = tfmdata.shared.otfdata local unicodes = otfdata.luatex.unicodes local chars = tfmdata.characters local changed = tfmdata.changed or { } local done = { } kind = kind or "unknown" local trace = fonts.otf.trace_features while true do local ok = false for k,v in pairs(ligatures) do local lig = v[1] if not done[lig] then local ligs = { } for s in lig:gmatch("[^ ]+") do ligs[#ligs+1] = s end if #ligs == 2 then local c, f, s = chars[v[2]], ligs[1], ligs[2] local uf, us = unicodes[f], unicodes[s] if changed[uf] or changed[us] then if trace then logs.report("define otf",string.format("%s: %s (%s) + %s (%s) ignored",kind,f,uf,s,us)) end else local first, second = chars[uf], us if first and second then local t = first.ligatures if not t then t = { } first.ligatures = t end t[second] = { char = unicodes[c.description.name], type = 0 } if trace then logs.report("define otf",string.format("%s: %s (%s) + %s (%s) = %s (%s)",kind,f,uf,s,us,c.description.name,unicodes[c.description.name])) end end end ok, done[lig] = true, c.description.name end end end if ok then for d,n in pairs(done) do local pattern = "^(" .. d .. ") " for k,v in pairs(ligatures) do v[1] = v[1]:gsub(pattern, function(str) return n .. " " end) end end else break end end end function fonts.otf.features.prepare_base_substitutions(tfmdata,kind,value) -- we can share some code with the node features if value then local ligatures = { } local otfdata = tfmdata.shared.otfdata local unicodes = otfdata.luatex.unicodes local trace = fonts.otf.trace_features local chars = tfmdata.characters local somevalid = fonts.otf.some_valid_feature(otfdata,kind,tfmdata.script,tfmdata.language) if not table.is_empty(somevalid) then tfmdata.changed = tfmdata.changed or { } local changed = tfmdata.changed local glyphs = otfdata.glyphs for k,c in pairs(chars) do local o = glyphs[c.description.index] if o and o.lookups then for lookup,ps in pairs(o.lookups) do if somevalid[lookup] then for i=1,#ps do local p = ps[i] local t = p[1] if t == 'substitution' then local pv = p[2] -- p.variant if pv then local upv = unicodes[pv] if upv and chars[upv] then if trace then logs.report("define otf",string.format("%s: %s (%s) => %s (%s)",kind,chars[k].description.name,k,chars[upv].description.name,upv)) end chars[k] = chars[upv] changed[k] = true end end elseif t == 'alternate' then local pc = p[2] -- p.components if pc then pc = pa.components:match("([^ ]+)") if pc then local upc = unicodes[pc] if upc and chars[upc] then if trace then logs.report("define otf",string.format("%s: %s (%s) => %s (%s)",kind,chars[k].description.name,k,chars[upc].description.name,upc)) end chars[k] = chars[upc] changed[k] = true end end end elseif t == 'ligature' and not changed[k] then local pc = p[2] if pc then if trace then logs.report("define otf",string.format("%s: %s => %s (%s)",kind,pc,chars[k].description.name,k)) end ligatures[#ligatures+1] = { pc, k } end end end end end end end fonts.otf.features.aux.resolve_ligatures(tfmdata,ligatures,kind) end else tfmdata.ligatures = tfmdata.ligatures or { } end end function fonts.initializers.base.otf.liga(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'liga',value) end function fonts.initializers.base.otf.dlig(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'dlig',value) end function fonts.initializers.base.otf.rlig(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'rlig',value) end function fonts.initializers.base.otf.hlig(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'hlig',value) end function fonts.initializers.base.otf.pnum(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'pnum',value) end function fonts.initializers.base.otf.onum(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'onum',value) end function fonts.initializers.base.otf.tnum(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'tnum',value) end function fonts.initializers.base.otf.lnum(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'lnum',value) end function fonts.initializers.base.otf.zero(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'zero',value) end function fonts.initializers.base.otf.smcp(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'smcp',value) end function fonts.initializers.base.otf.cpsp(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'cpsp',value) end function fonts.initializers.base.otf.c2sc(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'c2sc',value) end function fonts.initializers.base.otf.ornm(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'ornm',value) end function fonts.initializers.base.otf.aalt(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'aalt',value) end function fonts.initializers.base.otf.hwid(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'hwid',value) end function fonts.initializers.base.otf.fwid(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'fwid',value) end -- Here comes the real thing ... node processing! The next session prepares -- things. The main features (unchained by rules) have their own caches, -- while the private ones cache locally. do fonts.otf.features.prepare = { } local falsetable = { false, false, false } function fonts.otf.features.prepare.feature(tfmdata,kind,value) if value then local language, script = tfmdata.language or "dflt", tfmdata.script or "dflt" local shared = tfmdata.shared local otfdata = shared.otfdata local lookuptable = fonts.otf.valid_subtable(otfdata,kind,script,language) if lookuptable then local fullkind = kind .. script .. language if not shared.lookuptable [fullkind] then --~ print(tfmdata,file.basename(tfmdata.fullname or ""),kind,script,language,lookuptable,fullkind) local processes = { } -- featuredata and featurecache are indexed by lookup so we can share them shared.featuredata [kind] = shared.featuredata [kind] or { } shared.featurecache[kind] = shared.featurecache[kind] or false -- signal shared.lookuptable [fullkind] = lookuptable shared.processes [fullkind] = processes local types = otfdata.luatex.name_to_type local flags = otfdata.luatex.ignore_flags local preparers = fonts.otf.features.prepare local process = fonts.otf.features.process for i=1,#lookuptable do local lookupname = lookuptable[i] local lookuptype = types[lookupname] local prepare = preparers[lookuptype] if prepare then local processdata = prepare(tfmdata,kind,lookupname) if processdata then local processflags = flags[lookupname] or falsetable --- share false table -- local chain = (lookuptype == "gsub_contextchain") or (lookuptype == "gpos_contextchain") local chain = lookuptype:find("context") ~= nil processes[#processes+1] = { process[lookuptype], lookupname, processdata, processflags, chain } end end end end end end end -- helper: todo, we don't need to store non local ones for chains so we can pass the -- validator as parameter local pairs = pairs function fonts.otf.features.collect_ligatures(tfmdata,kind) -- ligs are spread all over the place local otfdata = tfmdata.shared.otfdata local unicodes = tfmdata.shared.otfdata.luatex.unicodes -- actually the char index is ok too local trace = fonts.otf.trace_features local ligatures = { } local function collect(lookup,o,ps) for i=1,#ps do local p = ps[i] if p[1] == 'ligature' then if trace then logs.report("define otf",string.format("feature %s lookup %s ligature %s => %s",kind,lookup,p[2],o.name)) end local t = ligatures[lookup] if not t then t = { } ligatures[lookup] = t end local first = true for s in p[2]:gmatch("[^ ]+") do local u = unicodes[s] if first then if not t[u] then t[u] = { { } } end t = t[u] first = false else local t1 = t[1] if not t1[u] then t1[u] = { { } } end t = t1[u] end end t[2] = o.unicode end end end local forced, always, okay = fonts.otf.valid_feature(otfdata,kind,tfmdata.script,tfmdata.language) for _,o in pairs(otfdata.glyphs) do local lookups = o.lookups if lookups then if forced then for lookup, ps in pairs(lookups) do collect(lookup,o,ps) end elseif okay then for lookup, ps in pairs(lookups) do if always[lookup] or okay[lookup] then collect(lookup,o,ps) end end else for lookup, ps in pairs(lookups) do if always[lookup] then collect(lookup,o,ps) end end end end end return ligatures end -- gsub_single -> done -- gsub_multiple -> done -- gsub_alternate -> done -- gsub_ligature -> done -- gsub_context -> todo -- gsub_contextchain -> done -- gsub_reversecontextchain -> todo -- we used to share code in the following functions but that was relatively -- due to extensive calls to functions (easily hundreds of thousands per -- document) function fonts.otf.features.prepare.gsub_single(tfmdata,kind,lookupname) local featuredata = tfmdata.shared.featuredata[kind] local substitutions = featuredata[lookupname] if not substitutions then substitutions = { } featuredata[lookupname] = substitutions local otfdata = tfmdata.shared.otfdata local unicodes = otfdata.luatex.unicodes local trace = fonts.otf.trace_features for _, o in pairs(otfdata.glyphs) do local lookups = o.lookups if lookups then for lookup,ps in pairs(lookups) do if lookup == lookupname then for i=1,#ps do local p = ps[i] if p[1] == 'substitution' then local old, new = o.unicode, unicodes[p[2]] substitutions[old] = new if trace then logs.report("define otf",string.format("%s:%s substitution %s => %s",kind,lookupname,old,new)) end end end end end end end end return substitutions end function fonts.otf.features.prepare.gsub_multiple(tfmdata,kind,lookupname) local featuredata = tfmdata.shared.featuredata[kind] local substitutions = featuredata[lookupname] if not substitutions then substitutions = { } featuredata[lookupname] = substitutions local otfdata = tfmdata.shared.otfdata local unicodes = otfdata.luatex.unicodes local trace = fonts.otf.trace_features for _,o in pairs(otfdata.glyphs) do local lookups = o.lookups if lookups then for lookup,ps in pairs(lookups) do if lookup == lookupname then for i=1,#ps do local p = ps[i] if p[1] == 'multiple' then local old, new = o.unicode, { } substitutions[old] = new for pc in p[2]:gmatch("[^ ]+") do new[#new+1] = unicodes[pc] end if trace then logs.report("define otf",string.format("%s:%s multiple %s => %s",kind,lookupname,old,table.concat(new," "))) end end end end end end end end return substitutions end function fonts.otf.features.prepare.gsub_alternate(tfmdata,kind,lookupname) -- todo: configurable preference list local featuredata = tfmdata.shared.featuredata[kind] local substitutions = featuredata[lookupname] if not substitutions then featuredata[lookupname] = { } substitutions = featuredata[lookupname] local otfdata = tfmdata.shared.otfdata local unicodes = otfdata.luatex.unicodes local trace = fonts.otf.trace_features for _,o in pairs(otfdata.glyphs) do local lookups = o.lookups if lookups then for lookup,ps in pairs(lookups) do if lookup == lookupname then for i=1,#ps do local p = ps[i] if p[1] == 'alternate' then local old = o.unicode local t = { } for pc in p[2]:gmatch("[^ ]+") do t[#t+1] = unicodes[pc] end substitutions[old] = t if trace then logs.report("define otf",string.format("%s:%s alternate %s => %s",kind,lookupname,old,table.concat(substitutions,"|"))) end end end end end end end end return substitutions end function fonts.otf.features.prepare.gsub_ligature(tfmdata,kind,lookupname) -- we collect them for all lookups, this saves loops, we only use the -- lookupname for testing, we need to check if this leads to redundant -- collections local ligatures = tfmdata.shared.featuredata[kind] if not ligatures[lookupname] then ligatures = fonts.otf.features.collect_ligatures(tfmdata,kind) tfmdata.shared.featuredata[kind] = ligatures end return ligatures[lookupname] end function fonts.otf.features.prepare.contextchain(tfmdata,kind,lookupname) local featuredata = tfmdata.shared.featuredata[kind] local contexts = featuredata[lookupname] if not contexts then featuredata[lookupname] = { } contexts = featuredata[lookupname] local otfdata = tfmdata.shared.otfdata local unicodes = otfdata.luatex.unicodes local internals = otfdata.luatex.internals local flags = otfdata.luatex.ignore_flags local types = otfdata.luatex.name_to_type otfdata.luatex.covers = otfdata.luatex.covers or { } local characters = tfmdata.characters local cache = otfdata.luatex.covers local function uncover(covers,result) -- lpeg hardly faster (.005 sec on mk) for n=1,#covers do local c = covers[n] local cc = cache[c] if not cc then local t = { } for s in c:gmatch("[^ ]+") do t[unicodes[s]] = true end cache[c] = t result[#result+1] = t else result[#result+1] = cc end end end local lookupdata = otfdata.lookups[lookupname] if not lookupdata then logs.error("otf process", string.format("missing lookupdata table %s",lookupname)) elseif lookupdata.rules then local rules = lookupdata.rules local center_match = fonts.otf.center_match for nofrules=1,#rules do local rule = rules[nofrules] local coverage = rule.coverage if coverage and coverage.current then local current, before, after, sequence = coverage.current, coverage.before, coverage.after, { } if before then uncover(before,sequence) end local start = #sequence + 1 uncover(current,sequence) local stop = #sequence if after then uncover(after,sequence) end if sequence[1] then local lookups, lookuptype = rule.lookups, 'self' -- for the moment only lookup index 1 if lookups then if #lookups > 1 then logs.report("otf process","WARNING: more than one lookup in rule") end lookuptype = types[lookups[1]] end -- this may be wrong; we cannot copy inside the for loop (out of memory with hz); -- so we may end up with a different usage of sequence in the chainproc handlers sequence = table.copy(sequence) -- we trigger on the first character in current for unic, _ in pairs(sequence[start]) do local t = contexts[unic] if not t then contexts[unic] = { lookups={}, flags=flags[lookupname] } t = contexts[unic].lookups end t[#t+1] = { nofrules, lookuptype, sequence, start, stop, lookups } end end end end end end return contexts end fonts.otf.features.prepare.gsub_context = fonts.otf.features.prepare.contextchain fonts.otf.features.prepare.gsub_contextchain = fonts.otf.features.prepare.contextchain fonts.otf.features.prepare.gsub_reversecontextchain = fonts.otf.features.prepare.contextchain -- ruled->lookup=ks_latn_l_27_c_4 => internal[ls_l_84] => valid[ls_l_84_s] -- gpos_mark2base -> done -- gpos_mark2ligature -> done -- gpos_mark2mark -> done -- gpos_single -> not done -- gpos_pair -> not done -- gpos_cursive -> not done -- gpos_context -> not done -- gpos_reversecontextchain -> not done function fonts.otf.features.prepare.anchors(tfmdata,kind,lookupname) -- tracing local featuredata = tfmdata.shared.featuredata[kind] local anchors = featuredata[lookupname] if not anchors then anchors = { } featuredata[lookupname] = anchors local otfdata = tfmdata.shared.otfdata local unicodes = otfdata.luatex.unicodes local validanchors = { } local glyphs = otfdata.glyphs local trace = fonts.otf.trace_features if otfdata.anchor_classes then local classes = otfdata.anchor_classes for k=1,#classes do local class = classes[k] if class.lookup == lookupname then if trace then logs.report("define otf",string.format("%s:%s anchor -> %s",kind,lookupname,class.name)) end validanchors[class.name] = true end end end for _,o in pairs(glyphs) do local oanchor = o.anchors if oanchor then local t, ok = { }, false for type, anchors in pairs(oanchor) do -- types local tt = false for name, anchor in pairs(anchors) do if validanchors[name] then if not tt then tt = { [name] = anchor } t[type] = tt ok = true else tt[name] = anchor end end end end if ok then anchors[o.unicode] = t end end end end --~ if kind == "mkmk" then print(lookupname,table.serialize(anchors)) end return anchors end fonts.otf.features.prepare.gpos_mark2base = fonts.otf.features.prepare.anchors fonts.otf.features.prepare.gpos_mark2ligature = fonts.otf.features.prepare.anchors fonts.otf.features.prepare.gpos_mark2mark = fonts.otf.features.prepare.anchors fonts.otf.features.prepare.gpos_cursive = fonts.otf.features.prepare.anchors fonts.otf.features.prepare.gpos_context = fonts.otf.features.prepare.contextchain fonts.otf.features.prepare.gpos_contextchain = fonts.otf.features.prepare.contextchain function fonts.otf.features.prepare.gpos_single(tfmdata,kind,lookupname) logs.report("otf define","gpos_single not yet supported") end -- ["kerns"]={ { ["char"]="ytilde", ["lookup"]="pp_l_1_s", ["off"]=-83, ... -- ["mykerns"] = { ["pp_l_1_s"] = { [67] = -28, ... function fonts.otf.features.prepare.gpos_pair(tfmdata,kind,lookupname) local featuredata = tfmdata.shared.featuredata[kind] local kerns = featuredata[lookupname] if not kerns then local trace = fonts.otf.trace_features featuredata[lookupname] = { } kerns = featuredata[lookupname] local otfdata = tfmdata.shared.otfdata local unicodes = otfdata.luatex.unicodes local glyphs = otfdata.glyphs -- ff has isolated kerns in a separate table for k,o in pairs(glyphs) do local list = o.mykerns if list then local omk = list[lookupname] if omk then local one = o.unicode for char, off in pairs(omk) do local two = char local krn = kerns[one] if krn then krn[two] = off else kerns[one] = { two = off } end if trace then logs.report("define otf",string.format("feature %s kern pair %s - %s",kind,one,two)) end end end elseif o.kerns then local one = o.unicode local okerns = o.kerns for ok=1,#okerns do local k = okerns[ok] if k.lookup == lookupname then local char = k.char if char then local two = unicodes[char] local krn = kerns[one] if krn then krn[two] = k.off else kerns[one] = { two = k.off } end if trace then logs.report("define otf",string.format("feature %s kern pair %s - %s",kind,one,two)) end end end end end list = o.lookups if list then local one = o.unicode for lookup,ps in pairs(list) do if lookup == lookupname then for i=1,#ps do local p = ps[i] if p[1] == 'pair' then local two = unicodes[p[2]] local krn = kerns[one] if krn then krn[two] = p else kerns[one] = { two = p } end if trace then logs.report("define otf",string.format("feature %s kern pair %s - %s",kind,one,two)) end end end end end end end end return kerns end fonts.otf.features.prepare.gpos_contextchain = fonts.otf.features.prepare.contextchain end -- can be generalized: one loop in main do local prepare = fonts.otf.features.prepare.feature function fonts.initializers.node.otf.aalt(tfm,value) return prepare(tfm,'aalt',value) end function fonts.initializers.node.otf.abvm(tfm,value) return prepare(tfm,'abvm',value) end function fonts.initializers.node.otf.afrc(tfm,value) return prepare(tfm,'afrc',value) end function fonts.initializers.node.otf.akhn(tfm,value) return prepare(tfm,'akhn',value) end function fonts.initializers.node.otf.blwm(tfm,value) return prepare(tfm,'blwm',value) end function fonts.initializers.node.otf.c2pc(tfm,value) return prepare(tfm,'c2pc',value) end function fonts.initializers.node.otf.c2sc(tfm,value) return prepare(tfm,'c2sc',value) end function fonts.initializers.node.otf.calt(tfm,value) return prepare(tfm,'calt',value) end function fonts.initializers.node.otf.case(tfm,value) return prepare(tfm,'case',value) end function fonts.initializers.node.otf.ccmp(tfm,value) return prepare(tfm,'ccmp',value) end function fonts.initializers.node.otf.clig(tfm,value) return prepare(tfm,'clig',value) end function fonts.initializers.node.otf.cpsp(tfm,value) return prepare(tfm,'cpsp',value) end function fonts.initializers.node.otf.cswh(tfm,value) return prepare(tfm,'cswh',value) end function fonts.initializers.node.otf.curs(tfm,value) return prepare(tfm,'curs',value) end function fonts.initializers.node.otf.dlig(tfm,value) return prepare(tfm,'dlig',value) end function fonts.initializers.node.otf.dnom(tfm,value) return prepare(tfm,'dnom',value) end function fonts.initializers.node.otf.expt(tfm,value) return prepare(tfm,'expt',value) end function fonts.initializers.node.otf.fin2(tfm,value) return prepare(tfm,'fin2',value) end function fonts.initializers.node.otf.fin3(tfm,value) return prepare(tfm,'fin3',value) end function fonts.initializers.node.otf.fina(tfm,value) return prepare(tfm,'fina',value) end function fonts.initializers.node.otf.frac(tfm,value) return prepare(tfm,'frac',value) end function fonts.initializers.node.otf.fwid(tfm,value) return prepare(tfm,'fwid',value) end function fonts.initializers.node.otf.haln(tfm,value) return prepare(tfm,'haln',value) end function fonts.initializers.node.otf.hist(tfm,value) return prepare(tfm,'hist',value) end function fonts.initializers.node.otf.hkna(tfm,value) return prepare(tfm,'hkna',value) end function fonts.initializers.node.otf.hlig(tfm,value) return prepare(tfm,'hlig',value) end function fonts.initializers.node.otf.hngl(tfm,value) return prepare(tfm,'hngl',value) end function fonts.initializers.node.otf.hwid(tfm,value) return prepare(tfm,'hwid',value) end function fonts.initializers.node.otf.init(tfm,value) return prepare(tfm,'init',value) end function fonts.initializers.node.otf.isol(tfm,value) return prepare(tfm,'isol',value) end function fonts.initializers.node.otf.ital(tfm,value) return prepare(tfm,'ital',value) end function fonts.initializers.node.otf.jp78(tfm,value) return prepare(tfm,'jp78',value) end function fonts.initializers.node.otf.jp83(tfm,value) return prepare(tfm,'jp83',value) end function fonts.initializers.node.otf.jp90(tfm,value) return prepare(tfm,'jp90',value) end function fonts.initializers.node.otf.kern(tfm,value) return prepare(tfm,'kern',value) end function fonts.initializers.node.otf.liga(tfm,value) return prepare(tfm,'liga',value) end function fonts.initializers.node.otf.lnum(tfm,value) return prepare(tfm,'lnum',value) end function fonts.initializers.node.otf.locl(tfm,value) return prepare(tfm,'locl',value) end function fonts.initializers.node.otf.mark(tfm,value) return prepare(tfm,'mark',value) end function fonts.initializers.node.otf.med2(tfm,value) return prepare(tfm,'med2',value) end function fonts.initializers.node.otf.medi(tfm,value) return prepare(tfm,'medi',value) end function fonts.initializers.node.otf.mgrk(tfm,value) return prepare(tfm,'mgrk',value) end function fonts.initializers.node.otf.mkmk(tfm,value) return prepare(tfm,'mkmk',value) end function fonts.initializers.node.otf.nalt(tfm,value) return prepare(tfm,'nalt',value) end function fonts.initializers.node.otf.nlck(tfm,value) return prepare(tfm,'nlck',value) end function fonts.initializers.node.otf.nukt(tfm,value) return prepare(tfm,'nukt',value) end function fonts.initializers.node.otf.numr(tfm,value) return prepare(tfm,'numr',value) end function fonts.initializers.node.otf.onum(tfm,value) return prepare(tfm,'onum',value) end function fonts.initializers.node.otf.ordn(tfm,value) return prepare(tfm,'ordn',value) end function fonts.initializers.node.otf.ornm(tfm,value) return prepare(tfm,'ornm',value) end function fonts.initializers.node.otf.pnum(tfm,value) return prepare(tfm,'pnum',value) end function fonts.initializers.node.otf.pref(tfm,value) return prepare(tfm,'pref',value) end function fonts.initializers.node.otf.pres(tfm,value) return prepare(tfm,'pres',value) end function fonts.initializers.node.otf.pstf(tfm,value) return prepare(tfm,'pstf',value) end function fonts.initializers.node.otf.rlig(tfm,value) return prepare(tfm,'rlig',value) end function fonts.initializers.node.otf.rphf(tfm,value) return prepare(tfm,'rphf',value) end function fonts.initializers.node.otf.salt(tfm,value) return prepare(tfm,'salt',value) end function fonts.initializers.node.otf.sinf(tfm,value) return prepare(tfm,'sinf',value) end function fonts.initializers.node.otf.smcp(tfm,value) return prepare(tfm,'smcp',value) end function fonts.initializers.node.otf.smpl(tfm,value) return prepare(tfm,'smpl',value) end function fonts.initializers.node.otf.ss01(tfm,value) return prepare(tfm,'ss01',value) end function fonts.initializers.node.otf.ss02(tfm,value) return prepare(tfm,'ss02',value) end function fonts.initializers.node.otf.ss03(tfm,value) return prepare(tfm,'ss03',value) end function fonts.initializers.node.otf.ss04(tfm,value) return prepare(tfm,'ss04',value) end function fonts.initializers.node.otf.ss05(tfm,value) return prepare(tfm,'ss05',value) end function fonts.initializers.node.otf.ss06(tfm,value) return prepare(tfm,'ss06',value) end function fonts.initializers.node.otf.ss07(tfm,value) return prepare(tfm,'ss07',value) end function fonts.initializers.node.otf.ss08(tfm,value) return prepare(tfm,'ss08',value) end function fonts.initializers.node.otf.ss09(tfm,value) return prepare(tfm,'ss09',value) end function fonts.initializers.node.otf.subs(tfm,value) return prepare(tfm,'subs',value) end function fonts.initializers.node.otf.sups(tfm,value) return prepare(tfm,'sups',value) end function fonts.initializers.node.otf.swsh(tfm,value) return prepare(tfm,'swsh',value) end function fonts.initializers.node.otf.titl(tfm,value) return prepare(tfm,'titl',value) end function fonts.initializers.node.otf.tnam(tfm,value) return prepare(tfm,'tnam',value) end function fonts.initializers.node.otf.tnum(tfm,value) return prepare(tfm,'tnum',value) end function fonts.initializers.node.otf.trad(tfm,value) return prepare(tfm,'trad',value) end function fonts.initializers.node.otf.unic(tfm,value) return prepare(tfm,'unic',value) end function fonts.initializers.node.otf.zero(tfm,value) return prepare(tfm,'zero',value) end end do -- todo: use nodes helpers local glyph = node.id('glyph') local glue = node.id('glue') local kern = node.id('kern') local disc = node.id('disc') local fontdata = fonts.tfm.id local has_attribute = node.has_attribute local set_attribute = node.set_attribute local state = attributes.numbers['state'] or 100 local marknumber = attributes.numbers['mark'] or 200 local format = string.format local report = logs.report local scale = tex.scale fonts.otf.features.process = { } -- we share some vars here, after all, we have no nested lookups and -- less code local tfmdata = false local otfdata = false local characters = false local marks = false local glyphs = false local currentfont = false -- we cheat a bit and assume that a font,attr combination are kind of ranged local context_setups = fonts.define.specify.context_setups local context_numbers = fonts.define.specify.context_numbers -- 1 loop over glyphs loop over lookups, quit at match -- 2 loop over glyphs loop over lookups, continue at match -- 3 loop over lookups loop over glyphs fonts.otf.strategy = 2 function fonts.otf.features.process.feature(head,font,attr,kind,attribute) tfmdata = fontdata[font] local shared = tfmdata.shared otfdata = shared.otfdata characters = tfmdata.characters marks = otfdata.luatex.marks glyphs = otfdata.glyphs currentfont = font local script, language, strategy if attr and attr > 0 then local features = context_setups[context_numbers[attr]] language, script, strategy = features.language or "dflt", features.script or "dflt", features.strategy or fonts.otf.strategy else language, script, strategy = tfmdata.language or "dflt", tfmdata.script or "dflt", tfmdata.strategy or fonts.otf.strategy end local fullkind = kind .. script .. language local lookuptable = shared.lookuptable[fullkind] if lookuptable then -- local strategy = fonts.otf.strategy local types = otfdata.luatex.name_to_type local start, done, ok = head, false, false local processes = shared.processes[fullkind] if #processes == 1 then local p = processes[1] while start do -- evt splitsen if start.id == glyph then if start.subtype<256 and start.font == font and (not attr or has_attribute(start,0,attr)) and -- dynamic feature (not attribute or has_attribute(start,state,attribute)) then -- we can make the p vars also global to this closure local pp = p[3] -- all lookups local pc = pp[start.char] if pc then start, ok = p[1](start,kind,p[2],pc,pp,p[4]) done = done or ok if start then start = start.next end else start = start.next end else start = start.next end elseif start.id == glue and p[5] then local pp = p[3] -- all lookups local pc = pp[32] -- space if pc then start, ok = p[1](start,kind,p[2],pc,pp,p[4]) done = done or ok if start then start = start.next end else start = start.next end else start = start.next end end elseif strategy == 3 then for i=1,#processes do local p = processes[i] local pp = p[3] start = head while start do if start.id == glyph then if start.subtype<256 and start.font == font and (not attr or has_attribute(start,0,attr)) and -- dynamic feature (not attribute or has_attribute(start,state,attribute)) then local pc = pp[start.char] if pc then start, ok = p[1](start,kind,p[2],pc,pp,p[4]) if ok then done = true end -- else if start then start = start.next end else start = start.next end else start = start.next end elseif start.id == glue then if p[5] then -- chain local pc = pp[32] if pc then start, ok = p[1](start,kind,p[2],pc,p[3],p[4]) if ok then done = true end if start then start = start.next end else start = start.next end else start = start.next end else start = start.next end end end else while start do if start.id == glyph then if start.subtype<256 and start.font == font and (not attr or has_attribute(start,0,attr)) and -- dynamic feature (not attribute or has_attribute(start,state,attribute)) then local chr = start.char -- used ? for i=1,#processes do local p = processes[i] local pp = p[3] --~ local pc = pp[chr] local pc = pp[start.char] if pc then start, ok = p[1](start,kind,p[2],pc,pp,p[4]) if ok then done = true if strategy == 1 then break end end -- else if not start then break end end end if start then start = start.next end elseif start.id == glue then for i=1,#processes do local p = processes[i] if p[5] then -- chain local pp = p[3] local pc = pp[32] if pc then start, ok = p[1](start,kind,p[2],pc,pp,p[4]) if ok then done = true if strategy == 1 then break end end if not start then break end end end end if start then start = start.next end else start = start.next end else start = start.next end end end return head, done else return head, false end end -- we can assume that languages that use marks are not hyphenated -- we can also assume that at most one discretionary is present local function toligature(start,stop,char,markflag,discfound) -- brr head if start ~= stop then if discfound then local lignode = node.copy(start) lignode.font = start.font lignode.char = char lignode.subtype = 2 start = node.do_ligature_n(start, stop, lignode) if start.id == disc then local prev = start.prev start = start.next end else local deletemarks = markflag ~= "mark" start.components = node.copy_list(start,stop) node.slide(start.components) -- todo: components start.subtype = 2 start.char = char local marknum = 1 local next = start.next while true do if marks[next.char] then if not deletemarks then set_attribute(next,marknumber,marknum) end else marknum = marknum + 1 end if next == stop then break else next = next.next end end next = stop.next while next do if next.id == glyph and next.font == currentfont and marks[next.char] then set_attribute(next,marknumber,marknum) next = next.next else break end end local next = start.next while next do if next == stop or deletemarks or marks[next.char] then local crap = next local np, nn = next.prev, next.next np.next = nn if nn then nn.prev = np end if next == stop then stop = crap.prev node.free(crap) break else next = nn node.free(crap) end else next = nn end end end end return start end function fonts.otf.features.process.gsub_single(start,kind,lookupname,replacements) if replacements then if fonts.otf.trace_replacements then report("otf process",format("%s:%s replacing 0x%04X by 0x%04X",kind,lookupname,start.char,replacements)) end start.char = replacements return start, true else return start, false end end function fonts.otf.features.process.gsub_alternate(start,kind,lookupname,alternatives) if alternatives then if fonts.otf.trace_replacements then report("otf process",format("%s:%s alternative 0x%04X => %s",kind,lookupname,start.char,table.hexed(alternatives))) end start.char = alternatives[1] -- will be preference return start, true else return start, false end end function fonts.otf.features.process.gsub_multiple(start,kind,lookupname,multiples) if multiples then if fonts.otf.trace_replacements then report("otf process",format("%s:%s multiple 0x%04X => %s",kind,lookupname,start.char,table.hexed(multiples))) end start.char = multiples[1] if #multiples > 1 then for k=2,#multiples do local n = node.copy(start) local sn = start.next n.char = multiples[k] n.next = sn n.prev = start if sn then sn.prev = n end start.next = n start = n end end return start, true else return start, false end end function fonts.otf.features.process.gsub_ligature(start,kind,lookupname,ligatures,alldata,flags) local s, stop, discfound = start.next, nil, false while s do local id = s.id if id == glyph and s.subtype<256 then if s.font == currentfont then if marks[s.char] then s = s.next else local lg = ligatures[1][s.char] if not lg then break else stop = s ligatures = lg s = s.next end end else break end elseif id == disc then discfound = true s = s.next else break end end if stop and ligatures[2] then start = toligature(start,stop,ligatures[2],flags[1],discfound) if fonts.otf.trace_ligatures then report("otf process",format("%s: inserting ligature 0x%04X (%s)",kind,start.char,utf.char(start.char))) end return start, true end return start, false end function fonts.otf.features.process.gpos_mark2base(start,kind,lookupname,m_anchors,b_anchors) local markchar = start.char if marks[markchar] then local markanchors = m_anchors['mark'] if markanchors then local component = start.prev while component and component.id == glyph and component.subtype<256 and component.font == currentfont do local basechar = component.char if marks[basechar] then component = component.prev else local baseanchors = b_anchors[basechar] if baseanchors then baseanchors = baseanchors['basechar'] if baseanchors then for anchor, ma in pairs(markanchors) do local ba = baseanchors[anchor] if ba then local factor = tfmdata.factor local dx, dy = scale(ba[1]-ma[1],factor), scale(ba[2]-ma[2],factor) start.xoffset, start.yoffset = component.xoffset - dx, component.yoffset + dy if fonts.otf.trace_anchors then report("otf process",format("%s: anchoring mark 0x%04X to basechar 0x%04X => (%s,%s) => (%s,%s)", kind,markchar,basechar,dx,dy,start.xoffset,start.yoffset)) end return start, true end end end end break end end end end return start, false end function fonts.otf.features.process.gpos_mark2ligature(start,kind,lookupname,m_anchors,b_anchors) -- maybe use copies local markchar = start.char if marks[markchar] then local markanchors = m_anchors['mark'] if markanchors then local component = start.prev while component and component.id == glyph and component.subtype<256 and component.font == currentfont do local basechar = component.char if marks[basechar] then component = component.prev else local baseanchors = b_anchors[basechar] if baseanchors then baseanchors = baseanchors['baselig'] if baseanchors then for anchor, ma in pairs(markanchors) do local ba = baseanchors[anchor] if ba then local n = has_attribute(start,marknumber) ba = ba[n] if ba then local factor = tfmdata.factor local dx, dy = scale(ba[1]-ma[1],factor), scale(ba[2]-ma[2],factor) start.xoffset, start.yoffset = component.xoffset - dx, component.yoffset + dy if fonts.otf.trace_anchors then report("otf process",format("%s: anchoring mark 0x%04X to baseligature 0x%04X => (%s,%s) => (%s,%s)", kind,markchar,basechar,dx,dy,component.xoffset,component.yoffset)) end return start, true end end end end end break end end return start, done end end return start, false end function fonts.otf.features.process.gpos_mark2mark(start,kind,lookupname,b_anchors,m_anchors) local basemarkchar = start.char --~ print(lookupname) if marks[basemarkchar] then --~ print('') --~ print('basemarkchar',basemarkchar) --~ print('basemarkanchors', table.serialize(b_anchors)) local baseanchors = b_anchors['basemark'] if baseanchors then local component = start.next while component and component.id == glyph and component.subtype<256 and component.font == currentfont do local markchar = component.char if not marks[markchar] then break else --~ print('markchar',markchar) local basemarkattr = has_attribute(start, marknumber) or 1 local markattr = has_attribute(component,marknumber) or 1 --~ print(basemarkattr,markattr) if basemarkattr == markattr then -- still needed? --~ print('markanchors *', table.serialize(m_anchors)) local markanchors = m_anchors[markchar] if markanchors then --~ print('markanchors') local markanchor = markanchors['mark'] if markanchor then for anchor,ma in pairs(markanchor) do local ba = baseanchors[anchor] if ba then local factor = tfmdata.factor local dx, dy = scale(ba[1]-ma[1],factor), scale(ba[2]-ma[2],factor) component.xoffset, component.yoffset = start.xoffset - dx, start.yoffset + dy if fonts.otf.trace_anchors then report("otf process",format("%s:%s:%s anchoring mark 0x%04X to basemark 0x%04X => (%s,%s) => (%s,%s)", kind,anchor,markattr,markchar,basemarkchar,dx,dy,component.xoffset,component.yoffset)) end return start, true end end end end component = component.next end end end end end return start, false end -- the following can be optimized, also, we can share the table (no need to collect) function fonts.otf.features.process.gpos_cursive(start,kind,lookupname,exitanchors,anchors) local trace = fonts.otf.trace_anchors local next, done, x, y, total_x, total_y, tx, ty, first = start.next, false, 0, 0, 0, 0, { }, { }, nil local factor = tfmdata.factor local function finish() local i = 0 while first and first.id == glyph do if marks[first.char] then first = first.next else i = i + 1 first.yoffset = scale(total_y, factor) if fonts.otf.trace_cursive then report("otf process",format("%s:%s move 0x%04X cursive (%s,%s)",kind,lookupname,first.char,"?",total_y)) end if first == next then break else total_y = total_y - (ty[i] or 0) first = first.next end end end x, y, total_x, total_y, tx, ty, first = 0, 0, 0, 0, { }, { }, nil end while next do if next.id == glyph and next.subtype<256 and next.font == currentfont then local nextchar = next.char if marks[nextchar] then next = next.next else local entryanchors, exitanchors = anchors[nextchar], anchors[start.char] if entryanchors and exitanchors then local centry, cexit = entryanchors['centry'], exitanchors['cexit'] if centry and cexit then for anchor, entry in pairs(centry) do local exit = cexit[anchor] if exit then if not first then first = start end local dx, dy = exit[1] + entry[1], -exit[2] + entry[2] tx[#tx+1], ty[#ty+1] = dx, dy total_x, total_y = total_x + dx, total_y + dy done = true break end end else finish() end else finish() end start = next next = start.next end else finish() break end end return start, done end function fonts.otf.features.process.gpos_single(start,kind,lookupname,basekerns,kerns) report("otf process","gpos_single not yet supported") return start, false end function fonts.otf.features.process.gpos_pair(start,kind,lookupname,basekerns,kerns) local next, prev, done = start.next, start, false -- to be optimized local trace = fonts.otf.trace_kerns local factor = tfmdata.factor while next and next.id == glyph and next.subtype<256 and next.font == currentfont do local cn = characters[next.char] if not cn or cn.description.class == 'mark' then prev = next next = next.next else local krn = basekerns[next.char] if not krn then -- skip elseif type(krn) == "table" then local a, b = krn[3], krn[7] if a and a ~= 0 then local k = nodes.kern(scale(a,factor)) k.next = next k.prev = prev prev.next = k next.prev = k if trace then -- todo end end if b and b ~= 0 then report("otf process","we need to do something with the second kern xoff " .. b) end else -- todo, just start, next = node.insert_before(head,next,nodes.kern(scale(kern,factor))) if fonts.otf.trace_kerns then report("otf process",format("%s: inserting kern %s between 0x%04X and 0x%04X",kind,krn,prev.char,next.char)) end local k = nodes.kern(scale(krn,factor)) k.next = next k.prev = prev prev.next = k next.prev = k end break end end return start, done end local chainprocs = { } -- we can probably optimize this because they're all internal lookups -- For the moment we save each looked up glyph in the sequence, which is ok because -- each lookup in the chain has its own sequence. This saves memory. Only ligatures -- are stored in the featurecache, because we don't want to loop over all characters -- in order to locate them. function chainprocs.gsub_single(start,stop,kind,lookupname,sequence,f,l,lookups) local trace = fonts.otf.trace_replacements local c, r = trace and { }, trace and { } local lookup, index, current = 1, f, start while current ~= nil do if current.id == glyph then -- test for more ? local char = current.char local cacheslot = sequence[index] local replacement = cacheslot[char] if replacement == true then if lookups then local looks = glyphs[tfmdata.characters[char].description.index].lookups -- SLOW, USE OTFDATA if looks then local glyphlookups = otfdata.luatex.internals[lookups[lookup]].lookups local unicodes = otfdata.luatex.unicodes for gl=1,#glyphlookups do local lv = looks[glyphlookups[gl]] if lv then replacement = unicodes[lv[1][2]] or char cacheslot[char] = replacement break end end else replacement, cacheslot[char] = char, char end else replacement, cacheslot[char] = char, char end end if trace then c[#c+1], r[#r+1] = char, replacement end current.char = replacement if current == stop then break else current, lookup, index = current.next, lookup + 1, index + 1 end elseif current == stop then break else current = current.next end end if trace then report("otf chain",format("%s: single replacement %s by %s",kind,table.hexed(c),table.hexed(r))) end return start end function chainprocs.gsub_multiple(start,stop,kind,lookupname,sequence,f,l,lookups) local char = start.char local cacheslot = sequence[f] -- [1] local replacement = cacheslot[char] if replacement == true then if lookups then local looks = glyphs[tfmdata.characters[char].description.index].lookups if looks then local lookups = otfdata.luatex.internals[lookups[1]].lookups local unicodes = otfdata.luatex.unicodes for l=1,#lookups do local lv = looks[lookups[l]] if lv then replacement = { } for c in lv[1][2]:gmatch("[^ ]+") do replacement[#replacement+1] = unicodes[c] end cacheslot[char] = replacement break end end else replacement = { char } cacheslot[char] = replacement end else replacement = { char } cacheslot[char] = replacement end end if fonts.otf.trace_replacements then report("otf chain",format("%s: replacing character 0x%04X by multiple 0x%04X",kind,char,table.hexed(replacement))) end start.char = replacement[1] if #replacement > 1 then for k=2,#replacement do local n = node.copy(start) local sn = start.next n.char = replacement[k] n.next = sn n.prev = start if sn then sn.prev = n end start.next = n start = n end end return start end function chainprocs.gsub_alternate(start,stop,kind,lookupname,sequence,f,l,lookups) local char = start.char local cacheslot = sequence[f] -- [1] local replacement = cacheslot[char] if replacement == true then if lookups then local looks = glyphs[tfmdata.characters[char].description.index].lookups if looks then local lookups = otfdata.luatex.internals[lookups[1]].lookups local unicodes = otfdata.luatex.unicodes for l=1,#lookups do local lv = looks[lookups[l]] if lv then replacement = { } for c in lv[1][2]:gmatch("[^ ]+") do replacement[#replacement+1] = unicodes[c] end cacheslot[char] = replacement break end end else replacement = { char } cacheslot[char] = replacement end else replacement = { char } cacheslot[char] = replacement end end if fonts.otf.trace_replacements then report("otf chain",format("%s: replacing character 0x%04X by alternate",kind,char)) end start.char = replacement[1] return start end function chainprocs.gsub_ligature(start,stop,kind,lookupname,sequence,f,l,lookups,flags) if lookups then local featurecache = fontdata[currentfont].shared.featurecache if not featurecache[kind] then featurecache[kind] = fonts.otf.features.collect_ligatures(tfmdata,kind) -- double cached ? end local lookups = otfdata.luatex.internals[lookups[1]].lookups local ligaturecache = featurecache[kind] local trace = fonts.otf.trace_ligatures for i=1,#lookups do local ligatures = ligaturecache[lookups[i]] if ligatures and ligatures[start.char] then ligatures = ligatures[start.char] local s, discfound = start.next, false while s do local id = s.id if id == disc then s = s.next discfound = true elseif characters[s.char].description.class == 'mark' then -- marks s = s.next else local lg = ligatures[1][s.char] if not lg then break else ligatures = lg if s == stop then break else s = s.next end end end end if ligatures[2] then if trace then if start == stop then report("otf chain",format("%s: replacing character 0x%04X by ligature 0x%04X",kind,start.char,ligatures[2])) else report("otf chain",format("%s: replacing character 0x%04X upto 0x%04X by ligature 0x%04X",kind,start.char,stop.char,ligatures[2])) end end return toligature(start,stop,ligatures[2],flags[1],discfound) end break end end end return stop end -- weird, mkmk can have a mark2base, in idris font function chainprocs.gpos_mark2base(start,stop,kind,lookupname,sequence,f,l,lookups,flags) -- dynamic resolver local markchar = start.char if marks[markchar] then local anchortag = sequence[f][markchar] if anchortag == true then local ok = false local classes = otfdata.anchor_classes local lookups = otfdata.luatex.internals[lookups[1]].lookups for k=1,#classes do local v = classes[k] if v.lookup == lookups[1] then -- let's gamble for uniqueness: and v.type == kind then anchortag = v.name sequence[f][markchar] = anchortag ok = true break end end if not ok and fonts.otf.trace_anchors then report("otf chain",format("%s: no matching mark2base anchor class for 0x%04X, lookup %s",kind,markchar,lookups[1])) end end if anchortag ~= true then local component = start.prev while component and component.id == glyph and component.subtype<256 and component.font == currentfont do local basechar = component.char if marks[basechar] then component = component.prev else local bglyph = glyphs[characters[basechar].description.index] -- startchar local baseanchors = bglyph.anchors['basechar'] if baseanchors then local ba = baseanchors[anchortag] if ba then local mglyph = glyphs[characters[markchar].description.index] local markanchors = mglyph.anchors['mark'] if markanchors then local ma = markanchors[anchortag] if ma then local factor = tfmdata.factor local dx, dy = scale(ba[1]-ma[1],factor), scale(ba[2]-ma[2],factor) start.xoffset, start.yoffset = component.xoffset - dx, component.yoffset + dy if fonts.otf.trace_anchors then report("otf chain",format("%s: anchoring mark 0x%04X to basechar 0x%04X => (%s,%s) => (%s,%s)", kind,markchar,basechar,dx,dy,start.xoffset,start.yoffset)) end return start, true end end end end break end end end end return start, false end function chainprocs.gpos_mark2ligature(start,stop,kind,lookupname,sequence,f,l,lookups,flags) -- dynamic resolver local markchar = start.char if marks[markchar] then local anchortag = sequence[f][markchar] if anchortag == true then local classes = otfdata.anchor_classes local lookups = otfdata.luatex.internals[lookups[1]].lookups local ok = false for k=1,#classes do local v = classes[k] if v.lookup == lookups[1] then -- and v.type == kind then anchortag = v.name sequence[f][markchar] = anchortag ok = true break end end if not ok and fonts.otf.trace_anchors then report("otf chain",format("%s: no matching mark2ligature anchor class for 0x%04X, lookup %s",kind,markchar,lookups[1])) end end if anchortag ~= true then local component = start.prev while component and component.id == glyph and component.subtype<256 and component.font == currentfont do local basechar = component.char if marks[basechar] then component = component.prev else local bglyph = glyphs[characters[basechar].description.index] -- startchar local baseanchors = bglyph.anchors['baselig'] if baseanchors then local ba = baseanchors[anchortag] if ba then local n = has_attribute(start,marknumber) ba = ba[n] -- ok ? if ba then local mglyph = glyphs[characters[markchar].description.index] local markanchors = mglyph.anchors['mark'] if markanchors then local ma = markanchors[anchortag] if ma then local factor = tfmdata.factor local dx, dy = scale(ba[1]-ma[1],factor), scale(ba[2]-ma[2],factor) start.xoffset, start.yoffset = component.xoffset - dx, component.yoffset + dy if fonts.otf.trace_anchors then report("otf chain",format("%s: anchoring mark 0x%04X to baseligature 0x%04X => (%s,%s) => (%s,%s)", kind,basechar,markchar,dx,dy,start.xoffset,start.yoffset)) end return start, true end end end end end break end end end end return start, false end -- to be checked function chainprocs.gpos_mark2mark(start,stop,kind,lookupname,sequence,f,l,lookups) local component = start.next if component and component.id == glyph and component.subtype<256 and component.font == currentfont and marks[component.char] then local markchar = start.char local anchortag = sequence[f][markchar] -- [1][char] if anchortag == true then local classes = otfdata.anchor_classes local ok = false for k=1,#classes do local v = classes[k] if v.lookup == lookupname then -- and v.type == kind then anchortag = v.name sequence[f][markchar] = anchortag ok = true break end end if not ok and fonts.otf.trace_anchors then report("otf chain",format("%s: no matching mark2mark anchor class for 0x%04X, lookup %s",kind,markchar,lookups[1])) end end if anchortag ~= true then -- the following may have been be spoiled while idrising the other ones local markattr = has_attribute(start, marknumber) or 1 -- i need to check this ! 1 is new ! local baseattr = has_attribute(component,marknumber) or 1 -- i need to check this ! 1 is new ! if baseattr == markattr then local glyph = glyphs[characters[markchar].description.index] if glyph.anchors and glyph.anchors[anchortag] then local trace = fonts.otf.trace_anchors local done = false local baseanchors = glyph.anchors['basemark'][anchortag] while true do local basechar = component.char local charnext = characters[basechar] local markanchors = glyphs[charnext.description.index].anchors['mark'][anchortag] if markanchors then for anchor,data in pairs(markanchors) do local ba = baseanchors[anchor] if ba then local factor = tfmdata.factor local dx, dy = scale(ba[1]-ma[1],factor), scale(ba[2]-ma[2],factor) start.xoffset, start.yoffset = component.xoffset - dx, component.yoffset + dy if fonts.otf.trace_anchors then report("otf chain",format("%s: anchoring mark 0x%04X to basemark 0x%04X => (%s,%s) => (%s,%s)", kind,markchar,basechar,dx,dy,component.xoffset,component.yoffset)) end done = true break end end end component = component.next if component and component.id == glyph and component.subtype<256 and component.font == currentfont and marks[component.char] then markattr = has_attribute(component,marknumber) if baseattr ~= markattr then break end else break end end return start, done end end end end return start, false end function chainprocs.gpos_cursive(start,stop,kind,lookupname,sequence,f,l,lookups) report("otf chain","chainproc gpos_cursive not yet supported") return start end function chainprocs.gpos_single(start,stop,kind,lookupname,sequence,f,l,lookups) report("otf process","chainproc gpos_single not yet supported") return start end function chainprocs.gpos_pair(start,stop,kind,lookupname,sequence,f,l,lookups) report("otf process","chainproc gpos_pair not yet supported") return start end function chainprocs.self(start,stop,kind,lookupname,sequence,f,l,lookups) report("otf process","self refering lookup cannot happen") return stop end local zwnj = 0x200C local zwj = 0x200D -- what pointer to return, spec says stop function fonts.otf.features.process.contextchain(start,kind,lookupname,contextdata) local contexts, flags, done = contextdata.lookups, contextdata.flags, false local skipmark, skipligature, skipbase = unpack(flags) -- unpack slower than assignment for k=1,#contexts do local match, next, last = true, start, start local rule, lookuptype, sequence, f, l, lookups = unpack(contexts[k]) -- unpack is slow local s = #sequence if s == 1 then match = next.id == glyph and next.subtype<256 and next.font == currentfont and sequence[1][next.char] else -- todo: better space check (maybe check for glue) local n = f while n <= l do if last then local id = last.id if id == glyph and last.subtype<256 and last.font == currentfont then local char = last.char local class = characters[char].description.class if class == skipmark or class == skipligature or class == skipbase then -- skip 'm last = last.next elseif sequence[n][char] then if n < l then last = last.next end n = n + 1 else match = false break end elseif id == disc then -- what to do with kerns? last = last.next else match = false break end else match = false break end end if match and f > 1 then local prev = start.prev if prev then if f == 2 then match = prev.id == glyph and prev.subtype<256 and prev.font == currentfont and sequence[1][prev.char] else local n = f-1 while n >= 1 do if prev then local id = prev.id if id == glyph and prev.subtype<256 and prev.font == currentfont then -- normal char local char = prev.char local class = characters[char].description.class if class == skipmark or class == skipligature or class == skipbase then -- skip 'm elseif sequence[n][char] then n = n -1 else match = false break end elseif id == disc then -- skip 'm elseif sequence[n][32] then n = n -1 else match = false break end prev = prev.prev elseif sequence[n][32] then n = n -1 else match = false break end end end elseif f == 2 then match = sequence[1][32] else for n=f-1,1 do if not sequence[n][32] then match = false break end end end end if match and s > l then local next = last.next if next then if s-l == 1 then match = next.id == glyph and next.subtype<256 and next.font == currentfont and sequence[s][next.char] else local n = l+ 1 while n <= s do if next then local id = next.id if id == glyph and next.subtype<256 and next.font == currentfont then -- normal char local char = next.char local class = characters[char].description.class if class == skipmark or class == skipligature or class == skipbase then -- skip 'm elseif sequence[n][char] then n = n + 1 else match = false break end elseif id == disc then -- skip 'm elseif sequence[n][32] then -- brrr n = n + 1 else match = false break end next = next.next elseif sequence[n][32] then n = n + 1 else match = false break end end end elseif s-l == 1 then match = sequence[s][32] else for n=l+1,s do if not sequence[n][32] then match = false break end end end end end if match then local trace = fonts.otf.trace_contexts if trace then local char = start.char report("otf chain",format("%s: rule %s of %s matches at char 0x%04X (%s) for (%s,%s,%s) chars, lookuptype %s",kind,rule,lookupname,char,utf.char(char),f-1,l-f+1,s-l,lookuptype)) end if lookups then local cp = chainprocs[lookuptype] if cp then start = cp(start,last,kind,lookupname,sequence,f,l,lookups,flags) else report("otf chain",format("%s: lookuptype %s not supported yet for %s",kind,lookuptype,lookupname)) end elseif trace then report("otf chain",format("%s: skipping match for %s",kind,lookupname)) end done = true break end end return start, done end --~ if true then --~ if n < f then --~ texio.write_nl(string.format("%s before %s %04x %s %s %s",lookupname,n,char,class,skipmark or "?",tostring(sequence[n][char]))) --~ elseif n > l then --~ texio.write_nl(string.format("%s after %s %04x %s %s %s",lookupname,n,char,class,skipmark or "?",tostring(sequence[n][char]))) --~ else --~ texio.write_nl(string.format("%s current %s %04x %s %s %s",lookupname,n,char,class,skipmark or "?",tostring(sequence[n][char]))) --~ end --~ end --~ elseif char == zwnj and sequence[n][32] then -- brrr -- this needs to be fixed ! ! ! ! ! ! ! ! function fonts.otf.features.process.reversecontextchain(start,kind,lookupname,contextdata) -- PROBABLY WRONG, WE NEED TO WALK BACK OVER THE LIST local done = false local contexts = contextdata.lookups local flags = contextdata.flags local skipmark, skipligature, skipbase = unpack(flags) for k=1,#contexts do local match, next, first, last = true, start, start, start local rule, lookuptype, sequence, f, l, lookups = unpack(contexts[k]) -- unpack is slow if #sequence == 1 then match = next.id == glyph and next.subtype<256 and next.font == currentfont and sequence[1][next.char] else local n, s = #sequence, 1 while n > 0 do if next then local id = next.id if id == glyph and next.subtype<256 and next.font == currentfont then -- normal char local char = next.char local class = characters[char].description.class if class == skipmark or class == skipligature or class == skipbase then -- skip elseif sequence[n][char] then if n == f then first = next -- ok ? end if n == l then last = next -- ok ? end n = n - 1 else match = false break end elseif id == disc then -- skip elseif not sequence[n][32] then -- brrr match = false break end next = next.next elseif sequence[n][32] then n = n - 1 else match = false break end end end if match then local trace = fonts.otf.trace_contexts if trace then local char = first.char report("otf reverse chain",format("%s: rule %s of %s matches, replacing starts at char 0x%04X (%s) lookuptype %s",kind,rule,lookupname,char,utf.char(char),lookuptype)) end if lookups then local cp = chainprocs[lookuptype] if cp then if start == first then start = cp(first,last,kind,lookupname,sequence,f,l,lookups,flags) else first = cp(first,last,kind,lookupname,sequence,f,l,lookups,flags) end else report("otf reverse chain",format("%s: lookuptype %s not supported yet for %s",kind,lookuptype,lookupname)) end elseif trace then report("otf reverse chain",format("%s: skipping match for %s",kind,lookupname)) end done = true break end end return start, done end fonts.otf.features.process.gsub_context = fonts.otf.features.process.contextchain fonts.otf.features.process.gsub_contextchain = fonts.otf.features.process.contextchain fonts.otf.features.process.gsub_reversecontextchain = fonts.otf.features.process.reversecontextchain fonts.otf.features.process.gpos_contextchain = fonts.otf.features.process.contextchain fonts.otf.features.process.gpos_context = fonts.otf.features.process.contextchain end do local process = fonts.otf.features.process.feature function fonts.methods.node.otf.aalt(head,font,attr) return process(head,font,attr,'aalt') end function fonts.methods.node.otf.abvm(head,font,attr) return process(head,font,attr,'abvm') end function fonts.methods.node.otf.afrc(head,font,attr) return process(head,font,attr,'afrc') end function fonts.methods.node.otf.akhn(head,font,attr) return process(head,font,attr,'akhn') end function fonts.methods.node.otf.blwm(head,font,attr) return process(head,font,attr,'blwm') end function fonts.methods.node.otf.c2pc(head,font,attr) return process(head,font,attr,'c2pc') end function fonts.methods.node.otf.c2sc(head,font,attr) return process(head,font,attr,'c2sc') end function fonts.methods.node.otf.calt(head,font,attr) return process(head,font,attr,'calt') end function fonts.methods.node.otf.case(head,font,attr) return process(head,font,attr,'case') end function fonts.methods.node.otf.ccmp(head,font,attr) return process(head,font,attr,'ccmp') end function fonts.methods.node.otf.clig(head,font,attr) return process(head,font,attr,'clig') end function fonts.methods.node.otf.cpsp(head,font,attr) return process(head,font,attr,'cpsp') end function fonts.methods.node.otf.cswh(head,font,attr) return process(head,font,attr,'cswh') end function fonts.methods.node.otf.curs(head,font,attr) return process(head,font,attr,'curs') end function fonts.methods.node.otf.dlig(head,font,attr) return process(head,font,attr,'dlig') end function fonts.methods.node.otf.dnom(head,font,attr) return process(head,font,attr,'dnom') end function fonts.methods.node.otf.expt(head,font,attr) return process(head,font,attr,'expt') end function fonts.methods.node.otf.fin2(head,font,attr) return process(head,font,attr,'fin2') end function fonts.methods.node.otf.fin3(head,font,attr) return process(head,font,attr,'fin3') end function fonts.methods.node.otf.fina(head,font,attr) return process(head,font,attr,'fina',3) end function fonts.methods.node.otf.frac(head,font,attr) return process(head,font,attr,'frac') end function fonts.methods.node.otf.fwid(head,font,attr) return process(head,font,attr,'fwid') end function fonts.methods.node.otf.haln(head,font,attr) return process(head,font,attr,'haln') end function fonts.methods.node.otf.hist(head,font,attr) return process(head,font,attr,'hist') end function fonts.methods.node.otf.hkna(head,font,attr) return process(head,font,attr,'hkna') end function fonts.methods.node.otf.hlig(head,font,attr) return process(head,font,attr,'hlig') end function fonts.methods.node.otf.hngl(head,font,attr) return process(head,font,attr,'hngl') end function fonts.methods.node.otf.hwid(head,font,attr) return process(head,font,attr,'hwid') end function fonts.methods.node.otf.init(head,font,attr) return process(head,font,attr,'init',1) end function fonts.methods.node.otf.isol(head,font,attr) return process(head,font,attr,'isol',4) end function fonts.methods.node.otf.ital(head,font,attr) return process(head,font,attr,'ital') end function fonts.methods.node.otf.jp78(head,font,attr) return process(head,font,attr,'jp78') end function fonts.methods.node.otf.jp83(head,font,attr) return process(head,font,attr,'jp83') end function fonts.methods.node.otf.jp90(head,font,attr) return process(head,font,attr,'jp90') end function fonts.methods.node.otf.kern(head,font,attr) return process(head,font,attr,'kern') end function fonts.methods.node.otf.liga(head,font,attr) return process(head,font,attr,'liga') end function fonts.methods.node.otf.lnum(head,font,attr) return process(head,font,attr,'lnum') end function fonts.methods.node.otf.locl(head,font,attr) return process(head,font,attr,'locl') end function fonts.methods.node.otf.mark(head,font,attr) return process(head,font,attr,'mark') end function fonts.methods.node.otf.med2(head,font,attr) return process(head,font,attr,'med2') end function fonts.methods.node.otf.medi(head,font,attr) return process(head,font,attr,'medi',2) end function fonts.methods.node.otf.mgrk(head,font,attr) return process(head,font,attr,'mgrk') end function fonts.methods.node.otf.mkmk(head,font,attr) return process(head,font,attr,'mkmk') end function fonts.methods.node.otf.nalt(head,font,attr) return process(head,font,attr,'nalt') end function fonts.methods.node.otf.nlck(head,font,attr) return process(head,font,attr,'nlck') end function fonts.methods.node.otf.nukt(head,font,attr) return process(head,font,attr,'nukt') end function fonts.methods.node.otf.numr(head,font,attr) return process(head,font,attr,'numr') end function fonts.methods.node.otf.onum(head,font,attr) return process(head,font,attr,'onum') end function fonts.methods.node.otf.ordn(head,font,attr) return process(head,font,attr,'ordn') end function fonts.methods.node.otf.ornm(head,font,attr) return process(head,font,attr,'ornm') end function fonts.methods.node.otf.pnum(head,font,attr) return process(head,font,attr,'pnum') end function fonts.methods.node.otf.pref(head,font,attr) return process(head,font,attr,'pref') end function fonts.methods.node.otf.pres(head,font,attr) return process(head,font,attr,'pres') end function fonts.methods.node.otf.pstf(head,font,attr) return process(head,font,attr,'pstf') end function fonts.methods.node.otf.rlig(head,font,attr) return process(head,font,attr,'rlig') end function fonts.methods.node.otf.rphf(head,font,attr) return process(head,font,attr,'rphf') end function fonts.methods.node.otf.salt(head,font,attr) return process(head,font,attr,'calt') end function fonts.methods.node.otf.sinf(head,font,attr) return process(head,font,attr,'sinf') end function fonts.methods.node.otf.smcp(head,font,attr) return process(head,font,attr,'smcp') end function fonts.methods.node.otf.smpl(head,font,attr) return process(head,font,attr,'smpl') end function fonts.methods.node.otf.ss01(head,font,attr) return process(head,font,attr,'ss01') end function fonts.methods.node.otf.ss02(head,font,attr) return process(head,font,attr,'ss02') end function fonts.methods.node.otf.ss03(head,font,attr) return process(head,font,attr,'ss03') end function fonts.methods.node.otf.ss04(head,font,attr) return process(head,font,attr,'ss04') end function fonts.methods.node.otf.ss05(head,font,attr) return process(head,font,attr,'ss05') end function fonts.methods.node.otf.ss06(head,font,attr) return process(head,font,attr,'ss06') end function fonts.methods.node.otf.ss07(head,font,attr) return process(head,font,attr,'ss07') end function fonts.methods.node.otf.ss08(head,font,attr) return process(head,font,attr,'ss08') end function fonts.methods.node.otf.ss09(head,font,attr) return process(head,font,attr,'ss09') end function fonts.methods.node.otf.subs(head,font,attr) return process(head,font,attr,'subs') end function fonts.methods.node.otf.sups(head,font,attr) return process(head,font,attr,'sups') end function fonts.methods.node.otf.swsh(head,font,attr) return process(head,font,attr,'swsh') end function fonts.methods.node.otf.titl(head,font,attr) return process(head,font,attr,'titl') end function fonts.methods.node.otf.tnam(head,font,attr) return process(head,font,attr,'tnam') end function fonts.methods.node.otf.tnum(head,font,attr) return process(head,font,attr,'tnum') end function fonts.methods.node.otf.trad(head,font,attr) return process(head,font,attr,'trad') end function fonts.methods.node.otf.unic(head,font,attr) return process(head,font,attr,'unic') end function fonts.methods.node.otf.zero(head,font,attr) return process(head,font,attr,'zero') end end -- common stuff function fonts.otf.features.language(tfmdata,value) if value then value = value:lower() if fonts.otf.tables.languages[value] then tfmdata.language = value end end end function fonts.otf.features.script(tfmdata,value) if value then value = value:lower() if fonts.otf.tables.scripts[value] then tfmdata.script = value end end end function fonts.otf.features.mode(tfmdata,value) if value then tfmdata.mode = value:lower() end end function fonts.otf.features.strategy(tfmdata,value) if value then tfmdata.strategy = tonumber(value) or fonts.otf.strategy end end fonts.initializers.base.otf.language = fonts.otf.features.language fonts.initializers.base.otf.script = fonts.otf.features.script fonts.initializers.base.otf.mode = fonts.otf.features.mode fonts.initializers.base.otf.method = fonts.otf.features.mode fonts.initializers.base.otf.strategy = fonts.otf.features.strategy -- not needed fonts.initializers.node.otf.language = fonts.otf.features.language fonts.initializers.node.otf.script = fonts.otf.features.script fonts.initializers.node.otf.mode = fonts.otf.features.mode fonts.initializers.node.otf.method = fonts.otf.features.mode fonts.initializers.node.otf.strategy = fonts.otf.features.strategy do local tlig_list = { endash = "hyphen hyphen", emdash = "hyphen hyphen hyphen", --~ quotedblleft = "quoteleft quoteleft", --~ quotedblright = "quoteright quoteright", --~ quotedblleft = "grave grave", --~ quotedblright = "quotesingle quotesingle", --~ quotedblbase = "comma comma", } local trep_list = { --~ [0x0022] = 0x201D, [0x0027] = 0x2019, --~ [0x0060] = 0x2018, } local tlig_feature = { features = { { scripts = { { script = "DFLT", langs = { "dflt" }, } }, tag = "tlig", comment = "added bij mkiv" }, }, name = "ctx_tlig", subtables = { { name = "ctx_tlig_1" } }, type = "gsub_ligature", flags = { }, always = true } local trep_feature = { features = { { scripts = { { script = "DFLT", langs = { "dflt" }, } }, tag = "trep", comment = "added bij mkiv" }, }, name = "ctx_trep", subtables = { { name = "ctx_trep_1" } }, type = "gsub_single", flags = { }, always = true } function fonts.otf.enhance.enrich(data,filename) for index, glyph in pairs(data.glyphs) do local l = tlig_list[glyph.name] if l then local o = glyph.lookups or { } o["ctx_tlig_1"] = { { "ligature", l, glyph.name } } glyph.lookups = o end local r = trep_list[glyph.unicode] if r then local replacement = data.map.map[r] if replacement then local o = glyph.lookups or { } o["ctx_trep_1"] = { { "substitution", data.glyphs[replacement].name } } --- glyph.lookups = o end end end data.gsub = data.gsub or { } logs.report("load otf","enhance: registering tlig feature") table.insert(data.gsub,1,table.fastcopy(tlig_feature)) logs.report("load otf","enhance: registering trep feature") table.insert(data.gsub,1,table.fastcopy(trep_feature)) end local prepare = fonts.otf.features.prepare.feature local process = fonts.otf.features.process.feature fonts.otf.tables.features['tlig'] = 'TeX Ligatures' fonts.otf.tables.features['trep'] = 'TeX Replacements' function fonts.initializers.node.otf.tlig(tfm,value) return prepare(tfm,'tlig',value) end function fonts.initializers.node.otf.trep(tfm,value) return prepare(tfm,'trep',value) end function fonts.methods.node.otf.tlig(head,font,attr) return process(head,font,attr,'tlig') end function fonts.methods.node.otf.trep(head,font,attr) return process(head,font,attr,'trep') end function fonts.initializers.base.otf.tlig(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'tlig',value) end function fonts.initializers.base.otf.trep(tfm,value) fonts.otf.features.prepare_base_substitutions(tfm,'trep',value) end end -- we need this because fonts can be bugged -- \definefontfeature[calt][language=nld,script=latn,mode=node,calt=yes,clig=yes,rlig=yes] -- \definefontfeature[dflt][language=nld,script=latn,mode=node,calt=no, clig=yes,rlig=yes] -- \definefontfeature[fixd][language=nld,script=latn,mode=node,calt=no, clig=yes,rlig=yes,ignoredrules={44,45,47}] -- \starttext -- {\type{dflt:}\font\test=ZapfinoExtraLTPro*dflt at 24pt \test \char57777\char57812 c/o} \endgraf -- {\type{calt:}\font\test=ZapfinoExtraLTPro*calt at 24pt \test \char57777\char57812 c/o} \endgraf -- {\type{fixd:}\font\test=ZapfinoExtraLTPro*fixd at 24pt \test \char57777\char57812 c/o} \endgraf -- \stoptext --~ table.insert(fonts.triggers,"ignoredrules") --~ function fonts.initializers.node.otf.ignoredrules(tfmdata,value) --~ if value then --~ -- these tests must move ! --~ tfmdata.unique = tfmdata.unique or { } --~ tfmdata.unique.ignoredrules = tfmdata.unique.ignoredrules or { } --~ local ignored = tfmdata.unique.ignoredrules --~ -- value is already ok now --~ for s in string.gmatch(value:gsub("[{}]","")..",", "%s*(.-),") do --~ ignored[tonumber(s)] = true --~ end --~ end --~ end fonts.initializers.base.otf.equaldigits = fonts.initializers.common.equaldigits fonts.initializers.node.otf.equaldigits = fonts.initializers.common.equaldigits fonts.initializers.base.otf.lineheight = fonts.initializers.common.lineheight fonts.initializers.node.otf.lineheight = fonts.initializers.common.lineheight fonts.initializers.base.otf.compose = fonts.initializers.common.compose fonts.initializers.node.otf.compose = fonts.initializers.common.compose -- temp hack, may change function fonts.initializers.base.otf.kern(tfmdata,value) fonts.otf.features.prepare_base_kerns(tfmdata,'kern',value) end -- bonus function function fonts.otf.name_to_slot(name) -- todo: afm en tfm local tfmdata = fonts.tfm.id[font.current()] if tfmdata and tfmdata.shared then local otfdata = tfmdata.shared.otfdata if otfdata and otfdata.luatex then return otfdata.luatex.unicodes[name] end end return nil end function fonts.otf.char(n) -- todo: afm en tfm if type(n) == "string" then n = fonts.otf.name_to_slot(n) end if n then tex.sprint(tex.ctxcatcodes,string.format("\\char%s ",n)) end end --~ function fonts.otf.name_to_table(name) --~ lcoal temp, result = { } --~ local tfmdata = fonts.tfm.id[font.current()] --~ if tfmdata and tfmdata.shared then --~ local otfdata = tfmdata.shared.otfdata --~ if otfdata and otfdata.luatex then --~ for k,v in pairs(otfdata.glyphs) do --~ if v.name:find(name) then --~ temp[v.name] = v.unicode --~ end --~ end --~ end --~ end --~ for k,v in pairs(table.sortedkeys(temp)) do --~ result[#result+1] = { v, temp[v] } --~ end --~ return result --~ end -- Here we plug in some analyzing code -- will move to font-tfm do local glyph = node.id('glyph') local glue = node.id('glue') local penalty = node.id('penalty') local fontdata = fonts.tfm.id local set_attribute = node.set_attribute local has_attribute = node.has_attribute local state = attributes.numbers['state'] or 100 local fcs = fonts.color.set local fcr = fonts.color.reset -- in the future we will use language/script attributes instead of the -- font related value, but then we also need dynamic features which is -- somewhat slower; and .. we need a chain of them local type = type local initializers, methods = fonts.analyzers.initializers, fonts.analyzers.methods function fonts.initializers.node.otf.analyze(tfmdata,value,attr) if attr and attr > 0 then script, language = a_to_script[attr], a_to_language[attr] else script, language = tfmdata.script, tfmdata.language end local action = initializers[script] if action then if type(action) == "function" then return action(tfmdata,value) else local action = action[language] if action then return action(tfmdata,value) end end end return nil end function fonts.methods.node.otf.analyze(head,font,attr) local tfmdata = fontdata[font] local script, language if attr and attr > 0 then script, language = a_to_script[attr], a_to_language[attr] else script, language = tfmdata.script, tfmdata.language end local action = methods[script] if action then if type(action) == "function" then return action(head,font,attr) else action = action[language] if action then return action(head,font,attr) end end end return head, false end fonts.otf.features.register("analyze",true) -- we always analyze table.insert(fonts.triggers,"analyze") -- we need a proper function for doing this -- latin fonts.analyzers.methods.latn = fonts.analyzers.aux.setstate -- this info eventually will go into char-def local zwnj = 0x200C local zwj = 0x200D local isol = { [0x0621] = true, [zwnj] = true, } local isol_fina = { [0x0622] = true, [0x0623] = true, [0x0624] = true, [0x0625] = true, [0x0627] = true, [0x062F] = true, [0x0630] = true, [0x0631] = true, [0x0632] = true, [0x0648] = true, [0xFEF5] = true, [0xFEF7] = true, [0xFEF9] = true, [0xFEFB] = true, } local isol_fina_medi_init = { [0x0626] = true, [0x0628] = true, [0x0629] = true, [0x062A] = true, [0x062B] = true, [0x062C] = true, [0x062D] = true, [0x062E] = true, [0x0633] = true, [0x0634] = true, [0x0635] = true, [0x0636] = true, [0x0637] = true, [0x0638] = true, [0x0639] = true, [0x063A] = true, [0x0640] = true, -- tadwil [0x0641] = true, [0x0642] = true, [0x0643] = true, [0x0644] = true, [0x0645] = true, [0x0646] = true, [0x0647] = true, [0x0649] = true, [0x064A] = true, [0x067E] = true, [0x0686] = true, [zwj] = true, } local arab_warned = { } local function warning(current,what) local char = current.char if not arab_warned[char] then log.report("analyze",string.format("arab: character %s (0x%04X) has no %s class", char, char, what)) arab_warned[char] = true end end function fonts.analyzers.methods.nocolor(head,font,attr) for n in node.traverse(head,glyph) do if not font or n.font == font then fcr(n) end end return head, true end function fonts.analyzers.methods.arab(head,font,attr) -- maybe make a special version with no trace local characters = fontdata[font].characters local first, last, current, done = nil, nil, head, false local trace = fonts.color.trace --~ local laststate = 0 local function finish() if last then if first == last then if isol_fina_medi_init[first.char] or isol_fina[first.char] then set_attribute(first,state,4) -- isol if trace then fcs(first,"font:isol") end else warning(first,"isol") set_attribute(first,state,0) -- error if trace then fcr(first) end end else if isol_fina_medi_init[last.char] or isol_fina[last.char] then -- why isol here ? -- if laststate == 1 or laststate == 2 or laststate == 4 then set_attribute(last,state,3) -- fina if trace then fcs(last,"font:fina") end else warning(last,"fina") set_attribute(last,state,0) -- error if trace then fcr(last) end end end first, last = nil, nil elseif first then -- first and last are either both set so we never com here if isol_fina_medi_init[first.char] or isol_fina[first.char] then set_attribute(first,state,4) -- isol if trace then fcs(first,"font:isol") end else warning(first,"isol") set_attribute(first,state,0) -- error if trace then fcr(first) end end first = nil end --~ laststate = 0 end while current do if current.id == glyph and current.subtype<256 and current.font == font then done = true local char = current.char local chardata = characters[char] -- some day we will make a characters.marks hash if not chardata then -- this is also more efficient since it's shared -- troubles -- elseif char == zwj then -- -- can probably be ignored, we could turn it into a kern or penalty -- elseif char == zwnj then -- -- acts like a space, we could turn it into a kern or penalty -- finish() elseif chardata.description.class == "mark" then set_attribute(current,state,5) -- mark if trace then fcs(current,"font:mark") end elseif isol[char] then finish() set_attribute(current,state,4) -- isol if trace then fcs(current,"font:isol") end first, last = nil, nil --~ laststate = 0 elseif not first then if isol_fina_medi_init[char] then set_attribute(current,state,1) -- init if trace then fcs(current,"font:init") end first, last = first or current, current --~ laststate = 1 elseif isol_fina[char] then set_attribute(current,state,4) -- isol if trace then fcs(current,"font:isol") end first, last = nil, nil --~ laststate = 0 else -- no arab finish() end elseif isol_fina_medi_init[char] then first, last = first or current, current set_attribute(current,state,2) -- medi if trace then fcs(current,"font:medi") end --~ laststate = 2 elseif isol_fina[char] then -- if not laststate == 1 then if not has_attribute(last,state,1) then -- tricky, we need to check what last may be ! set_attribute(last,state,2) -- medi if trace then fcs(last,"font:medi") end end set_attribute(current,state,3) -- fina if trace then fcs(current,"font:fina") end first, last = nil, nil --~ laststate = 0 elseif char >= 0x0600 and char <= 0x06FF then if trace then fcs(current,"font:rest") end finish() else --no finish() end else finish() end current = current.next end finish() return head, done end -- han (chinese) (unfinished) -- this info eventually will go into char-def -- in the future we will use language/script attributes instead of the -- font related value, but then we also need dynamic features which is -- somewhat slower; and .. we need a chain of them local type = type local opening_parenthesis_hw = table.tohash { -- half width 0x0028, 0x005B, 0x007B, 0x2018, -- ‘ 0x201C, -- “ } local opening_parenthesis_fw = table.tohash { -- full width 0x3008, -- 〈 Left book quote 0x300A, -- 《 Left double book quote 0x300C, -- 「 left quote 0x300E, -- 『 left double quote 0x3010, -- 【 left double book quote 0x3014, -- 〔 left book quote 0x3016, --〖 left double book quote 0x3018, -- left tortoise bracket 0x301A, -- left square bracket 0x301D, -- reverse double prime qm 0xFF08, -- ( left parenthesis 0xFF3B, -- [ left square brackets 0xFF5B, -- { left curve bracket 0xFF62, -- left corner bracket } local closing_parenthesis_hw = table.tohash { -- half width 0x0029, 0x005D, 0x007D, 0x2019, -- ’ right quote, right 0x201D, -- ” right double quote } local closing_parenthesis_fw = table.tohash { -- full width 0x3009, -- 〉 book quote 0x300B, -- 》 double book quote 0x300D, -- 」 right quote, right 0x300F, -- 』 right double quote 0x3011, -- 】 right double book quote 0x3015, -- 〕 right book quote 0x3017, -- 〗 right double book quote 0x3019, -- right tortoise bracket 0x301B, -- right square bracket 0x301E, -- double prime qm 0x301F, -- low double prime qm 0xFF09, -- ) right parenthesis 0xFF3D, -- ] right square brackets 0xFF5D, -- } right curve brackets 0xFF63, -- right corner bracket } local opening_vertical = table.tohash { 0xFE35, 0xFE37, 0xFE39, 0xFE3B, 0xFE3D, 0xFE3F, 0xFE41, 0xFE43, 0xFE47, } local closing_vertical = table.tohash { 0xFE36, 0xFE38, 0xFE3A, 0xFE3C, 0xFE3E, 0xFE40, 0xFE42, 0xFE44, 0xFE48, } local opening_punctuation_hw = table.tohash { -- half width } local opening_punctuation_fw = table.tohash { -- 0x2236, -- ∶ -- 0xFF0C, -- , } local closing_punctuation_hw = table.tohash { -- half width 0x0021, -- ! 0x002C, -- , 0x002E, -- . 0x003A, -- : 0x003B, -- ; 0x003F, -- ? 0xFF61, -- hw full stop } local closing_punctuation_fw = table.tohash { -- full width 0x3001, -- 、 0x3002, -- 。 0xFF01, -- ! 0xFF0C, -- , 0xFF0E, -- . 0xFF1A, -- : 0xFF1B, -- ; 0xFF1F, -- ? } local non_starter = table.tohash { -- japanese 0x3005, 0x3041, 0x3043, 0x3045, 0x3047, 0x3049, 0x3063, 0x3083, 0x3085, 0x3087, 0x308E, 0x3095, 0x3096, 0x309B, 0x309C, 0x309D, 0x309E, 0x30A0, 0x30A1, 0x30A3, 0x30A5, 0x30A7, 0x30A9, 0x30C3, 0x30E3, 0x30E5, 0x30E7, 0x30EE, 0x30F5, 0x30F6, 0x30FC, 0x30FD, 0x30FE, 0x31F0, 0x31F1, 0x30F2, 0x30F3, 0x30F4, 0x31F5, 0x31F6, 0x30F7, 0x30F8, 0x30F9, 0x31FA, 0x31FB, 0x30FC, 0x30FD, 0x30FE, 0x31FF, } -- the characters below are always appear in a double form, so there -- will be two Chinese ellipsis characters together that denote -- ellipsis marks and it is not allowed to break between them local hyphenation = table.tohash { 0x2026, -- … ellipsis 0x2014, -- — hyphen } local function is_han_character(char) return (char>=0x04E00 and char<=0x09FFF) or (char>=0x03400 and char<=0x04DFF) or (char>=0x20000 and char<=0x2A6DF) or (char>=0x0F900 and char<=0x0FAFF) or (char>=0x2F800 and char<=0x2FA1F) end --~ opening_parenthesis_hw / closing_parenthesis_hw --~ opening_parenthesis_fw / closing_parenthesis_fw --~ opening_punctuation_hw / closing_punctuation_hw --~ opening_punctuation_fw / closing_punctuation_fw --~ non_starter --~ hyphenation --~ opening_vertical / closing_vertical fonts.analyzers.methods.stretch_hang = true fonts.analyzers.methods.hang_data = { inter_char_stretch_factor = 2.00, -- we started with 0.5, then 1.0 inter_char_half_factor = 0.50, -- normally there is no reason to change this inter_char_half_shrink_factor = 0.25, -- normally there is no reason to change this } local hang_data = fonts.analyzers.methods.hang_data local insert_after, insert_before, delete = node.insert_after, node.insert_before, nodes.delete local function nobreak_before(head,current) local p = current.prev if p then p = p.prev if p and p.id == penalty then p.penalty = 10000 return head, current end end return insert_before(head,current,nodes.penalty(10000)) end function fonts.analyzers.methods.hani(head,font,attr) -- maybe make a special version with no trace local characters = fontdata[font].characters local current, done, stretch, prevclass = head, false, 0, 0 if fonts.analyzers.methods.stretch_hang then stretch = fontdata[font].parameters.quad end -- penalty before break local interspecialskip = - stretch * hang_data.inter_char_half_factor local interspecialshrink = stretch * hang_data.inter_char_half_shrink_factor local internormalstretch = stretch * hang_data.inter_char_stretch_factor local trace = fonts.color.trace -- todo: check for first and last while current do if current.id == glyph and current.subtype<256 then if current.font == font then local char = current.char if false then -- don't ask -) elseif opening_punctuation_fw[char] or opening_parenthesis_fw[char] then if trace then fcs(current,"font:init") end if head ~= current then head, _ = insert_before(head,current,nodes.glue(interspecialskip,0,interspecialshrink)) end head, current = insert_after(head,current,nodes.penalty(10000)) head, current = insert_after(head,current,nodes.glue(0,internormalstretch,0)) prevclass, done = 1, true elseif closing_punctuation_fw[char] or closing_parenthesis_fw[char] then if trace then fcs(current,"font:fina") end if prevclass > 0 then head, current = nobreak_before(head,current) head, current = insert_after(head,current,nodes.penalty(10000)) head, current = insert_after(head,current,nodes.glue(interspecialskip,0,interspecialshrink)) head, current = insert_after(head,current,nodes.penalty(0)) head, current = insert_after(head,current,nodes.glue(0,internormalstretch,0)) end prevclass, done = 2, true elseif opening_punctuation_hw[char] or opening_parenthesis_hw[char] then if trace then fcs(current,"font:init") end head, current = insert_after(head,current,nodes.penalty(10000)) head, current = insert_after(head,current,nodes.glue(0,internormalstretch,0)) prevclass, done = 3, true elseif closing_punctuation_hw[char] or closing_parenthesis_hw[char] then if trace then fcs(current,"font:fina") end if prevclass > 0 then head, current = nobreak_before(head,current) head, current = insert_after(head,current,nodes.penalty(0)) head, current = insert_after(head,current,nodes.glue(0,internormalstretch,0)) end prevclass, done = 4, true elseif hyphenation[char] then if trace then fcs(current,"font:medi") end if prevclass > 0 then head, current = nobreak_before(head,current) head, current = insert_after(head,current,nodes.penalty(0)) head, current = insert_after(head,current,nodes.glue(0,internormalstretch,0)) end prevclass, done = 5, true elseif non_starter[char] then if trace then fcs(current,"font:isol") end head, current = insert_after(head,current,nodes.penalty(10000)) head, current = insert_after(head,current,nodes.glue(0,internormalstretch,0)) prevclass, done = 6, true elseif is_han_character(char) then prevclass, done = 7, true head, current = insert_after(head,current,nodes.penalty(0)) head, current = insert_after(head,current,nodes.glue(0,internormalstretch,0)) end else prevclass = 0 end elseif prevclass > 0 and current.id == glue and current.spec and current.spec.width > 0 then head, current = delete(head,current) end if current then current = current.next end end return head, done end fonts.analyzers.methods.hang = fonts.analyzers.methods.hani end -- experimental and will probably change do local process = fonts.otf.features.process.feature local prepare = fonts.otf.features.prepare.feature function fonts.install_feature(type,...) if fonts[type] and fonts[type].install_feature then fonts[type].install_feature(...) end end function fonts.otf.install_feature(tag) fonts.methods.node.otf [tag] = function(head,font,attr) return process(head,font,attr,tag) end fonts.initializers.node.otf[tag] = function(tfm,value) return prepare(tfm,tag,value) end end end