Műszaki cikk

AP nélküli PDF-annotációk flattenelése Delphiben

A HotPDF v2.743.0 most már nem hagyja csendben figyelmen kívül azokat a PDF-annotációkat, amelyeknek nincs /AP appearance streamjük, hanem flatteneli őket. A FlattenLoadedAnnotations a megjelenés nélküli widgetet az EnsureLoadedFieldAppearanceStream felé küldi, a megjelenés nélküli markuphoz pedig az annotáció saját tulajdonságaiból Form XObjectot épít, így a /NeedAppearances űrlapba beírt értékek a flatteneléskor nem tűnnek el, hanem bekerülnek az oldal contentjébe. A változást kikényszerítő hiba no-opnak látszik. Egy ügyfél browserből nyomtatott, kitöltött jelentkezési űrlapot küld. Betöltöd HotPDF-be, meghívod a FlattenLoadedAnnotations-t, 0-t kapsz vissza, elmented, és egy olyan dokumentumot küldesz tovább, amelyben üres dobozok vannak a név és az összeg helyén. Semmi nem dobott kivételt, semmi nem került naplóba. Az értékek végig a fájlban voltak, minden mező /V bejegyzésében, a flatten pass pedig egyenesen elment mellettük, mert egyik widget sem hordozott appearance streamet

Miért veszíti el a browserből nyomtatott űrlap a beírt értékeket flatteneléskor?

Azért, mert egy /NeedAppearances űrlap úgy tárolja az értéket, hogy nem tárolja vele együtt az érték képét. Az ISO 32000-1 12.7.2 megengedi, hogy egy interaktív űrlap /NeedAppearances true értéket állítson be az AcroForm dictionaryban, ami azt mondja a viewernek, hogy megnyitáskor építse fel minden mező vizuális felületét a /V, /DA és /Q alapján. Az olcsón űrlapot generáló producerek — browseres print-útvonalak, szerveroldali kitöltők, egyes szkennelési frontendek — élnek ezzel a lehetőséggel, és egyáltalán nem írnak /AP-t. A flattenelés az ISO 32000-1 12.5.5 appearance algoritmusa szerint átírási feladat: vedd az annotáció normál appearance streamjét, képezd a /BBox-át a /Rect-re, hívd meg page contentból egy Do operátorral, majd töröld az annotációt. Forrásstream nélkül nincs mit átírni. Az eredeti, v2.386.0 óta létező HotPDF-megvalósítás ezt „skip”-ként kezelte, ami elszigetelten védhető, összesítve viszont katasztrofális: éppen a flattenelésre leginkább szoruló dokumentumok hordozzák legkevésbé az appearance-eket. Ugyanez a rés a markupot is elnyelte — egy review-eszköz Highlightját, redline-folyamat Square-jét vagy Ink-aláírását — amikor a producer arra hagyatkozott, hogy majd a viewer rajzolja ki

Hol kapcsolódik a szintézis a FlattenLoadedAnnotations menetbe?

A kapcsolódási pont szándékosan késői: az appearance lookup sikere után, nem előtte. A FlattenLoadedAnnotations továbbra is először a GetLoadedAnnotationAppearanceStream-tól kéri a normál appearance-t, és egy már meglévő appearance-t pontosan úgy süt be, ahogy a v2.386.0 tette. Csak a nil eredmény, nem degenerált /Rect-tel és hidden flag nélkül kerül a szintézis útvonalára. A sorrend számít: ha a dokumentum szerzője vette a fáradságot, hogy /AP-t írjon, akkor a saját byte-jait kapja vissza, nem a HotPDF rekonstrukcióját

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);
    // újra kérdezzük: a generátor az AP /N-t hozzáfűzte a widgethez
    NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
  end
  else
    NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;

Innen a két annotációcsalád szétválik. A widgetet a GetLoadedFormFieldIndexForAnnotation oldja vissza a tulajdonos mezőre, majd az EnsureLoadedFieldAppearanceStream-nak adja, annak a field appearance generátornak, amely v2.328.0 óta része ennek a Delphi PDF librarynek. Az újabb field renderer megírása helyett éppen az újrahasznosítás a lényeg: már kezeli a Type0 fontokat, a sortörést, a quaddinget, a checkbox- és radio-/AS állapotokat, valamint az /MK forgatását, ugyanazzal a gépezettel, amely az AcroForm mezők hozzáadását már betöltött PDF-hez végzi. Minden más a markup-szintetizálóhoz kerül. A hívó számára semmi sem változik: ugyanaz az egysoros flatten hívás most nem nulla darabszámot ad vissza azokon a dokumentumokon, amelyek korábban nullát adtak

