8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Delphi] VCL アルファレイヤ付きBitmapをTPngImageで保存

Last updated at Posted at 2025-12-18

本投稿はDelphi AdventCalender 2025 #19の記事です。

この記事は,Delphi13でコードを書き,Windows11で動作を確認しています。

はじめに

VCLアプリケーションで生成したアルファレイヤ付きのTBitmapをTPngImageを使ってPNGファイルに保存しようとしました。その時書いたコードは以下のものです。
ABitmap:TBitmapにアルファレイヤ付きのビットマップが格納されています。PixelFormatpf32bitで,AlphaFormatafDefinedです。

uses
  Vcl.Imaging.pngimage;

var
  LPng: TPngImage;
begin
  LPng := TPngImage.Create;
  LPng.Assign(ABitmap);
  LPng.CompressionLevel := 9; // PNGの圧縮率
  LPng.SaveToFile(APngFileName);
  LPng.Free;
end;

しかし,アルファレイアが適用されませんでした。
念のため,ABitmapをBMPで保存してみるとアルファレイヤがあることを確認できました。
そこで,アルファレイヤ付きのBitmapをTPngImageで保存する記事を探したのですが,透過PNGをアルファレイヤ付きのBitmapに保存する方法しか見つけることができませんでしたので,自前で書くことにしました。(もしも存在したらすいません,,)

KP.GraphUtils

TBitmapのScanlineを使います。PixelFormatpf32bitの場合,ピクセル情報は,B G R A のByteで記録されています。
一方,TPngImageのScanlineは,B G RのByteのみで,アルファレイヤの情報は,AlphaScanlineに格納されています。
そこで,TPngImage.CreateBlankで,アルファレイヤを持つ8ビットの画像を生成して,ScanlineとAlphaScanlineを使ってデータを書き込むことにしました。
なお,AlphaScanlineはVcl.Imaging.pngImage.pByteArrayで返され,その長さは[word]で定義されています。これは[0..65535]なので,ポインタで返されるScanlineをキャストする配列も同じサイズにしています。

追記: 初稿ではAlphaScanlineはSystem.SysUtils.PByteArrayが返されその範囲は[0..32767]であると書いていました。System.SysUtils.PByteArrayの範囲は正しいのですが,AlphaScanlineはVcl.Imaging.pngImage.pByteArrayですので[word]すなわち[0..65535]となります。

QiitaにはBMPファイルを貼り付けられないので,RGBとマスクのPNGファイルを読み込んで合成するおまけの関数BitmapAddMaskも作ってみました。こちらはpf24bitのABitmapのRGBとMBitmapのRをマスクにしたpf32bitのTBitmapを返します。

※ このコードはVCLでしか使えないので,条件指令で挟み込んでいます。

KP.GraphUtils.pas
KP.GraphUtils.pas
unit KP.GraphUtils;
(*
2025/12/16 VCL用 アルファレイヤを持つpf32bit BitmapをPNGファイルに保存

LICENSE
Copyright (c) 2025 Yuzuru Kato
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*)
interface

{$IFDEF FRAMEWORK_VCL}

uses
  Vcl.Graphics, Vcl.Imaging.pngimage;

// pf24bitのABitmapのRGBとMBitmapのRをマスクにしたpf32bitのTBitmapを返す
function BitmapAddMask(ABitmap, MBitmap: TBitmap):TBitmap;

// アルファレイヤを持つpf32bit BitmapをPNGファイルに保存する
procedure BitmapSaveAsPng(ABitmap: TBitmap; APngFileName:string;
  ACompressionLevel: TCompressionLevel=9);

{$ENDIF}

implementation

{$IFDEF FRAMEWORK_VCL}

type
  TRGB = record
    R,G,B:byte;
  end;
  TRGBA = record
    R,G,B,A:byte;
  end;
  TRGBAArray = array[word] of TRGBA;
  pRGBAArray = ^TRGBAArray;
  TRGBArray  = array[word] of TRGB;
  pRGBArray  = ^TRGBArray;

// pf24bitのABitmapのRGBとMBitmapのRをマスクにしたpf32bitのTBitmapを返す
function BitmapAddMask(ABitmap, MBitmap: TBitmap):TBitmap;
var
  ResultAr:   pRGBAArray;
  ABitmapAr:  pRGBArray;
  MBitmapAr:  pRGBArray;
  x, y, W, H: Integer;
begin
  W := ABitmap.Width;
  H := ABitmap.Height;
  Result := TBitmap.Create;
  try
    Result.PixelFormat := pf32bit;
    Result.AlphaFormat := afDefined;
    Result.SetSize(W, H);
    for y:=0 to H-1 do begin
      ResultAr  := Result.ScanLine[Y];
      ABitmapAr := ABitmap.Scanline[Y];
      MBitmapAr := MBitmap.Scanline[Y];
      for x:=0 to W-1 do begin
        ResultAr^[x].R := ABitmapAr^[x].R;
        ResultAr^[x].G := ABitmapAr^[x].G;
        ResultAr^[x].B := ABitmapAr^[x].B;
        ResultAr^[x].A := MBitmapAr^[x].R;
      end;
    end;
  finally
  end;
end;

