• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sile-typesetter / sile / 13909532704

17 Mar 2025 08:47PM UTC coverage: 58.465% (-8.1%) from 66.515%
13909532704

push

github

web-flow
Merge pull request #2237 from Omikhleia/feat-font-adjust

feat(core): Support ex-height and cap-height font adjustment

24 of 29 new or added lines in 3 files covered. (82.76%)

2396 existing lines in 68 files now uncovered.

12515 of 21406 relevant lines covered (58.46%)

3110.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.63
/core/font.lua
1
--- font
2
-- @module SILE.font
3
local icu = require("justenoughicu")
381✔
4

5
local lastshaper
6

7
local bits = require("core.parserbits")
381✔
8
local lpeg = require("lpeg")
381✔
9
local Ct, Cg, P = lpeg.Ct, lpeg.Cg, lpeg.P
381✔
10
local adjust_metric = P("ex-height") + P("cap-height")
381✔
11
local adjustment = Ct(Cg(bits.number, "amount")^-1 * bits.ws * Cg(adjust_metric, "unit"))
381✔
12

13
local function measureFontAdjustment (metric)
14
   if metric == "ex-height" then
8✔
15
      -- Uses the height of lowercase letters.
16
      -- This is used to normalize lowercase letters across fonts.
17
      -- The height of the lowercase letter "x" is used as the reference.
18
      -- Another option would be to use the OS/2 font table sxHeight value when available.
19
      return SILE.shaper:measureChar("x").height
16✔
20
   end
NEW
21
   if metric == "cap-height" then
×
22
      -- Uses the the height of uppercase letters.
23
      -- This is used to normalize uppercase letters across fonts.
24
      -- The height of the uppercase letter "H" is used as the reference.
25
      -- Another option would be to use the OS/2 font table sCapHeight value when available.
NEW
26
      return SILE.shaper:measureChar("H").height
×
27
   end
NEW
28
   SU.error("Unknown font adjust metric " .. metric)
×
29
end
30

31
local function adjustedFontSize(options)
32
   local adjust = options.adjust
4✔
33
   local parsed = adjustment:match(adjust)
4✔
34
   if not parsed then
4✔
NEW
35
      SU.error("Couldn't parse font adjust value " .. adjust)
×
36
   end
37
   -- Shallow copy: we don't want to modify the original AST as content may be reused
38
   -- in other contexts (e.g. running headers) and may need to adapt to different font sizes.
39
   local baseOpts = pl.tablex.copy(options)
4✔
40
   baseOpts.adjust = nil -- cancel for target font size calculation
4✔
41
   local currentMeasure = measureFontAdjustment(parsed.unit)
4✔
42
   local ratio = parsed.amount or 1
4✔
43
   local newMeasure
44
   -- Apply the target font size to measure the new font
45
   SILE.call("font", baseOpts, function ()
8✔
46
      newMeasure = measureFontAdjustment(parsed.unit)
8✔
47
   end)
48
   return SILE.settings:get("font.size") * ratio * (currentMeasure / newMeasure)
8✔
49
end
50

