Inside the Rive file format: .riv and .rev, object by object

Rive's file formats are not documented as a spec, but they are not opaque either: both formats are a short header followed by a flat object stream, and the public runtime and editor sources make the grammar recoverable. This page describes what is actually in the bytes.

The header

A .rev file starts with:

varuint major(7) · varuint minor(1) · varuint fileId · varuint schema(32) · bytes3 "ART"

followed by the object count and the objects themselves. Current editor projects are version 7.1, schema 32. A .riv file has no equivalent project header — that absence is exactly why the editor refuses to open one as a project.

The object stream

Every object is:

object   := varuint idClient · varuint idObject · varuint propCount · property[propCount]
property := varuint key · varuint byteLen · bytes[byteLen]

idClient distinguishes local objects (0) from collaborative records (1) — library components and spaces, which a converter must synthesise as local objects when the target project has no collaboration session. Properties are stored with a numeric *key* that maps to a name in the type schema, and the value is typed: an int, a uint, a bool, a double, a String, a Color, an Id, a FractionalIndex, and so on.

The type table itself is not in the file. It ships with the runtime and the editor, which is why any tool that reads .riv files needs the schema dictionaries — RIV2REV loads them once (about 1.3 MB, cached afterwards) and then works entirely offline.

Two encodings for the same keys

This is the part that makes conversion non-trivial: .riv and .rev use the same property keys with different wire encodings.

Property type.riv (runtime).rev (editor)
Idone varuint — the object's index in its listtwo varuints — a global (client, object) id pair
FractionalIndexdoes not existtwo varuints (num, den); siblings get consecutive values
doublealways float32float32 when it round-trips exactly, otherwise float64
intzigzag varuintsame
uint / Colorvaruint / varuint ARGBsame
boolone bytesame
String / bytesraw content, length from the enclosing propertyexplicit length prefix

Two consequences trip up naive converters:

Visibility flags

Runtime files store visibility as bit flags; the editor splits those bits into separate boolean properties. A converter has to translate the bits into individual properties or the layers come back in the wrong state.

Editor-only properties

Beyond the encoding differences, the editor keeps a handful of properties the runtime never needed: Animation.artboardId and order, KeyedObject.animationId, KeyedProperty.keyedObjectId, KeyFrame.keyedPropertyId, StateMachineLayer.machineId and machineOrder, LayerState.layerId plus its x/y, StateTransition.stateFromId and transitionOrder, FileAssetContents.assetId, Artboard.isComponent and includeInExport, and Backboard.activeArtboardId / mainArtboardId.

All of them can be derived from the structure of the runtime file — that is the whole trick. Timeline objects are contiguous runs inside an artboard, so a keyframe can find the keyed property that precedes it, and the keyed property can find its keyed object, and so on, all the way up to the animation.

Precision

When decoding, keep the original float32 bits. Parsers that round to six decimal places on the way in will write 2.1e-05 where the file said 2.1337521e-05, and the difference shows up as a visible drift in any animation that depends on exact values.

Two takeaways

  1. The runtime file is complete. Nothing artistic is thrown away; the loss is editorial metadata, not content. See what gets lost when converting.
  2. The conversion is a re-encoding, not a guess. A correct implementation produces the same bytes every time, which is what makes it verifiable: the same file through the browser converter and the CLI yields identical output.

Turn a .riv into an editable .rev

The converter runs entirely in your browser — your file is never uploaded. Preview the animation first, then download the editable project.

Open the converter or use the CLI