-- 
--  This is file `newpax.lua',
--  generated with the docstrip utility.
-- 
--  The original source files were:
-- 
--  newpax.dtx  (with options: `lua')
--  
--  Copyright (C) 2021-2025 Ulrike Fischer
--  
--  It may be distributed and/or modified under the conditions of
--  the LaTeX Project Public License (LPPL), either version 1.3c of
--  this license or (at your option) any later version.  The latest
--  version of this license is in the file:
--  
--     https://www.latex-project.org/lppl.txt
--  
--  This file is part of the "newpax bundle" (The Work in LPPL)
--  and all files in that bundle must be distributed together.
--  
local ProvidesLuaModule = {
    name          = "newpax",
    version       = "0.57",       --TAGVERSION
    date          = "2025-09-24", --TAGDATE
    description   = "newpax lua code",
    license       = "The LATEX Project Public License 1.3c"
}

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

local OPEN              = pdfe.open
local GETSIZE           = pdfe.getsize
local GETINFO           = pdfe.getinfo
local GETPAGE           = pdfe.getpage
local GETNAME           = pdfe.getname
local GETARRAY          = pdfe.getarray
local GETDICTIONARY     = pdfe.getdictionary
local GETFROMDICTIONARY = pdfe.getfromdictionary
local GETFROMARRAY      = pdfe.getfromarray
local PAGESTOTABLE      = pdfe.pagestotable
local DICTIONARYTOTABLE = pdfe.dictionarytotable
local ARRAYTOTABLE      = pdfe.arraytotable
local TYPE              = pdfe.type
local GETFROMREFERENCE  = pdfe.getfromreference
local FILENAMEONLY      = file.nameonly

local strENTRY_BEG = "\\["
local strENTRY_END = "\\\\\n"
local strCMD_BEG = "{"
local strCMD_END = "}"
local strARG_BEG = "{"
local strARG_END = "}"
local strKVS_BEG = "{"
local strKVS_END = "\n}"
local strKVS_EMPTY = "{}"
local strKV_BEG  = "\n  "
local strKV_END  = ","
local strKEY_BEG = ""
local strKEY_END = ""
local strVALUE_BEG = "={"
local strVALUE_END = "}"
local strHEX_STR_BEG = "\\<"
local strHEX_STR_END = "\\>"

local strLIT_STR_BEG = "("
local strLIT_STR_END = ")"
local strDICT_BEG= "<<"
local strDICT_END= ">>"
local strNAME= "/"
local strARRAY_BEG= "["
local strARRAY_END= "]"
local strARRAY_SEP = " "
local strRECT_SEP = " "

local constCMD_ANNOT   = "annot"
local constCMD_BASEURL = "baseurl" -- unused
local constCMD_DEST    = "dest"
local constCMD_FILE    = "file"
local constCMD_INFO    = "info"   -- unused
local constCMD_PAGENUM = "pagenum"
local constCMD_PAGE    = "page"
local constCMD_PAX     = "pax"

-- cmd file
local constKEY_SIZE    = "Size"
local constKEY_DATE    = "Date"

-- cmd annot attributes
local constKEY_C      = "C"
local constKEY_BORDER = "Border"
local constKEY_BS     = "BS"
local constKEY_H      = "H"

-- cmd annot/link/URI
local constKEY_URI    = "URI"
local constKEY_IS_MAP = "IsMap" -- not handled by pax, consider later

-- cmd annot/link/GoToR
local constKEY_FILE      = "File"
local constKEY_DEST_NAME = "DestName" -- handle??
local constKEY_DEST_PAGE = "DestPage" --ok
local constKEY_DEST_VIEW = "DestView" --ok

-- cmd annot/link/GoTo
local constKEY_DEST_RECT  = "DestRect"
local constKEY_DEST_X     = "DestX"
local constKEY_DEST_Y     = "DestY"
local constKEY_DEST_ZOOM  = "DestZoom"
local constKEY_DEST_LABEL = "DestLabel" --ok

-- cmd annot/link/Named
local constKEY_NAME = "Name"

