spac-prf.lua /size: 26 Kb    last modification: 2021-10-28 13:50
1
 if not modules then modules = { } end modules ['spac-prf'] = {
2    version   = 1.001,
3    comment   = "companion to spac-prf.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
9-- This is a playground, a byproduct of some experiments in a project where
10-- we needed something like this where it works ok, but nevertheless it's
11-- still experimental code. It is very likely to change (or extended).
12
13local unpack, rawget = unpack, rawget
14
15local formatters        = string.formatters
16
17local nodecodes         = nodes.nodecodes
18local gluecodes         = nodes.gluecodes
19local listcodes         = nodes.listcodes
20
21local glyph_code        = nodecodes.glyph
22local disc_code         = nodecodes.disc
23local kern_code         = nodecodes.kern
24local penalty_code      = nodecodes.penalty
25local glue_code         = nodecodes.glue
26local hlist_code        = nodecodes.hlist
27local vlist_code        = nodecodes.vlist
28local unset_code        = nodecodes.unset
29local math_code         = nodecodes.math
30local rule_code         = nodecodes.rule
31local marginkern_code   = nodecodes.marginkern
32
33local leaders_code      = gluecodes.leaders
34local lineskip_code     = gluecodes.lineskip
35local baselineskip_code = gluecodes.baselineskip
36
37local linelist_code     = listcodes.line
38
39local texlists          = tex.lists
40local settexattribute   = tex.setattribute
41
42local nuts              = nodes.nuts
43local tonut             = nodes.tonut
44local tonode            = nuts.tonode
45
46local getreplace        = nuts.getreplace
47local getattr           = nuts.getattr
48local getid             = nuts.getid
49local getnext           = nuts.getnext
50local getprev           = nuts.getprev
51local getsubtype        = nuts.getsubtype
52local getlist           = nuts.getlist
53local gettexbox         = nuts.getbox
54local getwhd            = nuts.getwhd
55local getglue           = nuts.getglue
56local getkern           = nuts.getkern
57local getshift          = nuts.getshift
58local getwidth          = nuts.getwidth
59local getheight         = nuts.getheight
60local getdepth          = nuts.getdepth
61local getboxglue        = nuts.getboxglue
62
63local setlink           = nuts.setlink
64local setlist           = nuts.setlist
65local setattr           = nuts.setattr
66local setwhd            = nuts.setwhd
67local setshift          = nuts.setshift
68local setwidth          = nuts.setwidth
69local setheight         = nuts.setheight
70local setdepth          = nuts.setdepth
71
72local properties        = nodes.properties.data
73local setprop           = nuts.setprop
74local getprop           = nuts.getprop
75local theprop           = nuts.theprop
76
77local floor             = math.floor
78local ceiling           = math.ceil
79
80local new_rule          = nuts.pool.rule
81local new_glue          = nuts.pool.glue
82local new_kern          = nuts.pool.kern
83local hpack_nodes       = nuts.hpack
84local find_node_tail    = nuts.tail
85local setglue           = nuts.setglue
86
87local a_visual          = attributes.private("visual")
88local a_snapmethod      = attributes.private("snapmethod")
89local a_profilemethod   = attributes.private("profilemethod")
90----- a_specialcontent  = attributes.private("specialcontent")
91
92local variables         = interfaces.variables
93local v_none            = variables.none
94local v_fixed           = variables.fixed
95local v_strict          = variables.strict
96
97local setcolor          = nodes.tracers.colors.set
98local settransparency   = nodes.tracers.transparencies.set
99
100local enableaction      = nodes.tasks.enableaction
101
102local profiling         = { }
103builders.profiling      = profiling
104
105local report            = logs.reporter("profiling")
106
107local show_profile      = false  trackers.register("profiling.show", function(v) show_profile  = v end)
108local trace_profile     = false  trackers.register("profiling.trace",function(v) trace_profile = v end)
109
110local function getprofile(line,step)
111
112    -- only l2r
113    -- no hz yet
114
115    local line    = tonut(line)
116    local current = getlist(line)
117
118    if not current then
119        return
120    end
121
122    local glue_set, glue_order, glue_sign  = getboxglue(line)
123
124    local heights  = { }
125    local depths   = { }
126    local width    = 0
127    local position = 0
128    local step     = step or 65536 -- * 2 -- 2pt
129    local margin   = step / 4
130    local min      = 0
131    local max      = ceiling(getwidth(line)/step) + 1
132    local wd       = 0
133    local ht       = 0
134    local dp       = 0
135
136    for i=min,max do
137        heights[i] = 0
138        depths [i] = 0
139    end
140
141    -- remember p
142
143    local function progress()
144        position = width
145        width    = position + wd
146            p = floor((position - margin)/step + 0.5)
147            w = floor((width    + margin)/step - 0.5)
148        if p < 0 then
149            p = 0
150        end
151        if w < 0 then
152            w = 0
153        end
154        if p > w then
155            w, p = p, w
156        end
157        if w > max then
158            for i=max+1,w+1 do
159                heights[i] = 0
160                depths [i] = 0
161            end
162            max = w
163        end
164        for i=p,w do
165            if ht > heights[i] then
166                heights[i] = ht
167            end
168            if dp > depths[i] then
169                depths[i] = dp
170            end
171        end
172    end
173
174    local function process(current) -- called nested in disc replace
175        while current do
176            local id = getid(current)
177            if id == glyph_code then
178                wd, ht, dp = getwhd(current)
179                progress()
180            elseif id == kern_code then
181                wd = getkern(current)
182                ht = 0
183                dp = 0
184                progress()
185            elseif id == disc_code then
186                local replace = getreplace(current)
187                if replace then
188                    process(replace)
189                end
190            elseif id == glue_code then
191                local width, stretch, shrink, stretch_order, shrink_order = getglue(current)
192                if glue_sign == 1 then
193                    if stretch_order == glue_order then
194                        wd = width + stretch * glue_set
195                    else
196                        wd = width
197                    end
198                elseif glue_sign == 2 then
199                    if shrink_order == glue_order then
200                        wd = width - shrink * glue_set
201                    else
202                        wd = width
203                    end
204                else
205                    wd = width
206                end
207                if getsubtype(current) >= leaders_code then
208                    local leader = getleader(current)
209                    local w
210                    w, ht, dp = getwhd(leader) -- can become getwhd(current) after 1.003
211                else
212                    ht = 0
213                    dp = 0
214                end
215                progress()
216            elseif id == hlist_code then
217                -- we could do a nested check .. but then we need to push / pop glue
218                local shift = getshift(current)
219                local w, h, d = getwhd(current)
220             -- if getattr(current,a_specialcontent) then
221                if getprop(current,"specialcontent") then
222                    -- like a margin note, maybe check for wd
223                    wd = w
224                    ht = 0
225                    dp = 0
226                else
227                    wd = w
228                    ht = h - shift
229                    dp = d + shift
230                end
231                progress()
232            elseif id == vlist_code or id == unset_code then
233                local shift = getshift(current) -- todo
234                wd, ht, dp = getwhd(current)
235                progress()
236            elseif id == rule_code then
237                wd, ht, dp = getwhd(current)
238                progress()
239            elseif id == math_code then
240                wd = getkern(current) + getwidth(current) -- surround
241                ht = 0
242                dp = 0
243                progress()
244            elseif id == marginkern_code then
245                -- not in lmtx
246                wd = getwidth(current)
247                ht = 0
248                dp = 0
249                progress()
250            else
251--     print(nodecodes[id])
252            end
253            current = getnext(current)
254        end
255    end
256
257    process(current)
258
259    return {
260        heights = heights,
261        depths  = depths,
262        min     = min, -- not needed
263        max     = max,
264        step    = step,
265    }
266
267end
268
269profiling.get = getprofile
270
271local function getpagelist()
272    local pagehead = texlists.page_head
273    if pagehead then
274        pagehead = tonut(texlists.page_head)
275        pagetail = find_node_tail(pagehead)
276    else
277        pagetail = nil
278    end
279    return pagehead, pagetail
280end
281
282local function setprofile(n,step)
283    local p = rawget(properties,n)
284    if p then
285        local pp = p.profile
286        if not pp then
287            pp = getprofile(n,step)
288            p.profile = pp
289        end
290        return pp
291    else
292        local pp = getprofile(n,step)
293        properties[n] = { profile = pp }
294        return pp
295    end
296end
297
298local function hasprofile(n)
299    local p = rawget(properties,n)
300    if p then
301        return p.profile
302    end
303end
304
305local function addstring(height,depth)
306    local typesetters = nuts.typesetters
307    local hashes   = fonts.hashes
308    local infofont = fonts.infofont()
309    local emwidth  = hashes.emwidths [infofont]
310    local exheight = hashes.exheights[infofont]
311    local httext   = height
312    local dptext   = depth
313    local httext   = typesetters.tohpack(height,infofont)
314    local dptext   = typesetters.tohpack(depth,infofont)
315    setshift(httext,- 1.2 * exheight)
316    setshift(dptext,  0.6 * exheight)
317    local text = hpack_nodes(setlink(
318        new_kern(-getwidth(httext)-emwidth),
319        httext,
320        new_kern(-getwidth(dptext)),
321        dptext
322    ))
323    setwhd(text,0,0,0)
324    return text
325end
326
327local function addprofile(node,profile,step)
328
329    local line = tonut(node)
330
331    if not profile then
332        profile = setprofile(line,step)
333    end
334
335    if not profile then
336        report("some error")
337        return node
338    end
339
340    if profile.shown then
341        return node
342    end
343
344    local list    = getlist(line)
345    profile.shown = true
346
347    local heights = profile.heights
348    local depths  = profile.depths
349    local step    = profile.step
350
351    local head    = nil
352    local tail    = nil
353
354    local lastht  = 0
355    local lastdp  = 0
356    local lastwd  = 0
357
358    local visual  = "f:s:t" -- this can change !
359
360    local function progress()
361        if lastwd == 0 then
362            return
363        end
364        local what = nil
365        if lastht == 0 and lastdp == 0 then
366            what = new_kern(lastwd)
367        else
368            what = new_rule(lastwd,lastht,lastdp)
369            setcolor(what,visual)
370            settransparency(what,visual)
371        end
372        if tail then
373            setlink(tail,what)
374        else
375            head = what
376        end
377        tail = what
378    end
379
380-- inspect(profile)
381
382    for i=profile.min,profile.max do
383        local ht = heights[i]
384        local dp = depths[i]
385        if ht ~= lastht or dp ~= lastdp and lastwd > 0 then
386            progress()
387            lastht = ht
388            lastdp = dp
389            lastwd = step
390        else
391            lastwd = lastwd + step
392        end
393    end
394    if lastwd > 0 then
395        progress()
396    end
397
398    local rule = hpack_nodes(head)
399
400    setwhd(rule,0,0,0)
401
402 -- if texttoo then
403 --
404 --     local text = addstring(
405 --         formatters["%0.4f"](getheight(rule)/65536),
406 --         formatters["%0.4f"](getdepth(rule) /65536)
407 --     )
408 --
409 --     setlink(text,rule)
410 --
411 --     rule = text
412 --
413 -- end
414
415    setlink(rule,list)
416    setlist(line,rule)
417
418end
419
420profiling.add = addprofile
421
422local methods = { }
423
424local function getdelta(t_profile,b_profile)
425    local t_heights  = t_profile.heights
426    local t_depths   = t_profile.depths
427    local t_max      = t_profile.max
428    local b_heights  = b_profile.heights
429    local b_depths   = b_profile.depths
430    local b_max      = b_profile.max
431
432    local max        = t_max
433    local delta      = 0
434
435    if t_max > b_max then
436        for i=b_max+1,t_max do
437            b_depths [i] = 0
438            b_heights[i] = 0
439        end
440        max = t_max
441    elseif b_max > t_max then
442        for i=t_max+1,b_max do
443            t_depths [i] = 0
444            t_heights[i] = 0
445        end
446        max = b_max
447    end
448
449    for i=0,max do
450        local ht = b_heights[i]
451        local dp = t_depths[i]
452        local hd = ht + dp
453        if hd > delta then
454            delta = hd
455        end
456    end
457
458    return delta
459end
460
461-- local properties = theprop(bot)
462-- local unprofiled = properties.unprofiled
463-- if not unprofiled then -- experiment
464--     properties.unprofiled = {
465--         height  = height,
466--         strutht = strutht,
467--     }
468-- end
469
470-- lineskip | lineskiplimit
471
472local function inject(top,bot,amount) -- todo: look at penalties
473    local glue = new_glue(amount)
474    --
475    setattr(glue,a_profilemethod,0)
476    setattr(glue,a_visual,getattr(top,a_visual))
477    --
478    setlink(top,glue,bot)
479end
480
481methods[v_none] = function()
482    return false
483end
484
485methods[v_strict] = function(top,bot,t_profile,b_profile,specification)
486
487    local top        = tonut(top)
488    local bot        = tonut(bot)
489
490    local strutht    = specification.height or texdimen.strutht
491    local strutdp    = specification.depth  or texdimen.strutdp
492    local lineheight = strutht + strutdp
493
494    local depth      = getdepth(top)
495    local height     = getheight(bot)
496    local total      = depth + height
497    local distance   = specification.distance or 0
498    local delta      = lineheight - total
499
500    -- there is enough room between the lines so we don't need
501    -- to add extra distance
502
503    if delta >= distance then
504        inject(top,bot,delta)
505        return true
506    end
507
508    local delta = getdelta(t_profile,b_profile)
509    local skip  = delta - total + distance
510
511    -- we don't want to be too tight so we limit the skip and
512    -- make sure we have at least lineheight
513
514    inject(top,bot,skip)
515    return true
516
517end
518
519-- todo: also set ht/dp of first / last (but what is that)
520
521methods[v_fixed] = function(top,bot,t_profile,b_profile,specification)
522
523    local top        = tonut(top)
524    local bot        = tonut(bot)
525
526    local strutht    = specification.height or texdimen.strutht
527    local strutdp    = specification.depth  or texdimen.strutdp
528    local lineheight = strutht + strutdp
529
530    local depth      = getdepth(top)
531    local height     = getheight(bot)
532    local total      = depth + height
533    local distance   = specification.distance or 0
534    local delta      = lineheight - total
535
536    local snapmethod = getattr(top,a_snapmethod)
537
538    if snapmethod then
539
540        -- no distance (yet)
541
542        if delta < lineheight then
543            setdepth(top,strutdp)
544            setheight(bot,strutht)
545            return true
546        end
547
548        local delta  = getdelta(t_profile,b_profile)
549
550        local dp = strutdp
551        while depth > lineheight - strutdp do
552            depth = depth - lineheight
553            dp = dp + lineheight
554        end
555        setdepth(top,dp)
556        local ht = strutht
557        while height > lineheight - strutht do
558            height = height - lineheight
559            ht = ht + lineheight
560        end
561        setheight(bot,ht)
562        local lines = floor(delta/lineheight)
563        if lines > 0 then
564            inject(top,bot,-lines * lineheight)
565        end
566
567        return true
568
569    end
570
571    if total < lineheight then
572        setdepth(top,strutdp)
573        setheight(bot,strutht)
574        return true
575    end
576
577    if depth < strutdp then
578        setdepth(top,strutdp)
579        total = total - depth + strutdp
580    end
581    if height < strutht then
582        setheight(bot,strutht)
583        total = total - height + strutht
584    end
585
586    local delta      = getdelta(t_profile,b_profile)
587
588    local target     = total - delta
589    local factor     = specification.factor or 1
590    local step       = lineheight / factor
591    local correction = 0
592    local nofsteps   = 0
593    while correction < target - step - distance do -- a loop is more accurate, for now
594        correction = correction + step
595        nofsteps   = nofsteps + 1
596    end
597
598    if trace_profile then
599        report("top line     : %s %05i > %s",t_profile.shown and "+" or "-",top,nodes.toutf(getlist(top)))
600        report("bottom line  : %s %05i > %s",b_profile.shown and "+" or "-",bot,nodes.toutf(getlist(bot)))
601        report("  depth      : %p",depth)
602        report("  height     : %p",height)
603        report("  total      : %p",total)
604        report("  lineheight : %p",lineheight)
605        report("  delta      : %p",delta)
606        report("  target     : %p",target)
607        report("  factor     : %i",factor)
608        report("  distance   : %p",distance)
609        report("  step       : %p",step)
610        report("  nofsteps   : %i",nofsteps)
611     -- report("  max lines  : %s",lines == 0 and "unset" or lines)
612        report("  correction : %p",correction)
613    end
614
615    inject(top,bot,-correction) -- we could mess with the present glue (if present)
616
617    return true -- remove interlineglue
618
619end
620
621function profiling.distance(top,bot,specification)
622    local step   = specification.step
623    local method = specification.method
624    local ptop   = getprofile(top,step)
625    local pbot   = getprofile(bot,step)
626    local action = methods[method or v_strict] or methods[v_strict]
627    return action(top,bot,ptop,pbot,specification)
628end
629
630local specifications = { } -- todo: save these !
631
632function profiling.fixedprofile(current)
633    local a = getattr(current,a_profilemethod)
634    if a then
635        local s = specifications[a]
636        if s then
637            return s.method == v_fixed
638        end
639    end
640    return false
641end
642
643local function profilelist(line,mvl)
644
645    local current       = line
646
647    local top           = nil
648    local bot           = nil
649
650    local t_profile     = nil
651    local b_profile     = nil
652
653    local specification = nil
654    local lastattr      = nil
655    local method        = nil
656    local action        = nil
657
658    local distance      = 0
659    local lastglue      = nil
660
661    local pagehead      = nil
662    local pagetail      = nil
663
664    if mvl then
665
666        pagehead, pagetail = getpagelist()
667
668        if pagetail then
669            local current = pagetail
670            while current do
671                local id = getid(current)
672                if id == hlist_code then
673                    local subtype = getsubtype(current)
674                    if subtype == linelist_code then
675                        t_profile = hasprofile(current)
676                        if t_profile then
677                            top = current
678                        end
679                    end
680                    break
681                elseif id == glue_code then
682                    local wd = getwidth(current)
683                    if not wd or wd == 0 then
684                        -- go on
685                    else
686                        break
687                    end
688                elseif id == penalty_code then
689                    -- ok
690                else
691                    break
692                end
693                current = getnext(current)
694            end
695        end
696
697    end
698
699    while current do
700
701        local attr = getattr(current,a_profilemethod)
702
703        if attr then
704
705            if attr ~= lastattr then
706                specification = specifications[attr]
707                method        = specification and specification.method
708                action        = method and methods[method] or methods[v_strict]
709                lastattr      = attr
710            end
711
712            local id = getid(current)
713
714            if id == hlist_code then -- check subtype
715                local subtype = getsubtype(current)
716                if subtype == linelist_code then
717                    if top == current then
718                        -- skip
719                        bot = nil -- to be sure
720                    elseif top then
721                        bot       = current
722                        b_profile = setprofile(bot)
723                        if show_profile then
724                            addprofile(bot,b_profile)
725                        end
726                        if not t_profile.done then
727                            if action then
728                                local ok = action(top,bot,t_profile,b_profile,specification)
729                                if ok and lastglue and distance ~= 0 then
730                                    setglue(lastglue)
731                                end
732                            end
733                            t_profile.done = true
734                        end
735                        top       = bot
736                        bot       = nil
737                        t_profile = b_profile
738                        b_profile = nil
739                        distance  = 0
740                    else
741                        top       = current
742                        t_profile = setprofile(top)
743                        bot       = nil
744                        if show_profile then
745                            addprofile(top,t_profile)
746                        end
747                    end
748                else
749                    top = nil
750                    bot = nil
751                end
752            elseif id == glue_code then
753                if top then
754                    local subtype = getsubtype(current)
755                 -- if subtype == lineskip_code or subtype == baselineskip_code then
756                        local wd   = getwidth(current)
757                        if wd > 0 then
758                            distance = wd
759                            lastglue = current
760                        elseif wd < 0 then
761                            top = nil
762                            bot = nil
763                        else
764                            -- ok
765                        end
766                 -- else
767                 --     top = nil
768                 --     bot = nil
769                 -- end
770                else
771                    top = nil
772                    bot = nil
773                end
774            elseif id == penalty_code then
775                -- okay
776            else
777                top = nil
778                bot = nil
779            end
780        else
781            top = nil
782            bot = nil
783        end
784        current = getnext(current)
785    end
786    if top then
787        t_profile = setprofile(top)
788        if show_profile then
789            addprofile(top,t_profile)
790        end
791    end
792end
793
794profiling.list = profilelist
795
796local enabled = false
797
798function profiling.set(specification)
799    if not enabled then
800        enableaction("mvlbuilders", "builders.profiling.pagehandler")
801     -- too expensive so we expect that this happens explicitly, we keep for reference:
802     -- enableaction("vboxbuilders","builders.profiling.vboxhandler")
803        enabled = true
804    end
805    local n = #specifications + 1
806    specifications[n] = specification
807    settexattribute(a_profilemethod,n)
808end
809
810function profiling.profilebox(specification)
811    local boxnumber = specification.box
812    local current   = getlist(gettexbox(boxnumber))
813    local top       = nil
814    local bot       = nil
815    local t_profile = nil
816    local b_profile = nil
817    local method    = specification and specification.method
818    local action    = method and methods[method] or methods[v_strict]
819    local lastglue  = nil
820    local distance  = 0
821    while current do
822        local id = getid(current)
823        if id == hlist_code then
824            local subtype = getsubtype(current)
825            if subtype == linelist_code then
826                if top then
827                    bot       = current
828                    b_profile = setprofile(bot)
829                    if show_profile then
830                        addprofile(bot,b_profile)
831                    end
832                    if not t_profile.done then
833                        if action then
834                            local ok = action(top,bot,t_profile,b_profile,specification)
835                            if ok and lastglue and distance ~= 0 then
836                                setglue(lastglue)
837                            end
838                        end
839                        t_profile.done = true
840                    end
841                    top       = bot
842                    t_profile = b_profile
843                    b_profile = nil
844                    distance  = 0
845                else
846                    top       = current
847                    t_profile = setprofile(top)
848                    if show_profile then
849                        addprofile(top,t_profile)
850                    end
851                    bot       = nil
852                end
853            else
854                top = nil
855                bot = nil
856            end
857        elseif id == glue_code then
858            local subtype = getsubtype(current)
859            if subtype == lineskip_code or subtype == baselineskip_code then
860                if top then
861                    local wd   = getwidth(current)
862                    if wd > 0 then
863                        distance = wd
864                        lastglue = current
865                    elseif wd < 0 then
866                        top = nil
867                        bot = nil
868                    else
869                        -- ok
870                    end
871                else
872                    top = nil
873                    bot = nil
874                end
875            else
876                top = nil
877                bot = nil
878            end
879        elseif id == penalty_code then
880            -- okay
881        else
882            top = nil
883            bot = nil
884        end
885        current = getnext(current)
886    end
887
888    if top then
889        t_profile = setprofile(top) -- not needed
890        if show_profile then
891            addprofile(top,t_profile)
892        end
893    end
894
895end
896
897-- local ignore = table.tohash {
898--     "split_keep",
899--     "split_off",
900--  -- "vbox",
901-- }
902--
903-- function profiling.vboxhandler(head,where)
904--     if head and not ignore[where] then
905--         if getnext(head) then
906--             profilelist(head)
907--         end
908--     end
909--     return head
910-- end
911
912function profiling.pagehandler(head)
913    if head then
914        profilelist(head,true)
915    end
916    return head
917end
918
919interfaces.implement {
920    name      = "setprofile",
921    actions   = profiling.set,
922    arguments = {
923        {
924            { "name" },
925            { "height", "dimen" },
926            { "depth", "dimen" },
927            { "distance", "dimen" },
928            { "factor", "integer" },
929            { "lines", "integer" },
930            { "method" }
931        }
932    }
933}
934
935interfaces.implement {
936    name      = "profilebox",
937    actions   = profiling.profilebox,
938    arguments = {
939        {
940            { "box", "integer" },
941            { "height", "dimen" },
942            { "depth", "dimen" },
943            { "distance", "dimen" },
944            { "factor", "integer" },
945            { "lines", "integer" },
946            { "method" }
947        }
948    }
949}
950