Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

\font-feature #1002

Merged
merged 11 commits into from
Mar 2, 2021
2 changes: 1 addition & 1 deletion core/debug-output.lua
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ SILE.outputters.debug = {
_deprecationCheck(self)
local font = SILE.font._key(options)
if lastFont ~= font then
writeline("Set font ", SILE.font._key(options))
writeline("Set font ", font)
lastFont = font
end
end,
Expand Down
11 changes: 10 additions & 1 deletion core/font.lua
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,16 @@ SILE.settings.declare({ parameter = "document.language", type = "string", defaul
SILE.fontCache = {}

local _key = function (options)
return table.concat({ options.family;("%g"):format(options.size);("%d"):format(options.weight);options.style;options.variant;options.features;options.direction;options.filename }, ";")
return table.concat({
options.family,
("%g"):format(options.size),
("%d"):format(options.weight),
options.style,
options.variant,
options.features,
options.direction,
options.filename,
}, ";")
end

SILE.font = {
Expand Down
124 changes: 89 additions & 35 deletions packages/features.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
local lpeg = require("lpeg")

local S, P, C = lpeg.S, lpeg.P, lpeg.C
local R, S, P, C = lpeg.R, lpeg.S, lpeg.P, lpeg.C
local Cf, Ct = lpeg.Cf, lpeg.Ct

local opentype = { -- Mapping of opentype features to friendly names
local otFeatureMap = {
Ligatures = {
Required = "rlig",
Common = "liga",
Expand All @@ -19,6 +19,9 @@ local opentype = { -- Mapping of opentype features to friendly names
StylisticSet = function (i)
return string.format("ss%02i", tonumber(i))
end,
CharacterVariant = function (i)
return string.format("cv%02i", tonumber(i))
end,
Letters = {
Uppercase = "case",
SmallCaps = "smcp",
Expand Down Expand Up @@ -53,7 +56,6 @@ local opentype = { -- Mapping of opentype features to friendly names
ScientificInferior = "sinf",
Ordinal = "ordn"
},
-- XXX Character variant support not implemented yet
Style = {
Alternate = "salt",
Italic = "ital",
Expand Down Expand Up @@ -104,53 +106,105 @@ local value = C(SILE.parserBits.integer)
local tag = C(S"+-") * featurename * (P"=" * value)^0 * S",;:"^-1 / tagpos
local featurestring = Cf(Ct"" * tag^0, rawset)

local featurestring2table = function (str)
return featurestring:match(str) or SU.error("Unparsable Opentype feature string '"..str.."'")
-- Parser for fontspec strings
-- Refer to fontspec.pdf (see doc), Chapter 3, Table 4 (p. 37)
local fontspecsafe = R("AZ", "az", "09") + P":"
local fontspecws = SILE.parserBits.whitespace^0
local fontspecsep = P"," * fontspecws
local fontspecname = C(fontspecsafe^1)
local fontspeclist = fontspecws * P"{" *
Ct(fontspecws * fontspecname *
(fontspecsep * fontspecname * fontspecws)^0) *
P"}" * fontspecws

local otFeatures = pl.class(pl.Map)

function otFeatures:_init ()
self:super()
local str = SILE.settings.get("font.features")
local tbl = featurestring:match(str)
if not tbl then
SU.error("Unparsable Opentype feature string '"..str.."'")
end
for feat, flag in pairs(tbl) do
self:set(feat, flag.posneg == "+")
end
end

local table2featurestring = function (tbl)
local t2 = {}
for k, v in pairs(tbl) do t2[#t2+1] = v.posneg..k..(v.value and "="..v.value or "") end
return table.concat(t2, ";")
function otFeatures:__tostring ()
local ret = {}
for _, f in ipairs(self:items()) do
ret[#ret+1] = (f[2] and "+" or "-") .. f[1]
end
return table.concat(ret, ";")
end

SILE.registerCommand("add-font-feature", function (options, _)
local t = featurestring2table(SILE.settings.get("font.features"))
for k, v in pairs(options) do
if not opentype[k] then SU.warn("Unknown Opentype feature "..k)
else
local posneg = "+"
v = v:gsub("^No", function () posneg= "-"; return "" end)
local res
if type(opentype[k]) == "function" then res = opentype[k](v) else res = opentype[k][v] end
if not res then SU.error("Bad OpenType value "..v.." for feature "..k) end
if type(res) == "string" then
t[res] = {posneg = posneg}
function otFeatures:loadOption (name, val, invert)
local posneg = not invert
local key = otFeatureMap[name]
if not key then
SU.warn("Unknown OpenType feature " .. name)
else
local matches = lpeg.match(fontspeclist, val)
for _, v in ipairs(matches or { val }) do
v = v:gsub("^No", function () posneg = false; return "" end)
local feat = type(key) == "function" and key(v) or key[v]
if not feat then
SU.warn("Bad OpenType value " .. v .. " for feature " .. name)
else
t[res.key] = { posneg = posneg, value = res.value}
self:set(feat, posneg)
end
end
end
end

SILE.settings.set("font.features", table2featurestring(t))
-- Input like {Ligatures = Historic} or {Ligatures = "{Historic, Discretionary}"}
--
-- Most real-world use should be single value, but multiple value use is not
-- that odd. Junicode, for example, a common font among medievalists, has many
-- Stylistic Sets and Character Variations, many of which make sense to enable
-- simultaneously.
function otFeatures:loadOptions (options, invert)
SU.debug("features", "Features was", self)
for k, v in pairs(options) do
self:loadOption(k, v, invert)
end
SU.debug("features", "Features interpreted as", self)
end

function otFeatures:unloadOptions (options)
self:loadOptions(options, true)
end

SILE.registerCommand("add-font-feature", function (options, _)
local otfeatures = otFeatures()
otfeatures:loadOptions(options)
SILE.settings.set("font.features", tostring(otfeatures))
end)

SILE.registerCommand("remove-font-feature", function (options, _)
local t = featurestring2table(SILE.settings.get("font.features"))
SILE.registerCommand("remove-font-feature", function(options, _)
local otfeatures = otFeatures()
otfeatures:unloadOptions(options)
SILE.settings.set("font.features", tostring(otfeatures))
end)

local fontfn = SILE.Commands.font
SILE.registerCommand("font", function (options, content)
local otfeatures = otFeatures()
-- It is guaranteed that future releases of SILE will not implement non-OT \font
-- features with capital letters.
-- Cf. https://github.com/sile-typesetter/sile/issues/992#issuecomment-665575353
-- So, we reserve 'em all. ⍩⃝
for k, v in pairs(options) do
if not opentype[k] then SU.warn("Unknown Opentype feature "..k)
else
v = v:gsub("^No", "")
local res
if type(opentype[k]) == "function" then res = opentype[k](v) else res = opentype[k][v] end
if not res then SU.error("Bad OpenType value "..v.." for feature "..k) end
if type(res) == "string" then t[res] = nil else t[res.key] = nil end
if k:match('^[A-Z]') then
otfeatures:loadOption(k, v)
options[k] = nil
end
end

SILE.settings.set("font.features", table2featurestring(t))
end)
SU.debug("features", "Font features parsed as:", otfeatures)
options.features = (options.features and options.features .. ";" or "") .. tostring(otfeatures)
return fontfn(options, content)
end, SILE.Help.font .. " (overridden)")

return { documentation = [[\begin{document}
As mentioned in Chapter 3, SILE automatically applies ligatures defined by the fonts
Expand Down
29 changes: 29 additions & 0 deletions tests/font-features-cvXX.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Set paper size 297.6377985 419.5275636
Begin page
Mx 14.8819
My 30.4442
Set font Gentium Plus;10;400;;normal;;LTR
T 858 w=5.5176 (һ)
Mx 23.0701
T 1306 w=5.0293 (ồ)
Mx 14.8819
My 42.4442
Set font Gentium Plus;10;400;;normal;+cv75;+cv81;LTR
T 859 w=5.2148 (һ)
Mx 22.7674
T 1307 w=5.0293 (ồ)
Mx 14.8819
My 54.4442
Set font Gentium Plus;10;400;;normal;+cv75;-cv81;LTR
T 858 w=5.5176 (һ)
Mx 23.0701
Set font Gentium Plus;10;400;;normal;-cv75;+cv81;LTR
T 1306 w=5.0293 (ồ)
Mx 14.8819
My 66.4442
Set font Gentium Plus;10;400;;normal;-cv75;-cv81;LTR
T 858 w=5.5176 (һ)
Mx 23.0701
T 1306 w=5.0293 (ồ)
End page
Finish
15 changes: 15 additions & 0 deletions tests/font-features-cvXX.sil
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
\begin[papersize=a6]{document}
\nofolios
\neverindent
\script[src=packages/features]
\set[parameter=harfbuzz.subshapers,value=ot]
һ ồ

\begin[CharacterVariant="{75,81}"]{font}
һ ồ

\font[CharacterVariant=No81]{һ} \font[CharacterVariant=No75]{ồ}

\font[CharacterVariant="{No75, No81}"]{һ ồ}
\end{font}
\end{document}
38 changes: 38 additions & 0 deletions tests/font-features.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Set paper size 297.6377985 419.5275636
Begin page
Mx 14.8819
My 27.2752
Set font Gentium Plus;10;400;;normal;+smcp;LTR
T 54 3871 3490 3850 3850 38 3490 3964 4007 w=47.2266 (SmallCaps)
My 39.2752
T 54 3871 3490 3850 3850 38 3490 3964 4007 w=47.2266 (SmallCaps)
My 51.2752
Set font Gentium Plus;10;400;;normal;+dlig;+hlig;-smcp;LTR
Mx 4.7607
Mx 2.7100
Mx 4.8779
Mx 4.5898
Mx 3.4424
Mx 5.2979
Mx 3.8086
Mx 4.6191
Mx 3.8623
T 47 a=4.7607 76 a=2.7100 74 a=4.8779 68 a=4.5898 87 a=3.4424 88 a=5.2979 85 a=3.9600 72 a=4.6191 86 a=3.8623 (Ligatures)
Mx 14.8819
My 63.2752
Mx 4.7607
Mx 2.7100
Mx 4.8779
Mx 4.5898
Mx 3.4424
Mx 5.2979
Mx 3.8086
Mx 4.6191
Mx 3.8623
T 47 a=4.7607 76 a=2.7100 74 a=4.8779 68 a=4.5898 87 a=3.4424 88 a=5.2979 85 a=3.9600 72 a=4.6191 86 a=3.8623 (Ligatures)
Mx 14.8819
My 75.2752
Set font Gentium Plus;10;400;;normal;-dlig;-hlig;-smcp;LTR
T 49 82 85 80 68 79 w=30.8105 (Normal)
End page
Finish
19 changes: 19 additions & 0 deletions tests/font-features.sil
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
\begin[papersize=a6]{document}
\neverindent
\nofolios
\script[src=packages/features]
ctrlcctrlv marked this conversation as resolved.
Show resolved Hide resolved
\set[parameter=harfbuzz.subshapers,value=ot]
\font[Letters=SmallCaps]{SmallCaps}

\add-font-feature[Letters=SmallCaps]
SmallCaps
\remove-font-feature[Letters=SmallCaps]

\font[Ligatures="{ Historic, Discretionary }"]{Ligatures}

\add-font-feature[Ligatures="{ Historic, Discretionary }"]
Ligatures
\remove-font-feature[Ligatures="{ Historic, Discretionary }"]

Normal
\end{document}