Bitmap Graphics
Volume Number: 14
Issue Number: 8
Column Tag: Yellow Box
Bitmap Graphics
by by R. D. Warner
Edited by Michael Rutman
Demonstrated in Ray Tracing Algorithm Code Snippet
Overview -- Why Use Bitmaps?
There are times when the programmer needs control over individual pixels, plus all
the speed that is possible. Rendering in 3D is one case where individual pixel values
are calculated using compute-intensive algorithms. Lighting calculations are
performed for each pixel (often recursively) and every little coding trick eventually
is used in the pursuit of speed.
Pswraps is one technique available to Rhapsody developers. The "wraps" are C function
calls that perform one to many PostScript operations in one interprocess message to
the Window Server. So it is an efficient technique, and it is possible to write to
individual pixels...sort of. For instance, one can define a rectangle with area of one
pixel and the appropriate color, and draw that. This means one function call to draw
each pixel. Display postscript is not really geared to individual pixel manipulation.
A slightly lower level approach is to use bitmaps. A data buffer is populated dependent
on various color schemes (monochrome, RGB, CYMK, HSB) and other factors, such as
whether or not the alpha channel is used. Then that data is rendered all at one time to
whatever NSView has focus locked. One can expect a boost in performance using this
approach and indeed that is the case. The pswraps code is simpler to use, but slower.
From games to medical imaging to paint programs, the need for high-speed pixel
manipulations is the driving force in the decision to use bitmaps. This article uses a
simple 3D rendering application as test code for comparing performance of a pswraps
implementation against a bitmap.
Included in this article is a code snippet from Physics Lab. It is ported to the Rhapsody
environment but this code should basically work fine in OPENSTEP 4.2 also. This
program uses a ray tracer algorithm to render a typical scene composed of various
graphics primitives (such as spheres, planes, cylinders, etc.) into an NSView object.
There are many algorithms for achieving shading effects on 3D objects and ray tracing
is one of the most popular ones for achieving photorealistic lighting effects. Rendering
is the term used for the process of generating the 3D scene based on mathematical
models. The NSView object is one of several in the Rhapsody AppKit that can display an
image on the screen.
The application also includes a second NSView alongside the first where a custom
renderer such as a subclassed ray tracer, can be displayed (disabled in screen shots).
This is useful in visualization projects where one wants to see how something would be
seen "normally" plus how it would appear using the custom viz algorithms.
Specifically, this will be used for the visualization of field phenomena such as gravity
-- one view displays the collection of objects as they would normally be seen with
visible light, and the other view will make a visible representation of the field
interactions between and inside the objects. (See Figures 1-3)
One will learn something about NSImage classes in this article too. They support
bitmaps plus many other types of image representations. In fact it is necessary to
understand them before advancing further.
Figure 1. Main window in Physics Lab with second NSView disabled.
Figure 2. Window for orienting the view volume.
Figure 3. Preferences Panel.
NSImage Classes
Description
An NSImage is a lightweight object compared to an NSView. An NSImage can draw
directly to the screen, or be used (as it is in this code) as a container for NSImageRep
subclass objects. The container composites the image to an NSView...which draws to the
screen. There are a number of configurations for using the various view-related
objects in AppKit, so this is only one approach. This approach is especially useful if
one has multiple NSImage objects drawing to different areas within an NSView.
Perhaps a collage-type scene where individual images (each one represented by an
NSImage) over time are moved around within the rectangle defined by the NSView.
The NSImage contains an NSMutableArray object that can contain numerous
NSImageRep subclass objects. NSMutableArray replaces the NXList found in NEXTSTEP
3.3. It is a dynamic array for containing groups of objects. Each representation is for
the same image. For example, one can have representations that are at various
resolution levels, or using different formats such as TIFF and EPS. Different color
schemes can be used as mentioned earlier and one can even make custom
representations to render images from other types of source information.
A representation can be initialized in a variety of ways, somewhat dependent on the
representation type. The most straightforward ways are to initialize the NSImage from
a file (typically in the application bundle) or from an NSData/NSMutableData object
created dynamically, and let it create the appropriate representations (which it
manages), based on the header information in the data. The image can also come from
the pasteboard, from an existing NSView, or raw bitmap data. Reading in a file of an
unsupported format type requires either a custom representation or a user-installed
filter service.
The real magic of NSImage classes comes from its ability to select the best image for
the current display device, from its list of representations. This logic algorithm is
somewhat customizable by the programmer. There are several flags that can be set to
modify the selection criteria and priorities (see below). The algorithm naturally only
can select from the NSImageReps that are managed by the NSImage in question. Thus
the selection process can also be controlled indirectly by the types of representations
added. The steps in the selection algorithm below are followed only until the first
match is established.
Selection Algorithm
1. Choose a color representation for a color device, and gray-scale for a
monochrome device.
2. Choose a representation with a matching resolution or if there is no
match, then choose the one with the highest resolution. Note that
setPrefersColorMatch:NO will cause the NSImage to try a resolution match
before a color match.
3. Resolutions that are multiples of the device depth are considered matches
by default. Choose the one closest to the device. Note that
setMatchesOnMultipleResolution:NO will cause only exact matches to be
considered.
4. The resolution matching discriminates against EPS representations since
they do not have defined resolutions. The
setUsesEPSOnResolutionMismatch:YES will cause the NSImage to select an EPS
representation (if one exists) if no exact resolution match can be found.
5. Choose the representation with the specified bits per sample that matches
the depth of the device. Note that the number of samples per pixel may be
different from the device (an RGB no-alpha color representation has three
samples per pixel, while a monochrome monitor would have one sample per
pixel, for example).
6. Choose the representation with the highest bits per sample.
Abbreviated Method Quick Reference (based on online
documentation)
-(id)initWithContentsOfFile:(NSString*)filename
Initializes the NSImage with the contents of filename and reads it at this time. If it
successfully creates one or more image representations, it returns self. Otherwise the
receiver is released and nil is returned.
-(id)initWithData:(NSData*)data
Initializes the NSImage with the contents of data. If it successfully creates one or more
image representations, it returns self. Otherwise the receiver is released and nil is
returned.
-(void)addRepresentation:(NSImageRep*)imageRep
Adds imageRep to receiver's list of managed representations. Any representation added
in this way is retained by the NSImage, and released when it is removed.
-(void)removeRepresentation:(NSImageRep*)imageRep
Removes and releases imageRep.
-(NSArray*)representations
Returns an array of all the representations managed by the NSImage.
-(void)lockFocus
-(void)lockFocusOnRepresentation:(NSImageRep*)imageRep
-(void)unlockFocus
Using the approach outlined in this code one does not have to bother with
lockFocus/unlockFocus pairs. This is because the NSImage performs its drawing to the
NSView from within the NSView's own drawRect: method. That method is called
indirectly by the NSView when it receives a display message from within the code.
Focus is automagically locked on the NSView as part of this whole process, so it does
not need to be done by the programmer.
These methods are mentioned here because one will use them in certain circumstances.
If the NSImage draws directly to the screen instead of to an NSView, these may be
needed. To force the NSImage to determine its best representation, or to test in advance
that an NSImage can actually interpret its image data, one may need to lock the focus.
In the last case, if the NSImage cannot understand the data it was given, it will raise an
exception when it fails to lock focus; the exception can be trapped by the code to
determine that the image file was the wrong format, garbled, or in general not
displayable.
Exceptions are a programming technique within the OPENSTEP/Rhapsody environment
that effectively take the place of error return codes and code for checking the return
codes. See the documentation for NSException objects for more details. The act of
locking focus does a variety of things but conceptually it determines what object will
be rendering the image information to the screen, and it establishes the coordinate
system that will be used to position the images.
-(void)compositeToPoint:(NSPoint*)aPoint
operation:(NSCompositingOperation)op
The aPoint argument specifies the lower-left corner in the NSView coordinate system
as to where the NSImage shall be composited. The op argument can take one of
currently fifteen enumerated values. The NSCompositeCopy is common if one simply
wants to copy the NSImage image into the NSView. If the alpha channel is used,
NSCompositeSourceOver would be used (source laid over destination with a specified
opacity). NSCompositeClear will clear or erase the affected destination area.
NSCompositeXOR does a logical exclusive-OR operation between the source and
destination data.
-(void)dissolveToPoint:(NSPoint*)aPoint
fraction:(float)aFloat
Composites the image to the location specified by aPoint, but it uses the dissolve
operator. The aFloat argument ranges between 0.0 and 1.0 and indicates how much of
the resulting composite will be derived from the NSImage. A slow dissolve can be
accomplished by repeatedly using this method with an ever-increasing fraction until
it reaches 1.0. Note that each dissolve operation must be performed between the source
data and the original destination data, not the cumulative result of each dissolve (keep
a copy of the original data somewhere and do the dissolve operation off-screen -- then
flush it to the screen). Note that a slow dissolve is not possible on a printer, but one
can do a single dissolve operation and print the results of that.
-(void)setFlipped:(BOOL)flag
-(BOOL)isFlipped
When working with raw bitmap data one may find it necessary to flip the coordinate
system y-axis. Thus (0,0) is in the upper-left corner instead of the lower-left
corner. It depends how the algorithm generates the data, but if images are being
displayed on the screen upside down, this is how to fix it:
setFlipped:YES
-(void)setSize:(NSSize)aSize
-(NSSize)size
With raw data in particular, one wants to explicitly make the image representation
(since the structure of the data needs to be described), then add the representation to
the list of managed representations for the NSImage. Contrast this approach to the
situation where one initializes an NSImage with the contents from a file, and the
appropriate representation(s) are created automatically. When working with raw
bitmap data the image representation must manually be created and told how many
bytes per pixel there are, whether or not there is an alpha channel, etc. Since the
NSImage did not create the image representation it does not know the size of the image.
Use setSize: to tell it.
-(NSData*)TIFFRepresentation
Returns a data object with the data in TIFF format for all representations, using their
default compressions.
Example Uses of an NSImage
1. Draw same image repeatedly to an NSView (offscreen cache).
2. Have optimized images for various types of displays, or printer.
3. Read file in one format and write to another (e.g., read bitmap, write
TIFF).
4. Draw to a bounded rectangle within an NSView.
5. Manipulate individual pixels.
6. Read image from an NSView, perform filtering operation, draw image
back to an NSView.
7. Draw to an NSView using a variety of logical operators other than simple
copy.
8. Make existing image in NSView dissolve into new image.
NSBitmapImageRep Classes
Description
There are currently four subclasses of the NSImageRep class: NSBitmapImageRep,
NSCachedImageRep, NSCustomImageRep, and NSEPSImageRep. For manipulating raw
bitmap data, use the bitmap image rep. The class has a data buffer of type: unsigned
char*. There are numerous initializers, but for raw bitmap data such as that which
will be generated in this example, use the initWithBitmapDataPlanes:::::::::: method. All
the many arguments are used to describe the structure of the data.
It takes a lot of arguments to canonically describe raw bitmap data. For example, there
is a "meshed" mode where the data color components are grouped together in memory.
An RGB pixel would typically have three bytes together, one for each color
component--possibly a fourth byte if the alpha channel is used. There is another mode
called "planar" where each color component is grouped in a separate plane. This means
in the previous example all the red bytes would come first, then the green, then the
blue, then the alpha.
Other arguments define the width and height of the image in pixels, plus the number of
bits per pixel and the total number of bytes in the image. There are instances where
the total number of bytes is different from the number of pixels times the number of
bits per pixel such as when the pixels are aligned on word boundaries and the pixel
bits are less than that. So it may seem to the programmer that more arguments are
being provided than is necessary, but this is not the case. The programmer can also
specify which color space is used and whether or not the alpha channel is present.
Currently, no less than nine color spaces are supported, including both calibrated
(device independent) and device-specific versions. The code snippet in this article
uses the RGB color space with no alpha, one byte per color component, and three bytes
per pixel.
One of the arguments to the initializers is a pointer to a data buffer. This may be set to
NULL. If so, the method will calculate the size of the buffer (based on the arguments
given it) and allocate it. The buffer is thus owned by the instance, and freed when it is
freed. The getBitmapDataPlanes or bitmapData methods are used as accessors to get a
pointer to the buffer that was allocated, so that it can be populated. The other approach
is to allocate the memory for the buffer before initializing the representation. If this
is the case then one does not have to use the data method to retrieve a pointer since one
already knows it. The instance does not own the buffer and it is the programmer's
responsibility to explicitly free it.
Abbreviated Method Quick Reference (based on online
documentation)
+(NSArray*)imageRepsWithData:(NSData*)bitmapData
Creates and returns an array of initialized NSBitmapImageRep objects based on images
in the bitmapData. Returns empty array if images cannot be interpreted.
-(id)initWithBitmapDataPlanes:(unsigned char**)planes
pixelsWide:((int)width pixelsHigh:(int) height
bitsPerSample:(int)bps samplesPerPixel:(int)spp
hasAlpha:(BOOL)alpha
isPlanar:(BOOL)planar
colorSpaceName:(NSString*)colorSpaceName
bytesPerRow:(int)rowBytes bitsPerPixel:(int)pixelBits
RGB data will have three or four planes (without or with alpha) and CMYK will have
four or five. White or black color space (grey-scales with 1.0 == white or 1.0 ==
black, respectively) will have one or two. If isPlanar is NO then only the first plane of
planes will be read. This effectively places the rep in meshed mode.
Colorspace Names