Artikel Teknis

Angka PDF vs JSON: NaN, Infinity dan null di Delphi

PDF Library for Delphi (PDFlibPas) menerbitkan JSON yang valid untuk setiap angka PDF sejak v3.539.31. GetObjectJSON menulis ulang token yang diterima ISO 32000-1 tapi ditolak RFC 8259, seperti -.25, +1.5, dan 007.5, menjadi -0.25, 1.5, dan 7.5 digit demi digit; GetDocumentJSON dan laporan-laporan analisis menulis null untuk NaN dan Infinity; dan PLDoubleToStr menulis 0 untuk NaN alih-alih me-raise EInvalidOp di tengah-tengah export. Sebelum perbaikan, library bisa menghasilkan JSON yang reader-nya sendiri menolak memuatnya kembali

Kenapa angka PDF yang valid merusak JSON?

Karena dua tata bahasa itu berselisih pada empat detail kecil, dan parser PDF yang menghormati teks sumber akan membawa detail-detail itu langsung ke output. ISO 32000-1 §7.3.3 membolehkan angka diawali tanda plus, menghilangkan bagian integer (.5), berakhir pada titik telanjang (4.), dan membawa nol di depan (007.5). RFC 8259 §6 tak mengizinkan satu pun: minus opsional, bagian integer yang berupa 0 atau diawali 1 sampai 9, dan minimal satu digit setelah titik desimal apa pun. Produser bebas menulis bentuk-bentuk PDF itu, dan banyak generator serta file suntingan tangan yang memang melakukannya

Kebocorannya datang dari fitur presisi yang disengaja. Sejak v3.539.19, TPDFNumeric.Output mengembalikan teks persis yang di-parse tokenizer untuk real number, yang menjaga nilai warna terkalibrasi tetap eksak saat save, seperti dijelaskan di menjaga presisi desimal PDF hasil parse. Tokenizer sudah menambal .5 menjadi 0.5 dan 4. menjadi 4.0 saat masuk, dan integer diformat ulang dari nilainya, sehingga +3 kembali sebagai 3. Yang selamat apa adanya adalah sisanya: titik di depan bertanda (-.25), plus eksplisit pada real (+1.5), dan nol di depan (007.5). Object writer lama menempelkan Output tepat setelah "value":, dan TJSONParser.ParseNumber di reader milik library sendiri berhenti pada setiap bentuk itu dengan "Invalid JSON number", sehingga export sukses dan re-import gagal dengan PDFLIB_ERROR_OBJECT_JSON_INVALID (105)

GetObjectJSON milik PDFlibPas menulis ulang token angka PDF yang ditolak RFC 8259 digit demi digit: -.25 menjadi -0.25, +1.5 kehilangan plusnya, 007.5 melepas nol di depannya, dan digit pecahan seperti 1.250000 selamat, karena memformat dari Double tersimpan akan menambah derau biner
Writer lama menempelkan teks hasil parse apa adanya, reader milik library sendiri berhenti dengan Invalid JSON number, dan error 105 memutus round trip yang pihak export sebut sukses
uses
  System.SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
  JSON: AnsiString;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('legacy-drawing.pdf', '') = 0 then
      raise Exception.Create('load failed');

    // Object 12 adalah array yang ditulis sebagai [-.25 +1.5 007.5]
    JSON := Lib.GetObjectJSON(12, 0);
    // v3.539.31 ke atas: nilai datang sebagai -0.25, 1.5 dan 7.5

    // SetObjectJSON tak menerima opsi, jadi kirimkan 0
    if Lib.SetObjectJSON(12, JSON, 0) = 0 then
      raise Exception.CreateFmt('round trip rejected, error %d',
        [Lib.LastErrorCode]);

    Lib.SaveToFile('legacy-drawing-roundtrip.pdf');
  finally
    Lib.Free;
  end;
end;

Bagaimana PDFNumberTextToJSON menjaga setiap digit?

PDFNumberTextToJSON mengeja ulang token alih-alih menghitungnya kembali dari sebuah Double. Fungsi di PDFlibObjectJSON membaca tanda opsional, mengumpulkan digit sebelum dan sesudah satu titik desimal, lalu hanya menerapkan suntingan yang diminta JSON: membuang plus, mencabut nol di depan sambil menyisakan satu, menyuplai 0 ketika bagian integer kosong, membuang titik ekor telanjang, dan mengembalikan minusnya. Token yang memuat karakter lain, atau tanpa digit sama sekali, jatuh ke PLJSONNumber(Value, 10), yang menulis null ketika nilainya tak finite

