lpdf-ren.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['lpdf-ren'] = {
    version   = 1.001,
    comment   = "companion to lpdf-ini.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

-- rendering

local tostring, tonumber, next = tostring, tonumber, next
local concat = table.concat
local formatters = string.formatters
local settings_to_array = utilities.parsers.settings_to_array
local getrandom = utilities.randomizer.get

local backends, lpdf, nodes, node = backends, lpdf, nodes, node

local nodeinjections      = backends.pdf.nodeinjections
local codeinjections      = backends.pdf.codeinjections
local registrations       = backends.pdf.registrations
local viewerlayers        = attributes.viewerlayers

local references          = structures.references

references.executers      = references.executers or { }
local executers           = references.executers

local variables           = interfaces.variables

local v_no                = variables.no
local v_yes               = variables.yes
local v_start             = variables.start
local v_stop              = variables.stop
local v_reset             = variables.reset
local v_auto              = variables.auto
local v_random            = variables.random

local pdfconstant         = lpdf.constant
local pdfdictionary       = lpdf.dictionary
local pdfarray            = lpdf.array
local pdfreference        = lpdf.reference
local pdfflushobject      = lpdf.flushobject
local pdfreserveobject    = lpdf.reserveobject

local addtopageattributes = lpdf.addtopageattributes
local addtopageresources  = lpdf.addtopageresources
local addtocatalog        = lpdf.addtocatalog

local escaped             = lpdf.escaped

local nuts                = nodes.nuts
local copy_node           = nuts.copy

local nodepool            = nuts.pool
local register            = nodepool.register
local pageliteral         = nodepool.pageliteral

local pdf_ocg             = pdfconstant("OCG")
local pdf_ocmd            = pdfconstant("OCMD")
local pdf_off             = pdfconstant("OFF")
local pdf_on              = pdfconstant("ON")
local pdf_view            = pdfconstant("View")
local pdf_design          = pdfconstant("Design")
local pdf_toggle          = pdfconstant("Toggle")
local pdf_setocgstate     = pdfconstant("SetOCGState")

local pdf_print = {
    [v_yes] = pdfdictionary { PrintState = pdf_on  },
    [v_no ] = pdfdictionary { PrintState = pdf_off },
}

local pdf_intent = {
    [v_yes] = pdf_view,
    [v_no]  = pdf_design,
}

local pdf_export = {
    [v_yes] = pdf_on,
    [v_no]  = pdf_off,
}

-- We can have references to layers before they are places, for instance from
-- hide and vide actions. This is why we need to be able to force usage of layers
-- at several moments.

-- management

local pdfln, pdfld = { }, { }
local textlayers, hidelayers, videlayers = pdfarray(), pdfarray(), pdfarray()
local pagelayers, pagelayersreference, cache = nil, nil, { }
local alphabetic = { }

local escapednames   = table.setmetatableindex(function(t,k)
    local v = escaped(k)
    t[k] = v
    return v
end)

local specifications = { }
local initialized    = { }

function codeinjections.defineviewerlayer(specification)
    if viewerlayers.supported and textlayers then
        local tag = specification.tag
        if not specifications[tag] then
            specifications[tag] = specification
        end
    end
end

local function useviewerlayer(name) -- move up so that we can use it as local
    if not environment.initex and not initialized[name] then
        local specification = specifications[name]
        if specification then
            specifications[name] = nil -- or not
            initialized   [name] = true
            if not pagelayers then
                pagelayers = pdfdictionary()
                pagelayersreference = pdfreserveobject()
            end
            local tag = specification.tag
            -- todo: reserve
            local nn = pdfreserveobject()
            local nr = pdfreference(nn)
            local nd = pdfdictionary {
                Type  = pdf_ocg,
                Name  = specification.title or "unknown",
                Usage = {
                    Intent = pdf_intent[specification.editable  or v_yes], -- disable layer hiding by user (useless)
                    Print  = pdf_print [specification.printable or v_yes], -- printable or not
                    Export = pdf_export[specification.export    or v_yes], -- export or not
                },
            }
            cache[#cache+1] = { nn, nd }
            pdfln[tag] = nr -- was n
            local dn = pdfreserveobject()
            local dr = pdfreference(dn)
            local dd = pdfdictionary {
                Type = pdf_ocmd,
                OCGs = pdfarray { nr },
            }
            cache[#cache+1] = { dn, dd }
            pdfld[tag] = dr
            textlayers[#textlayers+1] = nr
            alphabetic[tag] = nr
            if specification.visible == v_start then
                videlayers[#videlayers+1] = nr
            else
                hidelayers[#hidelayers+1] = nr
            end
            pagelayers[escapednames[tag]] = dr -- check
        else
            -- todo: message
        end
    end
end

codeinjections.useviewerlayer = useviewerlayer

local function layerreference(name)
    local r = pdfln[name]
    if r then
        return r
    else
        useviewerlayer(name)
        return pdfln[name]
    end
end

lpdf.layerreference = layerreference -- also triggered when a hide or vide happens

local function flushtextlayers()
    if viewerlayers.supported then
        if pagelayers then
            pdfflushobject(pagelayersreference,pagelayers)
        end
        for i=1,#cache do
            local ci = cache[i]
            pdfflushobject(ci[1],ci[2])
        end
        if textlayers and #textlayers > 0 then -- we can group them if needed, like: layout
            local sortedlayers = { }
            for k, v in table.sortedhash(alphabetic) do
                sortedlayers[#sortedlayers+1] = v -- maybe do a proper numeric sort as well
            end
            local d = pdfdictionary {
                OCGs = textlayers,
                D    = pdfdictionary {
                    Name      = "Document",
                 -- Order     = (viewerlayers.hasorder and textlayers) or nil,
                    Order     = (viewerlayers.hasorder and sortedlayers) or nil,
                    ON        = videlayers,
                    OFF       = hidelayers,
                    BaseState = pdf_on,
                    AS = pdfarray {
                        pdfdictionary {
                            Category = pdfarray { pdfconstant("Print") },
                            Event    = pdfconstant("Print"),
                            OCGs     = (viewerlayers.hasorder and sortedlayers) or nil,
                        }
                    },
                },
            }
            addtocatalog("OCProperties",d)
            textlayers = nil
        end
    end
end

local function flushpagelayers() -- we can share these
    if pagelayers then
        addtopageresources("Properties",pdfreference(pagelayersreference)) -- we could cache this
    end
end

lpdf.registerpagefinalizer    (flushpagelayers,"layers")
lpdf.registerdocumentfinalizer(flushtextlayers,"layers")

local function setlayer(what,arguments)
    -- maybe just a gmatch of even better, earlier in lpeg
    arguments = (type(arguments) == "table" and arguments) or settings_to_array(arguments)
    local state = pdfarray { what }
    for i=1,#arguments do
        local p = layerreference(arguments[i])
        if p then
            state[#state+1] = p
        end
    end
    return pdfdictionary {
        S     = pdf_setocgstate,
        State = state,
    }
end

function executers.hidelayer  (arguments) return setlayer(pdf_off,   arguments) end
function executers.videlayer  (arguments) return setlayer(pdf_on,    arguments) end
function executers.togglelayer(arguments) return setlayer(pdf_toggle,arguments) end

-- injection

local f_bdc = formatters["/OC /%s BDC"]
local s_emc = "EMC"

function codeinjections.startlayer(name) -- used in mp
    if not name then
        name = "unknown"
    end
    useviewerlayer(name)
    return f_bdc(escapednames[name])
end

function codeinjections.stoplayer(name) -- used in mp
    return s_emc
end

local cache = { }
local stop  = nil

function nodeinjections.startlayer(name)
    local c = cache[name]
    if not c then
        useviewerlayer(name)
        c = register(pageliteral(f_bdc(escapednames[name])))
        cache[name] = c
    end
    return copy_node(c)
end

function nodeinjections.stoplayer()
    if not stop then
        stop = register(pageliteral(s_emc))
    end
    return copy_node(stop)
end

-- experimental stacker code (slow, can be optimized): !!!! TEST CODE !!!!

local values     = viewerlayers.values
local startlayer = codeinjections.startlayer
local stoplayer  = codeinjections.stoplayer

function nodeinjections.startstackedlayer(s,t,first,last)
    local r = { }
    for i=first,last do
        r[#r+1] = startlayer(values[t[i]])
    end
    r = concat(r," ")
    return pageliteral(r)
end

function nodeinjections.stopstackedlayer(s,t,first,last)
    local r = { }
    for i=last,first,-1 do
        r[#r+1] = stoplayer()
    end
    r = concat(r," ")
    return pageliteral(r)
end

function nodeinjections.changestackedlayer(s,t1,first1,last1,t2,first2,last2)
    local r = { }
    for i=last1,first1,-1 do
        r[#r+1] = stoplayer()
    end
    for i=first2,last2 do
        r[#r+1] = startlayer(values[t2[i]])
    end
    r = concat(r," ")
    return pageliteral(r)
end

-- transitions

local pagetransitions = {
    {"split","in","vertical"}, {"split","in","horizontal"},
    {"split","out","vertical"}, {"split","out","horizontal"},
    {"blinds","horizontal"}, {"blinds","vertical"},
    {"box","in"}, {"box","out"},
    {"wipe","east"}, {"wipe","west"}, {"wipe","north"}, {"wipe","south"},
    {"dissolve"},
    {"glitter","east"}, {"glitter","south"},
    {"fly","in","east"}, {"fly","in","west"}, {"fly","in","north"}, {"fly","in","south"},
    {"fly","out","east"}, {"fly","out","west"}, {"fly","out","north"}, {"fly","out","south"},
    {"push","east"}, {"push","west"}, {"push","north"}, {"push","south"},
    {"cover","east"}, {"cover","west"}, {"cover","north"}, {"cover","south"},
    {"uncover","east"}, {"uncover","west"}, {"uncover","north"}, {"uncover","south"},
    {"fade"},
}

local mapping = {
    split      = { "S"  , pdfconstant("Split") },
    blinds     = { "S"  , pdfconstant("Blinds") },
    box        = { "S"  , pdfconstant("Box") },
    wipe       = { "S"  , pdfconstant("Wipe") },
    dissolve   = { "S"  , pdfconstant("Dissolve") },
    glitter    = { "S"  , pdfconstant("Glitter") },
    replace    = { "S"  , pdfconstant("R") },
    fly        = { "S"  , pdfconstant("Fly") },
    push       = { "S"  , pdfconstant("Push") },
    cover      = { "S"  , pdfconstant("Cover") },
    uncover    = { "S"  , pdfconstant("Uncover") },
    fade       = { "S"  , pdfconstant("Fade") },
    horizontal = { "Dm" , pdfconstant("H") },
    vertical   = { "Dm" , pdfconstant("V") },
    ["in"]     = { "M"  , pdfconstant("I") },
    out        = { "M"  , pdfconstant("O") },
    east       = { "Di" ,   0 },
    north      = { "Di" ,  90 },
    west       = { "Di" , 180 },
    south      = { "Di" , 270 },
}

local last = 0

-- n: number, "stop", "reset", "random", "a,b,c" delay: number, "none"

function codeinjections.setpagetransition(specification)
    local n, delay = specification.n, specification.delay
    if not n or n == "" then
        return -- let's forget about it
    elseif n == v_auto then
        if last >= #pagetransitions then
            last = 0
        end
        n = last + 1
    elseif n == v_stop then
        return
    elseif n == v_reset then
        last = 0
        return
    elseif n == v_random then
        n = getrandom("transition",1,#pagetransitions)
    else
        n = tonumber(n)
    end
    local t = n and pagetransitions[n] or pagetransitions[1]
    if not t then
        t = settings_to_array(n)
    end
    if t and #t > 0 then
        local d = pdfdictionary()
        for i=1,#t do
            local m = mapping[t[i]]
            d[m[1]] = m[2]
        end
        delay = tonumber(delay)
        if delay and delay > 0 then
            addtopageattributes("Dur",delay)
        end
        addtopageattributes("Trans",d)
    end
end