Artykuł techniczny

Osadzanie brakujących czcionek w PDF dla PDF/A w Delphi

losLab PDF Library potrafi osadzić brakujące programy czcionek już wczytanego dokumentu PDF jednym wywołaniem: EmbedMissingFonts przechodzi przez każdy słownik czcionki w dokumencie, odnajduje pasującą zainstalowaną czcionkę systemową po jej nazwie BaseFont i zapisuje program czcionki z powrotem do pliku. Dla zespołów naprawiających dokumenty od podmiotów trzecich, które nie przechodzą walidacji PDF/A na osadzaniu czcionek, to właśnie ta poprawka sprawia, że błąd preflight 00030 znika

Scenariusz jest przygnębiająco powszechny. Potok przyjmowania dokumentów do archiwum dostaje pliki PDF od dostawców, klientów albo z punktu skanowania; dokumenty wyświetlają się dobrze na każdym biurku w firmie; a potem walidator PDF/A odrzuca całą partię z tym samym zarzutem powtórzonym raz na plik: co najmniej jedna czcionka nie jest osadzona. Nikt wcześniej w łańcuchu nie wygeneruje tych plików ponownie, więc potok musi je naprawić. Ten artykuł opisuje właśnie tę ścieżkę naprawy. Jest towarzyszem artykułu o preflight, który opisuje wykrywanie naruszeń PDF/A i PDF/UA: tamten mówi ci, które dokumenty są zepsute, ten naprawia najczęstszy sposób, w jaki są zepsute

Dlaczego PDF/A wymaga osadzenia każdej czcionki?

ISO 19005-1 §6.3.4 wymaga, by każda czcionka używana przez zgodny dokument niosła swój program czcionki wewnątrz pliku, ponieważ cała obietnica PDF/A to odtwarzalność: dokument musi wyświetlić się identycznie na maszynie za pięćdziesiąt lat, która nie dzieli żadnej czcionki z maszyną, która go wyprodukowała. Czcionka nieosadzona to polecenie odnalezienia Arial gdzieś w systemie wyświetlającym, a stanowisko normy jest takie, że „gdzieś w systemie wyświetlającym” nie jest gwarancją archiwalną. Jakiekolwiek glify, metryki i pokrycie ma czcionka zastępcza, to właśnie dostaje czytelnik, a może to nie być to, co widział autor

Historycznym winowajcą jest konwencja Standard 14. PDF 1.0 obiecywał, że każda przeglądarka dostarcza Helvetica, Times, Courier, Symbol i ZapfDingbats, więc generatory nauczyły się odwoływać do tych czcionek po nazwie i nie osadzać niczego, a trzydzieści lat narzędzi wciąż robi dokładnie to samo. losLab PDF Library traktuje ten wymóg na tyle poważnie, że w trybie tworzenia PDF/A AddStandardFont celowo nic nie robi: biblioteka nie dostarcza programów czcionek Standard 14, nie może osadzić czegoś, czego nie ma, i odmawia zapisania nieosadzonego odwołania w dokumencie, który deklaruje zgodność. Zwraca 0 bez wybrania czcionki, więc dokument PDF/A musi zamiast tego użyć AddTrueTypeFont z osadzaniem, a każde żądanie Embed=0 jest po cichu podnoszone do Embed=1, dopóki tryb PDF/A jest aktywny. To strona zapisu. Trudniejszy problem jest po stronie odczytu: dokument, który ktoś inny już napisał, pełen słowników czcionek, których nie ty utworzyłeś

Jak EmbedMissingFonts naprawia wczytany dokument?

losLab PDF Library naprawia czcionki w miejscu, zamiast je przebudowywać. Gdy generator PDF zapisuje nieosadzoną czcionkę TrueType, wytwarzany przez niego słownik FontDescriptor jest już kompletny: FontName, FontBBox, Flags, Ascent, Descent, StemV, wszystko obecne. Jedyne, co dzieli go od czcionki osadzonej, to brak jednego wpisu, odwołania do strumienia /FontFile2 zawierającego właściwy program czcionki. Dlatego EmbedMissingFonts nie dotyka słownika czcionki, kodowania, tablicy szerokości ani żadnego strumienia treści, który odwołuje się do czcionki po nazwie zasobu. Odczytuje pasujący program czcionki z systemu, kompresuje go w nowy obiekt strumienia i dołącza pojedyncze odwołanie /FontFile2 (albo /FontFile3 dla czcionek CIDFontType0) do już istniejącego FontDescriptor. Wszystko, na co wskazują strony dokumentu, zostaje dokładnie tam, gdzie było, i to właśnie czyni tę operację bezpieczną na plikach, nad którymi nie masz kontroli

