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, rep, count = string.format, string.gsub, string.match, string.rep, string.count
12local utfchar = utf.char
13local md5HEX = md5.HEX
14local xmlfillin, xmldelete, xmltext = xml.fillin, xml.delete, xml.text
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
25
26local pdfdictionary = lpdf.dictionary
27local pdfconstant = lpdf.constant
28local pdfreference = lpdf.reference
29local pdfflushstreamobject = lpdf.flushstreamobject
30
31local pdfgetmetadata = lpdf.getmetadata
32
33
34
35
36local xpacket = format ( [[
37<?xpacket begin="%s" id="W5M0MpCehiHzreSzNTczkc9d"?>
38
39%%s
40
41<?xpacket end="w"?>]], utfchar(0xFEFF) )
42
43local mapping = {
44
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 ["TeX.Support"] = { "metadata","rdf:Description/pdfx:TeX.Support" },
51 ["LuaTeX.Version"] = { "metadata","rdf:Description/pdfx:LuaTeX.Version" },
52 ["LuaTeX.Functionality"] = { "metadata","rdf:Description/pdfx:LuaTeX.Functionality" },
53 ["LuaTeX.LuaVersion"] = { "metadata","rdf:Description/pdfx:LuaTeX.LuaVersion" },
54 ["LuaTeX.Platform"] = { "metadata","rdf:Description/pdfx:LuaTeX.Platform" },
55 ["ID"] = { "id", "rdf:Description/pdfx:ID" },
56
57 ["Keywords"] = { "metadata","rdf:Description/pdf:Keywords", true },
58 ["Producer"] = { "metadata","rdf:Description/pdf:Producer", true },
59
60
61 ["Format"] = { "metadata","rdf:Description/dc:format" },
62
63
64
65
66 ["Author"] = { "metadata","rdf:Description/dc:creator/rdf:Seq/rdf:li", true },
67 ["Subject"] = { "metadata","rdf:Description/dc:description/rdf:Alt/rdf:li", true },
68 ["Title"] = { "metadata","rdf:Description/dc:title/rdf:Alt/rdf:li", true },
69
70 ["CreateDate"] = { "date", "rdf:Description/xmp:CreateDate" },
71 ["CreationDate"] = { "date", "rdf:Description/xmp:CreationDate" },
72 ["CreatorTool"] = { "metadata","rdf:Description/xmp:CreatorTool" },
73
74 ["MetadataDate"] = { "date", "rdf:Description/xmp:MetadataDate" },
75 ["ModDate"] = { "date", "rdf:Description/xmp:ModDate" },
76 ["ModifyDate"] = { "date", "rdf:Description/xmp:ModifyDate" },
77
78 ["DocumentID"] = { "id", "rdf:Description/xmpMM:DocumentID" },
79 ["InstanceID"] = { "id", "rdf:Description/xmpMM:InstanceID" },
80 ["RenditionClass"] = { "pdf", "rdf:Description/xmpMM:RenditionClass" },
81 ["VersionID"] = { "pdf", "rdf:Description/xmpMM:VersionID" },
82
83
84 ["GTS_PDFXVersion"] = { "pdf", "rdf:Description/pdfxid:GTS_PDFXVersion" },
85
86
87
88 ["Marked"] = { "pdf", "rdf:Description/xmpRights:Marked" },
89
90
91 ["WebStatement"] = { "metadata", "rdf:Description/xmpRights:WebStatement" },
92
93 ["AuthorsPosition"] = { "metadata", "rdf:Description/photoshop:AuthorsPosition" },
94 ["Copyright"] = { "metadata", "rdf:Description/photoshop:Copyright" },
95 ["CaptionWriter"] = { "metadata", "rdf:Description/photoshop:CaptionWriter" },
96
97 ["Placeholder"] = { "metadata", "pdfaid-placeholder", true }
98}
99
100lpdf.setsuppressoptionalinfo (
101 0
102 + 1
103 + 2
104 + 4
105 + 8
106 + 16
107 + 32
108 + 64
109 + 128
110 + 256
111
112)
113
114local included = backends.included
115local lpdfid = lpdf.id
116
117function lpdf.id()
118 return lpdfid(included.date)
119end
120
121local settrailerid = lpdf.settrailerid
122
123local trailerid = nil
124local dates = nil
125
126local function update()
127 if trailer_id then
128 local b = toboolean(trailer_id) or trailer_id == ""
129 if b then
130 trailer_id = "This file is processed by ConTeXt and LuaTeX."
131 else
132 trailer_id = tostring(trailer_id)
133 end
134 local h = md5HEX(trailer_id)
135 if b then
136 report_info("using frozen trailer id")
137 else
138 report_info("using hashed trailer id %a (%a)",trailer_id,h)
139 end
140 settrailerid(format("[<%s> <%s>]",h,h))
141 end
142
143 local t = type(dates)
144 if t == "number" or t == "string" then
145 local d = converters.totime(dates)
146 if d then
147 included.date = true
148 included.id = "fake"
149 report_info("forced date/time information %a will be used",lpdf.settime(d))
150 settrailerid(false)
151 return
152 end
153 if t == "string" then
154 dates = toboolean(dates)
155 included.date = dates
156 if dates ~= false then
157 included.id = true
158 else
159 report_info("no date/time but fake id information will be added")
160 settrailerid(true)
161 included.id = "fake"
162 end
163 end
164 end
165end
166
167function lpdf.settrailerid(v) trailerid = v end
168function lpdf.setdates (v) dates = v end
169
170lpdf.registerdocumentfinalizer(update,"trailer id and dates",1)
171
172directives.register("backend.trailerid", lpdf.settrailerid)
173directives.register("backend.date", lpdf.setdates)
174
175local function permitdetail(what)
176 local m = mapping[what]
177 if m then
178 return included[m[1]] and m[2]
179 else
180 return included[what] and true or false
181 end
182end
183
184lpdf.permitdetail = permitdetail
185
186
187
188local xmp, xmpfile, xmpname = nil, nil, "lpdf-pdx.xml"
189
190local function setxmpfile(name)
191 if xmp then
192 report_xmp("discarding loaded file %a",xmpfile)
193 xmp = nil
194 end
195 xmpfile = name ~= "" and name
196end
197
198codeinjections.setxmpfile = setxmpfile
199
200interfaces.implement {
201 name = "setxmpfile",
202 arguments = "string",
203 actions = setxmpfile
204}
205
206local function valid_xmp()
207 if not xmp then
208
209 if xmpfile and xmpfile ~= "" then
210 xmpfile = resolvers.findfile(xmpfile) or ""
211 end
212 if not xmpfile or xmpfile == "" then
213 xmpfile = resolvers.findfile(xmpname) or ""
214 end
215 if xmpfile ~= "" then
216 report_xmp("using file %a",xmpfile)
217 end
218 local xmpdata = xmpfile ~= "" and io.loaddata(xmpfile) or ""
219 xmp = xml.convert(xmpdata, { strip_cm_and_dt = true })
220 end
221 return xmp
222end
223
224function lpdf.addxmpinfo(tag,value,check)
225 local pattern = permitdetail(tag)
226 if type(pattern) == "string" then
227 xmlfillin(xmp or valid_xmp(),pattern,value,check)
228 end
229end
230
231
232
233local pdfaddtoinfo = lpdf.addtoinfo
234local pdfaddxmpinfo = lpdf.addxmpinfo
235
236function lpdf.addtoinfo(tag,pdfvalue,strvalue)
237 local pattern = permitdetail(tag)
238 if pattern then
239 pdfaddtoinfo(tag,pdfvalue)
240 end
241 if type(pattern) == "string" then
242 local value = strvalue or gsub(tostring(pdfvalue),"^%((.*)%)$","%1")
243 if trace_info then
244 report_info("set %a to %a",tag,value)
245 end
246 xmlfillin(xmp or valid_xmp(),pattern,value,check)
247 end
248end
249
250local pdfaddtoinfo = lpdf.addtoinfo
251
252
253
254function lpdf.insertxmpinfo(pattern,whatever,prepend)
255 xml.insert(xmp or valid_xmp(),pattern,whatever,prepend)
256end
257
258function lpdf.injectxmpinfo(pattern,whatever,prepend)
259 xml.inject(xmp or valid_xmp(),pattern,whatever,prepend)
260end
261
262function lpdf.replacexmpinfo(pattern,whatever)
263 local xmp = xmp or valid_xmp()
264 if whatever == "" then
265 xml.delete(xmp,pattern)
266 else
267 xml.replace(xmp,pattern,whatever)
268 end
269end
270
271
272
273local add_xmp_blob = true directives.register("backend.xmp",function(v) add_xmp_blob = v end)
274
275local function flushxmpinfo()
276 commands.pushrandomseed()
277 commands.setrandomseed(os.time())
278
279 local documentid = "no unique document id here"
280 local instanceid = "no unique instance id here"
281 local metadata = pdfgetmetadata()
282 local time = metadata.time
283 local producer = metadata.producer
284 local creator = metadata.creator
285
286 if included.id ~= "fake" then
287 documentid = "uuid:" .. os.uuid()
288 instanceid = "uuid:" .. os.uuid()
289 end
290
291 pdfaddtoinfo("Producer",producer)
292 pdfaddtoinfo("Creator",creator)
293 pdfaddtoinfo("CreationDate",time)
294 pdfaddtoinfo("ModDate",time)
295
296 if add_xmp_blob then
297
298 pdfaddxmpinfo("DocumentID",documentid)
299 pdfaddxmpinfo("InstanceID",instanceid)
300 pdfaddxmpinfo("Producer",producer)
301 pdfaddxmpinfo("CreatorTool",creator)
302 pdfaddxmpinfo("CreateDate",time)
303 pdfaddxmpinfo("ModifyDate",time)
304 pdfaddxmpinfo("MetadataDate",time)
305 pdfaddxmpinfo("LuaTeX.Version",metadata.luatexversion)
306 pdfaddxmpinfo("LuaTeX.Functionality",metadata.luatexfunctionality)
307 pdfaddxmpinfo("LuaTeX.LuaVersion",metadata.luaversion)
308 pdfaddxmpinfo("LuaTeX.Platform",metadata.platform)
309
310
311
312 for tag, map in next, mapping do
313 if map[3] == true then
314 local pattern = map[2]
315 if type(pattern) == "string" and xmltext(xmp,pattern) == "" then
316 xmldelete(xmp,pattern .. rep("/..",count(pattern,"/")-1))
317 end
318 end
319 end
320
321 local blob = xml.tostring(xml.first(xmp or valid_xmp(),"/x:xmpmeta"))
322 local md = pdfdictionary {
323 Subtype = pdfconstant("XML"),
324 Type = pdfconstant("Metadata"),
325 }
326 if trace_xmp then
327 report_xmp("data flushed, see log file")
328 logs.pushtarget("logfile")
329 report_xmp("start xmp blob")
330 logs.newline()
331 logs.writer(blob)
332 logs.newline()
333 report_xmp("stop xmp blob")
334 logs.poptarget()
335 end
336 blob = format(xpacket,blob)
337 if not verbose and lpdf.compresslevel() > 0 then
338 blob = gsub(blob,">%s+<","><")
339 end
340 local r = pdfflushstreamobject(blob,md,false)
341 lpdf.addtocatalog("Metadata",pdfreference(r))
342 end
343
344 commands.poprandomseed()
345end
346
347
348
349lpdf.registerdocumentfinalizer(flushxmpinfo,1,"metadata")
350
351directives.register("backend.verbosexmp", function(v)
352 verbose = v
353end)
354 |