# `SignCore.PDF.Writer`
[🔗](https://github.com/utaladriz/pkcs11ex/blob/v0.1.0/lib/sign_core/pdf/writer.ex#L1)

Hand-rolled PAdES B-B incremental-update emitter.

The writer takes a base PDF and produces an incremental update with a
PKCS#7-detached `/Sig` dictionary whose `/Contents` is a fixed-size hex
placeholder. The caller then:

  1. signs the bytes the writer reports as `:signed_input` via
     `Pkcs11ex.sign_bytes/2` (or any equivalent CMS pipeline);
  2. calls `inject_signature/2` to splice the resulting CMS DER into
     the placeholder.

The two-phase shape is mandatory: PAdES `/ByteRange` covers everything
except the hex digits, so the bytes-to-be-signed cannot be known until
the placeholder's exact byte offset is fixed.

### What the writer emits

Three new objects are appended:

  * the existing catalog re-emitted with `/AcroForm` extended (or
    added) so the new `/Sig` field is reachable from `/Root`;
  * a signature field annotation (`/FT /Sig`, `/Subtype /Widget`,
    invisible 0×0 rect) referencing the `/Sig` dict via `/V`;
  * the `/Sig` dict itself: `/Filter /Adobe.PPKLite`,
    `/SubFilter /adbe.pkcs7.detached`, fixed-width `/ByteRange` and
    `/Contents` placeholders, plus optional `/M`, `/Reason`,
    `/Location`.

Followed by a fresh xref subsection (one entry per new object number,
plus one for the re-emitted catalog), trailer with `/Prev` pointing
at the original `startxref`, and the new `startxref` + `%%EOF`.

### v1 limitations

  * Refuses to merge with an existing `/AcroForm`. Re-signing a PDF
    that already has form fields needs full PDF object grammar; out
    of scope for v1. Returns
    `{:error, {:writer, :existing_acroform_unsupported_in_v1}}`.
  * Refuses to operate on PDFs whose `/Root` catalog uses a cross-
    reference stream (Reader returns `:xref_stream_unsupported`).
  * The `/Sig` field is invisible — no widget rectangle, no rendering
    hints. Visible signature appearance streams are a Phase 4b.x
    feature.

# `prepare_opts`

```elixir
@type prepare_opts() :: [
  placeholder_size: pos_integer(),
  signing_time: DateTime.t(),
  reason: String.t(),
  location: String.t(),
  contact_info: String.t()
]
```

Per-call options:

  * `:placeholder_size` — bytes the CMS DER will occupy. The hex
    placeholder in the PDF is exactly twice this many ASCII chars.
    Default `8192`. Real-world PAdES B-B
    signatures with a single end-entity cert and no timestamp run
    ~6 KiB; raise this when embedding LTV material.
  * `:signing_time` — `DateTime.t()` for the `/M` entry in the Sig
    dict. Default `DateTime.utc_now/0`. Note: PAdES verifiers
    generally trust the CMS `signing-time` attribute, not `/M`.
  * `:reason`, `:location`, `:contact_info` — optional `/Reason`,
    `/Location`, `/ContactInfo` entries in the Sig dict.

# `t`

```elixir
@type t() :: %SignCore.PDF.Writer{
  byte_range: [non_neg_integer()],
  contents_length: non_neg_integer(),
  contents_offset: non_neg_integer(),
  pdf: binary(),
  placeholder_size: pos_integer(),
  signed_input: binary()
}
```

Output of `prepare/2`. The caller hashes `:signed_input`, builds a
CMS over that hash, and feeds the CMS DER to `inject_signature/2`.

# `inject_signature`

```elixir
@spec inject_signature(t(), binary()) :: {:ok, binary()} | {:error, term()}
```

Splices `cms_der` into the `/Contents` placeholder of a prepared PDF.
Returns `{:error, {:writer, :cms_der_too_large}}` if the DER exceeds
the prepared placeholder size; pads with `0x00` bytes (hex `00`)
otherwise.

# `prepare`

```elixir
@spec prepare(binary(), prepare_opts()) :: {:ok, t()} | {:error, term()}
```

Builds the incremental-update bytes and returns the structure
describing what to sign.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
