unit EM.PdfiumCore;
{$IFDEF FPC}
  {$MODE DelphiUnicode}
{$ENDIF FPC}
{$IFNDEF FPC}
  {$A8,B-,E-,F-,G+,H+,I+,J-,K-,M-,N-,P+,Q-,R-,S-,T-,U-,V+,X+,Z1}
  {$STRINGCHECKS OFF}
{$ENDIF ~FPC}
interface
{.$UNDEF MSWINDOWS}
uses
  {$IFDEF MSWINDOWS}
  Windows, //WinSpool,
  {$ELSE}
    {$IFDEF FPC}
  LCLType,
    {$ENDIF FPC}
  ExtCtrls, // for TTimer
  {$ENDIF MSWINDOWS}
  Types, SysUtils, Classes, Contnrs,
  EM.PdfiumLib;
const
  // DIN A4
  PdfDefaultPageWidth = 595;
  PdfDefaultPageHeight = 842;
type
  EPdfException = class(Exception);
  EPdfUnsupportedFeatureException = class(EPdfException);
  EPdfArgumentOutOfRange = class(EPdfException);
  TPdfUnsupportedFeatureHandler = procedure(nType: Integer; const Typ: string) of object;
  TPdfDocument = class;
  TPdfPage = class;
  TPdfAttachmentList = class;
  TPdfAnnotationList = class;
  TPdfFormField = class;
  TPdfAnnotation = class;
  TPdfPoint = record
    X, Y: Double;
    procedure Offset(XOffset, YOffset: Double);
    class function Empty: TPdfPoint; static;
  end;
  TPdfRect = record
  private
    function GetHeight: Double; inline;
    function GetWidth: Double; inline;
    procedure SetHeight(const Value: Double); inline;
    procedure SetWidth(const Value: Double); inline;
  public
    property Width: Double read GetWidth write SetWidth;
    property Height: Double read GetHeight write SetHeight;
    procedure Offset(XOffset, YOffset: Double);
    function PtIn(const Pt: TPdfPoint): Boolean;
    class function New(Left, Top, Right, Bottom: Double): TPdfRect; static;
    class function Empty: TPdfRect; static;
  public
    case Integer of
      0: (Left, Top, Right, Bottom: Double);
      1: (TopLeft: TPdfPoint; BottomRight: TPdfPoint);
  end;
  TPdfRectArray = array of TPdfRect;
  TPdfFloatArray = array of FS_FLOAT;
  TPdfDocumentCustomReadProc = function(Param: Pointer; Position: LongWord; Buffer: PByte; Size: LongWord): Boolean;
  TPdfNamedActionType = (
    naPrint,
    naNextPage,
    naPrevPage,
    naFirstPage,
    naLastPage
  );
  TPdfPageRenderOptionType = (
    proAnnotations,            // Set if annotations are to be rendered.
    proLCDOptimized,           // Set if using text rendering optimized for LCD display.
    proNoNativeText,           // Don't use the native text output available on some platforms
    proNoCatch,                // Set if you don't want to catch exception.
    proLimitedImageCacheSize,  // Limit image cache size.
    proForceHalftone,          // Always use halftone for image stretching.
    proPrinting,               // Render for printing.
    proReverseByteOrder        // Set whether render in a reverse Byte order, this flag only enable when render to a bitmap.
  );
  TPdfPageRenderOptions = set of TPdfPageRenderOptionType;
  TPdfPageRotation = (
    prNormal             = 0,
    pr90Clockwise        = 1,
    pr180                = 2,
    pr90CounterClockwide = 3
  );
  TPdfDocumentSaveOption = (
    dsoIncremental    = 1,
    dsoNoIncremental  = 2,
    dsoRemoveSecurity = 3
  );
  TPdfDocumentLoadOption = (
    dloDefault,  // load the file by using PDFium's file load mechanism (file stays open)
    dloMemory,   // load the whole file into memory
    dloMMF,      // load the file by using a memory mapped file (file stays open)
    dloOnDemand  // load the file using the custom load function (file stays open)
  );
  TPdfDocumentPageMode = (
    dpmUnknown        = -1, // Unknown value
    dpmUseNone        = 0,  // Neither document outline nor thumbnail images visible
    dpmUseOutlines    = 1,  // Document outline visible
    dpmUseThumbs      = 2,  // Thumbnial images visible
    dpmFullScreen     = 3,  // Full-screen mode, with no menu bar, window controls, or any other window visible
    dpmUseOC          = 4,  // Optional content group panel visible
    dpmUseAttachments = 5   // Attachments panel visible
  );
  TPdfPrintMode = (
    pmEMF                          = FPDF_PRINTMODE_EMF,
    pmTextMode                     = FPDF_PRINTMODE_TEXTONLY,
    pmPostScript2                  = FPDF_PRINTMODE_POSTSCRIPT2,
    pmPostScript3                  = FPDF_PRINTMODE_POSTSCRIPT3,
    pmPostScriptPassThrough2       = FPDF_PRINTMODE_POSTSCRIPT2_PASSTHROUGH,
    pmPostScriptPassThrough3       = FPDF_PRINTMODE_POSTSCRIPT3_PASSTHROUGH,
    pmEMFImageMasks                = FPDF_PRINTMODE_EMF_IMAGE_MASKS,
    pmPostScript3Type42            = FPDF_PRINTMODE_POSTSCRIPT3_TYPE42,
    pmPostScript3Type42PassThrough = FPDF_PRINTMODE_POSTSCRIPT3_TYPE42_PASSTHROUGH
  );
  TPdfFileIdType = (
    pfiPermanent = 0,
    pfiChanging = 1
  );
  TPdfBitmapFormat = (
    bfGrays = FPDFBitmap_Gray, // Gray scale bitmap, one byte per pixel.
    bfBGR   = FPDFBitmap_BGR,  // 3 bytes per pixel, byte order: blue, green, red.
    bfBGRx  = FPDFBitmap_BGRx, // 4 bytes per pixel, byte order: blue, green, red, unused.
    bfBGRA  = FPDFBitmap_BGRA  // 4 bytes per pixel, byte order: blue, green, red, alpha.
  );
  TPdfFormFieldType = (
    fftUnknown         = FPDF_FORMFIELD_UNKNOWN,
    fftPushButton      = FPDF_FORMFIELD_PUSHBUTTON,
    fftCheckBox        = FPDF_FORMFIELD_CHECKBOX,
    fftRadioButton     = FPDF_FORMFIELD_RADIOBUTTON,
    fftComboBox        = FPDF_FORMFIELD_COMBOBOX,
    fftListBox         = FPDF_FORMFIELD_LISTBOX,
    fftTextField       = FPDF_FORMFIELD_TEXTFIELD,
    fftSignature       = FPDF_FORMFIELD_SIGNATURE,
    fftXFA             = FPDF_FORMFIELD_XFA,
    fftXFACheckBox     = FPDF_FORMFIELD_XFA_CHECKBOX,
    fftXFAComboBox     = FPDF_FORMFIELD_XFA_COMBOBOX,
    fftXFAImageField   = FPDF_FORMFIELD_XFA_IMAGEFIELD,
    fftXFAListBox      = FPDF_FORMFIELD_XFA_LISTBOX,
    fftXFAPushButton   = FPDF_FORMFIELD_XFA_PUSHBUTTON,
    fftXFASignature    = FPDF_FORMFIELD_XFA_SIGNATURE,
    fftXfaTextField    = FPDF_FORMFIELD_XFA_TEXTFIELD
  );
  TPdfFormFieldFlagsType = (
    fffReadOnly,
    fffRequired,
    fffNoExport,
    fffTextMultiLine,
    fffTextPassword,
    fffChoiceCombo,
    fffChoiceEdit,
    fffChoiceMultiSelect
  );
  TPdfFormFieldFlags = set of TPdfFormFieldFlagsType;
  TPdfObjectType = (
    otUnknown   = FPDF_OBJECT_UNKNOWN,
    otBoolean   = FPDF_OBJECT_BOOLEAN,
    otNumber    = FPDF_OBJECT_NUMBER,
    otString    = FPDF_OBJECT_STRING,
    otName      = FPDF_OBJECT_NAME,
    otArray     = FPDF_OBJECT_ARRAY,
    otDictinary = FPDF_OBJECT_DICTIONARY,
    otStream    = FPDF_OBJECT_STREAM,
    otNullObj   = FPDF_OBJECT_NULLOBJ,
    otReference = FPDF_OBJECT_REFERENCE
  );
  TPdfAnnotationLinkType = (
    altUnsupported  = PDFACTION_UNSUPPORTED, // Unsupported action type.
    altGoto         = PDFACTION_GOTO,        // Go to a destination within current document.
    altRemoteGoto   = PDFACTION_REMOTEGOTO,  // Go to a destination within another document.
    altURI          = PDFACTION_URI,         // Universal Resource Identifier, including web pages and
                                             // other Internet based resources.
    altLaunch       = PDFACTION_LAUNCH,      // Launch an application or open a file.
    altEmbeddedGoto = PDFACTION_EMBEDDEDGOTO // Go to a destination in an embedded file.
  );
  TPdfLinkGotoDestinationViewKind = (
    lgdvUnknown = PDFDEST_VIEW_UNKNOWN_MODE,
    lgdvXYZ     = PDFDEST_VIEW_XYZ,
    lgdvFit     = PDFDEST_VIEW_FIT,
    lgdvFitH    = PDFDEST_VIEW_FITH,
    lgdvFitV    = PDFDEST_VIEW_FITV,
    lgdvFitR    = PDFDEST_VIEW_FITR,
    lgdvFitB    = PDFDEST_VIEW_FITB,
    lgdvFitBH   = PDFDEST_VIEW_FITBH,
    lgdvFitBV   = PDFDEST_VIEW_FITBV
  );
  // Make the TObject.Create constructor private to hide it, so that the TPdfBitmap.Create
  // overloads won't allow calling TObject.Create.
  _TPdfBitmapHideCtor = class(TObject)
  private
    constructor Create;
  end;
  TPdfBitmap = class(_TPdfBitmapHideCtor)
  private
    FBitmap: FPDF_BITMAP;
    FOwnsBitmap: Boolean;
    FWidth: Integer;
    FHeight: Integer;
    FBytesPerScanLine: Integer;
  public
    constructor Create(ABitmap: FPDF_BITMAP; AOwnsBitmap: Boolean = False); overload;
    constructor Create(AWidth, AHeight: Integer; AAlpha: Boolean); overload;
    constructor Create(AWidth, AHeight: Integer; AFormat: TPdfBitmapFormat); overload;
    constructor Create(AWidth, AHeight: Integer; AFormat: TPdfBitmapFormat; ABuffer: Pointer; ABytesPerScanline: Integer); overload;
    destructor Destroy; override;
    procedure FillRect(ALeft, ATop, AWidth, AHeight: Integer; AColor: FPDF_DWORD);
    function GetBuffer: Pointer;
    property Width: Integer read FWidth;
    property Height: Integer read FHeight;
    property BytesPerScanline: Integer read FBytesPerScanLine;
    property Bitmap: FPDF_BITMAP read FBitmap;
  end;
  PPdfFormFillHandler = ^TPdfFormFillHandler;
  TPdfFormFillHandler = record
    FormFillInfo: FPDF_FORMFILLINFO;
    Document: TPdfDocument;
  end;
  TPdfFormField = class(TObject)
  private
    FPage: TPdfPage;
    FHandle: FPDF_ANNOTATION;
    FAnnotation: TPdfAnnotation;
    function GetFlags: TPdfFormFieldFlags;
    function GetReadOnly: Boolean;
    function GetName: string;
    function GetAlternateName: string;
    function GetFieldType: TPdfFormFieldType;
    function GetValue: string;
    function GetExportValue: string;
    function GetOptionCount: Integer;
    function GetOptionLabel(Index: Integer): string;
    function GetChecked: Boolean;
    function GetControlIndex: Integer;
    function GetControlCount: Integer;
    procedure SetValue(const Value: string);
    procedure SetChecked(const Value: Boolean);
  protected
    constructor Create(AAnnotation: TPdfAnnotation);
    function BeginEditFormField: FPDF_ANNOTATION;
    procedure EndEditFormField(LastFocusedAnnot: FPDF_ANNOTATION);
  public
    destructor Destroy; override;
    function IsXFAFormField: Boolean;
    function IsOptionSelected(OptionIndex: Integer): Boolean;
    function SelectComboBoxOption(OptionIndex: Integer): Boolean;
    function SelectListBoxOption(OptionIndex: Integer; Selected: Boolean = True): Boolean;
    property Flags: TPdfFormFieldFlags read GetFlags;
    property ReadOnly: Boolean read GetReadOnly;
    property Name: string read GetName;
    property AlternateName: string read GetAlternateName;
    property FieldType: TPdfFormFieldType read GetFieldType;
    property Value: string read GetValue write SetValue;
    property ExportValue: string read GetExportValue;
    // ComboBox/ListBox
    property OptionCount: Integer read GetOptionCount;
    property OptionLabels[Index: Integer]: string read GetOptionLabel;
    // CheckBox/RadioButton
    property Checked: Boolean read GetChecked write SetChecked;
    property ControlIndex: Integer read GetControlIndex;
    property ControlCount: Integer read GetControlCount;
    property Annotation: TPdfAnnotation read FAnnotation;
    property Handle: FPDF_ANNOTATION read FHandle;
  end;
  TPdfFormFieldList = class(TObject)
  private
    FItems: TList;
    function GetCount: Integer;
    function GetItem(Index: Integer): TPdfFormField;
  protected
    procedure DestroyingItem(Item: TPdfFormField);
  public
    constructor Create(AAnnotations: TPdfAnnotationList);
    destructor Destroy; override;
    property Count: Integer read GetCount;
    property Items[Index: Integer]: TPdfFormField read GetItem; default;
  end;
  TPdfLinkGotoDestination = class(TObject)
  private
    FPageIndex: Integer;
    FXValid: Boolean;
    FYValid: Boolean;
    FZoomValid: Boolean;
    FX: Single;
    FY: Single;
    FZoom: Single;
    FViewKind: TPdfLinkGotoDestinationViewKind;
    FViewParams: TPdfFloatArray;
  public
    constructor Create(APageIndex: Integer; AXValid, AYValid, AZoomValid: Boolean; AX, AY, AZoom: Single;
      AViewKind: TPdfLinkGotoDestinationViewKind; const AViewParams: TPdfFloatArray);
    property PageIndex: Integer read FPageIndex;
    property XValid: Boolean read FXValid;
    property YValid: Boolean read FYValid;
    property ZoomValid: Boolean read FZoomValid;
    property X: Single read FX;
    property Y: Single read FY;
    property Zoom: Single read FZoom;
    property ViewKind: TPdfLinkGotoDestinationViewKind read FViewKind;
    property ViewParams: TPdfFloatArray read FViewParams;
  end;
  TPdfAnnotation = class(TObject)
  private
    FPage: TPdfPage;
    FHandle: FPDF_ANNOTATION;
    FFormField: TPdfFormField;
    FSubType: FPDF_ANNOTATION_SUBTYPE;
    FLinkDest: FPDF_DEST;
    FLinkType: TPdfAnnotationLinkType;
    function GetPdfLinkAction: FPDF_ACTION;
    function GetFormField: TPdfFormField;
    function GetLinkUri: string;
    function GetAnnotationRect: TPdfRect;
    function GetLinkFileName: string;
  protected
    constructor Create(APage: TPdfPage; AHandle: FPDF_ANNOTATION);
  public
    destructor Destroy; override;
    function IsFormField: Boolean;
    function IsLink: Boolean;
    function GetLinkGotoDestination(var LinkGotoDestination: TPdfLinkGotoDestination; ARemoteDocument: TPdfDocument = nil): Boolean;
    // IsFormField:
    property FormField: TPdfFormField read GetFormField;
    // IsLink:
    property LinkType: TPdfAnnotationLinkType read FLinkType;
    property LinkUri: string read GetLinkUri;
    property LinkFileName: string read GetLinkFileName;
    property AnnotationRect: TPdfRect read GetAnnotationRect;
    property Handle: FPDF_ANNOTATION read FHandle;
  end;
  TPdfAnnotationList = class(TObject)
  private
    FPage: TPdfPage;
    FItems: TObjectList;
    FFormFields: TPdfFormFieldList;
    function GetCount: Integer;
    function GetItem(Index: Integer): TPdfAnnotation;
    function GetFormFields: TPdfFormFieldList;
    function GetAnnotationsLoaded: Boolean;
  protected
    procedure DestroyingItem(Item: TPdfAnnotation);
    procedure DestroyingFormField(FormField: TPdfFormField);
    function FindLink(Link: FPDF_LINK): TPdfAnnotation;
  public
    constructor Create(APage: TPdfPage);
    destructor Destroy; override;
    procedure CloseAnnotations;
    { NewTextAnnotation creates a new text annotation on the page. After adding one or more
      annotations you must call Page.ApplyChanges to show them and make the persist before
      saving the file. R is in page coordinates. }
    function NewTextAnnotation(const Text: string; const R: TPdfRect): Boolean; {experimental;}
    property AnnotationsLoaded: Boolean read GetAnnotationsLoaded;
    property Count: Integer read GetCount;
    property Items[Index: Integer]: TPdfAnnotation read GetItem; default;
    { A list of all form field annotations }
    property FormFields: TPdfFormFieldList read GetFormFields;
  end;
  TPdfLinkInfo = class(TObject)
  private
    FLinkAnnotation: TPdfAnnotation;
    FWebLinkUrl: string;
    function GetLinkFileName: string;
    function GetLinkType: TPdfAnnotationLinkType;
    function GetLinkUri: string;
  public
    constructor Create(ALinkAnnotation: TPdfAnnotation; const AWebLinkUrl: string);
    function GetLinkGotoDestination(var LinkGotoDestination: TPdfLinkGotoDestination; ARemoteDocument: TPdfDocument = nil): Boolean;
    function IsAnnontation: Boolean;
    function IsWebLink: Boolean;
    property LinkType: TPdfAnnotationLinkType read GetLinkType;
    property LinkUri: string read GetLinkUri;
    property LinkFileName: string read GetLinkFileName;
    property LinkAnnotation: TPdfAnnotation read FLinkAnnotation;
  end;
  { TPdfPageWebLinksInfo caches all the WebLinks for one page. This makes the IsWebLinkAt() methods
    much faster than always calling into the PDFium library. The URLs are not cached. }
  TPdfPageWebLinksInfo = class(TObject)
  private
    FPage: TPdfPage;
    FWebLinksRects: array of TPdfRectArray;
    procedure GetPageWebLinks;
    function GetWebLinkIndex(X, Y: Double): Integer;
    function GetCount: Integer;
    function GetRect(Index: Integer): TPdfRectArray;
    function GetURL(Index: Integer): string;
  public
    constructor Create(APage: TPdfPage);
    function IsWebLinkAt(X, Y: Double): Boolean; overload;
    function IsWebLinkAt(X, Y: Double; var Url: string): Boolean; overload;
    property Count: Integer read GetCount;
    property URLs[Index: Integer]: string read GetURL;
    property Rects[Index: Integer]: TPdfRectArray read GetRect;
  end;
  TPdfPage = class(TObject)
  private
    FDocument: TPdfDocument;
    FPage: FPDF_PAGE;
    FWidth: Single;
    FHeight: Single;
    FTransparency: Boolean;
    FRotation: TPdfPageRotation;
    FAnnotations: TPdfAnnotationList;
    FTextHandle: FPDF_TEXTPAGE;
    FSearchHandle: FPDF_SCHHANDLE;
    FPageLinkHandle: FPDF_PAGELINK;
    constructor Create(ADocument: TPdfDocument; APage: FPDF_PAGE);
    procedure UpdateMetrics;
    procedure Open;
    procedure SetRotation(const Value: TPdfPageRotation);
    function BeginText: Boolean;
    function BeginWebLinks: Boolean;
    class function GetDrawFlags(const Options: TPdfPageRenderOptions): Integer; static;
    procedure AfterOpen;
    function IsValidForm: Boolean;
    function ShiftStateToModifier(const Shift: TShiftState): Integer;
    function GetHandle: FPDF_PAGE;
    function GetTextHandle: FPDF_TEXTPAGE;
    function GetFormFields: TPdfFormFieldList;
  protected
    function GetPdfActionFilePath(Action: FPDF_ACTION): string;
    function GetPdfActionUriPath(Action: FPDF_ACTION): string;
  public
    destructor Destroy; override;
    procedure Close;
    function IsLoaded: Boolean;
    {$IFDEF MSWINDOWS}
    // Draw the PDF page and the form into the device context.
    procedure Draw(DC: HDC; X, Y, Width, Height: Integer; Rotate: TPdfPageRotation = prNormal;
      const Options: TPdfPageRenderOptions = []; PageBackground: TColorRef = $FFFFFF); overload;
    {$ENDIF MSWINDOWS}
    // Draw the PDF page and the form into the bitmap.
    procedure Draw(APdfBitmap: TPdfBitmap; X, Y, Width, Height: Integer; Rotate: TPdfPageRotation = prNormal;
      const Options: TPdfPageRenderOptions = []; PageBackground: TColorRef = $FFFFFF); overload;
    // Draw the PDF page without the form field values into the bitmap.
    procedure DrawToPdfBitmap(APdfBitmap: TPdfBitmap; X, Y, Width, Height: Integer; Rotate: TPdfPageRotation = prNormal;
      const Options: TPdfPageRenderOptions = []);
    // Draw the PDF form field values into the bitmap.
    procedure DrawFormToPdfBitmap(APdfBitmap: TPdfBitmap; X, Y, Width, Height: Integer; Rotate: TPdfPageRotation = prNormal;
      const Options: TPdfPageRenderOptions = []);
    function DeviceToPage(X, Y, Width, Height: Integer; DeviceX, DeviceY: Integer; Rotate: TPdfPageRotation = prNormal): TPdfPoint; overload;
    function PageToDevice(X, Y, Width, Height: Integer; PageX, PageY: Double; Rotate: TPdfPageRotation = prNormal): TPoint; overload;
    function DeviceToPage(X, Y, Width, Height: Integer; const R: TRect; Rotate: TPdfPageRotation = prNormal): TPdfRect; overload;
    function PageToDevice(X, Y, Width, Height: Integer; const R: TPdfRect; Rotate: TPdfPageRotation = prNormal): TRect; overload;
    procedure ApplyChanges;
    procedure Flatten(AFlatPrint: Boolean);
    function FormEventFocus(const Shift: TShiftState; PageX, PageY: Double): Boolean;
    function FormEventMouseWheel(const Shift: TShiftState; WheelDelta: Integer; PageX, PageY: Double): Boolean;
    function FormEventMouseMove(const Shift: TShiftState; PageX, PageY: Double): Boolean;
    function FormEventLButtonDown(const Shift: TShiftState; PageX, PageY: Double): Boolean;
    function FormEventLButtonUp(const Shift: TShiftState; PageX, PageY: Double): Boolean;
    function FormEventRButtonDown(const Shift: TShiftState; PageX, PageY: Double): Boolean;
    function FormEventRButtonUp(const Shift: TShiftState; PageX, PageY: Double): Boolean;
    function FormEventKeyDown(KeyCode: Word; const Shift: TShiftState): Boolean;
    function FormEventKeyUp(KeyCode: Word; const Shift: TShiftState): Boolean;
    function FormEventKeyPress(Key: Word; const Shift: TShiftState): Boolean;
    function FormEventKillFocus: Boolean;
    function FormGetFocusedText: string;
    function FormGetSelectedText: string;
    function FormReplaceSelection(const ANewText: string): Boolean;
    function FormReplaceAndKeepSelection(const ANewText: string): Boolean;
    function FormSelectAllText: Boolean;
    function FormCanUndo: Boolean;
    function FormCanRedo: Boolean;
    function FormUndo: Boolean;
    function FormRedo: Boolean;
    function BeginFind(const SearchString: string; MatchCase, MatchWholeWord: Boolean; FromEnd: Boolean): Boolean;
    function FindNext(var CharIndex, Count: Integer): Boolean;
    function FindPrev(var CharIndex, Count: Integer): Boolean;
    procedure EndFind;
    function GetCharCount: Integer;
    function ReadChar(CharIndex: Integer): WideChar;
    function GetCharFontSize(CharIndex: Integer): Double;
    function GetCharBox(CharIndex: Integer): TPdfRect;
    function GetCharIndexAt(PageX, PageY, ToleranceX, ToleranceY: Double): Integer;
    function ReadText(CharIndex, Count: Integer): String;
    function ReadText2(CharIndex, Count: Integer): String;
    function GetTextAt(const R: TPdfRect): string; overload;
    function GetTextAt(Left, Top, Right, Bottom: Double): string; overload;
    function GetTextRectCount(CharIndex, Count: Integer): Integer;
    function GetTextRect(RectIndex: Integer): TPdfRect;
    function HasFormFieldAtPoint(X, Y: Double): TPdfFormFieldType;
    { IsUriLinkAtPoint returns true if a Link annotation is at the specified coordinates.
      X, Y are in page coordinates. }
    function IsUriLinkAtPoint(X, Y: Double): Boolean; overload;
    { IsUriLinkAtPoint returns true if a Link annotation is at the specified coordinates. If one is found
      the Uri parameter is set to the link's URI.
      X, Y are in page coordinates. }
    function IsUriLinkAtPoint(X, Y: Double; var Uri: string): Boolean; overload;
    { GetLinkAtPoint returns the link annotation for the specified coordinates. If no link annotation
      was found it return nil. It not only returns Uri but also Goto, RemoteGoto, Launch, EmbeddedGoto
      link annotations. }
    function GetLinkAtPoint(X, Y: Double): TPdfAnnotation;
    { WebLinks are URLs that are parsed from the PDFs text content. No link annotation exists
      for them, so the IsUriLinkAtPoint and GetLinkAtPoint methods don't work for them. }
    function GetWebLinkCount: Integer;
    function GetWebLinkURL(LinkIndex: Integer): string;
    function GetWebLinkRectCount(LinkIndex: Integer): Integer;
    function GetWebLinkRect(LinkIndex, RectIndex: Integer): TPdfRect;
    function IsWebLinkAtPoint(X, Y: Double): Boolean; overload;
    function IsWebLinkAtPoint(X, Y: Double; var URL: string): Boolean; overload;
    property Handle: FPDF_PAGE read GetHandle;
    property TextHandle: FPDF_TEXTPAGE read GetTextHandle;
    property Width: Single read FWidth;
    property Height: Single read FHeight;
    property Transparency: Boolean read FTransparency;
    property Rotation: TPdfPageRotation read FRotation write SetRotation;
    property Annotations: TPdfAnnotationList read FAnnotations;
    property FormFields: TPdfFormFieldList read GetFormFields;
  end;
  TPdfFormInvalidateEvent = procedure(Document: TPdfDocument; Page: TPdfPage; const PageRect: TPdfRect) of object;
  TPdfFormOutputSelectedRectEvent = procedure(Document: TPdfDocument; Page: TPdfPage; const PageRect: TPdfRect) of object;
  TPdfFormGetCurrentPageEvent = procedure(Document: TPdfDocument; var CurrentPage: TPdfPage) of object;
  TPdfFormFieldFocusEvent = procedure(Document: TPdfDocument; Value: PWideChar; ValueLen: Integer; FieldFocused: Boolean) of object;
  TPdfExecuteNamedActionEvent = procedure(Document: TPdfDocument; NamedAction: TPdfNamedActionType) of object;
  TPdfAttachment = record
  private
    FDocument: TPdfDocument;
    FHandle: FPDF_ATTACHMENT;
    procedure CheckValid;
    function GetName: string;
    function GetKeyValue(const Key: string): string;
    procedure SetKeyValue(const Key, Value: string);
    function GetContentSize: Integer;
  public
    // SetContent/LoadFromXxx clears the Values[] dictionary.
    procedure SetContent(const ABytes: TBytes); overload;
    procedure SetContent(const ABytes: TBytes; Index: NativeInt; Count: Integer); overload;
    procedure SetContent(ABytes: PByte; Count: Integer); overload;
    procedure SetContent(const Value: RawByteString); overload;
    procedure SetContent(const Value: string; Encoding: TEncoding = nil); overload; // Default-encoding is UTF-8
    procedure LoadFromStream(Stream: TStream);
    procedure LoadFromFile(const FileName: string);
    procedure GetContent(var ABytes: TBytes); overload;
    procedure GetContent(Buffer: PByte); overload; // use ContentSize to allocate enough memory
    procedure GetContent(var Value: RawByteString); overload;
    procedure GetContent(var Value: string; Encoding: TEncoding = nil); overload;
    function GetContentAsBytes: TBytes;
    function GetContentAsRawByteString: RawByteString;
    function GetContentAsString(Encoding: TEncoding = nil): string; // Default-encoding is UTF-8
    procedure SaveToStream(Stream: TStream);
    procedure SaveToFile(const FileName: string);
    function HasContent: Boolean;
    function HasKey(const Key: string): Boolean;
    function GetValueType(const Key: string): TPdfObjectType;
    property Name: string read GetName;
    property Values[const Key: string]: string read GetKeyValue write SetKeyValue;
    property ContentSize: Integer read GetContentSize;
    property Handle: FPDF_ATTACHMENT read FHandle;
  end;
  TPdfAttachmentList = class(TObject)
  private
    FDocument: TPdfDocument;
    function GetCount: Integer;
    function GetItem(Index: Integer): TPdfAttachment;
  public
    constructor Create(ADocument: TPdfDocument);
    function Add(const Name: string): TPdfAttachment;
    procedure Delete(Index: Integer);
    function IndexOf(const Name: string): Integer;
    property Count: Integer read GetCount;
    property Items[Index: Integer]: TPdfAttachment read GetItem; default;
  end;
  TPdfDocument = class(TObject)
  private type
    PCustomLoadDataRec = ^TCustomLoadDataRec;
    TCustomLoadDataRec = record
      Param: Pointer;
      GetBlock: TPdfDocumentCustomReadProc;
      FileAccess: TFPDFFileAccess;
    end;
  private
    FDocument: FPDF_DOCUMENT;
    FPages: TObjectList;
    FAttachments: TPdfAttachmentList;
    FFileName: string;
    {$IFDEF MSWINDOWS}
    FFileHandle: THandle;
    FFileMapping: THandle;
    {$ELSE}
    FFileStream: TFileStream;
    {$ENDIF MSWINDOWS}
    FBuffer: PByte;
    FBytes: TBytes;
    FClosing: Boolean;
    FUnsupportedFeatures: Boolean;
    FCustomLoadData: PCustomLoadDataRec;
    FForm: FPDF_FORMHANDLE;
    FJSPlatform: IPDF_JsPlatform;
    FFormFillHandler: TPdfFormFillHandler;
    FFormFieldHighlightColor: TColorRef;
    FFormFieldHighlightAlpha: Integer;
    FPrintHidesFormFieldHighlight: Boolean;
    FFormModified: Boolean;
    FOnFormInvalidate: TPdfFormInvalidateEvent;
    FOnFormOutputSelectedRect: TPdfFormOutputSelectedRectEvent;
    FOnFormGetCurrentPage: TPdfFormGetCurrentPageEvent;
    FOnFormFieldFocus: TPdfFormFieldFocusEvent;
    FOnExecuteNamedAction: TPdfExecuteNamedActionEvent;
    procedure InternLoadFromFile(const FileName: string; const Password: UTF8String);
    procedure InternLoadFromMem(Buffer: PByte; Size: NativeInt; const Password: UTF8String);
    procedure InternLoadFromCustom(ReadFunc: TPdfDocumentCustomReadProc; Size: LongWord;
      Param: Pointer; const Password: UTF8String);
    function InternImportPages(Source: TPdfDocument; PageIndices: PInteger; PageIndicesCount: Integer;
      const Range: AnsiString; Index: Integer; ImportByRange: Boolean): Boolean;
    function GetPage(Index: Integer): TPdfPage;
    function GetPageCount: Integer;
    procedure ExtractPage(APage: TPdfPage);
    function ReloadPage(APage: TPdfPage): FPDF_PAGE;
    function GetPrintScaling: Boolean;
    function GetActive: Boolean;
    procedure CheckActive;
    function GetSecurityHandlerRevision: Integer;
    function GetDocPermissions: Integer;
    function GetFileVersion: Integer;
    function GetPageSize(Index: Integer): TPdfPoint;
    function GetPageMode: TPdfDocumentPageMode;
    function GetNumCopies: Integer;
    procedure DocumentLoaded;
    procedure SetFormFieldHighlightAlpha(Value: Integer);
    procedure SetFormFieldHighlightColor(const Value: TColorRef);
    function FindPage(Page: FPDF_PAGE): TPdfPage;
    procedure UpdateFormFieldHighlight;
  public
    constructor Create;
    destructor Destroy; override;
    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: NativeInt; const Password: UTF8String = ''); // Buffer must not be released until the document is closed
    procedure LoadFromBytes(const Bytes: TBytes; const Password: UTF8String = ''); overload;
    procedure LoadFromBytes(const Bytes: TBytes; Index: NativeInt; Count: NativeInt; const Password: UTF8String = ''); overload;
    procedure LoadFromStream(Stream: TStream; const Password: UTF8String = '');
    procedure LoadFromFile(const FileName: string; const Password: UTF8String = ''; LoadOption: TPdfDocumentLoadOption = dloDefault);
    procedure Close;
    procedure SaveToFile(const AFileName: string; Option: TPdfDocumentSaveOption = dsoRemoveSecurity; FileVersion: Integer = -1);
    procedure SaveToStream(Stream: TStream; Option: TPdfDocumentSaveOption = dsoRemoveSecurity; FileVersion: Integer = -1);
    procedure SaveToBytes(var Bytes: TBytes; Option: TPdfDocumentSaveOption = dsoRemoveSecurity; FileVersion: Integer = -1);
    function NewDocument: Boolean;
    class function CreateNPagesOnOnePageDocument(Source: TPdfDocument; NewPageWidth, NewPageHeight: Double; NumPagesXAxis, NumPagesYAxis: Integer): TPdfDocument; overload;
    class function CreateNPagesOnOnePageDocument(Source: TPdfDocument; NumPagesXAxis, NumPagesYAxis: Integer): TPdfDocument; overload;
    function ImportAllPages(Source: TPdfDocument; Index: Integer = -1): Boolean;
    function ImportPages(Source: TPdfDocument; const Range: string = ''; Index: Integer = -1): Boolean;
    function ImportPageRange(Source: TPdfDocument; PageIndex: Integer; Count: Integer = -1; Index: Integer = -1): Boolean;
    function ImportPagesByIndex(Source: TPdfDocument; const PageIndices: array of Integer; Index: Integer = -1): Boolean;
    procedure DeletePage(Index: Integer);
    function NewPage(Width, Height: Double; Index: Integer = -1): TPdfPage; overload;
    function NewPage(Index: Integer = -1): TPdfPage; overload;
    function ApplyViewerPreferences(Source: TPdfDocument): Boolean;
    function IsPageLoaded(PageIndex: Integer): Boolean;
    function GetFileIdentifier(IdType: TPdfFileIdType): string;
    function GetMetaText(const TagName: string): string;
    class function SetPrintMode(PrintMode: TPdfPrintMode): Boolean; static;
    property FileName: string read FFileName;
    property PageCount: Integer read GetPageCount;
    property Pages[Index: Integer]: TPdfPage read GetPage;
    property PageSizes[Index: Integer]: TPdfPoint read GetPageSize;
    property Attachments: TPdfAttachmentList read FAttachments;
    property Active: Boolean read GetActive;
    property PrintScaling: Boolean read GetPrintScaling;
    property NumCopies: Integer read GetNumCopies;
    property SecurityHandlerRevision: Integer read GetSecurityHandlerRevision;
    property DocPermissions: Integer read GetDocPermissions;
    property FileVersion: Integer read GetFileVersion;
    property PageMode: TPdfDocumentPageMode read GetPageMode;
    // if UnsupportedFeatures is True, then the document has unsupported features. It is updated
    // after accessing a page.
    property UnsupportedFeatures: Boolean read FUnsupportedFeatures;
    property Handle: FPDF_DOCUMENT read FDocument;
    property FormHandle: FPDF_FORMHANDLE read FForm;
    property FormFieldHighlightColor: TColorRef read FFormFieldHighlightColor write SetFormFieldHighlightColor default $FFE4DD;
    property FormFieldHighlightAlpha: Integer read FFormFieldHighlightAlpha write SetFormFieldHighlightAlpha default 100;
    property PrintHidesFormFieldHighlight: Boolean read FPrintHidesFormFieldHighlight write FPrintHidesFormFieldHighlight default True;
    property FormModified: Boolean read FFormModified write FFormModified;
    property OnFormInvalidate: TPdfFormInvalidateEvent read FOnFormInvalidate write FOnFormInvalidate;
    property OnFormOutputSelectedRect: TPdfFormOutputSelectedRectEvent read FOnFormOutputSelectedRect write FOnFormOutputSelectedRect;
    property OnFormGetCurrentPage: TPdfFormGetCurrentPageEvent read FOnFormGetCurrentPage write FOnFormGetCurrentPage;
    property OnFormFieldFocus: TPdfFormFieldFocusEvent read FOnFormFieldFocus write FOnFormFieldFocus;
    property OnExecuteNamedAction: TPdfExecuteNamedActionEvent read FOnExecuteNamedAction write FOnExecuteNamedAction;
  end;
  {$IFDEF MSWINDOWS}
  TPdfDocumentPrinterStatusEvent = procedure(Sender: TObject; CurrentPageNum, PageCount: Integer) of object;
  TPdfDocumentPrinter = class(TObject)
  private
    FBeginPrintCounter: Integer;
    FPrinterDC: HDC;
    FPrintPortraitOrientation: Boolean;
    FPaperSize: TSize;
    FPrintArea: TSize;
    FMargins: TPoint;
    FFitPageToPrintArea: Boolean;
    FOnPrintStatus: TPdfDocumentPrinterStatusEvent;
    function IsPortraitOrientation(AWidth, AHeight: Integer): Boolean;
    procedure GetPrinterBounds;
  protected
    function PrinterStartDoc(const AJobTitle: string): Boolean; virtual; abstract;
    procedure PrinterEndDoc; virtual; abstract;
    procedure PrinterStartPage; virtual; abstract;
    procedure PrinterEndPage; virtual; abstract;
    function GetPrinterDC: HDC; virtual; abstract;
    procedure InternPrintPage(APage: TPdfPage; X, Y, Width, Height: Double);
  public
    constructor Create;
    { BeginPrint must be called before printing multiple documents.
      Returns false if the printer can't print. (e.g. The user aborted the PDF Printer's FileDialog) }
    function BeginPrint(const AJobTitle: string = ''): Boolean;
    { EndPrint must be called after printing multiple documents were printed. }
    procedure EndPrint;
    { Prints a range of PDF document pages (0..PageCount-1) }
    function Print(ADocument: TPdfDocument; AFromPageIndex, AToPageIndex: Integer): Boolean; overload;
    { Prints all pages of the PDF document. }
    function Print(ADocument: TPdfDocument): Boolean; overload;

    { If FitPageToPrintArea is true the page fill be scaled to fit into the printable area. }
    property FitPageToPrintArea: Boolean read FFitPageToPrintArea write FFitPageToPrintArea default True;
    { OnPrintStatus is triggered after every printed page }
    property OnPrintStatus: TPdfDocumentPrinterStatusEvent read FOnPrintStatus write FOnPrintStatus;
  end;
  {$ENDIF MSWINDOWS}
