Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2013年01月06日] [更新 2013年07月22日] GDI+ 関係サンプル
GDI+ 関係サンプル 
G160_画像ファイルのプロパティとメタデータ
動作確認等 Windows 7 U64(SP1) + Delphi XE(UP1) Pro
G160_GDIPlus_Properties_MetaData.zip [4,497 KB] 2014年03月26日版 (EXE 同梱)



  • 2013年05月08日
  • 符号なし 16 ビット型、符号なし 32 ビット型、バイト配列型のプロパティの読み書きの参考コードを追加
  • 2013年07月21日
  • 分数形式のプロパティの配列を、動的配列を使用する方式に変更




画像ファイルのプロパティとメタデータ


ファイルのメタデータは、そのファイルを作成した日時やユーザー名、使用したソフトウェア名などを示す情報のことです。本体の情報そのものではありませんが、情報に付加する情報であることから「データのためのデータ」とも言われています。ファイルの「プロパティ」ダイアログで表示される内容もメタデータの一部と言えますが、それだけとは限りません。

GDI+ では、GPS 関係、Exif 情報関係も含めて、画像ファイル関係の 217 個のメタデータをプロパティとして操作できます。
これらは、取得だけでなく、変更が可能なプロパティもあります。その一部の操作に関するサンプルプログラムです。





01_プロパティ名の列挙


対象となる画像ファイルに、どのようなメタデータが保存されているか調べるために、そのリストを列挙するサンプルです。結果の図は、添付のデジカメの画像ファイルですが、このデジカメは、GPS は使用していません。したがって、GDP 関係のプロパティタグはありません。

GDIPUTIL.pas にプロパティの数値からプロパティの文字例を取得する関数があります。これを利用しています。


  • 図1
  • 設計時画面

  • 図2
  • 添付の画像ファイルでの実行結果

リスト1
メタデータのプロパティ名の列挙
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, GDIPAPI, GDIPOBJ, GDIPUTIL, ExtCtrls, ComCtrls;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    OpenDialog1: TOpenDialog;
    Panel1: TPanel;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Self.Caption := 'GDI+  画像ファイルのメタプロパティ';

  ListView1.Font.Size := 9;
  ListView1.GridLines := True;
  ListView1.Columns[0].Width := 220;
  ListView1.Columns[1].Width := 60;
  ListView1.Columns[2].Width := 180;
  ListView1.Columns[3].Width := 70;
end;

//=============================================================================
//  画像ファイルのメタデータのプロパティタグ名をリスト
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LImageFilePath : String;
  LGPImage       : TGPImage;
  LSize          : UINT;
  LCount         : UINT;
  LPropItemArray : array of TPropertyItem;
  LpPropItem     : PPropertyItem;
  LStrTag        : string;
  LStrTagType    : string;
  LListItem      : TListItem;
  i              : Integer;
begin
  //画像ファイルを読み込む
  LImageFilePath := '';
  if OpenDialog1.InitialDir = '' then begin
    OpenDialog1.InitialDir := ExtractFileDir(ExtractFileDir(ParamStr(0)));
  end;

  OpenDialog1.Filter := '画像ファイル|*.png;*.jpg;*.jpeg;*.bmp;*.gif';
  if OpenDialog1.Execute then begin
    OpenDialog1.InitialDir := ExtractFileDir(OpenDialog1.FileName);
    LImageFilePath := OpenDialog1.FileName;
  end;
  if LImageFilePath = '' then exit;
  Panel1.Caption := ExtractFileName(LImageFilePath);


  //画像ファイルからTGPImageを生成
  LGPImage := TGPImage.Create(LImageFilePath);

  LSize  := 0;
  LCount := 0;
  //プロパティのバイトサイズと数を取得して、その分のメモリを確保
  LGPImage.GetPropertySize(LSize, LCount);
  GetMem(Pointer(LPropItemArray), LSize);
  try
    //メモリを0でクリアして全てのプロパティをPropItemArrayに取得
    ZeroMemory(Pointer(LPropItemArray), LSize);
    LGPImage.GetAllPropertyItems(LSize, LCount, Pointer(LPropItemArray));

    //GDIPUTIL.pas内の関数を使用
    //プロパティの定数から定数名の文字列を取得してTListViewに追加していく
    ListView1.Items.Clear;
    ListView1.Items.BeginUpdate;
    for i := 0 to LCount - 1 do begin
      LpPropItem := @LPropItemArray[i];

      LStrTag     := GDIPUTIL.GetMetaDataIDString(LpPropItem^.id);
      LStrTagType := GDIPUTIL.ValueTypeFromULONG(LpPropItem^.type_);

      if LStrTag = '' then Continue;
      if (Pos('UnKnown', LStrTag) > 0)     then Continue;
      if (Pos('UnKnown', LStrTagType) > 0) then Continue;
      if (Pos('Nothing', LStrTagType) > 0) then Continue;

      LListItem := ListView1.Items.Add;
      LListItem.Caption :=  LStrTag;
      LListItem.SubItems.Add(Format('$%.4x', [LpPropItem^.id]));
      LListItem.SubItems.Add(LStrTagType);
      LListItem.SubItems.Add(IntToStr(LpPropItem^.length));

    end;
    ListView1.Items.EndUpdate;
  finally
    FreeMem(Pointer(LPropItemArray));
    LGPImage.Free;
  end;
end;

end.




02_プロパティの列挙 - GDIPlus


同じくプロパティの列挙ですが、Delphi 2009 GDI+ Library を使用したサンプルです。
Delphi 2009 GDI+ Library には IGPImage.PropertyItems という関数があり、列挙が簡単ですが、タグの値を文字列として取得する関数はありません。数値をそのまま表示しています。


図3
設計時画面
図4
添付の画像ファイルでの実行結果

リスト2
Delphi 2009 GDI+ Library を使用したプロパティの列挙 (数値のみ表示)
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, GDIPlus, ComCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    OpenDialog1: TOpenDialog;
    Panel1: TPanel;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Self.Caption := 'GDI+  プロパティリスト GDIPlus';

  ListView1.Font.Size := 9;
  ListView1.GridLines := True;
  ListView1.Columns[0].Width := 80;
  ListView1.Columns[1].Width := 80;
  ListView1.Columns[2].Width := 80;
end;

//=============================================================================
//  画像ファイルのメタデータのプロパティタグの値をリスト
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LImageFilePath : string;
  LGPImage       : IGPImage;
  LPropItem      : IGPPropertyItem;
  LListItem      : TListItem;
begin
  //画像ファイルを読み込む
  LImageFilePath := '';
  if OpenDialog1.InitialDir = '' then begin
    OpenDialog1.InitialDir := ExtractFileDir(ExtractFileDir(ParamStr(0)));
  end;

  OpenDialog1.Filter := '画像ファイル|*.png;*.jpg;*.jpeg;*.bmp;*.gif';
  if OpenDialog1.Execute then begin
    OpenDialog1.InitialDir := ExtractFileDir(OpenDialog1.FileName);
    LImageFilePath := OpenDialog1.FileName;
  end;
  if LImageFilePath = '' then exit;
  Panel1.Caption := ExtractFileName(LImageFilePath);


  //画像ファイルからTGPImageのインスタンスを生成
  LGPImage := TGPImage.Create(LImageFilePath);

  //プロパティの定数値をTListViewに追加していく
  ListView1.Items.Clear;
  ListView1.Items.BeginUpdate;
  for LPropItem in LGPImage.PropertyItems do begin
    LListItem := ListView1.Items.Add;
    LListItem.Caption :=  Format('$%.4x', [LPropItem.Id]);
    LListItem.SubItems.Add(IntToStr(LPropItem.ValueType));
    LListItem.SubItems.Add(IntToStr(LPropItem.Length));
  end;
  ListView1.Items.EndUpdate;
