unit uExtractImages;
{$I ..\..\..\Lib\PDFiumVcl.inc}

interface

uses
{$IFDEF XE2+}
  Winapi.Windows,
  Winapi.Messages,
  Winapi.ShellAPI,
  System.SysUtils,
  System.Variants,
  System.Classes,
  System.Types,
  System.StrUtils,
  System.IOUtils,
  Vcl.Graphics,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Dialogs,
  Vcl.XPMan,
  Vcl.Buttons,
  Vcl.StdCtrls,
  Vcl.ComCtrls,
  Vcl.ExtCtrls,
  Vcl.Samples.Spin,
  Vcl.FileCtrl,
  Vcl.Imaging.jpeg,
{$ELSE}
  Windows,
  Messages,
  ShellAPI,
  SysUtils,
  Variants,
  Classes,
  StrUtils,
  Graphics,
  Controls,
  Forms,
  Dialogs,
  XPMan,
  Buttons,
  StdCtrls,
  ComCtrls,
  ExtCtrls,
  Spin,
  FileCtrl,
  jpeg,
{$ENDIF}
  PDFium;

type
  TImageInfo= record
    FileName: string;
    PageNumber: Integer;
    ImageIndex: Integer;
    Width: Integer;
    Height: Integer;
    Format: string;
    Size: Int64;
    Bitmap: TBitmap;
  end;

  TFormMain= class(TForm)
    // Original controls
    Pdf: TPdf;
    OpenDialog: TOpenDialog;
    ProgressBar: TProgressBar;

    // New panels and layout
    PanelTop: TPanel;
    PanelProgress: TPanel;
    PanelMain: TPanel;
    PanelBottom: TPanel;
    PanelPreview: TPanel;
    PanelInfo: TPanel;
    SplitterMain: TSplitter;

    // Input controls
    LabelPdfFile: TLabel;
    EditPdfFile: TEdit;
    SpeedButtonPdfFile: TSpeedButton;
    LabelOutputDir: TLabel;
    EditOutputDir: TEdit;
    SpeedButtonOutputDir: TSpeedButton;
    LabelPageRange: TLabel;
    EditPageRange: TEdit;

    // Action buttons
    ButtonExtract: TButton;
    ButtonCancel: TButton;
    ButtonSaveAs: TButton;
    ButtonOpenFolder: TButton;

    // Status and progress
    LabelStatus: TLabel;
    LabelImageCount: TLabel;

    // Preview and info controls
    LabelPreviewTitle: TLabel;
    ImagePreview: TImage;
    LabelInfoTitle: TLabel;
    MemoInfo: TMemo;
    GroupBoxImageList: TGroupBox;
    ListBoxImages: TListBox;

    // Dialogs
    SaveDialog: TSaveDialog;
    Timer: TTimer;

    // Event handlers
    procedure FormCreate(Sender: TObject);
    procedure SpeedButtonPdfFileClick(Sender: TObject);
    procedure SpeedButtonOutputDirClick(Sender: TObject);
    procedure ButtonExtractClick(Sender: TObject);
    procedure ButtonCancelClick(Sender: TObject);
    procedure ButtonSaveAsClick(Sender: TObject);
    procedure ButtonOpenFolderClick(Sender: TObject);
    procedure EditPdfFileChange(Sender: TObject);
    procedure EditPageRangeChange(Sender: TObject);
    procedure ListBoxImagesClick(Sender: TObject);
    procedure TimerTimer(Sender: TObject);

  private
    { Private declarations }
    FExtractedImages: array of TImageInfo;
    FCurrentOutputDir: string;
    FCancelled: Boolean;
    FTotalImages: Integer;
    FProcessedImages: Integer;

    procedure ClearExtractedImages;
    procedure AddImageInfo(const AImageInfo: TImageInfo);
    procedure UpdateImageList;
    procedure UpdatePreview(Index: Integer);
    procedure UpdateStatus(const AMessage: string);
    procedure UpdateImageCount;
    procedure ParsePageRange(
      const ARange            : string;
      var AStartPage, AEndPage: Integer);
    function ValidatePageRange(const ARange: string): Boolean;
    function DetectImageFormat(ABitmap: TBitmap): string;
    function GetExtensionForFormat(const AFormat: string): string;
    procedure SaveBitmapInOptimalFormat(
      ABitmap        : TBitmap;
      const AFileName: string);
    procedure EnableControls(AEnabled: Boolean);
    procedure UpdateOutputDirectory;
  public
    { Public declarations }
  end;

