Före v3.539.30 returnerade TPDFlib.ImportAnnotationsFromFDFString i losLab PDF Library antalet FDF-annoteringposter den hade parsat, medan den inte lade till en enda av dem i dokumentet: varje post räknades, varje post kastades. Sedan v3.539.30 läser FDF-importören nycklar i valfri ordning, parsar /Rect korrekt och oberoende av locale, och den matchande exportören skriver annoteringens verkliga /Rect, så en export, en import och en andra export ger byte-identisk FDF. Resten av denna not förklarar hur en felaktig startoffset producerade ett perfekt tyst misslyckande, vilka tre andra defekter som gömde sig bakom den, och hur du kontrollerar en import själv i stället för att lita på returvärdet
Scenariot är helt vanligt. En granskare prickar av i ett kontrakt, kommentarerna färdas som en FDF-fil (Acrobat kallar det Export Comments), och din Delphi-tjänst slår ihop dem med en ren kopia via ImportAnnotationsFromFDF. Anropet returnerar 7, loggen säger "7 kommentarer importerade", jobbet blir grönt, och utdata-PDF:en saknar alla kommentarer. Inget kastades, inget varnade, och siffran såg rimlig ut för den var det sanna antalet poster i filen. Det är den värsta form en bugg kan ta: en funktion vars enda framgångssignal är en räknare som beräknas oberoende av arbetet den påstår sig rapportera
Varför rapporterade ImportAnnotationsFromFDFString framgång men lade till ingenting?
Importören läste varje /Subtype som en tom sträng, och hjälparen som skapar annoteringen avslutar tidigt vid en tom subtyp, medan anroparen ändå ökar resultatet. Nyckelsökaren returnerade positionen direkt efter /Subtype, alltså blanktecknet före värdet. ReadName började på det blanktecknet och stannade vid första blanktecknet, så den stannade innan den läst något alls. AddAnnotationToPage vägrar bygga en annotering utan subtyp, vilket är det korrekta defensiva valet i isolering, men det var en procedur utan returvärde, och Inc(Result) satt utanför den. Varje vakt var rimlig för sig; tillsammans omvandlade de "ingenting fungerade" till "allting fungerade". Fixen gör så att ReadName hoppar över blanksteg, kräver det inledande / hos ett PDF-name-objekt och stannar vid valfri avgränsare, inklusive [, ( och ), så att både /Subtype/Text och /Subtype /Text ger Text
Returvärdet förtjänade omsorg även efter den fixen. Genom v3.539.39 ökade ImportAnnotationsFromFDFString fortfarande sitt resultat för varje välbildad ordbok i /Annots-arrayen, inklusive poster vars nollbaserade /Page var utanför intervallet eller vars /Subtype saknades, vilket båda hoppas över. Sedan PDFlibPas v3.539.40 returnerar ImportAnnotationsFromFDFString och ImportAnnotationsFromFDF antalet annoteringar som faktiskt lades till, precis som XFDF-importen: FDF-hjälparen AddAnnotationToPage returnerar nu en Boolean och räknaren rör sig bara vid framgång. Att mäta dokumentet är fortfarande den starkare kontrollen, eftersom den också gäller äldre versioner, så skissen nedan jämför AnnotationCount på varje sida före och 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); // per vald sida, widgetar inkluderade
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 // lika sedan 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 fler defekter bakom den första
Att fixa bara subtypen hade exponerat tre ytterligare buggar i samma funktion, som var och en bara varit osynlig för att ingen annotering någonsin nådde en sida. För det första tog ReadNumber sin position som value-parameter, så att läsning av de fyra /Rect-talen i sekvens läste samma ställe fyra gånger, och den hoppade inte över inledande [, så i praktiken läste den ingenting alls. För det andra delade FindKey en enda framåtrörande kursor mellan alla uppslagningar. Exportören skriver /Subtype, /Rect, /Page, /Contents, /T, /Subj, men importören sökte i ordningen /Subtype, /Contents, /T, /Subj, /Page, /Rect; när kursorn väl passerat /Contents gick sökningen efter /Page och /Rect förbi den aktuella posten och fann antingen ingenting eller matchade nästa annoterings nycklar. Biblioteket kunde inte läsa sin egen utdata. För det tredje gick talen genom PLStrToFloat, som följer systemets decimaltecken. ISO 32000-1 §12.7.7 definierar FDF som PDF-objektsyntax, och ordboksnycklar i PDF är oordnade (§7.3.7), så varje FDF-parser som antar en nyckelordning är fel av konstruktion, oavsett vilket verktyg som producerade filen
Den lagade importören avgränsar varje post först. FindDictEnd går från inledande << till dess matchande >>, håller reda på nästlade ordböcker och hoppar över literala strängkroppar med deras omvända snedstreck-escape, så att en >> inuti en kommentar som (see section >> 4) inte kan avsluta posten i förtid. Varje nyckeluppslagning börjar sedan vid postens egen start och begränsas till dess slut, vilket gör nyckelordningen irrelevant och hindrar en annotering från att låna en annans /Page. Nyckelmatchningen accepterar också en avgränsare direkt efter namnet, eftersom /Contents(Hi) är lika giltig som /Contents (Hi), medan ordgränsregeln hindrar /Subj från att matcha början av /Subtype och /T från att matcha /Type. ReadNumber tar nu sin position som en var-parameter, hoppar över blanksteg och [ och parsar med PLTryStrToFloatInvariant, som fallerar mjukt på en felformad token i stället för att kasta. Om något av de fyra rektangeltalen fallerar faller alla fyra tillbaka till noll i stället för att ge en halvläst rektangel
Varför försköts varje annotering med sin egen höjd vid FDF-roundtrips?
Den gamla exportören skrev en rektangel i fel koordinatmodell. En annoterings /Rect är [llx lly urx ury] i default user space (ISO 32000-1 §12.5.2, med rektanglar definierade i §7.9.5), och FDF bär samma array. ExportAnnotationsToFDFString anropade däremot GetAnnotRectEx, som rapporterar Left, Top, Width och Height i bibliotekets ritkoordinater, det rum som SetOrigin styr, och serialiserade dem som [L T L+W T+H]. Importören, när den väl fungerade, skrev tillbaka de fyra värdena ordagrant som en PDF-rektangel, så att överkanten landade där nedre vänstra hörnet hörde hemma och varje roundtrip flyttade annoteringen uppåt med sin egen höjd. Exportören kopierar nu annoteringens egna /Rect-tal, tre decimaler, punkt som avgränsare, ingen exponent, och faller tillbaka till den beräknade rektangeln bara när den lagrade arrayen saknas eller inte är fyra tal
Regressionstestet som låser detta är värt att kopiera, eftersom det gör sina påståenden mot dokumentet och mot en andra export, inte mot importörens returvärde. Notera det förväntade antalet 2: AddNoteAnnotation skapar en Text-annotering plus dess Popup, och båda två färdas. Testet kör också export och import under ett komma-decimaltecken, och det är där den andra halvan av denna historia bor
var
Source, Target: TPDFlib;
FDF: AnsiString;
OldSep: Char;
begin
Source := TPDFlib.Create;
Target := TPDFlib.Create;
try
Source.NewPages(1); // nu två sidor
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 := ','; // simulera ett tyskt eller franskt skrivbord
try
FDF := Source.ExportAnnotationsToFDFString; // skriver fortfarande /Rect [50.5 ...
Target.ImportAnnotationsFromFDFString(FDF);
finally
FormatSettings.DecimalSeparator := OldSep;
end;
Target.SelectPage(2);
Assert(Target.AnnotationCount = 2); // noten och dess popup
Assert(Target.GetAnnotType(1) = 'Text');
Assert(Target.ExportAnnotationsToFDFString = Source.ExportAnnotationsToFDFString);
finally
Target.Free;
Source.Free;
end;
end;
Var tydlig med vad FDF-vägen bär med sig. Importören bygger upp varje post som en ordbok med /Type, /Subtype, /Rect, /Contents, /T och /Subj; färg, flaggor, kantstil, popup-länkar och appearance streams är inte en del av den rutten, och exportören hoppar över Widget-annoteringar eftersom formulärfält hör hemma hos formulärdatametoderna. Den vidare kartan över vilken data som färdas genom vilken metod finns i översikten om FDF-, XFDF- och XFA-formulärdatautbyte, och om du behöver inspektera vad som faktiskt anlänt täcks per-index-läsarna som GetAnnotType, GetAnnotTitle och GetAnnotContentsEx i introspektion av dispositioner, annoteringar och actions
Hur läser du FDF- och XFDF-filer med komma-decimaler från äldre exporter?
För FDF är svaret entydigt: ett komma är ingen avgränsare i PDF-syntax, så en tal-token som innehåller exakt ett komma och ingen punkt kan bara vara en decimal skriven på en maskin med komma-locale. Tidigare versioner skrev faktiskt sådana filer, till exempel /Rect [10,500 20,250 40,750 60,125], och den nya ReadNumber förvandlar det enskilda kommat till en punkt före parsning. En token med två komman, eller med ett komma och en punkt, avvisas i stället för att gissas fram. Läsaren konsumerar inte heller exponentnotation, vilket stämmer med ISO 32000-1 §7.3.3: PDF-tal använder den aldrig
XFDF är knepigare, för i XML-attribut är kommat avgränsaren. Standard-XFDF (ISO 19444-1) skriver rect="50.5,80.25,70.75,100.125" och dashes="4,2", medan v3.539.28 och tidigare, på ett system med komma-locale, skrev rect="50,500 80,250 70,750 100,125" och opacity="0,600", och dessutom fallerade med EConvertError vid läsning av en standard opacity="0.6". Sedan v3.539.29 är båda riktningarna invarianta, och den äldre formen känns igen av XFDFNormalizeLegacyDecimals bara när attributet delas på blanksteg till exakt det förväntade antalet token (fyra för rect, ett för opacity och width) och varje token har formen siffror-komma-siffror. En standard-rect matchar aldrig: den är antingen en token med tre komman eller token som slutar med komma. dashes lämnas medvetet orörd, eftersom 4,2 kan vara två strecklängder eller ett äldre 4.2, och ingen regel kan skilja dem åt
const
// Nycklar i omvänd exporterordning, plus komma-decimaler från en äldre komma-locale-export
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; // ett nytt dokument har en sida
try
Lib.ImportAnnotationsFromFDFString(LegacyFDF);
Assert(Lib.AnnotationCount = 1);
Assert(Lib.GetAnnotTitle(1) = 'Alpha');
// Återexporterad som XFDF med punkt-decimaler: rect="10.500 20.250 40.750 60.125"
Writeln(Lib.ExportAnnotationsToXFDFString);
finally
Lib.Free;
end;
end;
Vad ska ett annoteringsimporttest egentligen göra påståenden om?
Ett användbart importtest gör sina påståenden om måldokumentets tillstånd, aldrig bara om vad importören säger om sig själv. Ingenting i testsviten kontrollerade AnnotationCount efter en FDF-import, och returvärdet, det enda tal någon tittade på, var det enda tal buggen lämnade intakt. Tre påståenden hade fångat varje defekt som beskrivs här: annoteringsantalet på den förväntade sidan, ett fält läst tillbaka via GetAnnotType eller GetAnnotContentsEx, och en andra export jämförd byte för byte med den första. Samma disciplin gäller alla API:er som skriver om dokumentstruktur i bulk, inklusive fältsammanfogningen som beskrivs i att slå ihop duplicerade formulärfält: kontrollera det resulterande trädet, inte en returnerad summa. FDF- och XFDF-annoteringsmetoderna, med sina fil- och strängvarianter, följer med i losLab PDF Library for Delphi and C++Builder, och v3.539.30 eller senare är versionen att köra om kommentarer måste överleva färden, v3.539.40 eller senare om det returnerade antalet måste matcha det som lades till