{*******************************************************}
{                                                       }
{       Tocsg.Graphic                                   }
{                                                       }
{       Copyright (C) 2023 kku                          }
{                                                       }
{*******************************************************}

unit Tocsg.Graphic;

interface

uses
  Vcl.Graphics, System.SysUtils, Winapi.Windows, Vcl.Imaging.pngimage,
  Winapi.GDIPAPI;

type
  PColorMatrix = ^TColorMatrix;

function RotatePng(var aPng: TPNGImage; Angle: Extended): Boolean;

function RotatePngFile(sSrcPath, sDestpath: String; Angle: Extended): Boolean;

// RotateBitmap_STF() 이건 이미지 돌린 후 가로 넓이 AdjustSize 적용이 제대로 되지 않는다... 짤림
procedure RotateBitmap_STF(Bmp: Vcl.Graphics.TBitmap; Rads: Single; AdjustSize: Boolean;
  BkColor: TColor = clNone; nAddWidth: Integer = 0);
procedure RotateBitmap_PlgBlt(Bmp: Vcl.Graphics.TBitmap; Rads: Single; AdjustSize: Boolean;
  BkColor: TColor = clNone);
function ScalePercentBmp(bitmp: Vcl.Graphics.TBitmap; iPercent: Integer{100이면 원본}): Boolean;

function MakeColorMatrix(R, G, B, A: Single): TColorMatrix;
function MakeTransparentMatrix(A: Single): TColorMatrix;

procedure InitializeGDIPlus;
procedure FinalizeGDIPlus;

function DrawBitmapWaterEx(aDestDC: HDC; nX, nY: Integer; aSrcBmp: Vcl.Graphics.TBitmap; pCM: PColorMatrix = nil; nStretchW: Integer = 0; nStretchH: Integer = 0): Boolean;
function DrawBitmapWaterEx2(aDestDC: HDC; nX, nY: Integer; aSrcBmp: Vcl.Graphics.TBitmap; pCM: PColorMatrix = nil; nStretchW: Integer = 0; nStretchH: Integer = 0; fAngle: Single = 0): Boolean;
function DrawBitmapWaterEx3(
  aDestDC: HDC;
  nX, nY: Integer;
  aSrcBmp: Vcl.Graphics.TBitmap;
  pCM: PColorMatrix = nil;
  nStretchW: Integer = 0;
  nStretchH: Integer = 0;
  fAngle: Single = 0
): Boolean;

implementation

uses
  Tocsg.Safe, Tocsg.Exception, System.Classes, Winapi.GDIPOBJ, Winapi.ActiveX, System.UIConsts, System.Math;

