font-shp.lua /size: 13 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['font-shp'] = {
2    version   = 1.001,
3    comment   = "companion to font-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9local tonumber, next = tonumber, next
10local concat = table.concat
11local formatters, lower = string.formatters, string.lower
12
13local otf          = fonts.handlers.otf
14local afm          = fonts.handlers.afm
15local pfb          = fonts.handlers.pfb
16
17local hashes       = fonts.hashes
18local identifiers  = hashes.identifiers
19
20local version      = otf.version or 0.011
21local shapescache  = containers.define("fonts", "shapes",  version, true)
22local streamscache = containers.define("fonts", "streams", version, true)
23
24-- shapes (can be come a separate file at some point)
25
26local compact_streams = false
27
28directives.register("fonts.streams.compact", function(v) compact_streams = v end)
29
30local function packoutlines(data,makesequence)
31    local subfonts = data.subfonts
32    if subfonts then
33        for i=1,#subfonts do
34            packoutlines(subfonts[i],makesequence)
35        end
36        return
37    end
38    local common = data.segments
39    if common then
40        return
41    end
42    local glyphs = data.glyphs
43    if not glyphs then
44        return
45    end
46    if makesequence then
47        for index=0,#glyphs do
48            local glyph = glyphs[index]
49            if glyph then
50                local segments = glyph.segments
51                if segments then
52                    local sequence    = { }
53                    local nofsequence = 0
54                    for i=1,#segments do
55                        local segment    = segments[i]
56                        local nofsegment = #segment
57                        -- why last first ... needs documenting
58                        nofsequence = nofsequence + 1
59                        sequence[nofsequence] = segment[nofsegment]
60                        for i=1,nofsegment-1 do
61                            nofsequence = nofsequence + 1
62                            sequence[nofsequence] = segment[i]
63                        end
64                    end
65                    glyph.sequence = sequence
66                    glyph.segments = nil
67                end
68            end
69        end
70    else
71        local hash    = { }
72        local common  = { }
73        local reverse = { }
74        local last    = 0
75        for index=0,#glyphs do
76            local glyph = glyphs[index]
77            if glyph then
78                local segments = glyph.segments
79                if segments then
80                    for i=1,#segments do
81                        local h = concat(segments[i]," ")
82                        hash[h] = (hash[h] or 0) + 1
83                    end
84                end
85            end
86        end
87        for index=0,#glyphs do
88            local glyph = glyphs[index]
89            if glyph then
90                local segments = glyph.segments
91                if segments then
92                    for i=1,#segments do
93                        local segment = segments[i]
94                        local h = concat(segment," ")
95                        if hash[h] > 1 then -- minimal one shared in order to hash
96                            local idx = reverse[h]
97                            if not idx then
98                                last = last + 1
99                                reverse[h] = last
100                                common[last] = segment
101                                idx = last
102                            end
103                            segments[i] = idx
104                        end
105                    end
106                end
107            end
108        end
109        if last > 0 then
110            data.segments = common
111        end
112    end
113end
114
115local function unpackoutlines(data)
116    local subfonts = data.subfonts
117    if subfonts then
118        for i=1,#subfonts do
119            unpackoutlines(subfonts[i])
120        end
121        return
122    end
123    local common = data.segments
124    if not common then
125        return
126    end
127    local glyphs = data.glyphs
128    if not glyphs then
129        return
130    end
131    for index=0,#glyphs do
132        local glyph = glyphs[index]
133        if glyph then
134            local segments = glyph.segments
135            if segments then
136                for i=1,#segments do
137                    local c = common[segments[i]]
138                    if c then
139                        segments[i] = c
140                    end
141                end
142            end
143        end
144    end
145    data.segments = nil
146end
147
148-- todo: loaders per format
149
150local readers   = otf.readers
151local cleanname = otf.readers.helpers.cleanname
152
153local function makehash(filename,sub,instance)
154    local name = cleanname(file.basename(filename))
155    if instance then
156        return formatters["%s-%s-%s"](name,sub or 0,cleanname(instance))
157    else
158        return formatters["%s-%s"]   (name,sub or 0)
159    end
160end
161
162local function loadoutlines(cache,filename,sub,instance)
163    local base = file.basename(filename)
164    local name = file.removesuffix(base)
165    local kind = file.suffix(filename)
166    local attr = lfs.attributes(filename)
167    local size = attr and attr.size or 0
168    local time = attr and attr.modification or 0
169    local sub  = tonumber(sub)
170
171    -- fonts.formats
172
173    if size > 0 and (kind == "otf" or kind == "ttf" or kind == "tcc") then
174        local hash = makehash(filename,sub,instance)
175        data = containers.read(cache,hash)
176        if not data or data.time ~= time or data.size  ~= size then
177            data = otf.readers.loadshapes(filename,sub,instance)
178            if data then
179                data.size   = size
180                data.format = data.format or (kind == "otf" and "opentype") or "truetype"
181                data.time   = time
182                packoutlines(data)
183                containers.write(cache,hash,data)
184                data = containers.read(cache,hash) -- frees old mem
185            end
186        end
187        unpackoutlines(data)
188    elseif size > 0 and (kind == "pfb") then
189        local hash = containers.cleanname(base) -- including suffix
190        data = containers.read(cache,hash)
191        if not data or data.time ~= time or data.size  ~= size then
192            data = afm.readers.loadshapes(filename)
193            if data then
194                data.size   = size
195                data.format = "type1"
196                data.time   = time
197                packoutlines(data)
198                containers.write(cache,hash,data)
199                data = containers.read(cache,hash) -- frees old mem
200            end
201        end
202        unpackoutlines(data)
203    else
204        data = {
205            filename = filename,
206            size     = 0,
207            time     = time,
208            format   = "unknown",
209            units    = 1000,
210            glyphs   = { }
211        }
212    end
213    return data
214end
215
216local function cachethem(cache,hash,data)
217    containers.write(cache,hash,data,compact_streams) -- arg 4 aka fast
218    return containers.read(cache,hash) -- frees old mem
219end
220
221local function loadstreams(cache,filename,sub,instance)
222    local base = file.basename(filename)
223    local name = file.removesuffix(base)
224    local kind = lower(file.suffix(filename))
225    local attr = lfs.attributes(filename)
226    local size = attr and attr.size or 0
227    local time = attr and attr.modification or 0
228    local sub  = tonumber(sub)
229    if size > 0 and (kind == "otf" or kind == "ttf" or kind == "ttc") then
230        local hash = makehash(filename,sub,instance)
231        data = containers.read(cache,hash)
232        if not data or data.time ~= time or data.size  ~= size then
233            data = otf.readers.loadshapes(filename,sub,instance,true)
234            if data then
235                local glyphs  = data.glyphs
236                local streams = { }
237                if glyphs then
238                    for i=0,#glyphs do
239                        local glyph = glyphs[i]
240                        if glyph then
241                            streams[i] = glyph.stream or ""
242                        else
243                            streams[i] = ""
244                        end
245                    end
246                end
247                data.streams = streams
248                data.glyphs  = nil
249                data.size    = size
250                data.format  = data.format or (kind == "otf" and "opentype") or "truetype"
251                data.time    = time
252                data = cachethem(cache,hash,data)
253            end
254        end
255    elseif size > 0 and (kind == "pfb") then
256        local hash = makehash(filename,sub,instance)
257        data = containers.read(cache,hash)
258        if not data or data.time ~= time or data.size  ~= size then
259            local names, encoding, streams, metadata = pfb.loadvector(filename,false,true)
260            if streams then
261                local fontbbox = metadata.fontbbox or { 0, 0, 0, 0 }
262                for i=0,#streams do
263                    streams[i] = streams[i].stream or "\14"
264                end
265                data = {
266                    filename   = filename,
267                    size       = size,
268                    time       = time,
269                    format     = "type1",
270                    streams    = streams,
271                    fontheader = {
272                        fontversion = metadata.version,
273                        units       = 1000, -- can this be different?
274                        xmin        = fontbbox[1],
275                        ymin        = fontbbox[2],
276                        xmax        = fontbbox[3],
277                        ymax        = fontbbox[4],
278                    },
279                    horizontalheader = {
280                        ascender  = 0,
281                        descender = 0,
282                    },
283                    maximumprofile = {
284                        nofglyphs = #streams + 1,
285                    },
286                    names = {
287                        copyright = metadata.copyright,
288                        family    = metadata.familyname,
289                        fullname  = metadata.fullname,
290                        fontname  = metadata.fontname,
291                        subfamily = metadata.subfamilyname,
292                        trademark = metadata.trademark,
293                        notice    = metadata.notice,
294                        version   = metadata.version,
295                    },
296                    cffinfo = {
297                        familyname         = metadata.familyname,
298                        fullname           = metadata.fullname,
299                        italicangle        = metadata.italicangle,
300                        monospaced         = metadata.isfixedpitch and true or false,
301                        underlineposition  = metadata.underlineposition,
302                        underlinethickness = metadata.underlinethickness,
303                        weight             = metadata.weight,
304                    },
305                }
306                data = cachethem(cache,hash,data)
307            end
308        end
309    else
310        data = {
311            filename = filename,
312            size     = 0,
313            time     = time,
314            format   = "unknown",
315            streams  = { }
316        }
317    end
318    return data
319end
320
321local loadedshapes  = { }
322local loadedstreams = { }
323
324local function loadoutlinedata(fontdata,streams)
325    local properties = fontdata.properties
326    local filename   = properties.filename
327    local subindex   = fontdata.subindex
328    local instance   = properties.instance
329    local hash       = makehash(filename,subindex,instance)
330    local loaded     = loadedshapes[hash]
331    if not loaded then
332        loaded = loadoutlines(shapescache,filename,subindex,instance)
333        loadedshapes[hash] = loaded
334    end
335    return loaded
336end
337
338hashes.shapes = table.setmetatableindex(function(t,k)
339    local f = identifiers[k]
340    if f then
341        return loadoutlinedata(f)
342    end
343end)
344
345local function getstreamhash(fontid)
346    local fontdata = identifiers[fontid]
347    if fontdata then
348        local properties = fontdata.properties
349        return makehash(properties.filename,properties.subfont,properties.instance), fontdata
350    end
351end
352
353local function loadstreamdata(fontdata)
354    local properties = fontdata.properties
355    local shared     = fontdata.shared
356    local rawdata    = shared and shared.rawdata
357    local metadata   = rawdata and rawdata.metadata
358    local filename   = properties.filename
359    local subindex   = metadata and metadata.subfontindex
360    local instance   = properties.instance
361    local hash       = makehash(filename,subindex,instance)
362    local loaded     = loadedstreams[hash]
363    if not loaded then
364        loaded = loadstreams(streamscache,filename,subindex,instance)
365        loadedstreams[hash] = loaded
366    end
367    return loaded
368end
369
370hashes.streams = table.setmetatableindex(function(t,k)
371    local f = identifiers[k]
372    if f then
373        return loadstreamdata(f)
374    end
375end)
376
377otf.loadoutlinedata = loadoutlinedata -- not public
378otf.loadstreamdata  = loadstreamdata  -- not public
379otf.loadshapes      = loadshapes
380otf.getstreamhash   = getstreamhash   -- not public, might move to other namespace
381
382local streams = fonts.hashes.streams
383
384-- we can now assume that luatex has this one
385
386callback.register("glyph_stream_provider",function(id,index,mode)
387    if id > 0 then
388        local streams = streams[id].streams
389     -- print(id,index,streams[index])
390        if streams then
391            return streams[index] or ""
392        end
393    end
394    return ""
395end)
396