@jasy/pdf 1.0.0-alpha.8 → 1.0.0-alpha.9

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.
Files changed (97) hide show
  1. package/README.md +4 -3
  2. package/dist/api/content.d.ts +5 -4
  3. package/dist/api/content.js +13 -7
  4. package/dist/api/dimension.d.ts +56 -0
  5. package/dist/api/dimension.js +27 -0
  6. package/dist/api/font-url.d.ts +28 -0
  7. package/dist/api/font-url.js +134 -0
  8. package/dist/api/gradient.d.ts +53 -0
  9. package/dist/api/gradient.js +97 -0
  10. package/dist/api/index.d.ts +2 -0
  11. package/dist/api/index.js +2 -0
  12. package/dist/api/insets.d.ts +19 -12
  13. package/dist/api/insets.js +18 -7
  14. package/dist/api/layout.d.ts +20 -6
  15. package/dist/api/layout.js +31 -6
  16. package/dist/api/page-builder.d.ts +8 -0
  17. package/dist/api/page-builder.js +12 -0
  18. package/dist/api/structure.d.ts +5 -0
  19. package/dist/api/structure.js +16 -4
  20. package/dist/api/text.d.ts +42 -6
  21. package/dist/api/text.js +29 -5
  22. package/dist/edit/document.d.ts +14 -4
  23. package/dist/edit/document.js +15 -22
  24. package/dist/edit/fill.d.ts +19 -2
  25. package/dist/edit/fill.js +63 -34
  26. package/dist/edit/flatten.d.ts +13 -1
  27. package/dist/edit/flatten.js +38 -13
  28. package/dist/edit/index.d.ts +2 -0
  29. package/dist/edit/index.js +3 -1
  30. package/dist/edit/objects.d.ts +5 -0
  31. package/dist/edit/objects.js +27 -0
  32. package/dist/edit/writer.d.ts +7 -0
  33. package/dist/edit/writer.js +23 -1
  34. package/dist/elements/container-element.d.ts +21 -1
  35. package/dist/elements/container-element.js +25 -10
  36. package/dist/elements/image-element.d.ts +18 -4
  37. package/dist/elements/image-element.js +27 -12
  38. package/dist/elements/layout/expanded-element.d.ts +3 -1
  39. package/dist/elements/layout/expanded-element.js +12 -3
  40. package/dist/elements/layout/padding-element.d.ts +4 -2
  41. package/dist/elements/layout/padding-element.js +4 -2
  42. package/dist/elements/layout/page-builder-element.js +2 -0
  43. package/dist/elements/pdf-element.d.ts +59 -1
  44. package/dist/elements/pdf-element.js +62 -1
  45. package/dist/elements/rectangle-element.d.ts +20 -6
  46. package/dist/elements/rectangle-element.js +25 -12
  47. package/dist/elements/row-element.d.ts +24 -1
  48. package/dist/elements/row-element.js +25 -10
  49. package/dist/elements/text-element.d.ts +60 -1
  50. package/dist/elements/text-element.js +132 -33
  51. package/dist/ir/display-list.d.ts +23 -6
  52. package/dist/ir/display-list.js +8 -1
  53. package/dist/layout/alignment.d.ts +10 -0
  54. package/dist/layout/alignment.js +6 -0
  55. package/dist/layout/box-constraints.d.ts +53 -0
  56. package/dist/layout/box-constraints.js +72 -0
  57. package/dist/layout/insets.d.ts +28 -0
  58. package/dist/layout/insets.js +20 -0
  59. package/dist/renderer/image-renderer.js +2 -1
  60. package/dist/renderer/pdf-backend.d.ts +6 -0
  61. package/dist/renderer/pdf-backend.js +78 -31
  62. package/dist/renderer/pdf-document-renderer.js +8 -1
  63. package/dist/renderer/rectangle-renderer.js +8 -2
  64. package/dist/renderer/text-renderer.d.ts +2 -2
  65. package/dist/renderer/text-renderer.js +150 -42
  66. package/dist/text/advance.js +5 -2
  67. package/dist/text/arabic.d.ts +20 -0
  68. package/dist/text/arabic.js +101 -0
  69. package/dist/text/bidi.d.ts +33 -0
  70. package/dist/text/bidi.js +94 -0
  71. package/dist/text/font-fallback.d.ts +25 -0
  72. package/dist/text/font-fallback.js +38 -0
  73. package/dist/text/line-breaker.d.ts +35 -1
  74. package/dist/text/line-breaker.js +76 -16
  75. package/dist/text/orphans-widows.d.ts +27 -0
  76. package/dist/text/orphans-widows.js +53 -0
  77. package/dist/text/shape.d.ts +29 -0
  78. package/dist/text/shape.js +67 -0
  79. package/dist/text/text-style.d.ts +25 -0
  80. package/dist/text/text-style.js +44 -1
  81. package/dist/utils/flex-layout.d.ts +24 -4
  82. package/dist/utils/flex-layout.js +169 -7
  83. package/dist/utils/font-metrics.d.ts +5 -0
  84. package/dist/utils/gsub.d.ts +36 -0
  85. package/dist/utils/gsub.js +178 -0
  86. package/dist/utils/inflate.d.ts +16 -0
  87. package/dist/utils/inflate.js +52 -0
  88. package/dist/utils/pdf-object-manager.d.ts +21 -0
  89. package/dist/utils/pdf-object-manager.js +107 -9
  90. package/dist/utils/ttf-parser.d.ts +6 -0
  91. package/dist/utils/ttf-parser.js +19 -0
  92. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  93. package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
  94. package/dist/utils/woff.d.ts +10 -0
  95. package/dist/utils/woff.js +116 -0
  96. package/dist/validators/element-validator.js +9 -2
  97. package/package.json +2 -1
