unit uFormFill;
// ---------------------------------------------------------------------
//
// PDFium VCL Demo: PDF Form Fill
//
// Demonstrates the CORRECT workflow for filling AcroForm fields so that
// values are visible in all PDF readers without requiring focus, and are
// preserved when flattening.
//
// ROOT CAUSE OF THE REPORTED PROBLEM
// -----------------------------------
// FormField[i] := value  updates only the /V (value) entry in the
// annotation dictionary.  PDF readers use the /AP (appearance stream)
// for rendering without focus, and FPDFPage_Flatten bakes /AP into
// the page content.  If /AP is not regenerated after writing /V, the
// old (possibly empty) appearance is used, so:
//   - values appear only on focus  (reader regenerates from /V on hover)
//   - flattening loses all content  (FPDFPage_Flatten finds empty /AP)
//
// SOLUTION
// ---------
// After setting all field values, call RenderPage() for every page.
// This internally calls FPDF_FFLDraw, which causes PDFium to regenerate
// the /AP appearance stream for every widget on the page.
// Only then save or flatten.
//
// The key function is GenerateAppearances below.
//
// ---------------------------------------------------------------------
{$I ..\..\..\Lib\PDFiumVcl.inc}

interface

uses
{$IFDEF XE2+}
  Winapi.Windows,
  System.SysUtils,
  System.Classes,
  Vcl.Graphics,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Dialogs,
  Vcl.StdCtrls,
  Vcl.ExtCtrls,
  Vcl.ComCtrls,
{$ELSE}
  Windows,
  SysUtils,
  Classes,
  Graphics,
  Controls,
  Forms,
  Dialogs,
  StdCtrls,
  ExtCtrls,
  ComCtrls,
{$ENDIF}
  PDFium;

type
  // Maps a ListView row to its page+field position in the document.
  TFieldEntry = record
    PageIndex : Integer;  // 0-based PDF page index
    FieldIndex: Integer;  // 0-based annotation index on that page
  end;

  TFormMain = class(TForm)
    Pdf          : TPdf;
    OpenDialog   : TOpenDialog;
    SaveDialog   : TSaveDialog;
    PanelTop     : TPanel;
    PanelBottom  : TPanel;
    PanelMain    : TPanel;
    PanelLeft    : TPanel;
    Splitter     : TSplitter;
    PanelRight   : TPanel;
    ListView     : TListView;
    ButtonOpen   : TButton;
    ButtonSave   : TButton;
    ButtonFlatten: TButton;
    LabelCapField: TLabel;
    LabelCapType : TLabel;
    LabelCapPage : TLabel;
    LabelCapValue: TLabel;
    LabelFieldName: TLabel;
    LabelFieldType: TLabel;
    LabelPageNum : TLabel;
    EditValue    : TEdit;
    ButtonApply  : TButton;
    MemoInfo     : TMemo;
    LabelStatus  : TLabel;

    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure ButtonOpenClick(Sender: TObject);
    procedure ButtonSaveClick(Sender: TObject);
    procedure ButtonFlattenClick(Sender: TObject);
    procedure ButtonApplyClick(Sender: TObject);
    procedure ListViewSelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);

  private
    FFieldMap: array of TFieldEntry;

    procedure LoadFields;
    procedure GenerateAppearances;
    procedure ClearFieldEditor;
    procedure UpdateStatus(const Msg: string);
    function  FieldTypeName(FieldType: TPdfFormFieldType): string;
    function  GetSelectedEntry(out Entry: TFieldEntry): Boolean;
  end;

var
  FormMain: TFormMain;

implementation

{$R *.dfm}

const
  CInfoText =
    'HOW TO FILL PDF FORM FIELDS CORRECTLY'#13#10 +
    #13#10 +
    'Problem'#13#10 +
    '  After setting field values, values appear'#13#10 +
    '  only on focus, or are lost when flattening.'#13#10 +
    #13#10 +
    'Root Cause'#13#10 +
    '  FormField[i] := value  writes only the /V'#13#10 +
    '  entry. PDF readers render the /AP appearance'#13#10 +
    '  stream for display; FPDFPage_Flatten bakes'#13#10 +
    '  /AP into the page as static content.'#13#10 +
    '  FPDF_FFLDraw renders to a bitmap only —'#13#10 +
    '  it does NOT write /AP back to the document.'#13#10 +
    #13#10 +
    'Solution — GenerateFormAppearances'#13#10 +
    '  Focuses each widget so FFL loads /V into its'#13#10 +
    '  buffer, then writes a sentinel to /V. On'#13#10 +
    '  CommitData: buffer≠/V → ResetAppearance'#13#10 +
    '  writes a correct /AP stream. Page view is'#13#10 +
    '  reset per field to bound memory usage.'#13#10 +
    #13#10 +
    'Workflow'#13#10 +
    '  1. Open PDF  (FormFill := True)'#13#10 +
    '  2. Set field values  (FormField[i] := v)'#13#10 +
    '  3. GenerateFormAppearances   <-- KEY STEP'#13#10 +
    '  4a. SaveAs  — editable, values visible'#13#10 +
    '  4b. FlattenAllPages + SaveAs — static';