end;

end.


Delphi 2009 GDI+ Library でも、GDIPUTIL.pas を uses 部に追加して利用すれば、以下のコードでプロパティの文字列を取得できます。


  ListView1.Items.Clear;
  ListView1.Items.BeginUpdate;
  for LPropItem in LGPImage.PropertyItems do begin
    LStrTag     := GDIPUTIL.GetMetaDataIDString(LPropItem.Id);
    LStrTagType := GDIPUTIL.ValueTypeFromULONG(LPropItem.ValueType);

    if LStrTag = '' then Continue;
    if (Pos('UnKnown', LStrTag) > 0)     then Continue;
    if (Pos('UnKnown', LStrTagType) > 0) then Continue;
    if (Pos('Nothing', LStrTagType) > 0) then Continue;

    LListItem := ListView1.Items.Add;
    LListItem.Caption :=  LStrTag;
    LListItem.SubItems.Add(Format('$%.4x', [LPropItem.Id]));
    LListItem.SubItems.Add(LStrTagType);
    LListItem.SubItems.Add(IntToStr(LPropItem.Length));
  end;
  ListView1.Items.EndUpdate;




03_デジカメと写真のメタデータ  -  Exif 情報関係


デジカメと写真関係のメタデータの取得、操作の例です。これは主に Exif 情報関係のデータです。
このサンプルは、その内の極一部のメタデータのみを取得して操作します。
データの編集・変更のサンプルコードがありますが、これはデータの改ざんということになります。このことについては、筆者の関与するところではありません。

これらのメタデータの中で、露出時間は、分数の形式で格納されています。そこで、このサンプルでは、配列を使用して、分母と分子を各配列の要素に代入するようにしています。

次のように、pPropItem^.type でプロパティタグの種類を判定していますが、実際には必要ありません。type_ の型は、プロパティタグの種類によって固有の形式となっています。
サンプルとして、その取得方法を理解しやすいようにコードに書いているだけです。
多くのプロパティを操作する場合は、タイプ別に処理用の関数を作成した方が便利でしょう。


      if LpPropItem^.type_ = PropertyTagTypeASCII then begin
        LabeledEdit2.Text := string(PAnsiChar(LpPropItem^.value));
      end;


Windows 7 では、このサンプルで扱っている項目は、プロパティの削除はできないので、該当ボタンを無効化しています。

参考リンクの ExifTool は Eixif, GPS の情報の読み書きが可能な多言語対応のコマンドラインツールです。機能も豊富で、単体の EXE だけですから開発したアプリの配布にも便利です。
CCR Exif は JPEG 画像の Exif 情報が読み書きできるユニット群です。GDI+ は使用しません。プロパティ名は英語です。
CCR Exif をダウンロードして解凍すれば、デモプログラムのプロジェクトのコンパイルが可能です。必要なユニット類は、各プロジェクトに追加されています (各プロジェクトのソース参照)。

ExifInfo コンポーネントも GDI+ を使用しませんが、読み取り専用です。



図5
設計時画面
図6
画像ファイルの読み出し
  • 表示はオリジナルの値
  • 画像を描画しているので、画像のサイズが大きいと取得に時間がかかる
  • メタデータの操作だけであれば、画像を表示する必要はない
図7
プロパティを変更して [保存]
  • エラー処理はしていないので、入力には注意
  • 実用的なアプリでは、専用の入力欄を作成した方がいいだろう

下図は、エクスプローラの [プロパティ] ダイアログでの確認結果です。


図8
変更前
図9
変更後
  • 以下の項目の値が変化している
  • [カメラの製造元]
  • [カメラのモデル]
  • [露出時間]
  • この画像では見えていないが、撮影日時も変更されている

サンプルコードでは、日付として、PropertyTagExifDTOrig の値を取得または操作していますが、日付と時刻関係のプロパティタグには下表のものがあります (GPS 関係は除く)。

PropertyTagDateTime 画像が作成された日付と時刻
PropertyTagThumbnailDateTime サムネイルが作成された日付と時刻
PropertyTagExifDTOrig 元の画像データが生成された日付と時刻
PropertyTagExifDTDigitized 画像がデジタルデータとして格納された日付と時刻


リスト3
カメラと写真のメタデータの取得と操作
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, GDIPAPI, GDIPOBJ, GDIPUTIL, ComCtrls, ExtCtrls, ActiveX;

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenDialog1: TOpenDialog;
    Button2: TButton;
    Image1: TImage;
    LabeledEdit1: TLabeledEdit;
    LabeledEdit2: TLabeledEdit;
    LabeledEdit3: TLabeledEdit;
    LabeledEdit4: TLabeledEdit;
    Button3: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
    FImageFilePath : String;
    FMemStream     : TMemoryStream;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Self.Caption := 'GDI+  カメラと写真のプロパティ';

  FImageFilePath := '';
  //Windows Vista以降では、メタデータの削除ができないので該当ボタンの使用不可に
  if CheckWin32Version(6, 0) then Button3.Enabled := False;
end;

//=============================================================================
//  フォームのOnDestroyイベント処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  if Assigned(FMemStream) then FreeAndNil(FMemStream);
end;

//=============================================================================
//  [画像ファイル]ボタン
//  画像ファイルを読み込んで、メタデータを取得して表示
//
//  このサンプルでは、画像ファイルの[上書き保存]を可能にするために、
//  (1) 画像をメモリストリームに格納
//  (2) 画像のインスタンスのTGPImageは解放
//  (3) 他のボタンでの操作では、メモリストリームからTGPImageを生成
//  (4) このTGPImageを、読み出した時のファイル名で保存
//  としている
//  画像を表示する必要がなければ、
//  FMemStream.LoadFromFile(FImageFilePath);
//  として、最初からメモリストリームに画像を格納してもよい
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LGPImage       : TGPImage;
  LGPGraphic     : TGPGraphics;
  LAspectRatio   : Double;
  LpPropItem     : PPropertyItem;
  LIdValue       : Cardinal;
  LBufferSize    : Cardinal;
  LRationalValue : array of Cardinal;
  LImageGUID     : TGUID;
