https://txtfetch.com/extract/legacy-office/
The 97-2003 binary formats, read properly.
OLE2 Compound File Binary Format isn't a smaller version of OOXML. It's a different filesystem-in-a-file, and .doc text isn't stored in reading order.
the-problem
Before OOXML, Office files were OLE2, the Compound File Binary Format. That's essentially a tiny FAT filesystem embedded in one file, streams and all. It shares nothing structurally with the zip-of-XML that .docx/.xlsx/.pptx use. So a parser built for the modern formats throws outright on a legacy one, rather than degrading gracefully. Word's .doc format compounds this. The document text sits in a WordDocument stream as a piece table. That's a list of byte ranges that must be reassembled in a specific order to produce readable text. Read the stream's raw bytes start to finish and you get scrambled, out-of-order text. It's not garbage exactly, but it's wrong. There's also a classic naming trap. Plenty of files with a .doc extension are actually RTF or a renamed OOXML file underneath. Extension-based routing gets those wrong before extraction even starts.
one-request-solution
txtfetch detects OLE2's compound-file signature from the file's actual bytes, not its extension. It routes the file to Tika's POI-backed legacy parsers: HWPF for .doc, HSSF for .xls, HSLF for .ppt. Those parsers understand the piece table and reassemble .doc text in the correct reading order. Because detection is byte-based, a file named .doc that's really RTF or renamed OOXML still gets parsed correctly instead of misrouted.
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@1998-contract-template.docimport os
import requests
with open("1998-contract-template.doc", "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("1998-contract-template.doc")]);
const form = new FormData();
form.append("file", file, "1998-contract-template.doc");
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("1998-contract-template.doc")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "1998-contract-template.doc")
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)
}Or skip the download. Pass a url parameter and txtfetch fetches the document server-side:
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/archive/legacy-invoice.xls" \
-H "Authorization: Bearer $TXTFETCH_KEY"import os
import requests
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
params={"url": "https://example.com/archive/legacy-invoice.xls"},
)
print(r.json()["extracted_text"])const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/archive/legacy-invoice.xls");
const res = await fetch(endpoint, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
});
const { extracted_text } = await res.json();
console.log(extracted_text);package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
type extractResponse struct {
Status string `json:"status"`
ExtractedText string `json:"extracted_text"`
}
func main() {
endpoint, err := url.Parse("https://api.txtfetch.com/v1/extract")
if err != nil {
panic(err)
}
q := endpoint.Query()
q.Set("url", "https://example.com/archive/legacy-invoice.xls")
endpoint.RawQuery = q.Encode()
req, err := http.NewRequest("POST", endpoint.String(), nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
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)
}{
"status": "success",
"extracted_text": "..."
}what-comes-back
The corpus behind /diff has no recorded .doc document yet, so we have nothing honest to show you here. Run one of your own instead. The free converter reads the file in your browser, and nothing is uploaded.
formats-covered
.doc.xls.ppt.pub.wpd
faq
- Why does my .doc file come out as scrambled or out-of-order gibberish?
- The legacy .doc format stores text as a piece table: byte ranges in the WordDocument stream that aren't laid out in reading order. A byte-scraping extractor that reads the stream start-to-finish gets the pieces in the wrong order. Tika's HWPF parser reassembles them correctly.
- Do modern, OOXML-only libraries support .doc and .xls at all?
- Usually not. Many libraries built only for .docx/.xlsx/.pptx throw an error on an OLE2 file rather than degrading. txtfetch routes OLE2 signatures to dedicated legacy parsers instead: POI's HWPF, HSSF, and HSLF.
- What if a file is named .doc but is actually RTF or a renamed OOXML file?
- txtfetch detects the real format from the file's byte signature, not its extension, so a mislabeled file still routes to the correct parser.
- Are .pub (Publisher) and .wpd (WordPerfect) files supported too?
- Yes. Both are legacy binary formats handled through the same Apache Tika pipeline as .doc/.xls/.ppt.
go-further
- Read the .doc guide →
- Drop a real .doc and see the exact text it extracts to, free →
- Drop a real .xls and see every sheet's text, free →
- Drop a real .ppt and see its slide text and speaker notes, free →
- The modern OOXML formats (.docx/.pptx/.xlsx) →
- API quickstart →
- How the pipeline works →
- Extract it from your language →
- Get an API key →
Send a real .doc through it.
One HTTP call returns the text. Read one in your browser first, for free.
Get an API key →