The PLY header: format, elements, properties, and where the data begins.
Line endings are the format's sharpest edge
The header is text, the body may be binary, and the boundary between them
is a line terminator. Files in the wild end header lines with LF, CRLF, or
(rarely) a lone CR. Locating the end of the header by counting lines, or by
searching for "end_header\n", leaves a stray \r in front of the body on
CRLF files — which shifts every binary field by one byte and produces
plausible-looking garbage rather than an error.
There is one genuinely ambiguous case: a header terminated by a lone CR
whose body's first byte happens to be 0x0A. Those two bytes are
indistinguishable from a CRLF terminator when read in isolation. The parser
resolves it by remembering the terminator style the file has used so far —
a file that has been ending lines with a lone CR keeps doing so — rather
than guessing per line.
data_offset is an absolute byte position and part of the public struct on
purpose: it is the escape hatch for reading the body yourself with
:file.pread/3 when this library's API does not fit.
Comments
comment and obj_info lines may appear anywhere in the header, including
before the format line and between property declarations, and tools do use
them to carry metadata. They are preserved in :comments in the order
encountered, tagged by kind. Their position relative to elements is not
preserved.
Summary
Functions
Whether the body is binary rather than ASCII text.
Builds a header for writing.
Looks up an element by name.
Byte offset of an element's data, relative to the start of the file.
Endianness implied by the header's format.
Parses a PLY header from the start of a binary.
Renders the header back to PLY text, including the trailing end_header line.
Types
@type format() :: :ascii | :binary_little_endian | :binary_big_endian
@type t() :: %Ply.Header{ comments: [comment()], data_offset: non_neg_integer(), elements: [Ply.Element.t()], format: format(), version: String.t() }
Functions
Whether the body is binary rather than ASCII text.
@spec build(format(), [Ply.Element.t()], keyword()) :: t()
Builds a header for writing.
data_offset is set to 0 — it describes where data begins in a file being
read, and is computed by the writer for a file being written.
Comments may be given as {:comment, text} / {:obj_info, text} tuples or
as bare strings, which are treated as comments.
iex> header = Ply.Header.build(:ascii, [
...> Ply.Element.new("vertex", 1, [Ply.Property.scalar("x", :float32)])
...> ])
iex> header.format
:ascii
@spec element(t(), String.t()) :: {:ok, Ply.Element.t()} | :error
Looks up an element by name.
@spec element_offset(t(), String.t()) :: {:ok, non_neg_integer()} | :variable | :error
Byte offset of an element's data, relative to the start of the file.
Returns :variable when the position cannot be computed arithmetically and
the preceding data has to be read instead. That happens when any preceding
element has variable-width rows — and always for ASCII files past the
first element, where a "fixed-width" record still occupies a variable number
of bytes because its values are text of unpredictable length.
Returns :error for an element the header does not declare.
@spec endianness(t()) :: Ply.Types.endianness() | nil
Endianness implied by the header's format.
Returns nil for ASCII files, which have no byte order.
iex> {:ok, header} = Ply.Header.parse("ply\nformat binary_big_endian 1.0\nend_header\n")
iex> Ply.Header.endianness(header)
:big
@spec parse( binary(), keyword() ) :: {:ok, t()} | {:error, Ply.Error.t()}
Parses a PLY header from the start of a binary.
The binary need only contain the header; anything after end_header is
ignored, so callers can pass a bounded prefix of a large file.
Renders the header back to PLY text, including the trailing end_header line.