if not modules then modules = { } end modules ['lpdf-lmt'] = { version = 1.001, optimize = true, 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" } -- The code below was originally in back-lpd.lua but it makes more sense in this -- namespace. I will rename variables. Because we run into the 200 locals this file -- has to be split. -- There is no way that a lua based backend can compete performance wise with the -- original one for relative simple text runs. And we're talking seconds here on say -- 500 pages with paragraphs alternativng between three fonts and colors. But such -- documents are rare so in practice we are quite okay, especially because in -- ConTeXt we can gain quite a bit elsewhere. So, when we loose 30% on such simple -- documents, we break even on for instance the manual, and gain 30% on Thomas's -- turture test (also for other reasons). But .. who knows what magic I can cook up -- in due time. -- If you consider this complex, watch: -- -- https://www.youtube.com/watch?v=6H-cAzfB2qo -- -- or in distractionmode: -- -- https://www.youtube.com/watch?v=TYuTE_1jvvE -- https://www.youtube.com/watch?v=nnicGKX3lvM -- -- For the moment we have to support the built in backend as well as the alternative. So -- the next interface is suboptimal and will change at some time. At that moment I will -- also optimize and extend. local type, next, unpack, tonumber, rawget = type, next, unpack, tonumber, rawget local char, rep, find = string.char, string.rep, string.find local formatters, splitupstring = string.formatters, string.splitup local concat, sortedhash = table.concat, table.sortedhash local setmetatableindex = table.setmetatableindex local loaddata = io.loaddata local ceil = math.ceil local bpfactor = number.dimenfactors.bp local osuuid = os.uuid local zlibcompresssize = xzip.compresssize local nuts = nodes.nuts local pdfreference = lpdf.reference local pdfdictionary = lpdf.dictionary local pdfarray = lpdf.array local pdfconstant = lpdf.constant local pdfliteral = lpdf.literal -- not to be confused with a whatsit! local pdfreserveobject -- forward reference local pdfpagereference -- forward reference local pdfgetpagereference -- forward reference local pdfsharedobject -- forward reference local pdfflushobject -- forward reference local pdfflushstreamobject -- forward reference local pdfdeferredobject -- forward reference local pdfimmediateobject -- forward reference local pdfincludeimage -- forward reference local pdf_pages = pdfconstant("Pages") local pdf_page = pdfconstant("Page") local pdf_xobject = pdfconstant("XObject") local pdf_form = pdfconstant("Form") local pdf_pattern = pdfconstant("Pattern") local fonthashes = fonts.hashes local characters = fonthashes.characters local descriptions = fonthashes.descriptions local parameters = fonthashes.parameters local properties = fonthashes.properties local report = logs.reporter("backend") local report_objects = logs.reporter("backend","objects") local report_fonts = logs.reporter("backend","fonts") local report_encryption = logs.reporter("backend","encryption") local trace_objects = false trackers.register("backend.objects", function(v) trace_objects = v end) local trace_details = false trackers.register("backend.details", function(v) trace_details = v end) local trace_indices = false trackers.register("backend.fonts.details", function(v) trace_indices = v end) -- These two tables used a font id as index and will be metatabled in lpdf-emb.lmt: local usedfontnames = { } local usedfontobjects = { } lpdf.usedfontnames = usedfontnames lpdf.usedfontobjects = usedfontobjects -- experiment: local function compressdata(data,size) local guess = ((size // 4096) + 1) * 2048 local comp = zlibcompresssize(data,guess,3) -- if comp then -- report() -- report("size %i, guess %i, result %i => %s / %s",size,guess,#comp,guess>=#comp and "hit" or "miss") -- report() -- end return comp end -- local function compressdata(data,size) -- return zlibcompress(data,3) -- end -- we collect them: local flushers = { } -- used variables local pdf_h = 0 local pdf_v = 0 local need_tm local need_tf local need_font ----- cur_tmrx local need_width local need_mode local done_width local done_mode local mode local current_pdf ----- f_pdf_cur local current_font local current_effect local current_slant local current_weight local current_sx local current_sy local current_factor local f_x_scale local f_y_scale local tj_scale local tj_delta local tj_position ----- tmrx, tmry, tmsx, tmsy, tmtx, tmty local tmrx, tmry, tmsy, tmtx, tmty ----- cmrx, cmry, cmsx, cmsy, cmtx, cmty local cmrx, cmry, cmtx, cmty local tmef local tmef_f_x_scale local tmef_f_w_scale local usedfonts, usedxforms, usedximages, usedxgroups local getxformname, getximagename local boundingbox, shippingmode, objectnumber local function usefont(t,k) -- a bit redundant hash -- local v = pdfgetfontname(k) local v = usedfontnames[k] t[k] = v return v end local function reset_variables(specification) pdf_h, pdf_v = 0, 0 cmrx, cmry = 1.0, 1.0 -- cmsx, cmsy = 0.0, 0.0 cmtx, cmty = 0.0, 0.0 tmrx, tmry = 1.0, 1.0 -- tmsx, tmsy = 0.0, 0.0 tmsy = 0.0 -- sy makes no sense tmtx, tmty = 0.0, 0.0 tmef = 1.0 need_tm = false need_tf = false need_font = true need_width = 0 need_mode = 0 done_width = false done_mode = false mode = "page" shippingmode = specification.shippingmode objectnumber = specification.objectnumber -- cur_tmrx = 0.0 -- f_pdf_cur = 0 -- nullfont tj_scale = 1 tj_delta = 0.0 tj_position = 0.0 current_font = 0 current_pdf = 0 -- nullfont current_effect = nil current_slant = 0 current_weight = 0 current_factor = 0 current_sx = 1 current_sy = 1 f_x_scale = 1.0 f_y_scale = 1.0 tmef_f_x_scale = 1 tmef_f_w_scale = 1 usedfonts = setmetatableindex(usefont) usedxforms = { } usedximages = { } -- usedxgroups = { } boundingbox = specification.boundingbox end -- buffer local buffer = lua.newtable(1024,0) -- { } local b = 0 local function reset_buffer() b = 0 end -- fonts -- The text in a PDF file ends up in the page stream. Right from the start PDF files -- were supposed to be efficient but over time that concept was messed up by for -- instance mixed in tagging. One can argue if that was a good idea: just embed the -- source and use that as for special purposes instead of trying to turn something -- visual into something structure. We have these angle bracket formats for that -- already. Anyway, here we assume an uninterupted stream. -- -- A sequence of characters in the same font and with the same scale (and other -- rendering properties). When we started with the backend in Lua I just followed -- the same approach as in LuaTeX (which was modelled after pdfTeX) but at some -- point a (stepwise) transition took place. For instance, we always use a 10bp font -- size (instead of the scale in the document) and delegate all scaling to the font -- transform matrix. Because the text stream uses a different coordinate system -- (font units combined with 1000 based kerning) we need to keep track of where we -- are and when we drift too much we need to restart. In the engines this is -- complicated by integer math so there drift really plays a role, but in our case -- we can now approach it a bit different, although we don't really know the -- acceptable thresholds. This code evolved over time, also because we introduced -- additional scaling options. -- -- So, in the backend we have to deal with this: -- -- - positioning in target space (page) -- - positioning in the stream (text) -- - glyph scale (!), glyph xscale (!), glyph yscale -- - font scale (!) -- - horizontal extend (!) -- - vertical squeeze -- - slant -- - expansion factor (!) -- - font effects (mode and width/line) -- - temporary glyph effects (extend, squeeze, slant, mode, width/line) -- - drift (synchronization and check for threshold) -- -- This means that we have to check if one of these properties has changed and the -- horizontal scale is the most demanding one. We also need to mix the check for -- font specific changes and glyph specific effects. For now we assume that slanted -- font is just a copy but in the future we could just make it a glyph property like -- scale, xscale and yscale. An even more advanced approach is to integrate -- boldening but then we also need 4 offsets and runtime calculation, while it might -- also complicate e.g. extensibles in math. There is much more overhead so it will -- also impact performance. local fontcharacters ----- fontdescriptions local fontparameters local fontproperties local pdfcharacters local getstreamhash = fonts.handlers.otf.getstreamhash local usedfontstreams = utilities.storage.allocate() local usedindices = setmetatableindex(function(t,k) local n = 0 -- n = 31 local v = setmetatableindex(function(tt,kk) if n >= 0xFFFF then report_fonts("registering character index: overflow in hash %a, todo: use overflow font") else n = n + 1 end if trace_indices then report_fonts("registering character index: hash %a, charindex 0x%05X, slotindex 0x%04X",k,kk,n) end local vv = n tt[kk] = vv return vv end) t[k] = v return v end) local usedcharacters = setmetatableindex(function(t,k) local h, d = getstreamhash(k) if trace_indices then report_fonts("registering index table: hash %a, fontid %i",h,k) end usedfontstreams[h] = d local v = usedindices[h] t[k] = v return v end) lpdf.usedfontstreams = usedfontstreams -- [streamhash] -> fontdata lpdf.usedcharacters = usedcharacters -- [fontid] -> indices lpdf.usedindices = usedindices -- [streamhash][index] -> realindex (can also be dupindex) local horizontalmode = true local scalefactor = 1 local threshold = 655360 local tjfactor = 100 / 65536 function flushers.updatefontstate(font) -- virtual t3 fonts have negative font index fontcharacters = characters[font] -- fontdescriptions = descriptions[font] fontparameters = parameters[font] fontproperties = properties[font] local size = fontparameters.size -- or bad news local designsize = fontparameters.designsize or size pdfcharacters = usedcharacters[font] horizontalmode = fontparameters.writingmode ~= "vertical" scalefactor = (designsize/size) * tjfactor end local f_cm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N cm"] local f_cz = formatters["%.6N 0 0 %.6N %.6N %.6N cm"] ----- f_tm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N Tm"] local f_tm = formatters["%.6N 0 %.6N %.6N %.6N %.6N Tm"] directives.register("backend.pdf.accurate", function() f_cm = formatters["%.9N %.9N %.9N %.9N %.9N %.9N cm"] f_cz = formatters["%.9N 0 0 %.9N %.9N %.9N cm"] -- f_tm = formatters["%.9N %.9N %.9N %.9N %.9N %.9N Tm"] f_tm = formatters["%.9N 0 %.9N %.9N %.9N %.9N Tm"] end) local saved_text_pos_v = 0 local saved_text_pos_h = 0 local function begin_text() saved_text_pos_h = pdf_h saved_text_pos_v = pdf_v b = b + 1 ; buffer[b] = "BT" need_tf = true need_font = true need_width = 0 need_mode = 0 current_effect = nil current_slant = 0 current_weight = 0 mode = "text" end local function end_text() if done_width then b = b + 1 ; buffer[b] = "0 w" done_width = false end if done_mode then b = b + 1 ; buffer[b] = "0 Tr" done_mode = false end b = b + 1 ; buffer[b] = "ET" pdf_h = saved_text_pos_h pdf_v = saved_text_pos_v mode = "page" end local begin_chararray, end_chararray do local saved_chararray_pos_h local saved_chararray_pos_v local saved_b = 0 begin_chararray = function() saved_chararray_pos_h = pdf_h saved_chararray_pos_v = pdf_v tj_position = horizontalmode and saved_chararray_pos_h or - saved_chararray_pos_v tj_delta = 0 saved_b = b b = b + 1 ; buffer[b] = " [" mode = "chararray" end end_chararray = function() b = b + 1 ; buffer[b] = "] TJ" buffer[saved_b] = concat(buffer,"",saved_b,b) b = saved_b pdf_h = saved_chararray_pos_h pdf_v = saved_chararray_pos_v mode = "text" end end local function begin_charmode() b = b + 1 ; buffer[b] = "<" mode = "char" end local function end_charmode() b = b + 1 ; buffer[b] = ">" mode = "chararray" end local function calc_pdfpos(h,v) -- mostly char if mode == "page" then cmtx = h - pdf_h cmty = v - pdf_v return h ~= pdf_h or v ~= pdf_v elseif mode == "text" then tmtx = h - saved_text_pos_h tmty = v - saved_text_pos_v return h ~= pdf_h or v ~= pdf_v elseif horizontalmode then tmty = v - saved_text_pos_v tj_delta = tj_position - h return tj_delta ~= 0 or v ~= pdf_v else tmtx = h - saved_text_pos_h tj_delta = tj_position + v return tj_delta ~= 0 or h ~= pdf_h end end local function pdf_set_pos(h,v) local move = calc_pdfpos(h,v) if move then -- b = b + 1 ; buffer[b] = f_cm(cmrx, cmsx, cmsy, cmry, cmtx*bpfactor, cmty*bpfactor) b = b + 1 ; buffer[b] = f_cz(cmrx, cmry, cmtx*bpfactor, cmty*bpfactor) pdf_h = pdf_h + cmtx pdf_v = pdf_v + cmty end end local function pdf_reset_pos() if mode == "page" then cmtx = - pdf_h cmty = - pdf_v if pdf_h == 0 and pdf_v == 0 then return end elseif mode == "text" then tmtx = - saved_text_pos_h tmty = - saved_text_pos_v if pdf_h == 0 and pdf_v == 0 then return end elseif horizontalmode then tmty = - saved_text_pos_v tj_delta = tj_position if tj_delta == 0 and pdf_v == 0 then return end else tmtx = - saved_text_pos_h tj_delta = tj_position if tj_delta == 0 and pdf_h == 0 then return end end -- b = b + 1 ; buffer[b] = f_cm(cmrx, cmsx, cmsy, cmry, cmtx*bpfactor, cmty*bpfactor) b = b + 1 ; buffer[b] = f_cz(cmrx, cmry, cmtx*bpfactor, cmty*bpfactor) pdf_h = pdf_h + cmtx pdf_v = pdf_v + cmty end local function pdf_set_pos_temp(h,v) local move = calc_pdfpos(h,v) if move then -- b = b + 1 ; buffer[b] = f_cm(cmrx, cmsx, cmsy, cmry, cmtx*bpfactor, cmty*bpfactor) b = b + 1 ; buffer[b] = f_cz(cmrx, cmry, cmtx*bpfactor, cmty*bpfactor) end end -- these dummy returns makes using them a bit faster local function pdf_end_string_nl() if mode == "char" then end_charmode() return end_chararray() elseif mode == "chararray" then return end_chararray() end end local function pdf_goto_textmode() if mode == "page" then pdf_reset_pos() return begin_text() elseif mode ~= "text" then if mode == "char" then end_charmode() return end_chararray() else -- if mode == "chararray" then return end_chararray() end end end local function pdf_goto_pagemode() if mode ~= "page" then if mode == "char" then end_charmode() end_chararray() return end_text() elseif mode == "chararray" then end_chararray() return end_text() elseif mode == "text" then return end_text() end end end local function pdf_goto_fontmode() if mode == "char" then end_charmode() end_chararray() end_text() elseif mode == "chararray" then end_chararray() end_text() elseif mode == "text" then end_text() end pdf_reset_pos() mode = "page" end -- characters do local round = math.round -- across pages ... todo: clean up because we don't need to pass the font -- as fontparameters already has checked / set it we can also have a variable -- for it so local characterwidth = nil -- local descriptionwidth = nil local hshift = false local vshift = false local dupx = 100 / bpfactor -- 6578176.0 local dumx = - dupx local dupy = dupx local dumy = dumx directives.register("backend.pdf.drift", function(v) dupx = tonumber(v) or 100 if dupx < 10 then dupx = 10 elseif dupx > 100 then dupx = 100 end dupx = dupx / bpfactor end) local h_hex_2 = lpdf.h_hex_2 local h_hex_4 = lpdf.h_hex_4 -- The width array uses the original dimensions! This is different from e.g. -- luatex where we have more widths arrays and these reflect the cheated -- widths (goes wrong elsewhere). -- when changing this, check math: compact-001.tex (rule width) local characterwidths = setmetatableindex(function(t,font) local d = descriptions[font] local c = characters[font] local f = parameters[font].hfactor or parameters[font].factor local v = setmetatableindex(function(t,char) local w local e = c and c[char] if e then local a = e.advance if a then w = a else w = e.width or 0 end end if not w then e = d and d[char] if e then w = e.width if w then w = w * f end end end if not w then w = 0 end t[char] = w return w end) t[font] = v return v end) -- the descriptions are used for the width array local extend = 1 -- some more can move here local squeeze = 1 local tohex = h_hex_4 local function setup_fontparameters(font,factor,sx,sy,slant,weight,effect) -- current_sx = sx current_sy = sy -- if fontproperties.bitmapped then tohex = h_hex_2 elseif font < 0 then tohex = h_hex_2 else tohex = h_hex_4 end local format = fontproperties.format local expand = 1 + factor / 1000000 if effect then -- We have glyph specific effects and these have a higher priority than -- the font specific effects. if effect ~= current_effect then current_effect = effect tmrx = 1 tmry = 1 tmsy = effect.slant or fontparameters.slantfactor or slant or 0 extend = effect.extend or fontparameters.extendfactor or 1 squeeze = effect.squeeze or fontparameters.squeezefactor or 1 need_mode = effect.mode or fontparameters.mode or 0 need_width = effect.weight or fontparameters.weight or 0 sx = extend * sx sy = squeeze * sy else -- we could check if effects have changed but effects use unique tables; for -- now they win over font effects (only used in math) end else -- These are the font specific effects and the scales are part of the main -- font setup, so we use the factors and not the properties.effect data. current_effect = nil tmrx = 1 tmry = 1 -- local e = fontproperties.effect if e then tmsy = e.slant or slant or 0 extend = e.extend or 1 squeeze = e.squeeze or 1 need_mode = e.mode or 0 need_width = e.weight or 0 sx = extend * sx sy = squeeze * sy else tmsy = slant or 0 extend = 1 squeeze = 1 need_mode = 0 need_width = weight end end tmef = expand tmrx = expand * tmrx current_font = font current_pdf = usedfonts[font] -- cache current_factor = factor current_slant = tmsy current_weight = weight local sc = fontparameters.size * bpfactor / 10 -- kind of special: if format == "opentype" or format == "type1" then sc = sc * 1000 / fontparameters.units tj_scale = fontparameters.units / 1000 else tj_scale = 1 end tj_delta = 0 tj_position = 0 -- tmsy = tmsy * sc tmrx = tmrx * sc tmry = tmry * sc -- f_x_scale = sx if f_x_scale ~= 1.0 then tmrx = tmrx * f_x_scale end f_y_scale = sy if f_y_scale ~= 1.0 then tmsy = tmsy * f_y_scale tmry = tmry * f_y_scale end -- tmef_f_x_scale = tmef * f_x_scale -- if tmef_f_x_scale == 1.0 then tmef_f_x_scale = 1 end -- tj_scale = tj_scale * scalefactor / tmef_f_x_scale -- tmef_f_w_scale = tmef_f_x_scale / extend -- characterwidth = characterwidths[font] -- descriptionwidth = descriptionwidths[font] -- hshift = fontparameters.hshift vshift = fontparameters.vshift if need_width > 0 then if not need_mode or need_mode == 0 then need_mode = 2 end need_width = need_width * 2 need_width = need_width * tmrx -- if hshift then -- hshift = hshift * tmrx -- end end if current_weight > 0 then -- hshift = (hshift or 0) + current_weight end end local f_width = formatters["%.6N w"] local f_mode = formatters["%i Tr"] -- can be hash local f_font = formatters["/F%i 10 Tf"] local s_width = "0 w" local s_mode = "0 Tr" local width_factor = 72.27 / 72.0 local last_fpdf local function set_font() -- if need_width and need_width ~= 0 then if need_width ~= 0 then b = b + 1 ; buffer[b] = f_width(width_factor*need_width) done_width = true elseif done_width then b = b + 1 ; buffer[b] = s_width done_width = false end -- if need_mode and need_mode ~= 0 then if need_mode ~= 0 then b = b + 1 ; buffer[b] = f_mode(need_mode) done_mode = true elseif done_mode then b = b + 1 ; buffer[b] = s_mode done_mode = false end -- no need when the same if need_font or last_pdf ~= current_pdf then b = b + 1 ; buffer[b] = f_font(current_pdf) last_pdf = current_pdf need_font = false end -- f_pdf_cur = current_pdf need_tf = false need_tm = true end local function set_textmatrix(h,v) local move = calc_pdfpos(h,v) if need_tm or move then -- b = b + 1 ; buffer[b] = f_tm(tmrx, tmsx, tmsy, tmry, tmtx*bpfactor, tmty*bpfactor) b = b + 1 ; buffer[b] = f_tm(tmrx, tmsy, tmry, tmtx*bpfactor, tmty*bpfactor) pdf_h = saved_text_pos_h + tmtx pdf_v = saved_text_pos_v + tmty need_tm = false end -- cur_tmrx = tmrx end ----- f_skip = formatters["%.2N"] -- I will redo this mess ... we no longer have the mkiv pdf generator that we used in -- luatex (a precursor to lmtx and also for comparison) but only in lmtx now so ... -- time to move on I guess. -- factor is for hz flushers.character = function(current,pos_h,pos_v,pos_r,font,char,data,csx,csy,factor,sx,sy,slant,weight) -- ,naturalwidth,width) local s = data.scale local x = data.xoffset local y = data.yoffset local effect = data.effect if s then sx = s * sx sy = s * sy end if csx then sx = sx * csx csx = 1 end if csy then sy = sy * csy csy = 1 end if sx ~= current_sx -- was: f_x_scale or sy ~= current_sy -- was: f_x_scale or need_tf or font ~= current_font -- or current_pdf ~= f_pdf_cur or mode == "page" or slant ~= current_slant or weight ~= current_weight or effect ~= current_effect then pdf_goto_textmode() setup_fontparameters(font,factor,sx,sy,slant,weight,effect) -- too often due to page set_font() -- elseif mode == "page" then -- pdf_goto_textmode() -- set_font() elseif current_factor ~= factor -- or cur_tmrx ~= tmrx then -- check if this happens and when setup_fontparameters(font,factor,sx,sy,slant,weight,effect) need_tm = true end if x then pos_h = pos_h + x * tmef_f_x_scale end if y then pos_v = pos_v + y * f_y_scale end local move = calc_pdfpos(pos_h,pos_v) if move or need_tm then if not need_tm then if horizontalmode then if (saved_text_pos_v + tmty) ~= pdf_v then need_tm = true elseif tj_delta >= dupx or tj_delta <= dumx then need_tm = true end else if (saved_text_pos_h + tmtx) ~= pdf_h then need_tm = true elseif tj_delta >= dupy or tj_delta <= dumy then need_tm = true end end end if hshift then pos_h = pos_h + hshift end if vshift then pos_v = pos_v - vshift end if need_tm then pdf_goto_textmode() set_textmatrix(pos_h,pos_v) begin_chararray() move = calc_pdfpos(pos_h,pos_v) end if move then local d = tj_delta * tj_scale if d <= -0.5 or d >= 0.5 then if mode == "char" then end_charmode() end b = b + 1 ; buffer[b] = round(d) -- or f_skip(d) -- print(d,buffer[b]) end tj_position = tj_position - tj_delta -- tj_position = tj_position - tj_delta * tmef_f_x_scale end end if mode == "chararray" then begin_charmode() end -- When we have say 100 x without spaces we do accumulate some error but in practice -- we have spaces so we sync. This is the same as in pdftex and luatex. tj_position = tj_position + characterwidth[char] * tmef_f_w_scale local slot = pdfcharacters[data.index or char] -- registers usage -- b = b + 1 ; buffer[b] = font > 0 and h_hex_4[slot] or h_hex_2[slot] b = b + 1 ; buffer[b] = tohex[slot] end do -- We like a little bit of optimization, just for the fun of it. local spaces = setmetatableindex(function(t,font) local bytes = false local data = characters[font] if data then data = data[32] if data then local index = data.index if index then local dummy = usedfonts[font] local slot = usedcharacters[font][index] if slot then -- bytes = font > 0 and h_hex_4[slot] or h_hex_2[slot] bytes = tohex[slot] end end end end t[font] = bytes return bytes end) -- We have to be in the current font stream. flushers.space = function(font) if mode == "char" and font == current_font then local slot = spaces[font] if slot then tj_position = tj_position + characterwidth[32] * tmef_f_w_scale b = b + 1 ; buffer[b] = slot end end end end flushers.fontchar = function(font,char,data) local dummy = usedfonts[font] local slot = pdfcharacters[data.index or char] -- registers usage return dummy, slot end end -- literals local flushliteral do local nodeproperties = nodes.properties.data local literalvalues = nodes.literalvalues local originliteral_code = literalvalues.origin local pageliteral_code = literalvalues.page local alwaysliteral_code = literalvalues.always local rawliteral_code = literalvalues.raw local textliteral_code = literalvalues.text local fontliteral_code = literalvalues.font flushliteral = function(current,pos_h,pos_v) local p = nodeproperties[current] if p then local str = p.data if str and str ~= "" then local mode = p.mode if mode == originliteral_code then pdf_goto_pagemode() pdf_set_pos(pos_h,pos_v) elseif mode == pageliteral_code then pdf_goto_pagemode() elseif mode == textliteral_code then pdf_goto_textmode() elseif mode == fontliteral_code then pdf_goto_fontmode() elseif mode == alwaysliteral_code then -- aka direct pdf_end_string_nl() need_tm = true elseif mode == rawliteral_code then pdf_end_string_nl() else report("invalid literal mode %a when flushing %a",mode,str) return end b = b + 1 ; buffer[b] = str end end end flushers.literal = flushliteral function lpdf.print(mode,str) -- This only works inside objects, don't change this to flush -- in between. It's different from luatex but okay. if str then mode = literalvalues[mode] else mode, str = originliteral_code, mode end if str and str ~= "" then if mode == originliteral_code then pdf_goto_pagemode() -- pdf_set_pos(pdf_h,pdf_v) elseif mode == pageliteral_code then pdf_goto_pagemode() elseif mode == textliteral_code then pdf_goto_textmode() elseif mode == fontliteral_code then pdf_goto_fontmode() elseif mode == alwaysliteral_code then pdf_end_string_nl() need_tm = true elseif mode == rawliteral_code then pdf_end_string_nl() else report("invalid literal mode %a when flushing %a",mode,str) return end b = b + 1 ; buffer[b] = str end end end -- grouping & orientation do local matrices = { } local positions = { } local nofpositions = 0 local nofmatrices = 0 local flushsave = function(current,pos_h,pos_v) nofpositions = nofpositions + 1 positions[nofpositions] = { pos_h, pos_v, nofmatrices } pdf_goto_pagemode() pdf_set_pos(pos_h,pos_v) b = b + 1 ; buffer[b] = "q" end local flushrestore = function(current,pos_h,pos_v) if nofpositions < 1 then return end local t = positions[nofpositions] -- local h = pos_h - t[1] -- local v = pos_v - t[2] if shippingmode == "page" then nofmatrices = t[3] end pdf_goto_pagemode() pdf_set_pos(pos_h,pos_v) b = b + 1 ; buffer[b] = "Q" nofpositions = nofpositions - 1 end local nodeproperties = nodes.properties.data local s_matrix_0 = "1 0 0 1 0 0 cm" local f_matrix_2 = formatters["%.6N 0 0 %.6N 0 0 cm"] local f_matrix_4 = formatters["%.6N %.6N %.6N %.6N 0 0 cm"] local flushsetmatrix = function(current,pos_h,pos_v) local p = nodeproperties[current] if p then local m = p.matrix if m then local rx, sx, sy, ry = unpack(m) local s if not rx then rx = 1 elseif rx == 0 then rx = 0.0001 end if not ry then ry = 1 elseif ry == 0 then ry = 0.0001 end if not sx then sx = 0 end if not sy then sy = 0 end -- if sx == 0 and sy == 0 then if rx == 1 and ry == 1 then s = s_matrix_0 else s = f_matrix_2(rx,ry) end else s = f_matrix_4(rx,sx,sy,ry) end -- if shippingmode == "page" then local tx = pos_h * (1 - rx) - pos_v * sy local ty = pos_v * (1 - ry) - pos_h * sx if nofmatrices > 0 then local t = matrices[nofmatrices] local r_x, s_x, s_y, r_y, te, tf = t[1], t[2], t[3], t[4], t[5], t[6] rx, sx = rx * r_x + sx * s_y, rx * s_x + sx * r_y sy, ry = sy * r_x + ry * s_y, sy * s_x + ry * r_y tx, ty = tx * r_x + ty * s_y, tx * s_x + ty * r_y end nofmatrices = nofmatrices + 1 matrices[nofmatrices] = { rx, sx, sy, ry, tx, ty } end -- pdf_goto_pagemode() pdf_set_pos(pos_h,pos_v) -- b = b + 1 buffer[b] = s end end end flushers.setmatrix = flushsetmatrix flushers.save = flushsave flushers.restore = flushrestore function lpdf.hasmatrix() return nofmatrices > 0 end function lpdf.getmatrix() if nofmatrices > 0 then return unpack(matrices[nofmatrices]) else return 1, 0, 0, 1, 0, 0 end end flushers.pushorientation = function(orientation,pos_h,pos_v,pos_r) pdf_goto_pagemode() pdf_set_pos(pos_h,pos_v) b = b + 1 ; buffer[b] = "q" if orientation == 1 then b = b + 1 ; buffer[b] = "0 -1 1 0 0 0 cm" -- 90 elseif orientation == 2 then b = b + 1 ; buffer[b] = "-1 0 0 -1 0 0 cm" -- 180 elseif orientation == 3 then b = b + 1 ; buffer[b] = "0 1 -1 0 0 0 cm" -- 270 end end flushers.poporientation = function(orientation,pos_h,pos_v,pos_r) pdf_goto_pagemode() pdf_set_pos(pos_h,pos_v) b = b + 1 ; buffer[b] = "Q" end -- flushers.startmatrix = function(current,pos_h,pos_v) flushsave(current,pos_h,pos_v) flushsetmatrix(current,pos_h,pos_v) end flushers.stopmatrix = function(current,pos_h,pos_v) flushrestore(current,pos_h,pos_v) end flushers.startscaling = function(current,pos_h,pos_v) flushsave(current,pos_h,pos_v) flushsetmatrix(current,pos_h,pos_v) end flushers.stopscaling = function(current,pos_h,pos_v) flushrestore(current,pos_h,pos_v) end flushers.startrotation = function(current,pos_h,pos_v) flushsave(current,pos_h,pos_v) flushsetmatrix(current,pos_h,pos_v) end flushers.stoprotation = function(current,pos_h,pos_v) flushrestore(current,pos_h,pos_v) end flushers.startmirroring = function(current,pos_h,pos_v) flushsave(current,pos_h,pos_v) flushsetmatrix(current,pos_h,pos_v) end flushers.stopmirroring = function(current,pos_h,pos_v) flushrestore(current,pos_h,pos_v) end flushers.startclipping = function(current,pos_h,pos_v) flushsave(current,pos_h,pos_v) -- lpdf.print("origin",formatters["0 w %s W n"](nodeproperties[current].path)) pdf_goto_pagemode() b = b + 1 ; buffer[b] = formatters["0 w %s W n"](nodeproperties[current].path) end flushers.stopclipping = function(current,pos_h,pos_v) flushrestore(current,pos_h,pos_v) end end do local nodeproperties = nodes.properties.data flushers.setstate = function(current,pos_h,pos_v) local p = nodeproperties[current] if p then local d = p.data if d and d ~= "" then pdf_goto_pagemode() b = b + 1 ; buffer[b] = d end end end end -- rules local flushedxforms = { } -- actually box resources but can also be direct local localconverter = nil -- will be set local flushimage do local tonut = nodes.tonut local tonode = nuts.tonode local pdfbackend = backends.registered.pdf local nodeinjections = pdfbackend.nodeinjections local codeinjections = pdfbackend.codeinjections local newimagerule = nuts.pool.imagerule local newboxrule = nuts.pool.boxrule local setprop = nuts.setprop local getprop = nuts.getprop local setattrlist = nuts.setattrlist local getwhd = nuts.getwhd local flushlist = nuts.flushlist local getdata = nuts.getdata local rulecodes = nodes.rulecodes local normalrule_code = rulecodes.normal local boxrule_code = rulecodes.box local imagerule_code = rulecodes.image local emptyrule_code = rulecodes.empty local userrule_code = rulecodes.user local overrule_code = rulecodes.over local underrule_code = rulecodes.under local fractionrule_code = rulecodes.fraction local radicalrule_code = rulecodes.radical local outlinerule_code = rulecodes.outline ----- virtualrule_code = rulecodes.virtual local processrule = nodes.rules.process local f_fm = formatters["/Fm%d Do"] local f_im = formatters["/Im%d Do"] local f_gr = formatters["/Gp%d Do"] local s_b = "q" local s_e = "Q" local f_v = formatters["[] 0 d 0 J %.6N w 0 0 m %.6N 0 l S"] local f_h = formatters["[] 0 d 0 J %.6N w 0 0 m 0 %.6N l S"] local f_f = formatters["0 0 %.6N %.6N re f"] local f_o = formatters["[] 0 d 0 J 0 0 %.6N %.6N re S"] local f_w = formatters["[] 0 d 0 J %.6N w 0 0 %.6N %.6N re S"] local f_b = formatters["%.6N w 0 %.6N %.6N %.6N re f"] local f_x = formatters["[] 0 d 0 J %.6N w %.6N %.6N %.6N %.6N re S"] local f_y = formatters["[] 0 d 0 J %.6N w %.6N %.6N %.6N %.6N re S %.6N 0 m %.6N 0 l S"] -- Historically the index is an object which is kind of bad. local boxresources, n = { }, 0 getxformname = function(index) local l = boxresources[index] if l then return l.name else report("no box resource %S",index) end end lpdf.getxformname = getxformname local pdfcollectedresources = lpdf.collectedresources function codeinjections.saveboxresource(box,attributes,resources,immediate,kind,margin,onum) n = n + 1 local immediate = true local margin = margin or 0 -- or dimension local objnum = onum or pdfreserveobject() local list = tonut(type(box) == "number" and tex.takebox(box) or box) -- if resources == true then resources = pdfcollectedresources() end -- local width, height, depth = getwhd(list) -- local l = { width = width, height = height, depth = depth, margin = margin, attributes = attributes, resources = resources, list = nil, type = kind, name = n, index = objnum, objnum = objnum, } local r = boxresources[objnum] if r then flushlist(l.list) l.list = nil -- added end boxresources[objnum] = l if immediate then localconverter(list,"xform",objnum,l) flushedxforms[objnum] = { true , objnum } flushlist(list) else l.list = list end return objnum end function nodeinjections.useboxresource(index,wd,ht,dp) local l = boxresources[index] if l then if wd or ht or dp then wd, ht, dp = wd or 0, ht or 0, dp or 0 else wd, ht, dp = l.width, l.height, l.depth end local rule = newboxrule(wd,ht,dp) setattrlist(rule,true) setprop(rule,"index",index) return tonode(rule), wd, ht, dp else report("no box resource %S",index) end end local function getboxresourcedimensions(index) local l = boxresources[index] if l then return l.width, l.height, l.depth, l.margin else report("no box resource %S",index) end end nodeinjections.getboxresourcedimensions = getboxresourcedimensions function codeinjections.getboxresourcebox(index) local l = boxresources[index] if l then return l.list end end -- a bit of a mess: index is now objnum but that has to change to a proper index -- ... an engine inheritance local function flushpdfxform(current,pos_h,pos_v,pos_r,size_h,size_v) -- object properties local objnum = getprop(current,"index") local name = getxformname(objnum) local info = flushedxforms[objnum] local r = boxresources[objnum] if not info then info = { false , objnum } flushedxforms[objnum] = info end local wd, ht, dp = getboxresourcedimensions(objnum) -- or: wd, ht, dp = r.width, r.height, r.depth -- sanity check local htdp = ht + dp if wd == 0 or size_h == 0 or htdp == 0 or size_v == 0 then return end -- calculate scale local rx, ry = 1, 1 if wd ~= size_h or htdp ~= size_v then rx = size_h / wd ry = size_v / htdp end -- flush the reference usedxforms[objnum] = true pdf_goto_pagemode() calc_pdfpos(pos_h,pos_v) local tx = cmtx * bpfactor local ty = cmty * bpfactor b = b + 1 ; buffer[b] = s_b -- b = b + 1 ; buffer[b] = f_cm(rx,0,0,ry,tx,ty) b = b + 1 ; buffer[b] = f_cz(rx, ry,tx,ty) b = b + 1 ; buffer[b] = f_fm(name) b = b + 1 ; buffer[b] = s_e end -- place image also used in vf but we can use a different one if we need it local imagetypes = images.types -- pdf png jpg jp2 jbig2 stream local img_none = imagetypes.none local img_pdf = imagetypes.pdf local img_stream = imagetypes.stream local one_bp = 65536 * bpfactor local imageresources, n = { }, 0 getximagename = function(index) -- not used local l = imageresources[index] if l then return l.name else report("no image resource %S",index) end end -- Groups are flushed immediately but we can decide to make them into a -- specific whatsit ... but not now. We could hash them if needed when -- we use lot sof them in mp ... but not now. usedxgroups = { } local groups = 0 local group = nil local flushgroup = function(content,bbox) if not group then group = pdfdictionary { Type = pdfconstant("Group"), S = pdfconstant("Transparency"), } end local wrapper = pdfdictionary { Type = pdf_xobject, Subtype = pdf_form, FormType = 1, Group = group, BBox = pdfarray(bbox), Resources = lpdf.collectedresources { serialize = false }, } local objnum = pdfflushstreamobject(content,wrapper,false) -- why not compressed ? groups = groups + 1 usedxgroups[groups] = objnum return f_gr(groups) end flushers.group = flushgroup lpdf.flushgroup = flushgroup -- todo: access via driver in mlib-pps -- end of experiment local function flushpdfximage(current,pos_h,pos_v,pos_r,size_h,size_v) local width, height, depth = getwhd(current) local total = height + depth local transform = getprop(current,"transform") or 0 -- we never set it ... so just use rotation then local index = getprop(current,"index") or 0 local kind, xorigin, yorigin, xsize, ysize, rotation, -- transform / orientation / rotation : it's a mess (i need to redo this) objnum, groupref = pdfincludeimage(index) -- needs to be sorted out, bad name (no longer mixed anyway) if not kind then report("invalid image %S",index) return end local rx, sx, sy, ry, tx, ty = 1, 0, 0, 1, 0, 0 -- tricky: xsize and ysize swapped if kind == img_pdf or kind == img_stream then rx, ry, tx, ty = 1/xsize, 1/ysize, xorigin/xsize, yorigin/ysize else -- if kind == img_png then -- -- if groupref > 0 and img_page_group_val == 0 then -- -- img_page_group_val = groupref -- -- end -- end rx, ry = bpfactor, bpfactor end if (transform & 7) > 3 then -- mirror rx, tx = -rx, -tx end local t = (transform + rotation) & 3 if t == 0 then -- nothing elseif t == 1 then -- rotation over 90 degrees (counterclockwise) rx, sx, sy, ry, tx, ty = 0, rx, -ry, 0, -ty, tx elseif t == 2 then -- rotation over 180 degrees (counterclockwise) rx, ry, tx, ty = -rx, -ry, -tx, -ty elseif t == 3 then -- rotation over 270 degrees (counterclockwise) rx, sx, sy, ry, tx, ty = 0, -rx, ry, 0, ty, -tx end rx = rx * width sx = sx * total sy = sy * width ry = ry * total tx = pos_h - tx * width ty = pos_v - ty * total local t = transform + rotation if (transform & 7) > 3 then t = t + 1 end t = t & 3 if t == 0 then -- no transform elseif t == 1 then -- rotation over 90 degrees (counterclockwise) tx = tx + width elseif t == 2 then -- rotation over 180 degrees (counterclockwise) tx = tx + width ty = ty + total elseif t == 3 then -- rotation over 270 degrees (counterclockwise) ty = ty + total end -- a flaw in original, can go: -- -- if img_page_group_val == 0 then -- img_page_group_val = group_ref -- end usedximages[index] = objnum -- hm pdf_goto_pagemode() calc_pdfpos(tx,ty) tx = cmtx * bpfactor ty = cmty * bpfactor b = b + 1 ; buffer[b] = s_b b = b + 1 ; buffer[b] = f_cm(rx,sx,sy,ry,tx,ty) b = b + 1 ; buffer[b] = f_im(index) b = b + 1 ; buffer[b] = s_e end flushimage = function(index,width,height,depth,pos_h,pos_v) -- used in vf characters local total = height + depth local kind, xorigin, yorigin, xsize, ysize, rotation, objnum, groupref = pdfincludeimage(index) local rx = width / xsize local sx = 0 local sy = 0 local ry = total / ysize local tx = pos_h -- to be sorted out -- local ty = pos_v - depth local ty = pos_v -- we assume that depth is dealt with in the caller (for now) usedximages[index] = objnum pdf_goto_pagemode() calc_pdfpos(tx,ty) tx = cmtx * bpfactor ty = cmty * bpfactor b = b + 1 ; buffer[b] = s_b b = b + 1 ; buffer[b] = f_cm(rx,sx,sy,ry,tx,ty) b = b + 1 ; buffer[b] = f_im(index) b = b + 1 ; buffer[b] = s_e end flushers.image = flushimage -- For the moment we need this hack because the engine checks the 'image' -- command in virtual fonts (so we use lua instead). -- -- These will be replaced by a new more advanced one ... some day ... or -- never because the next are like the other engines and compensate for -- small sizes which is needed for inaccurate viewers. flushers.rule = function(current,pos_h,pos_v,pos_r,size_h,size_v,subtype) if subtype == emptyrule_code then return elseif subtype == boxrule_code then return flushpdfxform(current,pos_h,pos_v,pos_r,size_h,size_v) elseif subtype == imagerule_code then return flushpdfximage(current,pos_h,pos_v,pos_r,size_h,size_v) elseif subtype == userrule_code or (subtype >= overrule_code and subtype <= radicalrule_code) then pdf_goto_pagemode() b = b + 1 ; buffer[b] = s_b pdf_set_pos_temp(pos_h,pos_v) processrule(current,size_h,size_v,pos_r) -- so we pass direction b = b + 1 ; buffer[b] = s_e return end pdf_goto_pagemode() b = b + 1 ; buffer[b] = s_b local dim_h = size_h * bpfactor local dim_v = size_v * bpfactor local rule -- -- this fails for showglyphs so and i have no reason to look into it now and rectangles -- do a better job anyway -- if subtype == outlinerule_code then local linewidth = getdata(current) pdf_set_pos_temp(pos_h,pos_v) if linewidth > 0 then rule = f_w(linewidth * bpfactor,dim_h,dim_v) else rule = f_o(dim_h,dim_v) end elseif dim_v <= one_bp then pdf_set_pos_temp(pos_h,pos_v + 0.5 * size_v) rule = f_v(dim_v,dim_h) elseif dim_h <= one_bp then pdf_set_pos_temp(pos_h + 0.5 * size_h,pos_v) rule = f_h(dim_h,dim_v) else pdf_set_pos_temp(pos_h,pos_v) rule = f_f(dim_h,dim_v) end b = b + 1 ; buffer[b] = rule b = b + 1 ; buffer[b] = s_e end flushers.simplerule = function(pos_h,pos_v,pos_r,size_h,size_v) pdf_goto_pagemode() b = b + 1 ; buffer[b] = s_b local dim_h = size_h * bpfactor local dim_v = size_v * bpfactor local rule if dim_v <= one_bp then pdf_set_pos_temp(pos_h,pos_v + 0.5 * size_v) rule = f_v(dim_v,dim_h) elseif dim_h <= one_bp then pdf_set_pos_temp(pos_h + 0.5 * size_h,pos_v) rule = f_h(dim_h,dim_v) else pdf_set_pos_temp(pos_h,pos_v) rule = f_f(dim_h,dim_v) end b = b + 1 ; buffer[b] = rule b = b + 1 ; buffer[b] = s_e end flushers.specialrule = function(pos_h,pos_v,pos_r,width,height,depth,line,outline,baseline) pdf_goto_pagemode() b = b + 1 ; buffer[b] = s_b local width = bpfactor * width local height = bpfactor * height local depth = bpfactor * depth local total = height + depth local line = bpfactor * line local half = line / 2 local rule if outline then local d = -depth + half local w = width - line local t = total - line if baseline and w > 0 then rule = f_y(line,half,d,w,t,half,w) else rule = f_x(line,half,d,w,t) end else rule = f_b(line,-depth,width,total) end pdf_set_pos_temp(pos_h,pos_v) b = b + 1 ; buffer[b] = rule b = b + 1 ; buffer[b] = s_e end end --- basics local wrapupdocument, registerpage do local pages = { } local maxkids = 10 local nofpages = 0 local pagetag = "unset" registerpage = function(object) nofpages = nofpages + 1 local objnum = pdfpagereference(nofpages) pages[nofpages] = { page = nofpages, -- original number, only for diagnostics objnum = objnum, object = object, tag = pagetag, } end function lpdf.setpagetag(tag) pagetag = tag or "unset" end function lpdf.getnofpages() return nofpages end function lpdf.getpagetags() local list = { } for i=1,nofpages do list[i] = pages[i].tag end return list end function lpdf.setpageorder(mapping,p) -- mapping can be a hash so: local list = table.sortedkeys(mapping) local n = #list local nop = p or nofpages if n == nop then local done = { } local hash = { } for i=1,n do local order = mapping[list[i]] if hash[order] then report("invalid page order, duplicate entry %i",order) return elseif order < 1 or order > nofpages then report("invalid page order, no page %i",order) return else done[i] = pages[order] hash[order] = true end end pages = done else report("invalid page order, %i entries expected",nop) end end -- We can have this, but then via codeinjections etc. Later. -- function structures.pages.swapthem() -- local n = lpdf.getnofpages() -- local t = { } -- for i=1,n do -- t[i] = i -- end -- for i=2,math.odd(n) and n or (n-1),2 do -- t[i] = i+1 -- t[i+1] = i -- end -- lpdf.setpageorder(t) -- end wrapupdocument = function(driver) -- hook (to reshuffle pages) local pagetree = { } local parent = nil local minimum = 0 local maximum = 0 local current = 0 if #pages > 1.5 * maxkids then repeat local plist, pnode if current == 0 then plist, minimum = pages, 1 elseif current == 1 then plist, minimum = pagetree, 1 else plist, minimum = pagetree, maximum + 1 end maximum = #plist if maximum > minimum then local kids for i=minimum,maximum do local p = plist[i] if not pnode or #kids == maxkids then kids = pdfarray() parent = pdfreserveobject() pnode = pdfdictionary { objnum = parent, Type = pdf_pages, Kids = kids, Count = 0, } pagetree[#pagetree+1] = pnode end kids[#kids+1] = pdfreference(p.objnum) pnode.Count = pnode.Count + (p.Count or 1) p.Parent = pdfreference(parent) end end current = current + 1 until maximum == minimum -- flush page tree for i=1,#pagetree do local entry = pagetree[i] local objnum = entry.objnum entry.objnum = nil pdfflushobject(objnum,entry) end else -- ugly local kids = pdfarray() local list = pdfdictionary { Type = pdf_pages, Kids = kids, Count = nofpages, } parent = pdfreserveobject() for i=1,nofpages do local page = pages[i] kids[#kids+1] = pdfreference(page.objnum) page.Parent = pdfreference(parent) end pdfflushobject(parent,list) end for i=1,nofpages do local page = pages[i] local object = page.object object.Parent = page.Parent pdfflushobject(page.objnum,object) end lpdf.addtocatalog("Pages",pdfreference(parent)) end end local function initialize(driver,details) reset_variables(details) reset_buffer() end -- This will all move and be merged and become less messy. -- todo: more clever resource management: a bit tricky as we can inject -- stuff in the page stream local compact = false local encryptstream = false local encryptobject = false local encdict = nil local majorversion = 1 local minorversion = 7 -- Encryption -- This stuff is poorly documented so it took a while to figure out a way that made -- loading in a few programe working. Of course one you see the solution one can -- claim that it's easy and trivial. In the end we could even make acrobat accepting -- the file: it doesn't like the catalog to be in an object stream which to me -- smells like a bug. do -- move up (some already) or better: lpdf-aes.lmt or so local byte, sub, bytes, tohex, tobytes = string.byte, string.sub, string.bytes, string.tohex, string.tobytes local P, S, V, Cs, lpegmatch, patterns = lpeg.P, lpeg.S, lpeg.V, lpeg.Cs, lpeg.match, lpeg.patterns local digest256 = sha2.digest256 local digest384 = sha2.digest384 local digest512 = sha2.digest512 local aesencode = aes.encode local aesdecode = aes.decode local aesrandom = aes.random -- random and padding functions are gone here local function validpassword(str) return #str > 127 and sub(str,1,127) or str end local encryptionkey = false local objectparser = false do local function ps_encrypt(str) -- string is already unescaped str = aesencode(str,encryptionkey,true,true,true) return "<" .. tohex(str) .. ">" end local function hex_encrypt(str) -- string needs to be decoded str = tobytes(str) str = aesencode(str,encryptionkey,true,true,true) return "<" .. tohex(str) .. ">" end local whitespace = S("\000\009\010\012\013\032")^1 local anything = patterns.anything local space = patterns.space local spacing = whitespace^0 local newline = patterns.eol local cardinal = patterns.cardinal local p_psstring = ( P("(") * Cs(P { ( P("\\")/"" * anything + P("(") * V(1) * P(")") + (1 - P(")")) )^0 }) * P(")") ) / ps_encrypt local p_hexstring = ( P("<") * Cs((1-P(">"))^1) * P(">") ) / hex_encrypt local p_comment = P("%") * (1-newline)^1 * newline^1 local p_name = P("/") * (1 - whitespace - S("<>/[]()"))^1 local p_number = patterns.number local p_boolean = P("true") + P("false") local p_null = P("null") local p_reference = cardinal * spacing * cardinal * spacing * P("R") local p_other = p_name + p_reference + p_psstring + p_hexstring + p_number + p_boolean + p_null + p_comment local p_dictionary = { "dictionary", dictionary = ( P("<<") * (spacing * p_name * spacing * V("whatever"))^0 * spacing * P(">>") ), array = ( P("[") * (spacing * V("whatever"))^0 * spacing * P("]") ), whatever = ( V("dictionary") + V("array") + p_other ), } local p_object = P { "object", dictionary = p_dictionary.dictionary, array = p_dictionary.array, whatever = p_dictionary.whatever, object = spacing * (V("dictionary") + V("array") + p_other) } -- local p_object = cardinal -- * spacing -- * cardinal -- * spacing -- * P("obj") -- * p_object -- * P(1)^0 -- -- objectparser = Cs(p_object^1) objectparser = Cs(p_object^1) end local function makehash(password,salt,userkey) local k = digest256(password .. salt .. (userkey or "")) local n = 0 while true do local k1 = rep(password .. k .. (userkey or ""),64) local k2 = sub(k,1,16) local iv = sub(k,17,32) local e = aesencode(k1,k2,iv) local m = 0 local i = 1 for b in bytes(e) do m = m + b if i == 16 then break else i = i + 1 end end m = m % 3 if m == 0 then k = digest256(e) elseif m == 1 then k = digest384(e) else k = digest512(e) end n = n + 1 if n >= 64 and byte(sub(e,-1)) <= (n - 32) then break end end return sub(k,1,32) end local options = { -- unknown = 0x0001, -- bit 1 -- unknown = 0x0002, -- bit 2 print = 0x0004, -- bit 3 modify = 0x0008, -- bit 4 extract = 0x0010, -- bit 5 add = 0x0020, -- bit 6 -- unknown = 0x0040, -- bit 7 -- unknown = 0x0080, -- bit 8 fillin = 0x0100, -- bit 9 access = 0x0200, -- bit 10 assemble = 0x0400, -- bit 11 quality = 0x0800, -- bit 12 -- unknown = 0x1000, -- bit 13 -- unknown = 0x2000, -- bit 14 -- unknown = 0x4000, -- bit 15 -- unknown = 0x8000, -- bit 16 } -- 1111 0000 1100 0011 local mandate = 0x0200 local defaults = options.print | options.extract | options.quality -- majorversion = 2 -- minorversion = 0 function lpdf.setencryption(specification) if not encryptstream then local ownerpassword = specification.ownerpassword local userpassword = specification.userpassword local optionlist = specification.permissions if type(ownerpassword) == "string" and ownerpassword ~= "" then -- if type(userpassword) ~= "string" then userpassword = "" end userpassword = validpassword(userpassword) ownerpassword = validpassword(ownerpassword) -- encryptionkey = aesrandom(32) -- used earlier on -- local permissions = mandate if optionlist then optionlist = utilities.parsers.settings_to_array(optionlist) for i=1,#optionlist do local p = options[optionlist[i]] if p then permissions = permissions | p end end else permissions = permissions | defaults end -- permissions = permissions | 0xF0C3 -- needs work -- optionlist = { } for k, v in sortedhash(options) do if permissions & v == v then optionlist[#optionlist+1] = k end end -- local uservalidationsalt = aesrandom(8) local userkeysalt = aesrandom(8) local userhash = makehash(userpassword,uservalidationsalt) local userkey = userhash .. uservalidationsalt .. userkeysalt -- U local userintermediate = makehash(userpassword,userkeysalt) local useraes = aesencode(encryptionkey,userintermediate) -- UE -- local ownervalidationsalt = aesrandom(8) local ownerkeysalt = aesrandom(8) local ownerhash = makehash(ownerpassword,ownervalidationsalt,userkey) local ownerkey = ownerhash .. ownervalidationsalt .. ownerkeysalt -- O local ownerintermediate = makehash(ownerpassword,ownerkeysalt,userkey) local owneraes = aesencode(encryptionkey,ownerintermediate) -- OE -- -- still not ok test in qpdf -- local permissionsstring = sio.tocardinal4(0xFFFFFFFF) .. sio.tocardinal4(permissions) .. "T" -- EncryptMetadata .. "adb" .. aesrandom(4) local permissionsaes = aesencode(permissionsstring,encryptionkey) -- permissionsaes = tohex(permissionsaes) userkey = tohex(userkey) ownerkey = tohex(ownerkey) useraes = tohex(useraes) owneraes = tohex(owneraes) -- encdict = pdfdictionary { Filter = pdfconstant("Standard"), V = 5, -- variant R = 6, -- revision Length = 256, -- not needed StmF = pdfconstant("StdCF"), StrF = pdfconstant("StdCF"), P = permissions, Perms = pdfliteral(permissionsaes,true), -- #16 U = pdfliteral(userkey, true), -- #48 O = pdfliteral(ownerkey, true), -- #48 UE = pdfliteral(useraes, true), -- #32 OE = pdfliteral(owneraes, true), -- #32 CF = { StdCF = { AuthEvent = pdfconstant("DocOpen"), CFM = pdfconstant("AESV3"), Length = 32, -- #encryptionkey } }, -- bonus EncryptMetadata = true, } -- encryptstream = function(str) return aesencode(str,encryptionkey,true,true,true) -- random-iv add-iv add-padding end encryptobject = function(obj) if obj then if type(obj) == "table" then obj = obj() end return lpegmatch(objectparser,obj) or obj end end -- report_encryption("stream objects get encrypted") if not objectstream then report_encryption("strings are not encrypted, enable object streams") end report_encryption("permissions: % t",optionlist) if userpassword == "" then report_encryption("no user password") end -- end end end backends.registered.pdf.codeinjections.setencryption = lpdf.setencryption end do -- This is more a convenience feature and it might even be not entirely robust. -- It removes redundant color directives which makes the page stream look a bit -- nicer (also when figuring out issues). I might add more here but there is -- some additional overhead involved so runtime can be impacted. local P, R, S, Cs, lpegmatch = lpeg.P, lpeg.R, lpeg.S, lpeg.Cs, lpeg.match local p_ds = (R("09") + S(" ."))^1 ----- p_nl = S("\n\r")^1 local p_nl = S("\n")^1 local p_eg = P("Q") local p_cl = p_ds * (P("rg") + P("g") + P("k")) * p_ds * (P("RG") + P("G") + P("K")) ----- p_cl = (p_ds * (P("rg") + P("g") + P("k") + P("RG") + P("G") + P("K")))^1 local p_tr = P("/Tr") * p_ds * P("gs") local p_no_cl = (p_cl * p_nl) / "" local p_no_tr = (p_tr * p_nl) / "" local p_no_nl = 1 - p_nl local p_do_cl = p_cl * p_nl local p_do_tr = p_tr * p_nl local p_do_eg = p_eg * p_nl local pattern = Cs( ( (p_no_cl + p_no_tr)^0 * p_do_eg -- transparencies and colors before Q + p_no_tr * p_no_cl * p_do_tr * p_do_cl -- transparencies and colors before others + p_no_cl * p_do_cl -- successive colors + p_no_tr * p_do_tr -- successive transparencies + p_no_nl^1 + 1 )^1 ) local oldsize = 0 local newsize = 0 directives.register("backend.pdf.compact", function(v) compact = v and function(s) oldsize = oldsize + #s s = lpegmatch(pattern,s) or s newsize = newsize + #s return s end end) statistics.register("pdf pagestream",function() if oldsize ~= newsize then return string.format("old size: %i, new size %i",oldsize,newsize) end end) end local flushdeferred -- defined later local level = 0 local state = true function lpdf.setpagestate(s) state = s end local finalize do local f_font = formatters["F%d"] local f_form = formatters["Fm%d"] local f_group = formatters["Gp%d"] local f_image = formatters["Im%d"] local function checkedbox(mediabox,otherbox,what) if otherbox and #mediabox == 4 and #otherbox == 4 then local done = false if otherbox[1] < mediabox[1] then done = true ; otherbox[1] = mediabox[1] end if otherbox[2] < mediabox[2] then done = true ; otherbox[2] = mediabox[2] end if otherbox[3] > mediabox[3] then done = true ; otherbox[3] = mediabox[3] end if otherbox[4] > mediabox[4] then done = true ; otherbox[4] = mediabox[4] end if done then report("limiting %a to 'MediaBox'",what) end end return otherbox end finalize = function(driver,details) if not details then report("something is wrong, no details in 'finalize'") end level = level + 1 pdf_goto_pagemode() -- for now local objnum = details.objnum local specification = details.specification or { } local content = concat(buffer,"\n",1,b) if compact then content = compact(content) end local fonts = nil local xforms = nil if next(usedfonts) then fonts = pdfdictionary { } for k, v in next, usedfonts do fonts[f_font(v)] = pdfreference(usedfontobjects[k]) -- we can overload for testing end end -- messy: use real indexes for both ... so we need to change some in the -- full luatex part if next(usedxforms) or next(usedximages) or next(usedxgroups) then xforms = pdfdictionary { } for k in sortedhash(usedxforms) do xforms[f_form(getxformname(k))] = pdfreference(k) end for k, v in sortedhash(usedximages) do xforms[f_image(k)] = pdfreference(v) end for k, v in sortedhash(usedxgroups) do xforms[f_group(k)] = pdfreference(v) end end reset_buffer() -- finish_pdfpage_callback(shippingmode == "page") if shippingmode == "page" then local pageproperties = lpdf.getpageproperties() local pageresources = pageproperties.pageresources local pageattributes = pageproperties.pageattributes local pagesattributes = pageproperties.pagesattributes pageresources.Font = fonts pageresources.XObject = xforms pageresources.ProcSet = lpdf.procset() local bbox = pdfarray { boundingbox[1] * bpfactor, boundingbox[2] * bpfactor, boundingbox[3] * bpfactor, boundingbox[4] * bpfactor, } local contentsobj = pdfflushstreamobject(content,false,true) pageattributes.Type = pdf_page pageattributes.Contents = pdfreference(contentsobj) pageattributes.Resources = pageresources -- pageattributes.Resources = pdfreference(pdfflushobject(pageresources)) -- pageattributes.MediaBox = bbox pageattributes.MediaBox = pdfsharedobject(bbox) pageattributes.Parent = nil -- precalculate pageattributes.Group = nil -- todo -- resources can be indirect if state == "ignore" or state == false then else registerpage(pageattributes) lpdf.finalizepage(true) local TrimBox = pageattributes.TrimBox local CropBox = pageattributes.CropBox local BleedBox = pageattributes.BleedBox -- Indirect objects don't work in all viewers. if TrimBox then pageattributes.TrimBox = pdfsharedobject(checkedbox(bbox,TrimBox,"TrimBox")) end if CropBox then pageattributes.CropBox = pdfsharedobject(checkedbox(bbox,CropBox,"CropBox")) end if BleedBox then pageattributes.BleedBox = pdfsharedobject(checkedbox(bbox,BleedBox,"BleedBox")) end end else local xformtype = specification.type or 0 local margin = specification.margin or 0 local attributes = specification.attributes or "" local resources = specification.resources or "" local wrapper = nil if xformtype == 0 then wrapper = pdfdictionary { Type = pdf_xobject, Subtype = pdf_form, FormType = 1, BBox = nil, Matrix = nil, Resources = nil, } else wrapper = pdfdictionary { BBox = nil, Matrix = nil, Resources = nil, } end if xformtype == 0 or xformtype == 1 or xformtype == 3 then -- wrapper.BBox = pdfarray { -- -margin * bpfactor, -- -margin * bpfactor, -- (boundingbox[3] + margin) * bpfactor, -- (boundingbox[4] + margin) * bpfactor, wrapper.BBox = pdfarray { -ceil( margin * bpfactor), -ceil( margin * bpfactor), ceil((boundingbox[3] + margin) * bpfactor), ceil((boundingbox[4] + margin) * bpfactor), } end if xformtype == 0 or xformtype == 2 or xformtype == 3 then -- can be shared too wrapper.Matrix = pdfarray { 1, 0, 0, 1, 0, 0 } end local patterns = true if attributes.Type and attributes.Type == pdf_pattern then patterns = false end local boxresources = lpdf.collectedresources { patterns = patterns, serialize = false, } boxresources.Font = fonts boxresources.XObject = xforms -- todo: maybe share them -- wrapper.Resources = pdfreference(pdfflushobject(boxresources)) if resources ~= "" then boxresources = boxresources + resources end if attributes ~= "" then wrapper = wrapper + attributes end wrapper.Resources = next(boxresources) and boxresources or nil wrapper.ProcSet = lpdf.procset() pdfflushstreamobject(content,wrapper,true,specification.objnum) end for objnum in sortedhash(usedxforms) do local f = flushedxforms[objnum] if f[1] == false then f[1] = true local objnum = f[2] -- specification.objnum local specification = boxresources[objnum] local list = specification.list localconverter(list,"xform",f[2],specification) end end pdf_h, pdf_v = 0, 0 if level == 1 then flushdeferred() end level = level - 1 end end -- now comes the pdf file handling local objects = { } local streams = { } -- maybe just parallel to objects (no holes) local nofobjects = 0 local offset = 0 local f = false local flush = false local objectstream = true local compress = true local cache = false local info = "" local catalog = "" local lastdeferred = false local f_object = formatters["%i 0 obj\010%s\010endobj\010"] local f_stream_n_u = formatters["%i 0 obj\010<< /Length %i >>\010stream\010%s\010endstream\010endobj\010"] local f_stream_n_c = formatters["%i 0 obj\010<< /Filter /FlateDecode /Length %i >>\010stream\010%s\010endstream\010endobj\010"] local f_stream_d_u = formatters["%i 0 obj\010<< %s /Length %i >>\010stream\010%s\010endstream\010endobj\010"] local f_stream_d_c = formatters["%i 0 obj\010<< %s /Filter /FlateDecode /Length %i >>\010stream\010%s\010endstream\010endobj\010"] local f_stream_d_r = formatters["%i 0 obj\010<< %s >>\010stream\010%s\010endstream\010endobj\010"] ----- f_object_b = formatters["%i 0 obj\010"] local f_stream_b_n_u = formatters["%i 0 obj\010<< /Length %i >>\010stream\010"] local f_stream_b_n_c = formatters["%i 0 obj\010<< /Filter /FlateDecode /Length %i >>\010stream\010"] local f_stream_b_d_u = formatters["%i 0 obj\010<< %s /Length %i >>\010stream\010"] local f_stream_b_d_c = formatters["%i 0 obj\010<< %s /Filter /FlateDecode /Length %i >>\010stream\010"] local f_stream_b_d_r = formatters["%i 0 obj\010<< %s >>\010stream\010"] ----- s_object_e = "\010endobj\010" local s_stream_e = "\010endstream\010endobj\010" do -- Versions can be set but normally are managed by the official standards. When possible -- reading and writing should look at these values. function lpdf.setversion(major,minor) majorversion = tonumber(major) or majorversion minorversion = tonumber(minor) or minorversion end function lpdf.getversion(major,minor) return majorversion, minorversion end function lpdf.majorversion() return majorversion end function lpdf.minorversion() return minorversion end -- It makes no sense to support levels so we only enable and disable and stick to level 3 -- which is both fast and efficient. local frozen = false local clevel = 3 local olevel = 1 function lpdf.setcompression(level,objectlevel,freeze) if not frozen then compress = level and level ~= 0 and true or false objectstream = objectlevel and objectlevel ~= 0 and true or false frozen = freeze end end function lpdf.getcompression() return compress and olevel or 0, objectstream and clevel or 0 end function lpdf.compresslevel() return compress and olevel or 0 end function lpdf.objectcompresslevel() return objectstream and clevel or 0 end if environment.arguments.nocompression then lpdf.setcompression(0,0,true) end end local addtocache, flushcache, cache do local data, d = { }, 0 local list, l = { }, 0 local coffset = 0 local indices = { } local maxsize = 32 * 1024 -- uncompressed local maxcount = 0xFF addtocache = function(n,str) local size = #str if size == 0 then -- todo: message return end if coffset + size > maxsize or d == maxcount then flushcache() end if d == 0 then nofobjects = nofobjects + 1 objects[nofobjects] = false streams[nofobjects] = indices cache = nofobjects end objects[n] = - cache indices[n] = d d = d + 1 -- can have a comment n 0 obj as in luatex data[d] = str l = l + 1 ; list[l] = n l = l + 1 ; list[l] = coffset coffset = coffset + size + 1 end local p_ObjStm = pdfconstant("ObjStm") flushcache = function() -- references cannot be stored if l > 0 then list = concat(list," ") data[0] = list data = concat(data,"\010",0,d) local strobj = pdfdictionary { Type = p_ObjStm, N = d, First = #list + 1, } objects[cache] = offset local fb if compress then local size = #data local comp = compressdata(data,size) if comp and #comp < size then data = comp fb = f_stream_b_d_c else fb = f_stream_b_d_u end else fb = f_stream_b_d_u end local size = #data if encryptstream then data = encryptstream(data) size = #data end local b = fb(cache,strobj(),size) local e = s_stream_e flush(f,b) flush(f,data) flush(f,e) offset = offset + #b + size + #e data, d = { }, 0 list, l = { }, 0 coffset = 0 indices = { } end end end do local names = { } local cache = { } local nofpages = 0 local texgetcount = tex.getcount local c_realpageno = tex.iscount("realpageno") pdfreserveobject = function(name) nofobjects = nofobjects + 1 objects[nofobjects] = false if name then names[name] = nofobjects if trace_objects then report_objects("reserving number %a under name %a",nofobjects,name) end elseif trace_objects then report_objects("reserving number %a",nofobjects) end return nofobjects end pdfpagereference = function(n,complete) -- true | false | nil | n [true,false] if n == true or not n then complete = n n = texgetcount(c_realpageno) end if n > nofpages then nofpages = n end local r = pdfgetpagereference(n) return complete and pdfreference(r) or r end lpdf.reserveobject = pdfreserveobject lpdf.pagereference = pdfpagereference function lpdf.lastreferredpage() return nofpages end function lpdf.nofpages() -- this will change: document nofpages return structures.pages.nofpages end function lpdf.object(...) pdfdeferredobject(...) end function lpdf.delayedobject(data,n) if n then pdfdeferredobject(n,data) else n = pdfdeferredobject(data) end -- pdfreferenceobject(n) return n end pdfflushobject = function(name,data) if data then local named = names[name] if named then if not trace_objects then elseif trace_details then report_objects("flushing data to reserved object with name %a, data: %S",name,data) else report_objects("flushing data to reserved object with name %a",name) end return pdfimmediateobject(named,tostring(data)) else if not trace_objects then elseif trace_details then report_objects("flushing data to reserved object with number %s, data: %S",name,data) else report_objects("flushing data to reserved object with number %s",name) end return pdfimmediateobject(name,tostring(data)) end else if trace_objects and trace_details then report_objects("flushing data: %S",name) end return pdfimmediateobject(tostring(name)) end end pdfflushstreamobject = function(data,dict,compressed,objnum) -- default compressed if trace_objects then report_objects("flushing stream object of %s bytes",#data) end local dtype = type(dict) local kind = compressed == "raw" and "raw" or "stream" local nolength = nil if compressed == "raw" then compressed = false nolength = true -- data = string.formatters["<< %s >>stream\n%s\nendstream"](attr,data) end return pdfdeferredobject { objnum = objnum, immediate = true, nolength = nolength, compresslevel = compressed, type = "stream", string = data, attr = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil, } end function lpdf.flushstreamfileobject(filename,dict,compressed,objnum) -- default compressed if trace_objects then report_objects("flushing stream file object %a",filename) end local dtype = type(dict) return pdfdeferredobject { objnum = objnum, immediate = true, compresslevel = compressed, type = "stream", file = filename, attr = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil, } end local shareobjectcache, shareobjectreferencecache = { }, { } function lpdf.shareobject(content) if content == nil then -- invalid object not created else content = tostring(content) local o = shareobjectcache[content] if not o then o = pdfimmediateobject(content) shareobjectcache[content] = o end return o end end pdfsharedobject = function(content) if content == nil then -- invalid object not created else content = tostring(content) local r = shareobjectreferencecache[content] if not r then local o = shareobjectcache[content] if not o then o = pdfimmediateobject(content) shareobjectcache[content] = o end r = pdfreference(o) shareobjectreferencecache[content] = r end return r end end lpdf.flushobject = pdfflushobject lpdf.flushstreamobject = pdfflushstreamobject lpdf.shareobjectreference = pdfsharedobject lpdf.sharedobject = pdfsharedobject end local pages = table.setmetatableindex(function(t,k) local v = pdfreserveobject() t[k] = v return v end) pdfgetpagereference = function(n) return pages[n] end lpdf.getpagereference = pdfgetpagereference local function flushnormalobj(data,n) if not n then nofobjects = nofobjects + 1 n = nofobjects end if encryptobject then data = encryptobject(data) end data = f_object(n,data) if level == 0 then objects[n] = offset offset = offset + #data flush(f,data) else if not lastdeferred then lastdeferred = n elseif n < lastdeferred then lastdeferred = n end objects[n] = data end return n end local flushstreamobj, streamstatus do local uncompressed = 0 local compressed = 0 local notcompressed = 0 local threshold = 40 -- also #("/Filter /FlateDecode") (compression threshold) -- directives.register("backend.pdf.threshold",function(v) -- if v then -- threshold = tonumber(v) or 40 -- else -- threshold = -1000 -- end -- end) streamstatus = function() return { nofstreams = uncompressed + compressed + notcompressed, uncompressed = uncompressed, compressed = compressed, notcompressed = notcompressed, threshold = threshold, compresslevel = lpdf.compresslevel(), objectcompresslevel = lpdf.objectcompresslevel(), } end flushstreamobj = function(data,n,dict,comp,nolength) if not data then report("no data for %S",dict) return end if not n then nofobjects = nofobjects + 1 n = nofobjects end local size = #data if comp ~= false then comp = compress and size > threshold end if encryptobject then dict = encryptobject(dict) end if level == 0 then local b = nil local e = s_stream_e if nolength then -- probleem: we need to adapt length! b = f_stream_b_d_r(n,dict) -- raw object, already treated if encryptstream then print("check length") data = encryptstream(data) size = #data end uncompressed = uncompressed + 1 else if comp then local compdata = compressdata(data,size) if compdata then local compsize = #compdata if compsize <= size - threshold then data = compdata size = compsize else comp = false end else comp = false end end if encryptstream then data = encryptstream(data) size = #data end if comp then b = dict and f_stream_b_d_c(n,dict,size) or f_stream_b_n_c(n,size) compressed = compressed + 1 else b = dict and f_stream_b_d_u(n,dict,size) or f_stream_b_n_u(n,size) notcompressed = notcompressed + 1 end end flush(f,b) flush(f,data) flush(f,e) objects[n] = offset offset = offset + #b + size + #e else if nolength then if encryptstream then print("check length") data = encryptstream(data) end data = f_stream_d_r(n,dict,data) -- raw object, already treated uncompressed = uncompressed + 1 else if comp then local compdata = compressdata(data,size) if compdata then local compsize = #compdata if compsize <= size - threshold then data = compdata size = compsize else comp = false end else comp = false end end if encryptstream then data = encryptstream(data) size = #data end if comp then data = dict and f_stream_d_c(n,dict,size,data) or f_stream_n_c(n,size,data) compressed = compressed + 1 else data = dict and f_stream_d_u(n,dict,size,data) or f_stream_n_u(n,size,data) notcompressed = notcompressed + 1 end end if not lastdeferred then lastdeferred = n elseif n < lastdeferred then lastdeferred = n end objects[n] = data end return n end end flushdeferred = function() -- was forward defined if lastdeferred then for n=lastdeferred,nofobjects do local o = objects[n] if type(o) == "string" then objects[n] = offset offset = offset + #o flush(f,o) end end lastdeferred = false end end pdfimmediateobject = function(a,b,c,d) local kind --, immediate local objnum, data, attr, filename local compresslevel, objcompression, nolength local argtype = type(a) if argtype == "table" then kind = a.type -- raw | stream -- immediate = a.immediate objnum = a.objnum attr = a.attr compresslevel = a.compresslevel objcompression = a.objcompression filename = a.file data = a.string or a.stream or "" nolength = a.nolength if kind == "stream" then if filename then data = loaddata(filename) or "" end elseif kind == "raw"then if filename then data = loaddata(filename) or "" end elseif kind == "file"then kind = "raw" data = filename and loaddata(filename) or "" elseif kind == "streamfile" then kind = "stream" data = filename and loaddata(filename) or "" end else if argtype == "number" then objnum = a a, b, c = b, c, d else nofobjects = nofobjects + 1 objnum = nofobjects end if b then if a == "stream" then kind = "stream" data = b elseif a == "file" then -- kind = "raw" data = loaddata(b) elseif a == "streamfile" then kind = "stream" data = loaddata(b) else data = "" -- invalid object end attr = c else -- kind = "raw" data = a end end if not objnum then nofobjects = nofobjects + 1 objnum = nofobjects end -- todo: immediate if kind == "stream" then flushstreamobj(data,objnum,attr,compresslevel,nolength) -- nil == auto elseif objectstream and objcompression ~= false then addtocache(objnum,data) else flushnormalobj(data,objnum) end return objnum end pdfdeferredobject = pdfimmediateobject lpdf.deferredobject = pdfimmediateobject lpdf.immediateobject = pdfimmediateobject -- In lua 5.4 the methods are now moved one metalevel deeper so we need to get them -- from mt.__index instead. (I did get that at first.) It makes for a slightly (imo) -- nicer interface but no real gain in speed as we don't flush that often. local openfile, closefile do -- I used to do but then figured out that when I open and save a file in a mode -- that removes trailing spaces, the xref becomes invalid. The problem was then that a -- reconstruction of the file by a viewer gives weird effects probably because percent symbols -- gets interpreted then. Thanks to Ross Moore for noticing this side effect! local f_used = formatters["%010i 00000 n\013\010"] local f_link = formatters["%010i 00000 f\013\010"] local f_first = formatters["%010i 65535 f\013\010"] local f_pdf_tag = formatters["%%PDF-%i.%i\010"] local f_xref = formatters["xref\0100 %i\010"] local f_trailer_id = formatters["trailer\010<< %s /ID [ <%s> <%s> ] >>\010startxref\010%i\010%%%%EOF"] local f_trailer_no = formatters["trailer\010<< %s >>\010startxref\010%i\010%%%%EOF"] local f_startxref = formatters["startxref\010%i\010%%%%EOF"] local inmemory = false local close = false local update = false local usedname = false local usedsize = false directives.enable("backend.pdf.inmemory", function(v) inmemory = true end) -- local banner = "%\xCC\xD5\xC1\xD4\xC5\xD8\xD0\xC4\xC6\010" -- LUATEXPDF (+128) local banner = "%\xC3\xCF\xCE\xD4\xC5\xD8\xD4\xD0\xC4\xC6\010" -- CONTEXTPDF (+128) openfile = function(filename) -- local arguments = environment.arguments if arguments.ownerpassword then lpdf.setencryption { ownerpassword = arguments.ownerpassword, userpassword = arguments.userpassword, permissions = arguments.permissions, } end -- if inmemory then local n = 0 f = { } flush = function(f,s) n = n + 1 f[n] = s -- offset = offset + #s end close = function(f) f = concat(f) usedsize = #f io.savedata(filename,f) f = false end update = function(f,s) f[1] = s end -- local n = 0 -- f = { -- write = function(self,s) -- n = n + 1 f[n] = s -- end, -- close = function(self) -- f = concat(f) -- io.savedata(filename,f) -- f = false -- end, -- } else f = io.open(filename,"wb") if not f then report() report("quitting because file %a cannot be opened for writing",filename) report() os.exit() end -- f:setvbuf("full",64*1024) local m = getmetatable(f) flush = m.write or m.__index.write close = m.close or m.__index.close update = function(f,s) f:seek("set",0) f:write(s) end end local version = f_pdf_tag(majorversion,minorversion) flush(f,version) flush(f,banner) offset = offset + #version + #banner usedname = filename end closefile = function(abort) if abort then close(f) if not environment.arguments.nodummy then f = io.open(abort,"wb") if f then local name = resolvers.findfile("context-lmtx-error.pdf") if name then local data = io.loaddata(name) if data then f:write(data) f:close() return end end f:close() end end os.remove(abort) else local xrefoffset = offset local lastfree = 0 local noffree = 0 -- local os = objectstream if encryptstream then objectstream = false end local catalog = lpdf.getcatalog() objectstream = os -- local info = lpdf.getinfo() local trailerid = lpdf.gettrailerid() -- if objectstream then flushdeferred() flushcache() -- offset = lpdf.preparesignature and lpdf.preparesignature(flush,f,offset,objects) or offset -- xrefoffset = offset -- nofobjects = nofobjects + 1 objects[nofobjects] = offset -- + 1 -- -- combine these three in one doesn't really give less code so -- we go for the efficient ones -- local nofbytes = 4 local c1, c2, c3, c4 if offset <= 0xFFFF then nofbytes = 2 for i=1,nofobjects do local o = objects[i] if not o then noffree = noffree + 1 else local strm = o < 0 if strm then o = -o end c1 = (o>>8)&0xFF c2 = (o>>0)&0xFF if strm then objects[i] = char(2,c1,c2,streams[o][i]) else objects[i] = char(1,c1,c2,0) end end end if noffree > 0 then for i=nofobjects,1,-1 do local o = objects[i] if not o then local f1 = (lastfree>>8)&0xFF local f2 = (lastfree>>0)&0xFF objects[i] = char(0,f1,f2,0) lastfree = i end end end elseif offset <= 0xFFFFFF then nofbytes = 3 for i=1,nofobjects do local o = objects[i] if not o then noffree = noffree + 1 else local strm = o < 0 if strm then o = -o end c1 = (o>>16)&0xFF c2 = (o>> 8)&0xFF c3 = (o>> 0)&0xFF if strm then objects[i] = char(2,c1,c2,c3,streams[o][i]) else objects[i] = char(1,c1,c2,c3,0) end end end if noffree > 0 then for i=nofobjects,1,-1 do local o = objects[i] if not o then local f1 = (lastfree>>16)&0xFF local f2 = (lastfree>> 8)&0xFF local f3 = (lastfree>> 0)&0xFF objects[i] = char(0,f1,f2,f3,0) lastfree = i end end end else nofbytes = 4 for i=1,nofobjects do local o = objects[i] if not o then noffree = noffree + 1 else local strm = o < 0 if strm then o = -o end c1 = (o>>24)&0xFF c2 = (o>>16)&0xFF c3 = (o>> 8)&0xFF c4 = (o>> 0)&0xFF if strm then objects[i] = char(2,c1,c2,c3,c4,streams[o][i]) else objects[i] = char(1,c1,c2,c3,c4,0) end end end if noffree > 0 then for i=nofobjects,1,-1 do local o = objects[i] if not o then local f1 = (lastfree>>24)&0xFF local f2 = (lastfree>>16)&0xFF local f3 = (lastfree>> 8)&0xFF local f4 = (lastfree>> 0)&0xFF objects[i] = char(0,f1,f2,f3,f4,0) lastfree = i end end end end objects[0] = rep("\0",1+nofbytes+1) local data = concat(objects,"",0,nofobjects) local size = #data local xref = pdfdictionary { Type = pdfconstant("XRef"), Size = nofobjects + 1, W = pdfarray { 1, nofbytes, 1 }, Root = catalog, Info = info, ID = trailerid and pdfarray { pdfliteral(trailerid,true), pdfliteral(trailerid,true) } or nil, Encrypt = encdict or nil, } local fb -- if encryptstream then -- if compress then -- local comp = compressdata(data,size) -- if comp then -- data = comp -- size = #data -- fb = f_stream_b_d_c -- xref.Filter = pdfarray { -- pdfconstant("Crypt"), -- identity -- pdfconstant("FlateDecode") -- } -- else -- xref.Filter = pdfconstant("Crypt") -- identity -- end -- else -- xref.Filter = pdfconstant("Crypt") -- identity -- end -- fb = f_stream_b_d_u -- else if compress then local comp = compressdata(data,size) if comp then data = comp size = #data fb = f_stream_b_d_c else fb = f_stream_b_d_u end else fb = f_stream_b_d_u end -- end -- no encryption of data here flush(f,fb(nofobjects,xref(),size)) flush(f,data) flush(f,s_stream_e) flush(f,f_startxref(xrefoffset)) else flushdeferred() -- offset = lpdf.preparesignature and lpdf.preparesignature(flush,f,offset,objects) or offset -- -- if encryptstream then -- -- unencrypted ! -- local eo = encryptobject -- encryptobject = false -- encdict = pdfreference(pdfimmediateobject(tostring(encdict))) -- encryptobject = eo -- end -- xrefoffset = offset flush(f,f_xref(nofobjects+1)) local trailer = pdfdictionary { Size = nofobjects + 1, Root = catalog, Info = info, Encrypt = encdict or nil, } for i=1,nofobjects do local o = objects[i] if o then objects[i] = f_used(o) end end for i=nofobjects,1,-1 do local o = objects[i] if not o then objects[i] = f_link(lastfree) lastfree = i end end objects[0] = f_first(lastfree) flush(f,concat(objects,"",0,nofobjects)) trailer.Size = nofobjects + 1 if trailerid then flush(f,f_trailer_id(trailer(),trailerid,trailerid,xrefoffset)) else flush(f,f_trailer_no(trailer(),xrefoffset)) end end update(f,f_pdf_tag(majorversion,minorversion)) -- check this usedsize = f:seek("end") close(f) io.flush() if lpdf.finalizesignature then lpdf.finalizesignature(usedname,usedsize) end end io.flush() closefile = function() end end end -- For the moment we overload it here, although back-fil.lua eventually will -- be merged with back-pdf as it's pdf specific, or maybe back-imp-pdf or so. do -- We overload img but at some point it will even go away, so we just -- reimplement what we need in context. This will change completely i.e. -- we will drop the low level interface! local pdfbackend = backends.registered.pdf local nodeinjections = pdfbackend.nodeinjections local codeinjections = pdfbackend.codeinjections local imagetypes = images.types -- pdf png jpg jp2 jbig2 stream local img_none = imagetypes.none local tonode = nuts.tonode local newimagerule = nuts.pool.imagerule local setattrlist = nuts.setattrlist local setprop = nuts.setprop local report_images = logs.reporter("backend","images") local lastindex = 0 local indices = { } local bpfactor = number.dimenfactors.bp function codeinjections.newimage(specification) return specification end function codeinjections.copyimage(original) return setmetatableindex(original) end function codeinjections.scanimage(specification) -- placeholder, doesn't give back dimensions etc but will be plugged in return specification end local function embedimage(specification) if specification then lastindex = lastindex + 1 index = lastindex specification.index = index local xobject = pdfdictionary { } if not specification.notype then xobject.Type = pdf_xobject xobject.Subtype = pdf_form xobject.FormType = 1 end local bbox = specification.bbox if bbox and not specification.nobbox then xobject.BBox = pdfarray { bbox[1] * bpfactor, bbox[2] * bpfactor, bbox[3] * bpfactor, bbox[4] * bpfactor, } end xobject = xobject + specification.attr if bbox and not specification.width then specification.width = bbox[3] end if bbox and not specification.height then specification.height = bbox[4] end local dict = xobject() -- nofobjects = nofobjects + 1 local objnum = nofobjects local nolength = specification.nolength local stream = specification.stream or specification.string -- -- We cannot set type in native img so we need this hack or -- otherwise we need to patch too much. Better that i write -- a wrapper then. Anyway, it has to be done better: a key that -- tells either or not to scale by xsize/ysize when flushing. -- if not specification.type then local kind = specification.kind if kind then -- take that one elseif attr and find(attr,"BBox") then kind = img_stream else -- hack: a bitmap kind = img_none end specification.type = kind specification.kind = kind end flushstreamobj(stream,objnum,dict,compresslevel,nolength) specification.objnum = objnum specification.rotation = specification.rotation or 0 specification.orientation = specification.orientation or 0 specification.transform = specification.transform or 0 specification.stream = nil specification.attr = nil specification.type = specification.kind or specification.type or img_none indices[index] = specification -- better create a real specification return specification end end codeinjections.embedimage = embedimage function codeinjections.wrapimage(specification) -- local index = specification.index if not index then embedimage(specification) end -- local n = newimagerule( specification.width or 0, specification.height or 0, specification.depth or 0 ) setattrlist(n,true) setprop(n,"index",specification.index) return tonode(n) end pdfincludeimage = function(index) local specification = indices[index] if specification then local bbox = specification.bbox local xorigin = bbox[1] local yorigin = bbox[2] local xsize = bbox[3] - xorigin -- we need the original ones, not the 'rotated' ones local ysize = bbox[4] - yorigin -- we need the original ones, not the 'rotated' ones local transform = specification.transform or 0 local objnum = specification.objnum or pdfreserveobject() local groupref = nil local kind = specification.kind or specification.type or img_none -- determines scaling type return kind, xorigin, yorigin, xsize, ysize, transform, objnum, groupref end end lpdf.includeimage = pdfincludeimage end -- The driver. do -- local addsuffix = file.addsuffix local texgetbox = tex.getbox local pdfname = nil local converter = nil local useddriver = nil -- a bit of a hack local function outputfilename(driver) return pdfname end -- local outputfilename ; do -- old todo usedname in ^^ -- local filename = nil -- outputfilename = function(driver,usedname) -- if usedname and usedname ~= "" then -- filename = addsuffix(usedname,"pdf") -- elseif not filename or filename == "" then -- filename = addsuffix(tex.jobname,"pdf") -- end -- return filename -- end -- end -- todo: prevent twice local function prepare(driver) if not environment.initex then -- backends.initialize("pdf") -- also does bindings -- pdfname = tex.jobname .. ".pdf" openfile(pdfname) -- luatex.registerstopactions(1,function() if pdfname then lpdf.finalizedocument() closefile() pdfname = nil end end) -- luatex.registerpageactions(1,function() if pdfname then lpdf.finalizepage(true) end end) -- lpdf.registerdocumentfinalizer(wrapupdocument,nil,"wrapping up") -- statistics.register("result saved in file", function() local status = streamstatus() local outputfilename = environment.outputfilename or environment.jobname or tex.jobname or "" outputfilename = string.gsub(outputfilename,"^%./+","") -- todo: make/use a helper return string.format( "%s.pdf, compresslevel %s, objectcompresslevel %s, %i streams, %i uncompressed, %i compressed, %i not compressed, threshold %i", outputfilename, status.compresslevel, status.objectcompresslevel, status.nofstreams or 0, status.uncompressed or 0, status.compressed or 0, status.notcompressed or 0, status.threshold or 0 ) end) -- luatex.registerstopactions(function() if pdfname then local r = lpdf.lastreferredpage() -- somehow referenced local s = lpdf.getnofpages() -- in page tree, saved in file local t = lpdf.nofpages() -- in tuc file if r > s then report() report("referred pages: %i, saved pages %i, pages from tuc file: %i, possible corrupt file",r,s,t) report() end end end) end converter = drivers.converters.lmtx useddriver = driver end local function wrapup(driver) if pdfname then closefile() pdfname = nil end end local function cleanup(driver) if pdfname then closefile(pdfname) pdfname = nil end end local function convert(driver,boxnumber) converter(driver,texgetbox(boxnumber),"page") end -- localconverter = function(...) -- print(...) -- ok when we add this -- converter(useddriver,...) -- otherwise nil .. lua bug -- end localconverter = function(a,b,c,d) converter(useddriver,a,b,c,d) end drivers.install { name = "pdf", flushers = flushers, actions = { prepare = prepare, wrapup = wrapup, cleanup = cleanup, -- initialize = initialize, convert = convert, finalize = finalize, -- outputfilename = outputfilename, }, } end