DIRECTORY
Commander USING [CommandProc, Register],
Convert USING [RopeFromInt],
Font USING [BoundingBox, Box, Contains, Create, FONT, GetLigatureOrKern, LigatureOrKern, Name, WidthVector],
Imager USING [black, ConcatT, Context, Create, CurveTo, LineTo, MakeFont, MakeGray, MaskFill, MaskVector, MoveTo, Reset, Scale, SetColor, SetFont, SetXY, ShowCharacters, Trajectory, TrajectoryRep, Transform, Translate, Pair, Transformation],
ImagerBasic USING [PathMapType],
ImagerBridge USING [SetViewFromGraphicsContext],
ImagerTransform USING [Concat, Scale],
IO USING [CharClass, EndOfStream, GetTokenRope, PutF, PutRope, refAny, RIS, STREAM],
IPOutput USING [BeginMakeSimpleCO, BeginMaster, BeginPreamble, ConcatT, CorrectMask, CurveTo, EndMakeSimpleCO, EndMaster, EndPreamble, IGet, LineTo, MakeOutline, MakeVec, MaskFill, Master, MoveTo, PutIdentifier, PutInt, PutName, PutOp, PutReal, Scale, SetXRel, SetXYRel, Trans],
Menus USING [ClickProc, CreateEntry, CreateMenu, InsertMenuEntry, Menu],
Process USING [MsecToTicks, Pause],
ProcessExtras USING [CheckForAbort],
Real USING [RoundLI],
RealFns USING [Power],
Rope USING [Cat, Equal, Fetch, Find, Length, ROPE, Substr],
UFPressFontReader USING [GetCharOutline, Representation],
ViewerClasses USING [InitProc, PaintProc, ScrollProc, Viewer, ViewerClass, ViewerClassRec],
ViewerOps USING [CreateViewer, DestroyViewer, PaintViewer, RegisterViewerClass];
IPFontMasterImpl:
CEDAR MONITOR
IMPORTS Commander, Font, ImagerTransform, IO, IPOutput, Real, Rope, UFPressFontReader, ViewerOps, ImagerBridge, Imager, Convert, Menus, RealFns, Process, ProcessExtras
SHARES Imager -- for TrajectoryRep
= BEGIN
Master: TYPE ~ IPOutput.Master;
ROPE: TYPE ~ Rope.ROPE;
Pair:
TYPE ~ Imager.Pair;
MapTrajectory: ImagerBasic.PathMapType = {
t: Imager.Trajectory = NARROW[data];
move[t.lp];
FOR x: Imager.Trajectory ← t, x.prev
UNTIL x.prev=
NIL
DO
p0: Pair = x.prev.lp;
WITH x
SELECT
FROM
x: REF Imager.TrajectoryRep[line] => line[p0];
x: REF Imager.TrajectoryRep[curve] => curve[x.p2, x.p1, p0];
x: REF Imager.TrajectoryRep[conic] => conic[x.p1, p0, x.r];
x: REF Imager.TrajectoryRep[move] => ERROR;
ENDCASE => ERROR;
ENDLOOP;
};
ConstructSampleFontVector:
PROC [master: Master] ~ {
vecCounts: ARRAY [0..20) OF INT ← ALL[0];
vecTags: ARRAY [0..20) OF ATOM;
vecTop: INTEGER ← -1;
El: PROC ~ {vecCounts[vecTop] ← vecCounts[vecTop] + 1};
BeginVector:
PROC [tag:
ATOM] ~ {
vecTop ← vecTop + 1;
vecTags[vecTop] ← tag;
vecCounts[vecTop] ← 0;
};
EndVector:
PROC [tag:
ATOM] ~ {
IF vecTags[vecTop] # tag THEN ERROR;
master.MakeVec[vecCounts[vecTop]];
vecTags[vecTop] ← NIL;
vecCounts[vecTop] ← 0;
vecTop ← vecTop - 1;
};
PutInfoForChar:
PROC [c:
INT] ~ {
IF c = 32
THEN {
BeginVector[$spaceMetrics];
El[]; master.PutIdentifier["widthX"]; El[]; master.PutReal[0.34];
El[]; master.PutIdentifier["widthY"]; El[]; master.PutInt[0];
El[]; master.PutIdentifier["amplified"]; El[]; master.PutInt[1];
EndVector[$spaceMetrics];
}
ELSE {
BeginVector[$charMetrics];
El[]; master.PutIdentifier["widthX"]; El[]; master.PutReal[0.24];
EndVector[$charMetrics];
}
};
first: INT ← 32;
last: INT ← 34;
BeginVector[$font];
El[]; master.PutIdentifier["characterMetrics"];
El[]; BeginVector[$characterMetrics];
FOR c:
INT
IN [first..last]
DO
El[]; master.PutInt[c];
El[]; PutInfoForChar[c];
ENDLOOP;
EndVector[$characterMetrics];
El[]; master.PutIdentifier["name"];
El[]; master.PutName["xerox/ascii/times"];
EndVector[$font];
};
SampleMetricMaster:
PROC ~ {
master: Master ← IPOutput.BeginMaster["Times.IPFont"];
master.BeginPreamble;
ConstructSampleFontVector[master];
master.EndPreamble;
master.EndMaster;
};
Switches:
TYPE ~
RECORD [
operators: BOOLEAN ← FALSE,
correction: BOOLEAN ← FALSE,
bbox: BOOLEAN ← FALSE,
kerns: BOOLEAN ← FALSE,
ligatures: BOOLEAN ← FALSE,
integerCoding: BOOLEAN ← FALSE,
viewer: BOOLEAN ← FALSE
];
BadSwitch:
ERROR [offset:
INT] ~
CODE;
ParseSwitches:
PROC [rope:
ROPE]
RETURNS [switches: Switches] ~ {
FOR i:
INT
IN [1..rope.Length)
DO
SELECT rope.Fetch[i]
FROM
'o, 'O => switches.operators ← TRUE;
'c, 'C => switches.correction ← TRUE;
'b, 'B => switches.bbox ← TRUE;
'k, 'K => switches.kerns ← TRUE;
'l, 'L => switches.ligatures ← TRUE;
'e, 'E => switches ← [TRUE, TRUE, TRUE, TRUE, TRUE, FALSE, FALSE];
'i, 'I => switches.integerCoding ← TRUE;
'v, 'V => switches.viewer ← TRUE;
'} => IF i = rope.Length-1 THEN RETURN;
ENDCASE => ERROR BadSwitch[i];
ENDLOOP;
ERROR BadSwitch[rope.Length];
};
MustBeSplineFontToGetOperators:
ERROR [name:
ROPE] ~
CODE;
fiducial: REAL ← 15840;
lastInteger:
REAL ←
INTEGER.
LAST;
ConstructUnifiedFontVector:
PROC [master: Master, font: Font.
FONT, switches: Switches] ~ {
vecCounts: ARRAY [0..20) OF INT ← ALL[0];
vecTags: ARRAY [0..20) OF ATOM;
vecTop: INTEGER ← -1;
viewer: ViewerClasses.Viewer ← IF switches.viewer THEN ViewerOps.CreateViewer[$FontMasterViewerClass, [name: font.Name, column: left, iconic: TRUE]] ELSE NIL;
El: PROC ~ {vecCounts[vecTop] ← vecCounts[vecTop] + 1};
BeginVector:
PROC [tag:
ATOM] ~ {
vecTop ← vecTop + 1;
vecTags[vecTop] ← tag;
vecCounts[vecTop] ← 0;
};
EndVector:
PROC [tag:
ATOM] ~ {
IF vecTags[vecTop] # tag THEN ERROR;
master.MakeVec[vecCounts[vecTop]];
vecTags[vecTop] ← NIL;
vecCounts[vecTop] ← 0;
vecTop ← vecTop - 1;
};
PutOperatorForChar:
PROC [c:
CHAR] ~ {
width: Pair ← font.WidthVector[c];
nTrajectories: INT ← 0;
Scale:
PROC [r:
REAL]
RETURNS [s:
REAL] ~ {
IF NOT switches.integerCoding THEN s ← r
ELSE {
s ← fiducial*r;
IF ABS[s] IN [1..lastInteger] THEN s ← Real.RoundLI[s];
};
};
outline: LIST OF Imager.Trajectory ← NIL;
moveToProc:
PROCEDURE [x, y:
REAL] ~ {
outline ← CONS[Imager.MoveTo[[Scale[x], Scale[y]]], outline];
nTrajectories ← nTrajectories + 1;
};
lineToProc:
PROCEDURE [x, y:
REAL] ~ {
outline.first ← outline.first.LineTo[[Scale[x], Scale[y]]];
};
curveToProc:
PROCEDURE [x1, y1, x2, y2, x3, y3:
REAL] ~ {
outline.first ← outline.first.CurveTo[[Scale[x1], Scale[y1]], [Scale[x2], Scale[y2]], [Scale[x3], Scale[y3]]];
master.CurveTo[Scale[x1], Scale[y1], Scale[x2], Scale[y2], Scale[x3], Scale[y3]];
};
drawAreaProc: PROCEDURE ~ {};
IF UFPressFontReader.Representation[[font.graphicsKey, 0]] # outline THEN ERROR MustBeSplineFontToGetOperators[font.Name];
master.BeginMakeSimpleCO;
master.Trans;
IF switches.integerCoding THEN {master.Scale[1.0/fiducial]; master.ConcatT};
UFPressFontReader.GetCharOutline[[font.graphicsKey, 0], c, moveToProc, lineToProc, curveToProc, drawAreaProc];
IF viewer #
NIL
THEN {
data: ViewerData ← NARROW[viewer.data];
IF viewer.destroyed THEN ERROR ABORTED;
IF data #
NIL
THEN {
data.outline ← outline;
data.charCode ← ORD[c];
ViewerOps.PaintViewer[viewer, client];
WaitForNext[viewer];
};
};
IF nTrajectories > 0
THEN {
FOR s:
LIST
OF Imager.Trajectory ← outline, s.rest
UNTIL s =
NIL
DO
move: PROC[p: Pair] ~ {master.MoveTo[p.x, p.y]};
line: PROC[p: Pair] ~ {master.LineTo[p.x, p.y]};
curve: PROC[p1, p2, p3: Pair] ~ {master.CurveTo[p1.x, p1.y, p2.x, p2.y, p3.x, p3.y]};
conic: PROC[p1, p2: Pair, r: REAL] ~ {ERROR};
MapTrajectory[s.first, move, line, curve, conic];
ENDLOOP;
master.MakeOutline[nTrajectories];
master.MaskFill;
};
IF switches.integerCoding THEN {master.Scale[fiducial]; master.ConcatT};
IF c = '
THEN {
master.PutReal[width.x];
master.IGet[amplifySpace];
master.PutOp[mul];
IF width.y = 0 THEN master.PutOp[space]
ELSE {
master.PutReal[width.y];
master.IGet[amplifySpace];
master.PutOp[mul];
master.PutInt[2];
master.PutOp[copy];
master.PutOp[setxyrel];
master.PutOp[correctspace];
};
}
ELSE {
IF width.y = 0 THEN master.SetXRel[width.x] ELSE master.SetXYRel[width.x, width.y];
master.CorrectMask;
};
master.EndMakeSimpleCO;
};
PutInfoForChar:
PROC [c:
CHAR] ~ {
width: Pair ← font.WidthVector[c];
areAny: BOOLEAN ← FALSE;
BeginVector[$charMetrics];
IF width.x # 0
THEN {
El[]; master.PutIdentifier["widthX"]; El[]; master.PutReal[width.x];
};
IF width.y # 0
THEN {
El[]; master.PutIdentifier["widthY"]; El[]; master.PutReal[width.y];
};
IF c = '
THEN {
El[]; master.PutIdentifier["amplified"]; El[]; master.PutInt[1];
};
IF switches.correction THEN {El[]; master.PutIdentifier["correction"]; El[]; master.PutInt[IF c = ' THEN 1 ELSE 2]};
IF c # '
AND switches.bbox
THEN {
box: Font.Box ← font.BoundingBox[c];
IF box.xmin # 0
THEN {
El[]; master.PutIdentifier["leftExtent"]; El[]; master.PutReal[box.xmin];
};
IF box.xmax # 0
THEN {
El[]; master.PutIdentifier["rightExtent"]; El[]; master.PutReal[box.xmax];
};
IF box.ymin # 0
THEN {
El[]; master.PutIdentifier["descent"]; El[]; master.PutReal[box.ymin];
};
IF box.ymax # 0
THEN {
El[]; master.PutIdentifier["ascent"]; El[]; master.PutReal[box.ymax];
};
};
areAny ← FALSE;
IF switches.kerns
THEN
FOR c2:
CHAR
IN [font.bc..font.ec]
DO
IF font.Contains[c2]
THEN {
ligKern: Font.LigatureOrKern ← font.GetLigatureOrKern[c, c2];
WITH ligKern
SELECT
FROM
k: Font.LigatureOrKern.kern => {
IF
NOT areAny
THEN {
El[]; master.PutIdentifier["kerns"];
El[]; BeginVector[$kerns];
areAny ← TRUE;
};
El[]; BeginVector[$kern];
El[]; master.PutInt[ORD[c2]];
El[]; master.PutReal[k.kernAmount];
El[]; master.PutInt[0];
EndVector[$kern];
};
ENDCASE => NULL;
};
ENDLOOP;
IF areAny THEN EndVector[$kerns];
areAny ← FALSE;
IF switches.ligatures
THEN
FOR c2:
CHAR
IN [font.bc..font.ec]
DO
IF font.Contains[c2]
THEN {
ligKern: Font.LigatureOrKern ← font.GetLigatureOrKern[c, c2];
WITH ligKern
SELECT
FROM
lig: Font.LigatureOrKern.ligature => {
IF
NOT areAny
THEN {
El[]; master.PutIdentifier["ligatures"];
El[]; BeginVector[$ligatures];
areAny ← TRUE;
};
El[]; BeginVector[$ligature];
El[]; master.PutInt[ORD[c2]];
El[]; master.PutInt[ORD[lig.ligatureCode]];
EndVector[$ligature];
};
ENDCASE => NULL;
};
ENDLOOP;
IF areAny THEN EndVector[$ligatures];
areAny ← FALSE;
EndVector[$charMetrics];
};
BeginVector[$font];
IF switches.operators
THEN {
El[]; master.PutIdentifier["operators"];
El[]; BeginVector[$operators];
FOR c:
CHAR
IN [font.bc..font.ec]
DO
IF font.Contains[c]
THEN {
El[]; master.PutInt[ORD[c]];
El[]; PutOperatorForChar[c];
};
ENDLOOP;
EndVector[$operators];
};
El[]; master.PutIdentifier["characterMetrics"];
El[]; BeginVector[$characterMetrics];
FOR c:
CHAR
IN [font.bc..font.ec]
DO
IF font.Contains[c]
THEN {
El[]; master.PutInt[ORD[c]];
El[]; PutInfoForChar[c];
};
ENDLOOP;
EndVector[$characterMetrics];
El[]; master.PutIdentifier["name"];
El[]; master.PutName[font.Name];
EndVector[$font];
IF viewer # NIL AND NOT viewer.destroyed THEN ViewerOps.DestroyViewer[viewer];
};
GetToken:
PROC [stream:
IO.
STREAM]
RETURNS [token:
ROPE ←
NIL] = {
token ← stream.GetTokenRope[Break ! IO.EndOfStream => CONTINUE].token;
};
Break:
PROC [char:
CHAR]
RETURNS [
IO.CharClass] = {
IF char = '← OR char = '; THEN RETURN [break];
IF char = ' OR char = ' OR char = ', OR char = '\n THEN RETURN [sepr];
RETURN [other];
};
FontMetricMasterCommand: Commander.CommandProc ~ {
stream: IO.STREAM ← IO.RIS[cmd.commandLine];
outputName: ROPE ← GetToken[stream];
token: ROPE ← GetToken[stream];
globalSwitches: Switches;
master: Master;
IF
NOT token.Equal["←"]
THEN {
cmd.out.PutRope["Specify output ← input, please\n"];
RETURN;
};
master ← IPOutput.BeginMaster[outputName];
master.BeginPreamble;
token ← GetToken[stream];
WHILE token #
NIL
DO
switchesStart: INT ← token.Find["{"];
switches: Switches ← IF switchesStart >= 0 THEN ParseSwitches[token.Substr[start: switchesStart]] ELSE globalSwitches;
IF switchesStart = 0 THEN globalSwitches ← switches
ELSE {
fontName: ROPE ← IF switchesStart >= 0 THEN token.Substr[len: switchesStart] ELSE token;
font: Font.FONT ← Font.Create[fontName, ImagerTransform.Scale[1]];
cmd.out.PutRope[fontName];
cmd.out.PutF["%g", IO.refAny[NEW[Switches ← switches]]];
ConstructUnifiedFontVector[master, font, switches];
cmd.out.PutRope["\n"];
};
token ← GetToken[stream];
ENDLOOP;
master.EndPreamble;
master.EndMaster;
cmd.out.PutRope[outputName];
cmd.out.PutRope[" written.\n"];
};
ViewerData:
TYPE ~
REF ViewerDataRep;
ViewerStatus:
TYPE ~ {edited, filling, editing};
ViewerDataRep:
TYPE ~
RECORD [
transformation: Imager.Transformation,
outline: LIST OF Imager.Trajectory,
charCode: INT,
nextHit: BOOL ← FALSE,
inputEnabled: BOOL ← FALSE
];
ViewerPaint: ViewerClasses.PaintProc ~ {
imager: Imager.Context ← Imager.Create[$LFDisplay];
data: ViewerData ← NARROW [self.data];
origin: Pair;
IF data = NIL THEN RETURN;
ImagerBridge.SetViewFromGraphicsContext[imager, context];
origin ← Imager.Transform[data.transformation, [0, 0]];
Imager.SetColor[imager, Imager.MakeGray[0.5]];
Imager.MaskVector[imager, [origin.x - 0.01, origin.y], [origin.x + 0.01, origin.y]];
Imager.MaskVector[imager, [origin.x, origin.y - 0.01], [origin.x, origin.y + 0.01]];
Imager.ConcatT[imager, data.transformation];
Imager.SetColor[imager, Imager.black];
IF data.outline # NIL THEN Imager.MaskFill[imager, data.outline];
Imager.Reset[imager];
Imager.SetXY[imager, [0.005, 0.005]];
Imager.SetFont[imager, Imager.MakeFont["xerox/pressfonts/modern/mrr", 0.005]];
IF data.charCode >= 0
THEN Imager.ShowCharacters[imager,
Convert.RopeFromInt[data.charCode].Cat[
" ", Convert.RopeFromInt[data.charCode, 8],
" ", Convert.RopeFromInt[data.charCode, 16]
]
];
};
HScrollHit: Menus.ClickProc ~ {
viewer: ViewerClasses.Viewer ← NARROW[parent];
data: ViewerData ← NARROW[viewer.data];
IF data = NIL THEN RETURN;
data.transformation ← data.transformation.Concat[Imager.Translate[IF mouseButton = red THEN -16*metersPerPoint ELSE 16*metersPerPoint, 0]];
ViewerOps.PaintViewer[viewer, client];
};
scaleAmt: REAL ← RealFns.Power[2, 0.25];
statusChange:
CONDITION ← [timeout: Process.MsecToTicks[900]];
NextHit:
ENTRY Menus.ClickProc ~ {
ENABLE UNWIND => NULL;
viewer: ViewerClasses.Viewer ← NARROW[parent];
data: ViewerData ← NARROW[viewer.data];
done: BOOLEAN ← FALSE;
IF data = NIL THEN RETURN;
UNTIL data.inputEnabled DO WAIT statusChange ENDLOOP;
data.inputEnabled ← FALSE;
data.nextHit ← TRUE;
BROADCAST statusChange;
UNTIL viewer.destroyed
OR done
DO
UNTIL data.inputEnabled DO WAIT statusChange ENDLOOP;
SELECT mouseButton
FROM
red => done ← TRUE;
yellow => {IF data.outline # NIL AND data.outline.rest # NIL THEN done ← TRUE};
blue => {Process.Pause[Process.MsecToTicks[500]]};
ENDCASE => ERROR;
IF
NOT done
THEN {
data.inputEnabled ← FALSE;
data.nextHit ← TRUE;
BROADCAST statusChange;
};
ENDLOOP;
};
WaitForNext:
ENTRY
PROC [viewer: ViewerClasses.Viewer] ~ {
ENABLE UNWIND => NULL;
data: ViewerData ← NARROW[viewer.data];
IF data.nextHit THEN {data.nextHit ← FALSE; RETURN};
data.inputEnabled ← TRUE;
BROADCAST statusChange;
UNTIL data.nextHit DO WAIT statusChange; ProcessExtras.CheckForAbort[] ENDLOOP;
data.nextHit ← FALSE;
};
SizeHit:
ENTRY Menus.ClickProc ~ {
viewer: ViewerClasses.Viewer ← NARROW[parent];
data: ViewerData ← NARROW[viewer.data];
scale: REAL ← IF mouseButton = red THEN scaleAmt ELSE 1/scaleAmt;
IF data = NIL THEN RETURN;
UNTIL data.inputEnabled DO WAIT statusChange ENDLOOP;
data.transformation ← Imager.Scale[scale].Concat[data.transformation];
ViewerOps.PaintViewer[viewer, client];
};
ReverseHit:
ENTRY Menus.ClickProc ~ {
viewer: ViewerClasses.Viewer ← NARROW[parent];
data: ViewerData ← NARROW[viewer.data];
refn: REF INT ← NARROW[clientData];
IF data = NIL THEN RETURN;
UNTIL data.inputEnabled DO WAIT statusChange ENDLOOP;
IF refn #
NIL
THEN {
list: LIST OF Imager.Trajectory ← data.outline;
n: INT ← 0;
FOR t: LIST OF Imager.Trajectory ← list, t.rest UNTIL t=NIL DO n ← n + 1 ENDLOOP;
IF refn^ >= n THEN RETURN;
FOR i: INT IN [0..refn^) DO list ← list.rest ENDLOOP;
list.first ← ReverseTrajectory[list.first];
ViewerOps.PaintViewer[viewer, client];
};
};
ReverseTrajectory:
PROC [s: Imager.Trajectory]
RETURNS [t: Imager.Trajectory] ~ {
move: PROC[p: Pair] ~ {t ← Imager.MoveTo[p]};
line: PROC[p: Pair] ~ {t ← t.LineTo[p]};
curve: PROC[p1, p2, p3: Pair] ~ {t ← t.CurveTo[p1, p2, p3]};
conic: PROC[p1, p2: Pair, r: REAL] ~ {ERROR};
MapTrajectory[s, move, line, curve, conic];
};
ViewerInit: ViewerClasses.InitProc ~ {
viewerData: ViewerData ← NEW[ViewerDataRep];
menu: Menus.Menu ← Menus.CreateMenu[1];
Menus.InsertMenuEntry[menu, Menus.CreateEntry[name: "Next", proc: NextHit]];
Menus.InsertMenuEntry[menu, Menus.CreateEntry[name: "<<<-->>>", proc: HScrollHit]];
Menus.InsertMenuEntry[menu, Menus.CreateEntry[name: "Size", proc: SizeHit]];
Menus.InsertMenuEntry[menu, Menus.CreateEntry[name: "r5", proc: ReverseHit, clientData: NEW[INT ← 5]]];
Menus.InsertMenuEntry[menu, Menus.CreateEntry[name: "r4", proc: ReverseHit, clientData: NEW[INT ← 4]]];
Menus.InsertMenuEntry[menu, Menus.CreateEntry[name: "r3", proc: ReverseHit, clientData: NEW[INT ← 3]]];
Menus.InsertMenuEntry[menu, Menus.CreateEntry[name: "r2", proc: ReverseHit, clientData: NEW[INT ← 2]]];
Menus.InsertMenuEntry[menu, Menus.CreateEntry[name: "r1", proc: ReverseHit, clientData: NEW[INT ← 1]]];
Menus.InsertMenuEntry[menu, Menus.CreateEntry[name: "r0", proc: ReverseHit, clientData: NEW[INT ← 0]]];
viewerData.transformation ← Imager.Scale[0.1].Concat[Imager.Translate[0.025, 0.025]];
self.data ← viewerData;
self.menu ← menu;
};
metersPerPoint:
REAL ← 0.0254/72;
ViewerScroll:
ENTRY ViewerClasses.ScrollProc ~ {
data: ViewerData ← NARROW [self.data];
IF data = NIL OR op = query OR op = thumb THEN RETURN [0, 100];
IF amount = 0 THEN RETURN [0, 100];
IF op = down THEN amount ← -amount;
UNTIL data.inputEnabled DO WAIT statusChange ENDLOOP;
data.transformation ← data.transformation.Concat[Imager.Translate[0, amount*metersPerPoint]];
ViewerOps.PaintViewer[self, client];
RETURN [0, 100];
};
fontMasterViewerClass: ViewerClasses.ViewerClass ←
NEW [ViewerClasses.ViewerClassRec ← [
init: ViewerInit,
paint: ViewerPaint,
scroll: ViewerScroll,
coordSys: bottom,
icon: tool
]];
ViewerOps.RegisterViewerClass[$FontMasterViewerClass, fontMasterViewerClass];
Commander.Register["IPFontMaster", FontMetricMasterCommand, "Make an Interpress metric master from Imager fonts (output ← {<global switches>} fontname1{<local switches>} fontname2 . . .)\nO => operators\nC => correction\nB => bbox\nK => kerns\nL => ligatures\nE => all of the above\nI => integerCoding\nV => viewer\n"];
END.