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