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

interface

uses
{$IFDEF XE2+}
  Winapi.Windows,
  Winapi.Messages,
  System.SysUtils,
  System.IOUtils,
  System.Variants,
  System.Classes,
  Vcl.Graphics,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Dialogs,
  Vcl.XPMan,
  Vcl.Buttons,
  Vcl.StdCtrls,
  Vcl.ComCtrls,
  Vcl.FileCtrl,
{$ELSE}
  Windows,
  Messages,
  SysUtils,
  Variants,
  Classes,
  Graphics,
  Controls,
  Forms,
  Dialogs,
  XPMan,
  Buttons,
  StdCtrls,
  ComCtrls,
  FileCtrl,
{$ENDIF}
  PDFium;

type
  TIntArray= array of Integer;

  TFormMain= class(TForm)
    LabelPdfFile: TLabel;
    EditPdfFile: TEdit;
    SpeedButtonPdfFile: TSpeedButton;
    ButtonConvert: TButton;
    Pdf: TPdf;
    OpenDialog: TOpenDialog;
    ProgressBar: TProgressBar;
    LabelOutputDir: TLabel;
    EditOutputDir: TEdit;
    SpeedButtonOutputDir: TSpeedButton;
    LabelDPI: TLabel;
    EditDPI: TEdit;
    LabelQuality: TLabel;
    EditQuality: TEdit;
    LabelPages: TLabel;
    EditPages: TEdit;
    LabelPassword: TLabel;
    EditPassword: TEdit;
    ButtonCancel: TButton;
    procedure SpeedButtonPdfFileClick(Sender: TObject);
    procedure ButtonConvertClick(Sender: TObject);
    procedure EditPdfFileChange(Sender: TObject);
    procedure SpeedButtonOutputDirClick(Sender: TObject);
    procedure ButtonCancelClick(Sender: TObject);
  private
    { Private declarations }
    FCancelRequested: Boolean;
    function EnsureOutputDir(const BasePdfFile, UserOutDir: string): string;
    function ParsePageRanges(
      const S  : string;
      MaxPage  : Integer;
      out Pages: TIntArray): Boolean;
  public
    { Public declarations }
  end;

var
  FormMain: TFormMain;

implementation

uses
{$IFDEF XE2+}
  Vcl.Imaging.jpeg;
{$ELSE}
  jpeg;
{$ENDIF}
{$R *.dfm}

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

procedure TFormMain.ButtonConvertClick(Sender: TObject);
var
  I: Integer;
  OutDir: string;
  BaseName: string;
  FileName: string;
  JpegImage: TJpegImage;
  Bitmap: TBitmap;
  Dpi: Integer;
  Quality: Integer;
  Pages: TIntArray;
  PageDigits: Integer;
  UseAllPages: Boolean;
begin
  ProgressBar.Position:= 0;
  EditPdfFile.Enabled:= False;
  SpeedButtonPdfFile.Enabled:= False;
  ButtonConvert.Enabled:= False;
  ButtonCancel.Enabled:= True;
  FCancelRequested:= False;
  Screen.Cursor:= crHourGlass;
  try
    try
      if not FileExists(EditPdfFile.Text)
      then
        raise Exception.Create('PDF file does not exist.');

      // parse DPI (default 150)
      Dpi:= StrToIntDef(Trim(EditDPI.Text), 150);
      if Dpi<= 0
      then
        raise Exception.Create('DPI must be a positive integer.');

      // parse Quality (1..100, default 90)
      Quality:= StrToIntDef(Trim(EditQuality.Text), 90);
      if Quality< 1
      then
        Quality:= 1;
      if Quality> 100
      then
        Quality:= 100;

      // Prepare PDF
      Pdf.FileName:= EditPdfFile.Text;
      Pdf.Password:= EditPassword.Text; // set if needed
      Pdf.PageNumber:= 0;
      try
        Pdf.Active:= True;
      except
        on E: Exception do
        begin
          raise Exception.Create('Failed to open PDF: '+ E.Message);
        end;
      end;

      // Determine pages
      UseAllPages:= True;
      if Trim(EditPages.Text)<> ''
      then
      begin
        if not ParsePageRanges(EditPages.Text, Pdf.PageCount, Pages)
        then
          raise Exception.Create('Invalid page range. Use formats like 1-3,5,7-10.');
        UseAllPages:= Length(Pages)= 0;
      end;

      if UseAllPages
      then
      begin
        SetLength(Pages, Pdf.PageCount);
        for I:= 1 to Pdf.PageCount do
          Pages[I- 1]:= I;
      end;

      ProgressBar.Max:= Length(Pages);
      PageDigits:= Length(IntToStr(Pdf.PageCount));

      // Output directory and base name
      OutDir:= EnsureOutputDir(Pdf.FileName, EditOutputDir.Text);
      BaseName:= Copy(ExtractFileName(Pdf.FileName), 1, Length(ExtractFileName(Pdf.FileName))- Length(ExtractFileExt(Pdf.FileName)));

      JpegImage:= TJpegImage.Create;
      JpegImage.CompressionQuality:= Quality;
      try
        for I:= 0 to High(Pages) do
        begin
          if FCancelRequested
          then
            Break;

          Pdf.PageNumber:= Pages[I];

          Bitmap:= Pdf.RenderPage(0, 0, Round(PointsToPixels(Pdf.PageWidth, Dpi)), Round(PointsToPixels(Pdf.PageHeight, Dpi)));
          try
            JpegImage.Assign(Bitmap);
            FileName:= IncludeTrailingPathDelimiter(OutDir)+ Format('%s_Page%.*d.jpg', [BaseName, PageDigits, Pdf.PageNumber]);
            JpegImage.SaveToFile(FileName);
          finally
            Bitmap.Free;
          end;
          ProgressBar.Position:= I+ 1;
          Application.ProcessMessages;
        end;
      finally
        JpegImage.Free;
      end;
    except
      on E: Exception do
      begin
        Application.MessageBox(PChar('Conversion failed: '+ E.Message), 'Error', MB_ICONERROR or MB_OK);
      end;
    end;
  finally
    Screen.Cursor:= crDefault;
    Pdf.Active:= False;
    EditPdfFile.Enabled:= True;
    SpeedButtonPdfFile.Enabled:= True;
    ButtonConvert.Enabled:= True;
    ButtonCancel.Enabled:= False;
  end;
