[latex3-commits] [git/LaTeX3-latex3-luaotfload] variable-cff2: Add CFF2 variable fonts for harf mode -- Initial code (5419b60)
Marcel Fabian Krüger
tex at 2krueger.de
Wed Aug 4 13:40:24 CEST 2021
Repository : https://github.com/latex3/luaotfload
On branch : variable-cff2
Link : https://github.com/latex3/luaotfload/commit/5419b60409417b2ec3171b4dcaf7b2dd98c6bca5
>---------------------------------------------------------------
commit 5419b60409417b2ec3171b4dcaf7b2dd98c6bca5
Author: Marcel Fabian Krüger <tex at 2krueger.de>
Date: Wed Aug 4 12:56:09 2021 +0200
Add CFF2 variable fonts for harf mode -- Initial code
>---------------------------------------------------------------
5419b60409417b2ec3171b4dcaf7b2dd98c6bca5
src/luaotfload-harf-define.lua | 99 +++++++-
src/luaotfload-harf-var-cff2.lua | 442 ++++++++++++++++++++++++++++++++++
src/luaotfload-harf-var-t2-writer.lua | 62 +++++
3 files changed, 600 insertions(+), 3 deletions(-)
diff --git a/src/luaotfload-harf-define.lua b/src/luaotfload-harf-define.lua
index e66a8f1..29ff0ef 100644
--- a/src/luaotfload-harf-define.lua
+++ b/src/luaotfload-harf-define.lua
@@ -21,6 +21,7 @@ local gsub = string.gsub
local hb = luaotfload.harfbuzz
local scriptlang_to_harfbuzz = require'luaotfload-scripts'.to_harfbuzz
+local cff2_handler = require'luaotfload-harf-var-cff2'
local harf_settings = luaotfload.harf or {}
luaotfload.harf = harf_settings
@@ -70,14 +71,23 @@ local get_designsize do
end
local containers = luaotfload.fontloader.containers
-local hbcacheversion = 1.3
+local hbcacheversion = 1.4
local fontcache = containers.define("fonts", "hb", hbcacheversion, true)
local facecache = {}
+local variable_pattern do
+ local l = lpeg or require'lpeg'
+ local white = l.S' \t'^0
+ local number = l.C(l.S'+-'^-1 * (l.R'09'^1 * ('.' * l.R'09'^0)^-1 + '.' * l.R'09'^1))
+ local name_or_tag = l.C(l.R('AZ', 'az')^1)
+ local pair = l.Ct(name_or_tag * white * '=' * white * number)
+ variable_pattern = l.Ct(pair * (white * ',' * white * pair)^0)
+end
+
local function loadfont(spec)
local path, sub = spec.resolved, spec.sub or 1
- local key = string.format("%s:%d", gsub(path, "[/\\]", ":"), sub)
+ local key = gsub(string.format("%s:%d:%s", path, sub, instance), "[/\\]", ":")
local attributes = lfs.attributes(path)
if not attributes then return end
@@ -89,6 +99,66 @@ local function loadfont(spec)
facecache[key] = hbface
end
+ local normalized
+ local varkey
+ if hbface:ot_var_has_data() then
+ if hbface:get_table(cff2tag):get_length() == 0 then
+ error'Only CFF2 based Variable fonts are currently supported in harf mode'
+ end
+ local instance = spec.features.raw.instance
+ local assignments = instance and variable_pattern:match(instance)
+ if assignments then
+ local axes = hbface:ot_var_get_axis_infos()
+ for i = 1, #assignments do
+ local found
+ local name = assignments[i][1]
+ local tag
+ if #name <= 4 then
+ tag = hb.Tag.new(name)
+ end
+ name = string.lower(name)
+ for j = 1, #axes do
+ local axis = axes[j]
+ if tag and tag == axis.tag then
+ found = tag
+ break
+ end
+ if name == hbface:get_name(axis.name_id):lower() then
+ found = axis.tag
+ if not tag then break end
+ end
+ end
+ if found then
+ assignments[i] = hb.Variation.new(tostring(found) .. '=' .. assignments[i][2])
+ else
+ texio.write_nl'Warning (luaotfload): Unknown axis name ignored.'
+ assignments[i] = hb.Variation.new'XXXX=0'
+ end
+ end
+ normalized = {hbface:ot_var_normalize_variations(table.unpack(assignments))}
+ elseif instance then
+ instance = instance:lower()
+ local instances = hbface:ot_var_named_instance_get_infos()
+ for i = 1, #instances do
+ local inst = instances[i]
+ if instance == hbface:get_name(inst.subfamily_name_id):lower() then
+ normalized = {hbface:ot_var_normalize_coords(hbface:ot_var_named_instance_get_design_coords(inst.index))}
+ break
+ end
+ end
+ if not normalized then
+ texio.write_nl'Warning (luaotfload): Unknown instance name ignored.'
+ end
+ end
+ if not normalized then
+ normalized = {hbface:ot_var_normalize_variations()}
+ end
+ varkey = ':' .. table.concat(normalized, ':')
+ key = key .. varkey
+ else
+ varkey = ''
+ end
+
local cached = containers.read(fontcache, key)
local iscached = cached and cached.date == date and cached.size == size
@@ -97,6 +167,9 @@ local function loadfont(spec)
-- HarfBuzz can handle.
if not tags then return end
local hbfont = iscached and cached.font or hb.Font.new(hbface)
+ if normalized then
+ hbfont:set_var_coords_normalized(table.unpack(normalized))
+ end
if not iscached then
local upem = hbface:get_upem()
@@ -229,9 +302,10 @@ local function loadfont(spec)
nominals = nominals,
unicodes = characters,
psname = hbface:get_name(hb.ot.NAME_ID_POSTSCRIPT_NAME),
- fullname = hbface:get_name(hb.ot.NAME_ID_FULL_NAME),
+ fullname = hbface:get_name(hb.ot.NAME_ID_FULL_NAME) .. varkey,
haspng = hbface:ot_color_has_png(),
loaded = {}, -- Cached loaded glyph data.
+ normalized = normalized,
}
containers.write(fontcache, key, cached)
@@ -354,6 +428,7 @@ local function scalefont(data, spec)
resources = {
unicodes = data.name_to_char,
},
+ streamprovider = data.normalized and 1 or nil,
}
tfmdata.shared.processes = fonts.handlers.otf.setfeatures(tfmdata, features)
fonts.constructors.applymanipulators("otf", tfmdata, features, false)
@@ -414,3 +489,21 @@ luatexbase.add_to_callback('find_truetype_file', function(name)
return find_file(name, 'truetype fonts')
or name:gsub('^harfloaded:', '')
end, 'luaotfload.harf.strip_prefix')
+
+local glyph_stream_data
+local cb = luatexbase.remove_from_callback('glyph_stream_provider', 'luaotfload.glyph_stream')
+luatexbase.add_to_callback('glyph_stream_provider', function(fid, cid, kind)
+ if cid == 0 then -- Always the first call for a font
+ glyph_stream_data = nil
+ collectgarbage()
+ local fontdir = font.getfont(fid)
+ if fontdir and fontdir.hb then
+ glyph_stream_data = cff2_handler(fontdir.hb.shared.face, fontdir.hb.shared.font)
+ end
+ end
+ if glyph_stream_data then
+ return glyph_stream_data(cid)
+ else
+ return cb(fid, cid, kind)
+ end
+end, 'luaotfload.harf.glyphstream')
diff --git a/src/luaotfload-harf-var-cff2.lua b/src/luaotfload-harf-var-cff2.lua
new file mode 100644
index 0000000..72b7772
--- /dev/null
+++ b/src/luaotfload-harf-var-cff2.lua
@@ -0,0 +1,442 @@
+-----------------------------------------------------------------------
+-- FILE: luaotfload-harf-var-cff2.lua
+-- DESCRIPTION: part of luaotfload / HarfBuzz / Parse and convert CFF2 tables
+-----------------------------------------------------------------------
+do
+ assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
+ name = "luaotfload-harf-var-cff2",
+ version = "3.19-dev", --TAGVERSION
+ date = "2021-05-21", --TAGDATE
+ description = "luaotfload submodule / CFF2 table processing",
+ license = "GPL v2.0",
+ author = "Marcel Krüger",
+ copyright = "Luaotfload Development Team",
+ }
+end
+
+local hb = require'luaharfbuzz'
+local cff2 = hb.Tag.new'CFF2'
+local serialize = require'luaotfload-harf-var-t2-writer'
+
+local offsetfmt = ">I%i"
+local function parse_index(buf, i)
+ local count, offsize
+ count, offsize, i = string.unpack(">I4B", buf, i)
+ if count == 0 then return {}, i-1 end
+ local fmt = offsetfmt:format(offsize)
+ local offsets = {}
+ local dataoffset = i + offsize*count - 1
+ for j=1,count+1 do
+ offsets[j], i = string.unpack(fmt, buf, i)
+ end
+ for j=1,count+1 do
+ offsets[j] = offsets[j] + i - 1
+ end
+ return offsets, offsets[#offsets]
+end
+
+local real_mapping = { [0] = '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ '.', 'E', 'E-', nil, '-', nil}
+local function parse_real(cs, offset)
+ local c = cs:byte(offset)
+ if not c then return offset end
+ local c1, c2 = real_mapping[c>>4], real_mapping[c&0xF]
+ if not c1 or not c2 then
+ return c1 or offset, c1 and offset
+ else
+ return c1, c2, parse_real(cs, offset+1) --Warning: This is not a tail-call,
+ -- so we are affected by the stack limit. On the other hand, as long as
+ -- there are less than ~50 bytes we should be safe.
+ end
+end
+
+local function get_number(result)
+ if #result ~= 1 then
+ print(require'inspect'(result))
+ end
+ assert(#result == 1)
+ local num = result[1]
+ result[1] = nil
+ return num
+end
+
+local function get_bool(result)
+ return get_number(result) == 1
+end
+
+local function get_array(result)
+ local arr = table.move(result, 1, #result, 1, {})
+ for i=1,#result do result[i] = nil end
+ return arr
+end
+
+local function get_delta(result)
+ local arr = get_array(result)
+ local last = 0
+ for i=1,#arr do
+ arr[i] = arr[i]+last
+ last = arr[i]
+ end
+ return arr
+end
+
+local function get_private(result)
+ local arr = get_array(result)
+ assert(#arr == 2)
+ return arr
+end
+
+local function do_blend(result, vstore)
+ if not vstore then
+ error'blend operator only allowed in Private disctionary of variable fonts'
+ end
+ local vsindex = (result.vsindex or 0) + 1
+ local factors = vstore[vsindex]
+ local n = result[#result]
+ local k = #factors
+ local before = #result - 1 - n*(k+1)
+ for i = 1, n do
+ local val = result[before + i]
+ for j = 1, k do
+ val = val + factors[j] * result[before + n + (i-1) * k + j]
+ end
+ result[before + i] = math.floor(val + .5)
+ end
+ for i = before + n + 1, #result do
+ result[i] = nil
+ end
+ return arr
+end
+
+local function apply_matrix(m, x, y)
+ return (m[1] * x + m[3] * y + m[5])*1000, (m[2] * x + m[4] * y + m[6])*1000
+end
+
+local operators = {
+ [6] = {'BlueValues', get_delta},
+ [7] = {'OtherBlues', get_delta},
+ [8] = {'FamilyBlues', get_delta},
+ [9] = {'FamilyOtherBlues', get_delta},
+ [10] = {'StdHW', get_number},
+ [11] = {'StdVW', get_number},
+ [17] = {'CharStrings', get_number},
+ [18] = {'Private', get_private},
+ [19] = {'Subrs', get_number},
+ [22] = {'vsindex', get_number},
+ [23] = {'blend', do_blend},
+ [24] = {'vstore', get_number},
+ [-8] = {'FontMatrix', get_array},
+[-10] = {'BlueScale', get_number},
+[-11] = {'BlueShift', get_number},
+[-12] = {'BlueFuzz', get_number},
+[-13] = {'StemSnapH', get_delta},
+[-14] = {'StemSnapV', get_delta},
+[-15] = {'ForceBold', get_bool}, -- ???
+[-18] = {'LanguageGroup', get_number},
+[-19] = {'ExpansionFactor', get_number},
+[-20] = {'initialRandomSeed', get_number}, -- ???
+[-37] = {'FDArray', get_number},
+[-38] = {'FDSelect', get_number},
+}
+local function parse_dict(buf, i, j, vstore)
+ result = {}
+ while i<=j do
+ local cmd = buf:byte(i)
+ if cmd == 29 then
+ result[#result+1] = string.unpack(">i4", buf:sub(i+1, i+4))
+ i = i+4
+ elseif cmd == 28 then
+ result[#result+1] = string.unpack(">i2", buf:sub(i+1, i+2))
+ i = i+2
+ elseif cmd >= 251 then -- Actually "and cmd ~= 255", but 255 is reserved
+ result[#result+1] = -((cmd-251)*256)-string.byte(buf, i+1)-108
+ i = i+1
+ elseif cmd >= 247 then
+ result[#result+1] = (cmd-247)*256+string.byte(buf, i+1)+108
+ i = i+1
+ elseif cmd >= 32 then
+ result[#result+1] = cmd-139
+ elseif cmd == 30 then -- 31 is reserved again
+ local real = {parse_real(buf, i+1)}
+ i = real[#real]
+ real[#real] = nil
+ result[#result+1] = tonumber(table.concat(real))
+ else
+ if cmd == 12 then
+ i = i+1
+ cmd = -buf:byte(i)-1
+ end
+ local op = operators[cmd]
+ if not op then error[[Unknown CFF operator]] end
+ result[op[1]] = op[2](result, vstore)
+ end
+ i = i+1
+ end
+ return result
+end
+
+local function parse_charstring(buf, start, after, globalsubrs, subrs, result)
+ local lastresult = result[#result]
+ while start ~= after do
+ local cmd = buf:byte(start)
+ if cmd == 28 then
+ lastresult[#lastresult+1] = string.unpack(">i2", buf:sub(start+1, start+2))
+ start = start+2
+ elseif cmd == 255 then
+ lastresult[#lastresult+1] = string.unpack(">i4", buf:sub(start+1, start+4))/0x10000
+ start = start+4
+ elseif cmd >= 251 then
+ lastresult[#lastresult+1] = -((cmd-251)*256)-string.byte(buf, start+1)-108
+ start = start+1
+ elseif cmd >= 247 then
+ lastresult[#lastresult+1] = (cmd-247)*256+string.byte(buf, start+1)+108
+ start = start+1
+ elseif cmd >= 32 then
+ lastresult[#lastresult+1] = cmd-139
+ elseif cmd == 10 then
+ local idx = lastresult[#lastresult]+subrs.bias
+ local sub_start = subrs[idx]
+ local sub_stop = subrs[idx+1]
+ lastresult[#lastresult] = nil
+ parse_charstring(buf, sub_start, sub_stop, globalsubrs, subrs, result)
+ lastresult = result[#result]
+ elseif cmd == 29 then
+ local idx = lastresult[#lastresult]+globalsubrs.bias
+ local sub_start = globalsubrs[idx]
+ local sub_stop = globalsubrs[idx+1]
+ lastresult[#lastresult] = nil
+ parse_charstring(buf, sub_start, sub_stop, globalsubrs, subrs, result)
+ lastresult = result[#result]
+ elseif cmd == 11 then
+ break -- We do not keep subroutines, so drop returns and continue with the outer commands
+ elseif cmd == 15 then -- vsindex
+ assert(#lastresult == 2)
+ result.factors = result.vstore[lastresult[2] + 1]
+ lastresult[2] = nil
+ elseif cmd == 16 then -- blend
+ local factors = result.factors
+ if not factors then
+ error'blend operator outside of variable font or with invalid vsindex'
+ end
+ local n = lastresult[#lastresult]
+ local k = #factors
+ local before = #lastresult - 1 - n*(k+1)
+ for i = 1, n do
+ local val = lastresult[before + i]
+ for j = 1, k do
+ val = val + factors[j] * lastresult[before + n + (i-1) * k + j]
+ end
+ lastresult[before + i] = math.floor(val + .5)
+ end
+ for i = before + n + 1, #lastresult do
+ lastresult[i] = nil
+ end
+ elseif cmd == 12 then
+ start = start+1
+ cmd = buf:byte(start)
+ lastresult[1] = -cmd-1
+ lastresult = {false}
+ result[#result+1] = lastresult
+ elseif cmd == 19 or cmd == 20 then
+ if #result == 1 then
+ lastresult = {}
+ result[#result+1] = lastresult
+ end
+ lastresult[1] = cmd
+ local newi = start+(result.stemcount+7)//8
+ lastresult[2] = buf:sub(start+1, newi)
+ start = newi
+ else
+ if cmd == 21 and #result == 1 then
+ table.insert(result, 1, {false})
+ if #lastresult == 4 then
+ result[1][2] = lastresult[2]
+ table.remove(lastresult, 2)
+ end
+ elseif (cmd == 4 or cmd == 22) and #result == 1 then
+ table.insert(result, 1, {false})
+ if #lastresult == 3 then
+ result[1][2] = lastresult[2]
+ table.remove(lastresult, 2)
+ end
+ elseif cmd == 14 and #result == 1 then
+ table.insert(result, 1, {false})
+ if #lastresult == 2 or #lastresult == 6 then
+ result[1][2] = lastresult[2]
+ table.remove(lastresult, 2)
+ end
+ elseif cmd == 1 or cmd == 3 or cmd == 18 or cmd == 23 then
+ if #result == 1 then
+ table.insert(result, 1, {false})
+ if #lastresult % 2 == 0 then
+ result[1][2] = lastresult[2]
+ table.remove(lastresult, 2)
+ end
+ end
+ result.stemcount = result.stemcount + #lastresult//2
+ end
+ lastresult[1] = cmd
+ lastresult = {false}
+ result[#result+1] = lastresult
+ end
+ start = start+1
+ end
+ return result
+end
+
+local function parse_fdselect(buf, offset, CharStrings)
+ local format
+ format, offset = string.unpack(">B", buf, offset)
+ if format == 0 then
+ for i=0,#CharStrings-1 do
+ local code
+ code, offset = string.unpack(">B", buf, offset)
+ CharStrings[i][3] = code + 1
+ end -- Reimplement with string.byte
+ elseif format == 3 then
+ local count, last
+ count, offset = string.unpack(">I2", buf, offset)
+ for i=1,count do
+ local first, code, after = string.unpack(">I2BI2", buf, offset)
+ for j=first, after-1 do
+ CharStrings[j][3] = code + 1
+ end
+ offset = offset + 3
+ end
+ elseif format == 4 then
+ local count, last
+ count, offset = string.unpack(">I4", buf, offset)
+ for i=1,count do
+ local first, code, after = string.unpack(">I4I2I4", buf, offset)
+ for j=first, after-1 do
+ CharStrings[j][3] = code + 1
+ end
+ offset = offset + 6
+ end
+ else
+ error[[Invalid FDSelect format]]
+ end
+end
+
+local function parse_vstore(buf, offset, variation)
+ local size, format, region_list_off, item_variation_count, off = string.unpack(">I2I2I4I2", buf, offset)
+ if format ~= 1 then
+ error'Unsupported vstore format'
+ end
+ offset = offset + 2 -- Skip the size
+ region_list_off = offset + region_list_off
+
+ local axis_count, region_count
+ axis_count, region_count, region_list_off = string.unpack(">I2I2", buf, region_list_off)
+
+ local variation_regions = {}
+ for i = 1, region_count do
+ local factor = 1
+ for j = 1, axis_count do
+ local start, peak, stop
+ start, peak, stop, region_list_off = string.unpack(">i2i2i2", buf, region_list_off)
+ local coord = variation[j]
+ if peak == 0 then -- Skip
+ elseif peak == coord then
+ -- factor = factor * 1
+ elseif coord <= start or coord >= stop then
+ factor = 0
+ break
+ elseif coord < peak then
+ factor = factor * ((coord-start) / (peak-start))
+ else--if coord > peak then
+ factor = factor * ((stop-coord) / (stop-peak))
+ end
+ end
+ variation_regions[i] = factor
+ end
+
+ local variation_data = {}
+ for i = 1, item_variation_count do
+ local item_off
+ item_off, off = string.unpack(">I4", buf, off)
+ local i_count, short_count, region_count
+ i_count, short_count, region_count, item_off = string.unpack(">I2I2I2", buf, item_off + offset)
+ if i_count ~= 0 or short_count ~= 0 then
+ error'Unexpected variation items in CFF2 table'
+ end
+ local factors = {}
+ for j = 1, region_count do
+ local region
+ region, item_off = string.unpack(">I2", buf, item_off)
+ factors[j] = variation_regions[region+1]
+ end
+ variation_data[i] = factors
+ end
+ return variation_data
+end
+
+function parse_cff2(buf, i0, coords)
+ local fontid = 1
+ local major, minor, hdrSize, topSize = string.unpack(">BBBH", buf, i0)
+ if major ~= 2 then error[[Unsupported CFF version]] end
+ local i = i0 + hdrSize
+ local top = parse_dict(buf, i, i + topSize - 1)
+ i = i + topSize
+ local globalsubrs
+ globalsubrs, i = parse_index(buf, i)
+ globalsubrs.bias = #globalsubrs-1 < 1240 and 108 or #globalsubrs-1 < 33900 and 1132 or 32769
+ top.GlobalSubrs = globalsubrs
+ local CharStrings = parse_index(buf, i0+top.CharStrings)
+ for i=1,#CharStrings-1 do
+ CharStrings[i-1] = {CharStrings[i], CharStrings[i+1]-1}
+ end
+ CharStrings[#CharStrings] = nil
+ CharStrings[#CharStrings] = nil
+ local fonts = parse_index(buf, i0+top.FDArray)
+ top.FDArray = nil
+ top.vstore = parse_vstore(buf, i0 + top.vstore, coords)
+ local privates = {}
+ top.Privates = privates
+ for i=1,#fonts-1 do
+ local font = fonts[i]
+ local fontdir = parse_dict(buf, fonts[i], fonts[i+1]-1)
+ privates[i] = parse_dict(buf, i0+fontdir.Private[2], i0+fontdir.Private[2]+fontdir.Private[1]-1, top.vstore)
+ local subrs = privates[i].Subrs
+ if subrs then
+ subrs = parse_index(buf, i0+fontdir.Private[2]+subrs)
+ subrs.bias = #subrs-1 < 1240 and 108 or #subrs-1 < 33900 and 1132 or 32769
+ privates[i].Subrs = subrs
+ end
+ end
+ if top.FDSelect then
+ parse_fdselect(buf, i0+top.FDSelect, CharStrings)
+ else
+ for i=0,#CharStrings-1 do
+ CharStrings[i][3] = 1
+ end
+ end
+ top.CharStrings = CharStrings
+ local bbox
+ if top.FontMatrix then
+ local x0, y0 = apply_matrix(top.FontMatrix, top.FontBBox[1], top.FontBBox[2])
+ local x1, y1 = apply_matrix(top.FontMatrix, top.FontBBox[3], top.FontBBox[4])
+ bbox = {x0, y0, x1, y1}
+ else
+ bbox = top.FontBBox
+ end
+ return top, bbox
+end
+
+local function parse_glyph(buffer, top, gid)
+ local cs = top.CharStrings[gid]
+ local Private = top.Privates[cs[3]]
+ return parse_charstring(buffer, cs[1], cs[2] + 1,
+ top.GlobalSubrs, Private.Subrs,
+ {{false}, stemcount = 0, vstore = top.vstore, factors = top.vstore and top.vstore[(Private.vsindex or 0) + 1]})
+end
+
+return function(face, font)
+ local data = face:get_table(cff2):get_data()
+ local content = parse_cff2(data, 1, {font:get_var_coords_normalized()})
+ return function(gid)
+ local glyph = parse_glyph(data, content, gid)
+ glyph[1][2] = font:get_glyph_h_advance(gid)
+ return serialize(glyph)
+ end
+end
diff --git a/src/luaotfload-harf-var-t2-writer.lua b/src/luaotfload-harf-var-t2-writer.lua
new file mode 100644
index 0000000..a002efe
--- /dev/null
+++ b/src/luaotfload-harf-var-t2-writer.lua
@@ -0,0 +1,62 @@
+-----------------------------------------------------------------------
+-- FILE: luaotfload-harf-var-t2-writer.lua
+-- DESCRIPTION: part of luaotfload / HarfBuzz / Serialize Type 2 charstrings
+-----------------------------------------------------------------------
+do
+ assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
+ name = "luaotfload-harf-var-t2-writer",
+ version = "3.19-dev", --TAGVERSION
+ date = "2021-05-21", --TAGDATE
+ description = "luaotfload submodule / Type 2 charstring writer",
+ license = "GPL v2.0",
+ author = "Marcel Krüger",
+ copyright = "Luaotfload Development Team",
+ }
+end
+
+local pack = string.pack
+local function numbertot2(n)
+ if math.abs(n) > 2^15 then
+ error[[Number too big]]
+ end
+ local num = math.floor(n + .5)
+ if n ~= 0 and math.abs((num-n)/n) > 0.001 then
+ num = math.floor(n * 2^16 + 0.5)
+ return pack(">Bi4", 255, math.floor(n * 2^16 + 0.5))
+ elseif num >= -107 and num <= 107 then
+ return string.char(num + 139)
+ elseif num >= 108 and num <= 1131 then
+ return pack(">I2", num+0xF694) -- -108+(247*0x100)
+ elseif num >= -1131 and num <= -108 then
+ return pack(">I2", -num+0xFA94) -- -108+(251*0x100)
+ else
+ return pack(">Bi2", 28, num)
+ end
+end
+local function convert_cs(cs, upem)
+ local cs_parts = {}
+ local function add(cmd, first, ...)
+ if cmd == 19 or cmd == 20 then
+ cs_parts[#cs_parts+1] = string.char(cmd)
+ cs_parts[#cs_parts+1] = first
+ return
+ end
+ if first then
+ cs_parts[#cs_parts+1] = numbertot2(first*upem/1000)
+ return add(cmd, ...)
+ end
+ if cmd then
+ if cmd < 0 then
+ cs_parts[#cs_parts+1] = string.char(12, -cmd-1)
+ else
+ cs_parts[#cs_parts+1] = string.char(cmd)
+ end
+ end
+ end
+ for _, args in ipairs(cs) do if args then add(table.unpack(args)) end end
+ return table.concat(cs_parts)
+end
+
+return function(cs, upem)
+ return convert_cs(cs, upem or 1000)
+end
More information about the latex3-commits
mailing list.