Techninis straipsnis

FDF anotacijų importas Delphi: kaip ištaisytas tylusis nulis

Iki v3.539.30 TPDFlib.ImportAnnotationsFromFDFString losLab PDF Library grąžindavo išanalysuotų FDF anotacijų įrašų skaičių, nedėdama nė vieno į dokumentą: kiekvienas įrašas suskaičiuodavo, kiekvienas įrašas nukrisdavo. Nuo v3.539.30 FDF importuotojas skaito raktus bet kuria tvarka, teisingai ir nepriklausomai nuo lokalės išanalysuoja /Rect, o atitinkamas eksportuotojas rašo anotacijos tikrąjį /Rect, tad eksportas, importas ir antras eksportas duoda baitas į baitą identišką FDF. Šios pastabos likusi dalis paaiškina, kaip vienas neteisingas pradinis poslinkis davė tobulą tylų gedimą, kurie trys kiti defektai slėpėsi už jo ir kaip patikrinti importą patiems, vietoj to, kad pasitikėtumėte grąžinama reikšme

Scenarijus paprastas. Recenzentas pažymi sutartį, komentarai keliauja kaip FDF failas (Acrobat tai vadina Export Comments), o jūsų Delphi tarnyba sujungia juos su švaria kopija per ImportAnnotationsFromFDF. Iškvietimas grąžina 7, žurnale rašo „7 comments imported“, darbas pažaliuoja, o išvesties PDF neturi nė vieno komentaro. Niekas nesukėlė klaidos, niekas neįspėjo, o skaičius atrodė įtikinamai, nes buvo tikras įrašų faile kiekis. Tai blogiausia forma, kurią klaida gali įgyti: funkcija, kurios vienintelis sėkmės signalas yra skaitiklis, skaičiuojamas nepriklausomai nuo darbo, apie kurį jis tariamai praneša

Kodėl ImportAnnotationsFromFDFString pranešė sėkmę, bet nedėjo nieko?

Importuotojas kiekvieną /Subtype skaitė kaip tuščią eilutę, o pagalbinė funkcija, kuri sukuria anotaciją, tuščią potipį atmeta iš karto, nors kvietėjas vis tiek padidina rezultatą. Rakto ieškiklis grąžindavo poziciją iškart po /Subtype, tai yra tarpą prieš reikšmę. ReadName pradėdavo nuo to tarpo ir sustodavo ties pirmuoju tarpo simboliu, tad sustodavo neperskaitęs nieko. AddAnnotationToPage atsisako kurti anotaciją be potipio – atskirai žiūrint tai teisingas gynybinis sprendimas, bet tai buvo procedūra be grąžinamos reikšmės, o Inc(Result) stovėjo už jos ribų. Kiekviena apsauga atskirai buvo pagrįsta; kartu jos „niekas neveikė“ pavertė „viskas veikė“. Pataisa priverčia ReadName praleisti tarpus, reikalauti prieš PDF vardo objektą einančio / ir sustoti ties bet kuriuo skirtuku, įskaitant [, ( ir ), tad /Subtype/Text ir /Subtype /Text abu duoda Text

PDFlibPas ImportAnnotationsFromFDFString surado /Subtype, ReadName pradėjo nuo tarpo po raktu ir grąžino tuščią vardą, AddAnnotationToPage pasitraukė dėl dingusio potipio, o kvietėjas vis tiek padidino rezultatą, pranešdamas septynis importuotus komentarus, nedėdamas nė vieno į dokumentą
Kiekviena apsauga atskirai buvo pagrįsta; kartu jos niekas neveikė pavertė viskas veikė, todėl grąžinama reikšmė niekada negali būti vienintelis dalykas, kurį tikrina importo testas

Grąžinama reikšmė ir po tos pataisos nusipelnė dėmesio. Iki v3.539.39 ImportAnnotationsFromFDFString vis tiek didindavo rezultatą už kiekvieną gerai suformuotą žodyną /Annots masyve, įskaitant įrašus, kurių nuo nulio skaičiuojamas /Page išeidavo už ribų ar kurių /Subtype dingęs – abu variantai yra praleidžiami. Nuo PDFlibPas v3.539.40 ImportAnnotationsFromFDFString ir ImportAnnotationsFromFDF grąžina faktiškai pridėtų anotacijų skaičių, kaip ir XFDF importas: FDF pagalbinė funkcija AddAnnotationToPage dabar grąžina Boolean, o skaitiklis juda tik sėkmės atveju. Dokumento išmatavimas lieka stipresnė patikra, nes ji veikia ir su senesnėmis versijomis, todėl žemiau esanti schema lygina AnnotationCount kiekviename puslapyje prieš importą ir po jo

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);   // kiekvienam pasirinktam puslapiui, įskaitant valdiklius
  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   // lygu nuo 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;