// 이걸로 돌리면 아래에 검은선이 생긴다. 25_0401 10:07:20 kku
//function RotatePng(var aPng: TPNGImage; Angle: Extended): Boolean;
//
//  {Supporting functions}
//  function TrimInt(i, Min, Max: Integer): Integer;
//  begin
//    if      i>Max then Result:=Max
//    else if i<Min then Result:=Min
//    else               Result:=i;
//  end;
//  function IntToByte(i:Integer):Byte;
//  begin
//    if      i>255 then Result:=255
//    else if i<0   then Result:=0
//    else               Result:=i;
//  end;
//  function Min(A, B: Double): Double;
//  begin
//    if A < B then Result := A else Result := B;
//  end;
//  function Max(A, B: Double): Double;
//  begin
//    if A > B then Result := A else Result := B;
//  end;
//  function Ceil(A: Double): Integer;
//  begin
//    Result := Integer(Trunc(A));
//    if Frac(A) > 0 then
//      Inc(Result);
//  end;
//
//  {Calculates the png new size}
//  function newsize: tsize;
//  var
//    fRadians: Extended;
//    fCosine, fSine: Double;
//    fPoint1x, fPoint1y, fPoint2x, fPoint2y, fPoint3x, fPoint3y: Double;
//    fMinx, fMiny, fMaxx, fMaxy: Double;
//  begin
//    {Convert degrees to radians}
//    fRadians := (2 * PI * Angle) / 360;
//
//    fCosine := abs(cos(fRadians));
//    fSine := abs(sin(fRadians));
//
//    fPoint1x := (-apng.Height * fSine);
//    fPoint1y := (apng.Height * fCosine);
//    fPoint2x := (apng.Width * fCosine - apng.Height * fSine);
//    fPoint2y := (apng.Height * fCosine + apng.Width * fSine);
//    fPoint3x := (apng.Width * fCosine);
//    fPoint3y := (apng.Width * fSine);
//
//    fMinx := min(0,min(fPoint1x,min(fPoint2x,fPoint3x)));
//    fMiny := min(0,min(fPoint1y,min(fPoint2y,fPoint3y)));
//    fMaxx := max(fPoint1x,max(fPoint2x,fPoint3x));
//    fMaxy := max(fPoint1y,max(fPoint2y,fPoint3y));
//
//    Result.cx := ceil(fMaxx-fMinx);
//    Result.cy := ceil(fMaxy-fMiny);
//  end;
//type
//  TFColor  = record b,g,r:Byte end;
//var
//  Top, Bottom, Left, Right, eww,nsw, fx,fy, wx,wy: Extended;
//  cAngle, sAngle:   Double;
//  xDiff, yDiff, ifx,ify, px,py, ix,iy, x,y, cx, cy: Integer;
//  nw,ne, sw,se: TFColor;
//  anw,ane, asw,ase: Byte;
//  P1,P2,P3:Pbytearray;
//  A1,A2,A3: pbytearray;
//  dst: TPNGImage;
//  IsAlpha: Boolean;
//  new_colortype: Integer;
//begin
//  Result := false;
//  try
//    {Only allows RGB and RGBALPHA images}
//    if not (apng.Header.ColorType in [COLOR_RGBALPHA, COLOR_RGB]) then
//      raise Exception.Create('Only COLOR_RGBALPHA and COLOR_RGB formats' +
//      ' are supported');
//    IsAlpha := apng.Header.ColorType in [COLOR_RGBALPHA];
//    if IsAlpha then new_colortype := COLOR_RGBALPHA else
//      new_colortype := COLOR_RGB;
//
//    {Creates a copy}
//    dst := tpngobject.Create;
//    with newsize do
//      dst.createblank(new_colortype, 8, cx, cy);
//    cx := dst.width div 2; cy := dst.height div 2;
//
//    {Gather some variables}
//    Angle:=angle;
//    Angle:=-Angle*Pi/180;
//    sAngle:=Sin(Angle);
//    cAngle:=Cos(Angle);
//    xDiff:=(Dst.Width-apng.Width)div 2;
//    yDiff:=(Dst.Height-apng.Height)div 2;
//
//    {Iterates over each line}
//    for y:=0 to Dst.Height-1 do
//    begin
//      P3:=Dst.scanline[y];
//      if IsAlpha then A3 := Dst.AlphaScanline[y];
//      py:=2*(y-cy)+1;
//      {Iterates over each column}
//      for x:=0 to Dst.Width-1 do
//      begin
//        px:=2*(x-cx)+1;
//        fx:=(((px*cAngle-py*sAngle)-1)/ 2+cx)-xDiff;
//        fy:=(((px*sAngle+py*cAngle)-1)/ 2+cy)-yDiff;
//        ifx:=Round(fx);
//        ify:=Round(fy);
//
//        {Only continues if it does not exceed image boundaries}
//        if(ifx>-1)and(ifx<apng.Width)and(ify>-1)and(ify<apng.Height)then
//        begin
//          {Obtains data to paint the new pixel}
//          eww:=fx-ifx;
//          nsw:=fy-ify;
//          iy:=TrimInt(ify+1,0,apng.Height-1);
//          ix:=TrimInt(ifx+1,0,apng.Width-1);
//          P1:=apng.scanline[ify];
//          P2:=apng.scanline[iy];
//          if IsAlpha then A1 := apng.alphascanline[ify];
//          if IsAlpha then A2 := apng.alphascanline[iy];
//          nw.r:=P1[ifx*3];
//          nw.g:=P1[ifx*3+1];
//          nw.b:=P1[ifx*3+2];
//          if IsAlpha then anw:=A1[ifx];
//          ne.r:=P1[ix*3];
//          ne.g:=P1[ix*3+1];
//          ne.b:=P1[ix*3+2];
//          if IsAlpha then ane:=A1[ix];
//          sw.r:=P2[ifx*3];
//          sw.g:=P2[ifx*3+1];
//          sw.b:=P2[ifx*3+2];
//          if IsAlpha then asw:=A2[ifx];
//          se.r:=P2[ix*3];
//          se.g:=P2[ix*3+1];
//          se.b:=P2[ix*3+2];
//          if IsAlpha then ase:=A2[ix];
//
//
//          {Defines the new pixel}
//          Top:=nw.b+eww*(ne.b-nw.b);
//          Bottom:=sw.b+eww*(se.b-sw.b);
//          P3[x*3+2]:=IntToByte(Round(Top+nsw*(Bottom-Top)));
//          Top:=nw.g+eww*(ne.g-nw.g);
//          Bottom:=sw.g+eww*(se.g-sw.g);
//          P3[x*3+1]:=IntToByte(Round(Top+nsw*(Bottom-Top)));
//          Top:=nw.r+eww*(ne.r-nw.r);
//          Bottom:=sw.r+eww*(se.r-sw.r);
//          P3[x*3]:=IntToByte(Round(Top+nsw*(Bottom-Top)));
//
//          {Only for alpha}
//          if IsAlpha then
//          begin
//            Top:=anw+eww*(ane-anw);
//            Bottom:=asw+eww*(ase-asw);
//            A3[x]:=IntToByte(Round(Top+nsw*(Bottom-Top)));
//          end;
//
//        end;
//      end;
//    end;
//
//    apng.assign(dst);
//    dst.free;
//    Result := true;
//  except
//    on E: Exception do
//      ETgException.TraceException(E, 'Fail .. RotatePng()');
//  end;
//end;

