How to "flush" changes to a bitmap's ScanLine - delphi

I'm currently trying to add mirroring to our RotateBitmap routine (from http://www.efg2.com/Lab/ImageProcessing/RotateScanline.htm). This currently looks like this (BitMapRotated is a TBitmap) in pseudo-code:
var
RowRotatedQ: pRGBquadArray; //4 bytes
if must reflect then
begin
for each j do
begin
RowRotatedQ := BitmapRotated.Scanline[j];
manipulate RowRotatedQ
end;
end;
if must rotate then
begin
BitmapRotated.SetSize(NewWidth, NewHeight); //resize it for rotation
...
end;
This works if I either must rotate or reflect. If I do both then apparently the call to SetSize invalidates my previous changes via ScanLine. How can I "flush" or save my changes? I tried calling BitmapRotated.Handle, BitmapRotated.Dormant and setting BitmapRotated.Canvas.Pixels[0, 0] but without luck.
Edit: I found the real issue - I'm overwriting my changes with values from the original bitmap. Sorry for the effort.

Perhaps this is not really an answer, but this code works in both D2006 and XE3 and gives the expected result. There is no need to 'flush' anything.
procedure RotateBitmap(const BitMapRotated: TBitmap);
type
PRGBQuadArray = ^TRGBQuadArray;
TRGBQuadArray = array [Byte] of TRGBQuad;
var
RowRotatedQ: PRGBQuadArray;
t: TRGBQuad;
ix, iy: Integer;
begin
//first step
for iy := 0 to BitMapRotated.Height - 1 do begin
RowRotatedQ := BitMapRotated.Scanline[iy];
// make vertical mirror
for ix := 0 to BitMapRotated.Width div 2 - 1 do begin
t := RowRotatedQ[ix];
RowRotatedQ[ix] := RowRotatedQ[BitMapRotated.Width - ix - 1];
RowRotatedQ[BitMapRotated.Width - ix - 1] := t;
end;
end;
//second step
BitMapRotated.SetSize(BitMapRotated.Width + 50, BitMapRotated.Height + 50);
//some coloring instead of rotation
for iy := 0 to BitMapRotated.Height div 10 do begin
RowRotatedQ := BitMapRotated.Scanline[iy];
for ix := 0 to BitMapRotated.Width - 1 do
RowRotatedQ[ix].rgbRed := 0;
end;
end;
var
a, b: TBitmap;
begin
a := TBitmap.Create;
a.PixelFormat := pf32bit;
a.SetSize(100, 100);
a.Canvas.Brush.Color := clRed;
a.Canvas.FillRect(Rect(0, 0, 50, 50));
b := TBitmap.Create;
b.Assign(a);
RotateBitmap(b);
Canvas.Draw(0, 0, a);
Canvas.Draw(110, 0, b);

Related

Decorating TImageCollection images with code

