[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.