Tehnični članak

Uvoz opomb FDF v Delphiju: popravljanje tihe ničle

Pred v3.539.30 je TPDFlib.ImportAnnotationsFromFDFString v losLab PDF Library vrnil število razčlenjenih vnosov opomb FDF, ne da bi enega samega dodal v dokument: vsak vnos je bil preštet, vsak vnos odvržen. Od v3.539.30 uvoznik FDF bere ključe v poljubnem vrstnem redu, razčleni /Rect pravilno in ne glede na lokalne nastavitve, ustrezen izvoznik pa zapiše pravi /Rect opombe, zato izvoz, uvoz in drugi izvoz dajo bajtno enak FDF. Preostanek tega zapisa razloži, kako je en napačen začetni odmik ustvaril popolnoma tiho odpoved, kateri trije drugi napaki so se skrili za njim in kako uvoz preverite sami, namesto da bi zaupali vrnjeni vrednosti

Scenarij je vsakdanji. Recenzent popravi pogodbo, pripombe potujejo kot datoteka FDF (Acrobat temu pravi Export Comments), vaša storitev v Delphiju pa jih z ImportAnnotationsFromFDF zlije v čisto kopijo. Klic vrne 7, dnevnik pravi »7 pripomb uvoženih«, opravilo pozeleni, izhodni PDF pa nima nobene pripombe. Nič sproženo, nič opozorjeno, številka pa je izgledala verodostojno, ker je bila pravo število vnosov v datoteki. To je najslabša oblika, ki jo lahko vzame hrošč: funkcija, katere edini signal uspeha je števec, izračunan neodvisno od dela, za katerega trdi, da ga poroča

Zakaj je ImportAnnotationsFromFDFString poročal uspeh, dodal pa nič?

Uvoznik je vsak /Subtype prebral kot prazen niz, pomožnik, ki ustvari opombo, pa se ob praznem podtipu zgodaj izteče, medtem ko klicatelj rezultat vseeno poveča. Iskalec ključev je vrnil položaj takoj za /Subtype, to je beli prostor pred vrednostjo. ReadName se je začel na tem presledku in se ustavil ob prvem nadaljnjem belem znaku, zato je ustavil, preden je kaj prebral. AddAnnotationToPage odkloni izgradnjo opombe brez podtipa, kar je sama zase prava obrambna izbira, bila pa je procedura brez vrnjene vrednosti, Inc(Result) pa je sedel zunaj nje. Vsaka varovalka je bila sama zase smiselna; skupaj so »nič ni delalo« spremenile v »vse je delalo«. Popravek prisili ReadName, da preskoči beli prostor, zahteva vodilni / imenskega objekta PDF in se ustavi ob katerem koli ločilu, vključno s [, ( in ), tako da /Subtype/Text in /Subtype /Text oba dasta Text

PDFlibPas ImportAnnotationsFromFDFString najde /Subtype, zažene ReadName na belem prostoru za ključem, tako da vrne prazno ime, AddAnnotationToPage izteče ob manjkajočem podtipu, klicatelj pa rezultat vseeno poveča in poroča sedem uvoženih pripomb, ne da bi eno samo dodal v dokument
Vsaka varovalka je bila sama zase smiselna; skupaj so »nič ni delalo« spremenile v »vse je delalo«, zato vrnjena vrednost nikoli ne sme biti edino, kar preizkus uvoza preverja

Vrnjena vrednost je zaslužila pozornost tudi po tem popravku. Do v3.539.39 je ImportAnnotationsFromFDFString še vedno povečeval rezultat za vsak dobro oblikovan slovar v polju /Annots, vključno z vnosi, katerih 0-osnovni /Page je bil izven obsega ali katerih /Subtype je manjkal, oboje pa se preskoči. Od PDFlibPas v3.539.40 ImportAnnotationsFromFDFString in ImportAnnotationsFromFDF vrneta število dejansko dodanih opomb, tako kot uvoz XFDF: pomožnik FDF AddAnnotationToPage zdaj vrne Boolean in števec se premakne le ob uspehu. Merjenje dokumenta je še vedno močnejši preizkus, ker velja tudi na starejših različicah, zato spodnji oris primerja AnnotationCount na vsaki strani pred uvozom in po njem

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);   // na izbrani strani, vključno z gradniki
  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   // enako 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;

Še tri napake za prvo