Trys kiti defektai už pirmojo

Ištaisius vien potipį atsiskleistų dar trys tos pačios funkcijos klaidos, kurios liko nematomos tik todėl, kad nė viena anotacija niekada nepasiekdavo puslapio. Pirma, ReadNumber savo poziciją priimdavo kaip reikšmės parametrą, tad keturių /Rect skaičių skaitymas iš eilės keturis kartus skaitydavo tą pačią vietą, ir jis nepraleisdavo pradinio [, tad praktikoje neskaitydavo iš viso nieko. Antra, FindKey visoms paieškoms naudojo vieną į priekį slenkantį žymeklį. Eksportuotojas rašo /Subtype, /Rect, /Page, /Contents, /T, /Subj, bet importuotojas ieškodavo tvarka /Subtype, /Contents, /T, /Subj, /Page, /Rect; žymekliui pravėlus /Contents, paieška po /Page ir /Rect nubėgdavo pro dabartinį įrašą ir arba neradurdavo nieko, arba suimdavo kitos anotacijos raktus. Biblioteka negalėjo perskaityti savo pačios išvesties. Trečia, skaičiai ėjo pro PLStrToFloat, kuris seka sistemos dešimtainiu skirtuku. ISO 32000-1 §12.7.7 apibrėžia FDF kaip PDF objektų sintaksę, o PDF žodyno raktai neturi tvarkos (§7.3.7), tad bet koks FDF analizatorius, manydamas tam tikrą raktų tvarką, yra klaidingas pagal sukūrimą, nepriklausomai nuo to, kuris įrankis pagamino failą

Pataisytas importuotojas pirmiausia apibrėžia kiekvieno įrašo ribas. FindDictEnd eina nuo atsidarančio << iki jam poruojamo >>, sekdamas įdėtinius žodynus ir praleisdamas literalių eilučių kūnus su jų pasviraisiais brūkšniais, tad >> komentaro viduje, pvz. (see section >> 4), negali baigti įrašo anksčiau laiko. Kiekviena rakto paieška tada prasideda nuo paties įrašo pradžios ir ribojama jo pabaiga, kas padaro rakto tvarką nesvarbia ir neleidžia vienai anotacijai pasiskolinti kitos /Page. Rakto atitikimas taip pat priima skirtuką iškart po vardo, nes /Contents(Hi) taip pat teisėta kaip /Contents (Hi), o žodžių ribos taisyklė neleidžia /Subj suimti /Subtype pradžios ir /T suimti /Type. ReadNumber dabar savo poziciją priima kaip var parametrą, praleidžia tarpus ir [, o analizuoja su PLTryStrToFloatInvariant, kuri prieš blogai suformuotą žetoną švelniai traukiasi, nekeliaudama klaidos. Jei kuri nors iš keturių stačiakampio reikšmių nepavyksta, visos keturios sugrįžta į nulį, vietoj to, kad duotų pusiau perskaitytą stačiakampį

PDFlibPas FindDictEnd dabar apibrėžia kiekvieną FDF anotaciją nuo atsidarančio << iki poruojamojo >>, tad kiekviena rakto paieška iš naujo prasideda nuo įrašo pradžios ir sustoja ties jo pabaiga, o ReadNumber priima var poziciją, praleidžia skliaustą ir analizuoja su PLTryStrToFloatInvariant
Bendras žymeklis negalėjo perskaityti bibliotekos pačios eksporto: pravėlus /Contents, /Page ir /Rect paieškos įbėgdavo į kitos anotacijos raktus, todėl rakto tvarka nebeturi teisės turėti reikšmės

Kodėl FDF round-tripai keldavo kiekvieną anotaciją aukštyn per jos pačios aukštį?

Senas eksportuotojas rašė stačiakampį neteisingoje koordinačių sistemoje. Anotacijos /Rect yra [llx lly urx ury] numatytojoje vartotojo erdvėje (ISO 32000-1 §12.5.2, stačiakampiai apibrėžti §7.9.5), ir FDF neša tą patį masyvą. ExportAnnotationsToFDFString tačiau kviesdavo GetAnnotRectEx, kuris praneša Left, Top, Width ir Height bibliotekos piešimo koordinatėse – toje erdvėje, kurią valdo SetOrigin, – ir serializuodavo juos kaip [L T L+W T+H]. Importuotojas, kai jau suveikė, tuos keturis skaičius atgal rašydavo pažodžiui kaip PDF stačiakampį, tad viršutinis kraštas nusileisdavo ten, kur priklausė apatiniam kairiajam kampui, ir kiekvienas ciklas keldavo anotaciją aukštyn per jos pačios aukštį. Eksportuotojas dabar kopijuoja anotacijos pačios /Rect skaičius – trys dešimtainiai, taško skirtukas, be laipsnio – ir tik tada, kai saugomas masyvas dingęs arba nėra keturių skaičių, grįžta prie apskaičiuoto stačiakampio

PDFlibPas anksčiau serializuodavo FDF /Rect kaip left, top, width, height piešimo koordinatėse, tad importuojant tuos keturis skaičius atgal kaip llx lly urx ury viršutinis kraštas nusileisdavo ten, kur priklausė apatiniam kairiajam kampui, ir kiekvienas ciklas keldavo anotaciją aukštyn per jos pačios aukštį
Eksportuotojas dabar kopijuoja anotacijos pačios /Rect skaičius – trys dešimtainiai, taško skirtukas, be laipsnio – o regresijos testas antrą eksportą lygina baitas po baito su pirmuoju

Regresijos testas, kuris tai užfiksuoja, vertas nusikopijuoti, nes jis teiginius kelia dokumentui ir antram eksportui, o ne importuotojo grąžinamai reikšmei. Atkreipkite dėmesį į tikėtiną skaičių 2: AddNoteAnnotation sukuria Text anotaciją kartu su jos Popup, ir abi keliauja. Testas taip pat leidžia eksportą ir importą su kablelio dešimtainiu skirtuku – būtent ten gyvena kita šios istorijos pusė

var
  Source, Target: TPDFlib;
  FDF: AnsiString;
  OldSep: Char;
begin
  Source := TPDFlib.Create;
  Target := TPDFlib.Create;
  try
    Source.NewPages(1);                     // dabar du puslapiai
    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 := ',';   // imituojame vokišką arba prancūzišką darbalaukį
    try
      FDF := Source.ExportAnnotationsToFDFString;   // vis tiek rašo /Rect [50.5 ...
      Target.ImportAnnotationsFromFDFString(FDF);
    finally
      FormatSettings.DecimalSeparator := OldSep;
    end;

    Target.SelectPage(2);
    Assert(Target.AnnotationCount = 2);           // pastaba ir jos popup
    Assert(Target.GetAnnotType(1) = 'Text');
    Assert(Target.ExportAnnotationsToFDFString = Source.ExportAnnotationsToFDFString);
  finally
    Target.Free;
    Source.Free;
  end;
end;

Būkite aiškūs dėl to, ką FDF kelias neša. Importuotojas kiekvieną įrašą atkuria kaip žodyną su /Type, /Subtype, /Rect, /Contents, /T ir /Subj; spalva, vėliavėlės, kraštinės stilius, popup nuorodos ir išvaizdos srautai šiuo keliu nekeliauja, o eksportuotojas praleidžia Widget anotacijas, nes formos laukai priklauso formos duomenų metodams. Platesnis žemėlapis, kurie duomenys keliauja kuriuo metodu, yra FDF, XFDF ir XFA formos duomenų apsikeitimo apžvalgoje, o jei reikia apžiūrėti, kas iš tiesų atkeliavo, per indeksą skaitantys metodai GetAnnotType, GetAnnotTitle ir GetAnnotContentsEx aprašyti struktūros, anotacijų ir veiksmų introspekcijoje

Kaip skaityti kablelio dešimtainiu skirtuku parašytus FDF ir XFDF failus iš senesnių eksportų?

Su FDF atsakymas vienareikšmis: kablelis PDF sintaksėje nėra skirtukas, tad skaičiaus žetonas, kuriame lygiai vienas kablelis ir jokio taško, gali būti tik kablelio lokalės mašinoje parašytas dešimtainis. Ankstesnės versijos tokius failus iš tiesų rašė, pvz. /Rect [10,500 20,250 40,750 60,125], o naujasis ReadNumber tą vienintelį kablelį prieš analizę paverčia tašku. Žetonas su dviem kableliais arba su kableliu ir tašku atmetamas, o ne spėjamas. Skaitytuvas taip pat nevartoja laipsnio užrašo, kas sutampa su ISO 32000-1 §7.3.3: PDF skaičiai jo niekada nenaudoja

Su XFDF sudėtingiau, nes XML atributuose kablelis yra skirtukas. Standartinis XFDF (ISO 19444-1) rašo rect="50.5,80.25,70.75,100.125" ir dashes="4,2", o v3.539.28 ir ankstesnės, kablelio lokalės sistemoje, rašė rect="50,500 80,250 70,750 100,125" ir opacity="0,600", taip pat keldavo EConvertError skaitydamos standartinį opacity="0.6". Nuo v3.539.29 abi kryptys invariantiškos, o senasis pavidalas atpažįstamas XFDFNormalizeLegacyDecimals tik tada, kai atributas per tarpus išskyla į lygiai tikėtiną žetonų skaičių (keturi rect, vienas opacity ir width) ir kiekvienas žetonas turi skaitmenys-kablelis-skaitmenys formą. Standartinis rect niekada nesutampa: jis arba vienas žetonas su trimis kableliais, arba žetonai, besibaigiantys kableliu. dashes sąmoningai paliktas ramybėje, nes 4,2 gali būti du brūkšnių ilgiai arba senovinis 4.2, ir jokios taisyklės jų nepaskirs

const
  // Raktai ne eksportuotojo tvarka, plus kablelio dešimtainiai iš senesnio kablelio lokalės eksporto
  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;               // šviežias dokumentas turi vieną puslapį
  try
    Lib.ImportAnnotationsFromFDFString(LegacyFDF);
    Assert(Lib.AnnotationCount = 1);
    Assert(Lib.GetAnnotTitle(1) = 'Alpha');
    // Pakartotinai eksportuota kaip XFDF su taško dešimtainiais: rect="10.500 20.250 40.750 60.125"
    Writeln(Lib.ExportAnnotationsToXFDFString);
  finally
    Lib.Free;
  end;
end;

Ką anotacijų importo testas iš tikrųjų turėtų teigti?

Naudingas importo testas teiginius kelia tikslinio dokumento būsenai, o niekada tik tai, ką importuotojas pasako apie save. Nė vienas testų rinkinio patikrinimas nežiūrėjo AnnotationCount po FDF importo, o grąžinama reikšmė – vienintelis skaičius, kurį kas nors žiūrėdavo – buvo tas vienintelis skaičius, kurio klaida nepalietė. Trys teiginiai būtų pagavę kiekvieną čia aprašytą defektą: anotacijų skaičius tikėtiname puslapyje, vienas laukas, perskaitytas per GetAnnotType arba GetAnnotContentsEx, ir antras eksportas, lyginamas baitas po baito su pirmuoju. Ta pati disciplina galioja bet kuriai API, kuri masiškai perrašo dokumento struktūrą, įskaitant pasikartojančių formos laukų sujungimą: tikrinkite gautąjį medį, o ne grąžintą sumą. FDF ir XFDF anotacijų metodai, su savo failo ir eilutės variantais, keliauja kartu su losLab PDF Library for Delphi ir C++Builder, o v3.539.30 ar naujesnė yra versija, kurioje dirbti, jei komentarai turi išgyventi kelionę, v3.539.40 ar naujesnė – jei grąžintas skaičius turi sutapti su pridėtais