Technical Article

Yazı Tipi Alt Kümelemeyi Sessizce Devre Dışı Bırakan EndDoc Hatası

Bir rapor oluşturun, bir TrueType yazı tipi gömün; çıktı denediğiniz her görüntüleyicide doğru şekilde açılır. Karakterler doğrudur, metin seçilebilirdir, dosya geçerlidir. Yanlış olan tek şey boyuttur. Birkaç düzine Latin karakteri kullanan bir belge, 350 KB'lık yazı tipinin tamamını taşır. Çince bir paragraf yazdıran bir belge, ihtiyaç duyması gereken yarım megabaytlık dilim yerine 14 MB'lık bir CJK yazı tipini taşır. Hiçbir istisna oluşturulmadı, hiçbir uyarı günlüğe kaydedilmedi ve dosya doğrulamadan geçti. Bu, dışarıdan bakıldığında yanlış sıralanmış bir sonlandırma adımının nasıl göründüğüdür: hiçbir şey başarısız olmaz ve tek kanıt çok büyük bir sayıdır

Buna neden olan hata, HotPDF'in bir sürüm serisinde mevcuttu ve o zamandan beri düzeltildi. Bunu bir kusur bildirimi olarak değil, bir ders olarak yazmaya değer, çünkü hatanın şekli geneldir. Herhangi bir belge motorunun, nesneleri yazmadan hemen önce değiştiren bir sonlandırma aşaması vardır ve bu aşamanın doğruluğu tamamen adımlarının serileştirmeye göre sıralanmasına bağlıdır. Adımlardan birini yazma işleminin yanlış tarafına koyarsanız, sessizce hiçbir şey yapmaz

Yazı tipi alt kümelemenin ne yapması gerekir

Bir alt küme yazı tipi, bir belgenin gerçekten kullandığı bir TrueType dosyasının parçasıdır. ISO 32000-1 §9.9, gömülü bir yazı tipi programının yazı tipi tanımlayıcısı tarafından başvurulan bir akışta nasıl yer aldığını açıklar ve bir TrueType programı için bu akış, sıkıştırılmamış bayt sayısını veren bir /Length1 ile birlikte /FontFile2'dir. Alt kümeleme, glyf ve loca tablolarını yeniden yazarak yalnızca belgenin başvurduğu glifleri içermelerini sağlar, glif tanımlayıcılarını yeniden numaralandırır ve yazı tipini bir alt küme olarak işaretlemek için spesifikasyonun gerektirdiği şekilde /BaseFont adının önüne ABCDEF+ gibi altı harfli bir etiket ekler. On veya on beş kilobayta düşürülen Latin yazı tipi, yalın bir PDF ile tek bir başlık uğruna tüm bir yazı tipini gönderen bir PDF arasındaki farktır

Bunun gerçekleştiği nokta önemlidir. Alt kümeleme, diskte zaten bulunan baytlara uyguladığınız bir dönüştürme değildir. Bellekteki nesne grafiğini düzenler: /FontFile2 akış içeriğini küçültür, /Length1 değerini düzeltir ve /BaseFont dizesini yeniden yazar. Serileştirici grafiği dolaşırken ve baytları yayarken tüm bunların yerinde olması gerekir. Düzenlemeler baytlar yazıldıktan sonra gerçekleşirse, hiç kimsenin okumayacağı nesneleri güncellerler

Belirti ve neden hiçbir şeyin şikayet etmediği

Bildirilen davranış, herhangi bir teşhis olmadan çıktıda tam yazı tiplerinin bulunmasıydı. Bir Unicode TrueType yazı tipi kaydeden ve normal bir belge üreten kullanıcı, gömülü yazı tipi nesnesinin kaynak .ttf dosyasıyla aynı uzunlukta olduğunu ve /BaseFont adının altı harfli alt küme önekini taşımadığını gördü. Çıktı, on glif kullanan çalışmalar ile on bin glif kullanan çalışmalar arasında hiçbir zaman küçülmedi

Herhangi bir hatanın olmaması, bu tür hataları maliyetli kılan kısımdır. Yanlış zamanda çalışan bir alt kümeleme rutini yine de çalışır. Biriken kod noktası kullanımını dolaşır, mükemmel derecede doğru bir alt küme oluşturur ve bunu bellekteki nesne grafiğine uygular. Dahili olarak iş yapılır ve çağrı temiz bir şekilde döner. Yanlış olan tek şey, düzenlediği nesne grafiğinin artık yazılan şey olmamasıdır, çünkü yazıcı zaten bitmiştir. Arayan kişinin bakış açısından belge sorunsuz bir şekilde üretildi ve kaydedildi, bu da tam olarak sessiz bir hatanın verdiği izlenimdir

