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@inf.ethz.ch.
- Gfx
- Contexts
- Vector Graphics
- Graphical Attributes
- Text
- Coordinate Systems
- Bitmaps
- 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?
- What are paths?
- What are subpaths and why are they needed?
- How do I specify a path?
- What's the difference between Enter/Exit and Enter0/Exit0?
- What to do if I don't know initial and terminal directions of a closed subpath?
- 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?
- Text
- How do I display a string?
- How can I set the current point manually?
- 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?
- Bitmaps
- Why can't I just draw Oberon pictures?
- What are bitmap formats?
- Can I define my own bitmap formats?
- What is a blend operator?
- Can I define custom blend operators?
- What is a filter?
- And I can define custom filters, too?
- I'd still like to use existing Oberon pictures. How?
- And while we're at it, how about Oberon patterns?
- How can I integrate my images in XYZ format into Gfx?
- 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?
- How can I contribute to Gfx?
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.
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 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.
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 that, when interpreted
by a Postscript interpreter, draw what has been rendered with the context.
VAR ctxt: GfxDisplay.Context;
NEW(ctxt); GfxDisplay.Init(ctxt, llx, lly, urx, ury);
VAR ctxt: GfxBuffer.Context; map: GfxMaps.Map;
NEW(map); GfxMaps.Create(map, width, height, format);
NEW(ctxt); GfxBuffer.Init(ctxt, map);
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.
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 morph a path into something else.
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.Enter0(ctxt, x0, y0);
Gfx.LineTo(ctxt, x1, y1);
Gfx.Exit0(ctxt);
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, appending curve elements to it and exiting it again. 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.
The former are needed for properly rendering closed paths. They each expect an additional
direction vector (dx, dy). When rendering closed paths (especially when rendering
thick lines), supply the direction of the last subpath curve just before it gets to the entry/exit
point to Enter and the direction of the first subpath curve just after the
entry/exit point to Exit. As an example, let's render a unit square:
Gfx.Begin(ctxt, {Gfx.Stroke});
Gfx.Enter(ctxt, 0, 0, 0, -1);
Gfx.LineTo(ctxt, 1, 0); Gfx.LineTo(ctxt, 1, 1); Gfx.LineTo(ctxt, 0, 1); Gfx.LineTo(ctxt, 0, 0);
Gfx.Exit(ctxt, 0, 0, 1, 0);
Gfx.End(ctxt);
Because the last line goes from (0, 1) to (0, 0), its direction is
(0, -1), which we pass to Enter. Similarly, the first
line goes from (0, 0) to (1, 0), therefore we pass its direction
(1, 0) to Exit. Specifying closed subpaths in this
manner makes sure that there are no artifacts caused by missing join styles at the
shared entry/exit point.
As you might already have guessed, Enter0 and
Exit0 are just abbreviated calls which set (dx, dy)
to (0, 0), which indicates to Gfx that the current subpath is not closed.
You'll find that most of the time it's rather easy to figure out the direction vectors needed
for Enter and Exit. If for once it isn't, you might try the
following pattern:
Gfx.Begin(ctxt, {Gfx.Record});
Gfx.Enter0(ctxt, x, y); .... ; Gfx.Exit0(ctxt, x, y);
Gfx.End(ctxt);
GfxPaths.Close(ctxt.path);
Gfx.Render(ctxt, {...});
What this does is record the path without direction vectors (as if it weren't closed) and
then calling GfxPaths.Close, which automatically sets the correct
directions at subpath ends if a subpath has a common entry/exit point. Then you can render
the (now hopefully closed) path in the mode you originally wanted to render it.
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.Enter(ctxt, -2, 0, 0, 1);
Gfx.ArcTo(ctxt, 2, 0, 0, 0, -2, 0, 0, 1);
Gfx.Exit(ctxt, 0, -1);
Example 3: a circular arc on the unit circle from 45° to 135°:
Gfx.Enter(ctxt, 0.7071, 0.7071, -1, 1);
Gfx.ArcTo(ctxt, -0.7071, 0.7071, 0, 0, 1, 0, 0, 1);
Gfx.Exit(ctxt, -1, -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.
- If the directions at shared entry/exit points of closed subpaths are unknown,
calling GfxPaths.Close sets them to their correct values.
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 as a Gfx 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 bitmap 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 (not the color associated
with the bitmap). 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 map: GfxMaps.Map; pat: Gfx.Pattern;
NEW(map); GfxPictures.PatternToMap(2, map);
pat := Gfx.NewPattern(ctxt, map, 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 numbers. The first number of each pair
holds the length of a visible part (a dash), whereas the second holds the distance
from the end of the current dash to the start of the next dash. 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 p: ARRAY 4 OF REAL;
p[0] := 4; p[1] := 2; p[2] := 1; p[3] := 2;
Gfx.SetDashPattern(ctxt, p, 4, 0);
Here follows a simple dash pattern that starts in the middle of a dash:
VAR p: ARRAY 2 OF REAL;
p[0] := 10; p[1] := 10;
Gfx.SetDashPattern(ctxt, p, 2, 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 device pixel, 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). The question is now:
what happens at the entry and exit points of a subpath? Answer: a line cap style is rendered.
And what happens when there is a corner in a subpath because two consecutive lines have
different directions? Answer: a line join style is drawn.
Use Gfx.SetCapStyle to set the current cap style. The current cap
style is a procedure variable in the context that has a somewhat complicated interface.
Fortunately, the most common kinds of cap style are already predefined 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. If you need to define
a custom cap style, refer to the interface of module Gfx for details.
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.
As with cap styles you may define your own line join styles. However, a detailed description
of the join style interface is beyond the scope of this document. You will gain further
insight by studying the definition of Gfx.JoinStyle and the default
implementations of the predefined styles.
Except for a few special cases, rendering 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 closed, or any moment inbetween. It is obviously impossible to synchronize changing
attributes and output operations. Context attributes therefore mustn't be modified during
rendering.
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.
If you should ever need to set the current point manually, call
Gfx.SetPoint(ctxt, x, y);
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-BoldItalic"
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));
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 outline
fonts for Oberon metafonts
here
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 ater a path has been
begun. 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 map: GfxMaps.Map; done: BOOLEAN;
NEW(map); GfxMaps.Load(map, "Bart.Pict", done); ASSERT(done, 110);
Gfx.SetFillPattern(ctxt, Gfx.NewPattern(ctxt, map, 0, 0));
Gfx.SetFontName(ctxt, "Oberon-Bold", 64);
Gfx.Begin(ctxt, {Gfx.Fill});
Gfx.ShowAt(ctxt, 100, 100, "Bart");
Gfx.End(ctxt);
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.
Yes, if you have installed the OpenType type for Oberon available for download
here
and compile the module GfxOType in the Gfx distribution.
Also make sure that the following line is part of your Oberon.Text:
{ FontFormats = { TTF = 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
In the lower left corner of the drawable area. This is often equal to (0, 0).
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 dots per 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 itself to make special
cases more efficient.
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.
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 can be found by resetting the current transformation
matrix, inverting the default CTM and applying it to a unit vector.
Bitmaps
For a long time, Gfx has indeed been working with regular Oberon pictures. However,
they only support indexed bitmap formats with a depth of eight, which proved to be
too restrictive, especially in the context of filtered bitmap transformations. So now
there is a module GfxMaps, which is part of Gfx and offers
a lot of advanced features.
Gfx bitmaps are not restricted to indexed formats. They can include components
holding RGB information, palette index numbers, and alpha values. The number of bits
used per pixel may vary as well. All information about what is stored in a pixel
and how it is stored is gathered in a bitmap format. Several bitmap
formats are predefined and are exported as global format variables from
GfxMaps:
- A1 stores exactly one bit of alpha information per pixel.
This corresponds to the patterns used for Oberon raster fonts.
- A8 stores 8 bits of alpha information per pixel, i.e. each
pixel stores how opaque it is with 0 being fully transparent and 255 being fully
opaque.
- I8 corresponds to the format used for Oberon pictures.
Each byte represents one pixel and its value is used as an index into a color lookup
table (also called palette).
- BGR555 and BGR565 are
so called hi-color formats, using two bytes per pixel. Within a pixel
blue, green, and red values are stored using 5 bits each or 5 bits for blue and red
and 6 for green.
- BGR24 occupies three bytes per pixel, the first of which
holds the blue component, the second the green component, and the third the red
component.
- BGRA32 is similar to BGR24
but occupies four bytes per pixel, the fourth byte being used to store an alpha
value for each pixel.
- PixelFormat is a special format which is compatible
with the Pixel type also exported from GfxMaps.
Pixel values are used when accessing or modifying bitmaps.
It is strongly suggested that you use one of these predefined formats whenever
you create a bitmap with GfxMaps.Create.
Answer: yes, you can, if you are willing to accept a small performance hit. Apart
from information about which components are available and how many bits per
pixel are used, a format also contains two procedures pack
and unpack. They are supposed to store (pack) a pixel at
a given address and to load (unpack) a pixel when called. Call
GfxMaps.InitFormat to initialize all relevant fields of
your custom format. By the way, the performance hit comes from the fact
that the pack and unpack procedures are often bypassed for pixel transfers
between predefined formats, which cannot be done for formats defined
outside GfxMaps.
A blend operator is a generalization of the mode parameter used by many Oberon
Display procedures. It defines how source pixels affect destination pixels. Two
very often used blend operators are exported as global variables from
GfxMaps and should be used wherever appropriate:
- With GfxMaps.SrcCopy, source pixels just replace
whatever was stored in the destination pixels before. This corresponds to
the replace mode in the Oberon Display module.
- With GfxMaps.SrcAlpha, the alpha component
of the source pixel defines how much the source pixel affects the destination
pixel. If the source pixel is transparent (alpha=0), the destination pixel remains
unaffected. If the source pixel is opaque (alpha=255), it fully replaces the
destination pixel. Otherwise, source and destination pixels are blended
according to the alpha value of the source pixel.
Yes! Just declare a variable of type GfxMaps.BlendOp
and set its blend procedure to a procedure implementing your blending
scheme. The blend procedure gets a source and a destination pixel and
must store its result in a result parameter. However, because
GfxMaps uses optimized code for its predefined
blend operators which bypasses calling the blend procedure, you may
find that using your custom blend operator is slower than using one
of the builtin operators.
A filter is an extension of a blend operator. In addition to blending source
and destination pixels, it offers procedure variables for shifting and
scaling pixel rows and columns, which makes them suitable to implement
bitmap transformations. The problem with bitmap transformations is that
fast algorithms normally suffer from poor quality and that smarter algorithms
producing better quality are much slower. Filters allow GfxMaps
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 tendency to blur its result).
Yes. Call GfxMaps.InitFilter with a blend procedure 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 a Gfx map
using the following code:
VAR map: GfxMaps.Map; done: BOOLEAN;
NEW(map); GfxMaps.Load(map, "MyImage.Pict", done);
IF done THEN ... END;
Note: this only works if your Oberon.Text has been augmented by a line
{ ImageFormats = { Pict = GfxPictures.Install }}
Alternatively, if the picture is already available as a structure in memory, it can be
converted to a Gfx bitmap like this:
VAR map: GfxMaps.Map;
NEW(map); GfxPictures.PictToMap(pict, map);
Bear in mind that the picture contents are copied. If you later need to convert the
map back to a picture, you can do so with GfxPictures.MapToPict.
Converting a Gfx map to a picture requires an additional palette parameter to
select the set of colors that will be used in the picture. If the map is in
GfxMaps.I8 format (e.g. because it is a converted picture),
you should probably use the palette from the map format. If the picture
needs to be displayed, consider using GfxMaps.DefaultPal
which contains exactly the same colors as the Oberon display. Finally, if the
source bitmap is in a hi-color or true-color format, you might want to calculate
a palette that best represents the colors used in the bitmap with
GfxMaps.ComputePalette and use this palette for
the conversion.
Studying the definition of GfxPictures reveals a procedure
PatternToMap which converts an Oberon pattern to a
Gfx map in GfxMaps.A1 format. The conversion from
Gfx bitmaps to Oberon patterns is not supported.
Since Gfx bitmaps support so many different formats, it would be nice to be able
to load and store image files in all the dozens of file formats that exist. Currently
only Oberon picture files are supported, but it's easy to add other file formats if
you follow these steps:
- Implement procedures for loading and storing a bitmap in the new file format.
- Add a command procedure (e.g. "Install") to your module which sets the
global procedure variables GfxMaps.LoadProc and
GfxMaps.StoreProc to your load and store procedures.
- Add a line for installing your command to your Oberon text:
ImageFormats = {
Pict = GfxPictures.Install
Xyz = GfxXyz.Install
}
Clipping
This depends on the context type but usually the clipping region is a rectangle around
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:
Gfx.SaveClip(ctxt);
Gfx.Begin(ctxt, {Gfx.Clip}); ... ; Gfx.End(ctxt);
....
Gfx.RestoreClip(ctxt);
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
Gfx.Record mode must have been set 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 althouth 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. Other than 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 the
Institute for Computer Systems
at ETH Zürich.
Directly to the author of Gfx, Erich Oswald.
- Send a mail to oswald@inf.ethz.ch
and tell me what you think.
- Use it in a project and send bug reports and suggestions to
oswald@inf.ethz.ch.
- Implement a bitmap import/export module for GIF, JPG, PNG, TGA, TIFF, PIC, BMP
or whatever else excites you and make it available.
- Implement a Type1 font rasterizer and make it available.
- Make the world a better place.
Erich Oswald Apr 1999