function SetThreadPdfUnsupportedFeatureHandler(const Handler: TPdfUnsupportedFeatureHandler): TPdfUnsupportedFeatureHandler;

function PageToBitmap(aPage: TPdfPage; sPath: String; nDPI: Integer = 600): Boolean;
function PageToPng(aPage: TPdfPage; sPath: String; nDPI: Integer = 600): Boolean;
function GetPageOcrText(aPage: TPdfPage; sOcrMdPath, sTaskDir: String; sParam: String = ''; nDPI: Integer = 150): String;

var
  PDFiumDllDir: string = '';
  PDFiumDllFileName: string = ''; // use this instead of PDFiumDllDir if you want to change the DLLs file name
  {$IF declared(FPDF_InitEmbeddedLibraries)}
  PDFiumResDir: string = '';
  {$IFEND}
implementation

uses
  Vcl.Graphics, Tocsg.Process, Tocsg.Strings, Vcl.Imaging.pngimage,
  Tocsg.Exception, System.Math;
resourcestring
  RsUnsupportedFeature = 'Function %s not supported';
  RsArgumentsOutOfRange = 'Function argument "%s" (%d) out of range';
  RsDocumentNotActive = 'PDF document is not open';
  {$IFNDEF CPUX64}
  RsFileTooLarge = 'PDF file "%s" is too large';
  {$ENDIF ~CPUX64}
  RsPdfCannotDeleteAttachmnent = 'Cannot delete the PDF attachment %d';
  RsPdfCannotAddAttachmnent = 'Cannot add the PDF attachment "%s"';
  RsPdfCannotSetAttachmentContent = 'Cannot set the PDF attachment content';
  RsPdfAttachmentContentNotSet = 'Content must be set before accessing string PDF attachmemt values';
  RsPdfAnnotationNotAFormFieldError = 'The annotation is not a form field';
  RsPdfAnnotationLinkRemoteGotoRequiresRemoteDocument = 'A remote goto annotation link requires a remote document';
  RsPdfErrorSuccess   = 'No error';
  RsPdfErrorUnknown   = 'Unknown error';
  RsPdfErrorFile      = 'File not found or can''t be opened';
  RsPdfErrorFormat    = 'File is not a PDF document or is corrupted';
  RsPdfErrorPassword  = 'Password required oder invalid password';
  RsPdfErrorSecurity  = 'Security schema is not support';
  RsPdfErrorPage      = 'Page does not exist or data error';
  RsPdfErrorXFALoad   = 'Load XFA error';
  RsPdfErrorXFALayout = 'Layout XFA error';
