{$IFDEF FPC}
  {$MODE DelphiUnicode}
{$ENDIF FPC}

{$A8,B-,E-,F-,G+,H+,I+,J-,K-,M-,N-,P+,Q-,R-,S-,T-,U-,V+,X+,Z1}
{$STRINGCHECKS OFF}

unit EM.PdfiumCtrl;

// Show invalidated paint regions. Don't enable this if you aren't trying to optimize the repainting
{.$DEFINE REPAINTTEST}

{$IFDEF FPC}
  {$DEFINE USE_PRINTCLIENT_WORKAROUND}
{$ELSE}
  {$IF CompilerVersion <= 20.0} // 2009 and older
    {$DEFINE USE_PRINTCLIENT_WORKAROUND}
  {$IFEND}
  {$IF CompilerVersion >= 21.0} // 2010+
    {$DEFINE VCL_HAS_TOUCH}
  {$IFEND}
{$ENDIF FPC}

interface

uses
  {$IFDEF FPC}
  LCLType, PrintersDlgs, Win32Extra,
  {$ENDIF FPC}
  Windows, Messages, ShellAPI, Types, SysUtils, Classes, Contnrs, Graphics, Controls,
  Forms, Dialogs, EM.PdfiumCore;

type
  TPdfControlLinkOptionType = (
    loAutoGoto,                        // Jumps in the document are allowed and automatically handled
    loAutoRemoteGotoReplaceDocument,   // Jumps to a remote document are allowed and automatically handled by replacing the loaded document
    loAutoOpenURI,                     // Jumps to URI are allowed and automatically handled by using ShellExecuteEx. Disables OnWebLinkClick if loTreatWebLinkAsUriAnnotationLink is set
    loAutoLaunch,                      // Allow executing/opening a program/file automatically by using ShellExecuteEx
    loAutoEmbeddedGotoReplaceDocument, // Jumps to an attached PDF document are allowed and automatically handled by replacing the loaded document

    loTreatWebLinkAsUriAnnotationLink, // OnAnnotationLinkClick also handles WebLinks
    loAlwaysDetectWebAndUriLink        // If if OnWebLinkClick and OnAnnotationLinkClick aren't assigned, URI and WebLinks are detected
  );
  TPdfControlLinkOptions = set of TPdfControlLinkOptionType;

const
  cPdfControlDefaultDrawOptions = [proAnnotations];
  cPdfControlDefaultLinkOptions = [loAutoGoto, loTreatWebLinkAsUriAnnotationLink, loAlwaysDetectWebAndUriLink];
  cPdfControlAllAutoLinkOptions = [loAutoGoto, loAutoRemoteGotoReplaceDocument, loAutoOpenURI,
                                   loAutoLaunch, loAutoEmbeddedGotoReplaceDocument];

type
  TPdfControlScaleMode = (
    smFitAuto,
    smFitWidth,
    smFitHeight,
    smZoom
  );

  TPdfControlWebLinkClickEvent = procedure(Sender: TObject; Url: string) of object;
  TPdfControlAnnotationLinkClickEvent = procedure(Sender: TObject; LinkInfo: TPdfLinkInfo; var Handled: Boolean) of object;
  TPdfControlRectArray = array of TRect;

  TPdfControl = class(TCustomControl)
  private
    FDocument: TPdfDocument;
    FPageIndex: Integer;
    FRenderedPageIndex: Integer;
    FPageBitmap: HBITMAP;
    FDrawX: Integer;
    FDrawY: Integer;
    FDrawWidth: Integer;
    FDrawHeight: Integer;
    FRotation: TPdfPageRotation;
    {$IFDEF USE_PRINTCLIENT_WORKAROUND}
    FPrintClient: Boolean;
    {$ENDIF USE_PRINTCLIENT_WORKAROUND}
    FMousePressed: Boolean;
    FSelectionActive: Boolean;
    FAllowUserTextSelection: Boolean;
    FAllowUserPageChange: Boolean;
    FAllowFormEvents: Boolean;
    FBufferedPageDraw: Boolean;
    FSmoothScroll: Boolean;
    FScrollTimerActive: Boolean;
    FScrollTimer: Boolean;
    FChangePageOnMouseScrolling: Boolean;
    FSelStartCharIndex: Integer;
    FSelStopCharIndex: Integer;
    FMouseDownPt: TPoint;
    FCheckForTrippleClick: Boolean;
    FWebLinkInfo: TPdfPageWebLinksInfo;
    FDrawOptions: TPdfPageRenderOptions;
    FScaleMode: TPdfControlScaleMode;
    FZoomPercentage: Integer;
    FPageColor: TColor;
    FScrollMousePos: TPoint;
    FLinkOptions: TPdfControlLinkOptions;
    FHighlightTextRects: TPdfRectArray;
    FHighlightTexts: TObjectList;
    FFormOutputSelectedRects: TPdfRectArray;
    FFormFieldFocused: Boolean;
    FPageShadowSize: Integer;
    FPageShadowColor: TColor;
    FPageShadowPadding: Integer;
    FPageBorderColor: TColor;

    FOnWebLinkClick: TPdfControlWebLinkClickEvent;
    FOnAnnotationLinkClick: TPdfControlAnnotationLinkClickEvent;
    FOnPageChange: TNotifyEvent;
    FOnPaint: TNotifyEvent;
    FOnPrintDocument: TNotifyEvent;

    procedure WMTimer(var Message: TWMTimer); message WM_TIMER;
    procedure WMVScroll(var Message: TWMVScroll); message WM_VSCROLL;
    procedure WMHScroll(var Message: TWMHScroll); message WM_HSCROLL;
    procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
    procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;
    procedure CMColorchanged(var Message: TMessage); message CM_COLORCHANGED;
    {$IFDEF USE_PRINTCLIENT_WORKAROUND}
    procedure WMPrintClient(var Message: TWMPrintClient); message WM_PRINTCLIENT;
    {$ENDIF USE_PRINTCLIENT_WORKAROUND}
    procedure CMMouseleave(var Message: TMessage); message CM_MOUSELEAVE;

    procedure GetPageWebLinks;
    function GetCurrentPage: TPdfPage;
    function GetPageCount: Integer;
    procedure SetPageIndex(Value: Integer);
    function InternSetPageIndex(Value: Integer; ScrollTransition, InverseScrollTransition: Boolean): Boolean;
    procedure SetRotation(const Value: TPdfPageRotation);
    function SetSelStopCharIndex(X, Y: Integer): Boolean;
    function GetSelText: string;
    function GetSelLength: Integer;
    function GetSelStart: Integer;
    procedure SetSelection(Active: Boolean; StartIndex, StopIndex: Integer);
    procedure SetScaleMode(const Value: TPdfControlScaleMode);
    procedure SetPageBorderColor(const Value: TColor);
    procedure SetPageShadowColor(const Value: TColor);
    procedure SetPageShadowPadding(const Value: Integer);
    procedure SetPageShadowSize(const Value: Integer);
    procedure AdjustDrawPos;
    procedure UpdatePageDrawInfo;
    procedure SetPageColor(const Value: TColor);
    procedure SetDrawOptions(const Value: TPdfPageRenderOptions);
    procedure InvalidateRectDiffs(const OldRects, NewRects: TPdfControlRectArray);
    procedure InvalidatePdfRectDiffs(const OldRects, NewRects: TPdfRectArray);
    procedure StopScrollTimer;
    procedure DocumentLoaded;
    procedure DrawSelection(DC: HDC; Page: TPdfPage);
    procedure DrawHighlightText(DC: HDC; Page: TPdfPage);
    procedure DrawBorderAndShadow(DC: HDC);
    function InternPageToDevice(Page: TPdfPage; PageRect: TPdfRect; ANormalize: Boolean): TRect;
    procedure SetZoomPercentage(Value: Integer);
    procedure DrawPage(DC: HDC; Page: TPdfPage; DirectDrawPage: Boolean);
    procedure CalcHighlightTextRects;
    procedure InitDocument;
    function ShellOpenFileName(const FileName: string; Launch: Boolean): Boolean;

    procedure FormInvalidate(Document: TPdfDocument; Page: TPdfPage; const PageRect: TPdfRect);
    procedure FormOutputSelectedRect(Document: TPdfDocument; Page: TPdfPage; const PageRect: TPdfRect);
    procedure FormGetCurrentPage(Document: TPdfDocument; var Page: TPdfPage);
    procedure FormFieldFocus(Document: TPdfDocument; Value: PWideChar; ValueLen: Integer; FieldFocused: Boolean);
    procedure ExecuteNamedAction(Document: TPdfDocument; NamedAction: TPdfNamedActionType);

    procedure DrawAlphaRects(DC: HDC; Page: TPdfPage; const Rects: TPdfRectArray; Color: TColor);
    procedure DrawAlphaSelection(DC: HDC; Page: TPdfPage; const Rects: TPdfRectArray);
    procedure DrawFormOutputSelectedRects(DC: HDC; Page: TPdfPage);
  protected
    procedure Paint; override;
    procedure Resize; override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer; Y: Integer); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer; Y: Integer); override;
    procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
    function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint): Boolean; override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    procedure WMKeyDown(var Message: TWMKeyDown); message {$IFDEF FPC}CN_KEYDOWN{$ELSE}WM_KEYDOWN{$ENDIF};
    procedure WMKeyUp(var Message: TWMKeyUp); message {$IFDEF FPC}CN_KEYUP{$ELSE}WM_KEYUP{$ENDIF};
    procedure WMChar(var Message: TWMChar); message {$IFDEF FPC}CN_CHAR{$ELSE}WM_CHAR{$ENDIF};
    procedure WMKillFocus(var Message: TWMKillFocus); message WM_KILLFOCUS;

    function LinkHandlingNeeded: Boolean;
    function IsClickableLinkAt(X, Y: Integer): Boolean;
    procedure WebLinkClick(const Url: string); virtual;
    procedure AnnotationLinkClick(LinkInfo: TPdfLinkInfo); virtual;
    procedure PageChange; virtual;
    procedure PageContentChanged(Closing: Boolean);
    procedure PageLayoutChanged;
    function IsPageValid: Boolean;
    function GetSelectionRects: TPdfControlRectArray;
    procedure DestroyWnd; override;

    property DrawX: Integer read FDrawX;
    property DrawY: Integer read FDrawY;
    property DrawWidth: Integer read FDrawWidth;
    property DrawHeight: Integer read FDrawHeight;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    { InvalidatePage forces the page to be rendered again and invalidates the control. }
    procedure InvalidatePage;
    { PrintDocument uses OnPrintDocument to print. If OnPrintDocument is not assigned it does nothing. }
    procedure PrintDocument;

    procedure OpenWithDocument(Document: TPdfDocument); // takes ownership
    procedure LoadFromCustom(ReadFunc: TPdfDocumentCustomReadProc; Size: LongWord; Param: Pointer; const Password: UTF8String = '');
    procedure LoadFromActiveStream(Stream: TStream; const Password: UTF8String = ''); // Stream must not be released until the document is closed
    procedure LoadFromActiveBuffer(Buffer: Pointer; Size: Int64; const Password: UTF8String = ''); // Buffer must not be released until the document is closed
    procedure LoadFromBytes(const Bytes: TBytes; const Password: UTF8String = ''); overload; // The content of the Bytes array must not be changed until the document is closed
    procedure LoadFromBytes(const Bytes: TBytes; Index: Integer; Count: Integer; const Password: UTF8String = ''); overload; // The content of the Bytes array must not be changed until the document is closed
    procedure LoadFromStream(Stream: TStream; const Password: UTF8String = '');
    procedure LoadFromFile(const FileName: string; const Password: UTF8String = ''; LoadOption: TPdfDocumentLoadOption = dloDefault);
    procedure Close;

    function DeviceToPage(DeviceX, DeviceY: Integer): TPdfPoint; overload;
    function DeviceToPage(DeviceRect: TRect): TPdfRect; overload;
    function PageToDevice(PageX, PageY: Double): TPoint; overload;
    function PageToDevice(PageRect: TPdfRect): TRect; overload;
    function GetPageRect: TRect;

    procedure CopyFormTextToClipboard;
    procedure CutFormTextToClipboard;
    procedure PasteFormTextFromClipboard;
    procedure SelectAllFormText;

    procedure CopyToClipboard;
    procedure ClearSelection;
    procedure SelectAll;
    procedure SelectText(CharIndex, Count: Integer);
    function SelectWord(CharIndex: Integer): Boolean; // includes symbols like Chrome
    function SelectLine(CharIndex: Integer): Boolean;

    function GetTextInRect(const R: TRect): string;
    { HightlightText() highlights all occurences of the specified text and clears previously
      hightlighted texts. }
    procedure HightlightText(const SearchText: string; MatchCase, MatchWholeWord: Boolean);
    { AddHightlightText() highlights all occurences of the specified text but keeps previously
      hightlighted texts. }
    procedure AddHightlightText(const SearchText: string; MatchCase, MatchWholeWord: Boolean);
    procedure ClearHighlightText;

    function IsWebLinkAt(X, Y: Integer): Boolean; overload;
    function IsWebLinkAt(X, Y: Integer; var Url: string): Boolean; overload;
    function IsUriAnnotationLinkAt(X, Y: Integer): Boolean;
    function IsAnnotationLinkAt(X, Y: Integer): Boolean;
    function GetAnnotationLinkAt(X, Y: Integer): TPdfAnnotation;

    function GotoNextPage(ScrollTransition: Boolean = False): Boolean;
    function GotoPrevPage(ScrollTransition: Boolean = False): Boolean;
    function ScrollContent(XOffset, YOffset: Integer; Smooth: Boolean = False): Boolean; virtual;
    function ScrollContentTo(X, Y: Integer; Smooth: Boolean = False): Boolean;
    function GotoDestination(const LinkGotoDestination: TPdfLinkGotoDestination): Boolean;

    property Document: TPdfDocument read FDocument;
    property CurrentPage: TPdfPage read GetCurrentPage;

    property PageCount: Integer read GetPageCount;
    property PageIndex: Integer read FPageIndex write SetPageIndex;
    property SelStart: Integer read GetSelStart; // in CharIndex, not TextIndex (Length(SelText) may not be SelLength)
    property SelLength: Integer read GetSelLength; // in CharIndex, not TextIndex (Length(SelText) may not be SelLength)
    property SelText: string read GetSelText;

    property Canvas;
  published
    property ScaleMode: TPdfControlScaleMode read FScaleMode write SetScaleMode default smFitAuto;
    property ZoomPercentage: Integer read FZoomPercentage write SetZoomPercentage default 100;
    property PageColor: TColor read FPageColor write SetPageColor default clWhite;
    property Rotation: TPdfPageRotation read FRotation write SetRotation default prNormal;
    property BufferedPageDraw: Boolean read FBufferedPageDraw write FBufferedPageDraw default True;
    property AllowUserTextSelection: Boolean read FAllowUserTextSelection write FAllowUserTextSelection default True;
    property AllowUserPageChange: Boolean read FAllowUserPageChange write FAllowUserPageChange default True; // PgDn/PgUp
    property AllowFormEvents: Boolean read FAllowFormEvents write FAllowFormEvents default True;
    property DrawOptions: TPdfPageRenderOptions read FDrawOptions write SetDrawOptions default cPdfControlDefaultDrawOptions;
    property SmoothScroll: Boolean read FSmoothScroll write FSmoothScroll default False;
    property ScrollTimer: Boolean read FScrollTimer write FScrollTimer default True;
    property ChangePageOnMouseScrolling: Boolean read FChangePageOnMouseScrolling write FChangePageOnMouseScrolling default False;
    property LinkOptions: TPdfControlLinkOptions read FLinkOptions write FLinkOptions default cPdfControlDefaultLinkOptions;

    property PageBorderColor: TColor read FPageBorderColor write SetPageBorderColor default clNone;
    property PageShadowColor: TColor read FPageShadowColor write SetPageShadowColor default clNone;
    property PageShadowSize: Integer read FPageShadowSize write SetPageShadowSize default 4;
    property PageShadowPadding: Integer read FPageShadowPadding write SetPageShadowPadding default 44;

    { OnWebLinkClick is only called for WebLinks (URLs parsed from the document text). If OnAnnotationLinkClick is
      not assigned, OnWebLinkClick is also called URI link annontations for backward compatibility reasons. }
    property OnWebLinkClick: TPdfControlWebLinkClickEvent read FOnWebLinkClick write FOnWebLinkClick;
    { OnAnnotationLinkClick is called for all link annotation but not for WebLinks. }
    property OnAnnotationLinkClick: TPdfControlAnnotationLinkClickEvent read FOnAnnotationLinkClick write FOnAnnotationLinkClick;
    { OnPageChange is called if the current page is switched. }
    property OnPageChange: TNotifyEvent read FOnPageChange write FOnPageChange;
    { OnPrintDocument is called from PrintDocument }
    property OnPrintDocument: TNotifyEvent read FOnPrintDocument write FOnPrintDocument;

    property Align;
    property Anchors;
    property Color default clGray;
    property Constraints;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property Font;
    property ParentBackground default False;
    property ParentColor default False;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowHint;
    property TabOrder;
    property TabStop default True;
    property Visible;
    property OnClick;
    property OnContextPopup;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDock;
    property OnEndDrag;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    {$IFNDEF FPC}
    property OnMouseActivate;
    {$ENDIF ~FPC}
    property OnMouseDown;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnMouseMove;
    property OnMouseUp;
    property OnPaint: TNotifyEvent read FOnPaint write FOnPaint;
    property OnStartDock;
    property OnStartDrag;
    {$IFDEF VCL_HAS_TOUCH}
    property Touch;
    property OnGesture;
    {$ENDIF VCL_HAS_TOUCH}
  end;

  TPdfDocumentVclPrinter = class(TPdfDocumentPrinter)
  private
    FBeginDocCalled: Boolean;
    FPagePrinted: Boolean;
  protected
    function PrinterStartDoc(const AJobTitle: string): Boolean; override;
    procedure PrinterEndDoc; override;
    procedure PrinterStartPage; override;
    procedure PrinterEndPage; override;
    function GetPrinterDC: HDC; override;
  public
    { If AShowPrintDialog is false PrintDocument prints the document to the default printer.
      If AShowPrintDialog is true the print dialog is shown and the user can select the
      printer, page range and number of copies (if supported by the printer driver).
      Returns true if the page was send to the printer driver. }
    class function PrintDocument(ADocument: TPdfDocument; const AJobTitle: string;
      AShowPrintDialog: Boolean = True; AllowPageRange: Boolean = True;
      AParentWnd: HWND = 0): Boolean; static;
  end;