function RotatePng(var aPng: TPNGImage; Angle: Extended): Boolean;

  function TrimInt(i, Min, Max: Integer): Integer;
  begin
    if i < Min then Result := Min
    else if i > Max then Result := Max
    else Result := i;
  end;

  function IntToByte(i: Integer): Byte;
  begin
    if i < 0 then Result := 0
    else if i > 255 then Result := 255
    else Result := i;
  end;

  function CeilEx(A: Double): Integer;
  begin
    Result := Trunc(A);
    if Frac(A) > 0 then Inc(Result);
  end;

  function GetRotatedSize: TSize;
  var
    Radian, CosA, SinA: Extended;
    x1, y1, x2, y2, x3, y3, x4, y4: Extended;
    MinX, MaxX, MinY, MaxY: Extended;
  begin
    Radian := Angle * PI / 180;
    CosA := Abs(Cos(Radian));
    SinA := Abs(Sin(Radian));

    // 원래 이미지 꼭짓점 회전 후 좌표 계산
    x1 := 0 * CosA - 0 * SinA;
    y1 := 0 * SinA + 0 * CosA;

    x2 := 0 * CosA - aPng.Height * SinA;
    y2 := 0 * SinA + aPng.Height * CosA;

    x3 := aPng.Width * CosA - 0 * SinA;
    y3 := aPng.Width * SinA + 0 * CosA;

    x4 := aPng.Width * CosA - aPng.Height * SinA;
    y4 := aPng.Width * SinA + aPng.Height * CosA;

    MinX := Min(Min(x1, x2), Min(x3, x4));
    MinY := Min(Min(y1, y2), Min(y3, y4));
    MaxX := Max(Max(x1, x2), Max(x3, x4));
    MaxY := Max(Max(y1, y2), Max(y3, y4));

    Result.cx := CeilEx(MaxX - MinX);
    Result.cy := CeilEx(MaxY - MinY);
  end;

type
  TFColor = record r, g, b: Byte; end;

