LlmFiles
Converter to Markdown
LlmFiles/Guides/PDF to Markdown without the cloud

PDF to Markdown without the cloud

Every mainstream converter asks you to upload the file first, and the file is not the text inside it. This page measures what an upload actually carries, then runs the conversion with every outbound socket blocked and compares the output byte for byte.

Updated 5 Nov 2026 · LlmFiles · Russian version

Three promises that get confused

"We do not store your files" and "conversion runs inside your own network" sound alike in a pitch and are different claims. In the first case the document still leaves, once, in full. In the second it never leaves. Keep the levels apart:

What the service saysWhat it meansWhat leaves your machine
No retentionparsed in memory, nothing written to disk or backupthe whole file, every time
Own infrastructureone company's server instead of a resold SaaSthe whole file, to that company
Self-hosted or on-premthe converter runs inside your networknothing, if it makes no other outbound call

Weigh the upload before you decide

The fifteen-page arXiv paper used in the other guides here is 2,215,244 bytes and converts to 39,662 characters of Markdown: 5,868 words, 9,939 tokens in o200k_base. The text you wanted is 1.8% of what you sent. The other 98% is fonts, page objects, an embedded image pipeline and link annotations, and no converter needs any of it:

FileSizeMarkdown outEverything else in the file
paper.pdf, 15 pages2.2 MB39,662 charsfonts, page objects, 113 link annotations
rollout.pptx, 4 slides30,906 B346 chars44 zip parts: theme, layouts, slide XML
handbook.docx3,508 B666 charsalmost nothing: three parts, no metadata stream

The metadata is the part nobody reads

Metadata is not text, so no converter needs it — which is exactly why it survives an upload unnoticed. Read from the PDF Info dictionary and the Office archive's docProps streams:

FileWhere it sitsValue found
paper.pdfInfo / CreatorLaTeX with hyperref
paper.pdfInfo / ProducerpdfTeX-1.40.25
paper.pdfInfo / PTEX.Fullbannerthe TeX Live 2023 banner, 96 characters
paper.pdfInfo / CreationDateD:20240410211143Z
paper.pdf/Annots on 15 pages113 annotations
rollout.pptxcore.xml / lastModifiedBySteve Canny
rollout.pptxcore.xml / created2013-01-27T09:14:16Z
rollout.pptxapp.xml / ApplicationMicrosoft Macintosh PowerPoint, TotalTime 1
revenue.xlsxcore.xml + app.xmlopenpyxl / Microsoft Excel Compatible / Openpyxl 3.1.5
handbook.docxdocPropsabsent: the archive has three parts

The deck is the clearest case. Four slides written this month claim to have been last modified by Steve Canny in January 2013, because the file was built from a template that still carries its author, and the same stream reports a one-minute editing time. Any service you upload that deck to receives the claim as well, whether it wanted it or not. A file re-saved in Word or Excel usually carries more: real author names, the company, revision counters, sometimes a path on someone's disk.

It can be stripped — document inspector, exiftool, a fresh export — but only before the file leaves.

The converter used none of it

Grep the Markdown for each string above — pdfTeX, LaTeX with hyperref, PTEX.Fullbanner, Steve Canny — and every count is zero. What came out is 39,662 characters: zero headings, zero tables, zero code fences, zero links, fourteen form feeds where the pages break, 9,939 tokens. The metadata had no effect on the conversion; it travelled because the file was uploaded whole. Why that paper also arrives without a single heading is a separate problem, measured in the PDF guide and in the eight-document comparison.

Prove the conversion never calls out

Ask a service whether it forwards your file and you get a sentence. A test is better than a sentence. This guard blocks every outbound socket in the Python process and runs the converter inside it, so any attempt to reach the network fails and is recorded with the address it tried:

#!/usr/bin/env python3
"""netguard.py - run a Python program with every outbound network call denied.

    PYTHONPATH=. python3 netguard.py -m app.cli convert paper.pdf -o out

One summary line is printed when the program exits. "network attempts: 0"
means the program never needed the network; every attempt is listed with the
address it tried to reach.
"""
import runpy
import socket
import sys

attempts = []


def block(what):
    def inner(*args, **kwargs):
        attempts.append(f"{what} {args[:2]!r}"[:120])
        raise OSError(f"netguard: {what} blocked")
    return inner


socket.socket.connect = block("connect")
socket.socket.connect_ex = block("connect_ex")
socket.create_connection = block("create_connection")
socket.getaddrinfo = block("getaddrinfo")

argv = sys.argv[1:]
code = 0
try:
    if argv[0] == "-m":
        sys.argv = [argv[1]] + argv[2:]
        runpy.run_module(argv[1], run_name="__main__")
    else:
        sys.argv = argv
        runpy.run_path(argv[0], run_name="__main__")
except SystemExit as exc:
    code = int(exc.code or 0)
except OSError as exc:
    print(f"netguard: stopped: {exc}", file=sys.stderr)
    code = 1
print(f"netguard: exit {code}, network attempts: {len(attempts)}", file=sys.stderr)
for attempt in attempts:
    print("  attempt:", attempt, file=sys.stderr)
