summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/luaotfload/luaotfload-notdef.lua
blob: 9c0634f90d712dbac24791d16c4900ab6f9e96df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
-----------------------------------------------------------------------
--         FILE:  luaotfload-notdef.lua
--  DESCRIPTION:  part of luaotfload / notdef
-----------------------------------------------------------------------

local ProvidesLuaModule = { 
    name          = "luaotfload-notdef",
    version       = "3.11",       --TAGVERSION
    date          = "2019-11-10", --TAGDATE
    description   = "luaotfload submodule / color",
    license       = "GPL v2.0",
    author        = "Marcel Krüger"
}

if luatexbase and luatexbase.provides_module then
  luatexbase.provides_module (ProvidesLuaModule)
end  

local flush_node         = node.direct.flush_node
local getfont            = font.getfont
local getnext            = node.direct.getnext
local getwhd             = node.direct.getwhd
local insert             = table.insert
local insert_after       = node.direct.insert_after
local kern_id            = node.id'kern'
local nodenew            = node.direct.new
local otfregister        = fonts.constructors.features.otf.register
local protect_glyph      = node.direct.protect_glyph
local remove             = node.direct.remove
local setfont            = node.direct.setfont
local traverse_char      = node.direct.traverse_char

-- According to DerivedCoreProperties.txt, Default_Ignorable_Code_Point
-- is generated from:
--    Other_Default_Ignorable_Code_Point
--  + Cf (Format characters)
--  + Variation_Selector
--  - White_Space
--  - FFF9..FFFB (Interlinear annotation format characters)
--  - 13430..13438 (Egyptian hieroglyph format characters)
--  - Prepended_Concatenation_Mark (Exceptional format characters that should be visible)
-- Based on HarfBuzz, we add the exclusion
--  - Lo (Letter, Other)
-- This affects Hangul fillers.
local ignorable_codepoints do
  local sep = lpeg.P' '^0 * ';' * lpeg.P' '^0
  local codepoint = lpeg.S'0123456789ABCDEF'^4/function(c)return tonumber(c, 16)end
  local codepoint_range = codepoint * ('..' * codepoint + lpeg.Cc(false))
  local function multirawset(table, key1, key2, value)
    for key = key1,(key2 or key1) do
      rawset(table, key, value)
    end
    return table
  end
  local entry = lpeg.Cg(codepoint * ';' * (1-lpeg.P';')^0 * ';Cf;' * lpeg.Cc(true))^-1 * (1-lpeg.P'\n')^0 * '\n'
  local file = lpeg.Cf(
      lpeg.Ct''
    * entry^0
  , rawset)
  local f = io.open(kpse.find_file"UnicodeData.txt")
  ignorable_codepoints = file:match(f:read'*a')
  f:close()
  entry = lpeg.Cg(codepoint_range * sep * ('Other_Default_Ignorable_Code_Point' * lpeg.Cc(true)
                                               + 'Variation_Selector' * lpeg.Cc(true)
                                               + 'White_Space' * lpeg.Cc(nil)
                                               + 'Prepended_Concatenation_Mark' * lpeg.Cc(nil)
                                          ) * ' # ' * (1-lpeg.P'Lo'))^-1 * (1-lpeg.P'\n')^0 * '\n'
  file = lpeg.Cf(
      lpeg.Carg(1)
    * entry^0
  , multirawset)
  f = io.open(kpse.find_file"PropList.txt")
  ignorable_codepoints = file:match(f:read'*a', 1, ignorable_codepoints)
  f:close()
  for i = 0xFFF9,0xFFFB do
    ignorable_codepoints[i] = nil
  end
  for i = 0x13430,0x13438 do
    ignorable_codepoints[i] = nil
  end
end

local function setnotdef(tfmdata, factor)
  local desc = tfmdata.shared.rawdata.descriptions
  -- So we have to find the .notdef glyph. We only know that it has GID
  -- 0, but we need it's Unicode mapping. Normally it isn't mapped in
  -- the font, so we auto-assigned the first private slot:
  local guess = desc[0xF0000]
  if guess and guess.index == 0 then
    tfmdata.notdefcode = 0xF0000
    return
  end
  -- If this didn't happen, it might be mapped to one of the
  -- replacement characters:
  for code = 0xFFFC,0xFFFF do
    guess = desc[code]
    if guess and guess.index == 0 then
      tfmdata.notdefcode = code
      return
    end
  end
  -- Oh no, we couldn't find it. Maybe we can find it by name?
  local code = tfmdata.resources.unicodes[".notdef"]
  -- Better safe than sorry
  guess = code and desc[code]
  if guess and guess.index == 0 then
    tfmdata.notdefcode = code
    return
  end
  -- So the font didn't do the obvious things and then it lied to us.
  -- At this point we should think about sending an automated complain
  -- to the font author, but we probably can't trust the contact
  -- information either.
  -- We will fall back to brute force now:
  for code, char in pairs(desc) do
    if char.index == 0 then
      tfmdata.notdefcode = code
      return
    end
  end
  -- If we ever reach this point, something odd happened. Either there
  -- are no glyphs at all (then LuaTeX will complain anyway, so let's
  -- ignore that case) or someone tried to use this with a legacy font.
  -- In that case there most likely isn't a `.notdef` glyph anyway and
  -- inserting glyph 0 would insert a random character, so `notdefcode`
  -- better stays `nil`.
end

local glyph_id = node.id'glyph'
local function donotdef(head, font, _, _, _)
  local tfmdata = getfont(font)
  local notdef, chars = tfmdata.unscaled.notdefcode, tfmdata.characters
  if not notdef then return end
  for cur, cid, fid in traverse_char(head) do if fid == font then
    local w, h, d = getwhd(cur)
    if w == 0 and h == 0 and d == 0 and not chars[cid] and not ignorable_codepoints[cid] then
      local notdefnode = nodenew(glyph_id, 256)
      setfont(notdefnode, font, notdef)
      insert_after(cur, cur, notdefnode)
      protect_glyph(cur)
    end
  end end
end

otfregister {
  name        = "notdef",
  description = "Add notdef glyphs",
  default     = 1,
  initializers = {
    node = setnotdef,
  },
  processors = {
    node = donotdef,
  }
}

function fonts.handlers.otf.handlers.gsub_remove(head,char,dataset,sequence,replacement)
  local next
  head, next = remove(head, char)
  flush_node(char)
  if not head and not next then -- Avoid a double free if we were alone
    head = nodenew(kern_id)
  end
  return head, next, true, true
end

local sequence = {
  features = {invisible = {["*"] = {["*"] = true}}},
  flags = {false, false, false, false},
  name = "invisible",
  order = {"invisible"},
  nofsteps = 1,
  steps = {{
    coverage = ignorable_codepoints,
    index = 1,
  }},
  type = "gsub_remove",
}
local function invisibleinitialiser(tfmdata, value)
  local resources = tfmdata.resources
  local sequences = resources and resources.sequences
  if sequences then
    -- Now we get to the interesting part: At which point should our new sequence be inserted? Let's do it at the end, then they are still seen by all features.
    insert(sequences, sequence)
  end
end
otfregister {
  name = 'invisible',
  description = 'Remove invisible control characters',
  default = true,
  initializers = {
    node = invisibleinitialiser,
  },
}

--- vim:sw=2:ts=2:expandtab:tw=71