local concat, format, sort, type = table.concat, string.format, table.sort, type local setmetatable = setmetatable local pdf_escape = require'luatagging-pdf-escape' local type PdfObject = boolean | number | string | Name | Dict | Array | Ref | Raw local type Name = {string} local interface Ref is {integer} lazy: function(Ref): integer end local type Array = {PdfObject} local type Dict = {(string | RawKey): PdfObject} local type Raw = {string} local type RawKey = function(string): Raw local name_metatable: metatable = {} local dict_metatable: metatable = {} local array_metatable: metatable = {} local ref_metatable: metatable = {} local raw_metatable: metatable = {} local raw: RawKey = function(literal: string): Raw return setmetatable({literal}, raw_metatable) end local names: {string: Name} = setmetatable({}, {__index = function(t: {string: Name}, name: string): Name if name == nil then return nil end local name_dict: Name = setmetatable({name, pdf_escape.escape_name(name)}, name_metatable) t[name] = name_dict return name_dict end}) local append_obj: function({string}, integer, PdfObject): integer local function append_dict_content(existing: {string}, i: integer, obj: Dict): integer local keys, length: {string}, integer = {}, 0 for k in next, obj do if k ~= raw then length = length + 1 keys[length] = k as string end end sort(keys) for j = 1, length do i = i + 1 local key = keys[j] existing[i] = names[key][2] i = append_obj(existing, i, obj[key]) end local raw_content = obj[raw] if raw_content then i = i + 1 existing[i] = raw_content as string end return i end function append_obj(existing: {string}, i: integer, obj: PdfObject): integer local t = type(obj) local result: string if t == nil then elseif t == 'boolean' then result = obj as boolean and ' true' or ' false' elseif t == 'number' then if math.type(obj) == 'integer' then -- if obj is integer then result = format(' %i', obj as integer) else result = format(' %f', obj as number) -- TODO: Proper PDF syntax end elseif t == 'string' then result = pdf_escape.escape_text(obj as string) elseif t == 'table' then local mt = getmetatable(obj) as any if mt == name_metatable then result = (obj as Name)[2] elseif mt == dict_metatable then i = i + 1 existing[i] = '<<' i = append_dict_content(existing, i, obj as Dict) i = i + 1 existing[i] = '>>' return i elseif mt == array_metatable then local arr = obj as Array i = i + 1 existing[i] = '[' for j = 1, #arr do i = append_obj(existing, i, arr[j]) end i = i + 1 existing[i] = ']' return i elseif mt == ref_metatable then local ref = obj as Ref local lazy = ref.lazy local id: integer if lazy then id = lazy(ref) else id = ref[1] end result = format(' %i 0 R', id) elseif mt == raw_metatable then i = i + 1 existing[i] = ' ' result = (obj as Raw)[1] else print(require'inspect'(obj)) error'unknown table' end elseif t == 'function' then return error'unknown function' elseif obj == nil then i = i + 1 existing[i] = ' null' else return error'unknown object' end i = i + 1 existing[i] = result return i end local function to_pdf_string(data: PdfObject): string local existing = {} local i = append_obj(existing, 0, data) return concat(existing, '', 1, i) end local function to_pdf_dict_content(data: Dict): string local existing = {} local i = append_dict_content(existing, 0, data) return concat(existing, '', 1, i) end local record Objectwriter type PdfObject = PdfObject type Ref = Ref type Array = Array type Name = Name type Dict = Dict to_pdf_string: function(data: PdfObject): string to_pdf_dict_content: function(data: Dict): string names: {string: Name} dict: function(d: {(string | RawKey): PdfObject}): Dict array: function(d: {PdfObject}): Array ref: function(id: integer): Ref lazy: function(f: function(Ref): integer): Ref raw: RawKey end local objectwriter: Objectwriter = { to_pdf_string = to_pdf_string, to_pdf_dict_content = to_pdf_dict_content, names = names, dict = function(d: {(string | RawKey): PdfObject}): Dict return setmetatable(d, dict_metatable) end, array = function(d: {PdfObject}): Array return setmetatable(d, array_metatable) end, ref = function(id: integer): Ref return setmetatable({id}, ref_metatable) end, lazy = function(f: function(Ref): integer): Ref return setmetatable({lazy = f}, ref_metatable) end, raw = raw, } return objectwriter