Techninis straipsnis

Kodėl XFA laukų redagavimai išsaugant dingsta PDFium Delphi

TPdf.SetFocusedFormFieldText PDFium Component rašo į šiuo metu sufokusuoto formos lauko gyvąjį redagavimo buferį, ir XFA formai tas buferis niekada nepasiekia datasets paketo, kuris serializuojamas į diską — todėl reikšmė, kurią vartotojas įveda ir jūsų kodas patvirtina buvus priimta, tyliai dingsta kitą kartą atveriant failą. AcroForm laukai neturi šios problemos: ta pati iškvietis patvirtina į lauko /V įrašą akimirka, kai fokusas pajuda šalin. Vartotojas, kuris užpildo XFA intake formą, išsaugo ir atveria iš naujo, kad rastų sumos lauką vėl tuščią, nepataiko į renderavimo glitčią — jis pataiko į kraštą to, ką PDFium variklis pats atskleidžia formos duomenų rašymui

Tai yra siauresnis klausimas nei aptikti XFA formą iš viso arba paleisti jos JavaScript: ne „ar PDFium palaiko XFA" ir ne „kaip vykdyti AcroForm skriptus", o specialiai kas atsitinka reikšmei po to, kai SetFocusedFormFieldText praneša sėkmę. Trumpa versija yra, kad AcroForm ir XFA nėra dviejų to paties formos modelio dialektai, kiek tai liečia PDFium rašymo kelią — tai du formos modeliai su dviem visiškai skirtingais ryšiais tarp to, ką vartotojas įveda, ir to, ką išsaugojimas tikrai užfiksuoja, ir dviejų supainiojimas yra tai, kas vienos eilutės API iškvietimą paverčia palaikymo bilietu trys savaitės po to, kai kliento bandomasis diegimas pradeda veikti. AcroForm JavaScript straipsnis parodo vienos eilutės iškvietį ir nurodo AcroForm-prieš-XFA rezultatą kodo komentare; šis pasilieka prie to paties API ir pereina vidinį rašymo kelią, datasets-paketo įrodymą, kad XFA rašymas niekada nelanduoja, kodėl spraga sėdi pačiame PDFium, o ne Delphi susiejime, ir savo-XML pataisos apeigą dokumentams, kuriems redagavimas turi išlipti po išsaugojimo

Kaip SetFocusedFormFieldText įrašo lauko reikšmę?

TPdf.SetFocusedFormFieldText veikia imituodamas klavišų lygio redagavimą, o ne tiesiogiai įkišdamas reikšmę į dokumento modelį. Viduje jis iškviečia FORM_SelectAllText, kad pažymėtų sufokusuoto lauko dabartinį turinį, tada FORM_ReplaceSelection, kad perrašytų pažymėjimą nauju stringu — tas pačias dvi operacijas, kurias sukeltų klaviatūros valdomas pažymėk-viską-ir-įvesk. Kadangi rašymas eina per PDFium interaktyvaus teksto-redagavimo kelią, o ne aplink jį, bet kuris klavišo, formato ar skaičiavimo skriptas, susietas su lauku, paleidžiamas lygiai taip, kaip žmogui įvedant, kas ir daro API naudingą programiniam formų užpildymui žiūrovėje, kuri JavaScript laiko gyva. Skaitymo pusės atitikmuo yra FocusedFormFieldText, remiamas FORM_GetFocusedText, ir jis atspindi tą patį gyvą buferį, į kurį ką tik parašė SetFocusedFormFieldText

if Pdf.FocusedFormFieldIndex >= 0 then
begin
  if Pdf.SetFocusedFormFieldText('1284.50') then
    Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
  else
    Log('No field is focused, or it does not accept text');
end
else
  Log('Focus a field first - FocusFormField or a real click');

Kodėl AcroForm išlaiko reikšmę, o XFA praranda?

AcroForm teksto ir combo laukai išlieka, nes PDFium paties formos-užpildymo aplinka patvirtina redagavimo buferį už jus: akimirka, kai laukas praranda fokusą, buferis įrašomas į lauko /V įrašą, tą patį raktą, į kurį kiekvienas conforming PDF skaitytuvas žiūri, kad žinotų lauko saugomą reikšmę. TPdf.ClearFormFieldFocus — kuris po gaubtu iškviečia FORM_ForceToKillFocus — priverčia tą patvirtinimą pagal poreikį, todėl kodas, kuris reikšmę nustato programiškai, neturi laukti realaus pelės paspaudimo kitur UI. Išsaugokite iškart po to, ir naujas tekstas yra dokumento objektų grafo dalis dar prieš TPdf.SaveAs kada veikia, nes /V yra realus įrašas realiame lauko žodyne, o ne kažkas pridėta vėliau

