Ply.Header (Ply v0.1.0)

Copy Markdown View Source

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

comment()

@type comment() :: {:comment, String.t()} | {:obj_info, String.t()}

format()

@type format() :: :ascii | :binary_little_endian | :binary_big_endian

t()

@type t() :: %Ply.Header{
  comments: [comment()],
  data_offset: non_neg_integer(),
  elements: [Ply.Element.t()],
  format: format(),
  version: String.t()
}

Functions

binary?(header)

@spec binary?(t()) :: boolean()

Whether the body is binary rather than ASCII text.

build(format, elements, opts \\ [])

@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

element(header, name)

@spec element(t(), String.t()) :: {:ok, Ply.Element.t()} | :error

Looks up an element by name.

element_offset(header, 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.

endianness(header)

@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

parse(binary, opts \\ [])

@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.

to_iodata(header)

@spec to_iodata(t()) :: iodata()

Renders the header back to PLY text, including the trailing end_header line.