TweetFollow Us on Twitter

Winter 92 - MAKING THE MOST OF COLOR ON 1-BIT DEVICES

MAKING THE MOST OF COLOR ON 1-BIT DEVICES

KONSTANTIN OTHMER AND DANIEL LIPTON

Macintosh developers faced with the dilemma of which platform to develop software for--machines with the original QuickDraw or those with Color QuickDraw--can always choose to write code that runs adequately on the lower-end machines and gives additional functionality when running on the higher-end machines. While this sounds like a simple and elegant solution, it generally requires a great deal of development and testing effort. To make this effort easier and the outcome more satisfying, we offer techniques to save color images and process them for display on 1- bit (black-and-white) devices.


Suppose you're writing a program that controls a 24-bit color scanner and you'd like it to work on all Macintosh computers. The problem you'll run into is that machines with the original QuickDraw (those based on the 68000 microprocessor) only have support for bitmaps, thus severely crippling the potential of your scanner. But don't despair. In our continuing quest to add Color QuickDraw functionality to machines with original QuickDraw, we've worked out techniques to save color images and process them for display, albeit in black and white, on the latter machines. We've also come up with a technique to address the problem of a laser printer's inability to resolve single pixels, which results in distorted image output. This article and the accompanying sample code (on theDeveloper CD Series disc) share these techniques with you.

SAVING COLOR IMAGES

The key to saving color images is using pictures. Recall that a picture (or PICT) in QuickDraw is a transcript of calls to routines that draw something--anything. A PICT created on one Macintosh can be displayed on any other Macintosh (provided the version of system software on the machine doing the displaying is the same as or later than the version on the machine that created the picture). For example, on a Macintosh Plus you can draw a PICT containing an 8-bit image that was created on a Macintosh II. With System 7, you can even display PICTs containing 16-bit and 32-bit pixMaps on machines with original QuickDraw. (Of course, they will only be displayed as 1-bit images there.)

Creating a picture normally requires three steps:

  1. Call OpenPicture to begin picture recording.
  2. Perform the drawing commands you want to record.
  3. Call ClosePicture to end picture recording.
The catch is that the only drawing commands that can be recorded into a picture are those available on the Macintosh on which your application is running. Thus, using this procedure on a machine with original QuickDraw provides no way to save color pixMaps into a picture, since there's no call to draw a pixMap. In other words, you can't create an 8-bit PICT on a Macintosh Plus and see it in color on a Macintosh II. But that's exactly what would make a developer's life easier--the ability to create a PICT containing deep pixMap information on a machine without Color QuickDraw. With this ability, you could capture a color image in its full glory for someone with a Color QuickDraw machine to see, while still being able to display a 1-bit version on a machine with original QuickDraw.

To get around the limitations of the normal procedure, we came up with a routine called CreatePICT2 to manually create a PICT containing color information. Your application can display the picture using DrawPicture. Now, you may be wondering whether creating your own pictures is advisable. After all, Apple frowns on developers who directly modify private data structures, and isn't that what's going on here? To ease your mind, see "But Don't I Need a License to Do This?"

The parameters to CreatePICT2 are similar to those for the QuickDraw bottleneck procedure stdBits. The difference is that CreatePICT2 returns a PicHandle and does not use a maskRgn.

The first thing the routine does is calculate a worst-case memory scenario and allocate that amount of storage. If the memory isn't available, the routine aborts, returning a NIL PicHandle. You could easily extend this routine to spool the picture to disk if the memory is not available, but that's left as an exercise for you. (Hint : Rather than writing out the data inline as is done here, call a function that saves a specified number of bytes in the picture. Have that routine write the data to disk. Essentially, you need an equivalent to the putPicData bottleneck.)

