Før v3.539.30 returnerte TPDFlib.ImportAnnotationsFromFDFString i losLab PDF Library antallet FDF-annotasjonsoppføringer den hadde parset, mens den la ingen av dem inn i dokumentet: hver oppføring ble telt, hver oppføring ble kastet. Siden v3.539.30 leser FDF-importøren nøkler i vilkårlig rekkefølge, parser /Rect riktig og uavhengig av locale, og den tilhørende eksportøren skriver annotasjonens ekte /Rect, så en eksport, en import og en andre eksport gir byteidentisk FDF. Resten av dette notatet forklarer hvordan ett feil startoffset ga en perfekt stille feil, hvilke tre andre defekter som gjemte seg bak den, og hvordan du sjekker en import selv i stedet for å stole på returverdien
Scenarioet er helt vanlig. En korrekturleser kommenterer opp en kontrakt, kommentarene reiser som en FDF-fil (Acrobat kaller det Export Comments), og Delphi-tjenesten din fletter dem inn i en ren kopi med ImportAnnotationsFromFDF. Kallet returnerer 7, loggen sier «7 comments imported», jobben blir grønn, og utdata-PDF-en har ingen kommentarer i det hele tatt. Ingenting reist, ingen advarsel, og tallet så plausibelt ut fordi det var det ekte antallet oppføringer i filen. Det er den verste formen en bug kan ta: en funksjon hvis eneste suksesssignal er en teller som beregnes uavhengig av arbeidet den hevder å rapportere
Hvorfor rapporterte ImportAnnotationsFromFDFString suksess men la inn ingenting?
Importøren leste hver /Subtype som en tom streng, og hjelperen som oppretter annotasjonen, avslutter tidlig på en tom subtype mens kalleren øker resultatet likevel. Nøkkelsøkeren returnerte posisjonen rett etter /Subtype, som er whitespace-en foran verdien. ReadName startet på det mellomrommet og stoppet ved det første whitespace-tegnet, så den stoppet før den leste noe som helst. AddAnnotationToPage nekter å bygge en annotasjon uten subtype, som er det riktige defensive valget isolert sett, men den var en prosedyre uten returverdi, og Inc(Result) satt utenfor den. Hver vakt var fornuftig alene; sammen forvandlet de «ingenting fungerte» til «alt fungerte». Fiksen gjør at ReadName hopper over whitespace, krever den ledende /-en i et PDF name-objekt og stopper ved ethvert skilletegn, inkludert [, ( og ), så både /Subtype/Text og /Subtype /Text gir Text
Returverdien fortjente omsorg selv etter den fiks-en. Gjennom v3.539.39 økte ImportAnnotationsFromFDFString fortsatt resultatet for hver velformet ordbok i /Annots-arrayen, inkludert oppføringer hvis 0-baserte /Page var utenfor området eller hvis /Subtype manglet, noe som begge hoppes over. Siden PDFlibPas v3.539.40 returnerer ImportAnnotationsFromFDFString og ImportAnnotationsFromFDF antallet annotasjoner som faktisk ble lagt inn, som XFDF-importen: FDF-hjelperen AddAnnotationToPage returnerer nå en Boolean, og telleren flytter seg bare ved suksess. Å måle dokumentet er fortsatt den sterkere sjekken, fordi den også holder på eldre versjoner, så skissen under sammenligner AnnotationCount på hver side før og etter 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 valgt side, widgeter inkludert
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 // likt 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 flere defekter bak den første
Å fikse bare subtypen ville ha avslørt tre ytterligere buger i samme funksjon, som hver hadde vært usynlige bare fordi ingen annotasjon noen gang nådde en side. For det første tok ReadNumber posisjonen sin som value-parameter, så å lese de fire /Rect-tallene i rekkefølge leste samme sted fire ganger, og den hoppet ikke over åpningens [, så i praksis leste den ingenting i det hele tatt. For det andre delte FindKey én fremoverbevegelig cursor mellom alle oppslagene. Eksportøren skriver /Subtype, /Rect, /Page, /Contents, /T, /Subj, men importøren søkte i rekkefølgen /Subtype, /Contents, /T, /Subj, /Page, /Rect; når cursoren først hadde passert /Contents, rant søket etter /Page og /Rect forbi gjeldende oppføring og fant enten ingenting eller matchet neste annotasjons nøkler. Biblioteket klarte ikke å lese sitt eget output. For det tredje gikk tallene gjennom PLStrToFloat, som følger systemets desimalseparator. ISO 32000-1 §12.7.7 definerer FDF som PDF-objektsyntaks, og ordboknøkler i PDF er uordnede (§7.3.7), så enhver FDF-parser som antar en nøkkelrekkefølge, er feil av konstruksjon, uansett hvilket verktøy som produserte filen
Den reparerte importøren avgrenser hver oppføring først. FindDictEnd går fra den åpnende << til sin matchende >>, sporer nestede ordbøker og hopper over literal-strengkropper med deres backslash-escapes, så en >> inne i en kommentar som (see section >> 4) ikke kan avslutte oppføringen tidlig. Hvert nøkkeloppslag starter så på oppføringens egen start og er begrenset til dens slutt, noe som gjør nøkkelrekkefølgen irrelevant og stopper én annotasjon fra å låne en annens /Page. Nøkkelmatchen godtar også et skilletegn rett etter navnet, fordi /Contents(Hi) er like gyldig som /Contents (Hi), mens ordgrenseregelen holder /Subj fra å matche starten på /Subtype og /T fra å matche /Type. ReadNumber tar nå posisjonen sin som var-parameter, hopper over whitespace og [, og parser med PLTryStrToFloatInvariant, som feiler mykt på en misdannet token i stedet for å reise. Feiler noen av de fire rektangeltallene, faller alle fire tilbake til null i stedet for å produsere et halvlest rektangel
Hvorfor forskjøv en FDF-rundtur hver annotasjon med sin egen høyde?
Den gamle eksportøren skrev et rektangel i feil koordinatmodell. En annotasjons /Rect er [llx lly urx ury] i default user space (ISO 32000-1 §12.5.2, med rektangler definert i §7.9.5), og FDF bærer samme array. ExportAnnotationsToFDFString kalte imidlertid GetAnnotRectEx, som rapporterer Left, Top, Width og Height i bibliotekets tegnecoordinater, rommet som SetOrigin styrer, og serialiserte dem som [L T L+W T+H]. Importøren, når den først fungerte, skrev de fire verdiene rett tilbake som et PDF-rektangel, så toppkanten landet der nedre venstre hjørne hørte hjemme, og hver rundtur flyttet annotasjonen opp med sin egen høyde. Eksportøren kopierer nå annotasjonens egne /Rect-tall, tre desimaler, punktumseparator, ingen eksponent, og faller bare tilbake til det beregnede rektangelet når det lagrede arrayet mangler eller ikke har fire tall
Regresjonstesten som fester dette, er verdt å kopiere, fordi den hevder på dokumentet og på en andre eksport, ikke på importørens returverdi. Merk det forventede antallet 2: AddNoteAnnotation oppretter en Text-annotasjon pluss sin Popup, og begge reiser. Testen kjører også eksporten og importen under et komma som desimalseparator, og det er der den andre halvdelen av denne historien bor
var
Source, Target: TPDFlib;
FDF: AnsiString;
OldSep: Char;
begin
Source := TPDFlib.Create;
Target := TPDFlib.Create;
try
Source.NewPages(1); // nå 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 := ','; // simuler et tysk eller fransk skrivebord
try
FDF := Source.ExportAnnotationsToFDFString; // skriver fortsatt /Rect [50.5 ...
Target.ImportAnnotationsFromFDFString(FDF);
finally
FormatSettings.DecimalSeparator := OldSep;
end;
Target.SelectPage(2);
Assert(Target.AnnotationCount = 2); // notatet og popup-en
Assert(Target.GetAnnotType(1) = 'Text');
Assert(Target.ExportAnnotationsToFDFString = Source.ExportAnnotationsToFDFString);
finally
Target.Free;
Source.Free;
end;
end;
Vær klar over hva FDF-stien bærer med seg. Importøren bygger hver oppføring på nytt som en ordbok med /Type, /Subtype, /Rect, /Contents, /T og /Subj; farge, flagg, kantstil, popup-lenker og appearance-streams er ikke en del av denne ruten, og eksportøren hopper over Widget-annotasjoner fordi skjemafeltene hører til form-data-metodene. Det bredere kartet over hvilke data som reiser gjennom hvilken metode, finner du i oversikten over FDF-, XFDF- og XFA-formdatautveksling, og hvis du trenger å inspisere hva som faktisk ankom, dekkes per-indeks-leserne som GetAnnotType, GetAnnotTitle og GetAnnotContentsEx i outline-, annotasjons- og action-introspeksjon
Hvordan leser du FDF- og XFDF-filer med kommadeimaler fra eldre eksporter?
For FDF er svaret entydig: et komma er ikke et skilletegn i PDF-syntaks, så en token som inneholder nøyaktig ett komma og ingen punktum, kan bare være et desimaltall skrevet på en komma-locale-maskin. Tidligere versjoner skrev slike filer, for eksempel /Rect [10,500 20,250 40,750 60,125], og den nye ReadNumber gjør det eneste kommaet om til et punktum før parsing. En token med to kommaer, eller et komma og et punktum, avvises i stedet for å gjettes på. Leseren konsumerer heller ikke eksponentnotasjon, noe som samsvarer med ISO 32000-1 §7.3.3: PDF-numre bruker den aldri
XFDF er vanskeligere, 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 komma-locale-system, skrev rect="50,500 80,250 70,750 100,125" og opacity="0,600", og dessuten feilet med EConvertError ved lesing av en standard opacity="0.6". Siden v3.539.29 er begge retninger invariante, og den gamle formen gjenkjennes av XFDFNormalizeLegacyDecimals bare når attributten splittes på whitespace til nøyaktig det forventede antallet token (fire for rect, én for opacity og width) og hver token har formen siffer-komma-siffer. Et standard rect matcher aldri: det er enten én token med tre kommaer eller token som slutter med et komma. dashes er med vilje latt i fred, for 4,2 kan være to streklengder eller en gammel 4.2, og ingen regel kan skille dem
const
// Nøkler i annen rekkefølge enn eksportøren, pluss kommadeimaler fra en eldre komma-locale-eksport
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 ferskt dokument har én side
try
Lib.ImportAnnotationsFromFDFString(LegacyFDF);
Assert(Lib.AnnotationCount = 1);
Assert(Lib.GetAnnotTitle(1) = 'Alpha');
// Re-eksportert som XFDF med punktum som desimaltegn: rect="10.500 20.250 40.750 60.125"
Writeln(Lib.ExportAnnotationsToXFDFString);
finally
Lib.Free;
end;
end;
Hva bør en annotasjonsimporttest egentlig hevde?
En nyttig importtest hevder på tilstanden til måldokumentet, aldri bare på hva importøren sier om seg selv. Ingenting i testpakken sjekket AnnotationCount etter en FDF-import, og returverdien, det eneste tallet noen så på, var det eneste tallet bug-en lot stå intakt. Tre påstander ville ha fanget hver defekt beskrevet her: annotasjonsantallet på den forventede siden, ett felt lest tilbake gjennom GetAnnotType eller GetAnnotContentsEx, og en andre eksport sammenlignet byte for byte med den første. Samme disiplin gjelder enhver API som omskriver dokumentstruktur i bulk, inkludert feltkonsolideringen beskrevet i fletting av duplikate skjemafelt: sjekk det resulterende treet, ikke en returnert total. FDF- og XFDF-annotasjonsmetodene, med sine fil- og strengvarianter, følger med losLab PDF Library for Delphi og C++Builder, og v3.539.30 eller nyere er versjonen å kjøre hvis kommentarene skal overleve turen, v3.539.40 eller nyere hvis den returnerte tellingen må matche det som ble lagt inn