Teknik Makale

Single, AcroForm Alan Bayraklarını Delphi'de Neden Bozar?

HotPDF Delphi Component her PDF sayısını THPDFNumericObject.Value içinde saklar; bu alan Single olarak bildirilmiştir ve bir Single her 32 bitlik tamsayıyı tutamaz. AcroForm alan bayrakları (/Ff) 32 bitlik bir maskedir; yüksek biti ayarlı her maske yükleme sırasında yuvarlanıyordu: $8000FFFF, $80010000 olarak geri geliyordu. v2.989.5'ten itibaren alan bayrağı okumaları özgün tamsayı belirtecini tam olarak ayrıştırır ve bitlerini mevcut Integer API'sine kopyalar

Hata, PDF formlarına hiç bulaşmasanız bile yakından bakmaya değer. Hiçbir şey çökmüyordu, hiçbir istisna fırlamıyordu ve test dosyalarının çoğu geçiyordu; çünkü gerçek maskelerin çoğu küçüktür. Hata yalnızca yüksek bir bit ile düşük yaygın bayraklar birlikte bulunduğunda ortaya çıktı ve o zaman tek bir biti kaybetmiyor, birkaçını birden yeniden yazıyordu. Bir bit maskesini kayan noktalı bir türde saklamak klasik bir Delphi tuzağıdır; üretim kodundaki görünümü tam olarak budur

Bir Single, AcroForm alan bayrağı bitlerini neden kaybeder?

Bir Delphi Single, 24 bitlik bir anlamlı kısma (significand) sahiptir; 2^24'e kadar (16777216) her tamsayıyı tam olarak gösterir, ötesini ise boşluksuz gösteremez. 2^31 ile 2^32 arasında ardışık Single değerleri 256 arayla durur. Bit 32 ayarlı bir alan bayrağı maskesi tam o aralıkta yaşar; düşük sekiz biti, çoğu zaman daha fazlası, 256'nın en yakın katına yuvarlanır

Delphi için HotPDF AcroForm alan bayrakları yuvarlama şeması: 32 bitlik /Ff maskesi $8000FFFF (2147549183), 24 bitlik anlamlı kısma sahip bir Single üzerinden geçer ve $80010000 olarak döner; çünkü gösterilebilir Single değerleri 2^31 ile 2^32 arasında 256 aralıklıdır
2^31 üzerinde bir Delphi Single yalnızca 256 katlarını saklayabilir; en yakına yuvarlama bu yüzden 32 bitlik bir alan bayrağı maskesinin düşük baytını hiçbir istisna olmadan yeniden yazar

ISO 32000-1 §7.3.3, tamsayı ve gerçel nesneleri ayrı sayı türleri olarak tanımlar ve Ek C her biri için farklı uygulama sınırları listeler; bir PDF tamsayısının 32 bitlik bir float'a sığması gerekmez. Alan bayrakları girdisi Tablo 221'de tamsayı olarak tanımlanır ve bir miktar olarak değil bit bit okunur. Bir miktarı küçücük bir kesirle yuvarlamak zararsızdır; bir maskeyi yuvarlamaksa komşu bitlere taşınır. Bütün etkiyi birkaç satırlık sade Delphi ile yeniden üretebilirsiniz:

var
  S: Single;
  Mask: Int64;
begin
  S := $8000FFFF;              // 2147549183, Single olarak gösterilemez
  Mask := Round(S);            // 2147549184
  Writeln(IntToHex(Mask, 8));  // 80010000: 1-16 bitleri temizlendi, bit 17 ayarlı

  S := $80003FFF;
  Mask := Round(S);
  Writeln(IntToHex(Mask, 8));  // 80004000: 1-14 bitleri temizlendi, bit 15 ayarlı
end;

Ders, PDF'in ötesine genellenir. Bir Single, bir Double, float tutan bir Variant ya da float'a çözülmüş bir JSON sayısı, bit alanı için yanlış kaptır; tesadüfen test ettiğiniz değerler küçük olsa bile. Double, tam aralığı 2^53'e iter; bu 32 bitlik maskelerde sorunu gizler ama türün yanlış niyeti ifade etmesini önlemez. Cardinal, Integer ya da Int64 kullanın ve dönüştürmeyi zaten yuvarlanmış bir float'tan değil kaynak metinden yapın

Hangi alan bayrakları döner: Radio, Pushbutton, Password ve Multiline

