Tutorial: Photo Fun (page 2)



allow for are to adjust the colours, brightness and contrast.

The colours in the bitmaps are of type unsigned byte. To allow negative as well as positive values, we use integer properties for the red, green, blue, brightness and contrast adjustments. Limit the values to plus or minus 255 in the properties' Set methods.

Contrast is adjusted by increasing brightness of the brighter pixels and reducing the brightness of the darker ones. Add a “ContrastNeutral” integer property to hold the transition value between dark and bright.


Tip: Read/Write bitmaps


TBitmap has a Canvas property. The Canvas has a Pixels property of type TColor.
TColor is an Integer type. Individual red green and blue values can be accessed from this using the logical “and” keyword.
This provides a safe but slow method of implementing the processing.

For speed we will use the GetDataLineStart of TLazIntfImage (Delphi: Scanline property of TBitmap).
GetDataLineStart (Scanline) returns a pointer to the horizontal lines of raw data of the bitmap which is an array memory bytes.
First declare a record to represent an individual pixel.

TRGB24 = packed record
B: Byte;
G: Byte;
R: Byte;
end;


Next declare an array of these pixels

TRGB24Array = packed array[0..MaxInt div SizeOf(TRGB24)-1] of TRGB24;

The keyword “packed” indicates to the Delphi compiler that the pixel records are immediately adjoining each other in memory and not aligned for speed on Long Word boundaries (Quadruple Word boundaries on 64 bit systems).
Finally we declare a pointer type

PRGB24Array = ^TRGB24Array;

We typecast the Bitmap Scanlines as PRGB24Array.

var Line: PRGB24Array; ... Line := BitmapIntf.GetDataLineStart[y];

then manipulate the RGB values

Line[x].R := someRedByteValue;


Add a new form to the project to allow the user to set these various values from TrackBar components. Show the form from TBasePhoto by adding a DlgColourSettingsExecute procedure.

The colour settings dialogue needs to call the RedrawDisplay method of TBasePhoto each time the user makes an adjustment. To link to the method, add a TBasePhoto property to the form's public declarations. Have the TBasePhoto set this property to “Self” before showing the form.


Single and multi-layer images

For changing colours, converting to monochrome or inverting photographs we will work in a single image. Our other manipulations involve combining photographs together in layers to obtain the RawImage.


Image Layers

Encapsulate the layer functionality in a “TPhotoLayer” class. Add a Bitmap property to store the layer image.

To identify the layer to the user, add a “NameFriendly” string property. We cannot use the component “Name” property for this as Name cannot contain spaces.

Add a LoadFromFile method to get the bitmap from disc. Typically digital camera images are stored as JPEGs. Check the file extension then use a TJPEGImage to decompress it.


Create subclasses to define single and multi layer images

TSingleLayerPhoto = class (TBasePhoto);

TMultiLayerPhoto = class (TBasePhoto);

The MultiLayerPhoto requires a list of the layers it contains. This could be a simple TList but we can add additional functionality if we roll our own class. Declare TPhotoLayerList as a subclass of TComponentList. We override the methods of TComponentList to restrict list items to be of type TPhotoLayer (and descendents if any) only.

Another way of achieving this is to use Genetics which are supported in Delphi but not (yet) in Lazarus.


To allow the user to reposition and modify individual layers we need to know which layer has focus. In TBasePhoto add a TopLayer property so that we get Layer functionality in all photo classes we create. For single layer photos create the TopLayer in the constructor. For multi-layer images it will point to one of the layers in the list.


To open a new image, add a LoadFromFile method to TBasePhoto. This is declared virtual so we can override it in the derived classes. In TSingleLayerPhoto just call the TopLayer LoadFromFile method. In TMultiLayerPhoto we iterate through the Layers, loading each in turn.


Combine the layers

Add a RedrawRaw procedure to generate the raw image from the layers. There is no sensible implementation we can put in TBasePhoto so declare it virtual and abstract. The compiler will then check we have remembered to override the method it when we try to create the final Photo instances.


To implement RedrawRaw in TSingleLayerPhoto just assign the TopLayer bitmap to the RawImage.

In the TMultiLayerPhoto class override RedrawRaw to iterate through the layers to combine the image data.

The user may wish to reposition the layers in relation to each other before combining them. Identify the positions of the layers by adding “Top” and “Left” properties to TPhotoLayer.


To get the total drawing area of the stacked layers, add methods to the TPhotoLayerList class to return the maximum bounds of the layers. Set the Width and Height of RawImage using these bounds so that it is large enough.

Initialise the raw image with white.

RawImage.Canvas.Brush.Color := $FFFFFF;

RawImage.Canvas.FillRect(Rect(0,0,X,Y));


There are a many ways we can combine the images, adding the colours, taking an average of the layers or selecting a pixel from one layer in preference to the others.


Add a property “MergeType” to TMultiLayerPhoto. For now just define the type like this:

TLayerMergeType = (lmtAnd, lmtMelt);

You can add other types this enumeration to extend the project.


Some merge types are supported by Canvas CopyMode so we can simply copy the layer bitmaps to the RawImage

RawImage.Canvas.CopyMode := cmSrcAnd;

RawImage.Canvas.CopyRect( destRect, Layer[i].Bitmap.Canvas, sourceRect);

This does not seem to work correctly in Lazarus on Windows. See the project source code for my alternative solution.

For merge types not supported by CopyRect we need to iterate through the layer bitmap lines constructing each of the target RawImage pixels as required.


In Delphi we access the image lines using the bitmap Scanline property. In Lazarus this is not available – presumably for cross-platform compatibility reasons. The solution is to use the TLazIntfImage class. We can spawn a TLazIntfImage object using TBitmap.CreateIntfImage then write it back to the bitmap using TBitmap.LoadFromIntfImage. We can read and write the pixels on the TLazIntfImage using GetDataLineStart(y: integer) just the same as using Scanline in Delphi.


The lmtMelt merge is achieved by adding the difference between the layer and the raw