Teknisk artikel

Sätt formulärfältvärden i en inläst PDF med Delphi

HotPDF Delphi Component fyller ett befintligt AcroForm-fält i en inläst PDF genom THotPDF.SetFormFieldValue, adresserat antingen med nollbaserat fältindex eller med fullt kvalificerat fältnamn. Att skriva den nya /V-posten är den lätta delen; vad som gör anropet tillförlitligt på verkliga formulär är att samma metod också håller tre delar av tillståndet konsekventa som är osynliga tills de går fel: fältets avkodade identitet så att ett icke-ASCII-namn alls går att hitta, tillståndet /AS för utseende på kryssrute- och radiowidgets, och urvalsindexarrayen /I på valfält. Den synliga utseendeströmmen är ett separat, uttryckligt steg genom EnsureLoadedFieldAppearanceStream

Scenariot är det vardagliga: en kund skickar dig sitt eget formulär, en deklaration, en försäkringsanmälan, en inköpsorder som någon byggde i Acrobat för år sedan, och din Delphi-applikation ska fylla det från en databas och lämna tillbaka en fil som öppnas korrekt överallt. Du styr inte över hur formuläret skapades. Fältnamn kan vara UTF-16-kodade, kryssrutans exportvärde kan vara 2 i stället för Yes, och kombinationsrutor kan använda [export display]-optionspar. Var och en av de detaljerna har en regel i ISO 32000-1, och varje regel är något SetFormFieldValue nu hanterar åt dig. Den här artikeln handlar om vad den gör, varför, och var den slutar. För syskonproblemet att skapa fält som ännu inte finns, se att lägga till AcroForm-fält i en inläst PDF i Delphi

Varför hittar SetFormFieldValue inte ett fält med ett icke-ASCII-namn?

Före v2.752.1 var svaret kodning: fältet låg i filen under ett hexadecimalt UTF-16BE-namn, och namncachen lagrade hex-stavningen i stället för texten. ISO 32000-1 §12.7.3.1 definierar det partiella fältnamnet /T som en textsträng, och §7.9.2.2 säger att en textsträng får vara UTF-16BE med en inledande byteordningsmarkör FE FF. Författarverktyg serialiserar rutinmässigt sådana namn som hex-strängar enligt §7.3.4.3, så ett fält som heter Straße anländer som <FEFF005300740072006100DF0065>. Inuti HotPDF håller THPDFStringObject.Value den råa hexadecimala texten när IsHexadecimal är satt, vilket är precis vad du vill ha för en förlustfri rundtur av originalordboken och precis vad du inte vill ha som uppslagsnyckel. HPDFLoadedFormTextName skiljer de två sakerna åt. När relationscachen byggs går varje /T-värde genom den: om strängobjektet är hexadecimalt återställer HPDFHexToBytes bytesekvensen; om byten börjar med FE FF och har jämn längd avkodas payloaden som UTF-16BE och kodas om som UTF-8; resultatet fogas sedan samman med sitt föräldernamn med en punkt för att bilda det fullt kvalificerade namn som §12.7.3.1 beskriver, så ett barn som heter City under en förälder som heter Address registreras som Address.City. Cachenyckeln normaliseras till gemener, vilket gör att SetFormFieldValue('address.city', ...) också lyckas; det är en bekvämlighet utöver standarden, eftersom specifikationen behandlar namn som skiftlägeskänsliga. Avgörande är att bara cachenyckeln ändras. /T-objektet i fältordboken behåller sin hexadecimala kodning, så att spara dokumentet skriver inte om identiteten på ett fält du bara fyllde i

