Printing With The RISC OS Printer Drivers

Ben Summers shows how to steer your programs towards better printouts ...

Major programs on most other computers often come with several hundred printer drivers to cater for even the most obscure printer imaginable. This results in a lot of hassle for the user, who must choose an appropriate printer driver with every new program and a lot of work for the programmer who must write code to print with every program he writes - then test it on each printer.

Under RISC OS, the situation is rather different. Acorn cleverly realised that most of the code to print would be the same in each application, so they provided printer drivers as part of the operating system.

This approach has major advantages for both programmers and users. The programmer doesn't have to worry about writing code to print and doesn't have to test it on lots of different printers and the user only has to configure a printer driver once.

Using a RISC OS printer driver may seem a little complex at first but it's really very easy to use. The interface is totally independent of the actual type of printer you're using, so your program doesn't have to notice the difference between a humble dot matrix and a postscript imagesetter - just let the printer driver take care of all those nasty complexities.

When you are printing you draw the page using the same commands as you would to draw on the screen. However, the printer driver intercepts them and prints them out instead of plotting them on the screen.

It's slightly more complex than simply drawing the thing you want to print. The printer driver may request the page to be drawn more than once but it's easy to implement a simple printing routine. The principle is roughly the same as redrawing a window under the Wimp and it's quite easy to share code between redrawing and printing.

The current printer

The printer driver provides several calls to find out about the driver currently in use. The first is PDriver_Info, which returns general information about the current printer driver:

SYS "PDriver_Info" TO type%, x_resolution%, y_resolution%, features%, name$, halftone_x_res%, halftone_y_res%

The x and y resolutions of the printer are in dots per inch. You probably won't need to worry about this or the halftone resolutions (in units of repeats of the dot pattern used for greys per inch).

The most useful information is the name$ and the features%. The former is simply a textual name of the printer. By convention, this is shown in the print dialogue of your application.

The options the printer supports is given by features%. Bit 0 is set if the printer prints in colour, bit 11 is set if it supports transformed (rotated) sprites and bit 12 is set if it supports new font features (such as transforming fonts).

It also contains some other information which is less useful for an application. The full description is on page 3-603 of the PRM.

The other useful SWI is PDriver_PageSize:

SYS "PDriver_PageSize" TO , x_size%, y_size%, left%, bottom%, right%, top%

x_size% and y_size% are the size of the paper, and the last four parameters are the edges of the printable page (most applications show these as grey borders around the edge of the page and anything outside these is clipped). These are all in millipoints, which are 1/1000th of a point or 1/72000th of an inch.

Starting a print job

Before you can start printing you have to go through a setting up process. If you are writing a multitasking program you should use the Printer message protocol (page 3-259 of the PRM) to cooperate successfully with other tasks.

Before doing anything else you must open a file to which the data for the printer is sent. This file is always printer:. The printer manager (Printers under RISC OS 3) sets this so that it either sends the data to some hardware, such as the parallel port, or a file on a disk as required by the user.

While you are testing your program, I suggest you use the print to file option as much as possible to avoid wasting lots of paper.

The handle you obtain by opening the file is then passed to the printer driver. If you're using Basic you can use the command to open the file and start the print job with:

this_job% = OPENOUT("printer:")
SYS "PDriver_SelectJob", this_job%,title$ TO old_job%

old_job% then contains the handle of the job which was current before your printing started:You should keep this so that you can reselect that job afterwards to give maximum compatibility with other programs. title$ should be an informative name of the print job, but it could just be the name of your application.

If you're using C, you can't pass the FILE* returned by fopen() to the printer driver because it's actually a pointer to a structure maintained by the C run time system.

Instead, you have to use OS_Find to open the file. Equivalent C code to the Basic above, using the printing facilities (print.h) of the RISC OS Library, is:

_kernel_swi_regs r;
int this_job, old_job;

r.r[0] = 0x8f;
r.r[1] = (int) "printer:";

