diff options
Diffstat (limited to 'Master/texmf-dist/tex/context/base/font-otf.lua')
-rw-r--r-- | Master/texmf-dist/tex/context/base/font-otf.lua | 4811 |
1 files changed, 4811 insertions, 0 deletions
diff --git a/Master/texmf-dist/tex/context/base/font-otf.lua b/Master/texmf-dist/tex/context/base/font-otf.lua new file mode 100644 index 00000000000..5c0db392dba --- /dev/null +++ b/Master/texmf-dist/tex/context/base/font-otf.lua @@ -0,0 +1,4811 @@ + 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-- +<p>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.</p> + +<p>As with the <l n='afm'/> code, we may decide to store more information +in the <l n='otf'/> table.</p> + +<p>Incrementing the version number will force a re-cache. We jump the +number by one when there's a fix in the <l n='fontforge'/> library or +<l n='lua'/> code that results in different tables.</p> +--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-- +<p>We start with a lot of tables and related functions.</p> +--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-- +<p>Here we go.</p> +--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 |