-- destination views
local constDEST_XYZ = "XYZ"
local constDEST_FIT    = "Fit"
local constDEST_FITH   = "FitH"
local constDEST_FITV   = "FitV"
local constDEST_FITR   = "FitR"
local constDEST_FITB   = "FitB"
local constDEST_FITBH  = "FitBH"
local constDEST_FITBV  = "FitBV"

-- get/build data
-- returns table,pagecount where table objref ->page number
local function getpagesdata (pdfedoc)
  local type,pagecount,detail   = GETFROMDICTIONARY (pdfedoc.Catalog.Pages,"Count")
  local pagestable  = PAGESTOTABLE (pdfedoc)
  local pagereftonum ={}
  for i=1,#pagestable do
   pagereftonum[pagestable[i][3]]=i
  end
  return pagereftonum, pagecount
end

-- builds a table destination name -> obj reference (pdfedict) from the Names tree

local function processnamesarray (pdfearray,targettable)
  local tkey={}
  for i=1,#pdfearray do
    local type,value,detail= GETFROMARRAY(pdfearray,i)
    if (i % 2 == 1) then
     tkey = value
    else
      tvalue = value
      targettable[tkey]=tvalue
    end
  end
end

local function findallnamesarrays (pdfedict,deststable)
  local namesarray  = GETARRAY(pdfedict,"Names")
  if namesarray then
     processnamesarray (namesarray,deststable)
  else
     local kidsarray = GETARRAY(pdfedict,"Kids")
     if kidsarray then
       for i=1,#kidsarray do
         findallnamesarrays (kidsarray[i],deststable)
       end
     end
  end
end

-- returns destnames -> objref table
local function getdestreferences (pdfedoc)
  local deststable= {}
  local catnames  = GETDICTIONARY (pdfedoc.Catalog, "Names")
  if catnames then
    local destsdict = GETDICTIONARY (catnames, "Dests")
    if destsdict then
      findallnamesarrays (destsdict,deststable)
    end
  end
  return deststable
end

-- destinations can be an dict (/D [array]) or only an array!

local function getdestdata (name,pagereftonum,destnamestoref)
   local destref
   local type,ref,pagenum,destx,desty = nil, nil, 1,0,0
   local data = {{0,0}, {5,constDEST_XYZ}}
   -- if we get directly an array
   if (TYPE(name) == "pdfe.array") then
     data = ARRAYTOTABLE(name)
     type, ref, pageref = GETFROMARRAY(name,1)
     pagenum = pagereftonum[pageref]
   else
    destref = destnamestoref[name]
   end
   if destref then
     type,value,detail = GETFROMREFERENCE(destref)
     if TYPE(value) == "pdfe.dictionary" then
       local destarray = GETARRAY(value,"D")
       if destarray then
         data = ARRAYTOTABLE(destarray)
         type, ref, pageref = GETFROMARRAY(destarray,1)
         pagenum = pagereftonum[pageref]
       end
     elseif TYPE(value) == "pdfe.array" then
        data = ARRAYTOTABLE(value)
        type, ref, pageref = GETFROMARRAY(value,1)
        pagenum = pagereftonum[pageref]
     end
   end
  -- print("XXXXXXXXXX",table.serialize(data),pagenum)
  return pagenum, data
end

-- output functions
-- write the info
-- XXXXXX encode/escape the file name?
local function outputENTRY_file (file, pdfedoc)
  local bytes       = GETSIZE(pdfedoc)
  local date        = (GETINFO(pdfedoc) and GETINFO(pdfedoc).CreationDate) or  "D:22222222222222"
  -- file
  local a = strENTRY_BEG
  a = a .. strCMD_BEG .. constCMD_FILE .. strCMD_END
  a = a .. strARG_BEG .. strLIT_STR_BEG .. file .. strLIT_STR_END ..  strARG_END
  a = a .. strKVS_BEG
   a = a .. strKV_BEG .. constKEY_SIZE .. strVALUE_BEG .. bytes .. strVALUE_END .. strKV_END
   a = a .. strKV_BEG .. constKEY_DATE .. strVALUE_BEG .. date .. strVALUE_END.. strKV_END
  a = a .. strKVS_END
  a = a .. strENTRY_END
  return a
end