var
  dst, src: TPNGImage;
  x, y, cx, cy, SrcW, SrcH, DstW, DstH: Integer;
  fx, fy, px, py: Extended;
  ifx, ify, ix, iy: Integer;
  Radian, SinA, CosA: Extended;
  eww, nsw: Extended;
  IsAlpha: Boolean;
  nw, ne, sw, se: TFColor;
  anw, ane, asw, ase: Byte;
  P1, P2, P3: PByteArray;
  A1, A2, A3: PByteArray;
  newColorType: Integer;
begin
  Result := False;

  if not (aPng.Header.ColorType in [COLOR_RGB, COLOR_RGBALPHA]) then
    Exit;

  IsAlpha := aPng.Header.ColorType = COLOR_RGBALPHA;
  if IsAlpha then newColorType := COLOR_RGBALPHA else newColorType := COLOR_RGB;

  SrcW := aPng.Width;
  SrcH := aPng.Height;

  Radian := -Angle * PI / 180;
  SinA := Sin(Radian);
  CosA := Cos(Radian);

  // 원본 백업
  src := TPNGImage.Create;
  src.Assign(aPng);

  // 새 이미지 생성
  dst := TPNGImage.CreateBlank(newColorType, 8, GetRotatedSize.cx, GetRotatedSize.cy);
  DstW := dst.Width;
  DstH := dst.Height;
  cx := DstW div 2;
  cy := DstH div 2;

  for y := 0 to DstH - 1 do
  begin
    P3 := dst.ScanLine[y];
    if IsAlpha then A3 := dst.AlphaScanline[y];
    py := 2 * (y - cy) + 1;

    for x := 0 to DstW - 1 do
    begin
      px := 2 * (x - cx) + 1;

      fx := (((px * CosA - py * SinA) - 1) / 2 + SrcW / 2);
      fy := (((px * SinA + py * CosA) - 1) / 2 + SrcH / 2);

      ifx := Floor(fx);
      ify := Floor(fy);

      if (ifx >= 0) and (ifx < SrcW - 1) and (ify >= 0) and (ify < SrcH - 1) then
      begin
        eww := fx - ifx;
        nsw := fy - ify;

        ix := ifx + 1;
        iy := ify + 1;

        P1 := src.ScanLine[ify];
        P2 := src.ScanLine[iy];
        if IsAlpha then
        begin
          A1 := src.AlphaScanline[ify];
          A2 := src.AlphaScanline[iy];
        end;

        // 색상 복사
        nw.r := P1[ifx * 3]; nw.g := P1[ifx * 3 + 1]; nw.b := P1[ifx * 3 + 2];
        ne.r := P1[ix * 3];  ne.g := P1[ix * 3 + 1];  ne.b := P1[ix * 3 + 2];
        sw.r := P2[ifx * 3]; sw.g := P2[ifx * 3 + 1]; sw.b := P2[ifx * 3 + 2];
        se.r := P2[ix * 3];  se.g := P2[ix * 3 + 1];  se.b := P2[ix * 3 + 2];

        if IsAlpha then
        begin
          anw := A1[ifx]; ane := A1[ix];
          asw := A2[ifx]; ase := A2[ix];
        end;

        // 보간 계산
        P3[x * 3 + 2] := IntToByte(Round(nw.b + eww * (ne.b - nw.b) + nsw * ((sw.b + eww * (se.b - sw.b)) - (nw.b + eww * (ne.b - nw.b)))));
        P3[x * 3 + 1] := IntToByte(Round(nw.g + eww * (ne.g - nw.g) + nsw * ((sw.g + eww * (se.g - sw.g)) - (nw.g + eww * (ne.g - nw.g)))));
        P3[x * 3 + 0] := IntToByte(Round(nw.r + eww * (ne.r - nw.r) + nsw * ((sw.r + eww * (se.r - sw.r)) - (nw.r + eww * (ne.r - nw.r)))));

        if IsAlpha then
          A3[x] := IntToByte(Round(anw + eww * (ane - anw) + nsw * ((asw + eww * (ase - asw)) - (anw + eww * (ane - anw)))));
      end
      else
      begin
        // 경계 바깥은 흰색 또는 투명 처리
        P3[x * 3 + 0] := 255;
        P3[x * 3 + 1] := 255;
        P3[x * 3 + 2] := 255;
        if IsAlpha then A3[x] := 0;
      end;
    end;
  end;

  aPng.Assign(dst);
  dst.Free;
  src.Free;
  Result := True;
