Dokument-OCR-Tool mit GCP OCR und Node.js entwickeln
Extrahieren Sie Text aus einer lokalen PNG-, JPEG- oder PDF-Datei mit einem Node.js-Kommandozeilenprogramm. Bilder gehen direkt an Google Cloud Vision; PDFs werden in einen privaten Cloud-Storage-Bucket hochgeladen, asynchron verarbeitet und als JSON mit einem Texteintrag pro Seite zurückgegeben. Der PDF-Befehl prüft die Vollständigkeit der Seiten und die Identität des Jobs, bevor er sein JSON-Ergebnis ausgibt.
Voraussetzungen
- Node.js 26.8.2 oder eine neuere Version von Node.js 26 mit nativer TypeScript-Unterstützung. Installieren Sie die aktuelle Sicherheitsversion von Node.js.
- Corepack, separat installiert, das Yarn 4.12.0 über den im Projekt festgelegten Paketmanager bereitstellt.
- Die Google Cloud CLI.
- Eine PNG- oder JPEG-Datei, die Sie an Google Cloud senden dürfen. Legen Sie sie unter
sample.pngnebenocr-projectab oder passen Sie den Pfad im Bildbefehl an. - Ein Google-Cloud-Projekt, in dem Abrechnung, Vision API und Cloud Storage aktiviert sind. Ihr Konto benötigt die Berechtigung, das Kontingent des Projekts zu nutzen, den Beispiel-Bucket zu erstellen und zu löschen sowie dessen Objekte zu erstellen, aufzulisten, zu lesen und zu löschen. Bitten Sie bei Bedarf Ihre Projektadministration um Zugriff.
Die Befehle verwenden Bash unter macOS. Das Beispiel wurde mit Node.js 26.8.2, Yarn 4.12.0,
@google-cloud/vision 6.1.1 und @google-cloud/storage 8.2.0 getestet.
Dies ist eine sequenzielle lokale Anleitung: Verwenden Sie zunächst synthetische Dokumente und
teilen Sie ein Job-Präfix nicht mit einem anderen Prozess.
Google Cloud Vision API einrichten
Wählen Sie Ihr Projekt in der Google Cloud Console aus, bestätigen
Sie, dass die Abrechnung aktiviert ist, und aktivieren Sie die Cloud Vision API. Googles
Anleitung zur Einrichtung von Vision beschreibt die Projekteinrichtung.
Ersetzen Sie PROJECT_ID in den folgenden Befehlen durchgängig.
Authentifizierung einrichten
Verwenden Sie für die lokale Entwicklung Application Default Credentials (ADC) mit einem berechtigten Benutzerkonto. Wenn sowohl die CLI als auch ADC bereits Ihr vorgesehenes Konto und Kontingentprojekt verwenden, überspringen Sie diese Anmeldebefehle:
gcloud auth login &&
gcloud auth application-default login &&
gcloud auth application-default set-quota-project PROJECT_ID
ADC ist von der normalen CLI-Anmeldung getrennt. Das Kontingentprojekt erfordert
serviceusage.services.use; siehe Googles
Anleitung zur Vision-Authentifizierung.
Eine vorhandene Einstellung für GOOGLE_APPLICATION_CREDENTIALS hat Vorrang vor der lokalen ADC-Datei.
Prüfen Sie daher, ob sie Ihre vorgesehenen Zugangsdaten auswählt. Laden Sie für diese Anleitung
keinen Dienstkontoschlüssel herunter. Verwenden Sie für eine spätere Bereitstellung eine
zugewiesene Identität oder Workload Identity Federation mit ADC.
Google-Cloud-Vision-Clientbibliothek installieren
Fügen Sie diesen Block im Verzeichnis ein, in dem Sie das Beispiel anlegen möchten. Er prüft die
Werkzeuge vor dem Schreiben, lehnt ein vorhandenes ocr-project ab und lässt
Ihre Shell im ursprünglichen Verzeichnis:
(
set -eu
node --version
corepack --version
gcloud --version
test ! -e ocr-project && test ! -L ocr-project
mkdir ocr-project
cd ocr-project
cat > package.json <<'JSON'
{
"name": "node-document-ocr",
"private": true,
"type": "module",
"packageManager": "yarn@4.12.0",
"dependencies": {
"@google-cloud/storage": "8.2.0",
"@google-cloud/vision": "6.1.1",
"pdf-lib": "1.17.1",
"zod": "4.3.6"
}
}
JSON
touch yarn.lock
printf 'nodeLinker: node-modules\n' > .yarnrc.yml
YARN_IGNORE_PATH=1 corepack yarn install
)
Die untergeordnete Lockdatei und die festgelegte Paketmanagerversion halten die Installation von
einem übergeordneten Yarn-Projekt getrennt; die lokale Konfiguration wählt
node_modules aus. Wenn die Installation nach der Projekterstellung fehlschlägt,
behalten Sie das Verzeichnis bei und wiederholen Sie nur die Installation:
(cd ocr-project && YARN_IGNORE_PATH=1 corepack yarn install)
Wenn die Projekterstellung fehlgeschlagen ist, wählen Sie ein anderes übergeordnetes Verzeichnis,
bevor Sie die Einrichtung wiederholen. Speichern Sie die beiden folgenden TypeScript-Dateien in
ocr-project; spätere Befehle werden weiterhin im übergeordneten Verzeichnis ausgeführt.
Node.js-Code schreiben
Speichern Sie dieses vollständige Programm als ocr-project/ocr.ts. Es akzeptiert einen
Bildpfad oder einen PDF-Pfad zusammen mit einem Bucket-Namen und einer Job-ID. Der PDF-Upload ist
Teil des Befehls. Ein bereits vorhandenes Job-Präfix wird abgelehnt. Auch der Upload der Eingabe
verwendet eine Generationsvorbedingung, um ein Überschreiben zu verhindern.
import type { Bucket } from '@google-cloud/storage'
import { readFile } from 'node:fs/promises'
import { setTimeout as delay } from 'node:timers/promises'
import { Storage } from '@google-cloud/storage'
import { ImageAnnotatorClient, protos } from '@google-cloud/vision'
import { PDFDocument } from 'pdf-lib'
import { z } from 'zod'
const rpcOptions = { timeout: 30_000, retry: null }
const statusSchema = z.object({ code: z.number().optional() })
const pageSchema = z.object({
error: statusSchema.optional(),
context: z.object({ uri: z.string(), pageNumber: z.number().int() }),
fullTextAnnotation: z.object({ text: z.string().default('') }).optional(),
})
const fileSchema = z.object({
error: statusSchema.optional(),
inputConfig: z.object({ gcsSource: z.object({ uri: z.string() }) }),
responses: z.array(pageSchema),
})
const operationResultSchema = z.object({
responses: z.array(z.object({
outputConfig: z.object({ gcsDestination: z.object({ uri: z.string() }) }),
})).length(1),
})
class OcrError extends Error {}
function jobPrefix(job: string): string {
if (!/^[a-z0-9][a-z0-9-]{0,63}$/u.test(job)) {
throw new OcrError('Use a job ID containing lowercase letters, numbers, and hyphens')
}
return `node-ocr/${job}/`
}
async function removeJob(bucket: Bucket, prefix: string): Promise<void> {
const [files] = await bucket.getFiles({ prefix })
for (const file of files) await file.delete()
}
async function waitForOperation(
client: ImageAnnotatorClient,
name: string,
): Promise<protos.google.longrunning.IOperation> {
const deadline = Date.now() + 180_000
while (Date.now() < deadline) {
const [status] = await client.operationsClient.getOperation({ name }, rpcOptions)
if (status.done) return status
await delay(1000)
}
throw new OcrError('Wait expired; the cloud operation may still be running')
}
async function extractTextFromImage(
client: ImageAnnotatorClient,
bytes: Buffer,
): Promise<{ number: number; text: string }[]> {
const png = bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
const jpeg = bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255
if (!png && !jpeg) throw new OcrError('Image input must be a PNG or JPEG')
const [result] = await client.documentTextDetection({ image: { content: bytes } }, rpcOptions)
if (result.error?.code) {
throw Object.assign(new OcrError('Vision rejected the image', { cause: result.error }), {
code: result.error.code,
})
}
return [{ number: 1, text: (result.fullTextAnnotation?.text ?? '').trimEnd() }]
}
async function extractTextFromPDF(
client: ImageAnnotatorClient,
bucket: Bucket,
bytes: Buffer,
job: string,
project: string,
): Promise<{ number: number; text: string }[]> {
const pdf = await PDFDocument.load(bytes)
const pageCount = pdf.getPageCount()
if (pageCount < 1 || pageCount > 20) throw new OcrError('This example accepts 1–20 PDF pages')
const prefix = jobPrefix(job)
const outputPrefix = `${prefix}output/`
const source = `gs://${bucket.name}/${prefix}input.pdf`
const destination = `gs://${bucket.name}/${outputPrefix}`
const [existing] = await bucket.getFiles({ prefix })
if (existing.length > 0) throw new OcrError('Job prefix already exists; choose a new job ID')
let uploaded = false
let safeToClean = true
try {
await bucket.file(`${prefix}input.pdf`).save(bytes, {
resumable: false,
contentType: 'application/pdf',
preconditionOpts: { ifGenerationMatch: 0 },
})
uploaded = true
// A lost submission response can leave an operation running without a known name.
safeToClean = false
const [operation] = await client.asyncBatchAnnotateFiles({
parent: `projects/${project}/locations/eu`,
requests: [{
inputConfig: { mimeType: 'application/pdf', gcsSource: { uri: source } },
features: [{ type: 'DOCUMENT_TEXT_DETECTION' }],
outputConfig: { batchSize: 1, gcsDestination: { uri: destination } },
}],
}, rpcOptions)
if (!operation.name) throw new OcrError('Vision did not return an operation name')
console.error(`Operation: ${operation.name}`)
await bucket.file(`${prefix}operation.json`).save(`${JSON.stringify({ name: operation.name })}\n`, {
resumable: false,
contentType: 'application/json',
preconditionOpts: { ifGenerationMatch: 0 },
})
const status = await waitForOperation(client, operation.name)
safeToClean = true
if (status.error?.code) {
throw Object.assign(new OcrError('Vision operation failed', { cause: status.error }), {
code: status.error.code,
})
}
if (!(status.response?.value instanceof Uint8Array)) throw new OcrError('Operation has no result')
const decoded = protos.google.cloud.vision.v1.AsyncBatchAnnotateFilesResponse.decode(
status.response.value,
)
const result = operationResultSchema.parse(decoded)
if (result.responses[0].outputConfig.gcsDestination.uri !== destination) {
throw new OcrError('Operation returned a different output prefix')
}
const [files] = await bucket.getFiles({ prefix: outputPrefix })
const pages = new Map<number, string>()
for (const file of files) {
if (!file.name.startsWith(outputPrefix) || !file.name.endsWith('.json')) {
throw new OcrError('Unexpected output object')
}
const [contents] = await file.download()
const data = fileSchema.parse(JSON.parse(contents.toString('utf8')))
if (data.error?.code) throw new OcrError('Vision file failed')
if (data.inputConfig.gcsSource.uri !== source) throw new OcrError('Wrong input in result')
for (const page of data.responses) {
if (page.error?.code) throw new OcrError('Vision page failed')
const number = page.context.pageNumber
if (page.context.uri !== source || number < 1 || number > pageCount || pages.has(number)) {
throw new OcrError('Wrong source, duplicate page, or invalid page number')
}
pages.set(number, (page.fullTextAnnotation?.text ?? '').trimEnd())
}
}
if (pages.size !== pageCount) throw new OcrError('Incomplete PDF result')
return Array.from({ length: pageCount }, (_, index) => ({
number: index + 1,
text: pages.get(index + 1) ?? '',
}))
} finally {
if (uploaded && safeToClean) await removeJob(bucket, prefix)
if (uploaded && !safeToClean) {
console.error(`Retained pending job: gs://${bucket.name}/${prefix}`)
}
}
}
async function main(): Promise<void> {
const [mode, input, bucketName, job, ...extra] = process.argv.slice(2)
if (!input || extra.length > 0 ||
(mode !== 'image' && mode !== 'pdf' && mode !== 'cleanup') ||
(mode === 'image' && (bucketName !== undefined || job !== undefined))) {
throw new OcrError('Usage: image FILE | pdf FILE BUCKET JOB | cleanup OPERATION BUCKET JOB')
}
const project = z.string().min(1).parse(process.env.GOOGLE_CLOUD_PROJECT)
const client = new ImageAnnotatorClient({ projectId: project, apiEndpoint: 'eu-vision.googleapis.com' })
const storage = new Storage({ projectId: project, retryOptions: { autoRetry: false }, timeout: 30_000 })
let pages: { number: number; text: string }[] | undefined
try {
if (mode === 'image') {
pages = await extractTextFromImage(client, await readFile(input))
} else {
if (!bucketName || !job) throw new OcrError('PDF and cleanup modes require BUCKET and JOB')
if (mode === 'cleanup') {
if (!input.startsWith(`projects/${project}/locations/eu/operations/`)) {
throw new OcrError('Use the operation name printed by this project’s PDF command')
}
const prefix = jobPrefix(job)
const bucket = storage.bucket(bucketName)
const [receipt] = await bucket.file(`${prefix}operation.json`).download()
const record = z.object({ name: z.string() }).parse(JSON.parse(receipt.toString('utf8')))
if (record.name !== input) throw new OcrError('Operation does not belong to this job prefix')
await waitForOperation(client, input)
await removeJob(bucket, prefix)
console.error('Job objects removed')
} else {
pages = await extractTextFromPDF(client, storage.bucket(bucketName), await readFile(input), job, project)
}
}
} finally {
await client.close()
}
if (pages !== undefined) console.log(JSON.stringify({ pages }, null, 2))
}
main().catch((error: unknown) => {
const code = statusSchema.safeParse(error)
const hint = code.success && code.data.code === 7 ? 'Permission denied; check project and bucket access'
: code.success && code.data.code === 8 ? 'Quota exceeded; check Vision quota'
: error instanceof OcrError ? error.message : 'Check input, ADC, bucket access, and result format'
console.error(`OCR failed: ${hint}`)
process.exitCode = 1
})
Führen Sie den Bildmodus mit einem lokalen Bild aus, das Sie an Google Cloud senden dürfen:
GOOGLE_CLOUD_PROJECT=PROJECT_ID node ocr-project/ocr.ts image ./sample.png
Er gibt eine Seite mit dem erkannten Text ohne nachgestellte Leerzeichen aus. Ein leeres Bild gibt
einen leeren String für text zurück. Bei einer fehlenden Datei, einem
abgelehnten Bild oder einer fehlgeschlagenen Anfrage endet der Befehl mit Status eins und ohne
JSON-Ergebnis. Die Prüfung des Bild-Headers wählt das Format aus; Vision muss das Bild weiterhin
decodieren. Vergleichen Sie den OCR-Text mit dem Original, bevor Sie ihn als verbindliche
Transkription verwenden.
PDF-Dateien verarbeiten
Speichern Sie diesen Fixture-Generator als ocr-project/make-pdf.ts. Er erstellt zwei Seiten
mit unterschiedlichem Text und weigert sich, eine vorhandene Datei
sample.pdf zu überschreiben:
import { writeFile } from 'node:fs/promises'
import { PDFDocument, StandardFonts } from 'pdf-lib'
const pdf = await PDFDocument.create()
const font = await pdf.embedFont(StandardFonts.Helvetica)
for (const text of ['DEVTIPS FIRST PAGE 2026', 'DEVTIPS SECOND PAGE 2026']) {
const page = pdf.addPage([1000, 400])
page.drawText(text, { x: 50, y: 250, size: 32, font })
}
await writeFile(new URL('./sample.pdf', import.meta.url), await pdf.save(), { flag: 'wx' })
Generieren Sie die Datei und erstellen Sie dann einen neuen privaten Bucket. Ersetzen Sie
YOUR_NEW_BUCKET durch einen global eindeutigen Bucket-Namen, der Ihnen gehört.
Das von gcloud verwendete Konto muss ebenfalls für dieses Projekt
berechtigt sein:
node ocr-project/make-pdf.ts &&
gcloud storage buckets create gs://YOUR_NEW_BUCKET --project=PROJECT_ID --location=EU \
--uniform-bucket-level-access --public-access-prevention &&
GOOGLE_CLOUD_PROJECT=PROJECT_ID node ocr-project/ocr.ts pdf \
ocr-project/sample.pdf YOUR_NEW_BUCKET trial-1
Die CLI lädt node-ocr/trial-1/input.pdf hoch und fordert JSON-Ausgaben unter
node-ocr/trial-1/output/ an. Sie wartet auf den Vorgang, prüft das zurückgegebene Ziel und die
Quell-URI jeder Seite, lehnt fehlende oder doppelte Seiten ab und sortiert den Text nach
Seitennummer. Eine alphabetische Auflistung der Objekte reicht für die Seitenreihenfolge nicht aus.
Googles Anleitung zur PDF/TIFF-OCR erläutert die asynchrone Übergabe
und das JSON-Antwortformat.
Für dieses Fixture sollte das JSON-Ergebnis so aussehen:
{
"pages": [
{ "number": 1, "text": "DEVTIPS FIRST PAGE 2026" },
{ "number": 2, "text": "DEVTIPS SECOND PAGE 2026" }
]
}
Bei Erfolg entfernt der Befehl seine hochgeladene Eingabe und die OCR-Objekte, bevor er das Ergebnis ausgibt. Endgültige Fehler des Vorgangs und ungültige Ergebnisse lösen diese Bereinigung ebenfalls aus. Schlägt die Bereinigung fehl, wird Status eins zurückgegeben, statt ein erfolgreiches Ergebnis auszugeben. Vorhandene Inhalte unter dem Präfix bleiben erhalten, wenn der Befehl einen Job ablehnt. Wählen Sie eine neue Job-ID, statt fremde Objekte zu löschen.
Ausstehenden Job wiederaufnehmen und Bucket entfernen
Die CLI fragt den Status bis zu drei Minuten lang ab; eine laufende Statusanfrage kann bis zu 30 Sekunden hinzufügen. Ein Timeout oder Verbindungsverlust bricht Vision nicht ab. Der Befehl gibt den Vorgangsnamen aus und behält die Job-Objekte bei, wenn der Abschlussstatus unbekannt ist. Senden Sie zur Wiederaufnahme keine weitere OCR-Anfrage. Kopieren Sie den ausgegebenen Vorgangsnamen in diesen Befehl und verwenden Sie denselben Bucket und dieselbe Job-ID:
GOOGLE_CLOUD_PROJECT=PROJECT_ID node ocr-project/ocr.ts cleanup \
'projects/PROJECT_ID/locations/eu/operations/OPERATION_ID' YOUR_NEW_BUCKET trial-1
Die Bereinigung prüft den gespeicherten Vorgangsdatensatz, wartet auf einen endgültigen Status und entfernt dann die Objekte dieses Jobs, auch nach einem fehlgeschlagenen Vorgang. Tritt weiterhin ein Timeout auf, behalten Sie die Objekte bei und wiederholen Sie die Bereinigung später. Wenn die Antwort auf die Übermittlung verloren ging, bevor ein Vorgangsname eintraf, oder wenn das Speichern des Vorgangsdatensatzes fehlschlug, behalten Sie das Präfix bei und lassen Sie Ihre Projektadministration den Vorgang prüfen, bevor Sie dessen Objekte löschen. Verwenden Sie die Bereinigung nur für ein Präfix, das Sie selbst erstellt haben.
Sobald Ihr eigener Beispiel-Bucket leer ist, entfernen Sie ihn:
gcloud storage buckets delete gs://YOUR_NEW_BUCKET --project=PROJECT_ID --quiet
Wenn die Fixture-Generierung oder die Bucket-Erstellung fehlgeschlagen ist, behalten Sie Ihre Dateien bei und wiederholen Sie nur den fehlgeschlagenen Schritt. Der Generator lehnt eine vorhandene PDF-Datei ab. Verwenden Sie diese Datei für OCR weiter oder entfernen Sie gezielt nur dieses generierte Fixture, bevor Sie es erneut generieren. Führen Sie die Bucket-Löschung nur aus, wenn der Erstellungsschritt erfolgreich war. Bewahren Sie das JSON bei Bedarf separat auf; dieses Beispiel führt kein Archiv.
API-Einschränkungen und Preise
Vision akzeptiert PDF/TIFF-Dateien mit bis zu 2.000 Seiten; diese CLI akzeptiert bewusst nur PDFs mit einer bis 20 Seiten. Sie implementiert keine TIFF-Verarbeitung. Jede PDF-Seite wird für die angeforderte Funktion als Bild abgerechnet, daher nutzt das zweiseitige Fixture zwei OCR-Einheiten. Aktuelle Tarife finden Sie unter Cloud Vision-Preise; Storage-Gebühren fallen separat an. Bei der Übermittlung von Anfragen gibt es keine automatischen Wiederholungen, damit ein kostenpflichtiger Job nach einer unklaren Antwort nicht unbemerkt erneut übermittelt wird.
Regionale Konfiguration
Diese Anleitung verwendet den EU-Vision-Endpunkt, einen EU-Anfrage-Parent und einen EU-Storage-Bucket. Der Endpunkt steuert die Vision-Verarbeitung; er verlagert keinen bestehenden Bucket. Lesen Sie Googles Dokumentation zur multiregionalen OCR, bevor Sie den Standort anpassen. Die Anleitung wurde mit der EU-Konfiguration überprüft.
Fehlerbehebung
- Authentifizierungs- oder Kontingentfehler: Bestätigen Sie das vorgesehene ADC-Konto, das
Kontingentprojekt und die API-Aktivierung. Eine funktionierende Anmeldung mit
gcloudallein belegt keine SDK-Authentifizierung. - Fehler beim Bucket-Zugriff: Prüfen Sie die Berechtigungen zum Lesen der Eingabe sowie zum Erstellen, Auflisten, Lesen und Löschen der Ausgabe. Ein verweigerter Download darf nicht als leeres OCR-Ergebnis behandelt werden.
- Unvollständige oder nicht übereinstimmende PDF-Ausgabe: Lassen Sie den Fehler sichtbar. Schwächen Sie die Prüfungen der Seitenzahl oder der Quelle nicht ab, um Teiltexte auszugeben.
- Unterbrochener Job oder Job mit Timeout: Nehmen Sie den Vorgang wie oben beschrieben wieder auf, bevor Sie Ressourcen entfernen oder eine erneute Übermittlung in Betracht ziehen.
Fazit
Für einen Workflow zur Upload-Verarbeitung, der über Transloadit verwaltet wird, sehen Sie sich den Document OCR Robot an. Seine Einrichtung und sein Ergebnisvertrag sind von dieser direkten Google-Cloud-CLI getrennt.
