Artykuł techniczny

Generowanie PDF/X-1a, X-3 i X-4 w Delphi z PDFlibPas

PDFlibPas produkuje pliki PDF/X do druku z Delphi jednym wywołaniem, TPDFlib.SetPDFXMode, które wybiera PDF/X-1a:2003, PDF/X-3:2003 albo PDF/X-4. To wywołanie zapisuje identyfikację XMP pdfxid, dodaje output intent GTS_PDFX, deklaruje /Trapped /False, przypina wersję PDF i od tej chwili odmawia wywołań rysujących zabronionych przez wybraną część. CheckFileCompliance z testem 3 audytuje zapisany plik

To krótka odpowiedź. Dłuższa ma znaczenie, bo drukarnie nie odrzucają plików za brzydotę, odrzucają je na preflighcie, zwykle w piątek, zwykle o czymś, co kod generujący mógł wyłapać sam: RGB-owe logo w zleceniu X-1a, font, do którego się odwołano zamiast go osadzić, strona bez formatu cięcia. PDF/X istnieje po to, by takie porażki były strukturalne, a nie wizualne, a rola biblioteki polega na tym, żebyś w ogóle ich nie wyprodukował

Na jaką część PDF/X powinna celować aplikacja Delphi?

Wybieraj PDF/X-4, dopóki odbierający workflow nie powie inaczej, i wybieraj PDF/X-1a, gdy powie. Trzy części różnią się dokładnie wzdłuż trzech osi: koloru, przezroczystości i bazowej wersji PDF, a każda oś mapuje się na bramkę wewnątrz PDFlibPas:

  • PDF/X-1a:2003 (ISO 15930-4): składnia PDF 1.4, wyłącznie CMYK, szarości i kolory spot, bez koloru opartego na ICC, bez przezroczystości. Plik jest już rozseparowany na kolory maszynowe, więc drukarnia nie robi żadnej konwersji koloru
  • PDF/X-3:2003 (ISO 15930-6): nadal PDF 1.4 i nadal bez przezroczystości, ale przestrzenie zarządzane kolorem są dozwolone, a device RGB jest akceptowany, gdy sam output intent opisuje warunek RGB
  • PDF/X-4 (ISO 15930-7): składnia PDF 1.6, z żywą przezroczystością i treścią opcjonalną (warstwami). To część, dla której buduje się nowoczesne RIP-y, bo spłaszczanie przezroczystości na wyjściu było źródłem sporej części dawnego bólu X-1a

X-1a jest wciąż wszędzie w dostarczaniu reklam i w starszych workflow drukarskich, które chcą plik do naświetlenia bez myślenia. X-4 to lepszy inżynierski wybór, gdziekolwiek drukarnia go przyjmuje: przezroczystość przetrwa nietknięta, a decyzje kolorystyczne zapadają na RIP-ie z prawdziwym profilem maszynowym, a nie w twoim kodzie. Numery trybów to odpowiednio 1, 2 i 3, a GetInformation(203) raportuje z powrotem aktywny tryb. PDFlibPas definiuje też wyższe numery trybów dla rodziny PDF/X-6 na PDF 2.0, które podlegają innym regułom wersji i output intentu i są poza zakresem tego artykułu

Co SetPDFXMode zapisuje do pliku

TPDFlib.SetPDFXMode ogarnia identyfikację i boilerplate, które czytnik PDF/X sprawdza w pierwszej kolejności, więc kod aplikacji musi dostarczyć tylko prawdziwy profil maszynowy i treść. Konkretnie, udane wywołanie robi cztery rzeczy:

  • Zapisuje string edycji do XMP jako pdfxid:GTS_PDFXVersion (PDF/X-1a:2003, PDF/X-3:2003 albo PDF/X-4). Edycje 2003 identyfikują część całkowicie przez ten string, więc osobna właściwość zgodności nie jest zapisywana
  • Dodaje output intent, jeśli dokument go nie ma, i ustawia jego podtyp na /S /GTS_PDFX, podtyp, którego szukają czytniki PDF/X (ISO 32000-1 §14.11.5 definiuje słownik output intentu)
  • Ustawia /Trapped /False w słowniku informacji o dokumencie, bo writer nigdy nie trapuje, a PDF/X wymaga, by stan trapowania był zadeklarowany, a nie pozostawiony nieznany
  • Przypina bazę wersji: 1.4 dla X-1a i X-3, 1.6 dla X-4