Hur HotPDF löser upp icke-ASCII-namn i AcroForm: HPDFHexToBytes återställer UTF-16BE-payloaden bakom en hexadecimal /T-sträng, byteordningsmarkören FE FF avkodas och kodas om som UTF-8, och det kvalificerade namnet fogas till sin förälder så att både Applicant.FullName och ett fält som heter Straße hamnar i uppslagscachen
Bara cachenyckeln ändras: fältordboken behåller sin hexadecimala kodning, uppslag normaliseras till gemener som en bekvämlighet utöver standarden, och att spara dokumentet skriver aldrig om identiteten på ett fält du bara fyllde i
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Kvalificerade namn avkodas från UTF-16BE /T-strängar och
    // fogas samman med punkter, så nästlade och icke-ASCII-namn löses upp
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

    // Värden som inte är Latin-1 färdas som FEFF-prefixad UTF-16BE-hex
    // och skrivs som en PDF-hexsträng
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

Vad skriver SetFormFieldValue egentligen?

Båda overloaderna kör samma fem steg: lokalisera fältordboken, skriv /V genom HPDFSetDictFormValue, stäm av urvalsindex för valfält, markera ordboken smutsig, stäm av knapparnas utseendetillstånd, och registrera till sist fältindexet genom NoteLoadedFormFieldDirty. Det sista steget spelar roll om formuläret bär beräkningsskript, eftersom den smutsiga mängden är vad den parameterlösa overloaden av RecalculateLoadedFormFieldsIncremental konsumerar för att bara köra om de beräkningar som transitivt läser ett ändrat fält. HPDFSetDictFormValue är själv försiktig med vilken objekttyp den ersätter. Om den befintliga /V är ett namnobjekt, vilket kryssrute- och radiofält använder för sitt exportvärde, skrivs det nya värdet som ett namn, aldrig som en sträng, eftersom PDF-namn är ASCII-only till sin konstruktion. Annars skriver den ett strängobjekt och granskar värdet du skickade: en sträng som börjar med FEFF, har jämn längd och enbart består av hex-siffror behandlas som §7.9.2.2:s UTF-16BE-trådform och lagras med IsHexadecimal satt, så den serialiseras som <FEFF...> i stället för som en literal (FEFF...). Det är mekanismen som raden för City ovan bygger på; varje annan sträng lagras som en literal sträng med de byte du gav den, så för vanlig latinsk text skickar du vanlig text

Varför behåller en kryssruta sin gamla bock efter att värdet ändrats?

Därför att värdet ensamt inte avgör vad som ritas för ett knappfält. ISO 32000-1 §12.7.4.2.3 föreskriver att en kryssrutewidget bär ett utseendetillstånd /AS som namnger vilken ström i /AP /N som visas just nu, och viewers målar från /AS, inte från /V. Om du ändrar /V till Yes men lämnar /AS på Off är filen internt självmotsägande, och en flattening bak​ar gärna in det inaktuella icke-ikryssade utseendet i sidan medan formulärdata säger ikryssad. ReconcileLoadedButtonAppearanceStates finns för att täppa till den luckan: för ett fält vars /FT är Btn besöker den fältordboken själv och varje post i dess /Kids-array, läser på-lägets namn från /AP /N, och skriver om /AS till det namnet när det matchar fältvärdet eller till Off när det inte gör det

Varför en HotPDF-kryssruta behåller sin gamla bock när bara /V ändras: viewers målar från utseendetillståndet /AS in i /AP /N, så ReconcileLoadedButtonAppearanceStates besöker fältet och varje barn, läser på-lägets namn som den första nyckeln utom Off, och skriver om /AS vid matchning eller till Off annars
Radiogrupper jämför varje barn mot förälderns värde som InheritedButtonValue återställer genom att gå i /Parent-kedjan, så att sätta gruppen till ett exportvärde slår på exakt den widgeten och av alla syskon