51
SILE.registerCommand("font", function (options, content)
762✔
52
   if SU.ast.hasContent(content) then
1,880✔
53
      SILE.settings:pushState()
265✔
54
   end
55
   if options.adjust then
940✔
56
      if options.size then
4✔
NEW
57
         SU.error("Can't specify both 'size' and 'adjust' in a \\font command")
×
58
      end
59
      SILE.settings:set("font.size", adjustedFontSize(options))
8✔
60
   end
61
   if options.filename then
940✔
62
      SILE.settings:set("font.filename", options.filename)
4✔
63
   end
64
   if options.family then
940✔
65
      SILE.settings:set("font.family", options.family)
640✔
66
      SILE.settings:set("font.filename", "")
640✔
67
   end
68
   if options.size then
940✔
69
      local size = SU.cast("measurement", options.size)
775✔
70
      if not size then
775✔
71
         SU.error("Couldn't parse font size " .. options.size)
×
72
      end
73
      SILE.settings:set("font.size", size:absolute())
1,550✔
74
   end
75
   if options.weight then
940✔
76
      SILE.settings:set("font.weight", 0 + options.weight)
596✔
77
   end
78
   if options.style then
940✔
79
      SILE.settings:set("font.style", options.style)
666✔
80
   end
81
   if options.variant then
940✔
82
      SILE.settings:set("font.variant", options.variant)
569✔
83
   end
84
   if options.features then
940✔
85
      SILE.settings:set("font.features", options.features)
577✔
86
   end
87
   if options.variations then
940✔
88
      SILE.settings:set("font.variations", options.variations)
569✔
89
   end
90
   if options.direction then
940✔
91
      SILE.settings:set("font.direction", options.direction)
574✔
92
   end
93
   if options.language then
940✔
94
      if options.language ~= "und" and icu and icu.canonicalize_language then
594✔
95
         local newlang = icu.canonicalize_language(options.language)
594✔
96
         -- if newlang ~= options.language then
97
         -- SU.warn("Language '"..options.language.."' not canonical, '"..newlang.."' will be used instead")
98
         -- end
99
         options.language = newlang
594✔
100
      end
101
      SILE.languageSupport.loadLanguage(options.language)
594✔
102
      SILE.settings:set("document.language", options.language)
594✔
103
   end
104
   if options.script then
940✔
105
      SILE.settings:set("font.script", options.script)
1,142✔
106
   elseif SILE.settings:get("document.language") then
738✔
107
      local lang = SILE.languageSupport.languages[SILE.settings:get("document.language")]
738✔
108
      if lang and lang.defaultScript then
369✔
109
         SILE.settings:set("font.script", lang.defaultScript)
1✔
110
      end
111
   end
112
   if options.hyphenchar then
940✔
113
      -- must be in the form of, for example, "-" or "U+2010" or "0x2010" (Unicode hex codepoint)
114
      SILE.settings:set("font.hyphenchar", SU.utf8charfromcodepoint(options.hyphenchar))
1,140✔
115
   end
116

117
   -- We must *actually* load the font here, because by the time we're inside
118
   -- SILE.shaper.shapeToken, it's too late to respond appropriately to things
119
   -- that the post-load hook might want to do.
120
   SILE.font.cache(SILE.font.loadDefaults({}), SILE.shaper.getFace)
1,880✔
121

122
   if SU.ast.hasContent(content) then
1,880✔
123
      SILE.process(content)
265✔
124
      SILE.settings:popState()
265✔
125
      if SILE.shaper._name == "harfbuzzWithColor" and lastshaper then
265✔
126
         SU.debug("color-fonts", "Switching from color fonts shaper back to previous shaper")
1✔
127
         SILE.typesetter:leaveHmode(true)
1✔
128
         lastshaper, SILE.shaper = nil, lastshaper
1✔
129
      end
130
   end
131
end, "Set current font family, size, weight, style, variant, script, direction and language", nil, true)
1,321✔
132

133
SILE.settings:declare({ parameter = "font.family", type = "string or nil", default = "Gentium Plus" })
381✔
134
SILE.settings:declare({ parameter = "font.size", type = "number or integer", default = 10 })
381✔
135
SILE.settings:declare({ parameter = "font.weight", type = "integer", default = 400 })
381✔
136
SILE.settings:declare({ parameter = "font.variant", type = "string", default = "normal" })
381✔
137
SILE.settings:declare({ parameter = "font.script", type = "string", default = "" })
381✔
138
SILE.settings:declare({ parameter = "font.style", type = "string", default = "" })
381✔
139
SILE.settings:declare({ parameter = "font.direction", type = "string", default = "" })
381✔
140
SILE.settings:declare({ parameter = "font.filename", type = "string or nil", default = "" })
381✔
141
SILE.settings:declare({ parameter = "font.features", type = "string", default = "" })
381✔
142
SILE.settings:declare({ parameter = "font.variations", type = "string", default = "" })
381✔
143
SILE.settings:declare({ parameter = "font.hyphenchar", type = "string", default = "-" })
381✔
144

145
SILE.fontCache = {}
381✔
146