Dwie kolejne naprawy dzieją się w momencie zapisu. Każda strona bez TrimBox dostaje jeden równy swojemu MediaBox, bo PDF/X wymaga zadeklarowanego formatu wykończenia na każdej stronie (ISO 32000-1 §14.11.2 pokrywa granice strony), a adnotacje dostają flagę druku oczekiwaną przez standard. Fallback TrimBox to siatka bezpieczeństwa, nie decyzja layoutowa. Zlecenie full-bleed potrzebuje prawdziwego pola cięcia wewnątrz większego pola mediów, a przewodnik po domyślnych TrimBox, BleedBox i CropBox w PDFlibPas omawia, jak ustawić te pola i jak dziedziczenie potrafi zaskoczyć

uses
  PDFlibrary;

procedure BuildX1aAd(const OutFile, PressProfile: string);
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.SetPDFXMode(1) <> 1 then          // 1 = PDF/X-1a:2003
      raise Exception.CreateFmt('PDF/X mode refused (error %d)',
        [Pdf.LastErrorCode]);

    // Podmień domyślny profil na prawdziwy warunek maszynowy
    if Pdf.LoadOutputIntentProfile(PressProfile, 'DeviceCMYK') <> 1 then
      raise Exception.Create('Could not load the CMYK output profile');

    // Embed = 1 osadza pełny program fontu; PDF/X nie przyjmie 0
    if Pdf.AddTrueTypeFont('Arial {1252}', 1) = 0 then
      raise Exception.Create('Font could not be loaded for embedding');

    Pdf.SetTextSize(24);
    Pdf.SetFillColorCMYK(0, 1, 1, 0);           // procesowy czerwony, bez RGB
    Pdf.DrawText(72, 720, 'Spring catalog, page 1');

    Pdf.SaveToFile(OutFile);
  finally
    Pdf.Free;
  end;
end;

Kolejność w tym szkicu jest świadoma. LoadOutputIntentProfile podmienia profil output intentu, który już istnieje, więc musi się uruchomić po tym, jak SetPDFXMode go utworzy. To wywołanie nie potrafi też czarować intentu z niczego: na dokumencie bez output intentu po prostu zwraca 0

Dlaczego domyślny output intent trzeba podmienić?

Output intent, który SetPDFXMode dodaje dla trybów 1 do 3, deklaruje DeviceCMYK, ale bajty ICC za nim to dołączony do biblioteki profil sRGB z przepisaną na 4 liczbą komponentów. Ten domyślny istnieje dla wstecznej zgodności i wystarcza, żeby plik był strukturalnie PDF/X, tylko że osadzony profil nie opisuje żadnej drukarni. Narzędzie preflight, które zagląda w nagłówek profilu, zobaczy profil RGB w mundurku z etykietą CMYK, a drukarnia polegająca na incencie przy proofach dostanie bzdury

Traktuj więc LoadOutputIntentProfile(FileName, 'DeviceCMYK') jako obowiązkowe przy prawdziwej pracy drukarskiej i podaj profil ICC, który twój dostawca druku wskazuje dla danego papieru i maszyny. TPDFlib.LoadOutputIntentProfile podmienia strumień DestOutputProfile i aktualizuje /N, a resztę słownika intentu zostawia jak była. W szczególności identyfikator warunku wyjściowego i stringi opisowe domyślnego intentu wciąż niosą wartości w stylu sRGB, więc jeśli twoja drukarnia matchuje po identyfikatorze, a nie po osadzonym profilu, ustal z nią przed wysyłką, co dokładnie sprawdza. To prawdziwa luka domyślnych z ery 2003, nie coś, co twój kod zrobił źle

Pułapka pierwsza: wersja, której nie wybrałeś

Nowy dokument PDFlibPas startuje od writerowego domyślnego PDF 1.5, który jest ponad bazą 1.4 wymaganą przez PDF/X-1a i PDF/X-3. TPDFlib.SetPDFXMode załatwia to po cichu w zwykłym przypadku: jeśli wersja zapisu nie została zablokowana, obniża wersję dokumentu do bazy części i jedzie dalej. Pierwsza implementacja zamiast tego odmawiała i każde SetPDFXMode(1) na świeżym dokumencie zwracało 0, co jest dobrym przykładem reguły poprawnej na papierze i bezużytecznej w praktyce

