Technický článek

Flattenování PDF anotací bez streamu /AP v Delphi

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

Diagram rozhodovacího toku HotPDF: FlattenLoadedAnnotations se nejdřív zeptá GetLoadedAnnotationAppearanceStream a anotace, která už nese /AP, se zapeče jako dosud, zatímco výsledek nil projde kontrolami nedegenerovaného Rect a hidden příznaku a podle Subtype se rozvětví na EnsureLoadedFieldAppearanceStream pro widgety nebo SynthesizeMarkupAppearance pro markup, než se syntetizovaný Form XObject zapeče do contentu stránky
Syntetizační cesta se otevře až po selhání běžného lookupu appearance a po projití kontrol geometrie a viditelnosti, takže dokumenty nesoucí skutečné streamy /AP dostanou zpět bajty svého autora
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

Souřadnicový diagram HotPDF: QuadPoints, koncové body /L a cesty /InkList přicházejí v user space stránky podle Tables 174 a 176 ISO 32000-1, zatímco appearance, který SynthesizeMarkupAppearance sestaví, kreslí v BBox, jehož počátek leží v levém dolním rohu Rect, takže HotPDF odečte RL a RB od každé souřadnice dřív, než zapečovací cm box namapuje zpět
Každý bod vytažený z /QuadPoints, /L nebo /InkList se posune o negovaný levý dolní roh /Rect, než vstoupí do syntetizovaného streamu, a zapečovací cm ten posun vyruší, aby se obnovila absolutní geometrie stránky
// 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

Mapovací diagram HotPDF toho, co SynthesizeMarkupAppearance kreslí podle subtype: FreeText a Stamp vykreslí Contents fontem a barvou vytěženou z DA zarovnanou podle Q, Square a Circle kreslí obrys C a výplň IC se šířkou z BS, Line a Ink tahnou své vrcholy, Highlight vyplní každý quad a Underline, StrikeOut a Squiggly nakreslí linku nebo cikcak
Markup synthesizer čte jen dictionary anotace a nic víc, což udržuje výstup každého subtype předvídatelný a poctivě přiznává subtype, které správně nechává nedotčené

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