# `PdfElixide.Compliance`
[🔗](https://github.com/r8/pdf_elixide/blob/v0.23.0/lib/pdf_elixide/compliance.ex#L1)

Checks a document against the PDF/A, PDF/UA and PDF/X standards, and
converts an editor's document to PDF/A.

    {:ok, doc} = PdfElixide.Document.open("report.pdf")
    {:ok, report} = PdfElixide.Compliance.validate(doc, :pdf_a_2b)
    report.compliant?
    #=> false
    Enum.map(report.errors, & &1.code)
    #=> ["XMP-001", …]

`validate/2` returns a `PdfElixide.Compliance.Report` listing the errors and
warnings found. A document that fails a standard is an ordinary report with
`compliant?: false`, not an error; `{:error, %PdfElixide.Error{}}` means the
document could not be read far enough to check it.

## What a report establishes

Validation runs this library's own checks for the requested standard, which
cover a subset of each standard's requirements. `compliant?: true` means those
checks found no errors; it is not a conformance certification. Use a dedicated
conformance validator where a claim of compliance has to hold up.

PDF/A and PDF/UA reports list checks that were only partly performed among
their warnings, under code `"WARN-007"`, so even a report with no errors
carries warnings.

A PDF/A report does not compare the level a document declares with the one
requested, so a document declaring PDF/A-2b can be reported compliant with
PDF/A-1b. Compare `report.declared` with the standard where the declaration
matters. Nor does it check that the document information dictionary agrees
with the XMP metadata, or that the file carries a document identifier
(`/ID`), both of which PDF/A requires. A PDF/A-1 report does not check that
the XMP metadata is stored uncompressed, which PDF/A-1 also requires.

PDF/A and PDF/X forbid encryption, so an encrypted document always fails them.
PDF/UA-2 is not offered: this library performs no check specific to it.

PDF/X checks read each page's boxes from the page's own dictionary. A page
that inherits its `/MediaBox` from the page tree is reported as missing one
(`"XBOX-004"`), and its boxes are not checked for consistency.

## Converting to PDF/A

`convert/3` rewrites the document an editor holds and returns a
`PdfElixide.Compliance.Conversion` describing what changed:

    {:ok, editor} = PdfElixide.Editor.open("report.pdf")
    {:ok, conversion} = PdfElixide.Compliance.convert(editor, :pdf_a_2b)
    if conversion.report.compliant? do
      {:ok, pdf} = PdfElixide.Editor.to_binary(editor)
    end

Conversion fixes supported findings and reports anything left unresolved, so
it can finish with `compliant?: false`. Convert last, then validate the bytes
you write.

See the [PDF/A conversion](guides/pdf-a-conversion.md) guide for the fixes,
editor restrictions, metadata caveats and font-embedding trade-offs.

## Memory and locking

Validation reads a private copy of the document, parsed from its bytes, so
it holds about two extra copies of the file in memory for its duration. It
takes a *shared* read on the handle, and so runs alongside other reads — with
one exception: a document that cannot be opened without a password is
validated in place and takes the handle *exclusively*. See the
[Concurrency](guides/concurrency.md) guide.

Conversion takes the editor *exclusively*. It holds several copies of the
document at once: the written bytes it starts from, the converted document
and the editor rebuilt from it.

# `convert_opts`

```elixir
@type convert_opts() :: [
  embed_fonts: boolean(),
  remove_javascript: boolean(),
  remove_embedded_files: boolean(),
  icc_profile: binary() | nil
]
```

Options for `convert/3` and `convert!/3`. See `convert/3` for defaults.

# `pdf_a`

```elixir
@type pdf_a() ::
  :pdf_a_1a
  | :pdf_a_1b
  | :pdf_a_2a
  | :pdf_a_2b
  | :pdf_a_2u
  | :pdf_a_3a
  | :pdf_a_3b
  | :pdf_a_3u
```

A PDF/A level: the part of `t:standard/0` that `convert/3` accepts.

# `standard`

```elixir
@type standard() ::
  pdf_a()
  | :pdf_ua_1
  | :pdf_x_1a_2001
  | :pdf_x_1a_2003
  | :pdf_x_3_2002
  | :pdf_x_3_2003
  | :pdf_x_4
  | :pdf_x_4p
  | :pdf_x_5g
  | :pdf_x_5n
  | :pdf_x_5pg
  | :pdf_x_6
```

A standard and level to validate against.

  * PDF/A: `:pdf_a_1a`, `:pdf_a_1b`, `:pdf_a_2a`, `:pdf_a_2b`, `:pdf_a_2u`,
    `:pdf_a_3a`, `:pdf_a_3b`, `:pdf_a_3u`.
  * PDF/UA: `:pdf_ua_1`.
  * PDF/X: `:pdf_x_1a_2001`, `:pdf_x_1a_2003`, `:pdf_x_3_2002`,
    `:pdf_x_3_2003`, `:pdf_x_4`, `:pdf_x_4p`, `:pdf_x_5g`, `:pdf_x_5n`,
    `:pdf_x_5pg`, `:pdf_x_6`.

# `convert`

```elixir
@spec convert(PdfElixide.Editor.t(), pdf_a(), convert_opts()) ::
  {:ok, PdfElixide.Compliance.Conversion.t()} | {:error, PdfElixide.Error.t()}
```

Converts the editor's document to the PDF/A level `standard`.

Returns `{:ok, conversion}` whether or not the result complies; see
`PdfElixide.Compliance.Conversion` and
the [PDF/A conversion](guides/pdf-a-conversion.md) guide. The editor is
changed in place, and a full write produces the converted document. A
document declaring a different PDF/A level, or existing XMP metadata for a
PDF/A-1 target, is among the refusals the guide lists; each returns
`{:error, %PdfElixide.Error{reason: :unsupported}}` and leaves the editor
unchanged. A standard that is not a `t:pdf_a/0` raises `ArgumentError`.

## Options

  * `:embed_fonts` — embed fonts the document uses without carrying them,
    using fonts installed on this machine. Output can vary by machine; see
    the conversion guide before enabling it. Defaults to `false`.
  * `:remove_javascript` — remove document JavaScript and JavaScript
    actions, which PDF/A forbids. Defaults to `true`.
  * `:remove_embedded_files` — remove attached files, which a PDF/A-1 or
    PDF/A-2 report lists as a violation. They are kept for a PDF/A-3 target
    whatever this says. Defaults to `true`.
  * `:icc_profile` — the ICC profile to embed in the output intent, as the
    profile's bytes. Defaults to `nil`, which embeds an sRGB profile.
    Supplying one for a document that already has an output intent returns
    `{:error, %PdfElixide.Error{reason: :unsupported}}` and converts nothing.
    Profiles are embedded without validation and identified as sRGB, so
    supply an RGB profile.

# `convert!`

```elixir
@spec convert!(PdfElixide.Editor.t(), pdf_a(), convert_opts()) ::
  PdfElixide.Compliance.Conversion.t()
```

Converts the editor's document to the PDF/A level `standard`, raising an
error if the conversion cannot be carried out.

A conversion that finishes without achieving compliance is returned like any
other, not raised; check `conversion.report.compliant?`.

# `validate`

```elixir
@spec validate(PdfElixide.Document.t(), standard()) ::
  {:ok, PdfElixide.Compliance.Report.t()} | {:error, PdfElixide.Error.t()}
```

Validates the document against `standard`.

Returns `{:ok, report}` whether or not the document complies; see
`PdfElixide.Compliance.Report`. An atom that is not a `t:standard/0` raises
`ArgumentError`. An encrypted document opened without its password returns
`{:error, %PdfElixide.Error{reason: :encrypted}}` until
`PdfElixide.Document.authenticate/2` succeeds.

# `validate!`

```elixir
@spec validate!(PdfElixide.Document.t(), standard()) ::
  PdfElixide.Compliance.Report.t()
```

Validates the document against `standard`, raising an error if it cannot be
checked.

---

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