Odmowa nadal istnieje dla tego jednego przypadku, w którym jest słuszna. Jeśli coś wcześniej wywołało LockSaveVersion z wersją ponad bazę, SetPDFXMode zwraca 0 z LastErrorCode 601, bo wywołujący przypiął wersję celowo, a biblioteka nie będzie tego nadpisywać. We wspólnym kodzie konfiguracji dokumentu, gdzie helper blokuje wersję z powodów niezwiązanych z PDF/X, dokładnie tu umiera zlecenie X-1a:

Pdf := TPDFlib.Create;
try
  Pdf.LockSaveVersion('1.7');     // ustawione przez generyczny helper "domowych domyślnych"
  if Pdf.SetPDFXMode(1) = 0 then
    // LastErrorCode = 601: 1.7 zablokowane, PDF/X-1a potrzebuje 1.4.
    // Pomijaj blokadę przy zleceniach drukarskich. PDF/X-4 to tu nie ratunek:
    // jego baza 1.6 też jest pod blokadą 1.7 i pada tak samo.
    LogError('PDF/X refused, error %d', [Pdf.LastErrorCode]);
finally
  Pdf.Free;
end;

Ta sama bramka tłumaczy pokrewne ograniczenie. Tryb PDF/X to przełącznik tworzenia dokumentu: SetPDFXMode działa na nowym dokumencie i zwraca 0 na takim wczytanym z pliku, bo biblioteka nie może ręczyć za treść, której nie napisała. Konwersja istniejącego PDF-a do PDF/X to inna robota, wymagająca inspekcji i naprawy, nie flagi

Pułapka druga: bramki, które odmawiają po cichu

Gdy tryb PDF/X jest aktywny, PDFlibPas odmawia każdego wywołania treści zabronionego przez wybraną część, w momencie, gdy je robisz, zamiast pozwolić plikowi polec na preflighcie później. Odmowy obejmują wypełnienia i kontury w device RGB (całe X-1a; X-3 i X-4, chyba że output intent jest RGB), kolory oparte na ICC w X-1a, przezroczystość i tryby mieszania w X-1a i X-3, treść opcjonalną w X-1a i X-3, akcje JavaScript, załączniki plikowe, filtr obrazów LZW, fonty Standard 14, które poszłyby nieosadzone, oraz szyfrowanie z kluczem publicznym. Szyfrowanie hasłem przez Encrypt nie jest bramkowane w czasie tworzenia, ale walidator flaguje każdy zaszyfrowany plik, więc trzymaj zlecenia drukarskie niezaszyfrowane

Czego odmowy nie współdzielą, to kod błędu, i to jest pułapka. Bramki przestrzeni barw w SetFillColor, SetLineColor, wywołaniach koloru opartego na ICC oraz EncryptPublicKey ustawiają LastErrorCode na 601, kod zgodności biblioteki, którego używają też bramki PDF/A. Bramki przezroczystości, JavaScriptu, załączników, LZW i AddStandardFont po prostu zwracają 0 i zostawiają LastErrorCode nietknięte, co znaczy, że może on wciąż trzymać wartość z wcześniejszego wywołania. Kod, który po paczce wywołań rysujących sprawdza tylko LastErrorCode, przegapi połowę odmów, więc sprawdzaj każdą wartość zwracaną:

procedure Require(Ok: Integer; const What: string);
begin
  if Ok = 0 then
    raise EPrintConformance.CreateFmt('%s refused under PDF/X', [What]);
end;

Require(Pdf.SetFillColorCMYK(0, 0, 0, 1), 'CMYK fill');
Require(Pdf.SetTransparency(40), 'transparency');   // 0 w X-1a / X-3
Require(Pdf.AddLinkToJavaScript(72, 72, 120, 20,
  'app.alert(1)', 0), 'JavaScript link');           // 0 w każdej części

Fonty zasługują na własne ostrzeżenie. AddStandardFont zwraca 0 pod PDF/X, bo biblioteka nie dostarcza konturów Standard 14 i nie może ich osadzić. AddTrueTypeFont zachowuje się inaczej: w trybie PDF/A podbija Embed = 0 do pełnego osadzenia za ciebie, ale w trybie PDF/X już nie, więc systemowy font, do którego się tylko odwołano, trafia do pliku i wyłapuje go dopiero walidator. Przekazuj sam 1 (pełne) albo 2 (podzbiór) albo wywołaj raz przy konfiguracji SetEmbedAllFonts(1), żeby zbłąkane 0 nie przecisnęło się przez bramkę

