HotPDF v2.743.0 flatten-uje PDF anotacije koje nemaju /AP appearance stream umesto da ih tiho preskoči. FlattenLoadedAnnotations sada appearance-less widget šalje kroz EnsureLoadedFieldAppearanceStream, a za appearance-less markup iz sopstvenih svojstava anotacije gradi Form XObject, pa vrednosti unete u formu sa /NeedAppearances preživljavaju u sadržaju stranice umesto da nestanu tokom flattening-a. Neuspeh koji je pokrenuo ovu promenu izgleda kao no-op. Klijent pošalje popunjen obrazac, odštampan u PDF iz browser-a. Učitate ga u HotPDF, pozovete FlattenLoadedAnnotations, dobijete 0, sačuvate i isporučite dokument sa praznim poljima tamo gde je podnosilac uneo ime i iznos. Ništa nije podiglo grešku, ništa nije logovano. Vrednosti su sve vreme bile u fajlu, u /V unosu svakog polja, a flatten prolaz je prošao pravo pored njih zato što nijedan od tih widget-a nije nosio appearance stream
Zašto flattening obrasca odštampanog iz browser-a gubi unete vrednosti?
Zato što /NeedAppearances forma čuva vrednost, ali ne čuva sliku te vrednosti. ISO 32000-1 12.7.2 dozvoljava interaktivnoj formi da u AcroForm dictionary postavi /NeedAppearances true, što viewer-u govori da pri otvaranju izgradi vizuelnu površinu svakog polja iz /V, /DA i /Q. Proizvođači koji jeftino generišu forme — browser print putanje, server-side filleri i neki front-end-i za skeniranje — prihvataju tu mogućnost i uopšte ne upisuju /AP. Flattening, kako ga definiše appearance algoritam u ISO 32000-1 12.5.5, jeste posao transkripcije: uzeti normal appearance stream anotacije, preslikati njen /BBox na /Rect, pozvati je iz content stream-a stranice operatorom Do, pa obrisati anotaciju. Bez izvornog stream-a nema ničega što bi se transkribovalo. Izvorna HotPDF implementacija iz v2.386.0 tretirala je to kao „skip“, što je odbranjivo samo za sebe, a pogubno u zbiru: dokumenti kojima flattening najviše treba upravo su oni koji najmanje verovatno nose appearance. Ista rupa gutala je i markup — Highlight iz review alata, Square iz redline prolaza ili Ink potpis — kad god se proizvođač oslanjao na viewer da ga iscrta
Gde HotPDF umeće sintezu u FlattenLoadedAnnotations
Tačka priključivanja je namerno kasna: nakon što pretraga appearance-a ne uspe, a ne pre nje. FlattenLoadedAnnotations i dalje prvo traži normal appearance preko GetLoadedAnnotationAppearanceStream, a anotacija koja ga već ima ugrađuje se tačno kao u v2.386.0. Samo nil rezultat, na anotaciji sa nedegenerisanim /Rect i bez hidden zastavice, ulazi u putanju sinteze. Redosled je važan: autor dokumenta koji se potrudio da upiše /AP dobija svoje bajtove nazad, a ne HotPDF rekonstrukciju
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
if (NStrm= nil) and (RR> RL) and (RT> RB) and ((FlagsValue and 2)= 0) then
begin
if Subtype= 'Widget' then
begin
FieldIdx:= GetLoadedFormFieldIndexForAnnotation(Indices[PgI], AnI, WidgetIdx);
if FieldIdx>= 0 then
EnsureLoadedFieldAppearanceStream(FieldIdx);
// pitaj ponovo: generator je zakačio /AP /N za widget
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
end
else
NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;
Od tog mesta dve porodice anotacija se razdvajaju. Widget se vraća do svog vlasničkog polja kroz GetLoadedFormFieldIndexForAnnotation i prosleđuje EnsureLoadedFieldAppearanceStream, generatoru appearance-a polja koji je u ovoj Delphi PDF biblioteci od v2.328.0. Ponovna upotreba njega umesto pisanja drugog renderer-a polja cela je poenta — već pokriva Type0 fontove, prelamanje redova, quadding, checkbox i radio /AS stanja, kao i /MK rotaciju, istu mehaniku koja stoji iza dodavanja AcroForm polja u već učitani PDF. Sve ostalo ide synthesizer-u za markup. Za pozivaoca se ništa ne menja: isti jednolinijski flatten poziv sada vraća broj različit od nule na dokumentima koji su ranije vraćali nulu
Doc:= THotPDF.Create(nil);
try
Doc.LoadFromFile('needappearances-form.pdf');
// v2.743.0: widget-i i markup bez AP-a se sintetišu, zatim ugrađuju
Flattened:= Doc.FlattenLoadedAnnotations; // sve stranice, svi podtipovi
// Flattened:= Doc.FlattenLoadedAnnotations('1-3', 'Highlight');
if Flattened= 0 then
raise Exception.Create('nothing was flattened');
Doc.SaveLoadedDocument('flattened.pdf');
finally
Doc.Free;
end;
Zašto QuadPoints i InkList završavaju na pogrešnom mestu?
Zato što su te koordinate u page user space-u, dok sintetizovani appearance stream crta u sopstvenom /BBox prostoru, a ta dva početka nisu ista tačka. ISO 32000-1 tabela 176 definiše /QuadPoints za text markup anotacije u podrazumevanom user space-u, a tabela 174 isto radi za krajnje tačke anotacije Line /L; /InkList prati istu konvenciju. HotPDF sintetizovanom Form-u daje /BBox [0 0 W H], čiji je početak u donjem levom uglu /Rect. Zato svaka tačka izvučena iz /QuadPoints, /L ili /InkList mora pre upisa u content stream da se pomeri za negirani donji levi ugao /Rect. Pogrešite ovo i highlight na liniji 700 tačaka iznad stranice iscrtaće se 700 tačaka iznad sopstvenog okvira, što praktično znači da se neće videti nigde. Korekcija je jedno oduzimanje po koordinati i slaže se sa cm koji bake emituje nakon toga — ta matrica vraća /BBox na /Rect, pa se dva koraka poništavaju u ispravnu apsolutnu geometriju
// Krajnje tačke /L su u page user space-u (ISO 32000-1 tabela 174); početak
// forme BBox je na donjem levom uglu /Rect, pa pomeri za -(RL, RB)
X1:= ArrNum(LA, 0, 0)- RL;
Y1:= ArrNum(LA, 1, 0)- RB;
X2:= ArrNum(LA, 2, 0)- RL;
Y2:= ArrNum(LA, 3, 0)- RB;
StrokeOp:= ColorOp(DArr('C'), true);
if StrokeOp= '' then
StrokeOp:= '0 G';
Result:= _FloatToStrR(BW)+ ' w '#10+ StrokeOp+ #10+
_FloatToStrR(X1)+ ' '+ _FloatToStrR(Y1)+ ' m '+
_FloatToStrR(X2)+ ' '+ _FloatToStrR(Y2)+ ' l S'#10;
Šta sintetizovani markup appearance zaista crta
Markup synthesizer čita samo dictionary anotacije, što izlaz drži predvidljivim i poštenim u pogledu onoga što ne može da zna. FreeText i Stamp crtaju /Contents koristeći font i boju izvučene iz /DA, poravnate prema /Q, sa padding-om od 2 pt. Square i Circle crtaju re ili konturu od četiri Bezier luka iscrtanu u /C, popunjenu sa /IC kada postoji, širinom iz /BS /W. Line i Ink iscrtavaju svoje temene. Highlight popunjava svaki quad, dok Underline, StrikeOut i Squiggly iscrtavaju liniju na dnu quada, na sredini quada ili kao cik-cak od jedne tačke. /CA manji od 1 postaje ExtGState sa unosom ca, referenciranim kao /GSA gs na početku stream-a
Kodiranje teksta odlučuje se iz AcroForm unosa /DR /Font koji /DA imenuje. Ako je /Subtype tog fonta Type0, HotPDF upisuje string kao UTF-16BE hex literal sa oznakom redosleda bajtova FEFF; u suprotnom upisuje escaped literal string, pri čemu se zagrade i backslash znakovi escape-uju, a bajtovi iznad 126 upisuju oktalno. Operator Tf iz /DA emituje se pre BT, što je legalno jer stanje teksta opstaje između granica text object-a, i time se izbegava rastavljanje /DA stringa. Dva ograničenja treba reći otvoreno. Širina reda za prelamanje i quadding procenjuje se heuristikom od pola em-a i punog em-a, a ne pravim metrikama fonta, pa je poravnanje na proporcionalnom fontu približno, ali ne tačno. A podtipu bez ičega što može da se sintetiše — Popup, Link ili Stamp čiji je jedini sadržaj ime ikone — rezultat je nil i ostaje netaknut, potpuno kao ranije
Privremena zamena /Annots koja kažnjava korisno čišćenje
FlattenOneWidget, putanja po widget-u koju koristi FlattenLoadedFormFields, jeste aliasing zamka koju svaka izmena u deljenoj flatten petlji mora da poštuje. Ona privremeno zamenjuje vrednost stranice /Annots nizom od jednog elementa kako bi generički flatten prolaz radio nad jednim widget-om, a zatim u finally bloku vraća originalni pokazivač PHPDFDictionaryItem. Vraćanje upisuje nazad u dictionary slot koji je sačuvan pre poziva
DictItem:= PHPDFDictionaryItem(PageObj.Items.Items[AnnotsIndex]);
Item:= DictItem^.Value;
TemporaryAnnots:= THPDFArrayObject.Create(nil);
TemporaryAnnots.AddObject(Target);
DictItem^.Value:= TemporaryAnnots;
try
Result:= FlattenLoadedAnnotations(IntToStr(PageIndex+ 1), 'Widget')= 1;
finally
DictItem^.Value:= Item; // viseći pokazivač ako je unutrašnja petlja oslobodila ovu stavku
TemporaryAnnots.Free;
end;
Dodajte razumno sređivanje unutar deljene unutrašnje petlje — DeleteValue('Annots') kada se niz isprazni, kako sačuvana stranica ne bi nosila zaostali prazan niz — i taj poziv oslobađa baš dictionary item na koji DictItem pokazuje. finally zatim upisuje kroz viseći pokazivač i proces pada sa „Invalid pointer operation“. Dva postojeća testa odmah su ga uhvatila, što je jedini razlog što je ovo fusnota, a ne support ticket. Pravilo je šire: pre dodavanja čišćenja u deljenu petlju proverite ugovore pozivalaca o aliasima ili swap-u. Zaostali prazan niz /Annots jeste kozmetička mana i nije vredan menjanja garancije životnog veka pokazivača
Šta ostaje neugrađeno i koliko flattening košta
Skrivene anotacije namerno se izuzimaju. Anotacija čiji /F integer ima postavljen bit na poziciji 2 jeste hidden prema ISO 32000-1 12.5.3, a kada pritom nema /AP, postoji stvarno iskušenje da se jedan sintetizuje i ugrađuje kao i ostali. To bi bila greška sa bezbednosnim posledicama: ugrađivanje nevidljive beleške u sadržaj stranice učinilo bi je vidljivom svakome ko otvori fajl. HotPDF ostavlja takve anotacije tačno gde jesu i ne broji ih u povratnu vrednost. Jednako jasno objasnite korisnicima cenu onih koje budu ugrađene. Flattening je nepovratan — anotacija se briše iz niza /Annots stranice, a njen vizuelni prikaz sada je sadržaj stranice, pa više nema menjanja vrednosti polja, niti thread-a komentara, niti prebacivanja /AS stanja, niti načina da se povrate strukturisani podaci bez izvornog fajla. Flatten-ujte kopiju, čuvajte original i posegnite za njim tek kada dokument prestane da bude forma i postane zapis. Ako je vaš problem XFA-backed, a ne appearance-less, zasebna XFA u AcroForm flattening putanja u HotPDF-u jeste pravo mesto za početak, a ako formu tek gradite, beleške o povezivanju AcroForm akcija i validaciji polja pokrivaju upisnu stranu
Jedna napomena o verifikaciji, jer će vas u suprotnom koštati popodneva. ExtractLoadedPageGlyphs ne ulazi u Form XObject-e, a ugrađeni appearance živi unutar jednog — content stream stranice drži samo sekvencu q ... cm /FlatAn<n> Do Q. Ekstrakcija glifova na flatten-ovanoj stranici zato ne vraća ništa, i to je ispravno ponašanje, a ne izgubljeni bake. Proverite ili na nivou bajtova, tražeći ime resursa /FlatAn, poziv Do i /Subtype /Form, ili kroz rendering pipeline, koji proširuje XObject-e
Flattening anotacija izgleda kao tri linije transkripcije sve dok ne naiđete na dokumente koje ljudi zaista generišu. Ako u Delphi-ju ili C++Builder-u radite sa popunjenim obrascima, review markup-om ili arhivskim izlazom, vredi pročitati kako HotPDF Delphi PDF komponenta obrađuje učitanu stranu AcroForms i anotacija pre nego što na njoj izgradite sopstveni generator appearance-a