Två detaljer från verkliga formulär formade fixen i v2.752.3. För det första får en normal utseendeordbok innehålla bara på-läget; §12.7.4.2.3 kallar av-lägets utseende Off, men författarverktyg utelämnar ofta dess ström och låter viewern rita ingenting. Tidigare kod gav upp när ordboken hade färre än två poster, så de enkel tillstånd-kryssrutorna behöll tyst sin gamla bock. Kontrollen är nu helt enkelt att ordboken inte är tom, och på-lägets namn tas som den första nyckeln som inte är Off. För det andra är på-lägets namn vad författaren än valde. Riktiga formulär använder 2, Yes, On eller ett lokaliserat ord, så jämförelsen sker mot den faktiska nyckeln, skiftlägesokänsligt, aldrig mot ett hårdkodat Yes. Radioknappar lägger till ytterligare en egenhet, beskriven i §12.7.4.2.4: urvalet ligger i /V på förälderfältet, medan de enskilda barnen äger widgetarna och vanligen inte har någon egen /V. Den nästlade hjälparen InheritedButtonValue går därför uppåt i /Parent-kedjan, upp till 64 nivåer, tills den hittar ett icke-tomt värde, så varje barn jämförs mot värdet för den grupp det tillhör. Att sätta föräldern till ett barns exportvärde slår på exakt det barnet och av alla syskon

// Kryssruta: exportvärdet måste matcha på-lägets nyckel i /AP /N
// (ofta 'Yes', men riktiga formulär använder '2', 'On' eller något annat)
Pdf.SetFormFieldValue('Consent', 'Yes');

// Radiogrupp: /V skrivs på föräldern; varje barnwidget får
// /AS satt till sitt eget exportnamn eller till Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Att rensa en kryssruta: varje värde utan matchande på-läge ger /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

Valfält: att hålla /I i takt med /V

För en kombinationsruta eller listruta är /V inte den enda platsen där ett urval registreras. Tabell 231 i §12.7.4.4 definierar /I som en array av nollbaserade index i /Opt som identifierar de valda posterna, och en viewer som finner /I pekande på option 0 medan /V namnger option 3 kan markera fel rad. Sedan v2.754.1 körs HPDFReconcileChoiceSelection inuti varje SetFormFieldValue-anrop och bygger, när det ärvda /FT är Ch, om /I från det nya värdet. Ordningen på operationerna är avsiktlig. Den lokala /I-posten raderas först, utan att röra sitt innehåll: om den gamla arrayen var ett indirekt objekt delat med ett annat fält skulle en mutation på plats förstöra det andra fältets urval, så rutinen släpper referensen och skapar en färsk direkt array i stället. Den löser sedan upp /Opt genom /Parent-kedjan, eftersom valalternativ kan ärvas, och skannar posterna. Ett bart strängalternativ jämförs direkt; ett [export display]-par jämförs på sitt exportelement, och ett par med färre än två element hoppas över. Båda sidor går genom HPDFLoadedFormTextName, så ett hex-UTF-16-alternativ matchar ett hex-UTF-16-värde utan att du stavar dem identiskt. Vid första träffen skrivs ett ett-element-/I och skanningen stoppar; ett skalärt värde ersätter alltid ett tidigare flerurval, oavsett MultiSelect-flaggan

Hur HotPDF håller ett valfält konsekvent: HPDFReconcileChoiceSelection raderar den lokala /I-arrayen innan den rör den, löser upp /Opt genom /Parent-kedjan, jämför exporthalvan av varje alternativ genom HPDFLoadedFormTextName, skriver ett ett-element-/I vid första träffen och skriver ingenting när ett redigerbart combo-värde saknar index
Ett bart strängalternativ jämförs direkt och ett export display-par på sitt exportelement, medan ett värde utanför /Opt korrekt lämnar inget index — ett inaktuellt /I som pekar på fel rad vore värre än inget

När ingenting matchar skrivs inget /I alls. Det är rätt utfall för en redigerbar kombinationsruta, där §12.7.4.4 tillåter användaren att skriva ett värde utanför optionslistan; ett sådant värde har inget index, och ett inaktuellt index vore värre än inget. Det är också vad du får om du skickar en visningsetikett i stället för ett exportvärde till en parad optionslista, så när en kombinationsruta vägrar visa ditt val bör du kontrollera vilken halva av paret du skickade in

