lpdf-xmp.lua /size: 11 Kb    last modification: 2020-07-01 14:35
1if not modules then modules = { } end modules ['lpdf-xmp'] = {
2    version   = 1.001,
3    comment   = "companion to lpdf-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files",
7    comment   = "with help from Peter Rolf",
8}
9
10local tostring, type = tostring, type
11local format, gsub = string.format, string.gsub
12local utfchar = utf.char
13local xmlfillin = xml.fillin
14local md5HEX = md5.HEX
15
16local trace_xmp  = false  trackers.register("backend.xmp",  function(v) trace_xmp  = v end)
17local trace_info = false  trackers.register("backend.info", function(v) trace_info = v end)
18
19local report_xmp  = logs.reporter("backend","xmp")
20local report_info = logs.reporter("backend","info")
21
22local backends, lpdf = backends, lpdf
23
24local codeinjections       = backends.pdf.codeinjections -- normally it is registered
25
26local pdfdictionary        = lpdf.dictionary
27local pdfconstant          = lpdf.constant
28local pdfreference         = lpdf.reference
29local pdfflushstreamobject = lpdf.flushstreamobject
30
31local pdfgetmetadata       = lpdf.getmetadata
32
33-- The XMP packet wrapper is kind of fixed, see page 10 of XMPSpecificationsPart1.pdf from
34-- XMP-Toolkit-SDK-CC201607.zip. So we hardcode the id.
35
36local xpacket = format ( [[
37<?xpacket begin="%s" id="W5M0MpCehiHzreSzNTczkc9d"?>
38
39%%s
40
41<?xpacket end="w"?>]], utfchar(0xFEFF) )
42
43local mapping = {
44    -- user defined keys (pdfx:)
45    ["ConTeXt.Jobname"]      = { "context", "rdf:Description/pdfx:ConTeXt.Jobname" },
46    ["ConTeXt.Time"]         = { "date",    "rdf:Description/pdfx:ConTeXt.Time" },
47    ["ConTeXt.Url"]          = { "context", "rdf:Description/pdfx:ConTeXt.Url" },
48    ["ConTeXt.Support"]      = { "context", "rdf:Description/pdfx:ConTeXt.Support" },
49    ["ConTeXt.Version"]      = { "context", "rdf:Description/pdfx:ConTeXt.Version" },
50    ["ConTeXt.LMTX"]         = { "context", "rdf:Description/pdfx:ConTeXt.LMTX" },
51    ["TeX.Support"]          = { "metadata","rdf:Description/pdfx:TeX.Support" },
52    ["LuaTeX.Version"]       = { "metadata","rdf:Description/pdfx:LuaTeX.Version" },
53    ["LuaTeX.Functionality"] = { "metadata","rdf:Description/pdfx:LuaTeX.Functionality" },
54    ["LuaTeX.LuaVersion"]    = { "metadata","rdf:Description/pdfx:LuaTeX.LuaVersion" },
55    ["LuaTeX.Platform"]      = { "metadata","rdf:Description/pdfx:LuaTeX.Platform" },
56    ["ID"]                   = { "id",      "rdf:Description/pdfx:ID" },                         -- has date
57    -- Adobe PDF schema
58    ["Keywords"]             = { "metadata","rdf:Description/pdf:Keywords" },
59    ["Producer"]             = { "metadata","rdf:Description/pdf:Producer" },
60 -- ["Trapped"]              = { "pdf",     "rdf:Description/pdf:Trapped" },                     -- '/False' in /Info, but 'False' in XMP
61    -- Dublin Core schema
62    ["Author"]               = { "metadata","rdf:Description/dc:creator/rdf:Seq/rdf:li" },
63    ["Format"]               = { "metadata","rdf:Description/dc:format" },                       -- optional, but nice to have
64    ["Subject"]              = { "metadata","rdf:Description/dc:description/rdf:Alt/rdf:li" },
65    ["Title"]                = { "metadata","rdf:Description/dc:title/rdf:Alt/rdf:li" },
66    -- XMP Basic schema
67    ["CreateDate"]           = { "date",    "rdf:Description/xmp:CreateDate" },
68    ["CreationDate"]         = { "date",    "rdf:Description/xmp:CreationDate" },                -- dummy
69    ["Creator"]              = { "metadata","rdf:Description/xmp:CreatorTool" },
70    ["MetadataDate"]         = { "date",    "rdf:Description/xmp:MetadataDate" },
71    ["ModDate"]              = { "date",    "rdf:Description/xmp:ModDate" },                     -- dummy
72    ["ModifyDate"]           = { "date",    "rdf:Description/xmp:ModifyDate" },
73    -- XMP Media Management schema
74    ["DocumentID"]           = { "id",      "rdf:Description/xmpMM:DocumentID" },                -- uuid
75    ["InstanceID"]           = { "id",      "rdf:Description/xmpMM:InstanceID" },                -- uuid
76    ["RenditionClass"]       = { "pdf",     "rdf:Description/xmpMM:RenditionClass" },            -- PDF/X-4
77    ["VersionID"]            = { "pdf",     "rdf:Description/xmpMM:VersionID" },                 -- PDF/X-4
78    -- additional entries
79    -- PDF/X
80    ["GTS_PDFXVersion"]      = { "pdf",     "rdf:Description/pdfxid:GTS_PDFXVersion" },
81    -- optional entries
82    -- all what is visible in the 'document properties --> additional metadata' window
83    -- XMP Rights Management schema (optional)
84    ["Marked"]               = { "pdf",      "rdf:Description/xmpRights:Marked" },
85 -- ["Owner"]                = { "metadata", "rdf:Description/xmpRights:Owner/rdf:Bag/rdf:li" }, -- maybe useful (not visible)
86 -- ["UsageTerms"]           = { "metadata", "rdf:Description/xmpRights:UsageTerms" },           -- maybe useful (not visible)
87    ["WebStatement"]         = { "metadata", "rdf:Description/xmpRights:WebStatement" },
88    -- Photoshop PDF schema (optional)
89    ["AuthorsPosition"]      = { "metadata", "rdf:Description/photoshop:AuthorsPosition" },
90    ["Copyright"]            = { "metadata", "rdf:Description/photoshop:Copyright" },
91    ["CaptionWriter"]        = { "metadata", "rdf:Description/photoshop:CaptionWriter" },
92}
93
94lpdf.setsuppressoptionalinfo (
95        0 --
96    +   1 -- pdfnofullbanner
97    +   2 -- pdfnofilename
98    +   4 -- pdfnopagenumber
99    +   8 -- pdfnoinfodict
100    +  16 -- pdfnocreator
101    +  32 -- pdfnocreationdate
102    +  64 -- pdfnomoddate
103    + 128 -- pdfnoproducer
104    + 256 -- pdfnotrapped
105 -- + 512 -- pdfnoid
106)
107
108local included = backends.included
109local lpdfid   = lpdf.id
110
111function lpdf.id() -- overload of ini
112    return lpdfid(included.date)
113end
114
115local settrailerid = lpdf.settrailerid -- this is the wrapped one
116
117local trailerid = nil
118local dates     = nil
119
120local function update()
121    if trailer_id then
122        local b = toboolean(trailer_id) or trailer_id == ""
123        if b then
124            trailer_id = "This file is processed by ConTeXt and LuaTeX."
125        else
126            trailer_id = tostring(trailer_id)
127        end
128        local h = md5HEX(trailer_id)
129        if b then
130            report_info("using frozen trailer id")
131        else
132            report_info("using hashed trailer id %a (%a)",trailer_id,h)
133        end
134        settrailerid(format("[<%s> <%s>]",h,h))
135    end
136    --
137    local t = type(dates)
138    if t == "number" or t == "string" then
139        local d = converters.totime(dates)
140        if d then
141            included.date = true
142            included.id   = "fake"
143            report_info("forced date/time information %a will be used",lpdf.settime(d))
144            settrailerid(false)
145            return
146        end
147        if t == "string" then
148            dates = toboolean(dates)
149            included.date = dates
150            if dates ~= false then
151                included.id = true
152            else
153                report_info("no date/time but fake id information will be added")
154                settrailerid(true)
155                included.id = "fake"
156            end
157        end
158    end
159end
160
161function lpdf.settrailerid(v) trailerid = v end
162function lpdf.setdates    (v) dates     = v end
163
164lpdf.registerdocumentfinalizer(update,"trailer id and dates",1)
165
166directives.register("backend.trailerid", lpdf.settrailerid)
167directives.register("backend.date",      lpdf.setdates)
168
169local function permitdetail(what)
170    local m = mapping[what]
171    if m then
172        return included[m[1]] and m[2]
173    else
174        return included[what] and true or false
175    end
176end
177
178lpdf.permitdetail = permitdetail
179
180-- maybe some day we will load the xmp file at runtime
181
182local xmp, xmpfile, xmpname = nil, nil, "lpdf-pdx.xml"
183
184local function setxmpfile(name)
185    if xmp then
186        report_xmp("discarding loaded file %a",xmpfile)
187        xmp = nil
188    end
189    xmpfile = name ~= "" and name
190end
191
192codeinjections.setxmpfile = setxmpfile
193
194interfaces.implement {
195    name      = "setxmpfile",
196    arguments = "string",
197    actions   = setxmpfile
198}
199
200local function valid_xmp()
201    if not xmp then
202     -- local xmpfile = xmpfile or resolvers.findfile(xmpname) or ""
203        if xmpfile and xmpfile ~= "" then
204            xmpfile = resolvers.findfile(xmpfile) or ""
205        end
206        if not xmpfile or xmpfile == "" then
207            xmpfile = resolvers.findfile(xmpname) or ""
208        end
209        if xmpfile ~= "" then
210            report_xmp("using file %a",xmpfile)
211        end
212        local xmpdata = xmpfile ~= "" and io.loaddata(xmpfile) or ""
213        xmp = xml.convert(xmpdata)
214    end
215    return xmp
216end
217
218function lpdf.addxmpinfo(tag,value,check)
219    local pattern = permitdetail(tag)
220    if type(pattern) == "string" then
221        xmlfillin(xmp or valid_xmp(),pattern,value,check)
222    end
223end
224
225-- redefined
226
227local pdfaddtoinfo  = lpdf.addtoinfo
228local pdfaddxmpinfo = lpdf.addxmpinfo
229
230function lpdf.addtoinfo(tag,pdfvalue,strvalue)
231    local pattern = permitdetail(tag)
232    if pattern then
233        pdfaddtoinfo(tag,pdfvalue)
234    end
235    if type(pattern) == "string" then
236        local value = strvalue or gsub(tostring(pdfvalue),"^%((.*)%)$","%1") -- hack
237        if trace_info then
238            report_info("set %a to %a",tag,value)
239        end
240        xmlfillin(xmp or valid_xmp(),pattern,value,check)
241    end
242end
243
244local pdfaddtoinfo = lpdf.addtoinfo -- used later
245
246-- for the do-it-yourselvers
247
248function lpdf.insertxmpinfo(pattern,whatever,prepend)
249    xml.insert(xmp or valid_xmp(),pattern,whatever,prepend)
250end
251
252function lpdf.injectxmpinfo(pattern,whatever,prepend)
253    xml.inject(xmp or valid_xmp(),pattern,whatever,prepend)
254end
255
256-- flushing
257
258local add_xmp_blob = true  directives.register("backend.xmp",function(v) add_xmp_blob = v end)
259
260local function flushxmpinfo()
261    commands.pushrandomseed()
262    commands.setrandomseed(os.time())
263
264    local documentid = "no unique document id here"
265    local instanceid = "no unique instance id here"
266    local metadata   = pdfgetmetadata()
267    local time       = metadata.time
268    local producer   = metadata.producer
269    local creator    = metadata.creator
270
271    if included.id ~= "fake" then
272        documentid = "uuid:" .. os.uuid()
273        instanceid = "uuid:" .. os.uuid()
274    end
275
276    pdfaddtoinfo("Producer",producer)
277    pdfaddtoinfo("Creator",creator)
278    pdfaddtoinfo("CreationDate",time)
279    pdfaddtoinfo("ModDate",time)
280
281    if add_xmp_blob then
282
283        pdfaddxmpinfo("DocumentID",documentid)
284        pdfaddxmpinfo("InstanceID",instanceid)
285        pdfaddxmpinfo("Producer",producer)
286        pdfaddxmpinfo("CreatorTool",creator)
287        pdfaddxmpinfo("CreateDate",time)
288        pdfaddxmpinfo("ModifyDate",time)
289        pdfaddxmpinfo("MetadataDate",time)
290        pdfaddxmpinfo("LuaTeX.Version",metadata.luatexversion)
291        pdfaddxmpinfo("LuaTeX.Functionality",metadata.luatexfunctionality)
292        pdfaddxmpinfo("LuaTeX.LuaVersion",metadata.luaversion)
293        pdfaddxmpinfo("LuaTeX.Platform",metadata.platform)
294
295        local blob = xml.tostring(xml.first(xmp or valid_xmp(),"/x:xmpmeta"))
296        local md = pdfdictionary {
297            Subtype = pdfconstant("XML"),
298            Type    = pdfconstant("Metadata"),
299        }
300        if trace_xmp then
301            report_xmp("data flushed, see log file")
302            logs.pushtarget("logfile")
303            report_xmp("start xmp blob")
304            logs.newline()
305            logs.writer(blob)
306            logs.newline()
307            report_xmp("stop xmp blob")
308            logs.poptarget()
309        end
310        blob = format(xpacket,blob)
311        if not verbose and lpdf.compresslevel() > 0 then
312            blob = gsub(blob,">%s+<","><")
313        end
314        local r = pdfflushstreamobject(blob,md,false) -- uncompressed
315        lpdf.addtocatalog("Metadata",pdfreference(r))
316    end
317
318    commands.poprandomseed() -- hack
319end
320
321--  this will be enabled when we can inhibit compression for a stream at the lua end
322
323lpdf.registerdocumentfinalizer(flushxmpinfo,1,"metadata")
324
325directives.register("backend.verbosexmp", function(v)
326    verbose = v
327end)
328