https://txtfetch.com/tools/file-to-text/
Drop any file. Or a whole folder. Get the text.
You don't have to know what your files are, or sort them first. Drop one file, many files, a folder, or a .zip. This page reads every one and hands back the text. If there isn't any, it tells you plainly why.
Drop a file, many files, a whole folder, or a .zip below. This page works out what each one is from its bytes, then reads it. It runs entirely in your browser, and nothing is uploaded.
Up to 25 MB a file, 50 files or 100 MB a batch, read fully in-browser — bigger jobs still work through the API.
What's not in this text:
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@__FILENAME__import os
import requests
with open("__FILENAME__", "rb") as f:
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
files={"file": f},
)
print(r.json()["extracted_text"])import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("__FILENAME__")]);
const form = new FormData();
form.append("file", file, "__FILENAME__");
const res = await fetch("https://api.txtfetch.com/v1/extract", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
body: form,
});
const { extracted_text } = await res.json();
console.log(extracted_text);package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
type extractResponse struct {
Status string `json:"status"`
ExtractedText string `json:"extracted_text"`
}
func main() {
f, err := os.Open("__FILENAME__")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "__FILENAME__")
if err != nil {
panic(err)
}
if _, err := io.Copy(part, f); err != nil {
panic(err)
}
writer.Close()
req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result extractResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
fmt.Println(result.ExtractedText)
}for f in ./batch/*; do
curl -s -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@"$f" | jq -r '.extracted_text' > "${f%.*}.txt"
doneimport glob
import os
import requests
for path in glob.glob("./batch/*"):
with open(path, "rb") as f:
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
files={"file": f},
)
text = r.json()["extracted_text"]
print(f"{path} -> {len(text)} chars")import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
const dir = "./batch";
for (const name of await readdir(dir)) {
const file = new Blob([await readFile(join(dir, name))]);
const form = new FormData();
form.append("file", file, name);
const res = await fetch("https://api.txtfetch.com/v1/extract", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
body: form,
});
const { extracted_text } = await res.json();
console.log(`${name} -> ${extracted_text.length} chars`);
}package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
type extractResponse struct {
Status string `json:"status"`
ExtractedText string `json:"extracted_text"`
}
func main() {
dir := "./batch"
entries, err := os.ReadDir(dir)
if err != nil {
panic(err)
}
for _, entry := range entries {
f, err := os.Open(filepath.Join(dir, entry.Name()))
if err != nil {
panic(err)
}
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", entry.Name())
if err != nil {
panic(err)
}
io.Copy(part, f)
writer.Close()
f.Close()
req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
var result extractResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
resp.Body.Close()
fmt.Printf("%s -> %d chars\n", entry.Name(), len(result.ExtractedText))
}
}whats-hard-about-this
Sixteen of txtfetch's free tools each read one format well. But that means a visitor has to already know what their file is before picking the right page out of a grid. That's backwards for a product whose whole pitch is you shouldn't have to care what format you're holding. So this page fingerprints each file the same way the extraction API does. It reads the first few thousand bytes and routes the file to whichever reader understands them.
A few formats still get an honest "no" instead of a fake result. Images get a real OCR-readiness verdict, not invented text. Running Tesseract in a browser tab isn't practical, so txtfetch's API adds that step automatically. Video and audio get their caption track read when one exists, never a transcription of speech nobody wrote down. None of that is hidden in fine print. It's spelled out on the page.
Two readers were built just for this page: RTF and delimited text. Both carry the same failure modes as everything else here. RTF's \uN Unicode escapes are legally followed by ANSI fallback bytes for readers that don't understand \u. Getting the byte count wrong duplicates every non-ASCII character. A CSV's delimiter and quoting rules aren't one universal standard either. A file that isn't valid UTF-8 gets flagged, not silently decoded wrong.
A batch has limits. This page reports every one instead of failing quietly: 50 files, 100 MB total, 25 MB for any single file. A file over a limit still gets a row in the results, marked skipped, with the reason next to it.
what-this-doesnt-do
Said plainly, not left for you to discover. No OCR runs in your browser. A scanned page or a photo gets a readiness verdict, never invented text. The OCR-readiness checker goes deeper, and the API adds the OCR step itself. No audio or video is transcribed. Only an existing caption or subtitle track is read, the same way the caption reader works. No password-protected file is decrypted. A plain .zip does get read all the way through, member by member. A member this browser can't decompress is reported, not guessed at.
what-to-do-next
Got the text out and want the API call for it directly? The panel above already has it, with your file's real name. More than 50 files? More than 100 MB total, or a single file over 25 MB? Or a job you'd rather run on a server than in a tab? See the API quickstart →
faq
- How does it know what kind of file I dropped?
- From the bytes, not the filename. The same fingerprinting used on /formats reads the first few thousand bytes. It matches them against real format signatures: a ZIP header, a PDF header, an RTF control word, and more. A mislabeled extension doesn't fool it.
- Does it upload my files anywhere?
- No. Detection and extraction both run in your browser, one file at a time or a whole batch. The bytes never leave your machine — only the text you choose to copy or download does.
- Can I drop many files at once?
- Yes. Select several files, drag a group onto the drop zone, or pick a whole folder. Each file gets its own row: path, detected type, verdict, and character count. Open a row to read its text, or copy and download it on its own.
- What do I get for a whole folder?
- Pick the folder, or drop a .zip of it, and every file inside gets read. Download the results as a .zip, with one .txt file per input. Or download one .jsonl file with a {path, bytes, type, chars, text} line per file. Feed that .jsonl straight into an ingestion job.
- What happens with a scanned PDF or a photo of a document?
- You get an honest verdict, not invented text. A scanned PDF page is reported as a scan needing OCR. An image gets an OCR-readiness check (resolution, blur, skew) rather than an OCR attempt. txtfetch's API runs real OCR on both. This page tells you whether it's worth trying first.
- Can it get a transcript from a video or audio file?
- Only if the file already carries a caption or subtitle track. This reads that track, the same way the subtitle tool does. It never listens to the audio or guesses at spoken words. Neither does txtfetch's API.
- What about a .zip archive?
- Every file inside gets read, not just listed. Folder paths inside the archive are kept, so invoices/jan.pdf stays invoices/jan.pdf in the results and in the downloads. txtfetch's API reads a .zip the same way, in one request.
- Is there a file size limit?
- 25 MB for a single file, read entirely in your browser. A batch caps at 50 files or 100 MB total, and reports any file it has to skip. Bigger jobs still go through the API, which has no such limits.
- It said the file type is unknown — does that mean txtfetch can't read it?
- Not on its own. It means this page's smaller, in-browser fingerprint table didn't recognize the bytes. That's a different question from whether the API can read the file. Rather than guess, look the extension or media type up at txtfetch.com/formats/coverage. That list comes straight from the exact Apache Tika build the API runs, so it gives a real verdict, not a maybe.
That was one file. The API does the queue.
This page read your file on your own machine. The API reads a folder of them.
Read the quickstart →