Jak zweryfikować plik PDF/X, zanim pójdzie do drukarni?

Puść CheckFileCompliance z ComplianceTest = 3 na zapisanym pliku. TPDFlib.CheckFileCompliance zwraca 0, gdy nie zanotował ustaleń, a w przeciwnym razie uchwyt listy stringów, której pozycje to linie z prefiksem PDFX-. Audyt PDF/X czyta plik tym samym streamingowym czytnikiem co preflight PDF/A i PDF/UA w PDFlibPas i sprawdza reguły, które drukarski preflight sprawdza w pierwszej kolejności:

  • Plik nie jest zaszyfrowany, XMP niesie znany GTS_PDFXVersion, a wersja PDF nie przekracza bazy części
  • Istnieje dokładnie jeden output intent, jego podtyp to GTS_PDFX, a on niesie profil docelowy i identyfikator warunku wyjściowego
  • Słownik informacji deklaruje /Trapped
  • Każda strona ma TrimBox mieszczący się wewnątrz MediaBox, a BleedBox, jeśli jest, obejmuje TrimBox
  • Każdy font użyty na stronie ma osadzony program, przy czym fonty Type0 są sprawdzane przez ich font potomny
  • Żadnych zabronionych typów adnotacji (RichMedia, Screen, 3D, Movie, Sound, FileAttachment), żadnej akcji otwarcia JavaScript ani JavaScriptu poziomu dokumentu, żadnych osadzonych plików i żadnego filtra LZWDecode gdziekolwiek w grafie obiektów
function PreflightPDFX(Pdf: TPDFlib; const FileName: string;
  Report: TStrings): Boolean;
var
  ListId, I: Integer;
begin
  ListId := Pdf.CheckFileCompliance(FileName, '', 3, 0);  // 3 = PDF/X
  Result := ListId = 0;
  if Result then
    Exit;
  try
    for I := 1 to Pdf.GetStringListCount(ListId) do       // indeksy liczone od 1
      Report.Add(Pdf.GetStringListItem(ListId, I));
  finally
    Pdf.ReleaseStringList(ListId);
  end;
end;

Miej jasność, co znaczy ten przebieg. Walidator PDFlibPas audytuje strukturalne reguły wypisane wyżej i to one wyłapują błędy generatora. To nie jest preflight wierny kolorystycznie: nie sprawdza sumarycznego nakładu farby, rozdzielczości obrazów ani tego, czy osadzony profil pasuje do warunku, na którym drukuje twoja drukarnia. Używaj go jako bramki w buildzie albo potoku eksportu, a sprawdzenia specyficzne dla maszyny zostaw profilowi preflight drukarni, bo ten i tak puści własny

FAQ

Czy PDFlibPas wspiera przezroczystość PDF/X-4?

Tak. Przy SetPDFXMode(3) PDFlibPas pozwala na przezroczystość, tryby mieszania i grupy treści opcjonalnej oraz zapisuje plik PDF 1.6. Te same wywołania zwracają 0 pod PDF/X-1a i PDF/X-3

Czy PDFlibPas skonwertuje istniejący PDF do PDF/X?

Nie. SetPDFXMode dotyczy tylko dokumentów utworzonych w bieżącej sesji TPDFlib i zwraca 0 dla pliku wczytanego. Każdy istniejący PDF możesz nadal audytować przez CheckFileCompliance z testem 3

Jaki kod błędu zwraca odmówione wywołanie PDF/X?

Odmowy przestrzeni barw i szyfrowania z kluczem publicznym ustawiają LastErrorCode na 601. Pozostałe bramki, jak przezroczystość, JavaScript i fonty Standard 14, tylko zwracają 0, więc zawsze testuj wynik funkcji

Wyjście PDF/X, walidator PDF/X i reszta zestawu narzędzi drukarskich i archiwizacyjnych są częścią PDFlibPas, natywnej biblioteki PDF w Object Pascal od losLab dla Delphi. Zobacz stronę produktu PDFlibPas Delphi PDF Library, gdzie są edycje, platformy i pełne odniesienie API