mlib-pdf.lua /size: 32 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['mlib-pdf'] = {
2    version   = 1.001,
3    comment   = "companion to mlib-ctx.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files",
7}
8
9local gsub = string.gsub
10local concat, insert, remove = table.concat, table.insert, table.remove
11local abs, sqrt, round = math.abs, math.sqrt, math.round
12local setmetatable, rawset, tostring, tonumber, type = setmetatable, rawset, tostring, tonumber, type
13local P, S, C, Ct, Cc, Cg, Cf, Carg = lpeg.P, lpeg.S, lpeg.C, lpeg.Ct, lpeg.Cc, lpeg.Cg, lpeg.Cf, lpeg.Carg
14local lpegmatch = lpeg.match
15local formatters = string.formatters
16
17local report_metapost = logs.reporter("metapost")
18
19local trace_variables = false  trackers.register("metapost.variables",function(v) trace_variables = v end)
20
21local mplib           = mplib
22local context         = context
23
24local allocate        = utilities.storage.allocate
25
26local pen_info        = mplib.pen_info
27local getfields       = mplib.getfields or mplib.fields -- todo: in lmtx get them once and then use gettype
28
29local save_table      = false
30local force_stroke    = false
31
32metapost              = metapost or { }
33local metapost        = metapost
34
35metapost.flushers     = metapost.flushers or { }
36local pdfflusher      = { }
37metapost.flushers.pdf = pdfflusher
38
39metapost.n            = 0
40
41local experiment      = true -- uses context(node) that already does delayed nodes
42local savedliterals   = nil  -- needs checking
43local mpsliteral      = nodes.pool.originliteral
44
45local f_f  = formatters["%.6N"]
46local f_m  = formatters["%.6N %.6N m"]
47local f_c  = formatters["%.6N %.6N %.6N %.6N %.6N %.6N c"]
48local f_l  = formatters["%.6N %.6N l"]
49local f_cm = formatters["%.6N %.6N %.6N %.6N %.6N %.6N cm"]
50local f_M  = formatters["%.6N M"]
51local f_j  = formatters["%i j"]
52local f_J  = formatters["%i J"]
53local f_d  = formatters["[%s] %.6N d"]
54local f_w  = formatters["%.6N w"]
55
56directives.register("metapost.savetable",function(v)
57    if type(v) == "string" then
58        save_table = file.addsuffix(v,"mpl")
59    elseif v then
60        save_table = file.addsuffix(environment.jobname .. "-graphic","mpl")
61    else
62        save_table = false
63    end
64end)
65
66trackers.register("metapost.forcestroke",function(v)
67    force_stroke = v
68end)
69
70-- Because in MKiV we always have two passes, we save the objects. When an extra
71-- mp run is done (due to for instance texts identifier in the parse pass), we
72-- get a new result table and the stored objects are forgotten. Otherwise they
73-- are reused.
74
75local function getobjects(result,figure,index)
76    return figure:objects()
77end
78
79function metapost.convert(specification,result)
80    local flusher  = specification.flusher
81    local askedfig = specification.askedfig
82    if save_table then
83        table.save(save_table,metapost.totable(result,1)) -- direct
84    end
85    metapost.flush(specification,result)
86    return true -- done
87end
88
89function metapost.flushliteral(d)
90    if savedliterals then
91        context(mpsliteral(savedliterals[d]))
92    else
93        report_metapost("problem flushing literal %a",d)
94    end
95end
96
97function metapost.flushreset() -- will become obsolete and internal
98    savedliterals = nil
99end
100
101function pdfflusher.comment(message)
102    if message then
103        message = formatters["%% mps graphic %s: %s"](metapost.n,message)
104        if experiment then
105            context(mpsliteral(message))
106        elseif savedliterals then
107            local last = #savedliterals + 1
108            savedliterals[last] = message
109            context.MPLIBtoPDF(last)
110        else
111            savedliterals = { message }
112            context.MPLIBtoPDF(1)
113        end
114    end
115end
116
117function pdfflusher.startfigure(n,llx,lly,urx,ury,message)
118    savedliterals = nil
119    metapost.n = metapost.n + 1
120    context.startMPLIBtoPDF(f_f(llx),f_f(lly),f_f(urx),f_f(ury))
121    if message then pdfflusher.comment(message) end
122end
123
124function pdfflusher.stopfigure(message)
125    if message then pdfflusher.comment(message) end
126    context.stopMPLIBtoPDF()
127    context.MPLIBflushreset() -- maybe just at the beginning
128end
129
130function pdfflusher.flushfigure(pdfliterals) -- table
131    if #pdfliterals > 0 then
132        pdfliterals = concat(pdfliterals,"\n")
133        if experiment then
134            context(mpsliteral(pdfliterals))
135        else
136            if savedliterals then
137                local last = #savedliterals + 1
138                savedliterals[last] = pdfliterals
139                context.MPLIBtoPDF(last)
140            else
141                savedliterals = { pdfliterals }
142                context.MPLIBtoPDF(1)
143            end
144        end
145    end
146end
147
148function pdfflusher.textfigure(font,size,text,width,height,depth) -- we could save the factor
149    text = gsub(text,".","\\hbox{%1}") -- kerning happens in metapost (i have to check if this is true for mplib)
150    context.MPtextext(font,size,text,0,-number.dimenfactors.bp*depth)
151end
152
153local bend_tolerance = 131/65536
154
155local rx, sx, sy, ry, tx, ty, divider = 1, 0, 0, 1, 0, 0, 1
156
157local function pen_characteristics(object)
158    local t = pen_info(object)
159    rx, ry, sx, sy, tx, ty = t.rx, t.ry, t.sx, t.sy, t.tx, t.ty
160    divider = sx*sy - rx*ry
161    return not (sx==1 and rx==0 and ry==0 and sy==1 and tx==0 and ty==0), t.width
162end
163
164local function mpconcat(px, py) -- no tx, ty here / we can move this one inline if needed
165    return (sy*px-ry*py)/divider,(sx*py-rx*px)/divider
166end
167
168local function curved(ith,pth)
169    local d = pth.left_x - ith.right_x
170    if abs(ith.right_x - ith.x_coord - d) <= bend_tolerance and abs(pth.x_coord - pth.left_x - d) <= bend_tolerance then
171        d = pth.left_y - ith.right_y
172        if abs(ith.right_y - ith.y_coord - d) <= bend_tolerance and abs(pth.y_coord - pth.left_y - d) <= bend_tolerance then
173            return false
174        end
175    end
176    return true
177end
178
179local function flushnormalpath(path, t, open)
180    local pth, ith, nt
181    local length = #path
182    if t then
183        nt = #t
184    else
185        t = { }
186        nt = 0
187    end
188    for i=1,length do
189        nt = nt + 1
190        pth = path[i]
191        if not ith then
192            t[nt] = f_m(pth.x_coord,pth.y_coord)
193        elseif curved(ith,pth) then
194            t[nt] = f_c(ith.right_x,ith.right_y,pth.left_x,pth.left_y,pth.x_coord,pth.y_coord)
195        else
196            t[nt] = f_l(pth.x_coord,pth.y_coord)
197        end
198        ith = pth
199    end
200    if not open then
201        nt = nt + 1
202        local one = path[1]
203        if curved(pth,one) then
204            t[nt] = f_c(pth.right_x,pth.right_y,one.left_x,one.left_y,one.x_coord,one.y_coord )
205        else
206            t[nt] = f_l(one.x_coord,one.y_coord)
207        end
208    elseif length == 1 then
209        -- special case .. draw point
210        local one = path[1]
211        nt = nt + 1
212        t[nt] = f_l(one.x_coord,one.y_coord)
213    end
214    return t
215end
216
217local function flushconcatpath(path, t, open, transform)
218    local pth, ith, nt
219    local length = #path
220    if t then
221        nt = #t
222    else
223        t = { }
224        nt = 0
225    end
226    if transform then
227        nt = nt + 1
228        t[nt] = f_cm(sx,rx,ry,sy,tx,ty)
229    end
230    for i=1,length do
231        nt = nt + 1
232        pth = path[i]
233        if not ith then
234            t[nt] = f_m(mpconcat(pth.x_coord,pth.y_coord))
235        elseif curved(ith,pth) then
236            local a, b = mpconcat(ith.right_x,ith.right_y)
237            local c, d = mpconcat(pth.left_x,pth.left_y)
238            t[nt] = f_c(a,b,c,d,mpconcat(pth.x_coord,pth.y_coord))
239        else
240           t[nt] = f_l(mpconcat(pth.x_coord, pth.y_coord))
241        end
242        ith = pth
243    end
244    if not open then
245        nt = nt + 1
246        local one = path[1]
247        if curved(pth,one) then
248            local a, b = mpconcat(pth.right_x,pth.right_y)
249            local c, d = mpconcat(one.left_x,one.left_y)
250            t[nt] = f_c(a,b,c,d,mpconcat(one.x_coord, one.y_coord))
251        else
252            t[nt] = f_l(mpconcat(one.x_coord,one.y_coord))
253        end
254    elseif length == 1 then
255        -- special case .. draw point
256        nt = nt + 1
257        local one = path[1]
258        t[nt] = f_l(mpconcat(one.x_coord,one.y_coord))
259    end
260    return t
261end
262
263local function toboundingbox(path)
264    local size = #path
265    if size == 4 then
266        local pth = path[1]
267        local x = pth.x_coord
268        local y = pth.y_coord
269        local llx, lly, urx, ury = x, y, x, y
270        for i=2,size do
271            pth = path[i]
272            x   = pth.x_coord
273            y   = pth.y_coord
274            if x < llx then
275                llx = x
276            elseif x > urx then
277                urx = x
278            end
279            if y < lly then
280                lly = y
281            elseif y > ury then
282                ury = y
283            end
284        end
285        return { llx, lly, urx, ury }
286    else
287        return { 0, 0, 0, 0 }
288    end
289end
290
291metapost.flushnormalpath = flushnormalpath
292
293-- The flusher is pdf based, if another backend is used, we need to overload the
294-- flusher; this is beta code, the organization will change (already upgraded in
295-- sync with mplib)
296--
297-- We can avoid the before table but I like symmetry. There is of course a small
298-- performance penalty, but so is passing extra arguments (result, flusher, after)
299-- and returning stuff.
300--
301-- This variable stuff will change in lmtx.
302
303local ignore   = function() end
304
305local space    = P(" ")
306local equal    = P("=")
307local key      = C((1-equal)^1) * equal
308local newline  = S("\n\r")^1
309local number   = (((1-space-newline)^1) / tonumber) * (space^0)
310
311local p_number  = number
312local p_string  = C((1-newline)^0)
313local p_boolean = P("false") * Cc(false) + P("true") * Cc(true)
314local p_set     = Ct(number^1)
315local p_path    = Ct(Ct(number * number^-5)^1)
316
317local variable =
318    P("1:")            * p_number
319  + P("2:")            * p_string
320  + P("3:")            * p_boolean
321  + S("4568") * P(":") * p_set
322  + P("7:")            * p_path
323
324local pattern_tab = Cf ( Carg(1) * (Cg(variable * newline^0)^0), rawset)
325
326local variable =
327    P("1:")            * p_number
328  + P("2:")            * p_string
329  + P("3:")            * p_boolean
330  + S("4568") * P(":") * number^1
331  + P("7:")            * (number * number^-5)^1
332
333local pattern_lst = (variable * newline^0)^0
334
335metapost.variables  = { } -- currently across instances
336metapost.properties = { } -- to be stacked
337
338function metapost.untagvariable(str,variables) -- will be redone
339    if variables == false then
340        return lpegmatch(pattern_lst,str)
341    else
342        return lpegmatch(pattern_tab,str,1,variables or { })
343    end
344end
345
346-- function metapost.processspecial(str)
347--     lpegmatch(pattern_key,object.prescript,1,variables)
348-- end
349
350function metapost.processspecial(str)
351    local code = loadstring(str)
352    if code then
353        if trace_variables then
354            report_metapost("executing special code: %s",str)
355        end
356        code()
357    else
358        report_metapost("invalid special code: %s",str)
359    end
360end
361
362local stack = { }
363
364local function pushproperties(figure)
365    -- maybe there will be getters in lmtx
366    local boundingbox = figure:boundingbox()
367    local slot = figure:charcode() or 0
368    local properties = {
369        llx    = boundingbox[1],
370        lly    = boundingbox[2],
371        urx    = boundingbox[3],
372        ury    = boundingbox[4],
373        slot   = slot,
374        width  = figure:width(),
375        height = figure:height(),
376        depth  = figure:depth(),
377        italic = figure:italcorr(), -- figure:italic() in lmtx
378        number = slot,
379    }
380    insert(stack,properties)
381    metapost.properties = properties
382    return properties
383end
384
385local function popproperties()
386    metapost.properties = remove(stack)
387end
388
389local function nocomment() end
390
391metapost.comment = nocomment
392
393function metapost.flush(specification,result)
394    if result then
395        local flusher   = specification.flusher
396        local askedfig  = specification.askedfig
397        local incontext = specification.incontext
398        local figures   = result.fig
399        if figures then
400            flusher = flusher or pdfflusher
401            local resetplugins       = metapost.resetplugins       or ignore -- before figure
402            local processplugins     = metapost.processplugins     or ignore -- each object
403            local synchronizeplugins = metapost.synchronizeplugins or ignore
404            local pluginactions      = metapost.pluginactions      or ignore -- before / after
405            local startfigure        = flusher.startfigure
406            local stopfigure         = flusher.stopfigure
407            local flushfigure        = flusher.flushfigure
408            local textfigure         = flusher.textfigure
409            local processspecial     = flusher.processspecial or metapost.processspecial
410            metapost.comment         = flusher.comment or nocomment
411            for index=1,#figures do
412                local figure     = figures[index]
413                local properties = pushproperties(figure)
414                if askedfig == "direct" or askedfig == "all" or askedfig == properties.number then
415                    local objects    = getobjects(result,figure,index)
416                    local result     = { }
417                    local miterlimit = -1
418                    local linecap    = -1
419                    local linejoin   = -1
420                    local dashed     = false
421                    local linewidth  = false
422                    local llx        = properties.llx
423                    local lly        = properties.lly
424                    local urx        = properties.urx
425                    local ury        = properties.ury
426                    if urx < llx then
427                        -- invalid
428                        startfigure(properties.number,0,0,0,0,"invalid",figure)
429                        stopfigure()
430                    else
431
432                        -- we need to be indirect if we want the one-pass solution
433
434                        local groupstack = { }
435
436                        local function processfigure()
437                            result[#result+1] = "q"
438                            if objects then
439    --                             resetplugins(result) -- we should move the colorinitializer here
440                                local savedpath = nil
441                                local savedhtap = nil
442                                for o=1,#objects do
443                                    local object = objects[o]
444                                    local objecttype = object.type
445                                    if objecttype == "text" then
446                                        result[#result+1] = "q"
447                                        local ot = object.transform -- 3,4,5,6,1,2
448                                        result[#result+1] = f_cm(ot[3],ot[4],ot[5],ot[6],ot[1],ot[2])
449                                        flushfigure(result) -- flush accumulated literals
450                                        result = { }
451                                        textfigure(object.font,object.dsize,object.text,object.width,object.height,object.depth)
452                                        result[#result+1] = "Q"
453                                    elseif objecttype == "special" then
454                                        if processspecial then
455                                            processspecial(object.prescript)
456                                        end
457                                    elseif objecttype == "start_clip" then
458                                        local evenodd = not object.istext and object.postscript == "evenodd"
459                                        result[#result+1] = "q"
460                                        flushnormalpath(object.path,result,false)
461                                        result[#result+1] = evenodd and "W* n" or "W n"
462                                    elseif objecttype == "stop_clip" then
463                                        result[#result+1] = "Q"
464                                        miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false
465                                    elseif objecttype == "start_bounds" or objecttype == "stop_bounds" then
466                                        -- skip
467                                    elseif objecttype == "start_group" then
468                                        if lpdf.flushgroup then
469                                            local before, after = processplugins(object)
470                                            if before then
471                                                result[#result+1] = "q"
472                                                result = pluginactions(before,result,flushfigure)
473                                                insert(groupstack, {
474                                                    after  = after,
475                                                    result = result,
476                                                    bbox   = toboundingbox(object.path),
477                                                })
478                                                result = { }
479                                                miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false
480                                            else
481                                                insert(groupstack,false)
482                                            end
483                                        else
484                                            insert(groupstack,false)
485                                        end
486                                    elseif objecttype == "stop_group" then
487                                        local data = remove(groupstack)
488                                        if data then
489                                            local reference = lpdf.flushgroup(concat(result,"\r"),data.bbox)
490                                            result = data.result
491                                            result[#result+1] = reference
492                                            result = pluginactions(data.after,result,flushfigure)
493                                            result[#result+1] = "Q"
494                                            miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false
495                                        end
496                                    else
497                                        -- we use an indirect table as we want to overload
498                                        -- entries but this is not possible in userdata
499                                        --
500                                        -- can be optimized if no path
501                                        --
502                                        local original = object
503                                        local object   = { }
504                                        setmetatable(object, {
505                                            __index = original
506                                        })
507                                        local before,
508                                              after      = processplugins(object)
509                                        local evenodd    = false
510                                        local collect    = false
511                                        local both       = false
512                                        local flush      = false
513                                        local postscript = object.postscript
514                                        if not object.istext then
515                                            if postscript == "evenodd" then
516                                                evenodd = true
517                                            elseif postscript == "collect" then
518                                                collect = true
519                                            elseif postscript == "flush" then
520                                                flush   = true
521                                            elseif postscript == "both" then
522                                                both = true
523                                            elseif postscript == "eoboth" then
524                                                evenodd = true
525                                                both    = true
526                                            end
527                                        end
528                                        --
529                                        if flush and not savedpath then
530                                            -- forget about it
531                                        elseif collect then
532                                            if not savedpath then
533                                                savedpath = { object.path or false }
534                                                savedhtap = { object.htap or false }
535                                            else
536                                                savedpath[#savedpath+1] = object.path or false
537                                                savedhtap[#savedhtap+1] = object.htap or false
538                                            end
539                                        else
540                                            local objecttype = object.type -- can have changed
541                                            if before then
542                                                result = pluginactions(before,result,flushfigure)
543                                            end
544                                            local ml = object.miterlimit
545                                            if ml and ml ~= miterlimit then
546                                                miterlimit = ml
547                                                result[#result+1] = f_M(ml)
548                                            end
549                                            local lj = object.linejoin
550                                            if lj and lj ~= linejoin then
551                                                linejoin = lj
552                                                result[#result+1] = f_j(lj)
553                                            end
554                                            local lc = object.linecap
555                                            if lc and lc ~= linecap then
556                                                linecap = lc
557                                                result[#result+1] = f_J(lc)
558                                            end
559                                            if both then
560                                                if dashed ~= false then -- was just dashed test
561                                                   result[#result+1] = "[] 0 d"
562                                                   dashed = false
563                                                end
564                                            else
565                                                local dl = object.dash
566                                                if dl then
567                                                    local d = f_d(concat(dl.dashes or {}," "),dl.offset)
568                                                    if d ~= dashed then
569                                                        dashed = d
570                                                        result[#result+1] = d
571                                                    end
572                                                elseif dashed ~= false then -- was just dashed test
573                                                   result[#result+1] = "[] 0 d"
574                                                   dashed = false
575                                                end
576                                            end
577                                            local path        = object.path -- newpath
578                                            local transformed = false
579                                            local penwidth    = 1
580                                            local open        = path and path[1].left_type and path[#path].right_type -- at this moment only "end_point"
581                                            local pen         = object.pen
582                                            if pen then
583                                               if pen.type == "elliptical" then
584                                                    transformed, penwidth = pen_characteristics(original) -- boolean, value
585                                                    if penwidth ~= linewidth then
586                                                        result[#result+1] = f_w(penwidth)
587                                                        linewidth = penwidth
588                                                    end
589                                                    if objecttype == "fill" then
590                                                        objecttype = "both"
591                                                    end
592                                               else -- calculated by mplib itself
593                                                    objecttype = "fill"
594                                               end
595                                            end
596                                            if transformed then
597                                                result[#result+1] = "q"
598                                            end
599                                            if path then
600                                                if savedpath then
601                                                    for i=1,#savedpath do
602                                                        local path = savedpath[i]
603                                                        if transformed then
604                                                            flushconcatpath(path,result,open,i==1)
605                                                        else
606                                                            flushnormalpath(path,result,open)
607                                                        end
608                                                    end
609                                                    savedpath = nil
610                                                end
611                                                if flush then
612                                                    -- ignore this path
613                                                elseif transformed then
614                                                    flushconcatpath(path,result,open,true)
615                                                else
616                                                    flushnormalpath(path,result,open)
617                                                end
618                                                if force_stroke then
619                                                    result[#result+1] = open and "S" or "h S"
620                                                elseif objecttype == "fill" then
621                                                    result[#result+1] = evenodd and "h f*" or "h f" -- f* = eo
622                                                elseif objecttype == "outline" then
623                                                    if both then
624                                                        result[#result+1] = evenodd and "h B*" or "h B" -- B* = eo
625                                                    else
626                                                        result[#result+1] = open and "S" or "h S"
627                                                    end
628                                                elseif objecttype == "both" then
629                                                    result[#result+1] = evenodd and "h B*" or "h B" -- B* = eo -- b includes closepath
630                                                end
631                                            end
632                                            if transformed then
633                                                result[#result+1] = "Q"
634                                            end
635                                            local path = object.htap
636                                            if path then
637                                                if transformed then
638                                                    result[#result+1] = "q"
639                                                end
640                                                if savedhtap then
641                                                    for i=1,#savedhtap do
642                                                        local path = savedhtap[i]
643                                                        if transformed then
644                                                            flushconcatpath(path,result,open,i==1)
645                                                        else
646                                                            flushnormalpath(path,result,open)
647                                                        end
648                                                    end
649                                                    savedhtap = nil
650                                                    evenodd   = true
651                                                end
652                                                if transformed then
653                                                    flushconcatpath(path,result,open,true)
654                                                else
655                                                    flushnormalpath(path,result,open)
656                                                end
657                                                if force_stroke then
658                                                    result[#result+1] = open and "S" or "h S"
659                                                elseif objecttype == "fill" then
660                                                    result[#result+1] = evenodd and "h f*" or "h f" -- f* = eo
661                                                elseif objecttype == "outline" then
662                                                    result[#result+1] = open and "S" or "h S"
663                                                elseif objecttype == "both" then
664                                                    result[#result+1] = evenodd and "h B*" or "h B" -- B* = eo -- b includes closepath
665                                                end
666                                                if transformed then
667                                                    result[#result+1] = "Q"
668                                                end
669                                            end
670                                            if after then
671                                                result = pluginactions(after,result,flushfigure)
672                                            end
673                                        end
674                                        if object.grouped then
675                                            -- can be qQ'd so changes can end up in groups
676                                            miterlimit, linecap, linejoin, dashed, linewidth = -1, -1, -1, "", false
677                                        end
678                                    end
679                                end
680                            end
681                            result[#result+1] = "Q"
682                            flushfigure(result)
683                        end
684                        startfigure(properties.number,llx,lly,urx,ury,"begin",figure)
685                        if incontext then
686                            context(function() processfigure() end)
687                        else
688                            processfigure()
689                        end
690                        stopfigure("end")
691
692                    end
693                    if askedfig ~= "all" then
694                        break
695                    end
696                end
697                popproperties()
698            end
699            metapost.comment = nocomment
700            resetplugins(result) -- we should move the colorinitializer here
701        end
702    end
703end
704
705-- tracing:
706
707do
708
709    local result = { }
710
711    local flusher = {
712        startfigure = function()
713            result = { }
714            context.startnointerference()
715        end,
716        flushfigure = function(literals)
717            local n = #result
718            for i=1,#literals do
719                result[n+i] = literals[i]
720            end
721        end,
722        stopfigure = function()
723            context.stopnointerference()
724        end
725    }
726
727    local specification = {
728        flusher   = flusher,
729     -- incontext = true,
730    }
731
732    function metapost.pdfliterals(result)
733        metapost.flush(specification,result)
734        return result
735    end
736
737end
738
739function metapost.totable(result,askedfig)
740    local askedfig = askedfig or 1
741    local figure   = result and result.fig and result.fig[1]
742    if figure then
743        local results = { }
744        local objects = getobjects(result,figure,askedfig)
745        for o=1,#objects do
746            local object = objects[o]
747            local result = { }
748            local fields = getfields(object) -- hm, is this the whole list, if so, we can get it once
749            for f=1,#fields do
750                local field   = fields[f]
751                result[field] = object[field]
752            end
753            results[o] = result
754        end
755        local boundingbox = figure:boundingbox()
756        return {
757            boundingbox = {
758                llx = boundingbox[1],
759                lly = boundingbox[2],
760                urx = boundingbox[3],
761                ury = boundingbox[4],
762            },
763            objects = results
764        }
765    else
766        return nil
767    end
768end
769