spac-prf.lmt /size: 40 Kb    last modification: 2025-02-21 11:03
1if 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
19
20local glyph_code        <const> = nodecodes.glyph
21local disc_code         <const> = nodecodes.disc
22local kern_code         <const> = nodecodes.kern
23local penalty_code      <const> = nodecodes.penalty
24local glue_code         <const> = nodecodes.glue
25local hlist_code        <const> = nodecodes.hlist
26local vlist_code        <const> = nodecodes.vlist
27local unset_code        <const> = nodecodes.unset
28local math_code         <const> = nodecodes.math
29local rule_code         <const> = nodecodes.rule
30local marginkern_code   <const> = nodecodes.marginkern
31
32local leaders_code      <const> = gluecodes.leaders
33local lineskip_code     <const> = gluecodes.lineskip
34local baselineskip_code <const> = gluecodes.baselineskip
35
36local strutrule_code    <const> = nodes.rulecodes.strut
37local linelist_code     <const> = nodes.listcodes.line
38
39local texlists          = tex.lists
40local settexattribute   = tex.setattribute
41local texgetdimen       = tex.getdimen
42
43local d_strutht <const> = tex.isdimen("strutht")
44local d_strutdp <const> = tex.isdimen("strutdp")
45
46local newindex          = lua.newindex
47
48local nuts              = nodes.nuts
49local tonut             = nodes.tonut
50local tonode            = nuts.tonode
51
52local getreplace        = nuts.getreplace
53local getattr           = nuts.getattr
54local getid             = nuts.getid
55local getboth           = nuts.getboth
56local getnext           = nuts.getnext
57local getprev           = nuts.getprev
58local getsubtype        = nuts.getsubtype
59local getlist           = nuts.getlist
60local gettexbox         = nuts.getbox
61local getwhd            = nuts.getwhd
62local getglue           = nuts.getglue
63local getkern           = nuts.getkern
64local getshift          = nuts.getshift
65local getwidth          = nuts.getwidth
66local getheight         = nuts.getheight
67local getdepth          = nuts.getdepth
68local getboxglue        = nuts.getboxglue
69local effectiveglue     = nuts.effectiveglue
70local findattribute     = nuts.findattribute
71local getspeciallist    = nuts.getspeciallist
72
73local getlistdimensions = nuts.getlistdimensions
74
75local nextnode          = nuts.traversers.node
76local nextglue          = nuts.traversers.glue
77
78local setlink           = nuts.setlink
79local setlist           = nuts.setlist
80local setattr           = nuts.setattr
81local setwhd            = nuts.setwhd
82local setshift          = nuts.setshift
83local setwidth          = nuts.setwidth
84local setheight         = nuts.setheight
85local setdepth          = nuts.setdepth
86
87local properties        = nodes.properties.data
88local setprop           = nuts.setprop
89local getprop           = nuts.getprop
90local theprop           = nuts.theprop
91
92local floor             = math.floor
93local ceiling           = math.ceil
94local min               = math.min
95local max               = math.max
96
97local new_rule          = nuts.pool.rule
98local new_glue          = nuts.pool.glue
99local new_kern          = nuts.pool.kern
100local hpack_nodes       = nuts.hpack
101local find_node_tail    = nuts.tail
102local setglue           = nuts.setglue
103
104local a_visual          <const> = attributes.private("visual")
105local a_snapmethod      <const> = attributes.private("snapmethod")
106local a_profilemethod   <const> = attributes.private("profilemethod")
107----- a_specialcontent  <const> = attributes.private("specialcontent")
108
109local variables         = interfaces.variables
110local v_none            <const> = variables.none
111local v_fixed           <const> = variables.fixed
112local v_strict          <const> = variables.strict
113local v_yes             <const> = variables.yes
114
115local setcolor          = nodes.tracers.colors.set
116local settransparency   = nodes.tracers.transparencies.set
117
118local enableaction      = nodes.tasks.enableaction
119
120local profiling         = { }
121builders.profiling      = profiling
122
123local report            = logs.reporter("profiling")
124
125local show_profile      = false  trackers.register("profiling.show", function(v) show_profile  = v end)
126local trace_profile     = false  trackers.register("profiling.trace",function(v) trace_profile = v end)
127
128local function getprofile(line,step)
129
130    -- only l2r
131    -- no hz yet
132
133    local line    = tonut(line)
134    local current = getlist(line)
135
136    if not current then
137        return
138    end
139
140--     local glue_set, glue_order, glue_sign  = getboxglue(line)
141
142    local heights  = { }
143    local depths   = { }
144    local width    = 0
145    local position = 0
146    local step     = step or 65536 -- * 2 -- 2pt
147    local margin   = step / 4
148    local min      = 0
149    local max      = ceiling(getwidth(line)/step) + 1
150    local wd       = 0
151    local ht       = 0
152    local dp       = 0
153
154    for i=min,max do
155        heights[i] = 0
156        depths [i] = 0
157    end
158
159    -- remember p
160
161    local function process(current) -- called nested in disc replace
162        for current, id, subtype in nextnode, current do
163            if id == glyph_code then
164                wd, ht, dp = getwhd(current)
165            elseif id == kern_code then
166                wd = getkern(current)
167                ht = 0
168                dp = 0
169            elseif id == disc_code then
170                local replace = getreplace(current)
171                if replace then
172                    process(replace)
173                end
174                goto done
175            elseif id == glue_code then
176                wd = effectiveglue(current, line) -- geteffectivewhd
177                -- tricky
178                if subtype >= leaders_code then
179                    local leader = getleader(current)
180                    local w
181                    w, ht, dp = getwhd(leader) -- can become getwhd(current) after 1.003
182                else
183                    ht = 0
184                    dp = 0
185                end
186            elseif id == hlist_code then
187                -- maybe: offsets
188                -- we could do a nested check .. but then we need to push / pop glue
189                local shift = getshift(current)
190                local w, h, d = getwhd(current)
191                if getprop(current,"specialcontent") then
192                    -- like a margin note, maybe check for wd
193                    wd = w
194                    ht = 0
195                    dp = 0
196                else
197                    wd = w
198                    ht = h - shift
199                    dp = d + shift
200                end
201            elseif id == vlist_code or id == unset_code then
202                local shift = getshift(current) -- todo
203                wd, ht, dp = getwhd(current) -- todo: use combined getter
204            elseif id == rule_code then
205                wd, ht, dp = getwhd(current)
206            elseif id == math_code then
207                -- todo get glue
208                wd = getkern(current) + getwidth(current) -- surround
209                ht = 0
210                dp = 0
211            else
212                goto done
213            end
214            -- progress
215            position = width
216            width    = position + wd
217                p = floor((position - margin)/step + 0.5)
218                w = floor((width    + margin)/step - 0.5)
219            if p < 0 then
220                p = 0
221            end
222            if w < 0 then
223                w = 0
224            end
225            if p > w then
226                w, p = p, w
227            end
228            if w > max then
229                for i=max+1,w+1 do
230                    heights[i] = 0
231                    depths [i] = 0
232                end
233                max = w
234            end
235            for i=p,w do
236                if ht > heights[i] then
237                    heights[i] = ht
238                end
239                if dp > depths[i] then
240                    depths[i] = dp
241                end
242            end
243          ::done::
244        end
245    end
246
247    process(current)
248
249    return {
250        heights = heights,
251        depths  = depths,
252        min     = min, -- not needed
253        max     = max,
254        step    = step,
255    }
256
257end
258
259profiling.get = getprofile
260
261local function getpagelist()
262    local pagehead = texlists.pagehead
263    if pagehead then
264        pagehead = tonut(texlists.pagehead)
265        pagetail = find_node_tail(pagehead)
266    else
267        pagetail = nil
268    end
269    return pagehead, pagetail
270end
271
272local function setprofile(n,step)
273    local p = rawget(properties,n)
274    if p then
275        local pp = p.profile
276        if not pp then
277            pp = getprofile(n,step)
278            p.profile = pp
279        end
280        return pp
281    else
282        local pp = getprofile(n,step)
283        properties[n] = { profile = pp }
284        return pp
285    end
286end
287
288local function hasprofile(n)
289    local p = rawget(properties,n)
290    if p then
291        return p.profile
292    end
293end
294
295local function addstring(height,depth)
296    local typesetters = nuts.typesetters
297    local hashes   = fonts.hashes
298    local infofont = fonts.infofont()
299    local emwidth  = hashes.emwidths [infofont]
300    local exheight = hashes.exheights[infofont]
301    local httext   = height
302    local dptext   = depth
303    local httext   = typesetters.tohpack(height,infofont)
304    local dptext   = typesetters.tohpack(depth,infofont)
305    setshift(httext,- 1.2 * exheight)
306    setshift(dptext,  0.6 * exheight)
307    local text = hpack_nodes(setlink(
308        new_kern(-getwidth(httext)-emwidth),
309        httext,
310        new_kern(-getwidth(dptext)),
311        dptext
312    ))
313    setwhd(text,0,0,0)
314    return text
315end
316
317local function addprofile(node,profile,step)
318
319    local line = tonut(node)
320
321    if not profile then
322        profile = setprofile(line,step)
323    end
324
325    if not profile then
326        report("some error")
327        return node
328    end
329
330    if profile.shown then
331        return node
332    end
333
334    local list    = getlist(line)
335    profile.shown = true
336
337    local heights = profile.heights
338    local depths  = profile.depths
339    local step    = profile.step
340
341    local head    = nil
342    local tail    = nil
343
344    local lastht  = 0
345    local lastdp  = 0
346    local lastwd  = 0
347
348    local visual  = "f:s:t" -- this can change !
349
350    local function progress()
351        if lastwd == 0 then
352            return
353        end
354        local what = nil
355        -- beware: basically end of line so we actually need to put it elsewhere
356        if lastht == 0 and lastdp == 0 then
357            what = new_kern(lastwd)
358        else
359            what = new_rule(lastwd,lastht,lastdp)
360            setcolor(what,visual)
361            settransparency(what,visual)
362        end
363        if tail then
364            setlink(tail,what)
365        else
366            head = what
367        end
368        tail = what
369    end
370
371-- inspect(profile)
372
373    for i=profile.min,profile.max do
374        local ht = heights[i]
375        local dp = depths[i]
376        if ht ~= lastht or dp ~= lastdp and lastwd > 0 then
377            progress()
378            lastht = ht
379            lastdp = dp
380            lastwd = step
381        else
382            lastwd = lastwd + step
383        end
384    end
385    if lastwd > 0 then
386        progress()
387    end
388
389    local rule = hpack_nodes(head)
390
391    setwhd(rule,0,0,0)
392
393 -- if texttoo then
394 --
395 --     local text = addstring(
396 --         formatters["%0.4f"](getheight(rule)/65536),
397 --         formatters["%0.4f"](getdepth(rule) /65536)
398 --     )
399 --
400 --     setlink(text,rule)
401 --
402 --     rule = text
403 --
404 -- end
405
406    setlink(rule,list)
407    setlist(line,rule)
408
409end
410
411profiling.add = addprofile
412
413local methods = { }
414
415local function getdelta(t_profile,b_profile)
416    local t_heights  = t_profile.heights
417    local t_depths   = t_profile.depths
418    local t_max      = t_profile.max
419    local b_heights  = b_profile.heights
420    local b_depths   = b_profile.depths
421    local b_max      = b_profile.max
422
423    local max        = t_max
424    local delta      = 0
425
426    if t_max > b_max then
427        for i=b_max+1,t_max do
428            b_depths [i] = 0
429            b_heights[i] = 0
430        end
431        max = t_max
432    elseif b_max > t_max then
433        for i=t_max+1,b_max do
434            t_depths [i] = 0
435            t_heights[i] = 0
436        end
437        max = b_max
438    end
439
440    for i=0,max do
441        local ht = b_heights[i]
442        local dp = t_depths[i]
443        local hd = ht + dp
444        if hd > delta then
445            delta = hd
446        end
447    end
448
449    return delta
450end
451
452-- local properties = theprop(bot)
453-- local unprofiled = properties.unprofiled
454-- if not unprofiled then -- experiment
455--     properties.unprofiled = {
456--         height  = height,
457--         strutht = strutht,
458--     }
459-- end
460
461-- lineskip | lineskiplimit
462
463local function inject(top,bot,amount) -- todo: look at penalties
464    if amount ~= 0 then
465        local glue = new_glue(amount)
466        --
467        setattr(glue,a_profilemethod,0)
468    --     setattr(glue,a_visual,getattr(top,a_visual))
469        setattr(glue,a_visual,nodes.visualizers.modes.glue)
470        --
471        setlink(top,glue,bot)
472        --
473        report("injected correction %p at page",amount,tex.getcount("realpageno"))
474    end
475end
476
477methods[v_none] = function()
478    return false
479end
480
481methods[v_strict] = function(top,bot,t_profile,b_profile,specification)
482
483    local top        = tonut(top)
484    local bot        = tonut(bot)
485
486    local strutht    = specification.height or texgetdimen(d_strutht)
487    local strutdp    = specification.depth  or texgetdimen(d_strutdp)
488    local lineheight = strutht + strutdp
489
490    local depth      = getdepth(top)
491    local height     = getheight(bot)
492    local total      = depth + height
493    local distance   = specification.distance or 0
494    local delta      = lineheight - total
495
496    -- there is enough room between the lines so we don't need
497    -- to add extra distance
498
499    if delta >= distance then
500        inject(top,bot,delta)
501        return true
502    end
503
504    local delta = getdelta(t_profile,b_profile)
505    local skip  = delta - total + distance
506
507    -- we don't want to be too tight so we limit the skip and
508    -- make sure we have at least lineheight
509
510    inject(top,bot,skip)
511    return true
512
513end
514
515-- todo: also set ht/dp of first / last (but what is that)
516
517methods[v_fixed] = function(top,bot,t_profile,b_profile,specification)
518
519    local top        = tonut(top)
520    local bot        = tonut(bot)
521
522    local strutht    = specification.height or texgetdimen(d_strutht)
523    local strutdp    = specification.depth  or texgetdimen(d_strutdp)
524    local lineheight = strutht + strutdp
525
526    local depth      = getdepth(top)
527    local height     = getheight(bot)
528    local total      = depth + height
529    local distance   = specification.distance or 0
530    local delta      = lineheight - total
531
532    local snapmethod = getattr(top,a_snapmethod)
533
534    if snapmethod then
535
536        -- no distance (yet)
537
538        if delta < lineheight then
539            setdepth(top,strutdp)
540            setheight(bot,strutht)
541            return true
542        end
543
544        local delta = getdelta(t_profile,b_profile)
545
546        local dp = strutdp
547        while depth > lineheight - strutdp do
548            depth = depth - lineheight
549            dp = dp + lineheight
550        end
551        setdepth(top,dp)
552        local ht = strutht
553        while height > lineheight - strutht do
554            height = height - lineheight
555            ht = ht + lineheight
556        end
557        setheight(bot,ht)
558        local lines = floor(delta/lineheight)
559        if lines > 0 then
560            inject(top,bot,-lines * lineheight)
561        end
562
563        return true
564
565    end
566
567    if total < lineheight then
568        setdepth(top,strutdp)
569        setheight(bot,strutht)
570        return true
571    end
572
573    if depth < strutdp then
574        setdepth(top,strutdp)
575        total = total - depth + strutdp
576    end
577    if height < strutht then
578        setheight(bot,strutht)
579        total = total - height + strutht
580    end
581
582    local delta      = getdelta(t_profile,b_profile)
583
584    local target     = total - delta
585    local factor     = specification.factor or 1
586    local step       = lineheight / factor
587    local correction = 0
588    local nofsteps   = 0
589    while correction < target - step - distance do -- a loop is more accurate, for now
590        correction = correction + step
591        nofsteps   = nofsteps + 1
592    end
593
594    if trace_profile then
595        report("top line     : %s %05i > %s",t_profile.shown and "+" or "-",top,nodes.toutf(getlist(top)))
596        report("bottom line  : %s %05i > %s",b_profile.shown and "+" or "-",bot,nodes.toutf(getlist(bot)))
597        report("  depth      : %p",depth)
598        report("  height     : %p",height)
599        report("  total      : %p",total)
600        report("  lineheight : %p",lineheight)
601        report("  delta      : %p",delta)
602        report("  target     : %p",target)
603        report("  factor     : %i",factor)
604        report("  distance   : %p",distance)
605        report("  step       : %p",step)
606        report("  nofsteps   : %i",nofsteps)
607     -- report("  max lines  : %s",lines == 0 and "unset" or lines)
608        report("  correction : %p",correction)
609    end
610
611    inject(top,bot,-correction) -- we could mess with the present glue (if present)
612
613    return true -- remove interlineglue
614
615end
616
617function profiling.distance(top,bot,specification)
618    local step   = specification.step
619    local method = specification.method
620    local ptop   = getprofile(top,step)
621    local pbot   = getprofile(bot,step)
622    local action = methods[method or v_strict] or methods[v_strict]
623    return action(top,bot,ptop,pbot,specification)
624end
625
626local specifications = { } -- todo: save these !
627
628function profiling.fixedprofile(current)
629    local a = getattr(current,a_profilemethod)
630    if a then
631        local s = specifications[a]
632        if s then
633            return s.method == v_fixed
634        end
635    end
636    return false
637end
638
639local function profilelist(line,mvl)
640
641    local top           = nil
642    local bot           = nil
643
644    local t_profile     = nil
645    local b_profile     = nil
646
647    local specification = nil
648    local lastattr      = nil
649    local method        = nil
650    local action        = nil
651
652    local distance      = 0
653    local lastglue      = nil
654
655    local pagehead      = nil
656    local pagetail      = nil
657
658    if mvl then
659
660        pagehead, pagetail = getpagelist()
661
662        if pagetail then
663            for current, id, subtype in nextnode, pagetail do
664                if id == hlist_code then
665                    if subtype == linelist_code then
666                        t_profile = hasprofile(current)
667                        if t_profile then
668                            top = current
669                        end
670                    end
671                    break
672                elseif id == glue_code then
673                    local wd = getwidth(current)
674                    if not wd or wd == 0 then
675                        -- go on
676                    else
677                        break
678                    end
679                elseif id == penalty_code then
680                    -- ok
681                else
682                    break
683                end
684            end
685        end
686
687    end
688
689    for current, id, subtype in nextnode, line do
690
691        local attr = getattr(current,a_profilemethod)
692
693        if attr then
694
695            if attr ~= lastattr then
696                specification = specifications[attr]
697                method        = specification and specification.method
698                action        = method and methods[method] or methods[v_strict]
699                lastattr      = attr
700            end
701
702            if id == hlist_code then -- check subtype
703                if subtype == linelist_code then
704                    if top == current then
705                        -- skip
706                        bot = nil -- to be sure
707                    elseif top then
708                        bot       = current
709                        b_profile = setprofile(bot)
710                        if show_profile then
711                            addprofile(bot,b_profile)
712                        end
713                        if not t_profile.done then
714                            if action then
715                                local ok = action(top,bot,t_profile,b_profile,specification)
716                                if ok and lastglue and distance ~= 0 then
717                                    setglue(lastglue)
718                                end
719                            end
720                            t_profile.done = true
721                        end
722                        top       = bot
723                        bot       = nil
724                        t_profile = b_profile
725                        b_profile = nil
726                        distance  = 0
727                    else
728                        top       = current
729                        t_profile = setprofile(top)
730                        bot       = nil
731                        if show_profile then
732                            addprofile(top,t_profile)
733                        end
734                    end
735                else
736                    top = nil
737                    bot = nil
738                end
739            elseif id == glue_code then
740                if top then
741                 -- if subtype == lineskip_code or subtype == baselineskip_code then
742                        local wd   = getwidth(current)
743                        if wd > 0 then
744                            distance = wd
745                            lastglue = current
746                        elseif wd < 0 then
747                            top = nil
748                            bot = nil
749                        else
750                            -- ok
751                        end
752                 -- else
753                 --     top = nil
754                 --     bot = nil
755                 -- end
756                else
757                    top = nil
758                    bot = nil
759                end
760            elseif id == penalty_code then
761                -- okay
762            else
763                top = nil
764                bot = nil
765            end
766        else
767            top = nil
768            bot = nil
769        end
770    end
771    if top then
772        t_profile = setprofile(top)
773        if show_profile then
774            addprofile(top,t_profile)
775        end
776    end
777end
778
779profiling.list = profilelist
780
781local enabled = false
782
783-- todo: use attribute storage
784
785function profiling.set(specification)
786    if not enabled then
787        enableaction("mvlbuilders", "builders.profiling.pagehandler")
788     -- too expensive so we expect that this happens explicitly, we keep for reference:
789     -- enableaction("vboxbuilders","builders.profiling.vboxhandler")
790        enabled = true
791    end
792    local n = #specifications + 1
793    specifications[n] = specification
794    settexattribute(a_profilemethod,n)
795end
796
797function profiling.profilebox(specification)
798    local boxnumber = specification.box
799    local current   = getlist(gettexbox(boxnumber))
800    local top       = nil
801    local bot       = nil
802    local t_profile = nil
803    local b_profile = nil
804    local method    = specification and specification.method
805    local action    = method and methods[method] or methods[v_strict]
806    local lastglue  = nil
807    local distance  = 0
808    for current, id, subtype in nextnode, current do
809        if id == hlist_code then
810            if subtype == linelist_code then
811                if top then
812                    bot       = current
813                    b_profile = setprofile(bot)
814                    if show_profile then
815                        addprofile(bot,b_profile)
816                    end
817                    if not t_profile.done then
818                        if action then
819                            local ok = action(top,bot,t_profile,b_profile,specification)
820                            if ok and lastglue and distance ~= 0 then
821                                setglue(lastglue)
822                            end
823                        end
824                        t_profile.done = true
825                    end
826                    top       = bot
827                    t_profile = b_profile
828                    b_profile = nil
829                    distance  = 0
830                else
831                    top       = current
832                    t_profile = setprofile(top)
833                    if show_profile then
834                        addprofile(top,t_profile)
835                    end
836                    bot       = nil
837                end
838            else
839                top = nil
840                bot = nil
841            end
842        elseif id == glue_code then
843            if subtype == lineskip_code or subtype == baselineskip_code then
844                if top then
845                    local wd   = getwidth(current)
846                    if wd > 0 then
847                        distance = wd
848                        lastglue = current
849                    elseif wd < 0 then
850                        top = nil
851                        bot = nil
852                    else
853                        -- ok
854                    end
855                else
856                    top = nil
857                    bot = nil
858                end
859            else
860                top = nil
861                bot = nil
862            end
863        elseif id == penalty_code then
864            -- okay
865        else
866            top = nil
867            bot = nil
868        end
869    end
870
871    if top then
872        t_profile = setprofile(top) -- not needed
873        if show_profile then
874            addprofile(top,t_profile)
875        end
876    end
877
878end
879
880-- local ignore = table.tohash {
881--     "split_keep",
882--     "split_off",
883--  -- "vbox",
884-- }
885--
886-- function profiling.vboxhandler(head,where)
887--     if head and not ignore[where] then
888--         if getnext(head) then
889--             profilelist(head)
890--         end
891--     end
892--     return head
893-- end
894
895function profiling.pagehandler(head)
896    if head then
897        profilelist(head,true)
898    end
899    return head
900end
901
902interfaces.implement {
903    name      = "setprofile",
904    actions   = profiling.set,
905    arguments = {
906        {
907            { "name" },
908            { "height", "dimen" },
909            { "depth", "dimen" },
910            { "distance", "dimen" },
911            { "factor", "integer" },
912            { "lines", "integer" },
913            { "method" }
914        }
915    }
916}
917
918interfaces.implement {
919    name      = "profilebox",
920    actions   = profiling.profilebox,
921    arguments = {
922        {
923            { "box", "integer" },
924            { "height", "dimen" },
925            { "depth", "dimen" },
926            { "distance", "dimen" },
927            { "factor", "integer" },
928            { "lines", "integer" },
929            { "method" }
930        }
931    }
932}
933
934-- The following is an experiment that I picked up after demoing this already old but never
935-- used feature and in the process it got applied to document of hundreds of pages. Actually
936-- performance is quite okay but this mechanism is not really meant for that scenario. We'll
937-- see where this ends. We could (an d might) integrate it in the above but the next is more
938-- lightweight while the previous was basically some exploration with lots of options.
939
940do
941
942    -- we could share the two arrays if needed
943
944    local a_lineprofile   <const> = attributes.private("lineprofile")
945
946    local registervalue   = attributes.registervalue
947    local getvalue        = attributes.getvalue
948    local texsetattribute = tex.setattribute
949
950    local function getdepthprofile(line,step,margin,max,list)
951
952        local width    = 0
953        local position = 0
954        local profile  = newindex(max+2,0)
955        local wd       = 0
956        local ht       = 0
957        local dp       = 0
958
959        profile[0] = 0
960
961        local function process(current) -- called nested in disc replace
962            for current, id, subtype in nextnode, current do
963                if id == glyph_code then
964                    wd, ht, dp = getwhd(current)
965                elseif id == kern_code then
966                    wd = getkern(current)
967                    dp = 0
968                elseif id == disc_code then
969                    local replace = getreplace(current)
970                    if replace then
971                        process(replace)
972                    end
973                    goto done
974                elseif id == glue_code then
975                    wd = effectiveglue(current, line) -- geteffectivewhd
976                    -- tricky
977                    if subtype >= leaders_code then
978                        local leader = getleader(current)
979                        local w
980                        w, ht, dp = getwhd(leader)
981                    else
982                        dp = 0
983                    end
984                elseif id == hlist_code then
985                    local w, h, d, shift = getlistdimensions(current)
986                    if getprop(current,"specialcontent") then
987                        -- like a margin note, maybe check for wd
988                        wd = w
989                        dp = 0
990                    else
991                        wd = w
992                        dp = d + shift
993                    end
994                elseif id == vlist_code then
995                    local shift
996                    wd, ht, dp, shift = getlistdimensions(current)
997                    dp = dp + shift
998                elseif id == rule_code then
999                    if subtype == strutrule_code then
1000                        dp = 0
1001                    else
1002                        wd, ht, dp = getwhd(current)
1003                    end
1004                elseif id == math_code then
1005                    -- todo get glue
1006                    wd = getkern(current) + getwidth(current) -- surround
1007                    dp = 0
1008                else
1009                    goto done
1010                end
1011                -- progress
1012                position = width
1013                width    = position + wd
1014                    p = floor((position - margin)/step + 0.5)
1015                    w = floor((width    + margin)/step - 0.5)
1016                if p < 0 then
1017                    p = 0
1018                end
1019                if w < 0 then
1020                    w = 0
1021                end
1022                if p > w then
1023                    w, p = p, w
1024                end
1025                if w > max then
1026                    for i=max+1,w+1 do
1027                        profile[i] = 0
1028                    end
1029                    max = w
1030                end
1031                for i=p,w do
1032                    if dp > profile[i] then
1033                        profile[i] = dp
1034                    end
1035                end
1036              ::done::
1037            end
1038        end
1039
1040        process(list)
1041
1042        return profile
1043
1044    end
1045
1046    local function getheightprofile(line,step,margin,max,list)
1047
1048        local width    = 0
1049        local position = 0
1050        local profile  = newindex(max+2,0)
1051        local wd       = 0
1052        local ht       = 0
1053        local dp       = 0
1054
1055        profile[0] = 0
1056
1057        local function process(current) -- called nested in disc replace
1058            for current, id, subtype in nextnode, current do
1059                if id == glyph_code then
1060                    wd, ht, dp = getwhd(current)
1061                elseif id == kern_code then
1062                    wd = getkern(current)
1063                    ht = 0
1064                elseif id == disc_code then
1065                    local replace = getreplace(current)
1066                    if replace then
1067                        process(replace)
1068                    end
1069                    goto done
1070                elseif id == glue_code then
1071                    wd = effectiveglue(current, line) -- geteffectivewhd
1072                    -- tricky
1073                    if subtype >= leaders_code then
1074                        local leader = getleader(current)
1075                        local w
1076                        w, ht, dp = getwhd(leader)
1077                    else
1078                        ht = 0
1079                    end
1080                elseif id == hlist_code then
1081                    local w, h, d, shift = getlistdimensions(current)
1082                    if getprop(current,"specialcontent") then
1083                        -- like a margin note, maybe check for wd
1084                        wd = w
1085                        ht = 0
1086                    else
1087                        wd = w
1088                        ht = h - shift
1089                    end
1090                elseif id == vlist_code then
1091                    local shift
1092                    wd, ht, dp, shift = getlistdimensions(current)
1093                    ht = ht - shift
1094                elseif id == rule_code then
1095                    if subtype == strutrule_code then
1096                        ht = 0
1097                    else
1098                        wd, ht, dp = getwhd(current)
1099                    end
1100                elseif id == math_code then
1101                    -- todo get glue
1102                    wd = getkern(current) + getwidth(current) -- surround
1103                    ht = 0
1104                else
1105                    goto done
1106                end
1107                -- progress
1108                position = width
1109                width    = position + wd
1110                    p = floor((position - margin)/step + 0.5)
1111                    w = floor((width    + margin)/step - 0.5)
1112                if p < 0 then
1113                    p = 0
1114                end
1115                if w < 0 then
1116                    w = 0
1117                end
1118                if p > w then
1119                    w, p = p, w
1120                end
1121                if w > max then
1122                    for i=max+1,w+1 do
1123                        profile[i] = 0
1124                    end
1125                    max = w
1126                end
1127                for i=p,w do
1128                    if ht > profile[i] then
1129                        profile[i] = ht
1130                    end
1131                end
1132              ::done::
1133            end
1134        end
1135
1136        process(list)
1137
1138        return profile
1139
1140    end
1141
1142    local show_lineprofile = false
1143    local show_linedetails = false
1144
1145    trackers.register("profiling.lines.show", function(v)
1146        local visualizers = nodes.visualizers
1147        glue_mode = visualizers.modes.glue
1148        line_mode = visualizers.modes.line
1149        show_lineprofile  = v
1150        visualizers.enable()
1151    end)
1152
1153    trackers.register("profiling.lines.details", function(v)
1154        show_linedetail = v
1155    end)
1156
1157    local defaultstep   = 65536 * 2 -- 2pt
1158    local defaultmethod = "a"
1159    local defaultfactor = 1
1160
1161    -- I played with different methods (like only get depths and then on the fly check with heights
1162    -- but there is no gain and it is also fuzzy. So for now we just do the whole scan.
1163
1164    function profilelines(list,prev)
1165
1166        if not list then
1167            return
1168        end
1169
1170        local _, start = findattribute(list,a_lineprofile)
1171        if not start then
1172            return
1173        end
1174
1175        -- no height or depth ... skip
1176        for current, subtype in nextglue, start do
1177            if subtype == lineskip_code and not getprop(current,"profiled") then
1178                local detail = getattr(current,a_lineprofile)
1179                if detail then
1180                    local amount = getwidth(current)
1181                    if amount > 0 then
1182                        detail = getvalue(a_lineprofile,detail) or { }
1183                        --
1184                        local top, bot = getboth(current)
1185                        setprop(current,"profiled",amount) -- original amount, maybe move up
1186                        if not top and prev and detail.paragraph == v_yes then
1187                            top  = prev
1188                        end
1189                        if top then
1190                            if getid(top) == penalty_code then
1191                                top = getprev(top)
1192                            end
1193                            if top and bot then
1194                                if getid(top) == hlist_code and getsubtype(top) == linelist_code then
1195                                    if getid(bot) == hlist_code and getsubtype(bot) == linelist_code then
1196                                        local toplist = getlist(top)
1197                                        local botlist = getlist(bot)
1198                                        if toplist and botlist then
1199                                            --
1200                                            local step   = detail.step  or defaultstep
1201                                            local factor = tonumber(detail.factor) or defaultfactor
1202                                            local method = detail.method or defaultmethod
1203                                            local margin = step / 4
1204                                            --
1205                                            if factor > 1 then
1206                                                factor = 1
1207                                            elseif factor <= 0 then
1208                                                factor = 0 -- we could actually go the other way
1209                                            end
1210                                            --
1211                                            local natural   = getdepth(top) + getheight(bot)
1212                                            local added     = factor * amount
1213                                            local possible  = natural - added
1214                                            local overshoot = 0
1215                                            local topmax    = ceiling(getwidth(top)/step) + 1
1216                                            local botmax    = ceiling(getwidth(bot)/step) + 1
1217                                         -- if method == "a" then
1218                                                local depths  = getdepthprofile (top,step,margin,topmax,toplist)
1219                                                local heights = getheightprofile(bot,step,margin,botmax,botlist)
1220                                                local steps   = min(#depths,#heights)
1221                                                for i=1,steps do
1222                                                    local o = heights[i] + depths[i] - possible
1223                                                    if o > overshoot then
1224                                                        -- we can quit when >= added
1225                                                        overshoot = o
1226                                                     -- if overshoot > added then
1227                                                     --     break
1228                                                     -- end
1229                                                    end
1230                                                end
1231                                         -- end
1232                                         -- if overshoot < added / 2 then
1233                                         --     overshoot = added / 2
1234                                         -- end
1235                                            if overshoot ~= amount then -- shouldn't we round
1236                                                setwidth(current,overshoot)
1237                                                if show_lineprofile then
1238                                                    setattr(current,a_visual,glue_mode)
1239                                                    setattr(bot,a_visual,line_mode)
1240                                                    setattr(top,a_visual,line_mode)
1241                                                end
1242                                                if show_linedetail then
1243                                                    report("lineskip changed from %p to %p on page %i",amount,overshoot,tex.getcount("realpageno"))
1244                                                end
1245                                            end
1246                                        end
1247                                    end
1248                                end
1249                            end
1250                        end
1251                    end
1252                end
1253            end
1254            prev = nil
1255        end
1256    end
1257
1258    builders.profiling.profilelines = profilelines
1259
1260    function profiling.boxlinehandler(head)
1261        if head then
1262            profilelines(head)
1263        end
1264        return head
1265    end
1266
1267    function profiling.pagelinehandler(head,...)
1268        if head then
1269            local h, t = getspeciallist("pagehead")
1270            profilelines(head,t)
1271        end
1272        return head
1273    end
1274
1275    -- actually we need a proper callback for this kind of things ...
1276
1277    function profiling.setlines(specification)
1278        if not enabled then
1279            enableaction("mvlbuilders",  "builders.profiling.pagelinehandler")
1280            enableaction("vboxbuilders", "builders.profiling.boxlinehandler")
1281            enabled = true
1282        end
1283        texsetattribute(a_lineprofile,registervalue(a_lineprofile,specification))
1284    end
1285
1286    interfaces.implement {
1287        name      = "setlineprofile",
1288        actions   = profiling.setlines,
1289        arguments = {
1290            {
1291                { "name" },
1292                { "method" },
1293                { "step", "dimension" },
1294                { "factor" },
1295                { "paragraph" },
1296            }
1297        }
1298    }
1299
1300    interfaces.implement {
1301        name      = "lineprofilebox",
1302        public    = true,
1303        protected = true,
1304        actions   = function(box)
1305            local okay = nuts.getbox(box)
1306            local list = getlist(okay)
1307            if list then
1308                profiling.boxlinehandler(list)
1309            end
1310        end,
1311        arguments = "integer"
1312    }
1313
1314end
1315