local function outputENTRY_pagenum (pages)
  local a = strENTRY_BEG
  a = a .. strCMD_BEG .. constCMD_PAGENUM .. strCMD_END
  a = a .. strARG_BEG .. pages .. strARG_END
  a = a .. strENTRY_END
  return a
end

local function outputENTRY_page (pdfedoc,page) -- page=integer
  -- trimbox, bleedbox, cropbox,artbox,rotate etc could be put in
  -- the second argument as keyval:
  -- TrimBox={0 0 300 350},
  -- but pax skips the argument anyway, so not really useful
  local mediabox = pdfe.getbox(GETPAGE(pdfedoc,page),"MediaBox")
  local a=""
  if mediabox then
    a = strENTRY_BEG
    a = a .. strCMD_BEG .. constCMD_PAGE .. strCMD_END
    a = a .. strARG_BEG .. page .. strARG_END
    a = a .. strARG_BEG
      a = a .. mediabox[1]
      for j = 2, 4 do
       a = a .. strRECT_SEP.. mediabox[j]
      end
    a = a .. strARG_END
    a = a .. strKVS_EMPTY
    a = a .. strENTRY_END
  end
  return a
end

local function outputCMD_annot (pdfedict,page,type)
  local rectangle = ARRAYTOTABLE(GETARRAY(pdfedict,"Rect"))
  local a = strCMD_BEG .. constCMD_ANNOT .. strCMD_END
      a = a .. strARG_BEG .. page .. strARG_END
      a = a .. strARG_BEG .. GETNAME(pdfedict,"Subtype") .. strARG_END
      a = a .. strARG_BEG
       a = a .. rectangle[1][2]
        for k = 2, 4 do
         a = a.. strRECT_SEP..rectangle[k][2]
        end
      a = a .. strARG_END
      a = a .. strARG_BEG .. type .. strARG_END
  return a
end

local function outputKV_color (pdfedict)
  local color = GETARRAY(pdfedict,constKEY_C)
  local a =""
  if color then
    local colortable = ARRAYTOTABLE(color)
    a = strKV_BEG .. constKEY_C .. strVALUE_BEG .. strARRAY_BEG
      for i=1,#colortable do
        a = a.. colortable[i][2] .. strARRAY_SEP
      end
    a = a .. strARRAY_END .. strVALUE_END .. strKV_END
  end
  return a
end

local function outputKV_key (pdfedict,key)
  local name = GETNAME(pdfedict,key)
  local a = ""
  if name then
    a = strKV_BEG .. key .. strVALUE_BEG .. strNAME .. name .. strVALUE_END .. strKV_END
  end
  return a
end

-- XXX handle fourth argument
local function outputKV_Border (pdfedict)
  local border = GETARRAY(pdfedict,constKEY_BORDER)
  local a =""
  if border then
    local bordertable = ARRAYTOTABLE(border)
    a = strKV_BEG .. constKEY_BORDER .. strVALUE_BEG .. strARRAY_BEG
    for i=1,3 do
      a = a .. bordertable[i][2] .. strARRAY_SEP
    end
 -- fourth argument later, it is an array (type 7)
   a = a ..strARRAY_END .. strVALUE_END .. strKV_END
  end
  return a
end

local function outputKV_BS (pdfedict)
  local bsstyle = GETDICTIONARY(pdfedict,constKEY_BS)
  local a =""
  if bsstyle then
    local bsstyledict = DICTIONARYTOTABLE(bsstyle)
    a = strKV_BEG .. constKEY_BS .. strVALUE_BEG ..strDICT_BEG
    for k,v in pairs (bsstyledict) do
      a = a .. strNAME.. k
      if v[1]== 5 then
       a = a .. strNAME .. v[2]
      else
       a = a ..strRECT_SEP .. v[2]
      end
    end
    a = a .. strDICT_END .. strVALUE_END .. strKV_END
  end
  return a
end

local function outputKV_uri (pdfedict)
  local type, value, hex = GETFROMDICTIONARY(pdfedict,constKEY_URI)
  if TYPE(value) == "pdfe.reference" then
   type,value,hex  = GETFROMREFERENCE(value)
  end
  local a= strKV_BEG .. constKEY_URI .. strVALUE_BEG
  if hex then
    a = a .. strHEX_STR_BEG .. value .. strHEX_STR_END
  else
    a = a .. strLIT_STR_BEG ..value .. strLIT_STR_END
  end
    a = a .. strVALUE_END .. strKV_END
  return a