threadvar
  ThreadPdfUnsupportedFeatureHandler: TPdfUnsupportedFeatureHandler;
  UnsupportedFeatureCurrentDocument: TPdfDocument;
type
  { We don't want to use a TBytes temporary array if we can convert directly into the destination
    buffer. }
  TEncodingAccess = class(TEncoding)
  public
    function GetMemCharCount(Bytes: PByte; ByteCount: Integer): Integer;
    function GetMemChars(Bytes: PByte; ByteCount: Integer; Chars: PWideChar; CharCount: Integer): Integer;
  end;
function TEncodingAccess.GetMemCharCount(Bytes: PByte; ByteCount: Integer): Integer;
begin
  Result := GetCharCount(Bytes, ByteCount);
end;
function TEncodingAccess.GetMemChars(Bytes: PByte; ByteCount: Integer; Chars: PWideChar; CharCount: Integer): Integer;
begin
  Result := GetChars(Bytes, ByteCount, Chars, CharCount);
end;
function SetThreadPdfUnsupportedFeatureHandler(const Handler: TPdfUnsupportedFeatureHandler): TPdfUnsupportedFeatureHandler;
begin
  Result := ThreadPdfUnsupportedFeatureHandler;
  ThreadPdfUnsupportedFeatureHandler := Handler;
end;
{$IF defined(MSWINDOWS) and not declared(GetFileSizeEx)}
function GetFileSizeEx(hFile: THandle; var lpFileSize: Int64): BOOL; stdcall;
  external kernel32 name 'GetFileSizeEx';
{$IFEND}
procedure SwapInts(var X, Y: Integer);
var
  Tmp: Integer;
begin
  Tmp := X;
  X := Y;
  Y := Tmp;
end;
function GetUnsupportedFeatureName(nType: Integer): string;
begin
  case nType of
    FPDF_UNSP_DOC_XFAFORM:
      Result := 'XFA';
    FPDF_UNSP_DOC_PORTABLECOLLECTION:
      Result := 'Portfolios_Packages';
    FPDF_UNSP_DOC_ATTACHMENT,
    FPDF_UNSP_ANNOT_ATTACHMENT:
      Result := 'Attachment';
    FPDF_UNSP_DOC_SECURITY:
      Result := 'Rights_Management';
    FPDF_UNSP_DOC_SHAREDREVIEW:
      Result := 'Shared_Review';
    FPDF_UNSP_DOC_SHAREDFORM_ACROBAT,
    FPDF_UNSP_DOC_SHAREDFORM_FILESYSTEM,
    FPDF_UNSP_DOC_SHAREDFORM_EMAIL:
      Result := 'Shared_Form';
    FPDF_UNSP_ANNOT_3DANNOT:
      Result := '3D';
    FPDF_UNSP_ANNOT_MOVIE:
      Result := 'Movie';
    FPDF_UNSP_ANNOT_SOUND:
      Result := 'Sound';
    FPDF_UNSP_ANNOT_SCREEN_MEDIA,
    FPDF_UNSP_ANNOT_SCREEN_RICHMEDIA:
      Result := 'Screen';
    FPDF_UNSP_ANNOT_SIG:
      Result := 'Digital_Signature';
  else
    Result := 'Unknown';
  end;
end;
procedure UnsupportedHandler(pThis: PUNSUPPORT_INFO; nType: Integer); cdecl;
var
  Document: TPdfDocument;
begin
  Document := UnsupportedFeatureCurrentDocument;
  if Document <> nil then
    Document.FUnsupportedFeatures := True;
  if Assigned(ThreadPdfUnsupportedFeatureHandler) then
    ThreadPdfUnsupportedFeatureHandler(nType, GetUnsupportedFeatureName(nType));
  //raise EPdfUnsupportedFeatureException.CreateResFmt(@RsUnsupportedFeature, [GetUnsupportedFeatureName]);
end;
var
  PDFiumInitCritSect: TRTLCriticalSection;
  UnsupportInfo: TUnsupportInfo = (
    version: 1;
    FSDK_UnSupport_Handler: UnsupportedHandler;
  );
procedure InitLib;
{$J+}
const
  Initialized: Integer = 0;
{$J-}
begin
  if Initialized = 0 then
  begin
    EnterCriticalSection(PDFiumInitCritSect);
    try
      if Initialized = 0 then
      begin
        if PDFiumDllFileName <> '' then
          InitPDFiumEx(PDFiumDllFileName {$IF declared(FPDF_InitEmbeddedLibraries)}, PDFiumResDir{$IFEND})
        else
          InitPDFium(PDFiumDllDir {$IF declared(FPDF_InitEmbeddedLibraries)}, PDFiumResDir{$IFEND});
        FSDK_SetUnSpObjProcessHandler(@UnsupportInfo);
        Initialized := 1;
      end;
    finally
      LeaveCriticalSection(PDFiumInitCritSect);
    end;
  end;
end;
procedure RaiseLastPdfError;
begin
  case FPDF_GetLastError() of
    FPDF_ERR_SUCCESS:
      raise EPdfException.CreateRes(@RsPdfErrorSuccess);
    FPDF_ERR_FILE:
      raise EPdfException.CreateRes(@RsPdfErrorFile);
    FPDF_ERR_FORMAT:
      raise EPdfException.CreateRes(@RsPdfErrorFormat);
    FPDF_ERR_PASSWORD:
      raise EPdfException.CreateRes(@RsPdfErrorPassword);
    FPDF_ERR_SECURITY:
      raise EPdfException.CreateRes(@RsPdfErrorSecurity);
    FPDF_ERR_PAGE:
      raise EPdfException.CreateRes(@RsPdfErrorPage);
    {$IF declared(FPDF_ERR_XFALOAD)}
    FPDF_ERR_XFALOAD:
      raise EPdfException.CreateRes(@RsPdfErrorXFALoad);
    FPDF_ERR_XFALAYOUT:
      raise EPdfException.CreateRes(@RsPdfErrorXFALayout);
    {$IFEND}
  else
    raise EPdfException.CreateRes(@RsPdfErrorUnknown);
  end;
end;
procedure FFI_Invalidate(pThis: PFPDF_FORMFILLINFO; page: FPDF_PAGE; left, top, right, bottom: Double); cdecl;
var
  Handler: PPdfFormFillHandler;
  Pg: TPdfPage;
  R: TPdfRect;
begin
  Handler := PPdfFormFillHandler(pThis);
  if Assigned(Handler.Document.OnFormInvalidate) then
  begin
    Pg := Handler.Document.FindPage(page);
    if Pg <> nil then
    begin
      R.Left := left;
      R.Top := top;
      R.Right := right;
      R.Bottom := bottom;
      Handler.Document.OnFormInvalidate(Handler.Document, Pg, R);
    end;
  end;
end;
procedure FFI_Change(pThis: PFPDF_FORMFILLINFO); cdecl;
var
  Handler: PPdfFormFillHandler;
begin
  Handler := PPdfFormFillHandler(pThis);
  Handler.Document.FormModified := True;
end;
procedure FFI_OutputSelectedRect(pThis: PFPDF_FORMFILLINFO; page: FPDF_PAGE; left, top, right, bottom: Double); cdecl;
var
  Handler: PPdfFormFillHandler;
  Pg: TPdfPage;
  R: TPdfRect;
begin
  Handler := PPdfFormFillHandler(pThis);
  if Assigned(Handler.Document.OnFormOutputSelectedRect) then
  begin
    Pg := Handler.Document.FindPage(Page);
    if Pg <> nil then
    begin
      R.Left := left;
      R.Top := top;
      R.Right := right;
      R.Bottom := bottom;
      Handler.Document.OnFormOutputSelectedRect(Handler.Document, Pg, R);
    end;
  end;
end;
{$IFDEF MSWINDOWS}
type
  TFFITimer = record
    Id: UINT;
    Proc: TFPDFTimerCallback;
  end;
var
  FFITimers: array of TFFITimer;
  FFITimersCritSect: TRTLCriticalSection;
procedure FormTimerProc(hwnd: HWND; uMsg: UINT; timerId: UINT; dwTime: DWORD); stdcall;
var
  I: Integer;
  Proc: TFPDFTimerCallback;
begin
  Proc := nil;
  EnterCriticalSection(FFITimersCritSect);
  try
    for I := 0 to Length(FFITimers) - 1 do
    begin
      if FFITimers[I].Id = timerId then
      begin
        Proc := FFITimers[I].Proc;
        Break;
      end;
    end;
  finally
    LeaveCriticalSection(FFITimersCritSect);
  end;
  if Assigned(Proc) then
    Proc(timerId);
end;
function FFI_SetTimer(pThis: PFPDF_FORMFILLINFO; uElapse: Integer; lpTimerFunc: TFPDFTimerCallback): Integer; cdecl;
var
  I: Integer;
  Id: UINT;
begin
  Id := SetTimer(0, 0, uElapse, @FormTimerProc);
  Result := Integer(Id);
  if Id <> 0 then
  begin
    EnterCriticalSection(FFITimersCritSect);
    try
      for I := 0 to Length(FFITimers) - 1 do
      begin
        if FFITimers[I].Id = 0 then
        begin
          FFITimers[I].Id := Id;
          FFITimers[I].Proc := lpTimerFunc;
          Exit;
        end;
      end;
      I := Length(FFITimers);
      SetLength(FFITimers, I + 1);
      FFITimers[I].Id := Id;
      FFITimers[I].Proc := lpTimerFunc;
    finally
      LeaveCriticalSection(FFITimersCritSect);
    end;
  end;
end;
procedure FFI_KillTimer(pThis: PFPDF_FORMFILLINFO; nTimerID: Integer); cdecl;
var
  I: Integer;
begin
  if nTimerID <> 0 then
  begin
    KillTimer(0, nTimerID);
    EnterCriticalSection(FFITimersCritSect);
    try
      for I := 0 to Length(FFITimers) - 1 do
      begin
        if FFITimers[I].Id = UINT(nTimerID) then
        begin
          FFITimers[I].Id := 0;
          FFITimers[I].Proc := nil;
        end;
      end;
      I := Length(FFITimers) - 1;
      while (I >= 0) and (FFITimers[I].Id = 0) do
        Dec(I);
      if Length(FFITimers) <> I + 1 then
        SetLength(FFITimers, I + 1);
    finally
      LeaveCriticalSection(FFITimersCritSect);
    end;
  end;
end;
{$ELSE}
type
  TFFITimer = class(TTimer)
  public
    FId: Integer;
    FTimerFunc: TFPDFTimerCallback;
    procedure DoTimerEvent(Sender: TObject);
  end;
var
  FFITimers: array of TFFITimer;
  FFITimersCritSect: TRTLCriticalSection;
{ TFFITimer }
procedure TFFITimer.DoTimerEvent(Sender: TObject);
begin
  FTimerFunc(FId);
end;
function FFI_SetTimer(pThis: PFPDF_FORMFILLINFO; uElapse: Integer; lpTimerFunc: TFPDFTimerCallback): Integer; cdecl;
var
  I: Integer;
  Id: Integer;
  Timer: TFFITimer;
begin
  // Find highest Id
  EnterCriticalSection(FFITimersCritSect);
  try
    Id := 0;
    for I := 0 to Length(FFITimers) - 1 do
      if (FFITimers[I] <> nil) and (FFITimers[I].FId > Id) then
        Id := FFITimers[I].FId;
    Inc(Id);
    Timer := TFFITimer.Create(nil);
    Timer.FId := Id;
    Timer.FTimerFunc:= lpTimerFunc;
    Timer.OnTimer := Timer.DoTimerEvent;
    Timer.Interval := uElapse;
    Result := Id;
    for I := 0 to Length(FFITimers) - 1 do
    begin
      if FFITimers[I] = nil then
      begin
        FFITimers[I] := Timer;
        Exit;
      end;
    end;
    I := Length(FFITimers);
    SetLength(FFITimers, I + 1);
    FFITimers[I] := Timer;
  finally
    LeaveCriticalSection(FFITimersCritSect);
  end;
end;
procedure FFI_KillTimer(pThis: PFPDF_FORMFILLINFO; nTimerID: Integer); cdecl;
var
  I: Integer;
begin
  if nTimerID <> 0 then
  begin
    EnterCriticalSection(FFITimersCritSect);
    try
      for I := 0 to Length(FFITimers) - 1 do
        if (FFITimers[I] <> nil) and (FFITimers[I].FId = nTimerID) then
          FreeAndNil(FFITimers[I]);
      I := Length(FFITimers) - 1;
      while (I >= 0) and (FFITimers[I] = nil) do
        Dec(I);
      if Length(FFITimers) <> I + 1 then
        SetLength(FFITimers, I + 1);
    finally
      LeaveCriticalSection(FFITimersCritSect);
    end;
  end;
end;
{$ENDIF MSWINDOWS}
function FFI_GetLocalTime(pThis: PFPDF_FORMFILLINFO): FPDF_SYSTEMTIME; cdecl;
{$IF not declared(PSystemTime)}
type
  PSystemTime = ^TSystemTime;
{$IFEND}
begin
  GetLocalTime(PSystemTime(@Result)^);
end;
function FFI_GetPage(pThis: PFPDF_FORMFILLINFO; document: FPDF_DOCUMENT; nPageIndex: Integer): FPDF_PAGE; cdecl;
var
  Handler: PPdfFormFillHandler;
begin
  Handler := PPdfFormFillHandler(pThis);
  Result := nil;
  if (Handler.Document <> nil) and (Handler.Document.FDocument = document) then
  begin
    if (nPageIndex >= 0) and (nPageIndex < Handler.Document.PageCount) then
      Result := Handler.Document.Pages[nPageIndex].FPage;
  end;
end;
function FFI_GetCurrentPage(pThis: PFPDF_FORMFILLINFO; document: FPDF_DOCUMENT): FPDF_PAGE; cdecl;
var
  Handler: PPdfFormFillHandler;
  Pg: TPdfPage;
begin
  Handler := PPdfFormFillHandler(pThis);
  Result := nil;
  if (Handler.Document <> nil) and (Handler.Document.FDocument = document) and Assigned(Handler.Document.OnFormGetCurrentPage) then
  begin
    Pg := nil;
    Handler.Document.OnFormGetCurrentPage(Handler.Document, Pg);
    Result := nil;
    if Pg <> nil then
      Result := Pg.FPage;
  end;
end;
function FFI_GetRotation(pThis: PFPDF_FORMFILLINFO; page: FPDF_PAGE): Integer; cdecl;
begin
  Result := 0;
end;
procedure FFI_ExecuteNamedAction(pThis: PFPDF_FORMFILLINFO; namedAction: FPDF_BYTESTRING); cdecl;
var
  Handler: PPdfFormFillHandler;
  NamedActionType: TPdfNamedActionType;
  S: UTF8String;
begin
  Handler := PPdfFormFillHandler(pThis);
  if Assigned(Handler.Document.OnExecuteNamedAction) then
  begin
    S := namedAction;
    if S = 'Print' then
      NamedActionType := naPrint
    else if S = 'NextPage' then
      NamedActionType := naNextPage
    else if S = 'PrevPage' then
      NamedActionType := naPrevPage
    else if S = 'FirstPage' then
      NamedActionType := naFirstPage
    else if S = 'LastPage' then
      NamedActionType := naLastPage
    else
      Exit;
    Handler.Document.OnExecuteNamedAction(Handler.Document, NamedActionType);
  end;
end;
procedure FFI_SetCursor(pThis: PFPDF_FORMFILLINFO; nCursorType: Integer); cdecl;
begin
  // A better solution is to check what form field type is under the mouse cursor in the
  // MoveMove event. Chrome/Edge doesn't rely on SetCursor either.
end;
procedure FFI_SetTextFieldFocus(pThis: PFPDF_FORMFILLINFO; value: FPDF_WIDESTRING; valueLen: FPDF_DWORD; is_focus: FPDF_BOOL); cdecl;
var
  Handler: PPdfFormFillHandler;
begin
  Handler := PPdfFormFillHandler(pThis);
  if (Handler.Document <> nil) and Assigned(Handler.Document.OnFormFieldFocus) then
    Handler.Document.OnFormFieldFocus(Handler.Document, value, valueLen, is_focus <> 0);
end;
procedure FFI_FocusChange(param: PFPDF_FORMFILLINFO; annot: FPDF_ANNOTATION; page_index: Integer); cdecl;
begin
end;

{ TPdfRect }
procedure TPdfRect.Offset(XOffset, YOffset: Double);
begin
  Left := Left + XOffset;
  Top := Top + YOffset;
  Right := Right + XOffset;
  Bottom := Bottom + YOffset;
end;
class function TPdfRect.Empty: TPdfRect;
begin
  Result.Left := 0;
  Result.Top := 0;
  Result.Right := 0;
  Result.Bottom := 0;
end;
function TPdfRect.GetHeight: Double;
begin
  Result := Bottom - Top;
end;
function TPdfRect.GetWidth: Double;
begin
  Result := Right - Left;
end;
procedure TPdfRect.SetHeight(const Value: Double);
begin
  Bottom := Top + Value;
end;
procedure TPdfRect.SetWidth(const Value: Double);
begin
  Right := Left + Value;
end;
class function TPdfRect.New(Left, Top, Right, Bottom: Double): TPdfRect;
begin
  Result.Left := Left;
  Result.Top := Top;
  Result.Right := Right;
  Result.Bottom := Bottom;
end;
function TPdfRect.PtIn(const Pt: TPdfPoint): Boolean;
begin
  Result := (Pt.X >= Left) and (Pt.X < Right);
  if Result then
  begin
    // Page coordinates are upside down.
    if Top > Bottom then
      Result := (Pt.Y >= Bottom) and (Pt.Y < Top)
    else
      Result := (Pt.Y >= Top) and (Pt.Y < Bottom)
  end;
end;

{ TPdfDocument }
constructor TPdfDocument.Create;
begin
  inherited Create;
  FPages := TObjectList.Create;
  FAttachments := TPdfAttachmentList.Create(Self);
  {$IFDEF MSWINDOWS}
  FFileHandle := INVALID_HANDLE_VALUE;
  {$ENDIF MSWINDOWS}
  FFormFieldHighlightColor := $FFE4DD;
  FFormFieldHighlightAlpha := 100;
  FPrintHidesFormFieldHighlight := True;
  InitLib;
end;
destructor TPdfDocument.Destroy;
begin
  Close;
  FAttachments.Free;
  FPages.Free;
  inherited Destroy;
end;
procedure TPdfDocument.Close;
begin
  FClosing := True;
  try
    FPages.Clear;
    FUnsupportedFeatures := False;
    if FDocument <> nil then
    begin
      if FForm <> nil then
      begin
        FORM_DoDocumentAAction(FForm, FPDFDOC_AACTION_WC);
        FPDFDOC_ExitFormFillEnvironment(FForm);
        FForm := nil;
      end;
      FPDF_CloseDocument(FDocument);
      FDocument := nil;
    end;
    if FCustomLoadData <> nil then
    begin
      Dispose(FCustomLoadData);
      FCustomLoadData := nil;
    end;
    {$IFDEF MSWINDOWS}
    if FFileMapping <> 0 then
    begin
      if FBuffer <> nil then
      begin
        UnmapViewOfFile(FBuffer);
        FBuffer := nil;
      end;
      CloseHandle(FFileMapping);
      FFileMapping := 0;
    end
    else
    {$ENDIF MSWINDOWS}
    if FBuffer <> nil then
    begin
      FreeMem(FBuffer);
      FBuffer := nil;
    end;
    FBytes := nil;
    {$IFDEF MSWINDOWS}
    if FFileHandle <> INVALID_HANDLE_VALUE then
    begin
      CloseHandle(FFileHandle);
      FFileHandle := INVALID_HANDLE_VALUE;
    end;
    {$ELSE}
    FreeAndNil(FFileStream);
    {$ENDIF MSWINDOWS}
    FFileName := '';
    FFormModified := False;
  finally
    FClosing := False;
  end;
end;
{$IFDEF MSWINDOWS}
function ReadFromActiveFileHandle(Param: Pointer; Position: LongWord; Buffer: PByte; Size: LongWord): Boolean;
var
  NumRead: DWORD;
