Műszaki cikk

Delphi PDF annotációk áttekintése PDFium komponenssel

Egy PDF-jegyzet egy oldalhoz csatolt szótár, nem egy rárajzolt jel. Az ISO 32000-1 §12.5 nagyjából két tucat altípust határoz meg, és mindegyik hordoz egy /Subtype-ot, egy oldalkoordinátákban megadott téglalapot, jelzők egy halmazát, és rendszerint egy megjelenési adatfolyamot, amely eldönti, mit fest ki ténylegesen egy nézegető. Az altípusok nem mindegyike ugyanazt jelenti egy dokumentumot átnéző ember számára. Egy Highlight és egy Ink vonás megjegyzés; egy Link navigáció; egy Popup az a kis ablak, amely akkor nyílik meg, amikor rákattintasz egy ragasztott cetlire, saját objektumként tárolva, egy szülő által rámutatva. A válaszok teljes Text-jegyzetek, amelyek egy in-reply-to bejegyzésen keresztül hivatkoznak arra a megjegyzésre, amelyre válaszolnak. Így az oldalszintű jegyzettömb nem az átnéző megjegyzéslistája. Egy lapos zsák, amely megjegyzéseket, az őket összekötő vezetékrendszert, és néhány olyan dolgot tartalmaz, amit egyetlen átnéző sem nevezne megjegyzésnek. Egy panel, amely a tömböt megjegyzéslistaként kezeli, nem fog egyetérteni egyetlen más nézegetővel sem, amit az ügyfél futtat

Egy jegyzet-átnézési munkafolyamat felépítése a PDFium Component-en, a PDFium-alapú VCL/LCL komponensen Delphihez, C++Builderhez és Lazarushoz, azt jelenti, hogy azokra a pontokra összpontosítasz, ahol a nyers tömb és az emberi nézet közötti szakadék bajt okoz: számlálás, indexelés, olyan jelek átszínezése, amelyeket a motor már befagyasztott, törlés szellemek hátrahagyása nélkül, és saját jelek hozzáadása

Diagram: hogyan szűri egy Delphi PDFium lektori panel a nyers oldalannotációs tömböt megjegyzésekből, felugrókból, válaszokból és hivatkozásokból a válogatott megjegyzéslistává, amelyet a lektor lát
Az oldal annotációtömbje megjegyzéseket kever felugró ablakokkal, válaszokkal, hivatkozásokkal és rejtett jelekkel, ezért egy lektori panelnek számlálási szabályra van szüksége, mielőtt összesítést mutatna

Miért nem egyezik soha a számod az Acrobat megjegyzéspaneljével

Nyiss meg egy megjegyzésekkel teli szerződést egymás mellett a saját nézegetődben és az Acrobatban, és az összegek ritkán egyeznek. Az Acrobat egy szerkesztett nézetet mutat: a jelöléseket válaszszálakba csoportosítva, a popupokat beolvasztva azokba a jegyzetekbe, amelyekhez tartoznak, a linkeket és az űrlap-widgeteket kihagyva. A nyers tömb mindezt megkülönböztetés nélkül tartalmazza, így egy naiv számlálás egyes szempontból túl magasra, másokból egyszerre túl alacsonyra fut

A popupok felfújják az összeget, mert minden ragasztott cetli egy külön Popup objektummal érkezik, és mindkettő megszámlálása megduplázza a jegyzetet. A válaszok lecsökkentik, ha látható jelekre szűrsz, mivel egy válasz egy Text-jegyzet, amelyből semmi nincs kifestve, amíg valaki ki nem bontja a szálat, és elhagyása elveszíti a beszélgetést. A Hidden és a NoView jelzők leveszik a jegyzetet a képernyőről anélkül, hogy kivennék a tömbből, így egy jelzőkre vak számlálás olyan jeleket is tartalmaz, amelyeket a felhasználó nem lát. A Link-jegyzetek ugyanabban a tömbben ülnek, mint a megjegyzések, és sem a számlálásba, sem a listába nem tartoznak. Döntsd el a számlálási szabályt, mielőtt megírnád a ciklust, és írd le a döntést, mert az „miért mutat a paneled más számot, mint az Acrobat" az első jegy, amit egy átnézési funkció megkap