// ---------------------------------------------------------------------------

procedure TFormMain.FormCreate(Sender: TObject);
begin
  ButtonSave.Enabled    := False;
  ButtonFlatten.Enabled := False;
  ClearFieldEditor;
  MemoInfo.Text := CInfoText;
  UpdateStatus('Open a PDF file containing AcroForm fields.');
end;

procedure TFormMain.FormDestroy(Sender: TObject);
begin
  SetLength(FFieldMap, 0);
end;

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

procedure TFormMain.ClearFieldEditor;
begin
  LabelFieldName.Caption := '-';
  LabelFieldType.Caption := '-';
  LabelPageNum.Caption   := '-';
  EditValue.Text         := '';
  EditValue.Enabled      := False;
  ButtonApply.Enabled    := False;
end;

function TFormMain.FieldTypeName(FieldType: TPdfFormFieldType): string;
begin
  case FieldType of
    fiPushButton : Result := 'Push Button';
    fiCheckBox   : Result := 'Check Box';
    fiRadioButton: Result := 'Radio Button';
    fiComboBox   : Result := 'Combo Box';
    fiListBox    : Result := 'List Box';
    fiTextField  : Result := 'Text Field';
    fiSignature  : Result := 'Signature';
  else
    Result := 'Unknown';
  end;
end;

// ---------------------------------------------------------------------------
// LoadFields
//
// Iterates all pages and all form fields.  Builds the ListView and the
// FFieldMap[] lookup array (maps row index → page + field index).
// ---------------------------------------------------------------------------
procedure TFormMain.LoadFields;
var
  PageIdx, FieldIdx, Total: Integer;
  Info : TPdfFormFieldInfo;
  Item : TListItem;
begin
  ListView.Items.BeginUpdate;
  try
    ListView.Items.Clear;
    SetLength(FFieldMap, 0);
    Total := 0;

    for PageIdx := 0 to Pdf.PageCount - 1 do
    begin
      Pdf.PageNumber := PageIdx + 1;  // PageNumber is 1-based
      for FieldIdx := 0 to Pdf.FormFieldCount - 1 do
      begin
        Info := Pdf.FormFieldInfo[FieldIdx];

        Item := ListView.Items.Add;
        Item.Caption := Info.Name;
        Item.SubItems.Add(FieldTypeName(Info.FieldType));  // [0]
        Item.SubItems.Add(IntToStr(PageIdx + 1));           // [1] display 1-based
        Item.SubItems.Add(Pdf.FormField[FieldIdx]);         // [2]

        SetLength(FFieldMap, Total + 1);
        FFieldMap[Total].PageIndex  := PageIdx;  // stored 0-based for math
        FFieldMap[Total].FieldIndex := FieldIdx;
        Inc(Total);
      end;
    end;
  finally
    ListView.Items.EndUpdate;
  end;
end;

// ---------------------------------------------------------------------------
// GenerateAppearances
//
// Delegates to Pdf.GenerateFormAppearances, which iterates every page and
// calls RenderPage() — triggering FPDF_FFLDraw — so PDFium writes a fresh
// /AP appearance stream into each widget annotation from the current /V.
//
// This is the component-level solution to the "values visible only on focus"
// problem.  See TPdf.GenerateFormAppearances in PDFium.pas for full details.
//
// Call AFTER all FormField[] assignments and BEFORE SaveAs / FlattenAllPages.
// ---------------------------------------------------------------------------
procedure TFormMain.GenerateAppearances;
begin
  Pdf.GenerateFormAppearances;
end;

// ---------------------------------------------------------------------------

function TFormMain.GetSelectedEntry(out Entry: TFieldEntry): Boolean;
var
  Idx: Integer;
begin
  Result := False;
  if ListView.Selected = nil then Exit;
  Idx := ListView.Selected.Index;
  if (Idx < 0) or (Idx >= Length(FFieldMap)) then Exit;
  Entry  := FFieldMap[Idx];
  Result := True;
end;

// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------

procedure TFormMain.ListViewSelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
var
  Entry: TFieldEntry;
  Info : TPdfFormFieldInfo;