Kök neden sonlandırma sırasıydı

HotPDF'te kapatma işlemi EndDoc içinde gerçekleşir. Alt kümeleme adımı, BuildAndApplyUnicodeFontSubset adında dahili bir rutindir. Metin yayma yolunun glifler gösterildikçe doldurduğu bir bit eşleminde tutulan belge başına kullanılan kod noktaları kümesini okur, kullanılan her kod noktasını önbelleğe alınmış kod noktasından glife tablosu aracılığıyla gerçek bir glif tanımlayıcısına eşler ve yazı tipi programını bu kapsam çevresinde yeniden yazar. Bir Unicode TrueType yazı tipi kaydedildiğinde, yayma yolu çizdiği her karakter için kullanılan kod noktaları kümesinde bir bit ayarlar, böylece belge kapandığında motor alt kümenin hangi glifleri tutması gerektiğini tam olarak bilir

Kusur, BuildAndApplyUnicodeFontSubset işlevinin SaveToStream veya SaveToFile belgeyi zaten serileştirdikten sonra çağrılmasıydı. Alt kümeleyicinin /FontFile2 üzerindeki düzenlemeleri, düzeltilmiş /Length1 değeri ve altı harfli /BaseFont öneki, zaten baytlara dönüştürülmüş bir nesne grafiğine karşı hesaplanmıştı. Çözüm, tek satırlık bir yeniden sıralamaydı: alt küme çağrısını serileştirmenin önüne taşıyın, böylece yazıcı orijinal yazı tipi yerine alt kümelenmiş yazı tipini yayar. Düzeltilmiş sıra önce alt kümeleyiciyi çalıştırır ve ardından serileştirir

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
    Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
    Pdf.EndDoc;                 // subsetting runs here, before the write
    Pdf.SaveToFile('Report.pdf');
  finally
    Pdf.Free;
  end;
end;

Sıra düzeltildiğinde, çağıran kodla ilgili hiçbir şey değişmez. Bir Unicode TrueType yazı tipi kaydedildikten sonra alt kümeleme varsayılan olarak etkindir. Yazı tipini kaydedersiniz, belgeyi başlatırsınız, çizersiniz ve bitirirsiniz; alt küme, baytlar bellekten çıkmadan önce kullandığınız gliflerden oluşturulur

Neden yanlış yerleştirilmiş tek bir adım koca bir kategoridir

Bunun bir dipnot yerine bir ders niteliğinde olmasının nedeni, EndDoc işlevinin bir dizi kapatma adımı yayması ve bunlardan her birinin yazma işlemine göre konumuna duyarlı olmasıdır. Yazı tipi alt kümeleme bunlardan biridir. PDF/A çıktısı, alt kümede bulunan glif tanımlayıcılarını tam olarak listeleyen bir /CIDSet akışı gerektirir; bu, bir doğrulayıcının gömülü programın yazı tipi tanımlayıcısının iddia ettiği şeyle eşleştiğini onaylayabilmesi için ISO 19005'in getirdiği bir kısıtlamadır; bu akış aynı sonlandırma penceresinde yayılır ve önce alt kümenin oluşturulmuş olmasına bağlıdır. PDF/UA-1, ISO 14289-1 §7.18.3 uyarınca, açıklama taşıyan her sayfanın /S değerine sahip /Tabs bildirmesini gerektirir ve EnsurePDFUATabsOnAnnotatedPages adlı dahili bir rutin aynı aşamada bu anahtarı damgalar. Çıktı amacı kontrolleri de burada çalışır

Alt kümelemeyi devre dışı bırakan aynı sıralama hatası, açıklamalı sayfalardaki PDF/UA sekme sırası anahtarını da düşürdü, çünkü bu adım yazma işleminin aynı yanlış tarafında yer alıyordu. veraPDF ve PAC, eksik bir /Tabs /S ifadesini Matterhorn protokolü kontrol noktası 21-001'in ihlali olarak bildirir. Dolayısıyla, yanlış yerleştirilmiş tek bir çağrı yalnızca dosya boyutunu şişirmekle kalmadı; aynı zamanda herhangi bir hatanın olmamasıyla bir erişilebilirlik uyumluluk gereksinimini de sessizce bozdu. Sonlandırma aşamasının tehlikesi budur: adımları bir ön koşulu paylaşır ve tek bir sıralama hatası, her çağrı başarıyla dönmeye devam ederken aynı anda birkaçını devre dışı bırakabilir

