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

sile-typesetter / sile / 9441244789

10 Jun 2024 01:45AM UTC coverage: 49.048% (-11.6%) from 60.675%
9441244789

push

github

web-flow
Merge 8a4989eae into 519864108

8320 of 16963 relevant lines covered (49.05%)

1813.12 hits per line

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

76.0
/shapers/harfbuzz.lua
1
local hb = require("justenoughharfbuzz")
3✔
2
local icu = require("justenoughicu")
3✔
3
local bitshim = require("bitshim")
3✔
4

5
SILE.settings:declare({
3✔
6
   parameter = "harfbuzz.subshapers",
7
   type = "string or nil",
8
   default = "",
9
   help = "Comma-separated shaper list to pass to Harfbuzz",
10
})
11

12
local base = require("shapers.base")
3✔
13

14
local smallTokenSize = 20 -- Small words will be cached
3✔
15
local shapeCache = {}
3✔
16
local _key = function (options, text)
17
   return table.concat({
261✔
18
      text,
261✔
19
      options.tracking or "1",
261✔
20
      options.language,
261✔
21
      options.script,
261✔
22
      SILE.font._key(options),
522✔
23
   }, ";")
261✔
24
end
25

26
local substwarnings = {}
3✔
27
local usedfonts = {}
3✔
28

29
local shaper = pl.class(base)
3✔
30
shaper._name = "harfbuzz"
3✔
31

32
function shaper:shapeToken (text, options)
3✔
33
   local items
34
   if #text < smallTokenSize then
231✔
35
      items = shapeCache[_key(options, text)]
450✔
36
      if items then
225✔
37
         return items
189✔
38
      end
39
   end
40
   local face = SILE.font.cache(options, self.getFace)
42✔
41
   if self:checkHBProblems(text, face) then
84✔
42
      return {}
×
43
   end
44
   if not face then
42✔
45
      SU.error("Could not find requested font " .. options .. " or any suitable substitutes")
×
46
   end
47
   if not options.filename and face.family ~= options.family and not substwarnings[options.family] then
42✔
48
      substwarnings[options.family] = true
×
49
      SU.warn("Font family '" .. options.family .. "' not available, falling back to '" .. face.family .. "'")
×
50
   end
51
   usedfonts[face] = true
42✔
52
   items = {
42✔
53
      hb._shape(
84✔
54
         text,
42✔
55
         face,
42✔
56
         options.script,
42✔
57
         options.direction,
42✔
58
         options.language,
42✔
59
         face.pointsize,
42✔
60
         options.features,
42✔
61
         SILE.settings:get("harfbuzz.subshapers") or ""
84✔
62
      ),
42✔
63
   }
42✔
64
   for i = 1, #items do