implementation

uses
  Math, Clipbrd, Character, Printers;

const
  cScrollTimerId = 1;
  cTrippleClickTimerId = 2;
  cScrollTimerInterval = 50;
  cDefaultScrollOffset = 25;

type
  THighlightTextInfo = class(TObject)
  private
    FText: string;
    FMatchCase: Boolean;
    FMatchWholeWord: Boolean;
  public
    constructor Create(const AText: string; AMatchCase, AMatchWholeWord: Boolean);
    function IsSame(const AText: string; AMatchCase, AMatchWholeWord: Boolean): Boolean;

    property Text: string read FText;
    property MatchCase: Boolean read FMatchCase;
    property MatchWholeWord: Boolean read FMatchWholeWord;
  end;

function IsWhitespace(Ch: Char): Boolean;
begin
  {$IFDEF FPC}
  Result := TCharacter.IsWhiteSpace(Ch);
  {$ELSE}
    {$IF CompilerVersion >= 25.0} // XE4
  Result := Ch.IsWhiteSpace;
    {$ELSE}
  Result := TCharacter.IsWhiteSpace(Ch);
    {$IFEND}
  {$ENDIF FPC}
end;

function VclAbortProc(Prn: HDC; Error: Integer): Bool; stdcall;
begin
  Application.ProcessMessages;
  Result := not Printer.Aborted;
end;

function FastVclAbortProc(Prn: HDC; Error: Integer): Bool; stdcall;
begin
  Result := not Printer.Aborted;
end;


{ THighlightTextInfo }

constructor THighlightTextInfo.Create(const AText: string; AMatchCase, AMatchWholeWord: Boolean);
begin
  inherited Create;
  FText := AText;
  FMatchCase := AMatchCase;
  FMatchWholeWord := AMatchWholeWord;
end;

function THighlightTextInfo.IsSame(const AText: string; AMatchCase, AMatchWholeWord: Boolean): Boolean;
begin
  Result := (AMatchCase = FMatchCase) and
            (AMatchWholeWord = FMatchWholeWord) and
            (AText = FText);
end;

{ TPdfDocumentVclPrinter }

function TPdfDocumentVclPrinter.PrinterStartDoc(const AJobTitle: string): Boolean;
begin
  Result := False;
  FPagePrinted := False;
  if not Printer.Printing then
  begin
    if AJobTitle <> '' then
      Printer.Title := AJobTitle;
    Printer.BeginDoc;
    FBeginDocCalled := Printer.Printing;
    Result := FBeginDocCalled;
  end;
  if Result and Printer.Printing then
  begin
    // The Printers.AbortProc function calls ProcessMessages. That not only slows down the performance
    // but it also allows the user to do things in the UI.
    SetAbortProc(GetPrinterDC, @FastVclAbortProc);
  end;
end;

procedure TPdfDocumentVclPrinter.PrinterEndDoc;
begin
  if Printer.Printing then
  begin
    SetAbortProc(GetPrinterDC, @VclAbortProc); // restore default behavior
    if FBeginDocCalled then
      Printer.EndDoc;
  end;
end;

procedure TPdfDocumentVclPrinter.PrinterStartPage;
begin
  // Printer has only "NewPage" and the very first page doesn't need a NewPage call because
  // Printer.BeginDoc already called Windows.StartPage.
  if (Printer.PageNumber > 1) or FPagePrinted then
    Printer.NewPage;
end;

procedure TPdfDocumentVclPrinter.PrinterEndPage;
begin
  FPagePrinted := True;
  // The VCL uses "NewPage". For the very last page Printer.EndDoc calls Windows.EndPage.
end;

function TPdfDocumentVclPrinter.GetPrinterDC: HDC;
begin
  Result := Printer.Canvas.Handle;
end;

class function TPdfDocumentVclPrinter.PrintDocument(ADocument: TPdfDocument;
  const AJobTitle: string; AShowPrintDialog, AllowPageRange: Boolean; AParentWnd: HWND): Boolean;
var
  PdfPrinter: TPdfDocumentVclPrinter;
  Dlg: TPrintDialog;
  FromPage, ToPage: Integer;
begin
  Result := False;
  if ADocument = nil then
    Exit;

  FromPage := 1;
  ToPage := ADocument.PageCount;
    
  if AShowPrintDialog then
  begin
    Dlg := TPrintDialog.Create(nil);
    try
      // Set the PrintDialog options
      if AllowPageRange then
      begin
        Dlg.MinPage := 1;
        Dlg.MaxPage := ADocument.PageCount;
        Dlg.Options := Dlg.Options + [poPageNums];
      end;

      // Show the PrintDialog
      {$IFDEF FPC}
      Result := Dlg.Execute;
      {$ELSE}
      if (AParentWnd = 0) or not IsWindow(AParentWnd) then
        Result := Dlg.Execute
      else
        Result := Dlg.Execute(AParentWnd);
      {$ENDIF FPC}

      if not Result then
        Exit;

      // Adjust print options
      if AllowPageRange and (Dlg.PrintRange = prPageNums) then
      begin
        FromPage := Dlg.FromPage;
        ToPage := Dlg.ToPage;
      end;      
    finally
      Dlg.Free;
    end;
  end;

  PdfPrinter := TPdfDocumentVclPrinter.Create;
  try
    if PdfPrinter.BeginPrint(AJobTitle) then
    begin
      try
        Result := PdfPrinter.Print(ADocument, FromPage - 1, ToPage - 1);
      finally
        PdfPrinter.EndPrint;
      end;
    end;
  finally
    PdfPrinter.Free;
  end;
end;


{ TPdfControl }

