Artikel Teknis

HotPDF di Free Pascal: Deflate, AES, dan Batas Codec

HotPDF terkompilasi dan berjalan di bawah Free Pascal 3.2.2 dengan Lazarus, dan ringkasan jujur atas porting itu adalah dua kalimat. Pembuatan dokumen, pemuatan, penyimpanan, kompresi, dekompresi, enkripsi, dan dekripsi semuanya bekerja pada backend khusus Pascal, sehingga aplikasi Lazarus dapat memproduksi dan mengonsumsi PDF sungguhan tanpa dependensi C. Codec gambar native opsional tidak, karena object Win64 yang sudah jadi memakai varian COFF yang tidak bisa dikonsumsi linker Free Pascal mana pun, sehingga pada toolchain itu entry point-nya terselesaikan ke stub yang gagal secara tertutup

Peta kemampuan HotPDF di Free Pascal: backend deflate, AES, dan dokumen Pascal yang bekerja di samping stub codec gambar yang gagal tertutup
Fitur dokumen, kompresi, dan enkripsi berjalan pada backend khusus Pascal, sementara codec gambar native terselesaikan ke stub yang gagal tertutup

Menempuh jalan dari "terkompilasi" ke "berfungsi" membutuhkan sekumpulan perbaikan tertentu, dan setiap satunya adalah jebakan yang akan menemukan codebase Delphi lain mana pun yang berpindah ke Free Pascal. Semuanya layak dicatat dalam urutan kapan mereka menyakitkan

Mengapa unit yang terkompilasi tidak membuktikan apa pun?

Karena unit Pascal bisa mereferensikan simbol yang tidak akan pernah melakukan apa pun yang berguna dan tetap memuaskan kompiler. Pada titik ketika seluruh 113 unit pustaka terbangun bersih di Free Pascal, handler kontainer arsip benar-benar bekerja, diverifikasi oleh smoke test yang membuka CBZ dan mengonversinya ke PDF. Perataan form XFA sama sekali tidak bekerja, karena perataan harus meng-inflate stream paket /XFA yang terkompresi dan entry point deflate masih berupa stub. Tidak ada apa pun di output build yang membedakan kedua kasus itu

Aturan yang lahir dari sana pendek. Sebelum menulis di release note bahwa sebuah fitur bekerja pada toolchain baru, tulis probe runtime yang menjalankan fitur itu end to end pada toolchain tersebut. Cakupan kompilasi adalah prasyarat, bukan bukti. Gambaran lebih luas tentang apa yang dicakup porting ini ada di catatan dukungan Win64 Free Pascal dan Lazarus

Raise di dalam stub cdecl tidak sampai ke caller

Yang ini layak mendapat bagian sendiri karena gejalanya begitu menyesatkan. Unit stub memaparkan entry point C sebagaimana pustaka statis melakukannya, sehingga sebuah stub tampak seperti ini

// Tampak masuk akal. Tidak.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

Di Free Pascal untuk Win64 exception itu tidak merambat ke caller. Tidak ada handler try..except yang melihatnya, karena unwinding melintasi batas cdecl yang dideklarasikan dengan cara ini tidak membawa frame exception Pascal; proses berakhir dengan exit code 217. Dari sisi aplikasi tidak ada error, tidak ada pesan, tidak ada baris log, hanya program yang lenyap. Itu secara mutlak lebih buruk daripada jawaban yang salah, karena jawaban yang salah bisa ditangani

Mengapa exception yang dilempar di dalam stub cdecl mengakhiri proses Free Pascal dengan exit code 217 dan bagaimana gating entry point Pascal memperbaikinya
Frame exception Pascal tidak bisa melakukan unwinding melintasi batas cdecl, sehingga proses mati diam-diam; perbaikannya melakukan gating sebelum stub tersentuh

Perbaikan yang menggoda adalah membuat stub mengembalikan kode kegagalan, dan untuk inflate itu benar karena zlib punya return error yang terdefinisi baik. Secara umum itu keliru: stub untuk jpeg_read_header yang mengembalikan nol menyuruh caller melanjutkan dengan struktur yang tidak diinisialisasi siapa pun. Perbaikan yang tahan lama adalah melakukan gating di entry point Pascal alih-alih di dalam stub berbentuk C, memakai konvensi kegagalan apa pun yang sudah dimiliki API itu

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Menolak sebelum stub tersentuh, dengan konvensi kegagalan milik
  // API ini sendiri alih-alih exception melintasi cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib bukan zlib, dan perbedaannya adalah dua kelas dokumen

Implementasi deflate Pascal yang tersedia di Free Pascal menangani dua framing: wrapper zlib dan deflate mentah. Ia tidak menangani framing gzip, yang dipilih zlib melalui nilai windowBits 16 sampai 31, dan tidak menangani mode deteksi otomatis yang dipilih nilai 32 sampai 47. HotPDF membutuhkan keduanya. Jalur impor SVG yang aman meminta 31, dan loader punya tangga fallback yang meminta 47 ketika framing sebuah stream ambigu. Melewatkan salah satunya dan satu keluarga dokumen penuh berhenti terbuka, dengan decode error yang menunjuk ke stream alih-alih ke framing yang hilang

Cakupan windowBits paszlib versus zlib: rentang framing gzip dan auto-detect yang hilang untuk impor SVG HotPDF dan fallback loader
paszlib menangani wrapper zlib dan deflate mentah, tetapi HotPDF juga membutuhkan windowBits 31 dan 47, sehingga shim harus menyediakan framing gzip sendiri

