Artykuł techniczny

HotPDF: gdzie każda norma PDF składa swoje oświadczenie

Twój kod wykrywania raportuje, że plik PDF/UA od innego producenta niczego nie oświadcza, a skan PDF/R przestaje być PDF/R w chwili, gdy edytor dokleja do niego żółtą karteczkę. HotPDF czyta te oświadczenia z wczytanego dokumentu przez metody takie jak IsLoadedPDFUA, GetLoadedPDFR1Version i GetLoadedPDFDeclarations, z których każda zagląda tam, gdzie swój identyfikator składuje jej własna norma

Gdzie każda norma PDF zapisuje swój identyfikator

Normy podrzędne nigdy nie umówiły się co do jednego miejsca dla oświadczenia. Większość używa właściwości XMP w strumieniu /Metadata katalogu, kilka starszych wciąż opiera się na kluczach w słowniku informacji o dokumencie, PDF/R kładzie linię komentarza obok zwiastuna pliku, a generacja PDF 2.0 dorzuca dwa kolejne mechanizmy: słowniki rozszerzeń deweloperskich w katalogu i PDF Declarations wewnątrz XMP. Identyfikator to oświadczenie pliku o nim samym; czy treść dorówna oświadczeniu, decyduje walidator, a wytwarzanie zgodnego wyjścia PDF/A, PDF/X i PDF/UA to temat osobny. Do routingu (kolejka druku dla PDF/X, archiwum dla PDF/A, potok dostępności dla WTPDF) oświadczenie jest tym, czego potrzebujesz — byle odczytanym z właściwego miejsca:

NormaGdzie siedzi oświadczenieMetoda HotPDF
PDF/A (ISO 19005)XMP pdfaid:part i pdfaid:conformance, plus pdfaid:rev w PDF/A-4IsLoadedPDFA, GetLoadedPDFAVersion, GetLoadedPDFARevision
PDF/X (ISO 15930)GTS_PDFXVersion w słowniku Info dla PDF/X-1a i PDF/X-3, w XMP dla późniejszych częściIsLoadedPDFX, GetLoadedPDFXVersion
PDF/UA (ISO 14289)XMP pdfuaid:partIsLoadedPDFUA, GetLoadedPDFUAVersion
PDF/VT (ISO 16612-2)XMP pdfvtid:GTS_PDFVTVersionIsLoadedPDFVT, GetLoadedPDFVTVersion
PDF/E (ISO 24517-1)ISO_PDFEVersion, odczytywany ze słownika Info, katalogu albo XMPIsLoadedPDFE, GetLoadedPDFEVersion
PDF/R-1 (ISO 23504-1)komentarz %PDF-raster-x.y bezpośrednio nad ostatnim startxrefIsLoadedPDFR1, GetLoadedPDFR1Version
PDF/VCR-1 (ISO 16613-1)GTS_PDFVCRVersion w katalogu albo XMPIsLoadedPDFVCR, GetLoadedPDFVCRVersion
WTPDF 1.0, ISO/TS 32005wpisy pdfd:declarations w XMPIsLoadedWTPDF, GetLoadedWTPDFVersion, IsLoadedISO32005
Rozszerzenia deweloperskie PDF 2.0/Extensions /ISO_ w kataloguHasLoadedISODeveloperExtension, GetLoadedISODeveloperExtensionLevels
uses
  System.SysUtils, HPDFDoc;

procedure ReportClaims(const FileName: string; const Password: AnsiString);
var
  Pdf: THotPDF;
  PDFX, PDFXLevel: AnsiString;