@@ -11,10 +11,11 @@ import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
11
11
  import { PDFElement } from "../elements/pdf-element.js";
12
12
  import { MainAlign, CrossAlign } from "../utils/flex-layout.js";
13
13
  import { ColorInput } from "./color.js";
14
+ import { type GradientInput } from "./gradient.js";
14
15
  import { Insets } from "./insets.js";
15
- import { SizeInput } from "./dimension.js";
16
+ import { BoundsInput, SizeInput, RadiusInput } from "./dimension.js";
16
17
  /** Options shared by the `Column` and `Row` stacks (locked §4). */
17
- export interface StackOptions {
18
+ export interface StackOptions extends BoundsInput {
18
19
  /** Space inserted between children, in points. */
19
20
  gap?: number;
20
21
  /** Distribution along the main axis (CSS `justify-content`): start (default) · center · end ·
@@ -35,6 +36,13 @@ export interface StackOptions {
35
36
  /** Try to keep this stack on one page (CSS `break-inside: avoid`). If it does not fit here but would
36
37
  * fit on a fresh page, it moves there whole; if it is taller than a whole page, it splits anyway. */
37
38
  keepTogether?: boolean;
39
+ /** Lay the children out backwards along the axis (CSS `row-reverse` / `column-reverse`). */
40
+ reverse?: boolean;
41
+ /** Let the children flow onto further lines when they do not fit (CSS `flex-wrap: wrap`). The `gap`
42
+ * is used BETWEEN the lines as well as between the items. */
43
+ wrap?: boolean;
44
+ /** How the block of wrapped lines sits across the axis (CSS `align-content`). Default `start`. */
45
+ alignContent?: MainAlign;
38
46
  }
39
47
  /** A vertical stack. `Column(children)` or `Column(opts, children)`. */
40
48
  export declare function Column(children: PDFElement[]): PDFElement;
@@ -50,7 +58,7 @@ export declare function Row(opts: StackOptions, children: PDFElement[]): PDFElem
50
58
  */
51
59
  export declare function keepTogether(children: PDFElement[]): KeepTogetherElement;
52
60
  /** A bordered / filled box that wraps its children (locked §4). */
53
- export interface BoxOptions {
61
+ export interface BoxOptions extends BoundsInput {
54
62
  /** Border (stroke) color. A box has a border only when `border` or `borderWidth` is set. */
55
63
  border?: ColorInput;
56
64
  /** Per-side border colors - override/add to `border`. Any of these makes the box draw
@@ -62,8 +70,8 @@ export interface BoxOptions {
62
70
  borderLeft?: ColorInput;
63
71
  /** Border thickness in points (default 1 when a border is present). */
64
72
  borderWidth?: number;
65
- /** Background fill color. */
66
- bg?: ColorInput;
73
+ /** Background: a colour, or a gradient from `linearGradient()` / `radialGradient()`. */
74
+ bg?: ColorInput | GradientInput;
67
75
  /** Inner padding between the border and the children. */
68
76
  padding?: Insets;
69
77
  /** Size on each axis: a number of points (fixed) or a percentage string like `"50%"` (a fraction
@@ -72,7 +80,7 @@ export interface BoxOptions {
72
80
  width?: SizeInput;
73
81
  height?: SizeInput;
74
82
  /** Corner radius in points. */
75
- radius?: number;
83
+ radius?: RadiusInput;
76
84
  /** Make this box a positioning frame: `Positioned` children placed inside it resolve their
77
85
  * offsets against this box (CSS `position: relative`). */
78
86
  relative?: boolean;
@@ -173,6 +181,12 @@ export declare function PageBreak(): PageBreakElement;
173
181
  export interface ExpandedOptions {
174
182
  /** Share of the leftover space vs other flex children (default 1). */
175
183
  flex?: number;
184
+ /**
185
+ * Main-axis size reserved BEFORE the leftover is shared out (CSS `flex-basis`): points, or a
186
+ * percentage of what the line offers its items. The child ends up with `basis + its share of the
187
+ * rest`, so `flexBasis: 120` with `flex: 0` is simply a fixed 120pt slot. Default 0.
188
+ */
189
+ flexBasis?: SizeInput;
176
190
  }
177
191
  /**
178
192
  * Makes a child fill the leftover space along the stack's main axis (height in a Column,
@@ -12,14 +12,21 @@ import { AnchorElement } from "../elements/layout/anchor-element.js";
12
12
  import { RotatedElement } from "../elements/layout/rotated-element.js";
13
13
  import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
14
14
  import { toColor } from "./color.js";
15
+ import { isGradientInput } from "./gradient.js";
15
16
  import { toEdges } from "./insets.js";
16
- import { toDimension } from "./dimension.js";
17
+ import { toBounds, toDimension, toRadius, } from "./dimension.js";
17
18
  import { splitArgs } from "./args.js";
18
19
  /** Splits `StackOptions` width/height into the fixed points + fraction the stack elements expect. */
19
20
  function stackSize(opts) {
20
21
  const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
21
22
  const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
22
- return { width: w?.points, height: h?.points, widthFactor: w?.factor, heightFactor: h?.factor };
23
+ return {
24
+ width: w?.points,
25
+ height: h?.points,
26
+ widthFactor: w?.factor,
27
+ heightFactor: h?.factor,
28
+ ...toBounds(opts),
29
+ };
23
30
  }
24
31
  // The public default for `cross` is `start` (locked §5) - i.e. don't stretch a child unless
25
32
  // asked. The engine default is `stretch`; the factory sets the friendlier public one.
@@ -33,6 +40,9 @@ export function Column(a, b) {
33
40
  gap: opts.gap,
34
41
  main: opts.justify, // undefined → engine default `start` (matches §5)
35
42
  cross: opts.align ?? DEFAULT_CROSS,
43
+ reverse: opts.reverse,
44
+ wrap: opts.wrap,
45
+ alignContent: opts.alignContent,
36
46
  breakBefore: opts.breakBefore,
37
47
  breakAfter: opts.breakAfter,
38
48
  ...stackSize(opts),
@@ -45,6 +55,9 @@ export function Row(a, b) {
45
55
  gap: opts.gap,
46
56
  main: opts.justify,
47
57
  cross: opts.align ?? DEFAULT_CROSS,
58
+ reverse: opts.reverse,
59
+ wrap: opts.wrap,
60
+ alignContent: opts.alignContent,
48
61
  breakBefore: opts.breakBefore,
49
62
  breakAfter: opts.breakAfter,
50
63
  ...stackSize(opts),
@@ -53,7 +66,12 @@ export function Row(a, b) {
53
66
  /** Wraps `element` in a `keepTogether` group when the `keepTogether` option is set; otherwise returns it
54
67
  * unchanged (byte-identical). Shared by the `Box`/`Column`/`Row` prop shortcut. */
55
68
  function maybeKeepTogether(opts, element) {
56
- return opts.keepTogether ? new KeepTogetherElement({ child: element }) : element;
69
+ // alignSelf and order go on the OUTERMOST element - that is the one the surrounding stack places.
70
+ const wrapped = opts.keepTogether ? new KeepTogetherElement({ child: element }) : element;
71
+ return wrapped
72
+ .withAlignSelf(opts.alignSelf)
73
+ .withOrder(opts.order)
74
+ .withFlexShrink(opts.flexShrink);
57
75
  }
58
76
  /**
59
77
  * Keep a group of elements on ONE page (CSS `break-inside: avoid`). `keepTogether([...])` stacks the
@@ -92,13 +110,14 @@ export function Box(a, b) {
92
110
  y: 0,
93
111
  children: content,
94
112
  color: opts.border !== undefined ? toColor(opts.border) : undefined,
95
- backgroundColor: opts.bg !== undefined ? toColor(opts.bg) : undefined,
113
+ backgroundColor: opts.bg === undefined ? undefined : isGradientInput(opts.bg) ? opts.bg : toColor(opts.bg),
96
114
  borderWidth: hasBorder ? (opts.borderWidth ?? 1) : 0,
97
115
  width: w?.points,
98
116
  height: h?.points,
99
117
  widthFactor: w?.factor,
100
118
  heightFactor: h?.factor,
101
- radius: opts.radius,
119
+ ...toBounds(opts),
120
+ radius: opts.radius !== undefined ? toRadius(opts.radius) : undefined,
102
121
  sideBorders: hasPerSide
103
122
  ? {
104
123
  top: side(opts.borderTop),
@@ -201,5 +220,11 @@ export function Expanded(a, b) {
201
220
  const isOptsForm = b !== undefined;
202
221
  const opts = (isOptsForm ? a : {});
203
222
  const child = (isOptsForm ? b : a);
204
- return new ExpandedElement({ flex: opts.flex ?? 1, child });
223
+ const basis = opts.flexBasis !== undefined ? toDimension(opts.flexBasis) : undefined;
224
+ return new ExpandedElement({
225
+ flex: opts.flex ?? 1,
226
+ basis: basis?.points,
227
+ basisFactor: basis?.factor,
228
+ child,
229
+ });
205
230
  }
@@ -16,6 +16,10 @@ export type { PageInfo };
16
16
  * `pageNumber` is 1-based and counts PHYSICAL pages (an overflowing `Page` contributes several);
17
17
  * `pageCount` is the document total; `pageSize` is the media box in points.
18
18
  *
19
+ * `subPageNumber` / `subPageTotalPages` are the same counts WITHIN one logical `Page` element - what you
20
+ * want when a document is several sections and the footer should say "Attachment, page 1 of 4" beside
21
+ * "sheet 4 of 7". With a single `Page` they are identical to the document counts.
22
+ *
19
23
  * The numbers themselves are always exact. Two sizing caveats, both from the same chicken-and-egg (the
20
24
  * content decides the count that the content displays): in the flowing BODY the box is reserved before the
21
25
  * total is known, so a much wider final string can paint slightly past it; and a conditional header may
@@ -30,3 +34,7 @@ export interface PageNumberOptions extends TextOptions {
30
34
  export declare function PageNumber({ offset, ...style }?: PageNumberOptions): PageBuilderElement;
31
35
  /** The document's total page count as text. Sugar for `PageBuilder`. */
32
36
  export declare function PageCount({ offset, ...style }?: PageNumberOptions): PageBuilderElement;
37
+ /** The page number WITHIN its logical `Page` (a section), as text. Sugar for `PageBuilder`. */
38
+ export declare function SubPageNumber({ offset, ...style }?: PageNumberOptions): PageBuilderElement;
39
+ /** How many pages the surrounding logical `Page` produced, as text. Sugar for `PageBuilder`. */
40
+ export declare function SubPageCount({ offset, ...style }?: PageNumberOptions): PageBuilderElement;
@@ -14,6 +14,10 @@ import { Text } from "./text.js";
14
14
  * `pageNumber` is 1-based and counts PHYSICAL pages (an overflowing `Page` contributes several);
15
15
  * `pageCount` is the document total; `pageSize` is the media box in points.
16
16
  *
17
+ * `subPageNumber` / `subPageTotalPages` are the same counts WITHIN one logical `Page` element - what you
18
+ * want when a document is several sections and the footer should say "Attachment, page 1 of 4" beside
19
+ * "sheet 4 of 7". With a single `Page` they are identical to the document counts.
20
+ *
17
21
  * The numbers themselves are always exact. Two sizing caveats, both from the same chicken-and-egg (the
18
22
  * content decides the count that the content displays): in the flowing BODY the box is reserved before the
19
23
  * total is known, so a much wider final string can paint slightly past it; and a conditional header may
@@ -30,3 +34,11 @@ export function PageNumber({ offset = 0, ...style } = {}) {
30
34
  export function PageCount({ offset = 0, ...style } = {}) {
31
35
  return PageBuilder(({ pageCount }) => Text(String(pageCount + offset), style));
32
36
  }
37
+ /** The page number WITHIN its logical `Page` (a section), as text. Sugar for `PageBuilder`. */
38
+ export function SubPageNumber({ offset = 0, ...style } = {}) {
39
+ return PageBuilder(({ subPageNumber }) => Text(String(subPageNumber + offset), style));
40
+ }
41
+ /** How many pages the surrounding logical `Page` produced, as text. Sugar for `PageBuilder`. */
42
+ export function SubPageCount({ offset = 0, ...style } = {}) {
43
+ return PageBuilder(({ subPageTotalPages }) => Text(String(subPageTotalPages + offset), style));
44
+ }
@@ -7,6 +7,7 @@ import { PageSize } from "../constants/page-sizes.js";
7
7
  import { type EncryptOptions } from "../crypto/security-handler.js";
8
8
  export type { EncryptOptions, Permissions } from "../crypto/security-handler.js";
9
9
  import { StackOptions } from "./layout.js";
10
+ import { type UrlFontSource } from "./font-url.js";
10
11
  import { Insets } from "./insets.js";
11
12
  import { TextDefaults } from "./text.js";
12
13
  /** A custom page size. Use the `mm()` helper for millimetres, or pass points directly. */
@@ -78,6 +79,10 @@ export interface JasyDocument extends PDFDocumentElement {
78
79
  * path (read now, on Node), raw bytes, or a styled family. Re-adding a name overwrites it.
79
80
  * A registered font that no `Text` actually uses is dropped at render and costs nothing. */
80
81
  addFont(name: string, source: FontSource): this;
82
+ /** The same, from a URL. Async because the fetch happens NOW, exactly as `addFont` reads a path
83
+ * now - so a broken link fails where you asked for the font, not inside a later render.
84
+ * `await doc.addFontFromUrl("Inter", "https://…/Inter-Regular.ttf")`, or a styled family of URLs. */
85
+ addFontFromUrl(name: string, source: UrlFontSource): Promise<this>;
81
86
  /** The names of the registered fonts. */
82
87
  getFonts(): string[];
83
88
  /** Whether a font is registered under `name`. */
@@ -2,7 +2,8 @@ import { readFileBytes } from "../platform/node-fs.js";
2
2
  import { PDFDocumentElement } from "../elements/pdf-document-element.js";
3
3
  import { PageElement } from "../elements/page-element.js";
4
4
  import { DefaultTextStyleElement } from "../elements/layout/default-text-style-element.js";
5
- import { PageSize } from "../constants/page-sizes.js";
5
+ import { PageSize, pageFormats } from "../constants/page-sizes.js";
6
+ import { resolveEdges } from "../layout/insets.js";
6
7
  import { Orientation } from "../renderer/pdf-config.js";
7
8
  import { PDFDocument } from "../renderer/pdf-document-class.js";
8
9
  import { FontStyle } from "../utils/pdf-object-manager.js";
@@ -10,6 +11,7 @@ import { getArrayBuffer } from "../utils/utf8-to-windows1252-encoder.js";
10
11
  import { createSecurityHandler } from "../crypto/security-handler.js";
11
12
  import { uaXmp } from "../utils/ua-xmp.js";
12
13
  import { Column } from "./layout.js";
14
+ import { loadFontFromUrl } from "./font-url.js";
13
15
  import { toEdges } from "./insets.js";
14
16
  import { toTextStyleOverride } from "./text.js";
15
17
  const MM_TO_PT = 72 / 25.4; // 1 mm in PDF points
@@ -38,10 +40,16 @@ export function Page(a, b) {
38
40
  const isOpts = !Array.isArray(a);
39
41
  const opts = (isOpts ? a : {});
40
42
  const children = (isOpts ? (b ?? []) : a);
41
- const [top, right, bottom, left] = toEdges(opts.margin ?? DEFAULT_MARGIN);
43
+ const size = resolveSize(opts.size);
44
+ const landscape = opts.orientation === "landscape";
45
+ // A percentage margin resolves against the page WIDTH (see `Insets`), which is known right here -
46
+ // the page is the one box whose geometry does not depend on a parent.
47
+ const [w, h] = size.customSize ?? pageFormats[size.pageSize ?? PageSize.A4];
48
+ const pageWidth = landscape ? h : w;
49
+ const [top, right, bottom, left] = resolveEdges(toEdges(opts.margin ?? DEFAULT_MARGIN), pageWidth);
42
50
  const config = {
43
- ...resolveSize(opts.size),
44
- orientation: opts.orientation === "landscape" ? Orientation.landscape : Orientation.portrait,
51
+ ...size,
52
+ orientation: landscape ? Orientation.landscape : Orientation.portrait,
45
53
  margin: { top, right, bottom, left },
46
54
  };
47
55
  return new PageElement({
@@ -93,6 +101,10 @@ export function Document(a, b) {
93
101
  registry.set(name, resolveFontSource(source));
94
102
  return doc;
95
103
  };
104
+ doc.addFontFromUrl = async (name, source) => {
105
+ registry.set(name, await loadFontFromUrl(source));
106
+ return doc;
107
+ };
96
108
  doc.getFonts = () => [...registry.keys()];
97
109
  doc.hasFont = (name) => registry.has(name);
98
110
  return doc;
@@ -1,13 +1,16 @@
1
- import { TextElement, TextRole, TextSegment } from "../elements/text-element.js";
1
+ import { TextElement, TextRole, TextSegment, type VerticalTextAlign } from "../elements/text-element.js";
2
2
  import { TextOverflow } from "../text/line-breaker.js";
3
- import { ResolvedTextStyle } from "../text/text-style.js";
3
+ import type { Direction } from "../text/bidi.js";
4
+ import { ResolvedTextStyle, type TextTransform } from "../text/text-style.js";
4
5
  import { ColorInput } from "./color.js";
5
6
  export type { TextOverflow };
6
7
  /** Text styling shared by `Text`, `Paragraph` and `span`. `bold`/`italic` are booleans
7
8
  * (locked §7.3) and combine into one engine `FontStyle`. */
8
9
  export interface TextStyle {
9
10
  size?: number;
10
- font?: string;
11
+ /** Font family, or a STACK: each code point is drawn by the first family in the list that has a
12
+ * glyph for it, so `["Inter", "NotoSansCJK"]` sets mixed-script text without splitting it by hand. */
13
+ font?: string | string[];
11
14
  bold?: boolean;
12
15
  italic?: boolean;
13
16
  color?: ColorInput;
@@ -22,6 +25,19 @@ export interface TextStyle {
22
25
  skipInk?: boolean;
23
26
  /** Extra space after every glyph, in points (CSS `letter-spacing`). Negative tightens. Default 0. */
24
27
  letterSpacing?: number;
28
+ /** Base writing direction (CSS `direction`): `"rtl"` starts the line on the right, for Hebrew or
29
+ * Arabic. Mixed text is reordered per Unicode UAX #9 either way, so Hebrew inside an `ltr`
30
+ * paragraph already comes out right - this decides where the LINE starts. */
31
+ direction?: Direction;
32
+ /** CSS `text-transform`: recase the text. `capitalize` upper-cases the first letter of each word. */
33
+ textTransform?: TextTransform;
34
+ /** CSS `word-spacing`, in points: extra advance at every space. Negative tightens. */
35
+ wordSpacing?: number;
36
+ /** CSS `text-indent`, in points: how far the FIRST line of a paragraph starts in. */
37
+ textIndent?: number;
38
+ /** CSS `vertical-align` for a SPAN: raises a footnote marker or lowers an index. It shifts the
39
+ * baseline only - pass a smaller `size` too if you want the browser's `<sup>` look. */
40
+ verticalAlign?: VerticalTextAlign;
25
41
  /** External URL - makes this run an inline hyperlink (a /Link annotation over its glyphs). On a
26
42
  * `span` it links just that run; on a whole `Text` (plain string) it links the whole text. */
27
43
  href?: string;
@@ -31,10 +47,18 @@ export interface TextStyle {
31
47
  }
32
48
  export interface TextOptions extends TextStyle {
33
49
  /** Text-internal alignment (left/center/right) - independent of a parent's `cross` (§5). */
34
- align?: "left" | "center" | "right";
50
+ align?: "left" | "center" | "right" | "justify";
35
51
  /** Cap the number of wrapped lines (default: unlimited - the text grows down as far as it needs,
36
52
  * paginating onto the next page). Needs a bounded width (a Column/Expanded/`Box({ width })`). */
37
53
  maxLines?: number;
54
+ /**
55
+ * Minimum lines that must stay behind at a page break (CSS `orphans`), and minimum that must carry
56
+ * over (CSS `widows`). Both default to 2, the CSS initial value: a single line stranded at the foot of
57
+ * a page - or alone at the top of the next - reads as a mistake. Set both to 1 to switch the protection
58
+ * off and let a paragraph break wherever the page ends.
59
+ */
60
+ orphans?: number;
61
+ widows?: number;
38
62
  /** What happens past `maxLines`: `"clip"` (default) cuts hard, `"ellipsis"` ends with "…". */
39
63
  overflow?: TextOverflow;
40
64
  /** Line-height multiplier: each line is `size * lineHeight` tall. Unset means the font's own
@@ -62,16 +86,28 @@ export declare function Paragraph(content: string | TextSegment[], opts?: TextOp
62
86
  * Unset fields stay out so they keep inheriting. */
63
87
  export interface TextDefaults {
64
88
  size?: number;
65
- font?: string;
89
+ /** Font family, or a STACK: each code point is drawn by the first family in the list that has a
90
+ * glyph for it, so `["Inter", "NotoSansCJK"]` sets mixed-script text without splitting it by hand. */
91
+ font?: string | string[];
66
92
  bold?: boolean;
67
93
  italic?: boolean;
68
94
  color?: ColorInput;
69
- align?: "left" | "center" | "right";
95
+ align?: "left" | "center" | "right" | "justify";
70
96
  lineHeight?: number;
71
97
  underline?: boolean;
72
98
  strikethrough?: boolean;
73
99
  skipInk?: boolean;
74
100
  letterSpacing?: number;
101
+ /** Base writing direction (CSS `direction`): `"rtl"` starts the line on the right, for Hebrew or
102
+ * Arabic. Mixed text is reordered per Unicode UAX #9 either way, so Hebrew inside an `ltr`
103
+ * paragraph already comes out right - this decides where the LINE starts. */
104
+ direction?: Direction;
105
+ /** CSS `text-transform`: recase the text. `capitalize` upper-cases the first letter of each word. */
106
+ textTransform?: TextTransform;
107
+ /** CSS `word-spacing`, in points: extra advance at every space. Negative tightens. */
108
+ wordSpacing?: number;
109
+ /** CSS `text-indent`, in points: how far the FIRST line of a paragraph starts in. */
110
+ textIndent?: number;
75
111
  }
76
112
  /** Maps the `Text`-style option names onto a partial engine `ResolvedTextStyle` (only the set
77
113
  * fields), for seeding the inheritance cascade. */
package/dist/api/text.js CHANGED
@@ -1,4 +1,4 @@
1
- import { TextElement } from "../elements/text-element.js";
1
+ import { TextElement, } from "../elements/text-element.js";
2
2
  import { HorizontalAlignment } from "../elements/pdf-element.js";
3
3
  import { FontStyle } from "../utils/pdf-object-manager.js";
4
4
  import { toColor } from "./color.js";
@@ -12,10 +12,15 @@ function toFontStyle(bold, italic) {
12
12
  return FontStyle.Italic;
13
13
  return FontStyle.Normal;
14
14
  }
15
+ /** The family a run starts in - the first of a stack, or the single name given. */
16
+ const primaryFont = (font) => Array.isArray(font) ? font[0] : font;
17
+ /** The rest of the stack, tried in order for a code point the first family cannot draw. */
18
+ const fallbackFonts = (font) => Array.isArray(font) ? font.slice(1) : undefined;
15
19
  const ALIGN = {
16
20
  left: HorizontalAlignment.left,
17
21
  center: HorizontalAlignment.center,
18
22
  right: HorizontalAlignment.right,
23
+ justify: HorizontalAlignment.justify,
19
24
  };
20
25
  /**
21
26
  * Validates a font `size` (a number of points). Rejects anything that would silently become a NaN
@@ -45,7 +50,8 @@ export function span(text, style = {}) {
45
50
  return {
46
51
  content: text,
47
52
  fontSize: toFontSize(style.size),
48
- fontFamily: style.font,
53
+ fontFamily: primaryFont(style.font),
54
+ fontFallback: fallbackFonts(style.font),
49
55
  fontStyle: style.bold !== undefined || style.italic !== undefined
50
56
  ? toFontStyle(style.bold, style.italic)
51
57
  : undefined,
@@ -53,6 +59,7 @@ export function span(text, style = {}) {
53
59
  underline: style.underline,
54
60
  strikethrough: style.strikethrough,
55
61
  letterSpacing: style.letterSpacing,
62
+ verticalAlign: style.verticalAlign,
56
63
  href: style.href,
57
64
  dest: style.to,
58
65
  };
@@ -73,19 +80,26 @@ export function Text(content, opts = {}) {
73
80
  return new TextElement({
74
81
  content: body,
75
82
  fontSize: toFontSize(opts.size),
76
- fontFamily: opts.font,
83
+ fontFamily: primaryFont(opts.font),
84
+ fontFallback: fallbackFonts(opts.font),
77
85
  fontStyle: opts.bold !== undefined || opts.italic !== undefined
78
86
  ? toFontStyle(opts.bold, opts.italic)
79
87
  : undefined,
80
88
  color: opts.color !== undefined ? toColor(opts.color) : undefined,
81
89
  textAlignment: opts.align ? ALIGN[opts.align] : undefined,
82
90
  maxLines: opts.maxLines,
91
+ orphans: opts.orphans,
92
+ widows: opts.widows,
83
93
  overflow: opts.overflow,
84
94
  lineHeight: opts.lineHeight,
85
95
  underline: opts.underline,
86
96
  strikethrough: opts.strikethrough,
87
97
  skipInk: opts.skipInk,
88
98
  letterSpacing: opts.letterSpacing,
99
+ direction: opts.direction,
100
+ textTransform: opts.textTransform,
101
+ wordSpacing: opts.wordSpacing,
102
+ textIndent: opts.textIndent,
89
103
  role: opts.role,
90
104
  });
91
105
  }
@@ -99,8 +113,10 @@ export function toTextStyleOverride(opts) {
99
113
  const style = {};
100
114
  if (opts.size !== undefined)
101
115
  style.fontSize = toFontSize(opts.size);
102
- if (opts.font !== undefined)
103
- style.fontFamily = opts.font;
116
+ if (opts.font !== undefined) {
117
+ style.fontFamily = primaryFont(opts.font);
118
+ style.fontFallback = fallbackFonts(opts.font);
119
+ }
104
120
  if (opts.bold !== undefined || opts.italic !== undefined) {
105
121
  style.fontStyle = toFontStyle(opts.bold, opts.italic);
106
122
  }
@@ -118,5 +134,13 @@ export function toTextStyleOverride(opts) {
118
134
  style.skipInk = opts.skipInk;
119
135
  if (opts.letterSpacing !== undefined)
120
136
  style.letterSpacing = opts.letterSpacing;
137
+ if (opts.direction !== undefined)
138
+ style.direction = opts.direction;
139
+ if (opts.textTransform !== undefined)
140
+ style.textTransform = opts.textTransform;
141
+ if (opts.wordSpacing !== undefined)
142
+ style.wordSpacing = opts.wordSpacing;
143
+ if (opts.textIndent !== undefined)
144
+ style.textIndent = opts.textIndent;
121
145
  return style;
122
146
  }
@@ -1,5 +1,15 @@
1
1
  import { type PdfDict, type PdfObject, type PdfStream } from "./objects.js";
2
2
  declare const EMPTY: Uint8Array<ArrayBuffer>;
3
+ /** What `PdfDocument.load` accepts. */
4
+ export interface LoadOptions {
5
+ /** Ceiling on what ONE stream may inflate to, in bytes (default 64 MB). Raise it for a document that
6
+ * is genuinely enormous; a stream past it throws `PdfStreamTooLargeError` rather than being read. */
7
+ maxStreamSize?: number;
8
+ }
9
+ /** What `PdfDocument.open` accepts - everything `load` takes, plus the password. */
10
+ export interface OpenOptions extends LoadOptions {
11
+ password?: string;
12
+ }
3
13
  /** Thrown when a file cannot be opened because of its encryption - always saying which of the reasons it
4
14
  * is: no password, wrong password, or a revision we do not implement. */
5
15
  export declare class PdfEncryptedError extends Error {
@@ -23,8 +33,10 @@ export declare class PdfDocument {
23
33
  * update has to follow suit: mixing the two leaves readers that understand only one kind unable to
24
34
  * walk the chain back. */
25
35
  usesXrefStream: boolean;
36
+ /** Ceiling on what one stream may inflate to; see `inflate.ts`. */
37
+ private maxStreamSize;
26
38
  private constructor();
27
- static load(bytes: Uint8Array): PdfDocument;
39
+ static load(bytes: Uint8Array, opts?: LoadOptions): PdfDocument;
28
40
  /**
29
41
  * Open a document, decrypting it when it is password-protected.
30
42
  *
@@ -33,9 +45,7 @@ export declare class PdfDocument {
33
45
  * above them synchronous, which is the same trade the writer makes (`structure.ts` builds the handler
34
46
  * before the synchronous constructor).
35
47
  */
36
- static open(bytes: Uint8Array, opts?: {
37
- password?: string;
38
- }): Promise<PdfDocument>;
48
+ static open(bytes: Uint8Array, opts?: OpenOptions): Promise<PdfDocument>;
39
49
  /**
40
50
  * Build the handler for this file's `/Encrypt` dictionary, or say precisely why we cannot.
41
51
  *
@@ -1,6 +1,6 @@
1
- import { unzlibSync } from "fflate";
2
1
  import { Lexer } from "./lexer.js";
3
- import { get, isDict, isRef, isStream, isString, nameOf, numberOf, } from "./objects.js";
2
+ import { DEFAULT_MAX_STREAM_SIZE, inflateBounded, PdfStreamTooLargeError, } from "../utils/inflate.js";
3
+ import { get, isDict, isRef, isStream, isString, nameOf, numberOf, stringsIn, } from "./objects.js";
4
4
  import { StandardAes256 } from "../crypto/security-handler.js";
5
5
  import { StandardLegacy } from "../crypto/legacy-handler.js";
6
6
  const EMPTY = new Uint8Array(0);
@@ -13,22 +13,6 @@ export class PdfEncryptedError extends Error {
13
13
  }
14
14
  /** The raw bytes of a `/Encrypt` entry that must be a string (`/U`, `/UE`). */
15
15
  const stringBytes = (o) => isString(o) ? o.bytes : undefined;
16
- /** Every string reachable inside an object, so the whole tree can be decrypted in one walk. */
17
- function stringsIn(o, out = [], depth = 0) {
18
- if (o === undefined || o === null || depth > 64)
19
- return out;
20
- if (isString(o))
21
- out.push(o);
22
- else if (Array.isArray(o))
23
- for (const e of o)
24
- stringsIn(e, out, depth + 1);
25
- else if (isStream(o))
26
- stringsIn(o.dict, out, depth + 1);
27
- else if (isDict(o))
28
- for (const v of o.map.values())
29
- stringsIn(v, out, depth + 1);
30
- return out;
31
- }
32
16
  export class PdfDocument {
33
17
  constructor(bytes) {
34
18
  this.bytes = bytes;
@@ -46,9 +30,13 @@ export class PdfDocument {
46
30
  * update has to follow suit: mixing the two leaves readers that understand only one kind unable to
47
31
  * walk the chain back. */
48
32
  this.usesXrefStream = false;
33
+ /** Ceiling on what one stream may inflate to; see `inflate.ts`. */
34
+ this.maxStreamSize = DEFAULT_MAX_STREAM_SIZE;
49
35
  }
50
- static load(bytes) {
36
+ static load(bytes, opts = {}) {
51
37
  const doc = new PdfDocument(bytes);
38
+ if (opts.maxStreamSize !== undefined)
39
+ doc.maxStreamSize = opts.maxStreamSize;
52
40
  try {
53
41
  doc.readXrefChain();
54
42
  }
@@ -70,7 +58,7 @@ export class PdfDocument {
70
58
  * before the synchronous constructor).
71
59
  */
72
60
  static async open(bytes, opts = {}) {
73
- const doc = PdfDocument.load(bytes);
61
+ const doc = PdfDocument.load(bytes, opts);
74
62
  if (!doc.isEncrypted)
75
63
  return doc;
76
64
  doc.security = await doc.securityHandlerFor(opts.password);
@@ -577,9 +565,14 @@ export class PdfDocument {
577
565
  filters.forEach((filter, i) => {
578
566
  if (filter === "FlateDecode") {
579
567
  try {
580
- data = unzlibSync(data);
568
+ // Per pass, because /Filter is a list and [/FlateDecode /FlateDecode] is a legal nested bomb.
569
+ data = inflateBounded(data, this.maxStreamSize);
581
570
  }
582
- catch {
571
+ catch (e) {
572
+ // A stream that blew the ceiling is an attack, not a stream we failed to decode - swallowing
573
+ // it here would hand back silent garbage.
574
+ if (e instanceof PdfStreamTooLargeError)
575
+ throw e;
583
576
  return; // not our data to repair; hand back what we have
584
577
  }
585
578
  data = this.undoPredictor(data, this.resolve(parms[i]));
@@ -16,6 +16,8 @@ export type FieldValue = string | boolean | string[] | null;
16
16
  export interface FillOptions {
17
17
  /** The user password, for a document that is encrypted. Wrong or missing gives a named error. */
18
18
  password?: string;
19
+ /** Ceiling on what ONE stream may inflate to, in bytes (default 64 MB) - the zip-bomb guard. */
20
+ maxStreamSize?: number;
19
21
  /**
20
22
  * Draw the new value into the field's appearance (default `true`), the same way jasy bakes one when it
21
23
  * CREATES a field - so the value is visible in any viewer, not only in one that honours
@@ -23,10 +25,23 @@ export interface FillOptions {
23
25
  */
24
26
  fieldAppearances?: boolean;
25
27
  /**
26
- * Leave the drawing of the values to the viewer by setting `/NeedAppearances`, instead of generating
27
- * appearance streams. Default `true` today - baking them is the next step.
28
+ * Ask the viewer to draw the values itself, by setting `/NeedAppearances true` (default `true`).
29
+ *
30
+ * The two options answer the same question and exactly one of them ends up applying: when we DID
31
+ * bake, the flag is written as `false`, because a form that still asks for regeneration gets it and
32
+ * the viewer throws our drawing away. Turning BOTH off is refused - see `fillForm`.
28
33
  */
29
34
  needAppearances?: boolean;
35
+ /**
36
+ * Flatten the form after filling it (default `false`): the values become ordinary page content and
37
+ * the fields stop being editable. The whole form is flattened, not only the names in `values` - a
38
+ * document where some fields are frozen and others are not is the surprising outcome, and
39
+ * `flattenForm(bytes, { fields })` is there for anyone who wants that on purpose.
40
+ *
41
+ * Filling and flattening in one call produces ONE incremental update rather than two, so the file
42
+ * stays smaller than calling them in sequence - and the result is identical either way.
43
+ */
44
+ flatten?: boolean;
30
45
  }
31
46
  export interface FillResult {
32
47
  bytes: Uint8Array;
@@ -34,6 +49,8 @@ export interface FillResult {
34
49
  warnings: string[];
35
50
  /** The names actually written. */
36
51
  filled: string[];
52
+ /** The names turned into page content, when `flatten` was asked for. Empty otherwise. */
53
+ flattened: string[];
37
54
  }
38
55
  declare class FillError extends Error {
39
56
  }