npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pdf-ring-watermark

v1.2.0

Published

Embed a traceable ID into PDFs as a discreet, human-readable concentric-ring watermark for leak tracing.

Downloads

83

Readme

pdf-ring-watermark

Embed a traceable ID into a PDF as a discreet, human-readable concentric-ring watermark — so that any copy of a document can be traced back to its source.

ring-watermark.teckentrup.software — project website with docs and an interactive demo.

The watermark looks like a subtle decorative background of concentric rings. In reality each ring is one bit of an error-protected codeword: a solid ring is a 1, a dashed ring is a 0, and a distinctive dash-dot ring marks the start of the sequence. Because the pattern repeats across the whole page, even a small photographed crop usually contains a full copy of the ID.

Tagged document

Why this exists

Classic leak tracing ("traitor tracing"): give every recipient a personalized copy, and if a copy turns up where it shouldn't, read the embedded ID to trace the source. pdf-ring-watermark provides two carriers:

  • Metadata — the ID is written to the PDF's document properties. Convenient, but trivially stripped.
  • The ring pattern — the forensic channel. It survives copying, re-export, screenshots, and printing, and it can be read by a machine or by a human with no tools.

Features

  • Discreet — renders as a faint guilloché-style background; content stays fully legible.
  • Crop-robust — the codeword tiles radially, so a partial crop still carries the ID. A ~3 × 3 cm fragment is enough for the default profile (see capacity).
  • Error protection — an optional Hamming SECDED code corrects one misread ring and detects two, and guarantees every two IDs differ in ≥ 4 ring bits.
  • Human-readable fallback — read the rings by eye, type the bits, and a CLI recovers the ID.
  • Matched detection — even an incomplete or slightly wrong read can be matched against your list of issued IDs.
  • Three pattern modes — concentric rings (best camouflage, 2-D spread) or horizontal-lines / vertical-lines (axis-aligned, easiest to read and to decode automatically).
  • Fully parameterizable — pattern mode, ID capacity, error protection, spacing, stroke width, darkness, and opacity.

Install

npm install pdf-ring-watermark

Runtime dependencies are only pdf-lib and @pdf-lib/fontkit. Rendering a PDF to an image (needed only for the experimental automatic image decoder) is left to you.

Quick start

import { createRingWatermark } from "pdf-ring-watermark";
import { readFile, writeFile } from "node:fs/promises";

const watermark = createRingWatermark(); // defaults: 14-bit ID, SECDED, discreet

// Tag a copy with document ID 10234, fitting it onto A4:
const source = new Uint8Array(await readFile("contract.pdf"));
const tagged = await watermark.tagPdfOnPaper(source, { userId: 10234 });
await writeFile("contract-10234.pdf", tagged);

// Later, recover the ID from a manual read of a leaked copy:
console.log(watermark.decodeBits("01110011111111110100")); // { userId: 10234, ... }

Reading a leaked document by hand

No tools required — this is the whole point of the visible channel.

A corner crop

  1. Find a dash-dot ring — that is the start marker (bit 0 begins just outside it).
  2. Read the rings outward from there: solid = 1, dashed = 0.
  3. Note the sequence. You can also read the rings inward of the start marker — they belong to the previous, identical repetition and fill the high bits.
  4. Feed the bits to the CLI, marking the start ring with ., ,, or !:
npx pdf-ring-watermark decode-bits "1110100,0111001111"
  • A complete read returns the ID (and auto-corrects a single misread or a single gap).
  • An incomplete read — or one you're unsure about — is matched against your issued-ID list:
npx pdf-ring-watermark match-bits --ids issued-ids.txt "0111,00111111"

This ranks the candidates by likelihood; the correct ID wins even from a partial read, because your list bounds the search.

The line modes read the same way — find the dash-dot line and read across it (top→bottom for horizontal lines, left→right for vertical).

If you can't tell a ring's style apart, type x (or ?) in its place. It's kept as an erasure so the following bits stay aligned: with SECDED a single uncertain ring is still recovered automatically, and match-bits simply ignores the unknown positions.

The CLI must use the same profile the document was tagged with. If you changed idBits or protection, pass --id-bits <n> and --protection <none|secded>.

API

createRingWatermark(options?) → RingWatermark

