Noto Sans SC ile dizilmiş bir Çin faturası, fontu .otf dosyasından gömüldüğünde mutool içinde müşteri adının yerine alakasız ideogramlarla açılabilir; oysa TrueType bir font aynı sayfayı doğru render eder. Asıl neden fontun içinde yatar: CFF charset. PDFlibPas bunu gömülü kopyada identity olarak yeniden yazar; böylece glif indeksi kodları hedeflenen gliflere ulaşır
Bir fontun bu sorunu gösterip göstermemesi, nasıl inşa edildiğine bağlıdır. CIDFontType2 olarak gömülen bir TrueType programı CIDToGIDMap'i Identity değerinde alır ve content stream içindeki kod hedeflenen glife düşer. CFF outline taşıyan Latin OpenType fontlarının çoğu name-keyed olur ve name-keyed bir CFF programı gelen CID'i doğrudan glif indeksi olarak kullanır. ISO 32000-1, §9.7.4.2'de birçok fontta CID ile GID'in çakıştığını bile not eder. Hepsinde değil. CID-keyed inşa edilen büyük CJK aileleri — aralarında Noto Sans CJK ve Source Han kardeşleri de var — kendi CID numaralandırmalarını taşır; bu numaralandırmaya glif indekslerini doğrudan gönderen bir writer, eşleşen CID'in o an sahibi olan glifi neyse onu alır
CID-keyed bir CFF programı, CID ile ne yapar
ISO 32000-1 §9.7.4.2, gömülü CFF programlarını Top DICT'e bakarak iki duruma ayırır. Top DICT CIDFont operatörlerini kullanıyorsa — pratikte program ROS ile açılır (operatör 12 30: registry, ordering, supplement) — okuyucu CID'i content streamden alır, charset girdisinde o CID'i taşıyan glifi bulur ve outline'ı ancak ondan sonra CharStrings INDEX'ten okur. CIDFont operatörleri yoksa CID, glif indeksinin ta kendisidir; charset işin içinde değildir. Bu yüzden §9.7.4.2'yi yazıldığı gibi uygulayan her okuyucu, charset identity değilken glif indeksi kodu için yanlış glifi çizer; hangi görüntüleyiciye geçseniz sonuç değişmez. Adobe Technical Note #5176'da tanımlanan charset, iki özel durumu olan üç düzende gelir:
- Format 0, glif sırasına göre her glif için bir 2 baytlık SID veya CID saklar
- Format 1 aralık saklar: bir ilk CID ve ardından onu kesintisiz CID'lerle izleyen gliflerin 1 baytlık sayımı
- Format 2, 2 baytlık sayımlı aynı aralık düzenidir; uzun kesintisiz serilere sahip fontlara uyar
- Glif 0
.notdefolup hiçbir formatta girdisi yoktur; 0, 1 veya 2 değerindeki bir charset ofseti veriye işaret etmek yerine önceden tanımlı charset seçeneklerinden birini adlandırır
O halde önce elinizdeki fontun hangi tür olduğunu bulun. PDF tarafında Identity-H yalnızca 2 baytlık kodun CID'e eşit olduğunu söyler. O CID'in bir glife nasıl ulaşacağı tamamen font programının içinde yaşar. Aşağıdaki fonksiyon CFF tablosunu bir .otf içinden okur, Top DICT'i ROS için tarar ve her glifin aynı numaralı bir CID'e eşlenip eşlenmediğini charset üzerinden sınar. Yalnızca RTL yeterlidir
uses
System.SysUtils, System.IOUtils;
type
TCFFCharsetReport = record
IsCFF, CIDKeyed, IdentityCharset: Boolean;
CharsetFormat, GlyphCount: Integer;
end;
{$RANGECHECKS ON} // kırpılmış bir dosya yanlış okumak yerine ERangeError fırlatır
function InspectOtfCharset(const FileName: string): TCFFCharsetReport;
var
B: TBytes;
NumTables, I, Cff, P, Dict, DictEnd, Unused1, Unused2: Integer;
Op, Operand, CharsetOfs, CharStringsOfs, G, Left: Integer;
function BE(Pos, Count: Integer): Integer;
var
K: Integer;
begin
Result := 0;
for K := 0 to Count - 1 do
Result := (Result shl 8) or B[Pos + K];
end;
// Pos'taki INDEX'in sonunu döndürür ve ilk nesnesini çerçeveler
function SkipIndex(Pos: Integer; out ObjStart, ObjEnd: Integer): Integer;
var
Count, OffSize, Base: Integer;
begin
Count := BE(Pos, 2);
if Count = 0 then
begin
ObjStart := Pos + 2;
ObjEnd := ObjStart;
Exit(Pos + 2);
end;
OffSize := B[Pos + 2];
Base := Pos + 2 + (Count + 1) * OffSize; // ofsetler buradan itibaren 1 tabanlıdır
ObjStart := Base + BE(Pos + 3, OffSize);
ObjEnd := Base + BE(Pos + 3 + OffSize, OffSize);
Result := Base + BE(Pos + 3 + Count * OffSize, OffSize);
end;
begin
Result := Default(TCFFCharsetReport);
Result.CharsetFormat := -1;
B := TFile.ReadAllBytes(FileName);
if (Length(B) < 12) or (BE(0, 4) <> $4F54544F) then // 'OTTO'
Exit;
Cff := -1;
NumTables := BE(4, 2);
for I := 0 to NumTables - 1 do
if BE(12 + I * 16, 4) = $43464620 then // 'CFF '
Cff := BE(12 + I * 16 + 8, 4);
if Cff < 0 then
Exit;
Result.IsCFF := True;
P := SkipIndex(Cff + B[Cff + 2], Unused1, Unused2); // Name INDEX
SkipIndex(P, Dict, DictEnd); // Top DICT INDEX
Operand := 0;
CharsetOfs := 0;
CharStringsOfs := 0;
P := Dict;
while P < DictEnd do
begin
Op := B[P];
case Op of
12:
begin
if B[P + 1] = 30 then
Result.CIDKeyed := True; // ROS
Inc(P, 2);
end;
0..11, 13..21:
begin
if Op = 15 then
CharsetOfs := Operand
else if Op = 17 then
CharStringsOfs := Operand;
Inc(P);
end;
28: begin Operand := SmallInt(BE(P + 1, 2)); Inc(P, 3); end;
29: begin Operand := BE(P + 1, 4); Inc(P, 5); end;
30:
begin // gerçek sayı
repeat
Inc(P);
until ((B[P] and $F) = $F) or ((B[P] shr 4) = $F);
Inc(P);
end;
32..246: begin Operand := Op - 139; Inc(P); end;
247..250: begin Operand := (Op - 247) * 256 + B[P + 1] + 108; Inc(P, 2); end;
251..254: begin Operand := -(Op - 251) * 256 - B[P + 1] - 108; Inc(P, 2); end;
else
Exit; // ayrılmış bayt
end;
end;
if (not Result.CIDKeyed) or (CharsetOfs <= 2) or (CharStringsOfs <= 0) then
Exit;
Result.GlyphCount := BE(Cff + CharStringsOfs, 2);
P := Cff + CharsetOfs;
Result.CharsetFormat := B[P];
if Result.CharsetFormat > 2 then
Exit;
Inc(P);
Result.IdentityCharset := True;
G := 1; // .notdef girdisi taşımaz
while G < Result.GlyphCount do
begin
if BE(P, 2) <> G then
Result.IdentityCharset := False;
case Result.CharsetFormat of
0: Left := 0;
1: Left := B[P + 2];
else
Left := BE(P + 2, 2);
end;
Inc(P, Result.CharsetFormat + 2);
Inc(G, Left + 1);
end;
end;
CIDKeyed = True ve IdentityCharset = False bildiren bir rapor, bu yazının konusu olan fontu tanımlar: charset hesaba katılmadan content stream'e glif indeksleri yazan her writer, o fontla yanlış glifler çizer. Name-keyed bir sonuç ya da charset zaten identity olan bir CID-keyed font, aynı writer altında doğru render eder. Bir üründe bir CJK fontunun çalışıp bir sonrakinde çalışmaması tam olarak budur. Döngü, her aralığın yalnızca ilk CID'ini karşılaştırır. Bu yeterlidir: bir aralık, kesintisiz CID'ler taşıyan ardışık glifleri kapsar; kendi glif indeksinde başlayan bir aralık sonuna kadar onda kalır
Glif indeksleri tüm font sözlüğünde dolaşır
Bir PDF writer nadiren elinde CID ile bulunur. cmap tablosu da shaping motoru da glif indeksi döndürür; bu yüzden doğal tasarım, o indeksleri 2 baytlık Identity-H kodları olarak yazar ve gerisini hep onlara bağlar: /W genişlik dizisi, PDF/A'nın istediği /CIDSet bitmap'i ve extraction sayesinde kaynak metnin geri kazanılmasını sağlayan ToUnicode CMap'i. TrueType'ın bir kaçış kapısı vardır. Table 117 CIDToGIDMap sunar, ama yalnızca Type 2 CIDFontlar için; bir CFF programı CIDFontType0 fontta yaşar ve PDF tarafından bir CID'i yönlendirecek hiçbir yolu yoktur. Bu, bazı writerların TrueType ile kullandığı bir numarayı da eler: fontun sonunun ötesinde, yeniden kullanılan bir glife ikinci bir "alias" CID vermek, böylece ToUnicode her kullanıma farklı metin iliştirebilsin. CFF bir fontta o alias hiçbir glif seçmez ve mutool onu render edemediğini bildirir. U+4E00 ile U+2F00'i (KANGXI RADICAL ONE) tek outline'a eşleyen fontlar olağan durumdur. Belgeniz ikisinin farklı farklı extraction edilmesine ihtiyaç duyuyorsa, geçtiği yeri /ActualText ile bir marked-content span içine sarın (ISO 32000-1 §14.9.4); okuyucu fontun eşlemesi yerine onu kullanır
Charset'i yerinde yeniden yazmak
Glif indeksi kodlarını CID-keyed bir programla barıştırmanın iki yolu var. Biri, her glif indeksini yazmadan önce fontun CID'ine çevirir; bu, content stream kodlarına, /W anahtarlarına, /CIDSet içindeki bit konumlarına, ToUnicode kaynak kodlarına ve kayıt anındaki her subset'e dokunur. Öteki, anlaşmazlığa düşen tek yapıyı değiştirir: charset'i öyle yazar ki font, bir name-keyed program gibi glif indekslerine cevap versin. İkinci yol daha güvenli olandır. Shaper, genişlik tablosu, subset çıkarıcı ve ToUnicode üreticisi glif uzayında çalışmayı sürdürür; CID anlamak zorunda kalan tek kod, fontun gömülü kopyası üzerinde bir kez koşan kısa bir rutindir. PDFlibPas o yolu izler
Yeniden yazım, charset formatını ve bayt uzunluğunu olduğu gibi korur. Format 0'da her girdinin üzerine kendi glif indeksi yazılır. Format 1 ve 2'de yalnızca her aralığın ilk CID'i değişir; sayımlar oldukları gibi kalır. Büyüyen ya da küçülen hiçbir şey yoktur. CharStrings'e, Private DICT'lere, FDArray ve FDSelect'e işaret eden Top DICT ofsetleri hiçbir şeyi yeniden kodlamadan geçerli kalır; glif indeksine göre hinting sözlükleri atayan FDSelect, her glifi özgün FD'sinde tutar. Name-keyed programlar, önceden tanımlı charsetler, CFF2 tabloları (bunların charseti yoktur) ve parse edilemeyen programlar rahat bırakılır. Yeniden yazımı üç kod yolu koşturur:
AddTrueTypeFontFromFile, bir.otfüzerinde çıplakCFFtablosunuFontFile3olarak,CIDFontType0Calt türüyle gömer; charset yeniden yazılmış haldedir- Kayıt anındaki subset, özgün glif indekslerini kaynak CID'lerle birlikte taşır; bu yüzden onun charseti ikinci kez yeniden yazılır
DrawOpenTypeTextveDrawShapedTextin ardındaki shaping yardımcısı tüm sfnt'yi tutar; yeniden yazımdan sonraCFFtablosunun sağlaması yeniden hesaplanır veheadtablosununcheckSumAdjustmentdeğeri, dosyanın toplamından indirilmiş0xB1B0AFBAolur- O ayar yalnızca tek bir font için tanımlıdır; bu yüzden koleksiyondan alınan bir font yalnızca tablo sağlamasını alır
uses
SysUtils, PDFlibrary;
var
Lib: TPDFlib;
FontID: Integer;
begin
Lib := TPDFlib.Create;
try
// CFF tadındaki bir .otf, FontFile3 /CIDFontType0C alt türüyle gömülür;
// CID-keyed olduğu takdirde gömülü kopya identity charset alır
FontID := Lib.AddTrueTypeFontFromFile('C:\Fonts\NotoSansSC-Regular.otf');
if FontID = 0 then
raise Exception.Create('the font file could not be read or parsed');
Lib.SetTextSize(18);
Lib.DrawText(56, 760, '季度报表:华东区销售汇总');
Lib.DrawText(56, 730, '发票号码 2026-11-0187,金额 ¥12,480.00');
if Lib.SaveToFile('noto-sc.pdf') <> 1 then
raise Exception.CreateFmt('save failed, error %d', [Lib.LastErrorCode]);
finally
Lib.Free;
end;
end;
Çıktıyı render ederek doğrulayın. Extraction sorunu göstermez. pdftotext ve öteki tüm extractorlar ToUnicode okur; ToUnicode, content streamdeki koda bağlıdır ve fontun o kod için hangi outline'ı seçtiğini bilmez, bu yüzden yanlış ideogram gösteren bir sayfa bile doğru string'i extraction eder. mutool draw -r 150 -o page%d.png noto-sc.pdf sayfanın gerçekten nasıl göründüğünü gösterir; bu DrawText yolu için poppler'ın pdffonts noto-sc.pdf çıktısı fontu CID Type 0C olarak listelemelidir. Beklenen: her iki satırın, kaynak metnin adlandırdığı gliflerle Noto Sans SC içinde render edilmesi; aynı çıktının Ghostscript'te de görülmesi
Hangi gömme biçimi PDF 1.6 gerektirir?
OpenType olanı. ISO 32000-1'in Table 126 listesi, bir FontFile3 stream'ini CIDFontType0C alt türünde PDF 1.3 özelliği olarak sayarken, OpenType alt türü — CFF tablosuyla bütün sfnt — ilk kez PDF 1.6'da görünür. Yeni PDFlibPas belgeleri 1.5 sürümünden başlar; çıplak CFF kullanan DrawText yolu bunun içine sığar. CFF bir fontla yapılan ilk DrawOpenTypeText, DrawShapedText veya dikey yazım çağrısı bir OpenType yardımcısı oluşturur ve belgeyi yerinde 1.6'ya yükseltir; bunu GetPDFVersion anında raporlar. Sürüm kilidi işi değiştirir. LockSaveVersion belgeyi daha aşağı sabitlediyse yükseltme reddedilir ve SaveToFile, LastErrorCode 602 ile 0 döndürür; böylece hiçbir dosya fontlarının karşılamadığı bir sürüm iddia etmez. Dağıtım profili sürümü 1.6 altına sabitlediğinde — PDF 1.4 üzerine kurulu olduğu için olağan durum PDF/A-1'dir — CJK metnini DrawText üzerinden, bir .otf ile verin ya da shaping gerektiren her şey için ailenin bir TrueType kesitini kullanın
var
Lib: TPDFlib;
FontID: Integer;
begin
Lib := TPDFlib.Create;
try
FontID := Lib.AddTrueTypeFontFromFile('NotoSansSC-Regular.otf');
Writeln('before: ', Lib.GetPDFVersion); // Beklenen: 1.5
if Lib.DrawOpenTypeText(56, 760, FontID, 18, '《年度报告》第三章', 0) = 1 then
Writeln('after: ', Lib.GetPDFVersion); // Beklenen: 1.6
Lib.SaveToFile('shaped.pdf'); // pdffonts çıktısında beklenen: CID Type 0C (OT)
finally
Lib.Free;
end;
Lib := TPDFlib.Create;
try
Lib.LockSaveVersion('1.5');
FontID := Lib.AddTrueTypeFontFromFile('NotoSansSC-Regular.otf');
Lib.DrawOpenTypeText(56, 760, FontID, 18, '《年度报告》第三章', 0);
if Lib.SaveToFile('locked.pdf') = 0 then
Writeln('save refused, error ', Lib.LastErrorCode); // Beklenen: 602
finally
Lib.Free;
end;
end;
Aynı sayılara bağlı genişlikler ve ToUnicode
Charset identity olduğunda CID, glif indeksi ve content stream kodu tek bir sayıdır; genişlikler de onu izler. ISO 32000-1 §9.7.4.3, /W'yi CID'in kendisine göre indeksler: ya bir başlangıç CID'i ve ona izleyen genişlik dizisi ya da ilk CID, son CID ve ortak tek genişlik grupları halinde; simple fontların kullandığı türden bir /FirstChar ofseti yoktur. Dizinin dışında bıraktığı her CID /DW'yi alır ve Table 117, /DW için varsayılanı 1000'de sabitler. Sıfır yanlıştır. Listede olmayan CID'ler için 0'a sığınan bir okuyucu, bir ideogram satırını kendi üzerine çökertir. Bir .otf, AddTrueTypeFontFromFile üzerinden yüklendiğinde PDFlibPas, CID 0'dan başlayıp her glifin advance değerini listeleyen bir /W dizisi yazar ve yanına açık bir /DW 1000 koyar. Okuma tarafında, çöp gibi extraction edilen CJK metni genellikle aynı hattın codespace yarısında ters gider; genişlik, CharSet ve CIDSet üzerine PDF/UA font denetimi ise bu dizileri gömülü programla karşılaştırır
ToUnicode'in kendi numaralandırma kuralı vardır. Bir bfrange girdisi yalnızca ilk hedef string'ini saklar; §9.10.3 ise aralığın uzunluğunun o son baytı 255'in ötesine itmemesini ister, çünkü okuyucu izleyen hedefleri yalnızca o son baytı artırarak üretir. U+4EFE, U+4EFF, U+4F00 ve U+4F01'e eşlenen ardışık glifler tek bir aralık gibi görünür. Değildir: bir okuyucu üçüncüsünü U+4E00 olarak çözerdi. PDFlibPas yazıcısı, komşu glif-to-Unicode çiftlerini yalnızca kaynak kodlar da hedefler de tek bir 256 bloğu içinde kaldığında tek bir bfrange'de birleştirir, aksi halde seriyi böler. Dikey CJK dizgisi aynı yardımcı fontlardan geçer; bu yüzden dikey Japonca ve Çince metin, fazladan çağrı olmadan identity charseti ve bu ToUnicode aralıklarını devralır
InspectOtfCharset'ın CID-keyed ve identity dışı charsetli raporladığı her font, glif indeksi kodları ile fontun anlaşamadığı bir durumdur. Delphi için PDFlibPas üzerinden gömülen böyle bir font, outline'larını, ofsetlerini ve charset uzunluğunu korur; değişen şey, o tek tablodaki CID'ler ve tüm sfnt tutuluyorsa sağlamalardır