At this point the size of the picture is not known (since there's no way to know how well the pixMap will compress) so we simply skip the picSize field and put out the picture frame. Next is the picHeader. CreatePICT2 creates version $02FF pictures, with a header that has version $FFFF. This version of the header tells QuickDraw to ignore the header data. (OpenCPicture, available originally in 32-Bit QuickDraw version 1.2 and in Color QuickDraw in System 7, still creates version $02FF pictures, but the header version is now $FFFE and contains picture resolution information.)

In addition, the bounds of the clipping region of the current port are put in the picture. Without this, the default clipping region is wide open, and some versions of QuickDraw have trouble drawing pictures with wide-open clipping regions.

Next we put out an opcode--either $98 (PackBitsRect) or $9A (DirectBitsRect), depending on whether the pixMap is indexed or direct. Then the pixMap, srcRect, dstRect, and mode are put in the picture using the (are you ready for this?) PutOutPixMapSrcRectDstRectAndMode routine. Finally, either PutOutPackedDirectPixData or PutOutPackedIndexedPixData is called to put out the pixel data.

There's an important difference between indexed and direct pixMaps here. The baseAddr field is skipped when putting out indexed pixMaps and is set to $000000FF for direct pixMaps. This is done because machines without support for direct pixMaps (opcode $9A) read a word from the picture, skip that many bytes, and continue picture parsing. When such a machine encounters the $000000FF baseAddr, the number of bytes skipped is $0000 and the next opcode is $00FF, which ends the picture playback. A graceful exit from a tough situation.

An interesting fact buried in the PutOutPixMapSrcRectDstRectAndMode routine is the value of packType. All in-memory pixMaps (that aren't in a picture) are assumed to be unpacked. Thus, you can set the packType field to specify the type of packing the pixMap should get when put in a picture. "The Low-Down on Image Compression" (develop Issue 6, page 43) gives details of the different pixMap compression schemes used by QuickDraw. Note that all of QuickDraw's existing packing schemes lose no image quality. QuickTime (the new INIT described in detail in the lead article indevelop Issue 7) adds many new packing methods, most of which sacrifice some image quality to achieve much higher compression. Anyway, these routines support only the default packing formats: 1 (or unpacked) for any pixMap with rowBytes less than 8, 0 for all other indexed pixMaps, and 4 for 32-bit direct pixMaps with rowBytes greater than 8. Note that these routines do not support 16-bit pixMaps.

Finally, the end-of-picture opcode is put out and the handle is resized to the amount actually used.

PicHandle CreatePICT2(PixMap *srcBits, Rect *srcRect, Rect *dstRect,
    short mode)
{
PicHandle   myPic;
short           myRowBytes;
short           *picPtr;
short           iii;
long            handleSize;

#define CLIPSIZE 12
#define PIXMAPRECSIZE 50
#define HEADERSIZE 40
#define MAXCOLORTABLESIZE 256*8+8
#define OPCODEMISCSIZE 2+8+8+2  /* opcode+srcRect+dstRect+mode */
#define ENDOFPICTSIZE 2
#define PICSIZE PIXMAPRECSIZE + HEADERSIZE + MAXCOLORTABLESIZE + \
    ENDOFPICTSIZE + OPCODEMISCSIZE + CLIPSIZE

    myRowBytes = srcBits->rowBytes & 0x3fff;
/* Allocate worst-case memory scenario using PackBits packing. */
    myPic = (PicHandle) NewHandle(PICSIZE + (long)
        ((myRowBytes/127)+2+myRowBytes)*(long)(srcBits->bounds.bottom
        - srcBits->bounds.top));
    if(!myPic)
        return(0);

/* Skip picSize and put out picFrame (10 bytes). */
    picPtr = (short *) (((long)*myPic) + 2);
    *picPtr++ = dstRect->top;
    *picPtr++ = dstRect->left;
    *picPtr++ = dstRect->bottom;
    *picPtr++ = dstRect->right;

/* Put out header (30 bytes). This could be done from a resource or
    taken from an existing picture. */
    *picPtr++ = 0x11;       /* Version opcode. */
    *picPtr++ = 0x2ff;      /* Version number. */
    *picPtr++ = 0xC00;      /* Header opcode. */
    *picPtr++ = 0xFFFF;     /* Put out PICT header version. */
    *picPtr++ = 0xFFFF;
/* The rest of the header is ignored--0 it out. */
    for(iii = 10; iii > 0; iii--)
        *picPtr++ = 0;      /* Write out 20 bytes of 0. */

/* Put out current port's clipping region. */
    *picPtr++ = 0x01;       /* Clipping opcode. */
    *picPtr++ = 0x0A;       /* Clipping region only has */
                            /* bounds rectangle. */
    *picPtr++ = (**thePort->clipRgn).rgnBBox.top;
    *picPtr++ = (**thePort->clipRgn).rgnBBox.left;
    *picPtr++ = (**thePort->clipRgn).rgnBBox.bottom;
    *picPtr++ = (**thePort->clipRgn).rgnBBox.right;
    
    HLock(myPic);
    if(srcBits->pixelType == RGBDirect)
    {           /* Must be 32-bits/pixel */
    /* Put out opcode $9A, DirectBitsRect. */
        *picPtr++ = 0x9A;
        *picPtr++ = 0;  /* BaseAddr for direct pixMaps is 0x000000FF. */
        *picPtr++ = 0xFF;
        PutOutPixMapSrcRectDstRectAndMode(srcBits, &picPtr, srcRect, 
            dstRect, mode);
        if(PutOutPackedDirectPixData(srcBits, &picPtr))
            goto errorExit;     /* Nonzero indicates an error. */
    }
    else
    {
    /* Put out opcode $98, PackBitsRect. */
        *picPtr++ = 0x98;
        PutOutPixMapSrcRectDstRectAndMode(srcBits, &picPtr, srcRect,
            dstRect, mode);
        if(PutOutPackedIndexedPixData(srcBits, &picPtr))
            /* Nonzero indicates an error. */
            goto errorExit;
                            
    }
    HUnlock(myPic);
    
/* All done! Put out end-of-picture opcode, $00FF. */
    *picPtr++ = 0x00FF;

/* Size handle down to the amount actually used. */
    handleSize = (long) picPtr - (long) *myPic;
    SetHandleSize(myPic, handleSize);
    /* Write out picture size. */
    *((short *) *myPic) = (short) handleSize;
    return(myPic);

errorExit:
    DisposHandle(myPic);
    return(0);
}

Just remember that it's not advisable to pass a pixMap you create yourself to a trap. The reason is that although it's unlikely, the format of a pixMap could change (since it's not a persistent data structure, as a picture is); this would then break your application.

The subroutines the CreatePICT2 routine calls as well as some sample code that uses CreatePICT2 are on theDeveloper CD Series disc.

PROCESSING COLOR IMAGES FOR DISPLAY

The remainder of this article focuses on processing color images for display on 1-bit (black-and- white) devices, both monitors and laser printers.

There are many techniques for representing a full-color image on a monitor when color resources are limited. The Picture Utilities Package (new in System 7) offers routines for determining optimal colors to use when displaying a pixMap in a limited color space. For example, if you want to display a 32-bit image on an 8-bit monitor, Picture Utilities can tell you the 256 best colors to use to display the image. The CreatePICT2 routine just described creates a picture that you can legally analyze using the Picture Utilities.

You can also use the techniques of thresholding and of dithering, of which there are three varieties: error diffusion, ordered, and random. Ordered dithering, also known as halftoning, is particularly useful for producing images to be printed on a laser printer. We'll examine each of these techniques in turn.

USING A 50% THRESHOLD
The first technique that leaps to mind when one is faced with displaying a color picture on a 1-bit screen is to convert each color to a luminance and then use a threshold value to determine whether or not to set the corresponding pixel. It turns out that green contributes the most to the luminance and blue contributes the least. Red, green, and blue contribute approximately 30%, 59%, and 11%, respectively, to the luminance. Thus, our formula to convert an RGB value to a luminance becomes

Luminance = (30*RED + 59*GREEN + 11*BLUE)/100

If the resulting luminance is 128 (50% of 256) or greater, the pixel is set to white; otherwise it's set to black. This technique produces the results shown in Figure 1 for gray gradations and a lovely picture of one of the authors. Note that thresholding occurs at the source pixel resolution. Thus, even though the output device used to produce Konenna is 300 dpi, the thresholded picture appears to be 72 dpi. In contrast, the techniques of error-diffusion dithering and halftoning discussed on the following pages occur at the destination device resolution.

The results shown in Figure 1 are far from ideal. The gray gradations end up as a black rectangle beside a white rectangle, and the picture of Konenna, while still cute, is completely devoid of detail.

[IMAGE Othmer-Lipton_rev2.GIF]

[IMAGE OTH/LIP_FIG2_LF.Gif]

Figure 1 Gray Gradations and Konenna Pictured Using 50% Threshold

USING ERROR-DIFFUSION DITHERING
The major problem with the threshold algorithm is that a great deal of information is thrown away. The luminance is calculated as a value between 0 and 255, but the only information we use is whether it's 128 or greater.

An easy fix is to preserve the overall image lightness by maintaining an error term and then passing the error onto neighboring pixels. Both original and Color QuickDraw have dithering algorithms built in for precisely this purpose. (Yes, it's true--while a dither flag cannot be passed explicitly to any original QuickDraw trap, a picture containing a color bit image created using dither mode on a Color QuickDraw machine will dither when drawn with original QuickDraw.) The error is calculated as

Error = Requested Intensity - Closest Available Intensity

For a black-and-white destination, the closest available intensity is either 0 (black) or 255 (white). The requested intensity is the luminance of the current pixel plus some part of the error term of surrounding pixels. Ideally, the error term is spread evenly among all surrounding pixels. But to maintain acceptable performance, QuickDraw uses a shortcut. In original QuickDraw, the error term is pushed to the right on even scan lines and to the left on odd scan lines. Color QuickDraw uses the same technique, except it pushes only half the error to the left or right, and the other half to the pixel immediately below. The result of using this technique in Color QuickDraw at monitor resolution for the two test images is shown in Figure 2.

This form of dithering is normally referred to as error diffusion. That is to say that each pixel is thresholded at 50%, but the error incurred in that process is distributed across the image in some manner, thus minimizing information loss. Error diffusion produces very pleasing results when the device being drawn onto is capable of accurately rendering a single dot at the image resolution. Monitors are quite good at this; laser printers are not. If you want your application's output to look good on a laser printer, a different technique is called for.

USING ORDERED DITHERING (HALFTONING)
There are two kinds of laser printers: write-white and write-black. A write-white printer (such as some of the high-end Linotronic printers that use a photographic process) starts the image out black and uses the laser to turn off pixels. A write-black printer (such as Apple's LaserWriter) starts the image out white and turns on pixels with the laser. Since the pixels are thought of as being square and the laser beam is round,

[IMAGE Othmer/Lipton_rev3.GIF]

[IMAGE OTH-LIP_FIG2_RT.Gif]

Figure 2 Gray Gradations and Konenna Dithered at Monitor Resolution neither process can accurately turn on or off single pixels.

Generally, the circle generated by the laser beam is slightly bigger than the pixel as the computer "sees" it, to guarantee that all space is covered (see Figure 3). The effect of this with a write-black printer is that the black dots tend to be bigger than the individual pixels, causing any 1-bit image drawn at device resolution to appear too dark. The effect with a write-white printer is that the black dots tend to be smaller than the individual pixels, causing any 1-bit image drawn at device resolution to appear too light. If the area of the circle is 20% greater than the individual pixel, the percentage of unwanted toner, or error, for a single pixel is 20%.

[IMAGE Othmer/Lipton_rev4.GIF]

Figure 3 A Laser's Idea of a Square Pixel

Because the error is introduced only at the black/white boundaries, it's reduced when two or more pixels are drawn next to each other. Then the percentage of error is reduced to the perimeter of the pixel group. So in the case where the error for a single pixel is 20%, two pixels drawn next to each other would have only a 15.5% error, and four pixels in a square would have only a 10.25% error in the area covered.

Ordered dithering, or halftoning, minimizes the dot-to-pixel error just described by clumping pixels. Pixels are turned on and off in a specific order in relation to each other and the luminance of the source image. The order can be specified in such a way that clumps of pixels next to each other are turned on as the luminance decreases. This allows us to minimize the effects of the laser printer's dot-to-pixel error. The order is determined by what's known as a dither matrix. (Warning: From here on out, things get deep, so put on your waders. You don't really need to understand all the following to use the sample code we provide.)

About the dither matrix. With a dither matrix, to render intermediate shades of gray or primary colors, we sacrifice spatial resolution for shading--that is, we effectively lower the device's dots-per-inch rating while increasing the number of shades that we can print. For example, if we use a 2x2 cell of 300-dpi dots for every pixel on the page, we've lowered the spatial resolution of the device to 150 dpi but we now have 24 or 16 different patterns to choose from for each one of the pixels. Each pattern has anywhere from 0 to 4 of the 300-dpi dots blackened, or a density between 0 and 100%. In fact, for the 16 possible patterns there are only five possible densities: 0%, 25%, 50%, 75%, and 100%, corresponding to 0, 1, 2, 3, and 4 dots blackened in the cell. The dither matrix determines which five of the possible patterns to use to represent the five possible densities. It's left to you as an exercise to generate these matrixes using the algorithm we provide below. (The sample code on theDeveloper CD Series disc has a commonly useful example.)

If we construct a matrix with the same dimensions as the dot cell that we're going to use (2x2 for the described case) so that the matrix contains the values 25, 50, 75, and 100, we can use this matrix to determine each of the five possible patterns. Each dot in the pattern corresponds to a position in the matrix. To generate a pattern for 50% gray, we turn on all the dots in the pattern with corresponding matrix values less than or equal to 50. The position of the values in the matrix determines the shape of the pattern, as shown in Figure 4.

The dither matrix is used to render an image in much the same way as the 50% threshold described earlier. In fact, that process uses a 1x1 dither matrix whose single element has a value of 50%. The dither matrix is sampled with (x  mod m, y mod n ), where (x, y ) is the device pixel location and (m, n ) is the width and height of the dither matrix.

[IMAGE Othmer/Lipton_rev5.GIF]

Figure 4 A 2x2 Dither Matrix

It turns out that the spatial resolution of the device isn't really reduced by the size of the dither matrix. For regions that are all black, for example, the resolution remains the device resolution. Each pixel in the device is still sampled back to a pixel in the source image.

The basic algorithm for doing an ordered dither of an image onto a page becomes the following:

    For all device pixels x, y:

  • sx, sy  = transform(x, y ) where transform maps device pixel coordinates to source pixel coordinates
  • If sourceLuminance(sx, sy ) > ditherMatrix[x  mod m, y  mod n ], device-dot(x, y ) = black

The code on theDeveloper CD Series disc is an elaboration on this basic algorithm.

As stated before, the position of the various values in the dither matrix determines the patterns that various luminances generate. A general way to specify this order is to use a spot function, as the PostScript interpreter does. If the rectangle of the dither matrix is thought to be a continuous space whose domain is 0-1 in thex  and y  directions,spot-function (x, y ) will return some value that ultimately can be converted into a luminance threshold in the matrix. If the desired pattern is a dot that grows from the center as the luminance decreases (known as a clustered-dot halftone),spot-function (x, y ) is simply the distance from (x, y ) to the center of the cell (0.5, 0.5). The dither matrix would be generated from the spot function as follows:

for i  = 1 tom x  =i /m  for j  = 1 ton y  =j /n  matrix[i, j ] =spot-function (x, y )

The result of this process is that the matrix contains the spot function's results. What we really want in the matrix are threshold values for the luminance. The spot function result is converted as follows: Treating the dither matrix as a one-dimensional arrayA , generate a sort vectorV  such thatA [V [i ]] is sorted as i  goes from 1 tom *n . Then, replacing all of the values inA  withV [i ] * 100/(m *n ) will yield the desired threshold matrix, with each value being a percentage of luminance. (The code uses numbers that are more computer-friendly than percentages.) These percentages assume that the device is capable of accurately rendering a single pixel. The values can be modified by a gamma function to more accurately produce a linear relationship between image luminance and pixel density.

Ordered dithering is generally done at a specific angle and frequency. The frequency is the number of cells (or dither matrixes) per inch and the angle refers to how the produced patterns are oriented with respect to the device grid. In the preceding example, the frequency (if printing on a 300-dpi device) is 150 cells per inch and the angle is 0º.

Because of the way our brains work (our eyes tend to pick up patterns at 90º angles but not at 45º angles), it's desirable to orient these patterns at arbitrary angles. Since the dither matrix itself is never rotated with respect to the device, we must generate the dither matrix in such a way that it contains enough repetitions of the rotated cell to achieve the effect of being rotated itself. In other words, because a square device requires us to "tile" an area with 0º rectangles, we need to find a 0º rectangle enclosing a part of the rotated pattern that forms a repeatable tile. For some angles of rotation, this rectangle may be much larger than the pattern itself.

Suppose we want to halftone to a 300-dpi device at a frequency of 60 cells per inch and an angle of 45º. At 0º, the dither matrix would be 5x5 (300/60), yielding 26 possible shades of gray. However, as Figure 5 illustrates, we need an 8x8 matrix to approximate the desired angle. These dimensions are

[IMAGE Othmer/Lipton_rev6.GIF]

Figure 5 Approximating the Desired Angle

found by rotating the vectors (0, 5) and (5, 0) by 45º and pinning them to integers, yielding the vectors (4, 4) and (-4, 4). Since the magnitude of the vector (4, 4) is 4*sqrt(2), the actual halftone frequency achieved will be 300/(4*sqrt(2)), around 53. The error in frequency and angle is due to the need to pin the vectors to integer space.

Here's the basic algorithm for computing the dither matrix:

  1. The halftone cell is specified by the parallelogram composed of the vectors (x 1, y 1) and (x 2, y 2) and based at (0, 0).
  2. A,  the area of the modified halftone cell, is (x 1*y 2) - (x 2*y 1). For the required dither matrix, the horizontal dimension isA /P  and the vertical dimension isA /Q , where P  =GCD ( y 2, y 1) and Q  =GCD (x 2, x 1).
  3. For every point in the matrix, which is in (x, y ) orthogonal space, we want to find its relative position in the space of one of the repeated halftone cells, defined by the vectors (x 1, y 1) and (x 2, y 2). (See Figure 6.) Call this point (u, v ). The transformation is u  =A *x  + B *y, v =C *x  + D *y . Since the point (x 2, y 2) in (x, y ) space is the point (1, 0) in halftone cell space and the point (x 1, y 1) is the point (0, 1) in halftone cell space, the coefficientsA, B, C, and D  are found by solving the following simultaneous linear equations:
    A *x 1 + B *y 1 = 0
    C *x 1 + D *y 1 = 1
    A *x 2 + B *y 2 = 1
    C *x 2 + D *y 2 = 0

We compute the dither matrix in the rotated case as follows: For each position in the matrix (i, j ):

  • Get (x, y ) the center of the matrix point (i, j ) x  =i  + 0.5 y  =j  + 0.5
  • Transform (x, y ) to a point in halftone cell space (u, v ) u  =A *x  + B *y v  =C *x  + D *y u  and v  now express the point (x, y ) as multiples of the two cell vectors. Therefore, the fractional parts ofu  and v  represent the position as if the particular halftone cell at the point (x, y ) were the (0, 0) cell.
  • Z  =spot-function (u  - floor(u ),v  - floor(v ))
  • Find the index of the record (containing fields x, y, and Z ) such thatu  =x, v  =y.  If the record doesn't exist, enteru, v, Z into the table. (Note that the equality between [u, v ] and [x, y ] requires an allowable epsilon difference to account for fixed-point round-off error.)
  • matrix[i, j ] = index
Find the order of records sorted by values ofZ;  store order in sort vector (described earlier in connection with converting the spot function result). Reassign values of matrix based upon sort vector.

[IMAGE Othmer/Lipton_rev7.GIF]

Figure 6 Transforming a Halftone Cell

Figure 7 shows our example matrix with values from 0 through 255, representing luminances, filled in. A luminance from an image with this range could be sampled directly against the matrix. The values in this matrix are those that would actually be used for a 300-dpi, 60-line-per-inch, 45º halftone. As in Figure 5, the matrix is repeated four times for the sake of clarity, with the 45º halftone cells overlaid. The position of any particular number in the matrix relative to the 45º cell it falls in corresponds exactly to the relative position of that same number in any of the other 45º cells. Thus, the effect of having a rotated halftone cell is created with an unrotated dither matrix.

[IMAGE Othmer/Lipton_rev8.GIF]

Figure 7 Our Example Matrix With Luminance Values Filled In

This particular example leads us to some other interesting possibilities. It turns out that QuickDraw patterns are 8x8 matrixes, just like our example. This means that we can halftone other QuickDraw primitives besides pixMaps when drawing to a 300-dpi non-PostScript device (provided that pattern stretching is disabled, by setting the bPatScale field in the print record to 0) and achieve a look similar to what a PostScript device would give us.

Here's how. Suppose we want to paint a region with a luminance of 150 on the scale from 0 to 255. We simply create a QuickDraw pattern in which all of the 1 bits correspond to the cells in the 8x8 matrix that are greater than or equal to 150. This pattern (shown in Figure 8) can then be used to paint any region or other QuickDraw primitive to get the halftone effect. Furthermore, because QuickDraw patterns are aligned to the origin of the grafPort, separate objects drawn touching one another will not generate undesirable seams, even when drawn with different shades. The nature of the clustered dot pattern is such that gradations appear continuous to the extent possible at the resolution of the device.

[IMAGE Othmer-Lipton_rev9.GIF]

Figure 8 Pattern for an Image With a Luminance of 150 Gray gradations dither Gray gradations halftone

Figure 9 shows the gray gradations and Konenna printed on a laser printer using error-diffusion dithering compared with halftoning using the 8x8 matrix. The difference in print quality is radical. For more commentary on this difference, see "Printing: Ideal Versus Real."

[IMAGE OTH-LIP FIG9.GIF]

Figure 9 Gray Gradations and Konenna Dithered and Halftoned at Laser Printer Resolution

About the code. And now, about the code. To illustrate the principle of dithering, our sample code is pixel-based--that is, the calculations are done on a pixel basis. Thus, the perfomance is sluggish. A real-world commercial application would use an optimized version of this code. One way to do this is to make the routines work on a scan-line rather than a pixel basis. Also note that the routine that does the halftoning only supports input pixMaps of 8 or 32 bits. It would be easy to extend the routine to accept pixMaps of other depths.

[IMAGE Othmer/Lipton_rev10.GIF]

Figure 10 TRC Curves for the LaserWriter

The first routine we need is one that calculates the luminance given a pointer to the current pixel. The LUMVAL routine returns a long luminance in the range of 0 to 255 using the 30%-59%-11% formula described previously.

long LUMVAL(Ptr pPixel, PixMapPtr pMap)
    {
        long        red, green, blue;
        
        if (pMap->pixelSize == 32) {
            red = (long)(unsigned char)*(++pPixel);    /* Skip alpha,
                                                          get red. */
            green = (long)(unsigned char)*(++pPixel);/* Get green. */
            blue = (long)(unsigned char)*(++pPixel);  /* Get blue. */
            return((30 * red + 59 * green + 11 * blue)/100);
        } else if (pMap->pixelSize == 8) {
            RGBColor*           theColor;
            theColor = &((*(pMap->pmTable))->ctTable[ (unsigned
                char)*pPixel ].rgb);
            return( (30 * (theColor->red >> 8) + 
                     59 * (theColor->green >>8) + 
                     11 * (theColor->blue >> 8))/100);
        }   /* End if */
    }   /* LUMVAL */

The routine that actually does the halftoning is the HalftonePixMap routine. Rather than taking a PixMapPtr as the CreatePICT2 routine did, this routine takes a PixMapHandle. This enables us to pass in either a pixMap we create manually (as we did when we called CreatePICT2) or a PixMapHandle that QuickDraw creates (for example, from a GWorld). We must distinguish which one we pass in so that the routine knows whether it can access the fields of the pixMap directly (which it can if we created it) or if it must use QuickDraw to access the fields. This is relevant only for the LockPixels and GetPixBaseAddr routines.

Furthermore, the HalftonePixMap routine assumes the resolution of the source pixMap is 72 dpi (screen resolution) and only supports devices with square pixels (same hRes and vRes). You can pass in the resolution of the destination device in the Resolution parameter, but it must be greater than or equal to 72 dpi.

Like the CreatePICT2 routine, HalftonePixMap returns a PicHandle. In this case, the picture contains a 1-bit/pixel pixMap. You can display it using DrawPicture.

The prototype for the HalftonePixMap routine is

PicHandle HalftonePixMap(PixMapHandle hSource, Boolean qdPixMap,
short Resolution);

The source code for the complete routine can be found on theDeveloper CD Series disc.

USING RANDOM DITHERING
Random dithering is yet another kind of dither useful for drawing images. It's discussed last, however, because of its inherent limitations.

The method is simple. It's much the same as the 50% threshold method described earlier. The only difference is that instead of being compared to 50%, the luminance values are compared to a random number between 0 and 100%. The effect of this is that the probability of any dot in the device image being turned on is directly proportional to the luminance of the pixel in the source image at the corresponding point.

This method has three limitations. First, calculating a random number is an expensive operation that we would not want to do for every device pixel. Second, except at very high resolutions, images dithered in this manner appear very noisy, like bad reception on a black-and-white TV. And third, this method requires a random number generator that's very good at producing a uniform distribution.

Ironically, this least frequently used method of dithering most accurately models the physical process of photography. Photographic film is like laser printing in that it's composed of pixels. However, the pixels are grains of silver rather than toner. Additionally, there are tens of thousands of grains per inch rather than the 300 dots per inch we're used to with laser printers. The lower the ASA rating of the film, the higher the grain density.

The place on a film where a photon strikes one of these silver grains turns black when the film is developed (which is why you get negatives). Since photons are really, really, really small, the likelihood of a single photon striking one of the grains of silver is very low. However, the brighter the light, the more photons there are; so the probability of striking one of those silver grains increases in proportion to the luminance. Thus, we see how random dithering simulates photography.

Figure 11 shows the image of a frog's head produced using halftoning with an 8x8 matrix as compared with using a 72-dpi random dither. You can see that the randomly dithered image looks like a really grainy photograph.

[IMAGE Othmer/Lipton_rev11.GIF]

Figure 11 Frog's Head, Halftoned and Randomly Dithered

HASTA LA VISTA, BABY

This article has addressed several issues. First, the problem of saving deep pixMaps on machines with original QuickDraw was overcome by showing you how to manually create a PICT, which can then be rendered by calling DrawPicture. Such a PICT can be exported by an application so that it can be viewed in color on a Color QuickDraw machine.

Second, several solutions to the problem of displaying and printing color images on black-and-white devices were discussed. Images can be displayed on screen using a 50% threshold or error-diffusion dithering. Ordered dithering (halftoning) provides a way to get around the problem of the laser printer's inability to resolve single pixels. Random dithering has practical limitations but represents yet another alternative for producing color images on black-and-white devices.

Thanks to these techniques, the market for applications that deal with color images need not be limited to Color QuickDraw machines and PostScript printers. The necessary code is small (and already written for you) and the gain in functionality is very high. Now get to work on those applications!

BUT DON'T I NEED A LICENSE TO DO THIS?

The reason Apple doesn't want developers modifying data structures is that it makes it hard to change them in the future. For example, early Macintosh programs locked handles by manually setting the high bit of the handle rather than calling HLock. This caused numerous compatibility problems when the 32-bit-clean Memory Manager was introduced.

So what gives? What if Apple changes OpenPicture so that it creates a totally different data format--won't the manually created pictures break?

Calm down, because the answer is no. The difference between creating your own pictures and directly modifying other data structures is that Apple can't make the current picture data format obsolete without invalidating users' data that exists on disk. Just as you can still call DrawPicture on version 1 pictures and everything works, you will always be able to call DrawPicture on existing version 2 pictures, regardless of the format of pictures created in the future.

One possible pitfall is that you might create a picture with subtle compatibility risks that draws on the existing system software but breaks at some future date. To minimize the chances of such an occurrence, you should compare the pictures you generate with those that QuickDraw generates in identical circumstances. You must be able to account for any and all differences.

Creating your own pixMaps (as our example code does) is definitely in the gray area between risky and outright disastrous behavior, and you shouldn't do it. Then why would an article written by two upstanding citizens do such a thing? The answer is that the pixMaps used by this code are kept private; they're never passed as arguments to a trap. We could just as easily have called them something else, but pixMaps work for what we're doing, so we used them. If you want to pass a pixMap to a trap, you can generate it using the NewPixMap call (not available on machines with original QuickDraw) or let other parts of Color QuickDraw, like OpenCPort, generate it.

PRINTING: IDEAL VERSUS REAL

We've already talked about the error introduced in printing by the fact that the laser beam is round while the pixel is square. Many other factors also can make the transfer of toner to paper deviate from the ideal. Sources of error include differences in inks, papers, printer drums, and even humidity. Additionally, a printer's behavior changes over time as the drum wears. Compensating for all these factors to achieve ideal images would require constant calibration and recalibration of the printer.

An error appears most pronounced in the final print when imaging directly at device resolution, as Figure 9 shows. Halftoning hides much of this error and produces reasonably uniform results among printers with varying degrees of error.

The tonal reproduction curves (known as TRC or gamma curves) shown in Figure 10 indicate the gray levels produced by the Apple LaserWriter when dithering and halftoning. Note that with dithering, the measured luminance of an image remains dark much longer than with halftoning as requested luminance increases, due to the error when each pixel is printed. Of particular interest is the point on the dither curve right at 50% luminance. The measured luminance is actually darker than when 44% luminance is requested. The reason is that with a 50% dither, every other pixel is drawn, maximizing the effect of the laser error.

While the TRC curve for the halftone print doesn't match the ideal curve, it's much closer to the ideal than is the dither curve. To get the halftone even closer to ideal, you could adjust the luminance calculation by the amount indicated by the halftone TRC to compensate. Indeed, most image-processing applications perform this TRC adjustment to compensate for the nonlinearities of the output device. See Designing Cards and Drivers for the Macintosh Family,  Second Edition (Addison-Wesley, 1990) for more information about how gamma correction works on the Macintosh II family for monitors.

WANT TO READ MORE?

If you'd like to delve more deeply into the mysteries of processing color images for display, check out the following:
  • "An Optimum Algorithm for Halftone Generation for Displays and Hard Copies" by Thomas M. Holladay, in the Proceedings of the Society for Information Display,  Vol. 21, No. 2, 1980.
  • Digital Halftoning  by Robert Ulichney (MIT Press, 1987). This book, based on a Ph.D. thesis done at MIT, is devoted entirely to discussing halftoning algorithms; it's extremely thorough and includes many example images halftoned in different ways.
  • Fundamentals of Interactive Computer Graphics  by J. D. Foley and A. Van Dam (Addison-Wesley, 1982). The standard text on computer graphics. Not nearly as thorough as Ulichney, but has a solid discussion of the basics.

And then, of course, the two books all Macintosh programmers should own:

  • Programming with QuickDraw  by Dave Surovell, Frederick Hall, and Konstantin Othmer (Addison-Wesley, 1992). Everything you need to know about graphics on the Macintosh.
  • Debugging Macintosh Software with MacsBug  by Konstantin Othmer and Jim Straus (Addison-Wesley, 1991). Everything you need for debugging Macintosh software, including in-depth discussions of a number of the Macintosh managers.

KONSTANTIN OTHMER has wanted his photograph to appear in Sports Illustrated  for as long as he can remember. Unfortunately, his college was in the NCAA's Division III, which is often overlooked by SI's editors, and somehow they've missed his virtuosity on the ski slopes at Tahoe, Vail, and Red Lodge. So Kon's had to scale down his dream, setting his sights on making the pages of develop  instead. Here he's gotten to try on various alter egos. To come up with his latest persona, he spent a few late nights in a secret Apple lab with skilled pixel surgeon Jim Batson. *

DANIEL LIPTON (a.k.a. "The PostScript Kid") is a two-and-a-half-year veteran of Apple's System Software Imaging Group, where he's working on the next generation of printing software for the Macintosh. When he's not thinking backward, he enjoys taking in a good flick, spending time with his iguana, "Iggy" (who's never quite forgiven Dan for the time she nearly froze to death in the cargo compartment of a 747), and writing zany new lyrics to classic tunes (his "Working in the Print Shop Blues" is well known to his coworkers). Most of all, Dan enjoys building and flying model airplanes, and he's recently joined the competition circuit. In fact, when asked what he'd really like to do with his life, Dan replies:

sunny { { { { hours 8 { flying } for } rather_be } dayforall } } if *

The source of step 2 in the above algorithm is "An Optimum Algorithm for Halftone Generation for Displays and Hard Copies" by Thomas M. Holladay, from the Proceedings of the Society for Information Display  , Vol. 21, No. 2, 1980.*

THANKS TO OUR TECHNICAL REVIEWERSSean Parent, Forrest Tanaka, Dave Williams*

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Whitethorn Games combines two completely...
If you have ever gone fishing then you know that it is a lesson in patience, sitting around waiting for a bite that may never come. Well, that's because you have been doing it wrong, since as Whitehorn Games now demonstrates in new release Skate... | Read more »
Call of Duty Warzone is a Waiting Simula...
It's always fun when a splashy multiplayer game comes to mobile because they are few and far between, so I was excited to see the notification about Call of Duty: Warzone Mobile (finally) launching last week and wanted to try it out. As someone who... | Read more »
Albion Online introduces some massive ne...
Sandbox Interactive has announced an upcoming update to its flagship MMORPG Albion Online, containing massive updates to its existing guild Vs guild systems. Someone clearly rewatched the Helms Deep battle in Lord of the Rings and spent the next... | Read more »
Chucklefish announces launch date of the...
Chucklefish, the indie London-based team we probably all know from developing Terraria or their stint publishing Stardew Valley, has revealed the mobile release date for roguelike deck-builder Wildfrost. Developed by Gaziter and Deadpan Games, the... | Read more »
Netmarble opens pre-registration for act...
It has been close to three years since Netmarble announced they would be adapting the smash series Solo Leveling into a video game, and at last, they have announced the opening of pre-orders for Solo Leveling: Arise. [Read more] | Read more »
PUBG Mobile celebrates sixth anniversary...
For the past six years, PUBG Mobile has been one of the most popular shooters you can play in the palm of your hand, and Krafton is celebrating this milestone and many years of ups by teaming up with hit music man JVKE to create a special song for... | Read more »
ASTRA: Knights of Veda refuse to pump th...
In perhaps the most recent example of being incredibly eager, ASTRA: Knights of Veda has dropped its second collaboration with South Korean boyband Seventeen, named so as it consists of exactly thirteen members and a video collaboration with Lee... | Read more »
Collect all your cats and caterpillars a...
If you are growing tired of trying to build a town with your phone by using it as a tiny, ineffectual shover then fear no longer, as Independent Arts Software has announced the upcoming release of Construction Simulator 4, from the critically... | Read more »
Backbone complete its lineup of 2nd Gene...
With all the ports of big AAA games that have been coming to mobile, it is becoming more convenient than ever to own a good controller, and to help with this Backbone has announced the completion of their 2nd generation product lineup with their... | Read more »
Zenless Zone Zero opens entries for its...
miHoYo, aka HoYoverse, has become such a big name in mobile gaming that it's hard to believe that arguably their flagship title, Genshin Impact, is only three and a half years old. Now, they continue the road to the next title in their world, with... | Read more »

Price Scanner via MacPrices.net

B&H has Apple’s 13-inch M2 MacBook Airs o...
B&H Photo has 13″ MacBook Airs with M2 CPUs and 256GB of storage in stock and on sale for up to $150 off Apple’s new MSRP, starting at only $849. Free 1-2 day delivery is available to most US... Read more
M2 Mac minis on sale for $100-$200 off MSRP,...
B&H Photo has Apple’s M2-powered Mac minis back in stock and on sale today for $100-$200 off MSRP. Free 1-2 day shipping is available for most US addresses: – Mac mini M2/256GB SSD: $499, save $... Read more
Mac Studios with M2 Max and M2 Ultra CPUs on...
B&H Photo has standard-configuration Mac Studios with Apple’s M2 Max & Ultra CPUs in stock today and on Easter sale for $200 off MSRP. Their prices are the lowest available for these models... Read more
Deal Alert! B&H Photo has Apple’s 14-inch...
B&H Photo has new Gray and Black 14″ M3, M3 Pro, and M3 Max MacBook Pros on sale for $200-$300 off MSRP, starting at only $1399. B&H offers free 1-2 day delivery to most US addresses: – 14″ 8... Read more
Department Of Justice Sets Sights On Apple In...
NEWS – The ball has finally dropped on the big Apple. The ball (metaphorically speaking) — an antitrust lawsuit filed in the U.S. on March 21 by the Department of Justice (DOJ) — came down following... Read more
New 13-inch M3 MacBook Air on sale for $999,...
Amazon has Apple’s new 13″ M3 MacBook Air on sale for $100 off MSRP for the first time, now just $999 shipped. Shipping is free: – 13″ MacBook Air (8GB RAM/256GB SSD/Space Gray): $999 $100 off MSRP... Read more
Amazon has Apple’s 9th-generation WiFi iPads...
Amazon has Apple’s 9th generation 10.2″ WiFi iPads on sale for $80-$100 off MSRP, starting only $249. Their prices are the lowest available for new iPads anywhere: – 10″ 64GB WiFi iPad (Space Gray or... Read more
Discounted 14-inch M3 MacBook Pros with 16GB...
Apple retailer Expercom has 14″ MacBook Pros with M3 CPUs and 16GB of standard memory discounted by up to $120 off Apple’s MSRP: – 14″ M3 MacBook Pro (16GB RAM/256GB SSD): $1691.06 $108 off MSRP – 14... Read more
Clearance 15-inch M2 MacBook Airs on sale for...
B&H Photo has Apple’s 15″ MacBook Airs with M2 CPUs (8GB RAM/256GB SSD) in stock today and on clearance sale for $999 in all four colors. Free 1-2 delivery is available to most US addresses.... Read more
Clearance 13-inch M1 MacBook Airs drop to onl...
B&H has Apple’s base 13″ M1 MacBook Air (Space Gray, Silver, & Gold) in stock and on clearance sale today for $300 off MSRP, only $699. Free 1-2 day shipping is available to most addresses in... Read more

Jobs Board

Medical Assistant - Surgical Oncology- *Apple...
Medical Assistant - Surgical Oncology- Apple Hill Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Read more
Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
Operations Associate - *Apple* Blossom Mall...
Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Business Analyst | *Apple* Pay - Banco Popu...
Business Analyst | Apple PayApply now " Apply now + Apply Now + Start applying with LinkedIn Start + Please wait Date:Mar 19, 2024 Location: San Juan-Cupey, PR Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.