HotPDF v2.743.0 flattenuje PDF anotace, které nemají appearance stream /AP, místo aby je potichu přeskočil. FlattenLoadedAnnotations nyní pošle widget bez appearance přes EnsureLoadedFieldAppearanceStream a markup bez appearance vytvoří jako Form XObject z vlastností samotné anotace, takže hodnoty zadané do formuláře /NeedAppearances přežijí ve contentu stránky místo zmizení při flattenování. Selhání, které tuto změnu vynutilo, vypadá jako no-op. Zákazník pošle vyplněný formulář žádosti vytištěný do PDF z prohlížeče. Načtete ho v HotPDF, zavoláte FlattenLoadedAnnotations, dostanete 0, uložíte a odešlete dokument s prázdnými políčky tam, kde žadatel napsal jméno a částku. Nic nevyvolalo chybu ani se nezalogovalo. Hodnoty byly v souboru celou dobu v položce /V každého pole a flattenovací průchod je minul, protože žádný z těchto widgetů neměl appearance stream k zapečení
Proč flattenování formuláře vytištěného z prohlížeče ztratí zadané hodnoty
Protože formulář /NeedAppearances ukládá hodnotu, ale neobrázek této hodnoty. ISO 32000-1 12.7.2 dovoluje interaktivnímu formuláři nastavit /NeedAppearances true v dictionary AcroForm, což vieweru říká, aby při otevření zkonstruoval vizuální povrch každého pole z /V, /DA a /Q. Producenti, kteří formuláře generují levně — tiskové cesty prohlížečů, serverové fillery a některé front-endy skenování — tuto možnost využijí a žádné /AP nezapíší. Flattenování, jak ho definuje appearance algoritmus v ISO 32000-1 12.5.5, je přepisovací práce: vezměte normální appearance stream anotace, namapujte jeho /BBox na /Rect, vyvolejte ho z content streamu stránky operátorem Do a potom anotaci smažte. Bez zdrojového streamu není co přepsat. Původní implementace HotPDF od v2.386.0 to chápala jako „skip“, což je izolovaně obhajitelné a v součtu katastrofální: dokumenty, které flattenování potřebují nejvíc, nejméně často nesou appearance streamy. Stejná mezera pohltila markup — Highlight z review nástroje, Square z redline průchodu i Ink podpis — kdykoli producent spoléhal na vykreslení viewerem
Kde HotPDF napojuje syntézu do FlattenLoadedAnnotations
Hook point je záměrně pozdní: až po selhání lookupu appearance, nikoli před ním. FlattenLoadedAnnotations stále nejprve požádá GetLoadedAnnotationAppearanceStream o normální appearance a anotace, která ho už má, se zapeče přesně jako ve v2.386.0. Do syntézní cesty vstoupí pouze výsledek nil u anotace s nedegenerovaným /Rect a bez hidden příznaku. Na pořadí záleží: autor dokumentu, který se obtěžoval zapsat /AP, dostane zpět vlastní bajty, nikoli rekonstrukci 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);
// zeptej se znovu: generátor připojil k widgetu /AP /N
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
end
else
NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;
Odtud se dvě rodiny anotací rozdělí. Widget se přes GetLoadedFormFieldIndexForAnnotation vyřeší zpět na vlastnické field a předá se do EnsureLoadedFieldAppearanceStream, generátoru field appearance, který v této PDF knihovně pro Delphi existuje od v2.328.0. Opakované použití místo psaní druhého rendereru polí je celý smysl — už pokrývá Type0 fonty, zalamování řádků, quadding, stavy /AS checkboxů a radio buttonů i rotaci /MK, stejnou mechaniku za přidáváním AcroForm polí do už načteného PDF. Vůči volajícímu se nic nemění: stejný jednořádkový flatten call nyní vrátí nenulový počet na dokumentech, které dříve vracely nulu
Doc:= THotPDF.Create(nil);
try
Doc.LoadFromFile('needappearances-form.pdf');
// v2.743.0: widgety a markup bez AP se syntetizují a potom zapečou
Flattened:= Doc.FlattenLoadedAnnotations; // všechny stránky a typy
// Flattened:= Doc.FlattenLoadedAnnotations('1-3', 'Highlight');
if Flattened= 0 then
raise Exception.Create('nothing was flattened');
Doc.SaveLoadedDocument('flattened.pdf');
finally
Doc.Free;
end;
Proč QuadPoints a InkList skončí na špatném místě
Protože tyto souřadnice jsou v user space stránky, zatímco syntetizovaný appearance stream kreslí ve vlastním prostoru /BBox, a oba počátky neleží ve stejném bodě. ISO 32000-1 Table 176 definuje /QuadPoints pro text markup anotace ve výchozím user space a Table 174 totéž pro koncové body /L anotace čáry; /InkList se řídí stejnou konvencí. HotPDF dá syntetickému Formu /BBox [0 0 W H], jehož počátek leží v levém dolním rohu /Rect. Každý bod vytažený z /QuadPoints, /L nebo /InkList se proto před zápisem do content streamu musí posunout o negovaný levý dolní roh /Rect. Udělejte to špatně a highlight na řádku 700 bodů nahoře na stránce se vykreslí 700 bodů nad vlastním boxem, což v praxi znamená nikde. Oprava je jedno odečtení na souřadnici a skládá se s cm, který bake později zapíše — matice namapuje /BBox zpět na /Rect, takže se oba kroky vyruší do správné absolutní geometrie
// Koncové body /L jsou v user space stránky (ISO 32000-1 Table 174);
// počátek BBox leží v levém dolním rohu /Rect, proto posun 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;
Co syntetizovaný markup appearance skutečně kreslí
Markup synthesizer čte jen dictionary anotace, což udržuje výstup předvídatelný a poctivě ukazuje, co nemůže vědět. FreeText a Stamp kreslí /Contents pomocí fontu a barvy načtené z /DA, zarovnané podle /Q, s odsazením 2 pt. Square a Circle kreslí obrys re nebo Bezierovu křivku ze čtyř oblouků, stroke v /C, a pokud existuje, fill v /IC se šířkou z /BS /W. Line a Ink kreslí své vrcholy. Highlight vyplní každý quad, zatímco Underline, StrikeOut a Squiggly vykreslí čáru na spodku quadu, v jeho středu nebo jako cikcak o jednom bodu. /CA menší než 1 vytvoří ExtGState s položkou ca, na kterou odkazuje /GSA gs na začátku streamu
Kódování textu se rozhoduje podle položky /DR /Font AcroFormu pojmenované v /DA. Pokud je /Subtype tohoto fontu Type0, HotPDF zapíše string jako hex literal UTF-16BE s byte order markem FEFF; jinak zapíše escapovaný literal string, přičemž závorky a backslashe escapuje a bajty nad 126 zapisuje oktalově. Operátor Tf z /DA se emituje před BT, což je legální, protože stav textu přetrvává přes hranici textového objektu, a ušetří to rozebírání stringu /DA. Dvě omezení je dobré uvést jasně. Šířka řádku pro wrapping a quadding se odhaduje heuristikou půl emu / celého emu místo skutečných metrik fontu, takže zarovnání proporčního fontu je blízké, ale ne přesné. A subtype bez něčeho, co lze syntetizovat — Popup, Link nebo Stamp, jehož jediným obsahem je jméno ikony — vrátí nil a zůstane nedotčený jako dříve
Dočasná výměna /Annots, která potrestá užitečný cleanup
FlattenOneWidget, cesta pro jednotlivý widget používaná FlattenLoadedFormFields, je aliasingová past, kterou musí respektovat každá změna ve sdílené flatten smyčce. Dočasně nahradí hodnotu /Annots stránky jednoprvkovým polem, aby obecný flatten průchod pracoval s jediným widgetem, a pak v bloku finally obnoví původní pointer PHPDFDictionaryItem. Obnovení zapisuje zpět do slotu dictionary, který si zachytil před volání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, pokud vnitřní smyčka tuto položku uvolnila
TemporaryAnnots.Free;
end;
Přidejte do sdílené vnitřní smyčky rozumně vypadající úklid — DeleteValue('Annots'), jakmile se pole vyprázdní, aby uložená stránka nenesla pozůstatek prázdného pole — a toto volání uvolní právě dictionary item, na který ukazuje DictItem. finally potom zapíše přes dangling pointer a proces skončí hláškou „Invalid pointer operation“. Dva existující testy to zachytily okamžitě, což je jediný důvod, proč jde o poznámku pod čarou, nikoli support ticket. Pravidlo je obecnější: před přidáním cleanupu do sdílené smyčky zkontrolujte u volajících alias nebo swap kontrakty. Zbylé prázdné pole /Annots je kosmetická vada a nestojí za výměnu za záruku životnosti pointeru
Co zůstává nezapečené a kolik flattenování stojí
Skryté anotace se záměrně vylučují. Anotace, jejíž integer /F má nastavený bit na pozici 2, je podle ISO 32000-1 12.5.3 skrytá a když zároveň nemá /AP, svádí to k syntéze a zapečení stejně jako u ostatních. To by byla chyba s bezpečnostními důsledky: zapečení neviditelné poznámky do contentu stránky by ji zviditelnilo každému, kdo soubor otevře. HotPDF takové anotace ponechá přesně tam, kde jsou, a nezapočítá je do návratové hodnoty. Stejně jasně uživatelům vysvětlete cenu anotací, které se skutečně zapečou. Flattenování je nevratné — anotace se smaže z pole stránky /Annots a její vizuální podoba se stane obsahem stránky, takže už nelze upravovat hodnotu fieldu, vlákno komentářů, přepínat stav /AS ani obnovit strukturovaná data jinak než z původního souboru. Flattenujte kopii, originál si ponechte a po kopii sáhněte až ve chvíli, kdy dokument přestane být formulářem a stane se záznamem. Pokud je problém v XFA místo appearance-less anotace, začněte samostatnou cestou flattenování XFA na AcroForm v HotPDF, a pokud formulář teprve stavíte, poznámky o zapojení akcí a validace AcroForm polí pokrývají cestu zápisu
Jedna poznámka k ověření, jinak vás bude stát odpoledne. ExtractLoadedPageGlyphs nevstupuje do Form XObjects a právě uvnitř jednoho se nachází zapečený appearance — content stream stránky drží jen sekvenci q ... cm /FlatAn<n> Do Q. Extrakce glyfů na flattenované stránce proto nic nehlásí a je to správné chování, nikoli ztracený bake. Ověřujte buď na úrovni bajtů kontrolou jména resource /FlatAn, volání Do a /Subtype /Form, nebo přes renderovací pipeline, která XObjects rozvine
Flattenování anotací vypadá jako tři řádky přepisu až do chvíle, kdy potkáte dokumenty, které lidé skutečně generují. Pokud v Delphi nebo C++Builderu pracujete s vyplněnými formuláři, review markup nebo archivním výstupem, vyplatí se přečíst si, jak HotPDF Delphi PDF component obsluhuje loaded-document stranu AcroForms a anotací, než nad ní začnete stavět vlastní generátor appearance