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