end

local function outputKV_N (pdfedict)
  local name = GETNAME(pdfedict,"N")
  local a= strKV_BEG .. constKEY_NAME .. strVALUE_BEG .. name .. strVALUE_END .. strKV_END
  return a
end

-- if a gotoR has a filespec filespec we use this
-- to output the reference. It is rather crude and handles only names and strings
local function outputDICT (dictionary)
 local tkeys = {}
 local dict = DICTIONARYTOTABLE(dictionary)
 for name,value in pairs(dict) do
  table.insert(tkeys,name)
 end
 table.sort(tkeys)
 local a = "<<"
 for _,k in ipairs (tkeys) do
   v=dict[k]
   a = a .. strNAME.. k
      if v[1]== 5 then -- it is a name
       b = string.gsub(v[2], "/", "#2F")
       a = a .. strNAME .. b
      elseif v[1] == 6 then -- it is a string
       local b
       if v[3] then
         b = "<" .. v[2] .. ">"
       else
         b = "(" .. v[2] .. ")"
       end
       a = a ..strRECT_SEP .. b
       -- everything else is ignored for now!
      end
    end
 a = a .. ">>"
 return a
end

local function outputKV_gotor (pdfedict) -- action dictionary
  local type, value, hex = GETFROMDICTIONARY(pdfedict,"F")
  local desttype, destvalue, destdetail =  GETFROMDICTIONARY(pdfedict,"D")
  local a = strKV_BEG .. constKEY_FILE .. strVALUE_BEG
  if TYPE(value) == "pdfe.reference" then
     local x,dictionary  = GETFROMREFERENCE(value)
     if TYPE(dictionary) == "pdfe.dictionary" then
       a = a .. outputDICT (dictionary)
     else
      print("ERROR!? this is not a dictionary!!")
     end
  else
   if hex then
     a = a .. strHEX_STR_BEG .. value .. strHEX_STR_END
   else
     a = a .. strLIT_STR_BEG .. value .. strLIT_STR_END
   end
  end
  a = a .. strVALUE_END .. strKV_END
  if desttype == 7 then
    local type, pagenum    = GETFROMARRAY(GETARRAY (pdfedict,"D"),1)
    local type, fittype    = GETFROMARRAY(GETARRAY (pdfedict,"D"),2)
    a = a .. strKV_BEG .. constKEY_DEST_PAGE .. strVALUE_BEG .. pagenum .. strVALUE_END .. strKV_END
    a = a .. strKV_BEG .. constKEY_DEST_VIEW .. strVALUE_BEG .. strNAME .. fittype .. strVALUE_END .. strKV_END
  elseif desttype == 6 then
   a = a .. strKV_BEG .. constKEY_DEST_NAME .. strVALUE_BEG ..
          strLIT_STR_BEG .. destvalue .. strLIT_STR_END .. strVALUE_END .. strKV_END
  end
  return a
end

-- this outputs the key DestLabel with a count

local function outputKV_goto (count)
  local a = strKV_BEG .. constKEY_DEST_LABEL .. strVALUE_BEG .. count .. strVALUE_END .. strKV_END
  return a
end

-- this outputs the key DestName with a name
local function outputKV_goto_name (name)
  local a = strKV_BEG .. constKEY_DEST_NAME .. strVALUE_BEG .. name .. strVALUE_END .. strKV_END
  return a
end