// アルファレイヤを持つpf32bit BitmapをPNGファイルに保存する
procedure BitmapSaveAsPng(ABitmap: TBitmap; APngFileName:string;
  ACompressionLevel: TCompressionLevel=9);
var
  LPng: TPngImage;
  x, y, W, H: Integer;
  RGBAAr:   pRGBAArray;
  PngRGBAr: pRGBArray;
  PngALPAr: pByteArray;
begin
  if (ABitmap.PixelFormat=pf32bit)and(ABitmap.AlphaFormat<>afIgnored) then begin
    W := ABitmap.Width;
    H := ABitmap.Height;
    LPng := TPngImage.CreateBlank(COLOR_RGBALPHA, 8, W, H);
    try
      if ABitmap.AlphaFormat=afPremultiplied then begin
        for y:=0 to H-1 do begin
          RGBAAr   := ABitmap.Scanline[y];
          PngRGBAr := LPng.Scanline[y];
          PngALPAr := LPng.AlphaScanline[y];
          for x:=0 to W-1 do begin
            PngRGBAr^[x].B := RGBAAr^[x].B;
            PngRGBAr^[x].G := RGBAAr^[x].G;
            PngRGBAr^[x].R := RGBAAr^[x].R;
            PngALPAr^[x]   := RGBAAr^[x].A;
          end;
        end;
      end else begin
        for y:=0 to H-1 do begin
          RGBAAr   := ABitmap.Scanline[y];
          PngRGBAr := LPng.Scanline[y];
          PngALPAr := LPng.AlphaScanline[y];
          for x:=0 to W-1 do begin
            PngRGBAr^[x].B := trunc(RGBAAr^[x].B*(RGBAAr^[x].A/255));
            PngRGBAr^[x].G := trunc(RGBAAr^[x].G*(RGBAAr^[x].A/255));
            PngRGBAr^[x].R := trunc(RGBAAr^[x].R*(RGBAAr^[x].A/255));
            PngALPAr^[x]   := RGBAAr^[x].A;
          end;
        end;
      end;     
      LPng.CompressionLevel := ACompressionLevel;
      LPng.SaveToFile(APngFileName);
    finally
      LPng.Free;
    end;
  end else begin
    LPng := TPngImage.Create;
    LPng.Assign(ABitmap);
    LPng.CompressionLevel := ACompressionLevel;
    LPng.SaveToFile(APngFileName);
    LPng.Free;
  end;
end;

{$ENDIF}

end.

テストプログラム

KP.GraphUtilsを使ったテストプログラムを書いてみます。

フォームの設計

VCLアプリケーションを新規作成して,以下のように,Button1: TButtonOpenDialog1: TOpenDialogSaveDialog1: TSaveDialog を配置します。

Snap0001.png

テストコード

Button1のクリックイベントを以下のように書きます。

Unit1.pas

・・・

uses
  Vcl.Imaging.pngimage,
  KP.GraphUtils;

procedure TForm1.Button1Click(Sender: TObject);
var
  LPng: TPngImage;
  LBitmap:    TBitmap;
  LBitmapRGB: TBitmap;
  LBitmapMSK: TBitmap;
begin
  LPng:=TPngImage.Create;
  OpenDialog1.Title := 'PNGファイルを指定してください';
  OpenDialog1.Filter   := 'PNGファイル|*.png';
  OpenDialog1.FileName := '*_RGB.png';
  if OpenDialog1.Execute then begin
    LPng.LoadFromFile(OpenDialog1.FileName);
    LBitmapRGB := TBitmap.Create;
    LBitmapRGB.Assign(LPng);
    OpenDialog1.Title := 'PNG マスクファイルを指定してください';
    OpenDialog1.FileName := '*_MSK.png';
    if OpenDialog1.Execute then begin
      LPng.LoadFromFile(OpenDialog1.FileName);
      LBitmapMSK := TBitmap.Create;
      LBitmapMSK.Assign(LPng);
      LBitmap:=BitmapAddMask(LBitmapRGB, LBitmapMSK);
      LBitmapMSK.Free;
      SaveDialog1.Title := '保存するPNGファイルを指定してください';
      SaveDialog1.Filter   := 'PNGファイル|*.png';
      SaveDialog1.FileName := '*.png';
      if SaveDialog1.Execute then begin
        BitmapSaveAsPng(LBitmap, SaveDialog1.FileName, 9);
      end;
      LBitmap.Free;
    end;
    LBitmapRGB.Free;
  end;
  LPng.Free;
end;

・・・

テストで利用する画像

テスト用の画像を用意しました。以下の二つをダウンロードしてください。

IMG_7525_RGB.png
画像ファイル (IMG_7525_RGB.pngで保存しておくと読み出しやすいです)

IMG_7525_MSK.png
マスクファイル(IMG_7525_MSK.pngで保存しておくと読み出しやすいです)

実行する

Button1をクリックして,最初に画像ファイルを指定し,次にマスクファイルを指定し,保存するファイルを指定すると以下のようなPNGファイルが出力できるはずです。

IMG_7525_OUTPUT.png

※ Qiitaの仕様で,透過PNGが適切に表示されないかもしれません。このPNGファイルをダウンロードすると透過であることが確認できると思います。

謝辞

この記事やコードを書くにあたり,以下のサイトを参考にしました。この場を借りて感謝します。

8
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?