PDFium Delphi Component bygger XMP-paketet för PDF/A-utdata genom att sammanfoga UTF-8-fragment till en AnsiString, och i Free Pascal 3.2.2 slutade paketet tyst att vara giltig UTF-8 i samma ögonblick som en dokumenttitel bar ett icke-ASCII-tecken. ISO 19005-1 6.7.2 kräver att metadataströmmen är giltig UTF-8, så filen misslyckades med valideringen. Version 3.103.1 rättar själva kodaren i StringToUtf8. Det intressanta är inte patchen. Det är att en oförändrad källrad gav korrekta byte under Delphi, korrekta byte i en Lazarus LCL-applikation och korrupta byte i ett vanligt Free Pascal-konsolprogram kompilerat från exakt samma enhet. Tre separata Free Pascal-beteenden måste falla på plats innan det blir begripligt, och vart och ett är försvarbart på egen hand
Varför avger samma metadatakod olika byte i Delphi och FPC?
För att string inte är samma typ i de två kompilatorerna. FPC 3.2.2 i {$MODE Delphi} kompilerar string till en AnsiString märkt med DefaultSystemCodePage, medan Delphi kompilerar den till UnicodeString. Varje metadatafält i TPdfASaveOptions deklareras som string, så Title, Author, Subject, Keywords, Creator och Producer bär UTF-16-kodenheter i den ena kompilatorn och enbyte-tecken plus en kodsidetikett i den andra. Samma post, samma fält, olika nyttolast. Värdena kommer själva från dokumentet som UTF-16. TPdf.GetTitle och dess syskon returnerar WString, som är WideString i FPC och string i Delphi, och SaveAsPdfAToStream fyller varje tomt alternativfält från Info-ordboken innan markörer injiceras. Den tilldelningen är en förträngande omvandling i Free Pascal, och RTL utför den genom målets strängkodning. I ett LCL-program har LazUTF8 redan satt DefaultSystemCodePage till CP_UTF8, så förträngningen ger UTF-8 och allt nedströms råkar bli korrekt. I ett vanligt konsolprogram landar samma förträngning på ANSI-kodningen, och StringToUtf8 kopierade sedan oktetterna oförändrade eftersom den antog att de redan var UTF-8. Sex sparbryggor delar denna form: SaveAsPdfAToStream, SaveAsPdfUaToStream, SaveAsPdfEToStream, SaveAsPdfXToStream, SaveAsPdfRToStream och SaveAsPdfVTToStream, var och en med sin egen alternativpost
// PDFium.pas: dokumentaccessorerna är alltid UTF-16
// WString = WideString i FPC, = string (UnicodeString) i Delphi
function TPdf.GetTitle: WString;
// FPdfPdfa.pas: sparalternativsposten bär metadata som `string`
TPdfASaveOptions = record
Conformance: TPdfAConformance;
IccProfileData: TBytes;
Title: string; // UnicodeString i Delphi
// AnsiString + DefaultSystemCodePage i 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 fyller tomma fält från Info-ordboken.
// Förträngningen är nu utskriven i stället för att lämnas implicit:
if Eff.Title = '' then
Eff.Title := WStringToStr(GetTitle);
Att skicka alla sex bryggorna genom en enda hjälpare WStringToStr ändrar inte vad RTL gör, men placerar omvandlingen där en läsare kan se den och rensade 92 varningar om implicita omvandlingar som hade dolt exakt denna typ av problem. Det är spegelbilden av den Delphi-sidiga korruptionen som beskrivs i våra anteckningar om korskompilatorfällor i Delphi och FPC för PDFium-byggen, där en sammanfogning i Delphi förstör en högbyte som Free Pascal bevarar
Tre Free Pascal-beteenden som besegrar den uppenbara lösningen
Den uppenbara lösningen är att anropa UTF8Encode och vara klar. Det misslyckas tre gånger om i FPC 3.2.2 i Delphi-läge, och varje misslyckande är tyst
var
W: UnicodeString;
S: string;
U: UTF8String;
R: RawByteString;
Xmp: AnsiString;
begin
// Fälla 1: i Delphi-läge är en UTF8String- *variabel* en vanlig AnsiString,
// så tilldelningen transkodar oktetterna direkt tillbaka till värddatorns kodsida
U := UTF8Encode(W);
// Fälla 2: S är redan en AnsiString, så UTF8Encode gör ingenting alls
R := UTF8Encode(S); // ingen avkodning, ingen kodning, inget fel
R := UTF8Encode(UnicodeString(S)); // den här kodar faktiskt
// Fälla 3: sammanfogning förenar varje operand till destinationskodningen,
// och en RawByteString-destination är inget undantag
Xmp := Xmp + R;
end;
Fälla ett betyder att det kodade resultatet måste förbli i den AnsiString eller RawByteString det skapades i. Skicka det genom en tillfällig UTF8String på vägen ut och du har gjort arbetet ogjort. Fälla två är den som gömmer sig längst, eftersom UTF8Encode(S) kompileras, körs, returnerar ett värde med rätt längd och inte utför någon omvandling alls när argumentet redan är en AnsiString; först när du breddar till UnicodeString avkodas något. Fälla tre är varför en korrekt encoder ändå kan producera ett trasigt dokument: BuildXmpBytes samlar paketet i en lokal Xmp: AnsiString, och Free Pascal omvandlar varje operand i en sammanfogning till destinationsvariabelns kodsida, vilket viker ner multibytesekvenserna till enstaka ANSI-byte på vägen in
Vad garanterar SetCodePage med False egentligen?
SetCodePage(RawByteString(Result), DefaultSystemCodePage, False) märker om strängen utan att röra en enda byte. Den tredje parametern är Convert; att skicka False betyder "anta att nyttolasten redan finns i målkodningen och ändra bara etiketten". Det är en lögn om innehållet, berättad avsiktligt: oktetterna är verkligen UTF-8, men att märka dem som värddatorns kodsida är det som hindrar sammanfogningen i fälla tre från att omvandla dem. De fogas in i XMP-bufferten som råa byte och kommer ut på andra sidan oförändrade
function StringToUtf8(const S: string): AnsiString;
begin
{$IFDEF UNICODE}
// Delphi: UTF8Encode ger redan CP_UTF8-märkta oktetter, och
// sammanfogning till en AnsiString behåller dem
Result := AnsiString(UTF8Encode(S));
{$ELSE}
// FPC: bredda först, annars är UTF8Encode en no-op på ett AnsiString-argument
Result := UTF8Encode(UnicodeString(S));
// Märk om utan transkodning så att oktetterna överlever sammanfogning till
// de ANSI-märkta buffertar som bygger XMP-paket och PDF-strängobjekt
if Length(Result) > 0 then
SetCodePage(RawByteString(Result), DefaultSystemCodePage, False);
{$ENDIF}
end;
Var tydlig med gränsen. Ommärkningen är FPC-only och inte en allmän välsignelse för att blanda märkta och omärkta strängar. Den fungerar här eftersom exakt ett konsumentmönster finns nedströms: lägg till i en AnsiString och skriv sedan ut bufferten som byte. Allt som försökte tolka det ommärkta värdet som text i värddatorns kodsida skulle läsa mojibake, och det med rätta. Den omvända riktningen hanteras på andra hållet och är identisk på båda kompilatorerna: märk den inkommande bufferten med CP_UTF8 genom SetCodePage(..., False) och anropa sedan UTF8ToString
Varför bar regressionstesterna samma fälla?
För att ett test som bygger sina förväntade byte från en källliteral testar kompilatorn, inte biblioteket. En konstant som #$C3#$A9 i en Pascal-källfil bär filens kodsida vid kompilering, och när den skickas till en AnsiString-parameter kodar RTL om den, vilket är exakt den omvandling som testas. Förväntningen måste sättas ihop vid körning, byte för byte, och jämföras byte för byte, eftersom = mellan två AnsiString-värden med olika etiketter försonar kodsidorna före jämförelsen och returnerar ett glatt falskt negativt
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; // förträngningen som testas
Encoded := StringToUtf8(Narrowed);
finally
SetMultiByteConversionCodePage(Saved);
end;
// 'Caf' + U+00E9 som UTF-8, byggd vid körning så att ingen literal kan kodas om
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;
Testselen är ett LCL-program, så DefaultSystemCodePage är CP_UTF8 och felet är osynligt tills testet byter den. SetMultiByteConversionCodePage(1252) inne i ett try..finally återskapar konsolmiljön för en enda testkörning. End-to-end-kontrollen går längre och hävdar båda riktningarna: XMP-paketet som skapas genom markörinjektion måste innehålla $43 $61 $66 $C3 $A9 och får inte innehålla $43 $61 $66 $E9, så en framtida regression som återgår till råa enbyteutdata misslyckas högljutt i stället för att producera en fil som bara ser rimlig ut i en hexadump. Om du arbetar med metadata som inte är latinsk styr samma breddningsdisciplin fallen i emoji och CJK-text som bryter WideChar-hantering i Delphi
Var landar förträngningen annars?
XMP är det synliga offret, men varje brygga från TBytes till string i samma kodbas hade samma exponering. Två till rättades i v3.103.1: Utf8BytesToString och StringToUtf8Bytes i FPdfProduction, som gör en rundtur med XFA-datasetpaketet genom en sträng så att MergePdfXfaDatasets kan ersätta bundna värden, samt BytesToUtf8 i FPdfTrustedList, som avkodar XML för europeiska betrodda listor efter att byteordningsmarkören tagits bort. Båda mellanlagrar nu bufferten i en RawByteString, märker den som CP_UTF8 utan att konvertera och avkodar med UTF8ToString. En modul var redan immun, och orsaken är värd att kopiera. XFDF-skrivaren deklarerar sin egen texttyp som XFDFString, som löser till WideString i FPC och UnicodeString i Delphi, så dess encoder ser aldrig en kodsidmärkt AnsiString. Det är den strukturella lösningen: håll texten i en UTF-16-typ fram till den exakta serialiseringspunkten och låt en enda smal funktion äga omvandlingen till byte. Varje fel i denna familj kom från ett string-fält mitt i en pipeline som annars hade UTF-16 i ena änden och oktetter i den andra
Vad ska du kontrollera i din egen PDF-kod med två kompilatorer?
Om du levererar Object Pascal som körs på båda kompilatorerna och skriver metadata till en standardkonform PDF hittar fyra kontroller de flesta av denna klass innan en validator gör det
- Grep:a efter
UTF8Encodemed ettstring-argument. I FPC är det anropet en no-op, och det är den enskilt mest givande raden att granska - Behandla varje
UTF8String-variabel som misstänkt i Delphi-läge. Där är den en vanligAnsiString, och tilldelning av kodade byte till en sådan transkodar dem tillbaka - Kör minst en regression under
SetMultiByteConversionCodePagemed en enbytekodning. En LCL-testsele kör medCP_UTF8och återskapar aldrig ett vanligt konsolprogram - Bygg förväntade bytevektorer vid körning och jämför dem oktett för oktett. Källliteraler och
=går båda genom kodsidesförsoning och döljer felet du letar efter
Inget av detta är exotisk Free Pascal-trivia. Det är den vanliga kostnaden för ett språk som höll en byteorienterad strängtyp vid liv bredvid en UTF-16-typ, och de två kompilatorerna gjorde rimliga men olika val om vad string skulle betyda. Den praktiska följden för PDF-arbete är smal och skarp: metadata som ser rätt ut i IDE:n kan nå XMP-paketet som ogiltig UTF-8, och ISO 19005-1 6.7.2 bryr sig inte om vilken kompilator som placerade den där. Om du bygger en arkiveringspipeline förtjänar kodningslagret lika mycket uppmärksamhet som resten av arbetsflödet för PDF/A-arkivkompatibilitet runt det. PDFium Delphi Component levererar dessa omvandlingar som en del av biblioteket, så SaveAsPdfA och dess fem syskon för standarderna avger konform UTF-8-metadata i Delphi, Lazarus och vanliga Free Pascal-byggen utan någon kodsidekonfiguration från anroparen. Full API-dokumentation och aktuell version finns på produktsidan för PDFium Delphi Component