constructor TPdfControl.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ControlStyle := ControlStyle + [csOpaque];

  FScaleMode := smFitAuto;
  FZoomPercentage := 100;
  FPageColor := clWhite;
  FRotation := prNormal;
  FAllowUserTextSelection := True;
  FAllowUserPageChange := True;
  FAllowFormEvents := True;
  FDrawOptions := cPdfControlDefaultDrawOptions;
  FScrollTimer := True;
  FBufferedPageDraw := True;
  FLinkOptions := cPdfControlDefaultLinkOptions;

  FPageBorderColor := clNone;
  FPageShadowColor := clNone;
  FPageShadowSize := 4;
  FPageShadowPadding := 44;

  FDocument := TPdfDocument.Create;
  InitDocument;

  ParentDoubleBuffered := False;
  ParentBackground := False;
  ParentColor := False;
  TabStop := True;
  Color := clGray;
  Width := 130;
  Height := 180;
end;

destructor TPdfControl.Destroy;
begin
  if FPageBitmap <> 0 then
    DeleteObject(FPageBitmap);
  FreeAndNil(FWebLinkInfo);	
  FDocument.Free;
  inherited Destroy;
end;

procedure TPdfControl.InitDocument;
begin
  FDocument.OnFormInvalidate := FormInvalidate;
  FDocument.OnFormOutputSelectedRect := FormOutputSelectedRect;
  FDocument.OnFormGetCurrentPage := FormGetCurrentPage;
  FDocument.OnFormFieldFocus := FormFieldFocus;
  FDocument.OnExecuteNamedAction := ExecuteNamedAction;
end;

procedure TPdfControl.DestroyWnd;
begin
  StopScrollTimer;
  if FCheckForTrippleClick then
    KillTimer(Handle, cTrippleClickTimerId);
  inherited DestroyWnd;
end;

{$IFDEF USE_PRINTCLIENT_WORKAROUND}
procedure TPdfControl.WMPrintClient(var Message: TWMPrintClient);
// Emulate Delphi 2010's TControlState.csPrintClient
var
  LastPrintClient: Boolean;
begin
  LastPrintClient := FPrintClient;
  try
    FPrintClient := True;
    inherited;
  finally
    FPrintClient := LastPrintClient;
  end;
end;
{$ENDIF USE_PRINTCLIENT_WORKAROUND}

procedure TPdfControl.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
  Message.Result := 1;
end;

procedure TPdfControl.DrawAlphaSelection(DC: HDC; Page: TPdfPage; const Rects: TPdfRectArray);
begin
  DrawAlphaRects(DC, Page, Rects, RGB(50, 142, 254));
end;

procedure TPdfControl.DrawAlphaRects(DC: HDC; Page: TPdfPage; const Rects: TPdfRectArray; Color: TColor);
var
  Count: Integer;
  I: Integer;
  R: TRect;
  BmpDC: HDC;
  SelBmp: TBitmap;
  BlendFunc: TBlendFunction;
begin
  Count := Length(Rects);
  if Count > 0 then
  begin
    SelBmp := TBitmap.Create;
    try
      SelBmp.Canvas.Brush.Color := Color;
      SelBmp.SetSize(100, 50);
      {$IFDEF FPC}
      // Delphi fills the bitmap with the brush if it is resized, FPC doesn't
      SelBmp.Canvas.FillRect(0, 0, SelBmp.Width, SelBmp.Height);
      {$ENDIF FPC}
      BlendFunc.BlendOp := AC_SRC_OVER;
      BlendFunc.BlendFlags := 0;
      BlendFunc.SourceConstantAlpha := 127;
      BlendFunc.AlphaFormat := 0;
      BmpDC := SelBmp.Canvas.Handle;
      for I := 0 to Count - 1 do
      begin
        R := InternPageToDevice(Page, Rects[I], True);
        if RectVisible(DC, R) then
          AlphaBlend(DC, R.Left, R.Top, R.Right - R.Left, R.Bottom - R.Top,
                     BmpDC, 0, 0, SelBmp.Width, SelBmp.Height,
                     BlendFunc);
      end;
    finally
      SelBmp.Free;
    end;
  end;
end;

procedure TPdfControl.DrawSelection(DC: HDC; Page: TPdfPage);
var
  Count: Integer;
  I: Integer;
  Rects: TPdfRectArray;
begin
  Count := Page.GetTextRectCount(SelStart, SelLength);
  if Count > 0 then
  begin
    SetLength(Rects, Count);
    for I := 0 to Count - 1 do
      Rects[I] := Page.GetTextRect(I);
    DrawAlphaSelection(DC, Page, Rects);
  end;
end;

procedure TPdfControl.DrawFormOutputSelectedRects(DC: HDC; Page: TPdfPage);
begin
  DrawAlphaSelection(DC, Page, FFormOutputSelectedRects);
end;

procedure TPdfControl.DrawHighlightText(DC: HDC; Page: TPdfPage);
begin
  DrawAlphaRects(DC, Page, FHighlightTextRects, RGB(254, 142, 50));
end;

procedure TPdfControl.DrawBorderAndShadow(DC: HDC);
var
  BorderBrush, ShadowBrush: HBRUSH;
begin
  // Draw page borders
  if PageBorderColor <> clNone then
  begin
    BorderBrush := CreateSolidBrush(ColorToRGB(PageBorderColor));
    FillRect(DC, Rect(FDrawX, FDrawY, FDrawX + FDrawWidth, FDrawY + 1), BorderBrush);                             // top border
    FillRect(DC, Rect(FDrawX, FDrawY, FDrawX + 1, FDrawY + FDrawHeight), BorderBrush);                            // left border
    FillRect(DC, Rect(FDrawX + FDrawWidth - 1, FDrawY, FDrawX + FDrawWidth, FDrawY + FDrawHeight), BorderBrush);  // right border
    FillRect(DC, Rect(FDrawX, FDrawY + FDrawHeight - 1, FDrawX + FDrawWidth, FDrawY + FDrawHeight), BorderBrush); // bottom border
    DeleteObject(BorderBrush);
  end;

  // Draw page shadow
  if (PageShadowColor <> clNone) and (PageShadowSize > 0) then
  begin
    ShadowBrush := CreateSolidBrush(ColorToRGB(PageShadowColor));
    FillRect(DC, Rect(FDrawX + FDrawWidth, FDrawY + PageShadowSize,
                          FDrawX + FDrawWidth + PageShadowSize, FDrawY + FDrawHeight + PageShadowSize),
             ShadowBrush); // right shadow
    FillRect(DC, Rect(FDrawX + PageShadowSize, FDrawY + FDrawHeight,
                          FDrawX + FDrawWidth + PageShadowSize, FDrawY + FDrawHeight + PageShadowSize),
             ShadowBrush); // bottom shadow
    DeleteObject(ShadowBrush);
  end;
end;

procedure TPdfControl.DrawPage(DC: HDC; Page: TPdfPage; DirectDrawPage: Boolean);

  procedure Draw(DC: HDC; X, Y: Integer; Page: TPdfPage);
  var
    PageBrush: HBRUSH;
    ColorRef: TColorRef;
  begin
    if PageColor = clDefault then
      ColorRef := ColorToRGB(Color)
    else
      ColorRef := ColorToRGB(PageColor);

    // Page.Draw doesn't paint the background if proPrinting is enabled.
    if proPrinting in FDrawOptions then
    begin
      PageBrush := CreateSolidBrush(ColorRef);
      FillRect(DC, Rect(X, Y, X + FDrawWidth, Y + FDrawHeight), PageBrush);
      DeleteObject(PageBrush);
    end;

    Page.Draw(DC, X, Y, FDrawWidth, FDrawHeight, Rotation, FDrawOptions, ColorRef);
  end;

var
  PageDC: HDC;
  OldPageBmp: HBITMAP;
  bmi: TBitmapInfo;
  BmpData: Windows.TBitmap;
  Bits: Pointer;
begin
  if DirectDrawPage then
  begin
    if FPageBitmap <> 0 then
    begin
      DeleteObject(FPageBitmap);
      FPageBitmap := 0;
    end;
    FRenderedPageIndex := -1;
    Draw(DC, FDrawX, FDrawY, Page);
  end
  else
  begin
    if (FPageBitmap = 0) or
       (GetObject(FPageBitmap, SizeOf(BmpData), @BmpData) <> SizeOf(BmpData)) or
       (FDrawWidth <> BmpData.bmWidth) or
       (FDrawHeight <> BmpData.bmHeight) then
    begin
      FRenderedPageIndex := -1; // force rendering
      if FPageBitmap <> 0 then
        DeleteObject(FPageBitmap);
      if GetDeviceCaps(DC, BITSPIXEL) = 32 then
        FPageBitmap := CreateCompatibleBitmap(DC, FDrawWidth, FDrawHeight)
      else
      begin
        FillChar(bmi, SizeOf(bmi), 0);
        bmi.bmiHeader.biSize := SizeOf(TBitmapInfoHeader);
        bmi.bmiHeader.biWidth := FDrawWidth;
        bmi.bmiHeader.biHeight := -FDrawHeight; // top-down
        bmi.bmiHeader.biPlanes := 1;
        bmi.bmiHeader.biBitCount := 32;
        bmi.bmiHeader.biCompression := BI_RGB;
        FPageBitmap := CreateDIBSection(DC, bmi, DIB_RGB_COLORS, Bits, 0, 0);
      end;
    end;

    PageDC := CreateCompatibleDC(DC);
    OldPageBmp := SelectObject(PageDC, FPageBitmap);
    try
      if FRenderedPageIndex <> PageIndex then
      begin
        FRenderedPageIndex := PageIndex;
        Draw(PageDC, 0, 0, Page);
      end;
      BitBlt(DC, FDrawX, FDrawY, FDrawWidth, FDrawHeight, PageDC, 0, 0, SRCCOPY);
    finally
      SelectObject(PageDC, OldPageBmp);
      DeleteDC(PageDC);
    end;
  end;
end;

procedure TPdfControl.Paint;
var
  Page: TPdfPage;
  DC, DrawDC: HDC;
  DrawBmp, OldDrawBmp: HBITMAP;
  Rgn: HRGN;
  DirectPageDraw: Boolean;
  WndR, ClipR: TRect;
begin
  DC := Canvas.Handle;
  {$IFDEF REPAINTTEST}
  FillRect(DC, ClientRect, GetStockObject(BLACK_BRUSH));
  GdiFlush;
  Sleep(70);
  {$ENDIF REPAINTTEST}

  if IsPageValid then
  begin
    DirectPageDraw := not BufferedPageDraw or
                      ((Int64(FDrawWidth) * FDrawHeight) > (Int64(Width) * Height)) and
                       (Int64(FDrawWidth) * FDrawHeight > 4096*2160); // 4K is too much for the system resources

    if DirectPageDraw or FSelectionActive or (FHighlightTextRects <> nil) then
    begin
      case GetClipBox(DC, ClipR) of
        NULLREGION:
          Exit; // nothing to paint
        ERROR:
          Windows.GetClientRect(Handle, ClipR);
      end;
      // Double buffer, minimal bitmap size
      DrawDC := CreateCompatibleDC(DC);
      DrawBmp := CreateCompatibleBitmap(DC, ClipR.Right - ClipR.Left, ClipR.Bottom - ClipR.Top);
      OldDrawBmp := SelectObject(DrawDC, DrawBmp);
      OffsetWindowOrgEx(DrawDC, ClipR.Left, ClipR.Top, nil);

      // copy the clipping region and adjust to the bitmap's device units
      Rgn := CreateRectRgn(0, 0, 1, 1);
      {$IFDEF USE_PRINTCLIENT_WORKAROUND}
      if FPrintClient then
      {$ELSE}
      if csPrintClient in ControlState then
      {$ENDIF USE_PRINTCLIENT_WORKAROUND}
      begin
        if GetClipRgn(DC, Rgn) = 1 then // application clip region
        begin
          OffsetRgn(Rgn, -ClipR.Left, -ClipR.Top);
          if SelectClipRgn(DrawDC, Rgn) = NULLREGION then
            Exit; // nothing to paint
        end;
      end
      else
      begin
        if GetRandomRgn(DC, Rgn, SYSRGN) = 1 then // system clip region, set by BeginPaint, in screen coordinates
        begin
          GetWindowRect(Handle, WndR);
          OffsetRgn(Rgn, -WndR.Left - ClipR.Left, -WndR.Top - ClipR.Top);
          SelectClipRgn(DrawDC, Rgn);
          if SelectClipRgn(DrawDC, Rgn) = NULLREGION then
            Exit; // nothing to paint
        end;
      end;
      DeleteObject(Rgn);
    end
    else
    begin
      DrawDC := DC;
      DrawBmp := 0;
      OldDrawBmp := 0;
    end;

    try
      // Draw borders
      FillRect(DrawDC, Rect(0, 0, Width, FDrawY), Brush.Handle);                                      // top bar
      FillRect(DrawDC, Rect(0, FDrawY, FDrawX, FDrawY + FDrawHeight), Brush.Handle);                  // left bar
      FillRect(DrawDC, Rect(FDrawX + FDrawWidth, FDrawY, Width, FDrawY + FDrawHeight), Brush.Handle); // right bar
      FillRect(DrawDC, Rect(0, FDrawY + FDrawHeight, Width, Height), Brush.Handle);                   // bottom bar

      // Draw the page
      Page := CurrentPage;
      DrawPage(DrawDC, Page, DirectPageDraw);
      // Draw the selection overlay
      if FSelectionActive then
        DrawSelection(DrawDC, Page);

      DrawFormOutputSelectedRects(DrawDC, Page);

      // Draw the highlighted text overlay
      DrawHighlightText(DrawDC, Page);

      DrawBorderAndShadow(DrawDC);

      // User painting
      if Assigned(FOnPaint) then
      begin
        Canvas.Handle := DrawDC;
        try
          FOnPaint(Self);
        finally
          Canvas.Handle := DC;
        end;
      end;

      if DrawDC <> DC then
        BitBlt(DC, 0, 0, Width, Height, DrawDC, 0, 0, SRCCOPY);
    finally
      if DrawBmp <> 0 then
      begin
        SelectObject(DrawDC, OldDrawBmp);
        DeleteObject(DrawBmp);
      end;
      if DrawDC <> DC then
        DeleteDC(DrawDC);
    end;
  end
  else
  begin
    // empty page
    if FPageBitmap <> 0 then
    begin
      DeleteObject(FPageBitmap);
      FPageBitmap := 0;
    end;
    FillRect(DC, Rect(0, 0, Width, Height), Brush.Handle);
    DrawBorderAndShadow(DC);
    if Assigned(FOnPaint) then
      FOnPaint(Self);
  end;
