--[[ ****************************************************** * Provide Lua functions for typesetting font tables. * * Author: Scott Pakin * ****************************************************** --]] -- Send formatted output to TeX. function tprintf (fmt, ...) local st_fmt = string.format("\\scantokens{%s}", fmt) tex.print(string.format(st_fmt, ...)) end -- Return the human-readable name of \testfont. function get_font_name () local fid = font.id("testfont") local fnt = font.getfont(fid) local fontname = fnt.fontname or fnt.psname or fnt.fullname or fnt.name return fontname end -- Return a string corresponding to a symbol table's header row. function header_row (num_hexits) local hstr = {} for i = 0, 7 do hstr[#hstr + 1] = string.format([[& \multicolumn{1}{c|}{\ttfamily\rule{%.1fem}{1pt}%X}]], num_hexits*0.5, i) end hstr[#hstr + 1] = [[\\ \thickhline]] return table.concat(hstr, " ") end -- Return a string corresponding to a symbol table's footer row. function footer_row (num_hexits) local fstr = {} for i = 8, 15 do fstr[#fstr + 1] = string.format([[& \multicolumn{1}{c|}{\ttfamily\rule{%.1fem}{1pt}%X}]], num_hexits*0.5, i) end fstr[#fstr + 1] = [[\\]] return table.concat(fstr, " ") end -- Compute the area of a glyph's bounding box. function glyph_area (glyph) local bbox = glyph.boundingbox if bbox == nil then -- Metafont or PostScript font return glyph.width*(glyph.height + glyph.depth) else -- OpenType or TrueType font return (bbox[3] - bbox[1])*(bbox[4] - bbox[2]) end end -- Return a set of valid code points in \testfont and, for -- convenience, the minimum and maximum values in that table. function valid_code_points () -- First, consider the characters table. This is valid for -- Metafont fonts but may include duplicates (two code points, same -- Unicode value) for non-Metafont fonts. local valid_glyphs = {} local glyphmin, glyphmax = 2^30, -1 local fid = font.id("testfont") local fnt = font.getfont(fid) for i, v in pairs(fnt.characters) do if not (i == 'left_boundary' or i == 'right_boundary' or glyph_area(v) <= 0) then valid_glyphs[i] = true glyphmin = math.min(glyphmin, i) glyphmax = math.max(glyphmax, i) end end if glyphmax <= 255 then return valid_glyphs, glyphmin, glyphmax end -- Start over with a Unicode-centric approach. valid_glyphs = {} glyphmin, glyphmax = 2^30, -1 local fname = string.gsub(fnt.filename, "harfloaded:", "") fnt = fontloader.open(kpse.lookup(fname)) local ftbl = fontloader.to_table(fnt) for i, v in ipairs(ftbl.glyphs) do if v.unicode ~= -1 and glyph_area(v) > 0 then valid_glyphs[v.unicode] = true glyphmin = math.min(glyphmin, v.unicode) glyphmax = math.max(glyphmax, v.unicode) end end return valid_glyphs, glyphmin, glyphmax end -- Return a string corresponding to all rows of symbols. function all_table_rows (valid_glyphs, glyphmin, glyphmax) -- Determine the number of hexits in the largest code point and use -- this to define a format string for row headers. local num_hexits = string.len(string.format("%X", glyphmax)) local fmt_str = "%0" .. tostring(num_hexits) .. "X" -- Construct a list of row strings. local rows = {} for base = math.floor(glyphmin/16)*16, math.ceil(glyphmax/16)*16, 16 do -- Ensure we have at least one glyph in the current range. local have_glyph = false -- At least one glyph in [base, base+15] for ofs = 0, 15 do if valid_glyphs[base + ofs] then have_glyph = true break end end if not have_glyph then goto continue end -- Output exactly two rows. local base_str = string.format(fmt_str, base):sub(1, -2) rows[#rows + 1] = string.format([[\multirow{2}*{\ttfamily\char`\"%s\rule{0.5em}{1pt}}]], base_str) for ofs = 0, 7 do if valid_glyphs[base + ofs] then rows[#rows + 1] = string.format([[& \char"%X]], base + ofs) else rows[#rows + 1] = "&" end end rows[#rows + 1] = [[\\* \cline{2-9}]] for ofs = 8, 15 do if valid_glyphs[base + ofs] then rows[#rows + 1] = string.format([[& \char"%X]], base + ofs) else rows[#rows + 1] = "&" end end rows[#rows + 1] = [[\\ \thickhline]] ::continue:: end return table.concat(rows, " ") end -- Define a mapping from feature code to human-friendly name. Source: -- https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist -- (accessed 20-Dec-2025). This is extended by get_feature_name to -- cover cv01 through cv99 and ss01 through ss20. feature_names = { aalt = "Access All Alternates", abvf = "Above-base Forms", abvm = "Above-base Mark Positioning", abvs = "Above-base Substitutions", afrc = "Alternative Fractions", akhn = "Akhand", apkn = "Kerning for Alternate Proportional Widths", blwf = "Below-base Forms", blwm = "Below-base Mark Positioning", blws = "Below-base Substitutions", calt = "Contextual Alternates", case = "Case-sensitive Forms", ccmp = "Glyph Composition/Decomposition", cfar = "Conjunct Form After Ro", chws = "Contextual Half-width Spacing", cjct = "Conjunct Forms", clig = "Contextual Ligatures", cpct = "Centered CJK Punctuation", cpsp = "Capital Spacing", cswh = "Contextual Swash", curs = "Cursive Positioning", c2pc = "Petite Capitals From Capitals", c2sc = "Small Capitals From Capitals", dist = "Distances", dlig = "Discretionary Ligatures", dnom = "Denominators", dtls = "Dotless Forms", expt = "Expert Forms", falt = "Final Glyph on Line Alternates", fin2 = "Terminal Forms #2", fin3 = "Terminal Forms #3", fina = "Terminal Forms", flac = "Flattened Accent Forms", frac = "Fractions", fwid = "Full Widths", half = "Half Forms", haln = "Halant Forms", halt = "Alternate Half Widths", hist = "Historical Forms", hkna = "Horizontal Kana Alternates", hlig = "Historical Ligatures", hngl = "Hangul", hojo = "Hojo Kanji Forms (JIS X 0212-1990 Kanji Forms)", hwid = "Half Widths", init = "Initial Forms", isol = "Isolated Forms", ital = "Italics", jalt = "Justification Alternates", jp78 = "JIS78 Forms", jp83 = "JIS83 Forms", jp90 = "JIS90 Forms", jp04 = "JIS2004 Forms", kern = "Kerning", lfbd = "Left Bounds", liga = "Standard Ligatures", ljmo = "Leading Jamo Forms", lnum = "Lining Figures", locl = "Localized Forms", ltra = "Left-to-right Alternates", ltrm = "Left-to-right Mirrored 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 = "Oldstyle Figures", opbd = "Optical Bounds", ordn = "Ordinals", ornm = "Ornaments", palt = "Proportional Alternate Widths", pcap = "Petite Capitals", pkna = "Proportional Kana", 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", rclt = "Required Contextual Alternates", rkrf = "Rakar Forms", rlig = "Required Ligatures", rphf = "Reph Form", rtbd = "Right Bounds", rtla = "Right-to-left Alternates", rtlm = "Right-to-left Mirrored Forms", ruby = "Ruby Notation Forms", rvrn = "Required Variation Alternates", salt = "Stylistic Alternates", sinf = "Scientific Inferiors", size = "Optical size", smcp = "Small Capitals", smpl = "Simplified Forms", ssty = "Math Script-style Alternates", stch = "Stretching Glyph Decomposition", 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", vapk = "Kerning for Alternate Proportional Vertical Metrics", vatu = "Vattu Variants", vchw = "Vertical Contextual Half-width Spacing", vert = "Vertical Alternates", vhal = "Alternate Vertical Half Metrics", vjmo = "Vowel Jamo Forms", vkna = "Vertical Kana Alternates", vkrn = "Vertical Kerning", vpal = "Proportional Alternate Vertical Metrics", vrt2 = "Vertical Alternates and Rotation", vrtr = "Vertical Alternates for Rotation", zero = "Slashed Zero" } -- Given a feature code, return a human-friendly name. function get_feature_name (feature) local first2 = string.sub(feature, 1, 2) if first2 == "cv" then return string.format("Character Variant %d", tonumber(string.sub(feature, 3))) end if first2 == "ss" then return string.format("Stylistic Set %d", tonumber(string.sub(feature, 3))) end return feature_names[feature] end -- Render a section header for a font. We assume that \testfont -- already has been set to the desired font. function render_table_header (friendly_name) local fontname = get_font_name() tprintf([[\Needspace{0.25\textheight}]]) tprintf("\\markboth{%s}{%s}", fontname, fontname) if friendly_name ~= nil and friendly_name ~= fontname then tprintf("\\subsection[%s]{%s (%s)}", fontname, fontname, friendly_name) else tprintf("\\subsection{%s}", fontname) end end -- Render a subsection header for a font feature. function render_table_subheader (feature) local fontname = get_font_name() local featname = get_feature_name(feature) tprintf("\\markboth{%s, %s}{%s, %s}", fontname, featname, fontname, featname) tprintf("\\subsubsection{%s}", featname) end -- Render a font table. We assume that \testfont already has been set -- to the desired font. function render_table_body () local symtbl = {} -- Aquire a set of all defined code points in the font and use the -- largest code-point value to define a format string for column -- headers and footers. local valid_glyphs, glyphmin, glyphmax = valid_code_points() local num_hexits = string.len(string.format("%X", glyphmax)) -- Construct an entire symbol table. symtbl[#symtbl + 1] = [=[ \begin{longtable}{r|*8{>{\testfont}c|}} \multicolumn{9}{l}{\small\textit{(continued from previous page)}} \\[2ex] ]=] symtbl[#symtbl + 1] = header_row(num_hexits) symtbl[#symtbl + 1] = "\\endhead" symtbl[#symtbl + 1] = header_row(num_hexits) symtbl[#symtbl + 1] = "\\endfirsthead" symtbl[#symtbl + 1] = footer_row(num_hexits) symtbl[#symtbl + 1] = [=[ \multicolumn{9}{r}{} \\ \multicolumn{9}{r}{\small\textit{(continued on next page)}} \endfoot ]=] symtbl[#symtbl + 1] = footer_row(num_hexits) symtbl[#symtbl + 1] = "\\endlastfoot" symtbl[#symtbl + 1] = all_table_rows(valid_glyphs, glyphmin, glyphmax) symtbl[#symtbl + 1] = "\\end{longtable}" -- Render the resulting LaTeX code, reparsing each line with \scantokens. for i, ln in ipairs(symtbl) do tprintf([[\scantextokens{%s}]], ln) end end -- Mark the end of a font subsection. We assume that \testfont already -- has been set to the desired font. function render_table_subtrailer (feature) local fontname = get_font_name() local featname = get_feature_name(feature) tprintf("\\markboth{%s, %s}{%s, %s}", fontname, featname, fontname, featname) end -- Mark the end of a font section. We assume that \testfont already -- has been set to the desired font. function render_table_trailer () local fontname = get_font_name() tprintf("\\markboth{%s}{%s}", fontname, fontname) end -- For each specified feature, define \testfont then re-enter Lua to -- render a table. function render_feature_tables (filename, features, friendly_name) -- Set the font without features then render a header that -- represents that base font. local full_filename = kpse.lookup(filename) tprintf("\\font\\testfont=\"[%s]:mode=harf;\"\\relax", full_filename) tprintf("\\directlua{render_table_header(%q)}", friendly_name) -- Render one subheading and table per feature. for i, feat in ipairs(features) do -- "mode=harf" exposes the complete font to LuaLaTeX, not merely -- code points in [0x0, 0xFFFFF] or so. tprintf("\\font\\testfont=\"[%s]:mode=harf;%s;\"\\relax", full_filename, feat) tprintf([[ \directlua{ render_table_subheader(%q) render_table_body() render_table_subtrailer(%q) } ]], feat, feat) end -- Render one trailer for all feature tables. tprintf("\\directlua{render_table_trailer()}") end -- Define \testfont then re-enter Lua to render a table. If a list of -- features is provided, typeset one table per feature. function render_table (args) -- Extract mandatory and optional arguments. local filename = args[1] local features = args["features"] local friendly_name = args["name"] -- Handle the multi-feature case specially. if features ~= nil then render_feature_tables(filename, features, friendly_name) return end -- Handle the common case of no features. local base, ext = string.match(filename, "^(.*)(%.[^.]+)$") if ext == ".tfm" or ext == ".pfb" or ext == ".pfa" or ext == ".vf" then local stem = string.match(base, "^.-([^/]+)$") tprintf("\\font\\testfont={%s}\\relax", stem) else -- "mode=harf" exposes the complete font to LuaLaTeX, not merely -- code points in [0x0, 0xFFFFF] or so. tprintf("\\font\\testfont=\"[%s]:mode=harf;\"\\relax", kpse.lookup(filename)) end tprintf([=[ \directlua{ render_table_header(%q) render_table_body() render_table_trailer() } ]=], friendly_name) end