Ada ketidakcocokan kedua yang lebih tajam. Record z_stream yang dideklarasikan paszlib tidak punya layout memori yang sama dengan versi C: field msg-nya short string alih-alih pointer, dan total_in serta total_out berukuran 64-bit di mana ABI C memakai machine word. Record caller karenanya tidak bisa diteruskan begitu saja. Susunan yang bekerja adalah menyimpan status paszlib di belakang pointer state yang sudah dicadangkan record publik, dan menyalin field publik masuk dan keluar di sekitar setiap panggilan. CRC gzip dan trailer panjang delapan byte diperhitungkan di lapisan shim yang sama, yang merupakan tempat alaminya karena lapisan itu sudah memiliki keputusan framing

Meneruskan dynamic array ke parameter var tanpa tipe

Inilah bug yang paling mungkin sedang duduk di kode Anda saat ini. Ketika Anda meneruskan dynamic array ke parameter var tanpa tipe, yang diterima callee adalah alamat variabel array, yaitu alamat sebuah pointer, bukan alamat payload-nya. Jadi pembacaan ke dalamnya menimpa variabel itu sendiri dan apa pun yang berada di sebelahnya

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // Keliru: menyerahkan alamat variabel FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // Benar: menyerahkan alamat byte payload pertama
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

Di Delphi bentuk yang keliru sering tampak bekerja, karena yang dikorupsinya adalah slot stack tetangga yang tidak dibaca siapa pun setelahnya. Di Free Pascal baris yang sama segmentation fault pada pemakaian pertama. Yang membuatnya sulit ditangkap mata adalah static array tidak punya masalah semacam itu, karena variabel static array adalah payload-nya sendiri, sehingga kedua penulisan benar di berkas yang sama tergantung deklarasi beberapa ratus baris jauhnya

Kontainer ZIP tanpa System.Zip

Free Pascal tidak punya padanan unit zip RTL, dan alternatif yang tersedia punya permukaan API yang berbeda sekaligus tidak mendukung enkripsi legacy yang masih dipakai format kontainer lama, sehingga reader kecil di dalam pustaka ternyata lebih pendek daripada menyesuaikan diri kepadanya. Dua detail format memakan waktu dan mudah keliru

Yang pertama adalah byte pemeriksa header enkripsi. Byte keduabelas biasanya byte tinggi CRC, tetapi ketika bit 3 flag general-purpose di-set, yang berarti ukuran-ukuran berada di trailing data descriptor dan CRC belum diketahui, byte pemeriksa datang dari byte tinggi waktu modifikasi. Mengimplementasikan hanya bentuk CRC membuat setiap arsip yang ditulis dalam mode streaming menolak password yang benar. Yang kedua adalah extra field ZIP64: ketiga field 64-bit-nya muncul dalam urutan tetap tetapi hanya ditulis ketika field 32-bit yang bersangkutan jenuh, sehingga membacanya pada offset tetap bekerja pada arsip yang Anda uji dan gagal pada arsip berikutnya. Parse posisi mereka terhadap field 32-bit mana yang jenuh

Satu kemudahan yang layak diketahui: stream dekompresi Free Pascal menerima argumen constructor kedua yang melewati header zlib, yang persis dibutuhkan entri ZIP karena entri itu menyimpan deflate mentah. Jalur itu sama sekali tidak menyentuh shim zlib pustaka, sehingga tidak terpengaruh oleh backend C yang hilang

Transparansi glyph berwarna di bawah LCL

Membaca channel alpha dari glyph berwarna yang dirasterisasi adalah satu detail grafik tanpa terjemahan langsung. Kelas PNG LCL tidak punya accessor scanline yang memaparkan alpha, dan meng-assign PNG ke bitmap membuangnya, sehingga emoji berwarna datang sepenuhnya opak dan terkomposit dengan kotak hitam di belakangnya. Jalur yang bekerja adalah gambar interface: buat dari PNG, lalu baca piksel melalui accessor warna, dengan mengingat bahwa komponennya 16-bit dan perlu digeser turun delapan untuk menjadi byte. Permukaan itu juga memakai urutan baris top-down yang natural, sehingga inversi Height - 1 - Y yang dibutuhkan kode scanline VCL harus dihapus, bukan di-porting

Dua catatan build-system sebelum Anda melaporkan bug

Rebuild penuh kadang gagal dengan simbol tak terdefinisi yang namanya berakhiran suffix $crc dan nilai hex. Suffix itu dihitung dari tipe parameter, dan gagal cocok ketika satu build mengompilasi sebuah unit terhadap dua versi interface berbeda dalam lintasan yang sama. Menjalankan ulang build menghapusnya; signature-nya tidak salah

Kedua, Free Pascal 3.2.2 tidak punya anonymous methods, sehingga di mana pun pustaka memakai closure untuk menghubungkan pipeline paralel, build Free Pascal mengambil fallback serial yang deterministik sebagai gantinya. Output identik, throughput tidak; jika Anda bergantung pada rendering halaman paralel, itu alasan untuk tetap di Delphi untuk saat ini, dan desain pipeline-nya dijelaskan dalam artikel pipeline render paralel. Situasi codec gambar adalah tempat lain di mana pilihan toolchain mengubah kemampuan, bukan hanya kecepatan, sehingga deployment Lazarus sebaiknya merencanakan format gambarnya sesuai itu; matriks per toolchain saat ini ada di halaman produk HotPDF Delphi PDF component