var
  FormMain: TFormMain;

implementation

{$R *.dfm}
{ TFormMain }

procedure TFormMain.FormCreate(Sender: TObject);
begin
  FCancelled:= False;
  FTotalImages:= 0;
  FProcessedImages:= 0;

  // Set default output directory to Documents (will be updated when PDF is selected)
  FCurrentOutputDir:= '';
  EditOutputDir.Text:= FCurrentOutputDir;

  // Initialize UI
  UpdateImageCount;

end;

procedure TFormMain.SpeedButtonPdfFileClick(Sender: TObject);
begin
  with OpenDialog do
  begin
    FileName:= EditPdfFile.Text;
    if Execute
    then
    begin
      EditPdfFile.Text:= FileName;
      UpdateOutputDirectory;
    end;
  end;
end;

procedure TFormMain.SpeedButtonOutputDirClick(Sender: TObject);
var
  SelectedDir: string;
begin
  SelectedDir:= EditOutputDir.Text;
  if SelectDirectory('Select Output Directory', '', SelectedDir)
  then
  begin
    EditOutputDir.Text:= SelectedDir;
    FCurrentOutputDir:= SelectedDir;
    // Create output directory if it doesn't exist
    if not {$IFDEF XE2+}System.SysUtils.DirectoryExists{$ELSE}SysUtils.DirectoryExists{$ENDIF}(FCurrentOutputDir)
    then
{$IFDEF XE2+}System.SysUtils.ForceDirectories{$ELSE}SysUtils.ForceDirectories{$ENDIF}(FCurrentOutputDir);
  end;
end;

procedure TFormMain.EditPdfFileChange(Sender: TObject);
var
  CanExtract: Boolean;
begin
  // Update output directory based on PDF file location
  if FileExists(EditPdfFile.Text)
  then
    UpdateOutputDirectory;

  CanExtract:= FileExists(EditPdfFile.Text)and
{$IFDEF XE2+}System.SysUtils.DirectoryExists{$ELSE}SysUtils.DirectoryExists{$ENDIF}(EditOutputDir.Text)and ValidatePageRange(EditPageRange.Text);
  ButtonExtract.Enabled:= CanExtract;
end;

procedure TFormMain.EditPageRangeChange(Sender: TObject);
begin
  EditPdfFileChange(Sender);
end;

procedure TFormMain.ButtonExtractClick(Sender: TObject);
var
  I, J, StartPage, EndPage: Integer;
  BaseFileName: string;
  Bitmap: TBitmap;
  ImageInfo: TImageInfo;
  FullFileName, DetectedFormat, ImageExtension: string;
