Artikel Teknis

Menjaga Presisi Desimal PDF Saat Save di Delphi

PDFlibPas, PDF Developer Library dari losLab, menyimpan teks desimal persis seperti yang diparsenya untuk setiap bilangan real di dokumen dan menuliskan teks itu kembali apa adanya setiap kali nilainya tidak pernah diubah. Sejak v3.539.19, setelan SetPrecision hanya mengatur angka yang dibuat atau disunting oleh library, jadi load-and-save biasa tidak lagi membulatkan /Gamma CalRGB dari 2.22221 turun ke 2.2222 lalu menggeser warna halaman yang tidak disentuh siapa pun. Perubahannya kecil di kode dan besar di apa yang dikatakannya soal parser: nilai yang Anda decode dan literal yang Anda keluarkan adalah dua hal berbeda, dan perjalanan bolak-balik lewat Double bukan transformasi identitas

Kenapa save yang tidak mengubah apa pun menggeser warna halaman?

Karena parameter color space-nya yang diformat ulang, bukan gambarnya. File yang membongkar ini adalah dokumen kantor 35 halaman di korpus regresi lokal, dengan gambar header yang dipakai ulang di setiap halaman. Memuatnya lalu menyimpannya kembali langsung menghasilkan image stream yang identik byte per byte dengan inputnya, dan perbandingan hash stream melaporkan dokumennya tidak berubah. Perbandingan hasil render tidak setuju: setiap satu dari 35 halaman menunjukkan perbedaan piksel di header, dan tidak di tempat lain

Gambar headernya digambar lewat color space CalRGB, yang didefinisikan ISO 32000-1 §8.6.5.3 dengan sebuah /WhitePoint, array /Gamma tiga elemen yang opsional, dan /Matrix sembilan elemen yang juga opsional. Array-array itu adalah objek numerik biasa di dictionary color space. TPDFNumeric menyimpan masing-masingnya sebagai Double dan tidak sebagai apa pun yang lain, dan TPDFNumeric.Output memformat Double itu lewat PDFPrecNum, yang default-nya empat tempat desimal. Jadi /Gamma berubah dari 2.22221 jadi 2.2222, satu entry matrix berubah dari 0.71519 jadi 0.7152, dan renderer dengan setia menghasilkan warna yang sedikit berbeda dari kalibrasi yang sedikit berbeda. Byte gambarnya tidak bersalah; angka-angka di sekitarnya yang bersalah. Bagian yang tidak nyaman adalah betapa tak terlihatnya hal ini. Membandingkan byte stream hasil decode tidak bisa melihatnya, karena angka-angka itu hidup di dictionary, bukan di stream. Membandingkan payload lampiran juga tidak bisa. Bahkan revision diff yang dijelaskan di artikel modification level membuat fingerprint dari body objek yang sudah dinormalisasi, jadi kedua revisi menghasilkan hash yang sama dan diff-nya melaporkan keduanya identik. Hanya rendering yang menangkapnya, itulah kenapa baseline korpus merender setiap halaman alih-alih mempercayai pemeriksaan struktural saja

Di mana PDFlibPas kehilangan presisi CalRGB pada save tanpa perubahan di Delphi: /Gamma 2.22221 hasil parse dan satu entry matrix 0.71519 tersimpan di TPDFNumeric sebagai Double, Output memformatnya lewat PLDoubleToStr dengan PDFPrecNum di empat tempat desimal, setiap pemeriksaan struktural melaporkan dokumen tidak berubah, dan hanya perbandingan hasil render yang menunjukkan seluruh 35 gambar header bergeser
Byte gambarnya tidak bersalah: TPDFNumeric memformat ulang angka kalibrasi di sekitarnya lewat PDFPrecNum, jadi hash stream dan diff fingerprint sama-sama melaporkan revisi yang identik sementara renderer menghasilkan warna yang sedikit berbeda di setiap halaman

Nilai yang Anda parse bukan literal yang seharusnya Anda tulis

Bilangan real PDF adalah string desimal, dan ISO 32000-1 §7.3.3 eksplisit bahwa ia hanya string desimal: tanpa notasi radix, tanpa bentuk eksponen. Annex C lalu mencantumkan presisi yang diharapkan dihormati implementasi, sekitar lima digit desimal signifikan di bagian pecahannya. Presisi output default empat sudah di bawah itu, dan makin buruk di dekat nol: PLDoubleToStr menskalakan nilainya, membulatkan ke integer, dan mengeluarkan 0 ketika hasilnya nol, jadi entry matrix -0.000012345 tidak kehilangan satu digit, ia hilang sama sekali

Menaikkan default-nya hanya memindahkan tebingnya. Perbaikannya adalah berhenti berpura-pura bahwa Double itu bilangannya. Ketika tokenizer di TPDFStructure.Decode mengenali real standar, artinya token itu memuat titik desimal dan tanpa penanda eksponen, ia menyimpan teks sumbernya di field baru FOriginalText bersama nilai hasil konversinya. Output lalu memilih teks itu dan hanya jatuh kembali ke pemformatan ketika tidak ada yang bisa dipilih

Cara PDFlibPas mempertahankan teks desimal hasil parse di Delphi: tokenizer di TPDFStructure.Decode menyimpan literal sumbernya di FOriginalText untuk setiap token yang punya titik desimal dan tanpa eksponen, Output menuliskan teks itu apa adanya alih-alih memanggil PLDoubleToStr, SetTo membersihkannya karena angka yang disunting adalah angka baru
Nilai yang Anda decode dan literal yang Anda keluarkan adalah dua hal berbeda: mengutamakan teks hasil parse membuat 2.22221 tetap persis, sementara angka buatan library dan angka yang disunting tetap mengikuti PDFPrecNum dan setelannya tidak pernah menyentuh input yang tak tersentuh
// Lib/PDFlibStruct.pas: seluruh perbaikannya di sisi output
Function TPDFNumeric.Output: AnsiString;
Begin
  If FOriginalText<> '' Then
    Result:= FOriginalText
  Else
    Result:= PLDoubleToStr(FValue, Owner.PDFPrecNum);
End;

Procedure TPDFNumeric.SetTo(Const Value: Double);
Begin
  FOriginalText:= '';   // angka yang disunting adalah angka baru
  FValue:= Value;
  FChanged:= True;
End;

Dua batasnya disengaja. Integer tidak dipertahankan, karena pemformatan integer sudah lossless. Bentuk eksponen seperti 6.02E23 ditoleransi di input demi producer yang rusak, tapi tidak dipertahankan di output, karena menuliskannya kembali akan melestarikan sintaks yang dilarang §7.3.3; bentuk itu melewati formatter seperti angka lain buatan library. Tokenizer-nya juga menerapkan perbaikan minimal yang biasa sebelum menyimpan teks, jadi literal bertitik di depan seperti .5 disimpan sebagai 0.5 dan literal bertitik di belakang seperti 5. sebagai 5.0. Keduanya adalah angka yang sama bagi setiap reader dan jauh lebih luas diterima

Apa yang dijamin SetPrecision setelah v3.539.19?

TPDFlib.SetPrecision sekarang mengendalikan jumlah tempat desimal angka yang dihasilkan library sendiri: nilai yang digambar lewat painter, angka yang dibuat dari Double seperti lewat NewNumeric, dan nilai hasil parse mana pun yang sejak itu disunting dengan SetTo. Perhatikan bahwa teks yang didekode lewat object API, misalnya literal yang dikirim ke SetObjectFromString, melewati tokenizer yang sama dan dipertahankan dengan cara yang sama. Desimal hasil parse yang tidak pernah diubah mempertahankan presisi inputnya terlepas dari setelannya, dan mengubah setelan setelah load tidak menyentuhnya secara retroaktif. Entri referensi SetPrecision diperbarui di rilis yang sama untuk menyatakan persis ini, karena kata-kata yang lama menyiratkan setelannya berlaku untuk setiap angka di file

Pembersihannya terjadi di SetTo, bukan diturunkan dari flag Changed, dan perbedaan itu penting. Pipeline save mereset Changed pada objek begitu objek itu selesai ditulis, jadi pemeriksaan berbentuk "keluarkan teks asli kecuali berubah" akan mulai mengeluarkan teks basi untuk nilai yang disunting, disimpan, lalu disunting lagi di sesi yang sama. Mengikat teks aslinya pada assignment itu sendiri membuat keduanya mustahil berselisih. Test regresinya memaku setiap perilaku ini dengan nilai-nilai dari file aslinya

uses
  PDFlibStruct;

var
  Structure: TPDFStructure;
  Values: TPDFArray;
  Number: TPDFNumeric;
begin
  Structure := TPDFStructure.Create;
  try
    Structure.PDFPrecNum := 4;
    Values := TPDFArray(Structure.Decode('[2.22221 0.71519 -0.000012345 1 0.12567]'));
    // Input yang tidak disunting bertahan apa adanya, termasuk nilai yang
    // akan dilipat jadi 0 oleh pemformatan empat tempat
    Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');

    // Penyuntingan membuang teks aslinya lalu mengikuti PDFPrecNum
    Number := TPDFNumeric(Values.Item[0]);
    Number.SetTo(0.123456);
    Assert(Number.Output = '0.1235');
    Assert(Structure.NewNumeric(0.123456).Output = '0.1235');

    // Menurunkan presisi setelahnya tidak mencapai input yang tak tersentuh
    Structure.PDFPrecNum := 2;
    Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
  finally
    Structure.Free;
  end;
end;

Kenapa content model masih menormalisasi angka?

Karena TPDFContentProgram menjanjikan operand numerik yang kanonik, dan janji itu lebih bernilai daripada teks apa adanya di dalam content stream. Content model yang bisa disunting, model yang sama yang jadi fondasi graphics-state tracker, ada supaya NormalizeContentStreams, optimizer, dan Emit menghasilkan output yang stabil dan bisa dibandingkan dari input sembarang. Kalau operand hasil parse membawa teks aslinya masuk ke model, urutan operator seperti 0.50000 0 0 RG akan dikeluarkan berbeda dari 0.5 0 0 RG, dan setiap perbandingan di hilir akan melenceng mengikuti kebiasaan pemformatan producer-nya

Jadi model-nya membuang teks aslinya di kedua titik masuknya. NormalizeContentNumbers berjalan pada setiap operand saat parser mendorongnya masuk dan sekali lagi di dalam SetOperand ketika sumber pasokan pemanggil didekode, dan ia berulang menembus array serta dictionary supaya dash pattern, array TJ, dan dictionary properti marked content ikut tercakup. Memanggil SetTo(AsDouble) pada setiap numeric sudah cukup, karena itulah persis operasi yang membersihkan teksnya. Data inline image mentah dibiarkan seperti apa adanya, seperti selama ini

Kenapa content model PDFlibPas tetap menormalisasi angka: NormalizeContentNumbers berjalan saat parser mendorong setiap operand dan sekali lagi di dalam SetOperand, berulang menembus array dan dictionary sehingga dash pattern, array TJ, dan dictionary properti marked content tercakup, dan SetTo AsDouble membersihkan teks aslinya sehingga 0.50000 dan 0.5 dikeluarkan identik
Operand numerik kanonik adalah janji content model: data inline image mentah dibiarkan, dan angka dictionary yang tak tersentuh di luar content stream tetap mendapat jaminan apa adanya, jadi pasangan LoadFromFile dan SaveToFile biasa tetap mempertahankannya
// Lib/PDFlibContentModel.pas: content model menepati kontraknya
Procedure NormalizeContentNumbers(Obj: TPDFObject);
Var
  K: Integer;
Begin
  If Obj is TPDFNumeric Then
    TPDFNumeric(Obj).SetTo(TPDFNumeric(Obj).AsDouble)
  Else If Obj is TPDFArray Then
    For K:= 0 To TPDFArray(Obj).Count- 1 Do
      NormalizeContentNumbers(TPDFArray(Obj).Item[K])
  Else If Obj is TPDFDictionary Then
    For K:= 0 To TPDFDictionary(Obj).Count- 1 Do
      NormalizeContentNumbers(TPDFDictionary(Obj).Entry[K].Value);
End;

Aturan praktisnya bagi pemanggil karena itu sederhana. LoadFromFile biasa yang diikuti SaveToFile meninggalkan content stream yang tak tersentuh dan angka dictionary yang tak tersentuh seperti semula. Halaman yang melewati NormalizeContentStreams, atau penyuntingan apa pun yang dilakukan lewat content model, keluar dalam bentuk kanonik sebagaimana dirancang, dan sisa dokumennya tetap dipertahankan. Itu dua permintaan yang berbeda, dan sekarang keduanya melakukan dua hal yang berbeda

Berapa biayanya, dan di mana jaminannya berhenti

Setiap TPDFNumeric sekarang membawa satu referensi AnsiString tambahan, dan setiap desimal hasil parse menjaga teks sumbernya tetap hidup selama objeknya hidup. Pada dokumen dengan jutaan bilangan real itu memori yang nyata, dan itu termasuk dalam pengukuran dokumen besar mana pun, bukan sesuatu yang bisa diabaikan begitu saja. Jaminannya juga dibatasi pada dokumen milik angka itu sendiri: menyalin objek antar dokumen atau merekonstruksi nilai lewat object API menghasilkan angka baru, yang mengikuti presisi output seperti angka baru lainnya. Layak untuk tepat soal apa yang diklaim dan tidak diklaim rilis ini. Load-and-save atas dokumen yang tak tersentuh sekarang mempertahankan angka kalibrasi yang benar-benar dikonsumsi renderer, dan itulah properti yang diperiksa baseline korpus. Ia tidak mengklaim output yang identik byte per byte, yang juga bergantung pada penomoran objek, kompresi stream, dan identifier trailer yang dibahas di artikel PDF ID deterministik. Dan ia tidak membuat fingerprint diff bisa melihat perbedaan pembulatan di file buatan perangkat lunak lain, karena itu tetap menghash body yang sudah dinormalisasi. Pelajarannya berlanjut jauh melampaui CalRGB: ketika parser hanya menyimpan nilai hasil konversinya, setiap save adalah penyuntingan, dan satu-satunya cara menyadarinya adalah dengan melihat hasil rendernya. Penanganan numerik dan semantik SetPrecision didokumentasikan di halaman produk losLab PDF Developer Library