font-shp.lua /size: 13 Kb    last modification: 2023-12-21 09:44
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.015
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
153-- todo: shared hash for this but not accessed often
154
155local function makehash(filename,sub,instance)
156    local name = cleanname(file.basename(filename))
157    if instance then
158        return formatters["%s-%s-%s"](name,sub or 0,cleanname(instance))
159    else
160        return formatters["%s-%s"]   (name,sub or 0)
161    end
162end
163
164local function loadoutlines(cache,filename,sub,instance)
165    local base = file.basename(filename)
166    local name = file.removesuffix(base)
167    local kind = file.suffix(filename)
168    local attr = lfs.attributes(filename)
169    local size = attr and attr.size or 0
170    local time = attr and attr.modification or 0
171    local sub  = tonumber(sub)
172
173    -- fonts.formats
174
175    if size > 0 and (kind == "otf" or kind == "ttf" or kind == "tcc") then
176        local hash = makehash(filename,sub,instance)
177        data = containers.read(cache,hash)
178        if not data or data.time ~= time or data.size  ~= size then
179            data = otf.readers.loadshapes(filename,sub,instance)
180            if data then
181                data.size   = size
182                data.format = data.format or (kind == "otf" and "opentype") or "truetype"
183                data.time   = time
184                packoutlines(data)
185                containers.write(cache,hash,data)
186                data = containers.read(cache,hash) -- frees old mem
187            end
188        end
189        unpackoutlines(data)
190    elseif size > 0 and (kind == "pfb") then
191        local hash = containers.cleanname(base) -- including suffix
192        data = containers.read(cache,hash)
193        if not data or data.time ~= time or data.size  ~= size then
194            data = afm.readers.loadshapes(filename)
195            if data then
196                data.size   = size
197                data.format = "type1"
198                data.time   = time
199                packoutlines(data)
200                containers.write(cache,hash,data)
201                data = containers.read(cache,hash) -- frees old mem
202            end
203        end
204        unpackoutlines(data)
205    else
206        data = {
207            filename = filename,
208            size     = 0,
209            time     = time,
210            format   = "unknown",
211            units    = 1000,
212            glyphs   = { }
213        }
214    end
215    return data
216end
217
218local function cachethem(cache,hash,data)
219    containers.write(cache,hash,data,compact_streams) -- arg 4 aka fast
220    return containers.read(cache,hash) -- frees old mem
221end
222
223local function loadstreams(cache,filename,sub,instance)
224    local base = file.basename(filename)
225    local name = file.removesuffix(base)
226    local kind = lower(file.suffix(filename))
227    local attr = lfs.attributes(filename)
228    local size = attr and attr.size or 0
229    local time = attr and attr.modification or 0
230    local sub  = tonumber(sub)
231    if size > 0 and (kind == "otf" or kind == "ttf" or kind == "ttc") then
232        local hash = makehash(filename,sub,instance)
233        data = containers.read(cache,hash)
234        if not data or data.time ~= time or data.size  ~= size then
235            data = otf.readers.loadshapes(filename,sub,instance,true)
236            if data then
237                local glyphs  = data.glyphs
238                local streams = { }
239             -- local widths  = { }
240                if glyphs then
241                    for i=0,#glyphs do
242                        local glyph = glyphs[i]
243                        if glyph then
244                            streams[i] = glyph.stream or ""
245                         -- widths [i] = glyph.width  or 0
246                        else
247                            streams[i] = ""
248                         -- widths [i] = 0
249                        end
250                    end
251                end
252                data.streams = streams
253             -- data.widths  = widths -- maybe more reliable!
254                data.glyphs  = nil
255                data.size    = size
256                data.format  = data.format or (kind == "otf" and "opentype") or "truetype"
257                data.time    = time
258                data = cachethem(cache,hash,data)
259            end
260        end
261    elseif size > 0 and (kind == "pfb") then
262        local hash = makehash(filename,sub,instance)
263        data = containers.read(cache,hash)
264        if not data or data.time ~= time or data.size  ~= size then
265            local names, encoding, streams, metadata = pfb.loadvector(filename,false,true)
266            if streams then
267                local fontbbox = metadata.fontbbox or { 0, 0, 0, 0 }
268             -- local widths   = { }
269                for i=0,#streams do
270                    local s = streams[i]
271                    streams[i] = s.stream or "\14"
272                 -- widths [i] = s.width  or 0
273                end
274                data = {
275                    filename   = filename,
276                    size       = size,
277                    time       = time,
278                    format     = "type1",
279                    streams    = streams,
280                 -- widths     = widths,
281                    fontheader = {
282                        fontversion = metadata.version,
283                        units       = 1000, -- can this be different?
284                        xmin        = fontbbox[1],
285                        ymin        = fontbbox[2],
286                        xmax        = fontbbox[3],
287                        ymax        = fontbbox[4],
288                    },
289                    horizontalheader = {
290                        ascender  = 0,
291                        descender = 0,
292                    },
293                    maximumprofile = {
294                        nofglyphs = #streams + 1,
295                    },
296                    names = {
297                        copyright = metadata.copyright,
298                        family    = metadata.familyname,
299                        fullname  = metadata.fullname,
300                        fontname  = metadata.fontname,
301                        subfamily = metadata.subfamilyname,
302                        trademark = metadata.trademark,
303                        notice    = metadata.notice,
304                        version   = metadata.version,
305                    },
306                    cffinfo = {
307                        familyname         = metadata.familyname,
308                        fullname           = metadata.fullname,
309                        italicangle        = metadata.italicangle,
310                        monospaced         = metadata.isfixedpitch and true or false,
311                        underlineposition  = metadata.underlineposition,
312                        underlinethickness = metadata.underlinethickness,
313                        weight             = metadata.weight,
314                    },
315                }
316                data = cachethem(cache,hash,data)
317            end
318        end
319    else
320        data = {
321            filename = filename,
322            size     = 0,
323            time     = time,
324            format   = "unknown",
325            streams  = { }
326        }
327    end
328    return data
329end
330
331local loadedshapes  = { }
332local loadedstreams = { }
333
334local function loadoutlinedata(fontdata,streams)
335    local properties = fontdata.properties
336    local filename   = properties.filename
337    local subindex   = fontdata.subindex
338    local instance   = properties.instance
339    local hash       = makehash(filename,subindex,instance)
340    local loaded     = loadedshapes[hash]
341    if not loaded then
342        loaded = loadoutlines(shapescache,filename,subindex,instance)
343        loadedshapes[hash] = loaded
344    end
345    return loaded
346end
347
348hashes.shapes = table.setmetatableindex(function(t,k)
349    local f = identifiers[k]
350    if f then
351        return loadoutlinedata(f)
352    end
353end)
354
355local function getstreamhash(fontid)
356    local fontdata = identifiers[fontid]
357    if fontdata then
358        local properties = fontdata.properties
359        local fonthash   = makehash(properties.filename,properties.subfont,properties.instance)
360        return fonthash, fontdata
361    end
362end
363
364local function loadstreamdata(fontdata)
365    local properties = fontdata.properties
366    local shared     = fontdata.shared
367    local rawdata    = shared and shared.rawdata
368    local metadata   = rawdata and rawdata.metadata
369    local filename   = properties.filename
370    local subindex   = metadata and metadata.subfontindex
371    local instance   = properties.instance
372    local hash       = makehash(filename,subindex,instance)
373    local loaded     = loadedstreams[hash]
374    if not loaded then
375        loaded = loadstreams(streamscache,filename,subindex,instance)
376        loadedstreams[hash] = loaded
377    end
378    return loaded
379end
380
381hashes.streams = table.setmetatableindex(function(t,k)
382    local f = identifiers[k]
383    if f then
384        return loadstreamdata(f)
385    end
386end)
387
388otf.loadoutlinedata = loadoutlinedata -- not public
389otf.loadstreamdata  = loadstreamdata  -- not public
390otf.loadshapes      = loadshapes
391otf.getstreamhash   = getstreamhash   -- not public, might move to other namespace
392
393local streams = fonts.hashes.streams
394
395-- we can now assume that luatex has this one
396
397callback.register("glyph_stream_provider",function(id,index,mode)
398    if id > 0 then
399        local streams = streams[id].streams
400     -- print(id,index,streams[index])
401        if streams then
402            return streams[index] or ""
403        end
404    end
405    return ""
406end)
407