if not modules then modules = { } end modules ['lpdf-xmp'] = { 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", comment = "with help from Peter Rolf", } local tostring, type = tostring, type local format, gsub, match, rep, count = string.format, string.gsub, string.match, string.rep, string.count local concat = table.concat local utfchar = utf.char local md5HEX = md5.HEX local xmlfillin, xmldelete, xmltext = xml.fillin, xml.delete, xml.text local osdate, ostime, ostimezone, osuuid = os.date, os.time, os.timezone, os.uuid local settings_to_array = utilities.parsers.settings_to_array local trace_xmp = false trackers.register("backend.xmp", function(v) trace_xmp = v end) local trace_info = false trackers.register("backend.info", function(v) trace_info = v end) local report_xmp = logs.reporter("backend","xmp") local report_info = logs.reporter("backend","info") local backends = backends local pdfbackend = backends.registered.pdf local codeinjections = pdfbackend.codeinjections local lpdf = lpdf local pdfdictionary = lpdf.dictionary local pdfconstant = lpdf.constant local pdfunicode = lpdf.unicode local pdfstring = lpdf.string local pdfreference = lpdf.reference local pdfflushstreamobject = lpdf.flushstreamobject -- The XMP packet wrapper is kind of fixed, see page 10 of XMPSpecificationsPart1.pdf from -- XMP-Toolkit-SDK-CC201607.zip. So we hardcode the id. local xpacket = format ( [[ %%s ]], utfchar(0xFEFF) ) local unknown = { false, false } local mapping = table.setmetatableindex ( { -- user defined keys (pdfx:) ["ConTeXt.Jobname"] = { "context", "rdf:Description/pdfx:ConTeXt.Jobname" }, ["ConTeXt.Time"] = { "date", "rdf:Description/pdfx:ConTeXt.Time" }, ["ConTeXt.Url"] = { "context", "rdf:Description/pdfx:ConTeXt.Url" }, ["ConTeXt.Support"] = { "context", "rdf:Description/pdfx:ConTeXt.Support" }, ["ConTeXt.Version"] = { "context", "rdf:Description/pdfx:ConTeXt.Version" }, ["TeX.Support"] = { "metadata","rdf:Description/pdfx:TeX.Support" }, ["LuaTeX.Version"] = { "metadata","rdf:Description/pdfx:LuaTeX.Version" }, ["LuaTeX.Functionality"] = { "metadata","rdf:Description/pdfx:LuaTeX.Functionality" }, ["LuaTeX.LuaVersion"] = { "metadata","rdf:Description/pdfx:LuaTeX.LuaVersion" }, ["LuaTeX.Platform"] = { "metadata","rdf:Description/pdfx:LuaTeX.Platform" }, ["ID"] = { "id", "rdf:Description/pdfx:ID" }, -- has date -- Adobe PDF schema ["Keywords"] = { "metadata","rdf:Description/pdf:Keywords", true }, ["Producer"] = { "metadata","rdf:Description/pdf:Producer", true }, -- ["Trapped"] = { "pdf", "rdf:Description/pdf:Trapped" }, -- '/False' in /Info, but 'False' in XMP -- Dublin Core schema ["Format"] = { "metadata","rdf:Description/dc:format" }, -- optional, but nice to have -- see xml file for comment: -- ["Author"] = { "metadata","rdf:Description/dc:creator" }, -- ["Subject"] = { "metadata","rdf:Description/dc:description" }, -- ["Title"] = { "metadata","rdf:Description/dc:title" }, ["Author"] = { "metadata","rdf:Description/dc:creator/rdf:Seq/rdf:li", true }, ["Subject"] = { "metadata","rdf:Description/dc:description/rdf:Alt/rdf:li", true }, ["Title"] = { "metadata","rdf:Description/dc:title/rdf:Alt/rdf:li", true }, -- XMP Basic schema ["CreateDate"] = { "date", "rdf:Description/xmp:CreateDate" }, ["CreationDate"] = { "date", "rdf:Description/xmp:CreationDate" }, -- dummy ["CreatorTool"] = { "metadata","rdf:Description/xmp:CreatorTool" }, -- ["Creator"] = { "metadata","rdf:Description/xmp:CreatorTool" }, ["MetadataDate"] = { "date", "rdf:Description/xmp:MetadataDate" }, ["ModDate"] = { "date", "rdf:Description/xmp:ModDate" }, -- dummy ["ModifyDate"] = { "date", "rdf:Description/xmp:ModifyDate" }, -- XMP Media Management schema ["DocumentID"] = { "id", "rdf:Description/xmpMM:DocumentID" }, -- uuid ["InstanceID"] = { "id", "rdf:Description/xmpMM:InstanceID" }, -- uuid ["RenditionClass"] = { "pdf", "rdf:Description/xmpMM:RenditionClass" }, -- PDF/X-4 ["VersionID"] = { "pdf", "rdf:Description/xmpMM:VersionID" }, -- PDF/X-4 -- additional entries -- PDF/X ["GTS_PDFXVersion"] = { "pdf", "rdf:Description/pdfxid:GTS_PDFXVersion" }, -- optional entries -- all what is visible in the 'document properties --> additional metadata' window -- XMP Rights Management schema (optional) ["Marked"] = { "pdf", "rdf:Description/xmpRights:Marked" }, -- ["Owner"] = { "metadata", "rdf:Description/xmpRights:Owner/rdf:Bag/rdf:li" }, -- maybe useful (not visible) -- ["UsageTerms"] = { "metadata", "rdf:Description/xmpRights:UsageTerms" }, -- maybe useful (not visible) ["WebStatement"] = { "metadata", "rdf:Description/xmpRights:WebStatement" }, -- Photoshop PDF schema (optional) ["AuthorsPosition"] = { "metadata", "rdf:Description/photoshop:AuthorsPosition" }, ["Copyright"] = { "metadata", "rdf:Description/photoshop:Copyright" }, ["CaptionWriter"] = { "metadata", "rdf:Description/photoshop:CaptionWriter" }, -- ["Placeholder"] = { "metadata", "pdfaid-placeholder", true } }, function() return unknown end ) local metadata = nil local trailerid = true local creationdate = false local modificationdate = false local function pdftimestamp(str) local t = type(str) if t == "string" then 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)$") return Y and format("D:%s%s%s%s%s%s%s%s'%s",Y,M,D,h,m,s,Zs,Zh,Zm) else return osdate("D:%Y%m%d%H%M%S",t == "number" and str or ostime()) -- maybe "!D..." : universal time end end local function pdfgetmetadata() if not metadata then local contextversion = environment.version local luatexversion = LUATEXVERBOSE local luatexfunctionality = tostring(LUATEXFUNCTIONALITY) local jobname = environment.jobname or tex.jobname or "unknown" local documentid = trailerid and ("uuid:" .. osuuid()) or "no unique document id here" local instanceid = trailerid and ("uuid:" .. osuuid()) or "no unique instance id here" local producer = "LuaMetaTeX" local creator = format("LuaMetaTeX %s %s + ConTeXt LMTX %s",luatexversion,luatexfunctionality,contextversion) metadata = creationdate and { producer = producer, creator = creator, id = format("%s | %s",jobname,creationdate), documentid = documentid, instanceid = instanceid, jobname = jobname, -- luatexversion = luatexversion, contextversion = contextversion, luatexfunctionality = luatexfunctionality, luaversion = tostring(LUAVERSION), platform = os.platform, creationdate = creationdate, modificationdate = modificationdate, } or { producer = producer, creator = creator, id = jobname, documentid = documentid, instanceid = instanceid, jobname = jobname, } -- inspect(metadata) end return metadata end local function pdfsetmetadate(n,both) if n then n = converters.totime(n) if n then creationdate = osdate("%Y-%m-%dT%H:%M:%S",ostime(n)) .. ostimezone() if both then modificationdate = creationdate end end end return creationdate end lpdf.pdftimestamp = pdftimestamp function lpdf.gettrailerid() if trailerid == true then return md5.HEX(osuuid()) elseif type(trailerid) == "string" then return md5.HEX(trailerid) else return false end end -- string: use that, true: uuid, false: nothing directives.register("backend.trailerid", function(v) trailerid = type(v) and v or toboolean(v) end) -- year-mm-dd : use that for creation and modification local function setdates(v) local t = type(v) if t == "number" or t == "string" then local d = converters.totime(v) if d then report_info("forced date/time information %a will be used",pdfsetmetadate(d,true)) return end end if toboolean(v) then creationdate = osdate("%Y-%m-%dT%H:%M:%S") .. ostimezone() modificationdate = creationdate else creationdate = false modificationdate = false end end setdates(true) directives.register("backend.date", setdates) -- maybe some day we will load the xmp file at runtime local xmp, xmpfile, xmpname = nil, nil, "lpdf-pdx.xml" local function setxmpfile(name) if xmp then report_xmp("discarding loaded file %a",xmpfile) xmp = nil end xmpfile = name ~= "" and name end codeinjections.setxmpfile = setxmpfile interfaces.implement { name = "setxmpfile", arguments = "string", actions = setxmpfile } local function valid_xmp() if not xmp then -- local xmpfile = xmpfile or resolvers.findfile(xmpname) or "" if xmpfile and xmpfile ~= "" then xmpfile = resolvers.findfile(xmpfile) or "" end if not xmpfile or xmpfile == "" then xmpfile = resolvers.findfile(xmpname) or "" end if xmpfile ~= "" then report_xmp("using file %a",xmpfile) end local xmpdata = xmpfile ~= "" and io.loaddata(xmpfile) or "" xmp = xml.convert(xmpdata) end return xmp end function lpdf.addxmpinfo(tag,value,check) local pattern = mapping[tag][2] if type(pattern) == "string" then if not xmp then xmp = valid_xmp() end if xmp and value then xmlfillin(xmp,pattern,value,check) end end end -- redefined local pdfaddtoinfo = lpdf.addtoinfo local pdfaddxmpinfo = lpdf.addxmpinfo function lpdf.addtoinfo(tag,pdfvalue,strvalue) local pattern = mapping[tag][2] if pattern or strvalue == true then pdfaddtoinfo(tag,pdfvalue) end if type(pattern) == "string" then local value = (type(strvalue) == "string" and strvalue) or gsub(tostring(pdfvalue),"^%((.*)%)$","%1") -- hack if trace_info then report_info("set %a to %a",tag,value) end xmlfillin(xmp or valid_xmp(),pattern,value,check) end end local pdfaddtoinfo = lpdf.addtoinfo -- used later -- for the do-it-yourselvers function lpdf.insertxmpinfo(pattern,whatever,prepend) xml.insert(xmp or valid_xmp(),pattern,whatever,prepend) end function lpdf.injectxmpinfo(pattern,whatever,prepend) xml.inject(xmp or valid_xmp(),pattern,whatever,prepend) end function lpdf.replacexmpinfo(pattern,whatever) xml.replace(xmp or valid_xmp(),pattern,whatever) end -- flushing local add_xmp_blob = true -- indentity_done = false -- using "setupidentity = function() end" fails as the meaning is frozen in register local checkidentity checkidentity = function(metadata) local identity = interactions.general.getidentity() metadata.title = identity.title metadata.subtitle = identity.subtitle metadata.author = identity.author metadata.date = identity.date metadata.keywords = identity.keywords checkidentity = false end local function setupidentity() -- if not identity_done then -- local metadata = pdfgetmetadata() if checkidentity then checkidentity(metadata) end local title = metadata.title local subtitle = metadata.subtitle local author = metadata.author local date = metadata.date local keywords = metadata.keywords -- if date and date ~= "" then pdfsetmetadate(date) end if keywords then keywords = concat(settings_to_array(keywords), " ") end -- local creator = metadata.creator local contextversion = metadata.contextversion local id = metadata.id local jobname = metadata.jobname local creation = metadata.creationdate local modification = metadata.modificationdate -- if creator then pdfaddtoinfo("Creator",pdfunicode(creator),creator) end if creation then pdfaddtoinfo("CreationDate",pdfstring(pdftimestamp(creation)),creation) end if modification then pdfaddtoinfo("ModDate",pdfstring(pdftimestamp(modification)),modification) end if id then pdfaddtoinfo("ID",pdfstring(id),id) -- needed for pdf/x end -- if title and title ~= "" then pdfaddtoinfo("Title",pdfunicode(title),title) end if subtitle and subtitle ~= "" then pdfaddtoinfo("Subject",pdfunicode(subtitle),subtitle) end if author and author ~= "" then pdfaddtoinfo("Author",pdfunicode(author),author) end if keywords and keywords ~= "" then pdfaddtoinfo("Keywords",pdfunicode(keywords),keywords) end -- if contextversion then pdfaddtoinfo("ConTeXt.Version",contextversion) end if creation then pdfaddtoinfo("ConTeXt.Time",creation) end if jobname then pdfaddtoinfo("ConTeXt.Jobname",jobname) end -- -- pdfaddtoinfo("ConTeXt.Url","www.pragma-ade.com") pdfaddtoinfo("ConTeXt.Url","github.com/contextgarden/context") pdfaddtoinfo("ConTeXt.Support","contextgarden.net") pdfaddtoinfo("TeX.Support","tug.org") -- -- identity_done = true -- else -- -- no need for a message -- end return metadata end local function flushxmpinfo() commands.pushrandomseed() commands.setrandomseed(ostime()) local metadata = setupidentity() -- tod: merge into here and save code -- local metadata = pdfgetmetadata() -- if checkidentity then -- checkidentity(metadata) -- end local creation = metadata.time or metadata.creationdate or creationdate local modification = metadata.time or metadata.modificationdate or modificationdate or creation local producer = metadata.producer local creator = metadata.creator local documentid = metadata.documentid local instanceid = metadata.instanceid pdfaddtoinfo("Producer",producer) pdfaddtoinfo("Creator",creator) pdfaddtoinfo("CreationDate",creation) pdfaddtoinfo("ModDate",modification) if add_xmp_blob then pdfaddxmpinfo("DocumentID",documentid) pdfaddxmpinfo("InstanceID",instanceid) pdfaddxmpinfo("Producer",producer) pdfaddxmpinfo("CreatorTool",creator) pdfaddxmpinfo("CreateDate",creation) pdfaddxmpinfo("ModifyDate",modification) pdfaddxmpinfo("MetadataDate",creation) pdfaddxmpinfo("LuaTeX.Version",metadata.luatexversion) pdfaddxmpinfo("LuaTeX.Functionality",metadata.luatexfunctionality) pdfaddxmpinfo("LuaTeX.LuaVersion",metadata.luaversion) pdfaddxmpinfo("LuaTeX.Platform",metadata.platform) local title = metadata.title local subtitle = metadata.subtitle local author = metadata.author local keywords = metadata.keywords -- We need to wipe some fields in the xml because otherwise validators -- complain ... they don't see an empty (nonexistent, default) info field -- as being the same as an empty element. if title and title ~= "" then pdfaddxmpinfo("Title",pdfunicode(title),title) end if subtitle and subtitle ~= "" then pdfaddxmpinfo("Subject",pdfunicode(subtitle),subtitle) end if author and author ~= "" then pdfaddxmpinfo("Author",pdfunicode(author),author) end if keywords and keywords ~= "" then pdfaddxmpinfo("Keywords",pdfunicode(author),author) end -- checks for empty: for tag, map in next, mapping do if map[3] == true then local pattern = map[2] if type(pattern) == "string" and xmltext(xmp,pattern) == "" then xmldelete(xmp,pattern .. rep("/..",count(pattern,"/")-1)) end end end local blob = xml.tostring(xml.first(xmp or valid_xmp(),"/x:xmpmeta")) local md = pdfdictionary { Subtype = pdfconstant("XML"), Type = pdfconstant("Metadata"), } if trace_xmp then report_xmp("data flushed, see log file") logs.pushtarget("logfile") report_xmp("start xmp blob") logs.newline() logs.writer(blob) logs.newline() report_xmp("stop xmp blob") logs.poptarget() end blob = format(xpacket,blob) if not verbose and lpdf.compresslevel() > 0 then blob = gsub(blob,">%s+<","><") else -- todo: lpeg while true do local b = gsub(blob,"\n +\n( +<)","\n%1") if b == blob then break else blob = b end end end local r = pdfflushstreamobject(blob,md,false) -- uncompressed lpdf.addtocatalog("Metadata",pdfreference(r)) end commands.poprandomseed() -- hack end -- lpdf.registerpagefinalizer(setupidentity,"identity") lpdf.registerdocumentfinalizer(flushxmpinfo,1,"metadata") directives.register("backend.xmp", function(v) add_xmp_blob = v end) directives.register("backend.verbosexmp", function(v) verbose = v end)