The libraries you'd reach for are already inside Tika.

PDFBox for PDF, POI for Office, tess4j for OCR — solid libraries individually. Wiring them into one pipeline with consistent routing and error handling is a smaller version of the project Apache Tika already finished.

the-parser-zoo

Java's document-parsing libraries are genuinely good — which makes the trap subtler than "missing library," and closer to "you're about to rebuild Tika." Apache PDFBox extracts PDF text and structure well, but text-only PDFBox has no OCR path of its own — a scanned page still comes back blank. Apache POI covers the Office family, but its API is split down a historical fault line: HSSF for legacy binary .xls, XSSF for modern .xlsx, HWPF for .doc, XWPF for .docx — four class hierarchies for what a business user calls "just Word and Excel," plus version-compatibility issues between POI releases and Office file variants that surface as opaque exceptions. OCR means tess4j, a JNA binding to native Tesseract — which pulls in per-OS native library binaries (Windows/macOS/Linux, and increasingly per-CPU-architecture) into what was otherwise a portable JAR, and a mismatch between the JNA version and the bundled native binary is a common source of runtime-only failures that never show up at compile time. Apache Tika itself is this exact stack — PDFBox, POI, and Tesseract via tess4j — wired together with format detection and routing. Assembling that wiring yourself, correctly, is a meaningfully sized project rather than an afternoon's integration work.

librarycoversstops at
Apache PDFBoxPDF text and structure extractionNo OCR of its own — a scanned page with no text layer returns nothing
Apache POI (HSSF/XSSF)Excel — legacy .xls (HSSF) and modern .xlsx (XSSF)Two separate API families for one file type, plus version-compatibility quirks across POI releases
Apache POI (HWPF/XWPF)Word — legacy .doc (HWPF) and modern .docx (XWPF)Same split-API problem as Excel; no unified read-any-Word-file call
tess4jOCR via JNA bindings to native TesseractShips per-OS native binaries into your JAR; JNA/native-version mismatches fail only at runtime

one-request

txtfetch runs that stack for you, behind one endpoint: POST a file or pass ?url= to https://api.txtfetch.com/v1/extract with java.net.http from the JDK's standard library — no PDFBox/POI/tess4j version matrix to pin, and no per-OS native Tesseract binary to bundle into your JAR or container image. The response is the same { "status": "success", "extracted_text": "..." } whether the source needed PDFBox's job, POI's, or tess4j's.

Java
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;

class Main {
	public static void main(String[] args) throws Exception {
		byte[] fileBytes = Files.readAllBytes(Path.of("report.pdf"));
		String boundary = "txtfetch-" + System.nanoTime();

		ByteArrayOutputStream body = new ByteArrayOutputStream();
		body.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"report.pdf\"\r\nContent-Type: application/octet-stream\r\n\r\n").getBytes());
		body.write(fileBytes);
		body.write(("\r\n--" + boundary + "--\r\n").getBytes());

		HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.txtfetch.com/v1/extract"))
			.header("Authorization", "Bearer " + System.getenv("TXTFETCH_KEY"))
			.header("Content-Type", "multipart/form-data; boundary=" + boundary)
			.POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
			.build();

		HttpResponse<String> response = HttpClient.newHttpClient()
			.send(request, HttpResponse.BodyHandlers.ofString());

		// {"status": "success", "extracted_text": "...", "metadata": {...}} —
		// parse with your JSON library of choice (Jackson, org.json, Gson, ...).
		System.out.println(response.body());
	}
}
{
  "status": "success",
  "extracted_text": "..."
}

from-a-url

Skip the download entirely — pass a url parameter and txtfetch fetches the document server-side:

Java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;

class Main {
	public static void main(String[] args) throws Exception {
		String url = "https://api.txtfetch.com/v1/extract?url="
			+ URLEncoder.encode("https://example.com/report.pdf", StandardCharsets.UTF_8);

		HttpRequest request = HttpRequest.newBuilder(URI.create(url))
			.header("Authorization", "Bearer " + System.getenv("TXTFETCH_KEY"))
			.POST(HttpRequest.BodyPublishers.noBody())
			.build();

		HttpResponse<String> response = HttpClient.newHttpClient()
			.send(request, HttpResponse.BodyHandlers.ofString());

		System.out.println(response.body());
	}
}

errors

Every non-success response carries a stable error.code — match on that, not on error.message. See the full error reference for every code and HTTP status txtfetch can return.

Java
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;

class Main {
	static String field(String json, String name) {
		String key = "\"" + name + "\":\"";
		int start = json.indexOf(key);
		if (start < 0) return null;
		start += key.length();
		return json.substring(start, json.indexOf('"', start));
	}

