local hb = require("justenoughharfbuzz")
local icu = require("justenoughicu")
local bitshim = require("bitshim")
SILE.settings:declare({
parameter = "harfbuzz.subshapers",
type = "string or nil",
default = "",
help = "Comma-separated shaper list to pass to Harfbuzz",
})
local base = require("shapers.base")
local smallTokenSize = 20 local shapeCache = {}
local _key = function (options, text)
return table.concat({
text,
options.tracking or "1",
options.language,
options.script,
SILE.font._key(options),
}, ";")
end
local substwarnings = {}
local usedfonts = {}
local shaper = pl.class(base)
shaper._name = "harfbuzz"
function shaper:shapeToken (text, options)
local items
if #text < smallTokenSize then
items = shapeCache[_key(options, text)]
if items then
return items
end
end
local face = SILE.font.cache(options, self.getFace)
if self:checkHBProblems(text, face) then
return {}
end
if not face then
SU.error("Could not find requested font " .. options .. " or any suitable substitutes")
end
if not options.filename and face.family ~= options.family and not substwarnings[options.family] then
substwarnings[options.family] = true
SU.warn("Font family '" .. options.family .. "' not available, falling back to '" .. face.family .. "'")
end
usedfonts[face] = true
items = {
hb._shape(
text,
face,
options.script,
options.direction,
options.language,
face.pointsize,
options.features,
SILE.settings:get("harfbuzz.subshapers") or ""
),
}
for i = 1, #items do
local j = (i == #items) and #text or items[i + 1].index
items[i].text = text:sub(items[i].index + 1, j) if options.tracking then
items[i].width = items[i].width * options.tracking
end
end
if #text < smallTokenSize then
shapeCache[_key(options, text)] = items
end
return items
end
local _pretty_varitions = function (face)
local text = face.filename
if face.variations and face.variations ~= "" then
text = text .. "@" .. face.variations
end
local index = bitshim.band(face.index, 0xFFFF) or 0
local instance = bitshim.rshift(face.index, 16) or 0
if index or instance then
text = text .. "[" .. index .. "," .. instance .. "]"
end
return text
end
function shaper.getFace (opts)
local face = SILE.fontManager:face(opts)
SU.debug("fonts", "Resolved font family", opts.family, "->", face and face.filename)
if not face or not face.filename then
SU.error("Couldn't find face '" .. opts.family .. "'")
end
if SILE.makeDeps then
SILE.makeDeps:add(face.filename)
end
face.variations = opts.variations or ""
face.pointsize = ("%g"):format(SILE.types.measurement(opts.size):tonumber())
face.weight = ("%d"):format(opts.weight or 0)
face.tempfilename = face.filename
local data = hb.instanciate(face)
if data then
local tmp = os.tmpname()
local file = io.open(tmp, "wb")
file:write(data)
file:close()
face.tempfilename = tmp
SU.debug("fonts", "Instanciated", _pretty_varitions(face), "as", face.tempfilename)
elseif (face.variations ~= "") or (bitshim.rshift(face.index, 16) ~= 0) then
if not SILE.features.font_variations then
SU.warn([[
This build of SILE was compiled with font variations support disabled,
likely due to not having the subsetter library included in HarfBuzz >= 6.
This document specifies font variations which cannot be correctly rendered.
Please rebuild SILE with the necessary library support. Alternatively to procede
anyway *incorrectly* render this document run:
sile -e 'SILE.features.font_variations = true' ....
Or modify the document to remove variations options from font commands.]])
end
SU.error("Failed to instanciate: " .. _pretty_varitions(face))
end
return face
end
function shaper.preAddNodes (_, items, nnodeValue) for i = 1, #items do
if items[i].y_offset or items[i].x_offset or items[i].width ~= items[i].glyphAdvance then
nnodeValue.complex = true
break
end
end
end
function shaper.addShapedGlyphToNnodeValue (_, nnodevalue, shapedglyph)
if not nnodevalue.items then
nnodevalue.items = {}
end
nnodevalue.items[#nnodevalue.items + 1] = shapedglyph
if not nnodevalue.glyphString then
nnodevalue.glyphString = {}
end
if not nnodevalue.glyphNames then
nnodevalue.glyphNames = {}
end
table.insert(nnodevalue.glyphString, shapedglyph.gid)
table.insert(nnodevalue.glyphNames, shapedglyph.name)
end
function shaper.debugVersions (_)
local ot = require("core.opentype-parser")
print("Harfbuzz version: " .. hb.version())
print("Shapers enabled: " .. table.concat({ hb.shapers() }, ", "))
if icu then
print("ICU support enabled")
end
print("")
print("Fonts used:")
for face, _ in pairs(usedfonts) do
local font = ot.parseFont(face)
local version = "Unknown version"
if font and font.names and font.names[5] then
for _, v in pairs(font.names[5]) do
version = v[1]
break
end
end
print(face.filename .. ":" .. face.index, version)
end
end
function shaper.checkHBProblems (_, text, face)
if hb.version_lessthan(1, 0, 4) and #text < 1 then
return true
end
if hb.version_lessthan(2, 3, 0) and hb.get_table(face, "CFF "):len() > 0 and not substwarnings["CFF "] then
SILE.status.unsupported = true
SU.warn(
"Vertical spacing of CFF fonts may be subtly inconsistent between systems. Upgrade to Harfbuzz 2.3.0 if you need absolute consistency."
)
substwarnings["CFF "] = true
end
return false
end
return shaper