Doc:= THotPDF.Create(nil);
try
  Doc.LoadFromFile('needappearances-form.pdf');
  // v2.743.0: az AP nélküli widgetek és markupok szintetizálódnak, majd beégnek
  Flattened:= Doc.FlattenLoadedAnnotations;          // minden oldal, minden subtype
  // Flattened:= Doc.FlattenLoadedAnnotations('1-3', 'Highlight');
  if Flattened= 0 then
    raise Exception.Create('nothing was flattened');
  Doc.SaveLoadedDocument('flattened.pdf');
finally
  Doc.Free;
end;

Miért kerül rossz helyre a QuadPoints és az InkList?

Azért, mert ezek a koordináták page user space-ben vannak, a szintetizált appearance stream viszont a saját /BBox terében rajzol, és a két origó nem ugyanaz a pont. Az ISO 32000-1 176. táblája a text markup annotációk /QuadPoints mezőjét default user space-ben definiálja, a 174. tábla ugyanezt teszi a line annotáció /L végpontjaival, az /InkList pedig ugyanezt a konvenciót követi. A HotPDF olyan /BBox-ot ad a szintetizált formnak, hogy [0 0 W H], az origója pedig a /Rect bal alsó sarkán ül. Ezért a /QuadPoints, /L vagy /InkList minden pontját a /Rect bal alsó sarkának negált értékével kell eltolni, mielőtt a content streambe írjuk. Ha ezt elrontod, a 700 ponttal feljebb lévő soron található highlight 700 ponttal a saját doboza fölé kerül, a gyakorlatban pedig sehova sem rajzol. A korrekció koordinátánként egy kivonás, és együtt működik a bake által később kibocsátott cm-mel: az a mátrix a /BBox-ot visszaképezi a /Rect-re, így a két lépés helyes abszolút geometriára oltja egymást

// Az /L végpontjai page user space-ben vannak (ISO 32000-1 174. tábla),
// a form BBox origója a /Rect bal alsó sarkán ül, ezért -(RL, RB)-vel tolunk
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;

Mit rajzol valójában a szintetizált markup appearance?

A markup-szintetizáló csak az annotáció dictionaryját olvassa, semmi mást, ettől kiszámítható a kimenet, és őszintén jelzi, amit nem tudhat. A FreeText és a Stamp a /Contents-et a /DA-ból kiolvasott fonttal és színnel, a /Q szerinti igazítással és 2 pontos paddinggel rajzolja. A Square és Circle re operátort vagy négyíves Bezier-kontúrt rajzol, a /C szerint körvonalazva, jelen lévő /IC esetén kitöltve, a /BS /W szerinti szélességgel. A Line és Ink a csúcspontokat körvonalazza. A Highlight minden quadot kitölt, az Underline, StrikeOut és Squiggly pedig szabályt húz a quad aljára, közepére, illetve egypontos cikkcakkot rajzol. A /CA 1 alatti értékéből ca bejegyzést tartalmazó ExtGState lesz, amelyre a stream elején /GSA gs hivatkozik

A szövegkódolást az AcroForm /DR /Font bejegyzése dönti el, amelyet a /DA nevez meg. Ha ennek a fontnak a /Subtype-ja Type0, a HotPDF a stringet UTF-16BE hex literálként, FEFF byte order markkal írja ki; különben escape-elt literálstringet bocsát ki, a zárójeleket és backslash-eket escape-elve, a 126 feletti byte-okat pedig oktális alakban. A /DA-ból érkező Tf operátor még a BT előtt kerül ki, ami szabályos, mert a text state megmarad a text object határán, és nem kell szétszedni a /DA stringet. Két korlátot érdemes világosan kimondani. A sortöréshez és quaddinghez használt vonalszélesség half-em/full-em heurisztika, nem valódi fontmetrika, ezért proporcionális fonttal az igazítás közeli, de nem pontos. Egy olyan subtype pedig, amelyből semmi sem szintetizálható — Popup, Link, vagy olyan Stamp, amelynek egyetlen tartalma egy ikon neve — nil-t ad, és változatlanul érintetlen marad