begin
  Pdf := THotPDF.Create(nil);   // osobna instancja, używana wyłącznie do inspekcji
  try
    // Przeciążenie z hasłem odszyfrowuje strumienie, łącznie z pakietem XMP;
    // podaj '' dla plików otwierających się bez pytania
    if Pdf.LoadFromFile(FileName, Password) <= 0 then
      raise Exception.CreateFmt('Cannot open %s', [FileName]);

    if Pdf.IsLoadedPDFA then
      Writeln('PDF/A: ', Pdf.GetLoadedPDFAVersion);        // np. PDF/A-2b
    PDFX := Pdf.GetLoadedPDFXVersion;                       // najpierw XMP, potem Info
    if PDFX <> '' then
    begin
      PDFXLevel := Pdf.GetLoadedInfoKeyValue('GTS_PDFXConformance');
      if PDFXLevel <> '' then
        Writeln('PDF/X: ', PDFX, ', conformance ', PDFXLevel)
      else
        Writeln('PDF/X: ', PDFX);
    end;
    if Pdf.IsLoadedPDFUA then
      Writeln('PDF/UA: ', Pdf.GetLoadedPDFUAVersion);      // np. PDF/UA-1
    if Pdf.IsLoadedPDFVT then
      Writeln('PDF/VT: ', Pdf.GetLoadedPDFVTVersion);
    if Pdf.IsLoadedPDFE then
      Writeln('PDF/E: ', Pdf.GetLoadedPDFEVersion);
    if Pdf.IsLoadedPDFR1 then
      Writeln('PDF/R: ', Pdf.GetLoadedPDFR1Version);       // np. PDF/R-1.0
    if Pdf.IsLoadedWTPDF then
      Writeln('WTPDF: ', Pdf.GetLoadedWTPDFVersion);
  finally
    Pdf.Free;
  end;
end;

Do inspekcji użyj osobnej instancji THotPDF, jak robi to przykład. Funkcje IsLoaded… odpowiadają za dokument, który komponent akurat trzyma, także za taki, który właśnie buduje: IsLoadedPDFUA jest też True, gdy PDFUACompliance zostało włączone dla wyjścia, a IsLoadedPDFX zgadza się z niepustym PDFXCompliance. Wygodne w trakcie budowania, ale na komponencie używanym do obu robót miesza twoje ustawienia wyjścia z wyrokiem o cudzym pliku

PDF/X to ta jedna rodzina, której identyfikator wędrował między częściami. PDF/X-1a i PDF/X-3 to profile PDF 1.3, które kładą GTS_PDFXVersion w słowniku informacji o dokumencie i w ogóle nie potrzebują XMP, podczas gdy PDF/X-4 i późniejsze części wymagają właściwości XMP pdfx:GTS_PDFXVersion. GetLoadedPDFXVersion zagląda najpierw do XMP, pod prefiksem pdfx albo pdfxid, potem do słownika Info, potem do kluczy katalogu pisanych przez niektórych starszych producentów, i zwraca zadeklarowaną wartość bez zmian. PDF/X-1a ma jedną osobliwość, którą warto obsłużyć w kodzie: jego wartość wersji to PDF/X-1:2001, a poziom zgodności PDF/X-1a:2001 mieszka we własnym kluczu, /GTS_PDFXConformance, który przykład czyta przez GetLoadedInfoKeyValue, gdy musi odróżnić X-1a od zwykłego X-1

Gdzie każda norma PDF zapisuje swój identyfikator zgodności i jak HotPDF dla Delphi przeszukuje XMP, słownik Info i klucze katalogu we wczytanym PDF
Każda norma składuje swoje oświadczenie tam, gdzie wybrała jej specyfikacja, a czytelnik przeszukuje miejsca, między którymi oświadczenie może wędrować, w stałej kolejności

Jak wygląda poprawne oświadczenie w XMP?

XMP to RDF zserializowany jako XML, a RDF/XML pozwala zapisać prostą właściwość na dwa sposoby wewnątrz rdf:Description: jako element podrzędny, <pdfuaid:part>1</pdfuaid:part>, albo jako atrybut samego opisu, pdfuaid:part="1", w pojedynczych albo podwójnych cudzysłowach. ISO 16684-1 akceptuje oba, a pakiet może rozdzielić swoje właściwości między kilka elementów rdf:Description. Ten rozdział nie jest hipotetyczny. HotPDF zapisuje formę elementu podrzędnego dla PDF/UA, PDF/VT, PDF/E i PDF/X, podczas gdy korpus testowy veraPDF i wielu zewnętrznych producentów pisze formę skróconą, z opisami takimi jak pdfuaid:part="1" albo pdfvtid:GTS_PDFVTVersion="PDF/VT-1", niosącymi całe oświadczenie w atrybutach. Detektor zbudowany wokół Pos('<pdfuaid:part>', Xmp) i ćwiczony wyłącznie na własnym wyjściu przechodzi każdą lokalną kontrolę, po czym na pierwszej próbce veraPDF raportuje brak oświadczenia. Czytnik XMP HotPDF obsługuje obie formy dla każdej metody z tabeli. GetLoadedPDFAVersion skleja pdfaid:part i pdfaid:conformance w identyfikatory takie jak PDF/A-1b, PDF/A-2u, PDF/A-4 albo PDF/A-4f, a GetLoadedPDFARevision zwraca pdfaid:rev — rok edycji, którego PDF/A-4 wymaga (2020), a części 1-3 nie definiują:

procedure ReportPDFALevel(Pdf: THotPDF);
var
  Level, Revision: AnsiString;
begin
  Level := Pdf.GetLoadedPDFAVersion;      // np. 'PDF/A-1b', 'PDF/A-2u', 'PDF/A-4f', ...
  if Level = '' then
    Exit;
  Revision := Pdf.GetLoadedPDFARevision;  // '2020' dla PDF/A-4, pusto dla części 1-3
  if Revision <> '' then
    Writeln(Level, ', edition ', Revision)
  else
    Writeln(Level);
  // Oczekiwane: "PDF/A-2b" niezależnie od tego, czy producent zapisał
  // <pdfaid:part>2</pdfaid:part> czy pdfaid:part="2"
end;

Prefiksy to druga ruchoma część. W XML prefiks to tylko lokalny alias; tym, co identyfikuje schemat PDF/A, jest URI przestrzeni nazw http://www.aiim.org/pdfa/ns/id/, który zatwierdzone erraty ISO 19005-4 dopuszczają też w zapisie https://, a producent może powiązać go z dowolnym prefiksem, na jaki ma ochotę. GetLoadedPDFAVersion najpierw odszukuje prefiks, który pakiet deklaruje dla tego URI, zanim przeczyta część i poziom, więc pakiet piszący pdfa:part zamiast pdfaid:part nadal się identyfikuje. Każda inna właściwość XMP, którą sam odczytujesz z zdekodowanego pakietu zwracanego przez GetLoadedXMPMetadata, zasługuje na tę samą staranność: obie serializacje, prefiks rozwiązany z jego deklaracji xmlns i dozwolone białe znaki wokół znaku równości

Dwie poprawne serializacje jednego oświadczenia PDF/UA w XMP obok reguły przestrzeni nazw, dzięki której zmieniony prefiks nadal identyfikuje PDF/A dla HotPDF w Delphi
Zapis elementem podrzędnym i zapis atrybutem są równie poprawne, a schemat identyfikuje URI przestrzeni nazw, nie prefiks

Zaszyfrowane pliki chowają oświadczenie przed szukaniem po bajtach

Skan surowego pliku pod kątem pdfaid albo ISO_PDFEVersion pomija parsowanie i łamie się na dwóch zwyczajnych przypadkach. Większość strumieni XMP jest skompresowana Flate, więc nazwy właściwości nigdy nie pojawiają się na dysku jako tekst. A w zaszyfrowanym dokumencie strumień metadanych to szyfrogram: ISO 32000-2 §7.6.4.2 definiuje EncryptMetadata z domyślną wartością true, więc jeśli producent wyraźnie się z tego nie wyłączył, pakiet jest szyfrowany razem z każdym innym strumieniem. Przeszukiwanie bajtów zaszyfrowanego pliku PDF/E-1 raportuje więc oświadczenie ISO_PDFEVersion jako nieobecne, choć ono tam jest. Pewna kolejność to: wczytaj, odszyfruj, zdekoduj, szukaj. Otwórz plik przez przeciążenie z hasłem, LoadFromFile(FileName, Password), i podaj pusty ciąg dla plików niosących tylko hasło właściciela, które w dowolnej przeglądarce otwierają się bez pytania. Jednoargumentowe LoadFromFile(FileName) wczytuje strukturę dokumentu bez odszyfrowywania strumieni, co zostawia pakiet XMP nieczytelny; po przeciążeniu z hasłem GetLoadedXMPMetadata i każda metoda z wyżej działają na odszyfrowanym, zdekompresowanym pakiecie

Dlaczego plik PDF/R gubi tożsamość po edycji?

