'DEPLHI - Trying to get EXIF data on library images in Android

I am new at developing for Android, but not so new on Delphi development though. Anyway I am struggling to get EXIT data from an image (loaded from library) and show that image on the form.

Here is my code:

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  System.Messaging,
  {$IF CompilerVersion > 32}
  System.Permissions,
  {$ENDIF}
  Androidapi.JNI.JavaTypes, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics,
  FMX.Dialogs, FMX.StdActns, FMX.Controls.Presentation, FMX.Objects, FMX.Layouts,
  FMX.ScrollBox, FMX.Memo, FMX.Surfaces, FMX.ExtCtrls, FMX.StdCtrls,
  FMX.Helpers.Android, System.Actions, FMX.ActnList, FMX.MediaLibrary.Actions;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Layout1: TLayout;
    Memo1: TMemo;
    img1: TImageControl;
    procedure Button1Click(Sender: TObject);
  private
    FFileName: JString;
    procedure GetEXIF(const AFileName: JInputStream);
    procedure ResultNotificationMessageHandler(const Sender: TObject; const M: TMessage);
    procedure TakePhoto;
    {$IF CompilerVersion > 32}
    procedure TakePhotoPermissionsResultHandler(Sender: TObject; const APermissions: TArray<string>; const AGrantResults: TArray<TPermissionStatus>);
   {$ENDIF}
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

uses
  System.IOUtils, Androidapi.Helpers, Androidapi.JNI.Media, Androidapi.JNIBridge,
  Androidapi.JNI.Provider, Androidapi.JNI.App, Androidapi.JNI.Os,
  Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.Net,
  FMX.Platform.Android, DW.Androidapi.JNI.Os;

const
  cPermissionReadExternalStorage = 'android.permission.READ_EXTERNAL_STORAGE';
  cPermissionWriteExternalStorage = 'android.permission.WRITE_EXTERNAL_STORAGE';
  cPermissionCamera = 'android.permission.CAMERA';

{$IF CompilerVersion > 32}
type
  TGrantResults = TArray<TPermissionStatus>;

  TGrantResultsHelper = record helper for TGrantResults
  public
    function AreAllGranted: Boolean;
  end;

{ TGrantResultsHelper }

function TGrantResultsHelper.AreAllGranted: Boolean;
var
  LStatus: TPermissionStatus;
begin
  for LStatus in Self do
  begin
    if LStatus <> TPermissionStatus.Granted then
      Exit(False); // <======
  end;
  Result := True;
end;
{$ENDIF}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
{$IF CompilerVersion > 32}
  TPermissionsService.DefaultService.RequestPermissions([cPermissionReadExternalStorage, cPermissionWriteExternalStorage, cPermissionCamera], TakePhotoPermissionsResultHandler);
{$ELSE}
  TakePhoto;
{$ENDIF}
end;

constructor TForm1.Create(AOwner: TComponent);
begin
  inherited;
  TMessageManager.DefaultManager.SubscribeToMessage(TMessageResultNotification, ResultNotificationMessageHandler);
end;

destructor TForm1.Destroy;
begin
  TMessageManager.DefaultManager.Unsubscribe(TMessageResultNotification, ResultNotificationMessageHandler);
  inherited;
end;

procedure TForm1.GetEXIF(const AFileName: JInputStream);
var
  LEXIF: JExifInterface;
  LLatLong: TJavaArray<Single>;
  LStream: JFileInputStream;
begin
  try
    LEXIF := TJExifInterface.JavaClass.init(AFileName);
    Memo1.Lines.Clear;
    Memo1.Lines.Add('Date Taken: ' + JStringToString(LEXIF.getAttribute(TJExifInterface.JavaClass.TAG_DATETIME)));
    Memo1.Lines.Add('Camera Make: ' + JStringToString(LEXIF.getAttribute(TJExifInterface.JavaClass.TAG_MAKE)));
    Memo1.Lines.Add('Camera Model: ' + JStringToString(LEXIF.getAttribute(TJExifInterface.JavaClass.TAG_MODEL)));
    LLatLong := TJavaArray<Single>.Create(2);
    try
      if LEXIF.getLatLong(LLatLong) then
      begin
        Memo1.Lines.Add('Latitude: ' + LLatLong.Items[0].ToString);
        Memo1.Lines.Add('Longitude: ' + LLatLong.Items[1].ToString);
      end;
    finally
      LLatLong.Free;
    end;
  except
    on E: Exception do
      ShowMessage(e.Message);
  end;

end;

procedure TForm1.ResultNotificationMessageHandler(const Sender: TObject; const M: TMessage);
var
  LMessage: TMessageResultNotification;
  Str: string;
  FullPhotoUri: Jnet_Uri;
  ms: TMemoryStream;
  jis: JInputStream;
  b: TJavaArray<Byte>;
  NativeBitmap: JBitmap;
  Bitmap: TBitmapSurface;