if (_kernel_swi(OS_Find, &r, &r) != 0 || (this_job = r.r[0]) == 0)
  /* the file didn't open, so report
     the error and end */

print_selectjob(file_handle, "Print job title", &old_job);

It's a bit longer than for Basic as the method for calling SWIs is rather cumbersome, and errors are handled locally instead of using a global error handler.

When you're printing from a C program, it's a good idea to use the print module of RISCOSLib (page 246 of the Acorn Desktop C manual) as it's more efficient than _kernel_swi().

It's important that you handle errors properly. If the print job is not stopped some very strange things may happen, as the printer driver will still be intercepting graphics commands. In Basic, a suitable error handler for a single tasking program is:

ON ERROR SYS "PDriver_AbortJob",this_job%:CLOSE#this_job%:PRINT REPORT$;" at line ";ERL:END

Next, you must declare the fonts you want to use. This SWI is not supported in old printer drivers, so be prepared for an error to be returned. If it is, stop declaring fonts and continue normally with the print job.

It's important that you declare fonts, even if you declare that you don't actually use any. Some printer drivers, such as the PostScript driver, have to download the fonts if they aren't present in the printer before it can start using them. It can't do this once the print has started due to limitations of PostScript.

You can declare a font using either a font handle or a font name. The call to declare a font is:

SYS "PDriver_DeclareFont", 0, font_name$, flags%

font_name$ is the name of the font you are declaring. Set bit 0 of flags% if you don't want the font to be downloaded even if it isn't present within the printer, and set bit 1 if you're going to use kerning on the font - see the font manager article in the February '94 issue.

After you have declared all your fonts, you should tell the printer driver you have finished:

SYS "PDriver_DeclareFont", 0, 0, 0

Finally, you must tell the printer driver where you want to print on the page by specifying a list of rectangular printing areas, referred to as rectangles.

A simple printout would just declare one rectangle over the whole page, but you can define more than one on a page. You might want to do this if you were printing a two page spread where you could use two rectangles, one for each page on a landscape printout.

The contents of rectangles are clipped to the edge of the rectangle like VDU graphics windows. However, you shouldn't rely on them clipping exactly as rounding errors creep in.

For example, you shouldn't use them as frames in a DTP package. It is suggested you add a margin of about 2 OS units (1 point) around the edge as unintentional clipping tends to be rather noticeable.

To declare a rectangle you use PDriver_GiveRectangle. In the following code, the DIMensioned memory should be claimed in the initialisation of your program:

DIM rectangle% 16, transform% 16, plot% 8

rectangle%!0 = low_x%
rectangle%!4 = low_y%
rectangle%!8 = high_x%
rectangle%!12 = high_y%
transform%!0 = x_scale * 2^16
transform%!4 = 0
transform%!8 = 0
transform%!12 = y_scale * 2^16
plot%!0 = pos_x%
plot%!4 = pos_y%

SYS "PDriver_GiveRectangle", id%, rectangle%, transform%, plot%, col%

id% is a value you allocate, and is used by the printer driver to tell you which rectangle it wants you to draw. rectangle% is the size of the rectangle in OS units (1/180th of an inch), and all or any of the dimensions can be negative so you don't need to have the origin in the bottom left of the rectangle.

This rectangle is scaled by transform%. In this example, x_scale and y_scale control the scaling in the expected directions.