begin
  //画像ファイルを読み込む
  FImageFilePath := '';
  if OpenDialog1.InitialDir = '' then begin
    OpenDialog1.InitialDir := ExtractFileDir(ExtractFileDir(ParamStr(0)));
  end;

  OpenDialog1.Filter := '画像ファイル|*.png;*.jpg;*.jpeg;*.bmp;*.gif';
  if OpenDialog1.Execute then begin
    OpenDialog1.InitialDir := ExtractFileDir(OpenDialog1.FileName);
    FImageFilePath := OpenDialog1.FileName;
  end;
  if FImageFilePath = '' then exit;


  //画像ファイルからTGPImageのインスタンスを生成
  LGPImage := TGPBitmap.Create(FImageFilePath);

  //画像をTImageに表示(どんな画像かの確認用)
  Image1.Picture.Assign(nil);
  LAspectRatio := LGPImage.GetHeight / LGPImage.GetWidth;
  Image1.SetBounds(Image1.Left, Image1.Top, Image1.Width,
                                            Round(Image1.Width * LAspectRatio));
  LGPGraphic := TGPGraphics.Create(Image1.Canvas.Handle);
  LGPGraphic.DrawImage(LGPImage, 0, 0, Image1.Width, Image1.Height);


  LabeledEdit1.Text := '';
  LabeledEdit2.Text := '';
  LabeledEdit3.Text := '';
  LabeledEdit4.Text := '';

  //[装置の製造元]の取得
  //メタデータ取得用のバッファを確保し、そのバッファから読み取る
  //そのバッファの格納形式はTPropertyItem(ポインタ型はPPropertyItem)
  LIdValue := PropertyTagEquipMake;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      if LpPropItem^.type_ = PropertyTagTypeASCII then begin
        LabeledEdit1.Text := string(PAnsiChar(LpPropItem^.value));
      end;
    finally
      FreeMem(LpPropItem);
    end;
  end;

  //[カメラのモデル]名の取得
  LIdValue := PropertyTagEquipModel;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      if LpPropItem^.type_ = PropertyTagTypeASCII then begin
        LabeledEdit2.Text := string(PAnsiChar(LpPropItem^.value));
      end;
    finally
      FreeMem(LpPropItem);
    end;
  end;

  //[元画像データが生成された日時]の取得
  LIdValue := PropertyTagExifDTOrig;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      if LpPropItem^.type_ = PropertyTagTypeASCII then begin
        LabeledEdit3.Text := string(PAnsiChar(LpPropItem^.value));
      end;
    finally
      FreeMem(LpPropItem);
    end;
  end;

  //[露出時間]の取得
  //露出時間は、Rational(有理数・分数)形式で格納されている
  //分母と分子で配列要素は2つ必要
  SetLength(LRationalValue, 2);

  LIdValue := PropertyTagExifExposureTime;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      if LpPropItem^.type_ = PropertyTagTypeRational then begin
        LRationalValue    := LpPropItem^.value;
        LabeledEdit4.Text := IntToStr(LRationalValue[0]) + ' / ' +
                             IntToStr(LRationalValue[1]);
      end;
    finally
      FreeMem(LpPropItem);
    end;
  end;

  //[上書き保存]を可能にするために画像をJPEG画像としてメモリストリームに格納
  //メモリストリームに直接は格納できないので、IStreamを仲介する
  if Assigned(FMemStream) then FreeAndNil(FMemStream);
  FMemStream := TMemoryStream.Create;
  if GetEncoderClsid('image/jpeg', LImageGUID) >= 0 then begin
    LGPImage.Save(TStreamAdapter.Create(FMemStream), LImageGUID);
  end;

  LGPGraphic.Free;
  LGPImage.Free;
end;

//=============================================================================
//  [上書き保存](データの改ざん)ボタン
//  各TLabelEditの値で、プロパティを設定して、それをメタデータとして保存
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LIStrema       : IStream;
  LGPImage       : TGPImage;
  LPropItem      : TPropertyItem;
  LiPos          : Integer;
  LStrItem       : String;
  LAnsiStrItem   : AnsiString;
  LRationalValue : array of Cardinal;
  LImageGUID     : TGUID;
begin
  if not Assigned(FMemStream) then exit;

  //画像をIStreamを介してメモリストリームから読み出し、TGPImageを生成
  //メモリストリームからは直接生成できないので注意
  LIStrema := TStreamAdapter.Create(FMemStream);
  LGPImage := TGPBitmap.Create(LIStrema);

  //[装置の製造元]をアスキー文字列でTPropertyItemレコード型のメンバーにセット
  if Trim(LabeledEdit1.Text) <> '' then begin
    LStrItem     := LabeledEdit1.Text;
    LAnsiStrItem := AnsiString(LStrItem);

    LPropItem.id     := PropertyTagEquipMake;
    LPropItem.type_  := PropertyTagTypeASCII;
    LPropItem.value  := PAnsiChar(LAnsiStrItem);
    LPropItem.length := Length(LStrItem) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[カメラのモデル]をアスキー文字列でTPropertyItemレコード型のメンバーにセット
  if Trim(LabeledEdit2.Text) <> '' then begin
    LStrItem     := LabeledEdit2.Text;
    LAnsiStrItem := AnsiString(LStrItem);

    LPropItem.id     := PropertyTagEquipModel;
    LPropItem.type_  := PropertyTagTypeASCII;
    LPropItem.value  := PAnsiChar(LAnsiStrItem);
    LPropItem.length := Length(LStrItem) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[元画像データが生成された日付と時刻]を
  //アスキー文字列でTPropertyItemレコード型のメンバーにセット
  if Trim(LabeledEdit3.Text) <> '' then begin
    LStrItem     := LabeledEdit3.Text;
    LAnsiStrItem := AnsiString(LStrItem);

    LPropItem.id     := PropertyTagExifDTOrig;
    LPropItem.type_  := PropertyTagTypeASCII;
    LPropItem.value  := PAnsiChar(LAnsiStrItem);
    LPropItem.length := Length(LStrItem) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[露出時間]を分数形式でTPropertyItemレコード型のメンバーにセット
  //分母と分子で配列要素は2つ必要
  //ここでは分母の値のみ変更
  SetLength(LRationalValue, 2);

  if Trim(LabeledEdit4.Text) <> '' then begin
    LPropItem.id     := PropertyTagExifExposureTime;
    LPropItem.type_  := PropertyTagTypeRational;
    //動的配列のバイト数
    LPropItem.length := Length(LRationalValue) * SizeOf(LRationalValue);

    LStrItem := LabeledEdit4.Text;
    LiPos    := Pos('/', LStrItem);
    LStrItem := Trim(Copy(LStrItem, LiPos + 1, Length(LStrItem)));

    LRationalValue[0] := 1;
    LRationalValue[1] := StrToInt(LStrItem);
    LPropItem.value   := @LRationalValue[0];
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //読み出した時と同じファイル名で、JPEG画像として上書き保存
  //元の画像形式がJPEGでない場合はJPEGに変換して保存
  if GetEncoderClsid('image/jpeg', LImageGUID) >= 0 then begin
    FImageFilePath := ChangeFileExt(FImageFilePath, '.jpg');
    LGPImage.Save(FImageFilePath, LImageGUID);
  end;

  LGPImage.Free;
end;

//=============================================================================
//  [プロパティを削除して保存](データの改ざん)ボタン
//  Windows Vista以降では機能しない
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LIStrema   : IStream;
  LGPImage   : TGPImage;
  LImageGUID : TGUID;
