Documentation

Service & preview

Setup

provideJasy() is optional. Without it, inject(JasyPdf) works with the standard fonts and the engine's defaults. It belongs in app.config.ts: jasy is one service for the whole app, so provideJasy() in a lazy route's providers does not change it.

// app.config.ts
import { provideJasy } from "@jasy/angular";

export const appConfig: ApplicationConfig = {
  providers: [
    provideJasy({
      // Every document can use these by name: Text("...", { font: "Inter" }).
      fonts: {
        Inter: { normal: "/fonts/Inter-Regular.woff2", bold: "/fonts/Inter-Bold.woff2" },
      },
      // Applied to every PDF; the options of a single call win.
      defaults: { accessible: true },
      // Load the font data once the app is idle, so the first PDF does not wait for it.
      preload: true,
    }),
  ],
};
OptionWhat it does
fontsfonts by name: a URL or bytes, or a family { normal, bold?, italic?, boldItalic? }
defaultsrender options for every PDF, such as accessible or encrypt
preloadload the font data once the app has started and the browser is idle, instead of with the first PDF

A font URL is loaded the first time a PDF needs it and kept for the app's lifetime. .ttf, .woff and .woff2 all work. If a request fails, the next PDF tries again.

A URL, here and for a form, goes through Angular's HttpClient, so your interceptors apply: an auth header, a base URL. From Angular 21 there always is one; on 20 without provideHttpClient(), jasy uses fetch, and a 401 or 403 says that this is why.

A document

PdfDocumentFn<T> is the type of a document: (data: T) => Document(...), sync or async. PdfDocumentFn without a type argument is a document that takes no data.

A document runs in an injection context, like functional guards and resolvers. inject(LOCALE_ID) gives you the app's language, and inject() reaches your services. In a pdfResource, it runs in the injector of its component, so it also sees what the component and its route provide. A tagged PDF takes its language from LOCALE_ID too.

inject(JasyPdf)

MethodDoes
render(document, data, options?)The PDF bytes, a Uint8Array. Browser and server.
blob(document, data, options?)The PDF as a Blob, ready for an upload.
download(document, data, filename?, options?)Saves the PDF in the browser.
open(document, data, options?)Opens the PDF in a new tab, from a click handler.
download(bytes, filename?) · open(bytes)The same for a finished PDF, such as a filled form.
preload()Loads the font data now, e.g. on a hover, so the next PDF does not wait.

options are the engine's render options: { encrypt: { userPassword } } for AES-256, { accessible: true } for a tagged PDF, onDiagnostic for what the engine has to say. They are merged over the defaults of provideJasy.

A document without data needs none: render(doc), download(doc, undefined, "a.pdf"). Called on the service, a document runs in the app's root injector.

Two errors you can catch by name:

  • JasyBrowserOnlyError - download() and open() on the server, where there is no tab to save into. Use render() there.
  • JasyPopupBlockedError - the browser blocked the new tab, because open() was not called straight from a click. Your app stays where it is; download() always works.

pdfResource()

pdfResource(document, data?, { options?, injector? })

A PDF that follows your signals. It is an Angular ResourceRef - value(), isLoading(), error(), reload() - plus latest(), the last PDF that rendered.

  • data is read like resource({ params }): when a signal in it changes, the PDF renders again, and a result that a newer one replaced is dropped. Return undefined to wait, for example until an input has a value. A document without data takes none: pdfResource(doc).
  • options can be a function, and it is tracked like data. A signal in it renders the PDF again.
  • injector is for a call outside an injection context.

A rendered PDF holds its bytes, its blob: URL as url, and safeUrl, which goes straight into <iframe [src]>. The old URL is freed when a new one replaces it, and the last one when the component is destroyed.

Bind a preview to latest(), not to value(). Angular empties value() whenever the data changes, so an <iframe> behind hasValue() would be removed and created again on every keystroke, and lose its scroll position and zoom. latest() keeps the last PDF while the next one renders, and when one fails:

@if (preview.latest(); as pdf) {
<iframe [src]="pdf.safeUrl" title="Preview"></iframe>
} @if (preview.isLoading()) {
<p>Rendering ...</p>
} @if (preview.error(); as error) {
<p>{{ error.message }}</p>
}

pdfResource() builds on Angular's resource(), which Angular marks experimental in 20 and 21 and stable from 22. The service does not depend on it.

On the server

render() and blob() work in Node as they do in the browser, so a server can produce the bytes and send them with the content type application/pdf. A pdfResource stays idle on the server, since a preview there has nobody to look at it; in the browser after hydration, it renders.

In an SSR app, ng add also allows the CommonJS modules of jimp, which the server uses for images.

Bundle size

@jasy/angular does not put the engine into your start bundle; your documents bring it, wherever they are imported. The font data (158 KB minified, 12 KB gzipped) loads with the first PDF, or earlier with preload.

So put the PDF feature behind @defer or a lazy route. A new Angular 20 app with provideJasy() then starts at 231 KB. Rendered straight in the root component, it starts at 522 KB, a little over Angular's default 500 KB warning.

@Component({
  selector: "app-billing",
  imports: [InvoicePreview],
  template: `
    @defer (on interaction) {
      <app-invoice-preview [invoice]="invoice()" />
    } @placeholder {
      <button>Show the invoice</button>
    }
  `,
})
export class Billing {
  readonly invoice = input.required<Invoice>();
}

Testing

JasyPdf and JasyForms are injection tokens like any other:

TestBed.configureTestingModule({
  providers: [{ provide: JasyPdf, useValue: { download: vi.fn() } }],
});