Use for template field coordinates, PDF fill, font embedding, stamps, watermarks, handwriting overlay, and document version generation. PDFs are mutated only on the backend; defines coordinate origin, storageKey version naming, no overwrite of versions, and frontend preview/drag only.
Install
npx skillscat add sundaydev-arch/hrsign/pdf-pipeline Install via the SkillsCat registry.
SKILL.md
Skill 03: PDF processing pipeline
When to use
- Change template editor fields/coordinates, the fill engine, stamps and watermarks, or signature image overlay
- Add a DocumentVersion stage
- Handle CJK fonts or text overflow
Module map
| Concern | File |
|---|---|
| Field coordinate schema | schemas/coordinates.ts: { page, x, y, width, height, rotation }, page starts at 1 |
| Backend fill | lib/pdf/fill.ts (flat FillField, FILLABLE_TYPES=TEXT/DATE) |
| CJK fonts | lib/pdf/font.ts + public/fonts/NotoSansSC-Regular.ttf (static TrueType; embed with { subset: false } — fontkit CJK subsetting and CFF/OTF break in pdf.js) |
| Stamp/watermark | lib/pdf/stamp.ts stampPdf(bytes, images, options) |
| Sign orchestration | lib/signing.ts executeSign/loadTaskForSign |
| Provider | providers/signature/image-seal.ts (stage 1); pades/gm-sm2 are stage-2 empty implementations |
| Frontend preview/edit | components/pdf/PdfViewer.tsx, TemplateEditor.tsx (PDF_OVERLAY), DocumentEditor.tsx (DOCUMENT / TipTap), SignaturePad.tsx |
| Document → PDF | lib/pdf/document-render.ts renderDocumentToPdf on publish; TipTap JSON in TemplateVersion.contentJson |
| TipTap deps | @tiptap/react, @tiptap/pm, @tiptap/starter-kit, @tiptap/extension-placeholder — allowed for DOCUMENT editor mode only |
Hard rules
- Any PDF mutation happens only on the backend; frontend code must not call pdf-lib write/save. Frontend pdf.js only renders and collects coordinates.
- Never overwrite versions: each artifact gets a new key (current convention
documents/{docId}/v{n}-{STAGE}.pdf); putting over the same key is forbidden (StorageProvider semantics); always persist DocumentVersion (version/stage/storageKey/sha256/createdBy). - Compute
sha256before every write and store it on the version; template uploads also store sha256. - Coordinates are used only after CoordinatesSchema parse. Origin semantics: the schema specifies PDF-point origin at bottom-left; the editor currently stores top-left pixel semantics; a dedicated conversion module is still missing — do not flip values until that lands; annotate origin in comments and list it as leftover work.
- Field types: fillable TEXT/DATE; sign/seal domains SEAL/SIGNATURE (PERFORATION_SEAL reserved, not built). The old
placeholderfield is unified as TemplateField.defaultValue. - Pipeline order is fixed: fill → watermark → stamp → personal signature → (stage 2) encrypt → lock; stamp/watermark/fill must all happen before cryptographic signing.
- Watermark: global copy from
WATERMARK_TEXT(signing.ts passes through to StampOptions); per-download "downloader + timestamp" watermark is not implemented yet; adding it must not remove the global watermark path. - pdf.js worker loads from self-hosted
/pdf.worker.min.mjs, not a CDN; keep the currentGlobalWorkerOptions.workerSrcstyle. - CJK output must use the embedded-font path; do not rely on system fonts.
Standard steps (example: new field effect)
- Confirm field type and coordinate source (editor saves flat page/x/y…; PUT wraps them as coordinates Json)
- Implement pure functions in fill.ts/stamp.ts: buffer in, new buffer out
- Callers (signing or tasks routes) persist the new version + hash; pdf helpers must not touch Prisma
- Results that include CJK should have snapshot/sample verification (current test gap; new logic at least needs a manual sample PDF)
Do not
- ❌ Import pdf-lib in the browser to mutate files
- ❌ Reuse the same storageKey and overwrite an old version
- ❌ Instantiating PrismaClient or sending notifications inside PDF helpers
- ❌ Hardcoding font paths to system directories
Done when
- New version has version/stage/sha256/createdBy
- tsc and tests pass; CJK rendering verified manually
- Coordinate-semantics changes are documented in comments and the leftover list