To simplify the presentation, I will restrict myself here to the two operators ':' and '*'.
We will refer to the dimensions of a glyph in the font as its natural size, i.e. the size the glyph has before any scaling is done. Although in most reasonable fonts 'A1' has height 1 EM, dimensions of other glyphs may differ somewhat. E.g. the height of 'X1' may be 0.25 EM in one font and 0.30 EM in another. To be more precise, we assume the natural height and width of glyphs to be determined by the height and width of the bounding boxes around them. This also holds for rotated glyphs, and the bounding boxes remain orientated w.r.t. the normal x-axis and y-axis.
Furthermore, we assume there is a parameter of the scaling algorithm referred to as the unit size or US, which is a length expressed in terms of EM. It is used for determining how much subgroups of a vert_group (i.e. a group composed by the operator ':') or hor_group (i.e. a group composed by the operator '*') need to be scaled down. The sub_groups that are wider or higher, resp., than 1 US are scaled down to become precisely 1 US wide or high, but those that are not will keep their sizes, which may be less than 1 US. (We never scale up.) Normally 1 US equals 1 EM, but in RES one may also specify other unit sizes.
There is furthermore a value that we will call SEP here. This is the minimum distance between subgroups of a vert_group or hor_group, before this vert_group or hor_group is itself scaled down. A typical value for SEP, which is determined by the font, may be somewhere in the order of 0.15 EM.
To keep the presentation simple, we will discuss scaling and positioning as if it were done by one single algorithm. This algorithm basically works bottom-up, but with one top-down step at each level. The algorithm is bottom-up in the sense that after scaling has been performed for a subgroup, its individual glyphs thereafter maintain their relative sizes when the entire subgroup must be scaled down due to its composition with one or more neighbouring subgroups. However, the amount of white space within, say, the vert_groups in a hor_group, can only be determined after all vert_groups in that hor_group have been scaled, and this requires one top-down step before computation resumes in the normal bottom-up fashion.
TOPGROUP -> VERTGROUP | HORGROUP | 'g(' G ')' VERTGROUP -> ':(' VERTSUBS ')' VERTSUBS -> '[' VERTSUB ',' VERTSUB ']' | '[' VERTSUB '|' VERTSUBS ']' VERTSUB -> HORGROUP | 'g(' G ')' HORGROUP -> '*(' HORSUBS ')' HORSUBS -> '[' HORSUB ',' HORSUB ']' | '[' HORSUB '|' HORSUBS ']' HORSUB -> VERTGROUP | 'g(' G ')' G -> 'A1' | 'A2' | ... i.e. all glyphs ...As in most logical and functional programming languages, we assume that e.g. [ E1 | [E2, E3] ] and [E1, E2, E3] denote the same list. Thus, a VERTGROUP consists of functor ':' with an argument that is a list of at least 2 elements, each of which is a VERTSUB.
During execution of the algorithm, occurrences of glyphs will be assigned three values, and these occurrences will be represented as extended terms g(G,F,W,H), where F represents a factor that indicates by how much a glyph needs to be scaled down, and W and H represent the total amount of white space that needs to surround the glyph to the left and right ('W' is for 'width'), and to the top and bottom respectively ('H' is for 'height'). Initially, F is 1, and W and H are both 0. A typical case where H is nonzero is if a glyph occurs in isolation in a row, and this row is higher than the natural height of the glyph.
The same value W as above is also maintained at vert_groups, and the value H is maintained at hor_groups. In addition, groups obtain an argument that indicates the amount S of white space that should occur between each pair of consecutive subgroups. Initially this value is SEP. Thus groups are represented by terms of the form :(Ts,S,W) or *(Ts,S,H).
The width and height of terms are given by the following definitions:
width(g(G,F,W,H)) = F * width(G) + W width(:(Ts,S,W)) = max(map width Ts) + W width(*(Ts,S,H)) = sum(map width Ts) + S * (length(Ts) - 1) height(g(G,F,W,H)) = F * height(G) + H height(:(Ts,S,W)) = sum(map height Ts) + S * (length(Ts) - 1) height(*(Ts,S,H)) = max(map height Ts) + Hwhere we assume that width(G) and height(G) yield the natural height and width of a glyph G. We further assume sum, max, and length compute the sum, the maximum, and the length of a list, and map is defined by:
map f [a_1,a_2,...,a_n] = [f(a_1),f(a_2),...,f(a_n)]Below we will also use a function lambda informally defined by:
(lambda T exp(T)) v = exp(v)where exp(T) denotes some expression with subexpression T.
scale(g(G), W, H) = T_4 where T_0 = g(G,1,0,0) T_1 = if W == 0 then T_0 else scale_down(T_0, min(1,W/width(T_0)) T_2 = if H == 0 then T_1 else scale_down(T_1, min(1,H/height(T_1)) W' = max(W, width(T_2)) H' = max(H, height(T_2)) T_3 = distribute_width(T_2,W') T_4 = distribute_height(T_3,H') scale(:(Ts), W, H) = T_8 where Ts_1 = map ( lambda T scale(T,0,0) ) Ts Ts_2 = map ( lambda T scale_down(T,min(1,US/width(T) ) Ts_1 W' = max(map width Ts_2) Ts_3 = map ( lambda T distribute_width(T,W') ) Ts_2 T_4 = :(Ts_3,SEP,0) T_5 = if W == 0 then T_4 else scale_down(T_4, min(1,W/width(T_4)) T_6 = if H == 0 then T_5 else scale_down(T_5, min(1,H/height(T_5)) W'' = max(W, width(T_6)) H'' = max(H, height(T_6)) T_7 = distribute_width(T_6, W'') T_8 = distribute_height(T_7, H'') scale(*(Ts),W,H) = T_8 where ...analogous...where min obviously computes the minimum of its arguments.
We see that an isolated glyph is scaled down if the context dictates restrictions on width or height, and the glyph is centered if it is smaller than what the context dictates, by distributing surplus width or height. Note we never scale up, which explains the use of function min with first argument 1.
For a vert_group, we first apply the scaling algorithm recursively on each subgroup, and then scale down those subgroups that are wider than 1 US. The largest subgroup determines how wide the vert_group will be, and in the other subgroups we insert white space to match that width.
Thereafter we may scale down the vert_group in its entirety if needed to fit within the constraints of parameters W or H, and we distribute surplus width or height if the vert_group is smaller than what the context dictates.
The function scale_down makes all glyphs and white space in a group smaller, by a factor smaller than or equal to 1.
scale_down(g(G,F,W,H), F') = g(G, F'*F, F'*W, F'*H) scale_down(:(Ts,S,W), F) = :(Ts', F*S, F*W) where Ts' = map (lambda T scale_down(T,F)) Ts scale_down(*(Ts,S,H), F) = *(Ts', F*S, F*H) where Ts' = map (lambda T scale_down(T,F)) TsThe function distribute_width distributes horizontal white space to make a glyph or group of glyphs the required width. In the case of a single glyph or a vert_group, it distributes the white space on the left and right. In the case of a hor_group however, the space is distributed evenly between the subgroups.
distribute_width(g(G,F,W,H), W') = g(G, F, W + W'', H) where W'' = W' - width(g(G,F,W,H)) distribute_width(:(Ts,S,W), W') = :(Ts, S, W + W'') where W'' = W' - width(:(Ts,S,W)) distribute_width(*(Ts,S,H), W') = *(Ts, S + S', H) where S' = ( W' - width(*(Ts,S,H)) ) / (length(Ts)-1)Note that W' above can never be negative due to the way it is called by scale.
The function distribute_height is analogous.
That we investigate the glyphs internally in both subgroups is motivated by cases such as '(A1:A1)*(A1:A1)' versus 'A1*A1:A1*A1'. We would expect that both have roughly the same appearance, and that is indeed achieved here, since the '*' between the two occurrences of '(A1:A1)' introduces a smaller amount of white space than it would between two unscaled glyphs.
For the function insert, we do not investigate how much the glyphs in the first top_group have been scaled down, but we do investigate those in the second top_group that may border on the first top_group. Which those are depends on the specified place.
For boxes, we have a separate 'sep' value that is determined by the font. This is multiplied by the values for 'opensep' and 'closesep'. For 'undersep' and 'oversep' however, this only holds up to a value of 1, and a second linear mapping is used for values greater than 1, to ensure that a value of 10 represents half the inner distance between the two sides of the box; note that 9.99 is the largest real in RES.
T_4 = :(Ts_3,SEP,0)in the definition of scale, and SEP here is to be replaced by the outcome of the fitting algorithm, which is possibly a negative number.