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 |