	public static void main(String[] args) throws Exception {
		byte[] fileBytes = Files.readAllBytes(Path.of("report.pdf"));
		String boundary = "txtfetch-" + System.nanoTime();

		ByteArrayOutputStream body = new ByteArrayOutputStream();
		body.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"report.pdf\"\r\nContent-Type: application/octet-stream\r\n\r\n").getBytes());
		body.write(fileBytes);
		body.write(("\r\n--" + boundary + "--\r\n").getBytes());

		HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.txtfetch.com/v1/extract"))
			.header("Authorization", "Bearer " + System.getenv("TXTFETCH_KEY"))
			.header("Content-Type", "multipart/form-data; boundary=" + boundary)
			.POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
			.build();

		HttpResponse<String> response = HttpClient.newHttpClient()
			.send(request, HttpResponse.BodyHandlers.ofString());

		if (response.statusCode() == 200) {
			System.out.println(response.body());
		} else if (response.statusCode() == 429) {
			String retryAfter = response.headers().firstValue("Retry-After").orElse("?");
			System.out.println("back off: " + field(response.body(), "code") + ", retry after " + retryAfter + "s");
		} else {
			System.out.println("extraction failed: " + field(response.body(), "code"));
		}
	}
}

big-files-and-batches

Large uploads or slow documents are routed to an async job automatically — 202 plus a job_id to poll — and ?async=true forces that path for any request. Direct upload size ceilings by plan: Hobby 10 MB, Developer 50 MB, Scale 200 MB. Use ?url= for anything larger — server-side fetches aren't held to the upload ceiling. See async jobs & webhooks for the full lifecycle, including webhook delivery instead of polling.

Java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;

class Main {
	static String field(String json, String name) {
		String key = "\"" + name + "\":\"";
		int start = json.indexOf(key);
		if (start < 0) return null;
		start += key.length();
		return json.substring(start, json.indexOf('"', start));
	}

	public static void main(String[] args) throws Exception {
		HttpClient client = HttpClient.newHttpClient();
		String auth = "Bearer " + System.getenv("TXTFETCH_KEY");

		String submitUrl = "https://api.txtfetch.com/v1/extract?url="
			+ URLEncoder.encode("https://example.com/report.pdf", StandardCharsets.UTF_8) + "&async=true";
		HttpRequest submitReq = HttpRequest.newBuilder(URI.create(submitUrl))
			.header("Authorization", auth)
			.POST(HttpRequest.BodyPublishers.noBody())
			.build();
		HttpResponse<String> submitResp = client.send(submitReq, HttpResponse.BodyHandlers.ofString());
		String jobId = field(submitResp.body(), "job_id");

		String status;
		String body;
		do {
			Thread.sleep(2000);
			HttpRequest pollReq = HttpRequest.newBuilder(URI.create("https://api.txtfetch.com/v1/extract/" + jobId))
				.header("Authorization", auth)
				.GET()
				.build();
			body = client.send(pollReq, HttpResponse.BodyHandlers.ofString()).body();
			status = field(body, "status");
		} while ("processing".equals(status));

		// Once the job leaves "processing", print the finished result —
		// same success/error shape a synchronous 200 would have returned.
		System.out.println(body);
	}
}

gotchas

  • Apache Tika — the engine behind txtfetch — is PDFBox, POI, and Tesseract (via tess4j) wired together with format detection and routing already solved; assembling that stack yourself is closer to a rewrite of Tika than a quick integration.
  • POI's HSSF/XSSF and HWPF/XWPF split means "read this Excel file" and "read this Word file" are each two APIs, not one — code that only handles XSSF silently mishandles a legacy .xls someone still has lying around.
  • tess4j's native Tesseract binaries are platform- and architecture-specific; a JAR built and tested on one OS can fail to load its native library entirely on another unless you bundle every target you plan to deploy to.
  • PDFBox has no built-in OCR fallback — detecting that a PDF page is a blank scan and routing it to tess4j separately is logic you have to write and maintain yourself.

formats

faq

How do I extract text from a PDF in Java without Apache PDFBox?
POST the PDF (or pass ?url=) to https://api.txtfetch.com/v1/extract using java.net.http — no PDFBox dependency, and no separate OCR step to add for scanned pages, since txtfetch escalates to OCR automatically when a document's text pass comes back blank.
Why does Apache POI need different classes for .xls and .xlsx?
POI's HSSF/XSSF split reflects the two underlying file formats — OLE2 binary for .xls, OOXML zip-of-XML for .xlsx — which really are structurally different. txtfetch detects the actual format from the file's bytes and returns the same response shape either way, so your calling code doesn't need to know which one it received.
Is Apache Tika the same thing as txtfetch?
Tika is the open-source parsing engine txtfetch runs — PDFBox, POI, and Tesseract wired together with format detection and routing. txtfetch is that engine deployed as a hosted API, with metering, async job handling, and webhooks on top, so you don't run and patch the JVM stack yourself.
Do I need to bundle native Tesseract binaries in my Java project?
No — OCR runs server-side. If you were planning to use tess4j, txtfetch removes the need for its per-OS native Tesseract binaries and JNA version-matching entirely.

go-further

Stop parsing. Start shipping.

Create an account and get an API key in minutes — the free Hobby plan needs no card.

Get started →