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