font-web.lua /size: 6874 b    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['font-otr'] = {
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
9-- Okay, compressing fonts this way is rather simple but one might wonder what the gain
10-- is in this time of 4K youtube movies and most of the web pages wasting space and
11-- bandwidth on advertisements. For version 2 we can use "woff2_decompress" from google
12-- and in a tex environment one can as well store the ttf/otf files in the tex tree. So,
13-- eventually we might even remove this code when version 1 is obsolete.
14
15local ioopen         = io.open
16local replacesuffix  = file.replacesuffix
17
18local readers        = fonts and fonts.handlers.otf.readers
19
20local streamreader   = readers and readers.streamreader or utilities.files
21local streamwriter   = readers and readers.streamwriter or utilities.files
22
23local readstring     = streamreader.readstring
24local readcardinal2  = streamreader.readcardinal2
25local readcardinal4  = streamreader.readcardinal4
26local getsize        = streamreader.getsize
27local setposition    = streamreader.setposition
28local getposition    = streamreader.getposition
29
30local writestring    = streamwriter.writestring
31local writecardinal4 = streamwriter.writecardinal4
32local writecardinal2 = streamwriter.writecardinal2
33local writebyte      = streamwriter.writebyte
34
35local decompress     = zlib.decompress
36
37directives.register("fonts.streamreader",function()
38
39    streamreader  = utilities.streams
40
41    readstring    = streamreader.readstring
42    readcardinal2 = streamreader.readcardinal2
43    readcardinal4 = streamreader.readcardinal4
44    getsize       = streamreader.getsize
45    setposition   = streamreader.setposition
46    getposition   = streamreader.getposition
47
48end)
49
50local infotags = {
51    ["os/2"] = true,
52    ["head"] = true,
53    ["maxp"] = true,
54    ["hhea"] = true,
55    ["hmtx"] = true,
56    ["post"] = true,
57    ["cmap"] = true,
58}
59
60-- A WOFF font is just a compressed OpenType font so that can be dealt with
61-- efficiently. A WOFF2 on the other hand is compressed using some integer
62-- compression and that means that all data tables needs to be decompressed. Given
63-- the rediculous amount of data that gets transmitted over the web one can wonder
64-- what that saves: if we look at advertisements, useless videos and images,
65-- overdone screen real estate, a few fonts are the least of our worries. So we move
66-- around less bytes but then in memory expand that data which also costs time and
67-- in the end takes as much memory as an original fotn that can be cached on disk
68-- anyway. But well, if it can be done it usually is done, no matter what.
69
70local report = logs.reporter("fonts","woff")
71
72local runner = sandbox.registerrunner {
73    name     = "woff2otf",
74    method   = "execute",
75    program  = "woff2_decompress",
76    template = "%inputfile% %outputfile%",
77    reporter = report,
78    checkers = {
79        inputfile  = "readable",
80        outputfile = "writable",
81    }
82}
83
84local function woff2otf(inpname,outname,infoonly)
85
86    local outname = outname or replacesuffix(inpname,"otf")
87    local inp     = ioopen(inpname,"rb")
88
89    if not inp then
90        report("invalid input file %a",inpname)
91        return
92    end
93
94    local signature = readstring(inp,4)
95
96    if not (signature == "wOFF" or signature == "wOF2") then
97        inp:close()
98        report("invalid signature in %a",inpname)
99        return
100    end
101
102    local flavor = readstring(inp,4)
103
104    if not (flavor == "OTTO" or flavor == "true" or flavor == "\0\1\0\0") then
105        inp:close()
106        report("unsupported flavor %a in %a",flavor,inpname)
107        return
108    end
109
110    if signature == "wOF2" then
111        inp:close()
112        if false then
113            if runner then
114                runner {
115                    inputfile  = inpname,
116                    outputfile = outname,
117                }
118            end
119            return outname, flavor
120        else
121            report("skipping version 2 file %a",inpname)
122            return
123        end
124    end
125
126    local out = ioopen(outname,"wb")
127
128    if not out then
129        inp:close()
130        report("invalid output file %a",outname)
131        return
132    end
133
134    local header = {
135        signature      = signature,
136        flavor         = flavor,
137        length         = readcardinal4(inp),
138        numtables      = readcardinal2(inp),
139        reserved       = readcardinal2(inp),
140        totalsfntsize  = readcardinal4(inp),
141        majorversion   = readcardinal2(inp),
142        minorversion   = readcardinal2(inp),
143        metaoffset     = readcardinal4(inp),
144        metalength     = readcardinal4(inp),
145        metaoriglength = readcardinal4(inp),
146        privoffset     = readcardinal4(inp),
147        privlength     = readcardinal4(inp),
148    }
149
150    local entries = { }
151
152    for i=1,header.numtables do
153        local entry = {
154            tag        = readstring   (inp,4),
155            offset     = readcardinal4(inp),
156            compressed = readcardinal4(inp),
157            size       = readcardinal4(inp),
158            checksum   = readcardinal4(inp),
159        }
160        if not infoonly or infotags[lower(entry.tag)] then
161            entries[#entries+1] = entry
162        end
163    end
164
165    local nofentries    = #entries
166    local entryselector = 0  -- we don't need these
167    local searchrange   = 0  -- we don't need these
168    local rangeshift    = 0  -- we don't need these
169
170    writestring   (out,flavor)
171    writecardinal2(out,nofentries)
172    writecardinal2(out,entryselector)
173    writecardinal2(out,searchrange)
174    writecardinal2(out,rangeshift)
175
176    local offset  = 12 + nofentries * 16
177    local offsets = { }
178
179    for i=1,nofentries do
180        local entry = entries[i]
181        local size  = entry.size
182        writestring(out,entry.tag)
183        writecardinal4(out,entry.checksum)
184        writecardinal4(out,offset) -- the new offset
185        writecardinal4(out,size)
186        offsets[i] = offset
187        offset = offset + size
188        local p = 4 - offset % 4
189        if p > 0 then
190            offset = offset + p
191        end
192    end
193
194    for i=1,nofentries do
195        local entry  = entries[i]
196        local offset = offsets[i]
197        local size   = entry.size
198        setposition(inp,entry.offset+1)
199        local data = readstring(inp,entry.compressed)
200        if #data ~= size then
201            data = decompress(data)
202        end
203        setposition(out,offset+1)
204        writestring(out,data)
205        local p = 4 - offset + size % 4
206        if p > 0 then
207            for i=1,p do
208                writebyte(out,0)
209            end
210        end
211    end
212
213    inp:close()
214    out:close()
215
216    return outname, flavor
217
218end
219
220if readers then
221    readers.woff2otf = woff2otf
222else
223    return woff2otf
224end
225