end;

procedure TPdfControl.PageContentChanged(Closing: Boolean);
begin
  FSelStartCharIndex := 0;
  FSelStopCharIndex := 0;
  FSelectionActive := False;
  CalcHighlightTextRects;
  GetPageWebLinks;
  PageLayoutChanged;
  if not Closing then
    PageChange;
end;

procedure TPdfControl.PageLayoutChanged;
begin
  FRenderedPageIndex := -1;
  UpdatePageDrawInfo;
  Invalidate;
end;

procedure TPdfControl.InvalidatePage;
var
  R: TRect;
begin
  FRenderedPageIndex := -1;
  if HandleAllocated then
  begin
    R := GetPageRect;
    InvalidateRect(Handle, @R, True);
  end;
end;

procedure TPdfControl.PrintDocument;
begin
  if Document.Active then
  begin
    if Assigned(FOnPrintDocument) then
      FOnPrintDocument(Self)
    else
      TPdfDocumentVclPrinter.PrintDocument(Document, ExtractFileName(Document.FileName));
  end;
end;

function TPdfControl.GetCurrentPage: TPdfPage;
begin
  if IsPageValid then
    Result := FDocument.Pages[PageIndex]
  else
    Result := nil;
end;

function TPdfControl.GetPageCount: Integer;
begin
  Result := FDocument.PageCount;
end;

procedure TPdfControl.SetPageIndex(Value: Integer);
begin
  InternSetPageIndex(Value, False, False);
end;

function TPdfControl.InternSetPageIndex(Value: Integer; ScrollTransition, InverseScrollTransition: Boolean): Boolean;
var
  ScrollInfo: TScrollInfo;
  ScrollY: Integer;
  OldPageIndex: Integer;
begin
  if Value >= PageCount then
    Value := PageCount - 1;
  if Value < 0 then
    Value := 0;

  if Value <> FPageIndex then
  begin
    ClearSelection;
    // Close the previous page to keep memory usage low (especially for large PDF files)
    if (FPageIndex >= 0) and (FPageIndex < PageCount) and FDocument.IsPageLoaded(FPageIndex) and
       not FDocument.Pages[FPageIndex].Annotations.AnnotationsLoaded then // Issue #28: Don't close the page if annotations are loaded
    begin
      FDocument.Pages[FPageIndex].Close;
    end;
    OldPageIndex := FPageIndex;
    FPageIndex := Value;
    ScrollInfo.cbSize := SizeOf(ScrollInfo);
    if ScrollTransition then
    begin
      // Keep the Scroll XOffset but scroll the page to the top or the bottom depending on the
      // PageIndex change.
      ScrollY := 0;
      ScrollInfo.fMask := SIF_RANGE or SIF_PAGE or SIF_POS;
      if GetScrollInfo(Handle, SB_VERT, ScrollInfo) then
      begin
        if InverseScrollTransition then
        begin
          if FPageIndex < OldPageIndex then
            ScrollY := 0
          else
            ScrollY := ScrollInfo.nMax {- Integer(ScrollInfo.nPage)};
        end
        else
        begin
          if FPageIndex > OldPageIndex then
            ScrollY := 0
          else
            ScrollY := ScrollInfo.nMax {- Integer(ScrollInfo.nPage)};
        end;
      end;
      if ScrollInfo.nPos <> ScrollY then
      begin
        ScrollInfo.fMask := SIF_POS;
        ScrollInfo.nPos := ScrollY;
        SetScrollInfo(Handle, SB_VERT, ScrollInfo, True);
      end;
    end
    else // Scroll to the page to the left/top corner
    begin
      ScrollInfo.fMask := SIF_POS;
      ScrollInfo.nPos := 0;
      SetScrollInfo(Handle, SB_VERT, ScrollInfo, True);
      SetScrollInfo(Handle, SB_HORZ, ScrollInfo, True);
    end;
    PageContentChanged(False);
    Result := True;
  end
  else
    Result := False;
end;

function TPdfControl.GotoNextPage(ScrollTransition: Boolean): Boolean;
begin
  Result := PageIndex < PageCount - 1;
  if Result then
    InternSetPageIndex(PageIndex + 1, ScrollTransition, False);
end;

function TPdfControl.GotoPrevPage(ScrollTransition: Boolean): Boolean;
begin
  Result := PageIndex > 0;
  if Result then
    InternSetPageIndex(PageIndex - 1, ScrollTransition, False);
end;

procedure TPdfControl.PageChange;
begin
  if Assigned(FOnPageChange) then
    FOnPageChange(Self);
end;

function TPdfControl.IsPageValid: Boolean;
begin
  Result := FDocument.Active and (PageIndex < PageCount);
end;

procedure TPdfControl.DocumentLoaded;
begin
  FPageIndex := 0;
  PageContentChanged(False);
end;

procedure TPdfControl.OpenWithDocument(Document: TPdfDocument);
begin
  Close;
  if Document = nil then
    Exit;

  FreeAndNil(FDocument);
  FDocument := Document;
  InitDocument;
end;

procedure TPdfControl.LoadFromCustom(ReadFunc: TPdfDocumentCustomReadProc; Size: LongWord;
  Param: Pointer; const Password: UTF8String);
begin
  try
    FDocument.LoadFromCustom(ReadFunc, Size, Param, Password);
  finally
    DocumentLoaded;
  end;
end;

procedure TPdfControl.LoadFromActiveStream(Stream: TStream; const Password: UTF8String);
begin
  try
    FDocument.LoadFromActiveStream(Stream, Password);
  finally
    DocumentLoaded;
  end;
end;

procedure TPdfControl.LoadFromActiveBuffer(Buffer: Pointer; Size: Int64; const Password: UTF8String);
begin
  try
    FDocument.LoadFromActiveBuffer(Buffer, Size, Password);
  finally
    DocumentLoaded;
  end;
end;

procedure TPdfControl.LoadFromBytes(const Bytes: TBytes; Index, Count: Integer;
  const Password: UTF8String);
begin
  try
    FDocument.LoadFromBytes(Bytes, Index, Count, Password);
  finally
    DocumentLoaded;
  end;
end;

procedure TPdfControl.LoadFromBytes(const Bytes: TBytes; const Password: UTF8String);
begin
  try
    FDocument.LoadFromBytes(Bytes, Password);
  finally
    DocumentLoaded;
  end;
end;

procedure TPdfControl.LoadFromStream(Stream: TStream; const Password: UTF8String);
begin
  try
    FDocument.LoadFromStream(Stream, Password);
  finally
    DocumentLoaded;
  end;
end;

procedure TPdfControl.LoadFromFile(const FileName: string; const Password: UTF8String;
  LoadOption: TPdfDocumentLoadOption);
begin
  try
    FDocument.LoadFromFile(FileName, Password, LoadOption);
  finally
    DocumentLoaded;
  end;
end;

procedure TPdfControl.Close;
begin
  FDocument.Close;
  FPageIndex := 0;
  FFormFieldFocused := False;
  PageContentChanged(True);
end;

procedure TPdfControl.CMColorchanged(var Message: TMessage);
begin
  inherited;
  if PageColor = clDefault then
    PageLayoutChanged
  else
    Invalidate;
end;

procedure TPdfControl.Resize;
begin
  UpdatePageDrawInfo;
  inherited Resize;
end;

procedure TPdfControl.SetScaleMode(const Value: TPdfControlScaleMode);
begin
  if Value <> FScaleMode then
  begin
    FScaleMode := Value;
    UpdatePageDrawInfo;
    PageLayoutChanged;
  end;
end;

procedure TPdfControl.SetZoomPercentage(Value: Integer);
begin
  if Value < 1 then
    Value := 1
  else if Value > 10000 then
    Value := 10000;
  if Value <> FZoomPercentage then
  begin
    FZoomPercentage := Value;
    PageLayoutChanged;
  end;
end;

procedure TPdfControl.SetPageColor(const Value: TColor);
begin
  if Value <> FPageColor then
  begin
    FPageColor := Value;
    InvalidatePage;
  end;
end;

procedure TPdfControl.SetDrawOptions(const Value: TPdfPageRenderOptions);
begin
  if Value <> FDrawOptions then
  begin
    FDrawOptions := Value;
    InvalidatePage;
  end;
end;

procedure TPdfControl.SetRotation(const Value: TPdfPageRotation);
begin
  if Value <> FRotation then
  begin
    FRotation := Value;
    PageLayoutChanged;
  end;
end;

procedure TPdfControl.SetPageBorderColor(const Value: TColor);
begin
  if Value <> FPageBorderColor then
  begin
    FPageBorderColor := Value;
    InvalidatePage;
  end;
end;

procedure TPdfControl.SetPageShadowColor(const Value: TColor);
begin
  if Value <> FPageShadowColor then
  begin
    FPageShadowColor := Value;
    InvalidatePage;
  end;
end;

procedure TPdfControl.SetPageShadowPadding(const Value: Integer);
begin
  if Value <> FPageShadowPadding then
  begin
    FPageShadowPadding := Value;
    InvalidatePage;
  end;
end;

procedure TPdfControl.SetPageShadowSize(const Value: Integer);
begin
  if Value <> FPageShadowSize then
  begin
    FPageShadowSize := Value;
    InvalidatePage;
  end;
end;

function TPdfControl.GetPageRect: TRect;
begin
  Result := Rect(FDrawX, FDrawY, FDrawX + FDrawWidth, FDrawY + FDrawHeight);
end;

function TPdfControl.DeviceToPage(DeviceX, DeviceY: Integer): TPdfPoint;
var
  Page: TPdfPage;
begin
  Page := CurrentPage;
  if Page <> nil then
    Result := Page.DeviceToPage(FDrawX, FDrawY, FDrawWidth, FDrawHeight, DeviceX, DeviceY, Rotation)
  else
    Result := TPdfPoint.Empty;
end;

function TPdfControl.DeviceToPage(DeviceRect: TRect): TPdfRect;
var
  Page: TPdfPage;
begin
  Page := CurrentPage;
  if Page <> nil then
    Result := Page.DeviceToPage(FDrawX, FDrawY, FDrawWidth, FDrawHeight, DeviceRect, Rotation)
  else
    Result := TPdfRect.Empty;
end;

function TPdfControl.PageToDevice(PageX, PageY: Double): TPoint;
var
  Page: TPdfPage;