end;

procedure TFormMain.EditPdfFileChange(Sender: TObject);
begin
  ButtonConvert.Enabled:= FileExists(EditPdfFile.Text);
  if (Trim(EditOutputDir.Text)= '')and FileExists(EditPdfFile.Text)
  then
    EditOutputDir.Text:= ExtractFilePath(EditPdfFile.Text);
end;

procedure TFormMain.SpeedButtonOutputDirClick(Sender: TObject);
var
  Dir: string;
begin
  Dir:= EditOutputDir.Text;
{$IFDEF XE2+}
  if SelectDirectory('Select output folder', '', Dir)
  then
{$ELSE}
  if SelectDirectory('Select output folder', '', Dir)
  then
{$ENDIF}
    EditOutputDir.Text:= Dir;
end;

procedure TFormMain.ButtonCancelClick(Sender: TObject);
begin
  FCancelRequested:= True;
end;

function TFormMain.EnsureOutputDir(const BasePdfFile, UserOutDir: string): string;
begin
  Result:= Trim(UserOutDir);
  if Result= ''
  then
  begin
{$IFDEF XE2+}
    Result:= TPath.GetDirectoryName(BasePdfFile);
{$ELSE}
    Result:= ExtractFilePath(BasePdfFile);
{$ENDIF}
  end;
{$IFDEF XE2+}
  System.SysUtils.ForceDirectories(Result);
{$ELSE}
  SysUtils.ForceDirectories(Result);
{$ENDIF}
end;

function TFormMain.ParsePageRanges(
  const S  : string;
  MaxPage  : Integer;
  out Pages: TIntArray): Boolean;
var
  Tmp: string;
  List: TStringList;
  I, a, b, j: Integer;
  RangePos: Integer;
  Used: array of Boolean;
  Count: Integer;
begin
  Result:= False;
  SetLength(Pages, 0);
  Tmp:= StringReplace(Trim(S), ' ', '', [rfReplaceAll]);
  if Tmp= ''
  then
  begin
    Result:= True;
    Exit;
  end;
  SetLength(Used, MaxPage+ 1);
  for I:= 0 to High(Used) do
    Used[I]:= False;

  List:= TStringList.Create;
  try
    List.StrictDelimiter:= True;
    List.Delimiter:= ',';
    List.DelimitedText:= Tmp;
    for I:= 0 to List.Count- 1 do
    begin
      if List[I]= ''
      then
        Continue;
      RangePos:= Pos('-', List[I]);
      if RangePos> 0
      then
      begin
        // A-B (allow open ends like -B or A-)
        if RangePos= 1
        then
          a:= 1
        else
          a:= StrToIntDef(Copy(List[I], 1, RangePos- 1), - 1);
        if RangePos= Length(List[I])
        then
          b:= MaxPage
        else
          b:= StrToIntDef(Copy(List[I], RangePos+ 1, MaxInt), - 1);
        if (a< 1)or (b< 1)or (a> MaxPage)or (b> MaxPage)
        then
          Exit;
        if a> b
        then
        begin
          j:= a;
          a:= b;
          b:= j;
        end;
        for j:= a to b do
          Used[j]:= True;
      end
      else
      begin
        a:= StrToIntDef(List[I], - 1);
        if (a< 1)or (a> MaxPage)
        then
          Exit;
        Used[a]:= True;
      end;
    end;
  finally
    List.Free;
  end;

  Count:= 0;
  for I:= 1 to MaxPage do
    if Used[I]
    then
      Inc(Count);
  SetLength(Pages, Count);
  j:= 0;
  for I:= 1 to MaxPage do
    if Used[I]
    then
    begin
      Pages[j]:= I;
      Inc(j);
    end;
  Result:= True;
end;

end.