147
local _key = function (options)
148
   return table.concat({
42,749✔
149
      options.family,
42,749✔
150
      ("%g"):format(SILE.types.measurement(options.size):tonumber()),
128,247✔
151
      ("%d"):format(options.weight or 0),
42,749✔
152
      options.style,
42,749✔
153
      options.variant,
42,749✔
154
      options.features,
42,749✔
155
      options.variations,
42,749✔
156
      options.direction,
42,749✔
157
      options.filename,
42,749✔
158
   }, ";")
42,749✔
159
end
160

161
local font = {
381✔
162

163
   loadDefaults = function (options)
164
      if not options.family then
8,622✔
165
         options.family = SILE.settings:get("font.family")
11,196✔
166
      end
167
      if not options.size then
8,622✔
168
         options.size = SILE.settings:get("font.size")
11,196✔
169
      end
170
      if not options.weight then
8,622✔
171
         options.weight = SILE.settings:get("font.weight")
11,580✔
172
      end
173
      if not options.style then
8,622✔
174
         options.style = SILE.settings:get("font.style")
11,580✔
175
      end
176
      if not options.variant then
8,622✔
177
         options.variant = SILE.settings:get("font.variant")
17,244✔
178
      end
179
      if SILE.settings:get("font.filename") ~= "" then
17,244✔
180
         options.filename = SILE.settings:get("font.filename")
84✔
181
         options.family = ""
42✔
182
      end
183
      if not options.language then
8,622✔
184
         options.language = SILE.settings:get("document.language")
16,860✔
185
      end
186
      if not options.script then
8,622✔
187
         options.script = SILE.settings:get("font.script")
11,580✔
188
      end
189
      if not options.direction then
8,622✔
190
         options.direction = SILE.settings:get("font.direction")
17,244✔
191
         if not options.direction or options.direction == "" then
8,622✔
192
            options.direction = SILE.typesetter and SILE.typesetter.frame and SILE.typesetter.frame:writingDirection()
8,006✔
193
               or "LTR"
8,006✔
194
         end
195
      end
196
      if not options.features then
8,622✔
197
         options.features = SILE.settings:get("font.features")
17,244✔
198
      end
199
      if not options.variations then
8,622✔
200
         options.variations = SILE.settings:get("font.variations")
17,244✔
201
      end
202
      if not options.hyphenchar then
8,622✔
203
         options.hyphenchar = SILE.settings:get("font.hyphenchar")
17,244✔
204
      end
205
      return options
8,622✔
206
   end,
207

208
   cache = function (options, callback)
209
      local key = _key(options)
7,287✔
210
      if not SILE.fontCache[key] then
7,287✔
211
         SU.debug("fonts", "Looking for", key)
368✔
212
         local face = callback(options)
368✔
213
         SILE.fontCache[key] = face
368✔
214
      end
215
      local cached = SILE.fontCache[key]
7,287✔
216
      SILE.font.postLoadHook(cached)
7,287✔
217
      return cached
7,287✔
218
   end,
219

220
   finish = function ()
221
      for key, font in pairs(SILE.fontCache) do
560✔
222
         if font.tempfilename ~= font.filename then
368✔
223
            SU.debug("fonts", "Removing temporary file of", key, ":", font.tempfilename)
×
224
            os.remove(font.tempfilename)
×
225
         end
226
      end
227
   end,
228

229
   postLoadHook = function (face)
230
      local ot = require("core.opentype-parser")
7,287✔
231
      local font = ot.parseFont(face)
7,287✔
232
      if font.cpal then
7,287✔
233
         SILE.require("packages.color-fonts")
15✔
234
         if SILE.shaper._name ~= "harfbuzzWithColor" then
15✔
235
            SU.debug("color-fonts", "Switching to color font Shaper")
5✔
236
            SILE.typesetter:leaveHmode(true)
5✔
237
            lastshaper, SILE.shaper = SILE.shaper, SILE.shapers.harfbuzzWithColor()
10✔
238
         end
239
      end
240
   end,
241

242
   _key = _key,
381✔
243
}
244

245
return font
381✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc