Technický článek

Čtení a zápis značkovaného obsahu PDF v Delphi

Marked content je mechanismus, který ISO 32000-1 §14.6 definuje pro značkování obsahu stránky a na kterém jsou postavená značkovaná PDF i PDF/UA. PDFium Component ho vystavuje přímo: PageObjectMarks čte z objektu stránky každou BDC značku a její property list, AddPageObjectMark jednu zapisuje, RemovePageObjectMark jednu maže a PageObjectMarkedContentID hlásí MCID, které spojuje obsah se stromem struktury

Dokud nelze strom struktury spojit zpět s obsahem, který popisuje, je accessibility nástroj jen hádáním. Strom struktury říká „toto je nadpis"; MCID říká, které značky na které stránce ten nadpis skutečně jsou. Obě poloviny musí být čitelné, než aplikace může značkování zkontrolovat, opravit nebo ohlásit

Co je značka, v bajtech?

Operátor BDC s názvem značky a volitelným property listem, uzavřený EMC. V content streamu vypadá jako /P <</MCID 3>> BDC ... EMC: značka /P pojmenovává roli, slovník nese vlastnosti a vše mezi operátory je značkovaný obsah. Objekt stránky uvnitř tohoto rozpětí nese značku, což je to, co PDFium vrací a co PDFium Component převádí na záznam

TPdfContentMark drží handle, značku Name a pole TPdfContentMarkParam. Každý parametr má Key, Kind a jedno smysluplné pole hodnoty vybrané podle toho druhu: pmpInt, pmpFloat, pmpString nebo pmpBlob. Druh pochází z vlastního hlášení typu PDFium spíše než z toho, který getter zrovna uspěl, což je rozdíl mezi čtením property listu a hádáním

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;

Proč pmpUnknown znamená dvě různé věci

pmpUnknown se vrací, když PDFium nahlásí FPDF_OBJECT_UNKNOWN, a PDFium ho vrací i pro klíč, který neexistuje. Tyto dva případy nelze na této vrstvě rozlišit a předstírat opak by bylo horší než to říct na rovinu

Praktický důsledek pro váš kód: zacházejte s pmpUnknown jako se „zde není žádná použitelná hodnota" spíše než jako s typem, který byste mohli přesto dekódovat. Pokud na vlastnosti ve vaší rouře záleží, ověřte, že je přítomná s druhem, který rozpoznáte, a nevyvozujte absenci z unknown — značka, jejíž property list neumíte přečíst, je značka, o které byste měli hlásit, ne kterou byste potichu přijali

Záznam značky je snapshot, ne handle, který vlastníte

Pole Handle patří knihovně. Zastará ve chvíli, kdy je značka odstraněna, objekt stránky zničen nebo stránka uvolněna, takže záznam je read-only snapshot s krátkým životem. Uložíte-li ho do mezipaměti přes přepnutí stránky, držíte ukazatel do paměti, kterou si už engine přivlastnil

Stejná kázeň platí pro handle objektů stránky obecně v PDFium a chytá lidi na stejném místě: seznamový ovládací prvek naplněný záznamy značek, uživatel přejde na jinou stránku a pád, který vypadá nesouvisečně s navigací. Vytáhněte si hodnoty, které potřebujete — název, klíče, čísla — a handle pusťte. Poznámky k zastarávání handle objektů stránky po transformaci rozebírají obecné pravidlo a to, kde kousne jinde

Přidání značky a krok uložení, který se snadno přehlédne

AddPageObjectMark přijímá index objektu stránky, název značky a úplnou sadu parametrů. Parametry se zapisují jako sada spíše než patchované klíč po klíči, což je důvod, proč TPdfContentMarkParam nemá Has* sentinel — případ „aktualizovat jedno pole existujícího záznamu", který by chránily, nenastává

Stojí za to říct explicitně: přidání značky přestavuje content stream stránky, aby značka přežila uložení. To muselo být explicitní, protože SaveAs sám o sobě obsah neregeneruje — změna, která by žila jen v objektovém modelu, by byla zahazována a uložený soubor by vypadal přesně jako ten, se kterým jste začali. Pokud jste kdy do stránky PDFium něco přidali a nenašli to ve výstupu, tohle je obvykle důvod

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 z dokumentu to dělá a nedělá

Samotné značky nečiní značkované PDF. Konformní značkovaný dokument potřebuje strom struktury, jehož elementy odkazují na tyto MCID, položku /MarkInfo deklarující dokument jako značkovaný a názvy rolí, které znamenají to, co říká standard. Zápis značky /P s MCID, na kterou neukazuje žádný element struktury, vám dá obsah, který tvrdí, že je značkovaný, a strom struktury, který se o něm nikdy nezmíní

Kde marked content na této úrovni skutečně vydělává své místo, je inspekce a oprava: audit toho, které objekty stránky jsou značkované, hledání artefaktů, které měly být jako takové označeny, nebo matchování MCID vůči stromu struktury k nalezení sirotků. Pro polovinu té práce na stromu struktury viz průvodce validací stromu struktury PDF/UA a pro čtenářský zážitek, pro který značky nakonec jsou, poznámky k budování přístupné čtečky PDF v Delphi

PDFium Component dává aplikacím Delphi, C++Builder a Lazarus vysokoúrovňové VCL API nad enginem PDFium, s marked content, stromy struktury a validací přístupnosti dostupnými z běžného Pascal kódu — viz stránka produktu PDFium Component pro úplný API povrch