begin
  if M is TMessageResultNotification then
  begin
    LMessage := TMessageResultNotification(M);
    if LMessage.RequestCode = 10011 then
      if (LMessage.ResultCode = TJActivity.JavaClass.RESULT_OK) then
        if Assigned(LMessage.Value) then
        try
          try
            FullPhotoUri := LMessage.Value.getData();

            jis := TAndroidHelper.Context.getContentResolver.openInputStream(FullPhotoUri);

            GetEXIF(jis);

            ms := TMemoryStream.Create;
            b := TJavaArray<Byte>.Create(jis.available);
            jis.read(b);
            ms.Write(b.Data^, b.Length);

            img1.Bitmap.LoadFromStream(ms);

            jis.close;

          except
            on E: Exception do
              Application.ShowException(e);
          end;
        finally
          ms.Free;
        end;

  end;
end;

procedure TForm1.TakePhotoPermissionsResultHandler(Sender: TObject; const APermissions: TArray<string>; const AGrantResults: TArray<TPermissionStatus>);
begin
  if TGrantResults(AGrantResults).AreAllGranted then
    TakePhoto
  else
    ShowMessage('Not all photo permissions granted!');
end;


// Based on: https://developer.android.com/training/camera/photobasics#java
procedure TForm1.TakePhoto;
var
  LIntent: JIntent;
  LFile, LDir: JFile;
  LUri: Jnet_Uri;
  LFileName: string;
begin
  LIntent := TJIntent.Create;
  LIntent.setAction(TJIntent.JavaClass.ACTION_OPEN_DOCUMENT).addCategory(TJIntent.JavaClass.CATEGORY_OPENABLE).setType(StringToJString('image/*'));

  if LIntent.resolveActivity(TAndroidHelper.Context.getPackageManager) <> nil then
  begin
    MainActivity.startActivityForResult(LIntent, 10011);
  end
  else
    ShowMessage('Cannot take a photo!');

end;

end.

Now, the error comes from the line:

img1.Bitmap.LoadFromStream(ms);

and the error is:

Project ObtainPhotoInfoDemo.apk raised exception class Segmentation fault (11).

Thanks

UPDATE

FOUND THE SOLUTION!!!!

blackapps commented and gave me an idea, the stream should be closed and reopened again to be used in an another call like this:

    FullPhotoUri := LMessage.Value.getData();
    //get input stream
    jis := TAndroidHelper.Context.getContentResolver.openInputStream(FullPhotoUri);
    GetEXIF(jis);
    //have to close it because GetEXIF already consumed it
    jis.close;
    //open it again
    jis := TAndroidHelper.Context.getContentResolver.openInputStream(FullPhotoUri);

    NativeBitmap := TJBitmapFactory.JavaClass.decodeStream(jis);
    Surf := TBitmapSurface.Create;
    if JBitmapToSurface(NativeBitmap, Surf) then
      img1.Bitmap.Assign(Surf);

    jis.close;


Solution 1:[1]

Hi Dejan and blackapps learnt much about your code many thanks.

To contribute this topic, kindly be informed that Android updated image metadata and bitmap access with Android 29+ SAF:

Access documents and other files from shared storage Android Developers - Open a document - Bitmap

Delphi translation

//Examine document metadata

  procedure dumpImageMetaData(uri : JNet_Uri);  (* GoruntuMetaVerisiDokumu *)
  // The query, because it only applies to a single document, returns only
  // one row. There's no need to filter, sort, or select fields,
  // because we want all fields for one document.
  var
    displayName, size : JString;
    sizeIndex : integer;
    cursor : JCursor;
  begin
    cursor := TAndroidHelper.Activity.getContentResolver.query(uri,nil,nil,nil,nil,nil);
    try
      // moveToFirst() returns false if the cursor has 0 rows. Very handy for
      // "if there's anything to look at, look at it" conditionals.
      if (cursor<>nil) then
        if (cursor.moveToFirst) then
        begin
          displayName := cursor.getString (cursor.getColumnIndex (TJOpenableColumns.JavaClass.DISPLAY_NAME));
          Memo1.Lines.Add({TAG.ToString +} 'Display Name: ' + JStringToString (displayName));
          sizeIndex:=cursor.getColumnIndex(TJOpenableColumns.JavaClass.SIZE);
          size := nil;
          if not (cursor.isNull(sizeIndex)) then
            size := cursor.getString(sizeIndex)
          else
            size:=StringToJString ('Unknown');
          Memo1.Lines.Add({TAG.ToString +} 'Size: ' + JStringToString (size));
        end;
    finally
      cursor.close;
    end;
  end;



//Open a document - Bitmap

  function getBitmapFromUri(uri : JNet_Uri): JBitmap;   (* UridenBiteslemAl *)
  var
    fileDescriptor : JFileDescriptor;
    parcelFileDescriptor : JParcelFileDescriptor;
    image : JBitmap;
  begin
    Result := nil;
    try
      parcelFileDescriptor := TAndroidHelper.Activity
        .getContentResolver.openFileDescriptor(uri,StringToJString('r'));
      fileDescriptor := parcelFileDescriptor.getFileDescriptor;
      image := TJBitmapFactory.JavaClass.decodeFileDescriptor(fileDescriptor);
      parcelFileDescriptor.close;
      result := image;
    except
      on E: Exception do
        ShowMessage(e.Message);
    end;

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1