begin
  if not Selected or (Item = nil) or not GetSelectedEntry(Entry) then
  begin
    ClearFieldEditor;
    Exit;
  end;

  Pdf.PageNumber := Entry.PageIndex + 1;  // PageNumber is 1-based
  Info := Pdf.FormFieldInfo[Entry.FieldIndex];

  LabelFieldName.Caption := Info.Name;
  LabelFieldType.Caption := FieldTypeName(Info.FieldType);
  LabelPageNum.Caption   := IntToStr(Entry.PageIndex + 1);
  EditValue.Text         := Pdf.FormField[Entry.FieldIndex];

  // Push buttons and signatures cannot be edited as plain text
  EditValue.Enabled   := not (Info.FieldType in [fiPushButton, fiSignature]);
  ButtonApply.Enabled := EditValue.Enabled;
end;

procedure TFormMain.ButtonApplyClick(Sender: TObject);
var
  Entry: TFieldEntry;
begin
  if not GetSelectedEntry(Entry) then Exit;
  try
    Pdf.PageNumber := Entry.PageIndex + 1;  // PageNumber is 1-based
    Pdf.FormField[Entry.FieldIndex] := EditValue.Text;

    // Refresh the Value column in the list view
    ListView.Selected.SubItems[2] := EditValue.Text;

    UpdateStatus('Applied: "' + EditValue.Text + '"  →  ' +
      ListView.Selected.Caption);
  except
    on E: Exception do
      ShowMessage('Error setting field value: ' + E.Message);
  end;
end;

procedure TFormMain.ButtonOpenClick(Sender: TObject);
var
  Total: Integer;
begin
  OpenDialog.Filter := 'PDF Files (*.pdf)|*.pdf|All Files (*.*)|*.*';
  if not OpenDialog.Execute then Exit;
  try
    Pdf.Active   := False;
    Pdf.FileName := OpenDialog.FileName;
    Pdf.FormFill := True;   // REQUIRED: initialises the FPDF_FORMHANDLE
    Pdf.Active   := True;

    LoadFields;
    Total := Length(FFieldMap);

    ButtonSave.Enabled    := True;
    ButtonFlatten.Enabled := True;
    UpdateStatus(Format('"%s"  |  Pages: %d  |  Fields: %d',
      [ExtractFileName(OpenDialog.FileName), Pdf.PageCount, Total]));

    if Total = 0 then
      ShowMessage('This PDF contains no AcroForm fields.');
  except
    on E: Exception do
      ShowMessage('Error opening PDF: ' + E.Message);
  end;
end;

procedure TFormMain.ButtonSaveClick(Sender: TObject);
begin
  SaveDialog.Filter   := 'PDF Files (*.pdf)|*.pdf';
  SaveDialog.FileName :=
    ChangeFileExt(ExtractFileName(Pdf.FileName), '_filled.pdf');
  if not SaveDialog.Execute then Exit;
  try
    UpdateStatus('Generating appearance streams...');
    Application.ProcessMessages;

    // Regenerate /AP streams so values are visible without focus
    GenerateAppearances;

    Pdf.SaveAs(SaveDialog.FileName);

    UpdateStatus('Saved: ' + SaveDialog.FileName);
    ShowMessage('Saved successfully.' + sLineBreak +
      'Field values are visible in all PDF readers without requiring focus.');
  except
    on E: Exception do
      ShowMessage('Error saving: ' + E.Message);
  end;
end;

procedure TFormMain.ButtonFlattenClick(Sender: TObject);
begin
  SaveDialog.Filter   := 'PDF Files (*.pdf)|*.pdf';
  SaveDialog.FileName :=
    ChangeFileExt(ExtractFileName(Pdf.FileName), '_flattened.pdf');
  if not SaveDialog.Execute then Exit;
  try
    // Step 1 — Regenerate /AP streams (REQUIRED before flattening).
    // Without this, FPDFPage_Flatten finds empty /AP and silently loses
    // all field values.
    UpdateStatus('Generating appearance streams...');
    Application.ProcessMessages;
    Pdf.GenerateFormAppearances;

    // Step 2 — Flatten every page via the component helper.
    UpdateStatus('Flattening...');
    Application.ProcessMessages;
    if not Pdf.FlattenAllPages then
      raise EPdfError.Create('FlattenAllPages failed on one or more pages');

    // Step 3 — Save
    Pdf.SaveAs(SaveDialog.FileName);

    UpdateStatus('Flattened: ' + SaveDialog.FileName);
    ShowMessage('Flattened and saved successfully.' + sLineBreak +
      'Fields are now permanent, non-editable page content.');
  except
    on E: Exception do
      ShowMessage('Error: ' + E.Message);
  end;
end;

end.
