X-Ray: one family of .NET libraries for reading every file we index
Rafael
X-Ray, part 1 of 5. This post is the map: the packages, their origins and how Curiosity uses them. Part 2 covers the design rules they share, part 3 the Office port, part 4 the single-format packages, and part 5 our OCR stack.
A connector hands Curiosity a file. Before anything can search it, link it to the graph or show it to an AI assistant, something has to answer four questions about it: what text does it contain, what structure does that text have, what does it look like, and which of its parts (pages, images, attachments) have to be looked at separately.
In September I wrote about porting xberg to C#, which answered the first two questions for about fifty formats. That was one library. The product needs the others too: a rendered preview of a Word document, the attendees of an invitation, the pages of a OneNote notebook, the text in a scanned form. Curiosity has had a pipeline for all of this since the beginning. X-Ray replaces it with one that we control and can evolve, and that is faster and uses fewer resources. This post lists what we built or ported for it, what each piece stands on, and where the product uses it.
The family
Every package is C# on .NET 10 and installs from NuGet.
| Package | Reads | Comes from | Repository |
|---|---|---|---|
X-Ray.Content |
Text, Markdown, tables and metadata from 49 extractors | Port of the Rust engine xberg | x-ray-content |
X-Ray.Office |
Word, Excel, PowerPoint and ODF, rendered to PDF and images | C# reimplementation of LibreOffice's Writer, Calc and Impress import and layout | x-ray-office |
X-Ray.Psd |
Photoshop PSD and PSB | Port of the PSD engine of the Patchy image editor | x-ray-others |
X-Ray.Email |
Outlook MSG, EML | Derived from Aspose.Email FOSS | x-ray-others |
X-Ray.Ics |
iCalendar, vCalendar 1.0 | Written from RFC 5545, no upstream code | x-ray-others |
X-Ray.PS |
PostScript, EPS, XPS | Port of Aspose.Page FOSS for Python | x-ray-others |
X-Ray.OneNote |
OneNote .one |
Port of Aspose.Note FOSS for Python | x-ray-others |
X-Ray |
Nothing: it depends on all of the above | x-ray |
The documentation is at
xray.curiosity.ai, including a page on
which package to pick. Its rule of
thumb: use X-Ray.Content for what a document says, and a format package for what the document is
(its object model) or what it looks like (rendering).
Office, the single-format packages and OCR each get their own post. OCR is not in the table because it is a separate stack of three engines.
What installing X-Ray brings in
The umbrella package contains no code. Its NuGet metadata lists ten packages, and this is the dependency graph that results, read from the published packages:
Every member except X-Ray.Content stands alone or depends only on its own core package, so you
can install any one of them without the rest. X-Ray.Content is the only member that depends on
another one: it reads Photoshop files through X-Ray.Psd, and it brings the two OCR packages for
its optional OCR pass.
X-Ray.Office is a facade over twelve libraries, layered from Core up to the three document
families. In the published package the facade lists all twelve directly. The diagram draws only the
layering, which is the part that explains how they depend on each other.
Where the product uses them
The workspace documentation describes the routing from the outside: text PDFs, Office files, HTML and email go to a direct parser, scans and images to OCR page by page, archives are unpacked recursively, and audio and video go to speech-to-text. The extraction runs once, when a file is first indexed, and the result is cached on the file node. Here is what sits behind that description.
- Text and structure come from
X-Ray.Content. One extraction produces the plain text the indexes read and the same document as Markdown, with page numbers that mean the same page in both. The Markdown is stored beside the text, and is dropped when it says no more than the text does (a.txtfile, a source file), so only files with headings, lists or tables keep one. Archives are unpacked into child documents by the same library. - Previews and page images of Word, Excel and PowerPoint files come from
X-Ray.Office. It lays a document out and writes a PDF with searchable text, or a PNG per page or slide. - Format-specific objects come from the single-format packages: the layer tree of a Photoshop file, the attendees and recurrence of an invitation, the body and attachments of an Outlook message, the pages of a OneNote section.
- Scans and photos go through the OCR stack.
An empty result for a blank scan is not an error, and the OCR stack always has a fallback engine (see part 5).
This is the arrangement we run as of this post. Curiosity has had a pipeline for this since the beginning, built on third-party libraries. X-Ray replaces it with one that we control and can evolve, and that is faster and uses fewer resources than the one before. The rest of the series assumes the move is complete.
%% REVIEW (Rafael): the paragraph above says the whole pipeline now runs on X-Ray, and that it is faster and uses fewer resources than the previous one. In mosaik today only text extraction goes through X-Ray.Content; previews, e-mail, archives, OneNote and PSD still use the previous library. The faster and lower-resource claims come from you and carry no numbers here. Add measurements if you have them, and check this still reads true on the day it is published. %%
Why one family, and what it costs
The pipeline we started with combined several third-party libraries, each covering part of the format matrix. We could not change them, and the seams were where bugs lived. One family we own gives us one set of conventions: every call that does real work can be cancelled, large outputs are streamed, every reader treats its input as hostile, and every package is versioned and published the same way. Part 2 is about those rules.
The costs are real. We maintain ports whose upstreams keep moving, so each port has a documented
way of syncing. The family is young: the oldest repository's history starts in July, and
the single-format packages in x-ray-others start on 16 October. And a port
inherits the upstream's limits as well as its coverage, which each post states for its package.
What it builds on
Almost nothing here started from a blank page. The main upstreams, in the order the posts reach them:
- xberg, the Rust extraction engine behind
X-Ray.Content, which descends from Kreuzberg. - LibreOffice and The Document Foundation, whose Writer, Calc and Impress code
X-Ray.Officewas read from, and whose binary is the reference its output is measured against. Part 3. - Aspose, whose open-source Email, Page and Note libraries are the starting point for
X-Ray.Email,X-Ray.PSandX-Ray.OneNote. Part 4. - Datalab, PaddlePaddle and Microsoft, for the models behind NanoOCR, PaddleOCR-VL and Florence-2. Part 5.
Thank you to all of them. Each post also says what we changed from the original, and what we left alone.
Try it
dotnet add package X-Ray # the whole family
dotnet add package X-Ray.Content # or only the part you need
If you open an issue, say which package read the file badly. The family is several libraries with separate bugs, and an issue that names the package gets to the right place faster.
Further reading
- Show HN: Kreuzberg, the 2025 thread where the library xberg descends from was introduced. The questions in it (how is PDF extraction better than the usual Python tools, what about handwriting, what does async buy for CPU-bound work) are still the right ones to ask of any extraction library, ours included.
- Where your data goes, step by step, inside Curiosity, for the rest of the path a file takes after extraction.
Read next
Articles on context graphs, enterprise search and industrial AI