begin
  if not Assigned(FMemStream) then exit;
  if CheckWin32Version(6, 0) then exit;

  //画像をIStreamを介してメモリストリームから読み出し、TGPImageを生成
  //メモリストリームからは直接生成できないので注意
  LIStrema := TStreamAdapter.Create(FMemStream);
  LGPImage := TGPBitmap.Create(LIStrema);

  LGPImage.RemovePropertyItem(PropertyTagEquipMake);
  LGPImage.RemovePropertyItem(PropertyTagEquipModel);
  LGPImage.RemovePropertyItem(PropertyTagExifDTOrig);
  LGPImage.RemovePropertyItem(PropertyTagExifExposureTime);

  //読み出した時と同じファイル名で、JPEG画像として上書き保存
  //元の画像形式がJPEGでない場合はJPEGに変換して保存
  if GetEncoderClsid('image/jpeg', LImageGUID) >= 0 then begin
    FImageFilePath := ChangeFileExt(FImageFilePath, '.jpg');
    LGPImage.Save(FImageFilePath, LImageGUID);
  end;

  LGPImage.Free;
end;

end.




04_上書き保存機能の実装


上のサンプルプログラムでは、画像ファイルの [上書き保存] を実現するために、
  • (1) 画像をメモリストリームに格納して、TGPImage (TGPBitmap) は解放
  • (2) 他のボタンの操作では、メモリストリームから TGPImageを生成
  • (3) このTGPImageを、読み出した時のファイル名で保存
という手順で画像のオブジェクトを操作しています。
本ページのサンプルでは、JPEG 画像を読み込んで、JPEG 画像ファイルとして保存することを前提としています。他の形式の画像を扱う場合は、状況に応じて、該当箇所を変更してください。
以下のリンクの記事とサンプルも参考にしてください。





05_GPS の緯度と経度情報の取得


GPS 関係の情報の内、緯度と経度の情報のみの取得サンプルです。
緯度、経度は、度・分・秒がそれぞれ分数形式で格納されています。デジタルカメラの Exif 情報取得と同じように配列を定義しますが、要素が 6 の符号なし整数値配列を使用しています。

サンプルの写真は、東京の渋谷駅の南口にあるモヤイ像を撮影したものです。
緯度は北緯 (N) 、35 度 39 分 28.318 秒 (35 / 1 39 / 1 28318 / 1000) であることが分かります。

Google Maps 等では、緯度や経度を実数で取得したり設定することがあります。そこで、実数に換算した値も表示するようにしています。取得結果の 35.657866111, 139.700303056 を、グーグルマップの検索窓に入力すると、この位置の地図を表示することができます。
デジタルカメラや携帯電話、登山用の GPS の情報には数十メートル程度の誤差があることに注意してください。数センチの誤差しかない宇宙開発や軍用の GPS とは精度も確度も違います。実際の位置はもう少し北東側 (地図では右上方向) になります。

項目の編集。削除、保存の機能は省略しています。必要であれば上の方の [03_デジカメと写真のメタデータ - Exif 情報関係] のコードを参考にしてください。また、各プロパティ値のラベルは、プロパティタグの定数名にしています。


[備考]
東京大学空間情報科学研究センターの「人の流れプロジェクト」のサイトでは、携帯電話の GPS を使用した、人の流れを動画で見ることができます。参考までに。

[注意]
GPS の使用を制限している場所があります。そのような場所では、デジタルカメラや携帯電話の GPS の使用を避けてください。公共の交通機関でも、車内アナウンスにもかかわらず、堂々と携帯電話の電源を切らなかったり、マナーモードにしない方がいます。
このようなことは、関係者 (特に医療関係者) でないと理解できない内容もあります。詳しくは携帯電話の説明書やネットで調べてください。


図10
実行画面
  • 数百年後には、何故、ここに、このような像があるのか、調査が行われるかも知れない

リスト4
画像ファイルから、GPS の緯度と経度の情報を取得
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, GDIPAPI, GDIPOBJ, GDIPUTIL, ComCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenDialog1: TOpenDialog;
    Image1: TImage;
    LabeledEdit1: TLabeledEdit;
    LabeledEdit2: TLabeledEdit;
    LabeledEdit3: TLabeledEdit;
    LabeledEdit4: TLabeledEdit;
    LabeledEdit5: TLabeledEdit;
    LabeledEdit6: TLabeledEdit;
    LabeledEdit7: TLabeledEdit;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    FImageFilePath : string;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Self.Caption := 'GDI+  デジカメ写真のGPS情報 (一部)';
  FImageFilePath := '';
end;

//=============================================================================
//  [画像ファイル]ボタン
//  画像ファイルを読み込んで、GPSのメタデータの一部を取得して表示
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LGPImage        : TGPImage;
  LGPGraphic      : TGPGraphics;
  LAspectRatio    : Double;
  LpPropItem      : PPropertyItem;
  LIdValue        : Cardinal;
  LBufferSize     : Cardinal;
  LVersionValue   : PByte;
  LRationalValue  : array of Cardinal;
  LLatitudeDouble : Double;
begin
  //画像ファイルを読み込む
  FImageFilePath := '';
  if OpenDialog1.InitialDir = '' then begin
    OpenDialog1.InitialDir := ExtractFileDir(ExtractFileDir(ParamStr(0)));
  end;

  OpenDialog1.Filter := '画像ファイル|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tif;*.tiff';
  if OpenDialog1.Execute then begin
    OpenDialog1.InitialDir := ExtractFileDir(OpenDialog1.FileName);
    FImageFilePath := OpenDialog1.FileName;
  end;
  if FImageFilePath = '' then exit;


  //画像ファイルからTGPImageのインスタンスを生成
  LGPImage := TGPBitmap.Create(FImageFilePath);

  //画像をTImageに表示(どんな画像かの確認用)
  Image1.Picture.Assign(nil);
  LAspectRatio := LGPImage.GetHeight / LGPImage.GetWidth;
  Image1.SetBounds(Image1.Left,
                   Image1.Top,
                   Image1.Width,
                   Round(Image1.Width * LAspectRatio));

  LGPGraphic := TGPGraphics.Create(Image1.Canvas.Handle);
  LGPGraphic.DrawImage(LGPImage, 0, 0, Image1.Width, Image1.Height);


  LabeledEdit1.Text := '';
  LabeledEdit2.Text := '';
  LabeledEdit3.Text := '';
  LabeledEdit4.Text := '';
  LabeledEdit5.Text := '';
  LabeledEdit6.Text := '';
  LabeledEdit7.Text := '';

  //GPSのIFDのバージョン
  //メタデータ取得用のバッファを確保し、そのバッファから読み取る
  //そのバッファの格納形式はTPropertyItem(ポインタ型はPPropertyItem)
  LIdValue := PropertyTagGpsVer;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      if LpPropItem^.type_ = PropertyTagTypeByte then begin
        LVersionValue     := LpPropItem^.value;
        LabeledEdit1.Text := IntToStr(LVersionValue[0]) + ' ' +
                             IntToStr(LVersionValue[1]) + ' ' +
                             IntToStr(LVersionValue[2]) + ' ' +
                             IntToStr(LVersionValue[3]);
      end;
    finally
      FreeMem(LpPropItem);
    end;
  end;

  //緯度が北か南か
  LIdValue    := PropertyTagGpsLatitudeRef;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      if LpPropItem^.type_ = PropertyTagTypeASCII then begin
        LabeledEdit2.Text := string(PAnsiChar(LpPropItem^.value));
      end;
    finally
      FreeMem(LpPropItem);
    end;
  end;

  //緯度
  //緯度と経度は、度分秒ごとに分数形式で格納されている
  LIdValue    := PropertyTagGpsLatitude;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      if LpPropItem^.type_ = PropertyTagTypeRational then begin
        //分母と分子で1組なので配列要素は6つ必要
        SetLength(LRationalValue, 6);
        LRationalValue    := LpPropItem^.value;
        LabeledEdit3.Text := IntToStr(LRationalValue[0]) + ' / ' +
                             IntToStr(LRationalValue[1]) + ' ' +
                             IntToStr(LRationalValue[2]) + ' / ' +
                             IntToStr(LRationalValue[3]) + ' ' +
                             IntToStr(LRationalValue[4]) + ' / ' +
                             IntToStr(LRationalValue[5]);

       //緯度の値を実数に変換して表示
       LLatitudeDouble := LRationalValue[0] / LRationalValue[1] +
                         LRationalValue[2] / LRationalValue[3] / 60.0 +
                         LRationalValue[4] / LRationalValue[5] / 3600.0;
        LabeledEdit4.Text := FormatFloat('#0.000000000', LLatitudeDouble);
      end;
    finally
      FreeMem(LpPropItem);
    end;
  end;

  //経度が東か西か
  LIdValue    := PropertyTagGpsLongitudeRef;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      if LpPropItem^.type_ = PropertyTagTypeASCII then begin
        LabeledEdit5.Text := string(PAnsiChar(LpPropItem^.value));
      end;
    finally
      FreeMem(LpPropItem);
    end;
  end;

  //経度
  //緯度と経度は、度分秒ごとに分数形式で格納されている
  LIdValue    := PropertyTagGpsLongitude;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      if LpPropItem^.type_ = PropertyTagTypeRational then begin
        //分母と分子で1組なので配列要素は6つ必要
        SetLength(LRationalValue, 6);
        LRationalValue    := LpPropItem^.value;
        LabeledEdit6.Text := IntToStr(LRationalValue[0]) + ' / ' +
                             IntToStr(LRationalValue[1]) + ' ' +
                             IntToStr(LRationalValue[2]) + ' / ' +
                             IntToStr(LRationalValue[3]) + ' ' +
                             IntToStr(LRationalValue[4]) + ' / ' +
                             IntToStr(LRationalValue[5]);

       //経度の値を実数に変換して表示
       LLatitudeDouble := LRationalValue[0] / LRationalValue[1] +
                         LRationalValue[2] / LRationalValue[3] / 60.0 +
                         LRationalValue[4] / LRationalValue[5] / 3600.0;
        LabeledEdit7.Text := FormatFloat('#0.000000000', LLatitudeDouble);
      end;
    finally
      FreeMem(LpPropItem);
    end;
  end;

  LGPGraphic.Free;
  LGPImage.Free;