For testing purposes, in my Delphi 10.3 application, I'd like to decorate images in a TImageCollection with the dimensions of each image. For bitmaps, it's no problem but for PNG files, I can't paint to that canvas, neither can I assign from a BMP to a PNG in TWICImage because of a runtime exception "cannot assign a TPngImage to a TWICImage".
procedure DecorateImageCollection(imcMainMisc: TImageCollection);
var
i, j: Integer;
bmp:Graphics.TBitmap;
item:TImageCollectionItem;
img:TImageCollectionSourceItem;
begin
for i := 0 to imcMainMisc.Count - 1 do
begin
item:=imcMainMisc.Images.Items[i];
for j := 0 to item.SourceImages.Count - 1 do
begin
img:=item.SourceImages.Items[j];
case img.Image.ImageFormat of
wifBmp:
;
wifPng:
begin
bmp:=Graphics.TBitmap.Create;
try
bmp.Assign(img.Image);
bmp.Canvas.Font.Name:='Small Fonts';
bmp.Canvas.Font.Size:=6;
bmp.Canvas.Font.Color:=clRed;
bmp.Canvas.Brush.Style:=bsClear;
bmp.Canvas.Pen.Style:=psSolid;
bmp.Canvas.TextOut(0, 0, IntToStr(bmp.Height));
// *cannot assign a TPngImage to a TWICImage*
img.Image.Assign(bmp);
finally
bmp.Free;
end;
end;
wifJpeg:
;
wifGif:
;
wifTiff:
;
wifWMPhoto:
;
wifOther:
;
end;
end;
end;
end;
I expect such an operation should be simple but I haven't yet found out how.
Thank you!
The solution I ended up using was deleting the PNG source item, adding a new source item and using LoadFromStream( ).
procedure DecorateImageCollection(imc: TImageCollection);
var
i, j, x, y: Integer;
r:TRect;
rSize:TSize;
sTag:string;
bmp:TBitmap;
png:TPngImage;
item:TImageCollectionItem;
str:TMemoryStream;
img, icsiNew:TImageCollectionSourceItem;
Alpha: PByte;
begin
for i := 0 to imc.Count - 1 do
begin
item:=imc.Images.Items[i];
for j := item.SourceImages.Count - 1 downto 0 do
begin
img:=item.SourceImages.Items[j];
case img.Image.ImageFormat of
wifBmp:
begin
bmp:=TBitmap.Create;
try
bmp.Assign(img.Image);
sTag:=IntToStr(bmp.Height);
bmp.Canvas.Font.Name:='Small Fonts';
bmp.Canvas.Font.Size:=6;
rSize:=bmp.Canvas.TextExtent(sTag);
r.Top:=0;
r.Left:=0;
r.Width:=rSize.Width;
r.Height:=rSize.Height;
bmp.Canvas.Brush.Color:=clWhite;
bmp.Canvas.Brush.Style:=bsSolid;
bmp.Canvas.Font.Color:=clRed;
bmp.Canvas.Pen.Style:=psSolid;
bmp.Canvas.TextOut(r.Left, r.Top, sTag);
img.Image.Assign(bmp);
finally
bmp.Free;
end;
end;
wifPng:
begin
png:=TPngImage.Create;
str:=TMemoryStream.Create;
try
img.Image.SaveToStream(str);
str.Position:=0;
png.LoadFromStream(str);
sTag:=IntToStr(png.Height);
png.Canvas.Font.Name:='Small Fonts';
png.Canvas.Font.Size:=6;
rSize:=png.Canvas.TextExtent(sTag);
r.Top:=0;
r.Left:=0;
r.Width:=rSize.Width;
r.Height:=rSize.Height;
// knock out transparency in that area
for Y := r.Top to r.Bottom - 1 do
for X := r.Left to r.Right - 1 do
begin
Alpha := #png.AlphaScanline[Y]^[X];
Alpha^ := 255; // opaque
end;
png.Canvas.Brush.Color:=clWhite;
png.Canvas.Brush.Style:=bsSolid;
png.Canvas.Font.Color:=clRed;
png.Canvas.Pen.Style:=psSolid;
png.Canvas.TextOut(r.Left, r.Top, sTag);
str.Clear;
png.SaveToStream(str);
item.SourceImages.Delete(j);
icsiNew:=item.SourceImages.Add;
str.Position:=0;
icsiNew.Image.LoadFromStream(str);
finally
png.Free;
str.Free;
end;
end;
wifJpeg:
;
wifGif:
;
wifTiff:
;
wifWMPhoto:
;
wifOther:
;
end;
end;
end;
end;

In Delphi, how does TBitmap.Monochrome and .PixelFormat influcence the format of .ScanLine?

