rvt-rs
Inspect Revit files without installing Revit.
rvt-rs is an open-source toolkit for reading Autodesk Revit project, family and template files (.rvt, .rfa, .rte, .rft). It reads a file's metadata, preview images and embedded schema. On the Revit releases it supports, it also recovers the building's elements (walls, floors, roofs, doors, windows, stairs, curtain walls, furniture, MEP and structure) with the names, types, storeys, materials and identifiers Revit gives them, and exports them as IFC4 (the open building-model exchange format), glTF (3D), plan SVG and CSV schedules.
It comes as command-line tools, a Rust library, Python bindings and a browser viewer that reads your file inside the browser tab. It is built from public observations of the file format (CLEANROOM.md), licensed under the Apache License 2.0, and not affiliated with Autodesk.
Start here
Support and evidence
Support depends on the Revit release, the file type and the feature. Opening a file and reading its metadata does not mean its elements or geometry can be recovered: elements are recovered from Revit 2023, 2024 and 2025 projects, and experimentally from Revit 2015 to 2022, 2026 and 2027 projects. Geometry is exact where the file's bytes are decoded and the element's bounding box elsewhere. rvt-elements and Python list each element's saved parameters on Revit 2023 to 2027 files, and experimentally on 2015 to 2022 ones, where some records are refused (RE-281), and rvt-ifc --revit-property-sets writes the named ones in Revit's own property sets (on Autodesk's Snowdon Towers HVAC, every one Revit's). Every export comes with diagnostics that say what was approximated or left out; check them before relying on an export for measurement or coordination.
The recovery figures in this README compare rvt-rs's export with Revit's own IFC export of the same file, element by element, on the models named in What rvt-rs reads from real projects. Other models of the same releases are expected to behave alike but are not measured, and files saved by the same release can store their data differently. docs/support-matrix.json records each capability with its evidence and its status ceiling.
This README describes the development branch, main, which the browser viewer is also built from. Installed packages are the latest release; Release 0.12.0 and main says what differs.
What you can rely on, by input
| Input | Open, metadata, previews, schema | Elements | Geometry | Properties |
|---|
| Revit 2024 and 2025 projects | yes | typed IFC entities from each element's own record; by ElementId the set equals Revit's export on Core Interior, RE1 Architecture, Electrical and Mechanical, the 22 of puzzbobb's Revit 2025 models that have an export, and the BIM4LCA office as built; RE1 Plumbing and the office at building permit each write one element Revit's export leaves out, and 23 of Snowdon Towers' 5,968 are not in it (local only; details below) | measured per category: exact joins, profiles and cuts where decoded, the element's bounding box where not | names, family and type names, storey, materials and layers, Revit's GlobalId, stair dimensions, IFC export overrides; most other parameters not read |
| Revit 2026 projects | yes | experimental (since 0.5.0): typed IFC entities from each element's record (RE-124), on flowbim.ee's single-dwelling house (reference models) 113 of the 115 elements of Revit's export, none outside it | the element's bounding box, walls cut back at their joins (on that house wall thickness Revit's on 59 of 59, length on 35 of 59) | Revit's GlobalIds (113 of 113), storeys with Revit's names and elevations (4 of 4), rooms with Revit's number and name (13 of 13), doors and windows in their host walls, family and type names, wall and floor layer sets as thick as Revit's, walls joined by their join lists; roof slopes, stairs and IFC export overrides not read |
| Revit 2027 projects | yes; 325 fields of the 2027 schema carry a flag whose meaning is not measured (RE-264) | experimental (since 0.6.0): typed IFC entities from each element's record, with the constants the file's schema gives (RE-179); on Autodesk's five 2027 sample projects the same ElementIds and classes as their 2026 copies (20,818 of 20,818), with no Revit 2027 export measured | as on 2026 | as on 2026: storeys, rooms, doors and windows in their hosts, family and type names, layer sets, wall joins and mullion materials read the same as the 2026 copies; 79 corner mullions are not put in a curtain wall |
| Revit 2023 projects | yes | typed IFC entities (since 0.4.0) from each element's record (RE-81): on two 2023 projects every element of Revit's export (modelo_bim, 37 of 37) or all but the 8 window trims nested in windows (Exemplo_data, 37 of 45), none outside it | the element's bounding box, walls cut back by the walls they join (Exemplo_data 17 of 17 and modelo_bim 4 of 4 as Revit's, RE-120), beams stopped at the faces of the columns they frame into (modelo_bim 8 of 8, RE-122); rooms their outline (RE-102) |
Typical uses: inventory and audit folders of Revit files without Revit (rvt-info); open a 2024 or 2025 model in a browser or in an IFC viewer to check its layout, storeys, types and materials; take element and room schedules into a spreadsheet (rvt-schedule); feed IFC or glTF into a coordination, quantity or visualisation pipeline, checking the per-export diagnostics first; and research the format itself with the probes under examples/.
rvt-capabilities --matrix -f text prints the support matrix; docs/status.md and docs/supported-profile.md summarise it.
Release 0.12.0 and main
The latest release is v0.12.0 (2026-10-31). cargo install rvt --locked, pip install rvt, the release archives and the container image install it. "Upgrading from 0.11" in the changelog says what a user of 0.11 has to change:
- Rust code that names a removed item or builds an
IfcModel with a struct literal;
- readers of
rvt-capabilities --json (schema version 2) and rvt-schema --json;
- Python and viewer users who want doors and windows drawn as boxes (
saved_meshes=False, ?savedMeshes=0);
- models whose partitions are over 256 MiB;
- pipes'
InvertElevation on a raised storey;
- a floor's and a covering's
Width, no longer written.
What 0.12.0 changed, in brief:
-
Base quantities (#1016, #1046): quantities Revit computes from its joined geometry are written where the model's records decide them:
- walls' side areas, volumes and
Height, and the GrossFootprintArea of walls nothing joins to or cuts (RE-270, RE-290, RE-292, RE-293);
- the areas and volumes of box-drawn columns nothing cuts or crosses (RE-291);
- floors'
NetVolume and flat roofs' slab quantities where nothing cuts them (RE-295, RE-298), and ceilings' GrossArea (RE-297);
- spaces'
NetVolume where the room's solid is a prism (RE-296);
- curtain mullions'
NetSurfaceArea where their wall's mullions say which ends Revit keeps (RE-268).
A floor's and a covering's Width are left out, as Revit's exporters disagree on them (RE-294).
-
Property values:
- unrotated beams'
Roll (RE-276), level beams' Slope and the Span of beams drawn along their location line; a rotated beam gets no Roll, a sloped one no Slope;
- storeys'
Reference and EntranceLevel (RE-275);
- structural columns' and framing's
LoadBearing (RE-285);
- type objects' common sets, without
IsExternal, which Revit's newer exporters leave out (RE-286);
- a stair's
TreadLengthAtInnerSide, and with --revit-set-placement its flights' set, measured on 14 and 5 stairs (RE-287);
- curtain members'
IsExternal by their curtain wall's value (RE-288). Exterior members' values are checked against revit-ifc's source only.
- A pipe's
InvertElevation is its height above the model's zero.
-
The office census: on the BIM4LCA office as built, counting the objects rvt-rs writes and leaving out what the census counts apart, 29,262 of the 39,388 quantities and, with --revit-set-placement, 45,235 of the 45,525 property values Revit's export writes are equal, 1 different each (Measure runs 41213779326 and 41215480967). No quantity is written that Revit's IFC4 exports leave out. Of the 10 property values written that Revit's export leaves out (14 at permit), all but one are on elements it does not hold; the one is on a fixture it writes as another class (#792).
The browser viewer is built from main, so it runs what main has; each change main adds after 0.12.0 is a fragment in changelog.d/, which the next release moves into CHANGELOG.md. To use main before the next release, build from source (docs/install.md).
Rust 2024 edition (MSRV 1.87). Nineteen CLIs ship (rvt-analyze, rvt-info, rvt-inspect, rvt-schema, rvt-history, rvt-diff, rvt-corpus, rvt-dump, rvt-doc, rvt-ifc, rvt-ifc-compare, rvt-write, rvt-gltf, rvt-sheet, rvt-elem-table, rvt-elements, rvt-capabilities, rvt-schedule, gen-fixture) plus the reproducible probes under probes/examples/, one or more per format finding. Python bindings via pyo3+maturin in the rvt-py workspace member (so the core rvt crate can stay unconditionally #![forbid(unsafe_code)]) — pip install rvt.
What works today
| Layer | Status | Notes |
|---|
| OLE/CFB container open | ✓ | No Revit required |
| Truncated-gzip stream decode | ✓ | |
BasicFileInfo metadata | ✓ | Version, build, GUID, original path, plus the Key: value block every release 2016-2027 writes: worksharing state, central model path, last-saved-by user, save counter, single-user-cloud flag (rvt-info, rvt::metadata) |
PartAtom XML | ✓ | Title, OmniClass code, taxonomies |
| Stream preview extraction | ✓ | Clean PNG, wrapper stripped |
Formats/Latest schema parse | ✓ | the whole page-stripped schema: 4,126 classes and 11,562 fields on Revit 2024, 134,794 fields across the 12 releases (#410) |
| Field-type classification | ✓ | 9 to 12 fields per release unclassified out of 10,000 to 12,000, all listed in the CI gate (tests/field_type_coverage.rs, #410); the 100% figure before #410 covered only the first 64 KB of each schema |
| Cross-release tag-drift table | ✓ | First public 122×11 dataset |
| Layer 5a ADocument walker | partial | Reliable on Revit 2024–2026; 2016–2023 entry-point detection pending |
| Stream-level modifying writer | ✓ | 13/13 streams byte-preserving; rvt-write CLI + JSON patch manifests |
| Field-level semantic writer | pending | Gated by ADR-002; no stable API until decoder/validation evidence is strong enough |
| Layer 5b per-class decoders | partial | 81 classes are registered in elements::all_decoders(), each through SimpleDecoder; no test exercises them (their synthesized-byte suites were deleted in #573). Real project elements come from partition element records instead (next section); schema-field decoding of Wall/Floor/Door/Window from Global/Latest stays open (RE-01, RE-19). |
| IFC4 STEP export — spatial tree | ✓ | + + + + OmniClass classifications |
What rvt-rs reads from real projects
On Revit 2024 and 2025 project files, rvt-rs reads each element from its partition record: its ElementId, BuiltInCategory, bounding box and references, for a family instance its family and type, and for a wall, floor, ceiling, roof or railing its type's name. Every number below compares rvt-rs's IFC with Revit's own IFC export of the same file, element by element (the IFC Tag is the ElementId).
| Elements | IFC | Against Revit's own export |
|---|
| Walls | IfcWall, runs trimmed at their joins; a 2024 or 2025 wall its type's thickness either side of its centreline, running through or stopping at a butt join as its join lists say, layer by layer where both walls are layered alike, and stopping layer by layer where it ends part way along another wall, on a slanted line where the walls meet at an angle; a curved wall the ring sector along its arc; a tapered wall its cross-section, leaning on its exterior face, along its line; a wall's top at the underside of the floor, roof or footing its top join or a cut pair names, where that element covers the whole of the wall's plan | Core Interior 360 of 360, all world-exact (RE-127); the BIM4LCA office 482 of 607 with both ends Revit's, T joints, orientations and type cores read from the native records (RE-222, RE-223); Snowdon Towers 1,078 of 1,078, faces exactly Revit's on 1,030 of them (RE-54, RE-75, RE-86), both ends on 724 (RE-70 to RE-75; curved walls scored by radius and angle); the BIM4LCA office's wall tops Revit's on 64 of the 65 it cuts as built and 70 of 70 at building permit, a wall a floor covers in part (29 and 26) keeping its full height where Revit steps the top (RE-267) |
| Doors, windows | IfcDoor, IfcWindow, each filling an opening in its host wall | Core Interior 132 and 6; Snowdon Towers 132 and 68 |
| Floors, building pads | IfcSlab with its sketched plan profile | Core Interior 80 of 80, every one with its sketched outline, and its 20 shading-device plates with Revit's outline (RE-95); Snowdon Towers 176 of 199 with their outline, curved edges and shaft openings included, 154 with Revit's area (RE-96 to RE-100) |
| Columns | IfcColumn, minus what the walls cut; a square hollow steel section drawn as it, rounded corners and all | Core Interior 256 of 256, world-exact; the BIM4LCA office's 79 hollow-section columns are Revit's body (RE-221) |
| Roofs, ceilings | IfcRoof with its sketched outline, a sloped roof as its planes (shed, gable, hip, slope arrow, rafter cut; RE-56, RE-199), a Revit 2025 roof by extrusion as its sketch's section (RE-211); IfcCovering with its sketched outline (RE-98) | Snowdon Towers 20 and 68; 13 roofs carry their outline, 11 with the area of Revit's own roof (RE-50); a shed roof's slope, thickness and outline reproduce its recorded height to 1e-9 ft (RE-56); on puzzbobb's Revit 2025 models 18 of 19 roofs have Revit's body, every vertex within 0.001 ft and the volume within 1e-4 (RE-199); the 19th, a roof by extrusion, is drawn as its section, all 1,187 of Revit's vertices within 0.001 ft of it and 1.00015 times Revit's volume (RE-211); 66 of 68 Snowdon ceilings and 6 of 6 RE1 ceilings with Revit's outline (RE-98) |
Across Autodesk's Snowdon Towers architectural sample (Revit 2024), rvt-rs exports 5,968 elements. 5,945 of them are in Revit's own export (local only: that export cannot be redistributed, and the figures are from #309 and #328, not a committed report), which is every element Revit exports in these entities. The other 23 are curtain panels with no resolved type, which Revit's exporter leaves out (#309). The 6 slabs in the Legends phase that rvt-rs wrote before are no longer written, because they exist in no phase after Legends (#328, RE-184). Doors and windows whose type draws no geometry are written as their opening alone, as Revit writes them (RE-84). Elements in a design option set's non-primary options, elements that do not exist in the project's last phase (RE-184), 2D-only families and empty curtain panels are left out, as Revit leaves them out, and the export diagnostics count them. Rooms are not checked against the phase (see the gap below).
Core Interior and the RE1 models are licensed test files and run in CI. The BIM4LCA office and puzzbobb's Revit 2025 models are fetched by Measure. Snowdon Towers, the 255ribeiro projects, the 2026 house and BIMcollab's 2015 example have no licence to redistribute and are measured locally (BIMcollab's and the holdout pack's summary counts leave Measure's runner), so those figures cannot be re-run by others. By ElementId the exports equal Revit's on Core Interior (970), RE1 Architecture (84), Electrical (49) and Mechanical (74), the 22 R2025 models with an export and the office as built (12,974); RE1 Plumbing writes a water closet tank (442378) and the office at building permit a column (1927742, #792) that Revit's export leaves out, and one office fixture (3095244) is in another IFC class than Revit's (#792) (Measure run 40927543524). Parts Revit's exporter splits one element into under one ElementId (a roof or slab, #323) count once.
All of the above were used to develop the decoder. Three models that were not, Autodesk's Snowdon Towers Plumbing, HVAC and Electrical samples (2024), were measured once as they are against the VIM export of the sample: every exported element the VIM holds is in the expected category (10,385), every VIM instance both hold is exported, and all storeys and GlobalIds agree (held-out report, #408). Their bodies are bounding boxes.
What does not work yet
| Gap | Status | Evidence |
|---|
Element extraction from real .rvt project files | partial | Revit 2024 and 2025 projects decode their element records into typed IFC elements; see What rvt-rs reads from real projects. Revit 2023 projects decode their elements' identity, category and box (RE-81), and Revit 2026 projects their element records experimentally (since 0.5.0), measured on one project (RE-124). Revit 2015 to 2022 and 2027 projects decode their element records experimentally (since 0.6.0), measured against the same models saved by a neighbouring release, not against a Revit export (RE-179 to RE-181). Earlier releases open and report metadata and schema; their element records are not decoded. The per-finding history, RE-21 to RE-43, is in docs/status.md and reports/element-framing/. |
| 81 registered classes wired into walker | partial | MVP_TYPED_CLASSES are preferred via their registered decoder in iter_elements (fail closed). The remaining registry entries still use generic decode_instance. ArcWall uses a separate partition decoder. |
| Element geometry beyond what is listed above | partial | Where the bytes are not decoded, a body is the element record's bounding box, and the export diagnostics say so. Doors and windows are drawn by default from the mesh Revit saved for display, on Revit 2023 to 2027, where the whole mesh decodes (every vertex within 0.001 ft of Revit's on all 132 of Core Interior's doors and 6 windows and all 269 of the BIM4LCA office's windows, Measure run 40932136360; --no-saved-meshes keeps their box). Other loadable families stay boxes by default, turned with the instance where it is turned (RE-87); --saved-meshes draws them from their saved mesh too (RE-78: within 0.01 ft of Revit's export for 158 of 158 on Core Interior, 207 of 214 on RE1 (Revit 2025) and 3,332 of 3,736 on Snowdon Towers); ducts and pipe fittings, though pipes are cylinders where their ends are proven (RE-131); openings across and in height, which are the door's or window's box cut to the wall's depth (#227). Not decoded: roofs by extrusion outside Revit 2025 (RE-211) and hip roofs over a T or U footprint (#884), monolithic spiral stair flights and straight riserless ones with a nosing (#357), edited wall profiles and some wall ends (#358: both ends match Revit on 716 of Snowdon's 1,078 walls), the height of 78 of the BIM4LCA office's 579 walls, which Revit's export draws cut at one end, mostly at a floor the wall's top is joined to, and rvt-rs as tall as their record box (#1067, RE-257, RE-258), and the boundaries of 7 of Snowdon's 54 rooms, whose stored solids do not close at the floor (#90). A slab sketched as separate pieces exports one element per piece, as Revit's export does (#331). |
Why the schema matters
The openBIM community, around buildingSMART International and the IFC standard, has long worked on getting building data out of Revit. Autodesk's revit-ifc exporter runs inside Revit through the Revit API, so what it writes depends on what the API exposes and on how the export is configured; the OSArch wiki's guide to setting up Revit for openBIM covers the settings that matter. rvt-rs takes an independent route: it reads the file itself, so it runs where Revit is not installed, and its output can be checked against Revit's own export of the same file.
The schema work here, decoding the whole of Formats/Latest and classifying all but about ten field encodings per release across 11 Revit releases, is the dictionary a byte-level reader needs. The aim is an export that can also carry data the Revit API does not surface. Today the export reaches what the tables above measure, and the rest is open work (ROADMAP.md and the issues).
If you are building BIM or AEC tooling and want an Apache-2.0 Revit reader to build on:
- Revit 2016 to 2027 files: container open, stream decompression, metadata, previews, schema introspection (the whole schema, all but about ten field encodings per release classified) and stream-level byte-preserving writes.
- Revit 2024 and 2025 projects — typed elements with Revit's ElementIds, names, types, storeys, materials, layers and GlobalIds, as IFC4, glTF 2.0, plan SVG and CSV, each export with a diagnostics sidecar naming what was approximated or left out.
- Revit 2023 projects — typed elements with Revit's ElementIds and categories, each drawn as its bounding box (RE-81), walls cut back at their joins (RE-120), beams at their columns (RE-122), rooms as their outline (RE-102), on the storeys of Revit's own Levels (RE-107), family instances named as Revit names them (RE-109), walls, floors and roofs with their type, read from its layers (RE-111, RE-112).
- Revit 2026 projects, experimentally (since 0.5.0): typed elements, storeys, rooms, doors and windows, type names and layer sets, measured on one project (RE-124).
- Revit 2015 to 2022 and 2027 projects, experimentally (since 0.6.0): typed elements, measured against the same sample projects saved by Revit 2019 (for 2015 to 2018), 2023 (for 2019 to 2022) and 2026 (for 2027); Revit's own export is measured on one 2015 project (RE-198) and on none of the others (RE-179 to RE-181).
- Revit 2008 to 2014 projects, experimentally and outside the 1.0 range (2015 to 2027): typed elements, measured on a local copy of Autodesk's 2013
rac_advanced sample against its 2015 copy (RE-259); 2008 to 2010 only on their own files' consistency, with storeys and named materials (RE-260). The pre-2015 files are closed source and measured locally only.
- Not yet: element records of Revit 2007 and earlier, most parameters, family geometry, and semantic editing of a Revit file.
tests/fixtures/synthetic-project.ifc is a committed IFC from synthesized inputs, useful for testing a consumer without a Revit file. The browser viewer runs the same pipeline in the tab: drop a 2023, 2024 or 2025 project, or a 2015 to 2022, 2026 or 2027 one experimentally, and Export IFC writes its typed elements; earlier releases give the metadata and the spatial scaffold. The viewer is built from main, not from a release.
Quick demo
One command produces the full forensic picture — identity, upgrade history, format anchors, schema table, schema-to-data link histogram, content metadata, and a disclosure scan:
cargo build --release
./target/release/rvt-analyze --redact path/to/your.rfa
From Python
import rvt
f = rvt.RevitFile("my-project.rfa")
print(f.version, f.part_atom_title) # 2024 "0610 x 0915mm"
print(f.read_adocument()["fields"][-1]) # {name: m_devBranchInfo, kind: element_id, tag: 0, id: 35}
open("out.ifc", "w").write(f.write_ifc())
Install: pip install rvt — or build from source with maturin build --release --manifest-path rvt-py/Cargo.toml. Full API + Jupyter notebook walkthrough: docs/python.md and docs/rvt-python-quickstart.ipynb.
See docs/install.md for cargo, PyPI, source, and viewer
install/smoke-test paths, and for the prebuilt Linux / macOS / Windows CLI
archives each GitHub Release carries (from v0.2.0 on).
In the browser
Drop a .rvt / .rfa / .rte / .rft at https://drunkonjava.github.io/rvt-rs/: the file is read and converted inside the tab and is not uploaded. The viewer runs the core library compiled to WebAssembly by the rvt-wasm crate (wasm-pack build rvt-wasm --target web --out-dir ../viewer/pkg --out-name rvt, as docs/install.md describes), parses in a dedicated worker, and renders 3D with Three.js. One-click buttons export the model as glTF 2.0 binary, IFC4 STEP, or plan-view SVG. URL state (camera pose + category filters) is shareable via the hash fragment. Multi-megabyte projects fit inside wasm32's 4 GiB linear-memory ceiling: each gzip member reserves at most 1 MiB before decoding and the decoded buffer is trimmed (#256), which is what lets the 33.7 MB Core Interior demo finish at 419 MB of linear memory instead of growing to 4057 MB in 139 ms and trapping. A Rust panic in the wasm build is reported through console.error with its message and source location instead of a bare RuntimeError: unreachable.
The landing dropzone also includes a demo gallery staged from docs/viewer-demos.json (license/provenance + expected quality labels). It opens two real projects, Revit_IFC5_Einhoven.rvt (2023) and 2024_Core_Interior.rvt (2024) from the MIT-licensed magnetar-io/revit-test-datasets, hash-verified at staging time, alongside the tier1 synthetics. Einhoven is 913 KB and opens in about half a second; Core Interior is 33.7 MB and decodes in about 3 seconds on the deployed site since #266 (about 7 seconds including the 32 MB download), reporting 889 entities and 854 elements carrying geometry. Two Playwright tests gate both cards, and staging refuses a file whose sha256 does not match the catalog. Demo bytes are same-origin static assets only.
The deploy workflow (.github/workflows/deploy-viewer.yml) checks the viewer's privacy posture on every build, in two ways. viewer/scripts/audit-wasm-imports.mjs reads each import of the compiled .wasm and its shim in the JavaScript glue, and fails if any reaches fetch, XMLHttpRequest, WebSocket, EventSource or sendBeacon (the check before #735 could not fail). A Playwright test opens a real project in the built viewer and fails on any request to another origin, or on any request at all after the file is open. The page's Content-Security-Policy also limits its connections to its own origin. Together these cover the WebAssembly module's imports, the page's connection policy and the flow the test drives; they are checks, not a proof about every possible code path. docs/viewer-privacy-posture.md states the invariant and what each check covers.
Supported MVP workflow
The supported end-to-end shell is intentionally honest about partial decode:
- Open a supported Revit file locally (drop, file picker, or a redistributable gallery demo).
- Read the File status / confidence panel before trusting geometry.
- Inspect decoded entities in the scene tree or by picking in the 3-D view.
- Export IFC / glTF / plan only after checking the export-quality label.
- Download diagnostics when the export is scaffold-only or partial.
What still depends on decoder work: element records of Revit 2014 and earlier (2015 to 2022's, 2026's and 2027's are experimental), and on 2023 geometry beyond each element's box, wall joins and beam cuts, layered or angled joins, design options and IFC export overrides; roofs by extrusion outside Revit 2025 or over a T or U footprint, family geometry and opening profiles; most element parameters; and compound layers of elements drawn whole or sloped, which get no layer set. RE-19 / RE-20 (2026-08-29) closed negative on the magnetar corpora: there is no Door vs Window discriminator in the opening-index bytes and no recoverable Level ElementId map there, so do not re-probe those without a new corpus or signal. Typed categories come from each element record's BuiltInCategory instead (RE-21 onward). On Revit 2022 and earlier, treat IFC export as the spatial scaffold plus diagnostics; on 2023 elements are their boxes with the joins and properties listed by input, and on 2026 the export is experimental. See docs/status.md and docs/supported-profile.md.
Sample output (all pre-scrubbed with --redact, committed for review):
The --redact flag (on by default in every committed artifact) scrubs Windows usernames, Autodesk-internal paths, and project-ID folder names to <redacted> markers while preserving path shape so claims remain verifiable. Omit the flag when running privately against your own files.
Results at a glance
Running the shipped CLIs against one 400 KB RFA fixture:
- Metadata: version, build tag, creator path, file GUID, locale, worksharing state, central model path, last saved (time and user), save counter (
rvt-info)
- Folder inventory: one row per Revit file under a folder — release, worksharing, last saved — as a table, CSV, JSON or JSON Lines, reading only each file's two identity streams (
rvt-info <folder> -f csv)
- Atom XML: title, OmniClass code, taxonomies (
rvt-info parses PartAtom)
- Preview: clean PNG thumbnail, 300-byte Revit wrapper stripped (
rvt-info --extract-preview)
- Schema: every class and field of the embedded schema with its typed encoding, 3,490 classes on the 2016 sample to 4,285 on 2026 (
rvt-schema)
- History: every Revit release that ever saved this file (
rvt-history)
- Bulk strings: 3,746 length-prefixed UTF-16LE records from Partitions/NN — Autodesk unit/spec/parameter-group identifiers, OmniClass + Uniformat codes, Revit category labels, localized format strings (
rvt-history --partitions)
Every class and field name that rvt-schema extracts was cross-checked against the public RevitAPI.dll NuGet package's exported C++ symbol list. All top-level tagged class names we've inspected (ADocument, DBView, HostObj, LoadBCBase, Symbol, APIAppInfo, APropertyDouble3, ElementId, and the rest) appear in that export with their decorated signatures (e.g. __cdecl NotNull<class ADocument *,void>::NotNull(class ADocument *)), consistent with the on-disk schema naming the compiled classes.
A build-server path also appears in C++ assertion strings inside the same DLL; it is mentioned in the recon report for completeness and does not represent anything the reader extracts from .rvt / .rfa files.
Performance on large projects
Partition streams are inflated once per file and cached on the RevitFile handle (#266). On the 33.7 MB 2024_Core_Interior.rvt, rvt-ifc --mode geometry went from 26.07 s / 2641.4 MiB peak RSS to 1.69 s / 490.1 MiB (Apple Silicon, /usr/bin/time -l, best of three), and the exported IFC is byte-identical once the writer's two wall-clock stamps are normalised. In the browser the same file decodes in about 3 s on the deployed site, where it used to take about 28. Every tools/perf_budget.py --require-category medium row now passes with an order of magnitude of headroom, without any budget being loosened.
Findings of the first reconnaissance
Six reproducible findings from the first reconnaissance (April 2026), all documented in docs/rvt-moat-break-reconnaissance.md and reproducible with the programs in examples/ and probes/examples/:
-
The schema indexes the data. Class names do not appear as ASCII in Global/Latest; class tags from Formats/Latest (u16 after class name, with 0x8000 flag set) occur ~340× the uniform-random rate. The top tag, AbsCurveGStep, appears 19,415 times in 938 KB of decompressed Global/Latest. [examples/link_schema.rs]
-
Tags drift across releases but are stable-sort-assigned. ADocWarnings = 0x001b 2016→2026 because no class sorted alphabetically before it has ever been added. AbsCurveGStep shifted 0x0053 → 0x0066 across the decade as 19 new A-class entries were inserted. Full 122-class × 11-release drift table: docs/data/tag-drift-2016-2026.csv, visualised in docs/data/tag-drift-heatmap.svg. [examples/tag_drift.rs]
-
Revit 2021 changed the format substantially. On the sample families, Global/Latest grew about 27× (about 26 KB to about 715 KB) from 2020 to 2021, and the Forge Design Data Schema namespaces (autodesk.unit.*, autodesk.spec.*) first appear in Partitions/NN in the same release.
-
Parameter-group namespace shipped separately in Revit 2024. autodesk.parameter.group.* identifiers appear in 2024+ only — three releases after units/specs. Dating the Forge schema rollout from on-disk bytes: examples/tag_drift.rs, src/object_graph.rs.
-
A stable Revit format-identifier GUID in family files. Global/PartitionTable is 167 bytes decompressed in .rfa family files, and 165 of those bytes are byte-for-byte identical across every Revit release 2016-2026 (98.8% invariant). The invariant region contains a version-1 UUID, 3529342d-e51e-11d4-92d8-0000863f27ad, whose timestamp field decodes to 2001-01-08. Useful for family-file detection. Scope correction (2026-04-21): this invariant is a anchor, not a universal Revit-file anchor. Three real project files we probed carry three different GUIDs ( on Revit 2023, on 2024, all-zero on 2025) in a shorter 87-byte . File-type sniffers using the family GUID will correctly reject non-family files but can't identify them. See . []
Revit files, Autodesk's shipped samples among them, can carry authoring metadata: the path a file was saved from, a creator name in the Contents stream, and build-server paths in assertion strings of the public RevitAPI.dll. The values found are not repeated here.
Downstream safety: the rvt-analyze CLI has a --redact flag (used for all committed demo output in this repository) that rewrites creator paths, Autodesk-internal paths and build-server paths to <redacted> markers while preserving the surrounding structure. A tool that displays rvt-rs output publicly should do the same.
Library surface
Three runnable examples show the library end to end: basics_inspect reads what a file is, basics_elements lists its building elements by IFC type with their ElementIds, and basics_export writes IFC4, glTF, a CSV schedule and the export diagnostics. cargo run --example basics_inspect -- model.rvt runs one on your file, and CI runs all three on the synthetic project the repository ships and on a real one, Einhoven (MIT). The other programs under examples/ are research probes (examples/README.md).
All modules build for native targets and for wasm32-unknown-unknown, which the rvt-wasm crate compiles for the browser viewer. The type documentation is on docs.rs:
| Module | What it does |
|---|
reader | Open any Revit file with OpenLimits, enumerate every OLE stream, fetch raw stream bytes, bounded reads |
compression | Truncated-gzip decode (inflate_at, inflate_at_auto, inflate_at_with_limits) + multi-chunk (inflate_all_chunks_with_limits) + truncated-gzip encoder for write-back (truncated_gzip_encode) |
basic_file_info | Version, build tag, GUID, creator path, locale — read path + byte-back encoder (BasicFileInfo::encode) |
part_atom | Atom XML with Autodesk partatom namespace — title, OmniClass, taxonomies — read + encode |
formats | Parse + encode Formats/Latest with FieldType classification (all but 9 to 12 fields per release classified across the 12-release corpus, #410; 2027's flagged fields by their base type, RE-264) |
walker | Schema-directed instance walker + generic decode_instance + detect_adocument_start entry-point finder; candidates of the 7 classes in elements::MVP_TYPED_CLASSES go through their typed decoder from the registry (fail closed), and every other class decodes generically |
elements | 81 ElementDecoder registry entries in all_decoders(), all SimpleDecoder (Wall, Floor, Door, Window, Column, Beam, Stair, Railing, Rebar, Room, Furniture, …), and typed views of Level, Wall, Floor, Door, Window, Room and Material; the walker uses the 7 MVP_TYPED_CLASSES decoders, and no test exercises the rest (#573). Real project elements come from partition element records |
geometry | Point3, Curve (Line, Arc, Circle, Ellipse, Hermite spline, NURBS, cylindrical helix) and CurveLoop, and recovery, which reads wall location curves, floor boundaries, opening hosts and level elevations from the typed views |
object_graph | DocumentHistory, string-record extractor for Global/Latest + Partitions/NN |
class_index | Quick class-name inventory (BTreeSet) |
Runtime capabilities:
- Open any Revit file from disk (magic
D0 CF 11 E0 A1 B1 1A E1)
- Enumerate every OLE stream; find the version-specific
Partitions/NN
- Decompress any stream (truncated-gzip format — standard gzip header, no trailing CRC/ISIZE)
- Parse
BasicFileInfo, PartAtom, extract preview PNG
- Read every tagged class record in
Formats/Latest with its tag, parent and declared field count
- Decode a family file's 167-byte
Global/PartitionTable, including its stable format-identifier GUID
- Decode a family file's 307-byte
Contents stream, including its embedded UTF-16LE metadata chunk
- Produce a byte-for-byte round-trip copy of a
.rfa / .rvt file
- Run across the 11-release family corpus in under 500 ms per file (release build, measured in April 2026; for large projects see Performance on large projects)
Nineteen CLIs ship in the box:
cargo build --release
# One-shot forensic analysis — all subsystems in one report
./target/release/rvt-analyze --redact my-project.rvt
./target/release/rvt-analyze --redact --json my-project.rvt > report.json
# Quick metadata + schema summary
./target/release/rvt-info --show-classes my-project.rvt
# Machine-readable (JSON)
./target/release/rvt-info -f json my-project.rvt > meta.json
# Element and room schedules for Excel / Sheets / LibreOffice
./target/release/rvt-schedule my-project.rvt
./target/release/rvt-schedule my-project.rvt --schedule rooms --metric --excel
# Inventory every Revit file under a folder: release, worksharing, last saved
./target/release/rvt-info projects/
./target/release/rvt-info projects/ -f csv --redact > inventory.csv
# Pull the embedded thumbnail
./target/release/rvt-info --extract-preview preview.png my-project.rvt
# Plain-language file health and IFC export readiness
./target/release/rvt-inspect my-project.rvt
./target/release/rvt-inspect my-project.rvt --json
# Compare two versions of the same file (cross-version byte diff)
./target/release/rvt-diff --decompress 2018.rfa 2024.rfa
# Dump the full class schema (4,126 classes and 11,562 fields on a Revit 2024 file)
./target/release/rvt-schema my-project.rvt
# Document upgrade history (which Revit releases have opened this file)
./target/release/rvt-history my-project.rvt
# Pull every UTF-16LE string record out of Partitions/NN
# (categories, OmniClass, Uniformat, Autodesk unit identifiers, …)
./target/release/rvt-history --partitions my-project.rvt
# Hex-dump every decompressed stream (for format research)
./target/release/rvt-dump my-project.rvt
# IFC4 STEP export — everything decoded; the default mode accepts a scaffold-only result, with warnings
./target/release/rvt-ifc my-project.rvt -o out.ifc
# Require a stronger quality gate before writing IFC: exits non-zero, writing nothing,
# while the diagnostics list unsupported features (true of every reference model today)
./target/release/rvt-ifc my-project.rvt -o out.ifc --mode strict
# IFC4 export with a shareable JSON readiness/support sidecar
./target/release/rvt-ifc my-project.rvt -o out.ifc --diagnostics out.diagnostics.json
# Diagnostic IFC export — include low-confidence proxy candidates with provenance
./target/release/rvt-ifc my-project.rvt -o diagnostic.ifc --diagnostic-proxies
# Compare an rvt-rs IFC export against a Revit (or other) reference IFC
./target/release/rvt-ifc-compare out.ifc revit-reference.ifc --json /tmp/ifc-compare.json
# glTF 2.0 binary export — loads in Three.js / Blender / any glTF viewer
./target/release/rvt-gltf my-project.rvt -o out.glb
# 2D plan-view SVG — per-category colours, ready for plot/laser-cut/printing
./target/release/rvt-sheet my-project.rvt -o out.svg
# Global/ElemTable dump — declared element-ids + record layout (family 12B / project 28B/40B)
./target/release/rvt-elem-table my-project.rvt --limit 20
# Production decoded elements / class counts (JSON; mirrors Python element_counts)
./target/release/rvt-elements my-project.rvt --counts
# Stream-level write path — patch named OLE streams via JSON manifest
./target/release/rvt-write my-project.rvt --patches patches.json -o patched.rvt
# ADocument's instance fields, as text or JSON
./target/release/rvt-doc my-project.rvt > doc.txt
# Cross-version corpus analysis (12 releases in one pass; 3 or more files)
./target/release/rvt-corpus /path/to/corpus-dir/*.rfa
# Triage a folder of files into failure buckets
./target/release/rvt-corpus doctor /path/to/corpus-dir --json
Every format finding has a probe under probes/examples/, whose module documentation states the fact it proves and how to check it; the probe_re*.rs files carry the numbered findings in reports/element-framing/. The probes of the first reconnaissance:
cargo build --release --examples -p rvt -p rvt-probes
# --- schema ↔ data linkage ---
./target/release/examples/probe_link <file> # null-hypothesis: class names absent from Global/Latest
./target/release/examples/tag_bytes <file> # hex around known class names in Formats/Latest
./target/release/examples/tag_dump <file> # statistical sweep of post-name u16 patterns
./target/release/examples/link_schema <file> # tag-frequency histogram in Global/Latest (340× non-uniformity)
./target/release/examples/tag_drift <sample-dir> <out.csv> # per-class drift table 2016-2026
./target/release/examples/tag_drift_svg <in.csv> <out.svg> # render drift table as colour-coded SVG heatmap
# --- record framing ---
./target/release/examples/record_framing <file> # dump bytes at tagged-class defs + first tag occurrence
./target/release/examples/elem_table_probe <sample-dir> # Global/ElemTable structural sweep across releases
./target/release/examples/partitions_header_probe <sample-dir> # 44-byte Partitions/NN header + chunk offsets
./target/release/examples/contents_probe <file> # Contents stream decoder (creator name + build tag)
# --- stable anchors ---
./target/release/examples/partition_invariant <sample-dir> # find 165-byte invariant in Global/PartitionTable
./target/release/examples/partition_diff <sample-dir> # show the 2 varying bytes per release
./target/release/examples/partition_full <file> # full annotated hex dump + UUID decode
# --- write path ---
./target/release/examples/roundtrip # copy 2024 sample, verify all 13 streams identical
Format overview
Every Revit file is a Microsoft Compound File Binary (OLE2) container with
this stream layout (constant across 11 years of Revit releases):
<root>
├── BasicFileInfo UTF-16LE metadata
├── Contents custom 4-byte header + DEFLATE body
├── Formats/Latest DEFLATE — class schema inventory
├── Global/
│ ├── ContentDocuments tiny document list
│ ├── DocumentIncrementTable DEFLATE — change tracking
│ ├── ElemTable DEFLATE — element ID index
│ ├── History DEFLATE — edit history (GUIDs)
│ ├── Latest DEFLATE — current object state (17:1 ratio)
│ └── PartitionTable DEFLATE — partition metadata
├── PartAtom plain XML (Atom + Autodesk partatom namespace)
├── Partitions/NN bulk data: 5-10 concatenated DEFLATE segments
│ NN = 58, 60-70 for Revit 2016-2027
├── RevitPreview4.0 custom header + PNG thumbnail
└── TransmissionData UTF-16LE transmission metadata
All compressed streams use a "truncated gzip" format — the standard 10-byte
gzip header (magic 1F 8B 08 ...) followed by raw DEFLATE, but without
the trailing 8-byte CRC32 + ISIZE that conforming gzip writers produce.
Python's gzip.GzipFile and Rust's flate2::read::GzDecoder both refuse
these streams. The fix is to skip the 10-byte header manually and use
flate2::read::DeflateDecoder on the raw body.
Reverse engineering state
The format, layer by layer, and how far each layer is understood. Some rows record the research history they come from; the current, evidence-backed status of each capability is in docs/support-matrix.json.
| Layer | Description | Status |
|---|
| 1 · Container | OLE2 / Microsoft Compound File ([MS-CFB]) | Done |
| 2 · Compression | Truncated gzip → raw DEFLATE | Done |
| 3 · Stream framing | Per-stream custom headers, Partitions/NN chunk layout, Contents / Preview / PartitionTable wrappers | Done — 165/167 bytes of PartitionTable invariant; 44-byte Partitions/NN header decoded; 62 19 22 05 wrapper magic confirmed on Contents + RevitPreview4.0 |
| 4a · Schema table | Class names + fields + C++ type signatures from Formats/Latest; per-class tag + parent + declared field count; cross-release tag-drift map | Done |
| 4b · Schema→data link | Tags from Formats/Latest occur at ~340× the noise rate in Global/Latest; schema IS the live type dictionary for the object graph | Done |
| 4c.1 · Record framing | Tagged class records in Formats/Latest parse into structured records: {tag, parent, ancestor_tag, declared_field_count}; HostObjAttr → {parent=Symbol, declared_field_count=3}. The tag a record carries is its base class's tag and ancestor_tag is its grandparent's, on 53 files of 2016 to 2027 (RE-146) | Done |
| 4c.2 · Field-body decoding | FieldType enum classifies all but 9 to 12 schema fields per release across 8 variants (Primitive, String, Guid, ElementId, ElementIdRef, Pointer, Vector, Container). 11 discriminator bytes mapped, including generalized scalar-base Vector/Container ({kind} 0x10 ... / {kind} 0x50 ...) and the 0x0d point-type base. | Done (all but 9 to 12 of 10,000 to 12,000 fields per release since #410; the earlier 100.00% on 13,570 fields covered the first 64 KB of each schema; zero Unknown) |
| 4d · ElemTable | Global/ElemTable: the u32 record count at 0x02, then records from 0x06, 28 bytes through Revit 2023 and 40 from 2024, each with its ElementId and owner (#152, RE-41, #737) |
The original research questions are resolved: those on field encodings, Global/Latest and Partitions/NN as first answered, and the class preamble's flag word by RE-146, after its first reading did not reproduce (RE-137). Layer 4c.2 classified 100.00% of the 13,570 fields in the first 64 KB of each schema; #410 found the rest of Formats/Latest was being inflated without its page strip, and on the whole schema (122,107 fields across 11 releases) 9 to 12 fields per release remain unclassified. In April 2026 the next frontier was real-world project files, where one .rvt probe caught a gzip_header_len bounds bug that family files never hit; the partition element records (RE-21 onward) and the measured tables above are where that work went.
Key findings from this phase:
- Class preamble flag word (superseded, RE-137) The u16 "flag" word in each tagged-class preamble was read as a class-tag reference distinct from the parent. That reading did not reproduce under the page-stripped schema grammar (#410, #154): the nine classes behind it have the bases the grammar gives (
HostObjAttr → Symbol), not the "ancestor" named, on 17 files of 2016 to 2026, and every base reference resolves. RE-146 then identified the word: it is the grandparent class's tag, which an earlier addendum had looked up among base tags (#154).
- Field type encoding Each field's
type_encoding is [byte category][u16 sub_type][optional body]. 9 category bytes mapped (0x01 bool, 0x02 u16, 0x04/0x05 u32, 0x06 f32, 0x07 f64, 0x08 string, 0x09 GUID, 0x0b u64, 0x0e reference/container).
- Field-type coverage extended to 84% of fields,
then reaches 100% of the fields in the first 64 KB of each schema (13,570 across 11 releases; #410 later read the whole schema). Generalized
{scalar_base} 0x10 ... / {scalar_base} 0x50 ... as vector/container modifiers; added 0x0d point-type base; added 0x08 0x60 ... alternate string encoding; added ElementIdRef { referenced_tag, sub } for references that carry a specific target-class tag; added deprecated 0x03 i32-alias seen only in 2016–2018. See docs/rvt-moat-break-reconnaissance.md §Q5.2.
Global/Latest layout It is not an index + heap — it's a flat TLV stream.
- Instance data is schema-directed (tag-less, protobuf-style). Decoding requires schema-first sequential walk from a known entry point.
Partitions/NN trailers Their u32 fields are not per-chunk offsets. Gzip-magic scan remains correct.
The full analysis narrative with 12 dated addenda lives in docs/rvt-moat-break-reconnaissance.md. Session-length synthesis in docs/rvt-phase4c-session-2026-04-19.md.
Sample corpus
The real-file checks read public corpora that are fetched, never committed:
- Families: Autodesk's
rac_basic_sample_family, one per Revit release from 2016 to 2027, distributed through Git LFS in phi-ag/rvt. RVT_SAMPLES_DIR points at them.
- Projects: the MIT-licensed models of
magnetar-io/revit-test-datasets (Core Interior, Einhoven) and the RE1 models of Drshelden/IFC-ECS, each with Revit's own IFC export. RVT_PROJECT_CORPUS_DIR points at them.
A check whose files are absent skips with a message, so a partial corpus is fine. CONTRIBUTING.md shows how to fetch both, and docs/reference-models.md lists every model the measurements use.
Design choices
- cfb crate over custom OLE parser — the
cfb crate is mature,
tested against Office documents, and handles both short and regular
sectors. Faster than writing our own.
- flate2 over miniz_oxide direct —
flate2 wraps both miniz_oxide
(pure Rust) and libz backends. We pick the default pure-Rust build to
avoid a C toolchain dependency.
- quick-xml over xml-rs — ~3x faster, zero-copy friendly, and the
.from_str + event-loop pattern is closer to what Go/Python parsers do.
- encoding_rs over stdlib — Revit's UTF-16LE streams sometimes have
malformed pairs at boundaries (single-byte markers get interleaved).
encoding_rs recovers gracefully where stdlib panics.
- BTreeSet for class names — deterministic ordering in output (plus
sorted JSON) matters for diffable CLI output.
For contributors
Use the Codespaces development environment for remote
Linux builds and the viewer from VS Code, a terminal, Codex or Claude Code.
rvt-rs is a clean-room reader for Revit files: an Apache-2.0 Rust core with
Python bindings and a WebAssembly viewer, with no Revit install or Autodesk
SDK at build or run time. Contributing does not need private files either —
the checked-in corpus/tier1/ synthetic fixtures drive the default gates, and
corpus-backed tests skip themselves while RVT_PROJECT_CORPUS_DIR is unset.
- Build: stable Rust 1.87 or newer, then
cargo build. Viewer:
cd viewer && pnpm install --frozen-lockfile. Python bindings: docs/python.md.
- Gate:
tools/check-local.sh runs the checks a pull request must pass:
cargo fmt --check, cargo clippy -D warnings, rustdoc with -D warnings,
a build of every target, and the real-file, CLI and contract checks.
--viewer, --corpus, --deny, --audit add the optional gates.
- How changes are verified: against real Revit files and Revit's own
exports, through the built CLIs, and by contract checks that keep the
published claims honest.
tools/ci/test-targets.txt
classes every target under tests/ (corpus-gated ones skip without their
corpus); tests/fuzz_regressions.rs replays crash-shaped inputs on every
pull request; libFuzzer targets are in fuzz/, Playwright browser tests in
viewer/tests/ and Python tests in tests/python/. Unit tests written
before this policy stay in the source but are not run, and new ones are not
accepted (CONTRIBUTING.md says why).
- What "done" looks like: the gate is green, the pull-request template's
checklist is filled in, and any change to user-visible capability updates
docs/status.md and
docs/support-matrix.json in the same PR — the
project does not claim what its tests cannot show.
- Start here:
CONTRIBUTING.md walks from clone to a
first pull request;
maps the larger areas; small tasks carry the
label.
License and trademarks
- Code: Apache License 2.0. See
LICENSE for the full
text and NOTICE for attribution.
- Trademarks: "Autodesk" and "Revit" are registered trademarks of
Autodesk, Inc. rvt-rs is an independent project, not affiliated
with, endorsed by or sponsored by Autodesk; it uses the names only to
identify the file format it reads.
- Provenance: rvt-rs contains no Autodesk source code. Its knowledge of
the format comes from the bytes of publicly available files and from the
exported symbol names of the public
RevitAPI.dll NuGet package.
CLEANROOM.md lists the sources contributions may use and
the declaration a format finding carries, and NOTICE gives the
project's notices. This section describes how the project is built; it is
not legal advice.