Skip to content
Lamina PDF
Unreleased

These docs describe changes that aren’t released yet. For the newest release, see the released docs.

A PDF/A archive

You’ll write a letter confirming an order as a PDF/A file, which looks the same in any viewer for decades. You need it for documents kept for years, often by law: contracts, records, letters to customers. The recipe writes the letter twice, at two PDF/A levels.

The function takes the level, 2b or 2u, and builds the same letter under that level’s profiles. In TypeScript it needs the text module, init({ text: true }). Both languages write the same bytes:

packages/sdk/examples/pdf-a-archive.ts
// An archival letter, written as PDF/A-2b and as PDF/A-2u that is also PDF/UA-1.
import { Builder, Font, Image, PAGE_SIZES, type Profile, type Surface } from "@laminapdf/sdk";
const MARGIN = 56;
const BAND_HEIGHT = 70;
const TITLE_BASELINE = 110;
/** The top of the letter's text and of the photo beside it, and the height of both. */
const BLOCK_TOP = 130;
const BLOCK_HEIGHT = 120;
const LETTER_WIDTH = 300;
const PHOTO_LEFT = 380;
const PHOTO_WIDTH = 160;
const FOOTER_BASELINE = 300;
const TITLE = 22;
const BODY = 11;
const BAND_OPACITY = 0.35;
const PHOTO_ALT = "A photograph of the sprockets, packed in a box";
const LETTER =
"Thank you for your order of 12 sprockets. This letter confirms the delivery date and the price we agreed. " +
"Keep it: PDF/A files look the same in any viewer, now and in the future.";
/** PDF/A-2b, or PDF/A-2u (whose text maps to Unicode) with PDF/UA-1. */
const PROFILES: Record<"2b" | "2u", Profile[]> = {
"2b": ["PDF/A-2b"],
"2u": ["PDF/A-2u", "PDF/UA-1"],
};
/**
* The letter's bytes. Needs `init({ text: true })`.
*
* @param level - `2b` or `2u`.
* @param notoSans - the bytes of the Noto Sans font file.
* @param photo - the bytes of a JPEG photo.
*/
export function pdfAArchive(level: "2b" | "2u", notoSans: Uint8Array, photo: Uint8Array): Uint8Array<ArrayBuffer> {
using b = new Builder({ profiles: PROFILES[level], tagged: true });
b.metadata({ title: "Order confirmation 42", author: "Acme Widgets Ltd" });
b.lang("en-GB");
const font = Font.fromBytes(notoSans);
b.font(font);
const page = b.page("A4");
draw(page.surface(), font, Image.jpeg(photo));
page.finish();
using doc = b.finish();
return doc.save();
}
function draw(s: Surface, font: Font, photo: Image): void {
// A translucent band across the top: decoration, and transparency, which PDF/A-2 allows.
const [width] = PAGE_SIZES.A4;
s.artifact("layout", (s) => {
s.save().opacity(BAND_OPACITY, 1).fillColor([0.12, 0.47, 0.78]);
s.path((p) => p.rect([0, 0, width, BAND_HEIGHT])).fill();
s.restore();
});
s.tag("H1", (s) => s.text("Order confirmation 42", { font, size: TITLE, at: [MARGIN, TITLE_BASELINE] }));
s.tag("P", (s) => s.textBox(LETTER, { font, size: BODY, box: [MARGIN, BLOCK_TOP, LETTER_WIDTH, BLOCK_HEIGHT] }));
s.image(photo, [PHOTO_LEFT, BLOCK_TOP, PHOTO_WIDTH, BLOCK_HEIGHT], PHOTO_ALT);
s.tag("P", (s) =>
s.text("Acme Widgets Ltd, 1 Example Street, London", { font, size: BODY, at: [MARGIN, FOOTER_BASELINE] }),
);
}

The result is one A4 letter under a translucent blue band, with a photo beside its text: open the PDF/A-2b or the PDF/A-2u reference PDF.

  • PDF/A-2b makes the letter look the same everywhere. PDF/A-2u also maps every glyph to Unicode, so the text can be searched and copied. That copy is PDF/UA-1 too, so a screen reader can follow it.
  • PDF/A needs every font embedded, so the letter uses Noto Sans rather than one of the standard 14 fonts. finish() checks rules like this one and fails with a coded error for the first one the letter breaks.
  • The builder writes what PDF/A asks for: the PDF/A identification in the file’s XMP metadata, and an sRGB output intent that says what its RGB colours mean. See What PDF/A asks for.
  • Both copies draw the same tagged content: an H1 title, P paragraphs and the photo as a figure with alternate text. PDF/A-2b doesn’t ask for tags, but PDF/UA-1 does.
  • The blue band is drawn at 35% opacity, as a layout artifact that screen readers skip. PDF/A-2 allows transparency.
  • textBox breaks the letter into lines inside its box, beside the photo. The JPEG goes into the file as it is.
  • The letter: change LETTER. textBox leaves out lines that don’t fit, so make BLOCK_HEIGHT larger for a longer letter, or pass overflow: "error" to fail with text-overflow instead.
  • The photo: pass any RGB or gray JPEG, or Image.png(bytes) for a PNG (Image::png in Rust). A CMYK JPEG fails finish() with device-color-without-output-intent, because the output intent is sRGB.
  • Other profiles: pass the ones you need, such as ["PDF/A-2u"] alone or ["PDF/A-2b", "PDF/UA-1"]. A document can claim PDF/UA-1 and at most one PDF/A profile.
  • An attached file: PDF/A-2 refuses attachments, so switch to PDF/A-3b, as the PDF/A-3 invoice does.

Next: a PDF/A-3 invoice with XML, with its data attached for software to read.