PDFNumberTextToJSON milik PDFlibPas membaca tanda, mengumpulkan digit di sekitar satu titik desimal dan hanya menerapkan suntingan yang diminta JSON, sedangkan karakter lain atau run digit kosong jatuh ke PLJSONNumber, yang menulis null untuk NaN dan Infinity alih-alih angka
Mengeja ulang mengalahkan menghitung ulang: tokenizer sudah menambal .5 dan 4. saat masuk, jadi writer menyimpan setiap digit yang selamat dan round trip merekonstruksi nilai yang persis sama
  • -.25 menjadi -0.25, dan +.5 menjadi 0.5
  • +1.5 menjadi 1.5
  • 007.5 menjadi 7.5, sedangkan 0.75 tetap apa adanya
  • 4. menjadi 4 kalau token seperti itu pernah sampai ke writer
  • 2.22221 dan 1.250000 menyimpan setiap digit pecahan, nol di ekor termasuk

Memformat dari Double tersimpan akan lebih pendek dan salah, dengan alasan yang sama kenapa perbaikan presisi itu ada: presisi output default adalah empat desimal, dan bahkan konversi presisi penuh pun bisa menambah derau biner pada literal desimal. Menyimpan digit berarti SetObjectJSON dan ImportObjectJSON, yang menyerahkan teks setiap angka JSON ke tokenizer PDF, merekonstruksi nilai yang persis sama. Jaminannya mencakup nilai, bukan byte: setelah re-import, -.25 tersimpan dan tersave sebagai -0.25. Kedua ejaan setara di bawah §7.3.3, tapi diff level byte akan menandai perubahannya, jadi jangan perlakukan siklus export-import sebagai no-op pada dokumen yang byte-nya tercakup signature

Apa yang terjadi pada angka yang tak bisa direpresentasikan JSON?

GetDocumentJSON kini menulis null untuk angka apa pun yang NaN atau infinite, karena RFC 8259 §6 tak punya sintaks untuk keduanya. Infinity lebih mudah dihasilkan daripada kelihatannya: tokenizer PDF mengakumulasi digit dengan perkalian berulang ke dalam sebuah Double, yang mentok di sekitar 1.8 × 10308, sehingga literal integer sepanjang sedikit di atas 300 digit diam-diam menjadi +Inf. File yang jujur tak pernah memuat literal seperti itu; file hasil fuzz dan file jahat ya, dan itulah kenapa mereka pantas masuk corpus test yang sama dengan kasus di menghardening parser PDF Pascal terhadap file berbahaya. Document writer lama memformat non-integer dengan Str(D:0:6), dan untuk +Inf itu menulis teks +Inf, yang tak akan di-parse konsumen JSON mana pun

null-nya sengaja lossy. Konsumen output GetDocumentJSON harus menerima null di mana pun angka bisa muncul, dan sebaiknya membacanya sebagai "sebuah nilai ada tapi tak bisa direpresentasikan", bukan sebagai key yang hilang. Literal aslinya tak bisa dipulihkan dari document JSON, jadi pipeline yang peduli sebaiknya mencatat objeknya dan memperlakukan file itu sebagai tersangka alih-alih mengganti dengan default

Kenapa satu NaN bisa menggagalkan export SVG atau JSON?

Karena PLDoubleToStr, formatter angka invariant di balik content stream, SVG, XML, CSV, dan sebagian besar JSON di library, menskalakan inputnya lalu memanggil Round, dan Round(NaN) me-raise EInvalidOp pada target seperti Win32, di mana Delphi membiarkan exception invalid-operation x87 tak termask. Exception itu meledak setelah writer selesai menerbitkan sebagian outputnya, jadi satu pengukuran degenerate, sebuah 0/0 di metrik atau NaN yang dikirim caller, menyisakan file terpotong. PLDoubleToStr kini mengembalikan 0 untuk NaN, dan cabang integer-nya di-clamp ke ±9.2e18 seperti cabang pecahannya, sehingga Infinity juga keluar sebagai literal finite

Nol adalah jawaban yang benar untuk content stream, tempat slot angka harus berisi angka, dan jawaban yang salah untuk laporan, tempat 0 adalah pengukuran yang masuk akal. JSON writer yang harus menjaga pembedaan itu memakai PLJSONNumber(Value, Decimals) dari PDFlibExtra, yang menulis null untuk NaN atau Infinity dan digit invariant untuk lainnya. PLJSONNumber kini menopang GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON, dan laporan barcode, deskew, structured text, serta PDF/VCR; laporan deskew dulu menulis 0 untuk sudut non-finite dan kini menulis null

PDFlibPas menghentikan NaN dan Infinity lewat tiga jalur: AddPageMatrix, ScalePage dan RedactRegion menolak argumen non-finite di muka, PLDoubleToStr menulis 0 untuk slot content stream, dan PLJSONNumber menulis null di laporan, tempat nol akan terbaca sebagai pengukuran masuk akal, setelah Round(NaN) dulu me-raise EInvalidOp di tengah export
Nol jawaban yang benar untuk content stream dan yang salah untuk laporan, jadi report writer menyerahkan setiap Double ke PLJSONNumber dan membiarkan null berkata nilainya ada tapi tak terrepresentasi
uses
  SysUtils, PDFlibTypes, PDFlibExtra;

function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
  B: PLStringBuilder;
begin
  B := PLStringBuilder.Create(128);
  try
    // Format setiap Double ke teks lebih dulu; PLJSONNumber menulis null
    // untuk NaN atau Infinity dan selalu memakai titik desimal
    B.Append('{"page":').Append(Page)
     .Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
     .Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
     .Append('}');
    // Jangan B.Append(Angle): overload Double mengikuti locale pengguna
    Result := B.ToString;
  finally
    B.Free;
  end;
end;

Di mana locale pengguna masih menyelinap ke JSON?

Lewat formatter apa pun yang membaca pengaturan regional, dan audit penuh atas output yang terbaca mesin menemukan tepat satu tersisa: maxAcceptedMeanError di GetSimilarImageDeduplicationReportJSON, ditulis dengan PLFloatToStr, wrapper tipis di atas FloatToStr. Di desktop yang pemisah desimalnya koma, laporan memuat "maxAcceptedMeanError":1,5, yang dibaca parser JSON sebagai nilai 1 disusul token liar. Field itu melaporkan error piksel terburuk yang diterima dari deduplikasi gambar perseptual, dan kini melewati PLJSONNumber(Stats.MaxAcceptedMeanError, 6). Jebakan yang tersisa adalah PLStringBuilder: di Delphi ia alias polos dari System.SysUtils.TStringBuilder, yang overload Append(Double)-nya memformat lewat locale pengguna, sedangkan build FPC memakai class milik library, jadi test di Free Pascal atau mesin en-US tak akan pernah menangkapnya

uses
  System.SysUtils, System.JSON, PDFlibrary;

var
  Lib: TPDFlib;
  Report: WideString;
  Parsed: TJSONValue;
begin
  // Reproduksi desktop Jerman atau Prancis di dalam test run
  FormatSettings.DecimalSeparator := ',';
  Lib := TPDFlib.Create;
  try
    // Pakai fixture yang benar-benar berisi gambar nyaris duplikat,
    // kalau tidak mean error-nya 0 dan bug tetap tersembunyi
    Lib.LoadFromFile('scanned-batch.pdf', '');
    // Dry run dengan threshold 2, 2, 4: dokumen tak dimodifikasi
    Lib.GetSimilarImageDeduplicationReportJSON(2, 2, 4, Report);
    Parsed := TJSONObject.ParseJSONValue(Report);
    if Parsed = nil then
      raise Exception.Create('report is not valid JSON on a comma locale');
    Parsed.Free;
  finally
    Lib.Free;
  end;
end;

Regression suite untuk output JSON butuh tiga fixture agar tetap jujur: satu halaman yang membawa -.25, +1.5, dan 007.5, satu objek yang memegang integer 400 digit, dan satu laporan apa pun yang dijalankan di locale koma, masing-masing divalidasi dengan parser ketat bukan dilihat sepintas. Object JSON, document JSON, dan laporan analisis di PDF Library for Delphi berbagi aturan angka yang sama di seluruh Delphi, C++Builder, dan Free Pascal; daftar fitur lengkapnya ada di halaman produk PDF Library for Delphi