SignatureKit
Receitas

Preview com react-pdf

Renderize PDFs assinados e não assinados ao lado do signer e envie retângulos clicados para o workflow de PDF.

react-pdf pertence à sua app, não aos pacotes SignatureKit. Use-o como superfície de preview, mantenha object URLs na ação que os cria e passe o retângulo selecionado para prepareAndSignPdf.

PdfPreviewIsland.tsx
import type { PdfPreviewProps } from "./PdfPreview"
import { lazy, Suspense, useSyncExternalStore } from "react"

const LazyPdfPreview = lazy(() =>
  import("./PdfPreview").then((module) => ({ default: module.PdfPreview })),
)

const subscribe = (_onStoreChange: () => void) => () => {}
const getClientSnapshot = () => true
const getServerSnapshot = () => false

function useHydrated() {
  return useSyncExternalStore(subscribe, getClientSnapshot, getServerSnapshot)
}

function PdfPreviewFallback() {
  return <p role="status" aria-live="polite">Carregando preview do PDF…</p>
}

export function PdfPreviewIsland(props: PdfPreviewProps) {
  const hydrated = useHydrated()

  return (
    <Suspense fallback={<PdfPreviewFallback />}>
      {hydrated ? <LazyPdfPreview {...props} /> : <PdfPreviewFallback />}
    </Suspense>
  )
}

PdfPreviewIsland é o boundary cross-framework: o Waku renderiza o snapshot do servidor como false, então o callback lazy (e o módulo react-pdf/pdf.js que ele carrega) nunca é avaliado durante SSR ou na passagem inicial de hidratação. Quando o snapshot do navegador vira true, Suspense mantém o fallback visível até o chunk do cliente ser resolvido. O mesmo contrato React funciona em outros frameworks SSR sem um helper dynamic específico de framework.

PdfPreview.tsx
import { type PdfSignaturePage, type PdfSignatureRect } from "@signature-kit/pdf/config"
import { Document, Page } from "react-pdf"
import * as React from "react"

const STAMP_SIZE = { width: 180, height: 54 }

export type PdfPreviewProps = {
  readonly unsignedPdf: Uint8Array
  readonly pages: Array<PdfSignaturePage>
  readonly sign: (rect: PdfSignatureRect) => Promise<Uint8Array>
}

export function PdfPreview({ unsignedPdf, pages, sign }: PdfPreviewProps) {
  const [previewUrl, setPreviewUrl] = React.useState<string>()
  const [stampRect, setStampRect] = React.useState<PdfSignatureRect>()

  const showBytes = React.useCallback((bytes: Uint8Array) => {
    setPreviewUrl((current) => {
      if (current !== undefined) URL.revokeObjectURL(current)
      return URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }))
    })
  }, [])

  const clearPreview = React.useCallback(() => {
    setPreviewUrl((current) => {
      if (current !== undefined) URL.revokeObjectURL(current)
      return undefined
    })
  }, [])

  const placeOnClick = React.useCallback(
    (page: PdfSignaturePage, event: React.MouseEvent<HTMLDivElement>) => {
      const bounds = event.currentTarget.getBoundingClientRect()
      const x = ((event.clientX - bounds.left) / bounds.width) * page.width
      const y = ((event.clientY - bounds.top) / bounds.height) * page.height
      setStampRect({
        pageIndex: page.index,
        x: Math.min(Math.max(0, x), page.width - STAMP_SIZE.width),
        y: Math.min(Math.max(0, y), page.height - STAMP_SIZE.height),
        width: STAMP_SIZE.width,
        height: STAMP_SIZE.height,
      })
    },
    [],
  )

  const signPlaced = React.useCallback(async () => {
    if (stampRect === undefined) return
    const signedPdf = await sign(stampRect)
    showBytes(signedPdf)
  }, [showBytes, sign, stampRect])

  return (
    <section>
      <button type="button" onClick={() => showBytes(unsignedPdf)}>Preview sem assinatura</button>
      <button type="button" onClick={signPlaced}>Assinar retângulo</button>
      <button type="button" onClick={clearPreview}>Fechar preview</button>
      {previewUrl === undefined ? null : (
        <Document file={previewUrl}>
          {pages.map((page) => (
            <div key={page.index} onClick={(event) => placeOnClick(page, event)}>
              <Page pageNumber={page.index + 1} width={720} />
            </div>
          ))}
        </Document>
      )}
    </section>
  )
}
sign-from-preview.ts
import { a1SignaturesLayer } from "@signature-kit/a1/signer"
import { type PdfSignatureRect } from "@signature-kit/pdf/config"
import { prepareAndSignPdf } from "@signature-kit/pdf/workflow"
import { Effect, Layer, Redacted } from "effect"

const sign = (rect: PdfSignatureRect): Promise<Uint8Array> =>
  Effect.runPromise(
    prepareAndSignPdf({
      pdf: unsignedPdf,
      pages,
      stampRects: [rect],
      lines: ["Maria Souza", "ICP-Brasil"],
      rubric: { initials: "MS" },
      signing: { policy: "pades-icp-brasil", name: "Maria Souza" },
    }).pipe(
      Effect.provide(
        Layer.merge(
          a1SignaturesLayer({ pfx, password: Redacted.make(password) }),
          liteParseWorkerBrowserLayer,
        ),
      ),
    ),
  )

A app controla preview e react-pdf; SignatureKit continua responsável por parsing, carimbos, rubricas e assinatura.

Como está este guia?