begin
  Page := CurrentPage;
  if Page <> nil then
    Result := Page.PageToDevice(FDrawX, FDrawY, FDrawWidth, FDrawHeight, PageX, PageY, Rotation)
  else
    Result := Point(0, 0);
end;

function TPdfControl.PageToDevice(PageRect: TPdfRect): TRect;
var
  Page: TPdfPage;
begin
  Page := CurrentPage;
  if Page <> nil then
    Result := Page.PageToDevice(FDrawX, FDrawY, FDrawWidth, FDrawHeight, PageRect, Rotation)
  else
    Result := Rect(0, 0, 0, 0);
end;

function TPdfControl.InternPageToDevice(Page: TPdfPage; PageRect: TPdfRect; ANormalize: Boolean): TRect;
var
  Value: Integer;
begin
  Result := Page.PageToDevice(FDrawX, FDrawY, FDrawWidth, FDrawHeight, PageRect, Rotation);
  if ANormalize then
  begin
    if Result.Left > Result.Right then
    begin
      Value := Result.Right;
      Result.Right := Result.Left;
      Result.Left := Value;
    end;
    if Result.Top > Result.Bottom then
    begin
      Value := Result.Bottom;
      Result.Bottom := Result.Top;
      Result.Top := Value;
    end;
  end;
end;

function TPdfControl.SetSelStopCharIndex(X, Y: Integer): Boolean;
var
  PagePt: TPdfPoint;
  CharIndex: Integer;
  Active: Boolean;
  R: TRect;
  Page: TPdfPage;
begin
  Page := CurrentPage;
  if Page <> nil then
  begin
    PagePt := DeviceToPage(X, Y);
    CharIndex := Page.GetCharIndexAt(PagePt.X, PagePt.Y, MAXWORD, MAXWORD);
    Result := CharIndex >= 0;
    if not Result then
      CharIndex := FSelStopCharIndex;

    if FSelStartCharIndex <> CharIndex then
      Active := True
    else
    begin
      R := PageToDevice(Page.GetCharBox(FSelStartCharIndex));
      Active := PtInRect(R, FMouseDownPt) xor PtInRect(R, Point(X, Y));
    end;
    SetSelection(Active, FSelStartCharIndex, CharIndex);
  end
  else
    Result := False;
end;

procedure TPdfControl.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  PagePt: TPdfPoint;
  CharIndex: Integer;
  Page: TPdfPage;
begin
  inherited MouseDown(Button, Shift, X, Y);
  if Button = mbLeft then
  begin
    StopScrollTimer;
    SetFocus;
    FMousePressed := True;
    FMouseDownPt := Point(X, Y); // used to find out if the selection must be cleared or not
  end;

  Page := CurrentPage;
  if Page <> nil then
  begin
    if AllowFormEvents then
    begin
      PagePt := DeviceToPage(X, Y);
      if Button = mbLeft then
      begin
        if Page.FormEventLButtonDown(Shift, PagePt.X, PagePt.Y) then
          Exit;
      end
      else if Button = mbRight then
      begin
        if Page.FormEventFocus(Shift, PagePt.X, PagePt.Y) then
          Exit;
        if Page.FormEventRButtonDown(Shift, PagePt.X, PagePt.Y) then
          Exit;
      end;
    end;

    if AllowUserTextSelection and not FFormFieldFocused then
    begin
      if Button = mbLeft then
      begin
        PagePt := DeviceToPage(X, Y);
        CharIndex := Page.GetCharIndexAt(PagePt.X, PagePt.Y, MAXWORD, MAXWORD);
        if FCheckForTrippleClick and (CharIndex >= SelStart) and (CharIndex < SelStart + SelLength) then
        begin
          FMousePressed := False;
          KillTimer(Handle, cTrippleClickTimerId);
          FCheckForTrippleClick := False;
          SelectLine(CharIndex);
        end
        else if ssDouble in Shift then
        begin
          FMousePressed := False;
          SelectWord(CharIndex);
          FCheckForTrippleClick := True;
          SetTimer(Handle, cTrippleClickTimerId, GetDoubleClickTime, nil);
        end
        else
        begin
          FCheckForTrippleClick := False;
          SetSelection(False, CharIndex, CharIndex);
        end;
      end;
    end;
  end;
end;

procedure TPdfControl.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  PagePt: TPdfPoint;
  Url: string;
  Page: TPdfPage;
  LinkAnnotation: TPdfAnnotation;
  LinkInfo: TPdfLinkInfo;
begin
  inherited MouseUp(Button, Shift, X, Y);

  if AllowFormEvents and IsPageValid then
  begin
    PagePt := DeviceToPage(X, Y);
    Page := CurrentPage;
    if (Button = mbLeft) and Page.FormEventLButtonUp(Shift, PagePt.X, PagePt.Y) then
    begin
      if FMousePressed and (Button = mbLeft) then
      begin
        FMousePressed := False;
        StopScrollTimer;
      end;
      Exit;
    end;
    if (Button = mbRight) and Page.FormEventRButtonUp(Shift, PagePt.X, PagePt.Y) then
      Exit;
  end;

  if FMousePressed then
  begin
    if Button = mbLeft then
    begin
      FMousePressed := False;
      StopScrollTimer;
      if AllowUserTextSelection and not FFormFieldFocused then
        SetSelStopCharIndex(X, Y);
      if not FSelectionActive then
      begin
        if LinkHandlingNeeded then
        begin
          LinkAnnotation := GetAnnotationLinkAt(X, Y);
          LinkInfo := nil;
          if LinkAnnotation <> nil then
            LinkInfo := TPdfLinkInfo.Create(LinkAnnotation, '')
          else if IsWebLinkAt(X, Y, Url) then // If we have a Link Annotation and a WebLink, then the link annotation is prefered
          begin
            if loTreatWebLinkAsUriAnnotationLink in LinkOptions then
              LinkInfo := TPdfLinkInfo.Create(nil, Url)
            else
              WebLinkClick(Url);
          end;
          if LinkInfo <> nil then
          begin
            try
              AnnotationLinkClick(LinkInfo);
            finally
              LinkInfo.Free;
            end;
          end;
        end;
      end;
    end;
  end;
end;

procedure TPdfControl.MouseMove(Shift: TShiftState; X, Y: Integer);
var
  PagePt: TPdfPoint;
  Style: NativeInt;
  NewCursor: TCursor;
  Page: TPdfPage;
  Proceed: Boolean;
begin
  inherited MouseMove(Shift, X, Y);
  NewCursor := Cursor;
  try
    if AllowFormEvents and IsPageValid then
    begin
      PagePt := DeviceToPage(X, Y);
      Page := CurrentPage;
      if Page.FormEventMouseMove(Shift, PagePt.X, PagePt.Y) then
      begin
        Proceed := False;
        case Page.HasFormFieldAtPoint(PagePt.X, PagePt.Y) of
          fftUnknown:
            // Could be a annotation link with a URL
            Proceed := True;
          fftTextField:
            NewCursor := crIBeam;
          fftComboBox,
          fftSignature:
            NewCursor := crHandPoint;
        else
          NewCursor := crDefault;
        end;
        if not Proceed then
          Exit;
      end;
    end;

    if AllowUserTextSelection and not FFormFieldFocused then
    begin
      if FMousePressed then
      begin
        // Auto scroll
        FScrollMousePos := Point(X, Y);
        Style := GetWindowLong(Handle, GWL_STYLE);
        if ((Style and WS_VSCROLL <> 0) and ((Y < 0) or (Y > Height))) or
           ((Style and WS_HSCROLL <> 0) and ((X < 0) or (X > Width))) then
        begin
          if ScrollTimer and not FScrollTimerActive then
          begin
            SetTimer(Handle, cScrollTimerId, cScrollTimerInterval, nil);
            FScrollTimerActive := True;
          end;
        end
        else
          StopScrollTimer;

        if SetSelStopCharIndex(X, Y) then
        begin
          if NewCursor <> crIBeam then
          begin
            NewCursor := crIBeam;
            Cursor := NewCursor;
            SetCursor(Screen.Cursors[Cursor]); // show the mouse cursor change immediately
          end;
        end;
      end
      else
      begin
        if IsPageValid then
        begin
          PagePt := DeviceToPage(X, Y);
          if IsClickableLinkAt(X, Y) then
            NewCursor := crHandPoint
          else if CurrentPage.GetCharIndexAt(PagePt.X, PagePt.Y, 5, 5) >= 0 then
            NewCursor := crIBeam
          else if Cursor <> crDefault then
            NewCursor := crDefault;
        end;
      end;
    end;
  finally
    if NewCursor <> Cursor then
      Cursor := NewCursor;
  end;
end;

procedure TPdfControl.CMMouseleave(var Message: TMessage);
begin
  if (Cursor = crIBeam) or (Cursor = crHandPoint) then
  begin
    if AllowUserTextSelection or Assigned(FOnWebLinkClick) or Assigned(FOnAnnotationLinkClick) or (LinkOptions <> []) then
      Cursor := crDefault;
  end;
  inherited;
end;

function TPdfControl.GetTextInRect(const R: TRect): string;
begin
  if IsPageValid then
    Result := CurrentPage.GetTextAt(DeviceToPage(R))
  else
    Result := '';
end;

procedure TPdfControl.CopyToClipboard;
begin
  Clipboard.AsText := GetSelText;
end;

procedure TPdfControl.CopyFormTextToClipboard;
var
  S: string;
begin
  if FFormFieldFocused and IsPageValid then
  begin
    S := CurrentPage.FormGetSelectedText;
    if S <> '' then
      Clipboard.AsText := S;
  end;
end;

procedure TPdfControl.CutFormTextToClipboard;
begin
  if FFormFieldFocused and IsPageValid then
  begin
    CopyFormTextToClipboard;
    CurrentPage.FormReplaceSelection('');
  end;
end;

procedure TPdfControl.PasteFormTextFromClipboard;
begin
  if FFormFieldFocused and IsPageValid then
  begin
    Clipboard.Open;
    try
      if Clipboard.HasFormat(CF_UNICODETEXT) or Clipboard.HasFormat(CF_TEXT) then
        CurrentPage.FormReplaceSelection(Clipboard.AsText);
    finally
      Clipboard.Close;
    end;
  end;
end;

procedure TPdfControl.SelectAllFormText;
begin
  if FFormFieldFocused and IsPageValid then
    CurrentPage.FormSelectAllText;
end;

function TPdfControl.GetSelText: string;
begin
  if FSelectionActive and IsPageValid then
    Result := CurrentPage.ReadText(SelStart, SelLength)
  else
    Result := '';
end;

function TPdfControl.GetSelLength: Integer;
begin
  if FSelectionActive and IsPageValid then
    Result := Abs(FSelStartCharIndex - FSelStopCharIndex) + 1
  else
    Result := 0;
end;

function TPdfControl.GetSelStart: Integer;
begin
  if FSelectionActive and IsPageValid then
    Result := Min(FSelStartCharIndex, FSelStopCharIndex)
  else
    Result := 0;
end;

function TPdfControl.GetSelectionRects: TPdfControlRectArray;
var
  Count: Integer;
  I: Integer;
  Page: TPdfPage;
begin
  if FSelectionActive and HandleAllocated then
  begin
    Page := CurrentPage;
    if Page <> nil then
    begin
      Count := Page.GetTextRectCount(SelStart, SelLength);
      SetLength(Result, Count);
      for I := 0 to Count - 1 do
        Result[I] := InternPageToDevice(Page, Page.GetTextRect(I), True);
      Exit;
    end;
  end;
  Result := nil;
end;

procedure TPdfControl.InvalidateRectDiffs(const OldRects, NewRects: TPdfControlRectArray);

  function ContainsRect(const Rects: TPdfControlRectArray; const R: TRect): Boolean;
  var
    I: Integer;
  begin
    Result := True;
    for I := 0 to Length(Rects) - 1 do
      if (Rects[I].Left = R.Left) and (Rects[I].Top = R.Top) and (Rects[I].Right = R.Right) and (Rects[I].Bottom = R.Bottom) then
        Exit;
    Result := False;
  end;

var
  I: Integer;