This is a transformation matrix (as discussed in the Draw module article in the March '94 issue), but most printer drivers limit it to scaling. and rotations by multiples of 90 degrees.

As with the Draw module, the parameters are fixed point numbers with 16 integer and 16 fractional bits, in effect a real number multiplied by 2^16.

To print a landscape page, you need to rotate all the rectangles by 90 degrees, and would use the transformation matrix below:

transform%!0 = 0
transform%!4 = y_scale * 2^16
transform%!8 = x_scale * 2^16
transform%!12 = 0

pos_x% and pos_y% are the positions of the bottom left-hand corner of the rectangle (not the origin) in millipoints from the bottom left-hand corner of the page. col% is the colour of the background of the rectangle, in the form &BBGGRR00. White is &FFFFFF00.

The figure below shows the relationship between the rectangle and position coordinates and the position and size of the rectangle on the page.

After this lengthy setting up process, you are ready to draw the rectangles and actually produce some printed output. You start it off with:

DIM to_draw% 16
SYS "PDriver_DrawPage", copies%, to_draw%, page%, 0 TO more%, , id%

copies% is the number of copies of this page to print, page% is the page number, or zero if it's unimportant. After the call, id% is the id of the first rectangle to draw - specified when PDriver_GiveRectangle was called to create that rectangle above - and to_draw% contains the coordinates of the part of the rectangle which is to be redrawn.

to_draw%!0 and to_draw%!4 are the minimum coordinates, while to_draw%!8 and to_draw%!12 are the maximums of this area.

Printing speeds

Simple applications could just draw the entire rectangle but to speed up the process significantly when there are a lot of objects to be drawn, you could test each object and only plot it if it is within this specified area.

more% is non-zero if there are more rectangles to print. You need to perform a ioop to get the next part to redraw until there are no more to be redrawn:

WHILE (more% <> 0)
  IF (id% = 42) THEN
    REM draw rectangle 42
  SYS "PDriver_GetRectangle", , to_draw% TO more%, , id%

Each time around the loop, to_draw% and id% are updated. When you draw the rectangle, the printer driver intercepts most calls which draw graphics on the screen, including the Basic drawing commands. The idea is that you draw things as you would to plot them on the screen, but they appear on the printer instead.

You can plot sprites, draw lines, use the draw module and font manager and it will all appear on the printer instead of on the screen. Previous articles in this series have shown how to use the draw module and the font manager.

However, you mustn't use GCOL commands but use ColourTrans to set colours - see the item at the bottom of this document on how to do this. This allows the printer driver to select the best match available for the colour you actually want.

If you don't use ColourTrans your printout will vary depending on which mode you're in and how the palette is set up, and will certainly not look as good as if you had used ColourTrans.

The rectangle you gave the printer driver behaves as a VDU graphics window (or the whole screen) - anything which you plot outside it is clipped.

The output you draw is scaled by the transformation matrix you supplied to PDriver_GiveRectangle so you can use any units you want.

When the loop has been completed, you can either print another page by giving the printer driver some more rectangles and going through the redraw loop again, or finish the job with:

SYS "PDriver_EndJob", this_job%
IF old_job% <> 0 SYS "PDriver_SelectJob", old_job%, 0

This ends the current job, and if there was a job active before it is reselected to maintain compatibility. Finally, you need to close the file you opened at the beginning of the job. In Basic, you can use the close command provided:

CLOSE# this_job%

However, in C you need to close it by calling OS_Find.

r.r[0] = 0;
r.r[1] = this_job;
if (_kernel_swi(OS_Find, &r, &r) != 0)
  /* error: report and end */

This closes the file and you should use the print module of RISCOSLib to end this job and reselect the previous job, as in the Basic example.

There are several example programs on this month's MegaDisk. Example1 shows how to print a page using Basic drawing commands. Example2 extends this to include a draw module path and a font and also demonstrates clipping of rectangles, and Example3 shows multiple scaled rectangles.

This process may seem complex at first, but it's a lot easier than writing it all yourself. When you try using the printer drivers, you'll find that it's really easy to produce wonderful printouts.

Using ColourTrans while printing

While printing you should always call a ColourTrans SWI to set the required colour. You specify the colour you want using 24-bit colour values and call this SWI to tell the printer driver to select the closest colour currently available:

SYS "ColourTrans_SetGCOL",colour%,,,0,0

colour% is the 24-bit colour, specified in the form &BBGGRR00 (blue * 16777216 + green * 65536 + red * 256, with each colour intensity ranging from 0 to 255).

Click here to download the example files

Source: Acorn Computing August 1994
Publication: Acorn Computing
Contributor: Ben Summers