lpdf-emb.lmt /size: 107 Kb    last modification: 2024-01-16 09:02
1if not modules then modules = { } end modules ['lpdf-ini'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to lpdf-ini.mkiv",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10-- vkgoeswild: Pink Floyd - Shine on You Crazy Diamond - piano cover (around that
11-- time I redid the code, a reminder so to say)
12
13-- At some point I wanted to have access to the shapes so that we could use them in
14-- metapost. So, after looking at the cff and ttf specifications, I decided to write
15-- parsers. At somepoint we needed a cff parser anyway in order to calculate the
16-- dimensions. Then variable fonts came around and a option was added to recreate
17-- streams of operators and a logical next step was to do all inclusion that way. It
18-- was only then that I found out that some of the juggling also happens in the the
19-- backend, but spread over places, so I could have saved myself some time
20-- deciphering the specifications. Anyway, here we go.
21--
22-- Color fonts are a bit messy. Catching issues with older fonts will break new ones
23-- so I don't think that it's wise to build in too many catches (like for fonts with
24-- zero boundingboxes, weird dimensions, transformations that in a next version are
25-- fixed, etc.). Better is then to wait till something gets fixed. If a spec doesn't
26-- tell me how to deal with it ... I'll happily wait till it does. After all we're
27-- not in a hurry as these fonts are mostly meant for the web or special purposes
28-- with manual tweaking in desk top publishing applications. Keep in mind that Emoji
29-- can have funny dimensions (e.g. to be consistent within a font, so no tight ones).
30
31-- When we have moved to lmtx I will document a bit more. Till then it's experimental
32-- and subjected to change.
33
34local next, type, unpack, rawget = next, type, unpack, rawget
35local char, byte, gsub, sub, match, rep, gmatch, find = string.char, string.byte, string.gsub, string.sub, string.match, string.rep, string.gmatch, string.find
36local formatters, format = string.formatters, string.format
37local concat, sortedhash, sortedkeys, sort, count = table.concat, table.sortedhash, table.sortedkeys, table.sort, table.count
38local utfchar = utf.char
39local random, round, max, abs, ceiling = math.random, math.round, math.max, math.abs, math.ceiling
40local setmetatableindex = table.setmetatableindex
41
42local pdfnull              = lpdf.null
43local pdfdictionary        = lpdf.dictionary
44local pdfarray             = lpdf.array
45local pdfconstant          = lpdf.constant
46local pdfstring            = lpdf.string
47local pdfreference         = lpdf.reference
48
49local pdfreserveobject     = lpdf.reserveobject
50local pdfflushobject       = lpdf.flushobject
51local pdfflushstreamobject = lpdf.flushstreamobject
52
53local report_fonts         = logs.reporter("backend","fonts")
54
55local trace_fonts          = false
56local trace_details        = false
57
58local dimenfactors  = number.dimenfactors
59local bpfactor      = dimenfactors.bp
60local ptfactor      = dimenfactors.pt
61
62trackers.register("backend.fonts",        function(v) trace_fonts   = v end)
63trackers.register("backend.fonts.details",function(v) trace_details = v end)
64
65local readers = fonts.handlers.otf.readers
66local getinfo = readers.getinfo
67
68local setposition = utilities.files.setposition
69local readstring  = utilities.files.readstring
70local openfile    = utilities.files.open
71local closefile   = utilities.files.close
72
73local getmapentry = fonts.mappings.getentry
74
75-- needs checking: signed vs unsigned
76
77-- todo: use streams.tocardinal4 etc
78
79local tocardinal1 = char
80
81local function tocardinal2(n)
82 -- return char(extract8(n,8),extract8(n,0))
83    return char((n>>8)&0xFF,(n>>0)&0xFF)
84end
85
86local function tocardinal3(n)
87 -- return char(extract8(n,16),extract8(n,8),extract8(n,0))
88    return char((n>>16)&0xFF,(n>>8)&0xFF,(n>>0)&0xFF)
89end
90
91local function tocardinal4(n)
92 -- return char(extract8(n,24),extract8(n,16),extract8(n,8),extract8(n,0))
93    return char((n>>24)&0xFF,(n>>16)&0xFF,(n>>8)&0xFF,(n>>0)&0xFF)
94end
95
96local function tointeger2(n)
97 -- return char(extract8(n,8),extract8(n,0))
98    return char((n>>8)&0xFF,(n>>0)&0xFF)
99end
100
101local function tointeger3(n)
102 -- return char(extract8(n,16),extract8(n,8),extract8(n,0))
103    return char((n>>16)&0xFF,(n>>8)&0xFF,(n>>0)&0xFF)
104end
105
106local function tointeger4(n)
107 -- return char(extract8(n,24),extract8(n,16),extract8(n,8),extract8(n,0))
108    return char((n>>24)&0xFF,(n>>16)&0xFF,(n>>8)&0xFF,(n>>0)&0xFF)
109end
110
111local function tocardinal8(n)
112    local l = n // 0x100000000
113    local r = n % 0x100000000
114 -- return char(extract8(l,24) & 0xFF,extract8(l,16) & 0xFF,extract8(l,8) & 0xFF,extract8(l,0) & 0xFF,
115 --             extract8(r,24) & 0xFF,extract8(r,16) & 0xFF,extract8(r,8) & 0xFF,extract8(r,0) & 0xFF)
116    return char((l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,(l>>0)&0xFF,
117                (r>>24)&0xFF,(r>>16)&0xFF,(r>>8)&0xFF,(r>>0)&0xFF)
118end
119
120-- A couple of shared helpers.
121
122local tounicodedictionary, widtharray, lmtxregistry, collectindices, subsetname, includecidset, forcecidset, tocidsetdictionary
123
124do
125
126    -- Because we supply tounicodes ourselves we only use bfchar mappings (as in the
127    -- backend). In fact, we can now no longer pass the tounicodes to the frontend but
128    -- pick them up from the descriptions.
129
130    local f_mapping_2 = formatters["<%02X> <%s>"]
131    local f_mapping_4 = formatters["<%04X> <%s>"]
132
133    local tounicode = fonts.mappings.tounicode
134
135local tounicode_template <const> = [[
136%%!PS-Adobe-3.0 Resource-CMap
137%%%%DocumentNeededResources: ProcSet (CIDInit)
138%%%%IncludeResource: ProcSet (CIDInit)
139%%%%BeginResource: CMap (TeX-%s-0)
140%%%%Title: (TeX-%s-0 TeX %s 0)|
141%%%%Version: 1.000
142%%%%EndComments
143/CIDInit /ProcSet findresource begin
144  12 dict begin
145    begincmap
146      /CIDSystemInfo
147        << /Registry (TeX) /Ordering (%s) /Supplement 0 >>
148      def
149      /CMapName
150        /TeX-Identity-%s
151      def
152      /CMapType
153        2
154      def
155      1 begincodespacerange
156        <%s> <%s>
157      endcodespacerange
158      %i beginbfchar
159%s
160      endbfchar
161    endcmap
162    CMapName currentdict /CMap defineresource pop
163  end
164end
165%%%%EndResource
166%%%%EOF]]
167
168    tounicodedictionary = function(details,indices,maxindex,name,wide)
169        local mapping = { }
170        local length  = 0
171        if maxindex > 0 then
172            local f_mapping = wide and f_mapping_4 or f_mapping_2
173            for index=1,maxindex do
174                local data = indices[index]
175                if data then
176                    length = length + 1
177                    local unicode = data.unicode
178                    if unicode then
179                        unicode = tounicode(unicode)
180                    else
181                        unicode = "FFFD" -- wide only
182                    end
183                    mapping[length] = f_mapping(index,unicode)
184                end
185            end
186        end
187        local name  = gsub(name,"%+","-") -- like luatex does
188        local first = wide and "0000" or "00"
189        local last  = wide and "FFFF" or "FF"
190        local blob  = format(tounicode_template,name,name,name,name,name,first,last,length,concat(mapping,"\n"))
191        return blob
192    end
193
194    widtharray = function(details,indices,maxindex,units,correction)
195        local widths = pdfarray()
196        if maxindex > 0 then
197            local length    = 0
198            local factor    = 10000 / (units * (correction or 1))
199            local lastindex = -1
200            local sublist   = nil
201            for index=1,maxindex do
202                local data = indices[index]
203                if data then
204                    local width = data.width -- hm, is inaccurate for cff, so take from elsewhere
205                    if width then
206                        if correction then
207                         -- width = round(width * 10000 / units) / 10
208                            width = round(width * factor) / 10
209                        end
210                    else
211                        width = 0
212                    end
213                    if index == lastindex + 1 then
214                        sublist[#sublist+1] = width
215                    else
216                        if sublist then
217                            length = length + 1
218                            widths[length] = sublist
219                        end
220                        sublist = pdfarray { width }
221                        length  = length + 1
222                        widths[length] = index
223                    end
224                    lastindex = index
225                end
226            end
227            length = length + 1
228            widths[length] = sublist
229        end
230        return widths
231    end
232
233    local includebackmap = true
234
235    directives.register("backend.pdf.includebackmap", function(v)
236 -- directives.register("backend.pdf.lmtxregistry", function(v)
237        includebackmap = v
238    end)
239
240    lmtxregistry = function(details,include,blobs,minindex,maxindex)
241        if includebackmap and maxindex > 0 then
242            local backmap    = pdfarray()
243            local streamhash = details.hash
244            local properties = details.properties
245            local filename   = file.basename(properties.filename)
246            local filetype   = file.suffix(filename) -- for now
247            local fontname   = properties.name or file.nameonly(filename)
248            local instance   = properties.instance or ""
249            if filename and (filetype == "otf" or filetype == "ttf") then
250                local streams = details.streams
251                if streams then
252                    local fontheader = streams.fontheader
253                    if fontheader then
254                        local streams = streams.streams
255                        if streams then
256                            local subfont    = details.properties.subfont or 1
257                            local length     = 0
258                            local lastindex  = -1
259                            local sublist    = nil
260                            for index=minindex,maxindex do
261                                local idx = include[index]
262                                if idx then
263                                    local data = blobs[index]
264                                    if index == lastindex + 1 then
265                                        sublist[#sublist+1] = idx
266                                    else
267                                        if sublist then
268                                            length = length + 1
269                                            backmap[length] = sublist
270                                        end
271                                        sublist = pdfarray { idx }
272                                        length  = length + 1
273                                        backmap[length] = index
274                                    end
275                                    lastindex = index
276                                end
277                            end
278                            length = length + 1
279                            backmap[length] = sublist
280                            if instance == "" then
281                                instance = nil
282                            elseif find(instance,"=") then
283                                local i = fonts.handlers.otf.readers.helpers.axistofactors(instance)
284                                instance = pdfdictionary(i)
285                            end
286                            if subfont == 1 then
287                                subfont = nil
288                            end
289                            return pdfreference(pdfflushobject(pdfdictionary {
290                                IndexMap   = pdfreference(pdfflushobject(backmap)) or nil,
291                                StreamHash = streamhash or nil,
292                                FileName   = filename,
293                                FontName   = fontname,
294                                SubFont    = subfont,
295                                Instance   = instance,
296                                Version    = fontheader.fontversion,
297                                GlyphCount = #streams + (streams[0] and 1 or 0),
298                                FontMode   = fonts.mode(),
299                            } ))
300                        end
301                    end
302                end
303            end
304        end
305    end
306
307    -- we need to go through indices because descriptions can be different (take
308    -- basemode remappings)
309
310    collectindices = function(descriptions,indices,used,hash)
311        local minindex = 0xFFFF
312        local maxindex = 0
313        local reverse  = { }
314        local copies   = { }
315        local include  = { }
316        local resolved = { }
317
318        setmetatable(used,nil) -- prevent more index allocations
319        for unicode, data in next, descriptions do
320            local index = data.index
321            reverse[index or unicode] = data
322            if used[index] and data.dupindex then
323                copies[index] = data.dupindex
324            end
325        end
326        for index, usedindex in next, indices do
327            if usedindex > maxindex then
328                maxindex = usedindex
329            end
330            if usedindex < minindex then
331                minindex = usedindex
332            end
333            include[usedindex]  = copies[index] or index
334            resolved[usedindex] = reverse[index]
335        end
336        if minindex > maxindex then
337            minindex = maxindex
338        end
339        if trace_details then
340            report_fonts("embedded: hash %a, minindex %i, maxindex %i",hash,minindex,maxindex)
341            for k, v in sortedhash(include) do
342                local d = resolved[k]
343                if d and d.dupindex then
344                    report_fonts("  0x%04X : 0x%04X (duplicate 0x%05X)",k,v,d.index)
345                else
346                    report_fonts("  0x%04X : 0x%04X",k,v)
347                end
348            end
349        end
350        return resolved, include, minindex, maxindex
351    end
352
353    includecidset = true  -- nicer for standard checking
354    forcecidset   = false -- for private testing only
355
356    function lpdf.setincludecidset(v)
357        -- incredible ... it's obselete, no viewer needs it so why still this crap
358        -- so in the end it had to be introduced again
359        includecidset = v
360    end
361
362    directives.register("backend.pdf.forcecidset",function(v)
363        forcecidset = v
364    end)
365
366 -- tocidsetdictionary = function(indices,min,max) -- also works ok
367 --     if includecidset or forcecidset then
368 --         local s = sparse.new(1)
369 --         sparse.set(s,0)
370 --         for i=min,max do
371 --             if indices[i] then
372 --                 sparse.set(s,i)
373 --             end
374 --         end
375 --         s = sparse.concat(s,nil,nil,2) -- msb first
376 --         return pdfreference(pdfflushstreamobject(s))
377 --     end
378 -- end
379
380    local function tocidset(indices,min,max,forcenotdef)
381        if not max then
382            max = count(indices)
383        end
384        local b = { }
385        local m = (max // 8) + 1
386        for i=0,m do
387            b[i] = 0 -- we can do a newtable instead
388        end
389        if forcenotdef then
390            b[0] = b[0] | (1 << 7) -- force notdef into the file
391        end
392        for i in next, indices do
393            local bi = i // 8
394            local ni = i % 8
395            b[bi] = b[bi] | (1 << (7-ni))
396        end
397        return char(unpack(b,0,#b)) -- use helper
398    end
399
400    lpdf.tocidset = tocidset
401
402    tocidsetdictionary = function(indices,min,max)
403        if includecidset or forcecidset then
404            local b = tocidset(indices,min,max,true)
405            return pdfreference(pdfflushstreamobject(b))
406        end
407    end
408
409    -- Actually we can use the same as we only embed once.
410
411    -- subsetname = function(name)
412    --     return "CONTEXT" .. name
413    -- end
414
415    local prefixes = { } -- todo: set fixed one
416
417    subsetname = function(name)
418        local prefix
419        while true do
420            prefix = utfchar(random(65,90),random(65,90),random(65,90),random(65,90),random(65,90),random(65,90))
421            if not prefixes[prefix] then
422                prefixes[prefix] = true
423                break
424            end
425        end
426        return prefix .. "+" .. name
427    end
428
429end
430
431-- The three writers: opentype, truetype and type1.
432
433local mainwriters  = { }
434
435do
436
437    -- advh = os2.ascender - os2.descender
438    -- tsb  = default_advh - os2.ascender
439
440    -- truetype has the following tables:
441
442    -- head : mandate
443    -- hhea : mandate
444    -- vhea : mandate
445    -- hmtx : mandate
446    -- maxp : mandate
447    -- glyf : mandate
448    -- loca : mandate
449    --
450    -- cvt  : if needed (but we flatten)
451    -- fpgm : if needed (but we flatten)
452    -- prep : if needed (but we flatten)
453    -- PCLT : if needed (but we flatten)
454    --
455    -- name : not needed for T2: backend does that
456    -- post : not needed for T2: backend does that
457    -- OS/2 : not needed for T2: backend does that
458    -- cmap : not needed for T2: backend does that
459
460    local streams       = utilities.streams
461    local openstring    = streams.openstring
462    local readcardinal2 = streams.readcardinal2
463    ----- readcardinal4 = streams.readcardinal4
464
465    local otfreaders    = fonts.handlers.otf.readers
466
467    local function readcardinal4(f) -- this needs to be sorted out
468        local a = readcardinal2(f)
469        local b = readcardinal2(f)
470        if a and b then
471            return a * 0x10000 + b
472        end
473    end
474
475    -- -- --
476
477    local tablereaders  = { }
478    local tablewriters  = { }
479    local tablecreators = { }
480    local tableloaders  = { }
481
482    local openfontfile, closefontfile, makefontfile, makemetadata  do
483
484        local details    = {
485            details        = true,
486            platformnames  = true,
487            platformextras = true,
488        }
489
490        -- .022 sec on luatex manual, neglectable:
491
492     -- local function checksum(data)
493     --     local s = openstring(data)
494     --     local n = 0
495     --     local d = #data
496     --     while true do
497     --         local c = readcardinal4(s)
498     --         if c then
499     --             n = (n + c) % 0x100000000
500     --         else
501     --             break
502     --         end
503     --     end
504     --     return n
505     -- end
506
507        local function checksum(data)
508            local s = openstring(data)
509            local n = 0
510            local d = #data
511            while true do
512                local a = readcardinal2(s)
513                local b = readcardinal2(s)
514                if b then
515                    n = (n + a * 0x10000 + b) % 0x100000000
516                else
517                    break
518                end
519            end
520            return n
521        end
522
523        openfontfile = function(details)
524            return {
525                offset  = 0,
526                order   = { },
527                used    = { },
528                details = details,
529                streams = details.streams,
530            }
531        end
532
533        closefontfile = function(fontfile)
534            for k, v in next, fontfile do
535                fontfile[k] = nil -- so it can be collected asap
536            end
537        end
538
539        local metakeys = {
540            "uniqueid", "version",
541            "copyright", "license", "licenseurl",
542            "manufacturer", "vendorurl",
543            "family", "subfamily",
544            "typographicfamily", "typographicsubfamily",
545            "fullname", "postscriptname",
546        }
547
548    local template <const> = [[
549<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
550  <x:xmpmeta xmlns:x="adobe:ns:meta/">
551    <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
552      <rdf:Description rdf:about="" xmlns:pdfx="http://ns.adobe.com/pdfx/1.3/">
553
554%s
555
556      </rdf:Description>
557    </rdf:RDF>
558  </x:xmpmeta>
559<?xpacket end="w"?>]]
560
561        makemetadata = function(fontfile)
562            local names  = fontfile.streams.names
563            local list   = { }
564            local f_name = formatters["<pdfx:%s>%s</pdfx:%s>"]
565            for i=1,#metakeys do
566                local m = metakeys[i]
567                local n = names[m]
568                if n then
569                    list[#list+1] = f_name(m,n,m)
570                end
571            end
572            return format(template,concat(list,"\n"))
573        end
574
575        makefontfile = function(fontfile)
576            local order = fontfile.order
577            local used  = fontfile.used
578            local count = 0
579            for i=1,#order do
580                local tag  = order[i]
581                local data = fontfile[tag]
582                if data and #data > 0 then
583                    count = count + 1
584                else
585                    fontfile[tag] = false
586                end
587            end
588            local offset = 12 + (count * 16)
589            local headof = 0
590            local list   = {
591                "" -- placeholder
592            }
593            local i = 1
594            local k = 0
595            while i <= count do
596                i = i << 1
597                k = k + 1
598            end
599            local searchrange   = i << 3
600            local entryselector = k - 1
601            local rangeshift    = (count  << 4) - (i << 3)
602            local index  = {
603                tocardinal4(0x00010000), -- tables.version
604                tocardinal2(count),
605                tocardinal2(searchrange),
606                tocardinal2(entryselector),
607                tocardinal2(rangeshift),
608            }
609            --
610            local ni = #index
611            local nl = #list
612            for i=1,#order do
613                local tag  = order[i]
614                local data = fontfile[tag]
615                if data then
616                    local csum    = checksum(data)
617                    local dlength = #data
618                    local length  = ((dlength + 3) // 4) * 4
619                    local padding = length - dlength
620                    nl = nl + 1 ; list[nl] = data
621                    for i=1,padding do
622                        nl = nl + 1 ; list[nl] = "\0"
623                    end
624                    if #tag == 3 then
625                        tag = tag .. " "
626                    end
627                    ni = ni + 1 ; index[ni] = tag -- must be 4 chars
628                    ni = ni + 1 ; index[ni] = tocardinal4(csum)
629                    ni = ni + 1 ; index[ni] = tocardinal4(offset)
630                    ni = ni + 1 ; index[ni] = tocardinal4(dlength)
631                    used[i] = offset -- not used
632                    if tag == "head" then
633                        headof = offset
634                    end
635                    offset = offset + length
636                end
637            end
638            list[1] = concat(index)
639            local off = #list[1] + headof + 1 + 8
640            list = concat(list)
641            local csum = (0xB1B0AFBA - checksum(list)) % 0x100000000
642            list = sub(list,1,off-1) .. tocardinal4(csum) .. sub(list,off+4,#list)
643            return list
644        end
645
646        local function register(fontfile,name)
647            local u = fontfile.used
648            local o = fontfile.order
649            if not u[name] then
650                o[#o+1] = name
651                u[name] = true
652            end
653        end
654
655        local function create(fontfile,name)
656            local t = { }
657            fontfile[name] = t
658            return t
659        end
660
661        local function write(fontfile,name)
662            local t = fontfile[name]
663            if not t then
664                return
665            end
666            register(fontfile,name)
667            if type(t) == "table" then
668                if t[0] then
669                    fontfile[name] = concat(t,"",0,#t)
670                elseif #t > 0 then
671                    fontfile[name] = concat(t)
672                else
673                    fontfile[name] = false
674                end
675            end
676        end
677
678        tablewriters.head = function(fontfile)
679            register(fontfile,"head")
680            local t = fontfile.streams.fontheader
681            fontfile.head = concat {
682                tocardinal4(t.version),
683                tocardinal4(t.fontversionnumber),
684                tocardinal4(t.checksum),
685                tocardinal4(t.magic),
686                tocardinal2(t.flags),
687                tocardinal2(t.units),
688                tocardinal8(t.created),
689                tocardinal8(t.modified),
690                tocardinal2(t.xmin),
691                tocardinal2(t.ymin),
692                tocardinal2(t.xmax),
693                tocardinal2(t.ymax),
694                tocardinal2(t.macstyle),
695                tocardinal2(t.smallpixels),
696                tocardinal2(t.directionhint),
697                tocardinal2(t.indextolocformat),
698                tocardinal2(t.glyphformat),
699            }
700        end
701
702        tablewriters.hhea = function(fontfile)
703            register(fontfile,"hhea")
704            local t = fontfile.streams.horizontalheader
705            local n = t and fontfile.nofglyphs or 0
706            fontfile.hhea = concat {
707                tocardinal4(t.version),
708                tocardinal2(t.ascender),
709                tocardinal2(t.descender),
710                tocardinal2(t.linegap),
711                tocardinal2(t.maxadvancewidth),
712                tocardinal2(t.minleftsidebearing),
713                tocardinal2(t.minrightsidebearing),
714                tocardinal2(t.maxextent),
715                tocardinal2(t.caretsloperise),
716                tocardinal2(t.caretsloperun),
717                tocardinal2(t.caretoffset),
718                tocardinal2(t.reserved_1),
719                tocardinal2(t.reserved_2),
720                tocardinal2(t.reserved_3),
721                tocardinal2(t.reserved_4),
722                tocardinal2(t.metricdataformat),
723                tocardinal2(n) -- t.nofmetrics
724            }
725        end
726
727        tablewriters.vhea = function(fontfile)
728            local t = fontfile.streams.verticalheader
729            local n = t and fontfile.nofglyphs or 0
730            register(fontfile,"vhea")
731            fontfile.vhea = concat {
732                tocardinal4(t.version),
733                tocardinal2(t.ascender),
734                tocardinal2(t.descender),
735                tocardinal2(t.linegap),
736                tocardinal2(t.maxadvanceheight),
737                tocardinal2(t.mintopsidebearing),
738                tocardinal2(t.minbottomsidebearing),
739                tocardinal2(t.maxextent),
740                tocardinal2(t.caretsloperise),
741                tocardinal2(t.caretsloperun),
742                tocardinal2(t.caretoffset),
743                tocardinal2(t.reserved_1),
744                tocardinal2(t.reserved_2),
745                tocardinal2(t.reserved_3),
746                tocardinal2(t.reserved_4),
747                tocardinal2(t.metricdataformat),
748                tocardinal2(n) -- t.nofmetrics
749            }
750        end
751
752        tablewriters.maxp = function(fontfile)
753            register(fontfile,"maxp")
754            local t = fontfile.streams.maximumprofile
755            local n = fontfile.nofglyphs
756            -- if fontfile.streams.cffinfo then
757                -- error
758            -- end
759            fontfile.maxp = concat {
760                tocardinal4(0x00010000),
761                tocardinal2(n),
762                tocardinal2(t.points),
763                tocardinal2(t.contours),
764                tocardinal2(t.compositepoints),
765                tocardinal2(t.compositecontours),
766                tocardinal2(t.zones),
767                tocardinal2(t.twilightpoints),
768                tocardinal2(t.storage),
769                tocardinal2(t.functiondefs),
770                tocardinal2(t.instructiondefs),
771                tocardinal2(t.stackelements),
772                tocardinal2(t.sizeofinstructions),
773                tocardinal2(t.componentelements),
774                tocardinal2(t.componentdepth),
775            }
776        end
777
778        tablecreators.loca = function(fontfile) return create(fontfile,"loca") end
779        tablewriters .loca = function(fontfile) return write (fontfile,"loca") end
780
781        tablecreators.glyf = function(fontfile) return create(fontfile,"glyf") end
782        tablewriters .glyf = function(fontfile) return write (fontfile,"glyf") end
783
784        tablecreators.hmtx = function(fontfile) return create(fontfile,"hmtx") end
785        tablewriters .hmtx = function(fontfile) return write (fontfile,"hmtx") end
786
787        tablecreators.vmtx = function(fontfile) return create(fontfile,"vmtx") end
788        tablewriters .vmtx = function(fontfile) return write (fontfile,"vmtx") end
789
790        tableloaders .cmap = function(fontfile) return read  (fontfile,"cmap") end
791        tablewriters .cmap = function(fontfile) return write (fontfile,"cmap") end
792
793        tableloaders .name = function(fontfile) return read  (fontfile,"name") end
794        tablewriters .name = function(fontfile) return write (fontfile,"name") end
795
796        tableloaders .post = function(fontfile) return read  (fontfile,"post") end
797        tablewriters .post = function(fontfile) return write (fontfile,"post") end
798
799    end
800
801    local sharedmetadata = setmetatableindex(function(t,fontmeta)
802        local v = fontmeta or false
803        if fontmeta then
804            v = pdfreference(pdfflushstreamobject(fontmeta))
805        end
806        t[fontmeta] = v
807        return v
808    end)
809
810    mainwriters["truetype"] = function(details)
811        --
812        local fontfile         = openfontfile(details)
813        local basefontname     = details.basefontname
814        local streams          = details.streams
815        local blobs            = streams.streams
816        local fontheader       = streams.fontheader
817        local horizontalheader = streams.horizontalheader
818        local verticalheader   = streams.verticalheader
819        local maximumprofile   = streams.maximumprofile
820        local names            = streams.names
821        local descriptions     = details.rawdata.descriptions
822        local metadata         = details.rawdata.metadata
823        local indices          = details.indices
824        local metabbox         = { fontheader.xmin, fontheader.ymin, fontheader.xmax, fontheader.ymax }
825        local indices,
826              include,
827              minindex,
828              maxindex         = collectindices(descriptions,indices,details.used,details.hash)
829        local glyphstreams     = tablecreators.glyf(fontfile)
830        local locations        = tablecreators.loca(fontfile)
831        local horizontals      = tablecreators.hmtx(fontfile)
832        local verticals        = tablecreators.vmtx(fontfile)
833        --
834        local zero2            = tocardinal2(0)
835        local zero4            = tocardinal4(0)
836        --
837        local horizontal       = horizontalheader.nofmetrics > 0
838        local vertical         = verticalheader.nofmetrics > 0
839        --
840        local streamoffset     = 0
841        local lastoffset       = zero4
842        local g, h, v          = 0, 0, 0
843        --
844        -- todo: locate notdef
845        --
846        if minindex > 0 then
847            local blob = blobs[0]
848            if blob and #blob > 0 then
849                locations[0] = lastoffset
850                g = g + 1 ; glyphstreams[g] = blob
851                h = h + 1 ; horizontals [h] = zero4
852                if vertical then
853                    v = v + 1 ; verticals[v] = zero4
854                end
855                streamoffset = streamoffset + #blob
856                lastoffset = tocardinal4(streamoffset)
857            else
858                report_fonts("missing .notdef in font %a",basefontname)
859            end
860            -- todo: use a rep for h/v
861            for index=1,minindex-1 do -- no needed in new low range
862                locations[index] = lastoffset
863                h = h + 1 ; horizontals[h] = zero4
864                if vertical then
865                    v = v + 1 ; verticals[v] = zero4
866                end
867            end
868        end
869
870        for index=minindex,maxindex do
871            locations[index] = lastoffset
872            local data = indices[index]
873            if data then
874                local blob = blobs[include[index]] -- we assume padding
875                if blob and #blob > 0 then
876                    g = g + 1 ; glyphstreams[g] = blob
877                    h = h + 1 ; horizontals [h] = tocardinal2(round(data.width or 0))
878                    h = h + 1 ; horizontals [h] = tocardinal2(round(data.boundingbox[1]))
879                    if vertical then
880                        v = v + 1 ; verticals[v] = tocardinal2(round(data.height or 0)) -- how about depth
881                        v = v + 1 ; verticals[v] = tocardinal2(round(data.boundingbox[3]))
882                    end
883                    streamoffset = streamoffset + #blob
884                    lastoffset   = tocardinal4(streamoffset)
885                else
886                    h = h + 1 ; horizontals[h] = zero4
887                    if vertical then
888                        v = v + 1 ; verticals[v] = zero4
889                    end
890                    report_fonts("missing blob for index %i in font %a",index,basefontname)
891                end
892            else
893                h = h + 1 ; horizontals[h] = zero4
894                if vertical then
895                    v = v + 1 ; verticals[v] = zero4
896                end
897            end
898        end
899
900        locations[maxindex+1] = lastoffset -- cf spec
901        --
902        local nofglyphs             = maxindex + 1 -- include zero
903        --
904        fontheader.checksum         = 0
905        fontheader.indextolocformat = 1
906        maximumprofile.nofglyphs    = nofglyphs
907        --
908        fontfile.format             = "tff"
909        fontfile.basefontname       = basefontname
910        fontfile.nofglyphs          = nofglyphs
911        --
912        tablewriters.head(fontfile)
913        tablewriters.hhea(fontfile)
914        if vertical then
915            tablewriters.vhea(fontfile)
916        end
917        tablewriters.maxp(fontfile)
918
919        tablewriters.loca(fontfile)
920        tablewriters.glyf(fontfile)
921
922        tablewriters.hmtx(fontfile)
923        if vertical then
924            tablewriters.vmtx(fontfile)
925        end
926        --
927        local fontdata = makefontfile(fontfile)
928        local fontmeta = makemetadata(fontfile)
929        --
930        fontfile = closefontfile(fontfile)
931        --
932        local units       = metadata.units
933        local basefont    = pdfconstant(basefontname)
934        local widths      = widtharray(details,indices,maxindex,units,1)
935        local object      = details.objectnumber
936        local tounicode   = tounicodedictionary(details,indices,maxindex,basefontname,true)
937        local tocidset    = tocidsetdictionary(indices,minindex,maxindex)
938        local metabbox    = metadata.boundingbox or { 0, 0, 0, 0 }
939        local fontbbox    = pdfarray { unpack(metabbox) }
940        local ascender    = metadata.ascender
941        local descender   = metadata.descender
942--         local averagewidth= metadata.averagewidth
943        local capheight   = metadata.capheight or fontbbox[4]
944        local stemv       = metadata.weightclass
945        local italicangle = metadata.italicangle
946        local xheight     = metadata.xheight or fontbbox[4]
947        --
948        if stemv then
949            stemv = (stemv/65)^2 + 50
950        end
951        --
952        local function scale(n)
953            if n then
954                return round((n) * 10000 / units) / 10
955            else
956                return 0
957            end
958        end
959        --
960        local registry = lmtxregistry(details,include,blobs,minindex,maxindex)
961        local reserved = pdfreserveobject()
962        local child = pdfdictionary {
963            Type           = pdfconstant("Font"),
964            Subtype        = pdfconstant("CIDFontType2"),
965            BaseFont       = basefont,
966            FontDescriptor = pdfreference(reserved),
967            W              = pdfreference(pdfflushobject(widths)),
968            LMTXRegistry   = registry or nil,
969            CIDToGIDMap    = pdfconstant("Identity"),
970            CIDSystemInfo  = pdfdictionary {
971                Registry   = pdfstring("Adobe"),
972                Ordering   = pdfstring("Identity"),
973                Supplement = 0,
974            }
975        }
976        local descendants = pdfarray {
977            pdfreference(pdfflushobject(child)),
978        }
979        local descriptor = pdfdictionary {
980            Type        = pdfconstant("FontDescriptor"),
981            FontName    = basefont,
982            Flags       = 4,
983            FontBBox    = fontbbox,
984-- FontMatrix = pdfarray { 0.001, 0, 0, 0.001, 0, 0 },
985            Ascent      = scale(ascender),
986            Descent     = scale(descender),
987-- AvgWidth = scale(averagewidth),
988            ItalicAngle = round(italicangle or 0),
989            CapHeight   = scale(capheight),
990            StemV       = scale(stemv),
991            XHeight     = scale(xheight),
992            FontFile2   = pdfreference(pdfflushstreamobject(fontdata)),
993            CIDSet      = tocidset, -- no longer needed but verifyers want it
994            Metadata    = sharedmetadata[fontmeta] or nil,
995        }
996        local parent = pdfdictionary {
997            Type            = pdfconstant("Font"),
998            Subtype         = pdfconstant("Type0"),
999            Encoding        = pdfconstant(details.properties.writingmode == "vertical" and "Identity-V" or "Identity-H"),
1000            BaseFont        = basefont,
1001            DescendantFonts = descendants,
1002            ToUnicode       = pdfreference(pdfflushstreamobject(tounicode)),
1003        }
1004        pdfflushobject(reserved,descriptor)
1005        pdfflushobject(object,parent)
1006        --
1007     -- if trace_details then
1008     --     local name = "temp.ttf"
1009     --     report_fonts("saving %a",name)
1010     --     io.savedata(name,fontdata)
1011     --     inspect(fonts.handlers.otf.readers.loadfont(name))
1012     -- end
1013        --
1014    end
1015
1016    do
1017        -- todo : cff2
1018
1019        local details    = {
1020            details        = true,
1021            platformnames  = true,
1022            platformextras = true,
1023        }
1024
1025        tablecreators.cff = function(fontfile)
1026            fontfile.charstrings  = { }
1027            fontfile.charmappings = { }
1028            fontfile.cffstrings   = { }
1029            fontfile.cffhash      = { }
1030            return fontfile.charstrings , fontfile.charmappings
1031        end
1032
1033        local todictnumber, todictreal, todictinteger, todictoffset  do
1034
1035            local maxnum  =   0x7FFFFFFF
1036            local minnum  = - 0x7FFFFFFF - 1
1037            local epsilon = 1.0e-5
1038
1039            local int2tag = "\28"
1040            local int4tag = "\29"
1041            local realtag = "\30"
1042
1043            todictinteger = function(n)
1044                if not n then
1045                    return char(139 & 0xFF)
1046                elseif n >= -107 and n <= 107 then
1047                    return char((n + 139) & 0xFF)
1048                elseif n >= 108 and n <= 1131 then
1049                    n = 0xF700 + n - 108
1050                    return char((n >> 8) & 0xFF, n & 0xFF)
1051                elseif n >= -1131 and n <= -108 then
1052                    n = 0xFB00 - n - 108
1053                    return char((n >> 8) & 0xFF, n & 0xFF)
1054                elseif n >= -32768 and n <= 32767 then
1055                 -- return char(28,extract8(n,8),extract8(n,0))
1056                    return char(28,(n>>8)&0xFF,(n>>0)&0xFF)
1057                else
1058                 -- return char(29,extract8(n,24&0xFF,extract8(n,16),extract8(n,8),extract8(n,0))
1059                    return char(29,(n>>24)&0xFF,(n>>16)&0xFF,(n>>8)&0xFF,(n>>0)&0xFF)
1060                end
1061            end
1062
1063         -- -- not called that often
1064         --
1065         -- local encoder = readers.cffencoder
1066         --
1067         -- todictinteger = function(n)
1068         --     if not n then
1069         --         return encoder[0]
1070         --     elseif n >= -1131 and n <= 1131 then
1071         --         return encoder[n]
1072         --     elseif n >= -32768 and n <= 32767 then
1073         --      -- return char(28,extract8(n,8),extract8(n,0))
1074         --         return char(28,(n>>8)&0xFF,(n>>0)&0xFF)
1075         --     else
1076         --      -- return char(29,extract8(n,24),extract8(n,16),extract8(n,8),extract8(n,0))
1077         --         return char(29,(n>>24)&0xFF,(n>>16)&0xFF,(n>>8)&0xFF,(n>>0)&0xFF)
1078         --     end
1079         -- end
1080
1081            todictoffset = function(n)
1082                return int4tag .. tointeger4(n)
1083            end
1084
1085            local z  = byte("0")
1086            local dp = 10
1087            local ep = 11
1088            local em = 12
1089            local mn = 14
1090            local es = 15
1091
1092            local fg = formatters["%g"]
1093
1094            todictreal = function(v)
1095                local s = fg(v)
1096                local t = { [0] = realtag }
1097                local n = 0
1098                local e = false
1099                for s in gmatch(s,".") do
1100                    if s == "e" or s == "E" then
1101                        e = true
1102                    elseif s == "+" then
1103                        -- skip
1104                    elseif s == "-" then
1105                        n = n + 1
1106                        if e then
1107                            t[n] = em
1108                            e = false
1109                        else
1110                            t[n] = mn
1111                        end
1112                    else
1113                        if e then
1114                            n = n + 1
1115                            t[n] = ep
1116                            e = false
1117                        end
1118                        n = n + 1
1119                        if s == "." then
1120                            t[n] = dp
1121                        else
1122                            t[n] = byte(s) - z
1123                        end
1124                    end
1125                end
1126                n = n + 1
1127                t[n] = es
1128                if (n % 2) ~= 0 then
1129                    n = n + 1
1130                    t[n] = es
1131                end
1132                local j = 0
1133                for i=1,n,2 do
1134                    j = j + 1
1135                    t[j] = char(t[i]*0x10+t[i+1])
1136                end
1137                t = concat(t,"",0,j)
1138                return t
1139            end
1140
1141         -- -- An alternative
1142         --
1143         -- local fg = formatters["%0.14gE%i"]
1144         --
1145         -- todictreal = function(v)
1146         --     local E = 0
1147         --     if v >= 10.0 then
1148         --         while v >= 10.0 do
1149         --             v = v / 10.0
1150         --             E = E + 1
1151         --         end
1152         --     elseif v < 1.0 then
1153         --         while v < 1.0 do
1154         --             v = v * 10.0
1155         --             E = E - 1
1156         --         end
1157         --     end
1158         --     local s = fg(v,E)
1159         --     local t = { [0] = realtag }
1160         --     local n = 0
1161         --     local e = false
1162         --     for s in gmatch(s,".") do
1163         --         if s == "e" or s == "E" then
1164         --             e = true
1165         --         elseif s == "+" then
1166         --             -- skip
1167         --         elseif s == "-" then
1168         --             n = n + 1
1169         --             if e then
1170         --                 t[n] = em
1171         --                 e = false
1172         --             else
1173         --                 t[n] = mn
1174         --             end
1175         --         else
1176         --             if e then
1177         --                 n = n + 1
1178         --                 t[n] = ep
1179         --                 e = false
1180         --             end
1181         --             n = n + 1
1182         --             if s == "." then
1183         --                 t[n] = dp
1184         --             else
1185         --                 t[n] = byte(s) - z
1186         --             end
1187         --         end
1188         --     end
1189         --     n = n + 1
1190         --     t[n] = es
1191         --     if (n % 2) ~= 0 then
1192         --         n = n + 1
1193         --         t[n] = es
1194         --     end
1195         --     local j = 0
1196         --     for i=1,n,2 do
1197         --         j = j + 1
1198         --         t[j] = char(t[i]*0x10+t[i+1])
1199         --     end
1200         --     -- print(v,s)
1201         --     -- for i=0,j do
1202         --     --     print(string.format("%02X",utf.byte(t[i])))
1203         --     -- end
1204         --     t = concat(t,"",0,j)
1205         --     return t
1206         -- end
1207
1208            todictnumber = function(n)
1209                if not n or n == 0 then
1210                    return todictinteger(0)
1211                elseif (n > maxnum or n < minnum or (abs(n - round(n)) > epsilon)) then
1212                    return todictreal(n)
1213                else
1214                    return todictinteger(n)
1215                end
1216            end
1217
1218        end
1219
1220        local todictkey = char
1221
1222        local function todictstring(fontfile,value)
1223            if not value then
1224                value = ""
1225            end
1226            local s = fontfile.cffstrings
1227            local h = fontfile.cffhash
1228            local n = h[value]
1229            if not n then
1230                n = #s + 1
1231                s[n] = value
1232                h[value] = n
1233            end
1234            return todictinteger(390+n)
1235        end
1236
1237        local function todictboolean(b)
1238            return todictinteger(b and 1 or 0)
1239        end
1240
1241        local function todictdeltas(t)
1242            local r = { }
1243            for i=1,#t do
1244--                r[i] = todictnumber(t[i]-(t[i-1] or 0))
1245                r[i] = todictnumber(t[i]+(t[i-1] or 0))
1246            end
1247            return concat(r)
1248        end
1249
1250        local function todictarray(t)
1251            local r = { }
1252            for i=1,#t do
1253                r[i] = todictnumber(t[i])
1254            end
1255            return concat(r)
1256        end
1257
1258        local function writestring(target,source,offset,what)
1259            target[#target+1] = source
1260         -- report_fonts("string : %-11s %06i # %05i",what,offset,#source)
1261            return offset + #source
1262        end
1263
1264        local function writetable(target,source,offset,what)
1265            source = concat(source)
1266            target[#target+1] = source
1267         -- report_fonts("table  : %-11s %06i # %05i",what,offset,#source)
1268            return offset + #source
1269        end
1270
1271        local function writeindex(target,source,offset,what)
1272            local n = #source
1273            local t = #target
1274            t = t + 1 ; target[t] = tocardinal2(n)
1275            if n > 0 then
1276                local data = concat(source)
1277                local size = #data -- assume the worst
1278                local offsetsize, tocardinal
1279                if size < 0xFF then
1280                    offsetsize, tocardinal = 1, tocardinal1
1281                elseif size < 0xFFFF then
1282                    offsetsize, tocardinal = 2, tocardinal2
1283                elseif size < 0xFFFFFF then
1284                    offsetsize, tocardinal = 3, tocardinal3
1285                elseif size < 0xFFFFFFFF then
1286                    offsetsize, tocardinal = 4, tocardinal4
1287                end
1288             -- report_fonts("index  : %-11s %06i # %05i (%i entries with offset size %i)",what,offset,#data,n,offsetsize)
1289                offset = offset + 2 + 1 + (n + 1) * offsetsize + size
1290                -- bytes per offset
1291                t = t + 1 ; target[t] = tocardinal1(offsetsize)
1292                -- list of offsets (one larger for length calculation)
1293                local offset = 1 -- mandate
1294                t = t + 1 ; target[t] = tocardinal(offset)
1295                for i=1,n do
1296                    offset = offset + #source[i]
1297                    t = t + 1 ; target[t] = tocardinal(offset)
1298                end
1299                t = t + 1 ; target[t] = data
1300            else
1301             -- report_fonts("index  : %-11s %06i # %05i (no entries)",what,offset,0)
1302                offset = offset + 2
1303            end
1304         -- print("offset",offset,#concat(target))
1305            return offset
1306        end
1307
1308        tablewriters.cff = function(fontfile)
1309            --
1310            local streams            = fontfile.streams
1311            local cffinfo            = streams.cffinfo or { }
1312            local names              = streams.names or { }
1313            local fontheader         = streams.fontheader or { }
1314            local basefontname       = fontfile.basefontname
1315            --
1316            local offset             = 0
1317            local dictof             = 0
1318            local target             = { }
1319            --
1320            local charstrings        = fontfile.charstrings
1321            local nofglyphs          = #charstrings + 1
1322         -- local fontmatrix         = { 0.001, 0, 0, 0.001, 0, 0 } -- todo, best not
1323            local fontbbox           = fontfile.fontbbox
1324            local defaultwidth       = cffinfo.defaultwidth or 0
1325            local nominalwidth       = cffinfo.nominalwidth or 0
1326            local bluevalues         = cffinfo.bluevalues
1327            local otherblues         = cffinfo.otherblues
1328            local familyblues        = cffinfo.familyblues
1329            local familyotherblues   = cffinfo.familyotherblues
1330            local bluescale          = cffinfo.bluescale
1331            local blueshift          = cffinfo.blueshift
1332            local bluefuzz           = cffinfo.bluefuzz
1333            local stdhw              = cffinfo.stdhw
1334            local stdvw              = cffinfo.stdvw
1335            local stemsnaph          = cffinfo.stemsnaph
1336            local stemsnapv          = cffinfo.stemsnapv
1337            --
1338            if defaultwidth == 0 then defaultwidth     = nil end
1339            if nomimalwidth == 0 then nominalwidth     = nil end
1340            if bluevalues        then bluevalues       = todictarray(bluevalues) end
1341            if otherblues        then otherblues       = todictarray(otherblues) end
1342            if familyblues       then familyblues      = todictarray(familyblues) end
1343            if familyotherblues  then familyotherblues = todictarray(familyotherblues) end
1344            if bluescale         then bluescale        = todictnumber(bluescale) end
1345            if blueshift         then blueshift        = todictnumber(blueshift) end
1346            if bluefuzz          then bluefuzz         = todictnumber(bluefuzz) end
1347            if stemsnaph         then stemsnaph        = todictarray(stemsnaph) end
1348            if stemsnapv         then stemsnapv        = todictarray(stemsnapv) end
1349            if stdhw             then stdhw            = todictnumber(stdhw) end
1350            if stdvw             then stdvw            = todictnumber(stdvw) end
1351            --
1352            local fontversion        = todictstring(fontfile,fontheader.fontversion or "uknown version")
1353            local familyname         = todictstring(fontfile,cffinfo.familyname or names.family or basefontname)
1354            local fullname           = todictstring(fontfile,cffinfo.fullname or basefontname)
1355            local weight             = todictstring(fontfile,cffinfo.weight or "Normal")
1356            local fontbbox           = todictarray(fontbbox)
1357            local strokewidth        = todictnumber(cffinfo.strokewidth)
1358            local monospaced         = todictboolean(cffinfo.monospaced)
1359            local italicangle        = todictnumber(cffinfo.italicangle)
1360            local underlineposition  = todictnumber(cffinfo.underlineposition)
1361            local underlinethickness = todictnumber(cffinfo.underlinethickness)
1362            local charstringtype     = todictnumber(2)
1363         -- local fontmatrix         = todictarray(fontmatrix)
1364            local ros                = todictstring(fontfile,"Adobe")    -- registry
1365                                    .. todictstring(fontfile,"Identity") -- identity
1366                                    .. todictnumber(0)                   -- supplement
1367            local cidcount           = todictnumber(fontfile.nofglyphs)
1368            local fontname           = todictstring(fontfile,basefontname)
1369            local fdarrayoffset      = todictoffset(0)
1370            local fdselectoffset     = todictoffset(0)
1371            local charstringoffset   = todictoffset(0)
1372            local charsetoffset      = todictoffset(0)
1373            local privateoffset      = todictoffset(0)
1374            --
1375            local defaultwidthx      = todictnumber(defaultwidth)
1376            local nominalwidthx      = todictnumber(nominalwidth)
1377            -- the order of the blues is important!
1378            local private            = ""
1379                                    .. (bluevalues       and (bluevalues       .. todictkey( 6))    or "")
1380                                    .. (otherblues       and (otherblues       .. todictkey( 7))    or "")
1381                                    .. (familyblues      and (familyblues      .. todictkey( 8))    or "")
1382                                    .. (familyotherblues and (familyotherblues .. todictkey( 9))    or "")
1383                                    .. (bluescale        and (bluescale        .. todictkey(12, 9)) or "")
1384                                    .. (blueshift        and (blueshift        .. todictkey(12,10)) or "")
1385                                    .. (bluefuzz         and (bluefuzz         .. todictkey(12,11)) or "")
1386                                    .. (stdhw            and (stdhw            .. todictkey(10))    or "")
1387                                    .. (stdvw            and (stdvw            .. todictkey(11))    or "")
1388                                    .. (stemsnaph        and (stemsnaph        .. todictkey(12,12)) or "")
1389                                    .. (stemsnapv        and (stemsnapv        .. todictkey(12,13)) or "")
1390                                    .. (defaultwidthx    and (defaultwidthx    .. todictkey(20))    or "")
1391                                    .. (nominalwidthx    and (nominalwidthx    .. todictkey(21))    or "")
1392            local privatesize        = todictnumber(#private)
1393            local privatespec        = privatesize .. privateoffset
1394            --
1395            -- header (fixed @ 1)
1396            --
1397            local header =
1398                tocardinal1(1) -- major
1399             .. tocardinal1(0) -- minor
1400             .. tocardinal1(4) -- header size
1401             .. tocardinal1(4) -- offset size
1402            --
1403            offset = writestring(target,header,offset,"header")
1404            --
1405            -- name index (fixed @ 2) (has to be sorted)
1406            --
1407            local names = {
1408                basefontname,
1409            }
1410            --
1411            offset = writeindex(target,names,offset,"names")
1412            --
1413            -- topdict index (fixed @ 3)
1414            --
1415            local topvars =
1416                charstringoffset .. todictkey(17)
1417             .. charsetoffset    .. todictkey(15)
1418             .. fdarrayoffset    .. todictkey(12,36)
1419             .. fdselectoffset   .. todictkey(12,37)
1420             .. privatespec      .. todictkey(18)
1421            --
1422            local topdict = {
1423                ros                   .. todictkey(12,30) -- first
1424             .. cidcount              .. todictkey(12,34)
1425             .. familyname            .. todictkey( 3)
1426             .. fullname              .. todictkey( 2)
1427             .. weight                .. todictkey( 4)
1428             .. fontbbox              .. todictkey( 5)
1429             .. monospaced            .. todictkey(12, 1)
1430             .. italicangle           .. todictkey(12, 2)
1431             .. underlineposition     .. todictkey(12, 3)
1432             .. underlinethickness    .. todictkey(12, 4)
1433             .. charstringtype        .. todictkey(12, 6)
1434          -- .. fontmatrix            .. todictkey(12, 7)
1435             .. strokewidth           .. todictkey(12, 8)
1436             .. topvars
1437            }
1438            --
1439            offset = writeindex(target,topdict,offset,"topdict")
1440            dictof = #target
1441            --
1442            -- string index (fixed @ 4)
1443            --
1444            offset = writeindex(target,fontfile.cffstrings,offset,"strings")
1445            --
1446            -- global subroutine index (fixed @ 5)
1447            --
1448            offset = writeindex(target,{},offset,"globals")
1449            --
1450            -- Encoding (cff1)
1451            --
1452            -- offset = writeindex(target,{},offset,"encoding")
1453            --
1454            -- Charsets
1455            --
1456            charsetoffset = todictoffset(offset)
1457            offset        = writetable(target,fontfile.charmappings,offset,"charsets")
1458            --
1459            -- fdselect
1460            --
1461            -- see printer mail thread / experiments with Leah Neukirchen: some printers
1462            -- (probaby with an old GS on board) need this matrix because oitherwise they
1463            -- accumulate the top one (resulting in very tiny invisible results)
1464            --
1465            local fdselect =
1466                tocardinal1(3) -- format
1467             .. tocardinal2(1) -- n of ranges
1468             -- entry 1
1469             .. tocardinal2(0) -- first gid
1470             .. tocardinal1(0) -- fd index
1471             -- entry 2
1472          -- .. tocardinal2(fontfile.sparsemax-1) -- sentinel
1473             .. tocardinal2(fontfile.sparsemax) -- sentinel
1474            --
1475            fdselectoffset = todictoffset(offset)
1476            offset         = writestring(target,fdselect,offset,"fdselect")
1477            --
1478            -- charstrings
1479            --
1480            charstringoffset = todictoffset(offset)
1481            offset           = writeindex(target,charstrings,offset,"charstrings")
1482            --
1483            -- font dict
1484            --
1485            -- offset = writeindex(target,{},offset,"fontdict")
1486            --
1487            -- private
1488            --
1489            privateoffset = todictoffset(offset)
1490            privatespec   = privatesize .. privateoffset
1491            offset        = writestring(target,private,offset,"private")
1492            --
1493            local fdarray = {
1494                fontname       .. todictkey(12,38)
1495             .. privatespec    .. todictkey(18) -- case 1
1496            }
1497            fdarrayoffset = todictoffset(offset)
1498            offset        = writeindex(target,fdarray,offset,"fdarray")
1499            --
1500            topdict = target[dictof]
1501            topdict = sub(topdict,1,#topdict-#topvars)
1502            topvars =
1503                charstringoffset .. todictkey(17)
1504             .. charsetoffset    .. todictkey(15)
1505             .. fdarrayoffset    .. todictkey(12,36)
1506             .. fdselectoffset   .. todictkey(12,37)
1507             .. privatespec      .. todictkey(18) -- case 2
1508            target[dictof] = topdict .. topvars
1509            --
1510            target = concat(target)
1511         -- if trace_details then
1512         --   local name = "temp.cff"
1513         --   report_fonts("saving %a",name)
1514         --   io.savedata(name,target)
1515         --   inspect(fonts.handlers.otf.readers.cffcheck(name))
1516         -- end
1517            return target
1518        end
1519    end
1520
1521    -- todo: check widths (missing a decimal)
1522
1523    mainwriters["opentype"] = function(details)
1524        --
1525        local fontfile       = openfontfile(details)
1526        local basefontname   = details.basefontname
1527        local streams        = details.streams
1528        local blobs          = streams.streams
1529        local fontheader     = streams.fontheader
1530        local maximumprofile = streams.maximumprofile
1531        local names          = streams.names -- not used
1532        local descriptions   = details.rawdata.descriptions
1533        local metadata       = details.rawdata.metadata
1534        local indices        = details.indices
1535        local used           = details.used
1536        local usedfonts      = details.usedfonts -- in case of multiple loaded t1 fonts with no common description
1537        local metabbox       = { fontheader.xmin, fontheader.ymin, fontheader.xmax, fontheader.ymax }
1538        local correction     = 1
1539        if not descriptions or not next(descriptions) then
1540            -- (*) We share code with type1 and when we have old school tfm with pfb shapes
1541            -- we don't have descriptions, so we need to construct these. This could be done
1542            -- earlier but then we lack info about sharing. Horrible hackery. If Type1 wasn't
1543            -- obsolete I'd make a dedicated mainwriter that does the index and width collect
1544            -- more efficient but there is no gain now.
1545            if true then
1546                descriptions = { }
1547                setmetatable(indices,nil)
1548                setmetatable(used,nil)
1549                for u in next, usedfonts do
1550                    local param = fonts.hashes.parameters[u]
1551                    local chars = fonts.hashes.characters[u]
1552                    local units = 1000 -- to be checked (picked up)
1553                    correction = param.size / 1000
1554                 -- correction = correction * bpfactor / ptfactor
1555                    local factor = 1000 / (units * correction)
1556                    if false then
1557                        for k, v in sortedhash(chars) do
1558                            if descriptions[k] then
1559                                local w1 = descriptions[k].width
1560                                local w2 = round((v.advance or v.width or 0) * factor)
1561                                if w1 ~= w2 then
1562                                    local w = v.advance or v.width or 0
1563                                 -- print(
1564                                 --     u,k,utf.char(k),
1565                                 --     w1,w2,
1566                                 --     ((v.advance or v.width or 0)*param.designsize/param.size) / 1000
1567                                 -- )
1568                                end
1569                            else
1570                                descriptions[k] = {
1571                                    index   = v.index,
1572                                    width   = round((v.advance or v.width or 0) * factor),
1573                                    unicode = v.unicode,
1574                                }
1575                            end
1576                        end
1577                    else
1578                        for k, v in next, chars do
1579                            if descriptions[k] then
1580                                -- done
1581                            else
1582                                local index = v.index
1583                                if indices[index] or used[index] then -- play safe
1584                                    descriptions[k] = {
1585                                        index   = index,
1586                                        width   = round((v.advance or v.width or 0) * factor),
1587                                        unicode = v.unicode,
1588                                    }
1589                                end
1590                            end
1591                        end
1592                    end
1593                end
1594                correction = false
1595            else
1596                -- This is tricky as it can be the wrong one and incomplete so a first come
1597                -- and go issue. The basepoint correction needs checking.
1598                descriptions = details.fontdata.characters
1599                correction   = details.fontdata.parameters.size / 1000
1600                correction   = correction * bpfactor / ptfactor
1601            end
1602            metadata = { }
1603        end
1604        --
1605        local indices,
1606              include,
1607              minindex,
1608              maxindex       = collectindices(descriptions,indices,used,details.hash)
1609        local streamoffset   = 0
1610        local glyphstreams,
1611              charmappings   = tablecreators.cff(fontfile)
1612        --
1613        local zero2          = tocardinal2(0)
1614        local zero4          = tocardinal4(0)
1615        --
1616        -- we need to locate notdef (or store its unicode someplace)
1617        --
1618        local blob              = blobs[0] or "\14"
1619        local sparsemax         = 1
1620        local lastoffset        = zero4
1621        glyphstreams[sparsemax] = blob
1622        charmappings[sparsemax] = tocardinal1(0) -- format 0
1623        streamoffset            = streamoffset + #blob
1624        lastoffset              = tocardinal4(streamoffset)
1625        if minindex == 0 then
1626            minindex = 1
1627        end
1628        --
1629        for index=minindex,maxindex do
1630            local idx = include[index]
1631            if idx then
1632                local blob = blobs[idx]
1633                if not blob then
1634                    blob = "\14"
1635                end
1636                sparsemax               = sparsemax + 1
1637                glyphstreams[sparsemax] = blob
1638                charmappings[sparsemax] = tocardinal2(index)
1639                streamoffset            = streamoffset + #blob
1640                lastoffset              = tocardinal4(streamoffset)
1641            end
1642        end
1643        --
1644        fontfile.nofglyphs    = maxindex + 1
1645        fontfile.sparsemax    = sparsemax
1646        fontfile.format       = "cff"
1647        fontfile.basefontname = basefontname
1648        fontfile.fontbbox     = metabbox
1649        --
1650        local fontdata = tablewriters.cff(fontfile)
1651        local fontmeta = makemetadata(fontfile)
1652        --
1653        fontfile = closefontfile(fontfile)
1654        --
1655        local units = fontheader.units or metadata.units
1656        if not units or units ~= 1000 then
1657            -- maybe only otf
1658            -- public sans has 2000 so we need to mess different from e.g. ttf
1659            report_fonts("width units in %a are %i, forcing 1000 instead",basefontname,units)
1660            units = 1000
1661        end
1662        --
1663        local basefont    = pdfconstant(basefontname)
1664        local widths      = widtharray(details,indices,maxindex,units,correction)
1665        local object      = details.objectnumber
1666        local tounicode   = tounicodedictionary(details,indices,maxindex,basefontname,true)
1667        local tocidset    = tocidsetdictionary(indices,minindex,maxindex)
1668        local fontbbox    = pdfarray { unpack(metabbox) }
1669        local ascender    = metadata.ascender or 0
1670        local descender   = metadata.descender or 0
1671     -- local averagewidth= metadata.averagewidth
1672        local capheight   = metadata.capheight or fontbbox[4]
1673        local stemv       = metadata.weightclass
1674        local italicangle = metadata.italicangle
1675        local xheight     = metadata.xheight or fontbbox[4]
1676        if stemv then
1677            stemv = (stemv/65)^2 + 50
1678        else
1679         -- stemv = 2
1680        end
1681        --
1682        local function scale(n)
1683            if n then
1684                return round((n) * 10000 / units) / 10
1685            else
1686                return 0
1687            end
1688        end
1689        --
1690        local registry = lmtxregistry(details,include,blobs,minindex,maxindex)
1691        local reserved = pdfreserveobject()
1692        local child = pdfdictionary {
1693            Type           = pdfconstant("Font"),
1694            Subtype        = pdfconstant("CIDFontType0"),
1695            BaseFont       = basefont,
1696            FontDescriptor = pdfreference(reserved),
1697            W              = pdfreference(pdfflushobject(widths)),
1698            LMTXRegistry   = registry or nil,
1699            CIDSystemInfo  = pdfdictionary {
1700                Registry   = pdfstring("Adobe"),
1701                Ordering   = pdfstring("Identity"),
1702                Supplement = 0,
1703            }
1704        }
1705        local descendants = pdfarray {
1706            pdfreference(pdfflushobject(child)),
1707        }
1708        local fontstream = pdfdictionary {
1709            Subtype = pdfconstant("CIDFontType0C"),
1710        }
1711        local descriptor = pdfdictionary {
1712            Type        = pdfconstant("FontDescriptor"),
1713            FontName    = basefont,
1714            Flags       = 4,
1715            FontBBox    = fontbbox,
1716         -- FontMatrix  = pdfarray { 0.001, 0, 0, 0.001, 0, 0 },
1717            Ascent      = scale(ascender),
1718            Descent     = scale(descender),
1719         -- AvgWidth    = scale(averagewidth),
1720            ItalicAngle = round(italicangle or 0),
1721            CapHeight   = scale(capheight),
1722            StemV       = scale(stemv),
1723            XHeight     = scale(xheight),
1724            CIDSet      = tocidset,
1725            FontFile3   = pdfreference(pdfflushstreamobject(fontdata,fontstream())),
1726            Metadata    = sharedmetadata[fontmeta] or nil,
1727        }
1728        local parent = pdfdictionary {
1729            Type            = pdfconstant("Font"),
1730            Subtype         = pdfconstant("Type0"),
1731            Encoding        = pdfconstant(details.properties.writingmode == "vertical" and "Identity-V" or "Identity-H"),
1732            BaseFont        = basefont,
1733            DescendantFonts = descendants,
1734            ToUnicode       = pdfreference(pdfflushstreamobject(tounicode)),
1735        }
1736        pdfflushobject(reserved,descriptor)
1737        pdfflushobject(object,parent)
1738    end
1739
1740    mainwriters["type1"] = function(details)
1741        -- We abuse the cff includer which is ok but for special cases like
1742        -- tfm -> pfb we don't have the right descriptions and scale so this
1743        -- is why we cheat elsewhere. Maybe I should just drop that kind of
1744        -- support and assume afm files to be present. See (*) above.
1745        local s = details.streams
1746        local m = details.rawdata.metadata
1747        if m then
1748            local h = s.horizontalheader
1749            local c = s.cffinfo
1750            local n = s.names
1751            h.ascender  = m.ascender      or h.ascender
1752            h.descender = m.descender     or h.descender
1753            n.copyright = m.copyright     or n.copyright
1754            n.family    = m.familyname    or n.familyname
1755            n.fullname  = m.fullname      or n.fullname
1756            n.fontname  = m.fontname      or n.fontname
1757            n.subfamily = m.subfamilyname or n.subfamilyname
1758            n.version   = m.version       or n.version
1759            setmetatableindex(h,m)
1760            setmetatableindex(c,m)
1761            setmetatableindex(n,m)
1762        end
1763        mainwriters["opentype"](details)
1764    end
1765
1766    do
1767
1768        local version = 0.003
1769        local cache   = containers.define("fonts","converted",version,true)
1770        local loaded  = { }
1771
1772        local loadpk  = fonts.handlers.tfm.readers.loadpk
1773        local findpk  = resolvers.findpk
1774        local pktocff = fonts.handlers.otf.readers.potracetocff
1775
1776        local function loadstreams(filename,details,resolution,settings)
1777            -- when we have a tfm and pfb but no afm filename can have a pfb suffix
1778                  filename = file.removesuffix(filename) -- unless pk?
1779            local fullname = findpk(filename,resolution)
1780            local instance = fullname and fullname ~= "" and loadpk(fullname)
1781            if not instance then
1782                report_fonts("unable to locate %a",filename)
1783                return
1784            end
1785            local resolution = instance.resolution
1786            local streams    = { }
1787            local glyphs     = instance.glyphs or { }
1788            local settings   = setting or { }
1789            local xmin  = false
1790            local ymin  = false
1791            local xmax  = false
1792            local ymax  = false
1793            local count = 0
1794         -- local size  = 0
1795            for i=0,255 do
1796                local glyph = glyphs[i]
1797                if glyph then
1798                    local llx, lly, urx, ury, cff = pktocff(glyph,settings,resolution,i)
1799                    streams[i] = cff
1800                    if not xmin then
1801                        xmin = llx
1802                        ymin = lly
1803                        xmax = urx
1804                        ymax = ury
1805                    else
1806                        if xmin < llx then llx = xmin end
1807                        if ymin < lly then lly = ymin end
1808                        if xmax < urx then urx = xmax end
1809                        if ymax < ury then ury = ymax end
1810                    end
1811                    count = count + 1
1812                 -- size  = size + #cff
1813                end
1814            end
1815         -- print(filename,size)
1816            local filename = details.filename or "unknown"
1817            return {
1818                resolution = resolution,
1819                format     = "opentype",
1820                filename   = filename,
1821                fullname   = fullname,
1822                streams    = streams or { }, -- bad
1823                names = {
1824                    family   = filename,
1825                    fontname = filename,
1826                    fullname = filename,
1827                    notice   = "Converted from MetaFont bitmaps by ConTeXt LMTX.",
1828                    version  = "1.00",
1829                },
1830                fontheader = {
1831                    fontversion = "2.004",
1832                    units       = 1000,
1833                    xmin        = xmin or 0,
1834                    ymin        = ymin or 0,
1835                    xmax        = xmax or 0,
1836                    ymax        = ymax or 0,
1837                },
1838                cffinfo = {
1839                    familyname         = filename,
1840                    fullname           = filename,
1841                 -- italicangle        = 0,
1842                 -- monospaced         = false,
1843                 -- underlineposition  = -100,
1844                 -- underlinethickness = 50,
1845                 -- weight             = "Medium",
1846                    bluescale          = 0,
1847                    blueshift          = 0,
1848                    bluefuzz           = 0,
1849                    expansionfactor    = 0,
1850                },
1851                horizontalheader = {
1852                    ascender  = 0,
1853                    descender = 0,
1854                },
1855                maximumprofile = {
1856                   nofglyphs = count,
1857                },
1858            }
1859        end
1860
1861        mainwriters["pkcff"] = function(details)
1862            local filename   = details.filename
1863            local properties = details.properties
1864            local resolution = properties.resolution or 7200
1865            local extrahash  = properties.extrahash or resolution
1866            local cachename  = filename .. "-" .. extrahash
1867            local converted = loaded[cachename]
1868            if not converted then
1869                converted = containers.read(cache,cachename)
1870                if not converted or converted.version ~= version or converted.resolution ~= resolution or converted.extrahash ~= extrahash then
1871                    converted = loadstreams(filename,details,resolution,properties.potrace)
1872                    if converted then
1873                        converted.version = version
1874                     -- converted.resolution = resolution
1875                        converted.extrahash  = extrahash
1876                        containers.write(cache,cachename,converted)
1877                        converted = containers.read(cache,cachename) -- frees old mem
1878                        loaded[cachename] = converted
1879                    end
1880                end
1881            end
1882            details.streams = converted
1883            mainwriters["type1"](details)
1884        end
1885
1886    end
1887
1888    do
1889
1890        -- The methods might become plugins.
1891
1892        local methods    = { }
1893
1894        local pdfimage   = lpdf.epdf.image
1895        local openpdf    = pdfimage.open
1896        local closepdf   = pdfimage.close
1897        local copypage   = pdfimage.copy
1898
1899        local embedimage = images.embed
1900
1901        local f_glyph      = formatters["G%d"]
1902        local f_char       = formatters["BT /V%d 1 Tf [<%04X>] TJ ET"]
1903        local f_width      = formatters["%.6N 0 d0"]
1904        local f_index      = formatters["I%d"]
1905        local f_image_xy   = formatters["%.6N 0 d0 1 0 0 1 %.6N %.6N cm /%s Do"]
1906        local f_image_c    = formatters["/%s Do"]
1907        local f_image_c_xy = formatters["%.6N 0 0 %.6N %.6N %.6N cm /%s Do"]
1908        local f_image_w    = formatters["%.6N 0 d0 %s"]
1909        local f_image_d    = formatters["%.6N 0 d0 1 0 0 1 0 %.6N cm /%s Do"]
1910        local f_stream     = formatters["%.6N 0 d0 %s"]
1911        local f_stream_c   = formatters["%.6N 0 0 0 0 0 d1 %s"] -- last four bbox
1912        local f_stream_d   = formatters["%.6N 0 d0 1 0 0 1 0 %.6N cm %s"]
1913     -- local f_stream_s   = formatters["%.6N 0 0 %.6N 0 0 cm /%s Do"]
1914
1915        -- A type 3 font has at most 256 characters and Acrobat also wants a zero slot
1916        -- to be filled. We can share a mandate zero slot character. We also need to
1917        -- make sure that we use bytes as index in the page stream as well as in the
1918        -- tounicode vector.
1919
1920        -- All this type three magic is a tribute to Don Knuth who uses bitmap fonts in
1921        -- his books.
1922
1923        local c_notdef = nil
1924        local r_notdef = nil
1925        local w_notdef = nil
1926        local fontbbox = nil
1927
1928        -- pk inclusion (not really tested but not really used either)
1929
1930        -- Maybe this needs to be in font-shp .. but this is cheaper and it is only for
1931        -- demos anyway.
1932
1933        local version = 0.011
1934        local cache   = containers.define("fonts","traced",version,true)
1935        local loaded  = { }
1936
1937        function methods.pk(filename,details)
1938            local resolution = details.properties.resolution or 7200
1939            local extrahash  = details.properties.extrahash or resolution
1940            local potraced   = details.fontdata.potraced
1941            local pkfullname = resolvers.findpk(filename,resolution)
1942            if not pkfullname or pkfullname == "" then
1943                report_fonts("no pk file found: %a @ %i",filename,resolution)
1944                return
1945            end
1946            local readers = fonts.handlers.tfm.readers
1947            local result  = readers.loadpk(pkfullname) -- not cached (yet)
1948            if not result or result.error then
1949                return
1950            end
1951-- hm, maybe just resolution and fault on a missing file
1952                  resolution  = result.resolution
1953            local widthfactor = resolution / 72
1954            local scalefactor = 72 / resolution / 10
1955            local factor      = widthfactor / 65536
1956                              * (details.parameters.designsize / details.parameters.size) -- normalize width
1957            local pktopdf     = nil
1958            --
1959            if potraced then -- we can also use properties.potrace
1960                local cachename = filename .. "-" .. extrahash
1961                local traced = loaded[cachename]
1962                if not traced then
1963                    traced = containers.read(cache,cachename)
1964                    if not traced or traced.version ~= version or traced.resolution ~= resolution or traced.extrahash ~= extrahash then
1965                        local streams = { }
1966                        local convert = readers.potracedtopdf
1967                        -- we have this as helper already
1968                        local descriptions = details.fontdata.descriptions
1969                        local indices      = { }
1970                        local widths       = { }
1971                        for unicode, data in next, descriptions do
1972                            local di = data.index
1973                            if di then
1974                                indices[di] = data
1975                            end
1976                        end
1977                        --
1978                        local settings = details.properties.potrace
1979                        for index, glyph in sortedhash(result.glyphs) do
1980                            streams[index], widths[index] = convert(glyph,indices[index],factor,settings)
1981                        end
1982                        traced = {
1983                            version    = version,
1984                            resolution = resolution,
1985                            streams    = streams,
1986                            widths     = widths,
1987                            extrahash  = extrahash,
1988                        }
1989                        containers.write(cache,cachename,traced)
1990                        traced = containers.read(cache,cachename) -- frees old mem
1991                        loaded[cachename] = traced
1992                    end
1993                end
1994                local streams = traced.streams
1995                local widths  = traced.widths
1996                pktopdf = function(glyph,data)
1997                    local index = glyph.index
1998                    return streams[index], widths[index]
1999                end
2000            else
2001                local convert = readers.pktopdf
2002                pktopdf = function (glyph,data)
2003                    return convert(glyph,data,factor)  -- return pdfcode, width
2004                end
2005            end
2006            return result.glyphs, scalefactor, pktopdf, false, false
2007        end
2008
2009        -- pdf inclusion
2010
2011        local used = setmetatableindex("table")
2012
2013        function lpdf.registerfontmethod(name,f)
2014            if type(f) == "function" and not methods[name] then
2015                report_fonts("registering font method %a, continue on your own risk",name)
2016                methods[name] = f
2017            end
2018        end
2019
2020        function methods.pdf(filename,details)
2021            local properties = details.properties
2022            local pdfshapes  = properties.indexdata[1]
2023            local pdfdoc     = openpdf(pdfshapes.filename)
2024            local xforms     = pdfdictionary()
2025            local nofglyphs  = 0
2026            if pdfdoc then
2027                local scale    = 10 * details.parameters.size/details.parameters.designsize
2028                local units    = details.parameters.units
2029                local factor   = units * bpfactor / scale
2030                local fixdepth = pdfshapes.fixdepth
2031                local useddoc  = used[pdfdoc]
2032                local function pdftopdf(glyph,data)
2033                    local width     = (data.width or 0) * factor
2034                    local image     = useddoc[glyph]
2035                    local reference = nil
2036                    if not image then
2037                        image        = embedimage(copypage(pdfdoc,glyph))
2038                        nofglyphs    = nofglyphs + 1
2039                        local name   = f_glyph(nofglyphs)
2040                        local stream = nil
2041                        if fixdepth then
2042                            local depth = data.depth  or 0
2043                            if depth ~= 0 then
2044                                local d     = data.dropin.descriptions[data.index]
2045                                local b     = d.boundingbox
2046                                local l     = b[1]
2047                                local r     = b[3]
2048                                local w     = r - l
2049                                local scale = w / d.width
2050                                local x     = l
2051                             -- local y     = - b[4] - b[2] - (d.depth or 0)
2052                                local y     = - (d.depth or 0)
2053                                local scale = w / (image.width * bpfactor)
2054                                stream = f_image_c_xy(scale,scale,x,y,name)
2055                            end
2056                        end
2057                        if not stream then
2058                            stream = f_image_c(name)
2059                        end
2060                        useddoc[glyph]           = image
2061                        image.embedded_name      = name
2062                        image.embedded_stream    = stream
2063                        image.embedded_reference = pdfreference(image.objnum)
2064                    end
2065                    xforms[image.embedded_name] = image.embedded_reference
2066                    return f_image_w(width,image.embedded_stream), width
2067                end
2068                local function closepdf()
2069                 -- closepdf(pdfdoc)
2070                end
2071                local function getresources()
2072                    return pdfdictionary { XObject = xforms }
2073                end
2074                return pdfshapes, 1/units, pdftopdf, closepdf, getresources
2075            end
2076        end
2077
2078        -- box inclusion (todo: we can share identical glyphs if needed but the gain
2079        -- is minimal especially when we use compact font mode)
2080
2081        function methods.box(filename,details)
2082            local properties = details.properties
2083            local boxes      = properties.indexdata[1]
2084            local xforms     = pdfdictionary()
2085            local nofglyphs  = 0
2086            local scale      = 10 * details.parameters.size/details.parameters.designsize
2087                  scale      = scale * (7200/7227) -- test on extensibles
2088            local units      = details.parameters.units
2089            local function boxtopdf(image,data) -- image == glyph
2090                nofglyphs    = nofglyphs + 1
2091                local scale  = units / scale -- e.g. 1000 / 12
2092                local width  = (data.width or 0) * bpfactor * scale
2093                local depth  = - (data.depth or 0) * bpfactor * scale
2094                local name   = f_glyph(nofglyphs)
2095                local stream = f_image_c_xy(scale,scale,0,depth,name)
2096                image.embedded_name      = name
2097                image.embedded_stream    = stream
2098                image.embedded_reference = pdfreference(image.objnum)
2099                xforms[name] = image.embedded_reference
2100                if image.expose then
2101                    -- support color etc
2102                    return stream, width
2103                else
2104                    return f_image_w(width,stream), width
2105                end
2106            end
2107            local function wrapup()
2108            end
2109            local function getresources()
2110                return pdfdictionary { XObject = xforms }
2111            end
2112            return boxes, 1/units, boxtopdf, wrapup, getresources
2113        end
2114
2115        -- mps inclusion
2116
2117        local decompress      = gzip.decompress
2118        local metapost        = metapost
2119        local simplemprun     = metapost.simple
2120        local setparameterset = metapost.setparameterset
2121
2122        function methods.mps(filename,details)
2123            local properties = details.properties
2124            local parameters = details.parameters
2125            local mpshapes   = properties.indexdata[1] -- indexdata will change
2126            if mpshapes then
2127                local scale            = 10 * parameters.size/parameters.designsize
2128                local units            = mpshapes.units or parameters.units
2129                local factor           = units * bpfactor / scale
2130                local fixdepth         = mpshapes.fixdepth
2131                local usecolor         = mpshapes.usecolor
2132                local specification    = mpshapes.specification or { }
2133                local shapedefinitions = mpshapes.shapes
2134                local instance         = mpshapes.instance
2135                --
2136                simplemprun(instance,"begingroup;",true,true)
2137                setparameterset("mpsfont",specification)
2138                specification.scale        = specification.scale or scale
2139                specification.parameters   = parameters
2140                specification.properties   = properties
2141                specification.parentdata   = details.fontdata.parentdata
2142                -------------.characters   = details.fontdata.characters
2143                -------------.descriptions = details.fontdata.descriptions
2144                if shapedefinitions then
2145                    local preamble = shapedefinitions.parameters.preamble
2146                    if preamble then
2147                        simplemprun(instance,preamble,true,true)
2148                    end
2149                end
2150                --
2151                local function mpstopdf(mp,data)
2152                    local width = data.width
2153                    if decompress then
2154                        mp = decompress(mp)
2155                    end
2156                    local pdf   = simplemprun(instance,mp,true)
2157                    local width = width * factor
2158                    if usecolor then
2159                        return f_stream_c(width,pdf), width
2160                    elseif fixdepth then
2161                        local depth  = data.depth  or 0
2162                        local height = data.height or 0
2163                        if depth ~= 0 or height ~= 0 then
2164                            return f_stream_d(width,(-height-depth)*factor,pdf), width
2165                        end
2166                    end
2167                    return f_stream(width,pdf), width
2168                end
2169                --
2170                local function resetmps()
2171                    setparameterset("mpsfont")
2172                    simplemprun(instance,"endgroup;",true,true)
2173                    specification.parameters   = nil
2174                    specification.properties   = nil
2175                    specification.parentdata   = nil
2176                    -------------.characters   = nil
2177                    -------------.descriptions = nil
2178                end
2179                --
2180                local function getresources()
2181                    return lpdf.collectedresources {
2182                        serialize  = false,
2183                    }
2184                end
2185                --
2186                return mpshapes, 1/units, mpstopdf, resetmps, getresources
2187            end
2188        end
2189
2190        -- png inclusion
2191
2192        -- With d1 the image mask is used when given and obeys color. So it is needed for pure bw
2193        -- bitmap fonts, so here we really need d0.
2194        --
2195        -- Acrobat X pro only seems to see the image mask but other viewers are doing it ok. Acrobat
2196        -- reader crashes. We really need to add a notdef!
2197
2198        local files       = utilities.files
2199        local openfile    = files.open
2200        local closefile   = files.close
2201        local setposition = files.setposition
2202        local readstring  = files.readstring
2203
2204        function methods.png(filename,details)
2205            local properties = details.properties
2206            local pngshapes  = properties.indexdata[1]
2207            if pngshapes then
2208                local parameters = details.parameters
2209                local png        = properties.png
2210                local hash       = png.hash
2211                local xforms     = pdfdictionary()
2212                local nofglyphs  = 0
2213                local scale      = 10 * parameters.size/parameters.designsize
2214                local factor     = bpfactor / scale
2215             -- local units      = parameters.units -- / 1000
2216                local units      = 1000
2217                local filehandle = openfile(details.filename,true)
2218                local function pngtopdf(glyph,data)
2219                 -- local info    = graphics.identifiers.png(glyph.data,"string")
2220                    local offset  = glyph.o
2221                    local size    = glyph.s
2222                    local pdfdata = nil
2223                    if offset and size then
2224                        setposition(filehandle,offset)
2225                        local blob = readstring(filehandle,size)
2226                        local info = graphics.identifiers.png(blob,"string")
2227                        info.enforcecmyk = pngshapes.enforcecmyk
2228                        local image   = lpdf.injectors.png(info,"string")
2229                        local width   = (data.width or 0) * factor
2230                        if image then
2231                            embedimage(image)
2232                            nofglyphs     = nofglyphs + 1
2233                            local xoffset = (glyph.x or 0) / units
2234                            local yoffset = (glyph.y or 0) / units
2235                            local name    = f_glyph(nofglyphs)
2236                            xforms[name]  = pdfreference(image.objnum)
2237                            pdfdata       = f_image_xy(width,xoffset,yoffset,name)
2238                        end
2239                    end
2240                    return pdfdata or f_stream(width), width
2241                end
2242                local function closepng()
2243                    if filehandle then
2244                        closefile(filehandle)
2245                    end
2246                    pngshapes = nil
2247                end
2248                local function getresources()
2249                    return pdfdictionary { XObject = xforms }
2250                end
2251                return pngshapes, 1, pngtopdf, closepng, getresources
2252            end
2253        end
2254
2255        local function registercolors(hash)
2256            local kind     = hash.kind
2257            local data     = hash.data
2258            local direct   = lpdf.fonts.color_direct
2259            local indirect = lpdf.fonts.color_indirect
2260            if kind == "font" then
2261                return setmetatableindex(function(t,k)
2262                    local h = data[k]
2263                    local v = h and direct(h[1]/255,h[2]/255,h[3]/255) or false
2264                    t[k] = v
2265                    return v
2266                end)
2267            elseif kind == "user" then
2268                return setmetatableindex(function(t,k)
2269                    local list = data[k]
2270                    local v
2271                    if list then
2272                        local kind = list.kind
2273                        if kind == "values" then
2274                            local d = list.data
2275                            v = direct(d.r or 0,d.g or 0,d.b or 0)
2276                        elseif kind == "attributes" then
2277                            v = indirect(list.color,list.transparency)
2278                        else
2279                            v = false -- textcolor
2280                        end
2281                    else
2282                        v = false -- textcolor
2283                    end
2284                    t[k] = v
2285                    return v
2286                end)
2287            else
2288                return { }
2289            end
2290        end
2291
2292        -- we register way too much ... we can delay the t3 definition
2293
2294        local usedcharacters = lpdf.usedcharacters
2295
2296        function methods.color(filename,details)
2297            local colrshapes = details.properties.indexdata[1]
2298            local colrvalues = details.properties.indexdata[2]
2299            local usedfonts  = { }
2300            local function colrtopdf(description,data)
2301                -- descriptions by index
2302                local colorlist = description.colors
2303                if colorlist then
2304                    local dropdata     = data.dropin
2305                    local dropid       = dropdata.properties.id
2306                    local dropunits    = dropdata.parameters.units -- shared
2307                    local descriptions = dropdata.descriptions
2308                    local directcolors = registercolors(colrvalues)
2309                    local fontslots    = usedcharacters[dropid]
2310                    usedfonts[dropid]  = dropid
2311                    local w = description.width or 0
2312                    local s = #colorlist
2313                    local l = false
2314                    local t = { f_width(w) }
2315                    local n = 1
2316                    local d = #colrvalues
2317                    for i=1,s do
2318                        local entry = colorlist[i]
2319                        local class = entry.class or d
2320                        if class then
2321                            -- false is textcolor (we should actually go back)
2322                            local c = directcolors[class]
2323                            if c and l ~= c then
2324                                n = n + 1 ; t[n] = c
2325                                l = c
2326                            end
2327                        end
2328                        local e = descriptions[entry.slot]
2329                        if e then
2330                            n = n + 1 ; t[n] = f_char(dropid,fontslots[e.index])
2331                        end
2332                    end
2333                    -- we're not going to hash this ... could be done if needed (but who mixes different
2334                    -- color schemes ...)
2335                    t = concat(t," ")
2336                    return t, w / dropunits
2337                end
2338            end
2339            local function getresources()
2340                return lpdf.collectedresources {
2341                    serialize  = false,
2342                    fonts      = usedfonts,
2343                    fontprefix = "V",
2344                }
2345            end
2346            return colrshapes, 1, colrtopdf, false, getresources
2347        end
2348
2349        mainwriters["type3"] = function(details)
2350            local properties   = details.properties
2351            local basefontname = properties.basefontname or properties.name
2352            local askedmethod  = properties.method or "pk"
2353            local method       = methods[askedmethod] or methods.pk
2354            if not method or not basefontname or basefontname == "" then
2355                return
2356            end
2357            local glyphs, scalefactor, glyphtopdf, reset, getresources = method(basefontname,details)
2358            if not glyphs then
2359                return
2360            end
2361            local parameters  = details.parameters
2362            local object      = details.objectnumber
2363            local factor      = parameters.factor -- normally 1
2364            local fontmatrix  = pdfarray { scalefactor, 0, 0, scalefactor, 0, 0 }
2365            local indices,
2366                  include,
2367                  minindex,
2368                  maxindex    = collectindices(details.fontdata.characters,details.indices,details.used,details.hash)
2369            local widths      = pdfarray()
2370            local differences = pdfarray()
2371            local charprocs   = pdfdictionary()
2372            local basefont    = pdfconstant(basefontname)
2373            local d           = 0
2374            local w           = 0
2375            local forcenotdef = minindex > 0
2376            local lastindex   = -0xFF
2377            if forcenotdef then
2378                widths[0] = 0
2379                minindex  = 0
2380                lastindex = 0
2381                d         = 2
2382                if not c_notdef then
2383                    w_notdef = 0
2384                    c_notdef = pdfconstant(".notdef")
2385                    r_notdef = pdfreference(pdfflushstreamobject("0 0 d0"))
2386                end
2387                differences[1]       = w_notdef
2388                differences[2]       = c_notdef
2389                charprocs[".notdef"] = r_notdef
2390            end
2391            for i=1,maxindex-minindex+1 do
2392                widths[i] = 0
2393            end
2394            for index, data in sortedhash(indices) do
2395                local name  = f_index(index)
2396                local glyph = glyphs[include[index]]
2397                if glyph then
2398                    local stream, width = glyphtopdf(glyph,data)
2399                    if stream then
2400                        if index - 1 ~= lastindex then
2401                            d = d + 1 differences[d] = index
2402                        end
2403                        lastindex = index
2404                        d = d + 1 differences[d] = pdfconstant(name)
2405                        charprocs[name]          = pdfreference(pdfflushstreamobject(stream))
2406                        widths[index-minindex+1] = width
2407                    end
2408                else
2409                    report_fonts("missing glyph %i in type3 font %a",index,basefontname)
2410                end
2411            end
2412            if not fontbbox then
2413                -- The specification permits zero values and these are actually also more
2414                -- robust as then there are no assumptions and no accuracy is needed.
2415                fontbbox = pdfarray { 0, 0, 0, 0 }
2416            end
2417            local encoding = pdfdictionary {
2418                Type        = pdfconstant("Encoding"),
2419                Differences = differences,
2420            }
2421            local tounicode  = tounicodedictionary(details,indices,maxindex,basefontname,false)
2422            local resources  = getresources and getresources()
2423            if not resources or not next(resources) then
2424             -- resources = lpdf.procset(true)
2425                resources = nil
2426            end
2427            local descriptor = pdfdictionary {
2428                -- most is optional in type3
2429                Type        = pdfconstant("FontDescriptor"),
2430                FontName    = basefont,
2431                Flags       = 4,
2432                ItalicAngle = 0,
2433            }
2434            local parent = pdfdictionary {
2435                Type           = pdfconstant("Font"),
2436                Subtype        = pdfconstant("Type3"),
2437                Name           = basefont,
2438                FontBBox       = fontbbox,
2439                FontMatrix     = fontmatrix,
2440                CharProcs      = pdfreference(pdfflushobject(charprocs)),
2441                Encoding       = pdfreference(pdfflushobject(encoding)),
2442                FirstChar      = minindex,
2443                LastChar       = maxindex,
2444                Widths         = pdfreference(pdfflushobject(widths)),
2445                FontDescriptor = pdfreference(pdfflushobject(descriptor)),
2446                Resources      = resources,
2447                ToUnicode      = tounicode and pdfreference(pdfflushstreamobject(tounicode)),
2448            }
2449            pdfflushobject(object,parent)
2450            if reset then
2451                reset()
2452            end
2453        end
2454
2455    end
2456
2457end
2458
2459-- writingmode
2460
2461local usedfonts = fonts.hashes.identifiers -- for now
2462local noffonts  = 0
2463
2464-- The main injector.
2465
2466-- here we need to test for sharing otherwise we reserve too many objects
2467
2468local getstreamhash  = fonts.handlers.otf.getstreamhash
2469local loadstreamdata = fonts.handlers.otf.loadstreamdata
2470
2471local objects = setmetatableindex(lpdf.usedfontobjects,function(t,k) -- defined in lpdf-lmt.lmt
2472    local v
2473    if type(k) == "number" then
2474        local h = getstreamhash(k)
2475        v = rawget(t,h)
2476        if not v then
2477            v = pdfreserveobject()
2478            t[h] = v
2479        end
2480        if trace_fonts then
2481            report_fonts("font id %i bound to hash %s and object %i",k,h,v)
2482        end
2483    else
2484        -- no problem as it can be svg only
2485     -- report_fonts("fatal error, hash %s asked but not used",k,h,v)
2486        v = pdfreserveobject()
2487        t[k] = v
2488    end
2489    return v
2490end)
2491
2492local n = 0
2493
2494local names = setmetatableindex(lpdf.usedfontnames,function(t,k) -- defined in lpdf-lmt.lmt
2495    local v
2496    if type(k) == "number" then
2497        local h = getstreamhash(k)
2498        v = rawget(t,h)
2499        if not v then
2500            n = n + 1
2501            v = n
2502            t[h] = v
2503        end
2504        if trace_fonts then
2505            report_fonts("font id %i bound to hash %s and name %i",k,h,n)
2506        end
2507    end
2508    t[k] = v
2509    return v
2510end)
2511
2512function lpdf.flushfonts()
2513
2514    local mainfonts = { }
2515
2516    statistics.starttiming(objects)
2517
2518    -- We loop over used characters (old approach, when we wanted to be equivalent wrt
2519    -- indices with luatex) but can also decide to use usedindices. However, we then
2520    -- don't have the id.
2521
2522    -- we can combine the two for loops .. todo
2523
2524    for fontid, used in sortedhash(lpdf.usedcharacters) do
2525     -- for a bitmap we need a different hash unless we stick to a fixed high
2526     -- resolution which makes much sense
2527        local hash = getstreamhash(fontid)
2528        if hash then
2529            local parent = mainfonts[hash]
2530            if not parent then
2531                local fontdata   = usedfonts[fontid]
2532                local rawdata    = fontdata.shared and fontdata.shared.rawdata
2533                local resources  = fontdata.resources  -- not always there, nullfont
2534                local properties = fontdata.properties -- writingmode and type3
2535                local parameters = fontdata.parameters -- used in type3
2536                if not rawdata then
2537                    -- we have a virtual font that loaded directly ... at some point i will
2538                    -- sort this out (in readanddefine we need to do a bit more) .. the problem
2539                    -- is that we have a hybrid font then
2540                    for xfontid, xfontdata in next, fonts.hashes.identifiers do
2541                        if fontid ~= xfontid then
2542                            local xhash = getstreamhash(xfontid)
2543                            if hash == xhash then
2544                                rawdata  = xfontdata.shared and xfontdata.shared.rawdata
2545                                if rawdata then
2546                                    resources  = xfontdata.resources
2547                                    properties = xfontdata.properties
2548                                    parameters = xfontdata.parameters
2549                                    break
2550                                end
2551                            end
2552                        end
2553                    end
2554                end
2555             -- if rawdata then -- we don't have these when we nest vf's (as in antykwa)
2556                    parent = {
2557                        hash         = hash,
2558                        fontdata     = fontdata,
2559                        filename     = (resources and resources.filename) or properties.filename or "unset",
2560                        indices      = { },
2561                        usedfonts    = { [fontid] = true },
2562                        used         = used,
2563                        rawdata      = rawdata,
2564                        properties   = properties, -- we assume consistency
2565                        parameters   = parameters, -- we assume consistency
2566                        streams      = { },
2567                        objectnumber = objects[hash],
2568                        basefontname = subsetname(properties.psname or properties.name or "unset"),
2569                        name         = names[hash],
2570                    }
2571                    mainfonts[hash] = parent
2572                    noffonts = noffonts + 1
2573             -- end
2574            end
2575            if parent then
2576                parent.usedfonts[fontid] = true
2577                local indices = parent.indices
2578                for k, v in next, used do
2579                    indices[k] = v
2580                end
2581            end
2582        end
2583    end
2584
2585--     local function identifywriter(details)
2586--         local filename = details.filename
2587--         local encoding, pfbfile, encfile = getmapentry(filename)
2588--         if trace_fonts then
2589--             report_fonts("file %a resolved to encoding %a and file %a",filename,encoding,pfbfile)
2590--         end
2591--         if not pfbfile or pfbfile == "" then
2592--             pfbfile = file.replacesuffix(filename,"pfb")
2593--         end
2594--         if encoding and pfbfile then
2595--             local properties   = details.properties
2596--             local descriptions = { }
2597--             local characters   = details.fontdata.characters
2598--             --
2599--             local size   = details.fontdata.parameters.size
2600--             local factor = details.fontdata.parameters.factor
2601--             --
2602--             local names, _, _, metadata = fonts.constructors.handlers.pfb.loadvector(pfbfile)
2603--             local reverse  = table.swapped(names)
2604--             local vector   = encoding.vector
2605--             local indices  = details.indices
2606--             local remapped = { }
2607--             local factor   = bpfactor * size / 65536
2608--             for k, v in next, indices do
2609--                 local name  = vector[k]
2610--                 local index = reverse[name] or 0
2611--                 local width = factor * (characters[k].width or 0)
2612--                 descriptions[k] = {
2613--                     width = width,
2614--                     index = index,
2615--                     name  = name,
2616--                 }
2617--                 remapped[index] = true
2618--             end
2619--             details.indices = remapped
2620--             --
2621--             details.rawdata.descriptions = descriptions
2622--             details.filename             = pfbfile -- hm
2623--             details.rawdata.metadata     = { }
2624--             --
2625--             properties.filename = pfbfile -- hm
2626--             properties.format   = "type1"
2627--         end
2628--     end
2629
2630--     local function identifywriter(details)
2631--         local descriptions = details.fontdata.characters
2632--         for k, v in next, descriptions do
2633--             v.index = v.index or v.order or k
2634--         end
2635--     end
2636
2637    for hash, details in sortedhash(mainfonts) do
2638        -- the filename can be somewhat weird if we have a virtual font that starts out with some
2639        local filename = details.filename
2640        if next(details.indices) then
2641            local properties = details.properties
2642            local bitmap     = properties.usedbitmap
2643            local method     = properties.method -- will be pk | pdf | svg | ...
2644            local format     = properties.format
2645            if trace_fonts then
2646                if method then
2647                    report_fonts("embedding %a hashed as %a using method %a",filename,hash,method)
2648                else
2649                    report_fonts("embedding %a hashed as %a",filename,hash)
2650                end
2651            end
2652            if bitmap or method then
2653                local format = "type3"
2654                local writer = mainwriters[format]
2655                if trace_fonts then
2656                    report_fonts("using main writer %a",format)
2657                end
2658                writer(details)
2659            elseif format == "pkcff" then
2660                local writer = mainwriters[format]
2661                if trace_fonts then
2662                    report_fonts("using main writer %a",format)
2663                end
2664                writer(details)
2665            else
2666                local writer = mainwriters[format]
2667                if writer then
2668                    if trace_fonts then
2669                        report_fonts("using main writer %a",format)
2670                    end
2671-- if format == "type1" then
2672--     identifywriter(details)
2673-- end
2674                    -- better move this test to the writers .. cleaner
2675                    local streams = loadstreamdata(details.fontdata)
2676                    if streams and streams.fontheader and streams.names then
2677                        details.streams = streams
2678                        writer(details)
2679                        details.streams = { }
2680                    elseif trace_fonts then
2681                        -- can be ok for e.g. emoji
2682                        report_fonts("no streams in %a",filename)
2683                    end
2684                    -- free some  memory
2685                else -- if trace_fonts then
2686                    report_fonts("no %a writer for %a",format,filename)
2687                end
2688            end
2689        else -- not problem for svg ...
2690         -- report_fonts("no indices for %a",filename)
2691        end
2692        if trace_fonts then
2693            local indices = details.indices
2694            if indices and next(indices) then
2695                report_fonts("embedded indices: % t",sortedkeys(details.indices))
2696            end
2697        end
2698        mainfonts[details.hash] = false -- done
2699    end
2700
2701    statistics.stoptiming(objects)
2702
2703end
2704
2705statistics.register("font embedding time",function()
2706    if noffonts > 0 then
2707        return format("%s seconds, %s fonts", statistics.elapsedtime(objects),noffonts)
2708    end
2709end)
2710
2711-- this is temporary
2712
2713function lpdf.getfontobjectnumber(k)
2714    return objects[k]
2715end
2716
2717function lpdf.getfontname(k)
2718    return names[k]
2719end
2720
2721lpdf.registerdocumentfinalizer(lpdf.flushfonts,1,"wrapping up fonts")
2722