HotPDF Delphi Component gemmer hvert PDF-tal i THPDFNumericObject.Value, som er erklæret som Single, og en Single kan ikke rumme alle 32-bit heltal. AcroForm-feltflags (/Ff) er en 32-bit maske, så enhver maske med en høj bit sat blev afrundet ved indlæsning: $8000FFFF kom tilbage som $80010000. Siden v2.989.5 parser feltflag-læsninger det oprindelige heltals-token eksakt og kopierer dets bits ind i den eksisterende Integer-API
Fejlen er et kig værd, selv hvis du aldrig rører PDF-formularer. Intet crashede, ingen exception blev rejst, og de fleste testfiler bestod, fordi de fleste rigtige masker er små. Fejlen viste sig først, når en høj bit og de lave almindelige flags var til stede samtidig, og så tabte den ikke én bit — den omskrev flere af dem på én gang. At gemme en bitmaske i en floating-point-type er en klassisk Delphi-fælder, og sådan ser den ud i produktionskode
Hvorfor taber en Single AcroForm-feltflag-bits?
En Delphi Single har en 24-bit signifikand, så den repræsenterer alle heltal op til 2^24 (16777216) eksakt og intet derover uden huller. Mellem 2^31 og 2^32 ligger konsekutive Single-værdier 256 fra hinanden. En feltflag-maske med bit 32 sat lever præcis i det interval, så dens lave otte bits, og ofte flere, afrundes til det nærmeste multiplum af 256
ISO 32000-1 §7.3.3 definerer integer- og real-objekter som adskilte slags tal, og tillæg C lister forskellige implementationsgrænser for hver, så et PDF-heltal har ingen forpligtelse til at passe i en 32-bit float. Feltflag-posten er defineret i Tabel 221 som et heltal, og den læses bit for bit, ikke som en mængde. At afrunde en mængde med en lille brøkdel er harmløst; at afrunde en maske carry'er ind i nabo-bits. Du kan genskabe hele effekten i nogle få linjer almindelig Delphi:
var
S: Single;
Mask: Int64;
begin
S := $8000FFFF; // 2147549183, ikke repræsentérbar som Single
Mask := Round(S); // 2147549184
Writeln(IntToHex(Mask, 8)); // 80010000: bits 1-16 ryddet, bit 17 sat
S := $80003FFF;
Mask := Round(S);
Writeln(IntToHex(Mask, 8)); // 80004000: bits 1-14 ryddet, bit 15 sat
end;
Lektionen generaliserer ud over PDF. En Single, en Double, en Variant med en float eller et JSON-tal dekodet til en float er den forkerte beholder til et bitfelt, selv når de værdier, du tilfældigvis tester med, er små. Double skubber det eksakte interval ud til 2^53, hvilket skjuler problemet for 32-bit masker, men lader stadig typen udtrykke den forkerte intention. Brug Cardinal, Integer eller Int64 og konvertér fra kildeteksten, ikke fra en float, der allerede er afrundet
Hvilke feltflags flipper: Radio, Pushbutton, Password og Multiline
De to masker ovenfor er ikke tilfældige; de viser, hvordan ét enkelt afrundingstrin ændrer den felttype, en læser ser. PDF nummererer flag-bits fra 1, så bit n har værdien 2^(n-1). ISO 32000-1 §12.7.3.1 Tabel 226 definerer bits 1 til 3 for hvert felt, Tabel 228 definerer button-flags, og Tabel 230 definerer tekstfelt-flags
$8000FFFFtil$80010000: bit 16 (Radio,$8000, Tabel 228) ryddes, og bit 17 (Pushbutton,$10000) sættes, så en radioknap bliver til en trykknap- Samme afrunding rydder bits 1 til 3, så ReadOnly, Required og NoExport fra Tabel 226 forsvinder sammen med Radio
$80003FFFtil$80004000: bit 13 (Multiline,$1000) og bit 14 (Password,$2000) fra Tabel 230 ryddes begge, så en maskeret adgangskodeboks bliver til et enkeltlinje-almindeligt tekstfelt- Under bit 32 starter skaden ved 2^24:
16777219(Comb plus ReadOnly og Required) afrundes til16777220, hvilket dropper ReadOnly og Required og tilføjer NoExport
Sidste tilfælde betød noget for HotPDF ud over redigering af formularer. ExportLoadedPagesStructured bygger sin JSON- og XML-formularmodel ud fra den effektive maske og bruger NoExport i sin værdiundertrykkelsesbeslutning, så en afrundet maske kunne skjule en almindelig værdi eller rapportere de forkerte readOnly- og required-medlemmer. Den strukturerede eksportsti fik samme eksakte læsning i v2.989.8
Sådan læser HotPDF det eksakte 32-bit heltals-token
HotPDF Delphi Component fikser læsesiden uden at ændre den numeriske objektmodel. Parseren husker allerede den oprindelige token-tekst ved siden af Single-værdien, så feltflag-læseren parser det token som et heltal, akkumulerer det i en afgrænset Int64 og kopierer de resulterende 32 bits ind i den Integer, den offentlige API returnerer. Generiske numeriske API'er og den gamle ikke-heltals-fallback forbliver, som de var, og kun feltflag-forbrugere tager den eksakte sti
Parseren må acceptere begge måder, rigtige filer staver en maske med høj bit på. Nogle skrivere udsender den unsignede værdi 2147549183, som ligger over tillæg C's heltalsgrænse på 2^31 - 1, men er almindelig i praksis; andre udsender den signede værdi -2147418113. Begge beskriver de samme bits. En version af den afgrænsede parser ser sådan ud:
function TryParseFlagMask(const Token: AnsiString; out Mask: Cardinal): Boolean;
var
P, Digit: Integer;
Negative: Boolean;
Number, Limit: Int64;
begin
Result := False;
Mask := 0;
if Token = '' then Exit;
P := 1;
Negative := Token[1] = '-';
if Token[1] in ['+', '-'] then Inc(P);
if P > Length(Token) then Exit;
// Signede tokens ned til -2^31, unsignede tokens op til 2^32 - 1
if Negative then Limit := 2147483648 else Limit := 4294967295;
Number := 0;
while P <= Length(Token) do
begin
if not (Token[P] in ['0'..'9']) then Exit; // reals tager den gamle fallback
Digit := Ord(Token[P]) - Ord('0');
if Number > (Limit - Digit) div 10 then Exit; // afgrænset, intet overflow
Number := Number * 10 + Digit;
Inc(P);
end;
if Negative and (Number <> 0) then Number := 4294967296 - Number;
Mask := Cardinal(Number);
Result := True;
end;
To detaljer er bevidste. Grænsen tjekkes før hver multiplikation, så et 400-cifret token fejler rent i stedet for at løbe Int64 over. Og et token som 3.0 afvises, hvilket sender kalderen tilbage til Round(Value); den eksakte sti forbedrer heltalsmasker uden at opfinde ny semantik for misdannede
Fra applikationskode ændrer intet sig i signaturerne. THotPDF.GetLoadedFormFieldFlags(FieldIndex: Integer): Integer returnerer den effektive maske og går /Parent-kæden på samme måde, som beskrevet i hvordan HotPDF resolver arvede AcroForm-feltværdier og nulstiller, og THotPDF.SetLoadedFormFieldFlags(FieldIndex, AFlags: Integer) skriver en komplet lokal maske. En maske med bit 32 sat er simpelthen en negativ Integer, så flyt bits i stedet for at konvertere værdien:
const
FF_RADIO = $00008000; // bit 16, ISO 32000-1 Tabel 228
FF_PUSHBUTTON = $00010000; // bit 17, Tabel 228
var
Pdf: THotPDF;
I, Native: Integer;
Bits: Cardinal;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
for I := 0 to Pdf.GetFormFieldCount - 1 do
begin
Native := Pdf.GetLoadedFormFieldFlags(I);
Move(Native, Bits, SizeOf(Bits)); // samme 32 bits, ingen konvertering
if (Bits and FF_RADIO) <> 0 then
Writeln(Pdf.GetFormFieldName(I), ': radio, mask $', IntToHex(Bits, 8));
end;
Bits := $80000002; // høj bit plus Required
Move(Bits, Native, SizeOf(Native)); // Native = -2147483646
Pdf.SetLoadedFormFieldFlags(0, Native);
Pdf.SaveLoadedDocument('survey-out.pdf');
finally
Pdf.Free;
end;
end;
Læsning af en arvet maske efterlader child-dictionaryen urørt; skrivning erstatter hele masken med en lokal post på det valgte felt. Genvejene SetLoadedFormFieldReadOnly og SetLoadedFormFieldRequired starter fra den effektive arvede maske, skifter én bit og holder alle andre bits eksakte. Til at udfylde værdier frem for flags, se sætning af formularfeltværdier i en indlæst PDF med HotPDF
Hvorfor betyder rækkefølgen af Value og RememberSourceToken noget?
Rækkefølgen betyder noget, fordi THPDFNumericObject kun stoler på sit huskede token, mens Single-værdien er uændret. RememberSourceToken gemmer tokenet sammen med et snapshot af den aktuelle Value, og SourceToken-getteren returnerer en tom streng, så snart Value afviger fra det snapshot. Den invalideringsregel er det, der forhindrer et redigeret tal i at blive gemt med sin forældede originale tekst
Reglen bliver til en fælder på skrivesiden. Husk tokenet først og tildel værdien bagefter, og snapshotet holder den gamle værdi, getteren melder intet token, og serializeren falder tilbage til at formatere den afrundede Single. Hver flag-writer inde i biblioteket, inklusive genvejene og de native feltkonstruktører, følger samme sekvens:
// Korrekt: snapshotet matcher Value, så det eksakte token overlever gemning
Item := THPDFNumericObject.Create(nil);
Item.Value := AFlags;
Item.RememberSourceToken(AnsiString(IntToStr(AFlags)));
// Forkert: snapshotet tages, før Value ændres, så SourceToken
// invalides, og den afrundede Single skrives i stedet
Item := THPDFNumericObject.Create(nil);
Item.RememberSourceToken(AnsiString(IntToStr(AFlags)));
Item.Value := AFlags;
Fordi SaveNumericObject skriver det huskede token, når end et er gyldigt, er det at få rækkefølgen rigtigt, der gør, at en gemt og genåbnet fil bærer det oprindelige heltal, ikke en afrundet approksimation af det
Null-tokenet, der fik to felter til at dele ét navn
Samme release fiksede en anden parserdefekt fundet under test af disse masker: en null-værdi i en dictionary læst fra streamen blev aldrig forbrugt. HotPDF kigger på næste token for at klassificere det, og det tjek efterlader streampositionen ved tokenets start. otNull-grenen af den streamende dictionary-læser oprettede null-objektet, men gik ikke videre forbi de fire bytes af null
Konsekvensen var ikke et hæng, men en lydløs fejllæsning. Det næste nøgleopslag scannede fremad fra det forkerte sted, løb forbi slutningen af den aktuelle dictionary og læste nøgler fra det følgende indirekte objekt, som derefter overskrev det aktuelle felt. I feltlisten var symptomet to forskellige felter, der rapporterede samme navn og samme værdi. Array- og span-parsere forbrugte allerede null korrekt, så fixet rykker streamen fire bytes frem i den ene gren i stedet for at omskrive den generelle parser. En cursor, der ikke flytter sig, er samme fejlfamilie som PDF-parser-cursorstallene, der hang HotPDF på misdannet input, med et andet symptom
Fixet klargjorde også arv. En lokal /Ff null, eller en reference, der resolver til null, behandles som fraværende, og opslaget fortsætter til forælderen, mens en eksplicit 0 er en ægte maske, der tilsidesætter forælderen
Hvad et {$R-}-directive beviser, og hvad det ikke gør
Den første hypotese om flag-fejlen var en range-check-fejl, når en stor maske blev tildelt en Integer. Den var forkert, og grunden er en Delphi-detalje værd at kende: et lokalt {$R-}-directive placeret før ParseSimpleDictionary i HPDFDoc.pas havde ingen matchende gendannelse, så hver funktion efter det i uniten kompilerede med range checks slået fra. Delphi nulstiller ikke et switch-directive ved slutningen af en funktion, og en include-fil arver directive-tilstanden ved det punkt, hvor den inkluderes
Så et command-line -$R+ eller -Cr beviser kun unitens startindstilling. Det siger intet om en bestemt funktion længere nede. For at vide, om range checking er aktiv for en given rutine, så find det nærmeste directive over den, og bekræft derefter adfærden med en rigtig grænseværdi. De to ældre lokale flag-forbrugere rejste aldrig den range-exception, hypotesen forudsagde, så den manglende exception kunne ikke nævnes som bevis for en fix. Den reelle defekt var Single-afrundingen hen over kendte flag-bits
Eksakt-token-læsningen er også ærlig omkring sit omfang. Den ændrer ikke unit-vid directive-tilstanden, tildeler ingen betydning til reserverede bits og hævder ikke, at enhver bitkombination er et gyldigt felt. Den garanterer, at de 32 bits i filen er de 32 bits, din kode ser
FAQ
Hvorfor ikke ændre THPDFNumericObject.Value til Double eller Int64?
At ændre typen af et offentligt felt ville knække enhver kalder, der læser eller tildeler det, og PDF-tal er reelt lige så ofte som heltal. At beholde Single og læse feltflags fra det oprindelige token fikser fejlen for bitmasker uden at ændre nogen eksisterende erklæring eller kaldskonvention
Accepterer SetLoadedFormFieldFlags masker med bit 32 sat?
Ja. Giv masken med som en signed Integer; $80000002 og -2147483646 er de samme 32 bits. GetLoadedFormFieldFlags returnerer samme værdi efter gemning og genindlæsning, inklusive Required
Hvilken HotPDF-version læser eksakte AcroForm-feltflags?
Eksakte feltflag-læsninger og null-token-fixet kom i HotPDF v2.989.5, og struktureret JSON- og XML-eksport overtog den eksakte maske i v2.989.8. Tidligere versioner afrunder enhver maske over 2^24 gennem Single
Eksakte AcroForm-feltflags, arvet feltopslag og redigering af indlæste formularer er en del af HotPDF Component for Delphi og C++Builder, et native PDF-bibliotek til at oprette, indlæse og redigere PDF-dokumenter