PDF/R-1 identyfikuje się w sposób, jakiego nie stosuje żadna inna norma podrzędna. ISO 23504-1 w klauzuli 5 wymaga linii komentarza o treści %PDF-raster-1.0 (albo innej pasującej wersji) w linii bezpośrednio przed ostatnią linią z startxref. Parser PDF wyrzuca komentarze, gdy czyta dane odsyłaczy, więc model obiektów nie ma czego raportować; dowód istnieje tylko w surowych bajtach na końcu pliku. GetLoadedPDFR1Version wraca do źródła, z którego wczytano dokument, czyta najwyżej ostatnie 64 KiB, znajduje ostatni startxref i sprawdza linię nad nim. A teraz prześledź aktualizację przyrostową. Edytor dodający adnotację dokłada nowe obiekty, nową sekcję odsyłaczy, nowy zwiastun i nowy startxref po oryginalnym %%EOF. Linia %PDF-raster-1.0 dalej jest w pliku, ale siedzi teraz nad przedostatnim startxref, a norma nie zostawia wątpliwości, która się liczy. Oczekiwane: IsLoadedPDFR1 zwraca False po edycji, chyba że narzędzie edytujące wysyła komentarz ponownie we własnej aktualizacji. Detektor skanujący cały plik pod kątem %PDF-raster- powiedziałby tak, a według reguły z klauzuli 5 ta odpowiedź jest zła. Proces pozyskiwania obrazów, który musi trzymać swoje pliki w PDF/R, składa adnotacje do pliku sidecar albo do narzędzia, które przepisuje cały plik jako PDF/R

Ogon pliku przed i po aktualizacji przyrostowej: komentarz PDF/R zostaje, ale gubi ostatnią kotwicę startxref, którą HotPDF sprawdza w Delphi
Edytor dokładający aktualizację zostawia komentarz PDF/R nad przedostatnim startxref, gdzie norma już go nie uznaje

Rozszerzenia deweloperskie: poziomy całkowite, nazwy i tablice

Dokumenty PDF 2.0 ogłaszają specyfikacje techniczne ISO, na których się opierają (skróty SHA-3 z ISO/TS 32001, podpisy ECC z ISO/TS 32002, zasoby glTF z ISO/TS 32007) przez słownik /Extensions katalogu, zdefiniowany w ISO 32000-2 §7.12. Pod zarejestrowanym prefiksem /ISO_ tabela 48 pozwala albo na pojedynczy słownik rozszerzeń deweloperskich, albo — nowość w PDF 2.0 — na tablicę takich słowników, a tabela 49 razem z §7.12.5 czyni /ExtensionLevel liczbą całkowitą. Czytnik obsługujący tylko jeden z tych dwóch kształtów pojemnika gubi każdy plik zapisany drugim. HasLoadedISODeveloperExtension i GetLoadedISODeveloperExtensionLevels przyjmują oba i po drodze rozwiązują odwołania pośrednie, choć §7.12.1 każe, by te słowniki były obiektami bezpośrednimi:

procedure ReportISOExtensions(Pdf: THotPDF);
var
  Levels: THPDFIntegerArray;
  Revision, URL: AnsiString;
  I: Integer;
begin
  Levels := Pdf.GetLoadedISODeveloperExtensionLevels;
  for I := 0 to High(Levels) do
    Writeln('ISO_ extension level ', Levels[I]);

  if Pdf.HasLoadedISODeveloperExtension(32001, Revision, URL) then
    Writeln('ISO/TS 32001 revision ', Revision, ', documented at ', URL);
  // Oczekiwane dla pliku zapisanego przez EnsureISO32001Extensions:
  //   ISO/TS 32001 revision :2022, documented at https://www.iso.org/standard/45874.html
  if Pdf.HasLoadedISODeveloperExtension(32007) then
    Writeln('glTF 3D assets per ISO/TS 32007');
end;

Pierwsze wydanie ISO/TS 32007:2024 ma w §4.8 przykład, który zapisuje /ExtensionLevel /32007 jako nazwę; PDF Errata Issue #835 poprawiła to na liczbę całkowitą 32007, ale producent, który skopiował oryginalny przykład, dalej zapisuje nazwę. Czytnik HotPDF spada więc na parsowanie wartości nazwy albo łańcucha, z ukośnikiem na czele albo bez, gdy wyszukiwanie po liczbie całkowitej wróci puste, więc HasLoadedISODeveloperExtension(32007) znajduje oba zapisy. Druga errata dotyczy /URL: tabela 49 w ISO 32000-2:2020 wymienia /URL jako opcjonalny, a Issue #732 awansuje go do obowiązkowego. Po stronie zapisu HotPDF wysyła formę całkowitą i /URL w każdym wywołaniu EnsureISO…Extensions; po stronie odczytu pusty parametr wyjściowy URL to norma dla plików zapisanych według tekstu z 2020, więc traktuj go jako informację i nigdy jako powód do odrzucenia rozszerzenia