Diagram PDF Library for Delphi przedstawiający słownik FontDescriptor przed i po EmbedMissingFonts: każdy wpis już obecny i dołączony jedynie brakujący strumień FontFile2 z zainstalowanej czcionki systemowej
EmbedMissingFonts dołącza jedno odwołanie do strumienia w deskryptorze, który jest już kompletny

Zakres obejmuje obie architektury czcionek, jakie spotkasz w praktyce: proste czcionki TrueType oraz złożone czcionki Type0/CID, produkowane dla tekstu CJK i nowoczesnego wyjścia Unicode. Przejście celowo wylicza każdy słownik Font w drzewie obiektów dokumentu, zamiast polegać na obchodzie zasobów strona po stronie, więc czcionki przywoływane z adnotacji albo współdzielone między stronami też zostają wychwycone. API to pojedyncze wywołanie na wczytanym dokumencie

var
  PDF: TPDFlib;
  Repaired: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('supplier-invoice.pdf', '') <> 1 then
      raise Exception.Create('Could not load PDF');

    // Przechodzi każdy słownik Font; zwraca, ile czcionek
    // zyskało program czcionki. Czcionki, których programu nie
    // da się znaleźć w systemie, są pomijane, a nie zgłaszane jako błąd.
    Repaired := PDF.EmbedMissingFonts;
    Writeln(Format('%d font program(s) embedded', [Repaired]));

    PDF.SaveToFile('supplier-invoice-repaired.pdf');
  finally
    PDF.Free;
  end;
end;

Jeden szczegół warto znać, bo wyjaśnia, dlaczego dopasowanie nazw działa lepiej niż naiwne porównanie łańcuchów: biblioteka normalizuje nazwy BaseFont przed ich wyszukaniem. Prefiksy podzbiorów (wzorzec ABCDEF+ złożony z sześciu wielkich liter i plusa) są usuwane, przyrostki w stylu PostScript takie jak ArialMT rozwiązują się do Arial, a pliki TrueType Collection są wykrywane i rozpakowywane, więc krój mieszkający wewnątrz .ttc również osadza się poprawnie

Weryfikacja naprawy raportem preflight

CreatePreflightReport to krok weryfikacji, a pętla jest celowo domknięta: ten sam audyt, który potępił plik, powinien być tym, który go oczyści. Kod błędu 00030 to ustalenie z pogłębionego audytu PDF/A brzmiące „At least one font is not embedded (FontFile/FontFile2/FontFile3 missing)”, zgłaszane wobec pliku jako całości, więc jedna przeoczona czcionka utrzymuje je przy życiu. Uruchom raport na pliku źródłowym, napraw, zapisz i uruchom go ponownie na wyjściu

PDF Library for Delphi: domknięta pętla naprawy, w której CreatePreflightReport oznacza kod błędu 00030, EmbedMissingFonts osadza oznaczone czcionki, SaveToFile zapisuje naprawiony PDF, a drugi przebieg raportu potwierdza, że partia przechodzi walidację
Uruchom najpierw raport preflight, pozwól mu sterować naprawą, a potem niech ten sam audyt poświadczy wynik
function HasFontEmbeddingViolation(PDF: TPDFlib;
  const FileName: string): Boolean;
var
  Report: string;
begin
  // ComplianceTests = 1 wybiera kontrole PDF/A
  Report := PDF.CreatePreflightReport(FileName, '', 1, 0);
  Result := Pos('00030', Report) > 0;
end;

Jeśli zamiast werdyktu dla całego pliku potrzebujesz widoku dla każdej czcionki z osobna, wczytaj naprawiony dokument ponownie i wylicz je: FindFonts, a następnie SelectFont i GetFontIsEmbedded raportują status osadzenia czcionka po czcionce, co jest właściwym narzędziem, gdy zadanie wsadowe musi zapisać w logu, którego dokładnie kroju w którym pliku nie dało się naprawić. Ten sam wzorzec wyliczania pojawia się w artykule o wydobywaniu tekstu, obrazów i czcionek z wczytanych plików PDF, gdzie zasila ekstrakcję zamiast naprawy

Co się dzieje, gdy czcionki nie ma w systemie?

EmbedMissingFonts pomija każdą czcionkę, której programu nie potrafi znaleźć, i zgłasza pominięcie przez swoją wartość zwracaną: jeśli liczba wróci niższa niż liczba policzonych przez ciebie czcionek nieosadzonych, różnicą są czcionki, których system nie ma. To uczciwy tryb niepowodzenia i jest lepszy od alternatyw, ponieważ wymyślenie zastępczego programu dla czcionki nazwanej w dokumencie zmieniłoby wygląd, a właśnie tego naprawa archiwalna nigdy robić nie może. Na te przypadki losLab PDF Library udostępnia EmbedFontProgramFromFile, które osadza dostarczony przez wywołującego plik .ttf albo .otf w nazwanej czcionce, dzięki czemu potok może dostarczyć czcionki firmowe, których spodziewa się napotkać, i celowo na nie przechodzić