begin
  if Buffer <> nil then
  begin
    SetFilePointer(THandle(Param), Position, nil, FILE_BEGIN);
    Result := ReadFile(THandle(Param), Buffer^, Size, NumRead, nil) and (NumRead = Size);
  end
  else
    Result := Size = 0;
end;
{$ENDIF MSWINDOWS}
procedure TPdfDocument.LoadFromFile(const FileName: string; const Password: UTF8String; LoadOption: TPdfDocumentLoadOption);
{$IFDEF MSWINDOWS}
var
  Size: Int64;
  Offset: NativeInt;
  NumRead: DWORD;
  LastError: DWORD;
{$ENDIF MSWINDOWS}
begin
  Close;
  if LoadOption = dloDefault then
  begin
    InternLoadFromFile(FileName, Password);
    FFileName := FileName;
    Exit;
  end;
  {$IFDEF MSWINDOWS}
  FFileHandle := CreateFileW(PWideChar(FileName), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  if FFileHandle = INVALID_HANDLE_VALUE then
    RaiseLastOSError;
  try
    if not GetFileSizeEx(FFileHandle, Size) then
      RaiseLastOSError;
    if Size > High(Integer) then // PDFium LoadCustomDocument() can only handle PDFs up to 2 GB (see FPDF_FILEACCESS)
    begin
      {$IFDEF CPUX64}
      // FPDF_LoadCustomDocument wasn't updated to load larger files, so we fall back to MMF.
      if LoadOption = dloOnDemand then
        LoadOption := dloMMF;
      {$ELSE}
      raise EPdfException.CreateResFmt(@RsFileTooLarge, [ExtractFileName(FileName)]);
      {$ENDIF CPUX64}
    end;
    case LoadOption of
      dloMemory:
        begin
          if Size > 0 then
          begin
            try
              GetMem(FBuffer, Size);
              Offset := 0;
              while Offset < Size do
              begin
                if ((Size - Offset) and not $FFFFFFFF) <> 0 then
                  NumRead := $40000000
                else
                  NumRead := Size - Offset;
                if not ReadFile(FFileHandle, FBuffer[Offset], NumRead, NumRead, nil) then
                begin
                  LastError := GetLastError;
                  FreeMem(FBuffer);
                  FBuffer := nil;
                  RaiseLastOSError(LastError);
                end;
                Inc(Offset, NumRead);
              end;
            finally
              CloseHandle(FFileHandle);
              FFileHandle := INVALID_HANDLE_VALUE;
            end;
            InternLoadFromMem(FBuffer, Size, Password);
          end;
        end;
      dloMMF:
        begin
          FFileMapping := CreateFileMapping(FFileHandle, nil, PAGE_READONLY, 0, 0, nil);
          if FFileMapping = 0 then
            RaiseLastOSError;
          FBuffer := MapViewOfFile(FFileMapping, FILE_MAP_READ, 0, 0, Size);
          if FBuffer = nil then
            RaiseLastOSError;
          InternLoadFromMem(FBuffer, Size, Password);
        end;
      dloOnDemand:
        InternLoadFromCustom(ReadFromActiveFileHandle, Size, Pointer(FFileHandle), Password);
    end;
  except
    Close;
    raise;
  end;
  {$ELSE}
  FFileStream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    case LoadOption of
      dloMemory, dloMMF:
        begin
          try
            LoadFromStream(FFileStream, Password);
          finally
            FreeAndNil(FFileStream);
          end;
        end;
      dloOnDemand:
        LoadFromActiveStream(FFileStream, Password);
    end;
  except
    FreeAndNil(FFileStream);
    raise;
  end;
  {$ENDIF MSWINDOWS}
  FFileName := FileName;
end;
procedure TPdfDocument.LoadFromStream(Stream: TStream; const Password: UTF8String);
var
  Size: NativeInt;
begin
  Close;
  Size := Stream.Size;
  if Size > 0 then
  begin
    GetMem(FBuffer, Size);
    try
      Stream.ReadBuffer(FBuffer^, Size);
      InternLoadFromMem(FBuffer, Size, Password);
    except
      Close;
      raise;
    end;
  end;
end;
procedure TPdfDocument.LoadFromActiveBuffer(Buffer: Pointer; Size: NativeInt; const Password: UTF8String);
begin
  Close;
  InternLoadFromMem(Buffer, Size, Password);
end;
procedure TPdfDocument.LoadFromBytes(const Bytes: TBytes; const Password: UTF8String);
begin
  LoadFromBytes(Bytes, 0, Length(Bytes), Password);
end;
procedure TPdfDocument.LoadFromBytes(const Bytes: TBytes; Index, Count: NativeInt;
  const Password: UTF8String);
var
  Len: NativeInt;
begin
  Close;
  Len := Length(Bytes);
  if Index >= Len then
    raise EPdfArgumentOutOfRange.CreateResFmt(@RsArgumentsOutOfRange, ['Index', Index]);
  if Index + Count > Len then
    raise EPdfArgumentOutOfRange.CreateResFmt(@RsArgumentsOutOfRange, ['Count', Count]);
  FBytes := Bytes; // keep alive after return
  InternLoadFromMem(@Bytes[Index], Count, Password);
end;
function ReadFromActiveStream(Param: Pointer; Position: LongWord; Buffer: PByte; Size: LongWord): Boolean;
begin
  if Buffer <> nil then
  begin
    TStream(Param).Seek(Position, TSeekOrigin.soBeginning);
    Result := TStream(Param).Read(Buffer^, Size) = Integer(Size);
  end
  else
    Result := Size = 0;
end;
procedure TPdfDocument.LoadFromActiveStream(Stream: TStream; const Password: UTF8String);
begin
  if Stream = nil then
    Close
  else
    LoadFromCustom(ReadFromActiveStream, Stream.Size, Stream, Password);
end;
procedure TPdfDocument.LoadFromCustom(ReadFunc: TPdfDocumentCustomReadProc; Size: LongWord;
  Param: Pointer; const Password: UTF8String);
begin
  Close;
  InternLoadFromCustom(ReadFunc, Size, Param, Password);
end;
function GetLoadFromCustomBlock(Param: Pointer; Position: LongWord; Buffer: PByte; Size: LongWord): Integer; cdecl;
var
  Data: TPdfDocument.PCustomLoadDataRec;
begin
  Data := TPdfDocument(param).FCustomLoadData;
  Result := Ord(Data.GetBlock(Data.Param, Position, Buffer, Size));
end;
procedure TPdfDocument.InternLoadFromCustom(ReadFunc: TPdfDocumentCustomReadProc; Size: LongWord;
  Param: Pointer; const Password: UTF8String);
var
  OldCurDoc: TPdfDocument;
begin
  if Assigned(ReadFunc) then
  begin
    New(FCustomLoadData);
    FCustomLoadData.Param := Param;
    FCustomLoadData.GetBlock := ReadFunc;
    FCustomLoadData.FileAccess.m_FileLen := Size;
    FCustomLoadData.FileAccess.m_GetBlock := GetLoadFromCustomBlock;
    FCustomLoadData.FileAccess.m_Param := Self;
    OldCurDoc := UnsupportedFeatureCurrentDocument;
    try
      UnsupportedFeatureCurrentDocument := Self;
      FDocument := FPDF_LoadCustomDocument(@FCustomLoadData.FileAccess, PAnsiChar(Pointer(Password)));
    finally
      UnsupportedFeatureCurrentDocument := OldCurDoc;
    end;
    DocumentLoaded;
  end;
end;
procedure TPdfDocument.InternLoadFromMem(Buffer: PByte; Size: NativeInt; const Password: UTF8String);
var
  OldCurDoc: TPdfDocument;
begin
  if Size > 0 then
  begin
    OldCurDoc := UnsupportedFeatureCurrentDocument;
    try
      UnsupportedFeatureCurrentDocument := Self;
      FDocument := FPDF_LoadMemDocument64(Buffer, Size, PAnsiChar(Pointer(Password)));
    finally
      UnsupportedFeatureCurrentDocument := OldCurDoc;
    end;
    DocumentLoaded;
  end;
end;
procedure TPdfDocument.InternLoadFromFile(const FileName: string; const Password: UTF8String);
var
  OldCurDoc: TPdfDocument;
  Utf8FileName: UTF8String;
begin
  Utf8FileName := UTF8Encode(FileName);
  OldCurDoc := UnsupportedFeatureCurrentDocument;
  try
    UnsupportedFeatureCurrentDocument := Self;
    // UTF8 now works with LoadDocument and it can handle large PDF files (2 GB+) what
    // FPDF_LoadCustomDocument can't because of the data types in FPDF_FILEACCESS.
    FDocument := FPDF_LoadDocument(PAnsiChar(Utf8FileName), PAnsiChar(Pointer(Password)));
  finally
    UnsupportedFeatureCurrentDocument := OldCurDoc;
  end;
  DocumentLoaded;
end;
procedure TPdfDocument.DocumentLoaded;
begin
  FFormModified := False;
  if FDocument = nil then
    RaiseLastPdfError;
  FPages.Count := FPDF_GetPageCount(FDocument);
  FillChar(FFormFillHandler, SizeOf(TPdfFormFillHandler), 0);
  FFormFillHandler.Document := Self;
  FFormFillHandler.FormFillInfo.version := 1; // will be set to 2 if we use an XFA-enabled DLL
  FFormFillHandler.FormFillInfo.FFI_Invalidate := FFI_Invalidate;
  FFormFillHandler.FormFillInfo.FFI_OnChange := FFI_Change;
  FFormFillHandler.FormFillInfo.FFI_OutputSelectedRect := FFI_OutputSelectedRect;
  FFormFillHandler.FormFillInfo.FFI_SetTimer := FFI_SetTimer;
  FFormFillHandler.FormFillInfo.FFI_KillTimer := FFI_KillTimer;
  FFormFillHandler.FormFillInfo.FFI_GetLocalTime := FFI_GetLocalTime;
  FFormFillHandler.FormFillInfo.FFI_GetPage := FFI_GetPage;
  FFormFillHandler.FormFillInfo.FFI_GetCurrentPage := FFI_GetCurrentPage;
  FFormFillHandler.FormFillInfo.FFI_GetRotation := FFI_GetRotation;
  FFormFillHandler.FormFillInfo.FFI_ExecuteNamedAction := FFI_ExecuteNamedAction;
  FFormFillHandler.FormFillInfo.FFI_SetCursor := FFI_SetCursor;
  FFormFillHandler.FormFillInfo.FFI_SetTextFieldFocus := FFI_SetTextFieldFocus;
  FFormFillHandler.FormFillInfo.FFI_OnFocusChange := FFI_FocusChange;
//  FFormFillHandler.FormFillInfo.FFI_DoURIAction := FFI_DoURIAction;
//  FFormFillHandler.FormFillInfo.FFI_DoGoToAction := FFI_DoGoToAction;
  if PDF_USE_XFA then
  begin
    FJSPlatform.version := 3;
    // FJSPlatform callbacks not implemented
    FFormFillHandler.FormFillInfo.m_pJsPlatform := @FJSPlatform;
    FFormFillHandler.FormFillInfo.version := 2;
    FFormFillHandler.FormFillInfo.xfa_disabled := 1; // Disable XFA support for now
  end;
  FForm := FPDFDOC_InitFormFillEnvironment(FDocument, @FFormFillHandler.FormFillInfo);
  if FForm <> nil then
  begin
    if PDF_USE_XFA and (FFormFillHandler.FormFillInfo.xfa_disabled = 0) then
      FPDF_LoadXFA(FDocument);
    UpdateFormFieldHighlight;
    FORM_DoDocumentJSAction(FForm);
    FORM_DoDocumentOpenAction(FForm);
  end;
end;
procedure TPdfDocument.UpdateFormFieldHighlight;
begin
  FPDF_SetFormFieldHighlightColor(FForm, 0, FFormFieldHighlightColor);
  FPDF_SetFormFieldHighlightAlpha(FForm, FFormFieldHighlightAlpha);
end;
function TPdfDocument.IsPageLoaded(PageIndex: Integer): Boolean;
var
  Page: TPdfPage;
begin
  Page := TPdfPage(FPages[PageIndex]);
  Result := (Page <> nil) and Page.IsLoaded;
end;
function TPdfDocument.GetPage(Index: Integer): TPdfPage;
var
  LPage: FPDF_PAGE;
begin
  Result := TPdfPage(FPages[Index]);
  if Result = nil then
  begin
    LPage := FPDF_LoadPage(FDocument, Index);
    if LPage = nil then
      RaiseLastPdfError;
    Result := TPdfPage.Create(Self, LPage);
    FPages[Index] := Result;
  end
end;
function TPdfDocument.GetPageCount: Integer;
begin
  Result := FPages.Count;
end;
procedure TPdfDocument.ExtractPage(APage: TPdfPage);
begin
  if not FClosing then
    FPages.Extract(APage);
end;
function TPdfDocument.ReloadPage(APage: TPdfPage): FPDF_PAGE;
var
  Index: Integer;
begin
  CheckActive;
  Index := FPages.IndexOf(APage);
  Result := FPDF_LoadPage(FDocument, Index);
  if Result = nil then
    RaiseLastPdfError;
end;
function TPdfDocument.GetPrintScaling: Boolean;
begin
  CheckActive;
  Result := FPDF_VIEWERREF_GetPrintScaling(FDocument) <> 0;
end;
function TPdfDocument.GetActive: Boolean;
begin
  Result := FDocument <> nil;
end;
procedure TPdfDocument.CheckActive;
begin
  if not Active then
    raise EPdfException.CreateRes(@RsDocumentNotActive);
end;
class function TPdfDocument.CreateNPagesOnOnePageDocument(Source: TPdfDocument;
  NumPagesXAxis, NumPagesYAxis: Integer): TPdfDocument;
begin
  if Source.PageCount > 0 then
    Result := CreateNPagesOnOnePageDocument(Source, Source.PageSizes[0].X, Source.PageSizes[0].Y, NumPagesXAxis, NumPagesYAxis)
  else
    Result := CreateNPagesOnOnePageDocument(Source, PdfDefaultPageWidth, PdfDefaultPageHeight, NumPagesXAxis, NumPagesYAxis); // DIN A4 page
end;
class function TPdfDocument.CreateNPagesOnOnePageDocument(Source: TPdfDocument;
  NewPageWidth, NewPageHeight: Double; NumPagesXAxis, NumPagesYAxis: Integer): TPdfDocument;
var
  OldCurDoc: TPdfDocument;
begin
  Result := TPdfDocument.Create;
  try
    if (Source = nil) or not Source.Active then
      Result.NewDocument
    else
    begin
      OldCurDoc := UnsupportedFeatureCurrentDocument;
      try
        UnsupportedFeatureCurrentDocument := Result;
        Result.FDocument := FPDF_ImportNPagesToOne(Source.FDocument, NewPageWidth, NewPageHeight, NumPagesXAxis, NumPagesYAxis);
      finally
        UnsupportedFeatureCurrentDocument := OldCurDoc;
      end;
      if Result.FDocument <> nil then
        Result.DocumentLoaded
      else
        Result.NewDocument;
    end;
  except
    Result.Free;
    raise;
  end;
end;
function TPdfDocument.InternImportPages(Source: TPdfDocument; PageIndices: PInteger; PageIndicesCount: Integer;
  const Range: AnsiString; Index: Integer; ImportByRange: Boolean): Boolean;
var
  I, NewCount, OldCount, InsertCount: Integer;
begin
  CheckActive;
  Source.CheckActive;
  OldCount := FPDF_GetPageCount(FDocument);
  if Index < 0 then
    Index := OldCount;
  if ImportByRange then // Range = '' => Import all pages
    Result := FPDF_ImportPages(FDocument, Source.FDocument, PAnsiChar(Pointer(Range)), Index) <> 0
  else
    Result := FPDF_ImportPagesByIndex(FDocument, Source.FDocument, PageIndices, PageIndicesCount, Index) <> 0;
  NewCount := FPDF_GetPageCount(FDocument);
  InsertCount := NewCount - OldCount;
  if InsertCount > 0 then
  begin
    FPages.Count := NewCount;
    if Index < OldCount then
    begin
      Move(FPages.List[Index], FPages.List[Index + InsertCount], (OldCount - Index) * SizeOf(TObject));
      for I := Index to Index + InsertCount - 1 do
        FPages.List[Index] := nil;
    end;
  end;
end;
function TPdfDocument.ImportAllPages(Source: TPdfDocument; Index: Integer): Boolean;
begin
  Result := InternImportPages(Source, nil, 0, '', Index, False);
end;
function TPdfDocument.ImportPages(Source: TPdfDocument; const Range: string; Index: Integer): Boolean;
begin
  Result := InternImportPages(Source, nil, 0, AnsiString(Range), Index, True)
end;
function TPdfDocument.ImportPageRange(Source: TPdfDocument; PageIndex, Count, Index: Integer): Boolean;
begin
  Result := False;
  if (Source <> nil) and (PageIndex >= 0) then
  begin
    if Count = -1 then
      Count := Source.PageCount - PageIndex
    else if Count < 0 then
      Exit;
    if Count > 0 then
    begin
      if PageIndex + Count > Source.PageCount then
      begin
        Count := Source.PageCount - PageIndex;
        if Count = 0 then
          Exit;
      end;
      if (PageIndex = 0) and (Count = Source.PageCount) then
        Result := ImportAllPages(Source, Index)
      else
        Result := ImportPages(Source, Format('%d-%d', [PageIndex, PageIndex + Count - 1]));
    end;
  end;
end;
function TPdfDocument.ImportPagesByIndex(Source: TPdfDocument; const PageIndices: array of Integer; Index: Integer = -1): Boolean;
begin
  if Length(PageIndices) > 0 then
    Result := InternImportPages(Source, @PageIndices[0], Length(PageIndices), '', Index, False)
  else
    Result := ImportAllPages(Source, Index);
end;
procedure TPdfDocument.SaveToFile(const AFileName: string; Option: TPdfDocumentSaveOption; FileVersion: Integer);
var
  Stream: TFileStream;
begin
  Stream := TFileStream.Create(AFileName, fmCreate or fmShareDenyWrite);
  try
    SaveToStream(Stream, Option, FileVersion);
  finally
    Stream.Free;
  end;
end;
type
  PFPDFFileWriteEx = ^TFPDFFileWriteEx;
  TFPDFFileWriteEx = record
    Inner: TFPDFFileWrite; // emulate object inheritance
    Stream: TStream;
  end;
function WriteBlockToStream(pThis: PFPDF_FILEWRITE; pData: Pointer; size: LongWord): Integer; cdecl;
begin
  Result := Ord(LongWord(PFPDFFileWriteEx(pThis).Stream.Write(pData^, size)) = size);
end;
procedure TPdfDocument.SaveToStream(Stream: TStream; Option: TPdfDocumentSaveOption; FileVersion: Integer);
var
  FileWriteInfo: TFPDFFileWriteEx;
begin
  CheckActive;
  FileWriteInfo.Inner.version := 1;
  FileWriteInfo.Inner.WriteBlock := @WriteBlockToStream;
  FileWriteInfo.Stream := Stream;
  if FForm <> nil then
  begin
    FORM_ForceToKillFocus(FForm); // also save the form field data that is currently focused
    FORM_DoDocumentAAction(FForm, FPDFDOC_AACTION_WS); // BeforeSave
  end;
  if FileVersion <> -1 then
    FPDF_SaveWithVersion(FDocument, @FileWriteInfo, Ord(Option), FileVersion)
  else
    FPDF_SaveAsCopy(FDocument, @FileWriteInfo, Ord(Option));
  if FForm <> nil then
    FORM_DoDocumentAAction(FForm, FPDFDOC_AACTION_DS); // AfterSave
end;
procedure TPdfDocument.SaveToBytes(var Bytes: TBytes; Option: TPdfDocumentSaveOption; FileVersion: Integer);
var
  Stream: TBytesStream;
  Size: NativeInt;
begin
  CheckActive;
  Stream := TBytesStream.Create(nil);
  try
    SaveToStream(Stream, Option, FileVersion);
    Size := Stream.Size;
    Bytes := Stream.Bytes;
  finally
    Stream.Free;
  end;
  // Trim the byte array from the stream's capacity to the actual size
  if Length(Bytes) <> Size then
    SetLength(Bytes, Size);
end;
function TPdfDocument.NewDocument: Boolean;
begin
  Close;
  FDocument := FPDF_CreateNewDocument;
  Result := FDocument <> nil;
  FFormModified := False;
end;
procedure TPdfDocument.DeletePage(Index: Integer);
begin
  CheckActive;
  FPages.Delete(Index);
  FPDFPage_Delete(FDocument, Index);
end;
function TPdfDocument.NewPage(Width, Height: Double; Index: Integer): TPdfPage;
var
  LPage: FPDF_PAGE;
begin
  CheckActive;
  if Index < 0 then
    Index := FPages.Count; // append new page
  LPage := FPDFPage_New(FDocument, Index, Width, Height);
  if LPage <> nil then
  begin
    Result := TPdfPage.Create(Self, LPage);
    FPages.Insert(Index, Result);
  end
  else
    Result := nil;
end;
function TPdfDocument.NewPage(Index: Integer = -1): TPdfPage;
begin
  Result := NewPage(PdfDefaultPageWidth, PdfDefaultPageHeight, Index);
end;
function TPdfDocument.ApplyViewerPreferences(Source: TPdfDocument): Boolean;
begin
  CheckActive;
  Source.CheckActive;
  Result := FPDF_CopyViewerPreferences(FDocument, Source.FDocument) <> 0;
end;
function TPdfDocument.GetFileIdentifier(IdType: TPdfFileIdType): string;
var
  Len: Integer;
  A: AnsiString;
begin
  CheckActive;
  Len := FPDF_GetFileIdentifier(FDocument, FPDF_FILEIDTYPE(IdType), nil, 0) div SizeOf(AnsiChar) - 1;
  if Len > 0 then
  begin
    SetLength(A, Len);
    FPDF_GetFileIdentifier(FDocument, FPDF_FILEIDTYPE(IdType), PAnsiChar(A), (Len + 1) * SizeOf(AnsiChar));
    Result := string(A);
  end
  else
    Result := '';
end;
function TPdfDocument.GetMetaText(const TagName: string): string;
var
  Len: Integer;
  A: AnsiString;
begin
  CheckActive;
  A := AnsiString(TagName);
  Len := FPDF_GetMetaText(FDocument, PAnsiChar(A), nil, 0) div SizeOf(WideChar) - 1;
  if Len > 0 then
  begin
    SetLength(Result, Len);
    FPDF_GetMetaText(FDocument, PAnsiChar(A), PWideChar(Result), (Len + 1) * SizeOf(WideChar));
  end
  else
    Result := '';
end;
function TPdfDocument.GetSecurityHandlerRevision: Integer;
begin
  CheckActive;
  Result := FPDF_GetSecurityHandlerRevision(FDocument);
end;
function TPdfDocument.GetDocPermissions: Integer;
begin
  CheckActive;
  Result := Integer(FPDF_GetDocPermissions(FDocument));
end;
function TPdfDocument.GetFileVersion: Integer;
begin
  CheckActive;
  if FPDF_GetFileVersion(FDocument, Result) = 0 then
    Result := 0;
end;
function TPdfDocument.GetPageSize(Index: Integer): TPdfPoint;
var
  SizeF: TFSSizeF;
begin
  CheckActive;
  Result.X := 0;
  Result.Y := 0;
  if FPDF_GetPageSizeByIndexF(FDocument, Index, @SizeF) <> 0 then
  begin
    Result.X := SizeF.width;
    Result.Y := SizeF.height;
  end;
end;
function TPdfDocument.GetPageMode: TPdfDocumentPageMode;
begin
  CheckActive;
  Result := TPdfDocumentPageMode(FPDFDoc_GetPageMode(FDocument));
end;
function TPdfDocument.GetNumCopies: Integer;
begin
  CheckActive;
  Result := FPDF_VIEWERREF_GetNumCopies(FDocument);
end;
class function TPdfDocument.SetPrintMode(PrintMode: TPdfPrintMode): Boolean;
begin
  InitLib;
  {$IFDEF MSWINDOWS}
  Result := FPDF_SetPrintMode(Ord(PrintMode)) <> 0;
  {$ELSE}
  Result := False;
  {$ENDIF MSWINDOWS}
end;
procedure TPdfDocument.SetFormFieldHighlightAlpha(Value: Integer);
begin
  if Value < 0 then
    Value := 0;
  if Value > 255 then
    Value := 255;
  if Value <> FFormFieldHighlightAlpha then
  begin
    FFormFieldHighlightAlpha := Value;
    if Active then
      FPDF_SetFormFieldHighlightAlpha(FForm, FFormFieldHighlightAlpha);
  end;
end;
procedure TPdfDocument.SetFormFieldHighlightColor(const Value: TColorRef);
begin
  if Value <> FFormFieldHighlightColor then
  begin
    FFormFieldHighlightColor := Value;
    if Active then
      FPDF_SetFormFieldHighlightColor(FForm, 0, FFormFieldHighlightColor);
  end;
end;
function TPdfDocument.FindPage(Page: FPDF_PAGE): TPdfPage;
var
  I: Integer;
begin
  // The page must be already loaded
  for I := 0 to PageCount - 1 do
  begin
    Result := TPdfPage(FPages[I]);
    if (Result <> nil) and (Result.FPage = Page) then
      Exit;
  end;
  Result := nil;
end;

{ TPdfPage }
constructor TPdfPage.Create(ADocument: TPdfDocument; APage: FPDF_PAGE);
begin
  inherited Create;
  FDocument := ADocument;
  FPage := APage;
  FAnnotations := TPdfAnnotationList.Create(Self);
  AfterOpen;
end;
destructor TPdfPage.Destroy;
begin
  Close;
  FDocument.ExtractPage(Self);
  FreeAndNil(FAnnotations);
  inherited Destroy;
end;
function TPdfPage.IsValidForm: Boolean;
begin
  Result := (FDocument <> nil) and (FDocument.FForm <> nil) and (FPage <> nil);
end;
procedure TPdfPage.AfterOpen;
var
  OldCurDoc: TPdfDocument;
begin
  if IsValidForm then
  begin
    OldCurDoc := UnsupportedFeatureCurrentDocument;
    try
      UnsupportedFeatureCurrentDocument := FDocument;
      FORM_OnAfterLoadPage(FPage, FDocument.FForm);
      FORM_DoPageAAction(FPage, FDocument.FForm, FPDFPAGE_AACTION_OPEN);
    finally
      UnsupportedFeatureCurrentDocument := OldCurDoc;
    end;
  end;
  UpdateMetrics;
end;
procedure TPdfPage.Close;
begin
  FAnnotations.CloseAnnotations;
  if IsValidForm then
  begin
    FORM_DoPageAAction(FPage, FDocument.FForm, FPDFPAGE_AACTION_CLOSE);
    FORM_OnBeforeClosePage(FPage, FDocument.FForm);
  end;
  if FPageLinkHandle <> nil then
  begin
    FPDFLink_CloseWebLinks(FPageLinkHandle);
    FPageLinkHandle := nil;
  end;
  if FSearchHandle <> nil then
  begin
    FPDFText_FindClose(FSearchHandle);
    FSearchHandle := nil;
  end;
  if FTextHandle <> nil then
  begin
    FPDFText_ClosePage(FTextHandle);
    FTextHandle := nil;
  end;
  if FPage <> nil then
  begin
    FPDF_ClosePage(FPage);
    FPage := nil;
  end;
end;
procedure TPdfPage.Open;
begin
  if FPage = nil then
  begin
    FPage := FDocument.ReloadPage(Self);
    AfterOpen;
  end;
end;
function TPdfPage.GetPdfActionFilePath(Action: FPDF_ACTION): string;
var
  ByteSize: Integer;
  Buf: UTF8String;
begin
  Result := '';
  if Action <> nil then
  begin
    case FPDFAction_GetType(Action) of
      PDFACTION_LAUNCH,
      PDFACTION_REMOTEGOTO:
        begin
          ByteSize := FPDFAction_GetFilePath(Action, nil, 0);
          if ByteSize > 0 then
          begin
            SetLength(Buf, ByteSize); // we could optimize this with "SetLength(Buf, ByteSize - 1)" and use already existing #0 terminator
            ByteSize := FPDFAction_GetFilePath(Action, PAnsiChar(Buf), Length(Buf));
          end;
          if ByteSize > 0 then
          begin
            SetLength(Buf, ByteSize - 1); // ByteSize includes #0
            Result := UTF8ToString(Buf);
          end;
        end;
    end;
  end;
end;
function TPdfPage.GetPdfActionUriPath(Action: FPDF_ACTION): string;
var
  ByteSize: Integer;
  Buf: UTF8String;
begin
  Result := '';
  if Action <> nil then
  begin
    ByteSize := FPDFAction_GetURIPath(FDocument.Handle, Action, nil, 0);
    if ByteSize > 0 then
    begin
      SetLength(Buf, ByteSize); // we could optimize this with "SetLength(Buf, ByteSize - 1)" and use already existing #0 terminator
      ByteSize := FPDFAction_GetURIPath(FDocument.Handle, Action, PAnsiChar(Buf), Length(Buf));
    end;
    if ByteSize > 0 then
    begin
      SetLength(Buf, ByteSize - 1); // ByteSize includes #0
      Result := UTF8ToString(Buf);
    end;
  end;
end;
class function TPdfPage.GetDrawFlags(const Options: TPdfPageRenderOptions): Integer;
begin
  Result := 0;
  if proAnnotations in Options then
    Result := Result or FPDF_ANNOT;
  if proLCDOptimized in Options then
    Result := Result or FPDF_LCD_TEXT;
  if proNoNativeText in Options then
    Result := Result or FPDF_NO_NATIVETEXT;
  if proNoCatch in Options then
    Result := Result or FPDF_NO_CATCH;
  if proLimitedImageCacheSize in Options then
    Result := Result or FPDF_RENDER_LIMITEDIMAGECACHE;
  if proForceHalftone in Options then
    Result := Result or FPDF_RENDER_FORCEHALFTONE;
  if proPrinting in Options then
    Result := Result or FPDF_PRINTING;
  if proReverseByteOrder in Options then
    Result := Result or FPDF_REVERSE_BYTE_ORDER;
end;
{$IFDEF MSWINDOWS}
procedure TPdfPage.Draw(DC: HDC; X, Y, Width, Height: Integer; Rotate: TPdfPageRotation;
  const Options: TPdfPageRenderOptions; PageBackground: TColorRef);
var
  BitmapInfo: TBitmapInfo;
  Bmp, OldBmp: HBITMAP;
  BmpBits: Pointer;
  PdfBmp: TPdfBitmap;
  BmpDC: HDC;
  SavedCW: WORD;
begin
  Open;
  if proPrinting in Options then
  begin
    SetExceptionMask([exInvalidOp, exDenormalized, exZeroDivide, exOverflow, exUnderflow, exPrecision]);

    if IsValidForm and (FPDFPage_GetAnnotCount(FPage) > 0) then
    begin
      // Form content isn't printed unless it was flattend and the page was reloaded.
      ApplyChanges;
      Flatten(True);
      Close;
      Open;
    end;

  // FPU 예외 마스킹 (부동소수점 예외를 무시하게 함)
  // 현재 FPU 컨트롤 워드 저장
//    SavedCW := Get8087CW;
//    try
//    // 모든 FPU 예외 마스크
//      SetExceptionMask([exInvalidOp, exDenormalized, exZeroDivide, exOverflow, exUnderflow, exPrecision]);

      FPDF_RenderPage(DC, FPage, X, Y, Width, Height, Ord(Rotate), GetDrawFlags(Options));
//    finally
//    // 원래 상태 복원
//      Set8087CW(SavedCW);
//    end;
    Exit;
  end;
  FillChar(BitmapInfo, SizeOf(BitmapInfo), 0);
  BitmapInfo.bmiHeader.biSize := SizeOf(BitmapInfo);
  BitmapInfo.bmiHeader.biWidth := Width;
  BitmapInfo.bmiHeader.biHeight := -Height; // negative Height means top to bottom for Y values
  BitmapInfo.bmiHeader.biPlanes := 1;
  BitmapInfo.bmiHeader.biBitCount := 32;
  BitmapInfo.bmiHeader.biCompression := BI_RGB;
  BmpBits := nil;
  Bmp := CreateDIBSection(DC, BitmapInfo, DIB_RGB_COLORS, BmpBits, 0, 0);
  if Bmp <> 0 then
  begin
    try
      // Use the Windows Bitmap's bits for the PdfBmp
      PdfBmp := TPdfBitmap.Create(Width, Height, bfBGRA, BmpBits, Width * 4);
      try
        Draw(PdfBmp, 0, 0, Width, Height, Rotate, Options, PageBackground);
      finally
        PdfBmp.Free;
      end;
      BmpDC := CreateCompatibleDC(DC);
      OldBmp := SelectObject(BmpDC, Bmp);
      BitBlt(DC, X, Y, Width, Height, BmpDC, 0, 0, SRCCOPY);
      SelectObject(BmpDC, OldBmp);
      DeleteDC(BmpDC);
    finally
      DeleteObject(Bmp);
    end;
  end;
end;
{$ENDIF MSWINDOWS}
procedure TPdfPage.Draw(APdfBitmap: TPdfBitmap; X, Y, Width, Height: Integer; Rotate: TPdfPageRotation = prNormal;
  const Options: TPdfPageRenderOptions = []; PageBackground: TColorRef = $FFFFFF);
begin
  APdfBitmap.FillRect(0, 0, Width, Height, $FF000000 or PageBackground);
  DrawToPdfBitmap(APdfBitmap, 0, 0, Width, Height, Rotate, Options);
  DrawFormToPdfBitmap(APdfBitmap, 0, 0, Width, Height, Rotate, Options);
end;
procedure TPdfPage.DrawToPdfBitmap(APdfBitmap: TPdfBitmap; X, Y, Width, Height: Integer;
  Rotate: TPdfPageRotation; const Options: TPdfPageRenderOptions);
begin
  Open;
  FPDF_RenderPageBitmap(APdfBitmap.FBitmap, FPage, X, Y, Width, Height, Ord(Rotate), GetDrawFlags(Options));
end;
procedure TPdfPage.DrawFormToPdfBitmap(APdfBitmap: TPdfBitmap; X, Y, Width, Height: Integer;
  Rotate: TPdfPageRotation; const Options: TPdfPageRenderOptions);
begin
  Open;
  if IsValidForm then
  begin
    if proPrinting in Options then
    begin
      if FDocument.PrintHidesFormFieldHighlight then
        FPDF_RemoveFormFieldHighlight(FDocument.FForm);
        //FPDF_SetFormFieldHighlightAlpha(FDocument.FForm, 0); // hide the highlight
      FormEventKillFocus;
    end;
    try
      FPDF_FFLDraw(FDocument.FForm, APdfBitmap.FBitmap, FPage, X, Y, Width, Height, Ord(Rotate), GetDrawFlags(Options));
    finally
      if (proPrinting in Options) and FDocument.PrintHidesFormFieldHighlight then
        FDocument.UpdateFormFieldHighlight;
    end;
  end;
end;
procedure TPdfPage.UpdateMetrics;
begin
  FWidth := FPDF_GetPageWidthF(FPage);
  FHeight := FPDF_GetPageHeightF(FPage);
  FTransparency := FPDFPage_HasTransparency(FPage) <> 0;
  FRotation := TPdfPageRotation(FPDFPage_GetRotation(FPage));
end;
function TPdfPage.DeviceToPage(X, Y, Width, Height: Integer; DeviceX, DeviceY: Integer; Rotate: TPdfPageRotation): TPdfPoint;
begin
  Open;
  FPDF_DeviceToPage(FPage, X, Y, Width, Height, Ord(Rotate), DeviceX, DeviceY, Result.X, Result.Y);
end;
function TPdfPage.PageToDevice(X, Y, Width, Height: Integer; PageX, PageY: Double;
  Rotate: TPdfPageRotation): TPoint;
begin
  Open;
  FPDF_PageToDevice(FPage, X, Y, Width, Height, Ord(Rotate), PageX, PageY, Result.X, Result.Y);
end;
function TPdfPage.DeviceToPage(X, Y, Width, Height: Integer; const R: TRect; Rotate: TPdfPageRotation): TPdfRect;
begin
  Result.TopLeft := DeviceToPage(X, Y, Width, Height, R.Left, R.Top, Rotate);
  Result.BottomRight := DeviceToPage(X, Y, Width, Height, R.Right, R.Bottom, Rotate);
end;
function TPdfPage.PageToDevice(X, Y, Width, Height: Integer; const R: TPdfRect; Rotate: TPdfPageRotation): TRect;
var
  T: Integer;
begin
  Result.TopLeft := PageToDevice(X, Y, Width, Height, R.Left, R.Top, Rotate);
  Result.BottomRight := PageToDevice(X, Y, Width, Height, R.Right, R.Bottom, Rotate);
  // Page coordinales are upside down, but device coordinates aren't.
  if Result.Top > Result.Bottom then
  begin
    T := Result.Top;
    Result.Top := Result.Bottom;
    Result.Bottom := T;
  end;
end;
procedure TPdfPage.SetRotation(const Value: TPdfPageRotation);
begin
  Open;
  FPDFPage_SetRotation(FPage, Ord(Value));
  FRotation := TPdfPageRotation(FPDFPage_GetRotation(FPage));
end;
procedure TPdfPage.ApplyChanges;
begin
  if FPage <> nil then
  begin
    FPDFPage_GenerateContent(FPage);
    // Newly added text annotations will not show the text popup unless the page is notified.
    FAnnotations.CloseAnnotations;
    if IsValidForm then
    begin
      FORM_DoPageAAction(FPage, FDocument.FForm, FPDFPAGE_AACTION_CLOSE);
      FORM_OnBeforeClosePage(FPage, FDocument.FForm);
      FORM_OnAfterLoadPage(FPage, FDocument.FForm);
      FORM_DoPageAAction(FPage, FDocument.FForm, FPDFPAGE_AACTION_OPEN);
    end;
  end;
end;
procedure TPdfPage.Flatten(AFlatPrint: Boolean);
const
  Flags: array[Boolean] of Integer = (FLAT_NORMALDISPLAY, FLAT_PRINT);
begin
  if FPage <> nil then
    FPDFPage_Flatten(FPage, Flags[AFlatPrint]);
end;
function TPdfPage.BeginText: Boolean;
begin
  if FTextHandle = nil then
  begin
    Open;
    FTextHandle := FPDFText_LoadPage(FPage);
  end;
  Result := FTextHandle <> nil;
end;
function TPdfPage.BeginWebLinks: Boolean;
begin
  // WebLinks are not stored in the PDF but are created by parsing the page's text for URLs.
  // They are accessed differently than annotation links, which are stored in the PDF.
  if (FPageLinkHandle = nil) and BeginText then
    FPageLinkHandle := FPDFLink_LoadWebLinks(FTextHandle);
  Result := FPageLinkHandle <> nil;
end;
function TPdfPage.BeginFind(const SearchString: string; MatchCase, MatchWholeWord,
  FromEnd: Boolean): Boolean;
var
  Flags, StartIndex: Integer;
begin
  EndFind;
  if BeginText then
  begin
    Flags := 0;
    if MatchCase then
      Flags := Flags or FPDF_MATCHCASE;
    if MatchWholeWord then
      Flags := Flags or FPDF_MATCHWHOLEWORD;
    if FromEnd then
      StartIndex := -1
    else
      StartIndex := 0;
    FSearchHandle := FPDFText_FindStart(FTextHandle, PWideChar(SearchString), Flags, StartIndex);
  end;
  Result := FSearchHandle <> nil;
end;
procedure TPdfPage.EndFind;
begin
  if FSearchHandle <> nil then
  begin
    FPDFText_FindClose(FSearchHandle);
    FSearchHandle := nil;
  end;
end;
function TPdfPage.FindNext(var CharIndex, Count: Integer): Boolean;
begin
  CharIndex := 0;
  Count := 0;
  if FSearchHandle <> nil then
  begin
    Result := FPDFText_FindNext(FSearchHandle) <> 0;
    if Result then
    begin
      CharIndex := FPDFText_GetSchResultIndex(FSearchHandle);
      Count := FPDFText_GetSchCount(FSearchHandle);
    end;
  end
  else
    Result := False;
end;
function TPdfPage.FindPrev(var CharIndex, Count: Integer): Boolean;
begin
  CharIndex := 0;
  Count := 0;
  if FSearchHandle <> nil then
  begin
    Result := FPDFText_FindPrev(FSearchHandle) <> 0;
    if Result then
    begin
      CharIndex := FPDFText_GetSchResultIndex(FSearchHandle);
      Count := FPDFText_GetSchCount(FSearchHandle);
    end;
  end
  else
    Result := False;
end;
function TPdfPage.GetCharCount: Integer;
begin
  if BeginText then
    Result := FPDFText_CountChars(FTextHandle)
  else
    Result := 0;
end;
function TPdfPage.ReadChar(CharIndex: Integer): WideChar;
begin
  if BeginText then
    Result := FPDFText_GetUnicode(FTextHandle, CharIndex)
  else
    Result := #0;
end;
function TPdfPage.GetCharFontSize(CharIndex: Integer): Double;
begin
  if BeginText then
    Result := FPDFText_GetFontSize(FTextHandle, CharIndex)
  else
    Result := 0;
end;
function TPdfPage.GetCharBox(CharIndex: Integer): TPdfRect;
begin
  if BeginText then
    FPDFText_GetCharBox(FTextHandle, CharIndex, Result.Left, Result.Right, Result.Bottom, Result.Top)
  else
    Result := TPdfRect.Empty;
end;
function TPdfPage.GetCharIndexAt(PageX, PageY, ToleranceX, ToleranceY: Double): Integer;
begin
  if BeginText then
    Result := FPDFText_GetCharIndexAtPos(FTextHandle, PageX, PageY, ToleranceX, ToleranceY)
  else
    Result := 0;
end;
function TPdfPage.ReadText(CharIndex, Count: Integer): String;
var
  Len: Integer;
begin
  if (Count > 0) and BeginText then
  begin
    SetLength(Result, Count); // we let GetText overwrite our #0 terminator with its #0
    Len := FPDFText_GetText(FTextHandle, CharIndex, Count, PWideChar(Result)) - 1; // returned length includes the #0
    if Len <= 0 then
      Result := ''
    else if Len < Count then
      SetLength(Result, Len);
  end
  else
    Result := '';
end;

function TPdfPage.ReadText2(CharIndex, Count: Integer): String;
var
  Len: Integer;
  arrBuf: array of Char;
  i: Integer;
begin
  Result := '';
  if (Count > 0) and BeginText then
  begin
    for i := 0 to Count - 1 do
    begin
      Result := Result + FPDFText_GetUnicode(FTextHandle, i);
    end;
    exit;
    SetLength(arrBuf, Count + 1);
    if FPDFText_GetText(FTextHandle, 0, Count, @arrBuf[0]) > 0 then
    begin
      Result := String(@arrBuf[0]);
//      Result := Format('%s%s', [sLineBreak, PageText]) + sLineBreak + sLineBreak;
    end;
  end else
    Result := '';
end;

function TPdfPage.GetTextAt(Left, Top, Right, Bottom: Double): string;
var
  Len: Integer;
begin
  if BeginText then
  begin
    Len := FPDFText_GetBoundedText(FTextHandle, Left, Top, Right, Bottom, nil, 0); // excluding #0 terminator
    SetLength(Result, Len);
    if Len > 0 then
      FPDFText_GetBoundedText(FTextHandle, Left, Top, Right, Bottom, PWideChar(Result), Len);
  end
  else
    Result := '';
end;
function TPdfPage.GetTextAt(const R: TPdfRect): string;
begin
  Result := GetTextAt(R.Left, R.Top, R.Right, R.Bottom);
end;
function TPdfPage.GetTextRectCount(CharIndex, Count: Integer): Integer;
begin
  if BeginText then
    Result := FPDFText_CountRects(FTextHandle, CharIndex, Count)
  else
    Result := 0;
end;
function TPdfPage.GetTextRect(RectIndex: Integer): TPdfRect;
begin
  if BeginText then
    FPDFText_GetRect(FTextHandle, RectIndex, Result.Left, Result.Top, Result.Right, Result.Bottom)
  else
    Result := TPdfRect.Empty;
end;
function TPdfPage.IsUriLinkAtPoint(X, Y: Double): Boolean;
var
  Link: FPDF_LINK;
  Action: FPDF_ACTION;
begin
  Result := False;
  Link := FPDFLink_GetLinkAtPoint(Handle, X, Y);
  if Link <> nil then
  begin
    Action := FPDFLink_GetAction(Link);
    if (Action <> nil) and (FPDFAction_GetType(Action) = PDFACTION_URI) then
      Result := True;
  end;
end;
function TPdfPage.IsUriLinkAtPoint(X, Y: Double; var Uri: string): Boolean;
var
  Link: FPDF_LINK;
  Action: FPDF_ACTION;
begin
  Action := nil;
  Result := False;
  Link := FPDFLink_GetLinkAtPoint(Handle, X, Y);
  if Link <> nil then
  begin
    Action := FPDFLink_GetAction(Link);
    if (Action <> nil) and (FPDFAction_GetType(Action) = PDFACTION_URI) then
      Result := True;
  end;
  if Result then
    Uri := GetPdfActionUriPath(Action)
  else
    Uri := '';
end;
function TPdfPage.GetLinkAtPoint(X, Y: Double): TPdfAnnotation;
var
  Link: FPDF_LINK;
begin
  Link := FPDFLink_GetLinkAtPoint(Handle, X, Y);
  if Link <> nil then
  begin
    Result := Annotations.FindLink(Link);
    if (Result <> nil) and (Result.LinkType = altUnsupported) then
      Result := nil;
  end
  else
    Result := nil;
end;
function TPdfPage.GetWebLinkCount: Integer;
begin
  if BeginWebLinks then
  begin
    Result := FPDFLink_CountWebLinks(FPageLinkHandle);
    if Result < 0 then
      Result := 0;
  end
  else
    Result := 0;
end;
function TPdfPage.GetWebLinkURL(LinkIndex: Integer): string;
var
  Len: Integer;
begin
  Result := '';
  if BeginWebLinks then
  begin
    Len := FPDFLink_GetURL(FPageLinkHandle, LinkIndex, nil, 0) - 1; // including #0 terminator
    if Len > 0 then
    begin
      SetLength(Result, Len);
      FPDFLink_GetURL(FPageLinkHandle, LinkIndex, PWideChar(Result), Len + 1); // including #0 terminator
    end;
  end;
end;
function TPdfPage.GetWebLinkRectCount(LinkIndex: Integer): Integer;
begin
  if BeginWebLinks then
    Result := FPDFLink_CountRects(FPageLinkHandle, LinkIndex)
  else
    Result := 0;
end;
function TPdfPage.GetWebLinkRect(LinkIndex, RectIndex: Integer): TPdfRect;
begin
  if BeginWebLinks then
    FPDFLink_GetRect(FPageLinkHandle, LinkIndex, RectIndex, Result.Left, Result.Top, Result.Right, Result.Bottom)
  else
    Result := TPdfRect.Empty;
end;
function TPdfPage.IsWebLinkAtPoint(X, Y: Double): Boolean;
var
  LinkIndex, RectIndex: Integer;
  Pt: TPdfPoint;
begin
  Result := True;
  Pt.X := X;
  Pt.Y := Y;
  for LinkIndex := 0 to GetWebLinkCount - 1 do
    for RectIndex := 0 to GetWebLinkRectCount(LinkIndex) - 1 do
      if GetWebLinkRect(LinkIndex, RectIndex).PtIn(Pt) then
        Exit;
  Result := False;
end;
function TPdfPage.IsWebLinkAtPoint(X, Y: Double; var URL: string): Boolean;
var
  LinkIndex, RectIndex: Integer;
  Pt: TPdfPoint;
begin
  Result := True;
  Pt.X := X;
  Pt.Y := Y;
  for LinkIndex := 0 to GetWebLinkCount - 1 do
  begin
    for RectIndex := 0 to GetWebLinkRectCount(LinkIndex) - 1 do
    begin
      if GetWebLinkRect(LinkIndex, RectIndex).PtIn(Pt) then
      begin
        URL := GetWebLinkURL(LinkIndex);
        Exit;
      end;
    end;
  end;
  Result := False;
end;
function TPdfPage.ShiftStateToModifier(const Shift: TShiftState): Integer;
begin
  Result := 0;
  if ssShift in Shift then
    Result := Result or FWL_EVENTFLAG_ShiftKey;
  if ssCtrl in Shift then
    Result := Result or FWL_EVENTFLAG_ControlKey;
  if ssAlt in Shift then
    Result := Result or FWL_EVENTFLAG_AltKey;
  if ssLeft in Shift then
    Result := Result or FWL_EVENTFLAG_LeftButtonDown;
  if ssMiddle in Shift then
    Result := Result or FWL_EVENTFLAG_MiddleButtonDown;
  if ssRight in Shift then
    Result := Result or FWL_EVENTFLAG_RightButtonDown;
end;
function TPdfPage.FormEventFocus(const Shift: TShiftState; PageX, PageY: Double): Boolean;
begin
  if IsValidForm then
    Result := FORM_OnFocus(FDocument.FForm, FPage, ShiftStateToModifier(Shift), PageX, PageY) <> 0
  else
    Result := False;
end;
function TPdfPage.FormEventMouseWheel(const Shift: TShiftState; WheelDelta: Integer; PageX, PageY: Double): Boolean;
var
  Pt: TFSPointF;
  WheelX, WheelY: Integer;
begin
  if IsValidForm then
  begin
    Pt.X := PageX;
    Pt.Y := PageY;
    WheelX := 0;
    WheelY := 0;
    if ssShift in Shift then
      WheelX := WheelDelta
    else
      WheelY := WheelDelta;
    Result := FORM_OnMouseWheel(FDocument.FForm, FPage, ShiftStateToModifier(Shift), @Pt, WheelX, WheelY) <> 0;
  end
  else
    Result := False;
end;
function TPdfPage.FormEventMouseMove(const Shift: TShiftState; PageX, PageY: Double): Boolean;
begin
  if IsValidForm then
    Result := FORM_OnMouseMove(FDocument.FForm, FPage, ShiftStateToModifier(Shift), PageX, PageY) <> 0
  else
    Result := False;
end;
function TPdfPage.FormEventLButtonDown(const Shift: TShiftState; PageX, PageY: Double): Boolean;
begin
  if IsValidForm then
    Result := FORM_OnLButtonDown(FDocument.FForm, FPage, ShiftStateToModifier(Shift), PageX, PageY) <> 0
  else
    Result := False;
end;
function TPdfPage.FormEventLButtonUp(const Shift: TShiftState; PageX, PageY: Double): Boolean;
begin
  if IsValidForm then
    Result := FORM_OnLButtonUp(FDocument.FForm, FPage, ShiftStateToModifier(Shift), PageX, PageY) <> 0
  else
    Result := False;
end;
function TPdfPage.FormEventRButtonDown(const Shift: TShiftState; PageX, PageY: Double): Boolean;
begin
  if IsValidForm then
    Result := FORM_OnRButtonDown(FDocument.FForm, FPage, ShiftStateToModifier(Shift), PageX, PageY) <> 0
  else
    Result := False;
end;
function TPdfPage.FormEventRButtonUp(const Shift: TShiftState; PageX, PageY: Double): Boolean;
begin
  if IsValidForm then
    Result := FORM_OnRButtonUp(FDocument.FForm, FPage, ShiftStateToModifier(Shift), PageX, PageY) <> 0
  else
    Result := False;
end;
function TPdfPage.FormEventKeyDown(KeyCode: Word; const Shift: TShiftState): Boolean;
begin
  if IsValidForm then
    Result := FORM_OnKeyDown(FDocument.FForm, FPage, KeyCode, ShiftStateToModifier(Shift)) <> 0
  else
    Result := False;
end;
function TPdfPage.FormEventKeyUp(KeyCode: Word; const Shift: TShiftState): Boolean;
begin
  if IsValidForm then
    Result := FORM_OnKeyUp(FDocument.FForm, FPage, KeyCode, ShiftStateToModifier(Shift)) <> 0
  else
    Result := False;
end;
function TPdfPage.FormEventKeyPress(Key: Word;  const Shift: TShiftState): Boolean;
begin
  if IsValidForm then
    Result := FORM_OnChar(FDocument.FForm, FPage, Key, ShiftStateToModifier(Shift)) <> 0
  else
    Result := False;
end;
function TPdfPage.FormEventKillFocus: Boolean;
begin
  if IsValidForm then
    Result := FORM_ForceToKillFocus(FDocument.FForm) <> 0
  else
    Result := False;
end;
function TPdfPage.FormGetFocusedText: string;
var
  ByteLen: LongWord;
begin
  if IsValidForm then
  begin
    ByteLen := FORM_GetFocusedText(FDocument.FForm, FPage, nil, 0); // UTF 16 including #0 terminator in byte size
    if ByteLen <= 2 then // WideChar(#0) => empty string
      Result := ''
    else
    begin
      SetLength(Result, ByteLen div SizeOf(WideChar) - 1);
      FORM_GetFocusedText(FDocument.FForm, FPage, PWideChar(Result), ByteLen);
    end;
  end
  else
    Result := '';
end;
function TPdfPage.FormGetSelectedText: string;
var
  ByteLen: LongWord;
begin
  if IsValidForm then
  begin
    ByteLen := FORM_GetSelectedText(FDocument.FForm, FPage, nil, 0); // UTF 16 including #0 terminator in byte size
    if ByteLen <= 2 then // WideChar(#0) => empty string
      Result := ''
    else
    begin
      SetLength(Result, ByteLen div SizeOf(WideChar) - 1);
      FORM_GetSelectedText(FDocument.FForm, FPage, PWideChar(Result), ByteLen);
    end;
  end
  else
    Result := '';
end;
function TPdfPage.FormReplaceSelection(const ANewText: string): Boolean;
begin
  if IsValidForm then
  begin
    FORM_ReplaceSelection(FDocument.FForm, FPage, PWideChar(ANewText));
    Result := True;
  end
  else
    Result := False;
end;
function TPdfPage.FormReplaceAndKeepSelection(const ANewText: string): Boolean;
begin
  if IsValidForm then
  begin
    FORM_ReplaceAndKeepSelection(FDocument.FForm, FPage, PWideChar(ANewText));
    Result := True;
  end
  else
    Result := False;
end;
function TPdfPage.FormSelectAllText: Boolean;
begin
  if IsValidForm then
    Result := FORM_SelectAllText(FDocument.FForm, FPage) <> 0
  else
    Result := False;
end;
function TPdfPage.FormCanUndo: Boolean;
begin
  if IsValidForm then
    Result := FORM_CanUndo(FDocument.FForm, FPage) <> 0
  else
    Result := False;
end;
function TPdfPage.FormCanRedo: Boolean;
begin
  if IsValidForm then
    Result := FORM_CanRedo(FDocument.FForm, FPage) <> 0
  else
    Result := False;
end;
function TPdfPage.FormUndo: Boolean;
begin
  if IsValidForm then
    Result := FORM_Undo(FDocument.FForm, FPage) <> 0
  else
    Result := False;
end;
function TPdfPage.FormRedo: Boolean;
begin
  if IsValidForm then
    Result := FORM_Redo(FDocument.FForm, FPage) <> 0
  else
    Result := False;
end;
function TPdfPage.HasFormFieldAtPoint(X, Y: Double): TPdfFormFieldType;
begin
  Result := TPdfFormFieldType(FPDFPage_HasFormFieldAtPoint(FDocument.FForm, FPage, X, Y));
  if (Result < Low(TPdfFormFieldType)) or (Result > High(TPdfFormFieldType)) then
    Result := fftUnknown;
end;
function TPdfPage.GetHandle: FPDF_PAGE;
begin
  Open;
  Result := FPage;
end;
function TPdfPage.IsLoaded: Boolean;
begin
  Result := FPage <> nil;
end;
function TPdfPage.GetTextHandle: FPDF_TEXTPAGE;
begin
  if BeginText then
    Result := FTextHandle
  else
    Result := nil;
end;
function TPdfPage.GetFormFields: TPdfFormFieldList;
begin
  Result := Annotations.FormFields;
end;

{ _TPdfBitmapHideCtor }
constructor _TPdfBitmapHideCtor.Create;
begin
  inherited Create;
end;

{ TPdfBitmap }
constructor TPdfBitmap.Create(ABitmap: FPDF_BITMAP; AOwnsBitmap: Boolean);
begin
  inherited Create;
  FBitmap := ABitmap;
  FOwnsBitmap := AOwnsBitmap;
  if FBitmap <> nil then
  begin
    FWidth := FPDFBitmap_GetWidth(FBitmap);
    FHeight := FPDFBitmap_GetHeight(FBitmap);
    FBytesPerScanLine := FPDFBitmap_GetStride(FBitmap);
  end;
end;
constructor TPdfBitmap.Create(AWidth, AHeight: Integer; AAlpha: Boolean);
begin
  Create(FPDFBitmap_Create(AWidth, AHeight, Ord(AAlpha)), True);
end;
constructor TPdfBitmap.Create(AWidth, AHeight: Integer; AFormat: TPdfBitmapFormat);
begin
  Create(FPDFBitmap_CreateEx(AWidth, AHeight, Ord(AFormat), nil, 0), True);
end;
constructor TPdfBitmap.Create(AWidth, AHeight: Integer; AFormat: TPdfBitmapFormat; ABuffer: Pointer;
  ABytesPerScanLine: Integer);
begin
  Create(FPDFBitmap_CreateEx(AWidth, AHeight, Ord(AFormat), ABuffer, ABytesPerScanline), True);
end;
destructor TPdfBitmap.Destroy;
begin
  if FOwnsBitmap and (FBitmap <> nil) then
    FPDFBitmap_Destroy(FBitmap);
  inherited Destroy;
end;
function TPdfBitmap.GetBuffer: Pointer;
begin
  if FBitmap <> nil then
    Result := FPDFBitmap_GetBuffer(FBitmap)
  else
    Result := nil;
end;
procedure TPdfBitmap.FillRect(ALeft, ATop, AWidth, AHeight: Integer; AColor: FPDF_DWORD);
begin
  if FBitmap <> nil then
    FPDFBitmap_FillRect(FBitmap, ALeft, ATop, AWidth, AHeight, AColor);
end;
{ TPdfPoint }
procedure TPdfPoint.Offset(XOffset, YOffset: Double);
begin
  X := X + XOffset;
  Y := Y + YOffset;
end;
class function TPdfPoint.Empty: TPdfPoint;
begin
  Result.X := 0;
  Result.Y := 0;
end;

{ TPdfAttachmentList }
constructor TPdfAttachmentList.Create(ADocument: TPdfDocument);
begin
  inherited Create;
  FDocument := ADocument;
end;
function TPdfAttachmentList.GetCount: Integer;
begin
  FDocument.CheckActive;
  Result := FPDFDoc_GetAttachmentCount(FDocument.Handle);
end;
function TPdfAttachmentList.GetItem(Index: Integer): TPdfAttachment;
var
  Attachment: FPDF_ATTACHMENT;
begin
  FDocument.CheckActive;
  Attachment := FPDFDoc_GetAttachment(FDocument.Handle, Index);
  if Attachment = nil then
    raise EPdfArgumentOutOfRange.CreateResFmt(@RsArgumentsOutOfRange, ['Index']);
  Result.FDocument := FDocument;
  Result.FHandle := Attachment;
end;
procedure TPdfAttachmentList.Delete(Index: Integer);
begin
  FDocument.CheckActive;
  if FPDFDoc_DeleteAttachment(FDocument.Handle, Index) = 0 then
    raise EPdfException.CreateResFmt(@RsPdfCannotDeleteAttachmnent, [Index]);
end;
function TPdfAttachmentList.Add(const Name: string): TPdfAttachment;
begin
  FDocument.CheckActive;
  Result.FDocument := FDocument;
  Result.FHandle := FPDFDoc_AddAttachment(FDocument.Handle, PWideChar(Name));
  if Result.FHandle = nil then
    raise EPdfException.CreateResFmt(@RsPdfCannotAddAttachmnent, [Name]);
end;
function TPdfAttachmentList.IndexOf(const Name: string): Integer;
begin
  for Result := 0 to Count - 1 do
    if Items[Result].Name = Name then
      Exit;
  Result := -1;
end;

{ TPdfAttachment }
function TPdfAttachment.GetName: string;
var
  ByteLen: LongWord;
begin
  CheckValid;
  ByteLen := FPDFAttachment_GetName(Handle, nil, 0); // UTF 16 including #0 terminator in byte size
  if ByteLen <= 2 then
    Result := ''
  else
  begin
    SetLength(Result, ByteLen div SizeOf(WideChar) - 1);
    FPDFAttachment_GetName(FHandle, PWideChar(Result), ByteLen);
  end;
end;
procedure TPdfAttachment.CheckValid;
begin
  if FDocument <> nil then
    FDocument.CheckActive;
end;
procedure TPdfAttachment.SetContent(ABytes: PByte; Count: Integer);
begin
  CheckValid;
  if FPDFAttachment_SetFile(FHandle, FDocument.Handle, ABytes, Count) = 0 then
    raise EPdfException.CreateResFmt(@RsPdfCannotSetAttachmentContent, [Name]);
end;
procedure TPdfAttachment.SetContent(const Value: RawByteString);
begin
  if Value = '' then
    SetContent(nil, 0)
  else
    SetContent(PByte(PAnsiChar(Value)), Length(Value) * SizeOf(AnsiChar));
end;
procedure TPdfAttachment.SetContent(const Value: string; Encoding: TEncoding = nil);
begin
  CheckValid;
  if Value = '' then
    SetContent(nil, 0)
  else if (Encoding = nil) or (Encoding = TEncoding.UTF8) then
    SetContent(UTF8Encode(Value))
  else
    SetContent(Encoding.GetBytes(Value));
end;
procedure TPdfAttachment.SetContent(const ABytes: TBytes; Index: NativeInt; Count: Integer);
var
  Len: NativeInt;
begin
  CheckValid;
  Len := Length(ABytes);
  if Index >= Len then
    raise EPdfArgumentOutOfRange.CreateResFmt(@RsArgumentsOutOfRange, ['Index', Index]);
  if Index + Count > Len then
    raise EPdfArgumentOutOfRange.CreateResFmt(@RsArgumentsOutOfRange, ['Count', Count]);
  if Count = 0 then
    SetContent(nil, 0)
  else
    SetContent(@ABytes[Index], Count);
end;
procedure TPdfAttachment.SetContent(const ABytes: TBytes);
begin
  SetContent(ABytes, 0, Length(ABytes));
end;
procedure TPdfAttachment.LoadFromStream(Stream: TStream);
var
  StreamPos, StreamSize: Int64;
  Buf: PByte;
  Count: Integer;
begin
  CheckValid;
  StreamPos := Stream.Position;
  StreamSize := Stream.Size;
  Count := StreamSize - StreamPos;
  if Count = 0 then
    SetContent(nil, 0)
  else
  begin
    if Stream is TCustomMemoryStream then // direct access to the memory
    begin
      SetContent(PByte(TCustomMemoryStream(Stream).Memory) + StreamPos, Count);
      Stream.Position := StreamSize; // simulate the ReadBuffer call
    end
    else
    begin
      if Count = 0 then
        SetContent(nil, 0)
      else
      begin
        GetMem(Buf, Count);
        try
          Stream.ReadBuffer(Buf^, Count);
          SetContent(Buf, Count);
        finally
          FreeMem(Buf);
        end;
      end;
    end;
  end;
end;
procedure TPdfAttachment.LoadFromFile(const FileName: string);
var
  Stream: TFileStream;
begin
  CheckValid;
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    LoadFromStream(Stream);
  finally
    Stream.Free;
  end;
end;
function TPdfAttachment.HasKey(const Key: string): Boolean;
begin
  CheckValid;
  Result := FPDFAttachment_HasKey(FHandle, PAnsiChar(UTF8Encode(Key))) <> 0;
end;
function TPdfAttachment.GetValueType(const Key: string): TPdfObjectType;
begin
  CheckValid;
  Result := TPdfObjectType(FPDFAttachment_GetValueType(FHandle, PAnsiChar(UTF8Encode(Key))));
end;
procedure TPdfAttachment.SetKeyValue(const Key, Value: string);
begin
  CheckValid;
  if FPDFAttachment_SetStringValue(FHandle, PAnsiChar(UTF8Encode(Key)), PWideChar(Value)) = 0 then
    raise EPdfException.CreateRes(@RsPdfAttachmentContentNotSet);
end;
function TPdfAttachment.GetKeyValue(const Key: string): string;
var
  ByteLen: LongWord;
  Utf8Key: UTF8String;
begin
  CheckValid;
  Utf8Key := UTF8Encode(Key);
  ByteLen := FPDFAttachment_GetStringValue(FHandle, PAnsiChar(Utf8Key), nil, 0);
  if ByteLen = 0 then
    raise EPdfException.CreateRes(@RsPdfAttachmentContentNotSet);
  if ByteLen <= 2 then
    Result := ''
  else
  begin
    SetLength(Result, (ByteLen div SizeOf(WideChar) - 1));
    FPDFAttachment_GetStringValue(FHandle, PAnsiChar(Utf8Key), PWideChar(Result), ByteLen);
  end;
end;
function TPdfAttachment.GetContentSize: Integer;
var
  OutBufLen: LongWord;
begin
  CheckValid;
  if FPDFAttachment_GetFile(FHandle, nil, 0, OutBufLen) = 0 then
    Result := 0
  else
    Result := Integer(OutBufLen);
end;
function TPdfAttachment.HasContent: Boolean;
var
  OutBufLen: LongWord;
begin
  CheckValid;
  Result := FPDFAttachment_GetFile(FHandle, nil, 0, OutBufLen) <> 0;
end;
procedure TPdfAttachment.SaveToFile(const FileName: string);
var
  Stream: TStream;
begin
  CheckValid;
  Stream := TFileStream.Create(FileName, fmCreate or fmShareDenyWrite);
  try
    SaveToStream(Stream);
  finally
    Stream.Free;
  end;
end;
procedure TPdfAttachment.SaveToStream(Stream: TStream);
var
  Size: Integer;
  OutBufLen: LongWord;
  StreamPos: Int64;
  Buf: PByte;
begin
  Size := ContentSize;
  if Size > 0 then
  begin
    if Stream is TCustomMemoryStream then // direct access to the memory
    begin
      StreamPos := Stream.Position;
      if StreamPos + Size > Stream.Size then
        Stream.Size := StreamPos + Size; // allocate enough memory
      Stream.Position := StreamPos;
      FPDFAttachment_GetFile(FHandle, PByte(TCustomMemoryStream(Stream).Memory) + StreamPos, Size, OutBufLen);
      Stream.Position := StreamPos + Size; // simulate Stream.WriteBuffer
    end
    else
    begin
      GetMem(Buf, Size);
      try
        FPDFAttachment_GetFile(FHandle, Buf, Size, OutBufLen);
        Stream.WriteBuffer(Buf^, Size);
      finally
        FreeMem(Buf);
      end;
    end;
  end;
end;
procedure TPdfAttachment.GetContent(var Value: string; Encoding: TEncoding);
var
  Size: Integer;
  OutBufLen: LongWord;
  Buf: PByte;
begin
  Size := ContentSize;
  if Size <= 0 then
    Value := ''
  else if Encoding = TEncoding.Unicode then // no conversion needed
  begin
    SetLength(Value, Size div SizeOf(WideChar));
    FPDFAttachment_GetFile(FHandle, PWideChar(Value), Size, OutBufLen);
  end
  else
  begin
    if Encoding = nil then
      Encoding := TEncoding.UTF8;
    GetMem(Buf, Size);
    try
      FPDFAttachment_GetFile(FHandle, Buf, Size, OutBufLen);
      SetLength(Value, TEncodingAccess(Encoding).GetMemCharCount(Buf, Size));
      if Value <> '' then
        TEncodingAccess(Encoding).GetMemChars(Buf, Size, PWideChar(Value), Length(Value));
    finally
      FreeMem(Buf);
    end;
  end;
end;
procedure TPdfAttachment.GetContent(var Value: RawByteString);
var
  Size: Integer;
  OutBufLen: LongWord;
begin
  Size := ContentSize;
  if Size <= 0 then
    Value := ''
  else
  begin
    SetLength(Value, Size);
    FPDFAttachment_GetFile(FHandle, PAnsiChar(Value), Size, OutBufLen);
  end;
end;
procedure TPdfAttachment.GetContent(Buffer: PByte);
var
  OutBufLen: LongWord;
begin
  FPDFAttachment_GetFile(FHandle, Buffer, ContentSize, OutBufLen);
end;
procedure TPdfAttachment.GetContent(var ABytes: TBytes);
var
  Size: Integer;
  OutBufLen: LongWord;
begin
  Size := ContentSize;
  if Size <= 0 then
    ABytes := nil
  else
  begin
    SetLength(ABytes, Size);
    FPDFAttachment_GetFile(FHandle, @ABytes[0], Size, OutBufLen);
  end;
end;
function TPdfAttachment.GetContentAsBytes: TBytes;
begin
  GetContent(Result);
end;
function TPdfAttachment.GetContentAsRawByteString: RawByteString;
begin
  GetContent(Result);
end;
function TPdfAttachment.GetContentAsString(Encoding: TEncoding): string;
begin
  GetContent(Result, Encoding);
end;

{ TPdfAnnotationList }
constructor TPdfAnnotationList.Create(APage: TPdfPage);
begin
  inherited Create;
  FPage := APage;
  FItems := TObjectList.Create;
end;
destructor TPdfAnnotationList.Destroy;
begin
  FreeAndNil(FFormFields);
  FreeAndNil(FItems); // closes all annotations
  inherited Destroy;
end;
procedure TPdfAnnotationList.CloseAnnotations;
begin
  FreeAndNil(FFormFields);
  FreeAndNil(FItems); // closes all annotations
  FItems := TObjectList.Create;
end;
function TPdfAnnotationList.GetCount: Integer;
begin
  Result := FPDFPage_GetAnnotCount(FPage.Handle);
end;
function TPdfAnnotationList.GetItem(Index: Integer): TPdfAnnotation;
var
  Annot: FPDF_ANNOTATION;
begin
  FPage.FDocument.CheckActive;
  if (Index < 0) or (Index >= FItems.Count) or (FItems[Index] = nil) then
  begin
    Annot := FPDFPage_GetAnnot(FPage.Handle, Index);
    if Annot = nil then
      raise EPdfArgumentOutOfRange.CreateResFmt(@RsArgumentsOutOfRange, ['Index']);
    while FItems.Count <= Index do
      FItems.Add(nil);
    FItems[Index] := TPdfAnnotation.Create(FPage, Annot);
  end;
  Result := FItems[Index] as TPdfAnnotation;
end;
procedure TPdfAnnotationList.DestroyingItem(Item: TPdfAnnotation);
var
  Index: Integer;
begin
  if (Item <> nil) and (FItems <> nil) then
  begin
    Index := FItems.IndexOf(Item);
    if Index <> -1 then
      FItems.List[Index] := nil; // Bypass the Items[] setter to not destroy the Item twice
  end;
end;
procedure TPdfAnnotationList.DestroyingFormField(FormField: TPdfFormField);
begin
  if FFormFields <> nil then
    FFormFields.DestroyingItem(FormField);
end;
function TPdfAnnotationList.GetFormFields: TPdfFormFieldList;
begin
  if FFormFields = nil then
    FFormFields := TPdfFormFieldList.Create(Self);
  Result := FFormFields;
end;
function TPdfAnnotationList.GetAnnotationsLoaded: Boolean;
begin
  Result := FItems.Count > 0;
end;
function TPdfAnnotationList.NewTextAnnotation(const Text: string; const R: TPdfRect): Boolean;
var
  Annot: FPDF_ANNOTATION;
  SingleR: FS_RECTF;
begin
  FPage.FDocument.CheckActive;
  SingleR.left := R.Left;
  SingleR.right := R.Right;
  // Page coordinates are upside down
  if R.Top < R.Bottom then
  begin
    SingleR.top := R.Bottom;
    SingleR.bottom := R.Top;
  end
  else
  begin
    SingleR.top := R.Top;
    SingleR.bottom := R.Bottom;
  end;

  Annot := FPDFPage_CreateAnnot(FPage.Handle, FPDF_ANNOT_TEXT);
  Result := Annot <> nil;
  if Result then
  begin
    FPDFAnnot_SetRect(Annot, @SingleR);
    FPDFAnnot_SetStringValue(Annot, 'Contents', PWideChar(Text));
  end;
end;
function TPdfAnnotationList.FindLink(Link: FPDF_LINK): TPdfAnnotation;
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
  begin
    Result := Items[I];
    if (Result.IsLink) and (FPDFAnnot_GetLink(Result.Handle) = Link) then
      Exit;
  end;
  Result := nil;
end;

{ TPdfFormFieldList }
constructor TPdfFormFieldList.Create(AAnnotations: TPdfAnnotationList);
var
  I: Integer;
begin
  inherited Create;
  FItems := TList.Create;
  for I := 0 to AAnnotations.Count - 1 do
    if AAnnotations[I].IsFormField then
      FItems.Add(AAnnotations[I].FormField);
end;
destructor TPdfFormFieldList.Destroy;
begin
  FItems.Free;
  inherited Destroy;
end;
function TPdfFormFieldList.GetCount: Integer;
begin
  Result := FItems.Count;
end;
function TPdfFormFieldList.GetItem(Index: Integer): TPdfFormField;
begin
  Result := TObject(FItems[Index]) as TPdfFormField;
end;
procedure TPdfFormFieldList.DestroyingItem(Item: TPdfFormField);
begin
  if (Item <> nil) and (FItems <> nil) then
    FItems.Extract(Item);
end;

{ TPdfAnnotation }
constructor TPdfAnnotation.Create(APage: TPdfPage; AHandle: FPDF_ANNOTATION);
var
  Action: FPDF_ACTION;
begin
  inherited Create;
  FPage := APage;
  FHandle := AHandle;
  FSubType := FPDFAnnot_GetSubtype(FHandle);
  FLinkType := altUnsupported;
  case FSubType of
    FPDF_ANNOT_WIDGET,
    FPDF_ANNOT_XFAWIDGET:
      FFormField := TPdfFormField.Create(Self);
    FPDF_ANNOT_LINK:
      begin
        Action := GetPdfLinkAction;
        if Action <> nil then
          FLinkType := TPdfAnnotationLinkType(FPDFAction_GetType(Action))
        else
        begin
          // If we have a Dest-Link then we treat it like a Goto Action-Link (see GetLinkGotoDestination)
          FLinkDest := FPDFLink_GetDest(FPage.FDocument.Handle, FPDFAnnot_GetLink(Handle));
          if FLinkDest <> nil then
            FLinkType := altGoto;
        end;
      end;
  end;
end;
destructor TPdfAnnotation.Destroy;
begin
  FreeAndNil(FFormField);
  if FHandle <> nil then
  begin
    FPDFPage_CloseAnnot(FHandle);
    FHandle := nil;
  end;
  if FPage.FAnnotations <> nil then
    FPage.FAnnotations.DestroyingItem(Self);
  inherited Destroy;
end;
function TPdfAnnotation.GetPdfLinkAction: FPDF_ACTION;
var
  Link: FPDF_LINK;
begin
  Result := nil;
  if FSubType = FPDF_ANNOT_LINK then
  begin
    Link := FPDFAnnot_GetLink(Handle);
    if Link <> nil then
      Result := FPDFLink_GetAction(Link);
  end;
end;
function TPdfAnnotation.IsLink: Boolean;
begin
  Result := FSubType = FPDF_ANNOT_LINK;
end;
function TPdfAnnotation.IsFormField: Boolean;
begin
  Result := FFormField <> nil;
end;
function TPdfAnnotation.GetFormField: TPdfFormField;
begin
  if FFormField = nil then
    raise EPdfException.CreateRes(@RsPdfAnnotationNotAFormFieldError);
  Result := FFormField;
end;
function TPdfAnnotation.GetAnnotationRect: TPdfRect;
var
  R: FS_RECTF;
begin
  if FPDFAnnot_GetRect(Handle, @R) <> 0 then
    Result := TPdfRect.New(R.left, R.top, R.right, R.bottom)
  else
    Result := TPdfRect.Empty;
end;
function TPdfAnnotation.GetLinkUri: string;
begin
  if LinkType = altURI then
    Result := FPage.GetPdfActionUriPath(GetPdfLinkAction)
  else
    Result := '';
end;
function TPdfAnnotation.GetLinkFileName: string;
begin
  if LinkType in [altRemoteGoto, altLaunch, altEmbeddedGoto] then // PDFium documentation is missing the PDFACTION_EMBEDDEDGOTO part.
    Result := FPage.GetPdfActionFilePath(GetPdfLinkAction)
  else
    Result := '';
end;
function TPdfAnnotation.GetLinkGotoDestination(var LinkGotoDestination: TPdfLinkGotoDestination; ARemoteDocument: TPdfDocument): Boolean;
var
  Action: FPDF_ACTION;
  Dest: FPDF_DEST;
  Doc: TPdfDocument;
  PageIndex: Integer;
  HasXVal, HasYVal, HasZoomVal: FPDF_BOOL;
  X, Y, Zoom: FS_FLOAT;
  ViewKind: TPdfLinkGotoDestinationViewKind;
  NumViewParams: LongWord;
  ViewParams: TPdfFloatArray;
begin
  Result := False;
  Action := GetPdfLinkAction;
  if ((Action <> nil) or (FLinkDest <> nil)) and (LinkType in [altGoto, altRemoteGoto, altEmbeddedGoto]) then
  begin
    Doc := FPage.FDocument;
    if LinkType = altRemoteGoto then
    begin
      // For RemoteGoto the FPDFAction_GetDest function must be called with the remote document
      if ARemoteDocument <> nil then
        raise EPdfException.CreateRes(@RsPdfAnnotationLinkRemoteGotoRequiresRemoteDocument);
      ARemoteDocument.CheckActive;
      Doc := ARemoteDocument;
    end;
    // If we have a Dest-Link instead of a Goto Action-Link we treat it as if it was a Goto Action-Link
    if FLinkDest <> nil then
      Dest := FLinkDest
    else
      Dest := FPDFAction_GetDest(Doc.Handle, Action);
    // Extract the information
    if Dest <> nil then
    begin
      PageIndex := FPDFDest_GetDestPageIndex(Doc.Handle, Dest);
      if PageIndex <> -1 then
      begin
        if FPDFDest_GetLocationInPage(Dest, HasXVal, HasYVal, HasZoomVal, X, Y, Zoom) <> 0 then
        begin
          SetLength(ViewParams, 4); // max. 4 params
          NumViewParams := 4;
          ViewKind := TPdfLinkGotoDestinationViewKind(FPDFDest_GetView(Dest, @NumViewParams, @ViewParams[0]));
          if NumViewParams > 4 then // range check
            NumViewParams := 4;
          SetLength(ViewParams, NumViewParams);
          LinkGotoDestination := TPdfLinkGotoDestination.Create(
            PageIndex,
            HasXVal <> 0, HasYVal <> 0, HasZoomVal <> 0,
            X, Y, Zoom,
            ViewKind, ViewParams
          );
          Result := True;
        end;
      end;
    end;
  end;
end;

{ TPdfFormField }
constructor TPdfFormField.Create(AAnnotation: TPdfAnnotation);
begin
  inherited Create;
  FAnnotation := AAnnotation;
  FPage := FAnnotation.FPage;
  FHandle := FAnnotation.Handle;
end;
destructor TPdfFormField.Destroy;
begin
  FAnnotation.FFormField := nil;
  FAnnotation.FPage.Annotations.DestroyingFormField(Self);
  inherited Destroy;
end;
function TPdfFormField.IsXFAFormField: Boolean;
begin
  Result := IS_XFA_FORMFIELD(FPDFAnnot_GetFormFieldType(FPage.FDocument.FormHandle, Handle));
end;
function TPdfFormField.GetReadOnly: Boolean;
begin
  Result := fffReadOnly in Flags;
end;
function TPdfFormField.GetFlags: TPdfFormFieldFlags;
var
  FormFlags: Integer;
begin
  FormFlags := FPDFAnnot_GetFormFieldFlags(FPage.FDocument.FormHandle, Handle);
  Result := [];
  if FormFlags <> FPDF_FORMFLAG_NONE then
  begin
    if FormFlags and FPDF_FORMFLAG_READONLY <> 0 then
      Include(Result, fffReadOnly);
    if FormFlags and FPDF_FORMFLAG_REQUIRED <> 0 then
      Include(Result, fffRequired);
    if FormFlags and FPDF_FORMFLAG_NOEXPORT <> 0 then
      Include(Result, fffNoExport);
    if FormFlags and FPDF_FORMFLAG_TEXT_MULTILINE <> 0 then
      Include(Result, fffTextMultiLine);
    if FormFlags and FPDF_FORMFLAG_TEXT_PASSWORD <> 0 then
      Include(Result, fffTextPassword);
    if FormFlags and FPDF_FORMFLAG_CHOICE_COMBO <> 0 then
      Include(Result, fffChoiceCombo);
    if FormFlags and FPDF_FORMFLAG_CHOICE_EDIT <> 0 then
      Include(Result, fffChoiceEdit);
    if FormFlags and FPDF_FORMFLAG_CHOICE_MULTI_SELECT <> 0 then
      Include(Result, fffChoiceMultiSelect);
  end;
end;
function TPdfFormField.GetName: string;
var
  Len: Integer;
begin
  Len := FPDFAnnot_GetFormFieldName(FPage.FDocument.FormHandle, Handle, nil, 0) div SizeOf(WideChar) - 1;
  if Len > 0 then
  begin
    SetLength(Result, Len);
    FPDFAnnot_GetFormFieldName(FPage.FDocument.FormHandle, Handle, PWideChar(Result), (Len + 1) * SizeOf(WideChar));
  end
  else
    Result := '';
end;
function TPdfFormField.GetAlternateName: string;
var
  Len: Integer;
begin
  Len := FPDFAnnot_GetFormFieldAlternateName(FPage.FDocument.FormHandle, Handle, nil, 0) div SizeOf(WideChar) - 1;
  if Len > 0 then
  begin
    SetLength(Result, Len);
    FPDFAnnot_GetFormFieldAlternateName(FPage.FDocument.FormHandle, Handle, PWideChar(Result), (Len + 1) * SizeOf(WideChar));
  end
  else
    Result := '';
end;
function TPdfFormField.GetFieldType: TPdfFormFieldType;
begin
  Result := TPdfFormFieldType(FPDFAnnot_GetFormFieldType(FPage.FDocument.FormHandle, Handle));
  if (Result < Low(TPdfFormFieldType)) or (Result > High(TPdfFormFieldType)) then
    Result := fftUnknown;
end;
function TPdfFormField.GetValue: string;
var
  Len: Integer;
begin
  Len := FPDFAnnot_GetFormFieldValue(FPage.FDocument.FormHandle, Handle, nil, 0) div SizeOf(WideChar) - 1;
  if Len > 0 then
  begin
    SetLength(Result, Len);
    FPDFAnnot_GetFormFieldValue(FPage.FDocument.FormHandle, Handle, PWideChar(Result), (Len + 1) * SizeOf(WideChar));
  end
  else
    Result := '';
end;
function TPdfFormField.GetExportValue: string;
var
  Len: Integer;
begin
  Len := FPDFAnnot_GetFormFieldExportValue(FPage.FDocument.FormHandle, Handle, nil, 0) div SizeOf(WideChar) - 1;
  if Len > 0 then
  begin
    SetLength(Result, Len);
    FPDFAnnot_GetFormFieldExportValue(FPage.FDocument.FormHandle, Handle, PWideChar(Result), (Len + 1) * SizeOf(WideChar));
  end
  else
    Result := '';
end;
function TPdfFormField.GetOptionCount: Integer;
begin
  Result := FPDFAnnot_GetOptionCount(FPage.FDocument.FormHandle, Handle);
  if Result < 0 then // annotation types that don't support options will return -1
    Result := 0;
end;
function TPdfFormField.GetOptionLabel(Index: Integer): string;
var
  Len: Integer;
begin
  Len := FPDFAnnot_GetOptionLabel(FPage.FDocument.FormHandle, Handle, Index, nil, 0) div SizeOf(WideChar) - 1;
  if Len > 0 then
  begin
    SetLength(Result, Len);
    FPDFAnnot_GetOptionLabel(FPage.FDocument.FormHandle, Handle, Index, PWideChar(Result), (Len + 1) * SizeOf(WideChar));
  end
  else
    Result := '';
end;
function TPdfFormField.IsOptionSelected(OptionIndex: Integer): Boolean;
begin
  Result := FPDFAnnot_IsOptionSelected(FPage.FDocument.FormHandle, Handle, OptionIndex) <> 0;
end;
function TPdfFormField.GetChecked: Boolean;
begin
  Result := FPDFAnnot_IsChecked(FPage.FDocument.FormHandle, Handle) <> 0;
end;
function TPdfFormField.GetControlCount: Integer;
begin
  Result := FPDFAnnot_GetFormControlCount(FPage.FDocument.FormHandle, Handle);
end;
function TPdfFormField.GetControlIndex: Integer;
begin
  Result := FPDFAnnot_GetFormControlIndex(FPage.FDocument.FormHandle, Handle);
end;
function TPdfFormField.BeginEditFormField: FPDF_ANNOTATION;
var
  AnnotPageIndex: Integer;
begin
  FPage.FDocument.CheckActive;
  // Obtain the currently focused form field/annotation so that we can restore the focus after
  // editing our form field.
  if FORM_GetFocusedAnnot(FPage.FDocument.FormHandle, AnnotPageIndex, Result) = 0 then
    Result := nil;
end;
procedure TPdfFormField.EndEditFormField(LastFocusedAnnot: FPDF_ANNOTATION);
begin
  // Restore the focus to the form field/annotation that had the focus before changing our form field.
  // If no previous form field was focused, kill the focus.
  if LastFocusedAnnot <> nil then
  begin
    if FORM_SetFocusedAnnot(FPage.FDocument.FormHandle, Handle) = 0 then
      FORM_ForceToKillFocus(FPage.FDocument.FormHandle);
    FPDFPage_CloseAnnot(LastFocusedAnnot);
  end
  else
    FORM_ForceToKillFocus(FPage.FDocument.FormHandle);
end;
procedure TPdfFormField.SetValue(const Value: string);
var
  LastFocusedAnnot: FPDF_ANNOTATION;
begin
  FPage.FDocument.CheckActive;
  if not ReadOnly then
  begin
    LastFocusedAnnot := BeginEditFormField();
    try
      if FORM_SetFocusedAnnot(FPage.FDocument.FormHandle, Handle) <> 0 then
      begin
        FORM_SelectAllText(FPage.FDocument.FormHandle, FPage.Handle);
        FORM_ReplaceSelection(FPage.FDocument.FormHandle, FPage.Handle, PWideChar(Value));
      end;
    finally
      EndEditFormField(LastFocusedAnnot);
    end;
  end;
end;
function TPdfFormField.SelectComboBoxOption(OptionIndex: Integer): Boolean;
begin
  Result := SelectListBoxOption(OptionIndex, True);
end;
function TPdfFormField.SelectListBoxOption(OptionIndex: Integer; Selected: Boolean): Boolean;
var
  LastFocusedAnnot: FPDF_ANNOTATION;
begin
  FPage.FDocument.CheckActive;
  Result := False;
  if not ReadOnly then
  begin
    LastFocusedAnnot := BeginEditFormField();
    try
      if FORM_SetFocusedAnnot(FPage.FDocument.FormHandle, Handle) <> 0 then
        Result := FORM_SetIndexSelected(FPage.FDocument.FormHandle, FPage.Handle, OptionIndex, Ord(Selected <> False)) <> 0;
    finally
      EndEditFormField(LastFocusedAnnot);
    end;
  end;
end;
procedure TPdfFormField.SetChecked(const Value: Boolean);
var
  LastFocusedAnnot: FPDF_ANNOTATION;
begin
  FPage.FDocument.CheckActive;
  if not ReadOnly and (FieldType in [fftCheckBox, fftRadioButton, fftXFACheckBox]) then
  begin
    if Value <> Checked then
    begin
      LastFocusedAnnot := BeginEditFormField();
      try
        if FORM_SetFocusedAnnot(FPage.FDocument.FormHandle, Handle) <> 0 then
        begin
          // Toggle the RadioButton/Checkbox by emulating "pressing the space bar".
          FORM_OnKeyDown(FPage.FDocument.FormHandle, FPage.Handle, Ord(' '), 0);
          FORM_OnChar(FPage.FDocument.FormHandle, FPage.Handle, Ord(' '), 0);
          FORM_OnKeyUp(FPage.FDocument.FormHandle, FPage.Handle, Ord(' '), 0);
        end;
      finally
        EndEditFormField(LastFocusedAnnot);
      end;
    end;
  end;
end;

{ TPdfLinkGotoDestination }
constructor TPdfLinkGotoDestination.Create(APageIndex: Integer; AXValid, AYValid, AZoomValid: Boolean;
  AX, AY, AZoom: Single; AViewKind: TPdfLinkGotoDestinationViewKind; const AViewParams: TPdfFloatArray);
begin
  inherited Create;
  FPageIndex := APageIndex;
  FXValid := AXValid;
  FYValid := AYValid;
  FZoomValid := AZoomValid;
  FX := AX;
  FY := AY;
  FZoom := AZoom;
  FViewKind := AViewKind;
  FViewParams := AViewParams;
end;

{ TPdfLinkInfo }
constructor TPdfLinkInfo.Create(ALinkAnnotation: TPdfAnnotation; const AWebLinkUrl: string);
begin
  inherited Create;
  FLinkAnnotation := ALinkAnnotation;
  FWebLinkUrl := AWebLinkUrl;
end;
function TPdfLinkInfo.IsAnnontation: Boolean;
begin
  Result := FLinkAnnotation <> nil;
end;
function TPdfLinkInfo.IsWebLink: Boolean;
begin
  Result := FLinkAnnotation = nil;
end;
function TPdfLinkInfo.GetLinkFileName: string;
begin
  if FLinkAnnotation <> nil then
    Result := FLinkAnnotation.LinkFileName;
end;
function TPdfLinkInfo.GetLinkType: TPdfAnnotationLinkType;
begin
  if FLinkAnnotation <> nil then
    Result := FLinkAnnotation.LinkType
  else if FWebLinkUrl <> '' then
    Result := altURI
  else
    Result := altUnsupported;
end;
function TPdfLinkInfo.GetLinkUri: string;
begin
  if FLinkAnnotation <> nil then
    Result := FLinkAnnotation.LinkUri
  else
    Result := FWebLinkUrl;
end;
function TPdfLinkInfo.GetLinkGotoDestination(var LinkGotoDestination: TPdfLinkGotoDestination;
  ARemoteDocument: TPdfDocument): Boolean;
begin
  if FLinkAnnotation <> nil then
    Result := FLinkAnnotation.GetLinkGotoDestination(LinkGotoDestination, ARemoteDocument)
  else
    Result := False;
end;

{ TPdfPageWebLinksInfo }
constructor TPdfPageWebLinksInfo.Create(APage: TPdfPage);
begin
  inherited Create;
  FPage := APage;
  GetPageWebLinks;
end;
procedure TPdfPageWebLinksInfo.GetPageWebLinks;
var
  LinkIndex, LinkCount: Integer;
  RectIndex, RectCount: Integer;
begin
  if FPage <> nil then
  begin
    LinkCount := FPage.GetWebLinkCount;
    SetLength(FWebLinksRects, LinkCount);
    for LinkIndex := 0 to LinkCount - 1 do
    begin
      RectCount := FPage.GetWebLinkRectCount(LinkIndex);
      SetLength(FWebLinksRects[LinkIndex], RectCount);
      for RectIndex := 0 to RectCount - 1 do
        FWebLinksRects[LinkIndex][RectIndex] := FPage.GetWebLinkRect(LinkIndex, RectIndex);
    end;
  end;
end;
function TPdfPageWebLinksInfo.GetWebLinkIndex(X, Y: Double): Integer;
var
  RectIndex: Integer;
  Pt: TPdfPoint;
begin
  if FPage <> nil then
  begin
    Pt.X := X;
    Pt.Y := Y;
    for Result := 0 to Length(FWebLinksRects) - 1 do
      for RectIndex := 0 to Length(FWebLinksRects[Result]) - 1 do
        if FWebLinksRects[Result][RectIndex].PtIn(Pt) then
          Exit;
  end;
  Result := -1;
end;
function TPdfPageWebLinksInfo.GetCount: Integer;
begin
  Result := Length(FWebLinksRects);
end;
function TPdfPageWebLinksInfo.GetRect(Index: Integer): TPdfRectArray;
begin
  Result := FWebLinksRects[Index];
end;
function TPdfPageWebLinksInfo.GetURL(Index: Integer): string;
begin
  Result := FPage.GetWebLinkURL(Index);
end;
function TPdfPageWebLinksInfo.IsWebLinkAt(X, Y: Double): Boolean;
begin
  Result := GetWebLinkIndex(X, Y) <> -1;
end;
function TPdfPageWebLinksInfo.IsWebLinkAt(X, Y: Double; var Url: string): Boolean;
var
  Index: Integer;
begin
  Index := GetWebLinkIndex(X, Y);
  Result := Index <> -1;
  if Result then
    Url := FPage.GetWebLinkURL(Index)
  else
    Url := '';
end;
{$IFDEF MSWINDOWS}
{ TPdfDocumentPrinter }
constructor TPdfDocumentPrinter.Create;
begin
  inherited Create;
  FFitPageToPrintArea := True;
end;
function TPdfDocumentPrinter.IsPortraitOrientation(AWidth, AHeight: Integer): Boolean;
begin
  Result := AHeight > AWidth;
end;
procedure TPdfDocumentPrinter.GetPrinterBounds;
begin
  FPaperSize.cx := GetDeviceCaps(FPrinterDC, PHYSICALWIDTH);
  FPaperSize.cy := GetDeviceCaps(FPrinterDC, PHYSICALHEIGHT);
  FPrintArea.cx := GetDeviceCaps(FPrinterDC, HORZRES);
  FPrintArea.cy := GetDeviceCaps(FPrinterDC, VERTRES);
  FMargins.X := GetDeviceCaps(FPrinterDC, PHYSICALOFFSETX);
  FMargins.Y := GetDeviceCaps(FPrinterDC, PHYSICALOFFSETY);
end;
function TPdfDocumentPrinter.BeginPrint(const AJobTitle: string): Boolean;
begin
  Inc(FBeginPrintCounter);
  if FBeginPrintCounter = 1 then
  begin
    Result := PrinterStartDoc(AJobTitle);
    if Result then
    begin
      FPrinterDC := GetPrinterDC;
      GetPrinterBounds;
      FPrintPortraitOrientation := IsPortraitOrientation(FPaperSize.cx, FPaperSize.cy);
    end
    else
    begin
      FPrinterDC := 0;
      Dec(FBeginPrintCounter);
    end;
  end
  else
    Result := True;
end;
procedure TPdfDocumentPrinter.EndPrint;
begin
  Dec(FBeginPrintCounter);
  if FBeginPrintCounter = 0 then
  begin
    if FPrinterDC <> 0 then
    begin
      FPrinterDC := 0;
      PrinterEndDoc;
    end;
  end;
end;
function TPdfDocumentPrinter.Print(ADocument: TPdfDocument): Boolean;
begin
  if ADocument <> nil then
    Result := Print(ADocument, 0, ADocument.PageCount - 1)
  else
    Result := False;
end;
function TPdfDocumentPrinter.Print(ADocument: TPdfDocument; AFromPageIndex, AToPageIndex: Integer): Boolean;
var
  PageIndex: Integer;
  WasPageLoaded: Boolean;
  PdfPage: TPdfPage;
  PagePortraitOrientation: Boolean;
  X, Y, W, H: Integer;
  PrintedPageNum, PrintPageCount: Integer;
begin
  Result := False;
  if ADocument = nil then
    Exit;
  if AFromPageIndex < 0 then
    raise EPdfArgumentOutOfRange.CreateResFmt(@RsArgumentsOutOfRange, ['FromPage', AFromPageIndex]);
  if (AToPageIndex < AFromPageIndex) or (AToPageIndex >= ADocument.PageCount) then
    raise EPdfArgumentOutOfRange.CreateResFmt(@RsArgumentsOutOfRange, ['ToPage', AToPageIndex]);
  PrintedPageNum := 0;
  PrintPageCount := AToPageIndex - AFromPageIndex + 1;
  if BeginPrint then
  begin
    try
      if ADocument.FForm <> nil then
        FORM_DoDocumentAAction(ADocument.FForm, FPDFDOC_AACTION_WP); // BeforePrint
      for PageIndex := AFromPageIndex to AToPageIndex do
      begin
        PdfPage := nil;
        WasPageLoaded := ADocument.IsPageLoaded(PageIndex);
        try
          PdfPage := ADocument.Pages[PageIndex];
          PagePortraitOrientation := IsPortraitOrientation(Trunc(PdfPage.Width), Trunc(PdfPage.Height));
          if FitPageToPrintArea then
          begin
            X := 0;
            Y := 0;
            W := FPrintArea.cx;
            H := FPrintArea.cy;
          end
          else
          begin
            X := -FMargins.X;
            Y := -FMargins.Y;
            W := FPaperSize.cx;
            H := FPaperSize.cy;
          end;
          if PagePortraitOrientation <> FPrintPortraitOrientation then
          begin
            SwapInts(X, Y);
            SwapInts(W, H);
          end;
          // Print page
          PrinterStartPage;
          try
            if (W > 0) and (H > 0) then
              InternPrintPage(PdfPage, X, Y, W, H);
          finally
            PrinterEndPage;
          end;
          Inc(PrintedPageNum);
          if Assigned(OnPrintStatus) then
            OnPrintStatus(Self, PrintedPageNum, PrintPageCount);
        finally
          if not WasPageLoaded and (PdfPage <> nil) then
            PdfPage.Close; // release memory
        end;
        if ADocument.FForm <> nil then
          FORM_DoDocumentAAction(ADocument.FForm, FPDFDOC_AACTION_DP); // AfterPrint
      end;
    finally
      EndPrint;
    end;
    Result := True;
  end;
end;
procedure TPdfDocumentPrinter.InternPrintPage(APage: TPdfPage; X, Y, Width, Height: Double);
  function RoundToInt(Value: Double): Integer;
  var
    F: Double;
  begin
    Result := Trunc(Value);
    F := Frac(Value);
    if F < 0 then
    begin
      if F <= -0.5 then
        Result := Result - 1;
    end
    else if F >= 0.5 then
      Result := Result + 1;
  end;
var
  PageWidth, PageHeight: Double;
  PageScale, PrintScale: Double;
  ScaledWidth, ScaledHeight: Double;
begin
  PageWidth := APage.Width;
  PageHeight := APage.Height;
  PageScale := PageHeight / PageWidth;
  PrintScale := Height / Width;
  ScaledWidth := Width;
  ScaledHeight := Height;
  if PageScale > PrintScale then
    ScaledWidth := Width * (PrintScale / PageScale)
  else
    ScaledHeight := Height * (PageScale / PrintScale);
  X := X + (Width - ScaledWidth) / 2;
  Y := Y + (Height - ScaledHeight) / 2;
  APage.Draw(
    FPrinterDC,
    RoundToInt(X), RoundToInt(Y), RoundToInt(ScaledWidth), RoundToInt(ScaledHeight),
    prNormal, [proPrinting, proAnnotations]
  );
end;

function PageToBitmap(aPage: TPdfPage; sPath: String; nDPI: Integer = 600): Boolean;
var
  bmp: TBitmap;
  nW, nH: Integer;
begin
  Result := false;

  try
    bmp := TBitmap.Create;
    try
      nW := Round(aPage.Width / 72 * nDPI);
      nH := Round(aPage.Height / 72 * nDPI);

      bmp.PixelFormat := pf32bit;
      bmp.SetSize(nW, nH);

      aPage.Draw(bmp.Canvas.Handle, 0, 0, bmp.Width, bmp.Height, prNormal, [proPrinting]);
      bmp.SaveToFile(sPath);
      Result := FileExists(sPath);
    finally
      bmp.Free;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. PageToBitmap()');
  end;
end;

function PageToPng(aPage: TPdfPage; sPath: String; nDPI: Integer = 600): Boolean;
var
  bmp: TBitmap;
  png: TPngImage;
  nW, nH: Integer;
begin
  Result := false;

  try
    bmp := TBitmap.Create;
    png := TPngImage.Create;
    try
      nW := Round(aPage.Width / 72 * nDPI);
      nH := Round(aPage.Height / 72 * nDPI);

      bmp.PixelFormat := pf32bit;
      bmp.SetSize(nW, nH);

      aPage.Draw(bmp.Canvas.Handle, 0, 0, nW, nH, prNormal, [proPrinting]);
      png.Assign(bmp);
      png.SaveToFile(sPath);
      Result := FileExists(sPath);
    finally
      png.Free;
      bmp.Free;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. PageToPng()');
  end;
end;

function GetPageOcrText(aPage: TPdfPage; sOcrMdPath, sTaskDir: String; sParam: String = ''; nDPI: Integer = 150): String;
var
  sImgPath, sTxtPath: String;
begin
  Result := '';
  try
    sImgPath := sTaskDir + '$img.bmp';
    sTxtPath := sTaskDir + '$img.txt';
    try
      if PageToPng(aPage, sImgPath, nDPI) then
      begin
        if FileExists(sOcrMdPath) then
        begin
          if sParam <> '' then
            sParam := Trim(sParam) + ' ';
  //        ExecuteAppWaitUntilTerminate(sOcrPath,
  //          Format('-r "%s" "%s"', [sImgPath, sTxtPath]), SW_HIDE, 10000);
          ExecuteAppWaitUntilTerminate(sOcrMdPath, sParam + Format('"%s" "%s"', [sImgPath, sTxtPath]), SW_HIDE, 10000);

          if FileExists(sTxtPath) then
            Result := ExtractTextSafe(sTxtPath);
        end;
      end;
    finally
      DeleteFile(sImgPath);
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetPageOcrText()');
  end;
end;

{$ENDIF MSWINDOWS}
initialization
  {$IFDEF FPC}
  InitCriticalSection(PDFiumInitCritSect);
  InitCriticalSection(FFITimersCritSect);
  {$ELSE}
  InitializeCriticalSectionAndSpinCount(PDFiumInitCritSect, 4000);
  InitializeCriticalSectionAndSpinCount(FFITimersCritSect, 4000);
  {$ENDIF FPC}
finalization
  {$IFDEF FPC}
  DoneCriticalSection(FFITimersCritSect);
  DoneCriticalSection(PDFiumInitCritSect);
  {$ELSE}
  DeleteCriticalSection(FFITimersCritSect);
  DeleteCriticalSection(PDFiumInitCritSect);
  {$ENDIF FPC}
end.
