font-imp-quality.lua /size: 20 Kb    last modification: 2020-07-01 14:35
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
127local function initialize(tfmdata,value)
128    if value then
129        local class, vector = get_class_and_vector(tfmdata,value,"expansions")
130        if class then
131            if vector then
132                local stretch = class.stretch or 0
133                local shrink  = class.shrink  or 0
134                local step    = class.step    or 0
135                local factor  = class.factor  or 1
136                if trace_expansion then
137                    report_expansions("setting class %a, vector %a, factor %a, stretch %a, shrink %a, step %a",
138                        value,class.vector,factor,stretch,shrink,step)
139                end
140                tfmdata.parameters.expansion = {
141                    stretch = 10 * stretch,
142                    shrink  = 10 * shrink,
143                    step    = 10 * step,
144                    factor  = factor,
145                }
146                local data = characters and characters.data
147                for i, chr in next, tfmdata.characters do
148                    local v = vector[i]
149                    if data and not v then -- we could move the data test outside (needed for plain)
150                        local d = data[i]
151                        if d then
152                            local s = d.shcode
153                            if not s then
154                                -- sorry
155                            elseif type(s) == "table" then
156                                v = ((vector[s[1]] or 0) + (vector[s[#s]] or 0)) / 2
157                            else
158                                v = vector[s] or 0
159                            end
160                        end
161                    end
162                    if v and v ~= 0 then
163                        chr.expansion_factor = v*factor
164                    else -- can be option
165                        chr.expansion_factor = factor
166                    end
167                end
168            elseif trace_expansion then
169                report_expansions("unknown vector %a in class %a",class.vector,value)
170            end
171        elseif trace_expansion then
172            report_expansions("unknown class %a",value)
173        end
174    end
175end
176
177local specification = {
178    name        = "expansion",
179    description = "apply hz optimization",
180    initializers = {
181        base = initialize,
182        node = initialize,
183    }
184}
185
186registerotffeature(specification)
187registerafmfeature(specification)
188
189fonts.goodies.register("expansions",  function(...) return fonts.goodies.report("expansions", trace_expansion, ...) end)
190
191implement {
192    name      = "setupfontexpansion",
193    arguments = "2 strings",
194    actions   = function(class,settings) getparameters(classes,class,'preset',settings) end
195}
196
197-- -- -- -- -- --
198-- protrusion
199-- -- -- -- -- --
200
201fonts.protrusions   = allocate()
202local protrusions   = fonts.protrusions
203
204protrusions.classes = allocate()
205protrusions.vectors = allocate()
206
207local classes       = protrusions.classes
208local vectors       = protrusions.vectors
209
210-- the values need to be revisioned
211
212classes.preset = {
213    factor = 1,
214    left   = 1,
215    right  = 1,
216}
217
218classes['pure']        = { vector = 'pure',        factor = 1 }
219classes['punctuation'] = { vector = 'punctuation', factor = 1 }
220classes['alpha']       = { vector = 'alpha',       factor = 1 }
221classes['quality']     = { vector = 'quality',     factor = 1 }
222
223vectors['pure'] = {
224
225    [0x002C] = { 0, 1    }, -- comma
226    [0x002E] = { 0, 1    }, -- period
227    [0x003A] = { 0, 1    }, -- colon
228    [0x003B] = { 0, 1    }, -- semicolon
229    [0x002D] = { 0, 1    }, -- hyphen
230    [0x00AD] = { 0, 1    }, -- also hyphen
231    [0x2013] = { 0, 0.50 }, -- endash
232    [0x2014] = { 0, 0.33 }, -- emdash
233    [0x3001] = { 0, 1    }, -- ideographic comma      、
234    [0x3002] = { 0, 1    }, -- ideographic full stop  。
235    [0x060C] = { 0, 1    }, -- arabic comma           ،
236    [0x061B] = { 0, 1    }, -- arabic semicolon       ؛
237    [0x06D4] = { 0, 1    }, -- arabic full stop       ۔
238
239}
240
241vectors['punctuation'] = {
242
243    [0x003F] = { 0,    0.20 }, -- ?
244    [0x00BF] = { 0.20, 0    }, -- ¿
245    [0x0021] = { 0,    0.20 }, -- !
246    [0x00A1] = { 0.20, 0,   }, -- ¡
247    [0x0028] = { 0.05, 0    }, -- (
248    [0x0029] = { 0,    0.05 }, -- )
249    [0x005B] = { 0.05, 0    }, -- [
250    [0x005D] = { 0,    0.05 }, -- ]
251    [0x002C] = { 0,    0.70 }, -- comma
252    [0x002E] = { 0,    0.70 }, -- period
253    [0x003A] = { 0,    0.50 }, -- colon
254    [0x003B] = { 0,    0.50 }, -- semicolon
255    [0x002D] = { 0,    0.70 }, -- hyphen
256    [0x00AD] = { 0,    0.70 }, -- also hyphen
257    [0x2013] = { 0,    0.30 }, -- endash
258    [0x2014] = { 0,    0.20 }, -- emdash
259    [0x060C] = { 0,    0.70 }, -- arabic comma
260    [0x061B] = { 0,    0.50 }, -- arabic semicolon
261    [0x06D4] = { 0,    0.70 }, -- arabic full stop
262    [0x061F] = { 0,    0.20 }, -- ؟
263
264    -- todo: left and right quotes: .5 double, .7 single
265
266    [0x2039] = { 0.70, 0.70 }, -- left single guillemet   ‹
267    [0x203A] = { 0.70, 0.70 }, -- right single guillemet  ›
268    [0x00AB] = { 0.50, 0.50 }, -- left guillemet          «
269    [0x00BB] = { 0.50, 0.50 }, -- right guillemet         »
270
271    [0x2018] = { 0.70, 0.70 }, -- left single quotation mark             ‘
272    [0x2019] = { 0,    0.70 }, -- right single quotation mark            ’
273    [0x201A] = { 0.70, 0    }, -- single low-9 quotation mark            ,
274    [0x201B] = { 0.70, 0    }, -- single high-reversed-9 quotation mark  ‛
275    [0x201C] = { 0.50, 0.50 }, -- left double quotation mark             “
276    [0x201D] = { 0,    0.50 }, -- right double quotation mark            ”
277    [0x201E] = { 0.50, 0    }, -- double low-9 quotation mark            „
278    [0x201F] = { 0.50, 0    }, -- double high-reversed-9 quotation mark  ‟
279
280}
281
282vectors['alpha'] = {
283
284    [0x0041] = { .05, .05 }, -- A
285    [0x0046] = {   0, .05 }, -- F
286    [0x004A] = { .05,   0 }, -- J
287    [0x004B] = {   0, .05 }, -- K
288    [0x004C] = {   0, .05 }, -- L
289    [0x0054] = { .05, .05 }, -- T
290    [0x0056] = { .05, .05 }, -- V
291    [0x0057] = { .05, .05 }, -- W
292    [0x0058] = { .05, .05 }, -- X
293    [0x0059] = { .05, .05 }, -- Y
294
295    [0x006B] = {   0, .05 }, -- k
296    [0x0072] = {   0, .05 }, -- r
297    [0x0074] = {   0, .05 }, -- t
298    [0x0076] = { .05, .05 }, -- v
299    [0x0077] = { .05, .05 }, -- w
300    [0x0078] = { .05, .05 }, -- x
301    [0x0079] = { .05, .05 }, -- y
302
303}
304
305vectors['quality'] = table.merged(
306    vectors['punctuation'],
307    vectors['alpha']
308)
309
310-- As this is experimental code, users should not depend on it. The implications are still
311-- discussed on the ConTeXt Dev List and we're not sure yet what exactly the spec is (the
312-- next code is tested with a gyre font patched by / fea file made by Khaled Hosny). The
313-- double trick should not be needed it proper hanging punctuation is used in which case
314-- values < 1 can be used.
315--
316-- preferred (in context, usine vectors):
317--
318-- \definefontfeature[whatever][default][mode=node,protrusion=quality]
319--
320-- using lfbd and rtbd, with possibibility to enable only one side :
321--
322-- \definefontfeature[whocares][default][mode=node,protrusion=yes,  opbd=yes,script=latn]
323-- \definefontfeature[whocares][default][mode=node,protrusion=right,opbd=yes,script=latn]
324--
325-- idem, using multiplier
326--
327-- \definefontfeature[whocares][default][mode=node,protrusion=2,opbd=yes,script=latn]
328-- \definefontfeature[whocares][default][mode=node,protrusion=double,opbd=yes,script=latn]
329--
330-- idem, using named feature file (less frozen):
331--
332-- \definefontfeature[whocares][default][mode=node,protrusion=2,opbd=yes,script=latn,featurefile=texgyrepagella-regularxx.fea]
333
334classes['double'] = { -- for testing opbd
335    factor = 2,
336    left   = 1,
337    right  = 1,
338}
339
340local function map_opbd_onto_protrusion(tfmdata,value,opbd)
341    local characters   = tfmdata.characters
342    local descriptions = tfmdata.descriptions
343    local properties   = tfmdata.properties
344    local parameters   = tfmdata.parameters
345    local resources    = tfmdata.resources
346    local rawdata      = tfmdata.shared.rawdata
347    local lookuphash   = rawdata.lookuphash
348    local lookuptags   = resources.lookuptags
349    local script       = properties.script
350    local language     = properties.language
351    local units        = parameters.units
352    local done, factor, left, right = false, 1, 1, 1
353    local class = classes[value]
354    if class then
355        factor = class.factor or 1
356        left   = class.left   or 1
357        right  = class.right  or 1
358    else
359        factor = tonumber(value) or 1
360    end
361    local lfactor = left  * factor
362    local rfactor = right * factor
363    if trace_protrusion then
364        report_protrusions("left factor %0.3F, right factor %0.3F",lfactor,rfactor)
365    end
366    tfmdata.parameters.protrusion = {
367        factor = factor,
368        left   = left,
369        right  = right,
370    }
371    if opbd ~= "right" then
372        local validlookups, lookuplist = otf.collectlookups(rawdata,"lfbd",script,language)
373        if validlookups then
374            for i=1,#lookuplist do
375                local lookup = lookuplist[i]
376                local steps  = lookup.steps
377                if steps then
378                    if trace_protrusion then
379                        report_protrusions("setting left using lfbd")
380                    end
381                    for i=1,#steps do
382                        local step     = steps[i]
383                        local coverage = step.coverage
384                        if coverage then
385                            for k, v in next, coverage do
386                                if v == true then
387                                    -- zero
388                                else
389                                    local w = descriptions[k].width
390                                    local d = - v[1]
391                                    if w == 0 or d == 0 then
392                                        -- ignored
393                                    else
394                                        local p = lfactor * d/units
395                                        characters[k].left_protruding = p
396                                        if trace_protrusion then
397                                            report_protrusions("lfbd -> %0.3F %C",p,k)
398                                        end
399                                    end
400                                end
401                            end
402                        end
403                    end
404                    done = true
405                end
406            end
407        end
408    end
409    if opbd ~= "left" then
410        local validlookups, lookuplist = otf.collectlookups(rawdata,"rtbd",script,language)
411        if validlookups then
412            for i=1,#lookuplist do
413                local lookup = lookuplist[i]
414                local steps  = lookup.steps
415                if steps then
416                    if trace_protrusion then
417                        report_protrusions("setting right using rtbd")
418                    end
419                    for i=1,#steps do
420                        local step     = steps[i]
421                        local coverage = step.coverage
422                        if coverage then
423                            for k, v in next, coverage do
424                                if v == true then
425                                    -- zero
426                                else
427                                    local w = descriptions[k].width
428                                    local d = - v[3]
429                                    if w == 0 or d == 0 then
430                                        -- ignored
431                                    else
432                                        local p = rfactor * d/units
433                                        characters[k].right_protruding = p
434                                        if trace_protrusion then
435                                            report_protrusions("rtbd -> %0.3F %C",p,k)
436                                        end
437                                    end
438                                end
439                            end
440                        end
441                    end
442                end
443                done = true
444            end
445        end
446    end
447end
448
449-- The opbd test is just there because it was discussed on the context development list. However,
450-- the mentioned fxlbi.otf font only has some kerns for digits. So, consider this feature not supported
451-- till we have a proper test font.
452
453local function initialize(tfmdata,value)
454    if value then
455        local opbd = tfmdata.shared.features.opbd
456        if opbd then
457            -- possible values: left right both yes no (experimental)
458            map_opbd_onto_protrusion(tfmdata,value,opbd)
459        else
460            local class, vector = get_class_and_vector(tfmdata,value,"protrusions")
461            if class then
462                if vector then
463                    local factor = class.factor or 1
464                    local left   = class.left   or 1
465                    local right  = class.right  or 1
466                    if trace_protrusion then
467                        report_protrusions("setting class %a, vector %a, factor %a, left %a, right %a",
468                            value,class.vector,factor,left,right)
469                    end
470                    local data    = characters.data
471                    local lfactor = left  * factor
472                    local rfactor = right * factor
473                    if trace_protrusion then
474                        report_protrusions("left factor %0.3F, right factor %0.3F",lfactor,rfactor)
475                    end
476                    tfmdata.parameters.protrusion = {
477                        factor = factor,
478                        left   = left,
479                        right  = right,
480                    }
481                    for i, chr in next, tfmdata.characters do
482                        local v  = vector[i]
483                        local pl = nil
484                        local pr = nil
485                        if v then
486                            pl = v[1]
487                            pr = v[2]
488                        else
489                            local d = data[i]
490                            if d then
491                                local s = d.shcode
492                                if not s then
493                                    -- sorry
494                                elseif type(s) == "table" then
495                                    local vl = vector[s[1]]
496                                    local vr = vector[s[#s]]
497                                    if vl then pl = vl[1] end
498                                    if vr then pr = vr[2] end
499                                else
500                                    v = vector[s]
501                                    if v then
502                                        pl = v[1]
503                                        pr = v[2]
504                                    end
505                                end
506                            end
507                        end
508                        if pl and pl ~= 0 then
509                            local p = pl * lfactor
510                            chr.left_protruding  = p
511                            if trace_protrusion then
512                                report_protrusions("left  -> %0.3F %C ",p,i)
513                            end
514                        end
515                        if pr and pr ~= 0 then
516                            local p = pr * rfactor
517                            chr.right_protruding = p
518                            if trace_protrusion then
519                                report_protrusions("right -> %0.3F %C",p,i)
520                            end
521                        end
522                    end
523                elseif trace_protrusion then
524                    report_protrusions("unknown vector %a in class %a",class.vector,value)
525                end
526            elseif trace_protrusion then
527                report_protrusions("unknown class %a",value)
528            end
529        end
530    end
531end
532
533local specification = {
534    name         = "protrusion",
535    description  = "l/r margin character protrusion",
536    initializers = {
537        base = initialize,
538        node = initialize,
539    }
540}
541
542registerotffeature(specification)
543registerafmfeature(specification)
544
545fonts.goodies.register("protrusions", function(...) return fonts.goodies.report("protrusions", trace_protrusion, ...) end)
546
547implement {
548    name      = "setupfontprotrusion",
549    arguments = "2 strings",
550    actions   = function(class,settings) getparameters(classes,class,'preset',settings) end
551}
552
553local function initialize(tfmdata,value)
554    local properties = tfmdata.properties
555    local parameters = tfmdata.parameters
556    if properties then
557        value = tonumber(value)
558        if value then
559            if value < 0 then
560                value = 0
561            elseif value > 10 then
562                report_expansions("threshold for %a @ %p limited to 10 pct",properties.fontname,parameters.size)
563                value = 10
564            end
565            if value > 5 then
566                report_expansions("threshold for %a @ %p exceeds 5 pct",properties.fontname,parameters.size)
567            end
568        end
569        properties.threshold = value or nil -- nil enforces default
570    end
571end
572
573local specification = {
574    name         = "threshold",
575    description  = "threshold for quality features",
576    initializers = {
577        base = initialize,
578        node = initialize,
579    }
580}
581
582registerotffeature(specification)
583registerafmfeature(specification)
584