end;

function RotatePngFile(sSrcPath, sDestpath: String; Angle: Extended): Boolean;
var
  png: TPngImage;
begin
  Result := false;
  try
    Guard(png, TPngImage.Create);
    png.LoadFromFile(sSrcPath);
    if RotatePng(png, Angle) then
    begin
      png.SaveToFile(sDestpath);
      Result := true;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. RotatePngFile()');
  end;
end;

// SetWorldTransform
procedure RotateBitmap_STF(Bmp: Vcl.Graphics.TBitmap; Rads: Single; AdjustSize: Boolean;
  BkColor: TColor = clNone; nAddWidth: Integer = 0);
var
  C: Single;
  S: Single;
  XForm: tagXFORM;
  Tmp: Vcl.Graphics.TBitmap;
begin
  C := Cos(Rads);
  S := Sin(Rads);
  XForm.eM11 := C;
  XForm.eM12 := S;
  XForm.eM21 := -S;
  XForm.eM22 := C;
  Tmp := Vcl.Graphics.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)) + nAddWidth;
      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, 0, 0, Tmp.Width, Tmp.Height, Bmp.Canvas.Handle,
      0, 0, SRCCOPY);
    Bmp.Assign(Tmp);
  finally
    Tmp.Free;
  end;
end;

// PlgBlt
procedure RotateBitmap_PlgBlt(Bmp: Vcl.Graphics.TBitmap; Rads: Single; AdjustSize: Boolean;
  BkColor: TColor = clNone);
var
  C: Single;
  S: Single;
  Tmp: Vcl.Graphics.TBitmap;
  OffsetX: Single;
  OffsetY: Single;
  Points: array[0..2] of TPoint;
begin
  try
    C := Cos(Rads);
    S := Sin(Rads);
    Tmp := Vcl.Graphics.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.Canvas.Handle, 0, 0, Bmp.Width,
        Bmp.Height, 0, 0, 0);
      Bmp.Assign(Tmp);
    finally
      Tmp.Free;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. RotateBitmap_PlgBlt()');
  end;
end;

function ScalePercentBmp(bitmp: Vcl.Graphics.TBitmap; iPercent: Integer{100이면 원본}): Boolean;
var
  TmpBmp: Vcl.Graphics.TBitmap;
  ARect: TRect;
  h, w: Real;
  hi, wi: Integer;
begin
  Result := False;
  try
    TmpBmp := Vcl.Graphics.TBitmap.Create;
    try
//      h := bitmp.Height * (iPercent / 100);
//      w := bitmp.Width * (iPercent / 100);
//      hi := StrToInt(FormatFloat('#', h)) + bitmp.Height;
//      wi := StrToInt(FormatFloat('#', w)) + bitmp.Width;
      hi := Round((iPercent / 100) * bitmp.Height);
      wi := Round((iPercent / 100) * bitmp.Width);
      TmpBmp.Width := wi;
      TmpBmp.Height := hi;
      ARect := System.Classes.Rect(0, 0, wi, hi);
      TmpBmp.Canvas.StretchDraw(ARect, Bitmp);
      bitmp.Assign(TmpBmp);
    finally
      TmpBmp.Free;
    end;
    Result := True;
  except
  {$IFDEF DEBUG}
    ASSERT(false);
  {$ENDIF}
    Result := False;
  end;
end;