SetFocusedFormFieldText PDFium Component for Delphi skaidytoji diagrama: vienas gyvas redagavimo buferis šakojasi į AcroForm kelią, kur fokuso praradimas įrašo reikšmę į /V ir SaveAs ją išsaugoja, ir į XFA kelią, kuriame datasets paketas niekada neatnaujinamas, o išsaugotas failas atsiveria tuščias
Tas pats rašymas nusileidžia viename gyvame buferio, ir formų modelis nusprendžia jo lemtį. AcroForm įsipareigoja /V prarandant fokusą, kol XFA palieka duomenų rinkinių paketą neliečiamą
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus;              // dabar priverstinai atlieka /V patvirtinimą
Pdf.SaveAs('invoice-acroform.pdf');

// Atverkite iš naujo ir patvirtinkite - tai AcroForm dokumentas, todėl reikšmė išlieka
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50');   // pavyksta

Kur XFA lauko redagavimas iš tikrųjų gyvena?

XFA laukai neturi tokio kabeliavimo. Tekstas, kurį vartotojas įveda, nusileidžia į CPWL_Edit buferį, priklausantį PDFium XFA renderavimo ir sąveikos sluoksniui, ir tas sluoksnis neturi kodo kelio, kuris nukopijuotų buferį atgal į datasets paketą, saugomą PDF. TPdf.GetXfaDatasets padaro spragą matomą: iškvieskite jį prieš ir po redagavimo XFA lauke, ir baitai, kuriuos jis grąžina, bus identiški, nes metodas skaito originalų paketą, su kuriuo dokumentas buvo atvertas, o ne gyvą būseną valdiklio, kurį ką tik redagavote. Nieko iš to nėra cachinimo klaida arba atnaujinimo laiko problema — datasets paketas diske ir redagavimo buferis atmintyje yra tiesiog du skirtingi būsenos vienetai, kurių PDFium viešasis API niekada nesujungia

var
  Before, After: TBytes;
begin
  Before := Pdf.GetXfaDatasets;
  Pdf.FocusFormField(FieldIndex);
  Pdf.SetFocusedFormFieldText('1284.50');
  After := Pdf.GetXfaDatasets;
  // Before ir After yra baitas į baitą identiški XFA dokumente -
  // redagavimas niekada nepalietė paketo, iš kurio skaito GetXfaDatasets
end;

Ar tai PDFium Component klaida ar PDFium apribojimas?

Trūkstamas vienetas sėdi pačiame PDFium, o ne Delphi susiejime ant jo. PDFium viešajam API nėra FPDF_SetXFAPacket, kad įšvirkštų atnaujintą paketą, ir nėra FPDF_SaveAsXFA, kad paprašytų XFA variklio serializuoti savo dabartinį DOM atgal į datasets XML prieš išsaugojimą. FPDF_SaveAsCopy — eksportas, kuris remia TPdf.SaveAs — išrašo dokumento objektų grafą, kurį PDFium jau turi; jis neturi hook paprašyti XFA variklio išvalyti savo gyvos būsenos pirma, nes tas hook neegzistuoja aukštyne. PDFium Component negali pridėti suderinimo, kurio pats PDFium niekada neįgyvendino, o savadarbis DOM-į-XML serializatorius, kuris spėja PDFium vidinę XFA būseną, būtų blogesnis už sąžiningą spragą: jis atrodytų, kad veikia, iki kitos PDFium versijos ką nors pakeičia, ko niekas už projekto ribų nemato

Sluoksnių diagrama Delphi XFA išsaugojimui: PDFium Component susiejimas įvynioja PDFium viešąją API, kurios eksportuotos formų funkcijos baigiasi ties FPDF_SaveAsCopy, kol FPDF_SetXFAPacket ir FPDF_SaveAsXFA pirmtake neegzistuoja, todėl XFA varikliuko gyva būsena niekada neserializuojama atgal į datasets prieš išsaugojimą
Susiejimas kviečia kiekvieną eksportą, kurį siūlo PDFium, ir negali išrasti trūkstamųjų. Be serializacijos kabliuko XFA variklis išlaiko savąją gyvąją būseną sau

