PDFium Delphi Component menyusun XMP packet untuk output PDF/A dengan menggabungkan fragmen UTF-8 ke dalam AnsiString, dan pada Free Pascal 3.2.2 packet itu diam-diam berhenti menjadi UTF-8 yang valid ketika judul dokumen membawa karakter non-ASCII. ISO 19005-1 6.7.2 mewajibkan metadata stream berupa UTF-8 yang valid, sehingga file gagal divalidasi. Version 3.103.1 memperbaiki encoder itu sendiri, di dalam StringToUtf8. Bagian menariknya bukan patch-nya. Satu baris source yang tidak berubah menghasilkan byte yang benar di bawah Delphi, byte yang benar dalam aplikasi Lazarus LCL, dan byte korup dalam plain Free Pascal console program yang dikompilasi dari unit identik. Tiga perilaku string Free Pascal yang terpisah harus bertemu sebelum hasilnya masuk akal, dan masing-masing perilaku dapat dipertahankan secara terpisah
Mengapa metadata code yang sama menghasilkan byte berbeda di Delphi dan FPC?
Karena string bukan type yang sama pada kedua compiler. FPC 3.2.2 dalam {$MODE Delphi} mengompilasi string menjadi AnsiString yang diberi tag DefaultSystemCodePage, sedangkan Delphi mengompilasinya menjadi UnicodeString. Setiap field metadata dalam TPdfASaveOptions dideklarasikan sebagai string, sehingga Title, Author, Subject, Keywords, Creator, dan Producer membawa UTF-16 code unit pada satu compiler serta karakter single-byte plus codepage label pada compiler lain. Record sama, payload berbeda. Value itu sendiri datang dari dokumen sebagai UTF-16. TPdf.GetTitle dan sibling-nya mengembalikan WString, yang berarti WideString pada FPC dan string (UnicodeString) pada Delphi, dan SaveAsPdfAToStream mengisi setiap option field kosong dari Info dictionary sebelum menyuntikkan marker. Assignment itu adalah narrowing conversion di Free Pascal, dan RTL menjalankannya melalui target string codepage. Dalam program LCL, LazUTF8 sudah mengatur DefaultSystemCodePage ke CP_UTF8, sehingga narrowing menghasilkan UTF-8 dan semua langkah berikutnya kebetulan benar. Dalam plain console program, narrowing mendarat pada ANSI codepage, lalu StringToUtf8 menyalin octet tersebut tanpa perubahan karena mengira semuanya sudah UTF-8. Enam save bridge memiliki bentuk ini: SaveAsPdfAToStream, SaveAsPdfUaToStream, SaveAsPdfEToStream, SaveAsPdfXToStream, SaveAsPdfRToStream, dan SaveAsPdfVTToStream, masing-masing dengan option record sendiri
// PDFium.pas: accessor dokumen selalu berupa UTF-16
// WString = WideString pada FPC, = string (UnicodeString) pada Delphi
function TPdf.GetTitle: WString;
// FPdfPdfa.pas: save option record membawa metadata sebagai `string`
TPdfASaveOptions = record
Conformance: TPdfAConformance;
IccProfileData: TBytes;
Title: string; // UnicodeString pada Delphi
// AnsiString + DefaultSystemCodePage pada FPC
Author: string;
Subject: string;
Keywords: string;
Creator: string;
Producer: string;
CreationDate: string;
ModDate: string;
DocumentId: TBytes;
InstanceId: TBytes;
class function Default: TPdfASaveOptions; static;
end;
// SaveAsPdfAToStream mengisi field kosong dari Info dictionary.
// Narrowing kini dituliskan eksplisit, bukan dibiarkan implisit:
if Eff.Title = '' then
Eff.Title := WStringToStr(GetTitle);
Mengarahkan keenam bridge melalui satu helper WStringToStr tidak mengubah apa yang dilakukan RTL, tetapi menempatkan konversi di tempat yang dapat dilihat pembaca, dan menghapus 92 implicit-conversion warning yang sebelumnya menutupi kelas masalah ini. Ini adalah cermin dari korupsi sisi Delphi yang dijelaskan dalam catatan tentang pitfall cross-compiler Delphi dan FPC pada PDFium build, ketika concatenation di Delphi merusak high byte yang dipertahankan Free Pascal
Tiga perilaku Free Pascal yang mengalahkan perbaikan yang tampak jelas
Perbaikan yang tampak jelas adalah memanggil UTF8Encode lalu selesai. Pada FPC 3.2.2 dalam mode Delphi, langkah itu gagal tiga kali, dan setiap kegagalan bersifat diam-diam
var
W: UnicodeString;
S: string;
U: UTF8String;
R: RawByteString;
Xmp: AnsiString;
begin
// Trap 1: dalam mode Delphi variable UTF8String adalah AnsiString biasa,
// sehingga assignment mentranscode octet langsung kembali ke host codepage
U := UTF8Encode(W);
// Trap 2: S sudah merupakan AnsiString, sehingga UTF8Encode tidak melakukan apa-apa
R := UTF8Encode(S); // tidak ada decode, encode, atau error
R := UTF8Encode(UnicodeString(S)); // yang ini benar-benar melakukan encode
// Trap 3: concatenation menyatukan semua operand ke destination codepage,
// dan destination RawByteString bukan pengecualian
Xmp := Xmp + R;
end;
Trap pertama berarti hasil encode harus tetap berada di AnsiString atau RawByteString tempat hasil itu dibuat. Lewatkan hasilnya melalui temporary UTF8String saat keluar dan Anda sudah membatalkan pekerjaan tersebut. Trap kedua paling lama tersembunyi karena UTF8Encode(S) dapat dikompilasi, berjalan, mengembalikan value dengan panjang yang tepat, dan sama sekali tidak melakukan konversi ketika argument-nya sudah berupa AnsiString; hanya melakukan widening ke UnicodeString lebih dulu yang membuat pemanggilan itu melakukan decode. Trap ketiga menjelaskan mengapa encoder yang benar masih dapat menghasilkan dokumen rusak: BuildXmpBytes mengakumulasi packet dalam local Xmp: AnsiString, dan Free Pascal mengonversi setiap operand concatenation ke codepage variable tujuan, melipat kembali sequence multi-byte menjadi single ANSI byte saat masuk
Apa yang sebenarnya dijamin SetCodePage dengan False?
SetCodePage(RawByteString(Result), DefaultSystemCodePage, False) memberi ulang label string tanpa menyentuh satu byte pun. Parameter ketiga adalah Convert; memberikan False berarti "anggap payload sudah berada dalam target codepage dan ubah tag saja". Itu merupakan kebohongan tentang content, yang dilakukan dengan sengaja: octet tersebut sebenarnya UTF-8, tetapi memberi tag sebagai host codepage menghentikan concatenation pada trap ketiga agar tidak mengonversinya. Octet tersebut bergabung ke XMP buffer sebagai raw byte dan keluar di sisi lain tanpa perubahan
function StringToUtf8(const S: string): AnsiString;
begin
{$IFDEF UNICODE}
// Delphi: UTF8Encode sudah menghasilkan octet bertag CP_UTF8, dan
// concatenation ke AnsiString mempertahankannya
Result := AnsiString(UTF8Encode(S));
{$ELSE}
// FPC: lakukan widening lebih dulu, atau UTF8Encode no-op pada AnsiString
Result := UTF8Encode(UnicodeString(S));
// Beri label ulang tanpa transcoding agar octet bertahan saat digabungkan ke
// buffer bertag ANSI yang menyusun XMP packet dan PDF string object
if Length(Result) > 0 then
SetCodePage(RawByteString(Result), DefaultSystemCodePage, False);
{$ENDIF}
end;
Jelaskan batasnya dengan terang. Retag ini khusus FPC, dan bukan izin umum untuk mencampur string bertag dan tidak bertag. Cara ini bekerja karena hanya ada satu pola consumer setelahnya: append ke AnsiString, lalu tulis buffer sebagai byte. Apa pun yang mencoba menginterpretasikan value yang sudah di-retag sebagai teks pada host codepage akan membaca mojibake, dan itu benar. Arah sebaliknya ditangani dengan cara lain yang identik pada kedua compiler: tag incoming buffer sebagai CP_UTF8 dengan SetCodePage(..., False), lalu panggil UTF8ToString
Mengapa regression test membawa jebakan yang sama?
Karena test yang membangun expected byte dari source literal sedang menguji compiler, bukan library. Constant seperti #$C3#$A9 yang ditulis di source file Pascal membawa codepage compile-time file tersebut, dan ketika diteruskan ke parameter AnsiString, RTL meng-encode ulangnya, tepat konversi yang sedang diuji. Expectation harus disusun saat runtime, byte demi byte, lalu dibandingkan byte demi byte, karena operator = pada dua value AnsiString dengan tag berbeda mendamaikan codepage sebelum membandingkan dan mengembalikan false negative yang terlihat meyakinkan
function BytesPattern(const Values: array of Byte): AnsiString;
var
I: Integer;
begin
SetLength(Result, Length(Values));
for I := 0 to High(Values) do
Result[I + 1] := AnsiChar(Values[I]);
end;
procedure TPdfATests.StringToUtf8_AnsiCodePageString_EncodesUtf8;
var
Saved: Word;
Wide: WideString;
Narrowed: string;
Encoded, ExpectedUtf8: AnsiString;
begin
Saved := DefaultSystemCodePage;
try
SetMultiByteConversionCodePage(1252);
Wide := WideChar($0043) + WideChar($0061) + WideChar($0066) + WideChar($00E9);
Narrowed := Wide; // narrowing yang sedang diuji
Encoded := StringToUtf8(Narrowed);
finally
SetMultiByteConversionCodePage(Saved);
end;
// 'Caf' + U+00E9 sebagai UTF-8, dibuat saat runtime agar literal tidak di-encode ulang
ExpectedUtf8 := BytesPattern([$43, $61, $66, $C3, $A9]);
AssertTrue('StringToUtf8 must emit UTF-8 for a string carrying a non-UTF-8 codepage',
SameOctets(Encoded, ExpectedUtf8));
end;
Harness-nya sendiri adalah LCL program, sehingga DefaultSystemCodePage bernilai CP_UTF8 dan bug tidak terlihat sampai test menggantinya. SetMultiByteConversionCodePage(1252) di dalam try..finally mereproduksi environment plain console selama satu test. End-to-end check berjalan lebih jauh dan menegaskan kedua arah: XMP packet hasil marker injection harus memuat $43 $61 $66 $C3 $A9 dan tidak boleh memuat $43 $61 $66 $E9, sehingga regresi yang kembali menghasilkan raw single-byte output gagal dengan jelas, bukan menghasilkan file yang hanya tampak masuk akal di hex dump. Jika Anda bekerja dengan metadata non-Latin, widening discipline yang sama mengatur kasus dalam emoji dan teks CJK yang merusak WideChar handling di Delphi
Di mana lagi narrowing terjadi?
XMP adalah korban yang terlihat, tetapi setiap bridge TBytes ke string dalam codebase yang sama memiliki exposure yang sama. Dua bagian lain diperbaiki pada v3.103.1: Utf8BytesToString dan StringToUtf8Bytes dalam FPdfProduction, yang melakukan round-trip XFA datasets packet melalui string agar MergePdfXfaDatasets dapat mengganti bound value, serta BytesToUtf8 dalam FPdfTrustedList, yang mendekode European trusted-list XML setelah byte-order mark dihapus. Keduanya kini men-stage buffer dalam RawByteString, memberi tag CP_UTF8 tanpa konversi, lalu mendekode dengan UTF8ToString. Satu module sudah kebal, dan alasannya layak ditiru. XFDF writer mendeklarasikan text type sendiri sebagai XFDFString, yang menjadi WideString di bawah FPC dan UnicodeString di bawah Delphi, sehingga encoder-nya tidak pernah melihat AnsiString bertag codepage. Itulah structural fix-nya: pertahankan text dalam UTF-16 type sampai tepat di titik serialisasi, dan biarkan satu narrow function memiliki konversi ke byte. Setiap bug dalam keluarga ini berasal dari field string yang berada di tengah pipeline yang di satu ujungnya UTF-16 dan di ujung lain octet
Apa yang perlu diperiksa pada PDF dual-compiler Anda sendiri?
Jika Anda mengirim Object Pascal yang berjalan pada kedua compiler dan menulis metadata ke PDF yang conformant terhadap standard, empat pemeriksaan menemukan sebagian besar kelas masalah ini sebelum validator menemukannya
- Grep
UTF8Encodedengan argumentstring. Pada FPC pemanggilan itu no-op, dan ini adalah satu baris dengan hasil audit tertinggi - Anggap setiap variable
UTF8Stringmencurigakan dalam mode Delphi. Di sana variable itu adalahAnsiStringbiasa, dan assignment encoded byte ke dalamnya mentranscode byte tersebut kembali - Jalankan sedikitnya satu regression di bawah
SetMultiByteConversionCodePagedengan single-byte codepage. LCL test harness berjalan padaCP_UTF8dan tidak akan pernah mereproduksi plain console program - Bangun expected byte vector saat runtime dan bandingkan octet demi octet. Source literal serta
=sama-sama melewati codepage reconciliation dan akan menyembunyikan defect yang sedang Anda cari
Tidak ada yang eksotis dari trivia Free Pascal ini. Ini adalah biaya biasa dari bahasa yang mempertahankan byte-oriented string type berdampingan dengan type UTF-16, sementara kedua compiler membuat pilihan yang masuk akal tetapi berbeda tentang arti string. Konsekuensi praktis untuk PDF sempit dan tajam: metadata yang terbaca baik di IDE dapat mencapai XMP packet sebagai UTF-8 invalid, dan ISO 19005-1 6.7.2 tidak peduli compiler mana yang menaruhnya di sana. Jika Anda membangun archival pipeline, encoding layer pantas mendapat perhatian sebesar bagian lain dari workflow kepatuhan archival PDF/A yang mengelilinginya. PDFium Delphi Component mengirim konversi ini sebagai bagian dari library, sehingga SaveAsPdfA dan lima sibling standard-nya mengeluarkan metadata UTF-8 yang conformant pada Delphi, Lazarus, dan plain Free Pascal build tanpa konfigurasi codepage dari caller. Dokumentasi API lengkap dan release saat ini tersedia di halaman produk PDFium Delphi Component