local module = { name = "harf-load", description = "Harf font loading", version = "0.4.2", date = "2019-09-07", license = "GPL v2.0" } luatexbase.provides_module(module) local hb = require("harf-base") local hbfonts = hb.fonts local hbfonts = hbfonts or {} local cfftag = hb.Tag.new("CFF ") local cff2tag = hb.Tag.new("CFF2") local os2tag = hb.Tag.new("OS/2") local posttag = hb.Tag.new("post") local glyftag = hb.Tag.new("glyf") local function loadfont(spec) local path = spec.resolved or spec.name if not path then return nil end local index = spec.sub and spec.sub or 1 local key = string.format("%s:%d", path, index) local data = hbfonts[key] if data then return data end local hbface = hb.Face.new(path, index - 1) local tags = hbface and hbface:get_table_tags() -- If the face has no table tags then it isn’t a valid SFNT font that -- HarfBuzz can handle. if tags then local hbfont = hb.Font.new(hbface) local upem = hbface:get_upem() -- The engine seems to use the font type to tell whether there is a CFF -- table or not, so we check for that here. local fonttype = nil local hasos2 = false local haspost = false for i = 1, #tags do local tag = tags[i] if tag == cfftag or tag == cff2tag then fonttype = "opentype" elseif tag == glyftag then fonttype = "truetype" elseif tag == os2tag then hasos2 = true elseif tag == posttag then haspost = true end end local fontextents = hbfont:get_h_extents() local ascender = fontextents and fontextents.ascender or upem * .8 local descender = fontextents and fontextents.descender or upem * .2 local gid = hbfont:get_nominal_glyph(0x0020) local space = gid and hbfont:get_glyph_h_advance(gid) or upem / 2 local slant = 0 if haspost then local post = hbface:get_table(posttag) local length = post:get_length() local data = post:get_data() if length >= 32 and string.unpack(">i4", data) <= 0x00030000 then local italicangle = string.unpack(">i4", data, 5) / 2^16 if italicangle ~= 0 then slant = -math.tan(italicangle * math.pi / 180) * 65536.0 end end end -- Load glyph metrics for all glyphs in the font. We used to do this on -- demand to make loading fonts faster, but hit many limitations inside -- the engine (mainly with shared backend fonts, where the engine would -- assume all fonts it decides to share load the same set of glyphs). -- -- Getting glyph advances is fast enough, but glyph extents are slower -- especially in CFF fonts. We might want to have an option to ignore exact -- glyph extents and use font ascender and descender if this proved to be -- too slow. local glyphcount = hbface:get_glyph_count() local glyphs = {} for gid = 0, glyphcount - 1 do local width = hbfont:get_glyph_h_advance(gid) local height, depth, italic = nil, nil, nil local extents = hbfont:get_glyph_extents(gid) if extents then height = extents.y_bearing depth = extents.y_bearing + extents.height if extents.x_bearing < 0 then italic = -extents.x_bearing end end glyphs[gid] = { width = width, height = height or ascender, depth = -(depth or descender), italic = italic or 0, } end local unicodes = hbface:collect_unicodes() local characters = {} for _, uni in next, unicodes do characters[uni] = hbfont:get_nominal_glyph(uni) end local xheight, capheight = 0, 0 if hasos2 then local os2 = hbface:get_table(os2tag) local length = os2:get_length() local data = os2:get_data() if length >= 96 and string.unpack(">H", data) > 1 then -- We don’t need much of the table, so we read from hard-coded offsets. xheight = string.unpack(">H", data, 87) capheight = string.unpack(">H", data, 89) end end if xheight == 0 then local gid = characters[120] -- x if gid then xheight = glyphs[gid].height else xheight = ascender / 2 end end if capheight == 0 then local gid = characters[88] -- X if gid then capheight = glyphs[gid].height else capheight = ascender end end data = { face = hbface, font = hbfont, upem = upem, fonttype = fonttype, space = space, xheight = xheight, capheight = capheight, slant = slant, glyphs = glyphs, unicodes = characters, psname = hbface:get_name(hb.ot.NAME_ID_POSTSCRIPT_NAME), fullname = hbface:get_name(hb.ot.NAME_ID_FULL_NAME), haspng = hbface:ot_color_has_png(), loaded = {}, -- Cached loaded glyph data. } hbfonts[key] = data return data end end -- Drop illegal characters from PS Name, per the spec -- https://docs.microsoft.com/en-us/typography/opentype/spec/name#nid6 local function sanitize(psname) local new = psname:gsub(".", function(c) local b = c:byte() if (b < 33 or b > 126) or c == "[" or c == "]" or c == "(" or c == ")" or c == "{" or c == "}" or c == "<" or c == ">" or c == "/" or c == "%" then return "-" end return c end) return new end local tlig = hb.texlig local function scalefont(data, spec) local size = spec.size local hbface = data.face local hbfont = data.font local upem = data.upem local space = data.space if size < 0 then size = -655.36 * size end -- Extract features (that we pass to HarfBuzz) from the rest of the options. -- Mostly a heuristic. local features = {} local options = {} local rawfeatures = spec.features and spec.features.raw or {} for key, val in next, rawfeatures do if key == "language" then val = hb.Language.new(val) end if key == "colr" then key = "palette" end if key == "tlig" then key = "texlig" end if key:len() == 4 then -- 4-letter options are likely font features, but not always, so we do -- some checks below. We put non feature options in the `options` dict. if val == true or val == false then val = (val and '+' or '-')..key features[#features + 1] = hb.Feature.new(val) elseif tonumber(val) then val = '+'..key..'='..tonumber(val) - 1 features[#features + 1] = hb.Feature.new(val) else options[key] = val end else options[key] = val end end -- We shape in font units (at UPEM) and then scale output with the desired -- sfont size. local scale = size / upem hbfont:set_scale(upem, upem) -- Populate font’s characters table. local glyphs = data.glyphs local characters = {} for gid, glyph in next, glyphs do characters[hb.CH_GID_PREFIX + gid] = { index = gid, width = glyph.width * scale, height = glyph.height * scale, depth = glyph.depth * scale, italic = glyph.italic * scale, } end local unicodes = data.unicodes for uni, gid in next, unicodes do characters[uni] = characters[hb.CH_GID_PREFIX + gid] end -- Select font palette, we support `palette=index` option, and load the first -- one otherwise. local paletteidx = tonumber(options.palette) or 1 -- Load CPAL palette from the font. local palette = nil if hbface:ot_color_has_palettes() and hbface:ot_color_has_layers() then local count = hbface:ot_color_palette_get_count() if paletteidx <= count then palette = hbface:ot_color_palette_get_colors(paletteidx) end end local letterspace = 0 if options.letterspace then letterspace = tonumber(options.letterspace) / 100 * upem elseif options.kernfactor then letterspace = tonumber(options.kernfactor) * upem end space = space + letterspace local slantfactor = nil if options.slant then slantfactor = tonumber(options.slant) * 1000 end local mode = nil local width = nil if options.embolden then mode = 2 -- The multiplication by 7200.0/7227 is to undo the opposite conversion -- the engine is doing and make the final number written in the PDF file -- match XeTeX’s. width = (size * tonumber(options.embolden) / 6553.6) * (7200.0/7227) end local hscale = upem local extendfactor = nil if options.extend then extendfactor = tonumber(options.extend) * 1000 hscale = hscale * tonumber(options.extend) end local vscale = upem local squeezefactor = nil if options.squeeze then squeezefactor = tonumber(options.squeeze) * 1000 vscale = vscale * tonumber(options.squeeze) end if options.texlig then for char in next, characters do local ligatures = tlig[char] if ligatures then characters[char].ligatures = ligatures end end end return { name = spec.specification, filename = spec.resolved or spec.name, designsize = size, psname = sanitize(data.psname), fullname = data.fullname, index = (spec.sub and spec.sub or 1) - 1, size = size, units_per_em = upem, type = "real", embedding = "subset", tounicode = 1, nomath = true, format = data.fonttype, slant = slantfactor, mode = mode, width = width, extend = extendfactor, squeeze = squeezefactor, characters = characters, parameters = { slant = data.slant, space = space * scale, space_stretch = space * scale / 2, space_shrink = space * scale / 3, x_height = data.xheight * scale, quad = size, extra_space = space * scale / 3, [8] = data.capheight * scale, -- for XeTeX compatibility. }, hb = { scale = scale, features = features, options = options, palette = palette, shared = data, letterspace = letterspace, hscale = hscale, vscale = vscale, }, } end local function define_font(spec, size) local hbdata = loadfont(spec) return hbdata and scalefont(hbdata, spec) end return define_font