function MakeColorMatrix(R, G, B, A: Single): TColorMatrix;
begin
  Result[0, 0] := R; Result[0, 1] := 0; Result[0, 2] := 0; Result[0, 3] := 0; Result[0, 4] := 0;
  Result[1, 0] := 0; Result[1, 1] := G; Result[1, 2] := 0; Result[1, 3] := 0; Result[1, 4] := 0;
  Result[2, 0] := 0; Result[2, 1] := 0; Result[2, 2] := B; Result[2, 3] := 0; Result[2, 4] := 0;
  Result[3, 0] := 0; Result[3, 1] := 0; Result[3, 2] := 0; Result[3, 3] := A; Result[3, 4] := 0;
  Result[4, 0] := 0; Result[4, 1] := 0; Result[4, 2] := 0; Result[4, 3] := 0; Result[4, 4] := 1;
end;

function MakeTransparentMatrix(A: Single): TColorMatrix;
begin
  ZeroMemory(@Result, SizeOf(Result));
  Result[3, 3] := A;
end;

procedure InitializeGDIPlus;
var
  StartupInput: TGdiplusStartupInput;
begin
  StartupInput.GdiplusVersion := 1;
  StartupInput.DebugEventCallback := nil;
  StartupInput.SuppressBackgroundThread := False;
  StartupInput.SuppressExternalCodecs := False;

  if GdiplusStartup(GdiplusToken, @StartupInput, nil) <> Ok then
    exit;
//    raise Exception.Create('Failed to initialize GDI+');
end;

procedure FinalizeGDIPlus;
begin
  GdiplusShutdown(GdiplusToken);
end;

function MakePointF(X, Y: Single): TGPPointF;
begin
  Result.X := X;
  Result.Y := Y;
end;

function DrawBitmapWaterEx(aDestDC: HDC; nX, nY: Integer; aSrcBmp: Vcl.Graphics.TBitmap; pCM: PColorMatrix = nil; nStretchW: Integer = 0; nStretchH: Integer = 0): Boolean;
var
  GPGraphics: TGPGraphics;
  ms: TMemoryStream;
//  GPStream: IStream;
  GPImg: TGPImage;
  ImageAttributes: TGPImageAttributes;
  GPRect: TGPRect;
  nWW, nHH: Integer;
  Matrix: TGPMatrix;
begin
  Result := true;
  try
  // Guard() 쓰면 Invalid Pointer 예외뜸 24_0411 13:44:29 kku
    ms := TMemoryStream.Create; // 얘는 (GPStream: IStream) 여기서 알아서 해제함 24_0411 13:51:35 kku
    aSrcBmp.SaveToStream(ms);
    ms.Position := 0;

    Guard(GPGraphics, TGPGraphics.Create(aDestDC));
    GPGraphics.SetPageScale(0.33);

  // IStream 메모리 해제를 위해 여기에 변수 선언해야 함 24_0125 10:58:12 kku
    var GPStream: IStream := TStreamAdapter.Create(ms, soOwned) as IStream;
    Guard(GPImg, TGPImage.Create(GPStream));
    Guard(ImageAttributes, TGPImageAttributes.Create);

  // 흰색 배경 투명처리
    ImageAttributes.SetColorKey(System.UIConsts.MakeColor(255, 255, 255), System.UIConsts.MakeColor(255, 255, 255));
  // 빨간색 배경 투명처리
//    ImageAttributes.SetColorKey(System.UIConsts.MakeColor(255, 0, 0), System.UIConsts.MakeColor(255, 0, 0));

//    ImageAttributes.SetColorMatrix(MakeColorMatrix(0.1, 0.1, 0.5, 0.2)); // Gray, 하늘색
//    ImageAttributes.SetColorMatrix(MakeColorMatrix(0.1, 0.5, 0.5, 0.2)); // Gray, 녹색
//    ImageAttributes.SetColorMatrix(MakeColorMatrix(0.1, 1.1, 0.5, 0.2)); // Gray, 연두색
//    ImageAttributes.SetColorMatrix(MakeColorMatrix(0.2, 0.2, 0.2, 0.2)); // Gray, 실버
//    ImageAttributes.SetColorMatrix(MakeColorMatrix(0.2, 0.2, 0.2, 0.3)); // Gray, 회색
    if pCM <> nil then
