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