Bir font subsetter yalnızca yayınlanan kod noktalarından erişilebilen glifleri tutuyorsa, şekillendirilmiş glifler .notdef kutuları olarak görünür. Delphi ve C++Builder için yerel VCL PDF bileşeni olan HotPDF, sürüm 2.435.0'a kadar tam olarak bu kusuru taşıyordu: OpenType GSUB çıktısı, subsetter'ın onurlandıracağını beyan ettiği ama sonradan hiç okumadığı dahili bir kullanım bit haritasına kaydediliyordu
Bu, font subsetting'i sessizce devre dışı bırakan EndDoc hatasında anlatılandan farklı bir başarısızlıktır. O hata, subsetting'in serileştirmeye göre ne zaman çalıştığıyla ilgiliydi ve subsetting'i topyekûn devre dışı bırakıyordu. Bu ise, subsetting programa tam olarak uygun çalıştığında subset'in ne içerdiğiyle ilgilidir. Pipeline tam doğru anda tetiklenir, altı harfli subset öneki /BaseFont üzerinde ISO 32000-1 §9.6.4'ün gerektirdiği gibi görünür, dosya küçülür, her Latin sayfa temiz çıkar ve bir Arapça sayfa bir dizi boş dikdörtgen olarak gelir. Sıralama hataları bakıldığında hemen fark edilir. Closure hataları ise sonsuza dek sessiz kalır, çünkü subset yapısal olarak geçerlidir ve yalnızca kendi üyelik listesi konusunda yanlıştır
Şekillendirilmiş glifler neden .notdef olarak görünür?
Çünkü bir dokümanın yayınladığı kod noktaları kümesi, dokümanın çizdiği glif kümesiyle aynı değildir ve ikisini birbirine karıştıran bir subsetter, şekillendirmenin ürettiği her glifi düşürür. Metin şekillendirme (shaping), mantıksal bir karakter dizisini konumlandırılmış bir glif dizisine dönüştürür ve tüm amacı, tek bir girdi karakterinin eşleşmediği glifler üretmektir: Arapça bir orta heh, bir fi ligatürü, bir Devanagari bitişik yazı (conjunct), rclt özelliğiyle seçilen bağlamsal bir alternatif. Bunların her biri, bir GSUB araması tarafından üretilmiş bir glif ID'sidir; dizinizdeki herhangi bir karakter için cmap tablosunun size verdiği bir glif değildir. Yalnızca cmap tarafından yönlendirilen bir subsetter, bu yüzden yanlış dizini gezmektedir. Metnin şekillendirmeden önce kullanabileceği her glifi sadakatle korur ve metnin şekillendirmeden sonra gerçekten kullandığı glifleri tam olarak atar. Renderer daha sonra gömülü fonttan GID 1847'yi ister, subset o girdiyi loca içinde sıfırlamıştır ve bunun yerine glif indeksi 0 döner. Glif indeksi 0, OpenType tanımına göre .notdef'tir, bu yüzden hata imzası yanlış bir harf veya çökme değil boş bir kutudur. PDF'de hiçbir şey bozuk değildir; font, içerik akışının istediği glifi basitçe içermemektedir
Kod noktaları glif değildir: bir subset'in üç kaynağı
Doğru bir subset closure, her biri kendi biriktiricisine sahip üç bağımsız kaynağı birleştirmek zorundadır. Birincisi kod-noktasından türetilen kümedir: HotPDF, BMP karakterleri yayınlandıkça FUnicodeUsedCps'i, ek düzlem karakterleri surrogate çiftler üzerinden ulaşıldıkça FUnicodeSmpUsed'ı biriktirir, ardından her birini FUnicodeCpToGid üzerinden bir glif ID'sine eşler. İkincisi şekillendirmeden türetilen kümedir; bir GSUB ikamesinin ürettiği glif ID'leri, MarkUnicodeGlyphUsed ve EnableShapingFeatureForSubset aracılığıyla FUnicodeExtraUsedGlyphs'a kaydedilir. Üçüncüsü ise bileşik closure'dır: glyf içinde numberOfContours'u -1 olan bir glif, bileşen glif ID'lerinden oluşturulur ve bileşenlerini düşürerek bileşiği tutmak, .notdef'ten ziyade boş bir taslak üretir; bu da tartışmalı biçimde daha kötüdür çünkü bir boşluk hatası gibi okunur
HotPDF her zaman birinci ve üçüncü kaynağı ele almıştır. Serileştirmeden önce EndDoc'un çağırdığı subsetting giriş noktası olan BuildAndApplyUnicodeFontSubset, kullanılan-glif dizisini GID 0 ile başlatır, BMP kod noktalarını gezer, SMP kullanım listesini gezer ve diziyi bileşik bileşenleri dahili olarak çözen bir subset builder'a teslim eder. İkinci kaynak yazılmıştı ama hiç tüketilmiyordu ve üç kaynak farklı içeriklerde başarısız olduğundan, bu boşluk regresyon derlemi çoğunlukla Latin olan bir kod tabanında yıllarca gizli kalabilir
Yazılan ama hiç okunmayan dizi
Sözleşme üç yerde belgelenmiş ve hiçbirinde yerine getirilmemişti. FUnicodeExtraUsedGlyphs'ın bildirimi, EndDoc subsetter'ının bunu kod-noktasından türetilen kullanımla birleştirdiğini belirtiyordu; ApplyArabicGSUBRefinement üzerindeki başlık yorumu, yayınlanan her ikame GID'nin, subsetter'ın glifi gömülü fonta çekmesi için MarkUnicodeGlyphUsed'dan geçirildiğini vaat ediyordu; aynı vaat, rclt yolu için ApplyArabicGSUBContextualRefinement üzerinde birebir tekrarlanıyordu. Her iki çağıran da kendi payını yerine getirdi. Alana yapılan her referansın grep ile taranması, yaklaşık doksan saniyede diğer yarıyı ortaya çıkardı: bir bildirim, RegisterUnicodeTTF içinde bir SetLength tahsisi ve iki işaretleme rutininde yazmalar. Tek bir okuma bile yok. İçselleştirilmeye değer teşhis budur, çünkü fontların çok ötesine genellenir. Bir alan birden fazla çağrı noktası tarafından yazılıp hiçbirinden okunmuyorsa, ne kadar ayrıntılı yorumlanmış olursa olsun, temsil ettiği özellik yok demektir. Subsetter'ın 1. Adımı bir ekrana sığacak kadar küçüktür ve nereye bakılacağını bildikten sonra boşluk aşikardır
// Step 1: derive the used-glyph set (as it stood before 2.435.0)
SetLength(UsedGlyphs, FUnicodeNumGlyphs);
for I := 0 to FUnicodeNumGlyphs - 1 do
UsedGlyphs[I] := False;
UsedGlyphs[0] := True; // .notdef is always present
for Cp := 0 to $FFFF do // source 1a: BMP code points
if (Cp < Length(FUnicodeUsedCps)) and FUnicodeUsedCps[Cp]
and (Cp < Length(FUnicodeCpToGid)) then
begin
GID := FUnicodeCpToGid[Cp];
if (GID > 0) and (GID < FUnicodeNumGlyphs) then
UsedGlyphs[GID] := True;
end;
for I := 0 to High(FUnicodeSmpUsed) do // source 1b: SMP code points
begin
GID := FUnicodeSmpUsed[I].GID;
if (GID > 0) and (GID < FUnicodeNumGlyphs) then
UsedGlyphs[GID] := True;
end;
// source 2 was missing here: nothing ever consulted FUnicodeExtraUsedGlyphs
Tek döngülük düzeltme ve glifleri kendiniz işaretlemek
Onarım bir birleşimdir ve güvenlik argümanı işlemin yönünden gelir: yalnızca bit ayarlar, asla temizlemez, dolayısıyla daha önce subset'te hayatta kalan hiçbir glif düşürülmeye başlamaz
// v2.435.0: pull GSUB-derived extra glyphs into the subset.
// MarkUnicodeGlyphUsed / EnableShapingFeatureForSubset record GIDs that
// shaping produced but that no emitted code point maps to directly.
for I := 0 to FUnicodeNumGlyphs - 1 do
if (I < Length(FUnicodeExtraUsedGlyphs)) and FUnicodeExtraUsedGlyphs[I] then
UsedGlyphs[I] := True;
Üç özellik, bunu bir font motoru yeniden yazımı yerine düşük riskli bir değişiklik yapar. Yukarıdaki gibi monotondur. Hiçbir şey şekillendirmemiş fontlarda no-op'tur, çünkü FUnicodeExtraUsedGlyphs tamamen False kalır ve yalnızca Latin bir doküman için bayt çıktısı değişmeden kalır. Ve 2. Adım'dan önce yerleşir, böylece her iki subset builder da bunu devralır: orijinal GID numaralandırmasını koruyan seyrek builder ve HotPDF'nin PDF/A altında korunan glifleri yoğun bir aralığa yeniden numaralandırmak, maxp.numGlyphs'ı küçültmek ve eski-yeni eşlemeyi ISO 32000-1 §9.7.4.2'nin gerektirdiği /CIDToGIDMap akışı olarak yayınlamak için seçtiği kompakt builder _BuildCompactSubsetTTF. Her ikisi de dahili olarak _TTFWalkCompositeClosure'ı çağırır, bu yüzden bileşik olan şekillendirilmiş bir glif artık bileşenlerini de sürükler. Bileşik closure hiç bozuk değildi; bu glif ID'leri için basitçe hiç erişilmiyordu, çünkü glif ID'leri gezdiği kümede değildi. GSUB motorunu, yerleşik iyileştirme geçişlerine güvenmek yerine doğrudan siz sürüyorsanız, closure sizin sorumluluğunuz haline gelir ve yayınladığınız her ikame glif ID'si, EndDoc kullanılan-glif kümesini dondurmadan önce işaretlenmelidir
var
Pdf: THotPDF;
GIDs: array[0..1] of Word;
LigGID: Word;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'shaped.pdf';
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoNaskhArabic-Regular.ttf');
Pdf.ShapingFeatures := [sfArabicGSUB, sfStandardLigatures,
sfContextualAlternates];
GIDs[0] := Pdf.GetUnicodeGlyphForCodepoint($0644); // lam
GIDs[1] := Pdf.GetUnicodeGlyphForCodepoint($0627); // alef
if Pdf.ApplyLigatureSubstitution(GIDs, 0, 'liga', LigGID) then
Pdf.MarkUnicodeGlyphUsed(LigGID); // omit this and you get .notdef
Pdf.EnableShapingFeatureForSubset('rclt');
Pdf.CurrentPage.RtLTextOut(50, 700, 0, WideString(ArabicText));
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
EnableShapingFeatureForSubset, tek-GID çağrısının toplu karşılığıdır ve kasıtlı olarak muhafazakârdır. Şu anda seçili olan yazı/dil yolu altında, dört baytlık tek bir özellik etiketine bağlı arama listeleri için GSUB arama listesini gezer ve bu aramaların üretebileceği ikame glif ID'lerini işaretler. Font GSUB tablosu taşımadığında ya da özellik o yolda yoksa savunmacı bir no-op'tur, bu yüzden koşulsuz çağrılması güvenlidir. Aynı zamanda tasarım gereği bir aşırı-yaklaşımdır: belirli bir dokümanın hiç çizmediği glifleri tutabilir. Subsetting için aşırı-içerme bayt maliyeti, eksik-içerme ise doğruluk maliyeti getirir, bu da bu takası kolay bir seçim yapar. Bu aramaların yapısı ve hangi gliflerin katıldığına karar veren kapsam tabloları, saf Delphi'de GSUB üslupsal alternatiflerinin anlatıldığı yazıda ele alınmaktadır
Glifin gerçekten subset'te olduğunu nasıl kanıtlarsınız?
Arka planda sizden habersiz bir sistem fontunu ikame ediyor olabilecek bir görüntüleyicide sayfaya göz atarak değil, yayınlanan fontu okuyarak. Bu hata sınıfının tamamını yakalayan kontrol mekaniktir: çıktı PDF'inden /FontFile2 akışını çıkarın, loca'yı ayrıştırın ve beklediğiniz glif ID'sinin boş olmayan bir girdi taşıdığını, yani başlangıç ve bitiş ofsetlerinin farklı olduğunu doğrulayın. Boş bir girdi, subsetter'ın glifin kullanılmadığına karar verdiği anlamına gelir. İki alışkanlık, hatanın tekrar gönderilmesini çok daha zor hale getirir. Şekillendirilmiş yazı içeren bir sayfayı yalnızca manuel prova kümesinde değil, otomatik smoke derleminde de tutun, çünkü Arapça, Devanagari ve Khmer, hiçbir miktarda Latin kapsamının dokunmayacağı closure yollarını devreye sokar. Ve bir biriktirici var olduğunda, onu bir şeyin tükettiğini doğrulayın, çünkü yalnızca-yazılan bir alan, derlenen, yanlış derlemde yeşil test veren ve hiçbir şey yapmayan bir özelliktir
Düzeltme nerede duruyor
Subset closure, şekillendirilmiş bir glifin görüntülenmesi için gereklidir ve yeterli değildir. Glif ayrıca içerik akışından adreslenebilir olmalıdır, bu da kendi sınırına sahip ayrı bir sorundur. Yerleşik HotPDF Arapça iyileştirme geçişleri, bir ikameyi yalnızca her ikame glif ID'si, U+FB50–U+FDFF ve U+FE70–U+FEFF aralığında kabaca 690 kod noktası üzerinde ters bir cmap taraması yoluyla bir Unicode sunum-biçimi kod noktasından erişilebilir olduğunda taahhüt eder. Bir ikame o aralığın dışında bir glif ID'sine düşerse, okuyucunun adresleyemeyeceği bir şey yayınlamak yerine giriş penceresi değişmeden geçer; keyfi glif ID'lerindeki fonta özgü alternatiflerin, onları yayın yolundan taşımak için U+E000–U+F8FF'de tahsis edilmiş sentetik bir özel-kullanım kod noktasına ihtiyacı vardır. Dolayısıyla dürüst özet, 2.435.0 düzeltmesinin hikayeyi tamamlamak yerine sert bir engelleyiciyi kaldırdığıdır. Bundan önce, bir glif doğru şekillendirilebilir, doğru yayınlanabilir ve yine de subset zamanında yok olabilirdi; bu da şekillendirme motorunun aramaları ne kadar iyi olursa olsun uçtan uca güvenilemeyeceği anlamına geliyordu. Geriye kalan adreslenebilirliktir ve bu kısıt en azından izlediğiniz her şeyden sonra çalışan bir derleme adımında sessizce değil, yayın noktasında görünür biçimde başarısız olur. Aynı pipeline'ın yayın tarafı için bkz. Delphi PDF'lerinde Arapça ve RTL metin şekillendirme kılavuzu
Burada açıklanan font subsetting, GSUB motoru ve karmaşık yazı şekillendirme, Delphi ve C++Builder için standart HotPDF Component içinde sunulur; ürün sayfası yukarıda adı geçen Unicode font ve şekillendirme çağrıları için tam API referansını içerir