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. -- -- There is no way that a lua based backend can compete 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 band, extract = bit32.band, bit32.extract local concat, sortedhash = table.concat, table.sortedhash local setmetatableindex = table.setmetatableindex local loaddata = io.loaddata local bpfactor = number.dimenfactors.bp local md5HEX = md5.HEX local osuuid = os.uuid local zlibcompress = (xzip or zlib).compress local nuts = nodes.nuts local tonut = nodes.tonut local getdata = nuts.getdata local getsubtype = nuts.getsubtype local getwhd = nuts.getwhd local flushlist = nuts.flush_list local pdfincludeimage = lpdf.includeimage local pdfgetfontname = lpdf.getfontname local pdfgetfontobjnumber = lpdf.getfontobjnumber local pdfreserveobject = lpdf.reserveobject local pdfpagereference = lpdf.pagereference local pdfflushobject = lpdf.flushobject local pdfsharedobject = lpdf.shareobjectreference local pdfreference = lpdf.reference local pdfdictionary = lpdf.dictionary local pdfarray = lpdf.array local pdfconstant = lpdf.constant local pdfflushstreamobject = lpdf.flushstreamobject local pdfliteral = lpdf.literal -- not to be confused with a whatsit! local pdf_pages = pdfconstant("Pages") local pdf_page = pdfconstant("Page") local pdf_xobject = pdfconstant("XObject") local pdf_form = pdfconstant("Form") 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") -- used variables local pdf_h, pdf_v local need_tm, need_tf, cur_tmrx, cur_factor, cur_f, cur_e local need_width, need_mode, done_width, done_mode local mode local f_pdf_cur, f_pdf, fs_cur, fs, f_cur local tj_delta, cw local usedfonts, usedxforms, usedximages, usedxgroups local getxformname, getximagename local boundingbox, shippingmode, objectnumber local tmrx, tmry, tmsx, tmsy, tmtx, tmty local cmrx, cmry, cmsx, cmsy, cmtx, cmty local tmef local function usefont(t,k) -- a bit redundant hash local v = pdfgetfontname(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 tmtx, tmty = 0.0, 0.0 tmef = 1.0 need_tm = false need_tf = false 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_cur = 0 f_pdf_cur = 0 -- nullfont f_pdf = 0 -- nullfont fs_cur = 0 fs = 0 cur_factor = 0 cur_f = false cur_e = false tj_delta = 0.0 cw = 0.0 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 local function flush_buffer() b = 0 end -- fonts local fontcharacters local fontdescriptions local fontparameters local fontproperties local usedcharacters = setmetatableindex("table") local pdfcharacters local horizontalmode = true ----- widefontmode = true local scalefactor = 1 local threshold = 655360 local thresfactor = 100 local tjfactor = 100 / 65536 lpdf.usedcharacters = usedcharacters local function updatefontstate(font) 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" -- widefontmode = fontproperties.encodingbytes == 2 scalefactor = (designsize/size) * tjfactor local fthreshold = fontproperties.threshold threshold = (fthreshold and (size * fthreshold / 100)) or 655360 -- when we bolden the threshold should be smaller .. a hack .. i need to redo all this if (fontparameters.extendfactor or 1) == 1 then -- we're probably okay elseif fontparameters.hshift or fontparameters.vshift then -- we could be okay else -- some vf magic going on threshold = threshold / 5 end end -- helpers local f_cm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N cm"] local f_tm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N Tm"] 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_width = 0 need_mode = 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 saved_chararray_pos_h local saved_chararray_pos_v local saved_b = 0 local function begin_chararray() saved_chararray_pos_h = pdf_h saved_chararray_pos_v = pdf_v cw = 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 local function end_chararray() 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 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 = cw - h return tj_delta ~= 0 or v ~= pdf_v else tmtx = h - saved_text_pos_h tj_delta = cw + 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) 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 = cw if tj_delta == 0 and pdf_v == 0 then return end else tmtx = - saved_text_pos_h tj_delta = cw 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) 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) 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 local flushcharacter 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 naturalwidth = nil local hshift = false local vshift = false -- local naturalwidths = setmetatableindex(function(t,font) -- local d = descriptions[font] -- local c = characters[font] -- local f = parameters[font].hfactor -- local v = setmetatableindex(function(t,char) -- local w -- local e = d and d[char] -- if e then -- w = e.width -- if w then -- w = w * f -- end -- end -- if not w then -- e = c[char] -- if e then -- w = e.width or 0 -- end -- end -- if not w then -- w = 0 -- end -- t[char] = w -- return w -- end) -- t[font] = v -- return v -- end) local naturalwidths = setmetatableindex(function(t,font) local d = descriptions[font] local c = characters[font] local f = parameters[font].hfactor local v = setmetatableindex(function(t,char) local w local e = c[char] if e then w = e.width or 0 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) local function setup_fontparameters(font,factor,f,e) local slant = fontparameters.slantfactor or 0 local extend = fontparameters.extendfactor or 1 local squeeze = fontparameters.squeezefactor or 1 local expand = 1 + factor / 1000000 local format = fontproperties.format if e then extend = extend * e end tmef = expand tmrx = expand * extend tmsy = slant tmry = squeeze need_width = fontparameters.width or 0 need_mode = fontparameters.mode or 0 f_cur = font f_pdf = usedfonts[font] -- cache cur_factor = factor cur_f = f cur_e = e tj_delta = 0 fs = fontparameters.size * bpfactor if f then fs = fs * f end -- kind of special: if format == "opentype" or format == "type1" then fs = fs * 1000 / fontparameters.units -- can we avoid this ? end -- naturalwidth = naturalwidths[font] -- hshift = fontparameters.hshift vshift = fontparameters.vshift end local f_width = formatters["%.6N w"] local f_mode = formatters["%i Tr"] -- can be hash local f_font = formatters["/F%i %.6N Tf"] -- can be hash local s_width = "0 w" local s_mode = "0 Tr" local width_factor = 72.27 / 72000.0 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 b = b + 1 ; buffer[b] = f_font(f_pdf,fs) f_pdf_cur = f_pdf fs_cur = fs 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) pdf_h = saved_text_pos_h + tmtx pdf_v = saved_text_pos_v + tmty need_tm = false end cur_tmrx = tmrx end local f_hex_4 = formatters["%04X"] local f_hex_2 = formatters["%02X"] local h_hex_4 = setmetatableindex(function(t,k) -- we already have this somewhere if k < 256 then -- maybe 512 -- not sparse in this range for i=0,255 do t[i] = f_hex_4(i) end return t[k] else local v = f_hex_4(k) t[k] = v return v end end) local h_hex_2 = setmetatableindex(function(t,k) -- we already have this somewhere local v = k < 256 and f_hex_2(k) or "00" t[k] = v return v end) -- local trace_threshold = false trackers.register("backends.pdf.threshold", function(v) trace_threshold = v end) -- local 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. flushcharacter = function(current,pos_h,pos_v,pos_r,font,char,data,f,e,factor) -- ,naturalwidth,width) if need_tf or font ~= f_cur or f_pdf ~= f_pdf_cur or fs ~= fs_cur or mode == "page" then pdf_goto_textmode() setup_fontparameters(font,factor,f,e) set_font() elseif cur_tmrx ~= tmrx or cur_factor ~= factor or cur_f ~= f or cur_e ~= e then setup_fontparameters(font,factor,f,e) need_tm = true end local move = calc_pdfpos(pos_h,pos_v) -- if trace_threshold then -- report( -- "font %i, char %C, factor %i, naturalwidth %p, move %l, tm %l, hpos %p, delta %p, threshold %p, cw %p", -- font,char,factor,naturalwidth[char],move,need_tm,pos_h,tj_delta,threshold,cw -- ) -- end 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 >= threshold or tj_delta <= -threshold then need_tm = true end else if (saved_text_pos_h + tmtx) ~= pdf_h then need_tm = true elseif tj_delta >= threshold or tj_delta <= -threshold 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 * scalefactor 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) end cw = cw - tj_delta end end if mode == "chararray" then begin_charmode() end cw = cw + naturalwidth[char] * tmef local index = data.index or char b = b + 1 ; buffer[b] = font > 0 and h_hex_4[index] or h_hex_2[index] if not pdfcharacters[index] then pdfcharacters[index] = true end end flushfontchar = function(font,char,data) local dummy = usedfonts[font] local index = data.index or char if not pdfcharacters[index] then pdfcharacters[index] = true end return dummy 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,mode,str) if mode then if not str then mode, str = originliteral_code, mode elseif mode == "mode" then mode = literalvalues[str] 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 pdf_end_string_nl() need_tm = true elseif mode == rawliteral_code then pdf_end_string_nl() end return else mode = literalvalues[mode] end else local p = nodeproperties[current] if p then str = p.data mode = p.mode else str, mode = getdata(current) end end if str and str ~= "" then 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 pdf_end_string_nl() need_tm = true elseif mode == rawliteral_code then pdf_end_string_nl() else report("check literal") pdf_goto_pagemode() pdf_set_pos(pos_h,pos_v) end b = b + 1 ; buffer[b] = str end end updaters.register("backend.update.pdf",function() function pdf.print(mode,str) 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 pdf_goto_pagemode() -- pdf_set_pos(pdf_h,pdf_v) end b = b + 1 ; buffer[b] = str flush_buffer() end end end) end -- grouping & orientation local flushsave, flushrestore, flushsetmatrix do local matrices = { } local positions = { } local nofpositions = 0 local nofmatrices = 0 local f_matrix = formatters["%s 0 0 cm"] 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 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 function pdf_set_matrix(str,pos_h,pos_v) if shippingmode == "page" then local rx, sx, sy, ry = splitupstring(str," ") if rx and ry and sx and ry then rx, sx, sy, ry = tonumber(rx), tonumber(sx), tonumber(sy), tonumber(ry) 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 end end local nodeproperties = nodes.properties.data flushsetmatrix = function(current,pos_h,pos_v) local str if type(current) == "string" then str = current else local p = nodeproperties[current] if p then str = p.matrix else str = getdata(current) -- for the moment end end if str and str ~= "" then pdf_set_matrix(str,pos_h,pos_v) pdf_goto_pagemode() pdf_set_pos(pos_h,pos_v) b = b + 1 ; buffer[b] = f_matrix(str) end end do local function hasmatrix() return nofmatrices > 0 end local function getmatrix() if nofmatrices > 0 then return unpack(matrices[nofmatrices]) else return 1, 0, 0, 1, 0, 0 end end updaters.register("backend.update.pdf",function() pdf.hasmatrix = hasmatrix pdf.getmatrix = getmatrix end) end 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 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 -- pushorientation = function(orientation,pos_h,pos_v,pos_r) -- flushsave(false,pos_h,pos_v) -- if orientation == 1 then -- flushsetmatrix("0 -1 1 0",pos_h,pos_v) -- elseif orientation == 2 then -- flushsetmatrix("-1 0 0 -1",pos_h,pos_v) -- elseif orientation == 3 then -- flushsetmatrix("0 1 -1 0",pos_h,pos_v) -- end -- end -- poporientation = function(orientation,pos_h,pos_v,pos_r) -- flushrestore(false,pos_h,pos_v) -- end end -- rules local flushedxforms = { } -- actually box resources but can also be direct local localconverter = nil -- will be set local flushrule, flushsimplerule, flushspecialrule, flushimage, flushgroup do local rulecodes = nodes.rulecodes local newrule = nodes.pool.rule local setprop = nuts.setprop local getprop = nuts.getprop 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 local rule_callback = callbacks.functions.process_rule 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_f = formatters["[] 0 d 0 J %.6N w %.6N %.6N %.6N %.6N re 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 updaters.register("backend.update.pdf",function() pdf.getxformname = getxformname end) local function saveboxresource(box,attributes,resources,immediate,kind,margin) n = n + 1 local immediate = true local margin = margin or 0 -- or dimension local objnum = pdfreserveobject() local list = tonut(type(box) == "number" and tex.takebox(box) or box) -- 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, } 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 local function 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 = newrule(wd,ht,dp) -- newboxrule rule.subtype = boxrule_code setprop(tonut(rule),"index",index) return 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 local function getboxresourcebox(index) local l = boxresources[index] if l then return l.list end end updaters.register("backend.update.tex",function() tex.saveboxresource = saveboxresource tex.useboxresource = useboxresource tex.getboxresourcedimensions = getboxresourcedimensions tex.getboxresourcebox = getboxresourcebox 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) tx = cmtx * bpfactor 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_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 memstream local img_none = imagetypes.none local img_pdf = imagetypes.pdf local img_stream = imagetypes.stream local img_memstream = imagetypes.memstream local one_bp = 65536 * bpfactor local imageresources, n = { }, 0 getximagename = function(index) local l = imageresources[index] if l then return l.name else report("no image resource %S",index) end end updaters.register("backend.update.pdf",function() pdf.getximagename = getximagename 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 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) groups = groups + 1 usedxgroups[groups] = objnum return f_gr(groups) end 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 or kind == img_memstream 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 band(transform,7) > 3 then -- mirror rx, tx = -rx, -tx end local t = band(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 band(transform,7) > 3 then t = t + 1 end t = band(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 -- 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. flushrule = 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) end if 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) rule_callback(current,size_h,size_v,pos_r) -- so we pass direction b = b + 1 ; buffer[b] = s_e return end pdf_goto_pagemode() -- local saved_b = b 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) if subtype == outlinerule_code then local linewidth = getdata(current) if linewidth > 0 then rule = f_w(linewidth * bpfactor,dim_h,dim_v) else rule = f_o(dim_h,dim_v) end else rule = f_f(dim_h,dim_v) end end b = b + 1 ; buffer[b] = rule b = b + 1 ; buffer[b] = s_e -- buffer[saved_b] = concat(buffer," ",saved_b,b) -- b = saved_b end flushsimplerule = 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 flushspecialrule = function(pos_h,pos_v,pos_r,width,height,depth,line,outline) 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 rule = f_f(line,half,-depth+half,width-line,total-line) 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 wrapup, 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) -- mapping can be a hash so: local list = table.sortedkeys(mapping) local n = #list if n == nofpages 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",nofpages) 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 wrapup = 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 pdf_h, pdf_v = 0, 0 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 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("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 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"] finalize = function(driver,details) level = level + 1 pdf_goto_pagemode() -- for now local objnum = details.objnum local specification = details.specification 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(pdfgetfontobjnumber(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(k)] = pdfreference(k) 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 xorigin, yorigin, relocated = backends.codeinjections.getpageorigin() -- for now here local bbox = pdfarray { (boundingbox[1] + xorigin) * bpfactor, (boundingbox[2] + yorigin) * bpfactor, (boundingbox[3] + xorigin) * bpfactor, (boundingbox[4] + yorigin) * bpfactor, } if relocated then content = formatters["1 0 0 1 %.6N %.6N cm\n%s"](bbox[1],bbox[2],content) end local contentsobj = pdfflushstreamobject(content,false,false) 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 registerpage(pageattributes) lpdf.finalizepage(true) -- if relocated then -- if pageattributes.TrimBox then pageattributes.TrimBox = box end -- if pageattributes.CropBox then pageattributes.CropBox = box end -- if pageattributes.BleedBox then pageattributes.BleedBox = box end -- end if pageattributes.TrimBox then pageattributes.TrimBox = relocated and pdfsharedobject(box or pageattributes.TrimBox ) end if pageattributes.CropBox then pageattributes.CropBox = relocated and pdfsharedobject(box or pageattributes.CropBox ) end if pageattributes.BleedBox then pageattributes.BleedBox = relocated and pdfsharedobject(box or pageattributes.BleedBox) 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, } 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 -- todo: additional = resources local boxresources = lpdf.collectedresources { 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,false,objectnumber) pdfflushstreamobject(content,wrapper,false,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 updaters.register("backend.update.pdf",function() job.positions.registerhandlers { getpos = drivers.getpos, getrpos = drivers.getrpos, gethpos = drivers.gethpos, getvpos = drivers.getvpos, } end) updaters.register("backend.update",function() local saveboxresource = tex.boxresources.save -- -- also in lpdf-res .. brrr .. needs fixing -- backends.codeinjections.registerboxresource = function(n,offset) local r = saveboxresource(n,nil,nil,false,0,offset or 0) return r 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 threshold = 40 -- also #("/Filter /FlateDecode") (compression threshold) local objectstream = true local compress = true local cache = false local info = "" local catalog = "" local lastdeferred = false local majorversion = 1 local minorversion = 7 local trailerid = true directives.register("backend.pdf.threshold",function(v) if v then threshold = tonumber(v) or 40 else threshold = -1000 end end) 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 local function setinfo() end -- we get it local function setcatalog() end -- we get it local function settrailerid(v) trailerid = v or false end local function setmajorversion(v) majorversion = tonumber(v) or majorversion end local function setminorversion(v) minorversion = tonumber(v) or minorversion end local function getmajorversion(v) return majorversion end local function getminorversion(v) return minorversion end local function setcompresslevel (v) compress = v and v ~= 0 and true or false end local function setobjcompresslevel(v) objectstream = v and v ~= 0 and true or false end local function getcompresslevel (v) return compress and 3 or 0 end local function getobjcompresslevel(v) return objectstream and 1 or 0 end local function setpageresources () end -- needs to be sorted out local function setpageattributes () end local function setpagesattributes() end updaters.register("backend.update.pdf",function() pdf.setinfo = setinfo pdf.setcatalog = setcatalog pdf.settrailerid = settrailerid pdf.setmajorversion = setmajorversion pdf.setminorversion = setminorversion pdf.getmajorversion = getmajorversion pdf.getminorversion = getminorversion pdf.setcompresslevel = setcompresslevel pdf.setobjcompresslevel = setobjcompresslevel pdf.getcompresslevel = getcompresslevel pdf.getobjcompresslevel = getobjcompresslevel pdf.setpageresources = setpageresources pdf.setpageattributes = setpageattributes pdf.setpagesattributes = setpagesattributes 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 b = nil local e = s_stream_e if compress then local comp = zlibcompress(data,3) if comp and #comp < #data then data = comp b = f_stream_b_d_c(cache,strobj(),#data) else b = f_stream_b_d_u(cache,strobj(),#data) end else b = f_stream_b_d_u(cache,strobj(),#data) end flush(f,b) flush(f,data) flush(f,e) offset = offset + #b + #data + #e data, d = { }, 0 list, l = { }, 0 coffset = 0 indices = { } end end end local function pdfreserveobj() nofobjects = nofobjects + 1 objects[nofobjects] = false return nofobjects end local pages = table.setmetatableindex(function(t,k) local v = pdfreserveobj() t[k] = v return v end) local function getpageref(n) return pages[n] end local function refobj() -- not needed, as we have auto-delay end local function flushnormalobj(data,n) if not n then nofobjects = nofobjects + 1 n = nofobjects 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 function flushstreamobj(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 level == 0 then local b = nil local e = s_stream_e if nolength then b = f_stream_b_d_r(n,dict) elseif comp ~= false and compress and size > threshold then local compdata = zlibcompress(data,3) if compdata then local compsize = #compdata if compsize > size - threshold then b = dict and f_stream_b_d_u(n,dict,size) or f_stream_b_n_u(n,size) else data = compdata b = dict and f_stream_b_d_c(n,dict,compsize) or f_stream_b_n_c(n,compsize) end else b = dict and f_stream_b_d_u(n,dict,size) or f_stream_b_n_u(n,size) end else b = dict and f_stream_b_d_u(n,dict,size) or f_stream_b_n_u(n,size) end flush(f,b) flush(f,data) flush(f,e) objects[n] = offset offset = offset + #b + #data + #e else if nolength then data = f_stream_d_r(n,dict,data) elseif comp ~= false and compress and size > threshold then local compdata = zlibcompress(data,3) if compdata then local compsize = #compdata if compsize > size - threshold then data = dict and f_stream_d_u(n,dict,size,data) or f_stream_n_u(n,size,data) else data = dict and f_stream_d_c(n,dict,compsize,compdata) or f_stream_n_c(n,compsize,compdata) end else data = dict and f_stream_d_u(n,dict,size,data) or f_stream_n_u(n,size,data) end else data = dict and f_stream_d_u(n,dict,size,data) or f_stream_n_u(n,size,data) end if not lastdeferred then lastdeferred = n elseif n < lastdeferred then lastdeferred = n end objects[n] = data end return n 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 -- n = pdf.obj([n,] objtext) -- n = pdf.obj([n,] "file", filename) -- n = pdf.obj([n,] "stream", streamtext [, attrtext]) -- n = pdf.obj([n,] "streamfile", filename [, attrtext]) -- -- n = pdf.obj { -- type = , -- raw|stream -- immediate = , -- objnum = , -- attr = , -- compresslevel = , -- objcompression = , -- file = , -- string = , -- nolength = , -- } local function obj(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 and compresslevel > 0 or nil,nolength) elseif objectstream and objcompression ~= false then addtocache(objnum,data) else flushnormalobj(data,objnum) end return objnum end updaters.register("backend.update.pdf",function() pdf.reserveobj = pdfreserveobj pdf.getpageref = getpageref pdf.refobj = refobj pdf.flushstreamobj = flushstreamobj pdf.flushnormalobj = flushnormalobj pdf.obj = obj pdf.immediateobj = obj end) -- 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 = 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 removefile = os.remove openfile = function(filename) if inmemory then local n = 0 f = { } flush = function(f,s) n = n + 1 f[n] = s end close = function(f) f = concat(f) io.savedata(filename,f) f = false 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 -- message 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 end -- flush_buffer = function() if b > 0 then flush(f,concat(buffer,"",1,b)) b = 0 end end local v = f_pdf(majorversion,minorversion) -- local b = "%\xCC\xD5\xC1\xD4\xC5\xD8\xD0\xC4\xC6\010" -- LUATEXPDF (+128) local b = "%\xC3\xCF\xCE\xD4\xC5\xD8\xD4\xD0\xC4\xC6\010" -- CONTEXTPDF (+128) flush(f,v) flush(f,b) offset = #v + #b end closefile = function(abort) if abort then f:close() 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 catalog = lpdf.getcatalog() local info = lpdf.getinfo() if trailerid == true then trailerid = md5HEX(osuuid()) elseif trailerid and #trailerid > 32 then trailerid = md5HEX(trailerid) else trailerid = false end if objectstream then flushdeferred() flushcache() -- 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 = extract(o,8,8) c2 = extract(o,0,8) 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 = extract(lastfree,8,8) local f2 = extract(lastfree,0,8) 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 = extract(o,16,8) c2 = extract(o, 8,8) c3 = extract(o, 0,8) 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 = extract(lastfree,16,8) local f2 = extract(lastfree, 8,8) local f3 = extract(lastfree, 0,8) 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 = extract(o,24,8) c2 = extract(o,16,8) c3 = extract(o, 8,8) c4 = extract(o, 0,8) 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 = extract(lastfree,24,8) local f2 = extract(lastfree,16,8) local f3 = extract(lastfree, 8,8) local f4 = extract(lastfree, 0,8) 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 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, } if compress then local comp = zlibcompress(data,3) if comp then data = comp flush(f,f_stream_b_d_c(nofobjects,xref(),#data)) else flush(f,f_stream_b_d_u(nofobjects,xref(),#data)) end else flush(f,f_stream_b_d_u(nofobjects,xref(),#data)) end flush(f,data) flush(f,s_stream_e) flush(f,f_startxref(xrefoffset)) else flushdeferred() xrefoffset = offset flush(f,f_xref(nofobjects+1)) local trailer = pdfdictionary { Size = nofobjects+1, Root = catalog, Info = info, } 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 f:close() 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. updaters.register("backend.update.pdf",function() -- 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 codeinjections = backends.pdf.codeinjections local imagetypes = images.types -- pdf png jpg jp2 jbig2 stream memstream local img_none = imagetypes.none local rulecodes = nodes.rulecodes local setprop = nodes.nuts.setprop local report_images = logs.reporter("backend","images") local lastindex = 0 local indices = { } local bpfactor = number.dimenfactors.bp local imagerule_code = rulecodes.image function codeinjections.newimage(specification) return specification end function codeinjections.copyimage(original) return setmetatableindex(original) end function codeinjections.scanimgage(specification) 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 local compress = compresslevel and compresslevel > 0 or nil flushstreamobj(stream,objnum,dict,compress,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 width = specification.width or 0 local height = specification.height or 0 local depth = specification.depth or 0 -- newimagerule local n = nodes.pool.rule(width,height,depth) n.subtype = imagerule_code setprop(tonut(n),"index",specification.index) return n end function pdf.includeimage(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 pdfreserveobj() 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 end) updaters.register("backend.update.lpdf",function() -- todo: an md5 or sha2 hash can save space -- todo: make a type 3 font instead -- todo: move to lpdf namespace local pdfimage = lpdf.epdf.image local newpdf = pdfimage.new local openpdf = pdfimage.open local closepdf = pdfimage.close local copypage = pdfimage.copy local embedimage = images.embed local nofstreams = 0 local topdf = { } local toidx = { } local function storedata_s(pdf) local idx = toidx[pdf] if not idx then nofstreams = nofstreams + 1 idx = nofstreams toidx[pdf] = nofstreams topdf[idx] = pdf end return idx end local function vfimage_s(id,wd,ht,dp,pos_h,pos_v) local index = topdf[id] if type(index) == "string" then local pdfdoc = newpdf(index,#index) local image = copypage(pdfdoc) local bbox = image.bbox image.width = bbox[3] - bbox[1] image.height = bbox[4] - bbox[2] embedimage(image) index = image.index topdf[id] = index end -- pdf.print or pdf.literal flushimage(index,wd,ht,dp,pos_h,pos_v) end local function storedata_n(name,page) local idx = toidx[pdf] if not idx then nofstreams = nofstreams + 1 idx = nofstreams toidx[pdf] = nofstreams topdf[idx] = pdf end return idx end -- We need to have a way to close such a pdf ... esp for fonts. local pdfdocs = { } local function vfimage_n(name,page,wd,ht,dp,pos_h,pos_v) local d = pdfdocs[name] if not d then d = { doc = openpdf(name), pages = { } } pdfdocs[name] = d end local index = d.pages[page] if not index then local image = copypage(d.doc,page) local bbox = image.bbox image.width = bbox[3] - bbox[1] image.height = bbox[4] - bbox[2] embedimage(image) index = image.index d.pages[page] = index end flushimage(index,wd,ht,dp,pos_h,pos_v) end local function pdfvfimage(wd,ht,dp,data,name) if type(data) == "number" then return { "lua", function(font,char,pos_h,pos_v) vfimage_n(name,data,wd,ht,dp,pos_h,pos_v) end } else return { "lua", function(font,char,pos_h,pos_v) local id = storedata_s(data) vfimage_s(id,wd,ht,dp,pos_h,pos_v) end } end end lpdf.vfimage = pdfvfimage 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 -- todo: prevent twice local function prepare(driver) if not environment.initex then -- install new functions in pdf namespace updaters.apply("backend.update.pdf") -- install new functions in lpdf namespace updaters.apply("backend.update.lpdf") -- adapt existing shortcuts to lpdf namespace updaters.apply("backend.update.tex") -- adapt existing shortcuts to tex namespace updaters.apply("backend.update") -- -- if rawget(pdf,"setforcefile") then -- pdf.setforcefile(false) -- default anyway -- end -- -- pdfname = file.addsuffix(tex.jobname,"pdf") pdfname = tex.jobname .. ".pdf" openfile(pdfname) -- luatex.registerstopactions(1,function() if pdfname then lpdf.finalizedocument() closefile() end end) -- luatex.registerpageactions(1,function() if pdfname then lpdf.finalizepage(true) end end) -- -- lpdf.registerdocumentfinalizer(wrapup,nil,"wrapping up") -- end -- environment.lmtxmode = CONTEXTLMTXMODE -- 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(...) converter(useddriver,...) end drivers.install { name = "pdf", flushers = { character = flushcharacter, fontchar = flushfontchar, rule = flushrule, simplerule = flushsimplerule, specialrule = flushspecialrule, pushorientation = pushorientation, poporientation = poporientation, -- literal = flushliteral, setmatrix = flushsetmatrix, save = flushsave, restore = flushrestore, image = flushimage, group = flushgroup, -- updatefontstate = updatefontstate, }, actions = { prepare = prepare, wrapup = wrapup, cleanup = cleanup, -- initialize = initialize, convert = convert, finalize = finalize, -- outputfilename = outputfilename, }, } end