Yukarıdaki iki maske tesadüfi değil; tek bir yuvarlama adımının okuyucunun gördüğü alan türünü nasıl değiştirdiğini gösterirler. PDF, bayrak bitlerini 1'den sayar; bit n'in değeri 2^(n-1)'dir. ISO 32000-1 §12.7.3.1 Tablo 226 her alan için 1-3 bitlerini, Tablo 228 düğme bayraklarını, Tablo 230 ise metin alanı bayraklarını tanımlar

  • $8000FFFF'den $80010000'e: bit 16 (Radio, $8000, Tablo 228) temizlenir ve bit 17 (Pushbutton, $10000) ayarlanır; radyo düğmesi bir itme düğmesine dönüşür
  • Aynı yuvarlama 1-3 bitlerini de temizler; Tablo 226'daki ReadOnly, Required ve NoExport, Radio ile birlikte kaybolur
  • $80003FFF'ten $80004000'e: Tablo 230'daki bit 13 (Multiline, $1000) ile bit 14 (Password, $2000) ikisi de temizlenir; maskeli bir parola kutusu tek satırlık düz bir metin alanına dönüşür
  • Bit 32'nin altında hasar 2^24'te başlar: 16777219 (Comb artı ReadOnly ve Required), 16777220'ye yuvarlanır; bu da ReadOnly ile Required'ı düşürüp NoExport ekler

Son durum, HotPDF için form düzenlemenin ötesinde önemliydi. ExportLoadedPagesStructured, JSON ve XML form modelini etkin maskeden kurar ve değer bastırma kararında NoExport'ı kullanır; yuvarlanmış bir maske sıradan bir değeri gizleyebiliyor ya da yanlış readOnly ve required üyeleri raporlayabiliyordu. Yapılandırılmış dışa aktarma yolu aynı tam okumayı v2.989.8'de aldı

Delphi için HotPDF AcroForm alan bayrakları bit haritası: bit 32 ile 1-16 bitleri ayarlı /Ff maskesi $8000FFFF, tek bir Single yuvarlamasında 1-16 bitlerini kaybeder ve Pushbutton olan bit 17 kazanarak $80010000 olur
ISO 32000-1 Tablo 226, 228 ve 230 bayrakları grup hâlinde hareket eder: Radio Pushbutton'a dönerken ReadOnly, Required, NoExport, Multiline ve Password aynı yuvarlama adımında silinebilir

HotPDF, tam 32 bitlik tamsayı belirtecini nasıl okur

HotPDF Delphi Component, okuma tarafını sayısal nesne modelini değiştirmeden düzeltir. Ayrıştırıcı, özgün belirteç metnini Single değerin yanında zaten saklar; alan bayrağı okuyucu bu belirteci bir tamsayı olarak ayrıştırır, sınırlı bir Int64 içinde biriktirir ve ortaya çıkan 32 biti, genel API'nin döndürdüğü Integer'a kopyalar. Genel sayısal API'ler ile eski tamsayı olmayan yedek yol olduğu gibi kalır; tam yolu yalnızca alan bayrağı tüketicileri kullanır

Delphi'de HotPDF tam AcroForm alan bayrağı okuma yolu: ayrıştırıcı kaynak belirtecini Single değerin yanında saklar, TryParseFlagMask onu sınırlı bir Int64 içinde biriktirir ve 32 bit, GetLoadedFormFieldFlags ile SetLoadedFormFieldFlags'a bir Integer olarak ulaşır
Alan bayrakları, yuvarlanmış Single yerine saklanan belirteç metnini ayrıştırır; yüksek bitli maskenin her iki yazımı, 2147549183 ile -2147418113, özdeş bitlerle ulaşır

Ayrıştırıcı, gerçek dosyaların yüksek bitli maskeyi yazdığı her iki biçimi de kabul etmek zorundadır. Bazı yazarlar, Ek C tamsayı sınırı olan 2^31 - 1'in üzerinde olmasına rağmen pratikte yaygın olan işaretsiz 2147549183 değerini yazar; diğerleri işaretli -2147418113 değerini. İkisi de aynı bitleri anlatır. Sınırlı ayrıştırıcının bir sürümü şöyle görünü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;
  // İşaretli belirteçler -2^31'e dek, işaretsizler 2^32 - 1'e dek
  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;  // gerçel sayılar eski yedeği kullanır
    Digit := Ord(Token[P]) - Ord('0');
    if Number > (Limit - Digit) div 10 then Exit;  // sınırlı, taşma yok
    Number := Number * 10 + Digit;
    Inc(P);
  end;
  if Negative and (Number <> 0) then Number := 4294967296 - Number;
  Mask := Cardinal(Number);
  Result := True;
end;

İki ayrıntı bilinçlidir. Sınır her çarpmadan önce kontrol edilir; 400 haneli bir belirteç Int64'ü taşırma yerine temizce başarısız olur. 3.0 gibi bir belirteç de reddedilir; bu, çağıranı Round(Value)'a geri gönderir. Tam yol, tamsayı maskelerini iyileştirirken bozuk olanlar için yeni anlam icat etmez

Uygulama kodundan bakınca imzalarda hiçbir şey değişmez. THotPDF.GetLoadedFormFieldFlags(FieldIndex: Integer): Integer, etkin maskeyi döndürür ve HotPDF'in miras kalan AcroForm alan değerlerini ve resetleri nasıl çözdüğü makalesinde anlatıldığı gibi /Parent zincirini izler; THotPDF.SetLoadedFormFieldFlags(FieldIndex, AFlags: Integer) ise eksiksiz bir yerel maske yazar. Bit 32 ayarlı bir maske salt negatif bir Integer'dır; o yüzden değeri dönüştürmek yerine bitleri taşıyın:

const
  FF_RADIO      = $00008000;  // bit 16, ISO 32000-1 Tablo 228
  FF_PUSHBUTTON = $00010000;  // bit 17, Tablo 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));   // aynı 32 bit, dönüşüm yok
      if (Bits and FF_RADIO) <> 0 then
        Writeln(Pdf.GetFormFieldName(I), ': radio, mask $', IntToHex(Bits, 8));
    end;

    Bits := $80000002;                    // yüksek bit artı Required
    Move(Bits, Native, SizeOf(Native));   // Native = -2147483646
    Pdf.SetLoadedFormFieldFlags(0, Native);
    Pdf.SaveLoadedDocument('survey-out.pdf');
  finally
    Pdf.Free;
  end;
end;

Miras bir maskeyi okumak çocuk sözlüğüne dokunmaz; yazmak ise seçili alandaki maskenin tamamını yerel bir girdiyle değiştirir. SetLoadedFormFieldReadOnly ile SetLoadedFormFieldRequired kısayolları etkin miras maskeden başlar, bir biti çevirir ve öteki her biti tam olarak korur. Bayraklardan çok değerleri doldurmak için HotPDF ile yüklü bir PDF'de form alanı değeri ayarlama yazısına bakın

Value ve RememberSourceToken sırası neden önemlidir?

Sıra önemlidir çünkü THPDFNumericObject, sakladığı belirtece yalnızca Single değer değişmediği sürece güvenir. RememberSourceToken, belirteci geçerli Value anlık görüntüsüyle birlikte saklar ve SourceToken getter'ı, Value o görüntüden saptığı anda boş bir dizge döndürür. Düzenlenen bir sayının bayat özgün metniyle kaydedilmesini önleyen kural tam olarak bu geçersizleştirmedir

Kural, yazma tarafında tuzağa dönüşür. Önce belirteci hatırlayıp değeri sonra atarsanız anlık görüntü eski değeri tutar, getter belirteç bildirmez ve serileştirici yuvarlanmış Single'ı biçimlemeye döner. Kütüphanenin içindeki her bayrak yazıcısı — kısayollar ve yerel alan kurucuları dahil — aynı sırayı izler:

// Doğru: anlık görüntü Value ile eşleşir, tam belirteç kaydetmeyi atlatır
Item := THPDFNumericObject.Create(nil);
Item.Value := AFlags;
Item.RememberSourceToken(AnsiString(IntToStr(AFlags)));

// Yanlış: anlık görüntü Value değişmeden alınır, SourceToken
// geçersizleşir ve yerine yuvarlanmış Single yazılır
Item := THPDFNumericObject.Create(nil);
Item.RememberSourceToken(AnsiString(IntToStr(AFlags)));
Item.Value := AFlags;

SaveNumericObject, geçerli bir belirteç olduğunda onu yazdığı için sırayı doğru kurmak; kaydedilip yeniden açılan dosyanın yuvarlanmış bir yaklaşım değil özgün tamsayıyı taşımasını sağlayan şeydir

İki alanın tek adı paylaşmasına yol açan null belirteci

Aynı sürüm, bu maskeleri test ederken bulunan ikinci bir ayrıştırıcı kusurunu da düzeltti: akıştan okunan bir sözlükteki null değeri hiçbir zaman tüketilmiyordu. HotPDF bir sonraki belirteci sınıflandırmak için göz atar ve o kontrol, akış konumunu belirtecin başında bırakır. Akış sözlüğü okuyucusunun otNull dalı null nesnesini kuruyordu ama null'un dört baytının ötesine ilerlemiyordu