begin
  FCancelled:= False;
  FProcessedImages:= 0;
  FTotalImages:= 0;

  ClearExtractedImages;
  EnableControls(False);
  ButtonCancel.Enabled:= True;

  UpdateStatus('Initializing PDF...');

  try
    Pdf.FileName:= EditPdfFile.Text;
    Pdf.PageNumber:= 0;
    Pdf.Active:= True;

    ParsePageRange(EditPageRange.Text, StartPage, EndPage);
    if EndPage= - 1
    then
      EndPage:= Pdf.PageCount;

    // Calculate total images for progress
    for I:= StartPage to EndPage do
    begin
      Pdf.PageNumber:= I;
      FTotalImages:= FTotalImages+ Pdf.BitmapCount;
    end;

    ProgressBar.Max:= FTotalImages;
    ProgressBar.Position:= 0;

    BaseFileName:= ExtractFileName(Pdf.FileName);
    BaseFileName:= ChangeFileExt(BaseFileName, '');

    UpdateStatus(Format('Extracting %d images from %d pages...', [FTotalImages, EndPage- StartPage+ 1]));

    for I:= StartPage to EndPage do
    begin
      if FCancelled
      then
        Break;

      Pdf.PageNumber:= I;

      for J:= 0 to Pdf.BitmapCount- 1 do
      begin
        if FCancelled
        then
          Break;

        Application.ProcessMessages;

        UpdateStatus(Format('Processing Page %d, Image %d...', [I, J+ 1]));

        Bitmap:= Pdf.Bitmap[J];
        if Assigned(Bitmap)
        then
        begin
          try
            // Detect optimal format for this image
            DetectedFormat:= DetectImageFormat(Bitmap);
            ImageExtension:= GetExtensionForFormat(DetectedFormat);

            FullFileName:= Format('%s\Page%d_Image%d%s', [FCurrentOutputDir, I, J+ 1, ImageExtension]);

            SaveBitmapInOptimalFormat(Bitmap, FullFileName);

            // Create image info record
            ImageInfo.FileName:= FullFileName;
            ImageInfo.PageNumber:= I;
            ImageInfo.ImageIndex:= J+ 1;
            ImageInfo.Width:= Bitmap.Width;
            ImageInfo.Height:= Bitmap.Height;
            ImageInfo.Format:= DetectedFormat;
            ImageInfo.Size:= 0; // Will be calculated after saving
            ImageInfo.Bitmap:= TBitmap.Create;
            ImageInfo.Bitmap.Assign(Bitmap);

            if FileExists(FullFileName)
            then
            begin
{$IFDEF XE2+}
              ImageInfo.Size:= TFile.GetSize(FullFileName);
{$ELSE}
              // For older Delphi versions, use a simple file size calculation
              ImageInfo.Size:= 0;
{$ENDIF}
            end;

            AddImageInfo(ImageInfo);

            Inc(FProcessedImages);
            ProgressBar.Position:= FProcessedImages;

          finally
            Bitmap.Free;
          end;
        end;
      end;
    end;

    if FCancelled
    then
      UpdateStatus('Extraction cancelled by user.')
    else
      UpdateStatus(Format('Successfully extracted %d images.', [Length(FExtractedImages)]));

    UpdateImageList;
    UpdateImageCount;
    ButtonOpenFolder.Enabled:= True;

  except
    on E: Exception do
    begin
      UpdateStatus('Error: '+ E.Message);
      ShowMessage('Error extracting images: '+ E.Message + sLineBreak + 'Copy the DLL to the system or program directory first.');
    end;
  end;

  Pdf.Active:= False;
  EnableControls(True);
  ButtonCancel.Enabled:= False;
end;

procedure TFormMain.ButtonCancelClick(Sender: TObject);
begin
  FCancelled:= True;
  UpdateStatus('Cancelling extraction...');
end;

procedure TFormMain.ButtonSaveAsClick(Sender: TObject);
var
  SelectedIndex: Integer;
begin
  SelectedIndex:= ListBoxImages.ItemIndex;
  if (SelectedIndex>= 0)and (SelectedIndex< Length(FExtractedImages))
  then
  begin
    SaveDialog.FileName:= ExtractFileName(FExtractedImages[SelectedIndex].FileName);
    if SaveDialog.Execute
    then
    begin
      try
        FExtractedImages[SelectedIndex].Bitmap.SaveToFile(SaveDialog.FileName);
        ShowMessage('Image saved successfully.');
      except
        on E: Exception do
          ShowMessage('Error saving image: '+ E.Message);
      end;
    end;
  end;
end;

procedure TFormMain.ButtonOpenFolderClick(Sender: TObject);
begin
  if {$IFDEF XE2+}System.SysUtils.DirectoryExists{$ELSE}SysUtils.DirectoryExists{$ENDIF}(FCurrentOutputDir)
  then
    ShellExecute(Handle, 'open', PChar(FCurrentOutputDir), nil, nil, SW_SHOWNORMAL)
  else
    ShowMessage('Output directory does not exist.');
end;

procedure TFormMain.ListBoxImagesClick(Sender: TObject);
var
  SelectedIndex: Integer;
begin
  SelectedIndex:= ListBoxImages.ItemIndex;
  if (SelectedIndex>= 0)and (SelectedIndex< Length(FExtractedImages))
  then
  begin
    UpdatePreview(SelectedIndex);
    ButtonSaveAs.Enabled:= True;
  end
  else
    ButtonSaveAs.Enabled:= False;
end;

