已经有许多问题处理90°间隔的旋转,大多数不合适this one,但是我想要旋转一个真正的角度。最好有可能由于旋转而调整图像尺寸,并为要添加到图像表面的部件设置自定义(透明)背景颜色。那么我假设例程的签名看起来像:
procedure RotateBitmap(Bmp: TBitmap; Angle: Single; AdjustSize: Boolean; BackColor: TColor);
These answers提到以下候选人构建这个例程:SetWorldTransform,PlgBlt,GDI,但我想看到(高效)的实现。
解决方法
tl;dr; Use GDI+
SetWorldTransform
使用WinAPI的SetWorldTransform,您可以转换设备上下文的空间:旋转,剪切,偏移和缩放。这是通过设置XFORM类型的变换矩阵的成员完成的。按照the documentation填写会员。
procedure RotateBitmap(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean; BkColor: TColor = clNone); var C: Single; S: Single; XForm: tagXFORM; Tmp: TBitmap; begin C := Cos(Rads); S := Sin(Rads); XForm.eM11 := C; XForm.eM12 := S; XForm.eM21 := -S; XForm.eM22 := C; Tmp := TBitmap.Create; try Tmp.TransparentColor := Bmp.TransparentColor; Tmp.TransparentMode := Bmp.TransparentMode; Tmp.Transparent := Bmp.Transparent; Tmp.Canvas.Brush.Color := BkColor; if AdjustSize then begin Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S)); Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C)); XForm.eDx := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2; XForm.eDy := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2; end else begin Tmp.Width := Bmp.Width; Tmp.Height := Bmp.Height; XForm.eDx := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2; XForm.eDy := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2; end; SetGraphicsMode(Tmp.Canvas.Handle,GM_ADVANCED); SetWorldTransform(Tmp.Canvas.Handle,XForm); BitBlt(Tmp.Canvas.Handle,Tmp.Width,Tmp.Height,Bmp.Canvas.Handle,SRCCOPY); Bmp.Assign(Tmp); finally Tmp.Free; end; end;
PlgBlt
PlgBlt功能执行从源设备上下文中指定的矩形到目标设备上下文中指定的平行四边形的位块传输。通过lpPoint参数映射源图像的角点。
procedure RotateBitmap(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean; BkColor: TColor = clNone); var C: Single; S: Single; Tmp: TBitmap; OffsetX: Single; OffsetY: Single; Points: array[0..2] of TPoint; begin C := Cos(Rads); S := Sin(Rads); Tmp := TBitmap.Create; try Tmp.TransparentColor := Bmp.TransparentColor; Tmp.TransparentMode := Bmp.TransparentMode; Tmp.Transparent := Bmp.Transparent; Tmp.Canvas.Brush.Color := BkColor; if AdjustSize then begin Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S)); Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C)); OffsetX := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2; OffsetY := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2; end else begin Tmp.Width := Bmp.Width; Tmp.Height := Bmp.Height; OffsetX := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2; OffsetY := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2; end; Points[0].X := Round(OffsetX); Points[0].Y := Round(OffsetY); Points[1].X := Round(OffsetX + Bmp.Width * C); Points[1].Y := Round(OffsetY + Bmp.Width * S); Points[2].X := Round(OffsetX - Bmp.Height * S); Points[2].Y := Round(OffsetY + Bmp.Height * C); PlgBlt(Tmp.Canvas.Handle,Points,Bmp.Width,Bmp.Height,0); Bmp.Assign(Tmp); finally Tmp.Free; end; end;
Graphics32
Graphics32是专门用于快速位图处理的库。它需要一些经验来掌握其全部潜力,但the documentation以及所提供的示例应该让您开始。
TBitmap32图像的旋转是通过将许多可用的转换类之一进行转换来完成的。在这里需要TAffineTransformation类。首先,将图像的一半大小移动到左上角,然后旋转,然后将结果移回右下角,可能使用新的图像尺寸。
uses GR32,GR32_Transforms; procedure RotateBitmap(Bmp: TBitmap32; Degs: Integer; AdjustSize: Boolean; BkColor: TColor = clNone; Transparent: Boolean = False); overload; var Tmp: TBitmap32; Transformation: TAffineTransformation; begin Tmp := TBitmap32.Create; Transformation := TAffineTransformation.Create; try Transformation.BeginUpdate; Transformation.SrcRect := FloatRect(0,Bmp.Height); Transformation.Translate(-0.5 * Bmp.Width,-0.5 * Bmp.Height); Transformation.Rotate(0,-Degs); if AdjustSize then with Transformation.GetTransformedBounds do Tmp.SetSize(Round(Right - Left),Round(Bottom - Top)) else Tmp.SetSize(Bmp.Width,Bmp.Height); Transformation.Translate(0.5 * Tmp.Width,0.5 * Tmp.Height); Transformation.EndUpdate; Tmp.Clear(Color32(BkColor)); if not Transparent then Bmp.DrawMode := dmTransparent; Transform(Tmp,Bmp,Transformation); Bmp.Assign(Tmp); Bmp.OuterColor := Color32(BkColor); if Transparent then Bmp.DrawMode := dmTransparent; finally Transformation.Free; Tmp.Free; end; end; procedure RotateBitmap(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean; BkColor: TColor = clNone); overload; var Tmp: TBitmap32; Transparent: Boolean; begin Tmp := TBitmap32.Create; try Transparent := Bmp.Transparent; Tmp.Assign(Bmp); RotateBitmapGR32(Tmp,Degs,AdjustSize,BkColor,Transparent); Bmp.Assign(Tmp); if Transparent then Bmp.Transparent := True; finally Tmp.Free; end; end;
GDI
在Windows XP中引入,Microsoft的GDI+ API比默认的GDI API更有效率。对于Delphi 2009及以上版本,库可用from here.对于较旧的Delphi版本,库可用from here。
在GDI中,旋转也由变换矩阵完成。绘画的作品有很大的不同。创建TGPGraphics对象并将其附加到其构造函数的设备上下文中。随后,通过API翻译对象的绘制操作,并将其输出到目标上下文。
uses GDIPOBJ,GDIPAPI; // < D2009 GdiPlus; // >= D2009 procedure RotateBitmap(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean; BkColor: TColor = clNone); var Tmp: TGPBitmap; Matrix: TGPMatrix; C: Single; S: Single; NewSize: TSize; Graphs: TGPGraphics; P: TGPPointF; begin Tmp := TGPBitmap.Create(Bmp.Handle,Bmp.Palette); Matrix := TGPMatrix.Create; try Matrix.RotateAt(Degs,MakePoint(0.5 * Bmp.Width,0.5 * Bmp.Height)); if AdjustSize then begin C := Cos(DegToRad(Degs)); S := Sin(DegToRad(Degs)); NewSize.cx := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S)); NewSize.cy := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C)); Bmp.Width := NewSize.cx; Bmp.Height := NewSize.cy; end; Graphs := TGPGraphics.Create(Bmp.Canvas.Handle); try Graphs.Clear(ColorRefToARGB(ColorToRGB(BkColor))); Graphs.SetTransform(Matrix); Graphs.DrawImage(Tmp,(Cardinal(Bmp.Width) - Tmp.GetWidth) div 2,(Cardinal(Bmp.Height) - Tmp.GetHeight) div 2); finally Graphs.Free; end; finally Matrix.Free; Tmp.Free; end; end;
处理透明度
上面的例程保存了fead位图的透明设置,但需要一个额外的Transparent参数的Graphics32解决方案除外。
性能和图像质量
我写了一个测试应用程序(见下面的完整代码)来调整各种方法的性能,并比较所得到的图像质量。
第一个也是最重要的结论是,GDI使用反锯齿,其他的没有别的,导致最好的图像质量。 (我没有成功尝试通过设置CompositingQuality,InterpolationMode,SmoothingMode和PixelOffsetMode来防止抗锯齿,所以当不使用别名时不要使用GDI。)
unit RotateTestForm; interface uses Windows,SysUtils,Classes,Graphics,Controls,Forms,Dialogs,ExtCtrls,JPEG,Math,GR32,GR32_Transforms,GDIPOBJ,GDIPAPI {,GdiPlus}; type TTestForm = class(TForm) private FImage: TImage; FOpenDialog: TOpenDialog; procedure FormPaint(Sender: TObject); public constructor Create(AOwner: TComponent); override; end; var TestForm: TTestForm; implementation {$R *.dfm} procedure RotateBitmapSWT(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean; BkColor: TColor = clNone); var C: Single; S: Single; XForm: TXForm; Tmp: TBitmap; begin C := Cos(Rads); S := Sin(Rads); XForm.eM11 := C; XForm.eM12 := S; XForm.eM21 := -S; XForm.eM22 := C; Tmp := TBitmap.Create; try Tmp.TransparentColor := Bmp.TransparentColor; Tmp.TransparentMode := Bmp.TransparentMode; Tmp.Transparent := Bmp.Transparent; Tmp.Canvas.Brush.Color := BkColor; if AdjustSize then begin Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S)); Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C)); XForm.eDx := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2; XForm.eDy := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2; end else begin Tmp.Width := Bmp.Width; Tmp.Height := Bmp.Height; XForm.eDx := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2; XForm.eDy := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2; end; SetGraphicsMode(Tmp.Canvas.Handle,SRCCOPY); Bmp.Assign(Tmp); finally Tmp.Free; end; end; procedure RotateBitmapPLG(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean; BkColor: TColor = clNone); var C: Single; S: Single; Tmp: TBitmap; OffsetX: Single; OffsetY: Single; Points: array[0..2] of TPoint; begin C := Cos(Rads); S := Sin(Rads); Tmp := TBitmap.Create; try Tmp.TransparentColor := Bmp.TransparentColor; Tmp.TransparentMode := Bmp.TransparentMode; Tmp.Transparent := Bmp.Transparent; Tmp.Canvas.Brush.Color := BkColor; if AdjustSize then begin Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S)); Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C)); OffsetX := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2; OffsetY := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2; end else begin Tmp.Width := Bmp.Width; Tmp.Height := Bmp.Height; OffsetX := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2; OffsetY := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2; end; Points[0].X := Round(OffsetX); Points[0].Y := Round(OffsetY); Points[1].X := Round(OffsetX + Bmp.Width * C); Points[1].Y := Round(OffsetY + Bmp.Width * S); Points[2].X := Round(OffsetX - Bmp.Height * S); Points[2].Y := Round(OffsetY + Bmp.Height * C); PlgBlt(Tmp.Canvas.Handle,0); Bmp.Assign(Tmp); finally Tmp.Free; end; end; procedure RotateBitmapGR32(Bmp: TBitmap32; Degs: Integer; AdjustSize: Boolean; BkColor: TColor = clNone; Transparent: Boolean = False); overload; var Tmp: TBitmap32; Transformation: TAffineTransformation; begin Tmp := TBitmap32.Create; Transformation := TAffineTransformation.Create; try Transformation.BeginUpdate; Transformation.SrcRect := FloatRect(0,Transformation); Bmp.Assign(Tmp); Bmp.OuterColor := Color32(BkColor); if Transparent then Bmp.DrawMode := dmTransparent; finally Transformation.Free; Tmp.Free; end; end; procedure RotateBitmapGR32(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean; BkColor: TColor = clNone); overload; var Tmp: TBitmap32; Transparent: Boolean; begin Tmp := TBitmap32.Create; try Transparent := Bmp.Transparent; Tmp.Assign(Bmp); RotateBitmapGR32(Tmp,Transparent); Bmp.Assign(Tmp); if Transparent then Bmp.Transparent := True; finally Tmp.Free; end; end; procedure RotateBitmapGDIP(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean; BkColor: TColor = clNone); var Tmp: TGPBitmap; Matrix: TGPMatrix; C: Single; S: Single; NewSize: TSize; Graphs: TGPGraphics; P: TGPPointF; begin Tmp := TGPBitmap.Create(Bmp.Handle,(Cardinal(Bmp.Height) - Tmp.GetHeight) div 2); finally Graphs.Free; end; finally Matrix.Free; Tmp.Free; end; end; { TTestForm } constructor TTestForm.Create(AOwner: TComponent); begin inherited Create(AOwner); Font.Name := 'Tahoma'; Top := 0; ClientWidth := 560; ClientHeight := 915; Show; FImage := TImage.Create(Self); FOpenDialog := TOpenDialog.Create(Self); FOpenDialog.Title := 'Select an small sized image (min. 100 x 100)'; FOpenDialog.Options := FOpenDialog.Options + [ofFileMustExist]; FOpenDialog.Filter := 'JPEG|*.JPG|BMP|*.BMP'; if FOpenDialog.Execute then begin FImage.Picture.LoadFromFile(FOpenDialog.FileName); OnPaint := FormPaint; Invalidate; end else Application.Terminate; end; procedure TTestForm.FormPaint(Sender: TObject); var Img: TBitmap; Bmp: TBitmap; Bmp32: TBitmap32; BkColor: TColor; AdjustSize: Boolean; Degs: Integer; Rads: Single; RotCount: Integer; I: Integer; Tick: Cardinal; begin Img := TBitmap.Create; Bmp := TBitmap.Create; Bmp32 := TBitmap32.Create; try BkColor := clBtnFace; Img.Canvas.Brush.Color := BkColor; Img.Width := 100; Img.Height := 100; Img.Canvas.Draw(0,FImage.Picture.Graphic); AdjustSize := False; Degs := 45; Rads := DegToRad(Degs); RotCount := 1000; Canvas.TextOut(10,10,'Original:'); Canvas.Draw(10,30,Img); Canvas.TextOut(10,140,Format('Size = %d x %d',[Img.Width,Img.Height])); Canvas.TextOut(10,160,Format('Angle = %d°',[Degs])); Canvas.TextOut(10,250,Format('%d rotations:',[RotCount])); Canvas.TextOut(120,'SetWorldTransform:'); Bmp.Assign(Img); RotateBitmapSWT(Bmp,Rads,BkColor); Canvas.Draw(120,Bmp); if not AdjustSize then begin Tick := GetTickCount; for I := 0 to RotCount - 2 do RotateBitmapSWT(Bmp,BkColor); Canvas.TextOut(120,Format('%d msec',[GetTickCount - Tick])); Canvas.Draw(120,Bmp); end; Canvas.TextOut(230,'PlgBlt:'); Bmp.Assign(Img); RotateBitmapPLG(Bmp,BkColor); Canvas.Draw(230,Bmp); if not AdjustSize then begin Tick := GetTickCount; for I := 0 to RotCount - 2 do RotateBitmapPLG(Bmp,BkColor); Canvas.TextOut(230,[GetTickCount - Tick])); Canvas.Draw(230,Bmp); end; Canvas.TextOut(340,'Graphics32:'); Bmp.Assign(Img); RotateBitmapGR32(Bmp,BkColor); Canvas.Draw(340,Bmp); if not AdjustSize then begin Tick := GetTickCount; for I := 0 to RotCount - 2 do RotateBitmapGR32(Bmp,BkColor); Canvas.TextOut(340,[GetTickCount - Tick])); Canvas.Draw(340,Bmp); // Without in between conversion to TBitmap: Bmp32.Assign(Img); Tick := GetTickCount; for I := 0 to RotCount - 1 do RotateBitmapGR32(Bmp32,False); Canvas.TextOut(340,270,Format('%d msec (optimized)',[GetTickCount - Tick])); end; Canvas.TextOut(450,'GDI+ :'); Bmp.Assign(Img); RotateBitmapGDIP(Bmp,BkColor); Canvas.Draw(450,Bmp); if not AdjustSize then begin Tick := GetTickCount; for I := 0 to RotCount - 2 do RotateBitmapGDIP(Bmp,BkColor); Canvas.TextOut(450,[GetTickCount - Tick])); Canvas.Draw(450,Bmp); end; finally Bmp32.Free; Bmp.Free; Img.Free; OnPaint := nil; end; end; end.