Przepływ decyzyjny PDF Library for Delphi wyliczający każdy słownik czcionki, normalizujący nazwy BaseFont względem zainstalowanego systemu, osadzający znalezione programy przez EmbedFontProgram albo przechodzący na EmbedFontProgramFromFile i raportujący liczbę napraw
Normalizacja nazw czyni wyszukiwanie w systemie wyrozumiałym, a zwracana liczba utrzymuje pominięte kroje w polu widzenia
var
  I, FontID: Integer;
begin
  PDF.FindFonts;
  for I := 1 to PDF.FontCount do
  begin
    FontID := PDF.GetFontID(I);
    if (FontID > 0) and (PDF.SelectFont(FontID) = 1) then
      if PDF.GetFontIsEmbedded = 0 then
        // Spróbuj najpierw zainstalowanej czcionki systemowej, a potem
        // przejdź na plik czcionki dostarczany razem z aplikacją
        if PDF.EmbedFontProgram(PDF.FontName) = 0 then
          PDF.EmbedFontProgramFromFile(PDF.FontName,
            'fonts\CorporateSans.ttf');
  end;
end;

Dwie granice zasługują na jasne postawienie. Po pierwsze, czcionki Type1 nie są w obecnej implementacji naprawiane: ich wpis /FontFile wymaga trzysegmentowej struktury PFB z jawnymi kluczami długości, a biblioteka je pomija, zamiast zapisywać zniekształcony strumień; są rzadkie w nowoczesnych dokumentach, ale pojawiają się w starych archiwach. Po drugie, osadzenie czcionki jest aktem licencyjnym. Uprawnienia do osadzania czcionki TrueType należą do jej odlewni, a potok naprawczy, który upycha licencjonowane programy czcionek w dokumentach opuszczających organizację, powinien mieć kogoś, kto potwierdzi, że licencje czcionek faktycznie na to pozwalają. Biblioteka zrobi to, o co poprosisz; czy wolno ci prosić, to pytanie do twojego działu prawnego, a nie do kompilatora

Osadzanie jest konieczne, ale niewystarczające

Naprawa czcionek usuwa błąd 00030 i nic więcej. Dokument, który nie przechodzi PDF/A z powodu szyfrowania, brakujących metadanych XMP, przestrzeni barw zależnej od urządzenia bez OutputIntent albo nieobecnych map ToUnicode, nadal nie przejdzie po osadzeniu każdej czcionki, dlatego naprawa należy do pętli sterowanej preflightem, a nie zastępuje jej. Uruchom pełny raport, napraw to, co nazywa, i pozwól raportowi powiedzieć ci, kiedy skończyłeś. Jest też wymiar kosztowy: pełny program czcionki CJK sięga megabajtów, więc osadzenie kilku takich potrafi dramatycznie rozdąć niewielki dokument. Przeciwwagą jest tworzenie podzbiorów, opisane w artykule o optymalizacji rozmiaru pliku PDF i podzbiorach czcionek, które przycina każdy osadzony program do glifów faktycznie wyświetlanych w dokumencie

Jak uchronić nowe dokumenty przed regresją

SetEmbedAllFonts to zapobiegawcza połowa tej samej funkcji: zabezpieczenie po stronie zapisu, które powstrzymuje twój własny kod przed produkowaniem dokumentów naprawianych w tym artykule. Przy aktywnym SetEmbedAllFonts(1) każde kolejne wywołanie AddTrueTypeFont żądające Embed=0 jest podnoszone do odwołania osadzonego, co rozciąga na każdy dokument gwarancję, którą tryb PDF/A już wymusza. Działa na czcionki dodane po tym wywołaniu, a nie na czcionki już obecne we wczytanym pliku, więc podział pracy jest czysty: SetEmbedAllFonts dla dokumentów, które tworzysz, EmbedMissingFonts dla dokumentów, które dziedziczysz

PDF.NewDocument;
PDF.SetEmbedAllFonts(1);
// Od tego miejsca AddTrueTypeFont(Name, 0) zachowuje się
// jak AddTrueTypeFont(Name, 1): żadne nieosadzone
// odwołanie nie trafi do pliku wyjściowego

Obie połowy, zabezpieczenie po stronie zapisu i ścieżka wczytaj-napraw-zapisz, są częścią losLab PDF Library dla Delphi, C# i VB.NET, obok silnika preflight weryfikującego wynik; strona produktu zawiera pełne odniesienie do API czcionek wraz z wywołaniami osadzania i tworzenia podzbiorów dla pojedynczych czcionek