GCP OCR & Java: invoice automation
Manual invoice processing can be tedious, error-prone, and time-consuming. Optical Character Recognition (OCR) technology offers a powerful solution by automating text extraction from invoices, significantly improving accuracy and efficiency in financial workflows.
Prerequisites
Before getting started with GCP OCR and Java, ensure you have:
- Java 11 or later installed
- Maven 3.8+ for dependency management
- A Google Cloud account with billing enabled
- Cloud Vision API enabled in your Google Cloud project
- Google Cloud CLI installed
Why OCR matters in invoice processing
OCR technology converts images of text into machine-readable data. Integrating OCR into invoice processing workflows can:
- Reduce manual data entry errors
- Accelerate invoice processing times
- Enable scalable and automated financial operations
Setting up Google Cloud Vision API with Java
Authentication setup
Before using the Cloud Vision API, you need to set up authentication:
-
Install the Google Cloud CLI if you haven't already.
-
Initialize the CLI by running:
gcloud init -
Set up application default credentials:
gcloud auth application-default login -
Ensure the Cloud Vision API is enabled in your Google Cloud project:
gcloud services enable vision.googleapis.com
Adding dependencies
Add the Cloud Vision dependency to your Java project using Maven. The recommended approach is to use the Google Cloud BOM (Bill of Materials):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.56.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-vision</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
The BOM manages both versions. Set your Maven compiler release to 11 or later. Save each Java class below in its own file named after the class; the final example uses Gson to serialize JSON.
Extracting text from invoices
This example extracts text from a JPEG or PNG invoice image. PDF/TIFF documents require Vision's
file annotation API with an InputConfig, rather than
passing document bytes to Image. Each processed PDF page is a separate billable image.
import com.google.cloud.vision.v1.AnnotateImageRequest;
import com.google.cloud.vision.v1.AnnotateImageResponse;
import com.google.cloud.vision.v1.BatchAnnotateImagesResponse;
import com.google.cloud.vision.v1.Feature;
import com.google.cloud.vision.v1.Image;
import com.google.cloud.vision.v1.ImageAnnotatorClient;
import com.google.cloud.vision.v1.TextAnnotation;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class InvoiceOCR {
public static void main(String[] args) {
// The path to your invoice image
String filePath = "invoice.jpg";
try {
// Load the image
ByteString imgBytes = ByteString.copyFrom(Files.readAllBytes(Paths.get(filePath)));
Image img = Image.newBuilder().setContent(imgBytes).build();
// Create feature for text detection
Feature feature = Feature.newBuilder()
.setType(Feature.Type.DOCUMENT_TEXT_DETECTION)
.build();
// Build the request
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
.addFeatures(feature)
.setImage(img)
.build();
List<AnnotateImageRequest> requests = new ArrayList<>();
requests.add(request);
// Process the request
try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) {
BatchAnnotateImagesResponse response = client.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = response.getResponsesList();
for (AnnotateImageResponse res : responses) {
if (res.hasError()) {
throw new IOException("Vision OCR failed (code " + res.getError().getCode() + ")");
}
if (!res.hasFullTextAnnotation() || res.getFullTextAnnotation().getText().isBlank()) {
System.out.println("No text found in image");
return;
}
TextAnnotation annotation = res.getFullTextAnnotation();
System.out.println("Extracted Text:\n" + annotation.getText());
}
}
} catch (Exception e) {
System.err.println("Invoice OCR failed (" + e.getClass().getSimpleName() + ")");
System.exit(1);
}
}
}
Document text detection vs. Text detection
Google Cloud Vision offers two main OCR modes:
-
DOCUMENT_TEXT_DETECTION: Optimized for dense text in structured documents like invoices. It preserves the layout and structure of the text, making it ideal for invoice processing.
-
TEXT_DETECTION: Better for scene text or images with sparse text. It's less structured but works well for capturing text in natural scenes.
For invoice processing, DOCUMENT_TEXT_DETECTION is typically the better choice as it preserves the
document's structure.
Parsing and structuring invoice data
After extracting raw text, you'll need to parse it into structured data. Regular expressions or NLP libraries can help identify key fields like invoice number, date, total amount, and vendor details.
The following parser is deliberately limited to labeled lines: Invoice No: INV-123,
Date: 03/19/2025, Total: $1,234.56, and Vendor: Example Ltd. It accepts US month/day/year
dates and dot-decimal amounts, and rejects missing fields or invalid dates. OCR text with other
layouts or locales needs a different parser and human review before any financial submission.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class InvoiceParser {
public static Map<String, String> parseInvoiceData(String extractedText) {
Map<String, String> invoiceData = new HashMap<>();
// Match whole labeled lines so "Subtotal" and adjacent fields cannot be captured.
Pattern invoiceNumberPattern = Pattern.compile("(?im)^Invoice(?:[ \\t]+(?:No\\.?|Number)|[ \\t]*#)?[ \\t]*:[ \\t]*([A-Z0-9][A-Z0-9/-]*)[ \\t]*$");
Pattern datePattern = Pattern.compile("(?im)^(?:Invoice Date|Date)[ \\t]*:[ \\t]*(\\d{1,2}/\\d{1,2}/\\d{4})[ \\t]*$");
Pattern totalPattern = Pattern.compile("(?im)^(?:Total|Amount Due|Balance Due)[ \\t]*:[ \\t]*[$€£]?[ \\t]*((?:\\d{1,3}(?:,\\d{3})+|\\d+)\\.\\d{2})[ \\t]*$");
Pattern vendorPattern = Pattern.compile("(?im)^(?:From|Vendor|Supplier|Company)[ \\t]*:[ \\t]*([^\\r\\n]+)$");
// Extract invoice number
Matcher invoiceMatcher = invoiceNumberPattern.matcher(extractedText);
if (invoiceMatcher.find()) {
invoiceData.put("invoiceNumber", invoiceMatcher.group(1).trim());
}
// Extract date
Matcher dateMatcher = datePattern.matcher(extractedText);
if (dateMatcher.find()) {
invoiceData.put("date", dateMatcher.group(1).trim());
}
// Extract total amount
Matcher totalMatcher = totalPattern.matcher(extractedText);
if (totalMatcher.find()) {
invoiceData.put("totalAmount", totalMatcher.group(1).replace(",", ""));
}
// Extract vendor
Matcher vendorMatcher = vendorPattern.matcher(extractedText);
if (vendorMatcher.find()) {
invoiceData.put("vendor", vendorMatcher.group(1).trim());
}
validateInvoiceData(invoiceData);
return invoiceData;
}
private static void validateInvoiceData(Map<String, String> data) {
for (String field : new String[] {"invoiceNumber", "date", "totalAmount", "vendor"}) {
if (!data.containsKey(field) || data.get(field).isBlank()) {
throw new IllegalArgumentException("Missing invoice field: " + field);
}
}
DateTimeFormatter inputFormatter = DateTimeFormatter.ofPattern("M/d/uuuu")
.withResolverStyle(ResolverStyle.STRICT);
LocalDate date = LocalDate.parse(data.get("date"), inputFormatter);
data.put("date", DateTimeFormatter.ISO_LOCAL_DATE.format(date));
}
}
Automating data entry into financial systems
After validation and review, structured invoice data can be entered into financial systems via APIs or database integrations. Field presence alone does not establish that OCR read the correct values.
Here's a conceptual example of how to integrate with a financial system:
import com.google.gson.Gson;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
public class FinancialSystemIntegration {
private static final String API_ENDPOINT = "https://financial-system.example.invalid/invoices";
public static void submitInvoiceData(Map<String, String> invoiceData)
throws IOException, InterruptedException {
String apiKey = System.getenv("FINANCIAL_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("Set FINANCIAL_API_KEY before submitting invoices");
}
String jsonData = convertToJson(invoiceData);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_ENDPOINT))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(jsonData))
.build();
HttpResponse<Void> response = client.send(request, HttpResponse.BodyHandlers.discarding());
if (response.statusCode() >= 200 && response.statusCode() < 300) {
System.out.println("Invoice successfully submitted to financial system");
} else {
throw new IOException("Invoice submission failed (HTTP " + response.statusCode() + ")");
}
}
private static String convertToJson(Map<String, String> data) {
return new Gson().toJson(data);
}
}
Replace the reserved example endpoint with your system's documented HTTPS endpoint and inject the credential through your deployment's secret management. The caller must handle failures and interruption. Before retrying a submission, use the destination's idempotency mechanism or reconcile its status to avoid creating duplicate invoices.
