Carve PDFs out of Chrome's on-disk cache and save them with their original
filenames — including non-ASCII (Chinese, Japanese, …) filenames that tools
like ChromeCacheView display as \uXXXX escapes.
Bundled with a companion tool, convert_to_dark_mode.py, that converts any
PDF to a VS Code-style dark version (#1E1E1E background, #D4D4D4
foreground) while keeping text selectable and vector graphics sharp — handy
when the original asset is a white-background scan or a slide deck and you
want to read it without the glare.
- Scans every
Cache_Datadirectory underChrome/User Data/**. - Carves PDFs by
%PDF-/%%EOFmagic, including inside gzip-compressed response bodies. - Deduplicates by SHA-256.
- Validates each carve by opening it with PyMuPDF (drops false positives such
as JavaScript sources that happen to contain
%PDF-as a string literal). - Parses the Chrome blockfile cache (
data_0..data_3) to recover each entry's request URL andContent-Disposition, then saves the PDF under its real filename. UTF-8 safe — Chinese filenames round-trip cleanly. - Reads cache files in Windows shared mode (same trick as ChromeCacheView), so it works while Chrome is running.
- Python 3.9+
- Windows (Chrome's exclusive-lock workaround is Windows-specific; on macOS / Linux the scripts will still run, but cache files must be readable without contention, e.g. with Chrome closed).
Install dependencies:
pip install -r requirements.txt
python src/extract_cache_pdfs.py
Defaults:
--cache-dir%LOCALAPPDATA%/Google/Chrome/User Data(scans every profile)--out./extracted_pdfs--min-size1024(skip fragments smaller than 1 KB)
Example:
python src/extract_cache_pdfs.py --out ./pdfs --min-size 4096
When the original name cannot be recovered (e.g. for small PDFs that live
inline in the block files rather than as separate f_* resource files), the
tool falls back to <sha256-prefix>_<source>.pdf.
python src/convert_to_dark_mode.py extracted_pdfs/*.pdf
Writes <stem>.dark.pdf for each input into ./dark_pdfs/.
Colors are modelled on VS Code's Dark+ theme: pure black maps to #D4D4D4
(editor.foreground) and pure white maps to #1E1E1E (editor.background),
with everything in between remapped linearly. The result feels like reading
the PDF inside the VS Code editor rather than staring at a flashlight.
The remap happens inside the PDF, not by rasterizing pages. Three
full-page rectangles with PDF blend modes (Difference → Multiply →
Screen) are appended to each page; the viewer applies them at display time
on top of the original, untouched content. That means:
- Text stays selectable — drag-select / copy / paste works exactly like the source.
- Vector graphics stay vector — circuit diagrams and schematics stay crisp at any zoom.
- Embedded images keep their native resolution — no re-encode, no quality loss.
- Output file size tracks the source instead of ballooning into a pile of page-sized PNGs.
Known trade-off: coloured content has its hue flipped (red → cyan, blue → orange, etc.) because PDF blend modes are per-channel linear and can't do HSV hue rotation. For text-heavy scans and black-on-white circuit diagrams this is invisible; for colour-coded figures it is noticeable.
Wildcards work on any shell — the script expands */?/[...] itself, so
extracted_pdfs/*.pdf behaves the same in PowerShell, cmd, and bash.
Flags:
--out-dir(defaultdark_pdfs)
Chrome's classic disk cache (the one with data_0..data_3 + f_XXXXXX
files) stores each HTTP response as an EntryStore record — a fixed 256-byte
struct — inside one of the block files. The record holds:
- the full request URL (the "key"), inline or via a
long_keypointer, data_size[4]anddata_addr[4]arrays pointing at up to four streams (stream 0 = response headers, stream 1 = response body).
When the body is large, stream 1 is stored as a standalone f_XXXXXX file
whose number is encoded into the data_addr[1] CacheAddr as
0x80000000 | file_number.
Given a carved PDF from f_002456, we:
- Build the CacheAddr (
0x80002456) and search every block file for that 4-byte pattern. - The hit sits at offset 60 of the owning
EntryStore; we back up, parse the struct, and read the inline key bytes. - Inline keys are capped at 160 bytes, so long URLs get truncated. We fix
this by collecting every complete
.pdfURL found anywhere in the cache and matching the truncated prefix back to the full URL. - The request URL's path component — URL-decoded — is the original
filename. If
Content-Disposition: filename*=UTF-8''…is present in the response headers (stream 0), we prefer that.
Chrome holds the cache files with a restrictive share mode, so plain
open() fails with PermissionError while the browser is running. We call
CreateFileW directly via ctypes with
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE — the same
approach ChromeCacheView uses — to read them regardless.
- Only the classic blockfile cache is parsed. The newer Simple Cache format (used by Service Worker / HTTP/3 sometimes) is not yet supported.
- Small PDFs stored inline in
data_*block files rather than in separatef_*files won't get their filename resolved; they're saved with a content-hash prefix. - Shared-mode reads are Windows-only. On macOS / Linux you may need to close Chrome before running.
MIT — see LICENSE.