end;

end.




06_ファイルのプロパティ


エクスプローラのポップアップメニューに [プロパティ] があります。これを表示した時のタイトル等の値の取得と操作のサンプルです。
GDI+ のプロパティタグには、これらに該当する定数名はありません。

ファイルのメタデータですから、Windows XP でも 7 でも設定、変更はできますが、Windows 7 ではプロパティのダイアログには表示されません。PropertyTagImageDescription がタイトルに該当するようです。その他の項目については調べていません。
Windows Vista では機能上の大きな変更がありました。このプロパティのダイアログ関係の値も例外ではありません。使用する際は十分に注意してください。

このサンプルでは、プロパティのリストを一括して取得しています。上の GPS の情報取得のサンプルやプロパティの設定のコードのように個別に取得することもできます。

表1 
Windows XP の [プロパティ] ダイアログの各項目のプロパティの数値を調べた結果

タグの数値 項目名 (Windows XP)
$9C9B タイトル
$9C9C コメント
$9C9D 作成者
$9C9E キーワード
$9C9F 表題


図11
設計時画面
図12
添付の画像を読み出し
  • 何も設定されていない
  • 画像を描画しているので、画像のサイズが大きいと時間がかかる
  • プロパティの操作だけであれば、画像を表示する必要はない
図13
プロパティを入力して [保存]

エクスプローラの [プロパティ] ダイアログでの確認結果です。赤枠の部分が、操作したファイルのプロパティです。
下図は、Windows XP の場合です。Windows Vista, 7 では該当プロパティは表示されません。


図14
変更前
図15
変更後

リスト5
ファイルのプロパティの操作
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, GDIPAPI, GDIPOBJ, GDIPUTIL, ComCtrls, ExtCtrls, ActiveX;

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenDialog1: TOpenDialog;
    Button2: TButton;
    Image1: TImage;
    LabeledEdit1: TLabeledEdit;
    LabeledEdit2: TLabeledEdit;
    LabeledEdit3: TLabeledEdit;
    LabeledEdit4: TLabeledEdit;
    LabeledEdit5: TLabeledEdit;
    Button3: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
    FImageFilePath : string;
    FMemStream     : TMemoryStream;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Self.Caption  := 'GDI+  ファイルのプロパティ';
  FImageFilePath := '';
end;

//=============================================================================
//  フォームのOnDestroyイベント処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  if Assigned(FMemStream) then FreeAndNil(FMemStream);
end;

//=============================================================================
//  [画像ファイル]ボタン
//  画像ファイルを読み込んで、メタデータを取得して表示
//
//  このサンプルでは、画像ファイルの[上書き保存]を可能にするために、
//  (1) 画像をメモリストリームに格納
//  (2) 画像のインスタンスのTGPImageは解放
//  (3) 他のボタンでの操作では、メモリストリームからTGPImageを生成
//  (4) このTGPImageを、読み出した時のファイル名で保存
//  としている
//  画像を表示する必要がなければ、
//  FMemStream.LoadFromFile(FImageFilePath);
//  として、最初からメモリストリームに画像を格納してもよい
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LGPImage       : TGPImage;
  LGPGraphic     : TGPGraphics;
  LAspectRatio   : Double;
  LSize          : UINT;
  LCount         : UINT;
  LPropItemArray : array of TPropertyItem;
  LpPropItem     : PPropertyItem;
  LIdValue       : Cardinal;
  LBufferSize    : Cardinal;
  LBuffer        : WideString;
  LImageGUID     : TGUID;
  i              : Integer;