I want to assign a given buffer with a bitmap in Mono8 format (Monochrome 8 Bits) to a bitmap. I then assign the resulting bitmap to a TImage component to display it. The pictures are screenshots of the resulting display.
The following code works but seems a bit wasteful:
procedure CopyToBitmapMono824(_Buffer: PByte; _Bmp: TBitmap);
var
y: Integer;
x: Integer;
ScanLine: PdzRgbTripleArray;
begin
for y := 0 to _Bmp.Height - 1 do begin
ScanLine := _Bmp.ScanLine[y];
for x := 0 to _Bmp.Width - 1 do begin
// monochrome: all 3 colors set to the same value
ScanLine[x].Red := _Buffer^;
ScanLine[x].Green := _Buffer^;
ScanLine[x].Blue := _Buffer^;
Inc(_Buffer);
end;
end;
end;
// [...]
fBmp.PixelFormat := pf24Bit;
FBmp.Monochrome := False;
CopyToBitmap(Buffer, fBmp);
I would rather use a bitmap in pf8Bit format which I tried:
procedure CopyToBitmapMono8(_Buffer: PByte; _Bmp: TBitmap);
var
y: Integer;
x: Integer;
ScanLine: PByteArray;
begin
for y := 0 to _Bmp.Height - 1 do begin
ScanLine := _Bmp.ScanLine[y];
for x := 0 to _Bmp.Width - 1 do begin
ScanLine[x] := _Buffer^;
Inc(_Buffer);
end;
end;
end;
// [...]
FBmp.PixelFormat := pf8bit;
FBmp.Monochrome := False; // I also tried Monochrome := true
CopyToBitmapMono8(Buffer, FBmp)
If MonoChrome is true, the picture only has about 1/4 of the expected width, the rest is white.
If MonoChrome is false, the picture has the expected width, but the left 1/4 of it is monochrome, the rest contains false colors.
I'm obviously missing something, but what?
EDIT: The effect that the bitmap is only 1/4 of the expected size apparently was a side effect of converting it to a JPEG for saving prior to displaying it (code that I did not show above, mea culpa). So the problem was simply that I did not set a monochrome palette for the bitmap.
Monochrome has sense for pf1bit bitmaps.
Otherwise Monochrome := True changes bitmap format to DDB (pfDevice). Your screen is 32-bit, so call to Scanline caused DibNeeded call and transformation to 32bit, and using of function CopyToBitmapMono8 (intended for 8-bit) filled only 1/4 of screen.
For proper usage of 8-bit bitmaps you have to change standard weird palette (used in the right part of last image) to gray one.
procedure CopyToBitmapMono8(_Buffer: PByte; _Bmp: TBitmap);
var
y: Integer;
x: Integer;
ScanLine: PByteArray;
begin
for y := 0 to _Bmp.Height - 1 do begin
ScanLine := _Bmp.ScanLine[y];
for x := 0 to _Bmp.Width - 1 do begin
ScanLine[x] := _Buffer^;
Inc(_Buffer);
end;
end;
end;
var
FBmp: TBitmap;
Buffer: PbyteArray;
i: integer;
begin
GetMem(Buffer, 512 * 100);
for i := 0 to 512 * 100 - 1 do
Buffer[i] := (i and 511) div 2; // gray gradient
FBmp := Tbitmap.Create;
FBmp.Width := 512;
FBmp.Height := 100;
FBmp.PixelFormat := pf8bit;
CopyToBitmapMono8(PByte(Buffer), FBmp);
Canvas.Draw(0, 0, FBmp);
//now right approach
FBmp.Palette := MakeGrayPalette; // try to comment
CopyToBitmapMono8(PByte(Buffer), FBmp);
Canvas.Draw(0, 110, FBmp);
end;
function TForm1.MakeGrayPalette: HPalette;
var
i: integer;
lp: TMaxLogPalette;
begin
lp.palVersion := $300;
lp.palNumEntries := 256;
for i := 0 TO 255 do begin
lp.palPalEntry[i].peRed := i;
lp.palPalEntry[i].peGreen := i;
lp.palPalEntry[i].peBlue := i;
lp.palPalEntry[i].peFlags := PC_RESERVED;
end;
Result := CreatePalette(pLogPalette(#lp)^);
end;
And example at efg2 page

How do I draw with scanlines without loading an image first?

I'm trying to do the following:
bmp := TBitmap.Create;
bmp.Width := FWidth;
bmp.Height := FHeight;
for y := 0 to FHeight - 1 do
begin
sl := bmp.ScanLine[y];
for x := 0 to FWidth - 1 do
begin
//draw to the scanline, one pixel at a time
end;
end;
//display the image
bmp.Free;
Unfortunately, what I end up with is an image that's completely white, except for the bottom line, which is colored appropriately. A bit of debugging shows that each time I access the ScanLine property, it's calling TBitmap.FreeImage, and going into the if (FHandle <> 0) and (FHandle <> FDIBHandle) then block, which resets the whole image, so only the changes to the last line actually take.
In every demo I've seen so far using TBitmap.ScanLine, they start out by loading an image. (Apparently this sets up various handles correctly so that this doesn't end up happening?) But I'm not trying to load an image and work on it; I'm trying to capture image data from a camera.
How can I set up the bitmap so that I can draw to the scanlines without having to load an image first?
You should set the PixelFormat explicitly before starting to draw. For instance,
procedure TForm1.FormPaint(Sender: TObject);
var
bm: TBitmap;
y: Integer;
sl: PRGBQuad;
x: Integer;
begin
bm := TBitmap.Create;
try
bm.SetSize(1024, 1024);
bm.PixelFormat := pf32bit;
for y := 0 to bm.Height - 1 do
begin
sl := bm.ScanLine[y];
for x := 0 to bm.Width - 1 do
begin
sl.rgbBlue := 255 * x div bm.Width;
sl.rgbRed := 255 * y div bm.Height;
sl.rgbGreen := 255 * x div bm.Width;
inc(sl);
end;
end;
Canvas.Draw(0, 0, bm);
finally
bm.Free;
end;
end;

How to save an imagelist as one bitmap like a tilesheet?

I have an image list containing several bitmaps which I would like to save together as one single bitmap, but I need it saving just like how a spritesheet or tilesheet is drawn in 2d and rpg games etc.
Typically the tilesheet is drawn with several images across (in a row), so for example if I wanted a maximum of 6 images per row, it will only draw 6, with further images been drawn underneath in a new row.
I can save it in one single row like so:
var
CurrentFrame: Integer;
StripWidth: Integer;
Strip: TBitmap;
Bmp: TBitmap;
I: Integer;
begin
if SaveDialog.Execute then
begin
StripWidth := ImageList1.Width * ImageList1.Count - ImageList1.Width;
CurrentFrame := - ImageList1.Width;
Strip := TBitmap.Create;
try
Strip.SetSize(StripWidth, ImageList1.Height);
Bmp := TBitmap.Create;
try
for I := 0 to ImageList1.Count - 1 do
begin
CurrentFrame := CurrentFrame + ImageList1.Width;
ImageList1.GetImage(I, Bmp);
Strip.Canvas.Draw(CurrentFrame, 0, Bmp);
end;
finally
Bmp.Free;
end;
Strip.SaveToFile(SaveDialog.FileName);
finally
Strip.Free;
end;
end;
end;
So imagine the result for the above is:
The result I want is something like:
So the above would have considered in the procedure/ function a parameter to allow only 3 images per row as an example.
How do I export all images from an imagelist into one single bitmap, allowing only x amount if images to be drawn horizontally before creating a new row?
Thanks.
EDIT
Thanks to David's answer, I put together these procedures:
procedure DrawImageOnSheet(Images: TImageList; Sheet: TBitmap;
ImageIndex, X, Y: Integer);
var
Bitmap: TBitmap;
begin
Bitmap := TBitmap.Create;
try
Images.GetBitmap(ImageIndex, Bitmap);
Sheet.Canvas.Draw(X, Y, Bitmap);
finally
Bitmap.Free;
end;
end;
procedure SaveImageListAsSheet(Images: TImageList; FileName: string;
NumberOfColumns: Integer);
var
Sheet: TBitmap;
nImage: Integer;
nCol: Integer;
nRow: Integer;
nToDraw: Integer;
nRemaining: Integer;
ImageIndex: Integer;
X, Y: Integer;
I: Integer;
begin
Sheet := TBitmap.Create;
try
nImage := Images.Count;
nCol := NumberOfColumns;
nRow := (nImage + nCol - 1) div nCol;
Sheet.Height := nRow * Images.Height;
Sheet.Width := nCol * Images.Width;
nRemaining := nImage;
ImageIndex := 0;
Y := 0;
while nRemaining > 0 do
begin
nToDraw := Math.Min(nRemaining, nCol);
X := 0;
for I := 0 to nToDraw - 1 do
begin
DrawImageOnSheet(Images, Sheet, ImageIndex, X, Y);
Inc(ImageIndex);
Inc(X, Images.Width);
end;
Inc(Y, Images.Height);
Dec(nRemaining, nToDraw);
end;
Sheet.SaveToFile(FileName);
finally
Sheet.Free;
end;
end;
According to clarification from the comments, you are struggling with the counting of the images, the organisation of the rows/columns and so on. So, let's assume you already have this function which draws image ImageIndex to the output bitmap at a position of X, Y.
procedure Draw(ImageIndex, X, Y: Integer);
Let's also assume that the images have dimensions given by ImageWidth and ImageHeight. Finally, there are nImage images and you want to have nCol images per column.
So, first of all, how many rows do you need?
nRow := (nImage + nCol - 1) div nCol;
Now you can set the size of the output bitmap. Its width is nCol * ImageWidth and its height is nRow * ImageHeight.
Now to draw the images.
nRemaining := nImage;
ImageIndex := 0;
Y := 0;
while nRemaining > 0 do
begin
nToDraw := Math.Min(nRemaining, nCol);
X := 0;
for i := 0 to nToDraw - 1 do
begin
Draw(ImageIndex, X, Y);
inc(ImageIndex);
inc(X, ImageWidth);
end;
inc(Y, ImageHeight);
dec(nRemaining, nToDraw);
end;

Paint PNG image on other PNG

How to merge two PNG's together? I know that you can't use PNGObject.Draw because it doesn't copy alpha transaperncy (I am not sure but it doesn't work anyway) so custom procedure/function is needed. I didn't come with empty hands, I have this procedure:
procedure MergePNGLayer(Layer1,Layer2: TPNGObject; Const aLeft,aTop:Integer);
var
x, y: Integer;
SL1, SL2, SLBlended : pRGBLine;
aSL1, aSL2, aSLBlended: PByteArray;
blendCoeff: single;
blendedPNG, Lay2buff: TPNGObject;
begin
blendedPNG:=TPNGObject.Create;
blendedPNG.Assign(Layer1);
Lay2buff:=TPNGObject.Create;
Lay2buff.Assign(Layer2);
SetPNGCanvasSize(Layer2,Layer1.Width,Layer1.Height,aLeft,aTop);
for y := 0 to Layer1.Height-1 do
begin
SL1 := Layer1.Scanline[y];
SL2 := Layer2.Scanline[y];
aSL1 := Layer1.AlphaScanline[y];
aSL2 := Layer2.AlphaScanline[y];
SLBlended := blendedPNG.Scanline[y];
aSLBlended := blendedPNG.AlphaScanline[y];
for x := 0 to Layer1.Width-1 do
begin
blendCoeff:=aSL1[x] * 100/255/100;
aSLBlended[x] := round(aSL2[x] + (aSL1[x]-aSL2[x]) * blendCoeff);
SLBlended[x].rgbtRed := round(SL2[x].rgbtRed + (SL1[x].rgbtRed-SL2[x].rgbtRed) * blendCoeff);
SLBlended[x].rgbtGreen := round(SL2[x].rgbtGreen + (SL1[x].rgbtGreen-SL2[x].rgbtGreen) * blendCoeff);
SLBlended[x].rgbtBlue := round(SL2[x].rgbtBlue + (SL1[x].rgbtBlue-SL2[x].rgbtBlue) * blendCoeff);
end;
end;
Layer1.Assign(blendedPNG);
Layer2.Assign(Lay2buff);
blendedPNG.Free;
Lay2buff.Free;
end;
But sadly it doesn't work how it should, it does the job but not right. When it merges empty image with loaded image, it works fine, but when both images aren't empty, it makes them loss transparancy.
procedure TForm1.FormClick(Sender: TObject);
var
PNG1, PNG2, PNG3, Dest: TPNGObject;
begin
PNG1 := TPNGObject.Create;
PNG2 := TPNGObject.Create;
PNG3 := TPNGObject.Create;
PNG1.LoadFromFile('Aero\TopLeft.png');//Width 10
PNG2.LoadFromFile('Aero\Top.png');//Width 200
PNG3.LoadFromFile('Aero\TopRight.png');//Width 10
Dest := TPNGObject.CreateBlank(COLOR_RGBALPHA, 16, 220, 10);
MergePNGLayer(Dest, PNG1, 0, 0);
MergePNGLayer(Dest, PNG2, 10, 0);
MergePNGLayer(Dest, PNG3, 210, 0);
Dest.SaveToFile('C:\OUT.PNG');
end;
Wanted result:
Actual result:
I am not sure if you can see differences between these imgaes, but you should open these in PNG editor software and you will see the difference. So I need other procedure to merge PNGs. I am using newest version of PNGImage by the way.
Thanks and have a good day!
This seems to work just fine:
procedure DrawPngWithAlpha(Src, Dest: TPNGObject; const R: TRect);
var
X, Y: Integer;
Alpha: PByte;
begin
Src.Draw(Dest.Canvas, R);
// I have no idea why standard implementation of TPNGObject.Draw doesn't apply transparency.
for Y := R.Top to R.Bottom - 1 do
for X := R.Left to R.Right - 1 do
begin
Alpha := #Dest.AlphaScanline[Y]^[X];
Alpha^ := Min(255, Alpha^ + Src.AlphaScanline[Y - R.Top]^[X - R.Left]);
end;
end;

Resources