if not modules then modules = { } end modules ['lpdf-fmt'] = { version = 1.001, comment = "companion to lpdf-ini.mkiv", author = "Peter Rolf and Hans Hagen", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files", } -- Thanks to Luigi and Steffen for testing. -- context --directives="backend.format=PDF/X-1a:2001" --trackers=backend.format yourfile local tonumber = tonumber local lower, gmatch, format, find, gsub, tohex, pack = string.lower, string.gmatch, string.format, string.find, string.gsub, string.tohex, string.pack local concat, serialize, sortedhash = table.concat, table.serialize, table.sortedhash local setmetatableindex = table.setmetatableindex local trace_format = false trackers.register("backend.format", function(v) trace_format = v end) local trace_variables = false trackers.register("backend.variables", function(v) trace_variables = v end) local report_backend = logs.reporter("backend","profiles") local pdfbackend = backends.registered.pdf ----- nodeinjections = pdfbackend.nodeinjections local codeinjections = pdfbackend.codeinjections local variables = interfaces.variables local viewerlayers = attributes.viewerlayers local colors = attributes.colors local transparencies = attributes.transparencies local lpdf = lpdf local pdfdictionary = lpdf.dictionary local pdfarray = lpdf.array local pdfconstant = lpdf.constant local pdfreference = lpdf.reference local pdfflushobject = lpdf.flushobject local pdfstring = lpdf.string local pdfverbose = lpdf.verbose local pdfflushstreamfileobject = lpdf.flushstreamfileobject local addtoinfo = lpdf.addtoinfo local injectxmpinfo = lpdf.injectxmpinfo local insertxmpinfo = lpdf.insertxmpinfo local replacexmpinfo = lpdf.replacexmpinfo local settings_to_array = utilities.parsers.settings_to_array local settings_to_hash = utilities.parsers.settings_to_hash --[[ Comments by Peter: output intent : only one profile per color space (and device class) default color space : (theoretically) several profiles per color space possible The default color space profiles define the current gamuts (part of/all the colors we have in the document), while the output intent profile declares the gamut of the output devices (the colors that we get normally a printer or monitor). Example: I have two RGB pictures (both 'painted' in /DeviceRGB) and I declare sRGB as default color space for one picture and AdobeRGB for the other. As output intent I use ISO_coated_v2_eci.icc. If I had more than one output intent profile for the combination CMYK/printer I can't decide which one to use. But it is no problem to use several default color space profiles for the same color space as it's just a different color transformation. The relation between picture and profile is clear. ]]-- local channels = { gray = 1, grey = 1, rgb = 3, cmyk = 4, } local prefixes = { gray = "DefaultGray", grey = "DefaultGray", rgb = "DefaultRGB", cmyk = "DefaultCMYK", } local formatspecification = nil local formatname = nil local pdfformats = { } local formats = utilities.storage.allocate { version = { external = 1.4, -- 'p' in name; URL reference of output intent jbig2 = 1.4, jpeg2000 = 1.5, -- not supported yet nchannel = 1.6, -- 'n' in name; n-channel colorspace support prepress = 1.3, -- 'g' in name; reference to external graphics layers = 1.5, transparency = 1.4, compression = 1.5, attachments = 1.7, }, default = { version = 1.7, -- todo: block tex primitive format = "default", xmpfile = "lpdf-pdx.xml", gray = true, cmyk = true, rgb = true, spot = true, calibrated = true, -- rgb colors, not used cielab = true, -- unknown nchannel = true, -- unknown internal = true, -- controls profile inclusion external = true, -- controls profile inclusion intents = true, prepress = true, -- unknown layers = true, -- todo: block at lua level transparency = true, -- todo: block at lua level jbig2 = true, -- todo: block at lua level (dropped anyway) jpeg2000 = true, -- todo: block at lua level (dropped anyway) cidsets = false, -- only some standards charsets = true, -- not used procsets = false, -- also version driven info = true, acrobat = true, attachments = true, compression = false, -- object compression tagging = false, forms = false, metadata = { }, }, data = pdfformats, } setmetatableindex(pdfformats,function(t,k) k = lower(k) k = gsub(k,"^pdf","") k = gsub(k,"[^a-z0-9]+","") local v = rawget(t,k) if not v then local filename = "lpdf-fmt-imp-" .. k .. ".lmt" local fullname = resolvers.findfile(filename) or "" if fullname ~= "" then v = dofile(fullname) if v then v = v.features end if v then setmetatableindex(v,formats.default) else v = false end t[k] = v end end return v end) lpdf.formats = formats -- it does not hurt to have this one visible formatspecification = formats.default function lpdf.getformatoption(key) return formatspecification and formatspecification[key] end -- function codeinjections.getformatspecification() -- return formatspecification -- end function lpdf.supportedformats() local found = resolvers.findfile("lpdf-fmt-imp-a4.lmt") if found then local pattern = gsub(found,"a4.lmt", "*.lmt") local list = dir.glob(pattern) local count = 0 for i=1,#list do local n = gsub(file.nameonly(list[i]),"lpdf.fmt.imp.","") local d = pdfformats[n] end end return table.sortedkeys(pdfformats) end codeinjections.getformatoption = lpdf.getformatoption codeinjections.supportedformats = lpdf.supportedformats -- When we have an URL we can assume that embedding is not needed. local filenames = { "colorprofiles.xml", "colorprofiles.lua", } local function locatefile(filename) local fullname = resolvers.findfile(filename,"icc",1,true) if not fullname or fullname == "" then fullname = resolvers.finders.byscheme("loc",filename) -- could be specific to the project end return fullname or "" end local internalprofiles = { } local externalprofiles = { } local defaultprofiles = { } local loadeddefaults = { } local loadedprofiles = { } local function loadprofile(name,filename) local profile = loadedprofiles[name] if profile ~= nil then return profile end local databases = filename and filename ~= "" and settings_to_array(filename) or filenames for i=1,#databases do local filename = locatefile(databases[i]) if filename and filename ~= "" then local suffix = file.suffix(filename) local lname = lower(name) if suffix == "xml" then local xmldata = xml.load(filename) -- no need for caching it if xmldata then profile = xml.filter(xmldata,format('xml://profiles/profile/(info|filename)[lower(text())=="%s"]/../table()',lname)) end elseif suffix == "lua" then local luadata = loadfile(filename) luadata = ludata and luadata() if luadata then profile = luadata[name] or luadata[lname] -- hashed if not profile then for i=1,#luadata do local li = luadata[i] if lower(li.info) == lname then -- indexed profile = li break end end end end end if profile then if not next(profile) then profile = false end loadedprofiles[name] = profile if profile then report_backend("profile specification %a loaded from %a",name,filename) elseif trace_format then report_backend("profile specification %a loaded from %a but empty",name,filename) end return profile end end end report_backend("profile specification %a not found in %a",name,concat(filenames, ", ")) end local function profilename(filename) return lower(file.basename(filename)) end local function includeprofile(s) local filename = s.filename or "" local colorspace = s.colorspace or "" local colorspace = lower(colorspace) local channel = channels[colorspace] or nil if filename == "" or colorspace == "" then report_backend("error in profile specification: %s",serialize(s,false)) else local tag = profilename(filename) local profile = internalprofiles[tag] or externalprofiles[tag] if not profile then -- We check if the profile has an id other than "custom". If so, we -- don't embed it but create a reference. local id = s.id if id and lower(id) ~= "custom" then local name = s.info or s.filename or "" local d = pdfdictionary { ProfileName = name ~= "" and name or nil, ProfileCS = colorspace, URLs = urls(s.url or ""), } profile = pdfflushobject(d) externalprofiles[tag] = profile -- defaultprofiles[channel] = profile end end if not profile then local fullname = locatefile(filename) if fullname == "" then fullname = locatefile("colo-imp-" .. filename) end if fullname == "" then report_backend("error, couldn't locate profile %a",filename) elseif not channel then report_backend("error, couldn't resolve channel entry for colorspace %a",colorspace) else profile = pdfflushstreamfileobject(fullname,pdfdictionary{ N = channel },false) -- uncompressed internalprofiles[tag] = profile defaultprofiles[channel] = profile if trace_format then report_backend("including %a color profile from %a",colorspace,fullname) end end end return profile end end function codeinjections.defaultprofile(channel) -- print("get profile",channel,defaultprofiles[channel]) return defaultprofiles[channel] end local function urls(url) if not url or url == "" then return nil else local u = pdfarray() for url in gmatch(url,"([^, ]+)") do if find(url,"^http") then u[#u+1] = pdfdictionary { FS = pdfconstant("URL"), F = pdfstring(url), } end end return u end end local function processprofile(s,spec) -- specification local filename = s.filename or "" local colorspace = lower(s.colorspace or "") if filename == "" or colorspace == "" then report_backend("error in default profile specification: %s",serialize(s,false)) elseif not loadeddefaults[colorspace] then local tag = profilename(filename) local n = internalprofiles[tag] or externalprofiles[tag] if n == true then -- not internalized report_backend("no default profile %a for colorspace %a",filename,colorspace) elseif n then local a = pdfarray { pdfconstant("ICCBased"), pdfreference(n), } -- used in page /Resources, so this must be inserted at runtime lpdf.adddocumentcolorspace(prefixes[colorspace],pdfreference(pdfflushobject(a))) loadeddefaults[colorspace] = true report_backend("setting %a as default %a color space",filename,colorspace) else report_backend("no default profile %a for colorspace %a",filename,colorspace) end elseif trace_format then report_backend("a default %a colorspace is already in use",colorspace) end end -- pdfa sRGB GTS_PDFA1 -- pdfx CMYK GTS_PDFX -- pdfe ISO_PDFE1 -- OutputCondition : optional, meant for user interfacing -- OutputConditionIdentifier : mandate, something official -- sRGB v4 Preference -- PSOuncoated_v3_FOGRA52.icc (european) local loadedintents = { } local intents = pdfarray() local function processoutputintent(s,spec) local url = s.url or "" local filename = s.filename or "" local name = s.info or filename local id = s.id or "" local outputcondition = s.outputcondition or "" local info = s.info or "" if name == "" or id == "" then report_backend("error in output intent specification: %s",serialize(s,false)) elseif not loadedintents[name] then local tag = profilename(filename) local internal = internalprofiles[tag] local external = externalprofiles[tag] -- -- The OutputConditionIdentifier is mandate and can be an officially registered -- one. When we have an unofficial one, we need to provide DestOutputProfile -- but DestOutputProfileRef i soptional and can best be omitted. -- -- We can use Custom as indicator for a to be embedded profile and -- thereby play safe. In that case whatever also makes sense. -- if internal or external then local d = { Type = pdfconstant("OutputIntent"), -- mandate S = pdfconstant(spec.intent or "GTS_PDFX"), OutputConditionIdentifier = id, } if internal then -- optional d.RegistryName = url d.OutputCondition = outputcondition d.Info = info -- needed d.DestOutputProfile = pdfreference(internal) else -- an external one mandates a known id to be used end intents[#intents+1] = pdfreference(pdfflushobject(pdfdictionary(d))) -- if trace_format then report_backend("setting output intent to %a with id %a for entry %a",name,id,#intents) -- end else report_backend("invalid output intent %a",name) end loadedintents[name] = true elseif trace_format then report_backend("an output intent with name %a is already in use",name) end end -- In order to get a bit more reliable validation we made some choices. A mix of -- internal and external profiles gives a mess so we can best only handle embeded -- ones and just include the smallest possible set. Users who deal with real complex -- icc profiles (likely cmyk) will embed these anyway. For a previous implementation -- one can look in the archives (ok mkiv). We kind of assume PDF/A-4 to be used. local function handleiccprofile(message,spec,name,filename,dowithprofile) -- name is an intent or profile list if name and name ~= "" then local list = settings_to_array(name) for i=1,#list do local name = list[i] local profile = loadprofile(name,filename) if trace_format then report_backend("handling %s %a",message,name) end -- if not profile and file.suffix(name) == "icc" then -- so we have no description ... we can create a bogus entry: profile = { filename = name, id = "Custom", colorspace = find(lower(name),"rgb") and "RGB" or "CMYK", } if trace_format then report_backend("forcing internal profiles %a",name) end end -- if profile then if formatspecification.cmyk then profile.colorspace = profile.colorspace or "CMYK" else profile.colorspace = profile.colorspace or "RGB" end if trace_format then report_backend("handling internal profiles cf. %a",name) end includeprofile(profile) dowithprofile(profile,spec) elseif trace_format then report_backend("unknown profile %a",name) end end return list[1] end end local function flushoutputintents() if #intents > 0 then lpdf.addtocatalog("OutputIntents",pdfreference(pdfflushobject(intents))) end end lpdf.registerdocumentfinalizer(flushoutputintents,2,"output intents") function codeinjections.setformat(s) local format = s.format or "" local intent = s.intent or "" local profile = s.profile or "" local option = s.option or "" local filename = s.file or "" -- profile databases local level = tonumber(s.level) -- if format ~= "" then local spec = pdfformats[format] if spec then formatspecification = spec formatname = spec.format report_backend("setting format %a to %a",format,formatname) local xmpfile = formatspecification.xmpfile or "" if xmpfile == "" then -- weird error else codeinjections.setxmpfile(xmpfile) end if profile == "" then if intent == variables.default or intent == variables.rgb then intent = "sRGB-v4.icc" profile = "sRGB-v4.icc,CGATS001Compat-v2-micro.icc,sGrey-v4.icc" elseif intent == variables.cmyk then intent = "CGATS001Compat-v2-micro.icc" profile = "sRGB-v4.icc,CGATS001Compat-v2-micro.icc" else goto SKIP end report_backend("using color intent %a",intent) report_backend("using icc profiles %a",profile) ::SKIP:: elseif intent == variables.default or intent == variables.rgb or intent == variables.cmyk then -- these are non existent intent = "" end if not level then level = 3 -- good compromise, default anyway end local pdfversion = spec.version * 10 local injectmetadata = spec.metadata -- local injectconformance = spec.conformance local majorversion = math.floor(math.div(pdfversion,10)) local minorversion = math.floor(math.mod(pdfversion,10)) local objectcompression = spec.compression and pdfversion >= 15 local compresslevel = level or lpdf.compresslevel() -- keep default local objectcompresslevel = (objectcompression and (level or lpdf.objectcompresslevel())) or 0 lpdf.setcompression(compresslevel,objectcompresslevel) lpdf.setversion(majorversion,minorversion) if objectcompression then report_backend("forcing pdf version %s.%s, compression level %s, object compression level %s", majorversion,minorversion,compresslevel,objectcompresslevel) elseif compresslevel > 0 then report_backend("forcing pdf version %s.%s, compression level %s, object compression disabled", majorversion,minorversion,compresslevel) else report_backend("forcing pdf version %s.%s, compression disabled", majorversion,minorversion) end -- -- maybe block by pdf version -- codeinjections.settaggingsupport(formatspecification.tagging) codeinjections.setattachmentsupport(formatspecification.attachments) -- -- context.setupcolors { -- not this way -- cmyk = spec.cmyk and variables.yes or variables.no, -- rgb = spec.rgb and variables.yes or variables.no, -- } -- local rgb = spec.rgb and variables.yes or variables.no local cmy = spec.cmyk and variables.yes or variables.no report_backend("permitted colorspaces: rgb %a, cmyk %a",rgb,cmy) -- token.expandmacro ("colo_force_colormodel",true,rgb,true,cmy) -- colors.forcesupport( spec.gray or false, spec.rgb or false, spec.cmyk or false, spec.spot or false, spec.nchannel or false ) transparencies.forcesupport( spec.transparency or false ) viewerlayers.forcesupport( spec.layers or false ) viewerlayers.setfeatures( spec.has_order or false -- new ) -- -- spec.jbig2 : todo, block in image inclusion -- spec.jpeg2000 : todo, block in image inclusion -- if type(injectmetadata) == "function" then injectmetadata() elseif type(injectmetadata) == "table" then for i=1,#injectmetadata do local entry = injectmetadata[i] local action = lpdf[entry[1]] if action then action(entry[2],entry[3],entry[4]) end end end -- -- If needed we can make a dedicated imp file for this but currently we -- consider this not that useful, so we go for less code instead. -- -- if type(injectconformance) == "function" then -- injectconformance(environment.arguments.wtpdf) -- elseif type(injectconformance) == "table" then -- for i=1,#injectconformance do -- local entry = injectconformance[i] -- local action = lpdf[entry[1]] -- if action then -- action(entry[2],entry[3],entry[4]) -- end -- end -- end -- local first = handleiccprofile("color profile",spec,profile,filename,processprofile) if intent == "" then intent = first end handleiccprofile("output intent",spec,intent,filename,processoutputintent) -- if trace_variables then for k, v in sortedhash(formats.default) do local v = formatspecification[k] if type(v) ~= "function" then report_backend("%a = %a",k,v or false) end end end function codeinjections.setformat(noname) if trace_format then report_backend("error, format is already set to %a, ignoring %a",formatname,noname.format) end end else report_backend("error, format %a is not supported, valid formats: % | t", format,table.sortedkeys(formats.data)) end elseif level then lpdf.setcompression(level,level) else -- we ignore this as we hook it in \everysetupbackend end end directives.register("backend.format", function(v) -- table ! local tv = type(v) if tv == "table" then codeinjections.setformat(v) elseif tv == "string" then codeinjections.setformat { format = v } end end) interfaces.implement { name = "setformat", actions = codeinjections.setformat, arguments = { { "*" } } }