//      ImageAttributes.SetColorMatrix(pCM^, ColorMatrixFlagsDefault, ColorAdjustTypeBitmap)
      ImageAttributes.SetColorMatrix(pCM^)
    else
      ImageAttributes.SetColorMatrix(MakeColorMatrix(0.2, 0.2, 0.2, 0.3)); // Gray, 회색
//    ImageAttributes.SetColorMatrix(MakeTransparentMatrix(0.4)); // 그 외 mode, type 옵션 의미 없음..

    nWW := GPImg.GetWidth;
    nHH := GPImg.GetHeight;

//    var GPUnit: TUnit := GPGraphics.GetPageUnit;
    GPRect.X := nX; // nX div 2; // 이렇게 해야 위치가 맞음... 이유는 아직 모름
    GPRect.Y := nY; // nY div 2; // 이렇게 해야 위치가 맞음... 이유는 아직 모름
    GPRect.Width := nWW;
    GPRect.Height := nHH;

    if nStretchW > 0 then
      GPRect.Width := nStretchW;
    if nStretchH > 0 then
      GPRect.Height := nStretchH;

    GPGraphics.DrawImage(GPImg, GPRect, 0, 0, Round(nWW), Round(nHH), UnitPixel, ImageAttributes);
    GPGraphics.Flush(FlushIntentionFlush);
  except
    Result := false;
  end;
end;

function DrawBitmapWaterEx2(aDestDC: HDC; nX, nY: Integer; aSrcBmp: Vcl.Graphics.TBitmap; pCM: PColorMatrix = nil; nStretchW: Integer = 0; nStretchH: Integer = 0; fAngle: Single = 0): Boolean;
var
  GPGraphics: TGPGraphics;
  ms: TMemoryStream;
  GPImg: TGPImage;
  ImageAttributes: TGPImageAttributes;
  GPRect: TGPRect;
  nWW, nHH: Integer;
  offX, offY: Integer;
  SavedDC: Integer;
  Matrix: TGPMatrix;
begin
  Result := true;
  try
    SavedDC := SaveDC(aDestDC);
    try
      ms := TMemoryStream.Create;
      aSrcBmp.SaveToStream(ms);
      ms.Position := 0;

      Guard(GPGraphics, TGPGraphics.Create(aDestDC));
      GPGraphics.SetInterpolationMode(InterpolationModeHighQualityBicubic); // 보간법: 고품질
      GPGraphics.SetSmoothingMode(SmoothingModeAntiAlias);                  // 안티앨리어싱
      GPGraphics.SetPixelOffsetMode(PixelOffsetModeHighQuality);            // 픽셀 오프셋 품질

      // ★ SetPageScale 는 제거하는 걸 추천 (좌표 꼬임 방지)
      // GPGraphics.SetPageScale(0.33);
//      GPGraphics.SetPageUnit(UnitPixel);

      var GPStream: IStream := TStreamAdapter.Create(ms, soOwned) as IStream;
      Guard(GPImg, TGPImage.Create(GPStream));
      Guard(ImageAttributes, TGPImageAttributes.Create);

      ImageAttributes.SetColorKey(
        System.UIConsts.MakeColor(255, 255, 255),
        System.UIConsts.MakeColor(255, 255, 255)
      );

      if pCM <> nil then
        ImageAttributes.SetColorMatrix(pCM^)
      else
        ImageAttributes.SetColorMatrix(MakeColorMatrix(0.2, 0.2, 0.2, 0.3));

      nWW := GPImg.GetWidth;
      nHH := GPImg.GetHeight;

      GPRect.X := nX;
      GPRect.Y := nY;

      GPRect.Width  := nWW;
      GPRect.Height := nHH;

      if nStretchW > 0 then
        GPRect.Width := nStretchW;
      if nStretchH > 0 then
        GPRect.Height := nStretchH;

    // 프린터 물리 offset 구하기
      offX := GetDeviceCaps(aDestDC, PHYSICALOFFSETX);
      offY := GetDeviceCaps(aDestDC, PHYSICALOFFSETY);
      GPGraphics.TranslateTransform(offX, offY);

      if fAngle <> 0 then
      begin
        Matrix := TGPMatrix.Create;
        try
          Matrix.RotateAt(fAngle, MakePointF(GPRect.X, GPRect.Y));
          GPGraphics.SetTransform(Matrix);
        finally
          Matrix.Free;
        end;
      end;

      GPGraphics.DrawImage(GPImg, GPRect, 0, 0, nWW, nHH, UnitPixel, ImageAttributes);

      GPGraphics.Flush(FlushIntentionFlush);
    finally
      RestoreDC(aDestDC, SavedDC);
    end;
  except
    Result := false;
  end;
