font-imp-quality.lmt /size: 29 Kb    last modification: 2024-01-16 10:22
1if not modules then modules = { } end modules ['font-imp-quality'] = {
2    version   = 1.001,
3    comment   = "companion to font-ini.mkiv and hand-ini.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
9if not context then return end
10
11local next, type, tonumber = next, type, tonumber
12
13local fonts              = fonts
14local utilities          = utilities
15
16local handlers           = fonts.handlers
17local otf                = handlers.otf
18local afm                = handlers.afm
19local registerotffeature = otf.features.register
20local registerafmfeature = afm.features.register
21
22local allocate           = utilities.storage.allocate
23local getparameters      = utilities.parsers.getparameters
24
25local implement          = interfaces and interfaces.implement
26
27local trace_protrusion   = false  trackers.register("fonts.protrusion", function(v) trace_protrusion = v end)
28local trace_expansion    = false  trackers.register("fonts.expansion",  function(v) trace_expansion  = v end)
29
30local report_expansions  = logs.reporter("fonts","expansions")
31local report_protrusions = logs.reporter("fonts","protrusions")
32
33-- -- -- -- -- --
34-- shared
35-- -- -- -- -- --
36
37local function get_class_and_vector(tfmdata,value,where) -- "expansions"
38    local g_where = tfmdata.goodies and tfmdata.goodies[where]
39    local f_where = fonts[where]
40    local g_classes = g_where and g_where.classes
41    local f_classes = f_where and f_where.classes
42    local class = (g_classes and g_classes[value]) or (f_classes and f_classes[value])
43    if class then
44        local class_vector = class.vector
45        local g_vectors = g_where and g_where.vectors
46        local f_vectors = f_where and f_where.vectors
47        local vector = (g_vectors and g_vectors[class_vector]) or (f_vectors and f_vectors[class_vector])
48        return class, vector
49    end
50end
51
52-- -- -- -- -- --
53-- expansion (hz)
54-- -- -- -- -- --
55
56local expansions   = fonts.expansions or allocate()
57
58fonts.expansions   = expansions
59
60local classes      = expansions.classes or allocate()
61local vectors      = expansions.vectors or allocate()
62
63expansions.classes = classes
64expansions.vectors = vectors
65
66classes.preset = {
67    stretch = 2,
68    shrink  = 2,
69    step    = .5,
70    factor  = 1,
71}
72
73classes['quality'] = {
74    stretch = 2,
75    shrink  = 2,
76    step    = .5,
77    vector  = 'default',
78    factor  = 1,
79}
80
81vectors['default'] = {
82    [0x0041] = 0.5, -- A
83    [0x0042] = 0.7, -- B
84    [0x0043] = 0.7, -- C
85    [0x0044] = 0.5, -- D
86    [0x0045] = 0.7, -- E
87    [0x0046] = 0.7, -- F
88    [0x0047] = 0.5, -- G
89    [0x0048] = 0.7, -- H
90    [0x004B] = 0.7, -- K
91    [0x004D] = 0.7, -- M
92    [0x004E] = 0.7, -- N
93    [0x004F] = 0.5, -- O
94    [0x0050] = 0.7, -- P
95    [0x0051] = 0.5, -- Q
96    [0x0052] = 0.7, -- R
97    [0x0053] = 0.7, -- S
98    [0x0055] = 0.7, -- U
99    [0x0057] = 0.7, -- W
100    [0x005A] = 0.7, -- Z
101    [0x0061] = 0.7, -- a
102    [0x0062] = 0.7, -- b
103    [0x0063] = 0.7, -- c
104    [0x0064] = 0.7, -- d
105    [0x0065] = 0.7, -- e
106    [0x0067] = 0.7, -- g
107    [0x0068] = 0.7, -- h
108    [0x006B] = 0.7, -- k
109    [0x006D] = 0.7, -- m
110    [0x006E] = 0.7, -- n
111    [0x006F] = 0.7, -- o
112    [0x0070] = 0.7, -- p
113    [0x0071] = 0.7, -- q
114    [0x0073] = 0.7, -- s
115    [0x0075] = 0.7, -- u
116    [0x0077] = 0.7, -- w
117    [0x007A] = 0.7, -- z
118    [0x0032] = 0.7, -- 2
119    [0x0033] = 0.7, -- 3
120    [0x0036] = 0.7, -- 6
121    [0x0038] = 0.7, -- 8
122    [0x0039] = 0.7, -- 9
123}
124
125vectors['quality'] = vectors['default'] -- metatable ?
126
127-- Compression is new and used for a math experiment. Musical timestamp(s): November
128-- 2022, a cluster of live performances: RPWL (5), PT (7, yes!) and xPropaganda (10).
129
130local function initialize(tfmdata,value)
131    if value then
132        local class, vector = get_class_and_vector(tfmdata,value,"expansions")
133        if class then
134            if vector then
135                local stretch = class.stretch or 0
136                local shrink  = class.shrink  or 0
137                local step    = class.step    or 0
138                local factor  = class.factor  or 1
139                if trace_expansion then
140                    report_expansions("setting class %a, vector %a, factor %a, stretch %a, shrink %a, step %a",
141                        value,class.vector,factor,stretch,shrink,step)
142                end
143                tfmdata.parameters.expansion = {
144                    stretch = 10 * stretch,
145                    shrink  = 10 * shrink,
146                    step    = 10 * step,
147                    factor  = factor,
148                    class   = class,
149                    vector  = vector,
150                }
151--                 local data = characters and characters.data
152--                 for i, chr in next, tfmdata.characters do -- todo unicode
153--                     local v = vector[i]
154--                     if data and not v then -- we could move the data test outside (needed for plain)
155--                         local d = data[i]
156--                         if d then
157--                             local s = d.shcode
158--                             if not s then
159--                                 -- sorry
160--                             elseif type(s) == "table" then
161--                                 v = ((vector[s[1]] or 0) + (vector[s[#s]] or 0)) / 2
162--                             else
163--                                 v = vector[s] or 0
164--                             end
165--                         end
166--                     end
167--                     -- So, factor influences all shapes but we now obey zero when set!
168--                     if v then
169--                         if type(v) == "table" then
170--                             local e = v[1]
171--                             local c = v[2] or 0
172--                             if e ~= 0 then
173--                                 chr.expansion = e*factor
174--                             end
175--                             if c ~= 0 then
176--                                 chr.compression = c*factor
177--                             end
178--                         elseif v == 0 then
179--                          -- chr.expansion = 0
180--                         else
181--                             chr.expansion = v*factor
182--                         end
183--                     elseif factor ~= 1 then -- test is new
184--                         chr.expansion = factor
185--                     end
186--                 end
187            elseif trace_expansion then
188                report_expansions("unknown vector %a in class %a",class.vector,value)
189            end
190        elseif trace_expansion then
191            report_expansions("unknown class %a",value)
192        end
193    end
194end
195
196local specification = {
197    name        = "expansion",
198    description = "apply hz optimization",
199    initializers = {
200        base = initialize,
201        node = initialize,
202    }
203}
204
205registerotffeature(specification)
206registerafmfeature(specification)
207
208fonts.goodies.register("expansions",  function(...) return fonts.goodies.report("expansions", trace_expansion, ...) end)
209
210implement {
211    name      = "setupfontexpansion",
212    arguments = "2 strings",
213    actions   = function(class,settings) getparameters(classes,class,'preset',settings) end
214}
215
216-- special version for math 7/11/22
217
218classes.math = {
219    stretch = 2,
220    shrink  = 2,
221    step    = .5,
222    vector  = 'math',
223    factor  = 1,
224}
225
226vectors.math = {
227    [0x002B] = { 4, 0 }, -- +
228    [0x2212] = { 4, 0 }, -- -
229    [0x003C] = { 8, 0 }, -- <
230    [0x003D] = { 8, 0 }, -- =
231    [0x003E] = { 8, 0 }, -- >
232    [0x002F] = { 2, 0 }, -- /
233    [0x0028] = { 2, 0 }, -- (
234    [0x0029] = { 2, 0 }, -- )
235    [0x005B] = { 2, 0 }, -- [
236    [0x005D] = { 2, 0 }, -- ]
237}
238
239local function initialize(tfmdata,value)
240    if value then
241        local class, vector = get_class_and_vector(tfmdata,value,"expansions")
242        if class then
243            if vector then
244                local stretch = class.stretch or 0
245                local shrink  = class.shrink  or 0
246                local step    = class.step    or 0
247                local factor  = class.factor  or 1
248                if trace_expansion then
249                    report_expansions("setting class %a, vector %a, factor %a, stretch %a, shrink %a, step %a",
250                        value,class.vector,factor,stretch,shrink,step)
251                end
252                tfmdata.parameters.expansion = {
253                    stretch = 10 * stretch,
254                    shrink  = 10 * shrink,
255                    step    = 10 * step,
256                    factor  = factor,
257                }
258                local characters = tfmdata.characters
259                for u, v in next, vector do
260                    local chr = characters[u]
261                    if type(v) == "table" then
262                        local e = v[1]
263                        local c = v[2] or 0
264                        if e ~= 0 then
265                            chr.expansion = e*factor
266                        end
267                        if c ~= 0 then
268                            chr.compression = c*factor
269                        end
270                    elseif v ~= 0 then
271                        chr.expansion = v*factor
272                    end
273                end
274            elseif trace_expansion then
275                report_expansions("unknown vector %a in class %a",class.vector,value)
276            end
277        elseif trace_expansion then
278            report_expansions("unknown class %a",value)
279        end
280    end
281end
282
283registerotffeature {
284    name        = "mathexpansion",
285    description = "apply hz optimization to math",
286    initializers = {
287        base = initialize,
288        node = initialize,
289    }
290}
291
292-- -- -- -- -- --
293-- protrusion
294-- -- -- -- -- --
295
296fonts.protrusions   = allocate()
297local protrusions   = fonts.protrusions
298
299protrusions.classes = allocate()
300protrusions.vectors = allocate()
301
302local classes       = protrusions.classes
303local vectors       = protrusions.vectors
304
305-- the values need to be revisioned
306
307classes.preset = {
308    factor = 1,
309    left   = 1,
310    right  = 1,
311}
312
313classes['pure']        = { vector = 'pure',        factor = 1 }
314classes['punctuation'] = { vector = 'punctuation', factor = 1 }
315classes['alpha']       = { vector = 'alpha',       factor = 1 }
316classes['quality']     = { vector = 'quality',     factor = 1 }
317
318vectors['pure'] = {
319
320    [0x002C] = { 0, 1    }, -- comma
321    [0x002E] = { 0, 1    }, -- period
322    [0x003A] = { 0, 1    }, -- colon
323    [0x003B] = { 0, 1    }, -- semicolon
324    [0x002D] = { 0, 1    }, -- hyphen
325    [0x00AD] = { 0, 1    }, -- also hyphen
326    [0x2013] = { 0, 0.50 }, -- endash
327    [0x2014] = { 0, 0.33 }, -- emdash
328    [0x3001] = { 0, 1    }, -- ideographic comma      、
329    [0x3002] = { 0, 1    }, -- ideographic full stop  。
330    [0x060C] = { 0, 1    }, -- arabic comma           ،
331    [0x061B] = { 0, 1    }, -- arabic semicolon       ؛
332    [0x06D4] = { 0, 1    }, -- arabic full stop       ۔
333
334}
335
336vectors['punctuation'] = {
337
338    [0x003F] = { 0,    0.20 }, -- ?
339    [0x00BF] = { 0.20, 0    }, -- ¿
340    [0x0021] = { 0,    0.20 }, -- !
341    [0x00A1] = { 0.20, 0,   }, -- ¡
342    [0x0028] = { 0.05, 0    }, -- (
343    [0x0029] = { 0,    0.05 }, -- )
344    [0x005B] = { 0.05, 0    }, -- [
345    [0x005D] = { 0,    0.05 }, -- ]
346    [0x002C] = { 0,    0.70 }, -- comma
347    [0x002E] = { 0,    0.70 }, -- period
348    [0x003A] = { 0,    0.50 }, -- colon
349    [0x003B] = { 0,    0.50 }, -- semicolon
350    [0x002D] = { 0,    0.70 }, -- hyphen
351    [0x00AD] = { 0,    0.70 }, -- also hyphen
352    [0x2013] = { 0,    0.30 }, -- endash
353    [0x2014] = { 0,    0.20 }, -- emdash
354    [0x060C] = { 0,    0.70 }, -- arabic comma
355    [0x061B] = { 0,    0.50 }, -- arabic semicolon
356    [0x06D4] = { 0,    0.70 }, -- arabic full stop
357    [0x061F] = { 0,    0.20 }, -- ؟
358
359    -- todo: left and right quotes: .5 double, .7 single
360
361    [0x2039] = { 0.70, 0.70 }, -- left single guillemet   ‹
362    [0x203A] = { 0.70, 0.70 }, -- right single guillemet  ›
363    [0x00AB] = { 0.50, 0.50 }, -- left guillemet          «
364    [0x00BB] = { 0.50, 0.50 }, -- right guillemet         »
365
366    [0x2018] = { 0.70, 0.70 }, -- left single quotation mark             ‘
367    [0x2019] = { 0,    0.70 }, -- right single quotation mark            ’
368    [0x201A] = { 0.70, 0    }, -- single low-9 quotation mark            ,
369    [0x201B] = { 0.70, 0    }, -- single high-reversed-9 quotation mark  ‛
370    [0x201C] = { 0.50, 0.50 }, -- left double quotation mark             “
371    [0x201D] = { 0,    0.50 }, -- right double quotation mark            ”
372    [0x201E] = { 0.50, 0    }, -- double low-9 quotation mark            „
373    [0x201F] = { 0.50, 0    }, -- double high-reversed-9 quotation mark  ‟
374
375}
376
377vectors['alpha'] = {
378
379    [0x0041] = { .05, .05 }, -- A
380    [0x0046] = {   0, .05 }, -- F
381    [0x004A] = { .05,   0 }, -- J
382    [0x004B] = {   0, .05 }, -- K
383    [0x004C] = {   0, .05 }, -- L
384    [0x0054] = { .05, .05 }, -- T
385    [0x0056] = { .05, .05 }, -- V
386    [0x0057] = { .05, .05 }, -- W
387    [0x0058] = { .05, .05 }, -- X
388    [0x0059] = { .05, .05 }, -- Y
389
390    [0x006B] = {   0, .05 }, -- k
391    [0x0072] = {   0, .05 }, -- r
392    [0x0074] = {   0, .05 }, -- t
393    [0x0076] = { .05, .05 }, -- v
394    [0x0077] = { .05, .05 }, -- w
395    [0x0078] = { .05, .05 }, -- x
396    [0x0079] = { .05, .05 }, -- y
397
398}
399
400vectors['quality'] = table.merged(
401    vectors['punctuation'],
402    vectors['alpha']
403)
404
405-- As with math, this kind of optimal correction is likely to be messed up when defined in a font and
406-- heuristics work more reliable then. We keep this experiment around for a while but it has been
407-- disabled.
408
409-- -- As this is experimental code, users should not depend on it. The implications are still
410-- -- discussed on the ConTeXt Dev List and we're not sure yet what exactly the spec is (the
411-- -- next code is tested with a gyre font patched by / fea file made by Khaled Hosny). The
412-- -- double trick should not be needed it proper hanging punctuation is used in which case
413-- -- values < 1 can be used.
414-- --
415-- -- preferred (in context, usine vectors):
416-- --
417-- -- \definefontfeature[whatever][default][mode=node,protrusion=quality]
418-- --
419-- -- using lfbd and rtbd, with possibibility to enable only one side :
420-- --
421-- -- \definefontfeature[whocares][default][mode=node,protrusion=yes,  opbd=yes,script=latn]
422-- -- \definefontfeature[whocares][default][mode=node,protrusion=right,opbd=yes,script=latn]
423-- --
424-- -- idem, using multiplier
425-- --
426-- -- \definefontfeature[whocares][default][mode=node,protrusion=2,opbd=yes,script=latn]
427-- -- \definefontfeature[whocares][default][mode=node,protrusion=double,opbd=yes,script=latn]
428-- --
429-- -- idem, using named feature file (less frozen):
430-- --
431-- -- \definefontfeature[whocares][default][mode=node,protrusion=2,opbd=yes,script=latn,featurefile=texgyrepagella-regularxx.fea]
432--
433-- classes['double'] = { -- for testing opbd
434--     factor = 2,
435--     left   = 1,
436--     right  = 1,
437-- }
438--
439-- local function map_opbd_onto_protrusion(tfmdata,value,opbd)
440--     local characters   = tfmdata.characters
441--     local descriptions = tfmdata.descriptions
442--     local properties   = tfmdata.properties
443--     local parameters   = tfmdata.parameters
444--     local resources    = tfmdata.resources
445--     local rawdata      = tfmdata.shared.rawdata
446--     local lookuphash   = rawdata.lookuphash
447--     local lookuptags   = resources.lookuptags
448--     local script       = properties.script
449--     local language     = properties.language
450--     local units        = parameters.units
451--     local done, factor, left, right = false, 1, 1, 1
452--     local class = classes[value]
453--     if class then
454--         factor = class.factor or 1
455--         left   = class.left   or 1
456--         right  = class.right  or 1
457--     else
458--         factor = tonumber(value) or 1
459--     end
460--     local lfactor = left  * factor
461--     local rfactor = right * factor
462--     if trace_protrusion then
463--         report_protrusions("left factor %0.3F, right factor %0.3F",lfactor,rfactor)
464--     end
465--     tfmdata.parameters.protrusion = {
466--         factor = factor,
467--         left   = left,
468--         right  = right,
469--     }
470--     if opbd ~= "right" then
471--         local validlookups, lookuplist = otf.collectlookups(rawdata,"lfbd",script,language)
472--         if validlookups then
473--             for i=1,#lookuplist do
474--                 local lookup = lookuplist[i]
475--                 local steps  = lookup.steps
476--                 if steps then
477--                     if trace_protrusion then
478--                         report_protrusions("setting left using lfbd")
479--                     end
480--                     for i=1,#steps do
481--                         local step     = steps[i]
482--                         local coverage = step.coverage
483--                         if coverage then
484--                             for k, v in next, coverage do
485--                                 if v == true then
486--                                     -- zero
487--                                 else
488--                                     local w = descriptions[k].width
489--                                     local d = - v[1]
490--                                     if w == 0 or d == 0 then
491--                                         -- ignored
492--                                     else
493--                                         local p = lfactor * d/units
494--                                         characters[k].leftprotrusion = p
495--                                         if trace_protrusion then
496--                                             report_protrusions("lfbd -> %0.3F %C",p,k)
497--                                         end
498--                                     end
499--                                 end
500--                             end
501--                         end
502--                     end
503--                     done = true
504--                 end
505--             end
506--         end
507--     end
508--     if opbd ~= "left" then
509--         local validlookups, lookuplist = otf.collectlookups(rawdata,"rtbd",script,language)
510--         if validlookups then
511--             for i=1,#lookuplist do
512--                 local lookup = lookuplist[i]
513--                 local steps  = lookup.steps
514--                 if steps then
515--                     if trace_protrusion then
516--                         report_protrusions("setting right using rtbd")
517--                     end
518--                     for i=1,#steps do
519--                         local step     = steps[i]
520--                         local coverage = step.coverage
521--                         if coverage then
522--                             for k, v in next, coverage do
523--                                 if v == true then
524--                                     -- zero
525--                                 else
526--                                     local w = descriptions[k].width
527--                                     local d = - v[3]
528--                                     if w == 0 or d == 0 then
529--                                         -- ignored
530--                                     else
531--                                         local p = rfactor * d/units
532--                                         characters[k].rightprotrusion = p
533--                                         if trace_protrusion then
534--                                             report_protrusions("rtbd -> %0.3F %C",p,k)
535--                                         end
536--                                     end
537--                                 end
538--                             end
539--                         end
540--                     end
541--                 end
542--                 done = true
543--             end
544--         end
545--     end
546-- end
547
548-- The opbd test is just there because it was discussed on the context development list. However,
549-- the mentioned fxlbi.otf font only has some kerns for digits. So, consider this feature not supported
550-- till we have a proper test font.
551
552local function initialize(tfmdata,value)
553    if value then
554     -- local opbd = tfmdata.shared.features.opbd
555     -- if opbd then
556     --     -- possible values: left right both yes no (experimental)
557     --     map_opbd_onto_protrusion(tfmdata,value,opbd)
558     -- else
559            local class, vector = get_class_and_vector(tfmdata,value,"protrusions")
560            if class then
561                if vector then
562                    local factor = class.factor or 1
563                    local left   = class.left   or 1
564                    local right  = class.right  or 1
565                    if trace_protrusion then
566                        report_protrusions("setting class %a, vector %a, factor %a, left %a, right %a",
567                            value,class.vector,factor,left,right)
568                    end
569                    local data    = characters.data
570                    local lfactor = left  * factor
571                    local rfactor = right * factor
572                    if trace_protrusion then
573                        report_protrusions("left factor %0.3F, right factor %0.3F",lfactor,rfactor)
574                    end
575                    tfmdata.parameters.protrusion = {
576                        factor = factor,
577                        left   = left,
578                        right  = right,
579                        class  = class,
580                        vector = vector,
581                    }
582--                     for i, chr in next, tfmdata.characters do
583--                         local v  = vector[i]
584--                         local pl = nil
585--                         local pr = nil
586--                         if v then
587--                             pl = v[1]
588--                             pr = v[2]
589--                         else
590--                             local d = data[i]
591--                             if d then
592--                                 local s = d.shcode
593--                                 if not s then
594--                                     -- sorry
595--                                 elseif type(s) == "table" then
596--                                     local vl = vector[s[1]]
597--                                     local vr = vector[s[#s]]
598--                                     if vl then pl = vl[1] end
599--                                     if vr then pr = vr[2] end
600--                                 else
601--                                     v = vector[s]
602--                                     if v then
603--                                         pl = v[1]
604--                                         pr = v[2]
605--                                     end
606--                                 end
607--                             end
608--                         end
609--                         if pl and pl ~= 0 then
610--                             local p = pl * lfactor
611--                             chr.leftprotrusion  = p
612--                             if trace_protrusion then
613--                                 report_protrusions("left  -> %0.3F %C ",p,i)
614--                             end
615--                         end
616--                         if pr and pr ~= 0 then
617--                             local p = pr * rfactor
618--                             chr.rightprotrusion = p
619--                             if trace_protrusion then
620--                                 report_protrusions("right -> %0.3F %C",p,i)
621--                             end
622--                         end
623--                     end
624                elseif trace_protrusion then
625                    report_protrusions("unknown vector %a in class %a",class.vector,value)
626                end
627            elseif trace_protrusion then
628                report_protrusions("unknown class %a",value)
629            end
630     -- end
631    end
632end
633
634local specification = {
635    name         = "protrusion",
636    description  = "l/r margin character protrusion",
637    initializers = {
638        base = initialize,
639        node = initialize,
640    }
641}
642
643registerotffeature(specification)
644registerafmfeature(specification)
645
646fonts.goodies.register("protrusions", function(...) return fonts.goodies.report("protrusions", trace_protrusion, ...) end)
647
648implement {
649    name      = "setupfontprotrusion",
650    arguments = "2 strings",
651    actions   = function(class,settings) getparameters(classes,class,'preset',settings) end
652}
653
654local function initialize(tfmdata,value)
655    local properties = tfmdata.properties
656    local parameters = tfmdata.parameters
657    if properties then
658        value = tonumber(value)
659        if value then
660            if value < 0 then
661                value = 0
662            elseif value > 10 then
663                report_expansions("threshold for %a @ %p limited to 10 pct",properties.fontname,parameters.size)
664                value = 10
665            end
666            if value > 5 then
667                report_expansions("threshold for %a @ %p exceeds 5 pct",properties.fontname,parameters.size)
668            end
669        end
670        properties.threshold = value or nil -- nil enforces default
671    end
672end
673
674local specification = {
675    name         = "threshold",
676    description  = "threshold for quality features",
677    initializers = {
678        base = initialize,
679        node = initialize,
680    }
681}
682
683registerotffeature(specification)
684registerafmfeature(specification)
685
686--
687
688local function addquality(id)
689    local tfmdata = fonts.hashes.identifiers[id]
690    if tfmdata then
691        local expansion = tfmdata.parameters.expansion
692        if expansion then
693            local class  = expansion.class
694            local vector = expansion.vector
695            if class and vector then
696                local stretch = class.stretch or 0
697                local shrink  = class.shrink  or 0
698                local step    = class.step    or 0
699                local factor  = class.factor  or 1
700                local data    = characters and characters.data
701                for i, chr in next, tfmdata.characters do -- todo unicode
702                    local v = vector[i]
703                    if data and not v then -- we could move the data test outside (needed for plain)
704                        local d = data[i]
705                        if d then
706                            local s = d.shcode
707                            if not s then
708                                -- sorry
709                            elseif type(s) == "table" then
710                                v = ((vector[s[1]] or 0) + (vector[s[#s]] or 0)) / 2
711                            else
712                                v = vector[s] or 0
713                            end
714                        end
715                    end
716                    -- So, factor influences all shapes but we now obey zero when set!
717                    if v then
718                        if type(v) == "table" then
719                            local e = v[1]
720                            local c = v[2] or 0
721                            if e ~= 0 then
722                                chr.expansion = e * factor * 1000
723                            end
724                            if c ~= 0 then
725                                chr.compression = c * factor * 1000
726                            end
727                        elseif v == 0 then
728                         -- chr.expansion = 0
729                        else
730                            chr.expansion = v * factor * 1000
731                        end
732                    elseif factor ~= 1 then -- test is new
733                        chr.expansion = factor * 1000
734                    end
735                end
736            end
737        end
738        --
739        local protrusion = tfmdata.parameters.protrusion
740        if protrusion then
741            local class  = protrusion.class
742            local vector = protrusion.vector
743            if class and vector then
744                local factor  = class.factor or 1
745                local left    = class.left   or 1
746                local right   = class.right  or 1
747                local data    = characters.data
748                local lfactor = left  * factor
749                local rfactor = right * factor
750                for i, chr in next, tfmdata.characters do
751                    local v  = vector[i]
752                    local pl = nil
753                    local pr = nil
754                    if v then
755                        pl = v[1]
756                        pr = v[2]
757                    else
758                        local d = data[i]
759                        if d then
760                            local s = d.shcode
761                            if not s then
762                                -- sorry
763                            elseif type(s) == "table" then
764                                local vl = vector[s[1]]
765                                local vr = vector[s[#s]]
766                                if vl then pl = vl[1] end
767                                if vr then pr = vr[2] end
768                            else
769                                v = vector[s]
770                                if v then
771                                    pl = v[1]
772                                    pr = v[2]
773                                end
774                            end
775                        end
776                    end
777                    if pl and pl ~= 0 then
778                        pl = pl * lfactor * chr.width
779                        if trace_protrusion then
780                            report_protrusions("left  -> %0.3F %C ",pl,i)
781                        end
782                        chr.leftprotrusion = pl
783                    end
784                    if pr and pr ~= 0 then
785                        pr = pr * rfactor * chr.width
786                        chr.rightprotrusion = pr
787                        if trace_protrusion then
788                            report_protrusions("right -> %0.3F %C",pr,i)
789                        end
790                    end
791                end
792            end
793        end
794        --
795        if expansion or protrusion then
796            font.addquality(id,tfmdata)
797        end
798    end
799end
800
801callback.register("quality_font",addquality)
802