begin
  //画像ファイルを読み込む
  FImageFilePath := '';
  if OpenDialog1.InitialDir = '' then begin
    OpenDialog1.InitialDir := ExtractFileDir(ExtractFileDir(ParamStr(0)));
  end;

  OpenDialog1.Filter := '画像ファイル|*.png;*.jpg;*.jpeg;*.bmp;*.gif';
  if OpenDialog1.Execute then begin
    OpenDialog1.InitialDir := ExtractFileDir(OpenDialog1.FileName);
    FImageFilePath := OpenDialog1.FileName;
  end;
  if FImageFilePath = '' then exit;


  //画像ファイルからTGPImageのインスタンスを生成
  LGPImage := TGPBitmap.Create(FImageFilePath);

  //画像をTImageに表示(どんな画像かの確認用)
  Image1.Picture.Assign(nil);
  LAspectRatio := LGPImage.GetHeight / LGPImage.GetWidth;
  Image1.SetBounds(Image1.Left, Image1.Top, Image1.Width,
                                            Round(Image1.Width * LAspectRatio));
  LGPGraphic := TGPGraphics.Create(Image1.Canvas.Handle);
  LGPGraphic.DrawImage(LGPImage, 0, 0, Image1.Width, Image1.Height);


  LabeledEdit1.Text := '';
  LabeledEdit2.Text := '';
  LabeledEdit3.Text := '';
  LabeledEdit4.Text := '';
  LabeledEdit5.Text := '';

  LSize  := 0;
  LCount := 0;
  //プロパティのバイトサイズと数を取得して、その分のメモリを確保
  LGPImage.GetPropertySize(LSize, LCount);

  //メタデータ取得用のバッファを確保し、そのバッファから読み取る
  //そのバッファの格納形式はTPropertyItem(ポインタ型はPPropertyItem)
  GetMem(Pointer(LPropItemArray), LSize);
  try
    //メモリを0でクリアして全てのプロパティをPropItemArrayに取得
    ZeroMemory(Pointer(LPropItemArray), LSize);
    LGPImage.GetAllPropertyItems(LSize, LCount, Pointer(LPropItemArray));

    //プロパティを順次走査して、該当プロパティの時だけ処理
    for i := 0 to LCount - 1 do begin
      LpPropItem   := @LPropItemArray[i];
      LIdValue     := LpPropItem^.id;
      if (LIdValue >= $9C9B) and (LIdValue <= $9C9F) then begin
        LBufferSize := LpPropItem^.length;
        if LBufferSize > 0 then begin
          //バッファからデータを取得。取得文字列の長さを設定
          //長さを設定しないとゴミが入る
          LBuffer := PWideChar(LpPropItem^.value);
          SetLength(LBuffer, LBufferSize div SizeOf(Char));
          case LIdValue of
            $9C9B : LabeledEdit1.Text := String(LBuffer);
            $9C9F : LabeledEdit2.Text := LBuffer;
            $9C9E : LabeledEdit3.Text := LBuffer;
            $9C9C : LabeledEdit4.Text := LBuffer;
            $9C9D : LabeledEdit5.Text := LBuffer;
          end;
        end;
      end;
    end;
  finally
    FreeMem(Pointer(LPropItemArray));
  end;

  //[上書き保存]を可能にするために画像をJPEG画像としてメモリストリームに格納
  //メモリストリームに直接は格納できないので、IStreamを仲介する
  if Assigned(FMemStream) then FreeAndNil(FMemStream);
  FMemStream := TMemoryStream.Create;
  if GetEncoderClsid('image/jpeg', LImageGUID) >= 0 then begin
    LGPImage.Save(TStreamAdapter.Create(FMemStream), LImageGUID);
  end;

  LGPGraphic.Free;
  LGPImage.Free;
end;

//=============================================================================
//  [上書き保存]ボタン
//  各TLabelEditの値で、プロパティを設定して、それをメタデータとして保存
//
//  Dephi 2009以降であれば、WideStringはstring、PWideCharはPCharでよい
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LIStrema   : IStream;
  LGPImage   : TGPImage;
  LPropItem  : TPropertyItem;
  LStrItem   : WideString;
  LImageGUID : TGUID;
begin
  if not Assigned(FMemStream) then exit;

  //画像をIStreamを介してメモリストリームから読み出し、TGPImageを生成
  //メモリストリームからは直接生成できないので注意
  LIStrema := TStreamAdapter.Create(FMemStream);
  LGPImage := TGPBitmap.Create(LIStrema);

  //[タイトル]
  if Trim(LabeledEdit1.Text) <> '' then begin
    //文字コード(コードページ)の処理を確実にするには、一旦変数に代入した方がよい
    LStrItem := LabeledEdit1.Text;

    //TPropertyItemレコード型のメンバーに値を代入して、プロパティをセット
    LPropItem.id     := $9C9B;
    LPropItem.type_  := PropertyTagTypeByte;
    LPropItem.value  := PWideChar(LStrItem);
    LPropItem.length := Length(LStrItem) * SizeOf(Char) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[表題]
  if Trim(LabeledEdit2.Text) <> '' then begin
    LStrItem := LabeledEdit2.Text;

    LPropItem.id     := $9C9F;
    LPropItem.type_  := PropertyTagTypeByte;
    LPropItem.value  := PWideChar(LStrItem);
    LPropItem.length := Length(LStrItem) * SizeOf(Char) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[キーワード]
  if Trim(LabeledEdit3.Text) <> '' then begin
    LStrItem := LabeledEdit3.Text;

    LPropItem.id     := $9C9E;
    LPropItem.type_  := PropertyTagTypeByte;
    LPropItem.value  := PWideChar(LStrItem);
    LPropItem.length := Length(LStrItem) * SizeOf(Char) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[コメント]
  if Trim(LabeledEdit4.Text) <> '' then begin
    LStrItem := LabeledEdit4.Text;

    LPropItem.id     := $9C9C;
    LPropItem.type_  := PropertyTagTypeByte;
    LPropItem.value  := PWideChar(LStrItem);
    LPropItem.length := Length(LStrItem) * SizeOf(Char) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[作成者]
  if Trim(LabeledEdit5.Text) <> '' then begin
    LStrItem := LabeledEdit5.Text;

    LPropItem.id     := $9C9D;
    LPropItem.type_  := PropertyTagTypeByte;
    LPropItem.value  := PWideChar(LStrItem);
    LPropItem.length := Length(LStrItem) * SizeOf(Char) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //読み出した時と同じファイル名で、JPEG画像として上書き保存
  if GetEncoderClsid('image/jpeg', LImageGUID) >= 0 then begin
    LGPImage.Save(FImageFilePath, LImageGUID);
  end;

  LGPImage.Free;
end;

//=============================================================================
//  [プロパティを削除して保存]ボタン
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LIStrema   : IStream;
  LGPImage   : TGPImage;
  LImageGUID : TGUID;
begin
  if not Assigned(FMemStream) then exit;

  //画像をIStreamを介してメモリストリームから読み出し、TGPImageを生成
  //メモリストリームからは直接生成できないので注意
  LIStrema := TStreamAdapter.Create(FMemStream);
  LGPImage := TGPBitmap.Create(LIStrema);

  LGPImage.RemovePropertyItem($9C9B);
  LGPImage.RemovePropertyItem($9C9C);
  LGPImage.RemovePropertyItem($9C9D);
  LGPImage.RemovePropertyItem($9C9E);
  LGPImage.RemovePropertyItem($9C9F);

  //読み出した時と同じファイル名で、JPEG画像として上書き保存
  if GetEncoderClsid('image/jpeg', LImageGUID) >= 0 then begin
    LGPImage.Save(FImageFilePath, LImageGUID);
  end;

  LGPImage.Free;
end;

end.




07_ファイルのプロパティ - GDIPlus


同じく Delphi 2009 GDI+ Library を使用したサンプルです。動作仕様は上のサンプルと同じです。


リスト6
Delphi 2009 GDI+ Library を使用したファイルのプロパティの操作
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, GDIPlus, ComCtrls, ExtCtrls, ActiveX;

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenDialog1: TOpenDialog;
    Button2: TButton;
    Image1: TImage;
    LabeledEdit1: TLabeledEdit;
    LabeledEdit2: TLabeledEdit;
    LabeledEdit3: TLabeledEdit;
    LabeledEdit4: TLabeledEdit;
    LabeledEdit5: TLabeledEdit;
    Button3: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
    FImageFilePath : string;
    FMemStream     : TMemoryStream;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Self.Caption  := 'GDI+  ファイルのプロパティ GDIPlus';
  FImageFilePath := '';