begin
  if HandleAllocated then
  begin
    for I := 0 to Length(OldRects) - 1 do
      if not ContainsRect(NewRects, OldRects[I]) then
        InvalidateRect(Handle, @OldRects[I], True);

    for I := 0 to Length(NewRects) - 1 do
      if not ContainsRect(OldRects, NewRects[I]) then
        InvalidateRect(Handle, @NewRects[I], True);
  end;
end;

procedure TPdfControl.InvalidatePdfRectDiffs(const OldRects, NewRects: TPdfRectArray);
var
  I: Integer;
  OldRs, NewRs: TPdfControlRectArray;
  Page: TPdfPage;
begin
  Page := CurrentPage;
  if (Page <> nil) and HandleAllocated then
  begin
    SetLength(OldRs, Length(OldRects));
    for I := 0 to Length(OldRects) - 1 do
      OldRs[I] := InternPageToDevice(Page, OldRects[I], True);

    SetLength(NewRs, Length(NewRects));
    for I := 0 to Length(NewRects) - 1 do
      NewRs[I] := InternPageToDevice(Page, NewRects[I], True);

    InvalidateRectDiffs(OldRs, NewRs);
  end;
end;

procedure TPdfControl.SetSelection(Active: Boolean; StartIndex, StopIndex: Integer);
var
  OldRects, NewRects: TPdfControlRectArray;
begin
  if (Active <> FSelectionActive) or (StartIndex <> FSelStartCharIndex) or (StopIndex <> FSelStopCharIndex) then
  begin
    OldRects := GetSelectionRects;

    FSelStartCharIndex := StartIndex;
    FSelStopCharIndex := StopIndex;
    FSelectionActive := Active and (FSelStartCharIndex >= 0) and (FSelStopCharIndex >= 0);

    NewRects := GetSelectionRects;
    InvalidateRectDiffs(OldRects, NewRects);
  end;
end;

procedure TPdfControl.ClearSelection;
begin
  SetSelection(False, 0, 0);
end;

procedure TPdfControl.SelectAll;
begin
  SelectText(0, -1);
end;

procedure TPdfControl.SelectText(CharIndex, Count: Integer);
begin
  if (Count = 0) or not IsPageValid then
    ClearSelection
  else
  begin
    if Count = -1 then
      SetSelection(True, 0, CurrentPage.GetCharCount - 1)
    else
      SetSelection(True, CharIndex, Min(CharIndex + Count - 1, CurrentPage.GetCharCount - 1));
  end;
end;

function TPdfControl.SelectWord(CharIndex: Integer): Boolean;
var
  Ch: WideChar;
  StartCharIndex, StopCharIndex, CharCount: Integer;
  Page: TPdfPage;
begin
  Result := False;
  Page := CurrentPage;
  if Page <> nil then
  begin
    ClearSelection;
    CharCount := Page.GetCharCount;
    if (CharIndex >= 0) and (CharIndex < CharCount) then
    begin
      while (CharIndex < CharCount) and IsWhiteSpace(Page.ReadChar(CharIndex)) do
        Inc(CharIndex);

      if CharIndex < CharCount then
      begin
        StartCharIndex := CharIndex - 1;
        while StartCharIndex >= 0 do
        begin
          Ch := Page.ReadChar(StartCharIndex);
          if IsWhiteSpace(Ch) then
            Break;
          Dec(StartCharIndex);
        end;
        Inc(StartCharIndex);

        StopCharIndex := CharIndex + 1;
        while StopCharIndex < CharCount do
        begin
          Ch := Page.ReadChar(StopCharIndex);
          if IsWhiteSpace(Ch) then
            Break;
          Inc(StopCharIndex);
        end;
        Dec(StopCharIndex);

        SetSelection(True, StartCharIndex, StopCharIndex);
        Result := True;
      end;
    end;
  end;
end;

function TPdfControl.SelectLine(CharIndex: Integer): Boolean;
var
  Ch: WideChar;
  StartCharIndex, StopCharIndex, CharCount: Integer;
  Page: TPdfPage;
begin
  Result := False;
  Page := CurrentPage;
  if Page <> nil then
  begin
    ClearSelection;
    CharCount := Page.GetCharCount;
    if (CharIndex >= 0) and (CharIndex < CharCount) then
    begin
      StartCharIndex := CharIndex - 1;
      while StartCharIndex >= 0 do
      begin
        Ch := Page.ReadChar(StartCharIndex);
        case Ch of
          #10, #13:
            Break;
        end;
        Dec(StartCharIndex);
      end;
      Inc(StartCharIndex);

      StopCharIndex := CharIndex + 1;
      while StopCharIndex < CharCount do
      begin
        Ch := Page.ReadChar(StopCharIndex);
        case Ch of
          #10, #13:
            Break;
        end;
        Inc(StopCharIndex);
      end;
      Dec(StopCharIndex);

      SetSelection(True, StartCharIndex, StopCharIndex);
      Result := True;
    end;
  end;
end;

procedure TPdfControl.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
  inherited;
  Message.Result := Message.Result or DLGC_WANTARROWS or DLGC_WANTTAB;
end;

procedure TPdfControl.KeyDown(var Key: Word; Shift: TShiftState);
var
  XOffset, YOffset: Integer;
  ScrollInfo: TScrollInfo;
begin
  inherited KeyDown(Key, Shift);
  XOffset := 0;
  YOffset := 0;
  case Key of
    Ord('C'), VK_INSERT:
      if AllowUserTextSelection then
      begin
        if Shift = [ssCtrl] then
        begin
          if FSelectionActive then
            CopyToClipboard;
          Key := 0;
        end
      end;

    Ord('A'):
      if AllowUserTextSelection then
      begin
        if Shift = [ssCtrl] then
        begin
          SelectAll;
          Key := 0;
        end;
      end;

    VK_LEFT, VK_RIGHT:
      begin
        if ssShift in Shift then
          XOffset := cDefaultScrollOffset * 2
        else
          XOffset := cDefaultScrollOffset;
        if Key = VK_LEFT then
          XOffset := -XOffset;
      end;

    VK_UP, VK_DOWN:
      begin
        if ssShift in Shift then
          YOffset := cDefaultScrollOffset * 2
        else
          YOffset := cDefaultScrollOffset;
        if Key = VK_UP then
          YOffset := -YOffset;
      end;

    VK_PRIOR, VK_NEXT:
      begin
        ScrollInfo.cbSize := SizeOf(ScrollInfo);
        ScrollInfo.fMask := SIF_PAGE or SIF_RANGE or SIF_POS;
        if AllowUserPageChange and (GetWindowLong(Handle, GWL_STYLE) and WS_VSCROLL = 0) then
        begin
          if Key = VK_NEXT then
            GotoNextPage(True)
          else
            GotoPrevPage(True);
        end
        else if GetScrollInfo(Handle, SB_VERT, ScrollInfo) then
        begin
          if Key = VK_NEXT then
          begin
            if AllowUserPageChange and (ScrollInfo.nPos >= ScrollInfo.nMax - Integer(ScrollInfo.nPage)) then
              GotoNextPage(True)
            else
              YOffset := ScrollInfo.nPage
          end
          else
          begin
            if AllowUserPageChange and (ScrollInfo.nPos = 0) then
              GotoPrevPage(True)
            else
              YOffset := -ScrollInfo.nPage;
          end;
        end;
      end;

    VK_HOME, VK_END:
      begin
        if ssCtrl in Shift then
        begin
          if Key = VK_HOME then
            InternSetPageIndex(0, True, True)
          else
            InternSetPageIndex(PageCount - 1, True, True);
        end
        else
        begin
          ScrollInfo.cbSize := SizeOf(ScrollInfo);
          ScrollInfo.fMask := SIF_RANGE;

          if ssShift in Shift then
          begin
            if GetScrollInfo(Handle, SB_HORZ, ScrollInfo) then
            begin
              if Key = VK_END then
                XOffset := ScrollInfo.nMax
              else
                XOffset := -ScrollInfo.nMax;
            end;
          end
          else
          begin
            if GetScrollInfo(Handle, SB_VERT, ScrollInfo) then
            begin
              if Key = VK_END then
                YOffset := ScrollInfo.nMax
              else
                YOffset := -ScrollInfo.nMax;
            end;
          end;
        end;
      end;
  end;

  if (XOffset <> 0) or (YOffset <> 0) then
  begin
    ScrollContent(XOffset, YOffset, SmoothScroll);
    Key := 0;
  end;
end;

procedure TPdfControl.WMKeyDown(var Message: TWMKeyDown);
var
  Shift: TShiftState;
begin
  if AllowFormEvents and IsPageValid then
  begin
    Shift := KeyDataToShiftState(Message.KeyData);
    if CurrentPage.FormEventKeyDown(Message.CharCode, Shift) then
    begin
      // PDFium doesn't handle Copy&Paste&Cut keyboard shortcuts in form fields
      case Message.CharCode of
        Ord('C'), Ord('X'), Ord('V'), VK_INSERT, VK_DELETE:
          begin
            if Shift = [ssCtrl] then
            begin
              case Message.CharCode of
                Ord('C'), VK_INSERT:
                  CopyFormTextToClipboard;
                Ord('X'):
                  CutFormTextToClipboard;
                Ord('V'):
                  PasteFormTextFromClipboard;
              end;
            end
            else if Shift = [ssShift] then
            begin
              case Message.CharCode of
                VK_INSERT:
                  PasteFormTextFromClipboard;
                VK_DELETE:
                  CutFormTextToClipboard;
              end;
            end;
          end;
      end;
      Exit;
    end;
  end;
  inherited;
end;

procedure TPdfControl.WMKeyUp(var Message: TWMKeyUp);
begin
  if AllowFormEvents and IsPageValid
     and CurrentPage.FormEventKeyUp(Message.CharCode, KeyDataToShiftState(Message.KeyData)) then
    Exit;
  inherited;
end;

procedure TPdfControl.WMChar(var Message: TWMChar);
begin
  if AllowFormEvents and IsPageValid
     and CurrentPage.FormEventKeyPress(Message.CharCode, KeyDataToShiftState(Message.KeyData)) then
    Exit;
  inherited;
end;

procedure TPdfControl.WMKillFocus(var Message: TWMKillFocus);
begin
  if AllowFormEvents and IsPageValid then
    CurrentPage.FormEventKillFocus;
  inherited;
end;

procedure TPdfControl.GetPageWebLinks;
var
  Page: TPdfPage;
begin
  FreeAndNil(FWebLinkInfo);
  Page := CurrentPage;
  if Page <> nil then
    FWebLinkInfo := TPdfPageWebLinksInfo.Create(Page);
end;

function TPdfControl.LinkHandlingNeeded: Boolean;
begin
  // If an event handler is assigned, we need link handling
  Result := Assigned(FOnAnnotationLinkClick) or Assigned(FOnWebLinkClick);
  if not Result then
  begin
    // If no event handler is assigned, we may need link handling depending on the loAutoXXX options.
    Result := LinkOptions * cPdfControlAllAutoLinkOptions <> [];
  end;
end;

function TPdfControl.IsClickableLinkAt(X, Y: Integer): Boolean;
var
  LinkAnnotation: TPdfAnnotation;
begin
  Result := False;
  if LinkHandlingNeeded then
  begin
    LinkAnnotation := GetAnnotationLinkAt(X, Y);
    if LinkAnnotation <> nil then
    begin
      if Assigned(FOnAnnotationLinkClick) then
        Result := True
      else
      begin
        case LinkAnnotation.LinkType of
          altGoto:
            Result := loAutoGoto in LinkOptions;
          altRemoteGoto:
            Result := loAutoRemoteGotoReplaceDocument in LinkOptions;
          altURI:
            Result := (loAutoOpenURI in LinkOptions) or (loAlwaysDetectWebAndUriLink in LinkOptions) or Assigned(FOnWebLinkClick); // Fallback to OnWebLinkClick for URIs
          altLaunch:
            Result := loAutoLaunch in LinkOptions;
          altEmbeddedGoto:
            Result := loAutoEmbeddedGotoReplaceDocument in LinkOptions;
        else
          Result := False;
        end;
      end;
    end
    else if IsWebLinkAt(X, Y) then
    begin
      if Assigned(FOnWebLinkClick) or (loAlwaysDetectWebAndUriLink in LinkOptions) then
        Result := True
      else if Assigned(FOnAnnotationLinkClick) and (loTreatWebLinkAsUriAnnotationLink in LinkOptions) then
        Result := True
      else if not Assigned(FOnAnnotationLinkClick) and (loTreatWebLinkAsUriAnnotationLink in LinkOptions) and (loAutoOpenURI in LinkOptions) then
        Result := True;
    end;
  end;
