Imager.mesa
Last edited by:
Doug Wyatt, November 23, 1983 11:31 am
Michael Plass, February 20, 1984 9:15:15 am PST
DIRECTORY
Font USING [FONT],
ImagerBasic USING [ColorRep, IntPair, IntRectangle, nullREAL, Pair, PixelArray, StrokeEnd, Transformation, Visibility, State],
Rope USING [ROPE];
Imager: CEDAR DEFINITIONS
= BEGIN
The imaging model
The Imager provides a rich set of facilities for creating two-dimensional images on a variety of devices. (Though the following discussion often refers to "pages" for convenience, the Imager can be used to create images on any two-dimensional medium.) The Imager allows images to be specified in a way that is independent of any particular imaging device.
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.
Concurrency
Individual Imager contexts are independent objects, so there is never any need to synchronize calls to their procedures. If two processes are to share a single context, they must synchronize their accesses to that context at a level above the context operations; individual context procedures (ConcatT, ShowCharacters, etc.) are not guaranteed to be atomic. The implementor of a context class may use a monitor (probably either a module monitor on the implementation or an object monitor on the instance data) to provide atomicity at this level, but would do so to protect its own integrity, not to synchronize its callers.
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;
Errors
Error: ERROR[errorCode: ErrorCode];
ErrorCode: TYPE = ATOM;
This may be an enumerated type someday.
Creation
Context: TYPE = REF ContextRep;
ContextRep: TYPE = RECORD[class: REF ANY, data: REF ANY, state: ImagerBasic.State];
Create: PROC[deviceType: ATOM, data: REFNIL] RETURNS [Context];
For making a new context.
The data field is optional; its type is dependent on the deviceType.
Reset: PROC[context: Context];
Reset a context to its initial state (same as when it was created). Does not affect the view or the page image.
Imager state
The state of the imager is contained in two places:
(1) the page image itself, which is not directly accessible through the Imager interface, and
(2) the state of two sets of variables: the persistent imager variables and the non-persistent imager variables.
The client may access and modify the imager variables in the record pointed to by context.state. Certain devices (such as the one for creating Interpress masters) do not maintain copies of the imager variables; in this case context.state=NIL, and the variables may be set only via the various procedures defined below. Under normal circumstances, it should not be necessary for the client to access the imager variables directly.
Here are all the imager variables, with their types and initial values:
Persistent:
cpx, cpy: REAL ← 0
correctMX, correctMY: REAL ← 0
Non-persistent:
T: Transformation ← ICStoView
priorityImportant: BOOLFALSE
mediumXSize, mediumYSize: REAL -- in meters --
fieldXMin, fieldYMin: REAL -- in meters --
fieldXMax, fieldYMax: REAL -- in meters --
showVec: Vector ← emptyVector
color: Color ← black
noImage: BOOLFALSE
strokeWidth: REAL ← 0
strokeEnd: StrokeEnd ← square
underlineStart: REAL ← 0
amplifySpace: REAL ← 1
correctPass: [0..2] ← 0
correctShrink: REAL ← 0.5
correctTX, correctTY: REAL ← 0
clipOutline: Outline ← FullField[]
The variables differ in their treatment by the Do-operators below: the non-persistent variables are restored by DoSave; all variables are restored by DoSaveAll.
DoSave: PROC[context: Context, body: PROC];
DoSaveAll: PROC[context: Context, body: PROC];
SetPriorityImportant: PROC[context: Context, priorityImportant: BOOL];
Set the current value of priorityImportant.
A change to the image induced by a mask operator is said to be ordered if priorityImportant is TRUE, and unordered if priorityImportant is FALSE. The rule is that the priority order of all ordered image changes must be preserved; the imager is allowed to alter priority order among unordered changes, or between ordered and unordered changes. Because preserving priority order may require more computation than allowing arbitrary reordering of objects, creators should leave priorityImportant FALSE if possible (this is the default).
Transformations
pointsToMeters: Transformation; -- printer's points, 72.27 to the inch
micasToMeters: Transformation;
MakeT: PROC[a, b, c, d, e, f: REAL] RETURNS[Transformation];
OpenT: PROC[m: Transformation] RETURNS[a, b, c, d, e, f: REAL];
Translate: PROC[x, y: REAL] RETURNS[Transformation];
Scale: PROC[s: REAL] RETURNS[Transformation];
Scale2: PROC[sx, sy: REAL] RETURNS[Transformation];
Rotate: PROC[a: REAL] RETURNS[Transformation];
Concat: PROC[m, n: Transformation] RETURNS[Transformation];
Invert: PROC[m: Transformation] RETURNS[Transformation];
Transform: PROC[m: Transformation, p: Pair] RETURNS[Pair];
TransformVec: PROC[m: Transformation, p: Pair] RETURNS[Pair];
RoundXY: PROC[m: Transformation, p: Pair] RETURNS[Pair];
RoundXYVec: PROC[m: Transformation, p: Pair] RETURNS[Pair];
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]]
Color
The color that will be deposited on the page image is determined by the value of the color variable when a mask operator is invoked. Wherever the mask allows it, the color specified by the imager variable color is deposited on the page image, obliterating any color previously laid down at the same position on the page. There are two ways to specify color: a constant color, and color sampled on a raster. A value of type Color fully specifies a color; a subtype ConstantColor is used for constant colors (ConstantColor widens to Color).
MakeGray: PROC[f: REAL] RETURNS[ConstantColor];
Make a shade of gray specified by the fraction f: 0 means white, 1 means black.
black: ConstantColor; -- = MakeGray[1]
white: ConstantColor; -- = MakeGray[0]
SetColor: PROC[context: Context, color: Color];
Set the value of the color variable.
SetSampledColor: PROC[context: Context, pa: PixelArray, pixelT: Transformation, colorOperator: ATOM ← $Intensity];
SetSampledBlack: PROC[context: Context, pa: PixelArray, pixelT: Transformation, transparent: BOOLEANFALSE];
These operations set a sampled color with a transformation of
Concat[pixelT, context.state.nps.T].
For example, if the current coordinate system were in meters, and the client wished to set a 10-by-10 stipple pattern that repeated every 1 cm, the following would work:
pattern: PACKED ARRAY [0..100) OF [0..1] ← [1, 0, 1, 1, ... 0];
pixelArray: PixelArray;
TRUSTED {pixelArray ← MakePixelArrayFromBits[@pattern, 10, 10, 10]};
SetSampledBlack[context, pixelArray, Scale[0.001]];
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];
MakePixelArrayFromBits: PROC[
bitPointer: LONG POINTER TO PACKED ARRAY [0..0) OF [0..1],
bitsPerLine, samplesPerLine, numberOfLines: NAT
] RETURNS [PixelArray];
Manufactures a one-bit-per-sample/one-sample-per-pixel PixelArray from a bitmap. The bitmap is assumed to be oriented like a screen bitmap, so
bitPointer^[0] is the sample for the upper-left-hand corner,
bitPointer^[samplesPerLine-1] is for the upper-right-hand corner, and
bitPointer^[bitsPerLine*(numberOfLines-1)+samplesPerLine-1] is for the lower-right-hand corner.
MaskPixel: PROC[context: Context, pa: PixelArray];
Clipping
ClipOutline: PROC[context: Context, outline: REF];
ExcludeOutline: PROC[context: Context, outline: REF];
ClipRectangle: PROC[context: Context, x, y, w, h: REAL];
ExcludeRectangle: PROC[context: Context, x, y, w, h: REAL];
IntegerClipRectangle: PROC[context: Context, x, y, w, h: INTEGER];
IntegerExcludeRectangle: PROC[context: Context, x, y, w, h: INTEGER];
Character operators
The most common shapes are those used to make images of characters; these masks are specified in sets of pre-defined operators called fonts.
MakeFont: PROC[name: ROPE, size: REAL] RETURNS[FONT];
SetFont: PROC[context: Context, font: FONT];
ShowChar: PROC[context: Context, char: CHAR, font: FONTNIL];
ShowCharacters: PROC[context: Context, characters: RopeOrRefText, font: FONTNIL,
start: INT ← 0, length: INTLAST[INT]];
SetAmplifySpace: PROC[context: Context, amplifySpace: REAL];
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];
Rounding view coordinates.
DRound: PROC[v: Pair] RETURNS[Pair];
Accessing imager variables (not available on all devices)
GetCP: PROC[context: Context] RETURNS[Pair];
GetCPRounded: PROC[context: Context] RETURNS[Pair];
Special operations (not available on all devices)
MakeStipple: PROC[CARDINAL] RETURNS[Color];
XOR: Color;
TestRectangle: PROC[context: Context, x, y, w, h: REAL] RETURNS[Visibility];
SpecialOp: PROC[context: Context, op: ATOM, data: REF] RETURNS[REF];
NewPage: PROC[context: Context] ~ INLINE {[] ← SpecialOp[context, $NewPage, NIL]};
Property lists.
PutProp: PROC[context: Context, key: REF, value: REF];
GetProp: PROC[context: Context, key: REF] RETURNS [value: REF];
Provides a property-list mechanism. The key-value association is saved and restored by DoSave and DoSaveAll.
Window manager specials (not available on all devices)
IntPair: TYPE = ImagerBasic.IntPair;
IntRectangle: TYPE = ImagerBasic.IntRectangle;
GetSurfaceBounds: PROC[context: Context] RETURNS[IntRectangle];
For finding out where the usable parts of the Surface are. The results are in Surface coordinates.
SetViewOrigin: PROC[context: Context, viewOrigin: IntPair];
Sets the View-to-Surface transformation to map the origin in View coordinates to viewOrigin in Surface coordinates.
GetViewOrigin: PROC[context: Context] RETURNS[viewOrigin: IntPair];
Returns the current view origin, in surface coordinates.
SetViewBox: PROC[context: Context, viewBox: IntRectangle];
Sets the View-to-Surface clipper to show the viewBox, which is specified in View coordinates.
GetViewBox: PROC[context: Context] RETURNS[viewBox: IntRectangle];
Returns the bounding box of the current View-to-Surface clipper, in View coordinates.
ClipView: PROC[context: Context, clipBox: IntRectangle, exclude: BOOL];
For setting up fancier View-to-Surface clippers; the box is in View coordinates.
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 with the view origin, so the window package may move viewers around without problems.
END.