jsafa-pdf 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ import { type PathSegment, type Radii } from './box';
2
+ import type { Rect } from '../types';
3
+ /** Anything we can get pixels out of. */
4
+ export type ImageSource = HTMLImageElement | HTMLCanvasElement | SVGSVGElement;
5
+ export interface ImagePaint {
6
+ /** Where the image is drawn, after object-fit. May overflow the content box. */
7
+ draw: Rect;
8
+ /**
9
+ * Clip region in CSS px, or null when the image sits entirely inside its box
10
+ * with square corners. Needed for `object-fit: cover` and border-radius.
11
+ */
12
+ clip: PathSegment[] | null;
13
+ source: ImageSource;
14
+ opacity: number;
15
+ }
16
+ /** Natural (intrinsic) pixel size of a source, or null if not yet known. */
17
+ export declare function naturalSize(source: ImageSource): {
18
+ width: number;
19
+ height: number;
20
+ } | null;
21
+ /**
22
+ * Read a paintable image, or null when there is nothing to draw.
23
+ *
24
+ * Only the geometry is resolved here. Pixel bytes are fetched during emission,
25
+ * because that is async and capture must stay synchronous — layout must be read
26
+ * in one pass, before anything can change it.
27
+ */
28
+ export declare function readImage(element: Element, origin: DOMRect, radii: Radii): ImagePaint | null;
@@ -0,0 +1,17 @@
1
+ import type { Rect } from '../types';
2
+ export interface LinkPaint {
3
+ /**
4
+ * One rect per line box the anchor occupies — a link that wraps across lines
5
+ * needs an annotation per line, not one box spanning both.
6
+ */
7
+ rects: Rect[];
8
+ url: string;
9
+ }
10
+ /**
11
+ * Read a clickable link, or null when the anchor is not one.
12
+ *
13
+ * `javascript:` targets do nothing in a PDF, and a bare `#fragment` refers to
14
+ * page state that does not survive the export, so both are skipped rather than
15
+ * written as annotations that silently fail when clicked.
16
+ */
17
+ export declare function readLink(element: Element, origin: DOMRect): LinkPaint | null;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Wait until a subtree is safe to capture.
3
+ *
4
+ * Capture reads laid-out geometry in a single synchronous pass, so anything
5
+ * still loading is silently wrong rather than merely late: text measured before
6
+ * its webfont arrives gets fallback metrics, and an image that has not decoded
7
+ * reports no intrinsic size and is skipped entirely.
8
+ *
9
+ * Both failure modes produce a plausible-looking PDF, which is why this runs by
10
+ * default instead of being left to the caller to remember.
11
+ */
12
+ export declare function waitForAssets(target: Element | string): Promise<void>;
@@ -0,0 +1,32 @@
1
+ /** Colour with alpha, components in 0..1, ready for PDF. */
2
+ export interface Rgba {
3
+ r: number;
4
+ g: number;
5
+ b: number;
6
+ a: number;
7
+ }
8
+ /** The subset of computed style the emitter needs to paint a run. */
9
+ export interface RunStyle {
10
+ /** Font families in CSS fallback order, unquoted. */
11
+ families: string[];
12
+ /** Font size in CSS px. */
13
+ sizePx: number;
14
+ weight: number;
15
+ italic: boolean;
16
+ color: Rgba;
17
+ }
18
+ export declare const TRANSPARENT: Rgba;
19
+ /**
20
+ * Parse a computed colour. `getComputedStyle` always resolves to `rgb()` /
21
+ * `rgba()`, so names and hex never reach here.
22
+ */
23
+ export declare function parseRgba(value: string): Rgba;
24
+ /**
25
+ * Resolve any CSS colour string via the browser.
26
+ *
27
+ * Hand-parsing would mean reimplementing named colours, `hsl()`, `color-mix()`
28
+ * and everything else CSS keeps adding; letting the engine normalise it costs
29
+ * one throwaway element and is always right.
30
+ */
31
+ export declare function cssColorToRgba(value: string): Rgba;
32
+ export declare function readRunStyle(style: CSSStyleDeclaration): RunStyle;
@@ -0,0 +1,12 @@
1
+ import type { TextRun } from '../types';
2
+ /**
3
+ * Read back how the browser laid out a text node.
4
+ *
5
+ * This is the load-bearing primitive of the library. We do not decide where
6
+ * lines break or where bidi runs start — the engine already did, and this
7
+ * recovers those decisions as positioned, paintable runs.
8
+ *
9
+ * A line of mixed Arabic/Latin/digits yields one run per directional segment,
10
+ * each already in correct visual order.
11
+ */
12
+ export declare function extractTextRuns(node: Text): TextRun[];
@@ -0,0 +1,65 @@
1
+ /**
2
+ * CSS transform support.
3
+ *
4
+ * A transformed element defeats the usual approach outright: `getClientRects`
5
+ * reports axis-aligned bounding boxes of *already rotated* glyphs, so a rotated
6
+ * label decomposes into one run per character, each landing in its own vertical
7
+ * band. Painting those directly produces scattered, unrotated text.
8
+ *
9
+ * The way out is that **CSS transforms are a paint-time effect — they do not
10
+ * affect layout**. Nothing else on the page moves when a transform is removed.
11
+ * So capture neutralises them, reads the clean untransformed geometry, and
12
+ * re-applies each transform as a PDF matrix at paint time. Text stays a single
13
+ * run and stays selectable.
14
+ */
15
+ /** 2D affine `[a, b, c, d, e, f]`, mapping (x,y) → (ax+cy+e, bx+dy+f). */
16
+ export type Affine = [number, number, number, number, number, number];
17
+ export declare const IDENTITY: Affine;
18
+ /** Parse a computed `transform` value. Returns null for `none` or identity. */
19
+ export declare function parseTransform(value: string): Affine | null;
20
+ /** `transform-origin` in px, relative to the element's border box. */
21
+ export declare function parseOrigin(value: string): {
22
+ x: number;
23
+ y: number;
24
+ };
25
+ /** Apply `inner` first, then `outer`. */
26
+ export declare function compose(outer: Affine, inner: Affine): Affine;
27
+ /**
28
+ * Express an element's transform in container coordinates.
29
+ *
30
+ * CSS applies the matrix about `transform-origin`, i.e. translate(O) · M ·
31
+ * translate(−O), so the origin has to be folded into the translation.
32
+ */
33
+ export declare function aboutOrigin(matrix: Affine, originX: number, originY: number): Affine;
34
+ interface Entry {
35
+ element: HTMLElement | SVGElement;
36
+ matrix: Affine;
37
+ originRaw: string;
38
+ previousInline: string;
39
+ }
40
+ export interface SuspendedTransforms {
41
+ /** Elements that carried a transform, with the data needed to rebuild it. */
42
+ entries: Entry[];
43
+ /** Put every inline style back exactly as it was. */
44
+ restore(): void;
45
+ }
46
+ /**
47
+ * Temporarily remove every transform in a subtree.
48
+ *
49
+ * Safe because transforms never participate in layout — sibling and ancestor
50
+ * geometry is unaffected, so what capture measures afterwards is the same
51
+ * layout the browser already computed, minus the paint-time distortion.
52
+ */
53
+ export declare function suspendTransforms(root: Element): SuspendedTransforms;
54
+ /**
55
+ * Build a lookup from element to its composed transform, in container
56
+ * coordinates. Must be called while transforms are suspended, because the
57
+ * origin depends on the element's untransformed layout box.
58
+ */
59
+ export declare function buildTransformMap(suspended: SuspendedTransforms, origin: DOMRect): Map<Element, Affine>;
60
+ /**
61
+ * Compose the transforms of an element and all its transformed ancestors,
62
+ * outermost first. Cached, since most elements have no transform at all.
63
+ */
64
+ export declare function composedTransform(element: Element, own: Map<Element, Affine>, root: Element, cache: Map<Element, Affine | null>): Affine | null;
65
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { ImageSource } from '../capture/image';
2
+ /** PDF can embed JPEG and PNG directly; everything else must be rasterised. */
3
+ export interface ResolvedImage {
4
+ kind: 'jpg' | 'png';
5
+ bytes: Uint8Array;
6
+ }
7
+ /**
8
+ * Get embeddable bytes for an image.
9
+ *
10
+ * Fetching the original is preferred: a JPEG can be embedded byte-for-byte as
11
+ * DCTDecode with no re-encode, which keeps both quality and file size. Only if
12
+ * that fails, or the format is not one PDF understands, do we rasterise.
13
+ */
14
+ export declare function resolveImage(source: ImageSource, cssWidth: number, cssHeight: number): Promise<ResolvedImage>;
@@ -0,0 +1,98 @@
1
+ import type { CapturedDocument, PaintOp } from '../capture/document';
2
+ /** Page dimensions in PDF points (1/72 inch), portrait. */
3
+ export declare const PAGE_SIZES: {
4
+ readonly A3: readonly [841.89, 1190.55];
5
+ readonly A4: readonly [595.28, 841.89];
6
+ readonly A5: readonly [419.53, 595.28];
7
+ readonly Letter: readonly [612, 792];
8
+ readonly Legal: readonly [612, 1008];
9
+ };
10
+ export type PageSizeName = keyof typeof PAGE_SIZES;
11
+ export interface Margin {
12
+ top: number;
13
+ right: number;
14
+ bottom: number;
15
+ left: number;
16
+ }
17
+ /** A line of text drawn in the bottom margin of every page. */
18
+ export interface FooterOptions {
19
+ /** Called once per page; return the line to draw. */
20
+ text: (info: {
21
+ page: number;
22
+ pages: number;
23
+ }) => string;
24
+ /** Family to render with. Must be registered or declared via `@font-face`. */
25
+ fontFamily?: string;
26
+ /** Points. Default 9. */
27
+ fontSize?: number;
28
+ /** Any CSS colour string. Default mid grey. */
29
+ color?: string;
30
+ align?: 'left' | 'center' | 'right';
31
+ }
32
+ export interface PageOptions {
33
+ /** Named size or explicit `[width, height]` in points. */
34
+ size?: PageSizeName | [number, number];
35
+ orientation?: 'portrait' | 'landscape';
36
+ /** Points. Use the `mm` / `cm` / `inch` helpers for readability. */
37
+ margin?: number | Partial<Margin>;
38
+ /**
39
+ * `width` scales the captured element to fill the content width — the usual
40
+ * "fit to page" behaviour. `none` keeps 1 CSS px = 0.75pt, so wide content
41
+ * runs off the page.
42
+ */
43
+ fit?: 'width' | 'none';
44
+ /**
45
+ * Honour `break-inside: avoid` by moving a page boundary up rather than
46
+ * cutting through the element. Default true.
47
+ */
48
+ avoidBreakInside?: boolean;
49
+ /**
50
+ * Repeat `display: table-header-group` rows at the top of continuation pages.
51
+ * Default true.
52
+ */
53
+ repeatTableHeaders?: boolean;
54
+ footer?: FooterOptions;
55
+ }
56
+ /**
57
+ * How CSS pixels map into one page's PDF coordinates.
58
+ *
59
+ * `drawSvgPath` maps a path point (px, py) to (anchorX + px·scale,
60
+ * anchorY − py·scale), so a single anchor plus scale positions every kind of
61
+ * mark — text, paths and images alike — including the page's own vertical
62
+ * offset into the document.
63
+ */
64
+ export interface Placement {
65
+ anchorX: number;
66
+ anchorY: number;
67
+ /** Combined CSS px → PDF pt factor, including any fit scaling. */
68
+ scale: number;
69
+ }
70
+ /** A table header redrawn at the top of a continuation page. */
71
+ export interface RepeatedBlock {
72
+ headerIndex: number;
73
+ ops: PaintOp[];
74
+ placement: Placement;
75
+ }
76
+ export interface PlannedPage {
77
+ widthPt: number;
78
+ heightPt: number;
79
+ placement: Placement;
80
+ /** Content band for this page, in CSS px of the captured document. */
81
+ bandTopPx: number;
82
+ bandHeightPx: number;
83
+ ops: PaintOp[];
84
+ /** Headers redrawn above this page's band content. */
85
+ repeated: RepeatedBlock[];
86
+ margin: Margin;
87
+ footer?: FooterOptions;
88
+ }
89
+ /**
90
+ * Split a captured document into pages.
91
+ *
92
+ * Layout is sliced rather than re-flowed, so line breaks stay exactly where the
93
+ * browser put them. Page boundaries are nevertheless *chosen*: they snap above
94
+ * anything marked `break-inside: avoid`, and continuation pages reserve room for
95
+ * a repeated table header. That covers what a tabular report actually needs
96
+ * without re-running layout.
97
+ */
98
+ export declare function planPages(captured: CapturedDocument, options: PageOptions): PlannedPage[];
@@ -0,0 +1,14 @@
1
+ import type { CapturedDocument } from '../capture/document';
2
+ import { type PageOptions } from './page';
3
+ export interface EmitOptions {
4
+ /** Subset embedded fonts. Shrinks output; not supported by every font file. */
5
+ subset?: boolean;
6
+ title?: string;
7
+ /**
8
+ * Paginate onto fixed-size pages. Omit to emit a single page sized exactly to
9
+ * the captured element.
10
+ */
11
+ page?: PageOptions;
12
+ }
13
+ /** Turn a captured document into PDF bytes. */
14
+ export declare function emitPdf(captured: CapturedDocument, options?: EmitOptions): Promise<Uint8Array>;
@@ -0,0 +1,23 @@
1
+ export interface DiscoveredFont {
2
+ family: string;
3
+ weight: number;
4
+ style: 'normal' | 'italic';
5
+ url: string;
6
+ ok: boolean;
7
+ error?: string;
8
+ }
9
+ /**
10
+ * Register every `@font-face` the page declares.
11
+ *
12
+ * Without this, a document that already declares its fonts in CSS still has to
13
+ * repeat every one of them through `registerFont`, which is both tedious and
14
+ * easy to get subtly wrong. Reading the stylesheets means the PDF embeds exactly
15
+ * the files the browser rendered with.
16
+ *
17
+ * Failures are per-font and non-fatal: one unreachable file should not stop a
18
+ * document from being produced. Cross-origin stylesheets throw on `cssRules`
19
+ * access and are skipped, since their bytes are unreachable anyway.
20
+ */
21
+ export declare function registerFontsFromCss(doc?: Document): Promise<DiscoveredFont[]>;
22
+ /** Forget which @font-face rules were already processed. */
23
+ export declare function clearDiscoveredFonts(): void;
@@ -0,0 +1,39 @@
1
+ import { StandardFonts } from 'pdf-lib';
2
+ import type { RunStyle } from '../capture/style';
3
+ export interface FontSource {
4
+ /** CSS family name this font satisfies, e.g. "Noto Sans Arabic". */
5
+ family: string;
6
+ /** Font file bytes, or a URL / ArrayBuffer to load them from. */
7
+ src: string | ArrayBuffer | Uint8Array;
8
+ weight?: number;
9
+ style?: 'normal' | 'italic';
10
+ }
11
+ /**
12
+ * Make a font available for embedding.
13
+ *
14
+ * This is not optional polish — it is how the library gets glyphs. JavaScript
15
+ * cannot read arbitrary system font files, so any family that is not a PDF
16
+ * standard font must be registered (or reachable as a fetchable @font-face)
17
+ * before it can appear in a PDF. Notably, **any non-Latin script needs this**:
18
+ * the 14 standard PDF fonts have no Arabic coverage at all.
19
+ */
20
+ export declare function registerFont(source: FontSource): Promise<void>;
21
+ /** Forget all registered fonts. */
22
+ export declare function clearFonts(): void;
23
+ export declare function registeredFamilies(): string[];
24
+ export type FontChoice = {
25
+ kind: 'registered';
26
+ key: string;
27
+ bytes: Uint8Array;
28
+ } | {
29
+ kind: 'standard';
30
+ key: string;
31
+ name: StandardFonts;
32
+ };
33
+ /**
34
+ * Decide which font will paint a run. A registered face wins only if it can
35
+ * actually draw the text — see {@link matchRegistered}.
36
+ */
37
+ export declare function resolveFont(style: RunStyle, text: string): FontChoice;
38
+ /** True when the text needs glyphs the standard PDF fonts cannot provide. */
39
+ export declare function needsEmbeddedFont(text: string): boolean;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * CSS reference pixels are 1/96 inch; PDF user-space units are 1/72 inch.
3
+ * Every dimension crossing the boundary goes through here.
4
+ */
5
+ export declare const PX_TO_PT: number;
6
+ export declare const pxToPt: (px: number) => number;
7
+ export declare const ptToPx: (pt: number) => number;
8
+ /** PDF points are 1/72 inch. These make page margins readable at call sites. */
9
+ export declare const mm: (value: number) => number;
10
+ export declare const cm: (value: number) => number;
11
+ export declare const inch: (value: number) => number;
12
+ /**
13
+ * CSS space is y-down from the top-left; PDF space is y-up from the bottom-left.
14
+ *
15
+ * @param yPx distance from the top of the page, in CSS px
16
+ * @param heightPx total page height, in CSS px
17
+ * @returns distance from the bottom of the page, in PDF pt
18
+ */
19
+ export declare const flipY: (yPx: number, heightPx: number) => number;
@@ -0,0 +1,38 @@
1
+ import { type EmitOptions } from './emit/pdf';
2
+ export interface ToPdfOptions extends EmitOptions {
3
+ /**
4
+ * Register `@font-face` fonts declared on the page automatically. Default
5
+ * true — without it, every font has to be repeated through `registerFont`.
6
+ */
7
+ autoFonts?: boolean;
8
+ /**
9
+ * Wait for webfonts and images before reading layout. Default true. Turning
10
+ * this off is only safe when everything is known to be loaded already.
11
+ */
12
+ waitForAssets?: boolean;
13
+ }
14
+ /** Convert an element to PDF bytes. Text stays real text: selectable and copyable. */
15
+ export declare function toPdf(target: Element | string, options?: ToPdfOptions): Promise<Uint8Array>;
16
+ /** Convert an element to a PDF Blob. */
17
+ export declare function toBlob(target: Element | string, options?: ToPdfOptions): Promise<Blob>;
18
+ /** Convert an element to PDF and save it to the user's downloads. */
19
+ export declare function download(target: Element | string, filename?: string, options?: ToPdfOptions): Promise<void>;
20
+ /** Open the PDF in a new tab instead of saving it. */
21
+ export declare function open(target: Element | string, options?: ToPdfOptions): Promise<Window | null>;
22
+ export { registerFont, clearFonts, registeredFamilies, type FontSource, } from './fonts/registry';
23
+ export { registerFontsFromCss, clearDiscoveredFonts, type DiscoveredFont, } from './fonts/autodiscover';
24
+ export { PAGE_SIZES, type PageOptions, type PageSizeName, type Margin, type FooterOptions, } from './emit/page';
25
+ export { mm, cm, inch } from './geom/units';
26
+ export { captureElement, type CapturedDocument, type CapturedRun, type PaintOp } from './capture/document';
27
+ export { waitForAssets } from './capture/ready';
28
+ export { extractTextRuns } from './capture/text';
29
+ export type { LinkPaint } from './capture/link';
30
+ export type { Affine } from './capture/transform';
31
+ export { clearBaselineCache } from './capture/baseline';
32
+ export { planPages, type Placement, type PlannedPage } from './emit/page';
33
+ export { emitPdf, type EmitOptions } from './emit/pdf';
34
+ export { pxToPt, ptToPx } from './geom/units';
35
+ export type { TextRun, Rect, Direction } from './types';
36
+ export type { RunStyle, Rgba } from './capture/style';
37
+ export type { BoxPaint, BorderEdge, Radii } from './capture/box';
38
+ export type { ImagePaint, ImageSource } from './capture/image';