local function outputENTRY_dest (destcount,name,pagereftonum,destnamestoref,pdfedoc)
 local pagenum, data = getdestdata(name,pagereftonum,destnamestoref)
 local mediabox = pdfe.getbox(GETPAGE(pdfedoc,pagenum),"MediaBox")
 local a = strENTRY_BEG
 a = a .. strCMD_BEG .. constCMD_DEST .. strCMD_END
 a = a .. strARG_BEG .. pagenum .. strARG_END
 a = a .. strARG_BEG .. destcount .. strARG_END
 -- name
 a = a .. strARG_BEG .. data[2][2] .. strARG_END
 a = a .. strKVS_BEG
 if data[2][2] == constDEST_XYZ then
   if data[3] and data[3][2] then
    a = a .. strKV_BEG .. constKEY_DEST_X .. strVALUE_BEG .. data[3][2] .. strVALUE_END .. strKV_END
   else
    a = a .. strKV_BEG .. constKEY_DEST_X .. strVALUE_BEG .. mediabox[1] .. strVALUE_END .. strKV_END
   end
   if data[4] and data[4][2] then
    a = a .. strKV_BEG .. constKEY_DEST_Y .. strVALUE_BEG .. data[4][2] .. strVALUE_END .. strKV_END
   else
    a = a .. strKV_BEG .. constKEY_DEST_Y .. strVALUE_BEG .. mediabox[4] .. strVALUE_END .. strKV_END
   end
   if data[5] and data[5][2] then
    a = a .. strKV_BEG .. constKEY_DEST_ZOOM .. strVALUE_BEG .. data[5][2] .. strVALUE_END .. strKV_END
   end
 elseif data[2][2] == constDEST_FIT  then -- nothing to do
 elseif data[2][2] == constDEST_FITB then -- nothing to do
 elseif data[2][2] == constDEST_FITH then
   if data[3] and data[3][2] then
    a = a .. strKV_BEG .. constKEY_DEST_Y .. strVALUE_BEG .. data[3][2] .. strVALUE_END .. strKV_END
   end
 elseif data[2][2] == constDEST_FITBH then
   if data[3] and data[3][2] then
    a = a .. strKV_BEG .. constKEY_DEST_Y .. strVALUE_BEG  .. data[3][2] .. strVALUE_END .. strKV_END
   end
 elseif data[2][2] == constDEST_FITV then
   if data[3] and data[3][2] then
    a = a .. strKV_BEG .. constKEY_DEST_X .. strVALUE_BEG .. data[3][2] .. strVALUE_END .. strKV_END
   end
 elseif data[2][2] == constDEST_FITBV then
   if data[3] and data[3][2] then
    a = a .. strKV_BEG .. constKEY_DEST_X .. strVALUE_BEG .. data[3][2] .. strVALUE_END .. strKV_END
   end
 elseif data[2][2] == constDEST_FITR and data[6] then
   a = a ..  strKV_BEG .. constKEY_DEST_RECT .. strVALUE_BEG
   a = a .. data[3][2] .. strRECT_SEP
   a = a .. data[4][2] .. strRECT_SEP
   a = a .. data[5][2] .. strRECT_SEP
   a = a .. data[6][2] .. strVALUE_END .. strKV_END
 end
  -- output also the name. This perhaps need a check if the name exists
  -- print("DEBUG TYPE name", TYPE(name))
  if not (TYPE(name) == "pdfe.array") then
   a = a .. strKV_BEG .. constKEY_DEST_NAME .. strVALUE_BEG .. name .. strVALUE_END .. strKV_END
  end
 a = a .. strKVS_END .. strENTRY_END
 return a
end

-- the main function

