Artykuł techniczny

Pliki powiązane na poziomie strony w PDF 2.0 z PDFlibPas

PDFlibPas przypina plik osadzony do jednej konkretnej strony, a nie do dokumentu jako całości, wpisując tablicę /AF do słownika strony, podczas gdy sama zawartość pozostaje zarejestrowana w drzewie nazw EmbeddedFiles dokumentu. Tego rozdziału opisuje ISO 32000-2 §14.13 i to on pozwala czytnikowi odpowiedzieć na pytanie, na które załącznik poziomu dokumentu nie odpowie: do której strony te dane należą

Przypadki użycia są bardziej konkretne niż ogólne załączniki. Raport z badań, w którym każda strona wozi ze sobą surowe serie pomiarowe stojące za jej wykresem. Zeskanowana paczka, w której każda strona trzyma wynik OCR, z którego powstała jej warstwa tekstu. Zestaw rysunków, w którym każdy arkusz dowiezie wyciąg z CAD-a, z którego został wyrenderowany. W każdym z tych przypadków lista załączników poziomu dokumentu byłaby stertą plików z numerami stron zakodowanymi w nazwach, czyli konwencją, a nie strukturą

Jeden payload, dwa miejsca odwołań

Kluczowy punkt strukturalny: powiązanie na poziomie strony nie tworzy drugiej kopii czegokolwiek. Plik jest osadzany raz i rejestrowany w drzewie nazw EmbeddedFiles dokładnie tak jak załącznik poziomu dokumentu, z użyciem tej samej maszynierii specyfikacji pliku. Różni się miejsce, w które trafiają referencja i jej klucz relacji: do słownika strony zamiast do katalogu dokumentu

Płyną z tego dwie konsekwencje. Po pierwsze, czytnik znający wyłącznie załączniki poziomu dokumentu i tak znajdzie payload, bo leży w drzewie nazw, czyli tam, gdzie taki czytnik zagląda. Po drugie, wyczyszczenie powiązania strony usuwa wiązanie, a nie plik. ClearPageAssociatedFiles odczepia stronę od jej plików powiązanych i zostawia payload osiągalne przez drzewo nazw, czyli zachowuje się konserwatywnie: operacja, która mówi wyczyść powiązanie, nie powinna po cichu niszczyć danych, na które może wskazywać inna część dokumentu

Struktura pliku powiązanego na poziomie strony w dokumencie PDF 2.0 zapisanym przez PDFlibPas: payload jest osadzany raz i rejestrowany w drzewie nazw EmbeddedFiles pod katalogiem dokumentu, podczas gdy słownik strony niesie tablicę /AF referencjonującą tę samą specyfikację pliku z kluczem AFRelationship, dzięki czemu ClearPageAssociatedFiles odczepia wiązanie bez niszczenia danych
Powiązanie na poziomie strony dodaje drugą referencję, a nie drugą kopię: czytniki znające tylko załączniki poziomu dokumentu nadal znajdą payload w drzewie nazw, a wyczyszczenie wiązania strony pozostawia osadzony strumień osiągalny

Ta funkcja ma jeden świadomie wąski warunek sukcesu, o którym warto wiedzieć. Zgłasza sukces tylko wtedy, gdy strona faktycznie nosiła klucz /AF. Strona, która nigdy nie miała powiązań, zwraca porażkę zamiast radosnego potwierdzenia, więc wołający nie weźmie no-opu za dokończone sprzątanie

var
  Lib: TPDFlib;
  Idx, I: Integer;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.LoadFromFile('survey-report.pdf');

    // Dołącz serie pomiarowe, z których powstał wykres na stronie 3
    Idx := Lib.AddPageAssociatedFileFromFile(3,
      'series-03.csv',            // plik na dysku
      'measurements.csv',         // nazwa wyświetlana w PDF
      'text/csv',                 // typ MIME
      'Raw measurement series for figure 3',
      'Data');                    // AFRelationship, ISO 32000-2 14.13

    if Idx < 0 then
      raise Exception.Create('page association refused');

    for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
      Writeln('page 3 associated file, embedded index ',
        Lib.GetPageAssociatedFileEmbeddedIndex(3, I));

    Lib.SaveToFile('survey-report-with-data.pdf');
  finally
    Lib.Free;
  end;
end;

Ciąg relacji nie jest w praktyce dowolnym tekstem. ISO 32000-2 definiuje słownik: Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema i Unspecified, a konsumenci po nim kluczykują. Data dla liczb stojących za wykresem, Source dla dokumentu, z którego strona została wygenerowana, Alternative dla równoważnej reprezentacji. Wybieraj ze słownika nawet, gdy nic w twoim pipeline jeszcze tego nie czyta, bo następne narzędzie w łańcuchu może już czytać

Dlaczego to samo wyszukiwanie potrzebuje FollowRef w obie strony?

Bo podążanie za referencjami odpowiada na dwa różne pytania, a kod musi wiedzieć, o które pyta. Wyszukiwanie po kluczu, które podąża za referencjami pośrednimi, zwraca obiekt, na który referencja wskazuje. Wyszukiwanie, które nie podąża, zwraca samą referencję. Oba są poprawne, a użycie niewłaściwego daje ciche złe zachowanie zamiast błędu

Odczyt pliku powiązanego pokazuje pierwszy kierunek. Żeby zdobyć numer obiektu osadzonego strumienia kryjącego się za kluczami specyfikacji pliku /EF i /F, wyszukiwanie nie może podążać, bo podążanie rozwiązuje referencję do obiektu strumienia i numer obiektu przepada. Reguła się uogólnia: każda ścieżka kodu potrzebująca tożsamości obiektu, a nie jego zawartości, musi wziąć surową referencję