Popravek zgolj podtipa bi razkril tri nadaljnje napake v isti funkciji, vsaka od katerih je bila nevidna samo zato, ker nobena opomba nikoli ni dosegla strani. Prvič, ReadNumber je vzel svoj položaj kot parameter vrednosti, zato je branje štirih števil /Rect po vrsti bralo isto mesto štirikrat, in ni preskočilo odpirajočega [, tako da v praksi ni prebralo ničesar. Drugič, FindKey je čez vse poizvedbe delil en sam kazalec, ki se je pomikal samo naprej. Izvoznik zapiše /Subtype, /Rect, /Page, /Contents, /T, /Subj, uvoznik pa je iskal v vrstnem redu /Subtype, /Contents, /T, /Subj, /Page, /Rect; ko je kazalec enkrat prešel /Contents, je iskanje /Page in /Rect teklo čez trenutni vnos in ali nič ne najde ali pa se ujema s ključi naslednje opombe. Knjižnica ni znala prebrati lastnega izhoda. Tretjič, številke so šle skozi PLStrToFloat, ki sledi sistemskemu decimalnemu ločilu. ISO 32000-1 §12.7.7 definira FDF kot skladnjo objektov PDF, ključi slovarjev v PDF pa so neurejeni (§7.3.7), zato je vsak razčlenjevalnik FDF, ki domneva vrstni red ključev, po zgradbi narobe, ne glede na to, katero orodje je datoteko izdelalo

Popravljeni uvoznik najprej omeji vsak vnos. FindDictEnd se sprehodi od odpirajočega << do ujemajočega >>, spremlja ugnezdene slovarje in preskoči telesa literalnih nizov z njihovimi poševno-ubežnimi znaki, tako da >> znotraj pripombe, kot je (see section >> 4), ne more končati vnosa prezgodaj. Vsaka poizvedba po ključu se torej začne na lastnem začetku vnosa in je omejena na njegov konec, kar naredi vrstni red ključev nepomemben in prepreči eni opombi, da si izposodi /Page druge. Ujemanje ključa sprejme tudi ločilo takoj za imenom, ker je /Contents(Hi) enako veljaven kot /Contents (Hi), pravilo meje besede pa prepreči, da bi se /Subj ujemal z začetkom /Subtype in /T z /Type. ReadNumber zdaj vzame svoj položaj kot parameter var, preskoči beli prostor in [ ter razčlenjuje s PLTryStrToFloatInvariant, ki mehko odpove ob napačno oblikovanem žetonu namesto da bi sprožil izjemo. Če katero od štirih števil pravokotnika odpove, vsa štiri padejo nazaj na nič, namesto da bi dali napol prebran pravokotnik

PDFlibPas FindDictEnd zdaj omeji vsako opombo FDF od njenega odpirajočega << do ujemajočega >>, tako da se vsaka poizvedba po ključu znova začne na začetku vnosa in se ustavi na njegovem koncu, ReadNumber pa vzame položaj kot var, preskoči oglati oklepaj in razčlenjuje s PLTryStrToFloatInvariant
Deljeni kazalec ni znal prebrati lastnega izvoza knjižnice: ko je enkrat prešel /Contents, sta se iskanji /Page in /Rect zaleteli v ključe naslednje opombe, zato vrstni red ključev več ne sme biti pomemben

Zakaj so povratne poti FDF premaknile vsako opombo za njeno lastno višino?

Stari izvoznik je zapisal pravokotnik v napačnem modelu koordinat. /Rect opombe je [llx lly urx ury] v privzetem uporabniškem prostoru (ISO 32000-1 §12.5.2, pravokotniki definirani v §7.9.5), FDF pa nosi isto polje. ExportAnnotationsToFDFString pa je klical GetAnnotRectEx, ki poroča Left, Top, Width in Height v risalnih koordinatah knjižnice, prostoru, ki mu vlada SetOrigin, in jih serializiral kot [L T L+W T+H]. Uvoznik, ko je enkrat delal, je te štiri vrednosti zapisal nazaj dobesedno kot PDF pravokotnik, zato je zgornji rob pristal tam, kjer je pripadal spodnjemu levemu kotu, vsak povratni prehod pa je premaknil opombo navzgor za njeno lastno višino. Izvoznik zdaj kopira lastna števila /Rect opombe — tri decimalki, ločilo pika, brez eksponenta — in pade nazaj na izračunani pravokotnik le, ko shranjeno polje manjka ali ni štiri števila

PDFlibPas je serializiral FDF /Rect kot left, top, width, height v risalnih koordinatah, zato je uvoz teh štirih števil nazaj kot llx lly urx ury pristavi zgornji rob tam, kjer je pripadal spodnjemu levemu kotu, in premaknil vsako opombo navzgor za njeno lastno višino ob vsakem povratnem prehodu
Izvoznik zdaj kopira lastna števila /Rect opombe — tri decimalki, ločilo pika, brez eksponenta — regresijski preizkus pa primerja drugi izvoz bajt za bajt s prvim

Regresijski preizkus, ki to pribije, je vreden kopiranja, ker trditve postavlja na dokument in na drugi izvoz, ne na vrnjeno vrednost uvoznika. Upoštevajte pričakovani števec 2: AddNoteAnnotation ustvari opombo Text plus njen Popup in potujeta oba. Preizkus požene tudi izvoz in uvoz pod decimalnim ločilom vejica, kjer živi druga polovica te zgodbe

var
  Source, Target: TPDFlib;
  FDF: AnsiString;
  OldSep: Char;
begin
  Source := TPDFlib.Create;
  Target := TPDFlib.Create;
  try
    Source.NewPages(1);                     // zdaj dve strani
    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 := ',';   // simulacija nemškega ali francoskega namizja
    try
      FDF := Source.ExportAnnotationsToFDFString;   // še vedno zapiše /Rect [50.5 ...
      Target.ImportAnnotationsFromFDFString(FDF);
    finally
      FormatSettings.DecimalSeparator := OldSep;
    end;

    Target.SelectPage(2);
    Assert(Target.AnnotationCount = 2);           // opomba in njen popup
    Assert(Target.GetAnnotType(1) = 'Text');
    Assert(Target.ExportAnnotationsToFDFString = Source.ExportAnnotationsToFDFString);
  finally
    Target.Free;
    Source.Free;
  end;
end;

Bodite jasni glede tega, kaj pot FDF nosi. Uvoznik vsak vnos znova zgradi kot slovar s /Type, /Subtype, /Rect, /Contents, /T in /Subj; barva, zastavice, slog obrobe, povezave popup in tokovi videza niso del te poti, izvoznik pa preskoči opombe Widget, ker polja obrazcev pripadajo metodam podatkov obrazcev. Širša karta tega, kateri podatki potujejo skozi katero metodo, je v pregledu izmenjave podatkov obrazcev FDF, XFDF in XFA, če pa morate pregledati, kaj je dejansko prispelo, so bralci po indeksu, kot so GetAnnotType, GetAnnotTitle in GetAnnotContentsEx, pokriti v vpogledu v orise, opombe in dejanja

Kako berete datoteke FDF in XFDF z vejico kot decimalnim ločilom iz starejših izvozov?

Za FDF je odgovor nedvoumen: vejica ni ločilo v skladnji PDF, zato je žeton števila, ki vsebuje točno eno vejico in nobene pike, lahko samo decimalna številka, zapisana na stroju z vejico kot decimalnim ločilom. Starejše različice so take datoteke res zapisovale, na primer /Rect [10,500 20,250 40,750 60,125], novi ReadNumber pa to samotno vejico pretvori v piko, preden razčlenjuje. Žeton z dvema vejicama ali z vejico in piko je odklonjen, namesto da bi ga ugibali. Bralec ne sprejme niti eksponentnega zapisa, kar se ujema z ISO 32000-1 §7.3.3: števila PDF ga nikoli ne uporabljajo

XFDF je težji, ker je v atributih XML vejica ločilo. Standardni XFDF (ISO 19444-1) zapiše rect="50.5,80.25,70.75,100.125" in dashes="4,2", medtem ko je v3.539.28 in prej na sistemu z vejico kot ločilom zapisalo rect="50,500 80,250 70,750 100,125" in opacity="0,600" ter ob branju standardnega opacity="0.6" odpovedalo z EConvertError. Od v3.539.29 sta obe smeri invariantni, zapuščinsko obliko pa XFDFNormalizeLegacyDecimals prepozna le, ko se atribut razdeli po belem prostoru natanko v pričakovano število žetonov (štiri za rect, enega za opacity in width) in ima vsak žeton obliko številke-vejica-številke. Standardni rect se nikoli ne ujame: ali je en žeton s tremi vejicami ali pa žetoni, ki se končajo z vejico. dashes je namerno puščen pri miru, ker je 4,2 lahko dve dolžini črtkane črte ali zapuščinska 4.2, pravilo, ki bi jih ločilo, pa ne obstaja

const
  // Ključi izven vrstnega reda izvoznika, plus decimalna števila z vejico iz starejšega izvoza
  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;               // svež dokument ima eno stran
  try
    Lib.ImportAnnotationsFromFDFString(LegacyFDF);
    Assert(Lib.AnnotationCount = 1);
    Assert(Lib.GetAnnotTitle(1) = 'Alpha');
    // Ponovno izvoženo kot XFDF z decimalnimi pikami: rect="10.500 20.250 40.750 60.125"
    Writeln(Lib.ExportAnnotationsToXFDFString);
  finally
    Lib.Free;
  end;
end;

Kaj naj preizkus uvoza opomb dejansko trdi?

Koristen preizkus uvoza postavlja trditve na stanje ciljnega dokumenta, nikoli le na to, kar uvoznik pravi o sebi. Nič v zbirki preizkusov ni preverjalo AnnotationCount po uvozu FDF, vrnjena vrednost, edino število, ki si ga je kdo ogledal, pa je bilo tisto število, ki ga hrošč ni pokvaril. Tri trditve bi ujele vsako napako, opisano tukaj: število opomb na pričakovani strani, eno polje prebrano nazaj skozi GetAnnotType ali GetAnnotContentsEx in drugi izvoz, primerjan bajt za bajt s prvim. Ista disciplina velja za vsak API, ki množično prepiše strukturo dokumenta, vključno s konsolidacijo polj, opisano v združevanju podvojenih polj obrazcev: preverite nastalo drevo, ne vrnjene vsote. Metode opomb FDF in XFDF, z njihovimi različicami za datoteke in nize, so del losLab PDF Library for Delphi and C++Builder, in v3.539.30 ali novejša je različica, ki jo poganjate, če morajo pripombe preživeti pot, v3.539.40 ali novejša pa, če mora vrnjeni števec ustrezati temu, kar je bilo dodano