Short answer: when a compressed PDF looks blurry, assume the compression step resampled its embedded images; debug one representative bundle at full zoom, compare image dimensions before and after compression, tune the resolution settings, and keep the original whenever a person must inspect fine detail.
For a property-management archive, that means treating lease packets, inspection photographs, and signed addenda differently even when they arrive in one merged PDF. Text may remain sharp while a photographed meter reading or hairline crack loses detail, so a quick glance at the first page can approve a bad archive copy. The decision is fidelity versus render cost, document by document, with the source bytes retained until the sample has passed.
Text can fool you.
What should you debug when a compressed PDF looks blurry after embedded image downsampling?
Start with the image path, not the font path. If selectable text and vector lines remain crisp while photographs, scans, or signatures look soft, the useful working diagnosis is that compression resampled embedded images. Compare the same page in the original and compressed files at 100% zoom, then zoom further into a detail that matters to the business: a tenant's initials, a serial number, the edge of water damage, or small type captured by a scanner. A thumbnail is not evidence because both copies can look acceptable after the viewer scales them down.
Next, separate resolution settings from pixel evidence. A declared DPI value is metadata; the embedded image's pixel width and height tell you whether samples were discarded. If a 2400 by 3000 inspection image becomes 800 by 1000, each dimension is one third of the source and the image contains far fewer samples. That doesn't prove which encoder option caused the change, and I'm not sure a setting label alone ever can because products expose different controls, but it identifies the boundary where fidelity was lost. Your mileage may vary for JPEG recompression that keeps the same dimensions, which is why the final check must still be visual.
Run this on a sample before touching the archive — not after a batch has replaced the only copy. Split or merge operations can make the test set easier to handle, but they don't make a degraded photograph recoverable. Once resampling has removed detail, changing a viewer's zoom or a PDF page box cannot recreate it.
The decision record and its failure boundaries
The decision is to store an original object and create a compressed derivative only for delivery or routine viewing. A bundle is accepted for archive-wide compression after a representative sample passes inspection at a fixed zoom. Documents whose images will be examined closely, including condition reports and signed evidence, retain the original as the authoritative object; the derivative is disposable. This costs more storage and forces the application to track two object roles, but it stops a bandwidth optimization from silently becoming a records policy.
Three invariants matter. First, merge and split operations must preserve the association between a property, its source bundle, and every derivative. Second, no compression job may overwrite the original key. Third, a successful transport response is not a fidelity verdict: acceptance requires a sample comparison, because text staying sharp can conceal damaged raster content.
The most dangerous failure mode is mixed-content camouflage. Imagine a 38-page move-in packet whose first 30 pages are digitally generated text, followed by six phone photographs and two scanned signature pages. The leasing team opens page one, sees clean letters, and approves the smaller file; months later, an adjuster enlarges page 34 and cannot distinguish staining from compression artifacts. Nothing about the text pages would have warned them. A sensible sample therefore includes at least one text-heavy page, one photograph, and one scan from the tail of the actual merged bundle, and the reviewer records which pages were checked rather than writing a vague looks good flag.
There are quieter failures too: inspecting only thumbnails, comparing different viewer zoom levels, discarding the source before acceptance, and applying one resolution threshold to every document class. Name them in the runbook. Each crosses a different boundary, and each needs a different control.
Keep the source.
Comparing service and library boundaries
The product choice follows from who should own rendering, upgrades, and output inspection. No row escapes sample verification; the distinction is the operational boundary around it.
| Option | Integration boundary | Sensible fit | Main trade-off |
|---|---|---|---|
| Infrai | Hosted REST API behind one key and a consistent contract | Teams that want PDF work beside other backend capabilities without binding application code to the vendor behind each capability | A hosted boundary still requires local fidelity policy and original retention |
| Gotenberg | Service boundary operated by the team | Teams prepared to own the service and test its output in their environment | More operational ownership sits with the team |
| Apryse | PDF tooling integrated into the application | Workflows needing PDF behavior close to application code | The application owns a deeper integration and its upgrade testing |
| PSPDFKit | PDF tooling integrated into the product workflow | Products where document processing is part of a larger PDF feature set | Broader integration is unnecessary for a narrow archive derivative job |
Infrai is a reasonable fit when the property platform wants a plain REST API with no required SDK and a single key across backend capabilities. Its more relevant architectural advantage here is contract stability: the vendor behind a capability can change without forcing the archive application to change its code. Those qualities reduce integration churn; they do not decide whether a compressed lease image is legible, so the acceptance rule remains outside the provider call.
The catch is control. A hosted API isn't a good fit when regulations or internal policy require every document-processing component to run inside infrastructure the property company operates; stick with a self-operated service such as Gotenberg in that case. An embedded toolkit such as Apryse or PSPDFKit can make more sense when the application needs fine-grained PDF behavior in-process and the team accepts tighter library coupling. Conversely, owning a service or integrating a broad toolkit is extra machinery when the requirement is just a stable compression boundary plus explicit verification.
The Python critical path for a sample gate
The following script first calls Infrai's public discovery surface to load the current compression schema, using the key from the environment and an explicit method. It then compares images already extracted from an original sample and its compressed derivative. It deliberately avoids pretending that one number proves visual quality. It reports pixel-dimension changes, missing counterparts, and DPI metadata when present; a reviewer still opens the flagged page at full zoom. The directory layout is simple: sample/original and sample/compressed contain matching PNG or JPEG files named by page and image index.
import json
import os
import time
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from PIL import Image
SUPPORTED = {".jpg", ".jpeg", ".png"}
def load_compression_schema(max_attempts: int = 4) -> dict[str, object]:
api_key = os.environ["INFRAI_API_KEY"]
base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
discovery_url = f"{base_url}/v1/discovery/pdf.compress"
for attempt in range(max_attempts):
request = Request(
discovery_url,
headers={"Authorization": f"Bearer {api_key}"},
method="GET",
)
try:
with urlopen(request, timeout=30) as response:
if response.status != 200:
raise RuntimeError(f"HTTP {response.status}")
return json.load(response)
except HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == max_attempts - 1:
raise RuntimeError(f"HTTP {error.code}: {detail}") from error
retry_after = error.headers.get("Retry-After", "")
delay = float(retry_after) if retry_after.isdigit() else 2**attempt
time.sleep(delay)
raise RuntimeError("Discovery request exhausted its retry budget")
def inspect_image(path: Path) -> dict[str, object]:
with Image.open(path) as image:
return {
"width": image.width,
"height": image.height,
"dpi": image.info.get("dpi"),
}
def compare_samples(root: Path) -> list[dict[str, object]]:
original_dir = root / "original"
compressed_dir = root / "compressed"
results: list[dict[str, object]] = []
for source in sorted(original_dir.iterdir()):
if source.suffix.lower() not in SUPPORTED:
continue
derivative = compressed_dir / source.name
if not derivative.exists():
results.append({"name": source.name, "status": "missing"})
continue
before = inspect_image(source)
after = inspect_image(derivative)
width_ratio = after["width"] / before["width"]
height_ratio = after["height"] / before["height"]
results.append(
{
"name": source.name,
"status": "downsampled"
if width_ratio < 1 or height_ratio < 1
else "same_dimensions",
"before": before,
"after": after,
"width_ratio": round(width_ratio, 3),
"height_ratio": round(height_ratio, 3),
}
)
return results
if __name__ == "__main__":
schema = load_compression_schema()
print(
{
"method": schema["method"],
"path": schema["path"],
"idempotent": schema["idempotent"],
"params": schema["params"],
}
)
for result in compare_samples(Path("sample")):
print(result)
This gate catches a common form of embedded image downsampling. It won't detect every visual change: an encoder can keep width and height while increasing JPEG loss, color handling can alter appearance, and a missing extracted image can mean the two extraction runs did not produce comparable names. Those are reasons to keep the script modest and the human check explicit. For repeatable review, use the same viewer, the same zoom, and the same page details, then store the verdict beside the derivative's identity rather than beside a mutable filename.
The API boundary should be equally restrained. A workflow may use Infrai's verified image-extraction and PDF-compression capabilities for the two stages, but request fields should come from the service's current discovery schema instead of being guessed. The durable application code owns classifications such as authoritative_original, review_derivative, and fidelity_approved; provider-specific rendering controls stay at the adapter edge. This is the part teams are tempted to skip, and it's the part that makes a later vendor swap survivable.
Why the render-first archive was rejected
The rejected design stores only uniformly compressed bundles and treats a successful render as proof that the archive is acceptable. It is attractive because there is one object per bundle and every viewer reads the same artifact. It fails this property-management decision because the document classes are heterogeneous and the expensive mistake is irreversible: an inspection image can lose evidence while surrounding text remains sharp.
Render-first is still valid for disposable previews. If the user is browsing a bundle list, a derivative optimized for quick display may be exactly right, provided it is labeled as a preview and the original remains available for close inspection. It can also suit documents made entirely of text and vector content after the sample gate demonstrates that the relevant content survives. The policy should follow inspection value, not file extension.
Archive-first has its own downside — extra objects, lifecycle rules, and bookkeeping — and is not suitable when the inputs are already disposable derivatives with no evidentiary or close-review value. In that narrower case, keep the compressed copy and delete transient processing material according to the application's retention policy. For leases, signatures, and condition photographs, however, the original is the cheaper mistake to avoid measuring: preserve it, verify a sample, and let the compressed PDF remain a replaceable view.
Top comments (0)