Ši riba iškilo to paties v2.13.2 audito, kuris iš pradžių sukūrė SetFocusedFormFieldText, metu. FORM_ReplaceSelection buvo surištas DLL importo lentelėje versijoms be to, kad kada nors būtų iškviestas iš Paskalio kodo, ir rašymo kelio, kuris pagaliau jį panaudojo, pridėjimas yra tai, kas padarė išliekamosios spragos pakankamai konkretos dokumentuoti, o ne teorinės. Tas pats audito raundas atskleidė nesusijusią, bet dvasia giminingą spragą: AcroForm JavaScript buvo tyliai išjungtas nuo v2.13.0, nes JS platforma buvo prijungta tik XFA inicializacijos šakoje, todėl įprasti AcroForm dokumentai su app.alert arba apskaičiuotais laukais iš viso negavo skriptų variklio. Tas buvo pataisomas — JS platformos išplėtimas į kiekvieną dokumentą nepriklausomai nuo XFA — ir atsirado toje pačioje versijoje; čia aptarta išliekamoji spraga nebuvo pataisoma dėl pirmiau minėtų priežasčių. JavaScript pataisa ir host-veto įvykiai aplink ją yra aptariami AcroForm JavaScript vykdymo su PDFium Component

Ką daryti su tuo Delphi?

AcroForm dokumentams pataisa yra niekas daugiau nei geras įprotis: iškvieskite ClearFormFieldFocus (arba kitaip patraukite fokusą šalin) prieš SaveAs, kai reikšmė buvo nustatyta programiškai, užuot manęs, kad vėlesnė UI sąveika sukels patvirtinimą už jus. Dokumentui, kuris gali būti arba AcroForm, arba XFA — kas yra įprastas atvejis bendrosios paskirties žiūrovėje — patikrinkite FormType arba XFA boolean prieš žadėdami kviesčiui, kad išsaugojimas išliks, ir skaitykite XFA formų aptikimas ir XFA paketų ištraukimas pilnam zondų rinkiniui, įskaitant XFAF atvejį, kur XFA turinys yra sluoksniuojamas ant kitu atveju įprastų AcroForm valdiklių, kurie gerbia /V

Genuinai dinaminei XFA formai, kur redaguotos reikšmės turi išlikti po išsaugojimo, interaktyvus redagavimo buferis iš viso nėra tinkamas įrankis. Patvarus kelias yra elgtis su GetXfaDatasets kaip su savo pradine bazė, o ne rezultatu: perskaitykite jį vieną kartą, kai dokumentas atveriamas, laikykite savo įrašą apie tai, ką vartotojas pakeitė laukas po lauko — lygiai tas reikšmes, kurias jūsų UI jau turi, nes PDFium jums jų negrąžins po fakto — pataisykite jas į pradinę XML patys ir valdykite savo išvestį. Rašymas, kuris eina per XML, kurį jūsų kodas valdo, išlieka po išsaugojimo, kurio CPWL_Edit buferis niekada negalėtų

function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
  NewValue: string): TBytes;
var
  DatasetsXml: string;
begin
  // GetXfaDatasets pateikiamas su PDFium Component; toliau esantis PatchXmlNode yra
  // jūsų pačių pagalbinė funkcija virš jūsų pačių XML bibliotekos, PDFium jos neteikia
  DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
  DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
  Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;

Sugauti spragą anksčiau nei klientas

TPdf.SaveAs grąžina True, nepriklausomai nuo to, ar XFA lauko reikšmė išliko, nes iš PDFium požiūrio išsaugojimas tikrai pavyko — jis parašė kiekvieną baitą, kurio buvo paprašytas. Tai daro būtent tokio tipo defektą, kuris praslysta pro dūmų testą ir pasiekia klientą: niekas nemeta išimties, niekas neregistruoja, failas atsidaro gerai, tik konkreči reikšmė yra bloga. Round-trip testas, kuris tikrai atveria iš naujo išsaugotą failą ir palygina lauko reikšmę — arba palygina GetXfaDatasets prieš ir po, kaip ankstesniame pavyzdyje — priklauso regresijos rinkiniui bet kuriai žiūrovei, kuri leidžia vartotojams redaguoti XFA turinį, o ne tik AcroForm keliai, kurie atsitiktinai veikia pagal numatymą

Patvari XFA redagavimo grandinė Delphi: perskaitykite datasets bazinę būseną su GetXfaDatasets, sekite vartotojo redagavimus laukas po lauko savo UI būsenoje, pataisykite XML savo pagelbikliu ir išsaugokite išvestį, kurioje redaguota reikšmė išgyvena
Laikykite GetXfaDatasets bazine linija, o savąją UI — redagavimų įrašu. Paketo XML lopymas patys perkelia baitus, pasiekiančius diską, į jūsų kontrolę

Nieko iš to nėra defektas, kurį reikia pateikti prieš PDFium Component, tiek kiek riba, aplink kurią projektuoti: SetFocusedFormFieldText daro lygiai tai, ką sako jo pavadinimas, abiem formos modeliams, o rezultato skirtumas švariai siejasi su tuo, ką AcroForm ir XFA kiekvienas riša tą buferį PDFium pusėje. API, fokusavimo ir išsaugojimo primityvai bei paketų skaitytuvai, minimi čia, yra PDFium Component Delphi ir C++Builder dalis