The Gfx Howto
This document describes typical usage patterns of the Gfx library, organized as a hierarchical
sets of questions and answers. Its goal is to serve as an easily accessable resource on how to
use Gfx in practice. I've tried to come up with questions (and answers) that might arise in
everyday life. If you cannot find your question or a satisfying answer, suggest an update to
oswald@acm.org.
- Gfx
- Contexts
- Vector Graphics
- Graphical Attributes
- Text
- Coordinate Systems
- Images
- Clipping
- Paths
- What next?
- Gfx
- What is Gfx?
- Who uses Gfx?
- What can I use it for?
- Where do I get Gfx?
- Does Gfx require any other packages or files?
- Is there any example code available?
- Contexts
- What is a context?
- What kinds of contexts do exist?
- How do I create a context for drawing to the screen?
- How do I create a context for drawing to a bitmap?
- How do I create a context for printing my graphics?
- How do I create Postscript and EPS files?
- Vector Graphics
- How do I draw a line?
- How do I draw a rectangle?
- How do I draw circles and ellipses?
- How do I draw an arc?
- What are paths?
- What are subpaths and why are they needed?
- How do I specify a path?
- How do I render closed paths correcly?
- What are Enter and Exit for?
- Why do arcs have so many parameters?
- How are self-intersecting paths treated?
- What path modes do exist?
- What can I use recorded paths for?
- Graphical Attributes
- How do I set the current stroke and fill colors?
- How do I change the current stroke and fill patterns?
- How do I draw dashed curves?
- How do I change the current line width?
- How do I change the current line cap style?
- How do I change the current line join style?
- How do I control the quality of arc and bezier approximation?
- Why doesn't Gfx let me change attributes within a path?
- How do I preserve all attributes and restore them later?
- Text
- How do I display a string?
- How do I change the current font?
- How do I change the current text color?
- Can I convert characters to paths?
- How can I center a caption?
- Can I use my huge collection of TrueType fonts?
- How about my Type1 fonts?
- Coordinate Systems
- Where is the coordinate origin of a context?
- How can I move the current coordinate origin?
- What is the default unit of a context?
- How do I scale the current coordinate system?
- How do I rotate the current coordinate system?
- Can I specify other transformations to the current coordinate system?
- How can I undo the changes I've made to the current coordinate system?
- How can I return to the original coordinate system?
- What are the dimensions of a device pixel?
- Images
- Why can't I just draw Oberon pictures?
- What is the Images package? Where do I get it?
- How do I draw an image?
- What is a filter?
- Can I define custom filters?
- I'd still like to use existing Oberon pictures. How?
- And while we're at it, how about Oberon patterns?
- Clipping
- What is the initial clipping region of a context?
- How do I change a context's default clipping region?
- How do I convert a gadget mask to a clipping region?
- How can I change the current clipping region?
- How can I undo the changes I've made to the current clipping region?
- How can I return the the clipping region to its initial state?
- Paths
- How can I access the elements in the current path?
- How to calculate bounding box and length of path?
- How can I modify a recorded path?
- How to render an edited path?
- What next?
- Are there other sources of information?
- Who do I report bugs and suggestions to?
Gfx
Gfx is a set of modules for rendering two-dimensional graphics using high-level operations. Applications
use procedures operating on a context structure to describe the shapes that they wish to draw.
The context fulfills these requests e.g. by modifying pixels on a raster display or by appending text to a
page description that is being gathered in a file. Gfx currently runs on Oberon System 3.
The first application to use Gfx was Leonardo, the graphics editor provided as part of the System 3
distribution. In fact, Gfx was developed as an integral part of Leonardo but is now also available to
other applications.
Another project using Gfx is a PDF viewer that has been developed by Marcel Bösiger during
a semester project. Kaspar Fischer should be
finishing his dvi file viewer in spring 2000.
Producing graphical output in a device independent manner. Drawing complex vector graphics. Drawing
scaled and rotated bitmaps and text. Gfx should be suitable for producing any non-trivial graphical output.
Since Leonardo is part of all System 3 distributions, you might already have Gfx installed. The most current
release is available at the ETH FTP server.
Gfx requires that the
Images package is already installed (which used to be a part of Gfx in earlier
releases). Apart from that, Gfx runs on top of the basic Oberon system.
However, you might consider installing the
Metafont packages containing outlines for Oberon fonts and the
OpenType package for Oberon System 3 as well to take advantage of all
features provided by Gfx.
The Gfx distribution contains two modules which will usually never be imported by clients.
- GfxTest contains a set of commands which use most features of Gfx
and produce output on a variety of different contexts.
- GfxDemo demonstrates how to use Gfx within the System 3 display
hierarchy.
Contexts
When speaking about Gfx, a context is an abstract heap object of type Gfx.Context.
Contexts maintain a set of state variables and a set of methods for changing these state variables
and for rendering graphical shapes. To get some physical output you need to instance a concrete
extension of Gfx.Context.
The current distribution includes the following concrete context types:
- GfxDisplay.Context renders graphics on the Oberon display.
- GfxBuffer.Context renders graphics into a Gfx bitmap.
- GfxPrinter.Context renders graphics on the Oberon printer.
- GfxPS.Context creates Postscript and EPS files which, when interpreted
by a Postscript interpreter, draw what has been rendered through the context.
VAR ctxt: GfxDisplay.Context;
NEW(ctxt); GfxDisplay.Init(ctxt, llx, lly, urx, ury);
VAR ctxt: GfxBuffer.Context; img: Images.Image;
NEW(img); Images.Create(img, width, height, Images.DisplayFormat);
NEW(ctxt); GfxBuffer.Init(ctxt, img);
VAR ctxt: GfxPrinter.Context;
Printer.Open(...);
NEW(ctxt); GfxPrinter.Init(ctxt);
VAR ctxt: GfxPS.Context;
NEW(ctxt);
(* Postscript file: *)
GfxPS.Init(ctxt, FALSE, FALSE, GfxPS.A4W, GfxPS.A4H, border, border, border, border, 600);
GfxPS.Open(ctxt, Files.New("out.ps"));
....
GfxPS.ShowPage(ctxt);
GfxPS.Close(ctxt)
(* EPS file: *)
GfxPS.InitEPS(ctxt, FALSE, 600);
GfxPS.Open(ctxt, Files.New("out.eps"));
...
GfxPS.Close(ctxt)
Vector Graphics
The easiest way to stroke a line from (x0, y0) to (x1, y1) is
Gfx.DrawLine(ctxt, x0, y0, x1, y1, {Gfx.Stroke})
To stroke a rectangle, call
Gfx.DrawRect(ctxt, x0, y0, x1, y1, {Gfx.Stroke})
with (x0, y0) and (x1, y1) marking two opposite corners of the rectangle.
For drawing a filled rectangle, set the mode parameter to Gfx.Fill.
You can include both Gfx.Stroke and Gfx.Fill in
the mode parameter, in which case the rectangle will be first filled and then outlined.
To stroke a circle or an axis-aligned ellipse, call one of
Gfx.DrawCircle(ctxt, x, y, r, {Gfx.Stroke})
Gfx.DrawEllipse(ctxt, x, y, rx, ry, {Gfx.Stroke})
where the center of the circle or ellipse is at (x, y), the circle radius is r
and the ellipse half-radii are rx and ry. If you want the circle or ellipse
to be filled then include Gfx.Fill in the mode parameter.
Use
Gfx.DrawArc(ctxt, x, y, rx, ry, start, end, {Gfx.Stroke})
(x, y) determines the center of an ellipse, rx is its horizontal radius,
and ry its vertical radius (if both are equal, you get a circular arc). start
and end are the angles of the start and end point of the arc, measured counterclockwise
from the positive x axis in radians.
Paths are generalizations of primitive shapes such as lines, rectangles, circles, and ellipses. With paths
you can describe triangles, rings, etc. A path consists of any number of subpaths, each of which in turn
consisting of a connected sequence of lines, elliptical arcs and cubic Bézier curves. You can stroke
a path, fill its interior, or do both at the same time. You can even restrict future rendering operations
to the interior of a path or visit a recorded path to construct a new path.
A subpath is a connected sequence of lines, elliptical arcs and cubic Bézier curves. It starts at
one point (its entry point) and ends at another (its exit point). Entry and exit point may of course be
coincident. Although most paths consists of exactly one subpath, shapes that include holes can only
be specified by several subpaths. E.g. a ring is most easily specified by a subpath describing an outer
ring (consisting of a 360° counter-clockwise arc) and another subpath describing an inner ring
(consisting of a 360° clockwise arc with smaller radius).
A line could be rendered using
Gfx.Begin(ctxt, {Gfx.Stroke});
Gfx.MoveTo(ctxt, x0, y0);
Gfx.LineTo(ctxt, x1, y1);
Gfx.End(ctxt);
It's important to know that first you have to begin a current path in the rendering mode of your
choice (in our case to stroke the path). Then you can repeatedly render subpaths by entering a
subpath (using Gfx.MoveTo) and appending curve elements to it.
The end point of each curve element becomes the new current point of the path and serves
as the starting point of the next element. Don't forget to end the current path or some
pending draw operations might never be executed.
Rendering e.g. a triangle is best done like this:
Gfx.Begin(ctxt, {Gfx.Fill, Gfx.Stroke});
Gfx.MoveTo(ctxt, 0, 0);
Gfx.LineTo(ctxt, 100, 0);
Gfx.LineTo(ctxt, 50, 70);
Gfx.Close(ctxt);
Gfx.End(ctxt);
The directive to close the path has two purposes: first, it appends a line connecting the
current point with the subpath's first point if necessary. Second, it renders a line
join at the common first/last point instead of two line caps. This guarantees a
smooth transition at that point.
Enter and Exit provide an alternative
method for rendering closed paths. Enter is similar to
MoveTo but expects an additional vector argument
which indicates the direction of the last line segment in the subpath (which
must therefore be known in advance). Instead of closing the subpath, it is
terminated with Exit. The vector argument in this
case is the direction of the first line. The triangle from the above example
could therefore also be specified like this:
Gfx.Begin(ctxt, {Gfx.Fill, Gfx.Stroke});
Gfx.Enter(ctxt, 0, 0, -50, -70);
Gfx.LineTo(ctxt, 100, 0);
Gfx.LineTo(ctxt, 50, 70);
Gfx.LineTo(ctxt, 0, 0);
Gfx.Exit(ctxt, 100, 0);
Gfx.End(ctxt);
The motivation for having Enter and Exit
is that they allow to exit a closed subpath at some arbitrary point, render something
else, and re-enter the subpath at the same point later. This additional flexibility is
very convenient for some special applications.
The huge number of parameters for elliptic arcs might be scary at first but you'll find that
they offer a maximum of flexibility. The three points (x0, y0), (x1, y1) and
(x2, y2) define an ellipse having its center at (x0, y0). The other
two points are endpoints of so called conjugate diameter pairs. Assuming
(x0, y0) = (0, 0), if (x1, y1) = (A sin(t), B sin(t + d))
then (x2, y2) = (A cos(t), B cos(t + d)).
Another (maybe a bit more comprehensive) way of explaining conjugate diameter pairs
is to imagine a parallelogram centered at (x0, y0) and spanned by the vectors
from (x0, y0) to (x1, y1) and (x2, y2). The ellipse fits
into the parallelogram and touches it in the middle of its sides, two of which are located
at (x1, y1) and (x2, y2)..
If the current point of the path is not on the ellipse, a line from the current point to its
projection onto the ellipse boundary is rendered. From the projection of the current
point on the ellipse boundary, the ellipse is traversed until the projection of the end
coordinates onto the ellipse is reached. If the end point does not lie on the ellipse, a
line is drawn from the current point on the ellipse to the end point.
If the cross product of the conjugate diameter pair vectors is positive, the arc is traversed
in clockwise order, if it is negative, the traversal is in counter-clockwise order.
Example 1: a unit circle
Gfx.Enter(ctxt, 1, 0, 0, 1);
Gfx.ArcTo(ctxt, 1, 0, 0, 0, 1, 0, 0, 1);
Gfx.Exit(ctxt, 0, 1);
Example 2: a semi-ellipse from (-2, 0) to (2, 0) through
(0, 1) (note that it has to run counter-clockwise):
Gfx.MoveTo(ctxt, -2, 0);
Gfx.ArcTo(ctxt, 2, 0, 0, 0, -2, 0, 0, 1);
Example 3: a circular arc on the unit circle from 45° to 135°:
Gfx.MoveTo(ctxt, 0.7071, 0.7071);
Gfx.ArcTo(ctxt, -0.7071, 0.7071, 0, 0, 1, 0, 0, 1);
When using self-intersecting paths for filling and clipping, it's not inherently clear
which points are part of the path interior and which aren't. Imagine drawing
a ray originating at a point in question and examine its intersections with the path.
Value each intersection as +1 if the path crosses the ray from right to left and as
-1 if the path crosses the ray from left to right.
- With the non-zero winding rule (used by default), the point is considered inside if
the sum of all intersection values is not zero.
- With the even-odd rule (which is applied if the current drawing mode includes
Gfx.EvenOdd), the point is considered inside if the sum of all
intersection values is odd.
When beginning a new path with Gfx.Begin or when rendering
the current path with Gfx.Render, a mode parameter
describes how the path should be rendered. The drawing mode is a set which contains at
least one of the following elements:
- Gfx.Record requests that the path should be recorded in the
path field of the context.
- Gfx.Stroke requests that the path be stroked using the current
graphical attributes in the context.
- Gfx.Fill requests that the path be filled using the current graphical
attributes in the context.
- Gfx.Clip requests that the current clipping path is to be intersected
with the rendered path, reducing the area which can be drawn to.
- Gfx.EvenOdd activates the even-odd rule for computing the interior
of self-intersecting paths. If Gfx.EvenOdd is omitted, the non-zero
winding rule is used.
- Gfx.InPath is included in the drawing mode when
Gfx.Begin is called and removed when Gfx.End
is called. Never set or clear this yourself.
- Gfx.InSubpath is included in the drawing mode when
Gfx.Enter is called and removed when Gfx.Exit
is called. Never set or clear this yourself.
Since you usually use Gfx because you want to render graphics, it may not be obvious
why you should be content with just recording paths. However, it is sometimes
necessary to modify the current path before calling Gfx.Render
to render it.
- Gfx.Flatten replaces all arcs and Bézier curves in the
path by an approximation using straight lines. The quality of the approximation depends
on the current value of the context's flatness attribute.
- Gfx.Outline replaces the current path by a path that outlines
the current path. If you later fill the outlined path, you get the same result as if you
had stroked the original path. Of course you might have other things in mind about
what to do with the outlined path first. Note: This only works if the current
line width is larger than zero. For line width zero, the current path is replaced by
a series of dashes if a dash pattern is active.
Graphical Attributes
Gfx manages two current colors at once, one for stroking and one for filling paths,
which are both set to black whenever Gfx.Reset is called.
To set the current stroke color to red and the current fill color to white, call
Gfx.SetStrokeColor(ctxt, Gfx.Red);
Gfx.SetFillColor(ctxt, Gfx.White);
For convenience, the following colors are already defined in Gfx: black, white, red,
green, blue, cyan, magenta, yellow and three shades of grey. Of course other colors
may be defined as well, since a color is represented as a record type with three fields
holding values for red, green and blue.
A Gfx pattern is defined by an image bitmap and a pin point for anchoring the pattern origin.
A pattern must be instantiated with Gfx.NewPattern, please don't
initialize patterns yourself. After a pattern has been instantiated, you can pass it to
Gfx.SetStrokePattern or Gfx.SetFillPattern.
To turn pattern stroking and filling off again, pass a NIL value (which is also the default
value for both patterns).
If the image used for defining the pattern doesn't contain any color components (i.e.
is a pure alpha bitmap), the current stroke and fill colors are used. Patterns do not
undergo any transformations, they are always drawn
in the default coordinate system.
The following example shows how to fill a rectangle in green using the standard Oberon
pattern 2 (consisting of many small dots):
VAR img: Images.Image; pat: Gfx.Pattern;
NEW(img); PictImages.PatternToImage(2, img);
pat := Gfx.NewPattern(ctxt, img, 0, 0);
Gfx.SetFillColor(ctxt, Gfx.Green);
Gfx.SetFillPattern(ctxt, pat);
Gfx.DrawRect(ctxt, 10, 10, 80, 60, {Gfx.Fill});
All curves are rendered in one continuous stroke by default. You can change this
by passing a dash pattern to Gfx.SetDashPattern. A dash
pattern consists of several pairs of lengths. For each pair i, a dash of
length on[i] is rendered, then a distance of off[i] is
skipped. When i reaches the size of the pattern, it is reset to zero.
Another parameter
(called the dash phase) is used as an initial offset into the pattern at the entry point
of a subpath, which can be useful for slightly adjusting an already defined pattern.
Let's define a simple dash-dot pattern:
VAR on, off: ARRAY 2 OF REAL;
on[0] := 4; off[0] := 2; on[1] := 1; off[1] := 2;
Gfx.SetDashPattern(ctxt, on, off, 2, 0);
Here follows a simple dash pattern that starts in the middle of a dash:
VAR onoff: ARRAY 2 OF REAL;
onoff[0] := 10;
Gfx.SetDashPattern(ctxt, onoff, onoff, 1, 5);
Dash lengths and phase are in current user coordinates, i.e. they are subject to
the current transformation.
Use Gfx.SetLineWidth to set the line width to any positive real
number. The current transformation at the time a path is begun is applied to the line
width before any rendering. If the resulting width is smaller than a device pixel, a
hairline is drawn, which is the thinnest line that can be rendered on any device.
To render lines as thinly as possible you should therefore set the line width to zero.
However, to achieve consistent line widths on all output devices you should choose
a non-zero value. The default line width is one, which corresponds to one display pixel
in the default coordinate system.
When the current line width exceeds one and a half device pixels, curves are rendered by offsetting
the curve by half the line width from each side of the path and filling the resulting area
(or any equivalent method that a specific implementation chooses). To achieve a smooth
appearance, a line join is drawn wherever two consecutive line segments meet at an
angle. In addition, a line cap is drawn at the first and last point of a subpath, except
for closed subpaths where another line join is drawn.
Use Gfx.SetCapStyle to set the current cap style. There are
three predefined cap styles in Gfx:
- Gfx.ButtCap just cuts the line off at its start/end point along
a straight line that passes through the point and is perpendicular to the curve direction
at that point.
- Gfx.RoundCap ends curves with a semicircle. It's as if the curve
had been drawn with a round brush whose diameter matches the current line width.
- Gfx.SquareCap ends curves with half a square. It's as if the curve
had been drawn with a square brush whose side length matches the current line width.
Contexts use butt caps by default and whenever they have been reset.
Use Gfx.SetJoinStyle. Like with cap styles, Gfx offers a selection
of predefined join styles:
- Gfx.BevelJoin cuts off the outer corner of two lines meeting
in an angle along a straight line. Not very aesthetic.
- Gfx.RoundJoin replaces the corner by a circular arc that
smoothly joins the outer edges of the lines.
- Gfx.MiterJoin makes the outer edges at a corner continue
until they intersect and fills the interior.
Miter joins look natural and can be rendered reasonably quick and are therefore chosen
as default join style. However, if the angle between two lines becomes very small, the
intersection of their outer edges can be very far away from the original path. It would
be better to use a threshold angle and render bevel joins in these cases. For this reason,
contexts maintain a style limit attribute. The maximal distance between
any point rendered by a style and the original path must not exceed half the line width
times the style limit. The style limit can be set with Gfx.SetStyleLimit
and is by default set to 5.
Except for a few special cases, rendering thick and/or dashed arcs and Bézier
curves is very complicated.
Most concrete contexts therefore choose to approximate these curves with straight lines,
which are far easier to handle. The curves are usually subdivided until each part is close
enough to a straight line, where "close enough" means that the maximal distance
from any point on the original curve to the linear approximation is smaller than a given
limit. This limit is stored in the flatness attribute of a context and can be
modified with Gfx.SetFlatness. Unlike most other attributes, the
current flatness is measured in device pixels and is independent of the current
transformation matrix. The default flatness is set to one device pixel.
Between Gfx.Begin and Gfx.End, an attempt
to set any context attribute causes a run-time error. Gfx doesn't allow you to change graphical
attributes within a path because most of them depend on the current transformation matrix
(see Coordinate Systems), which can be changed
while inside a path. Now combine this with the fact that a context may render individual
path elements whenever it sees fit, which may be as soon as a curve is specified, when the
path is terminated, or any moment inbetween. It is obviously impossible to synchronize changing
attributes and output operations. Context attributes therefore mustn't be modified during
rendering.
Like this:
VAR state: Gfx.State;
...
Gfx.Save(ctxt, {Gfx.all}, state);
....
Gfx.Restore(ctxt, state);
Instead of saving all attributes, an arbitrary subset can be used. The corresponding elements
are exported as constants from Gfx.
Text
The most convenient way is to call
Gfx.DrawStringAt(ctxt, x, y, "Hello");
You can then immediately append another string to the one you've just displayed with
Gfx.DrawString(ctxt, " World!");
Both procedures will display the string using the current fill color and the current font.
To set the current font family to "Oberon", style to "Bold" at size 12 call
Gfx.SetFontName(ctxt, "Oberon-Bold", 12);
You can also create a font instance which is derived from an existing font by applying
an arbitrary transformation matrix. As an example, let's simulate a "Oberon-BoldSlanted"
font (which doesn't exist) by shearing an "Oberon-Bold" font, using size 20.
GfxMatrix.Init(mat, 1, 0, 0.25, 1, 0, 0); (* shear matrix *)
Gfx.SetFont(ctxt, GfxFonts.Open("Oberon-Bold", 20, mat));
While Gfx does allow you to specify any transformed font, these fonts are likely to look
too ugly to be useful say for general text display. Only if a raster font of the correct size is
available can high quality be expected, otherwise the character patterns of an existing
raster fonts are scaled to the correct size or an outline font (if available) is used to
create character patterns dynamically.
Since Gfx.DrawString uses the current fill color, set the
current text color by calling Gfx.SetFillColor with the
appropriate color value.
Yes, if you have the corresponding outline fonts installed. You can
download outlines for Oberon metafonts.
Once these outline fonts are available on your system, Gfx will use them automatically if you request a
font instance for which no matching Oberon raster font is found or if you explicitly request to
use character outlines inside a path by calling either of Gfx.ShowAt or
Gfx.Show. These work like Gfx.DrawStringAt
and Gfx.DrawString but can only be called within a path.
Instead of always using Gfx.Fill mode, however, they
render characters of a string in the current drawing mode of the path. That means
that you can stroke character outlines, draw patterned characters or use character
shapes as clipping regions.
The following example fills the characters of the string "Bart" with a pattern of "Bart.Pict"
bitmaps:
VAR img: Images.Image; done: BOOLEAN;
NEW(img); Images.Load(img, "Bart.Pict", done); ASSERT(done, 110);
Gfx.SetFillPattern(ctxt, Gfx.NewPattern(ctxt, img, 0, 0));
Gfx.SetFontName(ctxt, "Oberon-Bold", 64);
Gfx.DrawStringAt(ctxt, 100, 100, "Bart");
Use Gfx.GetStringWidth to find out how the current point
would be advanced if a string were drawn and displace the string by half this
vector. If you think it's odd that Gfx.GetStringWidth
returns two values, remember that fonts can be transformed and their
direction of advancement may no longer be horizontal.
Yes, if you have the
OpenType
package for Oberon installed
and compile the module GfxOType in the Gfx distribution.
Also make sure that the following line is part of your Oberon.Text:
{ FontFormats = { GfxOType.Install }}
You should then be able to use TrueType fonts just like regular Oberon fonts from
within Gfx.
Unfortunately there is no support for Type1 fonts in Gfx at the moment. If you have
a working Type1 rasterizer for Oberon, please don't hesitate and make it available
to Gfx and OType.
Coordinate Systems
Usually in the lower left corner of the drawable area.
It is often convenient to move the origin of the coordinate system somewhere else, e.g. if
you plan to describe a graphic that is symmetric around an origin and therefore uses both
positive and negative coordinate values.
Gfx.Translate(ctxt, dx, dy);
displaces the coordinate origin by the vector (dx, dy) for all following
rendering operations. This is effected by prepending a translation matrix to the
current transformation matrix of the context.
The default unit of a context corresponds to an Oberon Display unit, which corresponds
to 1/91.44 of an inch.
By calling one of
Gfx.Scale(ctxt, sx, sy);
Gfx.ScaleAt(ctxt, sx, sy, x, y);
all following operations will use a scaled coordinate system. The second call uses the
given point instead of (0, 0) as an invariant origin for the transformation.
With one of
Gfx.Rotate(ctxt, sin, cos);
Gfx.RotateAt(ctxt, sin, cos, x, y);
E.g. a counter-clockwise rotation by 90° has sin=1 and cos=0.
Gfx expects sine and cosine of an angle and not the value of the angle to avoid calculating
sine and cosine of trivial angles.
Yes, if you can describe the transformation with a three-row, two-column matrix. This includes any combination
of translation, scaling, rotation and shearing. Prepend this matrix to the current transformation matrix with
Gfx.Concat(ctxt, mat);
and it affects all following operations. The GfxMatrix module contains several operations for
dealing with matrices.
By saving the current transformation matrix before modifying the coordinate system and later
restoring it:
VAR save: GfxMatrix.Matrix;
save := ctxt.ctm;
...
Gfx.SetCTM(ctxt, save);
By calling Gfx.ResetCTM or Gfx.Reset, where
the latter also resets the clip region and all context attributes. If you didn't set up the context
yourself, you should rather save and restore the CTM instead because your caller might have
modified the coordinate system before calling you.
The dimensions of a device pixel in the current coordinate system can be found
by inverting the current CTM and applying it to a unit vector.
Images
For a long time, Gfx has indeed been working with regular Oberon pictures. However,
they only support indexed image formats with a depth of eight bits, which proved to be
too restrictive, especially in the context of filtered image transformations. Now Gfx
is based on the Images package, extending it by a module GfxImages
for transforming images.
Images lets you load and store image files of up to 32 bit depth in various file formats
and provides basic image processing capabilities. It can be downloaded from
ftp.inf.ethz.ch. Be sure to read the accompanying Images.Tool and register
image file extensions in Oberon.Text.
Gfx.DrawImageAt(ctxt, x, y, image, GfxImages.NoFilter);
The lower left corner of the image is positioned at the (x, y) and the
current transformation matrix is applied to the image. If the current transformation
has been modified and no longer corresponds to the default coordinate system,
the filter parameter controls the quality of the image transformation.
A filter is an extension of an Images transfer mode. In addition
to compositing source and destination pixels, it offers procedure variables
for shifting and scaling pixel rows and columns, which makes them suitable
for implementing image transformations. The problem with image transformations
is that fast algorithms suffer from poor quality and that smarter algorithms
producing better quality are much slower. Filters allow GfxImages
to leave that decision up to its callers. It offers two sets of filter procedures
and two predefined filters exported as global variables:
- NoFilter does what is known as a box filter
or a nearest neighbor filter. It's still called NoFilter
because its results correspond to what a naive implementation that has
never even heard of filtering would achieve.
- LinearFilter uses bilinear filtering for scaling and
interpolates linearly when shifting by fractional amounts. It's much slower
but also looks better (except for a strong tendency to blur its result).
Yes. Call GfxImages.InitFilter with a compositing operator and appropriate
shift and scale procedures and you have your custom filter. You might want to take a look
at the implementation of the predefined filter procedures first, though.
If you have an Oberon picture stored in a file, you can directly load it into an image
using the following code:
VAR img: Images.Image; done: BOOLEAN;
NEW(img); Images.Load(img, "MyImage.Pict", done);
IF done THEN ... END;
Alternatively, if the picture is already available as a structure in memory, it can be
converted to an image like this:
VAR img: Images.Image;
NEW(img); PictImages.PictToImage(pict, img);
Bear in mind that the picture contents are copied. If you later need to convert the
image back to a picture, you can do so with PictImages.ImageToPict.
Unless the image is in indexed format and has its own custom palette, the
default fixed palette defined in module Images is used for
initializing the picture palette.
Studying the definition of PictImages reveals a procedure
PatternToImage which converts an Oberon pattern to an
image in Images.A1 format. The reverse conversion from
images to patterns is done with PictImages.ImageToPattern.
Clipping
This depends on the context type but usually the clipping region is a rectangle containing
the drawable area of the context.
Many context modules export procedures for setting the default clipping region of
a context to a rectangle or a given region. The default clipping region is the one that
is reestablished when Gfx.ResetClip or Gfx.Reset
is called.
By enumerating the mask and adding the enumerated rectangles to the region:
VAR R: GfxRegions.Region;
PROCEDURE AddRect (x, y, w, h: INTEGER);
BEGIN
GfxRegions.AddRect(R, x, y, x + w, y + h)
END AddRect;
PROCEDURE MaskToRegion (mask: Display3.Mask; reg: GfxRegions.Region);
BEGIN
R := reg; GfxRegions.Clear(R);
Display3.EnumRect(mask, mask.X, mask.Y, mask.W, mask.H, AddRect)
END MaskToRegion;
The current clipping region of a context can be intersected with an arbitrary path by
rendering the path in Gfx.Clip mode. The following code snippet
restricts all following rendering operations to the interior of the letter "A" within a
circle:
Gfx.SetFontName(ctxt, "Oberon-Bold", 128);
Gfx.Begin(ctxt, {Gfx.Clip});
Gfx.ShowAt(ctxt, 100, 100, "A");
Gfx.End(ctxt);
Gfx.DrawCircle(ctxt, 150, 150, 50, {Gfx.Clip});
Save the current clipping region before you modify it and restore it later:
VAR clip: Gfx.ClipArea;
...
clip := Gfx.GetClip(ctxt);
Gfx.Begin(ctxt, {Gfx.Clip}); ... ; Gfx.End(ctxt);
....
Gfx.SetClip(ctxt, clip);
By calling Gfx.RestoreClip.
Paths
The module GfxPaths offers two methods for accessing the elements
stored in a path. The first is to open a path scanner (GfxPaths.Scanner)
on the path and advance from element to element. The second is to enumerate all path
elements by passing an enumerator procedure to GfxPaths.Enumerate.
The procedure is then called for each element in the path. While the enumeration method
offers less control over the traversal, it can be used to visit flattened paths and elements
or natural splines, which cannot be stored in paths at all.
If you access the elements in the current path of a context, bear in mind that the
drawing mode must include Gfx.Record and that the current
context matrix has been applied to all coordinates.
The module GfxPaths offers useful procedures for finding
out details about a path. GfxPaths.GetBox returns the bounding
box for all elements in the path and GfxPaths.Length
returns the length of a (flattened) path.
Due to the internal storage structure of paths, there are no procedures for
altering individual path elements once a path is built although you can apply
a transformation matrix to a path or reverse it. Other modifications can be
achieved by constructing a temporary path and copying back its contents to
the original path afterwards.
Gfx.DrawPath is one possibility. The current path
can be drawn with Gfx.Render.
What next?
Most of the topics touched in this document are presented with a bit more structure
in the Gfx Overview. Apart from that, most
features are also discussed where they are defined in a module interface. For the
deepest insight, you should look directly at the source code, which is part of the
distribution. If all this doesn't help, send inquiries and suggestions to
Erich Oswald.
Directly to the author of Gfx, Erich Oswald.
Erich Oswald Mar 2000