Który URI WTPDF zapisał producent?

Oświadczenia WTPDF 1.0 i ISO/TS 32005 to PDF Declarations: wpisy w torbie pdfd:declarations w XMP, pod przestrzenią nazw http://pdfa.org/declarations/ zdefiniowaną przez notę techniczną PDF Declarations od PDF Association. Każdy wpis niesie URI conformsTo i opcjonalne pola claimBy, claimDate, claimCredentials oraz claimReport, które GetLoadedPDFDeclarations zwraca jako tablicę rekordów THPDFDeclarationEntry; po stronie zapisu AddPDFDeclaration wkłada te same pięć pól do pakietu XMP, obok przestrzeni nazw identyfikacyjnych PDF 2.0, PDF/A-4 i PDF/UA-2. Cięcie torby na wpisy zwykłym szukaniem po tekście ma własny haczyk: potnij przy każdym wystąpieniu conformsTo, a znacznik zamykający </pdfd:conformsTo> formy elementu podrzędnego zostanie uznany za początek nowego wpisu, co utnie pierwszą deklarację przed jej własnym końcem. Czytnik HotPDF przeskakuje nad znacznikami zamykającymi i akceptuje obie serializacje

Samo URI to twardsza część. To samo oświadczenie dochodzi do ciebie jako http:// albo https://, a WTPDF 1.0 Issue #395 zmienił, czy / siedzi przed #, więc pliki zapisane przed erratą i po niej różnią się tym jednym znakiem; zwykłe porównanie ciągów bierze je za różne normy. HasLoadedPDFDeclaration porównuje URI bez rozróżniania wielkości liter, sprowadza https:// do http:// i zrzuca / tuż przed # po obu stronach, więc zapytanie poniżej łapie każdy zapis, na jaki zdecydował się producent. Fragment zostaje nienaruszony: #accessibility1.0 i #accessibility1.0-validated to różne oświadczenia, z których drugie głosi, że walidator sprawdził plik, a pytać powinieneś o to, na którym zależy twojemu procesowi. GetLoadedWTPDFVersion zlewa oba w 'WTPDF 1.0 Accessibility' albo 'WTPDF 1.0 Reuse' na potrzeby routingu

procedure ReportDeclarations(Pdf: THotPDF);
var
  Decls: THPDFDeclarationEntryArray;
  I: Integer;
begin
  Decls := Pdf.GetLoadedPDFDeclarations;
  for I := 0 to High(Decls) do
    Writeln(Decls[I].ConformsTo, ' claimed by ', Decls[I].ClaimBy,
      ' on ', Decls[I].ClaimDate);

  // Łapie http i https, z ukośnikiem przed '#' albo bez
  if Pdf.HasLoadedPDFDeclaration(
    'https://pdfa.org/declarations/wtpdf#accessibility1.0') then
    Writeln('WTPDF 1.0 Accessibility (unvalidated claim)');

  // Obejmuje zwykłe i warianty -validated dla Reuse i Accessibility
  Writeln('WTPDF profile: ', Pdf.GetLoadedWTPDFVersion);
end;

Dla procesu pozyskiwania obrazów albo routera druku kolejność operacji znaczy więcej niż jakakolwiek pojedyncza metoda: sprawdź IsLoadedPDFR1 na pliku takim, jaki przyszedł, zanim krok z adnotacją albo pieczątką dokłada swoją aktualizację, a PDF/X kieruj według GetLoadedPDFXVersion, które obejmuje słownik Info zleceń PDF/X-1a i PDF/X-3 oraz równie dobrze XMP zleceń PDF/X-4, czytając /GTS_PDFXConformance tylko tam, gdzie X-1a trzeba odróżnić od X-1. Obie kontrole działają na każdym dokumencie wczytanym do komponentu PDF HotPDF dla Delphi, łącznie z zaszyfrowanymi plikami otwartymi z hasłem