HotPDF Delphi Component lagrar varje PDF-tal i THPDFNumericObject.Value, som är deklarerat som Single, och en Single kan inte hålla varje 32-bitars heltal. AcroForm-fältflaggor (/Ff) är en 32-bitars mask, så varje mask med en hög bit satt avrundades vid inläsning: $8000FFFF kom tillbaka som $80010000. Sedan v2.989.5 parsar fältflaggläsningarna den ursprungliga heltals-token exakt och kopierar dess bitar in i det befintliga Integer-API:et
Buggen är värd en närmare titt även om du aldrig rör PDF-formulär. Inget krashade, inget undantag kastades, och de flesta testfiler gick igenom, eftersom de flesta verkliga masker är små. Felet visade sig bara när en hög bit och de vanliga låga flaggorna fanns tillsammans, och då tappade det inte en bit, det skrev om flera på en gång. Att lagra en bitmask i en flyttalstyp är en klassisk Delphi-fälla, och det här är hur den ser ut i produktionskod
Varför tappar en Single AcroForm-fältflaggbitar?
En Delphi Single har en 24-bitars mantissa, så den representerar varje heltal upp till 2^24 (16777216) exakt och inget därutöver utan gap. Mellan 2^31 och 2^32 ligger sammanhängande Single-värden 256 isär. En fältflaggmask med bit 32 satt bor exakt i det intervallet, så dess låga åtta bitar, och ofta fler, avrundas till närmaste multipel av 256
ISO 32000-1 §7.3.3 definierar heltals- och realobjekt som två skilda slags tal, och Appendix C listar olika implementationsgränser för vardera, så ett PDF-heltal har ingen skyldighet att passa en 32-bitars float. Fältflaggposten är definierad i tabell 221 som ett heltal, och den läses bit för bit, inte som en kvantitet. Att avrunda en kvantitet med en bråkdel är harmlöst; att avrunda en mask slår över i grannbitar. Du kan reproducera hela effekten på några rader vanlig Delphi:
var
S: Single;
Mask: Int64;
begin
S := $8000FFFF; // 2147549183, inte representerbar som Single
Mask := Round(S); // 2147549184
Writeln(IntToHex(Mask, 8)); // 80010000: bit 1-16 nollställda, bit 17 satt
S := $80003FFF;
Mask := Round(S);
Writeln(IntToHex(Mask, 8)); // 80004000: bit 1-14 nollställda, bit 15 satt
end;
Lärdomen generaliserar bortom PDF. En Single, en Double, en Variant som håller ett flyttal, eller ett JSON-tal avkodat till en float är fel behållare för ett bitfält, även när de värden du råkar testa med är små. Double skjuter det exakta intervallet ut till 2^53, vilket gömmer problemet för 32-bitarsmasker men lämnar fortfarande typen som uttrycker fel avsikt. Använd Cardinal, Integer eller Int64 och konvertera från källtexten, inte från en float som redan avrundats
Vilka fältflaggor vänder: Radio, Pushbutton, Password och Multiline
De två maskerna ovan är inte godtyckliga; de visar hur ett enda avrundningssteg ändrar fälttypen en läsare ser. PDF räknar flaggbitar från 1, så bit n har värdet 2^(n-1). ISO 32000-1 §12.7.3.1 tabell 226 definierar bit 1 till 3 för varje fält, tabell 228 definierar knappflaggorna, och tabell 230 definierar textfältflaggorna
$8000FFFFtill$80010000: bit 16 (Radio,$8000, tabell 228) nollställs och bit 17 (Pushbutton,$10000) sätts, så en radioknapp blir en tryckknapp- Samma avrundning nollställer bit 1 till 3, så ReadOnly, Required och NoExport från tabell 226 försvinner tillsammans med Radio
$80003FFFtill$80004000: bit 13 (Multiline,$1000) och bit 14 (Password,$2000) från tabell 230 nollställs båda, så en maskerad lösenordsruta blir ett vanligt textfält med en enda rad- Under bit 32 börjar skadan vid 2^24:
16777219(Comb plus ReadOnly och Required) avrundas till16777220, vilket tappar ReadOnly och Required och lägger till NoExport
Sista fallet betydde något för HotPDF bortom formulärsredigering. ExportLoadedPagesStructured bygger sin JSON- och XML-formulärmodell från den gällande masken och använder NoExport i sitt beslut om värdesundertryckning, så en avrundad mask kunde dölja ett vanligt värde eller rapportera fel readOnly- och required-medlemmar. Den strukturerade exportvägen fick samma exakta läsning i v2.989.8
Hur HotPDF läser den exakta 32-bitars heltals-token
HotPDF Delphi Component fixar lässidan utan att ändra den numeriska objektmodellen. Parsaren minns redan den ursprungliga tokentexten bredvid Single-värdet, så fältflaggläsaren parsar den tokenen som ett heltal, ackumulerar den i en avgränsad Int64, och kopierar de resulterande 32 bitarna in i det Integer som det publika API:et returnerar. Generiska numeriska API:er och den gamla icke-heltalsreservvägen blir som de var, och bara fältflaggkonsumenter tar den exakta vägen
Parsaren måste acceptera båda sätten verkliga filer stavar en hög-bit-mask på. Vissa skrivare sänder det oesignerade värdet 2147549183, som ligger över appendix C:s heltalsgräns 2^31 - 1 men är vanligt i praktiken; andra sänder det signerade värdet -2147418113. Båda beskriver samma bitar. En version av den avgränsade parsaren ser ut så här:
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;
// Signerade token ner till -2^31, oesignerade upp till 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; // reella tal tar gamla reservvägen
Digit := Ord(Token[P]) - Ord('0');
if Number > (Limit - Digit) div 10 then Exit; // avgränsat, inget spill
Number := Number * 10 + Digit;
Inc(P);
end;
if Negative and (Number <> 0) then Number := 4294967296 - Number;
Mask := Cardinal(Number);
Result := True;
end;
Två detaljer är medvetna. Gränsen kontrolleras före varje multiplikation, så en 400-siffrig token misslyckas rent i stället för att spilla över Int64. Och en token som 3.0 avvisas, vilket skickar anroparen tillbaka till Round(Value); den exakta vägen förbättrar heltalsmasker utan att uppfinna ny semantik för felformade
Från applikationskod ändras ingenting i signaturerna. THotPDF.GetLoadedFormFieldFlags(FieldIndex: Integer): Integer returnerar den gällande masken och vandrar /Parent-kedjan på samma sätt som beskrivs i hur HotPDF slår upp ärvda AcroForm-fältvärden och återställer, och THotPDF.SetLoadedFormFieldFlags(FieldIndex, AFlags: Integer) skriver en komplett lokal mask. En mask med bit 32 satt är helt enkelt ett negativt Integer, så flytta bitarna i stället för att konvertera värdet:
const
FF_RADIO = $00008000; // bit 16, ISO 32000-1 tabell 228
FF_PUSHBUTTON = $00010000; // bit 17, tabell 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)); // samma 32 bitar, ingen konvertering
if (Bits and FF_RADIO) <> 0 then
Writeln(Pdf.GetFormFieldName(I), ': radio, mask $', IntToHex(Bits, 8));
end;
Bits := $80000002; // hög bit plus Required
Move(Bits, Native, SizeOf(Native)); // Native = -2147483646
Pdf.SetLoadedFormFieldFlags(0, Native);
Pdf.SaveLoadedDocument('survey-out.pdf');
finally
Pdf.Free;
end;
end;
Att läsa en ärvd mask lämnar barnordboken orörd; att skriva ersätter hela masken med en lokal post på det valda fältet. Genvägarna SetLoadedFormFieldReadOnly och SetLoadedFormFieldRequired utgår från den gällande ärvda masken, växlar en bit, och håller varje annan bit exakt. För att fylla värden snarare än flaggor, se att sätta formulärfältvärden i en laddad PDF med HotPDF
Varför spelar ordningen mellan Value och RememberSourceToken roll?
Ordningen spelar roll eftersom THPDFNumericObject bara litar på sin ihågkomna token medan Single-värdet är oförändrat. RememberSourceToken lagrar tokenen tillsammans med en ögonblicksbild av aktuellt Value, och gettern SourceToken returnerar en tom sträng så snart Value skiljer sig från den ögonblicksbilden. Den ogiltigförklaringsregeln är det som hindrar ett redigerat tal från att sparas med sin inaktuella ursprungstext
Regeln blir en fälla på skrivsidan. Kom ihåg tokenen först och tilldela värdet sen, och ögonblicksbilden håller det gamla värdet, gettern rapporterar ingen token, och serialiseraren faller tillbaka på att formatera den avrundade Single:n. Varje flaggskrivare inuti biblioteket, inklusive genvägarna och de nativa fältkonstruktörerna, följer samma sekvens:
// Rätt: ögonblicksbilden matchar Value, så den exakta token överlever sparningen
Item := THPDFNumericObject.Create(nil);
Item.Value := AFlags;
Item.RememberSourceToken(AnsiString(IntToStr(AFlags)));
// Fel: ögonblicksbilden tas innan Value ändras, så SourceToken
// ogiltigförklaras och den avrundade Single:n skrivs i stället
Item := THPDFNumericObject.Create(nil);
Item.RememberSourceToken(AnsiString(IntToStr(AFlags)));
Item.Value := AFlags;
Eftersom SaveNumericObject skriver den ihågkomna tokenen närhelst en är giltig är det att få ordningen rätt som gör att en sparad och återöppnad fil bär det ursprungliga heltalet, inte en avrundad approximation av det
Null-tokenen som fick två fält att dela ett namn
Samma release fixade en andra parsardefekt som hittades medan dessa masker testades: ett null-värde i en ordbok läst från strömmen konsumerades aldrig. HotPDF tjuvtittar på nästa token för att klassificera den, och den kontrollen lämnar strömpositionen vid tokenens början. Grenen otNull i den strömmande ordboksläsaren skapade null-objektet men gick inte förbi de fyra bytena av null
Konsekvensen var inget hängande utan en tyst feltolkning. Nästa nyckelsökning skannade framåt från fel plats, sprang förbi slutet på nuvarande ordbok, och läste nycklar från följande indirekta objekt, som sedan skrev över aktuellt fält. I fältlistan var symptomet två olika fält som rapporterade samma namn och samma värde. Array- och span-parsarna konsumerade redan null korrekt, så fixen flyttar strömmen fyra byte framåt i just den grenen i stället för att skriva om den generella parsaren. En markör som inte rör sig är samma slags bugg som den i PDF-parsarens markörstopp som hängde HotPDF på felformad indata, med ett annat symptom
Fixen klargjorde också arvet. En lokal /Ff null, eller en referens som löser sig till null, behandlas som frånvarande och uppslagningen fortsätter till föräldern, medan en explicit 0 är en riktig mask som åsidosätter föräldern
Vad ett {$R-}-direktiv bevisar och inte bevisar
Den första hypotesen för flaggbuggen var ett intervallkontrollfel när en stor mask tilldelades till Integer. Den var fel, och skälet är en Delphi-detalj värd att känna till: ett lokalt {$R-}-direktiv placerat före ParseSimpleDictionary i HPDFDoc.pas hade ingen matchande återställning, så varje funktion efter den i uniten kompilerades med intervallkontroller avstängda. Delphi nollställer inte en switch-direktiv i slutet av en funktion, och en include-fil ärver direktivtillståndet vid punkten där den inkluderas
Så en kommandorads--$R+ eller -Cr bevisar bara unitens ursprungliga inställning. Den säger ingenting om en viss funktion längre ner. För att veta om intervallkontroll är aktiv för en given rutin, hitta det närmaste direktivet ovanför den, och bekräfta sedan beteendet med ett verkligt gränsvärde. De två äldre lokala flaggkonsumenterna kastade aldrig intervallundantaget som hypotesen förutsade, så det saknade undantaget kunde inte citeras som bevis för en fix. Den verkliga defekten var Single-avrundningen över kända flaggbitar
Den exakta token-läsningen är ärlig om sin räckvidd också. Den ändrar inte unitvids direktivtillstånd, tilldelar ingen betydelse åt reserverade bitar, och påstår inte att varje bitkombination är ett giltigt fält. Den garanterar att de 32 bitarna i filen är de 32 bitar din kod ser
FAQ
Varför inte ändra THPDFNumericObject.Value till Double eller Int64?
Att ändra typen på ett publikt fält skulle bryta varje anropare som läser eller tilldelar den, och PDF-tal är reella lika ofta som de är heltal. Att behålla Single och läsa fältflaggor från den ursprungliga tokenen fixar buggen för bitmasker utan att ändra någon befintlig deklaration eller anropskonvention
Accepterar SetLoadedFormFieldFlags masker med bit 32 satt?
Ja. Skicka masken som ett signerat Integer; $80000002 och -2147483646 är samma 32 bitar. GetLoadedFormFieldFlags returnerar samma värde efter sparande och återinläsning, inklusive Required
Vilken HotPDF-version läser exakta AcroForm-fältflaggor?
Exakta fältflaggläsningar och null-tokenfixen kom i HotPDF v2.989.5, och strukturerad JSON- och XML-export antog den exakta masken i v2.989.8. Tidigare versioner avrundar varje mask över 2^24 genom Single
Exakta AcroForm-fältflaggor, ärvd fältuppslagning och laddad formulärsredigering är del av HotPDF Component for Delphi and C++Builder, ett nativt PDF-bibliotek för att skapa, läsa in och redigera PDF-dokument