309✔
65
      local j = (i == #items) and #text or items[i + 1].index
267✔
66
      items[i].text = text:sub(items[i].index + 1, j) -- Lua strings are 1-indexed
534✔
67
      if options.tracking then
267✔
68
         items[i].width = items[i].width * options.tracking
×
69
      end
70
   end
71
   if #text < smallTokenSize then
42✔
72
      shapeCache[_key(options, text)] = items
72✔
73
   end
74
   return items
42✔
75
end
76

77
local _pretty_varitions = function (face)
78
   local text = face.filename
×
79
   if face.variations and face.variations ~= "" then
×
80
      text = text .. "@" .. face.variations
×
81
   end
82
   local index = bitshim.band(face.index, 0xFFFF) or 0
×
83
   local instance = bitshim.rshift(face.index, 16) or 0
×
84
   if index or instance then
×
85
      text = text .. "[" .. index .. "," .. instance .. "]"
×
86
   end
87
   return text
×
88
end
89

90
-- TODO: normalize this method to accept self as first arg
91
function shaper.getFace (opts)
3✔
92
   local face = SILE.fontManager:face(opts)
7✔
93
   SU.debug("fonts", "Resolved font family", opts.family, "->", face and face.filename)
7✔
94
   if not face or not face.filename then
7✔
95
      SU.error("Couldn't find face '" .. opts.family .. "'")
×
96
   end
97
   if SILE.makeDeps then
7✔
98
      SILE.makeDeps:add(face.filename)
×
99
   end
100
   face.variations = opts.variations or ""
7✔
101
   face.pointsize = ("%g"):format(SILE.types.measurement(opts.size):tonumber())
21✔
102
   face.weight = ("%d"):format(opts.weight or 0)
7✔
103

104
   -- Try instanciating the font, hb.instanciate() will return nil if it is not
105
   -- a variable font or if instanciation failed.
106
   face.tempfilename = face.filename
7✔
107
   local data = hb.instanciate(face)
7✔
108
   if data then
7✔
109
      local tmp = os.tmpname()
×
110
      local file = io.open(tmp, "wb")
×
111
      file:write(data)
×
112
      file:close()
×
113
      face.tempfilename = tmp
×
114
      SU.debug("fonts", "Instanciated", _pretty_varitions(face), "as", face.tempfilename)
×
115
   elseif (face.variations ~= "") or (bitshim.rshift(face.index, 16) ~= 0) then
7✔
116
      if not SILE.features.font_variations then
×
117
         SU.warn([[
×
118
  This build of SILE was compiled with font variations support disabled,
119
  likely due to not having the subsetter library included in HarfBuzz >= 6.
120
  This document specifies font variations which cannot be correctly rendered.
121
  Please rebuild SILE with the necessary library support. Alternatively to procede
122
  anyway *incorrectly* render this document run:
123

124
      sile -e 'SILE.features.font_variations = true' ....
125

126
  Or modify the document to remove variations options from font commands.]])
×
127
      end
128
      SU.error("Failed to instanciate: " .. _pretty_varitions(face))
×
129
   end
130

131
   return face
7✔
132
end
133

134
function shaper.preAddNodes (_, items, nnodeValue) -- Check for complex nodes
3✔
135
   for i = 1, #items do
553✔
136
      if items[i].y_offset or items[i].x_offset or items[i].width ~= items[i].glyphAdvance then
370✔
137
         nnodeValue.complex = true
36✔
138
         break
36✔
139
      end
140
   end
141
end
142

143
function shaper.addShapedGlyphToNnodeValue (_, nnodevalue, shapedglyph)
3✔
144
   -- Note: previously we stored the shaped items only for "complex" nodes
145
   -- (nodevalue.comple). We now always do it, so as to have them at hand for
146
   -- italic correction.
147
   if not nnodevalue.items then
196✔
148
      nnodevalue.items = {}
57✔
149
   end
150
   nnodevalue.items[#nnodevalue.items + 1] = shapedglyph
196✔
151

152
   if not nnodevalue.glyphString then
196✔
153
      nnodevalue.glyphString = {}
×
154
   end
155
   if not nnodevalue.glyphNames then
196✔
156
      nnodevalue.glyphNames = {}
57✔
157
   end
158
   table.insert(nnodevalue.glyphString, shapedglyph.gid)
196✔
159
   table.insert(nnodevalue.glyphNames, shapedglyph.name)
196✔
160
end
161

162
function shaper.debugVersions (_)
3✔
163
   local ot = require("core.opentype-parser")
3✔
164
   print("Harfbuzz version: " .. hb.version())
3✔
165
   print("Shapers enabled: " .. table.concat({ hb.shapers() }, ", "))
3✔
166
   if icu then
3✔
167
      print("ICU support enabled")
3✔
168
   end
169
   print("")
3✔
170
   print("Fonts used:")
3✔
171
   for face, _ in pairs(usedfonts) do
10✔
172
      local font = ot.parseFont(face)
7✔
173
      local version = "Unknown version"
7✔
174
      if font and font.names and font.names[5] then
7✔
175
         -- luacheck: ignore 512
176
         -- (It's OK to grab the first version we find in the name table)
177
         for _, v in pairs(font.names[5]) do
7✔
178
            version = v[1]
7✔
179
            break
7✔
180
         end
181
      end
182
      print(face.filename .. ":" .. face.index, version)
7✔
183
   end
184
end
185

186
function shaper.checkHBProblems (_, text, face)
3✔
187
   if hb.version_lessthan(1, 0, 4) and #text < 1 then
42✔
188
      return true
×
189
   end
190
   if hb.version_lessthan(2, 3, 0) and hb.get_table(face, "CFF "):len() > 0 and not substwarnings["CFF "] then
42✔
191
      SILE.status.unsupported = true
×
192
      SU.warn(
×
193
         "Vertical spacing of CFF fonts may be subtly inconsistent between systems. Upgrade to Harfbuzz 2.3.0 if you need absolute consistency."
194
      )
195
      substwarnings["CFF "] = true
×
196
   end
197
   return false
42✔
198
end
199

200
return shaper
3✔
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