end;

//=============================================================================
//  フォームのOnDestroyイベント処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  if Assigned(FMemStream) then FreeAndNil(FMemStream);
end;

//=============================================================================
//  [画像ファイル]ボタン
//  画像ファイルを読み込んで、メタデータを取得して表示
//
//  このサンプルでは、画像ファイルの[上書き保存]を可能にするために、
//  (1) 画像をメモリストリームに格納
//  (2) 画像のインスタンスのTGPImageは解放
//  (3) 他のボタンでの操作では、メモリストリームからTGPImageを生成
//  (4) このTGPImageを、読み出した時のファイル名で保存
//  としている
//  画像を表示する必要がなければ、
//  FMemStream.LoadFromFile(FImageFilePath);
//  として、最初からメモリストリームに画像を格納してもよい
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LGPImage     : IGPImage;
  LGPGraphic   : IGPGraphics;
  LAspectRatio : Double;
  LPropItem    : IGPPropertyItem;
  LIdValue     : Cardinal;
  LBuffer      : string;
  LBufferSize  : Cardinal;
  i            : Integer;
begin
  //画像ファイルを読み込む
  FImageFilePath := '';
  if OpenDialog1.InitialDir = '' then begin
    OpenDialog1.InitialDir := ExtractFileDir(ExtractFileDir(ParamStr(0)));
  end;

  OpenDialog1.Filter := '画像ファイル|*.png;*.jpg;*.jpeg;*.bmp;*.gif';
  if OpenDialog1.Execute then begin
    OpenDialog1.InitialDir := ExtractFileDir(OpenDialog1.FileName);
    FImageFilePath := OpenDialog1.FileName;
  end;
  if FImageFilePath = '' then exit;


  //画像ファイルからTGPImageのインスタンスを生成
  LGPImage := TGPBitmap.Create(FImageFilePath);

  //画像をTImageに表示(どんな画像かの確認用)
  Image1.Picture.Assign(nil);
  LAspectRatio := LGPImage.GetHeight / LGPImage.GetWidth;
  Image1.SetBounds(Image1.Left, Image1.Top, Image1.Width,
                                            Round(Image1.Width * LAspectRatio));
  LGPGraphic := TGPGraphics.Create(Image1.Canvas.Handle);
  LGPGraphic.DrawImage(LGPImage, 0, 0, Image1.Width, Image1.Height);


  LabeledEdit1.Text := '';
  LabeledEdit2.Text := '';
  LabeledEdit3.Text := '';
  LabeledEdit4.Text := '';
  LabeledEdit5.Text := '';

  //プロパティを順次走査して、該当プロパティの時だけ処理
  for i := 0 to LGPImage.PropertyIdList.Count - 1 do begin
    LIdValue  := LGPImage.PropertyIdList[i];

    if (LIdValue >= $9C9B) and (LIdValue <= $9C9F) then begin
      //この項目のTGPPropertyItemからBufferに値を取得
      //Bufferのバイト数から、文字例の長さを取得文字数分の長さにする
      //長さを設定しないとゴミが入る
      LPropItem   := LGPImage.GetPropertyItem(LIdValue);
      LBuffer     := PChar(LPropItem.Value);
      LBufferSize := LPropItem.Length;
      SetLength(LBuffer, LBufferSize div SizeOf(Char));
      case LIdValue of
        $9C9B : LabeledEdit1.Text := LBuffer;
        $9C9F : LabeledEdit2.Text := LBuffer;
        $9C9E : LabeledEdit3.Text := LBuffer;
        $9C9C : LabeledEdit4.Text := LBuffer;
        $9C9D : LabeledEdit5.Text := LBuffer;
      end;
    end;
  end;

  //[上書き保存]を可能にするために画像をJPEG画像としてメモリストリームに格納
  //メモリストリームに直接は格納できないので、IStreamを仲介する
  if Assigned(FMemStream) then FreeAndNil(FMemStream);
  FMemStream := TMemoryStream.Create;
  LGPImage.Save(TStreamAdapter.Create(FMemStream), TGPImageFormat.Jpeg);
end;

//=============================================================================
//  [上書き保存]ボタン
//  各TLabelEditの値で、プロパティを設定して、それをメタデータとして保存
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LIStrema  : IStream;
  LGPImage  : IGPImage;
  LPropItem : IGPPropertyItem;
