lpdf-lmt.lmt /size: 125 Kb    last modification: 2025-02-21 11:03
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. Because we run into the 200 locals this file
12-- has to be split.
13
14-- There is no way that a lua based backend can compete performance wise with the
15-- original one for relative simple text runs. And we're talking seconds here on say
16-- 500 pages with paragraphs alternativng between three fonts and colors. But such
17-- documents are rare so in practice we are quite okay, especially because in
18-- ConTeXt we can gain quite a bit elsewhere. So, when we loose 30% on such simple
19-- documents, we break even on for instance the manual, and gain 30% on Thomas's
20-- turture test (also for other reasons). But .. who knows what magic I can cook up
21-- in due time.
22
23-- If you consider this complex, watch:
24--
25-- https://www.youtube.com/watch?v=6H-cAzfB2qo
26--
27-- or in distractionmode:
28--
29-- https://www.youtube.com/watch?v=TYuTE_1jvvE
30-- https://www.youtube.com/watch?v=nnicGKX3lvM
31--
32-- For the moment we have to support the built in backend as well as the alternative. So
33-- the next interface is suboptimal and will change at some time. At that moment I will
34-- also optimize and extend.
35
36local type, next, unpack, tonumber, rawget = type, next, unpack, tonumber, rawget
37local char, rep, find, gsub = string.char, string.rep, string.find, string.gsub
38local formatters = string.formatters
39local concat, sortedhash = table.concat, table.sortedhash
40local setmetatableindex = table.setmetatableindex
41local loaddata = io.loaddata
42
43local bpfactor       <const> = number.dimenfactors.bp
44
45local osuuid                 = os.uuid
46local zlibcompresssize       = xzip.compresssize
47
48local nuts                   = nodes.nuts
49
50local pdfreference           = lpdf.reference
51local pdfdictionary          = lpdf.dictionary
52local pdfarray               = lpdf.array
53local pdfconstant            = lpdf.constant
54local pdfliteral             = lpdf.literal -- not to be confused with a whatsit!
55local pdfunicode             = lpdf.unicode
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")
83local report_encryption = logs.reporter("backend","encryption")
84
85local trace_objects     = false  trackers.register("backend.objects",       function(v) trace_objects = v end)
86local trace_details     = false  trackers.register("backend.details",       function(v) trace_details = v end)
87local trace_indices     = false  trackers.register("backend.fonts.details", function(v) trace_indices = v end)
88
89-- These two tables used a font id as index and will be metatabled in lpdf-emb.lmt:
90
91local usedfontnames     = { }
92local usedfontobjects   = { }
93
94lpdf.usedfontnames      = usedfontnames
95lpdf.usedfontobjects    = usedfontobjects
96
97-- experiment:
98
99local function compressdata(data,size)
100    local guess = ((size // 4096) + 1) * 2048
101    local comp  = zlibcompresssize(data,guess,3)
102 -- if comp then
103 --     report()
104 --     report("size %i, guess %i, result %i => %s",size,guess,#comp,guess>=#comp and "hit" or "miss")
105 --     report()
106 -- end
107    return comp
108end
109
110-- local function compressdata(data,size)
111--     return zlibcompress(data,3)
112-- end
113
114-- we collect them:
115
116local flushers = { }
117
118-- used variables
119
120local pdf_h = 0
121local pdf_v = 0
122
123local need_tm
124local need_tf
125local need_font
126----- cur_tmrx
127local need_width
128local need_mode
129local done_width
130local done_mode
131local mode
132local current_pdf
133----- f_pdf_cur
134local current_font
135local current_effect
136local current_slant
137local current_weight
138local current_sx
139local current_sy
140local current_factor
141local f_x_scale
142local f_y_scale
143local tj_scale
144local tj_delta
145local tj_position
146----- tmrx, tmry, tmsx, tmsy, tmtx, tmty
147local tmrx, tmry,       tmsy, tmtx, tmty
148----- cmrx, cmry, cmsx, cmsy, cmtx, cmty
149local cmrx, cmry,             cmtx, cmty
150local tmef
151local tmef_f_x_scale
152local tmef_f_w_scale
153
154local usedfonts, usedxforms, usedximages, usedxgroups
155local getxformname, getximagename
156local boundingbox, shippingmode, objectnumber
157
158local function usefont(t,k) -- a bit redundant hash
159 -- local v = pdfgetfontname(k)
160    local v = usedfontnames[k]
161    t[k] = v
162    return v
163end
164
165local function reset_variables(specification)
166    pdf_h, pdf_v   = 0, 0
167    cmrx, cmry     = 1.0, 1.0
168 -- cmsx, cmsy     = 0.0, 0.0
169    cmtx, cmty     = 0.0, 0.0
170    tmrx, tmry     = 1.0, 1.0
171 -- tmsx, tmsy     = 0.0, 0.0
172          tmsy     =      0.0 -- sy makes no sense
173    tmtx, tmty     = 0.0, 0.0
174    tmef           = 1.0
175    need_tm        = false
176    need_tf        = false
177    need_font      = true
178    need_width     = 0
179    need_mode      = 0
180    done_width     = false
181    done_mode      = false
182    mode           = "page"
183    shippingmode   = specification.shippingmode
184    objectnumber   = specification.objectnumber
185 -- cur_tmrx       = 0.0
186 -- f_pdf_cur      = 0 -- nullfont
187    tj_scale       = 1
188    tj_delta       = 0.0
189    tj_position    = 0.0
190    current_font   = 0
191    current_pdf    = 0 -- nullfont
192    current_effect = nil
193    current_slant  = 0
194    current_weight = 0
195    current_factor = 0
196    current_sx     = 1
197    current_sy     = 1
198    f_x_scale      = 1.0
199    f_y_scale      = 1.0
200    tmef_f_x_scale = 1
201    tmef_f_w_scale = 1
202    usedfonts      = setmetatableindex(usefont)
203    usedxforms     = { }
204    usedximages    = { }
205 -- usedxgroups    = { }
206    boundingbox    = specification.boundingbox
207end
208
209-- buffer
210
211local buffer = lua.newtable(1024,0) -- { }
212local b      = 0
213
214local function reset_buffer()
215    b = 0
216end
217
218-- fonts
219
220-- The text in a PDF file ends up in the page stream. Right from the start PDF files
221-- were supposed to be efficient but over time that concept was messed up by for
222-- instance mixed in tagging. One can argue if that was a good idea: just embed the
223-- source and use that as for special purposes instead of trying to turn something
224-- visual into something structure. We have these angle bracket formats for that
225-- already. Anyway, here we assume an uninterupted stream.
226--
227-- A sequence of characters in the same font and with the same scale (and other
228-- rendering properties). When we started with the backend in Lua I just followed
229-- the same approach as in LuaTeX (which was modelled after pdfTeX) but at some
230-- point a (stepwise) transition took place. For instance, we always use a 10bp font
231-- size (instead of the scale in the document) and delegate all scaling to the font
232-- transform matrix. Because the text stream uses a different coordinate system
233-- (font units combined with 1000 based kerning) we need to keep track of where we
234-- are and when we drift too much we need to restart. In the engines this is
235-- complicated by integer math so there drift really plays a role, but in our case
236-- we can now approach it a bit different, although we don't really know the
237-- acceptable thresholds. This code evolved over time, also because we introduced
238-- additional scaling options.
239--
240-- So, in the backend we have to deal with this:
241--
242-- - positioning in target space (page)
243-- - positioning in the stream (text)
244-- - glyph scale (!), glyph xscale (!), glyph yscale
245-- - font scale (!)
246-- - horizontal extend (!)
247-- - vertical squeeze
248-- - slant
249-- - expansion factor (!)
250-- - font effects (mode and width/line)
251-- - temporary glyph effects (extend, squeeze, slant, mode, width/line)
252-- - drift (synchronization and check for threshold)
253--
254-- This means that we have to check if one of these properties has changed and the
255-- horizontal scale is the most demanding one. We also need to mix the check for
256-- font specific changes and glyph specific effects. For now we assume that slanted
257-- font is just a copy but in the future we could just make it a glyph property like
258-- scale, xscale and yscale. An even more advanced approach is to integrate
259-- boldening but then we also need 4 offsets and runtime calculation, while it might
260-- also complicate e.g. extensibles in math. There is much more overhead so it will
261-- also impact performance.
262
263local fontcharacters
264----- fontdescriptions
265local fontparameters
266local fontproperties
267local pdfcharacters
268
269local getstreamhash = fonts.handlers.otf.getstreamhash
270
271local usedfontstreams = utilities.storage.allocate()
272
273local usedindices = setmetatableindex(function(t,k)
274    local n = 0
275-- n = 31
276    local v = setmetatableindex(function(tt,kk)
277        if n >= 0xFFFF then
278            report_fonts("registering character index: overflow in hash %a, todo: use overflow font")
279        else
280            n = n + 1
281        end
282        if trace_indices then
283            report_fonts("registering character index: hash %a, charindex 0x%05X, slotindex 0x%04X",k,kk,n)
284        end
285        local vv = n
286        tt[kk] = vv
287        return vv
288    end)
289    t[k] = v
290    return v
291end)
292
293local usedcharacters = setmetatableindex(function(t,k)
294    local h, d = getstreamhash(k)
295    if trace_indices then
296        report_fonts("registering index table: hash %a, fontid %i",h,k)
297    end
298    usedfontstreams[h] = d
299    local v = usedindices[h]
300    t[k] = v
301    return v
302end)
303
304lpdf.usedfontstreams = usedfontstreams -- [streamhash]        -> fontdata
305lpdf.usedcharacters  = usedcharacters  -- [fontid]            -> indices
306lpdf.usedindices     = usedindices     -- [streamhash][index] -> realindex (can also be dupindex)
307
308local horizontalmode = true
309local scalefactor    = 1
310local threshold      = 655360
311local tjfactor       = 100 / 65536
312
313function flushers.updatefontstate(font)
314    -- virtual t3 fonts have negative font index
315    fontcharacters   = characters[font]
316 -- fontdescriptions = descriptions[font]
317    fontparameters   = parameters[font]
318    fontproperties   = properties[font]
319    local size       = fontparameters.size -- or bad news
320    local designsize = fontparameters.designsize or size
321    pdfcharacters    = usedcharacters[font]
322    horizontalmode   = fontparameters.writingmode ~= "vertical"
323    scalefactor      = (designsize/size) * tjfactor
324end
325
326local f_cm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N cm"]
327local f_cz = formatters["%.6N 0 0 %.6N %.6N %.6N cm"]
328----- f_tm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N Tm"]
329local f_tm = formatters["%.6N 0 %.6N %.6N %.6N %.6N Tm"]
330
331directives.register("backend.pdf.accurate", function()
332    f_cm = formatters["%.9N %.9N %.9N %.9N %.9N %.9N cm"]
333    f_cz = formatters["%.9N 0 0 %.9N %.9N %.9N cm"]
334 -- f_tm = formatters["%.9N %.9N %.9N %.9N %.9N %.9N Tm"]
335    f_tm = formatters["%.9N 0 %.9N %.9N %.9N %.9N Tm"]
336end)
337
338local saved_text_pos_v = 0
339local saved_text_pos_h = 0
340
341local function begin_text()
342    saved_text_pos_h = pdf_h
343    saved_text_pos_v = pdf_v
344    b = b + 1 ; buffer[b] = "BT"
345    need_tf        = true
346    need_font      = true
347    need_width     = 0
348    need_mode      = 0
349    current_effect = nil
350    current_slant  = 0
351    current_weight = 0
352    mode           = "text"
353end
354
355local function end_text()
356    if done_width then
357        b = b + 1 ; buffer[b] = "0 w"
358        done_width = false
359    end
360    if done_mode then
361        b = b + 1 ; buffer[b] = "0 Tr"
362        done_mode = false
363    end
364    b = b + 1 ; buffer[b] = "ET"
365    pdf_h = saved_text_pos_h
366    pdf_v = saved_text_pos_v
367    mode  = "page"
368end
369
370local begin_chararray, end_chararray do
371
372    local saved_chararray_pos_h
373    local saved_chararray_pos_v
374
375    local saved_b = 0
376
377    begin_chararray = function()
378        saved_chararray_pos_h = pdf_h
379        saved_chararray_pos_v = pdf_v
380        tj_position = horizontalmode and saved_chararray_pos_h or - saved_chararray_pos_v
381        tj_delta = 0
382        saved_b = b
383        b = b + 1 ; buffer[b] = " ["
384        mode = "chararray"
385    end
386
387    end_chararray = function()
388        b = b + 1 ; buffer[b] = "] TJ"
389        buffer[saved_b] = concat(buffer,"",saved_b,b)
390        b = saved_b
391        pdf_h = saved_chararray_pos_h
392        pdf_v = saved_chararray_pos_v
393        mode  = "text"
394    end
395
396end
397
398local function begin_charmode()
399    b = b + 1 ; buffer[b] = "<"
400    mode = "char"
401end
402
403local function end_charmode()
404    b = b + 1 ; buffer[b] = ">"
405    mode = "chararray"
406end
407
408local function calc_pdfpos(h,v)
409    -- mostly char
410    if mode == "page" then
411        cmtx = h - pdf_h
412        cmty = v - pdf_v
413        return h ~= pdf_h or v ~= pdf_v
414    elseif mode == "text" then
415        tmtx = h - saved_text_pos_h
416        tmty = v - saved_text_pos_v
417        return h ~= pdf_h or v ~= pdf_v
418    elseif horizontalmode then
419        tmty = v - saved_text_pos_v
420        tj_delta = tj_position - h
421        return tj_delta ~= 0 or v ~= pdf_v
422    else
423        tmtx = h - saved_text_pos_h
424        tj_delta = tj_position + v
425        return tj_delta ~= 0 or h ~= pdf_h
426    end
427end
428
429local function pdf_set_pos(h,v)
430    local move = calc_pdfpos(h,v)
431    if move then
432     -- b = b + 1 ; buffer[b] = f_cm(cmrx, cmsx, cmsy, cmry, cmtx*bpfactor, cmty*bpfactor)
433        b = b + 1 ; buffer[b] = f_cz(cmrx, cmry,             cmtx*bpfactor, cmty*bpfactor)
434        pdf_h = pdf_h + cmtx
435        pdf_v = pdf_v + cmty
436    end
437end
438
439local function pdf_reset_pos()
440    if mode == "page" then
441        cmtx = - pdf_h
442        cmty = - pdf_v
443        if pdf_h == 0 and pdf_v == 0 then
444            return
445        end
446    elseif mode == "text" then
447        tmtx = - saved_text_pos_h
448        tmty = - saved_text_pos_v
449        if pdf_h == 0 and pdf_v == 0 then
450            return
451        end
452    elseif horizontalmode then
453        tmty = - saved_text_pos_v
454        tj_delta = tj_position
455        if tj_delta == 0 and pdf_v == 0 then
456            return
457        end
458    else
459        tmtx = - saved_text_pos_h
460        tj_delta = tj_position
461        if tj_delta == 0 and pdf_h == 0 then
462            return
463        end
464    end
465 -- b = b + 1 ; buffer[b] = f_cm(cmrx, cmsx, cmsy, cmry, cmtx*bpfactor, cmty*bpfactor)
466    b = b + 1 ; buffer[b] = f_cz(cmrx,             cmry, cmtx*bpfactor, cmty*bpfactor)
467    pdf_h = pdf_h + cmtx
468    pdf_v = pdf_v + cmty
469end
470
471local function pdf_set_pos_temp(h,v)
472    local move = calc_pdfpos(h,v)
473    if move then
474     -- b = b + 1 ; buffer[b] = f_cm(cmrx, cmsx, cmsy, cmry, cmtx*bpfactor, cmty*bpfactor)
475        b = b + 1 ; buffer[b] = f_cz(cmrx,             cmry, cmtx*bpfactor, cmty*bpfactor)
476    end
477end
478
479-- these dummy returns makes using them a bit faster
480
481local function pdf_end_string_nl()
482    if mode == "char" then
483        end_charmode()
484        return end_chararray()
485    elseif mode == "chararray" then
486        return end_chararray()
487    end
488end
489
490local function pdf_goto_textmode()
491    if mode == "page" then
492        pdf_reset_pos()
493        return begin_text()
494    elseif mode ~= "text" then
495        if mode == "char" then
496            end_charmode()
497            return end_chararray()
498        else -- if mode == "chararray" then
499            return end_chararray()
500        end
501    end
502end
503
504local function pdf_goto_pagemode()
505    if mode ~= "page" then
506        if mode == "char" then
507            end_charmode()
508            end_chararray()
509            return end_text()
510        elseif mode == "chararray" then
511            end_chararray()
512            return end_text()
513        elseif mode == "text" then
514            return end_text()
515        end
516    end
517end
518
519local function pdf_goto_fontmode()
520    if mode == "char" then
521        end_charmode()
522        end_chararray()
523        end_text()
524    elseif mode == "chararray" then
525        end_chararray()
526        end_text()
527    elseif mode == "text" then
528        end_text()
529    end
530    pdf_reset_pos()
531    mode = "page"
532end
533
534-- characters
535
536-- we could actually do this:
537--
538-- -- effactor (expand) -> into glyphxscale
539-- -- extend            -> into glyphxscale
540-- -- squeeze           -> into glyphyscale
541
542do
543
544    local round = math.round
545
546    -- across pages ... todo: clean up because we don't need to pass the font
547    -- as fontparameters already has checked / set it we can also have a variable
548    -- for it so
549
550    local characterwidth   = nil
551 -- local descriptionwidth = nil
552    local hshift           = false
553    local vshift           = false
554
555    local dupx = 100 / bpfactor -- 6578176.0
556    local dumx = - dupx
557    local dupy = dupx
558    local dumy = dumx
559
560    directives.register("backend.pdf.drift", function(v)
561        dupx = tonumber(v) or 100
562        if dupx < 10 then
563            dupx = 10
564        elseif dupx > 100 then
565            dupx = 100
566        end
567        dupx = dupx / bpfactor
568    end)
569
570    local h_hex_2 = lpdf.h_hex_2
571    local h_hex_4 = lpdf.h_hex_4
572
573    -- The width array uses the original dimensions! This is different from e.g.
574    -- luatex where we have more widths arrays and these reflect the cheated
575    -- widths (goes wrong elsewhere).
576
577    -- when changing this, check math: compact-001.tex (rule width)
578
579    local characterwidths = setmetatableindex(function(t,font)
580        local d = descriptions[font]
581        local c = characters[font]
582        local f = parameters[font].hfactor or parameters[font].factor
583        local v = setmetatableindex(function(t,char)
584            local w
585            local e = c and c[char]
586            if e then
587                local a = e.advance
588                if a then
589                    w = a
590                else
591                    w = e.width or 0
592                end
593            end
594            if not w then
595                e = d and d[char]
596                if e then
597                    w = e.width
598                    if w then
599                        w =  w * f
600                    end
601                end
602            end
603            if not w then
604                w = 0
605            end
606            t[char] = w
607            return w
608        end)
609        t[font] = v
610        return v
611    end)
612
613    -- the descriptions are used for the width array
614
615    local extend  = 1 -- some more can move here
616    local squeeze = 1
617    local tohex   = h_hex_4
618
619    local function setup_fontparameters(font,factor,sx,sy,slant,weight,effect)
620        --
621        current_sx = sx
622        current_sy = sy
623        --
624        if fontproperties.bitmapped then
625            tohex = h_hex_2
626        elseif font < 0 then
627            tohex = h_hex_2
628        else
629            tohex = h_hex_4
630        end
631
632        local format = fontproperties.format
633        local expand = 1 + factor / 1000000
634        if effect then
635            -- We have glyph specific effects and these have a higher priority than
636            -- the font specific effects.
637            if effect ~= current_effect then
638                current_effect   = effect
639                tmrx             = 1
640                tmry             = 1
641                tmsy             = effect.slant   or fontparameters.slantfactor   or slant or 0
642                extend           = effect.extend  or fontparameters.extendfactor  or 1
643                squeeze          = effect.squeeze or fontparameters.squeezefactor or 1
644                need_mode        = effect.mode    or fontparameters.mode          or 0
645                need_width       = effect.weight  or fontparameters.weight        or 0
646                sx               = extend  * sx
647                sy               = squeeze * sy
648            else
649             -- we could check if effects have changed but effects use unique tables; for
650             -- now they win over font effects (only used in math)
651tmrx             = 1
652tmry             = 1
653-- tmsy             = effect.slant   or fontparameters.slantfactor   or slant or 0
654            end
655        else
656            -- These are the font specific effects and the scales are part of the main
657            -- font setup, so we use the factors and not the properties.effect data.
658            current_effect = nil
659            tmrx           = 1
660            tmry           = 1
661            --
662            local e = fontproperties.effect
663            if e then
664                tmsy       = e.slant   or slant or 0
665                extend     = e.extend  or 1
666                squeeze    = e.squeeze or 1
667                need_mode  = e.mode    or 0
668                need_width = e.weight  or 0
669                sx         = extend  * sx
670                sy         = squeeze * sy
671            else
672                tmsy       = slant or 0
673                extend     = 1
674                squeeze    = 1
675                need_mode  = 0
676                need_width = weight
677            end
678        end
679
680        tmef = expand
681        tmrx = expand * tmrx
682
683        current_font   = font
684        current_pdf    = usedfonts[font] -- cache
685        current_factor = factor
686        current_slant  = tmsy
687        current_weight = weight
688
689        local sc = fontparameters.size * bpfactor / 10
690        -- kind of special:
691        if format == "opentype" or format == "type1" then
692            sc = sc * 1000 / fontparameters.units
693            tj_scale = fontparameters.units / 1000
694        else
695            tj_scale = 1
696        end
697        tj_delta    = 0
698        tj_position = 0
699        --
700        tmsy = tmsy * sc
701        tmrx = tmrx * sc
702        tmry = tmry * sc
703        --
704        f_x_scale = sx
705        if f_x_scale ~= 1.0 then
706            tmrx = tmrx * f_x_scale
707        end
708        f_y_scale = sy
709        if f_y_scale ~= 1.0 then
710            tmsy = tmsy * f_y_scale
711            tmry = tmry * f_y_scale
712        end
713        --
714        tmef_f_x_scale = tmef * f_x_scale
715        --
716        if tmef_f_x_scale == 1.0 then
717            tmef_f_x_scale = 1
718        end
719        --
720        tj_scale = tj_scale * scalefactor / tmef_f_x_scale
721        --
722        tmef_f_w_scale = tmef_f_x_scale / extend
723        --
724        characterwidth   = characterwidths[font]
725     -- descriptionwidth = descriptionwidths[font]
726        --
727        hshift = fontparameters.hshift
728        vshift = fontparameters.vshift
729
730        if need_width > 0 then
731            if not need_mode or need_mode == 0 then
732                need_mode = 2
733            end
734            need_width = need_width * 2
735            need_width = need_width * tmrx
736        end
737
738    end
739
740    local f_width = formatters["%.6N w"]
741    local f_mode  = formatters["%i Tr"]        -- can be hash
742    local f_font  = formatters["/F%i 10 Tf"]
743    local f_skip  = formatters["%.2N"]
744
745    local s_width = "0 w"
746    local s_mode  = "0 Tr"
747
748    local width_factor = 72.27 / 72.0
749
750    local last_fpdf
751
752    local function set_font()
753     -- if need_width and need_width ~= 0 then
754        if need_width ~= 0 then
755            b = b + 1 ; buffer[b] = f_width(width_factor*need_width)
756            done_width = true
757        elseif done_width then
758            b = b + 1 ; buffer[b] = s_width
759            done_width = false
760        end
761     -- if need_mode and need_mode ~= 0 then
762        if need_mode ~= 0 then
763            b = b + 1 ; buffer[b] = f_mode(need_mode)
764            done_mode = true
765        elseif done_mode then
766            b = b + 1 ; buffer[b] = s_mode
767            done_mode = false
768        end
769        -- no need when the same
770        if need_font or last_pdf ~= current_pdf then
771            b = b + 1 ; buffer[b] = f_font(current_pdf)
772            last_pdf  = current_pdf
773            need_font = false
774        end
775     -- f_pdf_cur = current_pdf
776        need_tf = false
777        need_tm = true
778    end
779
780    local function set_textmatrix(h,v)
781       local move = calc_pdfpos(h,v)
782       if need_tm or move then
783         -- b = b + 1 ; buffer[b] = f_tm(tmrx, tmsx, tmsy, tmry, tmtx*bpfactor, tmty*bpfactor)
784            b = b + 1 ; buffer[b] = f_tm(tmrx,       tmsy, tmry, tmtx*bpfactor, tmty*bpfactor)
785            pdf_h = saved_text_pos_h + tmtx
786            pdf_v = saved_text_pos_v + tmty
787            need_tm = false
788        end
789     -- cur_tmrx = tmrx
790    end
791
792    -- I will redo this mess ... we no longer have the mkiv pdf generator that we used in
793    -- luatex (a precursor to lmtx and also for comparison) but only in lmtx now so ...
794    -- time to move on I guess.
795
796    -- factor is for hz
797
798    flushers.character = function(current,pos_h,pos_v,pos_r,font,char,data,csx,csy,factor,sx,sy,slant,weight) -- ,naturalwidth,width)
799
800        local s = data.scale
801        local x = data.xoffset
802        local y = data.yoffset
803        local effect = data.effect
804
805        if s then
806            sx = s * sx
807            sy = s * sy
808        end
809        if csx then
810            sx = sx * csx
811            csx = 1
812        end
813        if csy then
814            sy = sy * csy
815            csy = 1
816        end
817        if     sx ~= current_sx -- was: f_x_scale
818            or sy ~= current_sy -- was: f_y_scale
819            or need_tf
820            or font ~= current_font
821         -- or current_pdf ~= f_pdf_cur
822            or mode == "page"
823            or slant  ~= current_slant
824            or weight ~= current_weight
825            or effect ~= current_effect
826        then
827            pdf_goto_textmode()
828            setup_fontparameters(font,factor,sx,sy,slant,weight,effect) -- too often due to page
829            set_font()
830     -- elseif mode == "page" then
831     --     pdf_goto_textmode()
832     --     set_font()
833        elseif current_factor ~= factor then -- or cur_tmrx ~= tmrx
834            -- check if this happens and when
835            setup_fontparameters(font,factor,sx,sy,slant,weight,effect)
836            need_tm = true
837        end
838
839        if x then
840            pos_h = pos_h + x * tmef_f_x_scale
841        end
842        if y then
843            pos_v = pos_v + y * f_y_scale
844        end
845
846        local move = calc_pdfpos(pos_h,pos_v)
847
848        if move or need_tm then
849            if not need_tm then
850                if horizontalmode then
851                    if (saved_text_pos_v + tmty) ~= pdf_v then
852                        need_tm = true
853                    elseif tj_delta >= dupx or tj_delta <= dumx then
854                        need_tm = true
855                    end
856                else
857                    if (saved_text_pos_h + tmtx) ~= pdf_h then
858                        need_tm = true
859                    elseif tj_delta >= dupy or tj_delta <= dumy then
860                        need_tm = true
861                    end
862                end
863            end
864            if hshift then pos_h = pos_h + hshift end
865            if vshift then pos_v = pos_v - vshift end
866            if need_tm then
867                pdf_goto_textmode()
868                set_textmatrix(pos_h,pos_v)
869                begin_chararray()
870                move = calc_pdfpos(pos_h,pos_v)
871            end
872            if move then
873                local d = tj_delta * tj_scale
874                if d <= -0.5 or d >= 0.5 then
875                    if mode == "char" then
876                        end_charmode()
877                    end
878                    -- ok for text with not too long words where spaces synchronize
879                 -- b = b + 1 ; buffer[b] = round(d) -- or f_skip(d)
880                    -- better for extensibles and incredible long words
881                    b = b + 1 ; buffer[b] = f_skip(d)
882                end
883                tj_position = tj_position - tj_delta
884             -- tj_position = tj_position - tj_delta * tmef_f_x_scale
885            end
886        end
887
888        if mode == "chararray" then
889            begin_charmode()
890        end
891
892        -- When we have say 100 x without spaces we do accumulate some error but in practice
893        -- we have spaces so we sync. This is the same as in pdftex and luatex.
894
895        tj_position = tj_position + characterwidth[char] * tmef_f_w_scale
896
897        local slot = pdfcharacters[data.index or char] -- registers usage
898
899--         b = b + 1 ; buffer[b] = font > 0 and h_hex_4[slot] or h_hex_2[slot]
900        b = b + 1 ; buffer[b] = tohex[slot]
901
902    end
903
904    do
905
906        -- We like a little bit of optimization, just for the fun of it.
907
908        local spaces = setmetatableindex(function(t,font)
909            local bytes = false
910            local data  = characters[font]
911            if data then
912                data = data[32]
913                if data then
914                    local index = data.index
915                    if index then
916                        local dummy = usedfonts[font]
917                        local slot  = usedcharacters[font][index]
918                        if slot then
919--                             bytes = font > 0 and h_hex_4[slot] or h_hex_2[slot]
920                            bytes = tohex[slot]
921                        end
922                    end
923                end
924            end
925            t[font] = bytes
926            return bytes
927        end)
928
929        -- We have to be in the current font stream.
930
931        flushers.space = function(font)
932            if mode == "char" and font == current_font then
933                local slot = spaces[font]
934                if slot then
935                    tj_position = tj_position + characterwidth[32] * tmef_f_w_scale
936                    b = b + 1 ; buffer[b] = slot
937                end
938            end
939        end
940
941    end
942
943    flushers.fontchar = function(font,char,data)
944        local dummy = usedfonts[font]
945        local slot  = pdfcharacters[data.index or char] -- registers usage
946        return dummy, slot
947    end
948
949end
950
951-- literals
952
953local flushliteral  do
954
955    local nodeproperties      = nodes.properties.data
956    local literalvalues       = nodes.literalvalues
957
958    local originliteral_code  <const> = literalvalues.origin
959    local pageliteral_code    <const> = literalvalues.page
960    local alwaysliteral_code  <const> = literalvalues.always
961    local rawliteral_code     <const> = literalvalues.raw
962    local textliteral_code    <const> = literalvalues.text
963    local fontliteral_code    <const> = literalvalues.font
964
965    flushliteral = function(current,pos_h,pos_v)
966        local p = nodeproperties[current]
967        if p then
968            local str = p.data
969            if str and str ~= "" then
970                local mode = p.mode
971                if mode == originliteral_code then
972                    pdf_goto_pagemode()
973                    pdf_set_pos(pos_h,pos_v)
974                elseif mode == pageliteral_code then
975                    pdf_goto_pagemode()
976                elseif mode == textliteral_code then
977                    pdf_goto_textmode()
978                elseif mode == fontliteral_code then
979                    pdf_goto_fontmode()
980                elseif mode == alwaysliteral_code then -- aka direct
981                    pdf_end_string_nl()
982                    need_tm = true
983                elseif mode == rawliteral_code then
984                    pdf_end_string_nl()
985                else
986                    report("invalid literal mode %a when flushing %a",mode,str)
987                    return
988                end
989                b = b + 1 ; buffer[b] = str
990            end
991        end
992    end
993
994    flushers.literal = flushliteral
995
996    function lpdf.print(mode,str)
997        -- This only works inside objects, don't change this to flush
998        -- in between. It's different from luatex but okay.
999        if str then
1000            mode = literalvalues[mode]
1001        else
1002            mode, str = originliteral_code, mode
1003        end
1004        if str and str ~= "" then
1005            if mode == originliteral_code then
1006                pdf_goto_pagemode()
1007             -- pdf_set_pos(pdf_h,pdf_v)
1008            elseif mode == pageliteral_code then
1009                pdf_goto_pagemode()
1010            elseif mode == textliteral_code then
1011                pdf_goto_textmode()
1012            elseif mode == fontliteral_code then
1013                pdf_goto_fontmode()
1014            elseif mode == alwaysliteral_code then
1015                pdf_end_string_nl()
1016                need_tm = true
1017            elseif mode == rawliteral_code then
1018                pdf_end_string_nl()
1019            else
1020                report("invalid literal mode %a when flushing %a",mode,str)
1021                return
1022            end
1023            b = b + 1 ; buffer[b] = str
1024        end
1025    end
1026
1027end
1028
1029-- grouping & orientation
1030
1031do
1032
1033    local matrices     = { }
1034    local positions    = { }
1035    local nofpositions = 0
1036    local nofmatrices  = 0
1037
1038    local flushsave = function(current,pos_h,pos_v)
1039        nofpositions = nofpositions + 1
1040        positions[nofpositions] = { pos_h, pos_v, nofmatrices }
1041        pdf_goto_pagemode()
1042        pdf_set_pos(pos_h,pos_v)
1043        b = b + 1 ; buffer[b] = "q"
1044    end
1045
1046    local flushrestore = function(current,pos_h,pos_v)
1047        if nofpositions < 1 then
1048            return
1049        end
1050        local t = positions[nofpositions]
1051     -- local h = pos_h - t[1]
1052     -- local v = pos_v - t[2]
1053        if shippingmode == "page" then
1054            nofmatrices = t[3]
1055        end
1056        pdf_goto_pagemode()
1057        pdf_set_pos(pos_h,pos_v)
1058        b = b + 1 ; buffer[b] = "Q"
1059        nofpositions = nofpositions - 1
1060    end
1061
1062    local nodeproperties = nodes.properties.data
1063
1064    local s_matrix_0 <const> = "1 0 0 1 0 0 cm"
1065    local f_matrix_2         = formatters["%.6N 0 0 %.6N 0 0 cm"]
1066    local f_matrix_4         = formatters["%.6N %.6N %.6N %.6N 0 0 cm"]
1067
1068    local flushsetmatrix = function(current,pos_h,pos_v)
1069        local p = nodeproperties[current]
1070        if p then
1071            local m = p.matrix
1072            if m then
1073                local rx, sx, sy, ry = unpack(m)
1074                local s
1075                if not rx then
1076                    rx = 1
1077                elseif rx == 0 then
1078                    rx = 0.0001
1079                end
1080                if not ry then
1081                    ry = 1
1082                elseif ry == 0 then
1083                    ry = 0.0001
1084                end
1085                if not sx then
1086                    sx = 0
1087                end
1088                if not sy then
1089                    sy = 0
1090                end
1091                --
1092                if sx == 0 and sy == 0 then
1093                    if rx == 1 and ry == 1 then
1094                        s = s_matrix_0
1095                    else
1096                        s = f_matrix_2(rx,ry)
1097                    end
1098                else
1099                    s = f_matrix_4(rx,sx,sy,ry)
1100                end
1101                --
1102                if shippingmode == "page" then
1103                    local tx = pos_h * (1 - rx) - pos_v * sy
1104                    local ty = pos_v * (1 - ry) - pos_h * sx
1105                    if nofmatrices > 0 then
1106                        local t = matrices[nofmatrices]
1107                        local r_x, s_x, s_y, r_y, te, tf = t[1], t[2], t[3], t[4], t[5], t[6]
1108                        rx, sx = rx * r_x + sx * s_y, rx * s_x + sx * r_y
1109                        sy, ry = sy * r_x + ry * s_y, sy * s_x + ry * r_y
1110                        tx, ty = tx * r_x + ty * s_y, tx * s_x + ty * r_y
1111                    end
1112                    nofmatrices = nofmatrices + 1
1113                    matrices[nofmatrices] = { rx, sx, sy, ry, tx, ty }
1114                end
1115                --
1116                pdf_goto_pagemode()
1117                pdf_set_pos(pos_h,pos_v)
1118                --
1119                b = b + 1
1120                buffer[b] = s
1121            end
1122        end
1123    end
1124
1125    flushers.setmatrix = flushsetmatrix
1126    flushers.save      = flushsave
1127    flushers.restore   = flushrestore
1128
1129    function lpdf.hasmatrix()
1130        return nofmatrices > 0
1131    end
1132
1133    function lpdf.getmatrix()
1134        if nofmatrices > 0 then
1135            return unpack(matrices[nofmatrices])
1136        else
1137            return 1, 0, 0, 1, 0, 0
1138        end
1139    end
1140
1141    flushers.pushorientation = function(orientation,pos_h,pos_v,pos_r)
1142        pdf_goto_pagemode()
1143        pdf_set_pos(pos_h,pos_v)
1144        b = b + 1 ; buffer[b] = "q"
1145        if orientation == 1 then
1146            b = b + 1 ; buffer[b] = "0 -1 1 0 0 0 cm"  --  90
1147        elseif orientation == 2 then
1148            b = b + 1 ; buffer[b] = "-1 0 0 -1 0 0 cm" -- 180
1149        elseif orientation == 3 then
1150            b = b + 1 ; buffer[b] = "0 1 -1 0 0 0 cm"  -- 270
1151        end
1152    end
1153
1154    flushers.poporientation = function(orientation,pos_h,pos_v,pos_r)
1155        pdf_goto_pagemode()
1156        pdf_set_pos(pos_h,pos_v)
1157        b = b + 1 ; buffer[b] = "Q"
1158    end
1159
1160    --
1161
1162    flushers.startmatrix = function(current,pos_h,pos_v)
1163        flushsave(current,pos_h,pos_v)
1164        flushsetmatrix(current,pos_h,pos_v)
1165    end
1166
1167    flushers.stopmatrix = function(current,pos_h,pos_v)
1168        flushrestore(current,pos_h,pos_v)
1169    end
1170
1171    flushers.startscaling = function(current,pos_h,pos_v)
1172        flushsave(current,pos_h,pos_v)
1173        flushsetmatrix(current,pos_h,pos_v)
1174    end
1175
1176    flushers.stopscaling = function(current,pos_h,pos_v)
1177        flushrestore(current,pos_h,pos_v)
1178    end
1179
1180    flushers.startrotation = function(current,pos_h,pos_v)
1181        flushsave(current,pos_h,pos_v)
1182        flushsetmatrix(current,pos_h,pos_v)
1183    end
1184
1185    flushers.stoprotation = function(current,pos_h,pos_v)
1186        flushrestore(current,pos_h,pos_v)
1187    end
1188
1189    flushers.startmirroring = function(current,pos_h,pos_v)
1190        flushsave(current,pos_h,pos_v)
1191        flushsetmatrix(current,pos_h,pos_v)
1192    end
1193
1194    flushers.stopmirroring = function(current,pos_h,pos_v)
1195        flushrestore(current,pos_h,pos_v)
1196    end
1197
1198    flushers.startclipping = function(current,pos_h,pos_v)
1199        flushsave(current,pos_h,pos_v)
1200     -- lpdf.print("origin",formatters["0 w %s W n"](nodeproperties[current].path))
1201        pdf_goto_pagemode()
1202        b = b + 1 ; buffer[b] = formatters["0 w %s W n"](nodeproperties[current].path)
1203    end
1204
1205    flushers.stopclipping = function(current,pos_h,pos_v)
1206        flushrestore(current,pos_h,pos_v)
1207    end
1208
1209end
1210
1211do
1212
1213    local nodeproperties = nodes.properties.data
1214
1215    flushers.setstate = function(current,pos_h,pos_v)
1216        local p = nodeproperties[current]
1217        if p then
1218            local d = p.data
1219            if d and d ~= "" then
1220                pdf_goto_pagemode()
1221                b = b + 1 ; buffer[b] = d
1222            end
1223        end
1224    end
1225
1226end
1227
1228-- rules
1229
1230local flushedxforms  = { } -- actually box resources but can also be direct
1231local localconverter = nil -- will be set
1232
1233do
1234
1235    local tonut             = nodes.tonut
1236    local tonode            = nuts.tonode
1237
1238    local pdfbackend        = backends.registered.pdf
1239    local nodeinjections    = pdfbackend.nodeinjections
1240    local codeinjections    = pdfbackend.codeinjections
1241
1242    local newimagerule      = nuts.pool.imagerule
1243    local newboxrule        = nuts.pool.boxrule
1244
1245    local setprop           = nuts.setprop
1246    local getprop           = nuts.getprop
1247    local setattrlist       = nuts.setattrlist
1248    local getoptions        = nuts.getoptions
1249
1250    local getwhd            = nuts.getwhd
1251    local flushlist         = nuts.flushlist
1252    local getdata           = nuts.getdata
1253
1254    local rulecodes         = nodes.rulecodes
1255    local normalrule_code   <const> = rulecodes.normal
1256    local boxrule_code      <const> = rulecodes.box
1257    local imagerule_code    <const> = rulecodes.image
1258    local emptyrule_code    <const> = rulecodes.empty
1259    local userrule_code     <const> = rulecodes.user
1260    local overrule_code     <const> = rulecodes.over
1261    local underrule_code    <const> = rulecodes.under
1262    local fractionrule_code <const> = rulecodes.fraction
1263    local radicalrule_code  <const> = rulecodes.radical
1264    local outlinerule_code  <const> = rulecodes.outline
1265    ----- virtualrule_code  <const> = rulecodes.virtual
1266
1267    local vertical_rule     <const> = tex.ruleoptioncodes.vertical
1268    local horizontal_rule   <const> = tex.ruleoptioncodes.horizontal
1269
1270    local processrule       = nodes.rules.process
1271
1272    local f_fm = formatters["/Fm%d Do"]
1273    local f_im = formatters["/Im%d Do"]
1274    local f_gr = formatters["/Gp%d Do"]
1275    local f_st = formatters["/Gs%d gs"]
1276
1277    local s_b <const> = "q"
1278    local s_e <const> = "Q"
1279
1280    local f_v   = formatters["[] 0 d 0 J %.6N w 0 0 m %.6N 0 l S"]
1281    local f_h   = formatters["[] 0 d 0 J %.6N w 0 0 m 0 %.6N l S"]
1282
1283    local f_f   = formatters["0 0 %.6N %.6N re f"]
1284    local f_o   = formatters["[] 0 d 0 J 0 0 %.6N %.6N re S"]
1285    local f_w   = formatters["[] 0 d 0 J %.6N w 0 0 %.6N %.6N re S"]
1286
1287    local f_b   = formatters["%.6N w 0 %.6N %.6N %.6N re f"]
1288    local f_x   = formatters["[] 0 d 0 J %.6N w %.6N %.6N %.6N %.6N re S"]
1289    local f_y   = formatters["[] 0 d 0 J %.6N w %.6N %.6N %.6N %.6N re S %.6N 0 m %.6N 0 l S"]
1290
1291 -- local f_v_d = formatters["[%.6N %.6N] 0 d 0 J %.6N w 0 0 m %.6N 0 l S"]
1292 -- local f_h_d = formatters["[%.6N %.6N] 0 d 0 J %.6N w 0 0 m 0 %.6N l S"]
1293
1294    local f_d_h = formatters["[%.6N %.6N] 0 d 0 J %.6N w 0 %.6N m %.6N %.6N l S"]
1295    local f_d_v = formatters["[%.6N %.6N] 0 d 0 J %.6N w %.6N 0 m %.6N %.6N l S"]
1296
1297    -- Historically the index is an object which is kind of bad.
1298
1299    local boxresources, n = { }, 0
1300
1301    getxformname = function(index)
1302        local l = boxresources[index]
1303        if l then
1304            return l.name
1305        else
1306            report("no box resource %S",index)
1307        end
1308    end
1309
1310    lpdf.getxformname = getxformname
1311
1312    local pdfcollectedresources = lpdf.collectedresources
1313
1314    function codeinjections.saveboxresource(box,attributes,resources,immediate,kind,margin,onum)
1315        n = n + 1
1316        local immediate = true
1317        local margin    = margin or 0 -- or dimension
1318        local objnum    = onum or pdfreserveobject()
1319        local list      = tonut(type(box) == "number" and tex.takebox(box) or box)
1320        --
1321        if resources == true then
1322            resources = pdfcollectedresources {
1323                serialize = false,
1324            }
1325        end
1326        --
1327        local width, height, depth = getwhd(list)
1328        --
1329        local l = {
1330            width      = width,
1331            height     = height,
1332            depth      = depth,
1333            margin     = margin,
1334            attributes = attributes,
1335            resources  = resources,
1336            list       = nil,
1337            type       = kind,
1338            name       = n,
1339            index      = objnum,
1340            objnum     = objnum,
1341        }
1342        local r = boxresources[objnum]
1343        if r then
1344            flushlist(l.list)
1345            l.list = nil -- added
1346        end
1347        boxresources[objnum] = l
1348        if immediate then
1349            localconverter(list,"xform",objnum,l)
1350            flushedxforms[objnum] = { true , objnum }
1351            flushlist(list)
1352        else
1353            l.list = list
1354        end
1355        return objnum
1356    end
1357
1358    function nodeinjections.useboxresource(index,wd,ht,dp)
1359        local l = boxresources[index]
1360        if l then
1361            if wd or ht or dp then
1362                wd, ht, dp = wd or 0, ht or 0, dp or 0
1363            else
1364                wd, ht, dp = l.width, l.height, l.depth
1365            end
1366            local rule = newboxrule(wd,ht,dp)
1367            setattrlist(rule,true)
1368            setprop(rule,"index",index)
1369            return tonode(rule), wd, ht, dp
1370        else
1371            report("no box resource %S",index)
1372        end
1373    end
1374
1375    local function getboxresourcedimensions(index)
1376        local l = boxresources[index]
1377        if l then
1378            return l.width, l.height, l.depth, l.margin
1379        else
1380            report("no box resource %S",index)
1381        end
1382    end
1383
1384    nodeinjections.getboxresourcedimensions = getboxresourcedimensions
1385
1386    function codeinjections.getboxresourcebox(index)
1387        local l = boxresources[index]
1388        if l then
1389            return l.list
1390        end
1391    end
1392
1393    -- a bit of a mess: index is now objnum but that has to change to a proper index
1394    -- ... an engine inheritance
1395
1396    local function flushpdfxform(current,pos_h,pos_v,pos_r,size_h,size_v)
1397        -- object properties
1398        local objnum = getprop(current,"index")
1399        local name   = getxformname(objnum)
1400        local info   = flushedxforms[objnum]
1401        local r      = boxresources[objnum]
1402        if not info then
1403            info = { false , objnum }
1404            flushedxforms[objnum] = info
1405        end
1406        local wd, ht, dp = getboxresourcedimensions(objnum)
1407     -- or:   wd, ht, dp = r.width, r.height, r.depth
1408        -- sanity check
1409        local htdp = ht + dp
1410        if wd == 0 or size_h == 0 or htdp == 0 or size_v == 0 then
1411            return
1412        end
1413        -- calculate scale
1414        local rx, ry = 1, 1
1415        if wd ~= size_h or htdp ~= size_v then
1416            rx = size_h / wd
1417            ry = size_v / htdp
1418        end
1419        -- flush the reference
1420        usedxforms[objnum] = true
1421        pdf_goto_pagemode()
1422        calc_pdfpos(pos_h,pos_v)
1423        local tx = cmtx * bpfactor
1424        local ty = cmty * bpfactor
1425        b = b + 1 ; buffer[b] = s_b
1426     -- b = b + 1 ; buffer[b] = f_cm(rx,0,0,ry,tx,ty)
1427        b = b + 1 ; buffer[b] = f_cz(rx,    ry,tx,ty)
1428        b = b + 1 ; buffer[b] = f_fm(name)
1429        b = b + 1 ; buffer[b] = s_e
1430    end
1431
1432    -- place image also used in vf but we can use a different one if we need it
1433
1434    local imagetypes     = images.types -- pdf png jpg jp2 jbig2 stream
1435    local img_none       = imagetypes.none
1436    local img_pdf        = imagetypes.pdf
1437    local img_stream     = imagetypes.stream
1438
1439    local one_bp <const> = 65536 * bpfactor
1440
1441    local imageresources, n = { }, 0
1442
1443    getximagename = function(index) -- not used
1444        local l = imageresources[index]
1445        if l then
1446            return l.name
1447        else
1448            report("no image resource %S",index)
1449        end
1450    end
1451
1452    -- Groups are flushed immediately but we can decide to make them into a
1453    -- specific whatsit ... but not now. We could hash them if needed when
1454    -- we use lot sof them in mp ... but not now.
1455
1456    usedxgroups = { }
1457
1458    local flushgroup do
1459
1460        local groups = 0
1461        local group  = nil
1462        local states = 0
1463        local names  = { }
1464
1465        flushgroup = function(content,bbox,detail)
1466            if not group then
1467                group = pdfdictionary {
1468                    Type = pdfconstant("Group"),
1469                    S    = pdfconstant("Transparency"),
1470                }
1471            end
1472            local wrapper = pdfdictionary {
1473                Type      = pdf_xobject,
1474                Subtype   = pdf_form,
1475                FormType  = 1,
1476                Group     = group,
1477                BBox      = pdfarray(bbox),
1478                Resources = lpdf.collectedresources {
1479                    serialize = false
1480                }, -- what if empty
1481            }
1482            local objnum = pdfflushstreamobject(content,wrapper,false) -- why not compressed ?
1483            groups = groups + 1
1484            usedxgroups[groups] = objnum
1485            local kind = detail and detail.type == "luminosity"
1486            local result = f_gr(groups)
1487            if not detail then
1488                -- nothing special
1489            elseif detail.type == "luminosity" then
1490                local action = detail.action
1491                if action == "register" then
1492                    local state = pdfdictionary {
1493                        Type  = pdfconstant("ExtGState"),
1494                        AIS   = false,
1495                        CA    = 1,
1496                        ca    = 1,
1497                        SMask = pdfdictionary {
1498                            G    = pdfreference(objnum),
1499                            S    = pdfconstant("Luminosity"),
1500                            Type = pdfconstant("Mask"),
1501                        }
1502                    }
1503                    states = states + 1
1504                    lpdf.adddocumentextgstate("Gs" .. states,pdfreference(pdfflushobject(state)))
1505                    names[detail.name or "default"] = f_st(states)
1506                    return
1507                elseif action == "apply" then
1508                    local name  = detail.name or "default"
1509                    local delay = names[name]
1510                    if delay then
1511                        result = delay .. " " .. result
1512                        names[name] = nil
1513                    end
1514                else
1515                    -- error
1516                end
1517            end
1518            return result
1519        end
1520
1521    end
1522
1523    flushers.group  = flushgroup
1524    lpdf.flushgroup = flushgroup -- todo: access via driver in mlib-pps
1525
1526    -- end of experiment
1527
1528    local function flushpdfximage(current,pos_h,pos_v,pos_r,size_h,size_v)
1529
1530        local width,
1531              height,
1532              depth     = getwhd(current)
1533        local total     = height + depth
1534        local transform = getprop(current,"transform") or 0  -- we never set it ... so just use rotation then
1535        local index     = getprop(current,"index") or 0
1536        local kind,
1537              xorigin,
1538              yorigin,
1539              xsize,
1540              ysize,
1541              rotation, -- transform / orientation / rotation : it's a mess (i need to redo this)
1542              objnum,
1543              groupref  = pdfincludeimage(index)  -- needs to be sorted out, bad name (no longer mixed anyway)
1544
1545        if not kind then
1546            report("invalid image %S",index)
1547            return
1548        end
1549
1550        local rx, sx, sy, ry, tx, ty = 1, 0, 0, 1, 0, 0
1551
1552        -- tricky: xsize and ysize swapped
1553
1554        if kind == img_pdf or kind == img_stream then
1555            rx, ry, tx, ty = 1/xsize, 1/ysize, xorigin/xsize, yorigin/ysize
1556        else
1557         -- if kind == img_png then
1558         --  -- if groupref > 0 and img_page_group_val == 0 then
1559         --  --     img_page_group_val = groupref
1560         --  -- end
1561         -- end
1562            rx, ry = bpfactor, bpfactor
1563        end
1564
1565        if (transform & 7) > 3 then
1566            -- mirror
1567            rx, tx = -rx, -tx
1568        end
1569        local t = (transform + rotation) & 3
1570        if t == 0 then
1571            -- nothing
1572        elseif t == 1 then
1573            -- rotation over 90 degrees (counterclockwise)
1574            rx, sx, sy, ry, tx, ty = 0, rx, -ry, 0, -ty, tx
1575        elseif t == 2 then
1576            -- rotation over 180 degrees (counterclockwise)
1577            rx, ry, tx, ty = -rx, -ry, -tx, -ty
1578        elseif t == 3 then
1579            -- rotation over 270 degrees (counterclockwise)
1580            rx, sx, sy, ry, tx, ty = 0, -rx, ry, 0, ty, -tx
1581        end
1582
1583        rx = rx * width
1584        sx = sx * total
1585        sy = sy * width
1586        ry = ry * total
1587        tx = pos_h - tx * width
1588        ty = pos_v - ty * total
1589
1590        local t = transform + rotation
1591
1592        if (transform & 7) > 3 then
1593            t = t + 1
1594        end
1595
1596        t = t & 3
1597
1598        if t == 0 then
1599            -- no transform
1600        elseif t == 1 then
1601            -- rotation over 90 degrees (counterclockwise)
1602            tx = tx + width
1603        elseif t == 2 then
1604            -- rotation over 180 degrees (counterclockwise)
1605            tx = tx + width
1606            ty = ty + total
1607        elseif t == 3 then
1608            -- rotation over 270 degrees (counterclockwise)
1609            ty = ty + total
1610        end
1611
1612     -- a flaw in original, can go:
1613     --
1614     -- if img_page_group_val == 0 then
1615     --     img_page_group_val = group_ref
1616     -- end
1617
1618        usedximages[index] = objnum -- hm
1619
1620        pdf_goto_pagemode()
1621
1622        calc_pdfpos(tx,ty)
1623
1624        tx = cmtx * bpfactor
1625        ty = cmty * bpfactor
1626
1627        b = b + 1 ; buffer[b] = s_b
1628        b = b + 1 ; buffer[b] = f_cm(rx,sx,sy,ry,tx,ty)
1629        b = b + 1 ; buffer[b] = f_im(index)
1630        b = b + 1 ; buffer[b] = s_e
1631    end
1632
1633    flushers.flushimage = function(index,width,height,depth,pos_h,pos_v)
1634
1635        -- used in vf characters
1636
1637        local total = height + depth
1638        local kind,
1639              xorigin, yorigin,
1640              xsize, ysize,
1641              rotation,
1642              objnum,
1643              groupref = pdfincludeimage(index)
1644
1645        local rx = width / xsize
1646        local sx = 0
1647        local sy = 0
1648        local ry = total / ysize
1649        local tx = pos_h
1650        -- to be sorted out
1651     -- local ty = pos_v - depth
1652        local ty = pos_v -- we assume that depth is dealt with in the caller (for now)
1653        usedximages[index] = objnum
1654
1655        pdf_goto_pagemode()
1656
1657        calc_pdfpos(tx,ty)
1658
1659        tx = cmtx * bpfactor
1660        ty = cmty * bpfactor
1661
1662        b = b + 1 ; buffer[b] = s_b
1663        b = b + 1 ; buffer[b] = f_cm(rx,sx,sy,ry,tx,ty)
1664        b = b + 1 ; buffer[b] = f_im(index)
1665        b = b + 1 ; buffer[b] = s_e
1666    end
1667
1668    -- For the moment we need this hack because the engine checks the 'image'
1669    -- command in virtual fonts (so we use lua instead).
1670    --
1671    -- These will be replaced by a new more advanced one ... some day ... or
1672    -- never because the next are like the other engines and compensate for
1673    -- small sizes which is needed for inaccurate viewers.
1674
1675    local function onoff(on,off,dim)
1676        on  = on  * bpfactor
1677        off = off * bpfactor
1678        if on + off > dim then
1679            return dim, 0
1680        else
1681            local n = ((dim -on) // (on + off)) + 1
1682            if n > 1 then
1683                off = (dim - n * on)/(n - 1)
1684            end
1685            return on, off
1686        end
1687    end
1688
1689    flushers.rule = function(current,pos_h,pos_v,pos_r,size_h,size_v,subtype,on,off,running)
1690        if subtype == emptyrule_code then
1691            return
1692        elseif subtype == boxrule_code then
1693            return flushpdfxform(current,pos_h,pos_v,pos_r,size_h,size_v)
1694        elseif subtype == imagerule_code then
1695            return flushpdfximage(current,pos_h,pos_v,pos_r,size_h,size_v)
1696        elseif subtype == userrule_code or (subtype >= overrule_code and subtype <= radicalrule_code) then
1697            pdf_goto_pagemode()
1698            b = b + 1 ; buffer[b] = s_b
1699            pdf_set_pos_temp(pos_h,pos_v)
1700            processrule(current,size_h,size_v,pos_r) -- so we pass direction
1701            b = b + 1 ; buffer[b] = s_e
1702            return
1703        end
1704
1705        pdf_goto_pagemode()
1706
1707        b = b + 1 ; buffer[b] = s_b
1708
1709        local dim_h = size_h * bpfactor
1710        local dim_v = size_v * bpfactor
1711        local rule
1712        --
1713        -- this fails for showglyphs so and i have no reason to look into it now and rectangles
1714        -- do a better job anyway
1715        --
1716        if subtype == outlinerule_code then
1717            local linewidth = getdata(current)
1718            pdf_set_pos_temp(pos_h,pos_v)
1719            if linewidth > 0 then
1720                rule = f_w(linewidth * bpfactor,dim_h,dim_v)
1721            else
1722                rule = f_o(dim_h,dim_v)
1723            end
1724        elseif on ~= 0 and off ~= 0 then -- and subtype ~= strutrule_code
1725            local options    = getoptions(current)
1726            local horizontal = (options & horizontal_rule) ~= 0
1727            local vertical   = (options & vertical_rule  ) ~= 0
1728            if not horizontal and not vertical then
1729                horizontal = true -- and no check for running, see autorule in tabulate
1730            elseif not running then
1731                horizontal = not horizontal
1732            end
1733            if horizontal then
1734                local offset = dim_v/2
1735                on, off = onoff(on,off,dim_h)
1736                pdf_set_pos_temp(pos_h,pos_v)
1737                rule = f_d_h(on,off,dim_v,offset,dim_h,offset)
1738            else
1739                local offset = dim_h/2
1740                on, off = onoff(on,off,dim_v)
1741                pdf_set_pos_temp(pos_h,pos_v)
1742                rule = f_d_v(on,off,dim_h,offset,offset,dim_v)
1743            end
1744        elseif dim_v <= one_bp then
1745            pdf_set_pos_temp(pos_h,pos_v + 0.5 * size_v)
1746            rule = f_v(dim_v,dim_h)
1747        elseif dim_h <= one_bp then
1748            pdf_set_pos_temp(pos_h + 0.5 * size_h,pos_v)
1749            rule = f_h(dim_h,dim_v)
1750        else
1751            pdf_set_pos_temp(pos_h,pos_v)
1752            rule = f_f(dim_h,dim_v)
1753        end
1754
1755        b = b + 1 ; buffer[b] = rule
1756        b = b + 1 ; buffer[b] = s_e
1757
1758    end
1759
1760    flushers.simplerule = function(pos_h,pos_v,pos_r,size_h,size_v)
1761        pdf_goto_pagemode()
1762
1763        b = b + 1 ; buffer[b] = s_b
1764
1765        local dim_h = size_h * bpfactor
1766        local dim_v = size_v * bpfactor
1767        local rule
1768
1769        if dim_v <= one_bp then
1770            pdf_set_pos_temp(pos_h,pos_v + 0.5 * size_v)
1771            rule = f_v(dim_v,dim_h)
1772        elseif dim_h <= one_bp then
1773            pdf_set_pos_temp(pos_h + 0.5 * size_h,pos_v)
1774            rule = f_h(dim_h,dim_v)
1775        else
1776            pdf_set_pos_temp(pos_h,pos_v)
1777            rule = f_f(dim_h,dim_v)
1778        end
1779
1780        b = b + 1 ; buffer[b] = rule
1781        b = b + 1 ; buffer[b] = s_e
1782    end
1783
1784    flushers.specialrule = function(pos_h,pos_v,pos_r,width,height,depth,line,outline,baseline)
1785        pdf_goto_pagemode()
1786
1787        b = b + 1 ; buffer[b] = s_b
1788
1789        local width  = bpfactor * width
1790        local height = bpfactor * height
1791        local depth  = bpfactor * depth
1792        local total  = height + depth
1793        local line   = bpfactor * line
1794        local half   = line / 2
1795        local rule
1796
1797        if outline then
1798            local d = -depth + half
1799            local w =  width - line
1800            local t =  total - line
1801            if baseline and w > 0 then
1802                rule = f_y(line,half,d,w,t,half,w)
1803            else
1804                rule = f_x(line,half,d,w,t)
1805            end
1806        else
1807            rule = f_b(line,-depth,width,total)
1808        end
1809        pdf_set_pos_temp(pos_h,pos_v)
1810
1811        b = b + 1 ; buffer[b] = rule
1812        b = b + 1 ; buffer[b] = s_e
1813    end
1814
1815end
1816
1817--- basics
1818
1819local wrapupdocument, registerpage  do
1820
1821    local pages    = { }
1822    local maxkids  = 10
1823    local nofpages = 0
1824    local pagetag  = "unset"
1825
1826    registerpage = function(object)
1827        nofpages = nofpages + 1
1828        local objnum = pdfpagereference(nofpages)
1829        pages[nofpages] = {
1830            page   = nofpages, -- original number, only for diagnostics
1831            objnum = objnum,
1832            object = object,
1833            tag    = pagetag,
1834        }
1835    end
1836
1837    function lpdf.setpagetag(tag)
1838        pagetag = tag or "unset"
1839    end
1840
1841    function lpdf.getnofpages()
1842        return nofpages
1843    end
1844
1845    function lpdf.getpagetags()
1846        local list = { }
1847        for i=1,nofpages do
1848            list[i] = pages[i].tag
1849        end
1850        return list
1851    end
1852
1853    function lpdf.setpageorder(mapping,p)
1854        -- mapping can be a hash so:
1855        local list = table.sortedkeys(mapping)
1856        local n    = #list
1857        local nop  = p or nofpages
1858        if n == nop then
1859            local done = { }
1860            local hash = { }
1861            for i=1,n do
1862                local order = mapping[list[i]]
1863                if hash[order] then
1864                    report("invalid page order, duplicate entry %i",order)
1865                    return
1866                elseif order < 1 or order > nofpages then
1867                    report("invalid page order, no page %i",order)
1868                    return
1869                else
1870                    done[i]     = pages[order]
1871                    hash[order] = true
1872                end
1873            end
1874            pages = done
1875        else
1876            report("invalid page order, %i entries expected",nop)
1877        end
1878    end
1879
1880    -- We can have this, but then via codeinjections etc. Later.
1881
1882 -- function structures.pages.swapthem()
1883 --     local n = lpdf.getnofpages()
1884 --     local t = { }
1885 --     for i=1,n do
1886 --         t[i] = i
1887 --     end
1888 --     for i=2,math.odd(n) and n or (n-1),2 do
1889 --         t[i]   = i+1
1890 --         t[i+1] = i
1891 --     end
1892 --     lpdf.setpageorder(t)
1893 -- end
1894
1895    wrapupdocument = function(driver)
1896
1897        -- hook (to reshuffle pages)
1898        local pagetree = { }
1899        local parent   = nil
1900        local minimum  = 0
1901        local maximum  = 0
1902        local current  = 0
1903        if #pages > 1.5 * maxkids then
1904            repeat
1905                local plist, pnode
1906                if current == 0 then
1907                    plist, minimum = pages, 1
1908                elseif current == 1 then
1909                    plist, minimum = pagetree, 1
1910                else
1911                    plist, minimum = pagetree, maximum + 1
1912                end
1913                maximum = #plist
1914                if maximum > minimum then
1915                    local kids
1916                    for i=minimum,maximum do
1917                        local p = plist[i]
1918                        if not pnode or #kids == maxkids then
1919                            kids   = pdfarray()
1920                            parent = pdfreserveobject()
1921                            pnode  = pdfdictionary {
1922                                objnum = parent,
1923                                Type   = pdf_pages,
1924                                Kids   = kids,
1925                                Count  = 0,
1926                            }
1927                            pagetree[#pagetree+1] = pnode
1928                        end
1929                        kids[#kids+1] = pdfreference(p.objnum)
1930                        pnode.Count = pnode.Count + (p.Count or 1)
1931                        p.Parent = pdfreference(parent)
1932                    end
1933                end
1934                current = current + 1
1935            until maximum == minimum
1936            -- flush page tree
1937            for i=1,#pagetree do
1938                local entry  = pagetree[i]
1939                local objnum = entry.objnum
1940                entry.objnum = nil
1941                pdfflushobject(objnum,entry)
1942            end
1943        else
1944            -- ugly
1945            local kids = pdfarray()
1946            local list = pdfdictionary {
1947                Type  = pdf_pages,
1948                Kids  = kids,
1949                Count = nofpages,
1950            }
1951            parent = pdfreserveobject()
1952            for i=1,nofpages do
1953                local page = pages[i]
1954                kids[#kids+1] = pdfreference(page.objnum)
1955                page.Parent = pdfreference(parent)
1956            end
1957            pdfflushobject(parent,list)
1958        end
1959        for i=1,nofpages do
1960            local page   = pages[i]
1961            local object = page.object
1962            object.Parent = page.Parent
1963            pdfflushobject(page.objnum,object)
1964        end
1965        lpdf.addtocatalog("Pages",pdfreference(parent))
1966    end
1967
1968end
1969
1970local function initialize(driver,details)
1971    reset_variables(details)
1972    reset_buffer()
1973end
1974
1975-- This will all move and be merged and become less messy.
1976
1977-- todo: more clever resource management: a bit tricky as we can inject
1978-- stuff in the page stream
1979
1980local compact       = false
1981local encryptstream = false
1982local encryptobject = false
1983local encdict       = nil
1984local majorversion  = 1
1985local minorversion  = 7
1986
1987-- Encryption
1988
1989-- This stuff is poorly documented so it took a while to figure out a way that made
1990-- loading in a few programe working. Of course one you see the solution one can
1991-- claim that it's easy and trivial. In the end we could even make acrobat accepting
1992-- the file: it doesn't like the catalog to be in an object stream which to me
1993-- smells like a bug.
1994
1995do
1996
1997    -- move up (some already) or better: lpdf-aes.lmt or so
1998
1999    local byte, sub, bytes, tohex, tobytes = string.byte, string.sub, string.bytes, string.tohex, string.tobytes
2000    local P, S, V, Cs, lpegmatch, patterns = lpeg.P, lpeg.S, lpeg.V, lpeg.Cs, lpeg.match, lpeg.patterns
2001
2002    local digest256 = sha2.digest256
2003    local digest384 = sha2.digest384
2004    local digest512 = sha2.digest512
2005
2006    local aesencode = aes.encode
2007    local aesdecode = aes.decode
2008    local aesrandom = aes.random
2009
2010    -- random and padding functions are gone here
2011
2012    local function validpassword(str)
2013        return #str > 127 and sub(str,1,127) or str
2014    end
2015
2016    local encryptionkey = false
2017    local objectparser  = false
2018
2019    do
2020
2021        local function ps_encrypt(str)
2022            -- string is already unescaped
2023            str = aesencode(str,encryptionkey,true,true,true)
2024            return "<" .. tohex(str) .. ">"
2025        end
2026
2027        local function hex_encrypt(str)
2028            -- string needs to be decoded
2029            str = tobytes(str)
2030            str = aesencode(str,encryptionkey,true,true,true)
2031            return "<" .. tohex(str) .. ">"
2032        end
2033
2034        local whitespace  = S("\000\009\010\012\013\032")^1
2035        local anything    = patterns.anything
2036        local space       = patterns.space
2037        local spacing     = whitespace^0
2038        local newline     = patterns.eol
2039        local cardinal    = patterns.cardinal
2040
2041        local p_psstring  = (
2042                              P("(")
2043                            * Cs(P { ( P("\\")/"" * anything + P("(") * V(1) * P(")") + (1 - P(")")) )^0 })
2044                            * P(")")
2045                          ) / ps_encrypt
2046
2047        local p_hexstring = (
2048                              P("<")
2049                            * Cs((1-P(">"))^1)
2050                            * P(">")
2051                          ) / hex_encrypt
2052
2053        local p_comment   = P("%") * (1-newline)^1 * newline^1
2054        local p_name      = P("/") * (1 - whitespace - S("<>/[]()"))^1
2055        local p_number    = patterns.number
2056        local p_boolean   = P("true") + P("false")
2057        local p_null      = P("null")
2058        local p_reference = cardinal * spacing * cardinal * spacing * P("R")
2059
2060        local p_other     = p_name + p_reference + p_psstring + p_hexstring + p_number
2061                          + p_boolean + p_null + p_comment
2062
2063        local p_dictionary  = { "dictionary",
2064            dictionary = (
2065                P("<<")
2066              * (spacing * p_name * spacing * V("whatever"))^0
2067              * spacing
2068              * P(">>")
2069            ),
2070            array = (
2071                P("[")
2072              * (spacing * V("whatever"))^0
2073              * spacing
2074              * P("]")
2075            ),
2076            whatever = (
2077                V("dictionary")
2078              + V("array")
2079              + p_other
2080            ),
2081        }
2082
2083        local p_object = P { "object",
2084            dictionary = p_dictionary.dictionary,
2085            array      = p_dictionary.array,
2086            whatever   = p_dictionary.whatever,
2087            object     = spacing * (V("dictionary") + V("array") + p_other)
2088        }
2089
2090     -- local p_object = cardinal
2091     --                * spacing
2092     --                * cardinal
2093     --                * spacing
2094     --                * P("obj")
2095     --                * p_object
2096     --                * P(1)^0
2097     --
2098     -- objectparser = Cs(p_object^1)
2099
2100        objectparser = Cs(p_object^1)
2101
2102    end
2103
2104    local function makehash(password,salt,userkey)
2105        local k = digest256(password .. salt .. (userkey or ""))
2106        local n = 0
2107        while true do
2108            local k1 = rep(password .. k .. (userkey or ""),64)
2109            local k2 = sub(k,1,16)
2110            local iv = sub(k,17,32)
2111            local e = aesencode(k1,k2,iv)
2112            local m = 0
2113            local i = 1
2114            for b in bytes(e) do
2115                m = m + b
2116                if i == 16 then
2117                    break
2118                else
2119                    i = i + 1
2120                end
2121            end
2122            m = m % 3
2123            if m == 0 then
2124                k = digest256(e)
2125            elseif m == 1 then
2126                k = digest384(e)
2127            else
2128                k = digest512(e)
2129            end
2130            n = n + 1
2131            if n >= 64 and byte(sub(e,-1)) <= (n - 32) then
2132                break
2133           end
2134        end
2135        return sub(k,1,32)
2136    end
2137
2138    local options = lpdf.permissions -- or { }
2139
2140    -- 1111 0000 1100 0011
2141
2142    local mandate  = 0x0200
2143    local defaults = options.print | options.extract | options.quality
2144
2145    -- majorversion = 2
2146    -- minorversion = 0
2147
2148    function lpdf.setencryption(specification)
2149        if not encryptstream then
2150            local ownerpassword = specification.ownerpassword
2151            local userpassword  = specification.userpassword
2152            local optionlist    = specification.permissions
2153            if type(ownerpassword) == "string" and ownerpassword ~= "" then
2154                --
2155                if type(userpassword) ~= "string" then
2156                    userpassword = ""
2157                end
2158                userpassword  = validpassword(userpassword)
2159                ownerpassword = validpassword(ownerpassword)
2160                --
2161                encryptionkey = aesrandom(32) -- used earlier on
2162                --
2163                local permissions = mandate
2164                if optionlist then
2165                    optionlist = utilities.parsers.settings_to_array(optionlist)
2166                    for i=1,#optionlist do
2167                        local p = options[optionlist[i]]
2168                        if p then
2169                            permissions = permissions | p
2170                        end
2171                    end
2172                else
2173                    permissions = permissions | defaults
2174                end
2175                --
2176                permissions = permissions | 0xF0C3 -- needs work
2177                --
2178                optionlist = { }
2179                for k, v in sortedhash(options) do
2180                    if permissions & v == v then
2181                        optionlist[#optionlist+1] = k
2182                    end
2183                end
2184                --
2185                local uservalidationsalt  = aesrandom(8)
2186                local userkeysalt         = aesrandom(8)
2187                local userhash            = makehash(userpassword,uservalidationsalt)
2188                local userkey             = userhash .. uservalidationsalt .. userkeysalt -- U
2189                local userintermediate    = makehash(userpassword,userkeysalt)
2190                local useraes             = aesencode(encryptionkey,userintermediate) -- UE
2191                --
2192                local ownervalidationsalt = aesrandom(8)
2193                local ownerkeysalt        = aesrandom(8)
2194                local ownerhash           = makehash(ownerpassword,ownervalidationsalt,userkey)
2195                local ownerkey            = ownerhash .. ownervalidationsalt .. ownerkeysalt -- O
2196                local ownerintermediate   = makehash(ownerpassword,ownerkeysalt,userkey)
2197                local owneraes            = aesencode(encryptionkey,ownerintermediate) -- OE
2198                --
2199                -- still not ok test in qpdf
2200                --
2201                local permissionsstring   = sio.tocardinal4(0xFFFFFFFF)
2202                                         .. sio.tocardinal4(permissions)
2203                                         .. "T" -- EncryptMetadata
2204                                         .. "adb"
2205                                         .. aesrandom(4)
2206                local permissionsaes      = aesencode(permissionsstring,encryptionkey)
2207                --
2208                permissionsaes = tohex(permissionsaes)
2209                userkey        = tohex(userkey)
2210                ownerkey       = tohex(ownerkey)
2211                useraes        = tohex(useraes)
2212                owneraes       = tohex(owneraes)
2213                --
2214                encdict  = pdfdictionary {
2215                    Filter = pdfconstant("Standard"),
2216                    V      = 5,   -- variant
2217                    R      = 6,   -- revision
2218                    Length = 256, -- not needed
2219                    StmF   = pdfconstant("StdCF"),
2220                    StrF   = pdfconstant("StdCF"),
2221                    P      = permissions,
2222                    Perms  = pdfliteral(permissionsaes,true), -- #16
2223                    U      = pdfliteral(userkey,       true), -- #48
2224                    O      = pdfliteral(ownerkey,      true), -- #48
2225                    UE     = pdfliteral(useraes,       true), -- #32
2226                    OE     = pdfliteral(owneraes,      true), -- #32
2227                    CF     = {
2228                        StdCF = {
2229                            AuthEvent = pdfconstant("DocOpen"),
2230                            CFM       = pdfconstant("AESV3"),
2231                            Length    = 32, -- #encryptionkey
2232                        }
2233                    },
2234                    -- bonus
2235                    EncryptMetadata = true,
2236                }
2237                --
2238                encryptstream = function(str)
2239                    return aesencode(str,encryptionkey,true,true,true) -- random-iv add-iv add-padding
2240                end
2241                encryptobject = function(obj)
2242                    if obj then
2243                        if type(obj) == "table" then
2244                            -- We could just loop over the table and encrypt strings
2245                            -- before we serialize but we gain little and we can get
2246                            -- already serialized objects anyway (mostly anyway).
2247                            obj = obj()
2248                        end
2249                        return lpegmatch(objectparser,obj) or obj
2250                    end
2251                end
2252                --
2253                report_encryption("stream objects get encrypted")
2254                if not objectstream then
2255                    report_encryption("strings are not encrypted, enable object streams")
2256                end
2257                report_encryption("permissions: % t",optionlist)
2258                if userpassword == "" then
2259                    report_encryption("no user password")
2260                end
2261                --
2262            end
2263        end
2264    end
2265
2266    backends.registered.pdf.codeinjections.setencryption = lpdf.setencryption
2267
2268end
2269
2270do
2271
2272    -- This is more a convenience feature and it might even be not entirely robust.
2273    -- It removes redundant color directives which makes the page stream look a bit
2274    -- nicer (also when figuring out issues). I might add more here but there is
2275    -- some additional overhead involved so runtime can be impacted.
2276
2277    local P, R, S, Cs, lpegmatch = lpeg.P, lpeg.R, lpeg.S, lpeg.Cs, lpeg.match
2278
2279    local p_ds    = (R("09") + S(" ."))^1
2280    ----- p_nl    = S("\n\r")^1
2281    local p_nl    = S("\n")^1
2282    local p_eg    = P("Q")
2283
2284    local p_cl    = p_ds * (P("rg") + P("g") + P("k")) * p_ds * (P("RG") + P("G") + P("K"))
2285    ----- p_cl    = (p_ds * (P("rg") + P("g") + P("k") + P("RG") + P("G") + P("K")))^1
2286    local p_tr    = P("/Tr") * p_ds * P("gs")
2287
2288    local p_no_cl = (p_cl * p_nl) / ""
2289    local p_no_tr = (p_tr * p_nl) / ""
2290    local p_no_nl = 1 - p_nl
2291
2292    local p_do_cl = p_cl * p_nl
2293    local p_do_tr = p_tr * p_nl
2294
2295    local p_do_eg = p_eg * p_nl
2296
2297    local pattern = Cs( (
2298        (p_no_cl + p_no_tr)^0 * p_do_eg           -- transparencies and colors before Q
2299      +  p_no_tr * p_no_cl    * p_do_tr * p_do_cl -- transparencies and colors before others
2300      +  p_no_cl              * p_do_cl           -- successive colors
2301      +  p_no_tr              * p_do_tr           -- successive transparencies
2302      +  p_no_nl^1
2303      +  1
2304    )^1 )
2305
2306    local oldsize = 0
2307    local newsize = 0
2308
2309    directives.register("backend.pdf.compact", function(v)
2310        compact = v and function(s)
2311            oldsize = oldsize + #s
2312            s = lpegmatch(pattern,s) or s
2313            newsize = newsize + #s
2314            return s
2315        end
2316    end)
2317
2318    statistics.register("pdf pagestream",function()
2319        if oldsize ~= newsize then
2320            return string.format("old size: %i, new size %i",oldsize,newsize)
2321        end
2322    end)
2323
2324end
2325
2326local flushdeferred -- defined later
2327
2328local level = 0
2329local state = true
2330
2331function lpdf.setpagestate(s)
2332    state = s
2333end
2334
2335local finalize  do
2336
2337    local ceil  = math.ceil
2338    local floor = math.floor
2339
2340    local f_font  = formatters["F%d"]
2341
2342    local f_form  = formatters["Fm%d"]
2343    local f_group = formatters["Gp%d"]
2344    local f_image = formatters["Im%d"]
2345    local f_state = formatters["Gs%d"]
2346
2347    local function checkedbox(mediabox,otherbox,what)
2348        if otherbox and #mediabox == 4 and #otherbox == 4 then
2349            local done = false
2350            if otherbox[1] < mediabox[1] then done = true ; otherbox[1] = mediabox[1] end
2351            if otherbox[2] < mediabox[2] then done = true ; otherbox[2] = mediabox[2] end
2352            if otherbox[3] > mediabox[3] then done = true ; otherbox[3] = mediabox[3] end
2353            if otherbox[4] > mediabox[4] then done = true ; otherbox[4] = mediabox[4] end
2354            if done then
2355                report("limiting %a to 'MediaBox'",what)
2356            end
2357        end
2358        return otherbox
2359    end
2360
2361    -- The Resources dictionary always was optional but for some reason (maybe because some
2362    -- application bark on it not being present, which makes bugs into a feature) it now is
2363    -- (per addendum to the standard). First it was only for version 2, but in the end also
2364    -- version 1 has been affected. Of course that makes older documents that just omit an
2365    -- empty one officially invalid. However, because often the procset was there anyway,
2366    -- changes are low, but as that one is also obsolete, one only gets an issue when that
2367    -- one is left out too. In the end it means that we always need to add a bogus resource
2368    -- in order to satisfy 'strict' checkers (one can argue that deprecated features should
2369    -- be reported as such, and not as error).
2370
2371    local dummy = false
2372
2373    local function checkedresources()
2374        if dummy == false then
2375         -- if majorversion > 1 then
2376                dummy = pdfsharedobject(pdfdictionary {
2377                 -- LMTX_Comment = pdfunicode("mandate resource dictionary cf 2.0+ specification")
2378                    LMTX_Comment = pdfunicode("mandate resource dictionary cf 'latest' specification")
2379                } )
2380         -- else
2381         --     dummy = nil
2382         -- end
2383        end
2384        return dummy
2385    end
2386
2387    lpdf.checkedresources = checkedresources
2388
2389    finalize = function(driver,details)
2390
2391        if not details then
2392            report("something is wrong, no details in 'finalize'")
2393        end
2394
2395        level = level + 1
2396
2397        pdf_goto_pagemode() -- for now
2398
2399        local objnum        = details.objnum
2400        local specification = details.specification or { }
2401
2402        local content = concat(buffer,"\n",1,b)
2403
2404        if compact then
2405            content = compact(content)
2406        end
2407
2408        local fonts   = nil
2409        local xforms  = nil
2410
2411        if next(usedfonts) then
2412            fonts = pdfdictionary { }
2413            for k, v in next, usedfonts do
2414                fonts[f_font(v)] = pdfreference(usedfontobjects[k]) -- we can overload for testing
2415            end
2416        end
2417
2418        -- messy: use real indexes for both ... so we need to change some in the
2419        -- full luatex part
2420
2421        if next(usedxforms) or next(usedximages) or next(usedxgroups) then
2422            xforms = pdfdictionary { }
2423            for k in sortedhash(usedxforms) do
2424                xforms[f_form(getxformname(k))] = pdfreference(k)
2425            end
2426            for k, v in sortedhash(usedximages) do
2427                xforms[f_image(k)] = pdfreference(v)
2428            end
2429            for k, v in sortedhash(usedxgroups) do
2430                xforms[f_group(k)] = pdfreference(v)
2431            end
2432        end
2433
2434        reset_buffer()
2435
2436     -- finish_pdfpage_callback(shippingmode == "page")
2437
2438        if shippingmode == "page" then
2439
2440            local pageproperties  = lpdf.getpageproperties()
2441
2442            local pageresources   = pageproperties.pageresources
2443            local pageattributes  = pageproperties.pageattributes
2444            local pagesattributes = pageproperties.pagesattributes
2445
2446            pageresources.Font    = fonts
2447            pageresources.XObject = xforms
2448         -- pageresources.ProcSet = lpdf.procset()
2449
2450            local bbox = pdfarray {
2451                boundingbox[1] * bpfactor,
2452                boundingbox[2] * bpfactor,
2453                boundingbox[3] * bpfactor,
2454                boundingbox[4] * bpfactor,
2455            }
2456
2457            local contentsobj = content ~= "" and pdfflushstreamobject(content,false,true) or false -- empty /Contents are forbidden
2458
2459            pageattributes.Type      = pdf_page
2460            pageattributes.Contents  = contentsobj and pdfreference(contentsobj) or nil
2461            pageattributes.MediaBox  = pdfsharedobject(bbox)
2462            pageattributes.Parent    = nil -- precalculate
2463            pageattributes.Group     = nil -- todo
2464
2465            -- resources can be indirect
2466
2467            if state == "ignore" or state == false then
2468
2469            else
2470
2471                registerpage(pageattributes)
2472
2473                lpdf.finalizepage(true)
2474
2475                local TrimBox  = pageattributes.TrimBox
2476                local CropBox  = pageattributes.CropBox
2477                local BleedBox = pageattributes.BleedBox
2478
2479                -- Indirect objects don't work in all viewers.
2480
2481                if TrimBox  then pageattributes.TrimBox  = pdfsharedobject(checkedbox(bbox,TrimBox,"TrimBox")) end
2482                if CropBox  then pageattributes.CropBox  = pdfsharedobject(checkedbox(bbox,CropBox,"CropBox")) end
2483                if BleedBox then pageattributes.BleedBox = pdfsharedobject(checkedbox(bbox,BleedBox,"BleedBox")) end
2484
2485            end
2486
2487            pageattributes.Resources = next(pageresources) and pdfsharedobject(pageresources) or checkedresources()
2488
2489        else
2490
2491            local xformtype  = specification.type or 0
2492            local margin     = specification.margin or 0
2493            local attributes = specification.attributes or ""
2494            local resources  = specification.resources or ""
2495            local wrapper    = nil
2496
2497            if xformtype == 0 then
2498                wrapper = pdfdictionary {
2499                    Type      = pdf_xobject,
2500                    Subtype   = pdf_form,
2501                    FormType  = 1,
2502                    BBox      = nil,
2503                    Matrix    = nil,
2504                    Resources = nil,
2505                }
2506            else
2507                wrapper = pdfdictionary {
2508                    BBox      = nil,
2509                    Matrix    = nil,
2510                    Resources = nil,
2511                }
2512            end
2513            if xformtype == 0 or xformtype == 1 or xformtype == 3 then
2514                margin = 1
2515                wrapper.BBox = pdfarray {
2516                     floor(boundingbox[1] * bpfactor) - margin,
2517                     floor(boundingbox[2] * bpfactor) - margin,
2518                     ceil (boundingbox[3] * bpfactor) + margin,
2519                     ceil (boundingbox[4] * bpfactor) + margin,
2520                }
2521            end
2522            if xformtype == 0 or xformtype == 2 or xformtype == 3 then
2523                -- can be shared too
2524                wrapper.Matrix = pdfarray { 1, 0, 0, 1, 0, 0 }
2525            end
2526
2527            local patterns = true
2528
2529            if attributes.Type and attributes.Type == pdf_pattern then
2530                patterns = false
2531            end
2532
2533            local boxresources = lpdf.collectedresources {
2534                patterns  = patterns,
2535                serialize = false,
2536            }
2537            boxresources.Font    = fonts
2538            boxresources.XObject = xforms
2539
2540         -- todo: maybe share them
2541         -- wrapper.Resources = pdfreference(pdfflushobject(boxresources))
2542
2543            if resources ~= "" then
2544                boxresources = boxresources + resources
2545            end
2546            if attributes ~= "" then
2547                wrapper = wrapper + attributes
2548            end
2549
2550        --  wrapper.Resources = next(boxresources) and boxresources or nil
2551            wrapper.Resources = next(boxresources) and boxresources or checkedresources()
2552        --  wrapper.ProcSet   = lpdf.procset()
2553
2554            pdfflushstreamobject(content,wrapper,true,specification.objnum)
2555
2556        end
2557
2558        for objnum in sortedhash(usedxforms) do
2559            local f = flushedxforms[objnum]
2560            if f[1] == false then
2561                f[1] = true
2562                local objnum        = f[2] -- specification.objnum
2563                local specification = boxresources[objnum]
2564                local list          = specification.list
2565                localconverter(list,"xform",f[2],specification)
2566            end
2567        end
2568
2569        pdf_h, pdf_v  = 0, 0
2570
2571        if level == 1 then
2572            flushdeferred()
2573        end
2574        level = level - 1
2575
2576    end
2577
2578end
2579
2580-- now comes the pdf file handling
2581
2582local objects       = { }
2583local streams       = { } -- maybe just parallel to objects (no holes)
2584local nofobjects    = 0
2585local offset        = 0
2586local f             = false
2587local flush         = false
2588local objectstream  = true
2589local compress      = true
2590local cache         = false
2591local info          = ""
2592local catalog       = ""
2593local lastdeferred  = false
2594
2595local f_object       = formatters["%i 0 obj\010%s\010endobj\010"]
2596local f_stream_n_u   = formatters["%i 0 obj\010<< /Length %i >>\010stream\010%s\010endstream\010endobj\010"]
2597local f_stream_n_c   = formatters["%i 0 obj\010<< /Filter /FlateDecode /Length %i >>\010stream\010%s\010endstream\010endobj\010"]
2598local f_stream_d_u   = formatters["%i 0 obj\010<< %s /Length %i >>\010stream\010%s\010endstream\010endobj\010"]
2599local f_stream_d_c   = formatters["%i 0 obj\010<< %s /Filter /FlateDecode /Length %i >>\010stream\010%s\010endstream\010endobj\010"]
2600local f_stream_d_r   = formatters["%i 0 obj\010<< %s >>\010stream\010%s\010endstream\010endobj\010"]
2601
2602----- f_object_b     = formatters["%i 0 obj\010"]
2603local f_stream_b_n_u = formatters["%i 0 obj\010<< /Length %i >>\010stream\010"]
2604local f_stream_b_n_c = formatters["%i 0 obj\010<< /Filter /FlateDecode /Length %i >>\010stream\010"]
2605local f_stream_b_d_u = formatters["%i 0 obj\010<< %s /Length %i >>\010stream\010"]
2606local f_stream_b_d_c = formatters["%i 0 obj\010<< %s /Filter /FlateDecode /Length %i >>\010stream\010"]
2607local f_stream_b_d_r = formatters["%i 0 obj\010<< %s >>\010stream\010"]
2608
2609----- s_object_e <const> = "\010endobj\010"
2610local s_stream_e <const> = "\010endstream\010endobj\010"
2611
2612do
2613
2614    -- Versions can be set but normally are managed by the official standards. When possible
2615    -- reading and writing should look at these values.
2616
2617    function lpdf.setversion(major,minor)
2618        majorversion = tonumber(major) or majorversion
2619        minorversion = tonumber(minor) or minorversion
2620    end
2621
2622    function lpdf.getversion(major,minor)
2623        return majorversion, minorversion
2624    end
2625
2626    function lpdf.majorversion() return majorversion end
2627    function lpdf.minorversion() return minorversion end
2628
2629    -- It makes no sense to support levels so we only enable and disable and stick to level 3
2630    -- which is both fast and efficient.
2631
2632    local frozen = false
2633    local clevel = 3
2634    local olevel = 1
2635
2636    function lpdf.setcompression(level,objectlevel,freeze)
2637        if not frozen then
2638            compress = level and level ~= 0 and true or false
2639         -- if next(objects) or next(streams) or nofobjects > 0 then -- too strict as we have one alreay
2640            if next(streams) then
2641                -- no way
2642            else
2643                objectstream = objectlevel and objectlevel ~= 0 and true or false
2644            end
2645            frozen = freeze
2646        end
2647    end
2648
2649    function lpdf.getcompression()
2650        return compress and olevel or 0, objectstream and clevel or 0
2651    end
2652
2653    function lpdf.compresslevel()
2654        return compress and olevel or 0
2655    end
2656
2657    function lpdf.objectcompresslevel()
2658        return objectstream and clevel or 0
2659    end
2660
2661    if environment.arguments.nocompression then
2662        lpdf.setcompression(0,0,true)
2663    end
2664
2665end
2666
2667local addtocache, flushcache, cache do
2668
2669    local data, d  = { }, 0
2670    local list, l  = { }, 0
2671    local coffset  = 0
2672    local indices  = { }
2673
2674    local maxsize  = 32 * 1024 -- uncompressed
2675    local maxcount = 0xFF
2676
2677    addtocache = function(n,str)
2678        local size = #str
2679        if size == 0 then
2680            -- todo: message
2681            return
2682        end
2683        if coffset + size > maxsize or d == maxcount then
2684            flushcache()
2685        end
2686        if d == 0 then
2687            nofobjects = nofobjects + 1
2688            objects[nofobjects] = false
2689            streams[nofobjects] = indices
2690            cache = nofobjects
2691        end
2692        objects[n] = - cache
2693        indices[n] = d
2694        d = d + 1
2695        -- can have a comment n 0 obj as in luatex
2696        data[d] = str
2697        l = l + 1 ; list[l] = n
2698        l = l + 1 ; list[l] = coffset
2699        coffset = coffset + size + 1
2700    end
2701
2702    local p_ObjStm = pdfconstant("ObjStm")
2703
2704    flushcache = function() -- references cannot be stored
2705        if l > 0 then
2706            list = concat(list," ")
2707            data[0] = list
2708            data = concat(data,"\010",0,d)
2709            local strobj = pdfdictionary {
2710                Type  = p_ObjStm,
2711                N     = d,
2712                First = #list + 1,
2713            }
2714            objects[cache] = offset
2715            local fb
2716            if compress then
2717                local size = #data
2718                local comp = compressdata(data,size)
2719                if comp and #comp < size then
2720                    data = comp
2721                    fb = f_stream_b_d_c
2722                else
2723                    fb = f_stream_b_d_u
2724                end
2725            else
2726                fb = f_stream_b_d_u
2727            end
2728            local size = #data
2729            if encryptstream then
2730                data = encryptstream(data)
2731                size = #data
2732            end
2733            local b = fb(cache,strobj(),size)
2734            local e = s_stream_e
2735            flush(f,b)
2736            flush(f,data)
2737            flush(f,e)
2738            offset = offset + #b + size + #e
2739            data, d = { }, 0
2740            list, l = { }, 0
2741            coffset = 0
2742            indices = { }
2743        end
2744    end
2745
2746end
2747
2748do
2749
2750    local names        = { }
2751    local cache        = { }
2752    local nofpages     = 0
2753
2754    local texgetcount  = tex.getcount
2755
2756    local c_realpageno <const> = tex.iscount("realpageno")
2757
2758    pdfreserveobject = function(name)
2759        nofobjects = nofobjects + 1
2760        objects[nofobjects] = false
2761        if name then
2762            names[name] = nofobjects
2763            if trace_objects then
2764                report_objects("reserving number %a under name %a",nofobjects,name)
2765            end
2766        elseif trace_objects then
2767            report_objects("reserving number %a",nofobjects)
2768        end
2769        return nofobjects
2770    end
2771
2772    pdfpagereference = function(n,complete) -- true | false | nil | n [true,false]
2773        if n == true or not n then
2774            complete = n
2775            n = texgetcount(c_realpageno)
2776        end
2777        if n > nofpages then
2778            nofpages = n
2779        end
2780        local r = pdfgetpagereference(n)
2781        return complete and pdfreference(r) or r
2782    end
2783
2784    lpdf.reserveobject = pdfreserveobject
2785    lpdf.pagereference = pdfpagereference
2786
2787    function lpdf.lastreferredpage()
2788        return nofpages
2789    end
2790
2791    function lpdf.nofpages() -- this will change: document nofpages
2792        return structures.pages.nofpages
2793    end
2794
2795    function lpdf.object(...)
2796        pdfdeferredobject(...)
2797    end
2798
2799    function lpdf.delayedobject(data,n)
2800        if n then
2801            pdfdeferredobject(n,data)
2802        else
2803            n = pdfdeferredobject(data)
2804        end
2805--         pdfreferenceobject(n)
2806        return n
2807    end
2808
2809    pdfflushobject = function(name,data)
2810        if data then
2811            local named = names[name]
2812            if named then
2813                if not trace_objects then
2814                elseif trace_details then
2815                    report_objects("flushing data to reserved object with name %a, data: %S",name,data)
2816                else
2817                    report_objects("flushing data to reserved object with name %a",name)
2818                end
2819                return pdfimmediateobject(named,tostring(data))
2820            else
2821                if not trace_objects then
2822                elseif trace_details then
2823                    report_objects("flushing data to reserved object with number %s, data: %S",name,data)
2824                else
2825                    report_objects("flushing data to reserved object with number %s",name)
2826                end
2827                return pdfimmediateobject(name,tostring(data))
2828            end
2829        else
2830            if trace_objects and trace_details then
2831                report_objects("flushing data: %S",name)
2832            end
2833            return pdfimmediateobject(tostring(name))
2834        end
2835    end
2836
2837    pdfflushstreamobject = function(data,dict,compressed,objnum) -- default compressed
2838        if trace_objects then
2839            report_objects("flushing stream object of %s bytes",#data)
2840        end
2841        local dtype    = type(dict)
2842        local kind     = compressed == "raw" and "raw" or "stream"
2843        local nolength = nil
2844        if compressed == "raw" then
2845            compressed = false
2846            nolength   = true
2847         -- data       = string.formatters["<< %s >>stream\n%s\nendstream"](attr,data)
2848        end
2849
2850        return pdfdeferredobject {
2851            objnum        = objnum,
2852            immediate     = true,
2853            nolength      = nolength,
2854            compresslevel = compressed,
2855            type          = "stream",
2856            string        = data,
2857            attr          = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil,
2858        }
2859    end
2860
2861    function lpdf.flushstreamfileobject(filename,dict,compressed,objnum) -- default compressed
2862        if trace_objects then
2863            report_objects("flushing stream file object %a",filename)
2864        end
2865        local dtype = type(dict)
2866        return pdfdeferredobject {
2867            objnum        = objnum,
2868            immediate     = true,
2869            compresslevel = compressed,
2870            type          = "stream",
2871            file          = filename,
2872            attr          = (dtype == "string" and dict) or (dtype == "table" and dict()) or nil,
2873        }
2874    end
2875
2876    local shareobjectcache, shareobjectreferencecache = { }, { }
2877
2878    function lpdf.shareobject(content)
2879        if content == nil then
2880            -- invalid object not created
2881        else
2882            content = tostring(content)
2883            local o = shareobjectcache[content]
2884            if not o then
2885                o = pdfimmediateobject(content)
2886                shareobjectcache[content] = o
2887            end
2888            return o
2889        end
2890    end
2891
2892    pdfsharedobject = function(content)
2893        if content == nil then
2894            -- invalid object not created
2895        else
2896            content = tostring(content)
2897            local r = shareobjectreferencecache[content]
2898            if not r then
2899                local o = shareobjectcache[content]
2900                if not o then
2901                    o = pdfimmediateobject(content)
2902                    shareobjectcache[content] = o
2903                end
2904                r = pdfreference(o)
2905                shareobjectreferencecache[content] = r
2906            end
2907            return r
2908        end
2909    end
2910
2911    lpdf.flushobject          = pdfflushobject
2912    lpdf.flushstreamobject    = pdfflushstreamobject
2913    lpdf.shareobjectreference = pdfsharedobject
2914    lpdf.sharedobject         = pdfsharedobject
2915
2916end
2917
2918local preallocated = false -- true will not preallocate
2919
2920local pages  do
2921
2922    local texgetcount = tex.getcount
2923
2924    pages = table.setmetatableindex(function(t,k)
2925        if not preallocated then
2926            local n = structures.counters.record("realpage")["last"]
2927            for i=1,n do
2928                t[i] = pdfreserveobject()
2929            end
2930            preallocated = true
2931            if k <= n then
2932                return t[k]
2933            end
2934        end
2935        local v = pdfreserveobject()
2936        t[k] = v
2937        return v
2938    end)
2939
2940    pdfgetpagereference = function(n)
2941        return pages[n]
2942    end
2943
2944    lpdf.getpagereference = pdfgetpagereference
2945
2946end
2947
2948
2949local function flushnormalobj(data,n)
2950    if not n then
2951        nofobjects = nofobjects + 1
2952        n = nofobjects
2953    end
2954    if encryptobject then
2955        data = encryptobject(data)
2956    end
2957    data = f_object(n,data)
2958    if level == 0 then
2959        objects[n] = offset
2960        offset = offset + #data
2961        flush(f,data)
2962    else
2963        if not lastdeferred then
2964            lastdeferred = n
2965        elseif n < lastdeferred then
2966            lastdeferred = n
2967        end
2968        objects[n] = data
2969    end
2970    return n
2971end
2972
2973local flushstreamobj, streamstatus do
2974
2975    local uncompressed  = 0
2976    local compressed    = 0
2977    local notcompressed = 0
2978
2979    local threshold     = 40 -- also #("/Filter /FlateDecode") (compression threshold)
2980
2981    -- directives.register("backend.pdf.threshold",function(v)
2982    --     if v then
2983    --         threshold = tonumber(v) or 40
2984    --     else
2985    --         threshold = -1000
2986    --     end
2987    -- end)
2988
2989    streamstatus = function()
2990        return {
2991            nofstreams          = uncompressed + compressed + notcompressed,
2992            uncompressed        = uncompressed,
2993            compressed          = compressed,
2994            notcompressed       = notcompressed,
2995            threshold           = threshold,
2996            compresslevel       = lpdf.compresslevel(),
2997            objectcompresslevel = lpdf.objectcompresslevel(),
2998
2999        }
3000    end
3001
3002    local function updatelength(d,size)
3003        -- qpdf wants + 1 but acrobat crashes
3004        d = gsub(d,"/Length %s*%d+%s*",        "/Length " .. size .. " ")
3005        -- this can happen when we copy
3006        d = gsub(d,"(/Length %d+ )%d+ R%s*", "%1")
3007        return d
3008    end
3009
3010    flushstreamobj = function(data,n,dict,comp,nolength)
3011        if not data then
3012            report("no data for %S",dict)
3013            return
3014        end
3015        if not n then
3016            nofobjects = nofobjects + 1
3017            n = nofobjects
3018        end
3019        local size = #data
3020        if comp ~= false then
3021            comp = compress and size > threshold
3022        end
3023        if encryptobject then
3024            dict = encryptobject(dict)
3025        end
3026        if dict == "" then
3027            dict = nil
3028        end
3029        -- level is output nesting level
3030        if level == 0 then
3031            local b = nil
3032            local e = s_stream_e
3033            if nolength then
3034                b = f_stream_b_d_r(n,dict) -- raw object, already treated
3035                if encryptstream then
3036                 -- print("check length, case 2")
3037                    data = encryptstream(data)
3038                    size = #data
3039                end
3040                b = updatelength(b,size)
3041                uncompressed = uncompressed + 1
3042            else
3043                if comp then
3044                    local compdata = compressdata(data,size)
3045                    if compdata then
3046                        local compsize = #compdata
3047                        if compsize <= size - threshold then
3048                            data = compdata
3049                            size = compsize
3050                        else
3051                            comp = false
3052                        end
3053                    else
3054                        comp = false
3055                    end
3056                end
3057                if encryptstream then
3058                    data = encryptstream(data)
3059                    size = #data
3060                end
3061                if comp then
3062                    b = dict and f_stream_b_d_c(n,dict,size) or f_stream_b_n_c(n,size)
3063                    compressed = compressed + 1
3064                else
3065                    b = dict and f_stream_b_d_u(n,dict,size) or f_stream_b_n_u(n,size)
3066                    notcompressed = notcompressed + 1
3067                end
3068            end
3069            flush(f,b)
3070            flush(f,data)
3071            flush(f,e)
3072            objects[n] = offset
3073            offset = offset + #b + size + #e
3074        else
3075            if nolength then
3076                if encryptstream then
3077                 -- print("check length, case 1")
3078                    data = encryptstream(data)
3079                    size = #data
3080                end
3081                dict = updatelength(dict,size)
3082                data = f_stream_d_r(n,dict,data) -- raw object, already treated
3083                uncompressed = uncompressed + 1
3084            else
3085                if comp then
3086                    local compdata = compressdata(data,size)
3087                    if compdata then
3088                        local compsize = #compdata
3089                        if compsize <= size - threshold then
3090                            data = compdata
3091                            size = compsize
3092                        else
3093                            comp = false
3094                        end
3095                    else
3096                        comp = false
3097                    end
3098                end
3099                if encryptstream then
3100                    data = encryptstream(data)
3101                    size = #data
3102                end
3103                if comp then
3104                    data = dict and f_stream_d_c(n,dict,size,data) or f_stream_n_c(n,size,data)
3105                    compressed = compressed + 1
3106                else
3107                    data = dict and f_stream_d_u(n,dict,size,data) or f_stream_n_u(n,size,data)
3108                    notcompressed = notcompressed + 1
3109                end
3110            end
3111            if not lastdeferred then
3112                lastdeferred = n
3113            elseif n < lastdeferred then
3114                lastdeferred = n
3115            end
3116            objects[n] = data
3117        end
3118        return n
3119    end
3120
3121end
3122
3123flushdeferred = function() -- was forward defined
3124    if lastdeferred then
3125        for n=lastdeferred,nofobjects do
3126            local o = objects[n]
3127            if type(o) == "string" then
3128                objects[n] = offset
3129                offset = offset + #o
3130                flush(f,o)
3131            end
3132        end
3133        lastdeferred = false
3134    end
3135end
3136
3137pdfimmediateobject = function(a,b,c,d)
3138    local kind --, immediate
3139    local objnum, data, attr, filename
3140    local compresslevel, objcompression, nolength
3141    local argtype = type(a)
3142    if argtype == "table" then
3143        kind           = a.type          -- raw | stream
3144     -- immediate      = a.immediate
3145        objnum         = a.objnum
3146        attr           = a.attr
3147        compresslevel  = a.compresslevel
3148        objcompression = a.objcompression
3149        filename       = a.file
3150        data           = a.string or a.stream or ""
3151        nolength       = a.nolength
3152        if kind == "stream" then
3153            if filename then
3154                data = loaddata(filename) or ""
3155            end
3156        elseif kind == "raw" then
3157            if filename then
3158                data = loaddata(filename) or ""
3159            end
3160        elseif kind == "file"then
3161            kind = "raw"
3162            data = filename and loaddata(filename) or ""
3163        elseif kind == "streamfile" then
3164            kind = "stream"
3165            data = filename and loaddata(filename) or ""
3166        end
3167    else
3168        if argtype == "number" then
3169            objnum = a
3170            a, b, c = b, c, d
3171        else
3172            nofobjects = nofobjects + 1
3173            objnum = nofobjects
3174        end
3175        if b then
3176            if a == "stream" then
3177                kind = "stream"
3178                data = b
3179            elseif a == "file" then
3180             -- kind = "raw"
3181                data = loaddata(b)
3182            elseif a == "streamfile" then
3183                kind = "stream"
3184                data = loaddata(b)
3185            else
3186                data = "" -- invalid object
3187            end
3188            attr = c
3189        else
3190         -- kind = "raw"
3191            data = a
3192        end
3193    end
3194    if not objnum then
3195        nofobjects = nofobjects + 1
3196        objnum = nofobjects
3197    end
3198    -- todo: immediate
3199    if kind == "stream" then
3200        flushstreamobj(data,objnum,attr,compresslevel,nolength) -- nil == auto
3201    elseif objectstream and objcompression ~= false then
3202        addtocache(objnum,data)
3203    else
3204        flushnormalobj(data,objnum)
3205    end
3206    return objnum
3207end
3208
3209pdfdeferredobject    = pdfimmediateobject
3210
3211lpdf.deferredobject  = pdfimmediateobject
3212lpdf.immediateobject = pdfimmediateobject
3213
3214-- In lua 5.4 the methods are now moved one metalevel deeper so we need to get them
3215-- from mt.__index instead. (I did get that at first.) It makes for a slightly (imo)
3216-- nicer interface but no real gain in speed as we don't flush that often.
3217
3218local openfile, closefile  do
3219
3220    -- I used to do <space><lf> but then figured out that when I open and save a file in a mode
3221    -- that removes trailing spaces, the xref becomes invalid. The problem was then that a
3222    -- reconstruction of the file by a viewer gives weird effects probably because percent symbols
3223    -- gets interpreted then. Thanks to Ross Moore for noticing this side effect!
3224
3225    local f_used       = formatters["%010i 00000 n\013\010"]
3226    local f_link       = formatters["%010i 00000 f\013\010"]
3227    local f_first      = formatters["%010i 65535 f\013\010"]
3228
3229    local f_pdf_tag    = formatters["%%PDF-%i.%i\010"]
3230    local f_xref       = formatters["xref\0100 %i\010"]
3231    local f_trailer_id = formatters["trailer\010<< %s /ID [ <%s> <%s> ] >>\010startxref\010%i\010%%%%EOF"]
3232    local f_trailer_no = formatters["trailer\010<< %s >>\010startxref\010%i\010%%%%EOF"]
3233    local f_startxref  = formatters["startxref\010%i\010%%%%EOF"]
3234
3235    local inmemory = false
3236    local close    = false
3237    local update   = false
3238    local usedname = false
3239    local usedsize = false
3240
3241    directives.enable("backend.pdf.inmemory", function(v) inmemory = true end)
3242
3243 -- local banner <const> = "%\xCC\xD5\xC1\xD4\xC5\xD8\xD0\xC4\xC6\010"     -- LUATEXPDF  (+128)
3244    local banner <const> = "%\xC3\xCF\xCE\xD4\xC5\xD8\xD4\xD0\xC4\xC6\010" -- CONTEXTPDF (+128)
3245
3246    openfile = function(filename)
3247        --
3248        local arguments = environment.arguments
3249        if arguments.ownerpassword then
3250            lpdf.setencryption {
3251                ownerpassword = arguments.ownerpassword,
3252                userpassword  = arguments.userpassword,
3253                permissions   = arguments.permissions,
3254            }
3255        end
3256        --
3257        if not preallocated then
3258            local dummy = pages[1]
3259        end
3260        --
3261        if inmemory then
3262            local n = 0
3263            f = { }
3264            flush = function(f,s)
3265                n = n + 1 f[n] = s
3266             -- offset = offset + #s
3267            end
3268            close = function(f)
3269                f = concat(f)
3270                usedsize = #f
3271                io.savedata(filename,f)
3272                f = false
3273            end
3274            update = function(f,s)
3275                f[1] = s
3276            end
3277         -- local n = 0
3278         -- f = {
3279         --     write = function(self,s)
3280         --         n = n + 1 f[n] = s
3281         --     end,
3282         --     close = function(self)
3283         --         f = concat(f)
3284         --         io.savedata(filename,f)
3285         --         f = false
3286         --     end,
3287         -- }
3288         else
3289            f = io.open(filename,"wb")
3290            if not f then
3291                report()
3292                report("quitting because file %a cannot be opened for writing",filename)
3293                report()
3294                os.exit()
3295            end
3296         -- f:setvbuf("full",64*1024)
3297            local m = getmetatable(f)
3298            flush = m.write or m.__index.write
3299            close = m.close or m.__index.close
3300            update = function(f,s)
3301                f:seek("set",0)
3302                f:write(s)
3303            end
3304        end
3305        local version = f_pdf_tag(majorversion,minorversion)
3306        flush(f,version)
3307        flush(f,banner)
3308        offset = offset + #version + #banner
3309        usedname = filename
3310    end
3311
3312    closefile = function(abort)
3313        if abort then
3314            close(f)
3315            if not environment.arguments.nodummy then
3316                f = io.open(abort,"wb")
3317                if f then
3318                    local name = resolvers.findfile("context-lmtx-error.pdf")
3319                    if name then
3320                        local data = io.loaddata(name)
3321                        if data then
3322                            f:write(data)
3323                            f:close()
3324                            return
3325                        end
3326                    end
3327                    f:close()
3328                end
3329            end
3330            os.remove(abort)
3331        else
3332            local xrefoffset = offset
3333            local lastfree   = 0
3334            local noffree    = 0
3335            --
3336            local os = objectstream
3337            if encryptstream then
3338                objectstream = false
3339            end
3340            local catalog = lpdf.getcatalog()
3341            objectstream = os
3342            --
3343            local info       = lpdf.getinfo()
3344            local trailerid  = lpdf.gettrailerid()
3345            --
3346            if objectstream then
3347                flushdeferred()
3348                flushcache()
3349                --
3350                offset = lpdf.preparesignature and lpdf.preparesignature(flush,f,offset,objects) or offset
3351                --
3352                xrefoffset = offset
3353                --
3354                nofobjects = nofobjects + 1
3355                objects[nofobjects] = offset -- + 1
3356                --
3357                -- combine these three in one doesn't really give less code so
3358                -- we go for the efficient ones
3359                --
3360                local nofbytes  = 4
3361                local c1, c2, c3, c4
3362                if offset <= 0xFFFF then
3363                    nofbytes = 2
3364                    for i=1,nofobjects do
3365                        local o = objects[i]
3366                        if not o then
3367                            noffree = noffree + 1
3368                        else
3369                            local strm = o < 0
3370                            if strm then
3371                                o = -o
3372                            end
3373                            c1 = (o>>8)&0xFF
3374                            c2 = (o>>0)&0xFF
3375                            if strm then
3376                                objects[i] = char(2,c1,c2,streams[o][i])
3377                            else
3378                                objects[i] = char(1,c1,c2,0)
3379                            end
3380                        end
3381                    end
3382                    if noffree > 0 then
3383                        for i=nofobjects,1,-1 do
3384                            local o = objects[i]
3385                            if not o then
3386                                local f1 = (lastfree>>8)&0xFF
3387                                local f2 = (lastfree>>0)&0xFF
3388                                objects[i] = char(0,f1,f2,0)
3389                                lastfree   = i
3390                            end
3391                        end
3392                    end
3393                elseif offset <= 0xFFFFFF then
3394                    nofbytes = 3
3395                    for i=1,nofobjects do
3396                        local o = objects[i]
3397                        if not o then
3398                            noffree = noffree + 1
3399                        else
3400                            local strm = o < 0
3401                            if strm then
3402                                o = -o
3403                            end
3404                            c1 = (o>>16)&0xFF
3405                            c2 = (o>> 8)&0xFF
3406                            c3 = (o>> 0)&0xFF
3407                            if strm then
3408                                objects[i] = char(2,c1,c2,c3,streams[o][i])
3409                            else
3410                                objects[i] = char(1,c1,c2,c3,0)
3411                            end
3412                        end
3413                    end
3414                    if noffree > 0 then
3415                        for i=nofobjects,1,-1 do
3416                            local o = objects[i]
3417                            if not o then
3418                                local f1 = (lastfree>>16)&0xFF
3419                                local f2 = (lastfree>> 8)&0xFF
3420                                local f3 = (lastfree>> 0)&0xFF
3421                                objects[i] = char(0,f1,f2,f3,0)
3422                                lastfree   = i
3423                            end
3424                        end
3425                    end
3426                else
3427                    nofbytes = 4
3428                    for i=1,nofobjects do
3429                        local o = objects[i]
3430                        if not o then
3431                            noffree = noffree + 1
3432                        else
3433                            local strm = o < 0
3434                            if strm then
3435                                o = -o
3436                            end
3437                            c1 = (o>>24)&0xFF
3438                            c2 = (o>>16)&0xFF
3439                            c3 = (o>> 8)&0xFF
3440                            c4 = (o>> 0)&0xFF
3441                            if strm then
3442                                objects[i] = char(2,c1,c2,c3,c4,streams[o][i])
3443                            else
3444                                objects[i] = char(1,c1,c2,c3,c4,0)
3445                            end
3446                        end
3447                    end
3448                    if noffree > 0 then
3449                        for i=nofobjects,1,-1 do
3450                            local o = objects[i]
3451                            if not o then
3452                                local f1 = (lastfree>>24)&0xFF
3453                                local f2 = (lastfree>>16)&0xFF
3454                                local f3 = (lastfree>> 8)&0xFF
3455                                local f4 = (lastfree>> 0)&0xFF
3456                                objects[i] = char(0,f1,f2,f3,f4,0)
3457                                lastfree   = i
3458                            end
3459                        end
3460                    end
3461                end
3462                objects[0] = rep("\0",1+nofbytes+1)
3463                local data = concat(objects,"",0,nofobjects)
3464                local size = #data
3465                local xref = pdfdictionary {
3466                    Type    = pdfconstant("XRef"),
3467                    Size    = nofobjects + 1,
3468                    W       = pdfarray { 1, nofbytes, 1 },
3469                    Root    = catalog,
3470                    Info    = info,
3471                    ID      = trailerid and pdfarray { pdfliteral(trailerid,true), pdfliteral(trailerid,true) } or nil,
3472                    Encrypt = encdict or nil,
3473                }
3474                local fb
3475             -- if encryptstream then
3476             --     if compress then
3477             --         local comp = compressdata(data,size)
3478             --         if comp then
3479             --             data = comp
3480             --             size = #data
3481             --             fb = f_stream_b_d_c
3482             --             xref.Filter = pdfarray {
3483             --                 pdfconstant("Crypt"), -- identity
3484             --                 pdfconstant("FlateDecode")
3485             --             }
3486             --         else
3487             --             xref.Filter = pdfconstant("Crypt") -- identity
3488             --         end
3489             --     else
3490             --         xref.Filter = pdfconstant("Crypt") -- identity
3491             --     end
3492             --     fb = f_stream_b_d_u
3493             -- else
3494                    if compress then
3495                        local comp = compressdata(data,size)
3496                        if comp then
3497                            data = comp
3498                            size = #data
3499                            fb = f_stream_b_d_c
3500                        else
3501                            fb = f_stream_b_d_u
3502                        end
3503                    else
3504                        fb = f_stream_b_d_u
3505                    end
3506             -- end
3507                -- no encryption of data here
3508                flush(f,fb(nofobjects,xref(),size))
3509                flush(f,data)
3510                flush(f,s_stream_e)
3511                flush(f,f_startxref(xrefoffset))
3512            else
3513                flushdeferred()
3514                --
3515                offset = lpdf.preparesignature and lpdf.preparesignature(flush,f,offset,objects) or offset
3516                --
3517             -- if encryptstream then
3518             --     -- unencrypted !
3519             --     local eo = encryptobject
3520             --     encryptobject = false
3521             --     encdict = pdfreference(pdfimmediateobject(tostring(encdict)))
3522             --     encryptobject = eo
3523             -- end
3524                --
3525                xrefoffset = offset
3526                flush(f,f_xref(nofobjects+1))
3527                local trailer = pdfdictionary {
3528                    Size    = nofobjects + 1,
3529                    Root    = catalog,
3530                    Info    = info,
3531                    Encrypt = encdict or nil,
3532                }
3533                for i=1,nofobjects do
3534                    local o = objects[i]
3535                    if o then
3536-- if o < 0 then
3537--     o = -o
3538-- end
3539                       objects[i] = f_used(o)
3540                    end
3541                end
3542                for i=nofobjects,1,-1 do
3543                    local o = objects[i]
3544                    if not o then
3545                        objects[i] = f_link(lastfree)
3546                        lastfree   = i
3547                    end
3548                end
3549                objects[0] = f_first(lastfree)
3550                flush(f,concat(objects,"",0,nofobjects))
3551                trailer.Size = nofobjects + 1
3552                if trailerid then
3553                    flush(f,f_trailer_id(trailer(),trailerid,trailerid,xrefoffset))
3554                else
3555                    flush(f,f_trailer_no(trailer(),xrefoffset))
3556                end
3557            end
3558            update(f,f_pdf_tag(majorversion,minorversion)) -- check this
3559            usedsize = f:seek("end")
3560            close(f)
3561            io.flush()
3562            if lpdf.finalizesignature then lpdf.finalizesignature(usedname,usedsize) end
3563        end
3564        io.flush()
3565        closefile = function() end
3566    end
3567
3568end
3569
3570-- For the moment we overload it here, although back-fil.lua eventually will
3571-- be merged with back-pdf as it's pdf specific, or maybe back-imp-pdf or so.
3572
3573do
3574
3575    -- We overload img but at some point it will even go away, so we just
3576    -- reimplement what we need in context. This will change completely i.e.
3577    -- we will drop the low level interface!
3578
3579    local pdfbackend     = backends.registered.pdf
3580    local nodeinjections = pdfbackend.nodeinjections
3581    local codeinjections = pdfbackend.codeinjections
3582
3583    local imagetypes     = images.types -- pdf png jpg jp2 jbig2 stream
3584    local img_none       = imagetypes.none
3585
3586    local tonode         = nuts.tonode
3587    local newimagerule   = nuts.pool.imagerule
3588    local setattrlist    = nuts.setattrlist
3589    local setprop        = nuts.setprop
3590
3591    local report_images  = logs.reporter("backend","images")
3592
3593    local lastindex      = 0
3594    local indices        = { }
3595
3596    local bpfactor       = number.dimenfactors.bp
3597
3598    function codeinjections.newimage(specification)
3599        return specification
3600    end
3601
3602    function codeinjections.copyimage(original)
3603        return setmetatableindex(original)
3604    end
3605
3606    function codeinjections.scanimage(specification)
3607        -- placeholder, doesn't give back dimensions etc but will be plugged in
3608        return specification
3609    end
3610
3611    local function embedimage(specification)
3612        if specification then
3613            lastindex = lastindex + 1
3614            index     = lastindex
3615            specification.index = index
3616            local xobject = pdfdictionary { }
3617            if not specification.notype then
3618                xobject.Type     = pdf_xobject
3619                xobject.Subtype  = pdf_form
3620                xobject.FormType = 1
3621            end
3622            local bbox = specification.bbox
3623            if bbox and not specification.nobbox then
3624                xobject.BBox = pdfarray {
3625                    bbox[1] * bpfactor,
3626                    bbox[2] * bpfactor,
3627                    bbox[3] * bpfactor,
3628                    bbox[4] * bpfactor,
3629                }
3630            end
3631            xobject = xobject + specification.attr
3632            if bbox and not specification.width then
3633                specification.width = bbox[3]
3634            end
3635            if bbox and not specification.height then
3636                specification.height = bbox[4]
3637            end
3638            local dict = xobject()
3639            --
3640            nofobjects     = nofobjects + 1
3641            local objnum   = nofobjects
3642            local nolength = specification.nolength
3643            local stream   = specification.stream or specification.string
3644            --
3645            -- We cannot set type in native img so we need this hack or
3646            -- otherwise we need to patch too much. Better that i write
3647            -- a wrapper then. Anyway, it has to be done better: a key that
3648            -- tells either or not to scale by xsize/ysize when flushing.
3649            --
3650            if not specification.type then
3651                local kind = specification.kind
3652                if kind then
3653                    -- take that one
3654                elseif attr and find(attr,"BBox") then
3655                    kind = img_stream
3656                else
3657                    -- hack: a bitmap
3658                    kind = img_none
3659                end
3660                specification.type = kind
3661                specification.kind = kind
3662            end
3663            flushstreamobj(stream,objnum,dict,compresslevel,nolength)
3664            specification.objnum      = objnum
3665            specification.rotation    = specification.rotation or 0
3666            specification.orientation = specification.orientation or 0
3667            specification.transform   = specification.transform or 0
3668            specification.stream      = nil
3669            specification.attr        = nil
3670            specification.type        = specification.kind or specification.type or img_none
3671            indices[index]            = specification -- better create a real specification
3672            return specification
3673        end
3674    end
3675
3676    codeinjections.embedimage = embedimage
3677
3678    function codeinjections.wrapimage(specification)
3679        --
3680        local index = specification.index
3681        if not index then
3682            embedimage(specification)
3683        end
3684        --
3685        local n = newimagerule(
3686            specification.width  or 0,
3687            specification.height or 0,
3688            specification.depth  or 0
3689        )
3690        setattrlist(n,true)
3691        setprop(n,"index",specification.index)
3692        return tonode(n)
3693    end
3694
3695    pdfincludeimage = function(index)
3696        local specification = indices[index]
3697        if specification then
3698            local bbox      = specification.bbox
3699            local xorigin   = bbox[1]
3700            local yorigin   = bbox[2]
3701            local xsize     = bbox[3] - xorigin -- we need the original ones, not the 'rotated' ones
3702            local ysize     = bbox[4] - yorigin -- we need the original ones, not the 'rotated' ones
3703            local transform = specification.transform or 0
3704            local objnum    = specification.objnum or pdfreserveobject()
3705            local groupref  = nil
3706            local kind      = specification.kind or specification.type or img_none -- determines scaling type
3707            return
3708                kind,
3709                xorigin, yorigin,
3710                xsize, ysize,
3711                transform,
3712                objnum,
3713                groupref
3714        end
3715    end
3716
3717    lpdf.includeimage = pdfincludeimage
3718
3719end
3720
3721-- The driver.
3722
3723do
3724
3725 -- local addsuffix  = file.addsuffix
3726    local texgetbox  = tex.getbox
3727
3728    local pdfname    = nil
3729    local converter  = nil
3730    local useddriver = nil -- a bit of a hack
3731
3732    local function outputfilename(driver)
3733        return pdfname
3734    end
3735
3736-- local outputfilename ; do -- old todo usedname in ^^
3737--     local filename = nil
3738--     outputfilename = function(driver,usedname)
3739--         if usedname and usedname ~= "" then
3740--             filename = addsuffix(usedname,"pdf")
3741--         elseif not filename or filename == "" then
3742--             filename = addsuffix(tex.jobname,"pdf")
3743--         end
3744--         return filename
3745--     end
3746-- end
3747
3748    -- todo: prevent twice
3749    local function prepare(driver)
3750        if not environment.initex then
3751            --
3752            backends.initialize("pdf") -- also does bindings
3753            --
3754            pdfname = tex.jobname .. ".pdf"
3755            openfile(pdfname)
3756            --
3757            luatex.registerstopactions(1,function()
3758                if pdfname then
3759                    lpdf.finalizedocument()
3760                    closefile()
3761                    pdfname = nil
3762                end
3763            end)
3764            --
3765            luatex.registerpageactions(1,function()
3766                if pdfname then
3767                    lpdf.finalizepage(true)
3768                end
3769            end)
3770            --
3771            lpdf.registerdocumentfinalizer(wrapupdocument,nil,"wrapping up")
3772            --
3773            statistics.register("result saved in file", function()
3774                local status = streamstatus()
3775                local outputfilename = environment.outputfilename or environment.jobname or tex.jobname or "<unset>"
3776                outputfilename = gsub(outputfilename,"^%./+","") -- todo: make/use a helper
3777                return string.format(
3778                    "%s.pdf, compresslevel %s, objectcompresslevel %s, %i streams, %i uncompressed, %i compressed, %i not compressed, threshold %i",
3779                    outputfilename,
3780                    status.compresslevel,
3781                    status.objectcompresslevel,
3782                    status.nofstreams or 0,
3783                    status.uncompressed or 0,
3784                    status.compressed or 0,
3785                    status.notcompressed or 0,
3786                    status.threshold or 0
3787                )
3788            end)
3789            --
3790            luatex.registerstopactions(function()
3791                if pdfname then
3792                    local r = lpdf.lastreferredpage() -- somehow referenced
3793                    local s = lpdf.getnofpages()      -- in page tree, saved in file
3794                    local t = lpdf.nofpages()         -- in tuc file
3795                    if r > s then
3796                        report()
3797                        report("referred pages: %i, saved pages %i, pages from tuc file: %i, possible corrupt file",r,s,t)
3798                        report()
3799                    end
3800                end
3801            end)
3802        end
3803        converter  = drivers.converters.lmtx
3804        useddriver = driver
3805    end
3806
3807    local function wrapup(driver)
3808        if pdfname then
3809            closefile()
3810            pdfname = nil
3811        end
3812    end
3813
3814    local function cleanup(driver)
3815        if pdfname then
3816            closefile(pdfname)
3817            pdfname = nil
3818        end
3819    end
3820
3821    local function convert(driver,boxnumber)
3822        converter(driver,texgetbox(boxnumber),"page")
3823    end
3824
3825--     localconverter = function(...)
3826--         print(...)                -- ok when we add this
3827--         converter(useddriver,...) -- otherwise nil .. lua bug
3828--     end
3829
3830    localconverter = function(a,b,c,d)
3831        converter(useddriver,a,b,c,d)
3832    end
3833
3834    drivers.install {
3835        name     = "pdf",
3836        flushers = flushers,
3837        actions  = {
3838            prepare         = prepare,
3839            wrapup          = wrapup,
3840            cleanup         = cleanup,
3841            --
3842            initialize      = initialize,
3843            convert         = convert,
3844            finalize        = finalize,
3845            --
3846            outputfilename  = outputfilename,
3847        },
3848    }
3849
3850end
3851