Ply.Decoder (Ply v0.1.0)

Copy Markdown View Source

Decodes element rows from a PLY body.

Row decoding is driven by a layout — a flat list of {name, type_or_list_spec} derived once from the element and reused for every row. Re-deriving it per row is the difference between one pass over a few dozen properties and one pass per row over millions.

Binary loops thread the remaining binary as the first argument of a tail-recursive function, which keeps the BEAM's match-context optimisation alive; splitting the binary up front discards it and reintroduces copying.

ASCII input is handled as a list of already-tokenised lines rather than a string that gets re-split on every call. Rejoining a remainder back into a string between chunks makes streaming quadratic — a large file would be re-tokenised once per chunk.

Summary

Types

ASCII input, pre-split into lines of whitespace-separated tokens.

A decoded row: property name to value (or list of values).

Functions

Decodes a fixed-width element into one packed binary per property.

Decodes count rows from tokenised ASCII lines.

Decodes count rows of element from the front of a binary body.

The flat {name, type} layout used to drive row decoding.

Splits ASCII body text into lines of tokens.

Types

lines()

@type lines() :: [[String.t()]]

ASCII input, pre-split into lines of whitespace-separated tokens.

row()

@type row() :: %{required(String.t()) => Ply.Types.value() | [Ply.Types.value()]}

A decoded row: property name to value (or list of values).

Functions

columns(binary, element, endianness, base_offset, only \\ nil)

@spec columns(
  binary(),
  Ply.Element.t(),
  Ply.Types.endianness(),
  non_neg_integer(),
  MapSet.t(String.t()) | nil
) :: {:ok, %{required(String.t()) => binary()}} | {:error, Ply.Error.t()}

Decodes a fixed-width element into one packed binary per property.

This is the path for large numeric elements such as Gaussian splats: it avoids materialising millions of boxed BEAM floats, and the result can be handed to Nx.from_binary/2 without a copy.

Output binaries are little-endian regardless of the source file, so callers get one predictable layout; big-endian input is byte-swapped during extraction.

Returns {:error, %Ply.Error{kind: :variable_width}} for elements containing list properties, which have no columnar representation.

only names the properties to extract; nil extracts all of them. Every property is still walked, because a column's offset depends on the widths of those before it — but the unwanted ones are never sliced, which is the whole saving on a splat file where three of sixty columns are wanted.

decode_ascii(lines, element, base_offset, opts \\ [])

@spec decode_ascii(lines(), Ply.Element.t(), non_neg_integer(), keyword()) ::
  {:ok, [row()], lines()} | {:error, Ply.Error.t()}

Decodes count rows from tokenised ASCII lines.

A row normally occupies exactly one line. A row is allowed to continue onto the next line when the current one runs out of values, since the format defines the body as a value stream rather than a line-oriented table — but leftover tokens on a line where a row ended are an error. Ignoring them silently shifts every subsequent row by one value, which corrupts the whole element while still returning {:ok, _}.

decode_rows(binary, element, endianness, base_offset, opts \\ [])

@spec decode_rows(
  binary(),
  Ply.Element.t(),
  Ply.Types.endianness(),
  non_neg_integer(),
  keyword()
) ::
  {:ok, [row()], binary(), non_neg_integer()} | {:error, Ply.Error.t()}

Decodes count rows of element from the front of a binary body.

Returns the rows, the unconsumed remainder, and the absolute byte offset reached, so errors can report where in the file they occurred rather than where in the chunk. :first_row keeps row numbers absolute across chunked reads.

layout(element)

@spec layout(Ply.Element.t()) :: [{String.t(), Ply.Property.kind()}]

The flat {name, type} layout used to drive row decoding.

Derived once per element and reused for every row.

tokenize(text)

@spec tokenize(binary()) :: lines()

Splits ASCII body text into lines of tokens.

Done once per file; the resulting list is threaded through decoding so no chunk ever re-scans text it has already seen.