sys.exit(code)

First the guard itself, on a script whose only job is to fetch a page. It has to fail, otherwise the guard proves nothing:

$ cat leak.py
import urllib.request
print(urllib.request.urlopen("http://example.com", timeout=5).status)

$ python3 netguard.py leak.py
netguard: stopped: <urlopen error netguard: create_connection blocked>
netguard: exit 1, network attempts: 1
  attempt: create_connection (('example.com', 80), 5)

Then the real conversion, on the same fifteen-page paper:

$ PYTHONPATH=. python3 netguard.py -m app.cli convert paper.pdf -o out
OK   paper.pdf (5868 words)
1 converted · 0 fallback · 0 skipped · 0 failed
netguard: exit 0, network attempts: 0

$ sha256sum out/paper.md out-plain/paper.md
64af02a0f3b9625facc8bf9d3a42313b875e0e6c1ade40b1fe9a9a57f6b5f740  out/paper.md
64af02a0f3b9625facc8bf9d3a42313b875e0e6c1ade40b1fe9a9a57f6b5f740  out-plain/paper.md

Byte-identical output, 5.7 seconds, zero attempts. The same guard around the HTTP service gives the same answer: three uploads to POST /api/convert answered 200 in 5.28 s (the paper), 0.03 s (a DOCX) and 0.03 s (an XLSX), and the process reported zero network attempts when it shut down. The conversion is library code in the same process — there is no OCR API, no document-AI endpoint and no model vendor behind it.

What still needs a network

Converting with the cable pulled

The command line tool is the same converter the endpoint uses — one registry in app/convertlib.py, so a format that works on the site works from a shell, without a quota and without a network:

# one file, Markdown on stdout
python -m app.cli convert contract.pdf --stdout > contract.md

# a folder, recursively, one .md per source plus report.md
python -m app.cli convert ~/documents/q4 -o ~/markdown/q4

# a ZIP of scans already OCR-ed, with a manifest, without clobbering earlier runs
python -m app.cli convert invoices.zip -o out --no-clobber

Exit codes are 0 for a clean run, 1 when a file failed and the rest still converted, 2 for bad arguments, so a cron job can tell one unreadable scan from a wrong flag. An input that is already .md is skipped rather than converted into itself.

On-prem: the CLI today, a container on the roadmap

There is no Team edition and nothing to buy — the product is one free plan. What runs on your own machine today is the command line above: the same registry as the endpoint, no quota and no network call. A packaged container for a whole team — a /data volume, a healthcheck on /healthz, one docker compose up — is on the roadmap, not shipped, so a document that cannot leave your network is handled today by the CLI, not by a licence.

Five questions before you upload anything

  1. Retention: for how long, written where, and is deletion immediate?
  2. Subprocessors: does the extracted text go on to an OCR API, a document-AI service or a model vendor?
  3. Ownership: who runs the service, in which jurisdiction, and who answers for the data?
  4. Offline option: is there an on-prem or CLI build, or is the cloud the only path?
  5. Failure behaviour: what comes back on an error? A service that echoes your bytes into a message has just shown you its storage.
Free tier: 25 conversions a day per IP, files up to 25 MB, no account, and the upload is parsed in memory without a copy on disk. There is no paid plan, no key to buy and no invoice, and no installed edition either: a packaged container is on the roadmap, not shipped, so a document that cannot leave your network is handled by the command line. The pricing page explains where the limits come from and takes a coffee if you feel like it. The conversion API is documented on /api-docs, and the plain upload page is the converter.

Frequently asked questions

Do you keep the files I convert?

No copy is written: the upload is read into memory and converted in the same process, and the free-quota key is a SHA-256 of the client IP rather than a cookie or an account. What the database records is an event row — event name, path, hashed IP, user agent, referrer, country — never file content.

Can I convert with no internet at all?

Yes, from the command line, with two exceptions: a scanned PDF needs local OCR first, and audio transcription downloads its model once, about 150 MB, unless the cache is pre-seeded.

How do I know that is not just a claim?

Run the guard from this page. The control script must fail with a blocked connection; the conversion must finish with zero attempts and the same SHA-256 as an unguarded run. Both results above come from the same 2,215,244-byte, fifteen-page paper.

Is the output worse without a cloud OCR step?

There is no cloud OCR step in the conversion at all — the same text came out byte for byte with the network denied. A scan needs OCR because it has no text layer, and that is a separate step you can run locally.

What about a large contract bundle?

Every upload stops at 25 MB; the CLI has no limit of its own apart from memory. A ZIP converts member by member, with a manifest and a per-file result.

Where is the on-prem build?

There is nothing to download yet. The offline path today is the command line in the repository: same converter, no quota, no network call. A packaged container — one docker compose up, a /data volume, a healthcheck on /healthz — is on the roadmap, not shipped, and it is not for sale anywhere on this site.

Convert a file

Drag a document into the window and get Markdown you can read, diff and feed to a model. Free: 25 files a day, up to 25 MB.