CLI tool
freelawproject/x-ray avatar
freelawproject/x-ray

x-ray: finding the black rectangles that never hid anything

A tool to detect whether a PDF has a bad redaction

832 stars56 forksPythonBSD-2-Clause

At a glance

What is it?
x-ray is a BSD licensed Python library and command line tool from the Free Law Project that finds bad redactions in PDF documents, drawings that cover text without removing it, meaning a filing can still be unredacted by selecting underneath. The judgement is that the method is a pragmatic colour heuristic rather than a PDF redaction audit, and the README is honest about that.
Who is it for?
x-ray fits a legal team, a records office or anyone publishing documents that arrive with redactions applied by other people, because it turns a silent disclosure risk into a list of page numbers and text fragments that can be checked. It is the wrong choice if you need a formal redaction audit across a corpus, since the method is a rendering heuristic and the README says outright that bad redactions take many forms and that other cases are not supported yet.
Can I use it commercially?
Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem is a rectangle drawn on top of text

The reason x-ray exists is stated plainly in the README. At the Free Law Project, the team collects millions of PDFs, and an ongoing problem is that people fail to properly redact things. Instead of doing it the right way, they draw a black rectangle or a black highlight on top of black text and call it a day. The consequence is obvious once you know it, because you can select the text under the rectangle and read it again. The project says it witnessed this for years, and gives the case it likes best, a document that shared Taylor Swift's personal phone number. The tool is the answer to that, since you give it the path to a PDF and it tells you whether the document has worthless redactions in it, and whether to call Taylor. The audience is therefore anyone who publishes or receives documents where a redaction is supposed to protect something.

Rectangles, letters, and a flat colour test

The How it works section describes the algorithm in four steps, and the brevity is the point. First it finds rectangles in a PDF. Then it finds letters in the same location. Then it renders the rectangle as an image. Finally it inspects the rectangle to see whether it is all one colour, and if it is, that is a bad redaction, while if it is not, the tool assumes you can see a mix of text and drawings, which indicates a redaction that is fine. Under the covers, x-ray uses the PyMuPDF project to parse PDFs, and the README calls that project a pleasure to work with. The reasoning behind the colour test is worth understanding, because it explains the tool's limits. A properly redacted page has no glyphs under the rectangle, so the rendered region is flat, and an improperly redacted page has glyphs that were merely covered, so the rendered region shows the shapes of letters mixed in with the fill.

Install with uv, or run it without installing

Two installation routes are given, and the first is the modern one:

text
uv add x-ray

With pip, the equivalent is:

text
pip install x-ray

Once installed, the command line takes a path and prints JSON, and a clean document comes back as an empty dictionary. The installed form is:

bash
% xray path/to/your/file.pdf

You can also run it without installing anything at all, since uvx will fetch the package for a single invocation, and the README shows that against a filing with no bad redactions, which returns an empty dict. If the file lives on a server, give it a URL instead, and anything that starts with https:// is treated as a PDF to download. For bulk work, put one URL per line in a file named urls.txt and run the tool over it with xargs, which is the shape most collections will need.

The JSON shape: page numbers, boxes and the text underneath

The output format is documented precisely, which makes it easy to pipe into other tooling. The result is a dictionary. The keys are page numbers. Each page number maps to a list of dictionaries, and each of those maps to two keys. The first key is bbox, a four-tuple giving the x and y positions of the upper left corner and then the lower right corner of the bad redaction. The second key is text, the text under the bad rectangle. A file with one problem therefore produces a single key with a list of one entry, and the README says the output can be used with tools like jq. The bulk invocation, with one URL per line, is worth writing out because it is the form a records office will actually use:

bash
xargs -n 1 xray  < urls.txt

The interesting part of that JSON is the text field, because a rectangle that leaks is only interesting if you can read what it leaked.

The same tool as an async Python module

The library form is available for people who prefer the long form, and the module can be run directly:

code
% python -m xray some-file.pdf

The README notes that this is not as easy to remember as the command. The asynchronous API is the part that matters for integration, and the shape is a single call:

python
from pprint import pprint

import xray

bad_redactions = await xray.inspect("some/path/to/your/file.pdf")
pprint(bad_redactions)

The output is the same as the JSON version except that it is a Python object rather than a JSON object. inspect() is asynchronous for every input type, including pathlib.Path, and in a synchronous script you import asyncio and use asyncio.run(xray.inspect(...)) at the entry point, while the command line usage stays the same. URL downloads use httpx2 with HTTP/2 enabled, redirects followed and a 10 second timeout, and the PDF analysis itself runs synchronously after the download completes.

The input type decides what x-ray does with it

Because inspect() has the same signature whatever you give it, the type of the object is what tells the tool what to do, and the README is explicit about the mapping. A str or a pathlib Path is treated as a local file. A str that starts with https:// is treated as a URL to download. A bytes object is treated as a PDF already in memory. That last case is the one with a sharp edge, because the README shows the failure directly, since providing the filename on disk as a bytes object is not going to work. The same applies to a bytes object that holds a path rather than the document. If you are building a service that fetches documents itself, the bytes path is the one to use, and the README shows the shape with an httpx2 async client configured with http2 enabled, follow_redirects and a timeout of 10, then handing response.content to the same inspect call.

The heuristic's limits, and the state of the project

The project is honest that the method is not complete. The PDF format is a big and complicated one, so it is difficult to do all this perfectly, the README says, and that they do their best while acknowledging there is always more to do. The What next section is blunter, noting that bad redactions take many forms and that the issues tab holds cases not yet supported, with an invitation to help with the tougher ones. Donations and sponsored work help. The packaging is modern and small, with the manifest naming the project x-ray at version 0.4.0, requiring Python 3.11 or later with classifiers up to 3.15 and PyPy, depending on PyMuPDF pinned at 1.24.14 and httpx2 with the http2 extra, and registering a console script called xray. The most recent published release is v0.3.5 from 2025-12-08, so the packaged version and the manifest version differ. Releases happen automatically through GitHub Actions after updating CHANGES.md, the version in pyproject.toml and tagging the commit, with a manual path through Poetry. The licence is the permissive BSD one, contributions need a signed contributor licence agreement, and the last push was on 2026-10-15 with the repository not archived.

Editorial conclusion

x-ray fits a legal team, a records office or anyone publishing documents that arrive with redactions applied by other people, because it turns a silent disclosure risk into a list of page numbers and text fragments that can be checked. It is the wrong choice if you need a formal redaction audit across a corpus, since the method is a rendering heuristic and the README says outright that bad redactions take many forms and that other cases are not supported yet. It is also a poor fit if your documents contain no redactions at all, where it simply returns an empty dictionary. Before you adopt it, run it across a set of documents from your own collection and read the issues list, because the cases it misses are documented there, and note that the packaged version and the version in the repository metadata do not currently match.

Frequently asked questions

How do I install and run x-ray to check a PDF for bad redactions?

Install it with uv add x-ray or pip install x-ray, then run xray with the path to your file, such as xray path/to/your/file.pdf. A document with no bad redactions returns an empty dictionary, and a URL starting with https:// is downloaded and analysed instead.

What does the output of xray look like?

It is a dictionary whose keys are page numbers, each mapping to a list of dictionaries with two keys, bbox, a four-tuple of the upper left and lower right corners of the bad redaction, and text, the text under the bad rectangle. The result can be piped through tools like jq.

Can x-ray analyse a PDF that is already in memory?

Yes. A bytes object is interpreted as a PDF in memory, and inspect() is asynchronous for every input type including pathlib.Path, so a synchronous script should call asyncio.run(xray.inspect(...)). Passing a filename on disk as bytes will not work, because the tool would read it as a document.

Official sources

  1. freelawproject/x-ray on GitHub
  2. Issues
  3. License: BSD-2-Clause
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/freelawproject-x-ray.svg)](https://hysenlabs.com/projects/freelawproject-x-ray)

Was this useful?