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
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į
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
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