end;
function DrawBitmapWaterEx3(aDestDC: HDC; nX, nY: Integer; aSrcBmp: Vcl.Graphics.TBitmap; pCM: PColorMatrix = nil; nStretchW: Integer = 0; nStretchH: Integer = 0; fAngle: Single = 0): Boolean;
var
  GPGraphics: TGPGraphics;
  ms: TMemoryStream;
  GPImg: TGPImage;
  ImageAttributes: TGPImageAttributes;
  GPRect: TGPRect;
  nWW, nHH: Integer;
  offX, offY: Integer;
  SavedDC: Integer;
  Matrix: TGPMatrix;
begin
  Result := true;
  try
    SavedDC := SaveDC(aDestDC);
    try
      ms := TMemoryStream.Create;
      aSrcBmp.SaveToStream(ms);
      ms.Position := 0;

      Guard(GPGraphics, TGPGraphics.Create(aDestDC));
      GPGraphics.SetInterpolationMode(InterpolationModeHighQualityBicubic); // 보간법: 고품질
      GPGraphics.SetSmoothingMode(SmoothingModeAntiAlias);                  // 안티앨리어싱
      GPGraphics.SetPixelOffsetMode(PixelOffsetModeHighQuality);            // 픽셀 오프셋 품질

      // ★ SetPageScale 는 제거하는 걸 추천 (좌표 꼬임 방지)
      // GPGraphics.SetPageScale(0.33);
      GPGraphics.SetPageUnit(UnitPixel);

      var GPStream: IStream := TStreamAdapter.Create(ms, soOwned) as IStream;
      Guard(GPImg, TGPImage.Create(GPStream));
      Guard(ImageAttributes, TGPImageAttributes.Create);

      ImageAttributes.SetColorKey(
        System.UIConsts.MakeColor(255, 255, 255),
        System.UIConsts.MakeColor(255, 255, 255)
      );

      if pCM <> nil then
        ImageAttributes.SetColorMatrix(pCM^)
      else
        ImageAttributes.SetColorMatrix(MakeColorMatrix(0.2, 0.2, 0.2, 0.3));

      nWW := GPImg.GetWidth;
      nHH := GPImg.GetHeight;

      GPRect.X := nX;
      GPRect.Y := nY;

      GPRect.Width  := nWW;
      GPRect.Height := nHH;

      if nStretchW > 0 then
        GPRect.Width := nStretchW;
      if nStretchH > 0 then
        GPRect.Height := nStretchH;

    // 프린터 물리 offset 구하기
      offX := GetDeviceCaps(aDestDC, PHYSICALOFFSETX);
      offY := GetDeviceCaps(aDestDC, PHYSICALOFFSETY);
      GPGraphics.TranslateTransform(offX, offY);

      if fAngle <> 0 then
      begin
        Matrix := TGPMatrix.Create;
        try
          Matrix.RotateAt(fAngle, MakePointF(GPRect.X, GPRect.Y));
          GPGraphics.SetTransform(Matrix);
        finally
          Matrix.Free;
        end;
      end;

      GPGraphics.DrawImage(GPImg, GPRect, 0, 0, nWW, nHH, UnitPixel, ImageAttributes);

      GPGraphics.Flush(FlushIntentionFlush);
    finally
      RestoreDC(aDestDC, SavedDC);
    end;
  except
    Result := false;
  end;
end;

end.
