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

sile-typesetter / sile / 14384044172

10 Apr 2025 03:07PM UTC coverage: 40.142% (+10.8%) from 29.317%
14384044172

push

github

alerque
Merge branch 'cairo-is-still-in-egypt'

3 of 51 new or added lines in 3 files covered. (5.88%)

302 existing lines in 20 files now uncovered.

6876 of 17129 relevant lines covered (40.14%)

2601.39 hits per line

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

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

5
local lastshaper
6

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

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

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

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

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

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

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

147
SILE.fontCache = {}
149✔
148

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

163
local font = {
149✔
164

165
   loadDefaults = function (options)
166
      if not options.family then
6,643✔
167
         options.family = SILE.settings:get("font.family")
6,834✔
168
      end
169
      if not options.size then
6,643✔
170
         options.size = SILE.settings:get("font.size")
6,744✔
171
      end
172
      if not options.weight then
6,643✔
173
         options.weight = SILE.settings:get("font.weight")
7,032✔
174
      end
175
      if not options.style then
6,643✔
176
         options.style = SILE.settings:get("font.style")
6,920✔
177
      end
178
      if not options.variant then
6,643✔
179
         options.variant = SILE.settings:get("font.variant")
12,578✔
180
      end
181
      if SILE.settings:get("font.filename") ~= "" then
13,286✔
UNCOV
182
         options.filename = SILE.settings:get("font.filename")
×
UNCOV
183
         options.family = ""
×
184
      end
185
      if not options.language then
6,643✔
186
         options.language = SILE.settings:get("document.language")
12,418✔
187
      end
188
      if not options.script then
6,643✔
189
         options.script = SILE.settings:get("font.script")
7,036✔
190
      end
191
      if not options.direction then
6,643✔
192
         options.direction = SILE.settings:get("font.direction")
12,578✔
193
         if not options.direction or options.direction == "" then
6,289✔
194
            options.direction = SILE.typesetter and SILE.typesetter.frame and SILE.typesetter.frame:writingDirection()
6,279✔
195
               or "LTR"
6,279✔
196
         end
197
      end
198
      if not options.features then
6,643✔
199
         options.features = SILE.settings:get("font.features")
12,566✔
200
      end
201
      if not options.variations then
6,643✔
202
         options.variations = SILE.settings:get("font.variations")
12,578✔
203
      end
204
      if not options.hyphenchar then
6,643✔
205
         options.hyphenchar = SILE.settings:get("font.hyphenchar")
12,576✔
206
      end
207
      return options
6,643✔
208
   end,
209

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

222
   finish = function ()
223
      for key, font in pairs(SILE.fontCache) do
251✔
224
         -- Don't do anything for Pango fonts
225
         if type(font) ~= "userdata" and type(font.insert) ~= "function" then
175✔
226
            if font.tempfilename ~= font.filename then
175✔
NEW
227
               SU.debug("fonts", "Removing temporary file of", key, ":", font.tempfilename)
×
NEW
228
               os.remove(font.tempfilename)
×
229
            end
230
         end
231
      end
232
   end,
233

234
   postLoadHook = function (face)
235
      -- Don't do anything for Pango fonts (here face could be a Pango Attribute Lists)
236
      if type(face) == "userdata" and type(face.insert) == "function" then
4,739✔
NEW
237
         return
×
238
      end
239
      local ot = require("core.opentype-parser")
4,739✔
240
      local font = ot.parseFont(face)
4,739✔
241
      if font.cpal then
4,739✔
UNCOV
242
         SILE.require("packages.color-fonts")
×
UNCOV
243
         if SILE.shaper._name ~= "harfbuzzWithColor" then
×
UNCOV
244
            SU.debug("color-fonts", "Switching to color font Shaper")
×
UNCOV
245
            SILE.typesetter:leaveHmode(true)
×
UNCOV
246
            lastshaper, SILE.shaper = SILE.shaper, SILE.shapers.harfbuzzWithColor()
×
247
         end
248
      end
249
   end,
250

251
   _key = _key,
149✔
252
}
253

254
return font
149✔
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