Sessiz bir yayma hatası gerçekte nasıl yakalanır

İstisna oluşturmayan bir hata, program çalıştırılarak yakalanmaz. Çıktının incelenmesi ve girdinin ne üretmesi gerektiği ile karşılaştırılmasıyla yakalanır. Yazı tipi alt kümeleme için kontroller somuttur. Çıktı dosyası boyutunu kaba bir beklentiyle karşılaştırın: birkaç glife dokunan bir belge, tam bir yazı tipi boyutunda olmamalıdır. Gömülü yazı tipi nesnesini açın ve bayt uzunluğunu okuyun; bir Latin yazı tipi için alt kümelenmiş bir /FontFile2, kaynak dosyanın küçük bir kısmıdır. /BaseFont adını okuyun ve altı harfli önekin mevcut olduğunu doğrulayın, çünkü bunun olmaması hiçbir alt kümenin uygulanmadığının doğrudan bir işaretidir

var
  Pdf: THotPDF;
  Output: TMemoryStream;
begin
  Output := TMemoryStream.Create;
  try
    Pdf := THotPDF.Create(nil);
    try
      Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
      Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
      Pdf.EndDoc;
      Pdf.SaveToStream(Output);
    finally
      Pdf.Free;
    end;
    // A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
    if Output.Size > 100 * 1024 then
      raise Exception.Create('Font subset did not shrink the output');
  finally
    Output.Free;
  end;
end;

PDF/A çıktısı için kontrol daha da keskindir, çünkü bir doğrulayıcı işi sizin yerinize yapar. Uyumluluk seviyesini ayarlayın ve sonucu veraPDF aracılığıyla çalıştırın: eksik bir /CIDSet veya tanımlayıcıyla eşleşmeyen bir alt küme, gözle fark etmeniz için bırakılmak yerine başarısız bir madde olarak bildirilir. Bu sonlandırma çalışmasını yönlendiren uyumluluk anahtarları belgedeki özelliklerdir. PDFACompliance, PDF/A-2 Seviye B için '2B' gibi bir dize alır ve PDFUACompliance, etiketli PDF ve sekme sırası gereksinimlerini etkinleştiren bir boole değeridir

Pdf := THotPDF.Create(nil);
try
  Pdf.PDFACompliance := '2B';     // PDF/A-2 Level B, drives /CIDSet emission
  Pdf.PDFUACompliance := True;    // stamps /Tabs /S on annotated pages
  Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
  Pdf.BeginDoc;
  Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
  Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
  Pdf.EndDoc;
  Pdf.SaveToFile('Report_PDFA.pdf');
finally
  Pdf.Free;
end;

Mühendislik dersi

Bundan iki kural çıkar. Birincisi, nesneleri değiştiren her sonlandırma adımının, bu nesneler serileştirilmeden önce çalışması gerektiğidir ve bir belge motorunun kapanış aşaması, serileştirmenin birkaç eylem arasındaki bir eylem değil, son eylem olduğu sıralı bir boru hattı olarak okunmalıdır. İkincisi ise burada en çok zamana mal olan kuraldır: bir yayma adımı için hatanın olmaması, başarının kanıtı değildir. Doğru alt kümeyi oluşturan ve bunu yanlış, zaten yazılmış grafiğe uygulayan bir rutin, yanlış hiçbir şey bildirmeyen raporlar sunar, çünkü kendi bakış açısından yanlış bir şey yoktur. Doğrulama dönüş koduna değil, yapıtın kendisine bakmalıdır. Çıktı boyutunu kontrol edin, gömülü yazı tipinin bayt uzunluğunu ve /BaseFont önekiyle birlikte okuyun ve eksik bir /CIDSet ifadesinin sessiz bir eksikliği adlandırılmış bir başarısızlığa dönüştürdüğü PDF/A çıktısını veraPDF'in yargılamasına izin verin

Yazı tipi işlemenin üretici tarafı, yazı tiplerinin rapor çıktısı için nasıl kaydedildiği ve gömüldüğü, rapor çıktısında yazı tipleri ve resimler hakkındaki makalemizde ele alınmıştır. Bu sonlandırma adımlarının standartlara göre kontrol edildiği doğrulama tarafı, PDF/A ve PDF/UA doğrulaması hakkındaki kılavuzda ele alınmıştır. Her ikisi de, bu blogun başka yerlerinde ele alınan yükleme, düzenleme, şifreleme ve imzalama API'lerinin yanı sıra Delphi ve C++Builder için HotPDF Bileşeni'nin bir parçası olarak sunulan burada açıklanan alt kümeleme ve uyumluluk çalışmasıyla eşleşir