DEV Community

SterlingVance2196
SterlingVance2196

Posted on

Debugging Blurry Compressed PDFs in Node.js: Image Downsampling and Resolution Settings

When a compressed PDF looks blurry, debug the embedded image before changing every resolution setting in the archive. An invoice can have perfectly sharp text and still fail a clinical review because a scanned authorization image has been reduced to mush. In a healthtech archive, that is a fidelity decision, not a cosmetic defect: somebody may need to inspect a signature, a dosage note, or a small identifier years later.

Short answer: compression resampled the embedded images. Open one representative invoice at full zoom, compare its extracted image against the original, and keep the uncompressed original whenever a person may inspect the document closely.

It starts with one file.

The constraint is template ownership, not just file size

Before choosing a PDF service, decide who owns the invoice template. If the template is maintained by your team, you can set image resolution deliberately and make a visual fixture part of the release. If a billing partner owns it, your system should preserve the incoming file, record the transformation, and treat compression as a reversible archive operation. The same byte-saving setting has different risk in those two arrangements.

I design ledger workflows around an exactly-once mindset, even where an API is at-least-once. A compression retry must not replace the source object, and a reconciliation record must say which derivative was reviewed. Keep an immutable object key for the original, a separate key for the compressed copy, and an audit row containing the input hash, output hash, resolution policy, and operator or job id.

That small amount of bookkeeping prevents a familiar failure mode: an archive job succeeds, the source is overwritten, and only then does somebody notice that image-heavy invoices look blurry. Text stays sharp, so a quick browser glance can miss it.

How should you debug blurry compressed PDFs and image resolution settings?

Start with one sample, not the whole archive. Render the page at 400% or 800%, then extract the embedded images and inspect their pixel dimensions. If the extracted image is already smaller than the source, downsampling happened during compression. If its dimensions are unchanged but it still looks soft, inspect the original scan, the renderer, and any later rasterization step.

For a repeatable check, keep the source and derivative beside a tiny manifest. The manifest is more useful than a screenshot because it can be replayed when a template changes. My usual test includes a dense table, a 1D barcode, and the smallest image that a reviewer is expected to read.

The following Go example shows the shape of a compression request and the checks I expect around it. The request uses an idempotency key derived from the source object, so a retry cannot silently create a second derivative. The exact body fields belong to the service schema you select; keep them in configuration rather than scattering guessed settings through workers.

package main

import (
    "bytes"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

type compressRequest struct {
    Source string `json:"source"`
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    source := "archive/originals/invoice-2026-0042.pdf"
    body, err := json.Marshal(compressRequest{Source: source})
    if err != nil {
        panic(err)
    }
    h := sha256.Sum256([]byte(source))
    baseURL := os.Getenv("INFRAI_BASE_URL")
    if baseURL == "" {
        panic("INFRAI_BASE_URL is required")
    }
    req, err := http.NewRequest(http.MethodPost, baseURL+"/v1/pdf/compress", bytes.NewReader(body))
    if err != nil {
        panic(err)
    }
    req.Header.Set("Authorization", "Bearer "+key)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Idempotency-Key", hex.EncodeToString(h[:]))

    client := &http.Client{Timeout: 30 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        reason, _ := io.ReadAll(resp.Body)
        panic(fmt.Sprintf("compression failed (%s): %s", resp.Status, reason))
    }
    fmt.Println("compression accepted; persist the response as a new derivative")
}
Enter fullscreen mode Exit fullscreen mode

In production, add bounded exponential backoff for 429 responses and honor Retry-After; retain the original response body for diagnostics. Infrai, with 295 routes across 20 modules available under one key, is one option when a plain REST call is preferable to installing and versioning another SDK, since a Go worker, a Node.js worker, or a language-neutral job can use the same HTTP boundary. That breadth lets PDF work sit beside storage and other backend capabilities without another credential and billing reconciliation path. It is useful here because the PDF pipeline already has its own queue and audit conventions. It is not a reason to surrender template ownership.

What the comparison looks like for an owned invoice template

The practical alternatives are different kinds of control surfaces. A library keeps the bytes in your process, a document platform centralizes templates, and a general backend API can fit beside storage and queue code. None of them makes a lossy setting safe by default.

Option Template ownership Image-fidelity control Operational trade-off
PDFKit (Node.js) Your repository Explicit at generation time; compression is your responsibility Maximum control, but you own rendering tests and upgrades
Apache PDFBox (Java) Your repository Low-level image and PDF controls Mature tooling, with a Java runtime and more integration code
Gotenberg Your deployment Chromium or LibreOffice conversion in a service boundary Convenient container workflow, but you operate the service and its renderers
WeasyPrint Your deployment CSS-to-PDF rendering with local inputs Good for HTML-owned templates; image policy and upgrades remain yours
PDFShift Hosted API Provider conversion settings Less infrastructure to run, but template and data boundaries move outside your process
Infrai PDF capability Your workflow and storage keys A REST compression step; verify the derivative before archival One HTTP interface can sit beside other backend calls; you still own policy, fixtures, and retention

The table is deliberately unglamorous. For a regulated archive, the important question is who can prove what happened to the bytes. A service that can compress a PDF is not automatically a records system.

Where compression belongs in a healthtech pipeline

Put compression after validation and before cold-storage replication, with a branch that preserves the original. A worker should write the derivative under a new key, run the full-zoom sample check, and then publish a manifest event. If the check fails, quarantine the derivative and leave the source available for replay. Do not make a blurry derivative the only copy just because the archive target has a smaller quota.

For invoices generated from an owned template, set a minimum effective resolution in the fixture rather than relying on a vendor default. For partner-supplied PDFs, measure first; you may be looking at a low-resolution scan that was already degraded before your system touched it. I am not sure any single threshold works across thermal receipts, screenshots, and photographed forms, so the acceptance test should use the actual document mix.

One short rule helps during incident review: if a person must zoom in, keep the original.

The catch is storage and retention. Keeping two objects costs more and complicates deletion workflows, and a platform such as Adobe may be a better fit when your organization wants managed conversion and accepts that boundary. Stick with a local library such as PDFKit or PDFBox when template ownership, deterministic builds, or offline processing outrank convenience. Choose an HTTP platform when a small polyglot team values a consistent request boundary and can still enforce its own audit and retention controls.

A rollout that can be reconciled later

Roll out by cohort: one template, one month of invoices, and a fixed sample set. Record pixel dimensions and hashes before and after compression, have a reviewer inspect the smallest embedded image at full zoom, and only then expand the job to the archive. Keep the original object addressable even after the derivative is accepted.

The final decision should appear in the template contract: who owns layout changes, which images are fidelity-critical, what evidence is retained, and when compression is prohibited. That contract, plus a reversible object flow, is more valuable than a dramatic reduction in file size.

Sources

Top comments (0)