A v3.539.30 előtt a losLab PDF Library TPDFlib.ImportAnnotationsFromFDFString metódusa annyi FDF annotációbejegyzést adott vissza, amennyit ki tudott parseolni, miközben egyetlenegyet sem vett fel a dokumentumba: minden bejegyzést megszámolt, minden bejegyzést eldobott. A v3.539.30 óta az FDF importáló bármilyen sorrendben olvassa a kulcsokat, helyesen és locale-függetlenül parseolja a /Rect-et, és a hozzá tartozó exportáló az annotáció valódi /Rect-jét írja, így egy export, import és második export bájtonként azonos FDF-et produkál. Ez a jegyzet a továbbiakban azt magyarázza el, hogyan született egy rossz kezdőoffsetből tökéletes csendes hiba, melyik három további hiba bújkált mögötte, és hogyan ellenőrizd te magad az importot a visszatérési értékbe vetett hit helyett
A forgatókönyv hétköznapi. Egy lektor megjegyzésekkel látja el a szerződést, a kommentek FDF fájlként utaznak (az Acrobatban ez az Export Comments), a Delphi szolgáltatásod pedig a ImportAnnotationsFromFDF-fel olvasztja őket egy tiszta példányba. A hívás 7-et ad vissza, a log azt írja, hogy „7 comments imported", a job zöldre vált, és a kimeneti PDF-ben nincs egyetlen komment sem. Semmi exception, semmi figyelmeztetés, és a szám hihetőnek tűnt, mert a fájlban lévő bejegyzések valódi darabszáma volt. Ez a legrosszabb alak, amiben egy hiba megjelenhet: egy függvényé, aminek az egyetlen sikerjele egy olyan számláló, ami attól a munkától függetlenül számolódik, amiről állítja, hogy jelenti
Miért jelentett sikert az ImportAnnotationsFromFDFString, miközben semmit nem vett fel?
Az importáló minden /Subtype-ot üres stringként olvasott, és az annotációt létrehozó helper üres altípusnál korán kilép, miközben a hívó amúgy is növeli az eredményt. A kulcskereső a /Subtype közvetlen utáni pozíciót adta vissza, ami az érték előtti whitespace. A ReadName ezen a szóköznél indult, és az első whitespace karakternél állt meg, tehát még bármi elolvasása előtt megállt. A AddAnnotationToPage altípus nélkül nem épít annotációt, ami önmagában helyes defenzív döntés, de az eljárásnak nem volt visszatérési értéke, és az Inc(Result) rajta kívül ült. Minden egyes őrzés önmagában észszerű volt; együtt a „semmi sem működött"-ből „minden működik"-et csináltak. A javítás miatt a ReadName átugorja a whitespace-t, megköveteli a PDF name objektum kezdő /-jét, és bármilyen határolónál megáll, a [-nál, a (-nél és a )-nél is, így a /Subtype/Text és a /Subtype /Text egyaránt Text-et ad
A visszatérési érték a javítás után is gondoskodást érdemelt. A v3.539.39-ig a ImportAnnotationsFromFDFString továbbra is növelte az eredményét a /Annots tömb minden jól formált szótáráért, azokért a bejegyzésekért is, amiknek a nullától számolt /Page-je kilógott a tartományból, vagy hiányzott a /Subtype-ja, pedig mindkettő kihagyásra kerül. A PDFlibPas v3.539.40 óta a ImportAnnotationsFromFDFString és a ImportAnnotationsFromFDF a ténylegesen felvett annotációk számát adja vissza, mint az XFDF import: az FDF helper, a AddAnnotationToPage mostantól Boolean-t ad vissza, és a számláló csak siker esetén mozdul. A dokumentum megmérése továbbra is az erősebb ellenőrzés, mert régebbi verziókon is áll, ezért az alábbi vázlat minden oldalon összehasonlítja a AnnotationCount-ot az import előtt és után
function TotalAnnotations(Lib: TPDFlib): Integer;
var
Page, Saved: Integer;
begin
Result := 0;
Saved := Lib.SelectedPage;
for Page := 1 to Lib.PageCount do
if Lib.SelectPage(Page) = 1 then
Inc(Result, Lib.AnnotationCount); // kijelölt oldalonként, widgetekkel együtt
Lib.SelectPage(Saved);
end;
var
Lib: TPDFlib;
Before, Reported, Added: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('contract.pdf', '');
Before := TotalAnnotations(Lib);
Reported := Lib.ImportAnnotationsFromFDF('review-comments.fdf');
Added := TotalAnnotations(Lib) - Before;
if Added <> Reported then // a v3.539.40 óta egyenlő
Writeln(Format('Importer reported %d, %d landed on a page', [Reported, Added]));
Lib.SaveToFile('contract-reviewed.pdf');
finally
Lib.Free;
end;
end;
Három további hiba az első mögött
Csak az altípus kijavítása három további hibát hozott volna felszínre ugyanabban a függvényben, amelyek mindegyike csak azért maradt láthatatlan, mert annotáció sosem jutott el oldalig. Először: a ReadNumber pozícióját értékparaméterként kapta, így a négy /Rect szám egymás utáni olvasása négyszer ugyanazt a helyet olvasta, és nem ugrotta át a nyitó [-t, a gyakorlatban tehát semmit sem olvasott. Másodszor: a FindKey egyetlen előremozduló kurzort osztott meg minden keresés közt. Az exportáló a /Subtype, /Rect, /Page, /Contents, /T, /Subj sorrendben ír, az importáló viszont a /Subtype, /Contents, /T, /Subj, /Page, /Rect sorrendben keresett; amint a kurzor túlment a /Contents-en, a /Page és a /Rect keresése túlfutott az aktuális bejegyzésen, és vagy nem talált semmit, vagy a következő annotáció kulcsaira csapott le. A library a saját kimenetét nem tudta elolvasni. Harmadszor: a számok a PLStrToFloat-on mentek át, ami a rendszer tizedeselválasztóját követi. Az ISO 32000-1 §12.7.7-e PDF objektumszintaxisként definiálja az FDF-et, a PDF szótárkulcsai pedig rendezetlenek (§7.3.7), tehát minden kulcssorrendet feltételező FDF parser konstrukciójából fakadóan rossz, akármelyik eszköz is gyártotta a fájlt
A megjavított importáló előbb minden bejegyzést határol be. A FindDictEnd a nyitó <<-től a hozzá tartozó >>-ig jár, követi az egymásba ágyazott szótárakat, és átugorja a literál stringek testét a backslash escape-jeikkel együtt, így egy kommentben megbúvó >>, mint az (see section >> 4) esetében, nem vethet véget korán a bejegyzésnek. Ezután minden kulcskeresés a bejegyzés saját kezdetén indul, és a végéig terjed, ami a kulcssorrendet lényegtelenné teszi, és megakadályozza, hogy egy annotáció kölcsönvegye egy másik /Page-jét. A kulcs-egyezés elfogad közvetlenül a név után álló határolót is, mert a /Contents(Hi) ugyanolyan érvényes, mint a /Contents (Hi), a szóhatár-szabály viszont megakadályozza, hogy a /Subj a /Subtype elejére csapjon le, vagy a /T a /Type-ra. A ReadNumber mostantól var paraméterként veszi a pozícióját, átugorja a whitespace-t és a [-t, és a PLTryStrToFloatInvariant-tal parseol, ami rosszul formált tokenen finoman megbukik exception dobása helyett. Ha a négy téglalapszám bármelyike megbukik, mind a négy nullára esik vissza, félbeolvasott téglalap gyártása helyett
Miért tolódott el minden annotáció a saját magasságával FDF oda-vissza utakon?
A régi exportáló rossz koordinátamodellben írta ki a téglalapot. Egy annotáció /Rect-je [llx lly urx ury] az alapértelmezett user space-ben (ISO 32000-1 §12.5.2, a téglalapok definíciója §7.9.5), és az FDF ugyanezt a tömböt hordozza. A ExportAnnotationsToFDFString viszont a GetAnnotRectEx-t hívta, ami Left, Top, Width és Height értékeket jelent a library rajzolási koordinátáiban, abban a térben, amit a SetOrigin irányít, majd ezeket [L T L+W T+H] alakban szerializálta. Az importáló, amint már működött, ezt a négy értéket változatlanul PDF téglalapként írta vissza, így a felső él oda került, ahová a bal alsó sarok tartozott, és minden oda-vissza út az annotációt a saját magasságával tolta felfelé. Az exportáló mostantól az annotáció saját /Rect számait másolja — három tizedes, pont elválasztó, exponenciális nélkül —, és csak akkor tér vissza a számított téglalapra, ha a tárolt tömb hiányzik, vagy nem négy szám
Ezt rögzítő regressziós tesztet érdemes átemelni, mert a dokumentumra és egy második exportra állít, nem az importáló visszatérési értékére. Jegyezd meg a várt 2-es darabszámot: a AddNoteAnnotation egy Text annotációt hoz létre a Popupjával együtt, és mindkettő utazik. A teszt az exportot és az importot vesszős tizedeselválasztó alatt futtatja is, itt lakik ugyanis a történet másik fele
var
Source, Target: TPDFlib;
FDF: AnsiString;
OldSep: Char;
begin
Source := TPDFlib.Create;
Target := TPDFlib.Create;
try
Source.NewPages(1); // mostantól két oldal
Source.SelectPage(2);
Source.AddNoteAnnotation(50.5, 60.25, 0, 80, 80, 120, 60,
'Reviewer', 'Check this', 0.25, 0.5, 0.75, 0);
Target.NewPages(1);
OldSep := FormatSettings.DecimalSeparator;
FormatSettings.DecimalSeparator := ','; // német vagy francia asztal szimulálása
try
FDF := Source.ExportAnnotationsToFDFString; // továbbra is /Rect [50.5 ... alakban ír
Target.ImportAnnotationsFromFDFString(FDF);
finally
FormatSettings.DecimalSeparator := OldSep;
end;
Target.SelectPage(2);
Assert(Target.AnnotationCount = 2); // a jegyzet és a popupja
Assert(Target.GetAnnotType(1) = 'Text');
Assert(Target.ExportAnnotationsToFDFString = Source.ExportAnnotationsToFDFString);
finally
Target.Free;
Source.Free;
end;
end;
Légy tisztában azzal, mit hordoz az FDF útvonal. Az importáló minden bejegyzést szótárként épít újra /Type, /Subtype, /Rect, /Contents, /T és /Subj kulcsokkal; a szín, a flagek, a keretstílus, a popup linkek és az appearance streamek nem részei ennek az útvonalnak, az exportáló pedig kihagyja a Widget annotációkat, mert az űrlapmezők a form-data metódusokhoz tartoznak. A teljes térkép arról, melyik adat melyik metóduson utazik, a FDF, XFDF és XFA form data interchange áttekintésében van, és ha meg kell nézned, mi érkezett meg valójában, az indexenkénti olvasókat, mint a GetAnnotType, a GetAnnotTitle és a GetAnnotContentsEx, a outline, annotation és action introspekció tárgyalja
Hogyan olvass vesszős tizedesű FDF és XFDF fájlokat régebbi exportokból?
Az FDF-re a válasz egyértelmű: vessző nem határoló a PDF szintaxisban, így egy pontosan egy vesszőt és egy pontot sem tartalmazó számtoken csak vesszős locale-s gépen írt tizedes lehet. A korábbi verziók tényleg írtak ilyen fájlokat, például /Rect [10,500 20,250 40,750 60,125], az új ReadNumber pedig még parseolás előtt ponttá alakítja ezt az egy vesszőt. Két vesszős, vagy vesszőt és pontot is hordozó tokent elutasít, nem találgat. Az olvasó az exponenciális jelölést sem fogyasztja el, ami az ISO 32000-1 §7.3.3-ával egyezik: a PDF számok sosem használják
Az XFDF nehezebb eset, mert XML attribútumokban a vessző maga az elválasztó. A standard XFDF (ISO 19444-1) rect="50.5,80.25,70.75,100.125" és dashes="4,2" értékeket ír, a v3.539.28 és korábbi viszont vesszős locale-s rendszeren rect="50,500 80,250 70,750 100,125" és opacity="0,600" alakot írt, és egy standard opacity="0.6" olvasásakor EConvertError-ral bukott el. A v3.539.29 óta mindkét irány invariant, és a legacy alakot a XFDFNormalizeLegacyDecimals csak akkor ismeri fel, ha az attribútum whitespace-en pontosan a várt számú tokenre esik szét (a rect-nél négy, a opacity és a width esetén egy), és minden token számjegy-vessző-számjegy alakú. Egy standard rect sosem egyezik: vagy egy token három vesszővel, vagy vesszőre végződő tokenek. A dashes-t szándékosan békén hagyja, mert a 4,2 lehet két kötőjelhossz vagy egy legacy 4.2, és szabály nem tudja megkülönböztetni őket
const
// Kulcsok az exportáló sorrendjén kívül, vesszős tizedekkel egy régi vesszős locale-s exportból
LegacyFDF: AnsiString = '%FDF-1.2'#10'1 0 obj'#10'<< /FDF << /Annots ['#10 +
'<< /Rect [10,500 20,250 40,750 60,125] /Page 0 /Contents (First) ' +
'/Subtype /Text /T (Alpha) /Type /Annot >>'#10 +
'] >> >>'#10'endobj'#10'trailer'#10'<< /Root 1 0 R >>'#10'%%EOF'#10;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create; // egy friss dokumentumnak egy oldala van
try
Lib.ImportAnnotationsFromFDFString(LegacyFDF);
Assert(Lib.AnnotationCount = 1);
Assert(Lib.GetAnnotTitle(1) = 'Alpha');
// Újraexportálva XFDF-ként pont tizedekkel: rect="10.500 20.250 40.750 60.125"
Writeln(Lib.ExportAnnotationsToXFDFString);
finally
Lib.Free;
end;
end;
Min állítson valójában egy annotációimport-teszt?
Egy hasznos importteszt a céldokumentum állapotára állít, sosem csak arra, amit az importáló önmagáról állít. Semmi a tesztcsomagban nem ellenőrizte az AnnotationCount-ot FDF import után, és a visszatérési érték, az egyetlen szám, amit bárki megnézett, pont az a szám volt, amit a hiba érintetlenül hagyott. Három állítás minden itt leírt hibát elkapott volna: az annotációszám a várt oldalon, egy mező visszakiolvasása a GetAnnotType-on vagy a GetAnnotContentsEx-en, és egy második export bájtonkénti összehasonlítása az elsővel. Ugyanez a fegyelem vonatkozik minden olyan API-ra, ami tömegesen írja újra a dokumentumstruktúrát, ideértve a duplikált űrlapmezők összefésülését is: az eredményül kapott fát ellenőrizd, nem egy visszaadott végösszeget. Az FDF és XFDF annotációs metódusok a fájl- és string-változataikkal együtt a Delphire és C++Builderre szánt losLab PDF Library for Delphi and C++Builder részei, és ha a kommenteknek túlélniük kell az utat, a v3.539.30 vagy újabb a verzió, ha pedig a visszaadott darabszámnak kell egyeznie a felvettekkel, a v3.539.40 vagy újabb