PDFium Delphi Component bygger XMP-pakken for PDF/A-output ved å sette sammen UTF-8-fragmenter i en AnsiString, og på Free Pascal 3.2.2 sluttet pakken stille å være gyldig UTF-8 i det øyeblikket en dokumenttittel fikk et ikke-ASCII-tegn. ISO 19005-1 6.7.2 krever at metadatakstrømmen er gyldig UTF-8, så filen feilet valideringen. Versjon 3.103.1 retter selve koderen i StringToUtf8. Det interessante er ikke patchen. Det er at én uendret kodelinje produserte riktige byte under Delphi, riktige byte i en Lazarus LCL-applikasjon og korrupte byte i et rent Free Pascal-konsollprogram kompilert fra den identiske enheten. Tre separate Free Pascal-strengoppførsler må falle på plass før dette gir mening, og hver av dem er forsvarlig alene
Hvorfor sender den samme metadatakoden forskjellige byte på Delphi og FPC?
Fordi string ikke er samme type på de to kompilatorene. FPC 3.2.2 i {$MODE Delphi} kompilerer string til en AnsiString merket med DefaultSystemCodePage, mens Delphi kompilerer den til UnicodeString. Hvert metadatafelt i TPdfASaveOptions er deklarert som string, så Title, Author, Subject, Keywords, Creator og Producer bærer UTF-16-kodenheter på den ene kompilatoren og enkeltbyte-tegn pluss en codepage-etikett på den andre. Samme record, samme felt, forskjellig payload. Selve verdiene kommer fra dokumentet som UTF-16. TPdf.GetTitle og søsknene returnerer WString, som er WideString på FPC og string på Delphi, og SaveAsPdfAToStream fyller alle blanke option-felter fra Info-ordboken før markører injiseres. Den tildelingen er en innsnevringskonvertering på Free Pascal, og RTL-en utfører den gjennom målstrengens codepage. I et LCL-program har LazUTF8 allerede satt DefaultSystemCodePage til CP_UTF8, så innsnevringen produserer UTF-8, og alt nedstrøms blir tilfeldigvis riktig. I et rent konsollprogram lander den samme innsnevringen på ANSI-codepage, og StringToUtf8 kopierte deretter oktettene uendret fordi den antok at de allerede var UTF-8. Seks lagringsbroer deler denne formen: SaveAsPdfAToStream, SaveAsPdfUaToStream, SaveAsPdfEToStream, SaveAsPdfXToStream, SaveAsPdfRToStream og SaveAsPdfVTToStream, hver med sin egen option-record
// PDFium.pas: dokumentaksessorene er alltid UTF-16
// WString = WideString på FPC, = string (UnicodeString) på Delphi
function TPdf.GetTitle: WString;
// FPdfPdfa.pas: save-option-recorden bærer metadata som `string`
TPdfASaveOptions = record
Conformance: TPdfAConformance;
IccProfileData: TBytes;
Title: string; // UnicodeString på Delphi
// AnsiString + DefaultSystemCodePage på 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 blanke felt på nytt fra Info-ordboken.
// Innsnevringen er nå skrevet eksplisitt i stedet for å være implisitt:
if Eff.Title = '' then
Eff.Title := WStringToStr(GetTitle);
Å rute alle seks broene gjennom én WStringToStr-hjelper endrer ikke hva RTL-en gjør, men plasserer konverteringen der en leser kan se den, og fjernet 92 advarsler om implisitte konverteringer som hadde maskert nøyaktig denne typen problem. Dette er speilbildet av korrupsjonen på Delphi-siden som beskrives i notatene våre om Delphi- og FPC-krysskompilatorfeller i PDFium-bygg, der en sammenkjedning på Delphi ødelegger en høy byte som Free Pascal bevarer
Tre Free Pascal-oppførsler som beseirer den åpenbare løsningen
Den åpenbare løsningen er å kalle UTF8Encode og være ferdig. Det feiler tre ganger på FPC 3.2.2 i mode Delphi, og hver feil er stille
var
W: UnicodeString;
S: string;
U: UTF8String;
R: RawByteString;
Xmp: AnsiString;
begin
// Felle 1: i mode Delphi er en UTF8String-*variabel* en vanlig AnsiString,
// så tildelingen transkoder oktettene rett tilbake til vertens codepage
U := UTF8Encode(W);
// Felle 2: S er allerede en AnsiString, så UTF8Encode gjør ingenting
R := UTF8Encode(S); // ingen dekoding, ingen koding, ingen feil
R := UTF8Encode(UnicodeString(S)); // denne koder faktisk
// Felle 3: sammenkjedning samler hver operand til målcodepage,
// og en RawByteString-destinasjon er ikke et unntak
Xmp := Xmp + R;
end;
Felle én betyr at det kodede resultatet må bli værende i AnsiString- eller RawByteString-typen det ble produsert i. Send det gjennom en UTF8String-midlertidig på vei ut, og du har gjort arbeidet om igjen. Felle to er den som skjuler seg lengst, fordi UTF8Encode(S) kompileres, kjører, returnerer en verdi med riktig lengde og ikke utfører noen konvertering overhodet når argumentet allerede er en AnsiString; bare en utvidelse til UnicodeString først gjør at kallet dekoder noe. Felle tre er grunnen til at en korrekt encoder fortsatt kan produsere et ødelagt dokument: BuildXmpBytes samler pakken i en lokal Xmp: AnsiString, og Free Pascal konverterer hver operand i en sammenkjedning til målvariabelens codepage og folder flerbyte-sekvensene tilbake til enkeltbyte ANSI på veien inn
Hva garanterer SetCodePage med False egentlig?
SetCodePage(RawByteString(Result), DefaultSystemCodePage, False) merker strengen på nytt uten å røre en eneste byte. Den tredje parameteren er Convert; å sende False betyr "anta at payloaden allerede er i målcodepage og bare endre merkelappen". Det er en løgn om innholdet, fortalt med hensikt: Oktettene er egentlig UTF-8, men å merke dem som vertens codepage er det som hindrer sammenkjedningen i felle tre fra å konvertere dem. De føyes til XMP-bufferen som rå byte og kommer uendret ut på den andre siden
function StringToUtf8(const S: string): AnsiString;
begin
{$IFDEF UNICODE}
// Delphi: UTF8Encode gir allerede CP_UTF8-merkede oktetter, og
// sammenkjedning til en AnsiString beholder dem
Result := AnsiString(UTF8Encode(S));
{$ELSE}
// FPC: utvid først, ellers er UTF8Encode no-op på et AnsiString-argument
Result := UTF8Encode(UnicodeString(S));
// Merk om uten transkoding, slik at oktettene overlever sammenkjedning til
// ANSI-merkede buffere som bygger XMP-pakker og PDF-strengobjekter
if Length(Result) > 0 then
SetCodePage(RawByteString(Result), DefaultSystemCodePage, False);
{$ENDIF}
end;
Vær tydelig på grensen. Ommerkingen er FPC-only, og den er ikke en generell tillatelse til å blande merkede og umerkede strenger. Den fungerer her fordi nøyaktig ett konsumentmønster finnes nedstrøms: Legg til i en AnsiString, og skriv deretter bufferet ut som byte. Alt som forsøkte å tolke den ommerkede verdien som tekst på vertens codepage, ville lese mojibake, og med rette. Den motsatte retningen håndteres på den andre måten og er identisk på begge kompilatorer: Merk den innkommende bufferen som CP_UTF8 med SetCodePage(..., False), og kall deretter UTF8ToString
Hvorfor hadde regresjonstestene den samme fellen?
Fordi en test som bygger forventede byte fra en kildeliteral, tester kompilatoren, ikke biblioteket. En konstant som #$C3#$A9 skrevet i en Pascal-kildefil, bærer kildefilens kompileringstidscodepage, og når den sendes til en AnsiString-parameter, koder RTL-en den på nytt, som er nøyaktig konverteringen som testes. Forventningen må bygges ved kjøring, byte for byte, og sammenlignes byte for byte, fordi = på to AnsiString-verdier med forskjellige merkelapper avstemmer codepage-ene før sammenligning og returnerer et muntert falskt negativt resultat
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; // innsnevringen som testes
Encoded := StringToUtf8(Narrowed);
finally
SetMultiByteConversionCodePage(Saved);
end;
// 'Caf' + U+00E9 som UTF-8, bygget ved kjøring slik at ingen literal kan kodes på nytt
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;
Selve harnessen er et LCL-program, så DefaultSystemCodePage er CP_UTF8 og feilen er usynlig til testen bytter den. SetMultiByteConversionCodePage(1252) inne i en try..finally gjenskaper det rene konsollmiljøet for varigheten av én test. End-to-end-kontrollen går lenger og hevder begge retninger: XMP-pakken som produseres ved markørinjeksjon, må inneholde $43 $61 $66 $C3 $A9 og må ikke inneholde $43 $61 $66 $E9, slik at en fremtidig regresjon som går tilbake til rå enkeltbyte-output, feiler tydelig i stedet for å produsere en fil som bare ser plausibel ut i en hex-dump. Hvis du arbeider med ikke-latinsk metadata, styrer den samme utvidelsesdisiplinen tilfellene i emoji og CJK-tekst som ødelegger WideChar-håndtering i Delphi
Hvor ellers havner innsnevringen?
XMP er den synlige skadelidte, men enhver TBytes-til-string-bro i den samme kodebasen hadde samme eksponering. To til ble rettet i v3.103.1: Utf8BytesToString og StringToUtf8Bytes i FPdfProduction, som går tur-retur med XFA-dataset-pakken gjennom en streng slik at MergePdfXfaDatasets kan erstatte bundne verdier, og BytesToUtf8 i FPdfTrustedList, som dekoder XML for europeiske tillitslister etter å ha fjernet byteordremarkøren. Begge legger nå bufferet i en RawByteString, merker det som CP_UTF8 uten å konvertere og dekoder med UTF8ToString. Én modul var allerede immun, og grunnen er verdt å kopiere. XFDF-skriveren deklarerer sin egen teksttype som XFDFString, som løser seg til WideString under FPC og UnicodeString under Delphi, så koderen ser aldri en codepage-merket AnsiString. Det er den strukturelle løsningen: Hold tekst i en UTF-16-type frem til det nøyaktige serialiseringstidspunktet, og la én smal funksjon eie konverteringen til byte. Hver feil i denne familien kom fra et string-felt midt i en pipeline som ellers var UTF-16 i den ene enden og oktetter i den andre
Hva bør du kontrollere i din egen PDF-kode med to kompilatorer?
Hvis du leverer Object Pascal som kjører på begge kompilatorene og skriver metadata inn i en standardkonform PDF, finner fire kontroller de fleste av denne typen før en validator gjør det
- Grep etter
UTF8Encodemed etstring-argument. På FPC er det kallet en no-op, og det er linjen med høyest treffrate å revidere - Behandle enhver
UTF8String-variabel som mistenkelig i mode Delphi. Den er en vanligAnsiStringder, og å tildele kodede byte til den transkoder dem tilbake - Kjør minst én regresjon under
SetMultiByteConversionCodePagemed en enkeltbyte-codepage. Et LCL-testharness kjører medCP_UTF8og gjenskaper aldri et rent konsollprogram - Bygg forventede bytevektorer ved kjøring og sammenlign dem oktett for oktett. Både kildeliteraler og
=går gjennom codepage-avstemming og vil skjule feilen du leter etter
Ingenting av dette er eksotisk Free Pascal-trivia. Det er den ordinære kostnaden ved et språk som holdt en byteorientert strengtype i live ved siden av en UTF-16-type, og de to kompilatorene tok rimelige, men forskjellige valg om hva string skulle bety. Den praktiske konsekvensen for PDF-arbeid er smal og skarp: Metadata som ser riktig ut i IDE-en din, kan nå XMP-pakken som ugyldig UTF-8, og ISO 19005-1 6.7.2 bryr seg ikke om hvilken kompilator som la den der. Hvis du bygger en arkiveringspipeline, fortjener kodelaget for koding like mye oppmerksomhet som resten av arbeidsflyten for PDF/A-arkivsamsvar rundt det. PDFium Delphi Component leverer disse konverteringene som del av biblioteket, så SaveAsPdfA og de fem standardsøsknene skriver UTF-8-metadata som oppfyller standarden på Delphi, Lazarus og rene Free Pascal-bygg uten codepage-konfigurasjon fra caller-en. Full API-dokumentasjon og gjeldende release finnes på produktsiden for PDFium Delphi Component