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) |
|---|---|---|
Id | one varuint — the object's index in its list | two varuints — a global (client, object) id pair |
FractionalIndex | does not exist | two varuints (num, den); siblings get consecutive values |
double | always float32 | float32 when it round-trips exactly, otherwise float64 |
int | zigzag varuint | same |
uint / Color | varuint / varuint ARGB | same |
bool | one byte | same |
String / bytes | raw content, length from the enclosing property | explicit length prefix |
Two consequences trip up naive converters:
- A runtime index is not an object id. The
.rivobject at index 0 is the first artboard, but that says nothing about its id in the editor project. Every reference has to be resolved to the object it actually points at and then re-pointed, not arithmetically shifted. - Unset is not the same as zero. An unset
Idis the sentinel{0xFFFFFFFF, 0xFFFFFFFF}, while{0, 0}is a *legal* reference. A real.revsimply omits unset ids; writing the sentinel makes the editor treat it as a reference.
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
- The runtime file is complete. Nothing artistic is thrown away; the loss is editorial metadata, not content. See what gets lost when converting.
- 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.