begin
  if not Assigned(FMemStream) then exit;

  //画像をIStreamを介してメモリストリームから読み出し、TGPImageを生成
  //メモリストリームからは直接生成できないので注意
  LIStrema := TStreamAdapter.Create(FMemStream);
  LGPImage := TGPBitmap.Create(LIStrema);

  //[タイトル]
  if Trim(LabeledEdit1.Text) <> '' then begin
    //TGPPropertyItemのインスタンスを生成し、各プロパティの値を代入
    LPropItem := TGPPropertyItem.Create;
    LPropItem.Id        := $9C9B;
    LPropItem.ValueType := PropertyTagTypeByte;
    LPropItem.Value     := PChar(LabeledEdit1.Text);
    LPropItem.Length    := Length(LabeledEdit1.Text) * SizeOf(Char) + 1;
    //プロパティをセット
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[表題]
  if Trim(LabeledEdit2.Text) <> '' then begin
    LPropItem := TGPPropertyItem.Create;
    LPropItem.Id        := $9C9F;
    LPropItem.ValueType := PropertyTagTypeByte;
    LPropItem.Value     := PChar(LabeledEdit2.Text);
    LPropItem.Length    := Length(LabeledEdit2.Text) * SizeOf(Char) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[キーワード
  if Trim(LabeledEdit3.Text) <> '' then begin
    LPropItem := TGPPropertyItem.Create;
    LPropItem.Id        := $9C9E;
    LPropItem.ValueType := PropertyTagTypeByte;
    LPropItem.Value     := PChar(LabeledEdit3.Text);
    LPropItem.Length    := Length(LabeledEdit3.Text) * SizeOf(Char) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[コメント]
  if Trim(LabeledEdit4.Text) <> '' then begin
    LPropItem := TGPPropertyItem.Create;
    LPropItem.Id        := $9C9C;
    LPropItem.ValueType := PropertyTagTypeByte;
    LPropItem.Value     := PChar(LabeledEdit4.Text);
    LPropItem.Length    := Length(LabeledEdit4.Text) * SizeOf(Char) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //[作成者]
  if Trim(LabeledEdit5.Text) <> '' then begin
    LPropItem := TGPPropertyItem.Create;
    LPropItem.Id        := $9C9D;
    LPropItem.ValueType := PropertyTagTypeByte;
    LPropItem.Value     := PChar(LabeledEdit5.Text);
    LPropItem.Length    := Length(LabeledEdit5.Text) * SizeOf(Char) + 1;
    LGPImage.SetPropertyItem(LPropItem);
  end;

  //読み出した時と同じファイル名で、JPEG画像として上書き保存
  //元の画像がJPEG形式でない場合はJPEGに変換して保存
  FImageFilePath := ChangeFileExt(FImageFilePath, '.jpg');
  LGPImage.Save(FImageFilePath, TGPImageFormat.Jpeg);
end;

//=============================================================================
//  [プロパティを削除して保存]ボタン
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LIStrema : IStream;
  LGPImage : IGPImage;
  LIdValue : Cardinal;
  i        : Integer;
begin
  if not Assigned(FMemStream) then exit;

  LGPImage := nil;
  //画像をIStreamを介してメモリストリームから読み出し、TGPImageを生成
  //メモリストリームからは直接生成できないので注意
  LIStrema := TStreamAdapter.Create(FMemStream);
  LGPImage := TGPBitmap.Create(LIStrema);

  //プロパティのリストから、該当プロパティの時に、そのプロパティを削除
  for i := LGPImage.PropertyIdList.Count - 1 downto 0 do begin
    LIdValue := LGPImage.PropertyIdList[i];
    if (LIdValue >= $9C9B) and (LIdValue <= $9C9F) then begin
      LGPImage.RemovePropertyItem(LIdValue);
    end;
  end;

  //読み出した時と同じファイル名で、JPEG画像として上書き保存
  //元の画像がJPEG形式でない場合はJPEGに変換して保存
  FImageFilePath := ChangeFileExt(FImageFilePath, '.jpg');
  LGPImage.Save(FImageFilePath, TGPImageFormat.Jpeg);
end;

end.




08_PropertyTagTypeLong 型プロパティの読み書き


プロパティの型が、PropertyTagTypeLong の場合のメタデータの読み書きの参考コードです。
MSDN の記事では配列となっていますが、データの位置は各々のデータを示すポインタの位置できまりますから、単一の値の場合は単なる符号なし 32 ビット整数型で処理できます。

読み出しのコードではポインタ型の変数を宣言しています。書き込みの方はポインタ型ではない変数宣言を使用していますが、読み出しの際に、ポインタ型の変数を使用しなければいけないという規則はありません。また、ULONG を使用していますが、Cardinal または LongWord でも構いません。

プロパティの書き込みは、該当プロパティが対象画像ファイルにない場合は、追加となります。
また、読み出しのコードで、BufferSize の値が 0 の時は、その画像ファイルには該当プロパティが存在しないか、存在しても値がない、つまり空であることを意味しています。



リスト7
プロパティの型が PropertyTagTypeLong のデータの読み出し
var
  LGPImage     : TGPImage;
  LpPropItem   : PPropertyItem;
  LIdValue     : Cardinal;
  LBufferSize  : Cardinal;
  LpLongValue  : ^ULONG;// PLongArrayValue;
begin


  LabeledEdit1.Text := '';

  LIdValue := PropertyTagExifPixXDim;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      LpLongValue := LpPropItem^.value;
      LabeledEdit1.Text := IntToStr(LpLongValue^);
    finally
      FreeMem(LpPropItem);
    end;
  end;


end;


リスト8
プロパティの型が PropertyTagTypeLong のデータの書き込み
var
  LGPImage   : TGPImage;
  LPropItem  : TPropertyItem;
  LStrItem   : string;
  LLongValue : ULONG;
begin


  if Trim(LabeledEdit1.Text) <> '' then begin
    LStrItem   := LabeledEdit1.Text;
    LLongValue := StrToIntDef(LStrItem, 0);

    LPropItem.id     := PropertyTagExifPixXDim;
    LPropItem.type_  := PropertyTagTypeLong;
    LPropItem.value  := @LLongValue;
    LPropItem.length := SizeOf(ULong);
    LGPImage.SetPropertyItem(LPropItem);
  end;


end;




09_PropertyTagTypeShort 型プロパティの読み書き


プロパティの型が、PropertyTagTypeShort の場合のメタデータの読み書きの参考コードです。
MSDN の記事では配列となっていますが、データの位置は各々のデータを示すポインタの位置で決まりますから、単一の値の場合は単なる符号なし 16 ビット整数型で処理できます。


リスト9
プロパティの型が PropertyTagTypeShort のデータの読み出し
var
  LGPImage     : TGPImage;
  LpPropItem   : PPropertyItem;
  LIdValue     : Cardinal;
  LBufferSize  : Cardinal;
  LpShortValue : PWORD;
begin


  LabeledEdit1.Text := '';

  LIdValue := PropertyTagExifISOSpeed;
  LBufferSize := LGPImage.GetPropertyItemSize(LIdValue);
  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      LGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      LpShortValue := LpPropItem^.value;
      LabeledEdit1.Text := IntToStr(LpShortValue^);
    finally
      FreeMem(LpPropItem);
    end;
  end;


end;


リスト10
プロパティの型が PropertyTagTypeShort のデータの書き込み
var
  LGPImage    : TGPImage;
  LPropItem   : TPropertyItem;
  LStrItem    : string;
  LShortValue : WORD;
begin


  if Trim(LabeledEdit1.Text) <> '' then begin
    LStrItem    := LabeledEdit1.Text;
    LShortValue := StrToIntDef(LStrItem, 0);

    LPropItem.id     := PropertyTagExifISOSpeed;
    LPropItem.type_  := PropertyTagTypeShort;
    LPropItem.value  := @LShortValue;
    LPropItem.length := SizeOf(WORD);
    LGPImage.SetPropertyItem(LPropItem);
  end;


end;




10_PropertyTagTypeByte 型プロパティの読み書き


プロパティの型が、PropertyTagTypeByte の場合のメタデータの読み書きの参考コードです。
プロパティがバイト配列の場合です。

PropertyTagTypeByte 型プロパティは、格納されているデータの種類によってデータの取り出し方と書き込み方が違ってきます。下のコードは、各々のバイトの値が意味を持っている場合です。
文字列の場合は、[06_ファイルのプロパティ] の項を参考にしてください。



リスト11
プロパティの型が PropertyTagTypeByte のデータの読み出し
var
  LpPropItem    : PPropertyItem;
  LIdValue      : Cardinal;
  LBufferSize   : Cardinal;
  LVersionValue : PByte;
  LStrText      : string;
  i             : Integer;
begin


  LabeledEdit1.Text := '';

  LIdValue    := PropertyTagGpsVer;
  LBufferSize := FGPImage.GetPropertyItemSize(LIdValue);

  if LBufferSize > 0 then begin
    GetMem(LpPropItem, LBufferSize);
    try
      FGPImage.GetPropertyItem(LIdValue, LBufferSize, LpPropItem);
      LVersionValue := LpPropItem^.value;
      LStrText := '';
      for i := 0 to LpPropItem^.length - 1 do begin
        LStrText := LStrText + IntToStr(LVersionValue[i]);
      end;
      LabeledEdit1.Text := LStrText;
    finally
      FreeMem(LpPropItem);
    end;
  end;


end;


リスト12
プロパティの型が PropertyTagTypeByte のデータの書き込み
var
  LPropItem  : TPropertyItem;
  LStrItem   : string;
  LByteArray : array of Byte;
  LIntErr    : Integer;
  i          : Integer;
begin


  if Trim(LabeledEdit1.Text) <> '' then begin
    LStrItem := LabeledEdit1.Text;

    LPropItem.id     := PropertyTagGpsVer;
    LPropItem.type_  := PropertyTagTypeByte;
    LPropItem.length := Length(Trim(LStrItem));

    SetLength(LByteArray, LPropItem.length);
    for i := 0 to LPropItem.length - 1 do begin
      Val(Copy(LStrItem, i + 1, 1), LByteArray[i], LIntErr);
    end;
    LPropItem.value  := LByteArray;
    FGPImage.SetPropertyItem(LPropItem);
  end;


end;