end;

function TPdfControl.IsWebLinkAt(X, Y: Integer): Boolean;
var
  PdfPt: TPdfPoint;
begin
  if (FWebLinkInfo <> nil) and IsPageValid then
  begin
    PdfPt := DeviceToPage(X, Y);
    Result := FWebLinkInfo.IsWebLinkAt(PdfPt.X, PdfPt.Y);
  end
  else
    Result := False;
end;

function TPdfControl.IsWebLinkAt(X, Y: Integer; var Url: string): Boolean;
var
  PdfPt: TPdfPoint;
begin
  Url := '';
  if (FWebLinkInfo <> nil) and IsPageValid then
  begin
    PdfPt := DeviceToPage(X, Y);
    Result := FWebLinkInfo.IsWebLinkAt(PdfPt.X, PdfPt.Y, Url);
  end
  else
    Result := False;
end;

function TPdfControl.IsUriAnnotationLinkAt(X, Y: Integer): Boolean;
var
  PdfPt: TPdfPoint;
begin
  if IsPageValid then
  begin
    PdfPt := DeviceToPage(X, Y);
    Result := CurrentPage.IsUriLinkAtPoint(PdfPt.X, PdfPt.Y);
  end
  else
    Result := False;
end;

function TPdfControl.IsAnnotationLinkAt(X, Y: Integer): Boolean;
begin
  Result := GetAnnotationLinkAt(X, Y) <> nil;
end;

function TPdfControl.GetAnnotationLinkAt(X, Y: Integer): TPdfAnnotation;
var
  PdfPt: TPdfPoint;
begin
  if IsPageValid then
  begin
    PdfPt := DeviceToPage(X, Y);
    Result := CurrentPage.GetLinkAtPoint(PdfPt.X, PdfPt.Y);
  end
  else
    Result := nil;
end;

function TPdfControl.ShellOpenFileName(const FileName: string; Launch: Boolean): Boolean;
var
  Info: TShellExecuteInfoW;
begin
  FillChar(Info, SizeOf(Info), 0);
  Info.cbSize := SizeOf(Info);
  if HandleAllocated then
    Info.Wnd := Handle;
  if Launch then
    Info.lpVerb := nil
  else
    Info.lpVerb := 'open';
  Info.lpFile := PChar(FileName);
  Info.lpDirectory := PChar(ExtractFileDir(Document.FileName));
  Info.nShow := SW_NORMAL;
  Result := ShellExecuteExW(@Info);
end;

procedure TPdfControl.WebLinkClick(const Url: string);
begin
  if Assigned(FOnWebLinkClick) then
    FOnWebLinkClick(Self, Url);
end;

function TPdfControl.GotoDestination(const LinkGotoDestination: TPdfLinkGotoDestination): Boolean;
var
  X, Y: Double;
  //Zoom: Integer;
  Pt: TPoint;
begin
  Result := False;
  if Document.Active then
  begin
    X := 0;
    Y := 0;
    //Zoom := 100;
    if LinkGotoDestination.XValid then
      X := LinkGotoDestination.X;
    if LinkGotoDestination.YValid then
      Y := LinkGotoDestination.Y;
    //if Dest.ZoomValid then
    //  Zoom := Int(Dest.Zoom);

    if (LinkGotoDestination.PageIndex >= 0) and (LinkGotoDestination.PageIndex < Document.PageCount) then
    begin
      Pt := PageToDevice(X, Y);

      PageIndex := LinkGotoDestination.PageIndex;
      //ZoomPercentage := Zoom;
      ScrollContentTo(Pt.X, Pt.Y);
      Result := True;
    end;
  end;
end;

procedure TPdfControl.AnnotationLinkClick(LinkInfo: TPdfLinkInfo);
var
  Handled: Boolean;
  Dest: TPdfLinkGotoDestination;
  FileName: string;
  RemoteDoc: TPdfDocument;
  DestValid: Boolean;
  AttachmentIndex: Integer;
begin
  Handled := False;
  if not Document.Active then
    Exit;

  if Assigned(FOnAnnotationLinkClick) then
    FOnAnnotationLinkClick(Self, LinkInfo, Handled)
  else if Assigned(FOnWebLinkClick) and (LinkInfo.LinkType = altURI) and not (loAutoOpenURI in LinkOptions) then
  begin
    WebLinkClick(LinkInfo.LinkUri);
    Exit;
  end;

  if not Handled and Document.Active then
  begin
    case LinkInfo.LinkType of
      altGoto:
        if loAutoGoto in LinkOptions then
        begin
          if LinkInfo.GetLinkGotoDestination(Dest) then
            GotoDestination(Dest);
        end;

      altRemoteGoto:
        if loAutoRemoteGotoReplaceDocument in LinkOptions then
        begin
          Dest := nil;
          RemoteDoc := TPdfDocument.Create;
          try
            // Open the remote document
            RemoteDoc.LoadFromFile(LinkInfo.LinkFileName);
            // Get the link destination from the remote document
            DestValid := LinkInfo.GetLinkGotoDestination(Dest, RemoteDoc);
          except
            RemoteDoc.Free;
            raise;
          end;
          if DestValid then
          begin
            // Replace the current document with the remote document
            OpenWithDocument(RemoteDoc);
            GotoDestination(Dest);
          end;
        end;

      altURI:
        if loAutoOpenURI in LinkOptions then
          ShellOpenFileName(LinkInfo.LinkUri, False);

      altLaunch:
        if loAutoLaunch in LinkOptions then
          ShellOpenFileName(LinkInfo.LinkFileName, True);

      altEmbeddedGoto:
        if loAutoEmbeddedGotoReplaceDocument in LinkOptions then
        begin
          FileName := LinkInfo.LinkFileName;
          AttachmentIndex := Document.Attachments.IndexOf(FileName);
          if AttachmentIndex <> -1 then
          begin
            // Same as RemoteGoto but with a byte array
            Dest := nil;
            RemoteDoc := TPdfDocument.Create;
            try
              // Open the embedded document
              RemoteDoc.LoadFromBytes(Document.Attachments[AttachmentIndex].GetContentAsBytes);
              // Get the link destination from the remote document
              DestValid := LinkInfo.GetLinkGotoDestination(Dest, RemoteDoc);
            except
              RemoteDoc.Free;
              raise;
            end;
            if DestValid then
            begin
              // Replace the current document with the remote document
              OpenWithDocument(RemoteDoc);
              GotoDestination(Dest);
            end;
          end;
        end;
    end;
  end;
end;

procedure TPdfControl.UpdatePageDrawInfo;

  procedure GetWidthHeight(PageWidth, PageHeight: Double; DpiX, DpiY, MaxWidth, MaxHeight: Integer; var W, H: Integer);
  begin
    case ScaleMode of
      smFitAuto:
        begin
          W := Round(MaxHeight * (PageWidth / PageHeight));
          H := MaxHeight;
          if W > MaxWidth then
          begin
            W := MaxWidth;
            H := Round(MaxWidth * (PageHeight / PageWidth));
          end;
        end;

      smFitWidth:
        begin
          W := MaxWidth;
          H := Round(MaxWidth * (PageHeight / PageWidth));
        end;

      smFitHeight:
        begin
          W := Round(MaxHeight * (PageWidth / PageHeight));
          H := MaxHeight;
        end;

      smZoom: // PDFium's 100% is not AcrobatReader's 100%
        begin
          W := Round(PageWidth / 72 * DpiX * (ZoomPercentage / 100));
          H := Round(PageHeight / 72 * DpiY * (ZoomPercentage / 100));
        end;
    end;

    if (PageShadowColor <> clNone) and (PageShadowSize > 0) and (PageShadowPadding > 0) then
    begin
      W := W - (PageShadowPadding + PageShadowSize);
      H := H - (PageShadowPadding + PageShadowSize);
    end;
  end;

var
  Page: TPdfPage;
  MaxWidth, MaxHeight: Integer;
  W, H: Integer;
  PageWidth, PageHeight: Double;
  DpiX, DpiY: Integer;
  ScrollInfo: TScrollInfo;
  Style: NativeInt;
begin
  Page := CurrentPage;
  if (Page <> nil) and (Page.Width > 0) and (Page.Height > 0) and HandleAllocated then
  begin
    Style := GetWindowLong(Handle, GWL_STYLE);

    ScrollInfo.cbSize := SizeOf(ScrollInfo);
    ScrollInfo.fMask := SIF_RANGE or SIF_PAGE;
    ScrollInfo.nMin := 0;

    // Take "Rotation" into account
    if Rotation in [prNormal, pr180] then
    begin
      PageWidth := Page.Width;
      PageHeight := Page.Height;
      DpiX := GetDeviceCaps(Canvas.Handle, LOGPIXELSX);
      DpiY := GetDeviceCaps(Canvas.Handle, LOGPIXELSY);
    end
    else
    begin
      PageHeight := Page.Width;
      PageWidth := Page.Height;
      DpiY := GetDeviceCaps(Canvas.Handle, LOGPIXELSX);
      DpiX := GetDeviceCaps(Canvas.Handle, LOGPIXELSY);
    end;


    MaxWidth := Width;
    MaxHeight := Height;
    GetWidthHeight(PageWidth, PageHeight, DpiX, DpiY, MaxWidth, MaxHeight, W, H);
    if W > MaxWidth then
    begin
      MaxHeight := MaxHeight - GetSystemMetrics(SM_CYHSCROLL);
      GetWidthHeight(PageWidth, PageHeight, DpiX, DpiY, MaxWidth, MaxHeight, W, H);
    end;
    if H > MaxHeight then
    begin
      MaxWidth := MaxWidth - GetSystemMetrics(SM_CXVSCROLL);
      GetWidthHeight(PageWidth, PageHeight, DpiX, DpiY, MaxWidth, MaxHeight, W, H);
    end;

    if W > MaxWidth then
    begin
      ScrollInfo.nMax := W;
      ScrollInfo.nPage := MaxWidth;
      SetScrollInfo(Handle, SB_HORZ, ScrollInfo, True);
    end
    else
    begin
      if Style and WS_HSCROLL <> 0 then
      begin
        ShowScrollBar(Handle, SB_HORZ, False);
        RedrawWindow(Handle, nil, 0, RDW_ERASE or RDW_INVALIDATE or RDW_FRAME);
        InvalidateRect(Handle, nil, True);
      end;
    end;

    if H > MaxHeight then
    begin
      ScrollInfo.nMax := H;
      ScrollInfo.nPage := MaxHeight;
      SetScrollInfo(Handle, SB_VERT, ScrollInfo, True);
      ShowScrollBar(Handle, SB_VERT, True);
    end
    else
    begin
      if Style and WS_VSCROLL <> 0 then
      begin
        ShowScrollBar(Handle, SB_VERT, False);
        RedrawWindow(Handle, nil, 0, RDW_ERASE or RDW_INVALIDATE or RDW_FRAME);
        InvalidateRect(Handle, nil, True);
      end;
    end;

    FDrawWidth := W;
    FDrawHeight := H;
    AdjustDrawPos;
  end;
end;

procedure TPdfControl.AdjustDrawPos;
var
  ScrollInfo: TScrollInfo;
  X, Y, HPos, VPos: Integer;
  Style: NativeInt;
  MaxWidth: Integer;
  MaxHeight: Integer;
begin
  Style := GetWindowLong(Handle, GWL_STYLE);
  MaxWidth := Width;
  MaxHeight := Height;
  HPos := 0;
  VPos := 0;

  ScrollInfo.cbSize := SizeOf(ScrollInfo);
  ScrollInfo.fMask := SIF_POS;
  if (Style and WS_HSCROLL <> 0) then
  begin
    MaxHeight := MaxHeight - GetSystemMetrics(SM_CXHSCROLL);
    if GetScrollInfo(Handle, SB_HORZ, ScrollInfo) then
      HPos := ScrollInfo.nPos;
  end;
  if (Style and WS_VSCROLL <> 0) then
  begin
    MaxWidth := MaxWidth - GetSystemMetrics(SM_CXVSCROLL);
    if GetScrollInfo(Handle, SB_VERT, ScrollInfo) then
      VPos := ScrollInfo.nPos;
  end;

  X := (MaxWidth - FDrawWidth) div 2;
  Y := (MaxHeight - FDrawHeight) div 2;
  if X < 0 then
    X := 0;
  if Y < 0 then
    Y := 0;

  Dec(X, HPos);
  Dec(Y, VPos);
  if (FDrawX <> X) or (FDrawY <> Y) then
  begin
    FDrawX := X;
    FDrawY := Y;
  end;
