summaryrefslogtreecommitdiff
path: root/tex/context/base/l-dimen.lua
diff options
context:
space:
mode:
authorHans Hagen <pragma@wxs.nl>2008-08-04 15:59:00 +0200
committerHans Hagen <pragma@wxs.nl>2008-08-04 15:59:00 +0200
commitf8ba0550d77fd6e2b307ff9dd3175fc0c613b8e2 (patch)
treeae27ca6edd0b2f1bcbe315d241b8152107d4e6a3 /tex/context/base/l-dimen.lua
parent1d63a6eae86a6b78d4563ed60521449e4bf89f3c (diff)
downloadcontext-f8ba0550d77fd6e2b307ff9dd3175fc0c613b8e2.tar.gz
stable 2008.08.04 15:59
Diffstat (limited to 'tex/context/base/l-dimen.lua')
-rw-r--r--tex/context/base/l-dimen.lua360
1 files changed, 360 insertions, 0 deletions
diff --git a/tex/context/base/l-dimen.lua b/tex/context/base/l-dimen.lua
new file mode 100644
index 000000000..33b2405a7
--- /dev/null
+++ b/tex/context/base/l-dimen.lua
@@ -0,0 +1,360 @@
+if not modules then modules = { } end modules ['l-dimen'] = {
+ version = 1.001,
+ comment = "support for dimensions",
+ author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+ copyright = "PRAGMA ADE / ConTeXt Development Team",
+ license = "see context related readme files"
+}
+
+--[[ldx--
+<p>Internally <l n='luatex'/> work with scaled point, which are
+represented by integers. However, in practice, at east at the
+<l n='tex'/> end we work with more generic units like points (pt). Going
+from scaled points (numbers) to one of those units can be
+done by using the conversion factors collected in the following
+table.</p>
+--ldx]]--
+
+local dimenfactors = {
+ ["pt"] = 1/65536,
+ ["in"] = ( 100/ 7227)/65536,
+ ["cm"] = ( 254/ 7227)/65536,
+ ["mm"] = ( 254/72270)/65536,
+ ["sp"] = 1,
+ ["bp"] = ( 7200/ 7227)/65536,
+ ["pc"] = ( 1/ 12)/65536,
+ ["dd"] = ( 1157/ 1238)/65536,
+ ["cc"] = ( 1157/14856)/65536,
+ ["nd"] = (20320/21681)/65536,
+ ["nc"] = ( 5080/65043)/65536
+}
+
+--[[ldx--
+<p>A conversion function that takes a number, unit (string) and optional
+format (string) is implemented using this table.</p>
+--ldx]]--
+
+local function todimen(n,unit,fmt)
+ if type(n) == 'string' then
+ return n
+ else
+ unit = unit or 'pt'
+ return (fmt or "%.5f%s"):format(n*dimenfactors[unit],unit)
+ end
+end
+
+--[[ldx--
+<p>We collect a bunch of converters in the <type>number</type> namespace.</p>
+--ldx]]--
+
+number = number or { }
+
+number.todimen = todimen
+number.dimenfactors = dimenfactors
+
+function number.topoints (n) return todimen(n,"pt") end
+function number.toinches (n) return todimen(n,"in") end
+function number.tocentimeters (n) return todimen(n,"cm") end
+function number.tomillimeters (n) return todimen(n,"mm") end
+function number.toscaledpoints(n) return todimen(n,"sp") end
+function number.toscaledpoints(n) return n .. "sp" end
+function number.tobasepoints (n) return todimen(n,"bp") end
+function number.topicas (n) return todimen(n "pc") end
+function number.todidots (n) return todimen(n,"dd") end
+function number.tociceros (n) return todimen(n,"cc") end
+function number.tonewdidots (n) return todimen(n,"nd") end
+function number.tonewciceros (n) return todimen(n,"nc") end
+
+--[[ldx--
+<p>More interesting it to implement a (sort of) dimen datatype, one
+that permits calculations too. First we define a function that
+converts a string to scaledpoints. We use <l n='lpeg'/>. We capture
+a number and optionally a unit. When no unit is given a constant
+capture takes place.</p>
+--ldx]]--
+
+local amount = lpeg.S("+-")^0 * lpeg.R("09")^0 * (lpeg.P(".") * lpeg.R("09")^1)^0
+local unit = lpeg.R("az")^1 / dimenfactors -- produces a capture
+
+local pattern = lpeg.C(amount) * (unit^1 + lpeg.Cc(1))
+
+--[[ldx--
+<p>We use a metatable to intercept errors. When no key is found in
+the table with factors, the metatable will be consulted for an
+alternative index function.</p>
+--ldx]]--
+
+local mt = { } setmetatable(dimenfactors,mt)
+
+mt.__index = function(t,s)
+ error("wrong dimension: " .. s)
+ return 1
+end
+
+function string:todimen()
+ if type(self) == "number" then
+ return self
+ else
+ local value, unit = pattern:match(self)
+ return value/unit
+ end
+end
+
+--[[ldx--
+<p>This converter accepts calls like:</p>
+
+<typing>
+string.todimen("10"))
+string.todimen(".10"))
+string.todimen("10.0"))
+string.todimen("10.0pt"))
+string.todimen("10pt"))
+string.todimen("10.0pt"))
+</typing>
+
+<p>And of course the often more efficient:</p>
+
+<typing>
+somestring:todimen("12.3cm")
+</typing>
+
+<p>With this in place, we can now implement a proper datatype for dimensions, one
+that permits us to do this:</p>
+
+<typing>
+s = dimen "10pt" + dimen "20pt" + dimen "200pt"
+ - dimen "100sp" / 10 + "20pt" + "0pt"
+</typing>
+
+<p>We create a local metatable for this new type:</p>
+--ldx]]--
+
+local dimensions = { }
+
+--[[ldx--
+<p>The main (and globally) visible representation of a dimen is defined next: it is
+a one-element table. The unit that is returned from the match is normally a number
+(one of the previously defined factors) but we also accept functions. Later we will
+see why.</p>
+--ldx]]--
+
+function dimen(a)
+ if a then
+ local ta= type(a)
+ if ta == "string" then
+ local value, unit = pattern:match(a)
+ if type(unit) == "function" then
+ k = value/unit()
+ else
+ k = value/unit
+ end
+ a = k
+ elseif ta == "table" then
+ a = a[1]
+ end
+ return setmetatable({ a }, dimensions)
+ else
+ return setmetatable({ 0 }, dimensions)
+ end
+end
+
+--[[ldx--
+<p>This function return a small hash with a metatable attached. It is
+through this metatable that we can do the calculations. We could have
+shared some of the code but for reasons of speed we don't.</p>
+--ldx]]--
+
+function dimensions.__add(a, b)
+ local ta, tb = type(a), type(b)
+ if ta == "string" then a = a:todimen() elseif ta == "table" then a = a[1] end
+ if tb == "string" then b = b:todimen() elseif tb == "table" then b = b[1] end
+ return setmetatable({ a + b }, dimensions)
+end
+
+function dimensions.__sub(a, b)
+ local ta, tb = type(a), type(b)
+ if ta == "string" then a = a:todimen() elseif ta == "table" then a = a[1] end
+ if tb == "string" then b = b:todimen() elseif tb == "table" then b = b[1] end
+ return setmetatable({ a - b }, dimensions)
+end
+
+function dimensions.__mul(a, b)
+ local ta, tb = type(a), type(b)
+ if ta == "string" then a = a:todimen() elseif ta == "table" then a = a[1] end
+ if tb == "string" then b = b:todimen() elseif tb == "table" then b = b[1] end
+ return setmetatable({ a * b }, dimensions)
+end
+
+function dimensions.__div(a, b)
+ local ta, tb = type(a), type(b)
+ if ta == "string" then a = a:todimen() elseif ta == "table" then a = a[1] end
+ if tb == "string" then b = b:todimen() elseif tb == "table" then b = b[1] end
+ return setmetatable({ a / b }, dimensions)
+end
+
+function dimensions.__unm(a)
+ local ta = type(a)
+ if ta == "string" then a = a:todimen() elseif ta == "table" then a = a[1] end
+ return setmetatable({ - a }, dimensions)
+end
+
+--[[ldx--
+<p>It makes no sense to implement the power and modulo function but
+the next two do make sense because they permits is code like:</p>
+
+<typing>
+local a, b = dimen "10pt", dimen "11pt"
+...
+if a > b then
+ ...
+end
+</typing>
+--ldx]]--
+
+-- makes no sense: dimensions.__pow and dimensions.__mod
+
+function dimensions.__lt(a, b)
+ return a[1] < b[1]
+end
+
+function dimensions.__eq(a, b)
+ return a[1] == b[1]
+end
+
+--[[ldx--
+<p>We also need to provide a function for conversion to string (so that
+we can print dimensions). We print them as points, just like <l n='tex'/>.</p>
+--ldx]]--
+
+function dimensions.__tostring(a)
+ return a[1]/65536 .. "pt" -- instead of todimen(a[1])
+end
+
+--[[ldx--
+<p>Since it does not take much code, we also provide a way to access
+a few accessors</p>
+
+<typing>
+print(dimen().pt)
+print(dimen().sp)
+</typing>
+--ldx]]--
+
+function dimensions.__index(tab,key)
+ local d = dimenfactors[key]
+ if not d then
+ error("illegal property of dimen: " .. key)
+ d = 1
+ end
+ return 1/d
+end
+
+--[[ldx--
+<p>In the converter from string to dimension we support functions as
+factors. This is because in <l n='tex'/> we have a few more units:
+<type>ex</type> and <type>em</type>. These are not constant factors but
+depend on the current font. They are not defined by default, but need
+an explicit function call. This is because at the moment that this code
+is loaded, the relevant tables that hold the functions needed may not
+yet be available.</p>
+--ldx]]--
+
+function dimensions.texify()
+ local fti, fc = fonts and fonts.tfm and fonts.tfm.id, font and font.current
+ if fti and fc then
+ dimenfactors["ex"] = function() return fti[fc()].ex_height end
+ dimenfactors["em"] = function() return fti[fc()].quad end
+ else
+ dimenfactors["ex"] = 1/65536* 4 -- 4pt
+ dimenfactors["em"] = 1/65536*10 -- 10pt
+ end
+end
+
+--[[ldx--
+<p>In order to set the defaults we call this function now. At some point
+the macro package needs to make sure the function is called again.</p>
+--ldx]]--
+
+dimensions.texify()
+
+--[[ldx--
+<p>The previous code is rather efficient (also thanks to <l n='lpeg'/>) but we
+can speed it up by caching converted dimensions. On my machine (2008) the following
+loop takes about 25.5 seconds.</p>
+
+<typing>
+for i=1,1000000 do
+ local s = dimen "10pt" + dimen "20pt" + dimen "200pt"
+ - dimen "100sp" / 10 + "20pt" + "0pt"
+end
+</typing>
+
+<p>When we cache converted strings this becomes 16.3 seconds. In order not
+to waste too much memory on it, we tag the values of the cache as being
+week which mean that the garbage collector will collect them in a next
+sweep. This means that in most cases the speed up is mostly affecting the
+current couple of calculations and as such the speed penalty is small.</p>
+
+<p>We redefine two previous defined functions that can benefit from
+this:</p>
+--ldx]]--
+
+local known = { } setmetatable(known, { __mode = "v" })
+
+function dimen(a)
+ if a then
+ local ta= type(a)
+ if ta == "string" then
+ local k = known[a]
+ if k then
+ a = k
+ else
+ local value, unit = pattern:match(a)
+ if type(unit) == "function" then
+ k = value/unit()
+ else
+ k = value/unit
+ end
+ known[a] = k
+ a = k
+ end
+ elseif ta == "table" then
+ a = a[1]
+ end
+ return setmetatable({ a }, dimensions)
+ else
+ return setmetatable({ 0 }, dimensions)
+ end
+end
+
+function string:todimen()
+ if type(self) == "number" then
+ return self
+ else
+ local k = known[self]
+ if not k then
+ local value, unit = pattern:match(self)
+ if value and unit then
+ k = value/unit
+ else
+ k = 0
+ end
+ -- print(self,value,unit)
+ known[self] = k
+ end
+ return k
+ end
+end
+
+--[[ldx--
+<p>In a similar fashion we can define a glue datatype. In that case we
+probably use a hash instead of a one-element table.</p>
+--ldx]]--
+
+--[[ldx--
+<p>Goodie:s</p>
+--ldx]]--
+
+function number.percent(n) return (n/100) * tex.hsize:todimen() end
+
+number["%"] = number.percent