local function __writepax (ext,file)
  local fileVAR     = assert(kpse.find_file(file ..".pdf"),"file `"..file..".pdf` not found")
  local filebaseVAR = FILENAMEONLY(fileVAR)
  -- getting the data for the concrete document:
  local writeVAR = io.open (filebaseVAR .."."..ext,"w") -- always in current directory
  local function WRITE(content)
    writeVAR:write(content)
  end
  local docVAR   = OPEN (fileVAR)
  local pagereftonumVAR, pagecountVAR = getpagesdata (docVAR)
  local destcountVAR   = 0
  -- build from names table:
  local destnamestorefVAR = getdestreferences (docVAR)
  local collected_destinations = {}
  local useddestnames = {}
  -- output ...
  WRITE(strENTRY_BEG .. "{pax}{0.1l}" .. strENTRY_END)
  WRITE(outputENTRY_file(fileVAR,docVAR))
  WRITE(outputENTRY_pagenum(pagecountVAR))
  for i=1, pagecountVAR do
    WRITE(outputENTRY_page(docVAR,i))
    local annots=GETPAGE(docVAR,i).Annots
    if annots then
      for j = 0,#annots-1 do
        local annot = GETDICTIONARY (annots,j)
        annottable = DICTIONARYTOTABLE (annot)
        if annottable.Dest then
          destcountVAR=destcountVAR + 1
          WRITE (strENTRY_BEG)
          WRITE (outputCMD_annot(annot,i,"GoTo"))
          WRITE (strKVS_BEG) -- begin KVS data
          WRITE ( outputKV_color(annot) )
          WRITE ( outputKV_key(annot,constKEY_H) )
          WRITE ( outputKV_Border (annot) )
          WRITE ( outputKV_BS (annot) )
          WRITE ( outputKV_goto (destcountVAR) )
          local type,annotgoto,hex = GETFROMDICTIONARY(annot,"Dest")
          -- if the Dest is a string add the string as a name:
          if type == 6 then
          WRITE ( outputKV_goto_name(annotgoto) )
          end
          WRITE(strKVS_END)   -- end KVS
          WRITE(strENTRY_END) -- end annot data
          table.insert(collected_destinations, outputENTRY_dest(destcountVAR,
               annotgoto,pagereftonumVAR,destnamestorefVAR,docVAR))
        else
          local annotaction = GETDICTIONARY(annot,"A")
          --  print("DEBUG A:",table.serialize(DICTIONARYTOTABLE(annotaction)))
          local annotactiontype =""
          if annotaction then
            annotactiontype = GETNAME(annotaction,"S")
            WRITE (strENTRY_BEG)
            WRITE (outputCMD_annot(annot,i,annotactiontype))
            WRITE (strKVS_BEG) -- begin KVS data
            WRITE ( outputKV_color(annot) )
            WRITE ( outputKV_key(annot,constKEY_H) )
            WRITE ( outputKV_Border (annot) )
            WRITE ( outputKV_BS (annot) )
            if annotactiontype == constKEY_URI then
              WRITE ( outputKV_uri(annotaction) )
            elseif annotactiontype =="GoTo" then
              local desttype, destname, destdetail =  GETFROMDICTIONARY(annotaction,"D")
              -- print("DEBUG annotaction", desttype, destname, destdetail)
              destcountVAR=destcountVAR + 1
              WRITE ( outputKV_goto (destcountVAR) )
              -- this needs perhaps a check if destname actually exists.
              if desttype == 6 then
                WRITE ( outputKV_goto_name(destname) )
              end
            elseif annotactiontype=="GoToR" then
              WRITE ( outputKV_gotor(annotaction) )
            elseif annotactiontype=="Named" then
              WRITE ( outputKV_N (annotaction) )
            end
            WRITE(strKVS_END)   -- end KVS
            WRITE(strENTRY_END) -- end annot data
            if annotactiontype =="GoTo" then
              local type,annotactiongoto,hex = GETFROMDICTIONARY(annotaction,"D")
               if type==6 then
               -- record used name
               useddestnames[annotactiongoto] = 1
              end
              table.insert(collected_destinations, outputENTRY_dest(destcountVAR,annotactiongoto,pagereftonumVAR,destnamestorefVAR,docVAR))
            end
          end
        end
      end
    end
  end
  for i=1,#collected_destinations do
   WRITE (collected_destinations[i])
  end
  -- write out the rest of the destinations.
  -- We order first the table so that the newpax file doesn't change between
  -- compilations, see issue #25 and PR #25
  local destnamessorted = {}
  for k,v in pairs (destnamestorefVAR) do
   -- ignore use dests and page destinations.
    i=string.find(k,"page.",1)
    if not useddestnames[k] and not i then
      table.insert (destnamessorted,k)
    end
  end
  table.sort(destnamessorted)
  for i = 1, #destnamessorted do
      k=destnamessorted[i]
      destcountVAR=destcountVAR + 1
      WRITE(outputENTRY_dest(destcountVAR,k,pagereftonumVAR,destnamestorefVAR,docVAR))
  end
  io.close(writeVAR)
end

local function writepax (file)
 __writepax ("pax",file)
end

local function writenewpax (file)
 __writepax ("newpax",file)
end

newpax = {}
newpax.writepax    = writepax
newpax.writenewpax = writenewpax

return newpax

-- 
--  End of File `newpax.lua'.