Sonuç askıda kalma değil sessiz bir yanlış okumaydı. Bir sonraki anahtar araması yanlış yerden ileriye taradı, geçerli sözlüğün sonunu aştı ve ardından gelen dolaylı nesneden anahtarlar okudu; bu anahtarlar geçerli alanın üzerine yazdı. Alan listesindeki belirti, iki farklı alanın aynı adı ve aynı değeri bildirmesiydi. Dizi ve span ayrıştırıcıları null'u zaten doğru tüketiyordu; düzeltme genel ayrıştırıcıyı yeniden yazmak yerine o tek daldaki akışı dört bayt ilerletir. Hareket etmeyen bir imleç, HotPDF'i bozuk girdide askıya alan PDF ayrıştırıcı imleç takılmalarındaki hatayla aynı ailedendir; belirtisi farklıdır

Düzeltme mirası da netleştirdi. Yerel bir /Ff null ya da null'a çözülen bir referans yok sayılır ve arama ebeveyne devam eder; açık bir 0 ise ebeveyni ezen gerçek bir maskedir

Bir {$R-} yönergesinin kanıtladığı ve kanıtlamadığı

Bayrak hatasına ilk hipotez, büyük bir maske Integer'a atandığında beklenen bir aralık kontrolü hatasıydı. Yanlış çıktı ve nedeni bilinmeye değer bir Delphi ayrıntısı: HPDFDoc.pas içinde ParseSimpleDictionary'den önce konmuş yerel bir {$R-} yönergesinin eşleşen bir geri yüklemesi yoktu; ünitedeki ondan sonraki her fonksiyon aralık kontrolleri kapalı derleniyordu. Delphi, bir anahtar yönergesini fonksiyon sonunda sıfırlamaz ve bir include dosyası, yönerge durumunu dahil edildiği noktadan miras alır

Bu yüzden komut satırı -$R+ ya da -Cr yalnızca ünitenin başlangıç ayarını kanıtlar; aşağıda kalan belirli bir fonksiyon hakkında hiçbir şey söylemez. Belirli bir rutin için aralık kontrolünün etkin olup olmadığını bilmek istiyorsanız üzerindeki en yakın yönergeyi bulun, sonra davranışı gerçek bir sınır değeriyle teyit edin. İki eski yerel bayrak tüketicisi, hipotezin öngördüğü aralık istisnasını hiç fırlatmadı; o eksik istisna bir düzeltmenin kanıtı olarak gösterilemezdi. Gerçek kusur, bilinen bayrak bitlerindeki Single yuvarlamasıydı

Tam belirteç okuması kapsamına karşı da dürüsttür. Ünite genelindeki yönerge durumunu değiştirmez, rezerve bitlere anlam yüklemez ve her bit kombinasyonunun geçerli bir alan olduğunu iddia etmez. Garantisi şudur: dosyadaki 32 bit, kodunuzun gördüğü 32 bittir

SSS

THPDFNumericObject.Value neden Double ya da Int64 yapılmıyor?

Genel bir alanın türünü değiştirmek onu okuyan ya da atayan her çağıranı kırar; üstelik PDF sayıları tamsayı olduğu kadar gerçeldir. Single'ı koruyup alan bayraklarını özgün belirteçten okumak, hiçbir mevcut bildirimi ya da çağrı kuralını değiştirmeden bit maskelerindeki hatayı düzeltir

SetLoadedFormFieldFlags, bit 32 ayarlı maskeleri kabul eder mi?

Evet. Maskeyi işaretli bir Integer olarak iletin; $80000002 ile -2147483646 aynı 32 bittir. GetLoadedFormFieldFlags, Required dahil, kaydedip yeniden yükledikten sonra aynı değeri döndürür

Hangi HotPDF sürümü tam AcroForm alan bayraklarını okur?

Tam alan bayrağı okumaları ile null belirteç düzeltmesi HotPDF v2.989.5 ile geldi; yapılandırılmış JSON ve XML dışa aktarımı tam maskeyi v2.989.8'de benimsedi. Daha eski sürümler 2^24'ün üzerindeki her maskeyi Single üzerinden yuvarlar

Tam AcroForm alan bayrakları, miras kalan alan araması ve yüklü form düzenleme, Delphi ve C++Builder için HotPDF Componentin parçasıdır; kütüphane, PDF belgeleri oluşturan, yükleyen ve düzenleyen yerel bir PDF kütüphanesidir