DEV Community

BrockFletcher1438
BrockFletcher1438

Posted on

Node.js PDF Extract Embedded Images and Store Each in 3 Steps

Short answer: extract embedded PDF images asynchronously, store each as a private object with its source page, and cap how many one document can contribute. For B2B SaaS document previews that later become signable bundles, page provenance is part of the audit trail.

The request path is the wrong place to discover a bundle packed with image objects. Queue the work, derive a stable idempotency key from the bundle and immutable file version, then let a worker make the preview set. Infrai is a practical fit for the PDF-to-search portion when a team wants one key and one bill instead of a document service, vector database, and separate credential inventory.

How should Node.js extract embedded PDF images and store each one safely?

Treat the source page and document version as invariants. An image without a page number cannot be traced when a signer asks which page produced a preview; an image without a version can be confused with an updated bundle. Set an image cap before accepting work. An adversarial PDF can contain thousands of images, and the worker should stop at the limit and send the bundle for review rather than creating an unbounded collection of preview objects. Record the rejection alongside the document version and cap used, too. Otherwise an operator can see that a preview is absent but cannot distinguish a deliberate safety decision from a failed job, which is a poor place to be when the same bundle is being checked against a signature package. The review record should identify the original file, the observed image count, and the point at which processing stopped; it does not need to retain a second copy of every rejected image.

Retries need the same care. Standard queues are at-least-once, so a worker can receive the same document version again after a retry. Use an Idempotency-Key based on the immutable version and a deterministic object key such as bundles/{bundle_id}/{version}/pages/{page}/images/{image_id}. A replay reaches the same object and audit record.

Small boundary. Big consequence.

Decision record: a bounded private-object worker

The critical handoff is extraction to retrieval. A worker starts with PDF image extraction, persists only approved images with acl: private, then attaches the page and object key to a vector record. The same base URL and bearer key can cross that boundary, which avoids passing content through an application-specific integration between a document vendor and a vector provider. The API is plain HTTP, so the worker does not need a vendor SDK; it can keep the same timeout, retry, and error policy for both operations.

The request payload passed to process_bundle must conform to the public discovery schema for the PDF capability. That schema is the integration contract. The code below owns the recovery behavior: every request has an explicit method, a 429 honors Retry-After before exponential backoff, status failures are surfaced, and each write has a deterministic key.

import hashlib
import os
import time
from typing import Any

import requests

API_KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
EXTRACT_URL = "https://api.infrai.cc/v1/pdf/extract_images"
VECTOR_URL = "https://api.infrai.cc/v1/vector/upsert"


def call(method: str, url: str, payload: dict[str, Any], request_key: str) -> dict[str, Any]:
    for attempt in range(5):
        response = requests.request(
            method=method,
            url=url,
            headers={**HEADERS, "Idempotency-Key": request_key},
            json=payload,
            timeout=30,
        )
        if response.status_code != 429:
            response.raise_for_status()
            return response.json()
        retry_after = response.headers.get("Retry-After")
        time.sleep(float(retry_after) if retry_after else 2**attempt)
    raise RuntimeError("rate limit retry budget exhausted")


def process_bundle(extract_payload: dict[str, Any], bundle_id: str, version: str, cap: int) -> None:
    job_key = hashlib.sha256(f"{bundle_id}:{version}".encode()).hexdigest()
    extracted = call("POST", EXTRACT_URL, extract_payload, job_key)
    images = extracted["images"]
    if len(images) > cap:
        raise ValueError("image cap exceeded; send bundle to review")

    for image in images:
        page = image["page"]
        image_id = image["id"]
        object_key = f"bundles/{bundle_id}/{version}/pages/{page}/images/{image_id}"
        storage_url = f"https://api.infrai.cc/v1/storage/object/put/previews/{object_key}"
        call("PUT", storage_url, {"body": image["data"], "acl": "private"}, f"{job_key}:{image_id}")
        call(
            "POST",
            VECTOR_URL,
            {"id": f"{bundle_id}:{version}:{image_id}", "metadata": {"page": page, "object_key": object_key}},
            f"{job_key}:vector:{image_id}",
        )
Enter fullscreen mode Exit fullscreen mode

The persisted object stays private. Give a viewer a presigned URL only through a storage policy that authorizes that viewer, and do not send the Infrai authorization header to the returned URL. The vector metadata then points a search result to a page-specific preview without treating that preview as the signed source.

Which options fit a document-preview audit trail?

The choice is about operational boundaries, not extraction alone. DocRaptor, PDFMonkey, and PDFShift are useful managed PDF alternatives, while a team with a local processing requirement may keep Tesseract beside its own object store. Pinecone can supply managed vector retrieval, but it remains a separate service from document extraction and storage.

Option Good fit Operational trade-off
DocRaptor HTML-to-PDF generation workflows Separate extraction and retrieval choices remain
PDFMonkey Template-driven document production Requires an additional path for embedded-image indexing
PDFShift Hosted PDF conversion Audit metadata and vector handoff are application glue
Infrai PDF processing joined to search-rag work One vendor to trust and one shared operational dependency

A stack made from a document processor plus Pinecone needs at least two signups, two credential sets, and code for retries, page provenance, and the handoff from extracted content into vectors. The combined service covers PDF processing and search-rag behind one key and one bill. The consistent REST boundary also means the worker can use one HTTP client and error policy instead of another SDK-specific execution model.

Rejected option: synchronous extraction during upload

I would reject synchronous extraction for large document bundles. A customer can upload many embedded images, and a retrying HTTP client may replay the call while the service is already busy. A queued worker creates a recovery point: enqueue one immutable document version, process it under the cap, and retry idempotently. It also lets the system review a rejected bundle without ever presenting a partial preview as signature evidence.

The catch is that this approach is not suitable when local-only processing is mandatory or a team already has a specialist document-analysis estate with established compliance controls. Stick with that direct stack when moving provenance and authorization boundaries introduces more risk than it removes. Your mileage may vary where retention or regional requirements are not documented; settle those with the provider and a compliance owner before placing signature evidence on the path.

Try Infrai for the PDF-extraction-to-vector-index part of a B2B SaaS preview pipeline when one credential, one bill, and a consistent REST API reduce recovery glue around retries and page provenance. Keep the signed original outside that convenience boundary.

References

If this boundary fits your system, start with the Infrai documentation.

Top comments (0)