font-imp-quality.lmt /size: 29 Kb    last modification: 2025-02-21 11:03
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",value,class.vector,factor,stretch,shrink,step)
141                    report_expansions("setting class %a, vector %a, factor %a",value,class.vector,factor)
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",value,class.vector,factor,stretch,shrink,step)
250                    report_expansions("setting class %a, vector %a, factor %a",value,class.vector,factor)
251                end
252                tfmdata.parameters.expansion = {
253                 -- stretch = 10 * stretch,
254                 -- shrink  = 10 * shrink,
255                 -- step    = 10 * step,
256                    factor  = factor,
257                }
258                --
259                local characters = tfmdata.characters
260                for u, v in next, vector do
261                    local chr = characters[u]
262                    if type(v) == "table" then
263                        local e = v[1]
264                        local c = v[2] or 0
265                        if e ~= 0 then
266                            chr.expansion = e*factor
267                        end
268                        if c ~= 0 then
269                            chr.compression = c*factor
270                        end
271                    elseif v ~= 0 then
272                        chr.expansion = v*factor
273                    end
274                end
275            elseif trace_expansion then
276                report_expansions("unknown vector %a in class %a",class.vector,value)
277            end
278        elseif trace_expansion then
279            report_expansions("unknown class %a",value)
280        end
281    end
282end
283
284registerotffeature {
285    name        = "mathexpansion",
286    description = "apply hz optimization to math",
287    initializers = {
288        base = initialize,
289        node = initialize,
290    }
291}
292
293-- -- -- -- -- --
294-- protrusion
295-- -- -- -- -- --
296
297fonts.protrusions   = allocate()
298local protrusions   = fonts.protrusions
299
300protrusions.classes = allocate()
301protrusions.vectors = allocate()
302
303local classes       = protrusions.classes
304local vectors       = protrusions.vectors
305
306-- the values need to be revisioned
307
308classes.preset = {
309    factor = 1,
310    left   = 1,
311    right  = 1,
312}
313
314classes['pure']        = { vector = 'pure',        factor = 1 }
315classes['punctuation'] = { vector = 'punctuation', factor = 1 }
316classes['alpha']       = { vector = 'alpha',       factor = 1 }
317classes['quality']     = { vector = 'quality',     factor = 1 }
318
319vectors['pure'] = {
320
321    [0x002C] = { 0, 1    }, -- comma
322    [0x002E] = { 0, 1    }, -- period
323    [0x003A] = { 0, 1    }, -- colon
324    [0x003B] = { 0, 1    }, -- semicolon
325    [0x002D] = { 0, 1    }, -- hyphen
326    [0x00AD] = { 0, 1    }, -- also hyphen
327    [0x2013] = { 0, 0.50 }, -- endash
328    [0x2014] = { 0, 0.33 }, -- emdash
329    [0x3001] = { 0, 1    }, -- ideographic comma      、
330    [0x3002] = { 0, 1    }, -- ideographic full stop  。
331    [0x060C] = { 0, 1    }, -- arabic comma           ،
332    [0x061B] = { 0, 1    }, -- arabic semicolon       ؛
333    [0x06D4] = { 0, 1    }, -- arabic full stop       ۔
334
335}
336
337vectors['punctuation'] = {
338
339    [0x003F] = { 0,    0.20 }, -- ?
340    [0x00BF] = { 0.20, 0    }, -- ¿
341    [0x0021] = { 0,    0.20 }, -- !
342    [0x00A1] = { 0.20, 0,   }, -- ¡
343    [0x0028] = { 0.05, 0    }, -- (
344    [0x0029] = { 0,    0.05 }, -- )
345    [0x005B] = { 0.05, 0    }, -- [
346    [0x005D] = { 0,    0.05 }, -- ]
347    [0x002C] = { 0,    0.70 }, -- comma
348    [0x002E] = { 0,    0.70 }, -- period
349    [0x003A] = { 0,    0.50 }, -- colon
350    [0x003B] = { 0,    0.50 }, -- semicolon
351    [0x002D] = { 0,    0.70 }, -- hyphen
352    [0x00AD] = { 0,    0.70 }, -- also hyphen
353    [0x2013] = { 0,    0.30 }, -- endash
354    [0x2014] = { 0,    0.20 }, -- emdash
355    [0x060C] = { 0,    0.70 }, -- arabic comma
356    [0x061B] = { 0,    0.50 }, -- arabic semicolon
357    [0x06D4] = { 0,    0.70 }, -- arabic full stop
358    [0x061F] = { 0,    0.20 }, -- ؟
359
360    -- todo: left and right quotes: .5 double, .7 single
361
362    [0x2039] = { 0.70, 0.70 }, -- left single guillemet   ‹
363    [0x203A] = { 0.70, 0.70 }, -- right single guillemet  ›
364    [0x00AB] = { 0.50, 0.50 }, -- left guillemet          «
365    [0x00BB] = { 0.50, 0.50 }, -- right guillemet         »
366
367    [0x2018] = { 0.70, 0.70 }, -- left single quotation mark             ‘
368    [0x2019] = { 0,    0.70 }, -- right single quotation mark            ’
369    [0x201A] = { 0.70, 0    }, -- single low-9 quotation mark            ,
370    [0x201B] = { 0.70, 0    }, -- single high-reversed-9 quotation mark  ‛
371    [0x201C] = { 0.50, 0.50 }, -- left double quotation mark             “
372    [0x201D] = { 0,    0.50 }, -- right double quotation mark            ”
373    [0x201E] = { 0.50, 0    }, -- double low-9 quotation mark            „
374    [0x201F] = { 0.50, 0    }, -- double high-reversed-9 quotation mark  ‟
375
376}
377
378vectors['alpha'] = {
379
380    [0x0041] = { .05, .05 }, -- A
381    [0x0046] = {   0, .05 }, -- F
382    [0x004A] = { .05,   0 }, -- J
383    [0x004B] = {   0, .05 }, -- K
384    [0x004C] = {   0, .05 }, -- L
385    [0x0054] = { .05, .05 }, -- T
386    [0x0056] = { .05, .05 }, -- V
387    [0x0057] = { .05, .05 }, -- W
388    [0x0058] = { .05, .05 }, -- X
389    [0x0059] = { .05, .05 }, -- Y
390
391    [0x006B] = {   0, .05 }, -- k
392    [0x0072] = {   0, .05 }, -- r
393    [0x0074] = {   0, .05 }, -- t
394    [0x0076] = { .05, .05 }, -- v
395    [0x0077] = { .05, .05 }, -- w
396    [0x0078] = { .05, .05 }, -- x
397    [0x0079] = { .05, .05 }, -- y
398
399}
400
401vectors['quality'] = table.merged(
402    vectors['punctuation'],
403    vectors['alpha']
404)
405
406-- As with math, this kind of optimal correction is likely to be messed up when defined in a font and
407-- heuristics work more reliable then. We keep this experiment around for a while but it has been
408-- disabled.
409
410-- -- As this is experimental code, users should not depend on it. The implications are still
411-- -- discussed on the ConTeXt Dev List and we're not sure yet what exactly the spec is (the
412-- -- next code is tested with a gyre font patched by / fea file made by Khaled Hosny). The
413-- -- double trick should not be needed it proper hanging punctuation is used in which case
414-- -- values < 1 can be used.
415-- --
416-- -- preferred (in context, usine vectors):
417-- --
418-- -- \definefontfeature[whatever][default][mode=node,protrusion=quality]
419-- --
420-- -- using lfbd and rtbd, with possibibility to enable only one side :
421-- --
422-- -- \definefontfeature[whocares][default][mode=node,protrusion=yes,  opbd=yes,script=latn]
423-- -- \definefontfeature[whocares][default][mode=node,protrusion=right,opbd=yes,script=latn]
424-- --
425-- -- idem, using multiplier
426-- --
427-- -- \definefontfeature[whocares][default][mode=node,protrusion=2,opbd=yes,script=latn]
428-- -- \definefontfeature[whocares][default][mode=node,protrusion=double,opbd=yes,script=latn]
429-- --
430-- -- idem, using named feature file (less frozen):
431-- --
432-- -- \definefontfeature[whocares][default][mode=node,protrusion=2,opbd=yes,script=latn,featurefile=texgyrepagella-regularxx.fea]
433--
434-- classes['double'] = { -- for testing opbd
435--     factor = 2,
436--     left   = 1,
437--     right  = 1,
438-- }
439--
440-- local function map_opbd_onto_protrusion(tfmdata,value,opbd)
441--     local characters   = tfmdata.characters
442--     local descriptions = tfmdata.descriptions
443--     local properties   = tfmdata.properties
444--     local parameters   = tfmdata.parameters
445--     local resources    = tfmdata.resources
446--     local rawdata      = tfmdata.shared.rawdata
447--     local lookuphash   = rawdata.lookuphash
448--     local lookuptags   = resources.lookuptags
449--     local script       = properties.script
450--     local language     = properties.language
451--     local units        = parameters.units
452--     local done, factor, left, right = false, 1, 1, 1
453--     local class = classes[value]
454--     if class then
455--         factor = class.factor or 1
456--         left   = class.left   or 1
457--         right  = class.right  or 1
458--     else
459--         factor = tonumber(value) or 1
460--     end
461--     local lfactor = left  * factor
462--     local rfactor = right * factor
463--     if trace_protrusion then
464--         report_protrusions("left factor %0.3F, right factor %0.3F",lfactor,rfactor)
465--     end
466--     tfmdata.parameters.protrusion = {
467--         factor = factor,
468--         left   = left,
469--         right  = right,
470--     }
471--     if opbd ~= "right" then
472--         local validlookups, lookuplist = otf.collectlookups(rawdata,"lfbd",script,language)
473--         if validlookups then
474--             for i=1,#lookuplist do
475--                 local lookup = lookuplist[i]
476--                 local steps  = lookup.steps
477--                 if steps then
478--                     if trace_protrusion then
479--                         report_protrusions("setting left using lfbd")
480--                     end
481--                     for i=1,#steps do
482--                         local step     = steps[i]
483--                         local coverage = step.coverage
484--                         if coverage then
485--                             for k, v in next, coverage do
486--                                 if v == true then
487--                                     -- zero
488--                                 else
489--                                     local w = descriptions[k].width
490--                                     local d = - v[1]
491--                                     if w == 0 or d == 0 then
492--                                         -- ignored
493--                                     else
494--                                         local p = lfactor * d/units
495--                                         characters[k].leftprotrusion = p
496--                                         if trace_protrusion then
497--                                             report_protrusions("lfbd -> %0.3F %C",p,k)
498--                                         end
499--                                     end
500--                                 end
501--                             end
502--                         end
503--                     end
504--                     done = true
505--                 end
506--             end
507--         end
508--     end
509--     if opbd ~= "left" then
510--         local validlookups, lookuplist = otf.collectlookups(rawdata,"rtbd",script,language)
511--         if validlookups then
512--             for i=1,#lookuplist do
513--                 local lookup = lookuplist[i]
514--                 local steps  = lookup.steps
515--                 if steps then
516--                     if trace_protrusion then
517--                         report_protrusions("setting right using rtbd")
518--                     end
519--                     for i=1,#steps do
520--                         local step     = steps[i]
521--                         local coverage = step.coverage
522--                         if coverage then
523--                             for k, v in next, coverage do
524--                                 if v == true then
525--                                     -- zero
526--                                 else
527--                                     local w = descriptions[k].width
528--                                     local d = - v[3]
529--                                     if w == 0 or d == 0 then
530--                                         -- ignored
531--                                     else
532--                                         local p = rfactor * d/units
533--                                         characters[k].rightprotrusion = p
534--                                         if trace_protrusion then
535--                                             report_protrusions("rtbd -> %0.3F %C",p,k)
536--                                         end
537--                                     end
538--                                 end
539--                             end
540--                         end
541--                     end
542--                 end
543--                 done = true
544--             end
545--         end
546--     end
547-- end
548
549-- The opbd test is just there because it was discussed on the context development list. However,
550-- the mentioned fxlbi.otf font only has some kerns for digits. So, consider this feature not supported
551-- till we have a proper test font.
552
553local function initialize(tfmdata,value)
554    if value then
555     -- local opbd = tfmdata.shared.features.opbd
556     -- if opbd then
557     --     -- possible values: left right both yes no (experimental)
558     --     map_opbd_onto_protrusion(tfmdata,value,opbd)
559     -- else
560            local class, vector = get_class_and_vector(tfmdata,value,"protrusions")
561            if class then
562                if vector then
563                    local factor = class.factor or 1
564                    local left   = class.left   or 1
565                    local right  = class.right  or 1
566                    if trace_protrusion then
567                        report_protrusions("setting class %a, vector %a, factor %a, left %a, right %a",
568                            value,class.vector,factor,left,right)
569                    end
570                    local data    = characters.data
571                    local lfactor = left  * factor
572                    local rfactor = right * factor
573                    if trace_protrusion then
574                        report_protrusions("left factor %0.3F, right factor %0.3F",lfactor,rfactor)
575                    end
576                    tfmdata.parameters.protrusion = {
577                        factor = factor,
578                        left   = left,
579                        right  = right,
580                        class  = class,
581                        vector = vector,
582                    }
583--                     for i, chr in next, tfmdata.characters do
584--                         local v  = vector[i]
585--                         local pl = nil
586--                         local pr = nil
587--                         if v then
588--                             pl = v[1]
589--                             pr = v[2]
590--                         else
591--                             local d = data[i]
592--                             if d then
593--                                 local s = d.shcode
594--                                 if not s then
595--                                     -- sorry
596--                                 elseif type(s) == "table" then
597--                                     local vl = vector[s[1]]
598--                                     local vr = vector[s[#s]]
599--                                     if vl then pl = vl[1] end
600--                                     if vr then pr = vr[2] end
601--                                 else
602--                                     v = vector[s]
603--                                     if v then
604--                                         pl = v[1]
605--                                         pr = v[2]
606--                                     end
607--                                 end
608--                             end
609--                         end
610--                         if pl and pl ~= 0 then
611--                             local p = pl * lfactor
612--                             chr.leftprotrusion  = p
613--                             if trace_protrusion then
614--                                 report_protrusions("left  -> %0.3F %C ",p,i)
615--                             end
616--                         end
617--                         if pr and pr ~= 0 then
618--                             local p = pr * rfactor
619--                             chr.rightprotrusion = p
620--                             if trace_protrusion then
621--                                 report_protrusions("right -> %0.3F %C",p,i)
622--                             end
623--                         end
624--                     end
625                elseif trace_protrusion then
626                    report_protrusions("unknown vector %a in class %a",class.vector,value)
627                end
628            elseif trace_protrusion then
629                report_protrusions("unknown class %a",value)
630            end
631     -- end
632    end
633end
634
635local specification = {
636    name         = "protrusion",
637    description  = "l/r margin character protrusion",
638    initializers = {
639        base = initialize,
640        node = initialize,
641    }
642}
643
644registerotffeature(specification)
645registerafmfeature(specification)
646
647fonts.goodies.register("protrusions", function(...) return fonts.goodies.report("protrusions", trace_protrusion, ...) end)
648
649implement {
650    name      = "setupfontprotrusion",
651    arguments = "2 strings",
652    actions   = function(class,settings) getparameters(classes,class,'preset',settings) end
653}
654
655local function initialize(tfmdata,value)
656    local properties = tfmdata.properties
657    local parameters = tfmdata.parameters
658    if properties then
659        value = tonumber(value)
660        if value then
661            if value < 0 then
662                value = 0
663            elseif value > 10 then
664                report_expansions("threshold for %a @ %p limited to 10 pct",properties.fontname,parameters.size)
665                value = 10
666            end
667            if value > 5 then
668                report_expansions("threshold for %a @ %p exceeds 5 pct",properties.fontname,parameters.size)
669            end
670        end
671        properties.threshold = value or nil -- nil enforces default
672    end
673end
674
675local specification = {
676    name         = "threshold",
677    description  = "threshold for quality features",
678    initializers = {
679        base = initialize,
680        node = initialize,
681    }
682}
683
684registerotffeature(specification)
685registerafmfeature(specification)
686
687--
688
689local function addquality(id)
690    local tfmdata = fonts.hashes.identifiers[id]
691    if tfmdata then
692        local expansion = tfmdata.parameters.expansion
693        if expansion then
694            local class  = expansion.class
695            local vector = expansion.vector
696            if class and vector then
697             -- local stretch = class.stretch or 0
698             -- local shrink  = class.shrink  or 0
699             -- local step    = class.step    or 0
700                local factor  = class.factor  or 1
701                local data    = characters and characters.data
702                for i, chr in next, tfmdata.characters do -- todo unicode
703                    local v = vector[i]
704                    if data and not v then -- we could move the data test outside (needed for plain)
705                        local d = data[i]
706                        if d then
707                            local s = d.shcode
708                            if not s then
709                                -- sorry
710                            elseif type(s) == "table" then
711                                v = ((vector[s[1]] or 0) + (vector[s[#s]] or 0)) / 2
712                            else
713                                v = vector[s] or 0
714                            end
715                        end
716                    end
717                    -- So, factor influences all shapes but we now obey zero when set!
718                    if v then
719                        if type(v) == "table" then
720                            local e = v[1]
721                            local c = v[2] or 0
722                            if e ~= 0 then
723                                chr.expansion = e * factor * 1000
724                            end
725                            if c ~= 0 then
726                                chr.compression = c * factor * 1000
727                            end
728                        elseif v == 0 then
729                         -- chr.expansion = 0
730                        else
731                            chr.expansion = v * factor * 1000
732                        end
733                    elseif factor ~= 1 then -- test is new
734                        chr.expansion = factor * 1000
735                    end
736                end
737            end
738        end
739        --
740        local protrusion = tfmdata.parameters.protrusion
741        if protrusion then
742            local class  = protrusion.class
743            local vector = protrusion.vector
744            if class and vector then
745                local factor  = class.factor or 1
746                local left    = class.left   or 1
747                local right   = class.right  or 1
748                local data    = characters.data
749                local lfactor = left  * factor
750                local rfactor = right * factor
751                for i, chr in next, tfmdata.characters do
752                    local v  = vector[i]
753                    local pl = nil
754                    local pr = nil
755                    if v then
756                        pl = v[1]
757                        pr = v[2]
758                    else
759                        local d = data[i]
760                        if d then
761                            local s = d.shcode
762                            if not s then
763                                -- sorry
764                            elseif type(s) == "table" then
765                                local vl = vector[s[1]]
766                                local vr = vector[s[#s]]
767                                if vl then pl = vl[1] end
768                                if vr then pr = vr[2] end
769                            else
770                                v = vector[s]
771                                if v then
772                                    pl = v[1]
773                                    pr = v[2]
774                                end
775                            end
776                        end
777                    end
778                    if pl and pl ~= 0 then
779                        pl = pl * lfactor * chr.width
780                        if trace_protrusion then
781                            report_protrusions("left  -> %0.3F %C ",pl,i)
782                        end
783                        chr.leftprotrusion = pl
784                    end
785                    if pr and pr ~= 0 then
786                        pr = pr * rfactor * chr.width
787                        chr.rightprotrusion = pr
788                        if trace_protrusion then
789                            report_protrusions("right -> %0.3F %C",pr,i)
790                        end
791                    end
792                end
793            end
794        end
795        --
796        if expansion or protrusion then
797            font.addquality(id,tfmdata)
798        end
799    end
800end
801
802callback.register("quality_font",addquality)
803