lpdf-lmt.lmt /size: 93 Kb    last modification: 2021-10-28 13:51
1if not modules then modules = { } end modules ['lpdf-lmt'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to lpdf-ini.mkiv",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10-- The code below was originally in back-lpd.lua but it makes more sense in this
11-- namespace. I will rename variables.
12
13-- There is no way that a lua based backend can compete performance wise with the
14-- original one for relative simple text runs. And we're talking seconds here on say
15-- 500 pages with paragraphs alternativng between three fonts and colors. But such
16-- documents are rare so in practice we are quite okay, especially because in
17-- ConTeXt we can gain quite a bit elsewhere. So, when we loose 30% on such simple
18-- documents, we break even on for instance the manual, and gain 30% on Thomas's
19-- turture test (also for other reasons). But .. who knows what magic I can cook up
20-- in due time.
21
22-- If you consider this complex, watch:
23--
24-- https://www.youtube.com/watch?v=6H-cAzfB2qo
25--
26-- or in distractionmode:
27--
28-- https://www.youtube.com/watch?v=TYuTE_1jvvE
29-- https://www.youtube.com/watch?v=nnicGKX3lvM
30--
31-- For the moment we have to support the built in backend as well as the alternative. So
32-- the next interface is suboptimal and will change at some time. At that moment I will
33-- also optimize and extend.
34
35local type, next, unpack, tonumber, rawget = type, next, unpack, tonumber, rawget
36local char, rep, find = string.char, string.rep, string.find
37local formatters, splitupstring = string.formatters, string.splitup
38local concat, sortedhash = table.concat, table.sortedhash
39local setmetatableindex = table.setmetatableindex
40local loaddata = io.loaddata
41
42local bpfactor               = number.dimenfactors.bp
43
44local osuuid                 = os.uuid
45local zlibcompresssize       = xzip.compresssize
46
47local nuts                   = nodes.nuts
48local tonut                  = nodes.tonut
49local tonode                 = nuts.tonode
50
51local pdfreference           = lpdf.reference
52local pdfdictionary          = lpdf.dictionary
53local pdfarray               = lpdf.array
54local pdfconstant            = lpdf.constant
55local pdfliteral             = lpdf.literal -- not to be confused with a whatsit!
56
57local pdfreserveobject       -- forward reference
58local pdfpagereference       -- forward reference
59local pdfgetpagereference    -- forward reference
60local pdfsharedobject        -- forward reference
61local pdfflushobject         -- forward reference
62local pdfflushstreamobject   -- forward reference
63local pdfdeferredobject      -- forward reference
64local pdfimmediateobject     -- forward reference
65
66local pdfincludeimage        -- forward reference
67
68local pdf_pages      = pdfconstant("Pages")
69local pdf_page       = pdfconstant("Page")
70local pdf_xobject    = pdfconstant("XObject")
71local pdf_form       = pdfconstant("Form")
72local pdf_pattern    = pdfconstant("Pattern")
73
74local fonthashes     = fonts.hashes
75local characters     = fonthashes.characters
76local descriptions   = fonthashes.descriptions
77local parameters     = fonthashes.parameters
78local properties     = fonthashes.properties
79
80local report         = logs.reporter("backend")
81local report_objects = logs.reporter("backend","objects")
82local report_fonts   = logs.reporter("backend","fonts")
83
84local trace_objects  = false  trackers.register("backend.objects",       function(v) trace_objects = v end)
85local trace_details  = false  trackers.register("backend.details",       function(v) trace_details = v end)
86local trace_indices  = false  trackers.register("backend.fonts.details", function(v) trace_indices = v end)
87
88-- These two tables used a font id as index and will be metatabled in lpdf-emb.lmt:
89
90local usedfontnames   = { }
91local usedfontobjects = { }
92
93lpdf.usedfontnames    = usedfontnames
94lpdf.usedfontobjects  = usedfontobjects
95
96-- experiment:
97
98local function compressdata(data,size)
99    local guess = ((size // 4096) + 1) * 2048
100    local comp  = zlibcompresssize(data,guess,3)
101 -- if comp then
102 --     report()
103 --     report("size %i, guess %i, result %i => %s / %s",size,guess,#comp,guess>=#comp and "hit" or "miss")
104 --     report()
105 -- end
106    return comp
107end
108
109-- local function compressdata(data,size)
110--     return zlibcompress(data,3)
111-- end
112
113-- we collect them:
114
115local flushers = { }
116
117-- used variables
118
119local pdf_h, pdf_v
120local need_tm, need_tf, cur_tmrx, cur_factor, cur_f, cur_e
121local need_width, need_mode, done_width, done_mode
122local mode
123local f_pdf_cur, f_pdf, fs_cur, fs, f_cur, f_x_scale, f_y_scale
124local tj_delta, cw
125local usedfonts, usedxforms, usedximages, usedxgroups
126local getxformname, getximagename
127local boundingbox, shippingmode, objectnumber
128local tmrx, tmry, tmsx, tmsy, tmtx, tmty
129local cmrx, cmry, cmsx, cmsy, cmtx, cmty
130local tmef
131
132local function usefont(t,k) -- a bit redundant hash
133 -- local v = pdfgetfontname(k)
134    local v = usedfontnames[k]
135    t[k] = v
136    return v
137end
138
139local function reset_variables(specification)
140    pdf_h, pdf_v  = 0, 0
141    cmrx, cmry    = 1.0, 1.0
142    cmsx, cmsy    = 0.0, 0.0
143    cmtx, cmty    = 0.0, 0.0
144    tmrx, tmry    = 1.0, 1.0
145    tmsx, tmsy    = 0.0, 0.0
146    tmtx, tmty    = 0.0, 0.0
147    tmef          = 1.0
148    need_tm       = false
149    need_tf       = false
150    need_width    = 0
151    need_mode     = 0
152    done_width    = false
153    done_mode     = false
154    mode          = "page"
155    shippingmode  = specification.shippingmode
156    objectnumber  = specification.objectnumber
157    cur_tmrx      = 0.0
158    f_cur         = 0
159    f_pdf_cur     = 0 -- nullfont
160    f_pdf         = 0 -- nullfont
161    fs_cur        = 0
162    fs            = 0
163    f_x_scale     = 1.0
164    f_y_scale     = 1.0
165    cur_factor    = 0
166    cur_f         = false -- todo: check for nil vs false
167    cur_e         = false -- todo: check for nil vs false
168    tj_delta      = 0.0
169    cw            = 0.0
170    usedfonts     = setmetatableindex(usefont)
171    usedxforms    = { }
172    usedximages   = { }
173 -- usedxgroups   = { }
174    boundingbox   = specification.boundingbox
175end
176
177-- buffer
178
179local buffer = lua.newtable(1024,0) -- { }
180local b      = 0
181
182local function reset_buffer()
183    b = 0
184end
185
186-- fonts
187
188local fontcharacters
189local fontdescriptions
190local fontparameters
191local fontproperties
192local pdfcharacters
193
194local getstreamhash = fonts.handlers.otf.getstreamhash
195
196local usedfontstreams = { }
197
198local usedindices = setmetatableindex(function(t,k)
199    local n = 0
200    local v = setmetatableindex(function(tt,kk)
201        if n >= 0xFFFF then
202            report_fonts("registering character index: overflow in hash %a, todo: use overflow font")
203        else
204            n = n + 1
205        end
206        if trace_indices then
207            report_fonts("registering character index: hash %a, charindex 0x%05X, slotindex 0x%04X",k,kk,n)
208        end
209        local vv = n
210        tt[kk] = vv
211        return vv
212    end)
213    t[k] = v
214    return v
215end)
216
217local usedcharacters = setmetatableindex(function(t,k)
218    local h, d = getstreamhash(k)
219    if trace_indices then
220        report_fonts("registering index table: hash %a, fontid %i",h,k)
221    end
222    usedfontstreams[h] = d
223    local v = usedindices[h]
224    t[k] = v
225    return v
226end)
227
228lpdf.usedfontstreams = usedfontstreams -- [streamhash]        -> fontdata
229lpdf.usedcharacters  = usedcharacters  -- [fontid]            -> indices
230lpdf.usedindices     = usedindices     -- [streamhash][index] -> realindex (can also be dupindex)
231
232local horizontalmode = true
233local scalefactor    = 1
234local threshold      = 655360
235local thresfactor    = 100
236local tjfactor       = 100 / 65536
237
238function flushers.updatefontstate(font)
239    fontcharacters   = characters[font]
240    fontdescriptions = descriptions[font]
241    fontparameters   = parameters[font]
242    fontproperties   = properties[font]
243    local size       = fontparameters.size -- or bad news
244    local designsize = fontparameters.designsize or size
245    pdfcharacters    = usedcharacters[font]
246    horizontalmode   = fontparameters.writingmode ~= "vertical"
247    scalefactor      = (designsize/size) * tjfactor
248    --
249    local fthreshold = fontproperties.threshold
250    threshold        = (fthreshold and (size * fthreshold / 100)) or 655360
251    -- when we bolden the threshold should be smaller .. a hack .. i need to redo all this
252    if (fontparameters.extendfactor or 1) == 1 then
253        -- we're probably okay
254    elseif fontparameters.hshift or fontparameters.vshift then
255        -- we could be okay
256    else
257        -- some vf magic going on
258        threshold = threshold / 5
259    end
260end
261
262-- helpers
263
264local f_cm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N cm"]
265local f_tm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N Tm"]
266
267local saved_text_pos_v = 0
268local saved_text_pos_h = 0
269
270local function begin_text()
271    saved_text_pos_h = pdf_h
272    saved_text_pos_v = pdf_v
273    b = b + 1 ; buffer[b] = "BT"
274    need_tf    = true
275    need_width = 0
276    need_mode  = 0
277    mode       = "text"
278end
279
280local function end_text()
281    if done_width then
282        b = b + 1 ; buffer[b] = "0 w"
283        done_width = false
284    end
285    if done_mode then
286        b = b + 1 ; buffer[b] = "0 Tr"
287        done_mode = false
288    end
289    b = b + 1 ; buffer[b] = "ET"
290    pdf_h = saved_text_pos_h
291    pdf_v = saved_text_pos_v
292    mode  = "page"
293end
294
295local saved_chararray_pos_h
296local saved_chararray_pos_v
297
298local saved_b = 0
299
300local function begin_chararray()
301    saved_chararray_pos_h = pdf_h
302    saved_chararray_pos_v = pdf_v
303    cw = horizontalmode and saved_chararray_pos_h or - saved_chararray_pos_v
304    tj_delta = 0
305    saved_b = b
306    b = b + 1 ; buffer[b] = " ["
307    mode = "chararray"
308end
309
310local function end_chararray()
311    b = b + 1 ; buffer[b] = "] TJ"
312    buffer[saved_b] = concat(buffer,"",saved_b,b)
313    b = saved_b
314    pdf_h = saved_chararray_pos_h
315    pdf_v = saved_chararray_pos_v
316    mode  = "text"
317end
318
319local function begin_charmode()
320    b = b + 1 ; buffer[b] = "<"
321    mode = "char"
322end
323
324local function end_charmode()
325    b = b + 1 ; buffer[b] = ">"
326    mode = "chararray"
327end
328
329local function calc_pdfpos(h,v)
330    -- mostly char
331    if mode == "page" then
332        cmtx = h - pdf_h
333        cmty = v - pdf_v
334        return h ~= pdf_h or v ~= pdf_v
335    elseif mode == "text" then
336        tmtx = h - saved_text_pos_h
337        tmty = v - saved_text_pos_v
338        return h ~= pdf_h or v ~= pdf_v
339    elseif horizontalmode then
340        tmty = v - saved_text_pos_v
341        tj_delta = cw - h
342        return tj_delta ~= 0 or v ~= pdf_v
343    else
344        tmtx = h - saved_text_pos_h
345        tj_delta = cw + v
346        return tj_delta ~= 0 or h ~= pdf_h
347    end
348end
349
350local function pdf_set_pos(h,v)
351    local move = calc_pdfpos(h,v)
352    if move then
353        b = b + 1 ; buffer[b] = f_cm(cmrx, cmsx, cmsy, cmry, cmtx*bpfactor, cmty*bpfactor)
354        pdf_h = pdf_h + cmtx
355        pdf_v = pdf_v + cmty
356    end
357end
358
359local function pdf_reset_pos()
360    if mode == "page" then
361        cmtx = - pdf_h
362        cmty = - pdf_v
363        if pdf_h == 0 and pdf_v == 0 then
364            return
365        end
366    elseif mode == "text" then
367        tmtx = - saved_text_pos_h
368        tmty = - saved_text_pos_v
369        if pdf_h == 0 and pdf_v == 0 then
370            return
371        end
372    elseif horizontalmode then
373        tmty = - saved_text_pos_v
374        tj_delta = cw
375        if tj_delta == 0 and pdf_v == 0 then
376            return
377        end
378    else
379        tmtx = - saved_text_pos_h
380        tj_delta = cw
381        if tj_delta == 0 and pdf_h == 0 then
382            return
383        end
384    end
385    b = b + 1 ; buffer[b] = f_cm(cmrx, cmsx, cmsy, cmry, cmtx*bpfactor, cmty*bpfactor)
386    pdf_h = pdf_h + cmtx
387    pdf_v = pdf_v + cmty
388end
389
390local function pdf_set_pos_temp(h,v)
391    local move = calc_pdfpos(h,v)
392    if move then
393        b = b + 1 ; buffer[b] = f_cm(cmrx, cmsx, cmsy, cmry, cmtx*bpfactor, cmty*bpfactor)
394    end
395end
396
397-- these dummy returns makes using them a bit faster
398
399local function pdf_end_string_nl()
400    if mode == "char" then
401        end_charmode()
402        return end_chararray()
403    elseif mode == "chararray" then
404        return end_chararray()
405    end
406end
407
408local function pdf_goto_textmode()
409    if mode == "page" then
410        pdf_reset_pos()
411        return begin_text()
412    elseif mode ~= "text" then
413        if mode == "char" then
414            end_charmode()
415            return end_chararray()
416        else -- if mode == "chararray" then
417            return end_chararray()
418        end
419    end
420end
421
422local function pdf_goto_pagemode()
423    if mode ~= "page" then
424        if mode == "char" then
425            end_charmode()
426            end_chararray()
427            return end_text()
428        elseif mode == "chararray" then
429            end_chararray()
430            return end_text()
431        elseif mode == "text" then
432            return end_text()
433        end
434    end
435end
436
437local function pdf_goto_fontmode()
438    if mode == "char" then
439        end_charmode()
440        end_chararray()
441        end_text()
442    elseif mode == "chararray" then
443        end_chararray()
444        end_text()
445    elseif mode == "text" then
446        end_text()
447    end
448    pdf_reset_pos()
449    mode = "page"
450end
451
452-- characters
453do
454
455    local round = math.round
456
457    -- across pages ... todo: clean up because we don't need to pass the font
458    -- as fontparameters already has checked / set it we can also have a variable
459    -- for it so
460
461    local naturalwidth = nil
462    local hshift       = false
463    local vshift       = false
464
465    -- The width array uses the original dimensions! This is different from e.g.
466    -- luatex where we have more widths arrays and these reflect the cheated
467    -- widths (goes wrong elsewhere).
468
469    -- when changing this, check math: compact-001.tex (rule width)
470
471    local naturalwidths = setmetatableindex(function(t,font)
472        local d = descriptions[font]
473        local c = characters[font]
474        local f = parameters[font].hfactor or parameters[font].factor
475        local v = setmetatableindex(function(t,char)
476            local w
477            local e = c and c[char]
478            if e then
479                w = e.width or 0
480            end
481            if not w then
482                e = d and d[char]
483                if e then
484                    w = e.width
485                    if w then
486                        w =  w * f
487                    end
488                end
489            end
490            if not w then
491                w = 0
492            end
493            t[char] = w
494            return w
495        end)
496        t[font] = v
497        return v
498    end)
499
500    local function setup_fontparameters(font,factor,f,e,sx,sy)
501        local slant   = fontparameters.slantfactor   or 0
502        local extend  = fontparameters.extendfactor  or 1
503        local squeeze = fontparameters.squeezefactor or 1
504        local expand  = 1 + factor / 1000000
505        local format  = fontproperties.format
506        if e then
507            extend = extend * e
508        end
509        tmef       = expand
510        tmrx       = expand * extend
511        tmsy       = slant
512        tmry       = squeeze
513        need_width = fontparameters.width or 0
514        need_mode  = fontparameters.mode or 0
515        f_cur      = font
516        f_pdf      = usedfonts[font] -- cache
517        cur_factor = factor
518        cur_f      = f
519        cur_e      = e
520        tj_delta   = 0
521        cw         = 0
522        f_x_scale  = 1.0
523        f_y_scale  = 1.0
524        fs         = fontparameters.size * bpfactor
525        if f then
526            fs = fs * f
527        end
528        -- kind of special:
529        if format == "opentype" or format == "type1" then
530            fs = fs * 1000 / fontparameters.units -- can we avoid this ?
531        end
532        --
533        f_x_scale = sx
534        if f_x_scale ~= 1.0 then
535            tmrx = tmrx * f_x_scale
536        end
537        f_y_scale = sy
538        if f_y_scale ~= 1.0 then
539            tmry = tmry * f_y_scale
540        end
541        --
542        naturalwidth = naturalwidths[font]
543        --
544        hshift = fontparameters.hshift
545        vshift = fontparameters.vshift
546    end
547
548    local f_width = formatters["%.6N w"]
549    local f_mode  = formatters["%i Tr"]        -- can be hash
550    local f_font  = formatters["/F%i %.6N Tf"] -- can be hash
551
552    local s_width = "0 w"
553    local s_mode  = "0 Tr"
554
555    local width_factor = 72.27 / 72000.0
556
557    local function set_font()
558     -- if need_width and need_width ~= 0 then
559        if need_width ~= 0 then
560            b = b + 1 ; buffer[b] = f_width(width_factor*need_width)
561            done_width = true
562        elseif done_width then
563            b = b + 1 ; buffer[b] = s_width
564            done_width = false
565        end
566     -- if need_mode and need_mode ~= 0 then
567        if need_mode ~= 0 then
568            b = b + 1 ; buffer[b] = f_mode(need_mode)
569            done_mode = true
570        elseif done_mode then
571            b = b + 1 ; buffer[b] = s_mode
572            done_mode = false
573        end
574        b = b + 1 ; buffer[b] = f_font(f_pdf,fs)
575        f_pdf_cur = f_pdf
576        fs_cur    = fs
577        need_tf   = false
578        need_tm   = true
579    end
580
581    local function set_textmatrix(h,v)
582       local move = calc_pdfpos(h,v)
583       if need_tm or move then
584            b = b + 1 ; buffer[b] = f_tm(tmrx, tmsx, tmsy, tmry, tmtx*bpfactor, tmty*bpfactor)
585            pdf_h = saved_text_pos_h + tmtx
586            pdf_v = saved_text_pos_v + tmty
587            need_tm = false
588        end
589        cur_tmrx = tmrx
590    end
591
592    local f_hex_4 = formatters["%04X"]
593    local f_hex_2 = formatters["%02X"]
594
595    local h_hex_4 = setmetatableindex(function(t,k) -- we already have this somewhere
596        if k < 256 then -- maybe 512
597            -- not sparse in this range
598            for i=0,255 do
599                t[i] = f_hex_4(i)
600            end
601            return t[k]
602        else
603            local v = f_hex_4(k)
604            t[k] = v
605            return v
606        end
607    end)
608    local h_hex_2 = setmetatableindex(function(t,k) -- we already have this somewhere
609        local v = k < 256 and f_hex_2(k) or "00"
610        t[k] = v
611        return v
612    end)
613
614 -- local trace_threshold = false  trackers.register("backends.pdf.threshold", function(v) trace_threshold = v end)
615
616 -- local f_skip = formatters["%.2N"]
617
618    -- I will redo this mess ... we no longer have the mkiv pdf generator that we used in
619    -- luatex (a precursor to lmtx and also for comparison) but only in lmtx now so ...
620    -- time to move on I guess.
621
622    flushers.character = function(current,pos_h,pos_v,pos_r,font,char,data,f,e,factor,sx,sy) -- ,naturalwidth,width)
623        if sx ~= f_x_scale or sy ~= f_y_scale or need_tf or font ~= f_cur or f_pdf ~= f_pdf_cur or fs ~= fs_cur or mode == "page" then
624            pdf_goto_textmode()
625            setup_fontparameters(font,factor,f,e,sx,sy)
626            set_font()
627        elseif cur_f ~= f then -- when ok move up
628            pdf_goto_textmode()
629            setup_fontparameters(font,factor,f,e,sx,sy)
630            set_font()
631--         elseif cur_tmrx ~= tmrx or cur_factor ~= factor or cur_f ~= f or cur_e ~= e then
632        elseif cur_tmrx ~= tmrx or cur_factor ~= factor or cur_e ~= e then
633            setup_fontparameters(font,factor,f,e,sx,sy)
634            need_tm = true
635        end
636
637        local move = calc_pdfpos(pos_h,pos_v)
638
639        if trace_threshold then
640            report_fonts(
641                "before: font %i, char %C, factor %i, naturalwidth %p, move %l, tm %l, hpos %p, delta %p, threshold %p, cw %p",
642                font,char,factor,naturalwidth[char],move,need_tm,pos_h,tj_delta,threshold,cw
643            )
644        end
645
646        if move or need_tm then
647            if not need_tm then
648                if horizontalmode then
649                    if (saved_text_pos_v + tmty) ~= pdf_v then
650                        need_tm = true
651                    elseif tj_delta >= threshold or tj_delta <= -threshold then
652                        need_tm = true
653                    end
654                else
655                    if (saved_text_pos_h + tmtx) ~= pdf_h then
656                        need_tm = true
657                    elseif tj_delta >= threshold or tj_delta <= -threshold then
658                        need_tm = true
659                    end
660                end
661            end
662
663            if hshift then pos_h = pos_h + hshift end
664            if vshift then pos_v = pos_v - vshift end
665
666            if need_tm then
667                pdf_goto_textmode()
668                set_textmatrix(pos_h,pos_v)
669                begin_chararray()
670                move = calc_pdfpos(pos_h,pos_v)
671            end
672            if move then
673                local d = tj_delta * scalefactor / f_x_scale
674                if d <= -0.5 or d >= 0.5 then
675                    if mode == "char" then
676                        end_charmode()
677                    end
678                    b = b + 1 ; buffer[b] = round(d) -- or f_skip(d)
679                end
680                cw = cw - tj_delta
681            end
682        end
683
684        if trace_threshold then
685            report_fonts(
686                 "after : font %i, char %C, factor %i, naturalwidth %p, move %l, tm %l, hpos %p, delta %p, threshold %p, cw %p",
687                 font,char,factor,naturalwidth[char],move,need_tm,pos_h,tj_delta,threshold,cw
688             )
689        end
690
691        if mode == "chararray" then
692            begin_charmode()
693        end
694
695        cw = cw + naturalwidth[char] * tmef * f_x_scale
696
697        local slot = pdfcharacters[data.index or char] -- registers usage
698
699        b = b + 1 ; buffer[b] = font > 0 and h_hex_4[slot] or h_hex_2[slot]
700
701    end
702
703    flushers.fontchar = function(font,char,data)
704        local dummy = usedfonts[font]
705        local slot  = pdfcharacters[data.index or char] -- registers usage
706        return dummy
707    end
708
709end
710
711-- literals
712
713local flushliteral  do
714
715    local nodeproperties      = nodes.properties.data
716    local literalvalues       = nodes.literalvalues
717
718    local originliteral_code  = literalvalues.origin
719    local pageliteral_code    = literalvalues.page
720    local alwaysliteral_code  = literalvalues.always
721    local rawliteral_code     = literalvalues.raw
722    local textliteral_code    = literalvalues.text
723    local fontliteral_code    = literalvalues.font
724
725    flushliteral = function(current,pos_h,pos_v)
726        local p = nodeproperties[current]
727        if p then
728            local str = p.data
729            if str and str ~= "" then
730                local mode = p.mode
731                if mode == originliteral_code then
732                    pdf_goto_pagemode()
733                    pdf_set_pos(pos_h,pos_v)
734                elseif mode == pageliteral_code then
735                    pdf_goto_pagemode()
736                elseif mode == textliteral_code then
737                    pdf_goto_textmode()
738                elseif mode == fontliteral_code then
739                    pdf_goto_fontmode()
740                elseif mode == alwaysliteral_code then -- aka direct
741                    pdf_end_string_nl()
742                    need_tm = true
743                elseif mode == rawliteral_code then
744                    pdf_end_string_nl()
745                else
746                    report("invalid literal mode %a when flushing %a",mode,str)
747                    return
748                end
749                b = b + 1 ; buffer[b] = str
750            end
751        end
752    end
753
754    flushers.literal = flushliteral
755
756    function lpdf.print(mode,str)
757        -- This only works inside objects, don't change this to flush
758        -- in between. It's different from luatex but okay.
759        if str then
760            mode = literalvalues[mode]
761        else
762            mode, str = originliteral_code, mode
763        end
764        if str and str ~= "" then
765            if mode == originliteral_code then
766                pdf_goto_pagemode()
767             -- pdf_set_pos(pdf_h,pdf_v)
768            elseif mode == pageliteral_code then
769                pdf_goto_pagemode()
770            elseif mode == textliteral_code then
771                pdf_goto_textmode()
772            elseif mode == fontliteral_code then
773                pdf_goto_fontmode()
774            elseif mode == alwaysliteral_code then
775                pdf_end_string_nl()
776                need_tm = true
777            elseif mode == rawliteral_code then
778                pdf_end_string_nl()
779            else
780                report("invalid literal mode %a when flushing %a",mode,str)
781                return
782            end
783            b = b + 1 ; buffer[b] = str
784        end
785    end
786
787end
788
789-- grouping & orientation
790
791do
792
793    local matrices     = { }
794    local positions    = { }
795    local nofpositions = 0
796    local nofmatrices  = 0
797
798    local flushsave = function(current,pos_h,pos_v)
799        nofpositions = nofpositions + 1
800        positions[nofpositions] = { pos_h, pos_v, nofmatrices }
801        pdf_goto_pagemode()
802        pdf_set_pos(pos_h,pos_v)
803        b = b + 1 ; buffer[b] = "q"
804    end
805
806    local flushrestore = function(current,pos_h,pos_v)
807        if nofpositions < 1 then
808            return
809        end
810        local t = positions[nofpositions]
811     -- local h = pos_h - t[1]
812     -- local v = pos_v - t[2]
813        if shippingmode == "page" then
814            nofmatrices = t[3]
815        end
816        pdf_goto_pagemode()
817        pdf_set_pos(pos_h,pos_v)
818        b = b + 1 ; buffer[b] = "Q"
819        nofpositions = nofpositions - 1
820    end
821
822    local nodeproperties = nodes.properties.data
823
824    local s_matrix_0 <const> = "1 0 0 1 0 0 cm"
825    local f_matrix_2         = formatters["%.6N 0 0 %.6N 0 0 cm"]
826    local f_matrix_4         = formatters["%.6N %.6N %.6N %.6N 0 0 cm"]
827
828    local flushsetmatrix = function(current,pos_h,pos_v)
829        local p = nodeproperties[current]
830        if p then
831            local m = p.matrix
832            if m then
833                local rx, sx, sy, ry = unpack(m)
834                local s
835                if not rx then
836                    rx = 1
837                elseif rx == 0 then
838                    rx = 0.0001
839                end
840                if not ry then
841                    ry = 1
842                elseif ry == 0 then
843                    ry = 0.0001
844                end
845                if not sx then
846                    sx = 0
847                end
848                if not sy then
849                    sy = 0
850                end
851                --
852                if sx == 0 and sy == 0 then
853                    if rx == 1 and ry == 1 then
854                        s = s_matrix_0
855                    else
856                        s = f_matrix_2(rx,ry)
857                    end
858                else
859                    s = f_matrix_4(rx,sx,sy,ry)
860                end
861                --
862                if shippingmode == "page" then
863                    local tx = pos_h * (1 - rx) - pos_v * sy
864                    local ty = pos_v * (1 - ry) - pos_h * sx
865                    if nofmatrices > 0 then
866                        local t = matrices[nofmatrices]
867                        local r_x, s_x, s_y, r_y, te, tf = t[1], t[2], t[3], t[4], t[5], t[6]
868                        rx, sx = rx * r_x + sx * s_y, rx * s_x + sx * r_y
869                        sy, ry = sy * r_x + ry * s_y, sy * s_x + ry * r_y
870                        tx, ty = tx * r_x + ty * s_y, tx * s_x + ty * r_y
871                    end
872                    nofmatrices = nofmatrices + 1
873                    matrices[nofmatrices] = { rx, sx, sy, ry, tx, ty }
874                end
875                --
876                pdf_goto_pagemode()
877                pdf_set_pos(pos_h,pos_v)
878                --
879                b = b + 1
880                buffer[b] = s
881            end
882        end
883    end
884
885    flushers.setmatrix = flushsetmatrix
886    flushers.save      = flushsave
887    flushers.restore   = flushrestore
888
889    function lpdf.hasmatrix()
890        return nofmatrices > 0
891    end
892
893    function lpdf.getmatrix()
894        if nofmatrices > 0 then
895            return unpack(matrices[nofmatrices])
896        else
897            return 1, 0, 0, 1, 0, 0
898        end
899    end
900
901    flushers.pushorientation = function(orientation,pos_h,pos_v,pos_r)
902        pdf_goto_pagemode()
903        pdf_set_pos(pos_h,pos_v)
904        b = b + 1 ; buffer[b] = "q"
905        if orientation == 1 then
906            b = b + 1 ; buffer[b] = "0 -1 1 0 0 0 cm"  --  90
907        elseif orientation == 2 then
908            b = b + 1 ; buffer[b] = "-1 0 0 -1 0 0 cm" -- 180
909        elseif orientation == 3 then
910            b = b + 1 ; buffer[b] = "0 1 -1 0 0 0 cm"  -- 270
911        end
912    end
913
914    flushers.poporientation = function(orientation,pos_h,pos_v,pos_r)
915        pdf_goto_pagemode()
916        pdf_set_pos(pos_h,pos_v)
917        b = b + 1 ; buffer[b] = "Q"
918    end
919
920    --
921
922    flushers.startmatrix = function(current,pos_h,pos_v)
923        flushsave(current,pos_h,pos_v)
924        flushsetmatrix(current,pos_h,pos_v)
925    end
926
927    flushers.stopmatrix = function(current,pos_h,pos_v)
928        flushrestore(current,pos_h,pos_v)
929    end
930
931    flushers.startscaling = function(current,pos_h,pos_v)
932        flushsave(current,pos_h,pos_v)
933        flushsetmatrix(current,pos_h,pos_v)
934    end
935
936    flushers.stopscaling = function(current,pos_h,pos_v)
937        flushrestore(current,pos_h,pos_v)
938    end
939
940    flushers.startrotation = function(current,pos_h,pos_v)
941        flushsave(current,pos_h,pos_v)
942        flushsetmatrix(current,pos_h,pos_v)
943    end
944
945    flushers.stoprotation = function(current,pos_h,pos_v)
946        flushrestore(current,pos_h,pos_v)
947    end
948
949    flushers.startmirroring = function(current,pos_h,pos_v)
950        flushsave(current,pos_h,pos_v)
951        flushsetmatrix(current,pos_h,pos_v)
952    end
953
954    flushers.stopmirroring = function(current,pos_h,pos_v)
955        flushrestore(current,pos_h,pos_v)
956    end
957
958    flushers.startclipping = function(current,pos_h,pos_v)
959        flushsave(current,pos_h,pos_v)
960     -- lpdf.print("origin",formatters["0 w %s W n"](nodeproperties[current].path))
961        pdf_goto_pagemode()
962        b = b + 1 ; buffer[b] = formatters["0 w %s W n"](nodeproperties[current].path)
963    end
964
965    flushers.stopclipping = function(current,pos_h,pos_v)
966        flushrestore(current,pos_h,pos_v)
967    end
968
969end
970
971do
972
973    local nodeproperties = nodes.properties.data
974
975    flushers.setstate = function(current,pos_h,pos_v)
976        local p = nodeproperties[current]
977        if p then
978            local d = p.data
979            if d and d ~= "" then
980                pdf_goto_pagemode()
981                b = b + 1 ; buffer[b] = d
982            end
983        end
984    end
985
986end
987
988-- rules
989
990local flushedxforms  = { } -- actually box resources but can also be direct
991local localconverter = nil -- will be set
992
993local flushimage  do
994
995    local pdfbackend        = backends.registered.pdf
996    local nodeinjections    = pdfbackend.nodeinjections
997    local codeinjections    = pdfbackend.codeinjections
998
999    local newimagerule      = nuts.pool.imagerule
1000    local newboxrule        = nuts.pool.boxrule
1001
1002    local setprop           = nuts.setprop
1003    local getprop           = nuts.getprop
1004    local setattrlist       = nuts.setattrlist
1005
1006    local getwhd            = nuts.getwhd
1007    local flushlist         = nuts.flushlist
1008    local getdata           = nuts.getdata
1009
1010    local rulecodes         = nodes.rulecodes
1011    local normalrule_code   = rulecodes.normal
1012    local boxrule_code      = rulecodes.box
1013    local imagerule_code    = rulecodes.image
1014    local emptyrule_code    = rulecodes.empty
1015    local userrule_code     = rulecodes.user
1016    local overrule_code     = rulecodes.over
1017    local underrule_code    = rulecodes.under
1018    local fractionrule_code = rulecodes.fraction
1019    local radicalrule_code  = rulecodes.radical
1020    local outlinerule_code  = rulecodes.outline
1021
1022    local processrule       = nodes.rules.process
1023
1024    local f_fm = formatters["/Fm%d Do"]
1025    local f_im = formatters["/Im%d Do"]
1026    local f_gr = formatters["/Gp%d Do"]
1027
1028    local s_b <const> = "q"
1029    local s_e <const> = "Q"
1030
1031    local f_v  = formatters["[] 0 d 0 J %.6N w 0 0 m %.6N 0 l S"]
1032    local f_h  = formatters["[] 0 d 0 J %.6N w 0 0 m 0 %.6N l S"]
1033
1034    local f_f  = formatters["0 0 %.6N %.6N re f"]
1035    local f_o  = formatters["[] 0 d 0 J 0 0 %.6N %.6N re S"]
1036    local f_w  = formatters["[] 0 d 0 J %.6N w 0 0 %.6N %.6N re S"]
1037
1038    local f_b  = formatters["%.6N w 0 %.6N %.6N %.6N re f"]
1039    local f_x  = formatters["[] 0 d 0 J %.6N w %.6N %.6N %.6N %.6N re S"]
1040    local f_y  = formatters["[] 0 d 0 J %.6N w %.6N %.6N %.6N %.6N re S %.6N 0 m %.6N 0 l S"]
1041
1042    -- Historically the index is an object which is kind of bad.
1043
1044    local boxresources, n = { }, 0
1045
1046    getxformname = function(index)
1047        local l = boxresources[index]
1048        if l then
1049            return l.name
1050        else
1051            report("no box resource %S",index)
1052        end
1053    end
1054
1055    lpdf.getxformname = getxformname
1056
1057    local pdfcollectedresources = lpdf.collectedresources
1058
1059    function codeinjections.saveboxresource(box,attributes,resources,immediate,kind,margin)
1060        n = n + 1
1061        local immediate = true
1062        local margin    = margin or 0 -- or dimension
1063        local objnum    = pdfreserveobject()
1064        local list      = tonut(type(box) == "number" and tex.takebox(box) or box)
1065        --
1066        if resources == true then
1067            resources = pdfcollectedresources()
1068        end
1069        --
1070        local width, height, depth = getwhd(list)
1071        --
1072        local l = {
1073            width      = width,
1074            height     = height,
1075            depth      = depth,
1076            margin     = margin,
1077            attributes = attributes,
1078            resources  = resources,
1079            list       = nil,
1080            type       = kind,
1081            name       = n,
1082            index      = objnum,
1083            objnum     = objnum,
1084        }
1085        boxresources[objnum] = l
1086        if immediate then
1087            localconverter(list,"xform",objnum,l)
1088            flushedxforms[objnum] = { true , objnum }
1089            flushlist(list)
1090        else
1091            l.list = list
1092        end
1093        return objnum
1094    end
1095
1096    function nodeinjections.useboxresource(index,wd,ht,dp)
1097        local l = boxresources[index]
1098        if l then
1099            if wd or ht or dp then
1100                wd, ht, dp = wd or 0, ht or 0, dp or 0
1101            else
1102                wd, ht, dp = l.width, l.height, l.depth
1103            end
1104            local rule = newboxrule(wd,ht,dp)
1105            setattrlist(rule,true)
1106            setprop(rule,"index",index)
1107            return tonode(rule), wd, ht, dp
1108        else
1109            report("no box resource %S",index)
1110        end
1111    end
1112
1113    local function getboxresourcedimensions(index)
1114        local l = boxresources[index]
1115        if l then
1116            return l.width, l.height, l.depth, l.margin
1117        else
1118            report("no box resource %S",index)
1119        end
1120    end
1121
1122    nodeinjections.getboxresourcedimensions = getboxresourcedimensions
1123
1124    function codeinjections.getboxresourcebox(index)
1125        local l = boxresources[index]
1126        if l then
1127            return l.list
1128        end
1129    end
1130
1131    -- a bit of a mess: index is now objnum but that has to change to a proper index
1132    -- ... an engine inheritance
1133
1134    local function flushpdfxform(current,pos_h,pos_v,pos_r,size_h,size_v)
1135        -- object properties
1136        local objnum = getprop(current,"index")
1137        local name   = getxformname(objnum)
1138        local info   = flushedxforms[objnum]
1139        local r      = boxresources[objnum]
1140        if not info then
1141            info = { false , objnum }
1142            flushedxforms[objnum] = info
1143        end
1144        local wd, ht, dp = getboxresourcedimensions(objnum)
1145     -- or:   wd, ht, dp = r.width, r.height, r.depth
1146        -- sanity check
1147        local htdp = ht + dp
1148        if wd == 0 or size_h == 0 or htdp == 0 or size_v == 0 then
1149            return
1150        end
1151        -- calculate scale
1152        local rx, ry = 1, 1
1153        if wd ~= size_h or htdp ~= size_v then
1154            rx = size_h / wd
1155            ry = size_v / htdp
1156        end
1157        -- flush the reference
1158        usedxforms[objnum] = true
1159        pdf_goto_pagemode()
1160        calc_pdfpos(pos_h,pos_v)
1161        tx = cmtx * bpfactor
1162        ty = cmty * bpfactor
1163        b = b + 1 ; buffer[b] = s_b
1164        b = b + 1 ; buffer[b] = f_cm(rx,0,0,ry,tx,ty)
1165        b = b + 1 ; buffer[b] = f_fm(name)
1166        b = b + 1 ; buffer[b] = s_e
1167    end
1168
1169    -- place image also used in vf but we can use a different one if we need it
1170
1171    local imagetypes     = images.types -- pdf png jpg jp2 jbig2 stream
1172    local img_none       = imagetypes.none
1173    local img_pdf        = imagetypes.pdf
1174    local img_stream     = imagetypes.stream
1175
1176    local one_bp = 65536 * bpfactor
1177
1178    local imageresources, n = { }, 0
1179
1180    getximagename = function(index) -- not used
1181        local l = imageresources[index]
1182        if l then
1183            return l.name
1184        else
1185            report("no image resource %S",index)
1186        end
1187    end
1188
1189    -- Groups are flushed immediately but we can decide to make them into a
1190    -- specific whatsit ... but not now. We could hash them if needed when
1191    -- we use lot sof them in mp ... but not now.
1192
1193          usedxgroups = { }
1194    local groups      = 0
1195    local group       = nil
1196
1197    local flushgroup = function(content,bbox)
1198        if not group then
1199            group = pdfdictionary {
1200                Type = pdfconstant("Group"),
1201                S    = pdfconstant("Transparency"),
1202            }
1203        end
1204        local wrapper = pdfdictionary {
1205            Type      = pdf_xobject,
1206            Subtype   = pdf_form,
1207            FormType  = 1,
1208            Group     = group,
1209            BBox      = pdfarray(bbox),
1210            Resources = lpdf.collectedresources { serialize = false },
1211        }
1212        local objnum = pdfflushstreamobject(content,wrapper,false) -- why not compressed ?
1213        groups = groups + 1
1214        usedxgroups[groups] = objnum
1215        return f_gr(groups)
1216    end
1217
1218    flushers.group  = flushgroup
1219    lpdf.flushgroup = flushgroup -- todo: access via driver in mlib-pps
1220
1221    -- end of experiment
1222
1223    local function flushpdfximage(current,pos_h,pos_v,pos_r,size_h,size_v)
1224
1225        local width,
1226              height,
1227              depth     = getwhd(current)
1228        local total     = height + depth
1229        local transform = getprop(current,"transform") or 0  -- we never set it ... so just use rotation then
1230        local index     = getprop(current,"index") or 0
1231        local kind,
1232              xorigin,
1233              yorigin,
1234              xsize,
1235              ysize,
1236              rotation, -- transform / orientation / rotation : it's a mess (i need to redo this)
1237              objnum,
1238              groupref  = pdfincludeimage(index)  -- needs to be sorted out, bad name (no longer mixed anyway)
1239
1240        if not kind then
1241            report("invalid image %S",index)
1242            return
1243        end
1244
1245        local rx, sx, sy, ry, tx, ty = 1, 0, 0, 1, 0, 0
1246
1247        -- tricky: xsize and ysize swapped
1248
1249        if kind == img_pdf or kind == img_stream then
1250            rx, ry, tx, ty = 1/xsize, 1/ysize, xorigin/xsize, yorigin/ysize
1251        else
1252         -- if kind == img_png then
1253         --  -- if groupref > 0 and img_page_group_val == 0 then
1254         --  --     img_page_group_val = groupref
1255         --  -- end
1256         -- end
1257            rx, ry = bpfactor, bpfactor
1258        end
1259
1260        if (transform & 7) > 3 then
1261            -- mirror
1262            rx, tx = -rx, -tx
1263        end
1264        local t = (transform + rotation) & 3
1265        if t == 0 then
1266            -- nothing
1267        elseif t == 1 then
1268            -- rotation over 90 degrees (counterclockwise)
1269            rx, sx, sy, ry, tx, ty = 0, rx, -ry, 0, -ty, tx
1270        elseif t == 2 then
1271            -- rotation over 180 degrees (counterclockwise)
1272            rx, ry, tx, ty = -rx, -ry, -tx, -ty
1273        elseif t == 3 then
1274            -- rotation over 270 degrees (counterclockwise)
1275            rx, sx, sy, ry, tx, ty = 0, -rx, ry, 0, ty, -tx
1276        end
1277
1278        rx = rx * width
1279        sx = sx * total
1280        sy = sy * width
1281        ry = ry * total
1282        tx = pos_h - tx * width
1283        ty = pos_v - ty * total
1284
1285        local t = transform + rotation
1286
1287        if (transform & 7) > 3 then
1288            t = t + 1
1289        end
1290
1291        t = t & 3
1292
1293        if t == 0 then
1294            -- no transform
1295        elseif t == 1 then
1296            -- rotation over 90 degrees (counterclockwise)
1297            tx = tx + width
1298        elseif t == 2 then
1299            -- rotation over 180 degrees (counterclockwise)
1300            tx = tx + width
1301            ty = ty + total
1302        elseif t == 3 then
1303            -- rotation over 270 degrees (counterclockwise)
1304            ty = ty + total
1305        end
1306
1307     -- a flaw in original, can go:
1308     --
1309     -- if img_page_group_val == 0 then
1310     --     img_page_group_val = group_ref
1311     -- end
1312
1313        usedximages[index] = objnum -- hm
1314
1315        pdf_goto_pagemode()
1316
1317        calc_pdfpos(tx,ty)
1318
1319        tx = cmtx * bpfactor
1320        ty = cmty * bpfactor
1321
1322        b = b + 1 ; buffer[b] = s_b
1323        b = b + 1 ; buffer[b] = f_cm(rx,sx,sy,ry,tx,ty)
1324        b = b + 1 ; buffer[b] = f_im(index)
1325        b = b + 1 ; buffer[b] = s_e
1326    end
1327
1328    flushimage = function(index,width,height,depth,pos_h,pos_v)
1329
1330        -- used in vf characters
1331
1332        local total = height + depth
1333        local kind,
1334              xorigin, yorigin,
1335              xsize, ysize,
1336              rotation,
1337              objnum,
1338              groupref = pdfincludeimage(index)
1339
1340        local rx = width / xsize
1341        local sx = 0
1342        local sy = 0
1343        local ry = total / ysize
1344        local tx = pos_h
1345        -- to be sorted out
1346     -- local ty = pos_v - depth
1347        local ty = pos_v -- we assume that depth is dealt with in the caller (for now)
1348
1349        usedximages[index] = objnum
1350
1351        pdf_goto_pagemode()
1352
1353        calc_pdfpos(tx,ty)
1354
1355        tx = cmtx * bpfactor
1356        ty = cmty * bpfactor
1357
1358        b = b + 1 ; buffer[b] = s_b
1359        b = b + 1 ; buffer[b] = f_cm(rx,sx,sy,ry,tx,ty)
1360        b = b + 1 ; buffer[b] = f_im(index)
1361        b = b + 1 ; buffer[b] = s_e
1362    end
1363
1364    flushers.image = flushimage
1365
1366    -- For the moment we need this hack because the engine checks the 'image'
1367    -- command in virtual fonts (so we use lua instead).
1368    --
1369    -- These will be replaced by a new more advanced one ... some day ... or
1370    -- never because the next are like the other engines and compensate for
1371    -- small sizes which is needed for inaccurate viewers.
1372
1373    flushers.rule = function(current,pos_h,pos_v,pos_r,size_h,size_v,subtype)
1374
1375        if subtype == emptyrule_code then
1376            return
1377        elseif subtype == boxrule_code then
1378            return flushpdfxform(current,pos_h,pos_v,pos_r,size_h,size_v)
1379        elseif subtype == imagerule_code then
1380            return flushpdfximage(current,pos_h,pos_v,pos_r,size_h,size_v)
1381        end
1382        if subtype == userrule_code or subtype >= overrule_code and subtype <= radicalrule_code then
1383            pdf_goto_pagemode()
1384            b = b + 1 ; buffer[b] = s_b
1385            pdf_set_pos_temp(pos_h,pos_v)
1386            processrule(current,size_h,size_v,pos_r) -- so we pass direction
1387            b = b + 1 ; buffer[b] = s_e
1388            return
1389        end
1390
1391        pdf_goto_pagemode()
1392
1393     -- local saved_b = b
1394
1395        b = b + 1 ; buffer[b] = s_b
1396
1397        local dim_h = size_h * bpfactor
1398        local dim_v = size_v * bpfactor
1399        local rule
1400
1401        if dim_v <= one_bp then
1402            pdf_set_pos_temp(pos_h,pos_v + 0.5 * size_v)
1403            rule = f_v(dim_v,dim_h)
1404        elseif dim_h <= one_bp then
1405            pdf_set_pos_temp(pos_h + 0.5 * size_h,pos_v)
1406            rule = f_h(dim_h,dim_v)
1407        else
1408            pdf_set_pos_temp(pos_h,pos_v)
1409            if subtype == outlinerule_code then
1410                local linewidth = getdata(current)
1411                if linewidth > 0 then
1412                    rule = f_w(linewidth * bpfactor,dim_h,dim_v)
1413                else
1414                    rule = f_o(dim_h,dim_v)
1415                end
1416            else
1417                rule = f_f(dim_h,dim_v)
1418            end
1419        end
1420
1421        b = b + 1 ; buffer[b] = rule
1422        b = b + 1 ; buffer[b] = s_e
1423
1424     -- buffer[saved_b] = concat(buffer," ",saved_b,b)
1425     -- b = saved_b
1426
1427    end
1428
1429    flushers.simplerule = function(pos_h,pos_v,pos_r,size_h,size_v)
1430        pdf_goto_pagemode()
1431
1432        b = b + 1 ; buffer[b] = s_b
1433
1434        local dim_h = size_h * bpfactor
1435        local dim_v = size_v * bpfactor
1436        local rule
1437
1438        if dim_v <= one_bp then
1439            pdf_set_pos_temp(pos_h,pos_v + 0.5 * size_v)
1440            rule = f_v(dim_v,dim_h)
1441        elseif dim_h <= one_bp then
1442            pdf_set_pos_temp(pos_h + 0.5 * size_h,pos_v)
1443            rule = f_h(dim_h,dim_v)
1444        else
1445            pdf_set_pos_temp(pos_h,pos_v)
1446            rule = f_f(dim_h,dim_v)
1447        end
1448
1449        b = b + 1 ; buffer[b] = rule
1450        b = b + 1 ; buffer[b] = s_e
1451    end
1452
1453    flushers.specialrule = function(pos_h,pos_v,pos_r,width,height,depth,line,outline,baseline)
1454        pdf_goto_pagemode()
1455
1456        b = b + 1 ; buffer[b] = s_b
1457
1458        local width  = bpfactor * width
1459        local height = bpfactor * height
1460        local depth  = bpfactor * depth
1461        local total  = height + depth
1462        local line   = bpfactor * line
1463        local half   = line / 2
1464        local rule
1465
1466        if outline then
1467            local d = -depth + half
1468            local w =  width - line
1469            local t =  total - line
1470            if baseline then
1471                rule = f_y(line,half,d,w,t,half,w)
1472            else
1473                rule = f_x(line,half,d,w,t)
1474            end
1475        else
1476            rule = f_b(line,-depth,width,total)
1477        end
1478        pdf_set_pos_temp(pos_h,pos_v)
1479
1480        b = b + 1 ; buffer[b] = rule
1481        b = b + 1 ; buffer[b] = s_e
1482    end
1483
1484end
1485
1486--- basics
1487
1488local wrapupdocument, registerpage  do
1489
1490    local pages    = { }
1491    local maxkids  = 10
1492    local nofpages = 0
1493    local pagetag  = "unset"
1494
1495    registerpage = function(object)
1496        nofpages = nofpages + 1
1497        local objnum = pdfpagereference(nofpages)
1498        pages[nofpages] = {
1499            page   = nofpages, -- original number, only for diagnostics
1500            objnum = objnum,
1501            object = object,
1502            tag    = pagetag,
1503        }
1504    end
1505
1506    function lpdf.setpagetag(tag)
1507        pagetag = tag or "unset"
1508    end
1509
1510    function lpdf.getnofpages()
1511        return nofpages
1512    end
1513
1514    function lpdf.getpagetags()
1515        local list = { }
1516        for i=1,nofpages do
1517            list[i] = pages[i].tag
1518        end
1519        return list
1520    end
1521
1522    function lpdf.setpageorder(mapping)
1523        -- mapping can be a hash so:
1524        local list = table.sortedkeys(mapping)
1525        local n    = #list
1526        if n == nofpages then
1527            local done = { }
1528            local hash = { }
1529            for i=1,n do
1530                local order = mapping[list[i]]
1531                if hash[order] then
1532                    report("invalid page order, duplicate entry %i",order)
1533                    return
1534                elseif order < 1 or order > nofpages then
1535                    report("invalid page order, no page %i",order)
1536                    return
1537                else
1538                    done[i]     = pages[order]
1539                    hash[order] = true
1540                end
1541            end
1542            pages = done
1543        else
1544            report("invalid page order, %i entries expected",nofpages)
1545        end
1546    end
1547
1548    -- We can have this, but then via codeinjections etc. Later.
1549
1550 -- function structures.pages.swapthem()
1551 --     local n = lpdf.getnofpages()
1552 --     local t = { }
1553 --     for i=1,n do
1554 --         t[i] = i
1555 --     end
1556 --     for i=2,math.odd(n) and n or (n-1),2 do
1557 --         t[i]   = i+1
1558 --         t[i+1] = i
1559 --     end
1560 --     lpdf.setpageorder(t)
1561 -- end
1562
1563    wrapupdocument = function(driver)
1564
1565        -- hook (to reshuffle pages)
1566        local pagetree = { }
1567        local parent   = nil
1568        local minimum  = 0
1569        local maximum  = 0
1570        local current  = 0
1571        if #pages > 1.5 * maxkids then
1572            repeat
1573                local plist, pnode
1574                if current == 0 then
1575                    plist, minimum = pages, 1
1576                elseif current == 1 then
1577                    plist, minimum = pagetree, 1
1578                else
1579                    plist, minimum = pagetree, maximum + 1
1580                end
1581                maximum = #plist
1582                if maximum > minimum then
1583                    local kids
1584                    for i=minimum,maximum do
1585                        local p = plist[i]
1586                        if not pnode or #kids == maxkids then
1587                            kids   = pdfarray()
1588                            parent = pdfreserveobject()
1589                            pnode  = pdfdictionary {
1590                                objnum = parent,
1591                                Type   = pdf_pages,
1592                                Kids   = kids,
1593                                Count  = 0,
1594                            }
1595                            pagetree[#pagetree+1] = pnode
1596                        end
1597                        kids[#kids+1] = pdfreference(p.objnum)
1598                        pnode.Count = pnode.Count + (p.Count or 1)
1599                        p.Parent = pdfreference(parent)
1600                    end
1601                end
1602                current = current + 1
1603            until maximum == minimum
1604            -- flush page tree
1605            for i=1,#pagetree do
1606                local entry  = pagetree[i]
1607                local objnum = entry.objnum
1608                entry.objnum = nil
1609                pdfflushobject(objnum,entry)
1610            end
1611        else
1612            -- ugly
1613            local kids = pdfarray()
1614            local list = pdfdictionary {
1615                Type  = pdf_pages,
1616                Kids  = kids,
1617                Count = nofpages,
1618            }
1619            parent = pdfreserveobject()
1620            for i=1,nofpages do
1621                local page = pages[i]
1622                kids[#kids+1] = pdfreference(page.objnum)
1623                page.Parent = pdfreference(parent)
1624            end
1625            pdfflushobject(parent,list)
1626        end
1627        for i=1,nofpages do
1628            local page   = pages[i]
1629            local object = page.object
1630            object.Parent = page.Parent
1631            pdfflushobject(page.objnum,object)
1632        end
1633        lpdf.addtocatalog("Pages",pdfreference(parent))
1634
1635    end
1636
1637end
1638
1639pdf_h, pdf_v  = 0, 0
1640
1641local function initialize(driver,details)
1642    reset_variables(details)
1643    reset_buffer()
1644end
1645
1646-- This will all move and be merged and become less messy.
1647
1648-- todo: more clever resource management: a bit tricky as we can inject
1649-- stuff in the page stream
1650
1651local compact = false
1652
1653do
1654
1655    -- This is more a convenience feature and it might even be not entirely robust.
1656    -- It removes redundant color directives which makes the page stream look a bit
1657    -- nicer (also when figuring out issues). I might add more here but there is
1658    -- some additional overhead involved so runtime can be impacted.
1659
1660    local P, R, S, Cs, lpegmatch = lpeg.P, lpeg.R, lpeg.S, lpeg.Cs, lpeg.match
1661
1662    local p_ds    = (R("09") + S(" ."))^1
1663    ----- p_nl    = S("\n\r")^1
1664    local p_nl    = S("\n")^1
1665    local p_eg    = P("Q")
1666
1667    local p_cl    = p_ds * (P("rg") + P("g") + P("k")) * p_ds * (P("RG") + P("G") + P("K"))
1668    ----- p_cl    = (p_ds * (P("rg") + P("g") + P("k") + P("RG") + P("G") + P("K")))^1
1669    local p_tr    = P("/Tr") * p_ds * P("gs")
1670
1671    local p_no_cl = (p_cl * p_nl) / ""
1672    local p_no_tr = (p_tr * p_nl) / ""
1673    local p_no_nl = 1 - p_nl
1674
1675    local p_do_cl = p_cl * p_nl
1676    local p_do_tr = p_tr * p_nl
1677
1678    local p_do_eg = p_eg * p_nl
1679
1680    local pattern = Cs( (
1681        (p_no_cl + p_no_tr)^0 * p_do_eg           -- transparencies and colors before Q
1682      +  p_no_tr * p_no_cl    * p_do_tr * p_do_cl -- transparencies and colors before others
1683      +  p_no_cl              * p_do_cl           -- successive colors
1684      +  p_no_tr              * p_do_tr           -- successive transparencies
1685      +  p_no_nl^1
1686      +  1
1687    )^1 )
1688
1689    local oldsize = 0
1690    local newsize = 0
1691
1692    directives.register("pdf.compact", function(v)
1693        compact = v and function(s)
1694            oldsize = oldsize + #s
1695            s = lpegmatch(pattern,s) or s
1696            newsize = newsize + #s
1697            return s
1698        end
1699    end)
1700
1701    statistics.register("pdf pagestream",function()
1702        if oldsize ~= newsize then
1703            return string.format("old size: %i, new size %i",oldsize,newsize)
1704        end
1705    end)
1706
1707
1708end
1709
1710local flushdeferred -- defined later
1711
1712local level = 0
1713
1714local finalize  do
1715
1716    local f_font  = formatters["F%d"]
1717
1718    local f_form  = formatters["Fm%d"]
1719    local f_group = formatters["Gp%d"]
1720    local f_image = formatters["Im%d"]
1721
1722    finalize = function(driver,details)
1723
1724        level = level + 1
1725
1726        pdf_goto_pagemode() -- for now
1727
1728        local objnum        = details.objnum
1729        local specification = details.specification
1730
1731        local content = concat(buffer,"\n",1,b)
1732
1733        if compact then
1734            content = compact(content)
1735        end
1736
1737        local fonts   = nil
1738        local xforms  = nil
1739
1740        if next(usedfonts) then
1741            fonts = pdfdictionary { }
1742            for k, v in next, usedfonts do
1743--                 fonts[f_font(v)] = pdfreference(pdfgetfontobjectnumber(k)) -- we can overload for testing
1744                fonts[f_font(v)] = pdfreference(usedfontobjects[k]) -- we can overload for testing
1745            end
1746        end
1747
1748        -- messy: use real indexes for both ... so we need to change some in the
1749        -- full luatex part
1750
1751        if next(usedxforms) or next(usedximages) or next(usedxgroups) then
1752            xforms = pdfdictionary { }
1753            for k in sortedhash(usedxforms) do
1754                xforms[f_form(getxformname(k))] = pdfreference(k)
1755            end
1756            for k, v in sortedhash(usedximages) do
1757                xforms[f_image(k)] = pdfreference(v)
1758            end
1759            for k, v in sortedhash(usedxgroups) do
1760                xforms[f_group(k)] = pdfreference(v)
1761            end
1762        end
1763
1764        reset_buffer()
1765
1766     -- finish_pdfpage_callback(shippingmode == "page")
1767
1768        if shippingmode == "page" then
1769
1770            local pageproperties  = lpdf.getpageproperties()
1771
1772            local pageresources   = pageproperties.pageresources
1773            local pageattributes  = pageproperties.pageattributes
1774            local pagesattributes = pageproperties.pagesattributes
1775
1776            pageresources.Font    = fonts
1777            pageresources.XObject = xforms
1778            pageresources.ProcSet = lpdf.procset()
1779
1780            local bbox = pdfarray {
1781                boundingbox[1] * bpfactor,
1782                boundingbox[2] * bpfactor,
1783                boundingbox[3] * bpfactor,
1784                boundingbox[4] * bpfactor,
1785            }
1786
1787            local contentsobj = pdfflushstreamobject(content,false,true)
1788
1789            pageattributes.Type      = pdf_page
1790            pageattributes.Contents  = pdfreference(contentsobj)
1791            pageattributes.Resources = pageresources
1792         -- pageattributes.Resources = pdfreference(pdfflushobject(pageresources))
1793         -- pageattributes.MediaBox  = bbox
1794            pageattributes.MediaBox  = pdfsharedobject(bbox)
1795            pageattributes.Parent    = nil -- precalculate
1796            pageattributes.Group     = nil -- todo
1797
1798            -- resources can be indirect
1799
1800            registerpage(pageattributes)
1801
1802            lpdf.finalizepage(true)
1803
1804            local TrimBox  = pageattributes.TrimBox
1805            local CropBox  = pageattributes.CropBox
1806            local BleedBox = pageattributes.BleedBox
1807
1808            -- Indirect objects don't work in all viewers.
1809
1810            if TrimBox  then pageattributes.TrimBox  = pdfsharedobject(TrimBox ) end
1811            if CropBox  then pageattributes.CropBox  = pdfsharedobject(CropBox ) end
1812            if BleedBox then pageattributes.BleedBox = pdfsharedobject(BleedBox) end
1813
1814        else
1815
1816            local xformtype  = specification.type or 0
1817            local margin     = specification.margin or 0
1818            local attributes = specification.attributes or ""
1819            local resources  = specification.resources or ""
1820
1821            local wrapper    = nil
1822
1823            if xformtype == 0 then
1824                wrapper = pdfdictionary {
1825                    Type      = pdf_xobject,
1826                    Subtype   = pdf_form,
1827                    FormType  = 1,
1828                    BBox      = nil,
1829                    Matrix    = nil,
1830                    Resources = nil,
1831                }
1832            else
1833                wrapper = pdfdictionary {
1834                    BBox      = nil,
1835                    Matrix    = nil,
1836                    Resources = nil,
1837                }
1838            end
1839            if xformtype == 0 or xformtype == 1 or xformtype == 3 then
1840                wrapper.BBox = pdfarray {
1841                    -margin * bpfactor,
1842                    -margin * bpfactor,
1843                    (boundingbox[3] + margin) * bpfactor,
1844                    (boundingbox[4] + margin) * bpfactor,
1845                }
1846            end
1847            if xformtype == 0 or xformtype == 2 or xformtype == 3 then
1848                -- can be shared too
1849                wrapper.Matrix = pdfarray { 1, 0, 0, 1, 0, 0 }
1850            end
1851
1852            local patterns = true
1853
1854            if attributes.Type and attributes.Type == pdf_pattern then
1855                patterns = false
1856            end
1857
1858            local boxresources   = lpdf.collectedresources {
1859                patterns  = patterns,
1860                serialize = false,
1861            }
1862            boxresources.Font    = fonts
1863            boxresources.XObject = xforms
1864
1865         -- todo: maybe share them
1866         -- wrapper.Resources = pdfreference(pdfflushobject(boxresources))
1867
1868            if resources ~= "" then
1869                 boxresources = boxresources + resources
1870            end
1871            if attributes ~= "" then
1872                wrapper = wrapper + attributes
1873            end
1874
1875            wrapper.Resources = next(boxresources) and boxresources or nil
1876            wrapper.ProcSet   = lpdf.procset()
1877
1878            pdfflushstreamobject(content,wrapper,true,specification.objnum)
1879
1880        end
1881
1882        for objnum in sortedhash(usedxforms) do
1883            local f = flushedxforms[objnum]
1884            if f[1] == false then
1885                f[1] = true
1886                local objnum        = f[2] -- specification.objnum
1887                local specification = boxresources[objnum]
1888                local list          = specification.list
1889                localconverter(list,"xform",f[2],specification)
1890            end
1891        end
1892
1893        pdf_h, pdf_v  = 0, 0
1894
1895        if level == 1 then
1896            flushdeferred()
1897        end
1898        level = level - 1
1899
1900    end
1901
1902end
1903
1904-- now comes the pdf file handling
1905
1906local objects       = { }
1907local streams       = { } -- maybe just parallel to objects (no holes)
1908local nofobjects    = 0
1909local offset        = 0
1910local f             = false
1911local flush         = false
1912local threshold     = 40 -- also #("/Filter /FlateDecode") (compression threshold)
1913local objectstream  = true
1914local compress      = true
1915local cache         = false
1916local info          = ""
1917local catalog       = ""
1918local lastdeferred  = false
1919local majorversion  = 1
1920local minorversion  = 7
1921
1922directives.register("backend.pdf.threshold",function(v)
1923    if v then
1924        threshold = tonumber(v) or 40
1925    else
1926        threshold = -1000
1927    end
1928end)
1929
1930local f_object       = formatters["%i 0 obj\010%s\010endobj\010"]
1931local f_stream_n_u   = formatters["%i 0 obj\010<< /Length %i >>\010stream\010%s\010endstream\010endobj\010"]
1932local f_stream_n_c   = formatters["%i 0 obj\010<< /Filter /FlateDecode /Length %i >>\010stream\010%s\010endstream\010endobj\010"]
1933local f_stream_d_u   = formatters["%i 0 obj\010<< %s /Length %i >>\010stream\010%s\010endstream\010endobj\010"]
1934local f_stream_d_c   = formatters["%i 0 obj\010<< %s /Filter /FlateDecode /Length %i >>\010stream\010%s\010endstream\010endobj\010"]
1935local f_stream_d_r   = formatters["%i 0 obj\010<< %s >>\010stream\010%s\010endstream\010endobj\010"]
1936
1937----- f_object_b     = formatters["%i 0 obj\010"]
1938local f_stream_b_n_u = formatters["%i 0 obj\010<< /Length %i >>\010stream\010"]
1939local f_stream_b_n_c = formatters["%i 0 obj\010<< /Filter /FlateDecode /Length %i >>\010stream\010"]
1940local f_stream_b_d_u = formatters["%i 0 obj\010<< %s /Length %i >>\010stream\010"]
1941local f_stream_b_d_c = formatters["%i 0 obj\010<< %s /Filter /FlateDecode /Length %i >>\010stream\010"]
1942local f_stream_b_d_r = formatters["%i 0 obj\010<< %s >>\010stream\010"]
1943
1944----- s_object_e <const> = "\010endobj\010"
1945local s_stream_e <const> = "\010endstream\010endobj\010"
1946
1947do
1948
1949    -- Versions can be set but normally are managed by the official standards. When possible
1950    -- reading and writing should look at these values.
1951
1952    function lpdf.setversion(major,minor)
1953        majorversion = tonumber(major) or majorversion
1954        minorversion = tonumber(minor) or minorversion
1955    end
1956
1957    function lpdf.getversion(major,minor)
1958        return majorversion, minorversion
1959    end
1960
1961    function lpdf.majorversion() return majorversion end
1962    function lpdf.minorversion() return minorversion end
1963
1964    -- It makes no sense to support levels so we only enable and disable and stick to level 3
1965    -- which is both fast and efficient.
1966
1967    local frozen = false
1968    local clevel = 3
1969    local olevel = 1
1970
1971    function lpdf.setcompression(level,objectlevel,freeze)
1972        if not frozen then
1973            compress     = level       and level       ~= 0 and true or false
1974            objectstream = objectlevel and objectlevel ~= 0 and true or false
1975            frozen       = freeze
1976        end
1977    end
1978
1979    function lpdf.getcompression()
1980        return compress and olevel or 0, objectstream and clevel or 0
1981    end
1982
1983    function lpdf.compresslevel()
1984        return compress and olevel or 0
1985    end
1986
1987    function lpdf.objectcompresslevel()
1988        return objectstream and clevel or 0
1989    end
1990
1991    if environment.arguments.nocompression then
1992        lpdf.setcompression(0,0,true)
1993    end
1994
1995end
1996
1997local addtocache, flushcache, cache do
1998
1999    local data, d  = { }, 0
2000    local list, l  = { }, 0
2001    local coffset  = 0
2002    local indices  = { }
2003
2004    local maxsize  = 32 * 1024 -- uncompressed
2005    local maxcount = 0xFF
2006
2007    addtocache = function(n,str)
2008        local size = #str
2009        if size == 0 then
2010            -- todo: message
2011            return
2012        end
2013        if coffset + size > maxsize or d == maxcount then
2014            flushcache()
2015        end
2016        if d == 0 then
2017            nofobjects = nofobjects + 1
2018            objects[nofobjects] = false
2019            streams[nofobjects] = indices
2020            cache = nofobjects
2021        end
2022        objects[n] = - cache
2023        indices[n] = d
2024        d = d + 1
2025        -- can have a comment n 0 obj as in luatex
2026        data[d] = str
2027        l = l + 1 ; list[l] = n
2028        l = l + 1 ; list[l] = coffset
2029        coffset = coffset + size + 1
2030    end
2031
2032    local p_ObjStm = pdfconstant("ObjStm")
2033
2034    flushcache = function() -- references cannot be stored
2035        if l > 0 then
2036            list = concat(list," ")
2037            data[0] = list
2038            data = concat(data,"\010",0,d)
2039            local strobj = pdfdictionary {
2040                Type  = p_ObjStm,
2041                N     = d,
2042                First = #list + 1,
2043            }
2044            objects[cache] = offset
2045            local fb
2046            if compress then
2047                local size = #data
2048                local comp = compressdata(data,size)
2049                if comp and #comp < size then
2050                    data = comp
2051                    fb = f_stream_b_d_c
2052                else
2053                    fb = f_stream_b_d_u
2054                end
2055            else
2056                fb = f_stream_b_d_u
2057            end
2058            local s = #data
2059            local b = fb(cache,strobj(),s)
2060            local e = s_stream_e
2061            flush(f,b)
2062            flush(f,data)
2063            flush(f,e)
2064            offset = offset + #b + s + #e
2065            data, d = { }, 0
2066            list, l = { }, 0
2067            coffset = 0
2068            indices = { }
2069        end
2070    end
2071
2072end
2073
2074do
2075
2076    local names       = { }
2077    local cache       = { }
2078    local nofpages    = 0
2079
2080    local texgetcount = tex.getcount
2081
2082    pdfreserveobject = function(name)
2083        nofobjects = nofobjects + 1
2084        objects[nofobjects] = false
2085        if name then
2086            names[name] = nofobjects
2087            if trace_objects then
2088                report_objects("reserving number %a under name %a",nofobjects,name)
2089            end
2090        elseif trace_objects then
2091            report_objects("reserving number %a",nofobjects)
2092        end
2093        return nofobjects
2094    end
2095
2096    pdfpagereference = function(n,complete) -- true | false | nil | n [true,false]
2097        if n == true or not n then
2098            complete = n
2099            n = texgetcount("realpageno")
2100        end
2101        if n > nofpages then
2102            nofpages = n
2103        end
2104        local r = pdfgetpagereference(n)
2105        return complete and pdfreference(r) or r
2106    end
2107
2108    lpdf.reserveobject = pdfreserveobject
2109    lpdf.pagereference = pdfpagereference
2110
2111    function lpdf.lastreferredpage()
2112        return nofpages
2113    end
2114
2115    function lpdf.nofpages() -- this will change: document nofpages
2116        return structures.pages.nofpages
2117    end
2118
2119    function lpdf.object(...)
2120        pdfdeferredobject(...)
2121    end
2122
2123    function lpdf.delayedobject(data,n)
2124        if n then
2125            pdfdeferredobject(n,data)
2126        else
2127            n = pdfdeferredobject(data)
2128        end
2129--         pdfreferenceobject(n)
2130        return n
2131    end
2132
2133    pdfflushobject = function(name,data)
2134        if data then
2135            local named = names[name]
2136            if named then
2137                if not trace_objects then
2138                elseif trace_details then
2139                    report_objects("flushing data to reserved object with name %a, data: %S",name,data)
2140                else
2141                    report_objects("flushing data to reserved object with name %a",name)
2142                end
2143                return pdfimmediateobject(named,tostring(data))
2144            else
2145                if not trace_objects then
2146                elseif trace_details then
2147                    report_objects("flushing data to reserved object with number %s, data: %S",name,data)
2148                else
2149                    report_objects("flushing data to reserved object with number %s",name)
2150                end
2151                return pdfimmediateobject(name,tostring(data))
2152            end
2153        else
2154            if trace_objects and trace_details then
2155                report_objects("flushing data: %S",name)
2156            end
2157            return pdfimmediateobject(tostring(name))
2158        end
2159    end
2160
2161    pdfflushstreamobject = function(data,dict,compressed,objnum) -- default compressed
2162        if trace_objects then
2163            report_objects("flushing stream object of %s bytes",#data)
2164        end
2165        local dtype    = type(dict)
2166        local kind     = compressed == "raw" and "raw" or "stream"
2167        local nolength = nil
2168        if compressed == "raw" then
2169            compressed = false
2170            nolength   = true
2171         -- data       = string.formatters["<< %s >>stream\n%s\nendstream"](attr,data)
2172        end
2173        return pdfdeferredobject {
2174            objnum        = objnum,
2175            immediate     = true,
2176            nolength      = nolength,
2177--             compresslevel = compressed == false and 0 or nil,
2178            compresslevel = compressed,
2179            type          = "stream",
2180            string        = data,
2181            attr          = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil,
2182        }
2183    end
2184
2185    function lpdf.flushstreamfileobject(filename,dict,compressed,objnum) -- default compressed
2186        if trace_objects then
2187            report_objects("flushing stream file object %a",filename)
2188        end
2189        local dtype = type(dict)
2190        return pdfdeferredobject {
2191            objnum        = objnum,
2192            immediate     = true,
2193--             compresslevel = compressed == false and 0 or nil,
2194            compresslevel = compressed,
2195            type          = "stream",
2196            file          = filename,
2197            attr          = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil,
2198        }
2199    end
2200
2201    local shareobjectcache, shareobjectreferencecache = { }, { }
2202
2203    function lpdf.shareobject(content)
2204        if content == nil then
2205            -- invalid object not created
2206        else
2207            content = tostring(content)
2208            local o = shareobjectcache[content]
2209            if not o then
2210                o = pdfimmediateobject(content)
2211                shareobjectcache[content] = o
2212            end
2213            return o
2214        end
2215    end
2216
2217    pdfsharedobject = function(content)
2218        if content == nil then
2219            -- invalid object not created
2220        else
2221            content = tostring(content)
2222            local r = shareobjectreferencecache[content]
2223            if not r then
2224                local o = shareobjectcache[content]
2225                if not o then
2226                    o = pdfimmediateobject(content)
2227                    shareobjectcache[content] = o
2228                end
2229                r = pdfreference(o)
2230                shareobjectreferencecache[content] = r
2231            end
2232            return r
2233        end
2234    end
2235
2236    lpdf.flushobject          = pdfflushobject
2237    lpdf.flushstreamobject    = pdfflushstreamobject
2238    lpdf.shareobjectreference = pdfsharedobject
2239    lpdf.sharedobject         = pdfsharedobject
2240
2241end
2242
2243local pages = table.setmetatableindex(function(t,k)
2244    local v = pdfreserveobject()
2245    t[k] = v
2246    return v
2247end)
2248
2249pdfgetpagereference = function(n)
2250    return pages[n]
2251end
2252
2253lpdf.getpagereference = pdfgetpagereference
2254
2255local function flushnormalobj(data,n)
2256    if not n then
2257        nofobjects = nofobjects + 1
2258        n = nofobjects
2259    end
2260    data = f_object(n,data)
2261    if level == 0 then
2262        objects[n] = offset
2263        offset = offset + #data
2264        flush(f,data)
2265    else
2266        if not lastdeferred then
2267            lastdeferred = n
2268        elseif n < lastdeferred then
2269            lastdeferred = n
2270        end
2271        objects[n] = data
2272    end
2273    return n
2274end
2275
2276local function flushstreamobj(data,n,dict,comp,nolength)
2277    if not data then
2278        report("no data for %S",dict)
2279        return
2280    end
2281    if not n then
2282        nofobjects = nofobjects + 1
2283        n = nofobjects
2284    end
2285    local size = #data
2286    if comp ~= false then
2287        comp = compress and size > threshold
2288    end
2289    if level == 0 then
2290        local b = nil
2291        local e = s_stream_e
2292        if nolength then
2293            b = f_stream_b_d_r(n,dict) -- raw object, already treated
2294        else
2295            if comp then
2296                local compdata = compressdata(data,size)
2297                if compdata then
2298                    local compsize = #compdata
2299                    if compsize <= size - threshold then
2300                        data = compdata
2301                        size = compsize
2302                    else
2303                        comp = false
2304                    end
2305                else
2306                    comp = false
2307                end
2308            end
2309            if comp then
2310                b = dict and f_stream_b_d_c(n,dict,size) or f_stream_b_n_c(n,size)
2311            else
2312                b = dict and f_stream_b_d_u(n,dict,size) or f_stream_b_n_u(n,size)
2313            end
2314        end
2315        flush(f,b)
2316        flush(f,data)
2317        flush(f,e)
2318        objects[n] = offset
2319        offset = offset + #b + size + #e
2320    else
2321        if nolength then
2322            data = f_stream_d_r(n,dict,data) -- raw object, already treated
2323        else
2324            if comp then
2325                local compdata = compressdata(data,size)
2326                if compdata then
2327                    local compsize = #compdata
2328                    if compsize <= size - threshold then
2329                        data = compdata
2330                        size = compsize
2331                    else
2332                        comp = false
2333                    end
2334                else
2335                    comp = false
2336                end
2337            end
2338            if comp then
2339                data = dict and f_stream_d_c(n,dict,size,data) or f_stream_n_c(n,size,data)
2340            else
2341                data = dict and f_stream_d_u(n,dict,size,data) or f_stream_n_u(n,size,data)
2342            end
2343        end
2344        if not lastdeferred then
2345            lastdeferred = n
2346        elseif n < lastdeferred then
2347            lastdeferred = n
2348        end
2349        objects[n] = data
2350    end
2351    return n
2352end
2353
2354flushdeferred = function() -- was forward defined
2355    if lastdeferred then
2356        for n=lastdeferred,nofobjects do
2357            local o = objects[n]
2358            if type(o) == "string" then
2359                objects[n] = offset
2360                offset = offset + #o
2361                flush(f,o)
2362            end
2363        end
2364        lastdeferred = false
2365    end
2366end
2367
2368pdfimmediateobject = function(a,b,c,d)
2369    local kind --, immediate
2370    local objnum, data, attr, filename
2371    local compresslevel, objcompression, nolength
2372    local argtype = type(a)
2373    if argtype == "table" then
2374        kind           = a.type          -- raw | stream
2375     -- immediate      = a.immediate
2376        objnum         = a.objnum
2377        attr           = a.attr
2378        compresslevel  = a.compresslevel
2379        objcompression = a.objcompression
2380        filename       = a.file
2381        data           = a.string or a.stream or ""
2382        nolength       = a.nolength
2383        if kind == "stream" then
2384            if filename then
2385                data = loaddata(filename) or ""
2386            end
2387        elseif kind == "raw"then
2388            if filename then
2389                data = loaddata(filename) or ""
2390            end
2391        elseif kind == "file"then
2392            kind = "raw"
2393            data = filename and loaddata(filename) or ""
2394        elseif kind == "streamfile" then
2395            kind = "stream"
2396            data = filename and loaddata(filename) or ""
2397        end
2398    else
2399        if argtype == "number" then
2400            objnum = a
2401            a, b, c = b, c, d
2402        else
2403            nofobjects = nofobjects + 1
2404            objnum = nofobjects
2405        end
2406        if b then
2407            if a == "stream" then
2408                kind = "stream"
2409                data = b
2410            elseif a == "file" then
2411             -- kind = "raw"
2412                data = loaddata(b)
2413            elseif a == "streamfile" then
2414                kind = "stream"
2415                data = loaddata(b)
2416            else
2417                data = "" -- invalid object
2418            end
2419            attr = c
2420        else
2421         -- kind = "raw"
2422            data = a
2423        end
2424    end
2425    if not objnum then
2426        nofobjects = nofobjects + 1
2427        objnum = nofobjects
2428    end
2429    -- todo: immediate
2430    if kind == "stream" then
2431        flushstreamobj(data,objnum,attr,compresslevel,nolength) -- nil == auto
2432    elseif objectstream and objcompression ~= false then
2433        addtocache(objnum,data)
2434    else
2435        flushnormalobj(data,objnum)
2436    end
2437    return objnum
2438end
2439
2440pdfdeferredobject    = pdfimmediateobject
2441
2442lpdf.deferredobject  = pdfimmediateobject
2443lpdf.immediateobject = pdfimmediateobject
2444
2445-- In lua 5.4 the methods are now moved one metalevel deeper so we need to get them
2446-- from mt.__index instead. (I did get that at first.) It makes for a slightly (imo)
2447-- nicer interface but no real gain in speed as we don't flush that often.
2448
2449local openfile, closefile  do
2450
2451    -- I used to do <space><lf> but then figured out that when I open and save a file in a mode
2452    -- that removes trailing spaces, the xref becomes invalid. The problem was then that a
2453    -- reconstruction of the file by a viewer gives weird effects probably because percent symbols
2454    -- gets interpreted then. Thanks to Ross Moore for noticing this side effect!
2455
2456    local f_used       = formatters["%010i 00000 n\013\010"]
2457    local f_link       = formatters["%010i 00000 f\013\010"]
2458    local f_first      = formatters["%010i 65535 f\013\010"]
2459
2460    local f_pdf_tag    = formatters["%%PDF-%i.%i\010"]
2461    local f_xref       = formatters["xref\0100 %i\010"]
2462    local f_trailer_id = formatters["trailer\010<< %s /ID [ <%s> <%s> ] >>\010startxref\010%i\010%%%%EOF"]
2463    local f_trailer_no = formatters["trailer\010<< %s >>\010startxref\010%i\010%%%%EOF"]
2464    local f_startxref  = formatters["startxref\010%i\010%%%%EOF"]
2465
2466    local inmemory = false
2467    local close    = false
2468    local update   = false
2469
2470 -- local banner <const> = "%\xCC\xD5\xC1\xD4\xC5\xD8\xD0\xC4\xC6\010"     -- LUATEXPDF  (+128)
2471    local banner <const> = "%\xC3\xCF\xCE\xD4\xC5\xD8\xD4\xD0\xC4\xC6\010" -- CONTEXTPDF (+128)
2472
2473
2474 -- local removefile = os.remove
2475
2476    openfile = function(filename)
2477        if inmemory then
2478            local n = 0
2479            f = { }
2480            flush = function(f,s)
2481                n = n + 1 f[n] = s
2482             -- offset = offset + #s
2483            end
2484            close = function(f)
2485                f = concat(f)
2486                io.savedata(filename,f)
2487                f = false
2488            end
2489            update = function(f,s)
2490                f[1] = s
2491            end
2492         -- local n = 0
2493         -- f = {
2494         --     write = function(self,s)
2495         --         n = n + 1 f[n] = s
2496         --     end,
2497         --     close = function(self)
2498         --         f = concat(f)
2499         --         io.savedata(filename,f)
2500         --         f = false
2501         --     end,
2502         -- }
2503         else
2504            f = io.open(filename,"wb")
2505            if not f then
2506                report()
2507                report("quitting because file %a cannot be opened for writing",filename)
2508                report()
2509                os.exit()
2510            end
2511         -- f:setvbuf("full",64*1024)
2512            local m = getmetatable(f)
2513            flush = m.write or m.__index.write
2514            close = m.close or m.__index.close
2515            update = function(f,s)
2516                f:seek("set",0)
2517                f:write(s)
2518            end
2519        end
2520        local version = f_pdf_tag(majorversion,minorversion)
2521        flush(f,version)
2522        flush(f,banner)
2523        offset = offset + #version + #banner
2524    end
2525
2526    closefile = function(abort)
2527        if abort then
2528            close(f)
2529            if not environment.arguments.nodummy then
2530                f = io.open(abort,"wb")
2531                if f then
2532                    local name = resolvers.findfile("context-lmtx-error.pdf")
2533                    if name then
2534                        local data = io.loaddata(name)
2535                        if data then
2536                            f:write(data)
2537                            f:close()
2538                            return
2539                        end
2540                    end
2541                    f:close()
2542                end
2543            end
2544            os.remove(abort)
2545        else
2546            local xrefoffset = offset
2547            local lastfree   = 0
2548            local noffree    = 0
2549            local catalog    = lpdf.getcatalog()
2550            local info       = lpdf.getinfo()
2551            local trailerid  = lpdf.gettrailerid()
2552            if objectstream then
2553                flushdeferred()
2554                flushcache()
2555                --
2556                xrefoffset = offset
2557                --
2558                nofobjects = nofobjects + 1
2559                objects[nofobjects] = offset -- + 1
2560                --
2561                -- combine these three in one doesn't really give less code so
2562                -- we go for the efficient ones
2563                --
2564                local nofbytes  = 4
2565                local c1, c2, c3, c4
2566                if offset <= 0xFFFF then
2567                    nofbytes = 2
2568                    for i=1,nofobjects do
2569                        local o = objects[i]
2570                        if not o then
2571                            noffree = noffree + 1
2572                        else
2573                            local strm = o < 0
2574                            if strm then
2575                                o = -o
2576                            end
2577                            c1 = (o>>8)&0xFF
2578                            c2 = (o>>0)&0xFF
2579                            if strm then
2580                                objects[i] = char(2,c1,c2,streams[o][i])
2581                            else
2582                                objects[i] = char(1,c1,c2,0)
2583                            end
2584                        end
2585                    end
2586                    if noffree > 0 then
2587                        for i=nofobjects,1,-1 do
2588                            local o = objects[i]
2589                            if not o then
2590                                local f1 = (lastfree>>8)&0xFF
2591                                local f2 = (lastfree>>0)&0xFF
2592                                objects[i] = char(0,f1,f2,0)
2593                                lastfree   = i
2594                            end
2595                        end
2596                    end
2597                elseif offset <= 0xFFFFFF then
2598                    nofbytes = 3
2599                    for i=1,nofobjects do
2600                        local o = objects[i]
2601                        if not o then
2602                            noffree = noffree + 1
2603                        else
2604                            local strm = o < 0
2605                            if strm then
2606                                o = -o
2607                            end
2608                            c1 = (o>>16)&0xFF
2609                            c2 = (o>> 8)&0xFF
2610                            c3 = (o>> 0)&0xFF
2611                            if strm then
2612                                objects[i] = char(2,c1,c2,c3,streams[o][i])
2613                            else
2614                                objects[i] = char(1,c1,c2,c3,0)
2615                            end
2616                        end
2617                    end
2618                    if noffree > 0 then
2619                        for i=nofobjects,1,-1 do
2620                            local o = objects[i]
2621                            if not o then
2622                                local f1 = (lastfree>>16)&0xFF
2623                                local f2 = (lastfree>> 8)&0xFF
2624                                local f3 = (lastfree>> 0)&0xFF
2625                                objects[i] = char(0,f1,f2,f3,0)
2626                                lastfree   = i
2627                            end
2628                        end
2629                    end
2630                else
2631                    nofbytes = 4
2632                    for i=1,nofobjects do
2633                        local o = objects[i]
2634                        if not o then
2635                            noffree = noffree + 1
2636                        else
2637                            local strm = o < 0
2638                            if strm then
2639                                o = -o
2640                            end
2641                            c1 = (o>>24)&0xFF
2642                            c2 = (o>>16)&0xFF
2643                            c3 = (o>> 8)&0xFF
2644                            c4 = (o>> 0)&0xFF
2645                            if strm then
2646                                objects[i] = char(2,c1,c2,c3,c4,streams[o][i])
2647                            else
2648                                objects[i] = char(1,c1,c2,c3,c4,0)
2649                            end
2650                        end
2651                    end
2652                    if noffree > 0 then
2653                        for i=nofobjects,1,-1 do
2654                            local o = objects[i]
2655                            if not o then
2656                                local f1 = (lastfree>>24)&0xFF
2657                                local f2 = (lastfree>>16)&0xFF
2658                                local f3 = (lastfree>> 8)&0xFF
2659                                local f4 = (lastfree>> 0)&0xFF
2660                                objects[i] = char(0,f1,f2,f3,f4,0)
2661                                lastfree   = i
2662                            end
2663                        end
2664                    end
2665                end
2666                objects[0] = rep("\0",1+nofbytes+1)
2667                local data = concat(objects,"",0,nofobjects)
2668                local size = #data
2669                local xref = pdfdictionary {
2670                    Type  = pdfconstant("XRef"),
2671                    Size  = nofobjects + 1,
2672                    W     = pdfarray { 1, nofbytes, 1 },
2673                    Root  = catalog,
2674                    Info  = info,
2675                    ID    = trailerid and pdfarray { pdfliteral(trailerid,true), pdfliteral(trailerid,true) } or nil,
2676                }
2677                local fb
2678                if compress then
2679                    local comp = compressdata(data,size)
2680                    if comp then
2681                        data = comp
2682                        size = #data
2683                        fb = f_stream_b_d_c
2684                    else
2685                        fb = f_stream_b_d_u
2686                    end
2687                else
2688                    fb = f_stream_b_d_u
2689                end
2690                flush(f,fb(nofobjects,xref(),size))
2691                flush(f,data)
2692                flush(f,s_stream_e)
2693                flush(f,f_startxref(xrefoffset))
2694            else
2695                flushdeferred()
2696                xrefoffset = offset
2697                flush(f,f_xref(nofobjects+1))
2698                local trailer = pdfdictionary {
2699                    Size = nofobjects+1,
2700                    Root = catalog,
2701                    Info = info,
2702                }
2703                for i=1,nofobjects do
2704                    local o = objects[i]
2705                    if o then
2706                        objects[i] = f_used(o)
2707                    end
2708                end
2709                for i=nofobjects,1,-1 do
2710                    local o = objects[i]
2711                    if not o then
2712                        objects[i] = f_link(lastfree)
2713                        lastfree   = i
2714                    end
2715                end
2716                objects[0] = f_first(lastfree)
2717                flush(f,concat(objects,"",0,nofobjects))
2718                trailer.Size = nofobjects + 1
2719                if trailerid then
2720                    flush(f,f_trailer_id(trailer(),trailerid,trailerid,xrefoffset))
2721                else
2722                    flush(f,f_trailer_no(trailer(),xrefoffset))
2723                end
2724            end
2725            update(f,f_pdf_tag(majorversion,minorversion))
2726            close(f)
2727        end
2728        io.flush()
2729        closefile = function() end
2730    end
2731
2732end
2733
2734-- For the moment we overload it here, although back-fil.lua eventually will
2735-- be merged with back-pdf as it's pdf specific, or maybe back-imp-pdf or so.
2736
2737do
2738
2739    -- We overload img but at some point it will even go away, so we just
2740    -- reimplement what we need in context. This will change completely i.e.
2741    -- we will drop the low level interface!
2742
2743    local pdfbackend     = backends.registered.pdf
2744    local nodeinjections = pdfbackend.nodeinjections
2745    local codeinjections = pdfbackend.codeinjections
2746
2747    local imagetypes     = images.types -- pdf png jpg jp2 jbig2 stream
2748    local img_none       = imagetypes.none
2749
2750    local newimagerule   = nuts.pool.imagerule
2751    local setattrlist    = nuts.setattrlist
2752    local setprop        = nuts.setprop
2753
2754    local report_images  = logs.reporter("backend","images")
2755
2756    local lastindex      = 0
2757    local indices        = { }
2758
2759    local bpfactor       = number.dimenfactors.bp
2760
2761    function codeinjections.newimage(specification)
2762        return specification
2763    end
2764
2765    function codeinjections.copyimage(original)
2766        return setmetatableindex(original)
2767    end
2768
2769    function codeinjections.scanimage(specification)
2770        -- placeholder, doesn't give back dimensions etc but will be plugged in
2771        return specification
2772    end
2773
2774    local function embedimage(specification)
2775        if specification then
2776            lastindex = lastindex + 1
2777            index     = lastindex
2778            specification.index = index
2779            local xobject = pdfdictionary { }
2780            if not specification.notype then
2781                xobject.Type     = pdf_xobject
2782                xobject.Subtype  = pdf_form
2783                xobject.FormType = 1
2784            end
2785            local bbox = specification.bbox
2786            if bbox and not specification.nobbox then
2787                xobject.BBox = pdfarray {
2788                    bbox[1] * bpfactor,
2789                    bbox[2] * bpfactor,
2790                    bbox[3] * bpfactor,
2791                    bbox[4] * bpfactor,
2792                }
2793            end
2794            xobject = xobject + specification.attr
2795            if bbox and not specification.width then
2796                specification.width = bbox[3]
2797            end
2798            if bbox and not specification.height then
2799                specification.height = bbox[4]
2800            end
2801            local dict = xobject()
2802            --
2803            nofobjects     = nofobjects + 1
2804            local objnum   = nofobjects
2805            local nolength = specification.nolength
2806            local stream   = specification.stream or specification.string
2807            --
2808            -- We cannot set type in native img so we need this hack or
2809            -- otherwise we need to patch too much. Better that i write
2810            -- a wrapper then. Anyway, it has to be done better: a key that
2811            -- tells either or not to scale by xsize/ysize when flushing.
2812            --
2813            if not specification.type then
2814                local kind = specification.kind
2815                if kind then
2816                    -- take that one
2817                elseif attr and find(attr,"BBox") then
2818                    kind = img_stream
2819                else
2820                    -- hack: a bitmap
2821                    kind = img_none
2822                end
2823                specification.type = kind
2824                specification.kind = kind
2825            end
2826            flushstreamobj(stream,objnum,dict,compresslevel,nolength)
2827            specification.objnum      = objnum
2828            specification.rotation    = specification.rotation or 0
2829            specification.orientation = specification.orientation or 0
2830            specification.transform   = specification.transform or 0
2831            specification.stream      = nil
2832            specification.attr        = nil
2833            specification.type        = specification.kind or specification.type or img_none
2834            indices[index]            = specification -- better create a real specification
2835            return specification
2836        end
2837    end
2838
2839    codeinjections.embedimage = embedimage
2840
2841    function codeinjections.wrapimage(specification)
2842        --
2843        local index = specification.index
2844        if not index then
2845            embedimage(specification)
2846        end
2847        --
2848        local n = newimagerule(
2849            specification.width  or 0,
2850            specification.height or 0,
2851            specification.depth  or 0
2852        )
2853        setattrlist(n,true)
2854        setprop(n,"index",specification.index)
2855        return tonode(n)
2856    end
2857
2858    pdfincludeimage = function(index)
2859        local specification = indices[index]
2860        if specification then
2861            local bbox      = specification.bbox
2862            local xorigin   = bbox[1]
2863            local yorigin   = bbox[2]
2864            local xsize     = bbox[3] - xorigin -- we need the original ones, not the 'rotated' ones
2865            local ysize     = bbox[4] - yorigin -- we need the original ones, not the 'rotated' ones
2866            local transform = specification.transform or 0
2867            local objnum    = specification.objnum or pdfreserveobject()
2868            local groupref  = nil
2869            local kind      = specification.kind or specification.type or img_none -- determines scaling type
2870            return
2871                kind,
2872                xorigin, yorigin,
2873                xsize, ysize,
2874                transform,
2875                objnum,
2876                groupref
2877        end
2878    end
2879
2880    lpdf.includeimage = pdfincludeimage
2881
2882end
2883
2884-- The driver.
2885
2886do
2887
2888 -- local addsuffix  = file.addsuffix
2889    local texgetbox  = tex.getbox
2890
2891    local pdfname    = nil
2892    local converter  = nil
2893    local useddriver = nil -- a bit of a hack
2894
2895    local function outputfilename(driver)
2896        return pdfname
2897    end
2898
2899-- local outputfilename ; do -- old todo usedname in ^^
2900--     local filename = nil
2901--     outputfilename = function(driver,usedname)
2902--         if usedname and usedname ~= "" then
2903--             filename = addsuffix(usedname,"pdf")
2904--         elseif not filename or filename == "" then
2905--             filename = addsuffix(tex.jobname,"pdf")
2906--         end
2907--         return filename
2908--     end
2909-- end
2910
2911    -- todo: prevent twice
2912
2913    local function prepare(driver)
2914        if not environment.initex then
2915            --
2916            backends.initialize("pdf") -- also does bindings
2917            --
2918            pdfname = tex.jobname .. ".pdf"
2919            openfile(pdfname)
2920            --
2921            luatex.registerstopactions(1,function()
2922                if pdfname then
2923                    lpdf.finalizedocument()
2924                    closefile()
2925                    pdfname = nil
2926                end
2927            end)
2928            --
2929            luatex.registerpageactions(1,function()
2930                if pdfname then
2931                    lpdf.finalizepage(true)
2932                end
2933            end)
2934            --
2935            lpdf.registerdocumentfinalizer(wrapupdocument,nil,"wrapping up")
2936            --
2937            statistics.register("result saved in file", function()
2938                local outputfilename = environment.outputfilename or environment.jobname or tex.jobname or "<unset>"
2939                outputfilename = string.gsub(outputfilename,"^%./+","") -- todo: make/use a helper
2940                return string.format("%s.%s, compresslevel %s, objectcompresslevel %s",outputfilename,"pdf",lpdf.getcompression())
2941            end)
2942            --
2943            luatex.registerstopactions(function()
2944                if pdfname then
2945                    local r = lpdf.lastreferredpage() -- somehow referenced
2946                    local s = lpdf.getnofpages()      -- in page tree, saved in file
2947                    local t = lpdf.nofpages()         -- in tuc file
2948                    if r > s then
2949                        report()
2950                        report("referred pages: %i, saved pages %i, pages from tuc file: %i, possible corrupt file",r,s,t)
2951                        report()
2952                    end
2953                end
2954            end)
2955        end
2956        converter  = drivers.converters.lmtx
2957        useddriver = driver
2958    end
2959
2960    local function wrapup(driver)
2961        if pdfname then
2962            closefile()
2963            pdfname = nil
2964        end
2965    end
2966
2967    local function cleanup(driver)
2968        if pdfname then
2969            closefile(pdfname)
2970            pdfname = nil
2971        end
2972    end
2973
2974    local function convert(driver,boxnumber)
2975        converter(driver,texgetbox(boxnumber),"page")
2976    end
2977
2978    localconverter = function(...)
2979        converter(useddriver,...)
2980    end
2981
2982    drivers.install {
2983        name     = "pdf",
2984        flushers = flushers,
2985        actions  = {
2986            prepare         = prepare,
2987            wrapup          = wrapup,
2988            cleanup         = cleanup,
2989            --
2990            initialize      = initialize,
2991            convert         = convert,
2992            finalize        = finalize,
2993            --
2994            outputfilename  = outputfilename,
2995        },
2996    }
2997
2998end
2999