local write_pdf = require'luatagging-objectwriter' local structelem = require'luatagging-structelem' local marked_content = require'luatagging-marked-content' local rolemaps = require'luatagging-rolemaps' local pdf_escape = require'luatagging-pdf-escape' local labels = require'luatagging-labels' local timing = require'luatagging-timing'.luamml_write_structure local names = write_pdf.names local mathml_namespace = rolemaps.namespace_lookup.mathml -- Create tokens from the main tagpdf commands. -- local struct_begin = token.create'tag_struct_begin:n' -- local struct_use = token.create'tag_struct_use:n' -- local struct_use_num = token.create'tag_struct_use_num:n' -- local struct_end = token.create'tag_struct_end:' -- local struct_prop_gput = token.create'__tag_struct_prop_gput:nnn' -- local mc_begin = token.create'tag_mc_begin:n' -- local mc_end = token.create'tag_mc_end:' -- local tagpdfsetup = token.create'tagpdfsetup' local lbrace, rbrace = token.new(string.byte'{', 1), token.new(string.byte'}', 2) local math_char_t = node.id'math_char' local escape_name = pdf_escape.escape_name local escape_string = pdf_escape.escape_text local bytes_to_luatex_string = pdf_escape.bytes_to_luatex_string local ltx local function get_ltx() ltx = _ENV.ltx if not ltx then tex.error("LaTeX PDF support not loaded", {"Maybe try adding \\DocumentMetadata."}) ltx = {pdf = {object_id = function() return 0 end}, __tag = {tables = {}}} end function get_ltx() return ltx end return get_ltx() end -- a function to retrieve the object number of the mathml NS. local mathml_ns_obj local function get_mathml_ns_obj() mathml_ns_obj = get_ltx().pdf.object_id'tag/NS/mathml' if not mathml_ns_obj then tex.error("Failed to find MathML namespace", {"The PDF support does not know the mathml namespace"}) mathml_ns_obj = 0 end function get_mathml_ns_obj() return mathml_ns_obj end return get_mathml_ns_obj() end local tag_tables local function get_tag_tables() tag_tables = assert(get_ltx().__tag.tables) function get_tag_tables() return tag_tables end return get_tag_tables() end local function get_struct_num_next() get_struct_num_next = get_ltx().tag.get_struct_num_next return get_struct_num_next() end -- a function to create and manage PDF MathML attributes. This is PDF 2.0 specific local attribute_counter = 0 local attributes = setmetatable({}, {__index = function(t, k) attribute_counter = attribute_counter + 1 local attr_name = names[string.format('luamml_attr_%i', attribute_counter)] t[k] = attr_name structelem.register_attribute(attr_name, {{data = write_pdf.dict{[write_pdf.raw] = '/O/NSO/NS ' .. get_mathml_ns_obj() .. ' 0 R' .. k}}}) return attr_name end}) local attribute_object_refs = setmetatable({}, {__index = function(t, k) local objref = pdf.immediateobj(string.format('<< /O/NSO/NS %i 0 R%s >>', get_mathml_ns_obj(), k)) .. ' 0 R' t[k] = objref return objref end}) -- the mc-(luatex)-attributes of luatagging local attr = assert(luatexbase.attributes['luatagging.mcc']) local function apply_mc_attributes(nodes) local value = tex.attribute[attr] for i = 1, #nodes do node.set_attribute(nodes[i], attr, value) end end local reused_attributes = {} local function build_attributes(tree_node) local data: write_pdf.Dict local i = 0 for attr, val in next, tree_node do if type(attr) == 'string' and not string.find(attr, ':') and attr ~= 'xmlns' then if not data then data = write_pdf.dict{} end data[attr] = pdf_escape.to_pdf_encoding(val) end end if not data then return end local key = write_pdf.to_pdf_dict_content(data) local attributes = reused_attributes[key] if not attributes then attributes = { ns = mathml_namespace, data = data, } reused_attributes[key] = attributes end return attributes end local function convert_tounicode_to_utf8(touni) if type(touni) ~= 'string' or #touni % 4 ~= 0 then return false end local state local cps, j = {}, 0 for i=1, #touni, 4 do local unit = tonumber(string.sub(touni, i, i + 3), 16) local high = unit & 0xFC00 if high == 0xD800 then if state then return false end state = (unit & 0x3FF) << 10 elseif high == 0xDC00 then if not state then return false end j = j + 1 cps[j], state = 0x10000 + (state | (unit & 0x3FF)), nil else if state then return false end j = j + 1 cps[j] = unit end end return utf8.char(table.unpack(cps, 1, i)) end local tounicode_font_cache = setmetatable({[0] = false}, {__index = function(t, fid) local fontdata = font.getfont(fid) if not fontdata then t[fid] = false return false end local characters = fontdata.characters local result = setmetatable({}, {__index = function(t, cp) local c = characters[cp] local touni = c and c.tounicode local result = false if touni then result = convert_tounicode_to_utf8(touni) or false if not result then print'ERROR: Invalid ToUnicode?' print(touni) end end t[cp] = result return result end}) t[fid] = result return result end}) local tounicode_fam_cache = setmetatable({}, {__index = function(t, mathfont_locator) local fam = mathfont_locator >> 2 local size = mathfont_locator & 3 if size ~= 0 then size = size - 1 end local fid = node.family_font(fam, size) -- Always using textsize. Should be enough for this usecase unless you do something weird. local result = tounicode_font_cache[fid] t[mathfont_locator] = result return result end}) local function try_derive_actual(tree) if tree[':actual'] then return end -- No need to derive actualtext if it's already there if tree[':artifact'] then return end -- No need to provide actual text for artifacts for i=1, #tree do if type(tree[i]) ~= 'string' then return end end local nodes = assert(tree[':nodes']) if #nodes == 0 then return -- Can't be used without nodes since there's no MC to annotate. end local intended_text = table.concat(tree) local derived_text = {} for i=1, #nodes do local n = nodes[i] if n.id ~= math_char_t then derived_text = false break end local fam, cp = n.fam, n.char local size = (tree[':style'] or 0) >> 1 local unicode_mapping = tounicode_fam_cache[(fam << 2) | size] unicode_mapping = unicode_mapping and unicode_mapping[cp] if not unicode_mapping then derived_text = false break end derived_text[i] = unicode_mapping end derived_text = derived_text and table.concat(derived_text) if intended_text ~= derived_text then tree[':actual'] = intended_text end end local stash_cnt = 0 local function write_elem(parent, tree, stash) if tree[':struct'] then local child = labels.get_must_exist('label', tree[':struct']) if not child then return end if child.parent then tex.error"Illegal attempt to attach structure element multiple times." return end return structelem.add_child(parent, child) end local attrs = build_attributes(tree) if tree[':structnum'] then return tex.runtoks(function() local child = labels.get_by_num(tree[':structnum']) if not child then return end if attrs then local data = assert(child.data) local existing_attrs = data.attributes if not existing_attrs then existing_attrs = {} data.attributes = existing_attrs end existing_attrs[#existing_attrs + 1] = attrs end return structelem.add_child(parent, child) end) end if not tree[0] then print('ERR', require'inspect'(tree)) end local struct = structelem.create_structelem( --[[tag = ]] tree[0], --[[namespace = ]] mathml_namespace, --[[parent = ]] not stash and (parent or true), --[[first_child = ]] false ) if stash then tree[':structnum'] = labels.assign_num(struct) end if attrs then struct.data.attributes = {attrs} end if tree[':nodes'] then local n = tree[':nodes'] try_derive_actual(tree) -- luamml-convert sets :actual on some nodes in delim_to_table and acc_to_table. if tree[':artifact'] then marked_content.artifact:activate() else marked_content.new(struct, nil):activate() if tree[':actual'] then -- FIXME: Set actualtext on MC to tree[':actual'] end end apply_mc_attributes(n) end for _, elem in ipairs(tree) do if type(elem) ~= 'string' then write_elem(struct, elem) end end structelem.pop_structelem() end return { write = timing.wrap(function(element, stash) return write_elem(structelem.current(), element, stash) end), -- TODO: This matches \ActivateMc. Consider delegating instead. restore_after_math = function() local current = structelem.current() if current is StructureElement then marked_content.new(current, nil):activate() else tex.error"Can not start MC segment outside any structure" end end, }