Optional content pokazuje kierunek przeciwny, i ten kosztował więcej wysiłku w znalezienie. Słownik właściwości optional content jest zapisywany do katalogu jako obiekt pośredni, więc kod czytający go bez podążania dostaje referencję zamiast słownika. Sprawdzenie typu na tej wartości potem się wysypuje, uruchamia naturalna gałąź awaryjna — skoro nie ma konfiguracji, utwórz ją — i nadpisuje konfigurację, która już tam była. Nic nie rzuca wyjątku. Warstwy opisane w artykule grupy optional content i warstwy po prostu tracą swój domyślny stan widoczności

Wniosek uogólnia się poza oba przypadki. Gdy wyszukiwanie może zwrócić referencję albo obiekt, gołe sprawdzenie typu nie jest obsługą błędów: to gałąź, którą w końcu wejdzie się z niewłaściwego powodu. Zdecyduj jawnie, czego potrzebuje każde miejsce wywołania, i przed zajrzeniem do chronionego akcesora po słownik katalogu wybieraj publiczne API odpowiadające na pytanie wprost, na przykład właściwość z liczbą obiektów optional content

Mapa decyzyjna podążania za referencjami w wyszukiwaniach PDF tak, jak implementuje to PDFlibPas: odczyt /EF i /F pod specyfikacją pliku nie może podążać za referencją, bo odpowiedzią jest numer obiektu osadzonego strumienia, natomiast pośredni słownik /OCProperties w katalogu wymaga podążenia, inaczej nieudane sprawdzenie typu po cichu nadpisze istniejącą konfigurację optional content
To samo wyszukiwanie odpowiada na dwa różne pytania: tożsamość potrzebuje surowej referencji, zawartość — rozwiązany obiekt, a gołe sprawdzenie typu zamiast tej decyzji w końcu pójdzie złą gałęzią bez rzucenia wyjątku
// Załączniki poziomu dokumentu i powiązania poziomu strony współistnieją.
// Plik osadzony można oznaczyć jako powiązany także na poziomie dokumentu
if Lib.IsEmbeddedFileAssociated(0) = 0 then
  Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');

Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files  : ',
        Lib.GetPageAssociatedFileCount(3));

// Czyszczenie odczepia wiązanie strony; payload zostaje w drzewie nazw
if Lib.ClearPageAssociatedFiles(3) > 0 then
  Writeln('page 3 associations removed, payloads still reachable');

Co tryby zgodności robią z załącznikami

Profile archiwalne ograniczają, co wolno osadzać, a ograniczenie jest egzekwowane w punkcie wejścia, a nie w momencie zapisu. PDF/A-1 zabrania plików osadzonych całkowicie, PDF/A-2 dopuszcza wyłącznie osadzone dokumenty PDF/A, a PDF/A-3 to profil, który otworzył osadzanie na dowolne typy plików, i właśnie dlatego hybrydowe formaty faktur stawia na nim

PDFlibPas odmawia załączenia, gdy aktywny tryb zgodności na to nie pozwala — w miejscu wywołania, a nie setki operacji później przy generowaniu wyjścia. To świadomy wybór miejsca, w którym reakcja na błąd jest najtańsza: odmowa w miejscu wywołania wymienia plik, który dodawałeś, a odmowa przy zapisie wymienia dokument i zostawia ci rozpracowanie, który z czterdziestu załączników jest winowajcą

Dlatego też pliki powiązane tak często pojawiają się w fakturowaniu elektronicznym. Hybrydowa faktura to PDF czytany przez człowieka z doklejoną maszynowo czytelną zawartością XML i oznaczoną właściwą relacją, a zarówno profil kontenera, jak i klucz relacji są częścią specyfikacji, a nie konwencji. Tę konstrukcję opisuje budowanie hybrydowych faktur Factur-X i ZUGFeRD, a stronę metadanych schemat rozszerzeń XMP w PDF/A-3

Kiedy powiązanie powinno być per strona, a nie per dokument?

Gdy konsument potrzebuje wiedzieć, do której strony dane należą — i tylko wtedy. Załączniki poziomu dokumentu są prostsze, szerzej wspierane przez przeglądarki i wystarczają zawsze, gdy payload opisuje cały dokument: XML faktury, manifest podpisu, archiwum źródeł. Po powiązanie na poziomie strony sięgaj, gdy payload naprawdę jest związany ze stroną, a tożsamość strony jest częścią jego znaczenia

Wsparcie to praktyczne ograniczenie. Pliki powiązane na poziomie strony to konstrukcja z PDF 2.0, a wsparcie w przeglądarkach jest szczuplejsze niż dla załączników poziomu dokumentu. Ponieważ payload i tak siedzi w drzewie nazw, przeglądarka ignorująca /AF na stronach nadal pokaże plik na liście załączników, więc degradacja jest łagodna. Ale jeśli wiązanie strony jest dla twojego konsumenta niezbędne, a jedynie użytecznym metadanym, zweryfikuj czytnik, który faktycznie targetujesz, zamiast zakładać

Pliki powiązane na poziomie strony, załączniki poziomu dokumentu i bramki profili archiwalnych rządzące oboma są częścią biblioteki PDF PDFlibPas dla Delphi. Jeśli po drodze naprawiasz też starsze pliki, to praca nad metadanymi i zgodnością opisana w konwersji do PDF/A z naprawą metadanych decyduje o tym, która z tych tras załączania jest ci w ogóle dostępna