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