// /Opt är [[US United States] [CA Canada] [MX Mexico]]:
// matcha på exportvärdet, och /I blir [1]
Pdf.SetFormFieldValue('Country', 'CA');

// Redigerbar combo med ett värde utanför /Opt: /V skrivs,
// /I tas bort, och inget index hittas på
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

Värde och utseende är två separata operationer

SetFormFieldValue rör aldrig utseendeströmmen för ett text- eller valfält. Efter anropet håller /V den nya texten medan /AP /N fortfarande målar den gamla, och vilken av de två en viewer visar beror på om AcroForm-ordboken bär /NeedAppearances true enligt §12.7.3.3 och på om viewern respekterar det. Om du behöver att filen renderar det nya värdet i varje läsare, inklusive flatteners och miniatyrgeneratorer som struntar i flaggan, anropa EnsureLoadedFieldAppearanceStream med fältindexet. Den bygger ett Form XObject från den ärvda /DA-strängen, /Q-justeringen, /MaxLen-komb-layouten och värdet, löser upp det namngivna typsnittet genom AcroForm-resurserna /DR så att ett Type0-typsnitt behåller sitt eget descendant-typsnitt i stället för att degraderas till Helvetica, och returnerar True när minst en widget fick en ström. By-name-overloaden av SetFormFieldValue ger dig inget index tillbaka, så hämta ett genom GetFormField, som returnerar en THPDFLoadedFormField som du äger och måste frigöra. Regressionstestet för ändringen i v2.752.1 är uttryckligt om den här delningen: det sätter ett värde, anropar EnsureLoadedFieldAppearanceStream, renderar sedan sidan och kontrollerar att pixlarna inuti widgetrektangeln ändrades medan pixlarna utanför den inte gjorde det. Att verifiera att /V ändrades bevisar ingenting om vad en användare kommer att se

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // Måla det nya värdet in i /AP så att viewers som ignorerar
    // /NeedAppearances ändå visar det
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

Gränser värda att känna till innan du bygger på detta

ReconcileLoadedButtonAppearanceStates testar den lokala /FT i den ordbok du adresserade, så den verkar på radioföräldern eller på en kryssruta som bär sin egen /FT; en barnwidget som adresseras på egen hand, med /FT bara hos sin förälder, stäms inte av genom den vägen. HPDFReconcileChoiceSelection hanterar ett enda skalärt värde och skriver högst ett index; flerurvalslistrutor med flera valda poster ligger utanför vad SetFormFieldValue modellerar. Ingen av rutinerna validerar värdet du skickar mot /Opt eller mot på-lägets nycklar, så ett stavfel ger en Off-kryssruta eller en combo utan index i stället för ett undantag. Och GetFormFieldValue returnerar den lagrade /V-texten precis som den står i ordboken, vilket för ett hex-kodat värde betyder den hexadecimala stavningen, inte den avkodade texten

När värdena väl är inne och utseendena målade ligger de två naturliga nästa stegen på varsin sida om den här operationen. Att utbyta fältdata med externa system i bulk, i stället för ett SetFormFieldValue-anrop i taget, är vad XFDF-import och export i Delphi täcker. Och när det ifyllda formuläret är färdigt och inte längre ska vara redigerbart bakar flattening av AcroForm- och XFA-fält i Delphi in exakt de /AS-tillstånd och utseendeströmmar som beskrivs här i statiskt sidinnehåll, vilket är varför det inte är valfritt att få dem konsekventa före flatteningen

API:t för redigering av inlästa formulär i den här artikeln, inklusive SetFormFieldValue, EnsureLoadedFieldAppearanceStream och grafen för inkrementell omräkning, levereras som en del av HotPDF Delphi Component för Delphi och C++Builder