All options are optional; defaults produce a discreet, A4-friendly watermark with 1-bit error protection.

| Option | Default | Meaning | | --- | --- | --- | | mode | "rings" | "rings", "horizontal-lines", or "vertical-lines". | | idBits | 14 | Payload bits — the ID space is 2 ** idBits (16,384 IDs). | | protection | "secded" | "none" (max density) or "secded" (correct 1 / detect 2 bit errors). | | spacingPt | 4 | Radial spacing between adjacent rings — the radius step from one ring to the next (for line modes: the perpendicular gap between lines), in PDF points. | | strokeWidthPt | 0.6 | Ring stroke width, in points. | | firstRadiusPt | 20 | Radius of the innermost ring, in points. | | darkness | 0.42 | Ink darkness of the rings, 01. | | opacity | 0.15 | Stroke opacity, 01 (lower = more discreet). |

Instance methods

// Encoding
watermark.createTestDocument({ userId, paper? }): Promise<Uint8Array>;   // blank demo page
watermark.tagPdf(source, { userId }): Promise<Uint8Array>;              // overlay, keep page size
watermark.tagPdfOnPaper(source, { userId, paper? }): Promise<Uint8Array>; // fit onto paper, full-bleed

// Manual decoding (fallback)
watermark.decodeBits(input): ManualDecodeResult;                        // full read -> ID
watermark.matchBits(input, candidateIds): ManualMatchResult;            // partial read -> ranked IDs

// Experimental automatic image decoding (see Limitations)
watermark.decodeImageExperimental(image, candidateIds): ImageDecodeResult;

// Geometry
watermark.codewordBits;        // total pattern bits per ID
watermark.elementsPerCodeword; // rings/lines spanned by one codeword (incl. sync)
watermark.periodMm();          // length of one codeword across the pattern, mm
watermark.safeCropSquareCm();  // side of a crop square that always contains a full ID

tagPdfOnPaper is the recommended way to tag a document you will print: it places the content on the target paper (A4 by default) and draws the pattern edge-to-edge, so it prints without margins. tagPdf keeps the source page size (useful when the source already matches your paper).

Capacity & crop size

More payload bits and stronger protection cost more ring bits, which means a larger crop is needed to capture a whole ID. Guaranteed error-tolerant capacity (minimum Hamming distance) for a 16-bit-class codeword:

| Protection | Guaranteed distance | Correctable | IDs (14-bit payload) | | --- | --- | --- | --- | | none | 1 | 0 bits | 16,384 | | secded | 4 | 1 bit (2 detected) | 16,384 |

For the default profile (14-bit, SECDED, 4 pt spacing): the codeword is 20 bits, one codeword spans 21 rings ≈ 3.0 cm, so a ≈ 3 × 3 cm crop guarantees a full ID in any position and rotation. Mere identification via match-bits typically needs only ~2 × 2 cm.

An interactive calculator (payload bits + protection → rings and crop size) is available on the website.

How it works

  • Geometry. Rings are concentric quarter-circles centered at a page corner, stepping outward by spacingPt. Every group of codewordBits + 1 rings starts with a dash-dot sync ring followed by the data rings; the codeword repeats outward for redundancy.
  • Bits. Each data ring's line style encodes one bit (solid 1 / dashed 0).
  • Codec. The user ID is turned into a codeword by the payload codec. With secded, a Hamming single-error-correct / double-error-detect code adds parity so every two IDs are far apart in bit space and single misreads self-correct.
  • Decoding. A reader measures each ring's duty cycle (ink along the arc) into a soft value, aggregates repetitions, and either decodes the codeword directly or correlates the soft values against a candidate ID list (matched detection). The sync ring anchors the absolute bit positions; rings can even be counted locally, without recovering the pattern center.

Limitations

  • The automatic image decoder is experimental. Recovering an ID directly from a photo/scan works well for full pages and corner-region crops, but degrades far from the center where the arcs are nearly straight. Always check the returned confidence, and prefer the manual decodeBits / matchBits path when in doubt. The metadata channel and the manual channel are the reliable core.
  • The metadata channel is trivially stripped — treat it as convenience only.
  • Printing a page onto a different paper size letterboxes the pattern; tag with tagPdfOnPaper for your target paper, or print at 100 %.

License

MIT — see LICENSE.