lpdf-fix-imp-fonts.lmt /size: 34 Kb    last modification: 2024-01-16 10:22
1if not modules then modules = { } end modules ['lpdf-fnt'] = {
2    version   = 1.001,
3    comment   = "companion to lpdf-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
9-- In LMTX we have to do this different than in MkIV. We also prepare ourselves
10-- variable fonts and such. In LuaTeX we use the original index but in LMTX we
11-- use a decent sequence which means that we need to resolve the original. This
12-- kind of hackery is fragile anyway, so we only merge files that are produced
13-- by ConTeXt.
14
15local next, type, getmetatable = next, type, getmetatable
16local gsub, format, match, find, gmatch = string.gsub, string.format, string.match, string.find, string.gmatch
17local setmetatableindex, sortedhash, sequenced = table.setmetatableindex, table.sortedhash, table.sequenced
18local nameonly, basename = file.nameonly, file.basename
19local hextointeger, chrtointeger = string.hextointeger, string.chrtointeger
20
21local pdfe             = lpdf.epdf
22local pdfreference     = lpdf.reference
23local pdfreserveobject = lpdf.reserveobject
24
25local trace_merge      = false  trackers.register("graphics.fonts",function(v) trace_merge = v end)
26local report_merge     = logs.reporter("graphics","fonts")
27
28local expanded         = pdfe.expanded
29local contenttostring  = pdfe.contenttostring
30local getpagecontent   = pdfe.getpagecontent
31local parsecontent     = pdfe.parsecontent
32
33----- definefont       = fonts.definers.internal
34local definefont       = fonts.definers.define
35local getstreamhash    = fonts.handlers.otf.getstreamhash
36local loadstreamdata   = fonts.handlers.otf.loadstreamdata
37local cleanfontname    = fonts.names.cleanname
38
39local chardata = fonts.hashes.characters
40
41local defined = setmetatableindex(function(t,filename)
42    local v = setmetatableindex(function(t,subfont)
43        local v = { }
44        t[subfont] = v
45        return v
46    end)
47    t[filename] = v
48    return v
49end)
50
51local function toinstance(instance)
52    if type(instance) == "table" then
53        return nil, "axis={" .. sequenced(instance.__raw__,",") .. "}"
54    elseif instance and instance ~= "" then
55        return instance, nil
56    else
57        return nil, nil
58    end
59end
60
61-- This is a bit of a hack ... we need to be able to set the instance directly
62-- on a file.
63
64local function isdefinedlmtx(filename,subfont,instance,hash,version,glyphcount)
65    local fontname = "file:" .. filename
66    local instance, features = toinstance(instance)
67    if instance then
68        fontname = "name:" .. nameonly(filename) .. instance -- not ok as it's not fontname
69        instance = nil
70    end
71    local id = defined[fontname][subfont][instance or features or false]
72    if not id then
73        -- we can try to avoid this
74        id = definefont {
75            name     = fontname,
76            instance = instance,
77            detail   = features,
78        }
79        if id > 0 then
80            local dummy = lpdf.usedcharacters[id] -- force embedding
81        else
82            id = false
83        end
84        defined[fontname][subfont][instance or features or false] = id
85    end
86    if id then
87        -- We double check here!
88        local shash, sdata = getstreamhash(id)
89        if hash ~= shash then
90            report_merge("inconsistent %s in %a","hashes",filename)
91            return false
92        end
93        sdata = loadstreamdata(sdata)
94        if not sdata then
95            report_merge("inconsistent %s in %a","streamdata",filename)
96            return false
97        end
98        local fontheader = sdata.fontheader
99        if version and fontheader and version ~= fontheader.fontversion then
100            report_merge("inconsistent %s in %a","versions",filename)
101            return false
102        end
103        local streams = sdata.streams
104        if glyphcount and streams and glyphcount ~= (#streams + (streams[0] and 1 or 0)) then
105            report_merge("inconsistent %s in %a","glyphcount",filename)
106            return false
107        end
108        return id
109    end
110    return false
111end
112
113-- todo: check some more
114
115local cleanname = fonts.names.cleanname
116
117local remap = {
118 -- ["helvetica"]            = { target = "file:uhvr8a.afm" },
119 -- ["helvetica-bold"]       = { target = "file:uhvb8a.afm" },
120 -- ["helveticabold"]        = { target = "file:uhvb8a.afm" },
121 -- ["courier"]              = { target = "file:ucrr8a.afm" }, -- notdef issues width
122 -- ["courier-bold"]         = { target = "file:ucrb8a.afm" },
123 -- ["courierbold"]          = { target = "file:ucrb8a.afm" },
124    ["helvetica"]            = { target = "file:texgyre-heros-regular.otf" },
125    ["helveticabold"]        = { target = "file:texgyre-heros-bold.otf" },
126    ["helveticaitalic"]      = { target = "file:texgyre-heros-italic.otf" },
127    ["helveticabolditalic"]  = { target = "file:texgyre-heros-bolditalic.otf" },
128    ["courier"]              = { target = "file:texgyre-cursor-regular.otf" },
129    ["courierbold"]          = { target = "file:texgyre-cursor-bold.otf" },
130    ["courieritalic"]        = { target = "file:texgyre-cursor-italic.otf" },
131    ["courierbolditalic"]    = { target = "file:texgyre-cursor-bolditalic.otf" },
132    ["timesroman"]           = { target = "file:texgyre-termes-regular.otf" },
133    ["timesromanbold"]       = { target = "file:texgyre-termes-bold.otf" },
134    ["timesromanitalic"]     = { target = "file:texgyre-termes-italic.otf" },
135    ["timesromanbolditalic"] = { target = "file:texgyre-termes-bolditalic.otf" },
136}
137
138function backends.codeinjections.registerfont(specification)
139    local source = specification.source
140    if source then
141        remap[cleanname(source)] = specification
142    end
143end
144
145local function isdefinedunknown(fontname)
146    local m = remap[cleanname(fontname)]
147    local r = m and m.target
148    if r then
149        report_merge("remapping %a to %a",fontname,r)
150        name = r
151    else
152        name = "name:" .. fontname
153    end
154    local id = definefont {
155        name = name,
156    }
157    if id > 0 then
158        local dummy = lpdf.usedcharacters[id] -- force embedding
159    else
160        id = false
161    end
162    if id then
163        local shash, sdata = getstreamhash(id)
164     -- sdata = loadstreamdata(sdata) -- no checking here
165        return shash, id
166    end
167    return false
168end
169
170-- todo: we can share the map within a pdfdoc .. using the object number
171
172local status = {
173    files     = { },
174    pages     = 0,
175    xobjects  = 0,
176    charprocs = 0,
177    merged    = 0,
178    notmerged = 0,
179    indices   = 0,
180}
181
182statistics.register("compact font inclusion", function()
183    if status.pages > 0 or status.xobjects > 0 or status.charprocs > 0 then
184        return string.format(
185            "%i files, %i pages, %i indices, %i xobjects, %i chrprocs, %i times merged, %i times not merged",
186            table.count(status.files),
187            status.pages,
188            status.indices,
189            status.xobjects,
190            status.charprocs,
191            status.merged,
192            status.notmerged
193        )
194    end
195end)
196
197local function checkedfontreference(k,v,key,value,o)
198    if key ~= k then
199-- print("different keys",key,k)
200        return value -- different keys
201    elseif v[1] ~= 10 or value[1] ~= 10 then
202-- print("different objects",key,k)
203        return value -- different objects
204    elseif v[3] ~= value[3] then
205-- print("different values",key,k)
206        return value -- different values
207    else
208        return pdfreference(o)
209    end
210end
211
212local getstates, getindexstate_composite, getindexstate_simple  do
213
214    local fromunicode16 = fonts.mappings.fromunicode16
215    local expandwidths  = pdfe.expandwidths
216    local mergewidths   = pdfe.mergewidths
217
218    local function initialize(t,k)
219        local v = {
220            unicodes = { },
221            widths   = { },
222            fontname = k,
223        }
224        t[k] = v
225        return v
226    end
227
228    getstates = function(pdfdoc)
229        local states = pdfdoc.fontstates
230        if not states then
231            states = {
232                Type1    = setmetatableindex(initialize), -- simple fonts, 1 byte index
233                Type3    = setmetatableindex(initialize), -- idem
234                TrueType = setmetatableindex(initialize), -- idem
235                OpenType = setmetatableindex(initialize), -- composite fonts, 2 byte index
236            }
237            pdfdoc.fontstates = states
238        end
239        return states
240    end
241
242    local splitsixteen  do
243
244        local lpegmatch = lpeg.match
245
246        local more = 0
247
248     -- local pattern = lpeg.P(true) / function() more = 0 end * (
249        local pattern = (
250            lpeg.C(4) / function(s) -- needs checking !
251                local now = hextointeger(s)
252                if more > 0 then
253                    now = (more-0xD800)*0x400 + (now-0xDC00) + 0x10000
254                    more = 0
255                    return now
256                elseif now >= 0xD800 and now <= 0xDBFF then
257                    more = now
258                else
259                    return now
260                end
261            end
262        )^0
263
264        splitsixteen = function(str)
265            if str and str ~= "" then
266                more = 0
267                return lpegmatch(pattern,str)
268            end
269        end
270
271    end
272
273    -- This could be an lpeg but there is not that much to gain here.
274
275    local function register1(pdfdoc,unicodes,index,uni)
276        local old = unicodes[index]
277        if not old then
278            unicodes[index] = uni
279        elseif old ~= uni then
280            report_merge("inconsistent unicode file %a, font %a, index 0x%04X, old %U, new %U, %s",pdfdoc.filename,fontname,index,old,new,"range")
281        end
282    end
283
284    local function register2(pdfdoc,unicodes,index,uni)
285        local old = unicodes[index]   -- unicode
286        local new, more = splitsixteen(uni) -- unicode16 or ligature
287        if more then
288            if not old then
289                unicodes[index] = uni -- string
290            elseif old ~= uni then
291                report_merge("inconsistent unicode file %a, font %a, index 0x%04X, old %a, new %a, %s",pdfdoc.filename,fontname,index,old,new,"bfchar")
292            end
293        else
294            if not old then
295                unicodes[index] = new
296            elseif old ~= new then
297                report_merge("inconsistent unicode file %a, font %a, index 0x%04X, old %U, new %U, %s",pdfdoc.filename,fontname,index,old,new,"bfchar")
298            end
299        end
300    end
301
302    local function getunicodes(pdfdoc,fontname,str,unicodes)
303        -- <0000> <005E> <0020> : first index, last index, first unicode
304        for s in gmatch(str,"beginbfrange%s*(.-)%s*endbfrange") do
305            for first, last, offset in gmatch(s,"<([^>]+)>%s+<([^>]+)>%s+<([^>]+)>") do
306                local first = tonumber(first,16)    -- index
307                local last  = tonumber(last,16)     -- index
308                local uni   = fromunicode16(offset) -- unicode16
309                for index=first,last do
310                    register1(pdfdoc,unicodes,index,uni)
311                    uni = uni + 1
312                end
313            end
314        end
315        -- <005F> <0061> [<00660066> <00660069> <00660066006C>] -- untested as not seen yet
316        for s in gmatch(str,"beginbfrange%s*(.-)%s*endbfrange") do
317            for first, last, offset in gmatch(s,"<([^>]+)>%s+<([^>]+)>%s+%[([^%]]+)%]") do
318                local index = tonumber(first,16) -- index
319                for uni in gmatch("%s*<([^>]+)>") do
320                    register2(pdfdoc,unicodes,index,uni)
321                    index = index + 1
322                end
323            end
324        end
325        -- <0000> <0020>     : index, single
326        -- <005F> <00660066> : index, ligature
327        for s in gmatch(str,"beginbfchar%s*(.-)%s*endbfchar") do
328            for idx, uni in gmatch(s,"<([^>]+)>%s+<([^>]+)>") do
329                local index = tonumber(idx,16)  -- index
330                register2(pdfdoc,unicodes,index,uni)
331            end
332        end
333    end
334
335    local function isembedded(descriptor)
336        return descriptor and (descriptor.FontFile or descriptor.FontFile2 or descriptor.FontFile3) and true or false
337    end
338
339    getindexstate_composite = function(pdfdoc,somefont,descendant,states)
340        local basefont = somefont.BaseFont
341        if basefont then
342            local fontname = match(basefont,"^[A-Z]+%+(.+)$")
343            if fontname then
344                local descriptor = descendant.FontDescriptor
345                if descriptor then
346                    local widths    = descendant.W
347                    local tounicode = somefont.ToUnicode
348                    if widths and tounicode then
349                        local fontstate  = states[fontname]
350                        local f_widths   = fontstate.widths
351                        local f_unicodes = fontstate.unicodes
352                        expandwidths(widths,f_widths)
353                        getunicodes(pdfdoc,fontname,tounicode(),f_unicodes)
354                        fontstate.embedded = isembedded(descriptor)
355                        return fontstate
356                    end
357               end
358            end
359        end
360    end
361
362    getindexstate_simple = function(pdfdoc,somefont,states)
363        local basefont = somefont.BaseFont
364        if basefont then
365            local fontname = match(basefont,"^[A-Z]+%+(.+)$") or basefont
366            if fontname then
367                local descriptor = somefont.FontDescriptor
368                if descriptor then
369                    local widths    = somefont.Widths
370                    local tounicode = somefont.ToUnicode
371                    if widths and tounicode then
372                        local fontstate  = states[fontname]
373                        local f_widths   = fontstate.widths
374                        local f_unicodes = fontstate.unicodes
375                        fontstate.narrow = true
376                        mergewidths(widths,f_widths)
377                        getunicodes(pdfdoc,fontname,tounicode(),f_unicodes)
378                        fontstate.embedded = isembedded(descriptor)
379                        return fontstate
380                    end
381                end
382                -- tricky when we have the same fontname twice, once as type 1 or truetype
383                -- and once as opentype .. it really happens
384                local encoding = somefont.Encoding
385                if encoding == "WinAnsiEncoding" then
386                    local r = table.load(resolvers.findfile("regi-cp1252.lua"))
387                    local fontstate  = states[fontname]
388                    fontstate.unicodes = r
389                    fontstate.narrow = true
390                    return fontstate
391                else
392                    -- todo: custom encoding, load from pfb if Type 1
393                end
394           end
395        end
396    end
397
398end
399
400
401local function makemap(fontname,id,state,unicode)
402    local map = { }
403    local r = remap[cleanname(fontname)]
404    if r and r.unicode ~= nil then
405        unicode = r.unicode
406    end
407    if unicode then
408        local chr = chardata[id]
409        for k, v in next, state.unicodes do
410            local d = chr[v]
411            if d then
412                map[k] = d.index
413            else
414                -- issue
415            end
416        end
417    else
418        for k, v in next, state.unicodes do
419            map[k] = k
420        end
421    end
422    return map
423end
424
425local function dontembed(basefont,state,embedding)
426    if not state.embedded then
427        report_merge("font %a is not embedded",basefont)
428    end
429    if embedding and state.embedded then
430        return false
431    else
432        return true
433    end
434end
435
436local function getstate_OpenType(pdfdoc,v,d,embedding)
437    local state = getindexstate_composite(pdfdoc,v,d,getstates(pdfdoc).OpenType)
438    if state then
439        local basefont = d.BaseFont
440        if basefont then
441            if dontembed(basefont,state,embedding) then
442                return false
443            end
444            local fontname = match(basefont,"^[A-Z]+%+(.+)$") or basefont
445            local cleanname = cleanfontname(fontname)
446            local streamhash, id = isdefinedunknown(fontname)
447            if streamhash then
448                return {
449                    id         = id,
450                    map        = makemap(fontname,id,state,false),
451                    streamhash = streamhash,
452                    filename   = fontname,
453                 -- subfont    = subfont,
454                 -- instance   = instance,
455                    used       = lpdf.usedindices[streamhash],
456                }
457            end
458        end
459    end
460end
461
462local function getstate_TrueType(pdfdoc,v,embedding)
463    local state = getindexstate_simple(pdfdoc,v,getstates(pdfdoc).TrueType)
464    if state then
465     -- needs checking when unicode ... NOT OK
466        local basefont = v.BaseFont
467        if basefont then
468            if dontembed(basefont,state,embedding) then
469                return false
470            end
471            local fontname = match(basefont,"^[A-Z]+%+(.+)$") or basefont
472            local cleanname = cleanfontname(fontname)
473            local streamhash, id = isdefinedunknown(fontname)
474            if streamhash then
475                return {
476                    id         = id,
477                    map        = makemap(fontname,id,state,true),
478                    narrow     = state.narrow,
479                    streamhash = streamhash,
480                    filename   = fontname,
481                 -- subfont    = subfont,
482                 -- instance   = instance,
483                    used       = lpdf.usedindices[streamhash],
484                }
485            end
486        end
487    end
488end
489
490local function getstate_Type1(pdfdoc,v,embedding)
491    local state = getindexstate_simple(pdfdoc,v,getstates(pdfdoc).Type1)
492    if state then
493        local basefont = v.BaseFont
494        if basefont then
495            if dontembed(basefont,state,embedding) then
496                return false
497            end
498            local fontname = match(basefont,"^[A-Z]+%+(.+)$") or basefont
499            local cleanname = cleanfontname(fontname)
500            local streamhash, id = isdefinedunknown(fontname)
501            if streamhash then
502                return {
503                    id         = id,
504                    map        = makemap(fontname,id,state,true),
505                    narrow     = state.narrow,
506                    streamhash = streamhash,
507                    filename   = fontname,
508                 -- subfont    = subfont,
509                 -- instance   = instance,
510                    used       = lpdf.usedindices[streamhash],
511                }
512            end
513        end
514    end
515end
516
517local function getstate_LMTX(pdfdoc,r)
518    local indexmap   = r.IndexMap
519    local streamhash = r.StreamHash
520    local filename   = r.FileName
521    local subfont    = r.SubFont or 1
522    local instance   = r.Instance or ""
523    local version    = r.Version or "0"
524    local glyphcount = r.GlyphCount or 0
525    if indexmap then
526        local index = -1
527        local map   = { }
528        for i=1,#indexmap do
529            local li = indexmap[i]
530            if type(li) == "number" then
531                index = li
532            else
533                for j=1,#li do
534                    map[index] = li[j]
535                    index = index + 1
536                end
537            end
538        end
539        if isdefinedlmtx(filename,subfont,instance,streamhash,version,glyphcount) then
540            return {
541                map        = map,
542                streamhash = streamhash,
543                filename   = filename,
544                subfont    = subfont,
545                instance   = instance,
546                used       = lpdf.usedindices[streamhash],
547            }
548        end
549    end
550end
551
552-- yes    : merge when we have a context file
553-- always : merge and assume original indices
554-- embed  : add missing fonts
555-- fix    : convert decimal into hexadecimal
556
557do
558
559    local h_hex_2 = lpdf.h_hex_2
560    local h_hex_4 = lpdf.h_hex_4
561
562    local function report_sharing(pdfdoc,what,v,shared,pagenumber,lmtx)
563        report_merge("page %i of %a, font %a, type %a, encoding %a, %sshared%s",
564            pagenumber,
565            basename(pdfdoc.filename),
566            v.BaseFont or "?",
567            what,
568            v.Encoding or "?",
569            shared and "" or "not ",
570            lmtx and ", lmtx registry found" or ""
571        )
572    end
573
574    local function plugin_Type0(pdfdoc,k,v,sharedfonts,data,wide,compactor,pagenumber)
575        if v.Encoding == "Identity-H"  then
576            -- The v table is unique and can be shared
577            local shared = sharedfonts[v]
578            if type(shared) == "table" then
579                data[k] = shared
580            elseif shared == nil then
581                shared = false
582                local d = v.DescendantFonts[1] -- how about more
583                if d and d.Subtype == "CIDFontType0" or d.Subtype == "CIDFontType2" then
584                    local r = d.LMTXRegistry
585                    if r then
586                        if compactor.merge.lmtx or compactor.merge.LMTX then
587                            shared = getstate_LMTX(pdfdoc,r)
588                            data[k] = shared
589                        end
590                    elseif find(pdfe.producer(pdfdoc),"^LuaMetaTeX") then
591                        -- This is a no go because for sure we have a different index order. Older
592                        -- versions append the version to the producer string.
593                    elseif compactor.merge.type0 then
594                        shared = getstate_OpenType(pdfdoc,v,d)
595                        data[k] = shared
596                    elseif compactor.embed.type0 then
597                        shared = getstate_OpenType(pdfdoc,v,d,true)
598                        data[k] = shared
599                    end
600                    if trace_merge then
601                        report_sharing(pdfdoc,"type0",v,shared,pagenumber,r and true or false)
602                    end
603                end
604                sharedfonts[v] = shared
605            end
606        else
607            -- what ...
608        end
609        wide[k] = true
610    end
611
612    local function plugin_TrueType(pdfdoc,k,v,sharedfonts,data,wide,compactor,pagenumber)
613        local shared = sharedfonts[v]
614        if type(shared) == "table" then
615            data[k] = shared
616        elseif shared == nil then
617            shared = false
618            if compactor.merge.truetype then
619                shared = getstate_TrueType(pdfdoc,v)
620                data[k] = shared
621            elseif compactor.embed.truetype then
622                shared = getstate_TrueType(pdfdoc,v,true)
623                data[k] = shared
624            end
625            if trace_merge then
626                report_sharing(pdfdoc,"truetype",v,shared,pagenumber)
627            end
628            sharedfonts[v] = shared
629        end
630        wide[k] = true
631    end
632
633    local function plugin_Type1(pdfdoc,k,v,sharedfonts,data,wide,compactor,pagenumber)
634        local shared = sharedfonts[v]
635        if type(shared) == "table" then
636            data[k] = shared
637        elseif shared == nil then
638            shared = false
639            if compactor.merge.type1 then
640                shared = getstate_Type1(pdfdoc,v)
641                data[k] = shared
642            elseif compactor.embed.type1 then
643                shared = getstate_Type1(pdfdoc,v,true)
644                data[k] = shared
645            end
646            if trace_merge then
647                report_sharing(pdfdoc,"type1",v,shared,pagenumber)
648            end
649        end
650        sharedfonts[v] = shared
651        wide[k] = false
652    end
653
654    local plugin -- defined after the next one
655
656    local function plugin_Type3(pdfdoc,k,v,sharedfonts,data,wide,compactor,pagenumber)
657        local charprocs = v.CharProcs
658        if charprocs then
659            local resources = v.Resources
660            if resources then
661                local fonts    = resources.Font
662                local xobjects = resources.XObject
663                if fonts or xobjects then
664                    for k, object in expanded(charprocs) do
665                        if not object.__remapped__ then
666                            local contents = object()
667                            contents = parsecontent(contents,true)
668                            local contents, indices = plugin(pdfdoc,contents,fonts,xobjects,pagenumber,compactor,{})
669                            if indices > 0 then
670                                getmetatable(object).__call = function() return contents end
671                            end
672                            object.__remapped__ = true
673                            status.charprocs = status.charprocs + 1
674                            status.indices   = status.indices + indices
675                        end
676                    end
677                end
678            end
679        end
680        wide[k] = false
681    end
682
683    local handlers = {
684        Type0    = plugin_Type0,
685        TrueType = plugin_TrueType,
686        Type1    = plugin_Type1,
687        Type3    = plugin_Type3,
688    }
689
690    -- not always ok .. every page can have different font references but let's
691    -- assume it for now
692
693    plugin = function(pdfdoc,contents,fonts,xobjects,pagenumber,compactor,adapted,depth)
694
695        local data = { }
696        local wide = { }
697
698        local sharedfonts  = pdfdoc.sharedfonts or { }
699        pdfdoc.sharedfonts = sharedfonts
700
701        if fonts then
702
703            -- check if ref for k is the same
704
705            for k, v in expanded(fonts) do
706                local subtype = v.Subtype
707                local handler = subtype and handlers[subtype]
708                if handler then
709                    handler(pdfdoc,k,v,sharedfonts,data,wide,compactor,pagenumber)
710                else
711                    -- weird
712                end
713            end
714
715            local r = false
716            local f = false
717            local n = false
718            local m = false
719            local u = false
720
721            local new     = { }
722            local old     = { }
723            local indices = 0
724
725            local process_hex = false
726--             local process_dec = false
727--             local convert_dec = false
728
729            -- if we move h and m outside the function we can use lpegs .. todo
730
731            local function process_hex_hexified(h)
732                local b = hextointeger(h)
733                local i = m[b]
734                if i then
735                    local n = h_hex_4[u[i]]
736                    if h ~= n then
737                        indices = indices + 1
738                    end
739                    return n
740                else
741                    return h_hex_4[b]
742                end
743            end
744
745--             local function process_dec_hexified(h)
746--                 local b = chrtointeger(h)
747--                 local i = m[b]
748--                 if i then
749--                     local n = h_hex_4[u[i]]
750--                     if h ~= n then
751--                         indices = indices + 1
752--                     end
753--                     return n
754--                 else
755--                     return h_hex_4[b]
756--                 end
757--             end
758
759            -- use helper
760
761--             local function convert_dec_hexified(h)
762--                 local b = chrtointeger(h)
763--                 return h_hex_2[b]
764--             end
765
766            local function process_hex_narrow(s) return (gsub(s,"..",  process_hex_hexified)) end
767            local function process_hex_wide  (s) return (gsub(s,"....",process_hex_hexified)) end
768--             local function process_dec_narrow(s) return (gsub(s,".",   process_dec_hexified)) end
769--             local function process_dec_wide  (s) return (gsub(s,"..",  process_dec_hexified)) end
770
771--             local function convert_dec_one   (s) return (gsub(s,".",   convert_dec_hexified)) end
772
773-- local fix = compactor.convert.hexadecimal
774
775
776            for i=1,#contents do
777                local ti = contents[i]
778                if ti[3] == "Tf" then
779                    -- maybe use /R<N> for replacement
780                    f = ti[1][2]
781                    d = data[f]
782                    if d then
783                        m = d.map
784                        u = d.used
785                        r = i
786                        new[f] = d.streamhash
787                        if d.narrow then
788                            process_hex = process_hex_narrow
789--                             process_dec = process_dec_narrow
790                        else
791                            process_hex = process_hex_wide
792--                             process_dec = process_dec_wide
793                        end
794                    else
795                        if d == false then
796                            old[f] = true
797                        end
798--                         if fix then
799--                             if w == wide[f] then
800--                                 convert_dec = false
801--                             else
802--                                 convert_dec = convert_dec_one
803--                             end
804--                         end
805                        f = false
806                    end
807                elseif ti[2] == "Tj" then
808                    if f then
809                        local ci = ti[1]
810                        if type(ci) == "table" then
811                            local tp = ci[1]
812                            if tp == "hex" then
813                                ci[2] = process_hex(ci[2])
814--                             elseif tp == "dec" then
815--                                 ci[1] = "hex"
816--                                 ci[2] = process_dec(ci[2])
817                            end
818--                     elseif fix and convert_dec then
819--                         local ci = ti[1]
820--                         if type(ci) == "table" then
821--                             local tp = ci[1]
822--                             if tp == "dec" then
823--                                 ci[1] = "hex"
824--                                 ci[2] = convert_dec(ci[2])
825--                             end
826                        end
827                    end
828                elseif ti[2] == "TJ" then
829                    if f then
830                        local c = ti[1][2]
831                        for i=1,#c do
832                            local ci = c[i]
833                            if type(ci) == "table" then
834                                local tp = ci[1]
835                                if tp == "hex" then
836                                    ci[2] = process_hex(ci[2])
837--                                 elseif tp == "dec" then
838--                                     ci[1] = "hex"
839--                                     ci[2] = process_dec(ci[2])
840                                end
841                            end
842--                     elseif fix and convert_dec then
843--                         local c = ti[1][2]
844--                         for i=1,#c do
845--                             local ci = c[i]
846--                             if type(ci) == "table" then
847--                                 local tp = ci[1]
848--                                 if tp == "dec" then
849--                                     ci[1] = "hex"
850--                                     ci[2] = convert_dec(ci[2])
851--                                 end
852--                             end
853                        end
854                    end
855                elseif xobjects and ti[2] == "Do" then
856                    -- can be recursive
857                    local object = xobjects[ti[1][2]]
858                    if object and not object.__remapped__ and object.Subtype == "Form" then
859                        local r = object.Resources
860                        if r then
861                            local contents = object()
862                            local fonts    = r.Font
863                            local xobjects = r.XObject
864                            contents = parsecontent(contents,true)
865                            local contents = plugin(pdfdoc,contents,fonts,xobjects,pagenumber,compactor,adapted,depth+1)
866                            if contents then
867                                contents = contenttostring(contents)
868                                object.__raw__.Length = #contents
869                                getmetatable(object).__call = function() return contents end
870                                object.__remapped__ = true
871                                status.xobjects = status.xobjects + 1
872                            else
873                                -- some issue
874                            end
875                        end
876                    end
877                end
878            end
879
880            local state = trace_merge and { } or false
881
882            if fonts then
883                for k, v in next, fonts.__raw__ do -- we need the raw values here
884--                 for k, v in expanded(fonts) do -- we need the raw values here
885                    if adapted[k] then
886                        -- already done
887                    elseif new[k] then
888                        local o = lpdf.getfontobjectnumber(new[k])
889                        fonts.__raw__[k] = { pdfe.objectcodes.lpdf, pdfreference(o) }
890                        adapted[k] = true
891                     -- adapted[k] = function(_,_,_,key,value)
892                     --     local ref = checkedfontreference(k,v,key,value,o)
893                     --     return ref
894                     -- end
895                        if state then
896                            state[k] = true
897                        end
898                    elseif state and old[k] then
899                        state[k] = false
900                    end
901                end
902            end
903
904            if state then
905                local filename = basename(pdfdoc.filename)
906                for k, v in sortedhash(state) do
907                    if v then
908                        local d = data[k]
909                        report_merge(
910                            "page %i of %a, font reference %a to %a, subfont %a, instance %a, merged",
911                            pagenumber,filename,k,d.filename,d.subfont,toinstance(d.instance)
912                        )
913                        status.merged = status.merged + 1
914                    else
915                        report_merge(
916                            "page %i of %a, font reference %a, not merged",
917                            pagenumber,filename,k
918                        )
919                        status.notmerged = status.notmerged + 1
920                    end
921                end
922            end
923
924            if indices then
925                status.indices = status.indices + indices
926            end
927
928            return contents
929        end
930
931    end
932
933    function pdfe.fontplugin(pdfdoc,page,pagenumber,resources,compactor)
934        local fonts    = resources.Font
935        local xobjects = resources.XObject
936        if fonts or xobjects then
937            local contents = getpagecontent(pdfdoc,pagenumber,true,true)
938            contents = plugin(pdfdoc,contents,fonts,xobjects,pagenumber,compactor,{},1)
939            resources.Font = fonts -- really needed
940            if contents then
941                page.Contents = contenttostring(contents)
942            end
943            status.pages = status.pages + 1
944            status.files[pdfdoc.filename] = (status.files[pdfdoc.filename] or 0) + 1
945        end
946    end
947
948    utilities.sequencers.appendaction("pdfcontentmanipulators","system","lpdf.epdf.fontplugin")
949    utilities.sequencers.enableaction("pdfcontentmanipulators","lpdf.epdf.fontplugin")
950
951end
952