The imaging model
The Imager synthesizes a complex image on a page by repeatedly laying down simple primitive images. For example, a single Imager operation might place an image of a specific character at a specific position on the page. A subsequent operator might place another character somewhere else, and so on until a complex image is built up. A painter performs a very similar set of operations to create a complex image: he selects a brush, dips it in paint, and lays down a stroke of color. Complex images are created by a series of these simple actions.
The imaging model involves three objects:
— The page image. The page image is a two-dimensional image that accumulates results from primitive images being laid down. It plays the role of the painter's canvas.
— The mask. The mask, specified for each primitive image to be added to the page image, determines exactly where the page image will be modified. In effect, the mask specifies an opening through which ink can be pressed onto the page image. The mask thus plays the role of the painter's brush stroke.
— The color. The color specifies the color of the ink to be pushed through the mask onto the page image in order to add the primitive image to the page image; it may take on many colors, various shades of gray (including white and solid black), and transparent. To continue the painting analogy, the color specifies the color of paint in which to dip the brush.
The Imager makes complicated images, then, by specifying a sequence of (mask, color) pairs to be laid down on the page image. Invocations of mask operators actually cause the page image to be altered. The color to be used is held in a state variable in the imager context; it applies to all masks until the color is changed.
At the beginning of each page, a new page image is initialized to ``paper-white,'' or the natural color of the material on which the image is being formed. The imaging operators, then, will control the ink deposited on the material. In other imaging applications, the initial state of the page image is chosen to achieve as nearly as possible the same effect. If images are being made on a television display, the initial state of the entire display is white. If images are being made on film, the initial state of the film is transparent, which would correspond to white if the film were projected with a white light. Although the following discussion frequently refers to "pages" for convenience, the Imager can be used to create images on any two-dimensional medium.
Types
Color: TYPE = REF ImagerBasic.ColorRep;
ConstantColor: TYPE = REF ImagerBasic.ColorRep[constant];
Pair: TYPE = ImagerBasic.Pair;
PixelArray: TYPE = ImagerBasic.PixelArray;
StrokeEnd: TYPE = ImagerBasic.StrokeEnd;
Transformation: TYPE = ImagerBasic.Transformation;
Visibility: TYPE = ImagerBasic.Visibility;
nullREAL:
REAL = ImagerBasic.nullREAL;
FONT: TYPE = Font.FONT;
ROPE: TYPE = Rope.ROPE;
RopeOrRefText: TYPE = REF;
Transformations
pointsToMeters: Transformation; -- printer's points, 72.27 to the inch
micasToMeters: Transformation;
ConcatT:
PROC[context: Context, m: Transformation];
TranslateT:
PROC[context: Context, x, y:
REAL];
Equivalent to ConcatT[context, Translate[x, y]]
RotateT:
PROC[context: Context, a:
REAL];
Equivalent to ConcatT[context, Rotate[a]]. The angle a is meaured in degrees. The rotation can be viewed in two ways: it will rotate coordinate axes clockwise by the angle a, while it will rotate geometrical figures counterclockwise by the angle a.
ScaleT:
PROC[context: Context, s:
REAL];
Equivalent to ConcatT[context, Scale[s]]
Scale2T:
PROC[context: Context, sx, sy:
REAL];
Equivalent to ConcatT[context, Scale2[sx, sy]]
Move:
PROC[context: Context];
Modify T so that the origin maps to the current position.
Trans:
PROC[context: Context];
Modify T so that the origin maps to the rounded current position.
The rounding in Trans implies that any coordinates to which T is subsequently applied will be translated by an integral number of grid points. This convention allows often-used instances such as characters to be scan-converted once and then translated at will. Trans is designed together with SetXYRel (see below) to achieve positioning precision, while still letting each instance of a character be scan-converted identically.
Current position operators
The imaging operators make it easy to locate a graphical object such as a character at the current position. The current position is measured in the view coordinate system, and is recorded in two persistent imager variables, cpx and cpy. It is by altering the current position that an operator displaying a character specifies where the next character on the text line should usually lie. The following operators change the current position.
SetXY: PROC[context: Context, p: Pair];
IntegerSetXY:
PROC[context: Context, x, y:
INTEGER];
Set the current position to p.
Precisely, [cpx, cpy] ← Transform[T, p]
SetXYRel: PROC[context: Context, v: Pair];
IntegerSetXYRel:
PROC[context: Context, x, y:
INTEGER];
Add the relative displacement v to the current position.
Precisely, [cpx, cpy] ← VectorAdd[[cpx, cpy] , TransformVec[T, v]]
SetXRel: PROC[context: Context, x: REAL];
IntegerSetXRel:
PROC[context: Context, x:
INTEGER];
Add a relative displacement in the x direction to the current position.
Equivalent to SetXYRel[context, [x, 0]]
SetYRel: PROC[context: Context, y: REAL];
IntegerSetYRel:
PROC[context: Context, y:
INTEGER];
Add a relative displacement in the y direction to the current position.
Equivalent to SetXYRel[context, [0, y]]
Mask operators
The mask operators are the central focus of the imager, for they determine the shapes of primitive images that are laid down on the image. Mask operators are available to make images of rectangles, line drawings, or filled outlines, and to use a pixel array to specify samples of the mask.
When a mask operator is executed, the page image is altered. The operation of a mask operator is controlled in part by its arguments and in part by imager variables:
— The current transformation, T, transforms the specified mask shape to determine the coordinates of the mask on the image.
— The current color governs the color of the object that will be placed on the image.
— If priorityImportant is TRUE, the priority order of objects laid down is preserved.
— If noImage is TRUE, mask operators will have no effect on the image, although they will have the proper effect on the imager variables.
Shapes are defined geometrically in termes of segments, trajectories, and outlines. A segment is a directed segment of a straight line, cubic curve, or conic section; it has a start point and an end point. A trajectory is a sequence of connected segments; the end point of a segment coincides with the start point ot the next one. A closed trajectory is a trajectory that closes upon itself, that is, the end point of the last segment in the trajectory coincides with the start point of the first segment. An outline is a collection of trajectories; each trajectory in an outline is implicitly closed by a straight-line segment linking the end point of the last segment with the start point of the first segment.
Trajectory:
TYPE =
REF TrajectoryRep;
TrajectoryRep:
TYPE =
PRIVATE
RECORD[
prev: Trajectory, -- trajectory preceding this segment
lp: Pair, -- the last point
variant:
SELECT tag: *
FROM
move => [], -- begin new trajectory at lp
line => [], -- straight line segment, endpoints [prev.lp, lp]
curve => [p1, p2: Pair], -- cubic curve segment, Bezier control points [prev.lp, p1, p2, lp]
conic => [p1: Pair, r: REAL], -- conic section segment; see Full Interpress
ENDCASE
];
Trajectories may be constructed with the following operators. MoveTo creates a new trajectory; LineTo, CurveTo, and ConicTo add a segment and return an extended trajectory. Trajectories are immutable values. A last point (lp) is always associated with a trajectory: it is the end point of the last segment in the trajectory.
LastPoint:
PROC[t: Trajectory]
RETURNS[Pair];
Return t's lp.
MoveTo:
PROC[p: Pair]
RETURNS[Trajectory];
Create a new trajectory whose lp is p.
LineTo:
PROC[t: Trajectory, p: Pair]
RETURNS[Trajectory];
Extend t with a straight line segment from t's lp to p. The lp of the result is p.
LineToX:
PROC[t: Trajectory, x:
REAL]
RETURNS[Trajectory];
Equivalent to LineTo[t, [x, LastPoint[t].y]].
LineToY:
PROC[t: Trajectory, y:
REAL]
RETURNS[Trajectory];
Equivalent to LineTo[t, [LastPoint[t].x, y]].
CurveTo:
PROC[t: Trajectory, p1, p2, p3: Pair]
RETURNS[Trajectory];
Extend t with a cubic curve segment. The segment is a Bezier curve defined by four control points: t's lp, p1, p2, and p3 in order. The lp of the result is p3.
Bezier curves are extremely versatile: any parametric cubic curve can be expressed as a Bezier curve; a simple computation converts coefficients of the parametric cubic equations into Bezier control points. A wide range of curves can be generated by fitting several Bezier curves together so as to preserve continuity at the joints. Note that because the x and y components of a Bezier curve are defined by independent parametric equations, the same curve results whether the curve is first drawn and then transformed into another coordinate system, or the control points are first transformed and then used to draw the Bezier curve defined by them.
ConicTo:
PROC[t: Trajectory, p1, p2: Pair, r:
REAL]
RETURNS[Trajectory];
Extend t with a segment of a conic section.
Let p0=LastPoint[t], and let M be the midpoint of the segment between p0 and p2. The conic starts at p0, ends at p2, and intersects the segment from M to p1 in a point I. The ratio of the length of segment MI to the length of the segment from M to p1 is equal to r. The conic piece is bounded by the triangle with vertices p0, p1, and p2, and is a straight line if r=0, an ellipse if 0<r<1/2, a parabola if r=1/2, or a hyperbola if 1/2<r<1.
ArcTo:
PROC[t: Trajectory, p1, p2: Pair]
RETURNS[Trajectory];
Extend t with a circular arc.
Polygon: TYPE = REF PolygonRep;
PolygonRep:
TYPE =
RECORD[length:
NAT, vertices:
SEQUENCE maxLength:
NAT
OF Pair];
Outline:
TYPE =
REF OutlineRep;
OutlineRep: TYPE = RECORD[list: LIST OF Trajectory];
MakeOutline:
PROC[
LIST
OF Trajectory]
RETURNS[Outline];
The trajectories together form an outline. Each of the trajectories will be closed if necessary.
Trajectories and outlines are given a geometrical interpretation only when they are used as a mask. At this point, the numbers describing the trajectory or outline are interpreted as defining geometry in the master coordinate system, which the imager operators MaskFill and MaskStroke convert to the device coordinate system by applying the current transformation T. Thus the value of T while a trajectory or outline is constructed is ignored; only the value of T when the mask operator is executed is important.
Operations that take an outline argument, such as MaskFill, need to decide which points lie "inside" the outline. If no trajectory in the outline intersects itself or another trajectory, the inside of the outline is unambiguous. In other cases, to decide if a point lies inside an outline, it is necessary to compute the point's winding number. The winding number counts the number of times the point is surrounded by an outline: it is the number of closed trajectories in the outline that are wound clockwise around the point. The imager uses the convention that points with non-zero winding number lie inside the outline. Note that for multi-trajectory outlines, the order in which points on a trajectory are specified is important.
MaskFill:
PROC[context: Context, outline:
REF];
The mask is defined as the region inside the outline obtained by transforming the given outline with the current transformation T. The outline argument may have any of the following types: Outline, LIST OF Trajectory, Trajectory, Polygon.
SetStrokeWidth: PROC[context: Context, strokeWidth: REAL];
SetStrokeEnd:
PROC[context: Context, strokeEnd: StrokeEnd];
defaultStrokeWidth: REAL = nullREAL;
defaultStrokeEnd: StrokeEnd = nil;
MaskStroke:
PROC[context: Context, t: Trajectory,
strokeWidth: REAL ← defaultStrokeWidth, strokeEnd: StrokeEnd ← defaultStrokeEnd];
The trajectory t is first broadened to have uniform width strokeWidth, fitted with the endpoints specified by strokeEnd, then transformed by the current transformation T, and used as a mask to alter the image. Joints between segments of the trajectory are mitered, i.e., sides of the stroke are extended until they meet. Segments of a trajectory meeting at an acute angle will thus generate long, sharp corners. Possible values for strokeEnd are:
square — A butt end is formed after extending the line a distance of half its width in the direction in which the trajectory was pointed at its endpoint.
butt — Ends are simply squared off at the specified endpoint.
round — Ends are capped with a semicircle whose diameter is the same as the line width and whose center coincides with the trajectory endpoint.
If square or butt end geometry is undetermined because the trajectory starts or ends with a segment whose start and end points coincide, an error occurs.
To generate strokes with rounded joints between segments, rather than mitered joints, MaskStroke should be called separately for each segment, using strokeEnd=round.
MaskStrokeClosed:
PROC[context: Context, t: Trajectory,
strokeWidth: REAL ← defaultStrokeWidth];
The trajectory t is first closed, then broadened to have uniform width strokeWidth, then transformed by the current transformation T, and used as a mask to alter the image. All joints between segments are mitered.
MaskVector:
PROC[context: Context, p1, p2: Pair,
strokeWidth: REAL ← defaultStrokeWidth, strokeEnd: StrokeEnd ← defaultStrokeEnd];
Equivalent to MaskStroke[context, MoveTo[p1].LineTo[p2], strokeWidth, strokeEnd].
MaskRectangle: PROC[context: Context, x, y, w, h: REAL];
IntegerMaskRectangle:
PROC[context: Context, x, y, w, h:
INTEGER];
Equivalent to MaskFill[context, MoveTo[[x, y]].LineToX[x+w].LineToY[y+h].LineToX[x]].
Note that the coordinates of the corners are first computed and then transformed to device coordinates using the current value of T; for this reason, the mask on the page image may not be rectangular.
Underlining. Character strings can be underlined by placing a rectangle of appropriate width and height just below the string. The width of the rectangle will be determined by the width of the character string. The position of the underline along the baseline will be determined by the current position, but because of spacing corrections the current position cannot be anticipated accurately when the master is created. The operators StartUnderline and MaskUnderline are provided to help position underlines accurately. They assume a master coordinate system in which the baseline is oriented in the positive x direction.
StartUnderline:
PROC[context: Context];
Remember the x component of the current position as the starting point for an underline.
MaskUnderline: PROC[context: Context, dy, h: REAL];
IntegerMaskUnderline:
PROC[context: Context, dy, h:
INTEGER];
The text starting at the point previously identified by StartUnderline and ending at the current position will be underlined with a rectangle of height h and top a distance dy below the current position. For example, to underline the word Hello, the client might call:
StartUnderline[context]; ShowCharacters[context, "Hello"]; MaskUnderline[context, 4, 1];
MaskPixel:
PROC[context: Context, pa: PixelArray];
Spacing correction
CorrectMask: PROC[context: Context];
CorrectSpace:
PROC[context: Context, v: Pair];
Correct:
PROC[context: Context, body:
PROC];
SetCorrectMeasure: PROC[context: Context, v: Pair];
SetCorrectTolerance:
PROC[context: Context, v: Pair];
SetCorrectShrink: PROC[context: Context, correctShrink: REAL];
Space: PROC[context: Context, x: REAL];
IntegerSpace:
PROC[context: Context, x:
INTEGER];
*** Remaining warts ***
Reset:
PROC[context: Context];
Reset a context to its initial state (same as when it was created).
IntPair: TYPE = ImagerBasic.IntPair;
IntRectangle:
TYPE = ImagerBasic.IntRectangle;
SetView:
PROC[context: Context, box: IntRectangle, halftoneOrigin: IntPair ← [0, 0]];
Sets the View-to-Surface transformation to map the origin in View coordinates to (box.x and box.y) in Surface coordinates, and the View-to-Surface clipper to show the specified box. The halftoneOrigin parameter is for controlling the phase of halftones, and is also expressed in terms of Surface coordinates.
ClipView:
PROC[context: Context, box: IntRectangle, exclude:
BOOL];
For setting up fancier clippers; the box is in Surface coordinates.
DrawBitmap:
PROC[context: Context, base:
LONG
POINTER, raster:
CARDINAL, area: IntRectangle];
MaskBits:
PROC[context: Context, base:
LONG
POINTER, raster:
CARDINAL, tile: IntRectangle, area: IntRectangle];
Use a set of bits beginning at "base" to form a single-bit per pixel tile with dimensions given by "tile". The tile will be used (repetitively if necessary) to color the "area". The tile is filled with bits from "base" as follows:
line: LONG POINTER ← base;
FOR y DECREASING IN [0..tile.h) DO
FOR x IN [0..tile.w) DO
tile[x, y] ← GetBit[line, x];
ENDLOOP;
line ← line+raster;
ENDLOOP;
where GetBit[line, i] gets the ith bit after line.
MoveSurfaceRectangle:
PROC [context: Context, source: IntRectangle, dest: IntPair];
Moves the source rectangle so that its new origin is at dest. Both source and dest are in Surface coordinates. Overlapping source and dest are allowed. Clippers are ignored. Beware of moving images containing halftones, because this may cause misaligned halftone screens. The halftone screens are aligned by SetView, so the window package may move viewers around without problems.
MakeStipple:
PROC[
CARDINAL]
RETURNS[Color];
TestRectangle:
PROC[context: Context, x, y, w, h:
REAL]
RETURNS[Visibility];
GetSurfaceBounds:
PROC[context: Context]
RETURNS[IntRectangle];
For finding out where the usable parts of the Surface are. The results are in Surface coordinates.
GetViewBounds:
PROC[context: Context]
RETURNS[IntRectangle];
Yields the bounding box of the View clipper, in Surface coordinates.
SpecialOp:
PROC[context: Context, op:
ATOM, data:
REF]
RETURNS[
REF];