end;

function TPdfControl.ScrollContent(XOffset, YOffset: Integer; Smooth: Boolean): Boolean;
var
  ScrollInfo: TScrollInfo;
  X, Y: Integer;
  Style: NativeInt;
  Flags: UINT;
begin
  if Smooth then
    Update;

  Style := GetWindowLong(Handle, GWL_STYLE);
  ScrollInfo.cbSize := SizeOf(ScrollInfo);
  ScrollInfo.fMask := SIF_POS;

  // Vertical scroll
  if (YOffset <> 0) and (Style and WS_VSCROLL <> 0) and GetScrollInfo(Handle, SB_VERT, ScrollInfo) then
  begin
    Y := ScrollInfo.nPos;
    ScrollInfo.nPos := Y + YOffset;
    SetScrollInfo(Handle, SB_VERT, ScrollInfo, True);
    GetScrollInfo(Handle, SB_VERT, ScrollInfo); // let Windows do the range checking
    YOffset := Y - ScrollInfo.nPos;
  end
  else
    YOffset := 0;

  // Horizontal scroll
  if (XOffset <> 0) and (Style and WS_HSCROLL <> 0) and GetScrollInfo(Handle, SB_HORZ, ScrollInfo) then
  begin
    X := ScrollInfo.nPos;
    ScrollInfo.nPos := X + XOffset;
    SetScrollInfo(Handle, SB_HORZ, ScrollInfo, True);
    GetScrollInfo(Handle, SB_HORZ, ScrollInfo); // let Windows do the range checking
    XOffset := X - ScrollInfo.nPos;
  end
  else
    XOffset := 0;

  if (XOffset <> 0) or (YOffset <> 0) then
  begin
    AdjustDrawPos; // adjust DrawX/DrawY for ScrollWindowEx
    Flags := 0;
    if Smooth then
      Flags := Flags or SW_SMOOTHSCROLL or (150 shl 16);
    ScrollWindowEx(Handle, XOffset, YOffset, nil, nil, 0, nil, SW_INVALIDATE or Flags);
    UpdateWindow(Handle);
    Result := True;
  end
  else
    Result := False;
end;

function TPdfControl.ScrollContentTo(X, Y: Integer; Smooth: Boolean = False): Boolean;
var
  ScrollInfo: TScrollInfo;
  XOffset, YOffset: Integer;
begin
  XOffset := 0;
  YOffset := 0;
  ScrollInfo.cbSize := SizeOf(ScrollInfo);
  ScrollInfo.fMask := SIF_POS;
  if GetScrollInfo(Handle, SB_HORZ, ScrollInfo) then
    XOffset := X - ScrollInfo.nPos;
  if GetScrollInfo(Handle, SB_VERT, ScrollInfo) then
    YOffset := Y - ScrollInfo.nPos;
  Result := ScrollContent(XOffset, YOffset, Smooth);
end;

procedure TPdfControl.WMVScroll(var Message: TWMVScroll);
var
  ScrollInfo: TScrollInfo;
  Offset: Integer;
begin
  ScrollInfo.cbSize := SizeOf(ScrollInfo);
  ScrollInfo.fMask := SIF_ALL;
  GetScrollInfo(Handle, SB_VERT, ScrollInfo);
  Offset := 0;
  case Message.ScrollCode of
    SB_LINEUP:
      Offset := -cDefaultScrollOffset;
    SB_LINEDOWN:
      Offset := cDefaultScrollOffset;
    SB_PAGEUP:
      Offset := -ScrollInfo.nPage;
    SB_PAGEDOWN:
      Offset := ScrollInfo.nPage;
    SB_THUMBTRACK:
      Offset := ScrollInfo.nTrackPos - ScrollInfo.nPos;
  end;
  ScrollContent(0, Offset, SmoothScroll);
  Message.Result := 0;
end;

procedure TPdfControl.WMHScroll(var Message: TWMHScroll);
var
  ScrollInfo: TScrollInfo;
  Offset: Integer;
begin
  ScrollInfo.cbSize := SizeOf(ScrollInfo);
  ScrollInfo.fMask := SIF_ALL;
  GetScrollInfo(Handle, SB_HORZ, ScrollInfo);
  Offset := 0;
  case Message.ScrollCode of
    SB_LINELEFT:
      Offset := -cDefaultScrollOffset;
    SB_LINERIGHT:
      Offset := cDefaultScrollOffset;
    SB_PAGELEFT:
      Offset := -ScrollInfo.nPage;
    SB_PAGERIGHT:
      Offset := ScrollInfo.nPage;
    SB_THUMBTRACK:
      Offset := ScrollInfo.nTrackPos - ScrollInfo.nPos;
  end;
  ScrollContent(Offset, 0, SmoothScroll);
  Message.Result := 0;
end;

function TPdfControl.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
  MousePos: TPoint): Boolean;
var
  PagePt: TPdfPoint;
begin
  Result := inherited DoMouseWheel(Shift, WheelDelta, MousePos);

  if not Result then
  begin
    if IsPageValid and AllowFormEvents then
    begin
      PagePt := DeviceToPage(MousePos.X, MousePos.Y);
      if CurrentPage.FormEventMouseWheel(Shift, WheelDelta, PagePt.X, PagePt.Y) then
        Exit;
    end;

    if ssCtrl in Shift then
    begin
      if ScaleMode = smZoom then
      begin
        ZoomPercentage := ZoomPercentage + (WheelDelta div WHEEL_DELTA) * 5;
        Result := True;
      end;
    end
    else
    begin
      if ssShift in Shift then
        Result := ScrollContent(-WheelDelta, 0, SmoothScroll)
      else
        Result := ScrollContent(0, -WheelDelta, SmoothScroll);

      if not Result and FChangePageOnMouseScrolling then
      begin
        if WheelDelta < 0 then
          GotoNextPage()
        else if PageIndex > 0 then
        begin
          GotoPrevPage();
          ScrollContentTo(0, MaxInt);
        end;
      end
      else
        Result := True;
    end;
  end;
end;

procedure TPdfControl.WMTimer(var Message: TWMTimer);
var
  XOffset, YOffset: Integer;
begin
  case Message.TimerID of
    cScrollTimerId:
      begin
        if FMousePressed and FScrollTimerActive then
        begin
          XOffset := 0;
          YOffset := 0;
          if FScrollMousePos.X < 0 then
            XOffset := -cDefaultScrollOffset
          else if FScrollMousePos.X >= Width then
            XOffset := cDefaultScrollOffset
          else if FScrollMousePos.Y < 0 then
            YOffset := -cDefaultScrollOffset
          else if FScrollMousePos.Y >= Height then
            YOffset := cDefaultScrollOffset;
          ScrollContent(XOffset, YOffset, SmoothScroll);
        end
        else
          StopScrollTimer;
      end;

    cTrippleClickTimerId:
      begin
        FCheckForTrippleClick := False;
        KillTimer(Handle, cTrippleClickTimerId);
      end;
  else
    inherited;
  end;
end;

procedure TPdfControl.StopScrollTimer;
begin
  if FScrollTimerActive then
  begin
    KillTimer(Handle, cScrollTimerId);
    FScrollTimerActive := False;
  end;
end;

procedure TPdfControl.HightlightText(const SearchText: string; MatchCase, MatchWholeWord: Boolean);
begin
  if FHighlightTexts <> nil then
    FHighlightTexts.Clear;
  AddHightlightText(SearchText, MatchCase, MatchWholeWord);
end;

procedure TPdfControl.AddHightlightText(const SearchText: string; MatchCase, MatchWholeWord: Boolean);
var
  HLTextInfo: THighlightTextInfo;
  I: Integer;
begin
  if SearchText = '' then
    Exit;

  // Prevent duplicates
  if FHighlightTexts <> nil then
    for I := 0 to FHighlightTexts.Count - 1 do
      if (FHighlightTexts[I] as THighlightTextInfo).IsSame(SearchText, MatchCase, MatchWholeWord) then
        Exit;

  if FHighlightTexts = nil then
    FHighlightTexts := TObjectList.Create;
  HLTextInfo := THighlightTextInfo.Create(SearchText, MatchCase, MatchWholeWord);
  FHighlightTexts.Add(HLTextInfo);

  CalcHighlightTextRects;
end;

procedure TPdfControl.CalcHighlightTextRects;
var
  OldHighlightTextRects: TPdfRectArray;
  HLTextInfo: THighlightTextInfo;
  Page: TPdfPage;
  CharIndex, CharCount, I, Count, TextsIndex: Integer;
  Num: Integer;
begin
  OldHighlightTextRects := FHighlightTextRects;
  FHighlightTextRects := nil;
  if (FHighlightTexts <> nil) and (FHighlightTexts.Count > 0) and IsPageValid then
  begin
    Page := CurrentPage;
    Num := 0;
    for TextsIndex := 0 to FHighlightTexts.Count - 1 do
    begin
      HLTextInfo := FHighlightTexts[TextsIndex] as THighlightTextInfo;
      if HLTextInfo.Text <> '' then // prevent infinite loop in FPDFText_FindNext()
      begin
        if Page.BeginFind(HLTextInfo.Text, HLTextInfo.MatchCase, HLTextInfo.MatchWholeWord, False) then
        begin
          try
            while Page.FindNext(CharIndex, CharCount) do
            begin
              Count := Page.GetTextRectCount(CharIndex, CharCount);
              if Num + Count > Length(FHighlightTextRects) then
                SetLength(FHighlightTextRects, (Num + Count) * 2);
              for I := 0 to Count - 1 do
              begin
                FHighlightTextRects[Num] := Page.GetTextRect(I);
                Inc(Num);
              end;
            end;
          finally
            Page.EndFind;
          end;
        end;
      end;
    end;

    // truncate to the actual number
    if Num <> Length(FHighlightTextRects) then
      SetLength(FHighlightTextRects, Num);
  end;
  InvalidatePdfRectDiffs(OldHighlightTextRects, FHighlightTextRects);
end;

procedure TPdfControl.ClearHighlightText;
begin
  FreeAndNil(FHighlightTexts);
  InvalidatePdfRectDiffs(FHighlightTextRects, nil);
  FHighlightTextRects := nil;
end;

procedure TPdfControl.FormInvalidate(Document: TPdfDocument; Page: TPdfPage;
  const PageRect: TPdfRect);
var
  R: TRect;
begin
  FRenderedPageIndex := -1; // content has changed => render into the background bitmap
  FFormOutputSelectedRects := nil;
  if HandleAllocated then
  begin
    R := InternPageToDevice(Page, PageRect, True);
    InvalidateRect(Handle, @R, True);
  end;
end;

procedure TPdfControl.FormOutputSelectedRect(Document: TPdfDocument; Page: TPdfPage;
  const PageRect: TPdfRect);
begin
  if HandleAllocated then
  begin
    SetLength(FFormOutputSelectedRects, Length(FFormOutputSelectedRects) + 1);
    FFormOutputSelectedRects[Length(FFormOutputSelectedRects) - 1] := PageRect;
  end;
end;

procedure TPdfControl.FormGetCurrentPage(Document: TPdfDocument; var Page: TPdfPage);
begin
  Page := CurrentPage;
end;

procedure TPdfControl.FormFieldFocus(Document: TPdfDocument; Value: PWideChar;
  ValueLen: Integer; FieldFocused: Boolean);
begin
  ClearSelection;
  FFormFieldFocused := FieldFocused;
end;

procedure TPdfControl.ExecuteNamedAction(Document: TPdfDocument; NamedAction: TPdfNamedActionType);
begin
  case NamedAction of
    naPrint:
      PrintDocument;
    naNextPage:
      PageIndex := PageIndex + 1;
    naPrevPage:
      PageIndex := PageIndex - 1;
    naFirstPage:
      PageIndex := 0;
    naLastPage:
      PageIndex := Document.PageCount - 1;
  end;
end;

end.

