Prije v3.539.30, TPDFlib.ImportAnnotationsFromFDFString u losLab PDF Library vraćao je broj FDF anotacijskih unosa koje je parsirao, a da nijedan nije dodao u dokument: svaki je unos bio prebrojen, svaki je unos otpao. Od v3.539.30 FDF uvoznik čita ključeve u bilo kojem redoslijedu, parsira /Rect ispravno i neovisno o lokalu, a pripadni izvoznik zapisuje stvarni /Rect anotacije, pa izvoz, uvoz i drugi izvoz daju bajtno identičan FDF. Ostatak ove bilješke objašnjava kako je jedan pogrešan početni offset proizveo savršeno tihi kvar, koja su se tri druga nedostatka skrivala iza njega i kako sami provjeriti uvoz umjesto da vjerujete povratnoj vrijednosti
Scenarij je svakidašnji. Recenzent popravi ugovor komentarima, komentari putuju kao FDF datoteka (Acrobat to zove Export Comments), a vaša Delphi usluga spoji ih u čistu kopiju s ImportAnnotationsFromFDF. Poziv vrati 7, log kaže „7 comments imported", posao postane zelen, a izlazni PDF nema nijedan komentar. Ništa nije bačeno, ništa nije upozorilo, a broj je izgledao vjerodostojno jer je bio stvarni broj unosa u datoteci. To je najgori oblik koji bug može uzeti: funkcija čiji je jedini signal uspjeha brojač koji se računa neovisno o poslu za koji tvrdi da ga izvještava
Zašto je ImportAnnotationsFromFDFString javio uspjeh, a nije dodao ništa?
Uvoznik je svaki /Subtype čitao kao prazan string, a helper koji stvara anotaciju izaše ranije na prazan subtype dok je pozivatelj svejedno uvećao rezultat. Tražilica ključa vratila je poziciju odmah nakon /Subtype, a to je bijeli prostor ispred vrijednosti. ReadName krenuo je od tog razmaka i stao na prvom bijelom znaku, pa je stao prije nego što je išta pročitao. AddAnnotationToPage odbija izgraditi anotaciju bez subtypea, što je u izolaciji ispravna obrambena odluka, ali je bila procedura bez povratne vrijednosti, a Inc(Result) sjedio je izvan nje. Svaka je zaštita bila razumna sama za sebe; zajedno pretvorile su „ništa nije radilo" u „sve je radilo". Ispravak tjera ReadName da preskoči bijeli prostor, zahtijeva vodeći / PDF name objekta i staje na bilo kojem ograničitelju, uključivo [, ( i ), pa i /Subtype/Text i /Subtype /Text daju Text
Povratna je vrijednost zasluživala pažnju i nakon tog ispravka. Do v3.539.39 ImportAnnotationsFromFDFString i dalje je uvećavao rezultat za svaki dobro oblikovan rječnik u polju /Annots, uključivo unose čiji je 0-based /Page bio izvan raspona ili kojima je /Subtype nedostajao, a oba se slučaja preskaču. Od PDFlibPas v3.539.40 ImportAnnotationsFromFDFString i ImportAnnotationsFromFDF vraćaju broj anotacija koje su stvarno dodane, kao XFDF uvoz: FDF helper AddAnnotationToPage sada vraća Boolean, a brojač se miče samo na uspjeh. Mjerenje dokumenta i dalje je jača provjera, jer vrijedi i na starijim verzijama, pa skica u nastavku uspoređuje AnnotationCount po stranici prije i poslije uvoza
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); // po odabranoj stranici, widgeti uključeni
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 // jednako od v3.539.40
Writeln(Format('Importer reported %d, %d landed on a page', [Reported, Added]));
Lib.SaveToFile('contract-reviewed.pdf');
finally
Lib.Free;
end;
end;
Tri daljnja nedostatka iza prvoga
Popravak samo subtypea izbio bi tri daljnja buga u istoj funkciji, od kojih je svaki bio nevidljiv samo zato što nijedna anotacija nikad nije stigla do stranice. Prvo, ReadNumber primao je svoju poziciju kao value parametar, pa je čitanje četiriju /Rect brojeva zaredom čitalo isto mjesto četiri puta, i nije preskakalo otvorenu [, pa u praksi nije pročitalo ništa. Drugo, FindKey dijelio je jedan naprijed idući kursor između svih traženja. Izvoznik zapisuje /Subtype, /Rect, /Page, /Contents, /T, /Subj, ali je uvoznik tražio redoslijedom /Subtype, /Contents, /T, /Subj, /Page, /Rect; jednom kad je kursor prešao /Contents, traženje /Page i /Rect otrčalo je preko trenutnog unosa i ili nije našlo ništa ili je poklopilo ključeve sljedeće anotacije. Biblioteka nije mogla pročitati vlastiti izlaz. Treće, brojevi su išli kroz PLStrToFloat, koji slijedi decimalni separator sustava. ISO 32000-1 §12.7.7 definira FDF kao PDF object syntax, a ključevi rječnika u PDF-u su neuređeni (§7.3.7), pa je svaki FDF parser koji pretpostavlja redoslijed ključeva pogrešan po konstrukciji, bez obzira koji je alat proizveo datoteku
Popravljeni uvoznik najprije ograniči svaki unos. FindDictEnd hoda od otvarajućeg << do njemu pripadajućeg >>, prateći ugniježđene rječnike i preskačući tijela literalnih stringova s njihovim backslash escapama, pa >> unutar komentara poput (see section >> 4) ne može rano završiti unos. Svako traženje ključa tada kreće od vlastitog početka unosa i ograničeno je na njegov kraj, što redoslijed ključeva čini nebitnim i sprječava jednu anotaciju da posudi tuđi /Page. Poklapanje ključa prihvaća i ograničitelj odmah nakon imena, jer je /Contents(Hi) jednako valjan kao /Contents (Hi), dok pravilo granice riječi sprječava /Subj da poklopi početak /Subtype i /T da poklopi /Type. ReadNumber sada prima poziciju kao var parametar, preskače bijeli prostor i [, te parsira s PLTryStrToFloatInvariant, koja na deformiranom tokenu pada blago umjesto da baci iznimku. Ako bilo koji od četiriju brojeva pravokutnika padne, sva četiri se vrate na nulu umjesto da proizvedu napola pročitan pravokutnik
Zašto su FDF round-tripovi pomicali svaku anotaciju za vlastitu visinu?
Stari je izvoznik zapisivao pravokutnik u krivom koordinatnom modelu. /Rect anotacije jest [llx lly urx ury] u zadanom user spaceu (ISO 32000-1 §12.5.2, uz pravokutnike definirane u §7.9.5), a FDF nosi isto polje. ExportAnnotationsToFDFString zvao je, međutim, GetAnnotRectEx, koji javlja Left, Top, Width i Height u crtaćim koordinatama biblioteke, u prostoru kojim upravlja SetOrigin, te ih serijalizirao kao [L T L+W T+H]. Uvoznik, jednom kad je proradio, zapisao je te četiri vrijednosti doslovno kao PDF pravokutnik, pa je gornji rub sletio tamo gdje pripada donji lijevi kut i svaki je round trip pomaknuo anotaciju gore za vlastitu visinu. Izvoznik sada kopira vlastite /Rect brojeve anotacije, tri decimale, točka kao separator, bez eksponenta, a na izračunati pravokutnik vraća se samo kad pohranjeno polje nedostaje ili nema četiri broja
Regresijski test koji to pribija vrijedi za kopiranje, jer tvrdi na dokumentu i na drugom izvozu, ne na povratnoj vrijednosti uvoznika. Napomenite očekivani broj 2: AddNoteAnnotation stvara Text anotaciju plus njezin Popup, i oboje putuje. Test također vrti izvoz i uvoz pod zarezom kao decimalnim separatorom, a tu živi druga polovica ove priče
var
Source, Target: TPDFlib;
FDF: AnsiString;
OldSep: Char;
begin
Source := TPDFlib.Create;
Target := TPDFlib.Create;
try
Source.NewPages(1); // sada dvije stranice
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 := ','; // simuliraj njemačku ili francusku radnu površinu
try
FDF := Source.ExportAnnotationsToFDFString; // i dalje zapisuje /Rect [50.5 ...
Target.ImportAnnotationsFromFDFString(FDF);
finally
FormatSettings.DecimalSeparator := OldSep;
end;
Target.SelectPage(2);
Assert(Target.AnnotationCount = 2); // bilješka i njezin popup
Assert(Target.GetAnnotType(1) = 'Text');
Assert(Target.ExportAnnotationsToFDFString = Source.ExportAnnotationsToFDFString);
finally
Target.Free;
Source.Free;
end;
end;
Budite jasni oko toga što FDF put nosi. Uvoznik svaki unos ponovno izgrađuje kao rječnik s /Type, /Subtype, /Rect, /Contents, /T i /Subj; boja, flagovi, stil ruba, popup linkovi i appearance streamovi nisu dio ovog puta, a izvoznik preskače Widget anotacije jer pripadaju metodama za podatke formulara. Šira karta koji podaci putuju kojom metodom jest u pregledu razmjene podataka formulara FDF, XFDF i XFA, a ako trebate pregledati što je stvarno stiglo, čitače po indeksu poput GetAnnotType, GetAnnotTitle i GetAnnotContentsEx pokriva introspekcija outlinea, anotacija i akcija
Kako čitati FDF i XFDF datoteke sa zarezom kao decimalom iz starijih izvoza?
Za FDF odgovor je nedvosmislen: zarez nije ograničitelj u PDF sintaksi, pa numerički token koji sadrži točno jedan zarez i nijednu točku može biti samo decimala zapisana na stroju sa zarez-lokalom. Starije verzije jesu zapisivale takve datoteke, primjerice /Rect [10,500 20,250 40,750 60,125], a novi ReadNumber taj jedini zarez pretvori u točku prije parsiranja. Token s dva zareza, ili sa zarezom i točkom, odbija se umjesto da se nagađa. Čitač ne konzumira ni eksponencijalni zapis, što se slaže s ISO 32000-1 §7.3.3: PDF brojevi ga nikad ne koriste
XFDF je teži, jer je u XML atributima zarez separator. Standardni XFDF (ISO 19444-1) zapisuje rect="50.5,80.25,70.75,100.125" i dashes="4,2", dok su v3.539.28 i stariji, na sustavu sa zarez-lokalom, zapisivali rect="50,500 80,250 70,750 100,125" i opacity="0,600", i pri tome padali s EConvertError pri čitanju standardnog opacity="0.6". Od v3.539.29 oba su smjera invarijantna, a legacy oblik prepoznaje XFDFNormalizeLegacyDecimals samo kad se atribut po bijelom prostoru razdvoji u točno očekivani broj tokena (četiri za rect, jedan za opacity i width) i svaki token ima oblik znamenke-zarez-znamenke. Standardni rect se nikad ne poklopi: ili je jedan token s tri zareza, ili su tokeni koji završavaju zarezom. dashes se namjerno ostavlja na miru, jer 4,2 mogu biti dvije duljine crtica ili legacy 4.2, i nijedno pravilo ih ne može razlučiti
const
// Ključevi izvan izvoznog redoslijeda, plus decimalni zarezi iz starijeg izvoza sa zarez-lokalom
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; // svježi dokument ima jednu stranicu
try
Lib.ImportAnnotationsFromFDFString(LegacyFDF);
Assert(Lib.AnnotationCount = 1);
Assert(Lib.GetAnnotTitle(1) = 'Alpha');
// Ponovno izvezeno kao XFDF s točkastim decimalama: rect="10.500 20.250 40.750 60.125"
Writeln(Lib.ExportAnnotationsToXFDFString);
finally
Lib.Free;
end;
end;
Na što bi test uvoza anotacija stvarno trebao tvrditi?
Koristan test uvoza tvrdi na stanju ciljnog dokumenta, nikad samo na tome što uvoznik kaže o sebi. Ništa u test suiteu nije provjerilo AnnotationCount nakon FDF uvoza, a povratna vrijednost, jedini broj kojeg je itko gledao, bila je upravo onaj broj koji je bug ostavio netaknutim. Tri tvrdnje ulovile bi svaki nedostatak opisan ovdje: broj anotacija na očekivanoj stranici, jedno polje pročitano natrag kroz GetAnnotType ili GetAnnotContentsEx, i drugi izvoz uspoređen bajt po bajt s prvim. Ista se disciplina odnosi na bilo koji API koji u komadu prepravlja strukturu dokumenta, uključivo konsolidaciju polja opisanu u spajanju dupliciranih polja formulara: provjerite nastalo stablo, ne vraćeni zbroj. Metode za FDF i XFDF anotacije, s varijantama za datoteku i string, isporučuju se u losLab PDF Library za Delphi i C++Builder, a v3.539.30 ili noviji verzija je za vrtiti ako komentari moraju preživjeti put, v3.539.40 ili noviji ako vraćeni broj mora odgovarati onome što je dodano