procedure TFormMain.TimerTimer(Sender: TObject);
begin
  // Timer for periodic updates during extraction
  Application.ProcessMessages;
end;

// Private methods

procedure TFormMain.ClearExtractedImages;
var
  I: Integer;
begin
  for I:= 0 to Length(FExtractedImages)- 1 do
  begin
    if Assigned(FExtractedImages[I].Bitmap)
    then
      FExtractedImages[I].Bitmap.Free;
  end;
  SetLength(FExtractedImages, 0);
  ListBoxImages.Items.Clear;
  ImagePreview.Picture.Assign(nil);
  MemoInfo.Lines.Clear;
end;

procedure TFormMain.AddImageInfo(const AImageInfo: TImageInfo);
begin
  SetLength(FExtractedImages, Length(FExtractedImages)+ 1);
  FExtractedImages[Length(FExtractedImages)- 1]:= AImageInfo;
end;

procedure TFormMain.UpdateImageList;
var
  I: Integer;
begin
  ListBoxImages.Items.Clear;
  for I:= 0 to Length(FExtractedImages)- 1 do
  begin
    ListBoxImages.Items.Add(Format('Page %d, Image %d (%dx%d)', [FExtractedImages[I].PageNumber, FExtractedImages[I].ImageIndex, FExtractedImages[I].Width,
      FExtractedImages[I].Height]));
  end;
end;

procedure TFormMain.UpdatePreview(Index: Integer);
var
  Info: TImageInfo;
begin
  if (Index>= 0)and (Index< Length(FExtractedImages))
  then
  begin
    Info:= FExtractedImages[Index];

    // Update preview image
    if Assigned(Info.Bitmap)
    then
      ImagePreview.Picture.Assign(Info.Bitmap);

    // Update info memo
    MemoInfo.Lines.Clear;
    MemoInfo.Lines.Add('File: '+ ExtractFileName(Info.FileName));
    MemoInfo.Lines.Add('Page: '+ IntToStr(Info.PageNumber));
    MemoInfo.Lines.Add('Image: '+ IntToStr(Info.ImageIndex));
    MemoInfo.Lines.Add('Dimensions: '+ IntToStr(Info.Width)+ ' x '+ IntToStr(Info.Height));
    MemoInfo.Lines.Add('Format: '+ Info.Format);
    if Info.Size> 0
    then
      MemoInfo.Lines.Add('Size: '+ FormatFloat('#,##0', Info.Size)+ ' bytes');
    MemoInfo.Lines.Add('');
    MemoInfo.Lines.Add('Full Path:');
    MemoInfo.Lines.Add(Info.FileName);
  end;
end;

procedure TFormMain.UpdateStatus(const AMessage: string);
begin
  LabelStatus.Caption:= AMessage;
  LabelStatus.Refresh;
end;

procedure TFormMain.UpdateImageCount;
begin
  LabelImageCount.Caption:= Format('Total Images: %d', [Length(FExtractedImages)]);
end;

procedure TFormMain.ParsePageRange(
  const ARange            : string;
  var AStartPage, AEndPage: Integer);
var
  RangeStr: string;
  DashPos: Integer;
begin
  RangeStr:= Trim(ARange);
  AStartPage:= 1;
  AEndPage:= - 1; // -1 means to end

  if (RangeStr= '')or (UpperCase(RangeStr)= 'ALL')
  then
    Exit;

  DashPos:= Pos('-', RangeStr);
  if DashPos> 0
  then
  begin
    // Range format: start-end
    AStartPage:= StrToIntDef(Trim(Copy(RangeStr, 1, DashPos- 1)), 1);
    AEndPage:= StrToIntDef(Trim(Copy(RangeStr, DashPos+ 1, Length(RangeStr))), - 1);
  end
  else
  begin
    // Single page
    AStartPage:= StrToIntDef(RangeStr, 1);
    AEndPage:= AStartPage;
  end;
end;

function TFormMain.ValidatePageRange(const ARange: string): Boolean;
var
  StartPage, EndPage: Integer;
begin
  try
    ParsePageRange(ARange, StartPage, EndPage);
    Result:= (StartPage>= 1)and ((EndPage= - 1)or (EndPage>= StartPage));
  except
    Result:= False;
  end;
end;