Mindent egyszer indexelj, aztán soha ne elemezz újra egy oldalt

Egyetlen tervezési szabály vezérli mindazt, ami ezután következik: a szerző, típus vagy oldal szerinti szűrés soha nem elemezheti újra az oldalobjektumokat. Egy 300 oldalas, erősen jelölt dokumentumon az újraelemzés minden legördülő menü-váltásnál olyanná teszi a panelt, amely másodpercekig akadozik egyszerre. A komponens felkínálja az AnnotationCount-ot és az indexelt Annotation[] tulajdonságot, mindkettőt az aktuálisan betöltött oldalra korlátozva, és a TPdfAnnotation rekord, amelyet visszaadnak, azt hordozza, amire egy listanézetnek szüksége van: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. A helyes lépés az, hogy megnyitáskor egyszer végigsöpröd az összes oldalt, és fenntartod a saját lapos indexedet:

procedure TReviewPanel.BuildIndex;
var
  PageNo, i: Integer;
  A: TPdfAnnotation;
begin
  FItems.Clear;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    for i := 0 to Pdf.AnnotationCount - 1 do
    begin
      A := Pdf.Annotation[i];
      // Csak az átnéző szempontjából releváns altípusokat tartsd meg; jegyezd fel az oldalt
      // és index párost, mert minden későbbi szerkesztés ezzel van címezve
      if A.Subtype in [anText, anHighlight, anInk] then
        FItems.Add(TReviewItem.Create(PageNo, i,
          A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
    end;
  end;
end;

Az aláhúzásra érdemes pár a (PageNo, i). Minden későbbi módosítás, legyen az átszínezés vagy törlés, oldalszám plusz jegyzetindex alapján van címezve, és az index törékeny: egy jegyzet eltávolítása újraszámozza mindazt, ami utána következik azon az oldalon. Ezért tervezd meg, hogy bármilyen törlés után újraépíted az érintett oldal bejegyzéseit, ahelyett hogy helyben javítanád az indexszámokat. Az újraépítés egy ezredmásodpercbe kerül. Egy elavult index ezzel szemben a rossz átnéző megjegyzését törli, ami az a fajta hiba, amely aláássa a bizalmat az egész funkcióban

A szálazás megérdemel egy helyet az indexben, még akkor is, ha az első kiadásod csak megszámolja a válaszokat, nem mutatja meg őket. Csoportosítsd az elemeket a szülőhivatkozásuk szerint, amíg az oldal nyitva van, hogy a panel később úgy tudja összecsukni a szálat, ahogy az Acrobat teszi. Ha ezt a csoportosítást lustán, görgetés közben építed újra, azzal meghiúsítod az egyszeri indexelés teljes lényegét, mert olyan oldalakat nyitsz meg újra, amelyek elemzéséért már fizettél. A geometria ugyanazt a fegyelmet igényli. A Rectangle minden rekordban oldaltéri, és az átalakítását nézetkoordinátákká egyetlen közös segédfüggvényben kell elhelyezni, nem szétszórva a kódban. A panelek koordinátahibákat növesztenek, amikor a kijelölés, a hit-teszt és a festés mindegyike a saját zoom- és forgatásmatematikáját találja ki; vezesd mindhármat egyetlen átalakításon keresztül, és egy kiemelés, a listában lévő sora, valamint a kattintási célpontja ugyanahhoz a tintavonalhoz marad rögzítve

Jelölések átszínezése és a megjelenési adatfolyam vétója

Egy kiemelés sárgáról borostyánszínűre változtatása egysorosnak hangzik, és néha az is. A csapda az ISO 32000-1 §12.5.5. Amikor egy jegyzet /AP megjelenési adatfolyamot hordoz, egy szabványkövető nézegető ezt az előre elkészített adatfolyamot festi ki, és a szótár színbejegyzését holt metaadatként kezeli. Az Acrobat lényegében mindenhez ír megjelenési adatfolyamot, amit létrehoz, így az ügyfelektől érkező legtöbb jegyzet már ebben az állapotban van, és a szín, amit magabiztosan beállítottál, soha nem jut el a képernyőre. Az átszínezés egy olvas-módosít-ír művelet az Annotation[] tulajdonságon keresztül, és a komponens őszinte a konfliktussal kapcsolatban: amikor a motor megtagadja, hogy egy szótárszín felülírjon egy beégetett megjelenést, az írás EPdfError-t dob

Diagram az olvasmódosítír újraszínezési útról egy Delphi PDFium komponensben: beégetett megjelenési stream megvétózza a szótárszínt, és EPdfErrort emel
Ha egy annotáció előre elkészített /AP streamet hordoz, a motor elutasítja a szótár színét, és EPdfError-t dob, így a panel átszínezi a saját fedőrétegét, vagy megjelöli a sort megjelenés-zároltként
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF;       // borostyán
A.ColorAlpha := 160;
try
  Pdf.Annotation[Item.Index] := A;
except
  on EPdfError do
  begin
    // A jegyzet egy előre renderelt /AP adatfolyamot birtokol; önmagában a szótár
    // színe nem tudja megváltoztatni, mit festenek ki a nézegetők
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

Fogd el ezt a kivételt minden alkalommal, és kezeld információként, ne hibaként. Ha kihagyod a védelmet, a paneled vidáman borostyánszínt mutat a saját listájában, miközben az oldal továbbra is sárgát fest; a felhasználó heteken belül úgy jelenti be, hogy „a nézegetőd figyelmen kívül hagyja a szerkesztéseimet", te pedig egy délutánt töltesz azzal, hogy sikertelenül próbálod reprodukálni egy olyan fájlon, amelynek éppenséggel nincs megjelenési adatfolyama. Amint tudod, hogy a megjelenés zárolva van, két becsületes válaszod van: átszínezed a saját kijelölés-overlayedet a jegyzet helyett, hogy az átnéző legalább lássa a kiválasztott kiemelést, vagy megjelölöd a sort megjelenés-zároltként, hogy senki ne várja, hogy a változás megmaradjon

Jegyzetek törlése szellemek hátrahagyása nélkül

A DeleteAnnotation eltávolítja az objektumot az aktuális oldal jegyzetfájából, de érintetlenül hagyja a gyorsítótárazott oldalraszter-t. Ha közvetlenül a hívás után festesz, a törölt kiemelés még mindig a képernyőn van, egy olyan bitképben ülve, amely már nem egyezik a mögötte lévő dokumentummodellel. A megoldás az, hogy az újrarendelést a törlés részének kezeled, nem egy olyan lépésnek, amelyet a hívó elfelejthet:

Diagram a háromlépéses Delphi PDFium törlési ciklusról: eltávolítja az annotációt, reAnnotationsnel rendereli újra az oldalt, és újjáépíti az oldalindexet
A törlés csak az annotációfát érinti, ezért a panelnek újra kell renderelnie reAnnotations-szel, és újra kell építenie az oldalbejegyzéseket, mielőtt a kijelző és az index újra becsületes lenne
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // hiba esetén EPdfError-t dob
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // a RenderPage a hívónak adja a bitkép tulajdonjogát
end;
RebuildPageEntries(Item.PageNo);  // az Item.Index utáni indexek eltolódtak

Két részletet könnyű elrontani ebben a blokkban. A reAnnotations opciónak jelen kell lennie, különben az új raszter minden megmaradt jegyzetet elejt, és az oldal úgy néz ki, mintha az egész megjegyzéskészletet törölted volna egyetlen jel helyett. És a Bmp.Free nem opcionális: a függvényszerű RenderPage túlterhelés a hívónak adja a bitkép tulajdonjogát, így egy elmaradt felszabadítás minden egyes törlésnél egy teljes oldalas rasztert szivárogtat, amit egy hosszú dokumentumon dolgozó átnéző perceken belül valódi memórianyomássá alakít

Átnézői jelek hozzáadása a saját felhasználói felületedről

A jegyzetek létrehozása a CreateAnnotation-on keresztül történik, amely egy kitöltött TPdfAnnotation rekordot (altípus, téglalap, szín, tartalom, szerző) vesz át, és az aktuális oldalhoz csatolja. Egy ragasztott cetli, anText altípussal, az egyszerű eset: állítsd be a pozíciót, a tartalmat és a szerzőt, és kész vagy. Az Ink-jegyzeteknél akadnak fenn az emberek. A rekord téglalapja csak a rajzot határolja; maguk a vonások pontok tömbjei, amelyeket külön kell csatolni a motor tintavonás-hívásán keresztül, a FPDFAnnot_AddInkStroke-on, amelynek FS_POINTF adatokat adsz, egér- vagy tollbemenetből egyenként rögzített vonásonként. Ha egy Ink-jegyzetet csak egy téglalapból építesz fel, és semmi másból, egy üres firkát kapsz, amely üres helyként renderelődik, ami úgy néz ki, mint egy hiba a motorban, valójában pedig egy félbehagyott jegyzet

Ugyanabban a lélegzetvételben rendezd le a szerzőségi politikát is. Minden jelnek, amelyet a felhasználói felületed létrehoz, egy konzisztens AuthorText-et kell hordoznia, mert az átnéző-szűrő, amit jövő hónapban építesz, csak annyira jó, amennyire a nevek, amelyeket ma a megjegyzésekre bélyegzel. Az üres vagy inkonzisztens szerzőkarakterláncok visszamenőlegesen nem javíthatók minden fájl újbóli megnyitása nélkül

Az átnézés kihozatala a nézegetőből

Az átnézési adatok akkor válnak igazán hasznossá, amikor el tudnak hagyni a nézegetőt, mint egy összefoglaló, amelyet a projektvezető a fájl megnyitása nélkül olvas el, vagy egy CSV, amely egy nyomkövető táblázatot táplál. Exportálj a már felépített indexből, soha ne egy friss elemzésből, és válassz egy stabil módot arra, hogy visszahivatkozz minden egyes jelre. Egy oldalszám a jegyzet téglalapjával párosítva túlél olyan oda-vissza utakat, amelyeket egy tömbindex nem, mert a következő törlés csendben újraszámozza az indexeket, és a CSV-d rossz megjegyzésekre kezd mutatni

Egy megtartásra érdemes sor hordozza az oldalt, az altípust, a szerzőt, a létrehozási időbélyeget, ha a fájl rögzít egyet, a tartalmi szöveget, és egy státuszoszlopot, amelyet te birtokolsz, nem pedig egyet, amit a PDF szolgáltat. Ugyanaz az indexelési átfutás korábban is hasznos, a beérkezéskor, amikor egy dokumentum a csapaton kívülről érkezik, és tudni akarod, mi van benne, mielőtt bárki átnézné. A PDF-beérkezési munkapad-cikk végigvezet ezen a triázson, a form-mező navigáció pedig a tükörképi problémát fedi le: olyan dokumentumok átnézését, amelyeket adatgyűjtésre építettek, nem megjegyzésekre

Egy eset, amit a tömb nem fog megmutatni

Egy hibamód megérdemel egy figyelmeztető jelzést, mert úgy néz ki, mint egy hiba a kódodban, pedig nem az. Egy ügyfél látható kiemeléseket jelent egy egész oldalon, de a paneled semmit sem listáz, és az AnnotationCount nullával tér vissza. A szokásos magyarázat az, hogy a jeleket valahol feljebb belesimították. A belesimítás beégeti a jegyzetmegjelenéseket a hétköznapi oldaltartalomba, így a kiemelések az oldalgrafika részévé válnak, és teljesen megszűnnek jegyzetobjektumként létezni. Nem marad semmi, amit egy jegyzet-API felsorolhatna, átszínezhetne vagy törölhetne. Amikor kifestett jelöléseket látsz nulla számmal, hagyd abba a hiba keresését a felsorolási ciklusodban, és kérdezd meg, hogyan készült a fájl

Az itt használt jegyzetfelület, a felsorolástól és létrehozástól kezdve az átszínezésen és törlésen át a megjelenítést becsületesen tartó renderelési opciókig, a Delphihez, C++Builderhez és Lazarus/FPC-hez készült PDFium Component részeként érkezik