util-dim.lua /size: 13 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['util-dim'] = {
2    version   = 1.001,
3    comment   = "support for dimensions",
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--[[ldx--
10<p>Internally <l n='luatex'/> work with scaled point, which are
11represented by integers. However, in practice, at east at the
12<l n='tex'/> end we work with more generic units like points (pt). Going
13from scaled points (numbers) to one of those units can be
14done by using the conversion factors collected in the following
15table.</p>
16--ldx]]--
17
18local format, match, gsub, type, setmetatable = string.format, string.match, string.gsub, type, setmetatable
19local P, S, R, Cc, C, lpegmatch = lpeg.P, lpeg.S, lpeg.R, lpeg.Cc, lpeg.C, lpeg.match
20
21local allocate          = utilities.storage.allocate
22local setmetatableindex = table.setmetatableindex
23local formatters        = string.formatters
24
25local texget            = tex and tex.get or function() return 65536*10*100 end
26
27local p_stripzeros      = lpeg.patterns.stripzeros
28
29--this might become another namespace
30
31number = number or { }
32local number = number
33
34number.tonumberf = function(n) return lpegmatch(p_stripzeros,format("%.20f",n)) end
35number.tonumberg = function(n) return format("%.20g",n) end
36
37local dimenfactors = allocate {
38    ["pt"] =             1/65536,
39    ["in"] = (  100/ 7227)/65536,
40    ["cm"] = (  254/ 7227)/65536,
41    ["mm"] = ( 2540/ 7227)/65536,
42    ["sp"] =                   1, -- 65536 sp in 1pt
43    ["bp"] = ( 7200/ 7227)/65536,
44    ["pc"] = (    1/   12)/65536,
45    ["dd"] = ( 1157/ 1238)/65536,
46    ["cc"] = ( 1157/14856)/65536,
47 -- ["nd"] = (20320/21681)/65536,
48 -- ["nc"] = ( 5080/65043)/65536
49}
50
51-- print(table.serialize(dimenfactors))
52--
53--  %.99g:
54--
55--  t={
56--   ["bp"]=1.5201782378580324e-005,
57--   ["cc"]=1.1883696112892098e-006,
58--   ["cm"]=5.3628510057769479e-007,
59--   ["dd"]=1.4260435335470516e-005,
60--   ["em"]=0.000152587890625,
61--   ["ex"]=6.103515625e-005,
62--   ["in"]=2.1113586636917117e-007,
63--   ["mm"]=5.3628510057769473e-008,
64-- --["nc"]=1.1917446679504327e-006,
65-- --["nd"]=1.4300936015405194e-005,
66-- --["pc"]=1.2715657552083333e-006,
67--   ["pt"]=1.52587890625e-005,
68--   ["sp"]=1,
69--  }
70--
71--  patched %s and tonumber
72--
73--  t={
74--   ["bp"]=0.00001520178238,
75--   ["cc"]=0.00000118836961,
76--   ["cm"]=0.0000005362851,
77--   ["dd"]=0.00001426043534,
78--   ["em"]=0.00015258789063,
79--   ["ex"]=0.00006103515625,
80--   ["in"]=0.00000021113587,
81--   ["mm"]=0.00000005362851,
82-- --["nc"]=0.00000119174467,
83-- --["nd"]=0.00001430093602,
84--   ["pc"]=0.00000127156576,
85--   ["pt"]=0.00001525878906,
86--   ["sp"]=1,
87--  }
88
89--[[ldx--
90<p>A conversion function that takes a number, unit (string) and optional
91format (string) is implemented using this table.</p>
92--ldx]]--
93
94local f_none = formatters["%s%s"]
95local f_true = formatters["%0.5F%s"]
96
97local function numbertodimen(n,unit,fmt) -- will be redefined later !
98    if type(n) == 'string' then
99        return n
100    else
101        unit = unit or 'pt'
102        n = n * dimenfactors[unit]
103        if not fmt then
104            fmt = f_none(n,unit)
105        elseif fmt == true then
106            fmt = f_true(n,unit)
107        else
108            return formatters[fmt](n,unit)
109        end
110    end
111end
112
113--[[ldx--
114<p>We collect a bunch of converters in the <type>number</type> namespace.</p>
115--ldx]]--
116
117number.maxdimen     = 1073741823
118number.todimen      = numbertodimen
119number.dimenfactors = dimenfactors
120
121function number.topoints      (n,fmt) return numbertodimen(n,"pt",fmt) end
122function number.toinches      (n,fmt) return numbertodimen(n,"in",fmt) end
123function number.tocentimeters (n,fmt) return numbertodimen(n,"cm",fmt) end
124function number.tomillimeters (n,fmt) return numbertodimen(n,"mm",fmt) end
125function number.toscaledpoints(n,fmt) return numbertodimen(n,"sp",fmt) end
126function number.toscaledpoints(n)     return            n .. "sp"      end
127function number.tobasepoints  (n,fmt) return numbertodimen(n,"bp",fmt) end
128function number.topicas       (n,fmt) return numbertodimen(n "pc",fmt) end
129function number.todidots      (n,fmt) return numbertodimen(n,"dd",fmt) end
130function number.tociceros     (n,fmt) return numbertodimen(n,"cc",fmt) end
131-------- number.tonewdidots   (n,fmt) return numbertodimen(n,"nd",fmt) end
132-------- number.tonewciceros  (n,fmt) return numbertodimen(n,"nc",fmt) end
133
134--[[ldx--
135<p>More interesting it to implement a (sort of) dimen datatype, one
136that permits calculations too. First we define a function that
137converts a string to scaledpoints. We use <l n='lpeg'/>. We capture
138a number and optionally a unit. When no unit is given a constant
139capture takes place.</p>
140--ldx]]--
141
142local amount = (S("+-")^0 * R("09")^0 * P(".")^0 * R("09")^0) + Cc("0")
143local unit   = R("az")^1 + P("%")
144
145local dimenpair = amount/tonumber * (unit^1/dimenfactors + Cc(1)) -- tonumber is new
146
147lpeg.patterns.dimenpair = dimenpair
148
149local splitter = amount/tonumber * C(unit^1)
150
151function number.splitdimen(str)
152    return lpegmatch(splitter,str)
153end
154
155--[[ldx--
156<p>We use a metatable to intercept errors. When no key is found in
157the table with factors, the metatable will be consulted for an
158alternative index function.</p>
159--ldx]]--
160
161setmetatableindex(dimenfactors, function(t,s)
162 -- error("wrong dimension: " .. (s or "?")) -- better a message
163    return false
164end)
165
166--[[ldx--
167<p>We redefine the following function later on, so we comment it
168here (which saves us bytecodes.</p>
169--ldx]]--
170
171-- function string.todimen(str)
172--     if type(str) == "number" then
173--         return str
174--     else
175--         local value, unit = lpegmatch(dimenpair,str)
176--         return value/unit
177--     end
178-- end
179--
180-- local stringtodimen = string.todimen
181
182local stringtodimen -- assigned later (commenting saves bytecode)
183
184local amount = S("+-")^0 * R("09")^0 * S(".,")^0 * R("09")^0
185local unit   = P("pt") + P("cm") + P("mm") + P("sp") + P("bp") + P("in")  +
186               P("pc") + P("dd") + P("cc") + P("nd") + P("nc")
187
188local validdimen = amount * unit
189
190lpeg.patterns.validdimen = validdimen
191
192--[[ldx--
193<p>This converter accepts calls like:</p>
194
195<typing>
196string.todimen("10")
197string.todimen(".10")
198string.todimen("10.0")
199string.todimen("10.0pt")
200string.todimen("10pt")
201string.todimen("10.0pt")
202</typing>
203
204<p>With this in place, we can now implement a proper datatype for dimensions, one
205that permits us to do this:</p>
206
207<typing>
208s = dimen "10pt" + dimen "20pt" + dimen "200pt"
209        - dimen "100sp" / 10 + "20pt" + "0pt"
210</typing>
211
212<p>We create a local metatable for this new type:</p>
213--ldx]]--
214
215local dimensions = { }
216
217--[[ldx--
218<p>The main (and globally) visible representation of a dimen is defined next: it is
219a one-element table. The unit that is returned from the match is normally a number
220(one of the previously defined factors) but we also accept functions. Later we will
221see why. This function is redefined later.</p>
222--ldx]]--
223
224-- function dimen(a)
225--     if a then
226--         local ta= type(a)
227--         if ta == "string" then
228--             local value, unit = lpegmatch(pattern,a)
229--             if type(unit) == "function" then
230--                 k = value/unit()
231--             else
232--                 k = value/unit
233--             end
234--             a = k
235--         elseif ta == "table" then
236--             a = a[1]
237--         end
238--         return setmetatable({ a }, dimensions)
239--     else
240--         return setmetatable({ 0 }, dimensions)
241--     end
242-- end
243
244--[[ldx--
245<p>This function return a small hash with a metatable attached. It is
246through this metatable that we can do the calculations. We could have
247shared some of the code but for reasons of speed we don't.</p>
248--ldx]]--
249
250function dimensions.__add(a, b)
251    local ta, tb = type(a), type(b)
252    if ta == "string" then a = stringtodimen(a) elseif ta == "table" then a = a[1] end
253    if tb == "string" then b = stringtodimen(b) elseif tb == "table" then b = b[1] end
254    return setmetatable({ a + b }, dimensions)
255end
256
257function dimensions.__sub(a, b)
258    local ta, tb = type(a), type(b)
259    if ta == "string" then a = stringtodimen(a) elseif ta == "table" then a = a[1] end
260    if tb == "string" then b = stringtodimen(b) elseif tb == "table" then b = b[1] end
261    return setmetatable({ a - b }, dimensions)
262end
263
264function dimensions.__mul(a, b)
265    local ta, tb = type(a), type(b)
266    if ta == "string" then a = stringtodimen(a) elseif ta == "table" then a = a[1] end
267    if tb == "string" then b = stringtodimen(b) elseif tb == "table" then b = b[1] end
268    return setmetatable({ a * b }, dimensions)
269end
270
271function dimensions.__div(a, b)
272    local ta, tb = type(a), type(b)
273    if ta == "string" then a = stringtodimen(a) elseif ta == "table" then a = a[1] end
274    if tb == "string" then b = stringtodimen(b) elseif tb == "table" then b = b[1] end
275    return setmetatable({ a / b }, dimensions)
276end
277
278function dimensions.__unm(a)
279    local ta = type(a)
280    if ta == "string" then a = stringtodimen(a) elseif ta == "table" then a = a[1] end
281    return setmetatable({ - a }, dimensions)
282end
283
284--[[ldx--
285<p>It makes no sense to implement the power and modulo function but
286the next two do make sense because they permits is code like:</p>
287
288<typing>
289local a, b = dimen "10pt", dimen "11pt"
290...
291if a > b then
292    ...
293end
294</typing>
295--ldx]]--
296
297-- makes no sense: dimensions.__pow and dimensions.__mod
298
299function dimensions.__lt(a, b)
300    return a[1] < b[1]
301end
302
303function dimensions.__eq(a, b)
304    return a[1] == b[1]
305end
306
307--[[ldx--
308<p>We also need to provide a function for conversion to string (so that
309we can print dimensions). We print them as points, just like <l n='tex'/>.</p>
310--ldx]]--
311
312function dimensions.__tostring(a)
313    return a[1]/65536 .. "pt" -- instead of todimen(a[1])
314end
315
316--[[ldx--
317<p>Since it does not take much code, we also provide a way to access
318a few accessors</p>
319
320<typing>
321print(dimen().pt)
322print(dimen().sp)
323</typing>
324--ldx]]--
325
326function dimensions.__index(tab,key)
327    local d = dimenfactors[key]
328    if not d then
329        error("illegal property of dimen: " .. key)
330        d = 1
331    end
332    return 1/d
333end
334
335--[[ldx--
336<p>In the converter from string to dimension we support functions as
337factors. This is because in <l n='tex'/> we have a few more units:
338<type>ex</type> and <type>em</type>. These are not constant factors but
339depend on the current font. They are not defined by default, but need
340an explicit function call. This is because at the moment that this code
341is loaded, the relevant tables that hold the functions needed may not
342yet be available.</p>
343--ldx]]--
344
345   dimenfactors["ex"] =  4 * 1/65536 --   4pt
346   dimenfactors["em"] = 10 * 1/65536 --  10pt
347-- dimenfactors["%"]  =  4 * 1/65536 -- 400pt/100
348
349--[[ldx--
350<p>The previous code is rather efficient (also thanks to <l n='lpeg'/>) but we
351can speed it up by caching converted dimensions. On my machine (2008) the following
352loop takes about 25.5 seconds.</p>
353
354<typing>
355for i=1,1000000 do
356    local s = dimen "10pt" + dimen "20pt" + dimen "200pt"
357        - dimen "100sp" / 10 + "20pt" + "0pt"
358end
359</typing>
360
361<p>When we cache converted strings this becomes 16.3 seconds. In order not
362to waste too much memory on it, we tag the values of the cache as being
363week which mean that the garbage collector will collect them in a next
364sweep. This means that in most cases the speed up is mostly affecting the
365current couple of calculations and as such the speed penalty is small.</p>
366
367<p>We redefine two previous defined functions that can benefit from
368this:</p>
369--ldx]]--
370
371local known = { } setmetatable(known, { __mode = "v" })
372
373function dimen(a)
374    if a then
375        local ta= type(a)
376        if ta == "string" then
377            local k = known[a]
378            if k then
379                a = k
380            else
381                local value, unit = lpegmatch(dimenpair,a)
382                if value and unit then
383                    k = value/unit -- to be considered: round
384                else
385                    k = 0
386                end
387                known[a] = k
388                a = k
389            end
390        elseif ta == "table" then
391            a = a[1]
392        end
393        return setmetatable({ a }, dimensions)
394    else
395        return setmetatable({ 0 }, dimensions)
396    end
397end
398
399function string.todimen(str) -- maybe use tex.sp when available
400    local t = type(str)
401    if t == "number" then
402        return str
403    else
404        local k = known[str]
405        if not k then
406            if t == "string" then
407                local value, unit = lpegmatch(dimenpair,str)
408                if value and unit then
409                    k = value/unit -- to be considered: round
410                else
411                    k = 0
412                end
413            else
414                k = 0
415            end
416            known[str] = k
417        end
418        return k
419    end
420end
421
422-- local known = { }
423--
424-- function string.todimen(str) -- maybe use tex.sp
425--     local k = known[str]
426--     if not k then
427--         k = tex.sp(str)
428--         known[str] = k
429--     end
430--     return k
431-- end
432
433stringtodimen = string.todimen -- local variable defined earlier
434
435function number.toscaled(d)
436    return format("%0.5f",d/0x10000) -- 2^16
437end
438
439--[[ldx--
440<p>In a similar fashion we can define a glue datatype. In that case we
441probably use a hash instead of a one-element table.</p>
442--ldx]]--
443
444--[[ldx--
445<p>Goodie:s</p>
446--ldx]]--
447
448function number.percent(n,d) -- will be cleaned up once luatex 0.30 is out
449    d = d or texget("hsize")
450    if type(d) == "string" then
451        d = stringtodimen(d)
452    end
453    return (n/100) * d
454end
455
456number["%"] = number.percent
457