Før v3.539.30 returnerede TPDFlib.ImportAnnotationsFromFDFString i losLab PDF Library antallet af FDF-annotering-poster, den havde parset, uden at tilføje nogen af dem til dokumentet: hver post blev talt, hver post blev smidt væk. Siden v3.539.30 læser FDF-importøren nøgler i vilkårlig rækkefølge, parser /Rect korrekt og uafhængigt af locale, og den tilhørende eksportør skriver annoteringens rigtige /Rect, så en eksport, en import og en anden eksport giver byte-identisk FDF. Resten af denne note forklarer, hvordan ét forkert start-offset blev til et perfekt lydløst svigt, hvilke tre andre defekter der gemte sig bag, og hvordan du tjekker en import selv i stedet for at stole på returværdien
Scenariet er hverdagsagtigt. En reviewer sætter kommentarer på en kontrakt, kommentarerne rejser som en FDF-fil (Acrobat kalder det Export Comments), og din Delphi-service merger dem ind i en ren kopi med ImportAnnotationsFromFDF. Kaldet returnerer 7, loggen siger "7 comments imported", jobbet bliver grønt, og output-PDF'en har slet ingen kommentarer. Ingen exception, ingen advarsel, og tallet så plausibelt ud, fordi det var det rigtige antal poster i filen. Det er den værste form, en fejl kan antage: en funktion, hvis eneste successignal er en tæller, der beregnes uafhængigt af det arbejde, den påstår at rapportere
Hvorfor rapporterede ImportAnnotationsFromFDFString succes, men tilføjede intet?
Importøren læste hver /Subtype som en tom streng, og helperen, der opretter annoteringen, bryder tidligt af ved en tom subtype, mens kaldkoden tæller resultatet op alligevel. Nøglefinderen returnerede positionen lige efter /Subtype, altså whitespace'en før værdien. ReadName startede på det mellemrum og standsede ved det første whitespace-tegn, så den standsede, inden den overhovedet havde læst noget. AddAnnotationToPage nægter at bygge en annotering uden en subtype, hvilket er det korrekte defensive valg i isolation, men det var en procedure uden returværdi, og Inc(Result) lå uden for den. Hvert vagtpost var fornuftigt hver for sig; tilsammen forvandlede de "intet virkede" til "alt virkede". Fixet får ReadName til at springe whitespace over, kræve den indledende / i et PDF name-object og standsse ved ethvert skilletegn, inklusive [, ( og ), så både /Subtype/Text og /Subtype /Text giver Text
Returværdien krævede også omtanke efter det fix. Frem til v3.539.39 tælte ImportAnnotationsFromFDFString stadig resultatet op for hver velformet dictionary i /Annots-arrayet, inklusive poster, hvis 0-baserede /Page var ude af interval, eller hvis /Subtype manglede, og begge dele bliver sprunget over. Siden PDFlibPas v3.539.40 returnerer ImportAnnotationsFromFDFString og ImportAnnotationsFromFDF antallet af annoteringer, der faktisk blev tilføjet, ligesom XFDF-importen: FDF-helperen AddAnnotationToPage returnerer nu en Boolean, og tælleren flytter sig kun ved succes. At måle på dokumentet er stadig det stærkere tjek, fordi det også gælder på ældre versioner, så skitsen nedenfor sammenligner AnnotationCount på hver side før og efter importen
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); // pr. valgt side, widgets tælles med
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 // ens siden 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;
Tre andre defekter bag den første
At rette subtypen alene ville have afsløret tre fejl mere i samme funktion, som hver især kun havde været usynlige, fordi ingen annotering nogensinde nåede en side. For det første tog ReadNumber sin position som value-parameter, så læsning af de fire /Rect-tal i træk læste samme sted fire gange, og den sprang ikke den indledende [ over, så i praksis læste den slet ingenting. For det andet delte FindKey én fremadrykkende cursor mellem alle opslag. Eksportøren skriver /Subtype, /Rect, /Page, /Contents, /T, /Subj, men importøren søgte i rækkefølgen /Subtype, /Contents, /T, /Subj, /Page, /Rect; da cursoren først var forbi /Contents, løb søgningen efter /Page og /Rect forbi den aktuelle post og fandt enten ingenting eller ramte den næste annoterings nøgler. Biblioteket kunne ikke læse sit eget output. For det tredje gik tallene gennem PLStrToFloat, som følger systemets decimaltegn. ISO 32000-1 §12.7.7 definerer FDF som PDF-objektsyntaks, og dictionary-nøgler i PDF er uordnede (§7.3.7), så enhver FDF-parser, der antager en nøglerækkefølge, er forkert af konstruktion, uanset hvilket værktøj der har lavet filen
Den reparerede importør afgrænser først hver post. FindDictEnd går fra den indledende << til den tilhørende >>, holder styr på indlejrede dictionaries og springer literal string-legemer med deres backslash-escapes over, så en >> inde i en kommentar som (see section >> 4) ikke kan afslutte posten for tidligt. Hvert nøgleopslag starter derefter ved postens egen start og er begrænset til dens slutning, hvilket gør nøglerækkefølgen irrelevant og forhindrer én annotering i at låne en andens /Page. Nøglematchet accepterer også et skilletegn direkte efter navnet, fordi /Contents(Hi) er lige så gyldig som /Contents (Hi), mens word-boundary-reglen holder /Subj fra at matche begyndelsen af /Subtype og /T fra at matche /Type. ReadNumber tager nu sin position som var-parameter, springer whitespace og [ over og parser med PLTryStrToFloatInvariant, som fejler blidt på en misdannet token i stedet for at rejse en exception. Hvis et af de fire rektangel-tal fejler, falder alle fire tilbage til nul i stedet for at give et halvlæst rektangel
Hvorfor flyttede FDF-round-trips hver annotering op med dens egen højde?
Den gamle eksportør skrev et rektangel i den forkerte koordinatmodel. En annoterings /Rect er [llx lly urx ury] i default user space (ISO 32000-1 §12.5.2, med rektangler defineret i §7.9.5), og FDF bærer samme array. ExportAnnotationsToFDFString kaldte imidlertid GetAnnotRectEx, som rapporterer Left, Top, Width og Height i bibliotekets tegnekoordinater, det rum, SetOrigin styrer, og serialiserede dem som [L T L+W T+H]. Importøren, da den så virkede, skrev de fire værdier uændret tilbage som et PDF-rektangel, så topkanten landede dér, hvor nedre venstre hjørne hørte hjemme, og hver round-trip flyttede annoteringen op med dens egen højde. Eksportøren kopierer nu annoteringens egne /Rect-tal — tre decimaler, punktum som separator, ingen exponent — og falder kun tilbage til det beregnede rektangel, når det gemte array mangler eller ikke har fire tal
Regressionstesten, der låser dette fast, er værd at kopiere, fordi den assert'er på dokumentet og på en anden eksport, ikke på importørens returværdi. Bemærk det forventede antal på 2: AddNoteAnnotation opretter en Text-annotering plus dens Popup, og begge to rejser med. Testen kører også eksport og import under et komma som decimaltegn, og dér bor den anden halvdel af historien
var
Source, Target: TPDFlib;
FDF: AnsiString;
OldSep: Char;
begin
Source := TPDFlib.Create;
Target := TPDFlib.Create;
try
Source.NewPages(1); // nu to sider
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 := ','; // simulér en tysk eller fransk desktop
try
FDF := Source.ExportAnnotationsToFDFString; // skriver stadig /Rect [50.5 ...
Target.ImportAnnotationsFromFDFString(FDF);
finally
FormatSettings.DecimalSeparator := OldSep;
end;
Target.SelectPage(2);
Assert(Target.AnnotationCount = 2); // noten og dens popup
Assert(Target.GetAnnotType(1) = 'Text');
Assert(Target.ExportAnnotationsToFDFString = Source.ExportAnnotationsToFDFString);
finally
Target.Free;
Source.Free;
end;
end;
Vær klar over, hvad FDF-stien bærer med. Importøren genopbygger hver post som en dictionary med /Type, /Subtype, /Rect, /Contents, /T og /Subj; farve, flags, border style, popup-links og appearance streams er ikke en del af denne rute, og eksportøren springer Widget-annoteringer over, fordi formfelter hører til form-data-metoderne. Det bredere kort over, hvilke data der rejser gennem hvilken metode, ligger i gennemgangen af FDF-, XFDF- og XFA-form-data-udveksling, og hvis du skal inspicere, hvad der faktisk ankom, er per-indeks-læserne som GetAnnotType, GetAnnotTitle og GetAnnotContentsEx dækket i outline-, annoterings- og action-introspektion
Hvordan læser du FDF- og XFDF-filer med komma-decimaler fra ældre eksporter?
For FDF er svaret entydigt: et komma er ikke et skilletegn i PDF-syntaks, så en taltoken, der indeholder præcis ét komma og intet punktum, kan kun være en decimal skrevet på en maskine med komma-locale. Tidligere versioner skrev faktisk sådanne filer, for eksempel /Rect [10,500 20,250 40,750 60,125], og den nye ReadNumber omsætter det enkelte komma til et punktum før parsing. En token med to kommaer, eller et komma og et punktum, afvises i stedet for at blive gættet. Readeren indtager heller ikke exponent-notation, hvilket matcher ISO 32000-1 §7.3.3: PDF-tal bruger den aldrig
XFDF er sværere, for i XML-attributter er kommaet separatoren. Standard-XFDF (ISO 19444-1) skriver rect="50.5,80.25,70.75,100.125" og dashes="4,2", mens v3.539.28 og tidligere, på et system med komma-locale, skrev rect="50,500 80,250 70,750 100,125" og opacity="0,600" og desuden fejlede med EConvertError, når en standard opacity="0.6" skulle læses. Siden v3.539.29 er begge retninger invariante, og legacy-formen genkendes af XFDFNormalizeLegacyDecimals kun, når attributten splitter på whitespace til netop det forventede antal tokens (fire for rect, én for opacity og width), og hver token har formen cifre-komma-cifre. Et standard-rect matcher aldrig: det er enten én token med tre kommaer eller tokens, der slutter med et komma. dashes efterlades bevidst i fred, for 4,2 kan være to strenglængder eller en legacy 4.2, og ingen regel kan skelne dem
const
// Nøgler i en anden rækkefølge end eksportørens, plus komma-decimaler fra en ældre eksport med komma-locale
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; // et nyt dokument har én side
try
Lib.ImportAnnotationsFromFDFString(LegacyFDF);
Assert(Lib.AnnotationCount = 1);
Assert(Lib.GetAnnotTitle(1) = 'Alpha');
// Re-eksporteret som XFDF med punktum-decimaler: rect="10.500 20.250 40.750 60.125"
Writeln(Lib.ExportAnnotationsToXFDFString);
finally
Lib.Free;
end;
end;
Hvad skal en annoterings-importtest egentlig asserte?
En nyttig importtest assert'er på target-dokumentets tilstand, aldrig kun på, hvad importøren siger om sig selv. Intet i test-suiten tjekkede AnnotationCount efter en FDF-import, og returværdien, det eneste tal, nogen kiggede på, var netop det tal, fejlen lod være intakt. Tre assertions ville have fanget hver defekt beskrevet her: annoteringstallet på den forventede side, ét felt læst tilbage gennem GetAnnotType eller GetAnnotContentsEx og en anden eksport sammenlignet byte for byte med den første. Samme disciplin gælder enhver API, der omskriver dokumentstruktur in bulk, inklusive feltkonsolideringen beskrevet i merging af duplikerede formfelter: tjek det resulterende træ, ikke en returneret total. FDF- og XFDF-annoteringsmetoderne, med deres fil- og strengvarianter, følger med i losLab PDF Library for Delphi and C++Builder, og v3.539.30 eller nyere er versionen at køre, hvis kommentarer skal overleve turen, v3.539.40 eller nyere, hvis det returnerede tal skal matche, hvad der blev tilføjet