HotPDF v2.743.0 flattenuje anotácie PDF, ktoré nemajú appearance stream /AP, namiesto ich tichého preskočenia. FlattenLoadedAnnotations teraz pošle widget bez appearance cez EnsureLoadedFieldAppearanceStream a pre markup bez appearance vytvorí Form XObject z vlastností samotnej anotácie, takže hodnoty zadané do formulára /NeedAppearances prežijú v page content namiesto zmiznutia pri flattenovaní. Zlyhanie, ktoré si túto zmenu vynútilo, vyzerá ako no-op. Zákazník pošle vyplnený formulár žiadosti vytlačený do PDF z browsera. Načítate ho v HotPDF, zavoláte FlattenLoadedAnnotations, dostanete späť 0, uložíte a odošlete dokument s prázdnymi políčkami tam, kde žiadateľ napísal meno a sumu. Nič nevyvolalo chybu, nič sa nezalogovalo. Hodnoty boli v súbore celý čas, sedeli v položke /V každého poľa a flattenovací prechod ich obišiel, pretože žiadny z týchto widgetov nemal appearance stream, ktorý by sa dal zapiecť
Prečo flattenovanie formulára vytlačeného z browsera stratí zadané hodnoty?
Pretože formulár /NeedAppearances uchováva hodnotu bez toho, aby uchovával obraz tejto hodnoty. ISO 32000-1 12.7.2 umožňuje interaktívnemu formuláru nastaviť /NeedAppearances true v AcroForm dictionary, čím vieweru povie, aby pri otvorení zostavil vizuálnu plochu každého poľa z /V, /DA a /Q. Producenti, ktorí formuláre generujú lacno — browser print paths, server-side fillery, niektoré scanning frontendy — túto ponuku využijú a nezapíšu vôbec žiadne /AP. Flattenovanie definované appearance algoritmom v ISO 32000-1 12.5.5 je prepisovacia úloha: vezmite normal appearance stream anotácie, namapujte jeho /BBox na /Rect, zavolajte ho z page content streamu operátorom Do a potom anotáciu vymažte. Bez zdrojového streamu nie je čo prepísať. Pôvodná implementácia HotPDF z v2.386.0 to považovala za „skip“, čo je izolovane obhájiteľné a v súhrne katastrofálne: dokumenty, ktoré flattenovanie potrebujú najviac, majú appearance najmenej často. Rovnaká diera pohltila markup — Highlight z review nástroja, Square z redline prechodu, Ink podpis — vždy, keď producent spoliehal na viewer, že ho vykreslí
Kde HotPDF pripája syntézu do FlattenLoadedAnnotations
Bod napojenia je zámerne neskorý: až po zlyhaní lookupu appearance, nie pred ním. FlattenLoadedAnnotations sa stále najprv opýta GetLoadedAnnotationAppearanceStream na normal appearance a anotácia, ktorá ho už má, sa zapečie presne tak, ako vo v2.386.0. Do syntetizačnej cesty vstúpi iba výsledok nil na anotácii s nedegenerovaným /Rect a bez hidden flagu. Na poradí záleží: autor dokumentu, ktorý si dal námahu zapísať /AP, dostane späť vlastné bajty, nie rekonštrukciu od HotPDF
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);
// opýtaj sa znova: generátor pripojil /AP /N k widgetu
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
end
else
NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;
Odtiaľ sa obe rodiny anotácií rozdelia. Widget sa cez GetLoadedFormFieldIndexForAnnotation vyrieši späť na vlastné field a odovzdá sa do EnsureLoadedFieldAppearanceStream, generátora appearance poľa, ktorý je v tejto Delphi PDF library od v2.328.0. Znovu použiť ho namiesto písania druhého field renderera je celý zmysel — už pokrýva fonty Type0, zalamovanie riadkov, quadding, stavy checkbox a radio /AS aj otočenie /MK, teda rovnaký mechanizmus za pridávaním polí AcroForm do už načítaného PDF. Všetko ostatné ide do markup synthesizera. Pre volajúceho sa nič nemení: rovnaké jednoriadkové volanie flatten teraz vráti nenulový počet aj pri dokumentoch, ktoré kedysi vracali nulu
Doc:= THotPDF.Create(nil);
try
Doc.LoadFromFile('needappearances-form.pdf');
// v2.743.0: widgety a markup bez AP sa syntetizujú a potom zapečú
Flattened:= Doc.FlattenLoadedAnnotations; // všetky stránky, všetky subtypy
// Flattened:= Doc.FlattenLoadedAnnotations('1-3', 'Highlight');
if Flattened= 0 then
raise Exception.Create('nothing was flattened');
Doc.SaveLoadedDocument('flattened.pdf');
finally
Doc.Free;
end;
Prečo skončia QuadPoints a InkList na nesprávnom mieste?
Pretože tieto súradnice sú v page user space, zatiaľ čo syntetizovaný appearance stream kreslí vo vlastnom priestore /BBox a tieto dva počiatky nie sú tým istým bodom. ISO 32000-1 Table 176 definuje /QuadPoints pre text markup anotácie v default user space a Table 174 robí to isté pre endpointy /L anotácie čiary; /InkList sa riadi tou istou konvenciou. HotPDF dá syntetizovanému formu /BBox [0 0 W H], ktorého počiatok leží v ľavom dolnom rohu /Rect. Každý bod vytiahnutý z /QuadPoints, /L alebo /InkList preto treba pred zápisom do content streamu preložiť o negovaný ľavý dolný roh /Rect. Urobte to nesprávne a highlight na riadku 700 bodov nad stránkou sa vykreslí 700 bodov nad vlastným boxom, čo v praxi znamená, že sa nevykreslí nikde. Oprava je jedno odčítanie na súradnicu a skladá sa s cm, ktoré bake emituje neskôr — táto matica mapuje /BBox späť na /Rect, takže oba kroky sa zrušia do správnej absolútnej geometrie
// Endpointy /L sú v page user space (ISO 32000-1 Table 174); počiatok
// BBox formu sedí na ľavom dolnom rohu /Rect, preto posuň o -(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;
Čo syntetizovaný markup appearance skutočne kreslí
Markup synthesizer číta annotation dictionary a nič iné, čím udržiava výstup predvídateľný a poctivý v tom, čo nevie. FreeText a Stamp kreslia /Contents pomocou fontu a farby parsovaných z /DA, zarovnané podľa /Q, s paddingom 2 pt. Square a Circle kreslia obrys re alebo Bezierovku zo štyroch oblúkov strokovanú v /C, pri prítomnosti vyplnenú pomocou /IC, šírkou z /BS /W. Line a Ink strokujú svoje vrcholy. Highlight vyplní každý quad, zatiaľ čo Underline, StrikeOut a Squiggly strokujú čiaru pri spodku quadu, v strede quadu alebo ako cik-cak s jedným bodom. /CA pod 1 sa stane ExtGState s položkou ca, odkazovanou ako /GSA gs na začiatku streamu
Kódovanie textu sa rozhodne z položky AcroForm /DR /Font pomenovanej v /DA. Ak je /Subtype daného fontu Type0, HotPDF zapíše string ako hex literal UTF-16BE s byte-order markom FEFF; inak zapíše escapovaný literal string, pričom escapuje zátvorky a spätné lomky a bajty nad 126 zapisuje osmičkovo. Operátor Tf z /DA sa emituje pred BT, čo je legálne, pretože text state pretrváva cez hranicu textového objektu, a ušetrí rozoberanie stringu /DA. Dve obmedzenia treba povedať otvorene. Šírka riadku pre wrapping a quadding sa odhaduje heuristikou polovice em a celého em namiesto skutočných font metrics, takže zarovnanie v proporcionálnom fonte je blízke, ale nie presné. A subtype, z ktorého nemožno nič syntetizovať — Popup, Link, Stamp, ktorého jediným obsahom je názov ikony — vráti nil a zostane nedotknutý presne ako predtým
Dočasná výmena /Annots, ktorá potrestá užitočné čistenie
FlattenOneWidget, cesta pre jeden widget používaná FlattenLoadedFormFields, je aliasingová pasca, ktorú musí rešpektovať každá zmena vo zdieľanom flatten loop. Dočasne nahradí hodnotu /Annots stránky jednoprvkovým poľom, aby generický flatten prechod pracoval s jediným widgetom, a potom v bloku finally obnoví pôvodný pointer PHPDFDictionaryItem. Obnova zapisuje späť do dictionary slotu, ktorý zachytila pred volaním
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; // dangling, ak vnútorná slučka tento item uvoľnila
TemporaryAnnots.Free;
end;
Pridajte do zdieľanej vnútornej slučky rozumne vyzerajúce upratanie — DeleteValue('Annots'), keď sa pole vyprázdni, aby uložená stránka neniesla pozostatkové prázdne pole — a toto volanie uvoľní práve dictionary item, na ktorý ukazuje DictItem. finally potom zapisuje cez dangling pointer a proces zomrie s „Invalid pointer operation“. Dva existujúce testy to okamžite zachytili, čo je jediný dôvod, prečo je to poznámka pod čiarou, nie support ticket. Pravidlo je všeobecné: pred pridaním cleanupu do zdieľanej slučky skontrolujte u volajúcich alias alebo swap kontrakty. Zostávajúce prázdne pole /Annots je kozmetická vada a nestojí za výmenu garancie životnosti pointera
Čo zostane nezapečené a čo stojí flattenovanie
Skryté anotácie sa vylučujú zámerne. Anotácia, ktorej celé číslo /F má nastavený bit na pozícii 2, je podľa ISO 32000-1 12.5.3 skrytá a keď zároveň nemá /AP, je veľké pokušenie jednu syntetizovať a zapiecť ako zvyšok. To by bola chyba s bezpečnostnými dôsledkami: zapečenie neviditeľnej poznámky do page content ju sprístupní každému, kto súbor otvorí. HotPDF tieto anotácie nechá presne tam, kde sú, a nezapočíta ich do návratovej hodnoty. Rovnako jasne používateľom vysvetlite cenu tých, ktoré sa zapečú. Flattenovanie je nevratné — anotácia sa vymaže z poľa stránky /Annots a jej vizuál je teraz page content, takže už niet úpravy hodnoty poľa, vlákna komentárov, prepínania stavu /AS ani spôsobu, ako obnoviť štruktúrované údaje okrem pôvodného súboru. Flattenujte kópiu, originál si ponechajte a po flattenovaní siahnite až vtedy, keď dokument prestane byť formulárom a stane sa záznamom. Ak je váš problém podopretý XFA namiesto chýbajúceho appearance, začnite oddelenou cestou flattenovania XFA do AcroForm v HotPDF a ak formulár ešte len staviate, poznámky o zapojení akcií a validácie polí AcroForm pokrývajú write side
Jedna poznámka k overeniu, pretože inak vás bude stáť popoludnie. ExtractLoadedPageGlyphs nevstupuje do Form XObjects a zapečený appearance žije vo vnútri jedného — page content stream drží iba sekvenciu q ... cm /FlatAn<n> Do Q. Extrakcia glyfov na flattenovanej stránke preto nenahlási nič a je to správne správanie, nie stratený bake. Overujte buď na úrovni bajtov kontrolou resource name /FlatAn, volania Do a /Subtype /Form, alebo cez rendering pipeline, ktorý XObjects rozbaľuje
Flattenovanie anotácií vyzerá ako tri riadky prepisu až do chvíle, keď narazíte na dokumenty, ktoré ľudia skutočne generujú. Ak v Delphi alebo C++Builderi pracujete s vyplnenými formulármi, review markup alebo archivačným výstupom, oplatí sa prečítať si, ako HotPDF Delphi PDF component rieši stranu načítaného dokumentu pre AcroForms a anotácie, skôr než nad ním postavíte vlastný appearance generator