TPTruPDF

Security & redaction

DRM permission enforcement, watermarking, audit logging, structural redaction. No CSS overlay redaction — the engine refuses to burn unless a structural adapter is registered.

DRMEngine

Permission gate. Doesn't encrypt — that's a function of secure mode + the underlying PDF.

import { DRMEngine } from '@trupdf/viewer';
 
const drm = new DRMEngine({
  permissions: new Set(['view', 'print']),
  expiresAt: '2026-12-31T23:59:59Z',
  maxOpens: 10,
});
 
drm.enforce('print'); // throws if not in the set
drm.enforce('copy'); // throws → host catches → snack 'Copy blocked'

Permissions: view, print, copy, edit, export, annotate, redact, sign. DRMPolicy also takes expiresAt (ISO timestamp) and maxOpens.

Secure mode

secureMode: true on DocViewer does the following:

  • No DOM text layer — selection by browser is impossible.
  • No getTextContent() — APIs that would expose text are gated.
  • DOM-level blocks — Ctrl+C / Cut / Drag / right-click context.
  • Selection overlays suppressed.
  • AI Explain Selection / Explain Page short-circuit with a typed error; Document Summary falls back to the in-browser OCR route.
  • Screen reader access via @trupdf/a11y structural reading order (live region) instead of the text layer.
<DocViewer source="…" licenseManager={license} secureMode />

Watermark

applyWatermark(input, options) draws diagonal repeating text into the page content stream, not as a /Annot. The watermark survives flattening and renders in every compliant viewer.

import { applyWatermark } from '@trupdf/viewer';
 
const watermarked = await applyWatermark(bytes, {
  text: 'CONFIDENTIAL',
  fontSize: 60, // default 60
  rotation: 30, // degrees, default 30
  opacity: 0.18, // default 0.18
  color: { r: 0.5, g: 0.5, b: 0.5 },
  pages: [1, 3, 5], // 1-indexed; omit for all pages
});

For the trial-tier auto-watermark, WatermarkOverlay is instantiated automatically by LicenseManager and painted onto every render + burned into every save.

RedactionEngine

Structural removal of text objects from the PDF byte stream, audited and non-cancellable. The engine refuses to burn if no structural adapter is registered. bindPdfboxRedactionAdapter binds the document-service redaction adapter (see the TruPDF document service below); custom backends register their own adapter instead.

import { RedactionEngine, AuditLogger, bindPdfboxRedactionAdapter } from '@trupdf/viewer';
 
// Call once at app boot.
bindPdfboxRedactionAdapter({
  endpoint: process.env.NEXT_PUBLIC_TRUPDF_DOCSERVICE_URL!,
});
 
const audit = new AuditLogger();
const redactor = new RedactionEngine();
 
const result = await redactor.apply(
  srcBytes,
  [
    {
      page: 4,
      x: 100,
      y: 200,
      width: 80,
      height: 14,
      exemptionCode: 'FOIA b6',
    },
  ],
  { audit },
);
 
console.log(result.objectsRemoved); // count of text-showing ops removed

The audit entry is appended before the bytes are returned. If the audit append throws, the burn aborts and the original bytes are left untouched.

The TruPDF document service

The reference structural-removal backend — a self-hosted container that powers redaction burns. The burn pipeline runs a two-pass content-stream rewrite:

  1. Pass 1 — tag every text-showing operator whose glyph bbox intersects a redaction rect.
  2. Pass 2 — rewrite the page content stream replacing those operators with [] TJ.

A solid black rectangle is drawn on top as the visual indicator.

docker compose --profile phase8 up -d

Custom redaction adapter

Hosts that prefer their own redaction backend can register a custom structural adapter via registerStructuralRedactionAdapter:

import { registerStructuralRedactionAdapter } from '@trupdf/viewer';
 
registerStructuralRedactionAdapter(async () => ({
  async removeIntersectingObjects(bytes, marks) {
    // structurally remove text-showing operators intersecting marks
    return { bytes: rewritten, removed: opsRemoved };
  },
}));

From the viewer

ViewerEngine.applyRedactions() collects every redaction-mark annotation, calls the engine, replaces the loaded document with the burned bytes, and removes the marks from the manager.

const result = await engine.applyRedactions({ actor: currentUser });

Audit log

Append-only event log. Events are frozen; subscribers forward to backend storage via the appended event.

import { AuditLogger } from '@trupdf/viewer';
 
const audit = new AuditLogger();
audit.on('appended', (entry) =>
  fetch('/audit', {
    method: 'POST',
    body: JSON.stringify(entry),
  }),
);

Event kinds recorded (the union grows without breaking consumers — the log is append-only):

redaction.applied, redaction.failed, signature.applied, signature.verified, signature.failed, compare.run, document.opened, document.printed, document.exported, ai.query, ai.response, ai.cache_hit, ai.error, ai.extraction.completed, ai.parsing.completed.

Each entry carries { id, at, kind, payload, actor? }; query past entries with audit.listByKind(kind).

License gating

RedactionEngine calls gate.require('redaction') in its constructor; trial tokens cannot burn redactions. UIs can hide the "Redact" button with gate.has('redaction') ahead of time.