Komponenta PDFium Delphi za izhod PDF/A sestavi paket XMP tako, da fragmente UTF-8 združi v AnsiString, in v Free Pascalu 3.2.2 je ta paket tiho prenehal biti veljaven UTF-8 v trenutku, ko je naslov dokumenta vseboval ne-ASCII znak. ISO 19005-1 6.7.2 zahteva, da je tok metapodatkov veljaven UTF-8, zato je datoteka padla pri preverjanju. Različica 3.103.1 popravi sam kodirnik v StringToUtf8. Zanimiv del ni popravek. Ena nespremenjena izvorna vrstica je pod Delphijem ustvarila pravilne bajte, v aplikaciji Lazarus LCL pravilne bajte in v navadnem konzolnem programu Free Pascal, prevedenem iz iste enote, pokvarjene bajte. Preden je to smiselno, se morajo poravnati tri ločena vedenja nizov Free Pascala, in vsako je samo zase zagovarljivo
Zakaj ista koda metapodatkov na Delphiju in FPC izda različne bajte?
Ker string pri obeh prevajalnikih ni isti tip. FPC 3.2.2 v {$MODE Delphi} prevede string v AnsiString, označen z DefaultSystemCodePage, Delphi pa v UnicodeString. Vsako polje metapodatkov v TPdfASaveOptions je deklarirano kot string, zato Title, Author, Subject, Keywords, Creator in Producer na enem prevajalniku nosijo kodne enote UTF-16, na drugem pa enobajtne znake in oznako kodne strani. Isti zapis, isto polje, drugačen tovor. Vrednosti same pridejo iz dokumenta kot UTF-16. TPdf.GetTitle in njegovi sorodniki vrnejo WString, ki je na FPC WideString, na Delphiju pa string, SaveAsPdfAToStream pa vsako prazno polje možnosti napolni iz slovarja Info, preden vstavi oznake. Ta prireditev je na Free Pascalu zožitvena pretvorba, RTL pa jo izvede prek ciljne kodne strani niza. V programu LCL je LazUTF8 že nastavil DefaultSystemCodePage na CP_UTF8, zato zožitev ustvari UTF-8 in je vse za njo po naključju pravilno. V navadnem konzolnem programu ista zožitev pristane na kodni strani ANSI, StringToUtf8 pa te oktete nato kopira nespremenjene, ker je predpostavil, da so že UTF-8. Šest mostov za shranjevanje ima isto obliko: SaveAsPdfAToStream, SaveAsPdfUaToStream, SaveAsPdfEToStream, SaveAsPdfXToStream, SaveAsPdfRToStream in SaveAsPdfVTToStream, vsak s svojim zapisom možnosti
// PDFium.pas: dostopi do dokumenta so vedno UTF-16
// WString = WideString na FPC, = string (UnicodeString) na Delphiju
function TPdf.GetTitle: WString;
// FPdfPdfa.pas: zapis možnosti shranjuje metapodatke kot `string`
TPdfASaveOptions = record
Conformance: TPdfAConformance;
IccProfileData: TBytes;
Title: string; // UnicodeString na Delphiju
// AnsiString + DefaultSystemCodePage na 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 napolni prazna polja iz slovarja Info.
// Zožitev je zdaj zapisana namesto prepuščena implicitni pretvorbi:
if Eff.Title = '' then
Eff.Title := WStringToStr(GetTitle);
Vodenje vseh šestih mostov skozi en pomočnik WStringToStr ne spremeni tega, kar naredi RTL, vendar pretvorbo postavi tja, kjer jo bralec vidi, in odstrani 92 opozoril implicitnih pretvorb, ki so zakrivala prav ta razred težave. To je zrcalna slika korupcije na strani Delphija, opisane v naših zapiskih o pasteh večprevajalskih gradenj Delphi in FPC v PDFiumu, kjer združevanje na Delphiju uniči visoki bajt, ki ga Free Pascal ohrani
Tri vedenja Free Pascala, ki premagajo očiten popravek
Očiten popravek je poklicati UTF8Encode in končati. To na FPC 3.2.2 v načinu Delphi odpove trikrat, vsakič tiho
var
W: UnicodeString;
S: string;
U: UTF8String;
R: RawByteString;
Xmp: AnsiString;
begin
// Past 1: v načinu Delphi je spremenljivka UTF8String navaden AnsiString,
// zato prireditev oktete takoj znova transkodira v gostiteljsko kodno stran
U := UTF8Encode(W);
// Past 2: S je že AnsiString, zato UTF8Encode sploh ničesar ne naredi
R := UTF8Encode(S); // brez dekodiranja, brez kodiranja, brez napake
R := UTF8Encode(UnicodeString(S)); // ta klic zares kodira
// Past 3: združevanje vse operande poenoti v kodno stran cilja,
// cilj RawByteString pa ni izjema
Xmp := Xmp + R;
end;
Prva past pomeni, da mora kodirani rezultat ostati v AnsiString ali RawByteString, v katerem je nastal. Posredujte ga skozi začasno spremenljivko UTF8String na poti ven in delo ste razveljavili. Druga past se najdlje skriva, ker se UTF8Encode(S) prevede, zažene, vrne vrednost pravilne dolžine in ne izvede nobene pretvorbe, ko je argument že AnsiString; šele širitev najprej v UnicodeString klic prisili v dekodiranje. Tretja past je razlog, da lahko pravilen kodirnik še vedno izda pokvarjen dokument: BuildXmpBytes paket kopiči v lokalnem Xmp: AnsiString, Free Pascal pa vsak operand združevanja pretvori v kodno stran ciljne spremenljivke in večbajtna zaporedja na poti nazaj stisne v enobajtne znake ANSI
Kaj dejansko zagotavlja SetCodePage z False?
SetCodePage(RawByteString(Result), DefaultSystemCodePage, False) niz samo preoznači, ne da bi se dotaknil bajta. Tretji parameter je Convert; vrednost False pomeni »predpostavi, da je tovor že v ciljni kodni strani in samo spremeni oznako«. To je namerna laž o vsebini: okteti so v resnici UTF-8, vendar jih oznaka gostiteljske kodne strani ustavi pred pretvorbo v tretji pasti. V XMP-medpomnilnik se pridružijo kot surovi bajti in na drugi strani pridejo nespremenjeni
function StringToUtf8(const S: string): AnsiString;
begin
{$IFDEF UNICODE}
// Delphi: UTF8Encode že vrne oktete z oznako CP_UTF8, združevanje
// v AnsiString pa jih ohrani
Result := AnsiString(UTF8Encode(S));
{$ELSE}
// FPC: najprej razširi, sicer je UTF8Encode nad argumentom AnsiString brez učinka
Result := UTF8Encode(UnicodeString(S));
// Preoznači brez transkodiranja, da okteti preživijo združevanje v
// medpomnilnike z oznako ANSI, ki sestavljajo pakete XMP in nize PDF
if Length(Result) > 0 then
SetCodePage(RawByteString(Result), DefaultSystemCodePage, False);
{$ENDIF}
end;
Jasno razmejite mejo. Preoznačitev je samo za FPC in ni splošna odobritev mešanja označenih in neoznačenih nizov. Tukaj deluje, ker za njo obstaja natanko en vzorec porabe: dodaj v AnsiString, nato medpomnilnik zapiši kot bajte. Vse, kar bi preoznačeno vrednost poskušalo interpretirati kot besedilo na gostiteljski kodni strani, bi prebralo mojibake, in prav bi bilo tako. Obratna smer je obravnavana drugače in je na obeh prevajalnikih enaka: vhodni medpomnilnik označite s CP_UTF8 z SetCodePage(..., False), nato pokličite UTF8ToString
Zakaj so regresijski testi nosili isto past?
Ker test, ki pričakovane bajte sestavi iz izvornega literala, preizkuša prevajalnik, ne knjižnice. Konstanta, kot je #$C3#$A9, zapisana v izvorni datoteki Pascala, nosi kodno stran te datoteke pri prevajanju, ko pa jo posredujete parametru AnsiString, jo RTL znova kodira, kar je natanko pretvorba v preizkusu. Pričakovanje je treba sestaviti med izvajanjem, bajt za bajtom, in primerjati bajt za bajtom, ker = med dvema vrednostma AnsiString z različnima oznakama pred primerjavo uskladi kodni strani in vrne veselo lažni negativni rezultat
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; // zožitev v preizkusu
Encoded := StringToUtf8(Narrowed);
finally
SetMultiByteConversionCodePage(Saved);
end;
// 'Caf' + U+00E9 kot UTF-8, sestavljeno med izvajanjem, da noben literal ni znova kodiran
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;
Sam testni okvir je program LCL, zato je DefaultSystemCodePage CP_UTF8 in napaka ostane nevidna, dokler je test ne spremeni. SetMultiByteConversionCodePage(1252) znotraj try..finally za čas enega testa ponovi okolje navadnega konzolnega programa. Preverjanje od začetka do konca gre dlje in preveri obe smeri: paket XMP, ustvarjen z vstavljanjem oznak, mora vsebovati $43 $61 $66 $C3 $A9 in ne sme vsebovati $43 $61 $66 $E9, zato prihodnja regresija, ki se vrne na surovi enobajtni izhod, glasno odpove, namesto da bi ustvarila datoteko, ki je na izpisu v šestnajstiškem zapisu samo videti verjetna. Če delate z ne-latinskimi metapodatki, ista disciplina širjenja ureja primere v članku emoji in besedilo CJK, ki v Delphiju pokvarita WideChar
Kje drugje pristane ta zožitev
XMP je vidna žrtev, vendar je bila enako izpostavljena vsaka povezava TBytes v string v isti kodni zbirki. V v3.103.1 sta bili popravljeni še dve: Utf8BytesToString in StringToUtf8Bytes v FPdfProduction, ki paket podatkovnih zbirk XFA pretvorita skozi niz, da lahko MergePdfXfaDatasets zamenja vezane vrednosti, in BytesToUtf8 v FPdfTrustedList, ki dekodira XML evropskega seznama zaupanja po odstranitvi oznake vrstnega reda bajtov. Obe zdaj medpomnilnik pripravita v RawByteString, ga brez pretvorbe označita kot CP_UTF8 in dekodirata z UTF8ToString. En modul je bil že imun in razlog je vreden posnemanja. Zapisovalnik XFDF lastni tip besedila deklarira kot XFDFString, ki se pri FPC razreši v WideString, pri Delphiju pa v UnicodeString, zato njegov kodirnik nikoli ne vidi AnsiString z oznako kodne strani. To je strukturni popravek: besedilo ohranite v tipu UTF-16 do natančne točke serializacije, nato pa naj ena ozka funkcija prevzame pretvorbo v bajte. Vsaka napaka v tej družini je prišla iz polja string sredi cevovoda, ki je bil sicer na enem koncu UTF-16 in na drugem okteti
Kaj preveriti v lastni PDF-kodi za dva prevajalnika
Če dostavljate Object Pascal, ki teče na obeh prevajalnikih in zapisuje metapodatke v standardno skladen PDF, štiri preverjanja najdejo večino tega razreda napak, še preden jih najde validator
- Poiščite
UTF8Encodez argumentomstring. Na FPC ta klic nima učinka in je vrstica z največjim izplenom za revizijo - Vsako spremenljivko
UTF8Stringv načinu Delphi obravnavajte kot sumljivo. Tam je navadenAnsiStringin prireditev kodiranih bajtov vanj jih transkodira nazaj - Izvedite vsaj eno regresijo pod
SetMultiByteConversionCodePagez enobajtno kodno stranjo. Testni okvir LCL teče priCP_UTF8in nikoli ne bo ponovil navadnega konzolnega programa - Pričakovane vektorje bajtov zgradite med izvajanjem in jih primerjajte oktet za oktetom. Izvorni literali in
=oba uporabita usklajevanje kodne strani in skrijeta napako, ki jo iščete
Nič od tega ni eksotična posebnost Free Pascala. To je običajni strošek jezika, ki je ob tipu nizov, usmerjenem v bajte, ohranil še tip UTF-16, prevajalnika pa sta razumno, vendar različno izbrala, kaj naj pomeni string. Praktična posledica za delo s PDF-ji je ozka in ostra: metapodatki, ki so v vašem IDE-ju videti pravilni, lahko v paket XMP prispejo kot neveljaven UTF-8, ISO 19005-1 6.7.2 pa ne zanima, kateri prevajalnik jih je tja postavil. Če gradite arhivski cevovod, si plast kodiranja zasluži toliko pozornosti kot preostali potek arhivske skladnosti PDF/A, ki jo obdaja. Komponenta PDFium Delphi te pretvorbe dostavlja kot del knjižnice, zato SaveAsPdfA in njenih pet sorodnikov za standarde izda skladne metapodatke UTF-8 na Delphiju, Lazarusu in v navadnih gradnjah Free Pascala brez konfiguracije kodne strani pri klicatelju. Popolna dokumentacija API-ja in trenutna izdaja sta na strani izdelka PDFium Delphi Component