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, match = string.format, string.gsub, string.match
12local concat = table.concat
13local settings_to_array = utilities.parsers.settings_to_array
14local utfchar = utf.char
15local xmlfillin = xml.fillin
16local md5HEX = md5.HEX
17local osdate, ostime, ostimezone, osuuid = os.date, os.time, os.timezone, os.uuid
18
19local trace_xmp = false trackers.register("backend.xmp", function(v) trace_xmp = v end)
20local trace_info = false trackers.register("backend.info", function(v) trace_info = v end)
21
22local report_xmp = logs.reporter("backend","xmp")
23local report_info = logs.reporter("backend","info")
24
25local backends = backends
26local pdfbackend = backends.registered.pdf
27local codeinjections = pdfbackend.codeinjections
28
29local lpdf = lpdf
30local pdfdictionary = lpdf.dictionary
31local pdfconstant = lpdf.constant
32local pdfunicode = lpdf.unicode
33local pdfstring = lpdf.string
34local pdfreference = lpdf.reference
35local pdfflushstreamobject = lpdf.flushstreamobject
36
37
38
39
40local xpacket = format ( [[
41<?xpacket begin="%s" id="W5M0MpCehiHzreSzNTczkc9d"?>
42
43%%s
44
45<?xpacket end="w"?>]], utfchar(0xFEFF) )
46
47local unknown = { false, false }
48local mapping = table.setmetatableindex ( {
49
50 ["ConTeXt.Jobname"] = { "context", "rdf:Description/pdfx:ConTeXt.Jobname" },
51 ["ConTeXt.Time"] = { "date", "rdf:Description/pdfx:ConTeXt.Time" },
52 ["ConTeXt.Url"] = { "context", "rdf:Description/pdfx:ConTeXt.Url" },
53 ["ConTeXt.Support"] = { "context", "rdf:Description/pdfx:ConTeXt.Support" },
54 ["ConTeXt.Version"] = { "context", "rdf:Description/pdfx:ConTeXt.Version" },
55 ["ConTeXt.LMTX"] = { "context", "rdf:Description/pdfx:ConTeXt.LMTX" },
56 ["TeX.Support"] = { "metadata","rdf:Description/pdfx:TeX.Support" },
57 ["LuaTeX.Version"] = { "metadata","rdf:Description/pdfx:LuaTeX.Version" },
58 ["LuaTeX.Functionality"] = { "metadata","rdf:Description/pdfx:LuaTeX.Functionality" },
59 ["LuaTeX.LuaVersion"] = { "metadata","rdf:Description/pdfx:LuaTeX.LuaVersion" },
60 ["LuaTeX.Platform"] = { "metadata","rdf:Description/pdfx:LuaTeX.Platform" },
61 ["ID"] = { "id", "rdf:Description/pdfx:ID" },
62
63 ["Keywords"] = { "metadata","rdf:Description/pdf:Keywords" },
64 ["Producer"] = { "metadata","rdf:Description/pdf:Producer" },
65
66
67 ["Author"] = { "metadata","rdf:Description/dc:creator/rdf:Seq/rdf:li" },
68 ["Format"] = { "metadata","rdf:Description/dc:format" },
69 ["Subject"] = { "metadata","rdf:Description/dc:description/rdf:Alt/rdf:li" },
70 ["Title"] = { "metadata","rdf:Description/dc:title/rdf:Alt/rdf:li" },
71
72 ["CreateDate"] = { "date", "rdf:Description/xmp:CreateDate" },
73 ["CreationDate"] = { "date", "rdf:Description/xmp:CreationDate" },
74 ["Creator"] = { "metadata","rdf:Description/xmp:CreatorTool" },
75 ["MetadataDate"] = { "date", "rdf:Description/xmp:MetadataDate" },
76 ["ModDate"] = { "date", "rdf:Description/xmp:ModDate" },
77 ["ModifyDate"] = { "date", "rdf:Description/xmp:ModifyDate" },
78
79 ["DocumentID"] = { "id", "rdf:Description/xmpMM:DocumentID" },
80 ["InstanceID"] = { "id", "rdf:Description/xmpMM:InstanceID" },
81 ["RenditionClass"] = { "pdf", "rdf:Description/xmpMM:RenditionClass" },
82 ["VersionID"] = { "pdf", "rdf:Description/xmpMM:VersionID" },
83
84
85 ["GTS_PDFXVersion"] = { "pdf", "rdf:Description/pdfxid:GTS_PDFXVersion" },
86
87
88
89 ["Marked"] = { "pdf", "rdf:Description/xmpRights:Marked" },
90
91
92 ["WebStatement"] = { "metadata", "rdf:Description/xmpRights:WebStatement" },
93
94 ["AuthorsPosition"] = { "metadata", "rdf:Description/photoshop:AuthorsPosition" },
95 ["Copyright"] = { "metadata", "rdf:Description/photoshop:Copyright" },
96 ["CaptionWriter"] = { "metadata", "rdf:Description/photoshop:CaptionWriter" },
97}, function() return unknown end )
98
99
100local metadata = nil
101local trailerid = true
102local creationdate = false
103local modificationdate = false
104
105local function pdftimestamp(str)
106 local t = type(str)
107 if t == "string" then
108 local Y, M, D, h, m, s, Zs, Zh, Zm = match(str,"^(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)([%+%-])(%d%d):(%d%d)$")
109 return Y and format("D:%s%s%s%s%s%s%s%s'%s'",Y,M,D,h,m,s,Zs,Zh,Zm)
110 else
111 return osdate("D:%Y%m%d%H%M%S",t == "number" and str or ostime())
112 end
113end
114
115local function pdfgetmetadata()
116 if not metadata then
117 local contextversion = environment.version
118 local luatexversion = format("%1.2f",LUATEXVERSION)
119 local luatexfunctionality = tostring(LUATEXFUNCTIONALITY)
120 local jobname = environment.jobname or tex.jobname or "unknown"
121 local documentid = trailerid and ("uuid:" .. osuuid()) or "no unique document id here"
122 local instanceid = trailerid and ("uuid:" .. osuuid()) or "no unique instance id here"
123 metadata = creationdate and {
124 producer = format("LuaMetaTeX-%s",luatexversion),
125 creator = format("LuaMetaTeX %s %s + ConTeXt LMTX %s",luatexversion,luatexfunctionality,contextversion),
126 luatexversion = luatexversion,
127 contextversion = contextversion,
128 luatexfunctionality = luatexfunctionality,
129 luaversion = tostring(LUAVERSION),
130 platform = os.platform,
131 creationdate = creationdate,
132 modificationdate = modificationdate,
133 id = format("%s | %s",jobname,creationdate),
134 documentid = documentid,
135 instanceid = instanceid,
136 jobname = jobname,
137 } or {
138 producer = "LuaMetaTeX",
139 creator = "LuaMetaTeX + ConTeXt LMTX",
140 id = jobname,
141 documentid = documentid,
142 instanceid = instanceid,
143 jobname = jobname,
144 }
145
146 end
147 return metadata
148end
149
150local function pdfsetmetadate(n,both)
151 if n then
152 n = converters.totime(n)
153 if n then
154 creationdate = osdate("%Y-%m-%dT%H:%M:%S",ostime(n)) .. ostimezone()
155 if both then
156 modificationdate = creationdate
157 end
158 end
159 end
160 return creationdate
161end
162
163lpdf.pdftimestamp = pdftimestamp
164
165function lpdf.gettrailerid()
166 if trailerid == true then
167 return md5.HEX(osuuid())
168 elseif type(trailerid) == "string" then
169 return md5.HEX(trailerid)
170 else
171 return false
172 end
173end
174
175
176
177directives.register("backend.trailerid", function(v)
178 trailerid = type(v) and v or toboolean(v)
179end)
180
181
182
183local function setdates(v)
184 local t = type(v)
185 if t == "number" or t == "string" then
186 local d = converters.totime(v)
187 if d then
188 report_info("forced date/time information %a will be used",pdfsetmetadate(d,true))
189 return
190 end
191 end
192 if toboolean(v) then
193 creationdate = osdate("%Y-%m-%dT%H:%M:%S") .. ostimezone()
194 modificationdate = creationdate
195 else
196 creationdate = false
197 modificationdate = false
198 end
199end
200
201setdates(true)
202
203directives.register("backend.date", setdates)
204
205
206
207local xmp, xmpfile, xmpname = nil, nil, "lpdf-pdx.xml"
208
209local function setxmpfile(name)
210 if xmp then
211 report_xmp("discarding loaded file %a",xmpfile)
212 xmp = nil
213 end
214 xmpfile = name ~= "" and name
215end
216
217codeinjections.setxmpfile = setxmpfile
218
219interfaces.implement {
220 name = "setxmpfile",
221 arguments = "string",
222 actions = setxmpfile
223}
224
225local function valid_xmp()
226 if not xmp then
227
228 if xmpfile and xmpfile ~= "" then
229 xmpfile = resolvers.findfile(xmpfile) or ""
230 end
231 if not xmpfile or xmpfile == "" then
232 xmpfile = resolvers.findfile(xmpname) or ""
233 end
234 if xmpfile ~= "" then
235 report_xmp("using file %a",xmpfile)
236 end
237 local xmpdata = xmpfile ~= "" and io.loaddata(xmpfile) or ""
238 xmp = xml.convert(xmpdata)
239 end
240 return xmp
241end
242
243function lpdf.addxmpinfo(tag,value,check)
244 local pattern = mapping[tag][2]
245 if type(pattern) == "string" then
246 xmlfillin(xmp or valid_xmp(),pattern,value,check)
247 end
248end
249
250
251
252local pdfaddtoinfo = lpdf.addtoinfo
253local pdfaddxmpinfo = lpdf.addxmpinfo
254
255function lpdf.addtoinfo(tag,pdfvalue,strvalue)
256 local pattern = mapping[tag][2]
257 if pattern then
258 pdfaddtoinfo(tag,pdfvalue)
259 end
260 if type(pattern) == "string" then
261 local value = strvalue or gsub(tostring(pdfvalue),"^%((.*)%)$","%1")
262 if trace_info then
263 report_info("set %a to %a",tag,value)
264 end
265 xmlfillin(xmp or valid_xmp(),pattern,value,check)
266 end
267end
268
269local pdfaddtoinfo = lpdf.addtoinfo
270
271
272
273function lpdf.insertxmpinfo(pattern,whatever,prepend)
274 xml.insert(xmp or valid_xmp(),pattern,whatever,prepend)
275end
276
277function lpdf.injectxmpinfo(pattern,whatever,prepend)
278 xml.inject(xmp or valid_xmp(),pattern,whatever,prepend)
279end
280
281
282
283local add_xmp_blob = true
284local indentity_done = false
285
286local function setupidentity()
287 if not done then
288
289 local identity = interactions.general.getidentity()
290 local title = identity.title
291 local subtitle = identity.subtitle
292 local author = identity.author
293 local date = identity.date
294 local keywords = identity.keywords
295
296 if date and date ~= "" then
297 pdfsetmetadate(date)
298 end
299 if keywords then
300 keywords = concat(settings_to_array(keywords), " ")
301 end
302
303 local metadata = pdfgetmetadata()
304 local creator = metadata.creator
305 local contextversion = metadata.contextversion
306 local id = metadata.id
307 local jobname = metadata.jobname
308 local creator = metadata.creator
309 local creation = metadata.creationdate
310 local modification = metadata.modificationdate
311
312 if creator then
313 pdfaddtoinfo("Creator",pdfunicode(creator),creator)
314 end
315 if creation then
316 pdfaddtoinfo("CreationDate",pdfstring(pdftimestamp(creation)),creation)
317 end
318 if modification then
319 pdfaddtoinfo("ModDate",pdfstring(pdftimestamp(modification)),modification)
320 end
321 if id then
322 pdfaddtoinfo("ID",pdfstring(id),id)
323 end
324
325 if title ~= "" then
326 pdfaddtoinfo("Title",pdfunicode(title),title)
327 end
328 if subtitle ~= "" then
329 pdfaddtoinfo("Subject",pdfunicode(subtitle),subtitle)
330 end
331 if author ~= "" then
332 pdfaddtoinfo("Author",pdfunicode(author),author)
333 end
334 if keywords and keywords ~= "" then
335 pdfaddtoinfo("Keywords",pdfunicode(keywords),keywords)
336 end
337
338 if contextversion then
339 pdfaddtoinfo("ConTeXt.Version",contextversion)
340 end
341 if creation then
342 pdfaddtoinfo("ConTeXt.Time",creation)
343 end
344 if jobname then
345 pdfaddtoinfo("ConTeXt.Jobname",jobname)
346 end
347
348 pdfaddtoinfo("ConTeXt.Url","www.pragma-ade.com")
349 pdfaddtoinfo("ConTeXt.Support","contextgarden.net")
350 pdfaddtoinfo("TeX.Support","tug.org")
351
352 done = true
353 else
354
355 end
356end
357
358local function flushxmpinfo()
359 commands.pushrandomseed()
360 commands.setrandomseed(ostime())
361
362 local metadata = pdfgetmetadata()
363 local time = metadata.time
364 local producer = metadata.producer
365 local creator = metadata.creator
366 local documentid = metadata.documentid
367 local instanceid = metadata.instanceid
368
369 pdfaddtoinfo("Producer",producer)
370 pdfaddtoinfo("Creator",creator)
371 pdfaddtoinfo("CreationDate",time)
372 pdfaddtoinfo("ModDate",time)
373
374 if add_xmp_blob then
375
376 pdfaddxmpinfo("DocumentID",documentid)
377 pdfaddxmpinfo("InstanceID",instanceid)
378 pdfaddxmpinfo("Producer",producer)
379 pdfaddxmpinfo("CreatorTool",creator)
380 pdfaddxmpinfo("CreateDate",time)
381 pdfaddxmpinfo("ModifyDate",time)
382 pdfaddxmpinfo("MetadataDate",time)
383 pdfaddxmpinfo("LuaTeX.Version",metadata.luatexversion)
384 pdfaddxmpinfo("LuaTeX.Functionality",metadata.luatexfunctionality)
385 pdfaddxmpinfo("LuaTeX.LuaVersion",metadata.luaversion)
386 pdfaddxmpinfo("LuaTeX.Platform",metadata.platform)
387
388 local blob = xml.tostring(xml.first(xmp or valid_xmp(),"/x:xmpmeta"))
389 local md = pdfdictionary {
390 Subtype = pdfconstant("XML"),
391 Type = pdfconstant("Metadata"),
392 }
393 if trace_xmp then
394 report_xmp("data flushed, see log file")
395 logs.pushtarget("logfile")
396 report_xmp("start xmp blob")
397 logs.newline()
398 logs.writer(blob)
399 logs.newline()
400 report_xmp("stop xmp blob")
401 logs.poptarget()
402 end
403 blob = format(xpacket,blob)
404 if not verbose and lpdf.compresslevel() > 0 then
405 blob = gsub(blob,">%s+<","><")
406 end
407 local r = pdfflushstreamobject(blob,md,false)
408 lpdf.addtocatalog("Metadata",pdfreference(r))
409
410 end
411
412 commands.poprandomseed()
413end
414
415lpdf.registerpagefinalizer(setupidentity,"identity")
416lpdf.registerdocumentfinalizer(flushxmpinfo,1,"metadata")
417
418directives.register("backend.xmp", function(v) add_xmp_blob = v end)
419directives.register("backend.verbosexmp", function(v) verbose = v end)
420 |