Tincture.PDF.Structure (Tincture v0.1.0)

Copy Markdown View Source

The logical structure of a tagged document.

An untagged PDF is a picture of a document: the bytes say where every glyph sits, and nothing says which glyphs form a heading, which form a table cell, or what order a human would read them in. Assistive technology has nothing to work with. Tagging adds that missing layer.

Two halves have to agree:

  • a structure tree of elements — /Document containing /H1 and /P, a /Table containing /TR containing /TD — stored in the document catalog;
  • marked content in the page's content stream, where drawing operators are bracketed by BDC/EMC and stamped with a marked-content id (MCID).

An element points at its MCIDs, and a per-page number tree points back from each MCID to its element. That two-way link is what lets a reader walk the document logically rather than by position.

This module models the tree. See Tincture.tag/4 for the API that builds it.

Container and content elements

Elements divide into two kinds, and the difference decides whether marked content is emitted at all:

  • containers (:document, :table, :tr, :l…) group other elements and never wrap drawing operators directly. They get no MCID, because there is no content of their own to point at.
  • content elements (:p, :h1, :td, :figure…) bracket the operators that draw them, and carry an MCID.

Nesting content inside content is legal — a :span inside a :p — and the innermost open sequence owns whatever is drawn. The parent keeps its own MCID for everything outside its children.

Summary

Functions

Whether a tag groups other elements rather than wrapping drawn content.

Whether a tag brackets drawing operators and so needs an MCID.

Whether a tree contains anything at all.

Walk a tree of elements depth-first, parents before children.

The PDF structure type name for a tag, e.g. :h1 becomes "H1".

Every tag this module understands, as {atom, pdf_name} pairs.

Types

t()

@type t() :: %{
  :tag => tag(),
  :page_number => pos_integer(),
  :mcid => non_neg_integer() | nil,
  :kids => [t()],
  optional(:alt) => String.t(),
  optional(:actual_text) => String.t(),
  optional(:lang) => String.t(),
  optional(:title) => String.t(),
  optional(:scope) => :row | :column | :both
}

tag()

@type tag() :: atom()

Functions

container?(tag)

@spec container?(tag()) :: boolean()

Whether a tag groups other elements rather than wrapping drawn content.

A container gets no marked content and no MCID.

content?(tag)

@spec content?(tag()) :: boolean()

Whether a tag brackets drawing operators and so needs an MCID.

empty?(elements)

@spec empty?([t()]) :: boolean()

Whether a tree contains anything at all.

flatten(elements)

@spec flatten([t()]) :: [t()]

Walk a tree of elements depth-first, parents before children.

This is the order elements are written as objects, so an element's id is always lower than its children's.

tag_name(tag)

@spec tag_name(tag()) :: String.t()

The PDF structure type name for a tag, e.g. :h1 becomes "H1".

tags()

@spec tags() :: %{required(tag()) => String.t()}

Every tag this module understands, as {atom, pdf_name} pairs.