Artykuł techniczny

Odczyt i zapis oznaczonej treści PDF w Delphi

Oznaczona treść to mechanizm, który ISO 32000-1 §14.6 definiuje do tagowania treści strony, a na nim zbudowane są otagowany PDF i PDF/UA. PDFium Component wystawia go bezpośrednio: PageObjectMarks czyta z obiektu strony każdy znacznik BDC wraz z jego listą właściwości, AddPageObjectMark zapisuje jeden, RemovePageObjectMark usuwa jeden, a PageObjectMarkedContentID raportuje MCID, które łączy treść z drzewem struktury

Dopóki drzewa struktury nie da się połączyć z powrotem z treścią, jaką opisuje, narzędzia dostępności są zgadywanką. Drzewo struktury mówi „to jest nagłówek"; MCID mówi, którymi znacznikami na której stronie ten nagłówek faktycznie jest. Obie połowy muszą być czytelne, zanim aplikacja będzie mogła sprawdzać, naprawiać lub raportować o tagowaniu

Czym jest znacznik, w bajtach?

Operator BDC z nazwą znacznika i opcjonalną listą właściwości, zamknięty przez EMC. W strumieniu treści wygląda to jak /P <</MCID 3>> BDC ... EMC: znacznik /P nazywa rolę, słownik niesie właściwości, a wszystko między operatorami jest oznaczoną treścią. Obiekt strony wewnątrz tego zakresu niesie znacznik, co jest tym, co PDFium oddaje i co PDFium Component zamienia w rekord

TPdfContentMark trzyma uchwyt, Name znacznika i tablicę TPdfContentMarkParam. Każdy parametr ma Key, Kind i jedno znaczące pole wartościowe wybrane przez ten rodzaj: pmpInt, pmpFloat, pmpString lub pmpBlob. Rodzaj pochodzi z własnego raportu typu PDFium, a nie od tego, który getter akurat się powiódł, co jest różnicą między czytaniem listy właściwości a zgadywaniem jej

var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber is 1-based
  for I := 0 to Pdf.ObjectCount - 1 do    // page object indexes are 0-based
  begin
    Marks := Pdf.PageObjectMarks(I);
    for M in Marks do
    begin
      Memo1.Lines.Add('mark ' + M.Name +
        ' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
      for P in M.Params do
        case P.Kind of
          pmpInt:    Memo1.Lines.Add('  ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpString: Memo1.Lines.Add('  ' + P.Key + ' = ' + P.StringValue);
          pmpFloat:  Memo1.Lines.Add('  ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpBlob:   Memo1.Lines.Add('  ' + P.Key + ' = ' +
                       IntToStr(Length(P.BlobValue)) + ' bytes');
        end;
    end;
  end;
end;

Dlaczego pmpUnknown oznacza dwie różne rzeczy

pmpUnknown jest zwracany, gdy PDFium raportuje FPDF_OBJECT_UNKNOWN, a PDFium zwraca to także dla klucza, który nie istnieje. Te dwa przypadki nie dają się odróżnić na tej warstwie, a udawanie, że tak, byłoby gorsze niż powiedzenie o tym

Praktycznym skutkiem dla Twojego kodu: traktuj pmpUnknown jako „tutaj brak użytecznej wartości" zamiast jako typ, który możesz jednak zdekodować. Jeśli właściwość ma znaczenie dla Twojego przepływu, zweryfikuj, że jest obecna z rodzajem, który rozpoznajesz, i nie wnioskuj o braku z nieznanej — znacznik, którego listy właściwości nie potrafisz przeczytać, to znacznik, o którym powinieneś raportować, nie taki, który powinieneś po cichu zaakceptować

Rekord znacznika to migawka, nie uchwyt, który posiadasz

Pole Handle należy do biblioteki. Staje się nieświeże w momencie, gdy znacznik zostanie usunięty, obiekt strony zniszczony lub strona wyładowana, więc rekord jest migawką tylko do odczytu o krótkim życiu. Zapamiętaj ją przez przełączenie strony i trzymasz wskaźnik w pamięci, którą silnik już odzyskał

To ta sama dyscyplina, która dotyczy uchwytów obiektów strony w PDFium ogólnie, i łapie ludzi w tym samym miejscu: kontrolka listy zapełniona rekordami znaczników, użytkownik przechodzący na inną stronę i awaria wyglądająca na niezwiązaną z nawigacją. Skopiuj wartości, których potrzebujesz — nazwę, klucze, liczby — i puść uchwyt. Notatki o uchwytach obiektów strony stających się nieświeżymi po transformacji omawiają ogólną regułę i to, jak gryzie gdzie indziej

Dodawanie znacznika i krok zapisu, który łatwo przegapić

AddPageObjectMark przyjmuje indeks obiektu strony, nazwę znacznika i kompletny zestaw parametrów. Parametry są zapisywane jako zestaw zamiast łatane jeden klucz naraz, dlatego TPdfContentMarkParam nie ma wartowników Has* — przypadek „zaktualizuj jedno pole istniejącego rekordu", jaki one by chroniły, nie zachodzi

Warto wypowiedzieć tę część wprost: dodanie znacznika przebudowuje strumień treści strony, żeby znacznik przeżył zapis. To musiało być wprost, bo SaveAs nie regeneruje treści samodzielnie — zmiana, która żyła tylko w modelu obiektów, zostałaby odrzucona, a zapisany plik wyglądałby dokładnie jak ten, od którego wystartowałeś. Jeśli kiedykolwiek dodałeś coś do strony PDFium i znalazłeś tego brak na wyjściu, to zazwyczaj dlatego

var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // rebuilds the content stream
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

Co to czyni z dokumentu, a czego nie czyni

Same znaczniki nie czynią z niego otagowanego PDF. Zgodny otagowany dokument potrzebuje drzewa struktury, którego elementy referencjonują te MCID, wpisu /MarkInfo deklarującego dokument jako otagowany i nazw ról znaczących to, co mówi norma. Zapisanie znacznika /P z MCID, na który nie wskazuje żaden element struktury, daje treść twierdzącą, że jest otagowana, i drzewo struktury, które o niej nigdy nie wspomina

Gdzie oznaczona treść na tym poziomie naprawdę się opłaca, to inspekcja i naprawa: audyt tego, które obiekty strony są otagowane, znajdowanie artefaktów, które powinny były być jako takie oznaczone, albo dopasowywanie MCID do drzewa struktury, by znaleźć sieroty. Dla połowy tej pracy leżącej w drzewie struktury zobacz przewodnik po walidacji drzewa struktury PDF/UA, a dla doświadczenia czytelniczego, dla którego te tagi ostatecznie są, notatki o budowie dostępnego czytnika PDF w Delphi

PDFium Component daje aplikacjom Delphi, C++Builder i Lazarus API VCL wysokiego poziomu nad silnikiem PDFium, z oznaczoną treścią, drzewami struktury i walidacją dostępności osiągalnymi ze zwykłego kodu Pascala — zobacz stronę produktu PDFium Component, by poznać pełną powierzchnię API