function TFormMain.DetectImageFormat(ABitmap: TBitmap): string;
begin
  // Intelligent image format detection based on characteristics
  // For now, we'll use simple heuristics:

  // Check if image has transparency (alpha channel)
  if ABitmap.PixelFormat= pf32bit
  then
  begin
    // Could be PNG with transparency
    Result:= 'PNG';
  end
  // Check if it's likely a photographic image (complex colors)
  else if (ABitmap.Width* ABitmap.Height> 100000)and (ABitmap.PixelFormat in [pf24bit, pf32bit])
  then
  begin
    // Large, complex image - likely photographic
    Result:= 'JPEG';
  end
  // Simple graphics, text, diagrams
  else
  begin
    // Small or simple image - preserve quality with BMP
    Result:= 'BMP';
  end;
end;

function TFormMain.GetExtensionForFormat(const AFormat: string): string;
begin
  case UpperCase(AFormat)[1] of
  'J':
    Result:= '.jpg'; // JPEG
  'P':
    Result:= '.png'; // PNG
  'B':
    Result:= '.bmp'; // BMP
else
  Result:= '.bmp'; // Default
  end;
end;

procedure TFormMain.SaveBitmapInOptimalFormat(
  ABitmap        : TBitmap;
  const AFileName: string);
var
  JpegImg: TJPEGImage;
  FileExt: string;
begin
  FileExt:= UpperCase(ExtractFileExt(AFileName));

  if FileExt= '.JPG'
  then
  begin
    // Save as JPEG with good quality
    JpegImg:= TJPEGImage.Create;
    try
      JpegImg.Assign(ABitmap);
      JpegImg.CompressionQuality:= 85; // Good balance of quality/size
      JpegImg.SaveToFile(AFileName);
    finally
      JpegImg.Free;
    end;
  end
  else if FileExt= '.PNG'
  then
  begin
    // For PNG, save as BMP for now (full compatibility)
    // In production, use a PNG library like Vampyre Imaging
    ABitmap.SaveToFile(AFileName);
  end
  else
  begin
    // Default to BMP (lossless)
    ABitmap.SaveToFile(AFileName);
  end;
end;

procedure TFormMain.EnableControls(AEnabled: Boolean);
begin
  EditPdfFile.Enabled:= AEnabled;
  SpeedButtonPdfFile.Enabled:= AEnabled;
  EditOutputDir.Enabled:= AEnabled;
  SpeedButtonOutputDir.Enabled:= AEnabled;
  EditPageRange.Enabled:= AEnabled;
  ButtonExtract.Enabled:= AEnabled and FileExists(EditPdfFile.Text);
end;

procedure TFormMain.UpdateOutputDirectory;
var
  PdfDir, NewOutputDir: string;
begin
  if (EditPdfFile.Text<> '')and FileExists(EditPdfFile.Text)
  then
  begin
    // Get PDF file directory and name
    PdfDir:= ExtractFilePath(EditPdfFile.Text);

    // Create subdirectory based on PDF filename
    NewOutputDir:= PdfDir+ 'Images';

    // Update the output directory
    FCurrentOutputDir:= NewOutputDir;
    EditOutputDir.Text:= FCurrentOutputDir;

    // Create directory if it doesn't exist
    if not {$IFDEF XE2+}System.SysUtils.DirectoryExists{$ELSE}SysUtils.DirectoryExists{$ENDIF}(FCurrentOutputDir)
    then
    begin
      try
{$IFDEF XE2+}System.SysUtils.ForceDirectories{$ELSE}SysUtils.ForceDirectories{$ENDIF}(FCurrentOutputDir);
      except
        // If unable to create, fall back to Documents
        FCurrentOutputDir:= GetEnvironmentVariable('USERPROFILE')+ '\Documents\ExtractedImages';
        EditOutputDir.Text:= FCurrentOutputDir;
        if not {$IFDEF XE2+}System.SysUtils.DirectoryExists{$ELSE}SysUtils.DirectoryExists{$ENDIF}(FCurrentOutputDir)
        then
{$IFDEF XE2+}System.SysUtils.ForceDirectories{$ELSE}SysUtils.ForceDirectories{$ENDIF}(FCurrentOutputDir);
      end;
    end;
  end;
end;

end.