Az ideiglenes /Annots-csere, amely megbünteti a segítőkész takarítást

A FlattenOneWidget, a FlattenLoadedFormFields által használt widgetenkénti útvonal aliasing-csapda, amelyet a közös flatten ciklus bármely módosításának tiszteletben kell tartania. Ideiglenesen egyetlen elemű tömbre cseréli az oldal /Annots értékét, hogy az általános flatten pass csak egy widgeten fusson, majd finally blokkban visszaállítja az eredeti PHPDFDictionaryItem pointert. A visszaállítás egy dictionary-slotba ír, amelyet a hívás előtt mentett el

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, ha a belső ciklus felszabadította ezt az itemet
  TemporaryAnnots.Free;
end;

Adj a közös belső ciklushoz egy ésszerűnek látszó rendrakást — egy DeleteValue('Annots') hívást, amikor a tömb kiürül, hogy a mentett oldal ne hordozzon maradék üres tömböt —, és ez a hívás felszabadítja ugyanazt a dictionary itemet, amelyre a DictItem mutat. A finally ezután dangling pointeren keresztül ír, a folyamat pedig „Invalid pointer operation” hibával elhasal. Két meglévő teszt azonnal elkapta, és ez az egyetlen ok, hogy most lábjegyzet, nem pedig support-ticket. A szabály általános: mielőtt cleanupot adsz egy közös ciklushoz, nézd meg a hívókat alias- vagy csere-szerződésekért. Egy maradék üres /Annots tömb kozmetikai hiba, nem éri meg feláldozni érte a pointer élettartamának garanciáját

Mi marad beégetetlen, és mibe kerül a flattenelés?

A rejtett annotációk szándékosan kimaradnak. Az olyan annotáció /F egész számának, amelynek 2-es bitpozíciója be van állítva, az ISO 32000-1 12.5.3 szerint hidden a státusza, és amikor /AP sincs rajta, nagy a kísértés, hogy szintetizáljunk hozzá egyet, majd a többivel együtt beégessük. Ez biztonsági következményű hiba lenne: egy láthatatlan megjegyzés page contentbe égetése minden fájlmegnyitó számára láthatóvá teszi azt. A HotPDF ezeket pontosan érintetlenül hagyja, és nem számolja bele a visszatérési értékbe. Ugyanilyen világosan kell elmondani a felhasználóknak a beégetett annotációk árát. A flattenelés visszafordíthatatlan — az annotáció törlődik az oldal /Annots tömbjéből, a vizuális tartalma pedig page contentté válik, így nincs több mezőérték-szerkesztés, kommentfolyam, /AS állapotváltás és a strukturált adatok visszanyerésének módja sem, az eredeti fájl kivételével. Flattenelj másolatot, tartsd meg az eredetit, és csak akkor nyúlj hozzá, amikor a dokumentum űrlapból rekorddá válik. Ha a problémád XFA-alapú, nem appearance nélküli, akkor a különálló HotPDF XFA–AcroForm flatten útvonallal kezdd, ha pedig még építed az űrlapot, az AcroForm mezőműveletek és validáció bekötéséről szóló jegyzet tárgyalja az írási oldalt

Egy ellenőrzési csapdát is érdemes megemlíteni, mert különben elvisz egy délutánt. Az ExtractLoadedPageGlyphs nem lép be a Form XObjectokba, a beégetett appearance pedig pontosan egy ilyenben él — az oldal content streamje csak egy q ... cm /FlatAn<n> Do Q sorozatot tartalmaz. Egy flattenelt oldal glyphkinyerése ezért semmit sem jelent, és ez helyes működés, nem elveszett bake. Ellenőrizd bájtszinten, az /FlatAn erőforrásnév, a Do hívás és a /Subtype /Form meglétét keresve, vagy a renderelési pipeline-on keresztül, amely ténylegesen kibontja az XObjectokat

Az annotáció-flattenelés addig tűnik háromsoros átírásnak, amíg nem találkozol azokkal a dokumentumokkal, amelyeket a valóságban generálnak. Ha Delphiben vagy C++Builderben kitöltött űrlapokkal, review-markuppal vagy archiválási kimenettel dolgozol, érdemes elolvasni, hogyan kezeli a HotPDF Delphi PDF component a betöltött dokumentum AcroForm- és annotációs oldalát, mielőtt saját appearance-generátort építenél rá