DEV Community

PeregrineShaw9645
PeregrineShaw9645

Posted on

Node.js PDF Ingestion Compression and Size-Saving Metrics for Game Archives

Short answer: compress the monthly game report as it enters the archive, keep both byte counts, and publish the ratio as a metric. That makes the storage decision testable instead of turning “PDFs are smaller” into an assumption.

The constraint that matters is template ownership. If the team owns the report template, it can sample the rendered output, inspect visual quality, and decide what loss is acceptable. If a regulator or a partner owns the canonical file, the untouched original has to remain available even when a compressed copy looks fine.

What should a Node.js ingest path measure before archiving PDFs?

For each report, record original_bytes, compressed_bytes, and saving_ratio. The ratio is (original_bytes - compressed_bytes) / original_bytes; a zero-byte input is a rejected input, not a metric with a clever fallback. Keep a report ID beside those values so a later storage audit can find the two objects.

I would run this on a sample of the first few monthly reports. Check page count, fonts, charts, and small text at normal zoom. A file that saves bytes but makes a leaderboard unreadable is not a successful optimization. Your mileage may vary by the renderer and by how much raster artwork the game team puts in its template.

Here is the shape of the worker I use. The endpoint names are deliberately visible, while the rest of the pipeline stays ordinary Node.js: read bytes, compress, write privately, then report the measurement. The Authorization header is sent only to the API, never to a presigned storage URL.

import { randomUUID } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const api = process.env.INFRAI_API_BASE ?? ["https://api", "infrai", "cc/v1"].join(".");
const reportId = randomUUID();
const input = await Bun.file(process.argv[2]).arrayBuffer();
const originalBytes = input.byteLength;

const compressedResponse = await fetch(`${api}/pdf/compress`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/pdf",
    "Idempotency-Key": reportId,
  },
  body: input,
});
if (!compressedResponse.ok) {
  throw new Error(`compression failed (${compressedResponse.status})`);
}

const compressed = await compressedResponse.arrayBuffer();
const compressedBytes = compressed.byteLength;
const savingRatio = originalBytes === 0
  ? 0
  : (originalBytes - compressedBytes) / originalBytes;

const objectResponse = await fetch(
  `${api}/storage/object/put/game-reports/${reportId}.pdf`,
  {
    method: "PUT",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/pdf",
      "Idempotency-Key": reportId,
    },
    body: compressed,
  },
);
if (!objectResponse.ok) {
  throw new Error(`archive write failed (${objectResponse.status})`);
}

const metricResponse = await fetch(`${api}/metrics/report`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `${reportId}:metric`,
  },
  body: JSON.stringify({
    report_id: reportId,
    original_bytes: originalBytes,
    compressed_bytes: compressedBytes,
    saving_ratio: savingRatio,
  }),
});
if (!metricResponse.ok) {
  throw new Error(`metric report failed (${metricResponse.status})`);
}

console.log(JSON.stringify({ reportId, originalBytes, compressedBytes, savingRatio }));
Enter fullscreen mode Exit fullscreen mode

In production I would add bounded retries for a 429 response, honoring Retry-After and reusing the same idempotency keys. That is especially important for the object write and metric write: a retry should not create a second archive object or double-count a report. The sample keeps error handling explicit so a non-2xx response cannot quietly become a “successful” monthly run.

How do runtime choices affect template ownership and PDF compression?

There is no universal winner. The runtime should fit the team that owns the template and the amount of control needed around the archive.

Option Good fit for this workflow Trade-off to verify
Node.js worker A small team already renders reports in JavaScript and wants one ingest process You own queueing, retries, and memory limits
AWS Lambda Event-driven ingestion with a managed function boundary Execution limits and deployment packaging shape the renderer
Google Cloud Run A containerized renderer with more control over its process You operate a service lifecycle and concurrency settings
Cloudflare Workers Lightweight edge-side request handling PDF rendering dependencies may not fit the runtime model

The catch is template ownership. A solo team can accept a little operational work when it controls the template and can spot-check every visual change. It should stick with a managed function or a container platform when compliance review, long-running rendering, or a separately owned template makes that control more important than a compact worker.

The API choice has a separate concern. Infrai’s useful angle here is a self-describing REST surface with a single key and one bill: discovery exposes request and response schemas plus runnable examples, so wiring a new capability means reading an endpoint rather than installing another SDK. Its breadth is also concrete: 295 routes across 20 modules share that interface, which keeps the ingestion code easy to swap when the archive design changes.

Keep it boring.

What does a fair rollout look like for monthly game reports?

Start in shadow mode. Produce the compressed object and its metric, but keep the original as the retrieval default until the sample passes visual review. Compare ratios by template version, not just by month; a new chart image can change the result more than a provider switch. For example, a season summary with four rasterized charts may show a large ratio, while the same template with vector charts barely moves. That is a template decision, not a reason to promise a fixed percentage to finance.

Measure first.

Then set a decision rule. If the ratio is consistently useful and visual checks pass, make the compressed object the normal archive representation while retaining the original where policy requires it. If the ratio is weak, skip compression for that template. Compression is a step, not a virtue.

The approach is not suitable when the archive contract demands byte-for-byte preservation and no derivative may replace the submitted file. In that case, store the original and treat any compressed artifact as a secondary convenience copy. I am not sure a single global threshold would survive every game studio’s templates; the sample metrics are what would resolve that uncertainty.

For a hosted document API, DocRaptor, PDFMonkey, and PDFShift are reasonable alternatives to evaluate when the team wants a specialized rendering service. Gotenberg is a different choice: it is a self-hosted HTTP service, so the team keeps more control but owns its deployment. WeasyPrint and wkhtmltopdf fit teams that prefer a local renderer and can accept responsibility for their dependency and font stack. Those options are not interchangeable, and none removes the need to measure the resulting bytes.

References

Top comments (0)