@jasy/pdf 1.0.0-alpha.7 → 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.
- package/README.md +11 -10
- package/dist/api/content.d.ts +5 -4
- package/dist/api/content.js +13 -7
- package/dist/api/descriptor.js +23 -0
- package/dist/api/dimension.d.ts +56 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +134 -0
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +3 -0
- package/dist/api/index.js +3 -0
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +18 -7
- package/dist/api/layout.d.ts +20 -6
- package/dist/api/layout.js +31 -6
- package/dist/api/page-builder.d.ts +8 -0
- package/dist/api/page-builder.js +12 -0
- package/dist/api/structure.d.ts +11 -0
- package/dist/api/structure.js +17 -4
- package/dist/api/text.d.ts +42 -6
- package/dist/api/text.js +29 -5
- package/dist/crypto/legacy-handler.d.ts +43 -0
- package/dist/crypto/legacy-handler.js +145 -0
- package/dist/crypto/security-handler.d.ts +16 -0
- package/dist/crypto/security-handler.js +26 -0
- package/dist/edit/acroform-reader.d.ts +70 -0
- package/dist/edit/acroform-reader.js +126 -0
- package/dist/edit/appearance.d.ts +47 -0
- package/dist/edit/appearance.js +171 -0
- package/dist/edit/document.d.ts +145 -0
- package/dist/edit/document.js +642 -0
- package/dist/edit/fill.d.ts +65 -0
- package/dist/edit/fill.js +424 -0
- package/dist/edit/flatten.d.ts +54 -0
- package/dist/edit/flatten.js +462 -0
- package/dist/edit/index.d.ts +20 -0
- package/dist/edit/index.js +18 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +68 -0
- package/dist/edit/objects.js +83 -0
- package/dist/edit/writer.d.ts +60 -0
- package/dist/edit/writer.js +283 -0
- package/dist/elements/container-element.d.ts +21 -1
- package/dist/elements/container-element.js +25 -10
- package/dist/elements/forms/checkbox-element.d.ts +51 -0
- package/dist/elements/forms/checkbox-element.js +48 -0
- package/dist/elements/forms/choice-element.d.ts +53 -0
- package/dist/elements/forms/choice-element.js +59 -0
- package/dist/elements/forms/push-button-element.d.ts +56 -0
- package/dist/elements/forms/push-button-element.js +49 -0
- package/dist/elements/forms/radio-element.d.ts +52 -0
- package/dist/elements/forms/radio-element.js +49 -0
- package/dist/elements/forms/signature-element.d.ts +54 -0
- package/dist/elements/forms/signature-element.js +47 -0
- package/dist/elements/forms/text-field-element.d.ts +61 -0
- package/dist/elements/forms/text-field-element.js +54 -0
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/index.d.ts +6 -0
- package/dist/elements/index.js +6 -0
- package/dist/elements/layout/expanded-element.d.ts +3 -1
- package/dist/elements/layout/expanded-element.js +12 -3
- package/dist/elements/layout/padding-element.d.ts +4 -2
- package/dist/elements/layout/padding-element.js +4 -2
- package/dist/elements/layout/page-builder-element.js +2 -0
- package/dist/elements/pdf-element.d.ts +59 -1
- package/dist/elements/pdf-element.js +62 -1
- package/dist/elements/rectangle-element.d.ts +20 -6
- package/dist/elements/rectangle-element.js +25 -12
- package/dist/elements/row-element.d.ts +24 -1
- package/dist/elements/row-element.js +25 -10
- package/dist/elements/text-element.d.ts +60 -1
- package/dist/elements/text-element.js +132 -33
- package/dist/forms/acroform.d.ts +63 -0
- package/dist/forms/acroform.js +427 -0
- package/dist/forms/appearance.d.ts +58 -0
- package/dist/forms/appearance.js +204 -0
- package/dist/forms/field.d.ts +164 -0
- package/dist/forms/field.js +1 -0
- package/dist/forms/pdf.d.ts +19 -0
- package/dist/forms/pdf.js +28 -0
- package/dist/ir/display-list.d.ts +40 -7
- package/dist/ir/display-list.js +8 -1
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +53 -0
- package/dist/layout/box-constraints.js +72 -0
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/image-renderer.js +2 -1
- package/dist/renderer/page-renderer.js +8 -2
- package/dist/renderer/pdf-backend.d.ts +6 -0
- package/dist/renderer/pdf-backend.js +86 -31
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/pdf-renderer.js +19 -1
- package/dist/renderer/rectangle-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +2 -2
- package/dist/renderer/text-renderer.js +150 -42
- package/dist/text/advance.js +5 -2
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/line-breaker.d.ts +35 -1
- package/dist/text/line-breaker.js +76 -16
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +67 -0
- package/dist/text/text-style.d.ts +25 -0
- package/dist/text/text-style.js +44 -1
- package/dist/utils/dest-registry.d.ts +5 -2
- package/dist/utils/dest-registry.js +5 -5
- package/dist/utils/flex-layout.d.ts +24 -4
- package/dist/utils/flex-layout.js +169 -7
- package/dist/utils/font-metrics.d.ts +5 -0
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/outline.js +1 -2
- package/dist/utils/pdf-object-manager.d.ts +39 -0
- package/dist/utils/pdf-object-manager.js +166 -15
- package/dist/utils/struct-tree.js +2 -3
- package/dist/utils/ttf-parser.d.ts +6 -0
- package/dist/utils/ttf-parser.js +19 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/validators/element-validator.js +14 -3
- package/package.json +10 -2
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { PDFElement } from "../elements/pdf-element.js";
|
|
2
|
+
import { TextFieldElement } from "../elements/forms/text-field-element.js";
|
|
3
|
+
import { RadioElement } from "../elements/forms/radio-element.js";
|
|
4
|
+
import { ChoiceElement } from "../elements/forms/choice-element.js";
|
|
5
|
+
import { PushButtonElement } from "../elements/forms/push-button-element.js";
|
|
6
|
+
import { SignatureFieldElement } from "../elements/forms/signature-element.js";
|
|
7
|
+
import type { FieldAlign } from "../forms/field.js";
|
|
8
|
+
import { ColorInput } from "./color.js";
|
|
9
|
+
/** A choice option: a plain string (value = label) or `{ value, label }`. */
|
|
10
|
+
export type ChoiceOptionInput = string | {
|
|
11
|
+
value: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
};
|
|
14
|
+
/** Options for a `TextField` - an interactive text input in the generated PDF. */
|
|
15
|
+
export interface TextFieldOptions {
|
|
16
|
+
/** The field name - unique in the document; you read the value back by it. */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Initial value. */
|
|
19
|
+
value?: string;
|
|
20
|
+
/** Tooltip / accessible name shown on hover. */
|
|
21
|
+
tooltip?: string;
|
|
22
|
+
/** Accept multiple lines instead of one (give the box a `height` to match). */
|
|
23
|
+
multiline?: boolean;
|
|
24
|
+
/** Mask the typed characters (a password field). */
|
|
25
|
+
password?: boolean;
|
|
26
|
+
/** How the value sits in the box: left (default), center or right. */
|
|
27
|
+
align?: FieldAlign;
|
|
28
|
+
/** Maximum number of characters. */
|
|
29
|
+
maxLength?: number;
|
|
30
|
+
/** Show the value but do not let the user edit it. */
|
|
31
|
+
readOnly?: boolean;
|
|
32
|
+
/** Mark it as a field that must be filled in before the form is submitted. */
|
|
33
|
+
required?: boolean;
|
|
34
|
+
/** Hide the widget entirely - neither on screen nor in print. */
|
|
35
|
+
hidden?: boolean;
|
|
36
|
+
/** Include the widget when printing (default true). `false` keeps it on screen only. */
|
|
37
|
+
print?: boolean;
|
|
38
|
+
/** Box width in points; omit to fill the offered width. */
|
|
39
|
+
width?: number;
|
|
40
|
+
/** Box height in points; omit for a single-line default. */
|
|
41
|
+
height?: number;
|
|
42
|
+
/** Value font size in points; `0` lets the viewer auto-size. */
|
|
43
|
+
fontSize?: number;
|
|
44
|
+
/** Value text colour (named / hex / rgb). */
|
|
45
|
+
color?: ColorInput;
|
|
46
|
+
/** Box border colour; omit for no border. */
|
|
47
|
+
border?: ColorInput;
|
|
48
|
+
/** Box background fill; omit for transparent. */
|
|
49
|
+
background?: ColorInput;
|
|
50
|
+
/** Border thickness in points (default 1 when a border is set). */
|
|
51
|
+
borderWidth?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* An interactive text field the reader of the PDF can type into (AcroForm /Tx). Place it like any element -
|
|
55
|
+
* in a `Column`, `Row`, `Box` or a table cell; it reserves its box and becomes a fillable widget.
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* TextField({ name: "email", border: "gray", height: 22 })
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function TextField(opts: TextFieldOptions): TextFieldElement;
|
|
62
|
+
/** Options for a `Checkbox` - a toggle the reader of the PDF can tick. */
|
|
63
|
+
export interface CheckboxOptions {
|
|
64
|
+
/** The field name - unique in the document. */
|
|
65
|
+
name: string;
|
|
66
|
+
/** Text placed beside the box. Given one, the box and the text become a row - the same shape
|
|
67
|
+
* `RadioGroup` builds for its choices, so a lone check box does not have to be assembled by hand. */
|
|
68
|
+
label?: string;
|
|
69
|
+
/** Size of the label text (default 12). */
|
|
70
|
+
labelSize?: number;
|
|
71
|
+
/** Colour of the label text. */
|
|
72
|
+
labelColor?: ColorInput;
|
|
73
|
+
/** Space between the box and its label, in points (default 8). */
|
|
74
|
+
labelGap?: number;
|
|
75
|
+
/** Whether it starts checked. */
|
|
76
|
+
checked?: boolean;
|
|
77
|
+
/** The "on" export value stored when checked (default `"Yes"`). */
|
|
78
|
+
onValue?: string;
|
|
79
|
+
/** Tooltip / accessible name shown on hover. */
|
|
80
|
+
tooltip?: string;
|
|
81
|
+
/** Show the state but do not let the user toggle it. */
|
|
82
|
+
readOnly?: boolean;
|
|
83
|
+
/** Mark it as a field that must be filled in before the form is submitted. */
|
|
84
|
+
required?: boolean;
|
|
85
|
+
/** Hide the widget entirely - neither on screen nor in print. */
|
|
86
|
+
hidden?: boolean;
|
|
87
|
+
/** Include the widget when printing (default true). `false` keeps it on screen only. */
|
|
88
|
+
print?: boolean;
|
|
89
|
+
/** Box side length in points (default 14). */
|
|
90
|
+
size?: number;
|
|
91
|
+
/** Checkmark colour (default black). */
|
|
92
|
+
color?: ColorInput;
|
|
93
|
+
/** Box border colour (default a mid grey). */
|
|
94
|
+
border?: ColorInput;
|
|
95
|
+
/** Box background fill. */
|
|
96
|
+
background?: ColorInput;
|
|
97
|
+
/** Border thickness in points (default 1). */
|
|
98
|
+
borderWidth?: number;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* An interactive checkbox (AcroForm /Btn). It bakes its own checkmark appearance, so the tick shows in
|
|
102
|
+
* any viewer, in print, and under PDF/A - not only in ones that regenerate appearances.
|
|
103
|
+
*
|
|
104
|
+
* ```ts
|
|
105
|
+
* Row({ gap: 6, align: "center" }, [Checkbox({ name: "agree" }), Text("I agree")])
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export declare function Checkbox(opts: CheckboxOptions): PDFElement;
|
|
109
|
+
/** Options for a single `Radio` button. Several sharing a `group` are one mutually-exclusive field. */
|
|
110
|
+
export interface RadioOptions {
|
|
111
|
+
/** The shared group name - radios with the same `group` are one field. */
|
|
112
|
+
group: string;
|
|
113
|
+
/** This button's export value - unique within the group. */
|
|
114
|
+
value: string;
|
|
115
|
+
/** Whether it starts selected. */
|
|
116
|
+
selected?: boolean;
|
|
117
|
+
/** Tooltip / accessible name. */
|
|
118
|
+
tooltip?: string;
|
|
119
|
+
/** Show but do not allow changing. */
|
|
120
|
+
readOnly?: boolean;
|
|
121
|
+
/** Mark it as a field that must be filled in before the form is submitted. */
|
|
122
|
+
required?: boolean;
|
|
123
|
+
/** Hide the widget entirely - neither on screen nor in print. */
|
|
124
|
+
hidden?: boolean;
|
|
125
|
+
/** Include the widget when printing (default true). `false` keeps it on screen only. */
|
|
126
|
+
print?: boolean;
|
|
127
|
+
/** Button diameter in points (default 14). */
|
|
128
|
+
size?: number;
|
|
129
|
+
/** Dot colour. */
|
|
130
|
+
color?: ColorInput;
|
|
131
|
+
/** Ring colour. */
|
|
132
|
+
border?: ColorInput;
|
|
133
|
+
/** Fill behind the ring. */
|
|
134
|
+
background?: ColorInput;
|
|
135
|
+
/** Ring thickness. */
|
|
136
|
+
borderWidth?: number;
|
|
137
|
+
}
|
|
138
|
+
/** A single radio button. Use `RadioGroup` for the common labelled-list layout, or this for custom ones. */
|
|
139
|
+
export declare function Radio(opts: RadioOptions): RadioElement;
|
|
140
|
+
/** One option in a `RadioGroup`. */
|
|
141
|
+
export interface RadioChoice {
|
|
142
|
+
/** The export value stored when this option is picked. */
|
|
143
|
+
value: string;
|
|
144
|
+
/** The text shown next to the button. */
|
|
145
|
+
label: string;
|
|
146
|
+
}
|
|
147
|
+
/** Options for a `RadioGroup` - the labelled list of mutually-exclusive buttons. */
|
|
148
|
+
export interface RadioGroupOptions {
|
|
149
|
+
/** The field name (shared by every button). */
|
|
150
|
+
name: string;
|
|
151
|
+
/** Mark the group as required. */
|
|
152
|
+
required?: boolean;
|
|
153
|
+
/** Hide every button - neither on screen nor in print. */
|
|
154
|
+
hidden?: boolean;
|
|
155
|
+
/** Include the buttons when printing (default true). */
|
|
156
|
+
print?: boolean;
|
|
157
|
+
/** The value of the option that starts selected (matches one option's `value`). */
|
|
158
|
+
value?: string;
|
|
159
|
+
/** Vertical space between options (default 8). */
|
|
160
|
+
gap?: number;
|
|
161
|
+
/** Button diameter (default 14). */
|
|
162
|
+
size?: number;
|
|
163
|
+
/** Button ring / dot / background colours, applied to every option. */
|
|
164
|
+
color?: ColorInput;
|
|
165
|
+
border?: ColorInput;
|
|
166
|
+
background?: ColorInput;
|
|
167
|
+
/** Label font size (default 12) and colour. */
|
|
168
|
+
labelSize?: number;
|
|
169
|
+
labelColor?: ColorInput;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A labelled group of mutually-exclusive radio buttons. Sugar over `Radio` + `Text`: it lays each option
|
|
173
|
+
* out as a button-and-label row in a `Column`, and marks the one whose `value` matches the group `value`.
|
|
174
|
+
*
|
|
175
|
+
* ```ts
|
|
176
|
+
* RadioGroup({ name: "size", value: "m" }, [
|
|
177
|
+
* { value: "s", label: "Small" },
|
|
178
|
+
* { value: "m", label: "Medium" },
|
|
179
|
+
* { value: "l", label: "Large" },
|
|
180
|
+
* ])
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export declare function RadioGroup(opts: RadioGroupOptions, options: RadioChoice[]): PDFElement;
|
|
184
|
+
/** Style + geometry shared by `Dropdown` and `ListBox`. */
|
|
185
|
+
interface ChoiceStyleOptions {
|
|
186
|
+
/** How the value sits in the box: left (default), center or right. */
|
|
187
|
+
align?: FieldAlign;
|
|
188
|
+
tooltip?: string;
|
|
189
|
+
readOnly?: boolean;
|
|
190
|
+
/** Mark it as a field that must be filled in before the form is submitted. */
|
|
191
|
+
required?: boolean;
|
|
192
|
+
/** Hide the widget entirely - neither on screen nor in print. */
|
|
193
|
+
hidden?: boolean;
|
|
194
|
+
/** Include the widget when printing (default true). `false` keeps it on screen only. */
|
|
195
|
+
print?: boolean;
|
|
196
|
+
width?: number;
|
|
197
|
+
height?: number;
|
|
198
|
+
fontSize?: number;
|
|
199
|
+
color?: ColorInput;
|
|
200
|
+
border?: ColorInput;
|
|
201
|
+
background?: ColorInput;
|
|
202
|
+
borderWidth?: number;
|
|
203
|
+
}
|
|
204
|
+
/** Options for a `Dropdown` (combo box). */
|
|
205
|
+
export interface DropdownOptions extends ChoiceStyleOptions {
|
|
206
|
+
/** The field name. */
|
|
207
|
+
name: string;
|
|
208
|
+
/** The selected value. */
|
|
209
|
+
value?: string;
|
|
210
|
+
/** Let the reader type a value that is not in the list. */
|
|
211
|
+
editable?: boolean;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* A dropdown / combo box (AcroForm /Ch). Options are plain strings or `{ value, label }`.
|
|
215
|
+
*
|
|
216
|
+
* ```ts
|
|
217
|
+
* Dropdown({ name: "country", value: "de" }, [{ value: "de", label: "Germany" }, "France"])
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
export declare function Dropdown(opts: DropdownOptions, options: ChoiceOptionInput[]): ChoiceElement;
|
|
221
|
+
/** `Select` is an alias for `Dropdown`. */
|
|
222
|
+
export declare const Select: typeof Dropdown;
|
|
223
|
+
/** Options for a `ListBox`. */
|
|
224
|
+
export interface ListBoxOptions extends ChoiceStyleOptions {
|
|
225
|
+
/** The field name. */
|
|
226
|
+
name: string;
|
|
227
|
+
/** The selected value (single-select). */
|
|
228
|
+
value?: string;
|
|
229
|
+
/** The selected values (when `multiSelect`). */
|
|
230
|
+
values?: string[];
|
|
231
|
+
/** Allow more than one selection. */
|
|
232
|
+
multiSelect?: boolean;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* A scrollable list box (AcroForm /Ch). Give it a `height` to show more rows. Set `multiSelect` for
|
|
236
|
+
* several selections at once.
|
|
237
|
+
*/
|
|
238
|
+
export declare function ListBox(opts: ListBoxOptions, options: ChoiceOptionInput[]): ChoiceElement;
|
|
239
|
+
/**
|
|
240
|
+
* What a `PushButton` does when clicked: `"reset"` clears the form, `{ submit: url }` posts the field
|
|
241
|
+
* values, `{ open: url }` opens a link. Omit it for a button that does nothing. Scripted (JavaScript)
|
|
242
|
+
* actions are deliberately not offered - a jasy form is data, not a program.
|
|
243
|
+
*
|
|
244
|
+
* VIEWER SUPPORT. All three actions we emit are spec-correct; what differs is what a viewer implements.
|
|
245
|
+
* PROVEN, not assumed - the same cases built with pdf-lib (an independent producer) behave identically,
|
|
246
|
+
* so none of this is a jasy bug. Measured 2026-07 in pdf.js (VS Code) and poppler/Evince:
|
|
247
|
+
* - `"reset"` - works in both. The safe one.
|
|
248
|
+
* - `{ open: url }` - works in pdf.js, dead in Evince, and NOT fixable from here: poppler's glib
|
|
249
|
+
* layer surfaces actions only through `poppler_page_get_link_mapping` (i.e.
|
|
250
|
+
* /Link annotations); its form-field API has no action getter at all, so a
|
|
251
|
+
* WIDGET's /A can never be reached. **For a plain hyperlink use `Link({ href })`**
|
|
252
|
+
* - the identical URI action on a real /Link annotation works in both.
|
|
253
|
+
* - `{ submit: url }` - Acrobat only. `POPPLER_ACTION_SUBMIT_FORM` does not exist in poppler-glib (no
|
|
254
|
+
* such enum value), and pdf.js does not implement /SubmitForm either.
|
|
255
|
+
* Ruled out as causes: pdf-lib splits field and widget, sets /P and a /D appearance - and still fails
|
|
256
|
+
* the same way, so none of those structural differences matter.
|
|
257
|
+
*/
|
|
258
|
+
export type ButtonActionInput = "reset" | {
|
|
259
|
+
submit: string;
|
|
260
|
+
} | {
|
|
261
|
+
open: string;
|
|
262
|
+
};
|
|
263
|
+
/** Options for a `PushButton`. */
|
|
264
|
+
export interface PushButtonOptions {
|
|
265
|
+
/** The field name - unique in the document. */
|
|
266
|
+
name: string;
|
|
267
|
+
/** The caption drawn on the button. */
|
|
268
|
+
label: string;
|
|
269
|
+
/** What clicking it does. */
|
|
270
|
+
action?: ButtonActionInput;
|
|
271
|
+
/** Tooltip / accessible name shown on hover. */
|
|
272
|
+
tooltip?: string;
|
|
273
|
+
/** Draw it but do not let it be clicked. */
|
|
274
|
+
readOnly?: boolean;
|
|
275
|
+
/** Mark it as a field that must be filled in before the form is submitted. */
|
|
276
|
+
required?: boolean;
|
|
277
|
+
/** Hide the widget entirely - neither on screen nor in print. */
|
|
278
|
+
hidden?: boolean;
|
|
279
|
+
/** Include the widget when printing (default true). `false` keeps it on screen only. */
|
|
280
|
+
print?: boolean;
|
|
281
|
+
/** Width in points; omit to fill the offered width. */
|
|
282
|
+
width?: number;
|
|
283
|
+
/** Height in points; omit for a comfortable default. */
|
|
284
|
+
height?: number;
|
|
285
|
+
/** Caption font size (default 12). */
|
|
286
|
+
fontSize?: number;
|
|
287
|
+
/** Caption colour. */
|
|
288
|
+
color?: ColorInput;
|
|
289
|
+
/** Border colour. */
|
|
290
|
+
border?: ColorInput;
|
|
291
|
+
/** Button face fill. */
|
|
292
|
+
background?: ColorInput;
|
|
293
|
+
/** Border thickness in points. */
|
|
294
|
+
borderWidth?: number;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* A clickable push button (AcroForm /Btn). It holds no value - it fires an action. Its caption is baked
|
|
298
|
+
* into the appearance, so the button looks the same in every viewer and in print.
|
|
299
|
+
*
|
|
300
|
+
* ```ts
|
|
301
|
+
* PushButton({ name: "send", label: "Submit", action: { submit: "https://example.com/f" }, width: 120 })
|
|
302
|
+
* PushButton({ name: "clear", label: "Reset", action: "reset", width: 90 })
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
export declare function PushButton(opts: PushButtonOptions): PushButtonElement;
|
|
306
|
+
/** Options for a `SignatureField`. */
|
|
307
|
+
export interface SignatureFieldOptions {
|
|
308
|
+
/** The field name - unique in the document. */
|
|
309
|
+
name: string;
|
|
310
|
+
/** A hint drawn inside the empty box, e.g. "Signature" or "Approved by". */
|
|
311
|
+
label?: string;
|
|
312
|
+
/** Tooltip / accessible name shown on hover. */
|
|
313
|
+
tooltip?: string;
|
|
314
|
+
/** Draw it but do not let it be signed. */
|
|
315
|
+
readOnly?: boolean;
|
|
316
|
+
/** Mark it as a field that must be filled in before the form is submitted. */
|
|
317
|
+
required?: boolean;
|
|
318
|
+
/** Hide the widget entirely - neither on screen nor in print. */
|
|
319
|
+
hidden?: boolean;
|
|
320
|
+
/** Include the widget when printing (default true). `false` keeps it on screen only. */
|
|
321
|
+
print?: boolean;
|
|
322
|
+
/** Width in points; omit to fill the offered width. */
|
|
323
|
+
width?: number;
|
|
324
|
+
/** Height in points (default 48). */
|
|
325
|
+
height?: number;
|
|
326
|
+
/** Hint font size (default 9). */
|
|
327
|
+
fontSize?: number;
|
|
328
|
+
/** Hint colour. */
|
|
329
|
+
color?: ColorInput;
|
|
330
|
+
/** Box border + signing-rule colour. */
|
|
331
|
+
border?: ColorInput;
|
|
332
|
+
/** Box background fill. */
|
|
333
|
+
background?: ColorInput;
|
|
334
|
+
/** Border thickness in points. */
|
|
335
|
+
borderWidth?: number;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* A signature field (AcroForm /Sig) - a placeholder someone can sign later in a PDF tool. jasy creates
|
|
339
|
+
* the field and draws its empty "sign here" box; it does not apply signatures itself (real signing needs
|
|
340
|
+
* a certificate and a byte-range digest).
|
|
341
|
+
*
|
|
342
|
+
* ```ts
|
|
343
|
+
* SignatureField({ name: "approver", label: "Signature", width: 240 })
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
export declare function SignatureField(opts: SignatureFieldOptions): SignatureFieldElement;
|
|
347
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { TextFieldElement } from "../elements/forms/text-field-element.js";
|
|
2
|
+
import { CheckboxElement } from "../elements/forms/checkbox-element.js";
|
|
3
|
+
import { RadioElement } from "../elements/forms/radio-element.js";
|
|
4
|
+
import { ChoiceElement } from "../elements/forms/choice-element.js";
|
|
5
|
+
import { PushButtonElement } from "../elements/forms/push-button-element.js";
|
|
6
|
+
import { SignatureFieldElement } from "../elements/forms/signature-element.js";
|
|
7
|
+
import { toColor } from "./color.js";
|
|
8
|
+
import { Column, Row } from "./layout.js";
|
|
9
|
+
import { Text } from "./text.js";
|
|
10
|
+
const toOptions = (opts) => opts.map((o) => (typeof o === "string" ? { value: o } : o));
|
|
11
|
+
/**
|
|
12
|
+
* An interactive text field the reader of the PDF can type into (AcroForm /Tx). Place it like any element -
|
|
13
|
+
* in a `Column`, `Row`, `Box` or a table cell; it reserves its box and becomes a fillable widget.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* TextField({ name: "email", border: "gray", height: 22 })
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function TextField(opts) {
|
|
20
|
+
return new TextFieldElement({
|
|
21
|
+
...opts,
|
|
22
|
+
color: opts.color !== undefined ? toColor(opts.color) : undefined,
|
|
23
|
+
border: opts.border !== undefined ? toColor(opts.border) : undefined,
|
|
24
|
+
background: opts.background !== undefined ? toColor(opts.background) : undefined,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* An interactive checkbox (AcroForm /Btn). It bakes its own checkmark appearance, so the tick shows in
|
|
29
|
+
* any viewer, in print, and under PDF/A - not only in ones that regenerate appearances.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* Row({ gap: 6, align: "center" }, [Checkbox({ name: "agree" }), Text("I agree")])
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function Checkbox(opts) {
|
|
36
|
+
const box = new CheckboxElement({
|
|
37
|
+
...opts,
|
|
38
|
+
color: opts.color !== undefined ? toColor(opts.color) : undefined,
|
|
39
|
+
border: opts.border !== undefined ? toColor(opts.border) : undefined,
|
|
40
|
+
background: opts.background !== undefined ? toColor(opts.background) : undefined,
|
|
41
|
+
});
|
|
42
|
+
// Without a label the box IS the element, so nothing wraps it and the output is unchanged.
|
|
43
|
+
if (opts.label === undefined)
|
|
44
|
+
return box;
|
|
45
|
+
return Row({ gap: opts.labelGap ?? 8, align: "center" }, [
|
|
46
|
+
box,
|
|
47
|
+
Text(opts.label, { size: opts.labelSize ?? 12, color: opts.labelColor }),
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
/** A single radio button. Use `RadioGroup` for the common labelled-list layout, or this for custom ones. */
|
|
51
|
+
export function Radio(opts) {
|
|
52
|
+
return new RadioElement({
|
|
53
|
+
...opts,
|
|
54
|
+
color: opts.color !== undefined ? toColor(opts.color) : undefined,
|
|
55
|
+
border: opts.border !== undefined ? toColor(opts.border) : undefined,
|
|
56
|
+
background: opts.background !== undefined ? toColor(opts.background) : undefined,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A labelled group of mutually-exclusive radio buttons. Sugar over `Radio` + `Text`: it lays each option
|
|
61
|
+
* out as a button-and-label row in a `Column`, and marks the one whose `value` matches the group `value`.
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* RadioGroup({ name: "size", value: "m" }, [
|
|
65
|
+
* { value: "s", label: "Small" },
|
|
66
|
+
* { value: "m", label: "Medium" },
|
|
67
|
+
* { value: "l", label: "Large" },
|
|
68
|
+
* ])
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function RadioGroup(opts, options) {
|
|
72
|
+
const rows = options.map((o) => Row({ gap: 8, align: "center" }, [
|
|
73
|
+
Radio({
|
|
74
|
+
group: opts.name,
|
|
75
|
+
value: o.value,
|
|
76
|
+
selected: o.value === opts.value,
|
|
77
|
+
required: opts.required,
|
|
78
|
+
hidden: opts.hidden,
|
|
79
|
+
print: opts.print,
|
|
80
|
+
size: opts.size,
|
|
81
|
+
color: opts.color,
|
|
82
|
+
border: opts.border,
|
|
83
|
+
background: opts.background,
|
|
84
|
+
}),
|
|
85
|
+
Text(o.label, { size: opts.labelSize ?? 12, color: opts.labelColor }),
|
|
86
|
+
]));
|
|
87
|
+
return Column({ gap: opts.gap ?? 8 }, rows);
|
|
88
|
+
}
|
|
89
|
+
const choiceStyle = (o) => ({
|
|
90
|
+
align: o.align,
|
|
91
|
+
tooltip: o.tooltip,
|
|
92
|
+
readOnly: o.readOnly,
|
|
93
|
+
required: o.required,
|
|
94
|
+
hidden: o.hidden,
|
|
95
|
+
print: o.print,
|
|
96
|
+
width: o.width,
|
|
97
|
+
height: o.height,
|
|
98
|
+
fontSize: o.fontSize,
|
|
99
|
+
color: o.color !== undefined ? toColor(o.color) : undefined,
|
|
100
|
+
border: o.border !== undefined ? toColor(o.border) : undefined,
|
|
101
|
+
background: o.background !== undefined ? toColor(o.background) : undefined,
|
|
102
|
+
borderWidth: o.borderWidth,
|
|
103
|
+
});
|
|
104
|
+
/**
|
|
105
|
+
* A dropdown / combo box (AcroForm /Ch). Options are plain strings or `{ value, label }`.
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* Dropdown({ name: "country", value: "de" }, [{ value: "de", label: "Germany" }, "France"])
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function Dropdown(opts, options) {
|
|
112
|
+
return new ChoiceElement({
|
|
113
|
+
name: opts.name,
|
|
114
|
+
options: toOptions(options),
|
|
115
|
+
combo: true,
|
|
116
|
+
editable: opts.editable,
|
|
117
|
+
value: opts.value,
|
|
118
|
+
...choiceStyle(opts),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** `Select` is an alias for `Dropdown`. */
|
|
122
|
+
export const Select = Dropdown;
|
|
123
|
+
/**
|
|
124
|
+
* A scrollable list box (AcroForm /Ch). Give it a `height` to show more rows. Set `multiSelect` for
|
|
125
|
+
* several selections at once.
|
|
126
|
+
*/
|
|
127
|
+
export function ListBox(opts, options) {
|
|
128
|
+
return new ChoiceElement({
|
|
129
|
+
name: opts.name,
|
|
130
|
+
options: toOptions(options),
|
|
131
|
+
combo: false,
|
|
132
|
+
multiSelect: opts.multiSelect,
|
|
133
|
+
value: opts.value,
|
|
134
|
+
values: opts.values,
|
|
135
|
+
...choiceStyle(opts),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const toAction = (a) => {
|
|
139
|
+
if (a === undefined)
|
|
140
|
+
return undefined;
|
|
141
|
+
if (a === "reset")
|
|
142
|
+
return { kind: "reset" };
|
|
143
|
+
return "submit" in a ? { kind: "submit", url: a.submit } : { kind: "url", url: a.open };
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* A clickable push button (AcroForm /Btn). It holds no value - it fires an action. Its caption is baked
|
|
147
|
+
* into the appearance, so the button looks the same in every viewer and in print.
|
|
148
|
+
*
|
|
149
|
+
* ```ts
|
|
150
|
+
* PushButton({ name: "send", label: "Submit", action: { submit: "https://example.com/f" }, width: 120 })
|
|
151
|
+
* PushButton({ name: "clear", label: "Reset", action: "reset", width: 90 })
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export function PushButton(opts) {
|
|
155
|
+
return new PushButtonElement({
|
|
156
|
+
name: opts.name,
|
|
157
|
+
label: opts.label,
|
|
158
|
+
action: toAction(opts.action),
|
|
159
|
+
tooltip: opts.tooltip,
|
|
160
|
+
readOnly: opts.readOnly,
|
|
161
|
+
required: opts.required,
|
|
162
|
+
hidden: opts.hidden,
|
|
163
|
+
print: opts.print,
|
|
164
|
+
width: opts.width,
|
|
165
|
+
height: opts.height,
|
|
166
|
+
fontSize: opts.fontSize,
|
|
167
|
+
color: opts.color !== undefined ? toColor(opts.color) : undefined,
|
|
168
|
+
border: opts.border !== undefined ? toColor(opts.border) : undefined,
|
|
169
|
+
background: opts.background !== undefined ? toColor(opts.background) : undefined,
|
|
170
|
+
borderWidth: opts.borderWidth,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A signature field (AcroForm /Sig) - a placeholder someone can sign later in a PDF tool. jasy creates
|
|
175
|
+
* the field and draws its empty "sign here" box; it does not apply signatures itself (real signing needs
|
|
176
|
+
* a certificate and a byte-range digest).
|
|
177
|
+
*
|
|
178
|
+
* ```ts
|
|
179
|
+
* SignatureField({ name: "approver", label: "Signature", width: 240 })
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export function SignatureField(opts) {
|
|
183
|
+
return new SignatureFieldElement({
|
|
184
|
+
...opts,
|
|
185
|
+
color: opts.color !== undefined ? toColor(opts.color) : undefined,
|
|
186
|
+
border: opts.border !== undefined ? toColor(opts.border) : undefined,
|
|
187
|
+
background: opts.background !== undefined ? toColor(opts.background) : undefined,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ColorInput } from "./color.js";
|
|
2
|
+
import type { Gradient } from "../ir/display-list.js";
|
|
3
|
+
/**
|
|
4
|
+
* Gradients for a box background.
|
|
5
|
+
*
|
|
6
|
+
* The public form is BOX-RELATIVE - an angle and some stops - because that is what an author knows
|
|
7
|
+
* when writing the document. The engine's `Gradient` wants absolute page coordinates, so the renderer
|
|
8
|
+
* resolves one against the box it is painting; see `resolveGradient`.
|
|
9
|
+
*/
|
|
10
|
+
/** A stop as written: a colour, optionally pinned to a position 0..1 along the axis. */
|
|
11
|
+
export type StopInput = ColorInput | {
|
|
12
|
+
color: ColorInput;
|
|
13
|
+
at: number;
|
|
14
|
+
};
|
|
15
|
+
export interface LinearGradientInput {
|
|
16
|
+
kind: "linear";
|
|
17
|
+
/** Direction in degrees, CSS convention: 0 = to the top, 90 = to the right (default 180, downwards). */
|
|
18
|
+
angle: number;
|
|
19
|
+
stops: StopInput[];
|
|
20
|
+
}
|
|
21
|
+
export interface RadialGradientInput {
|
|
22
|
+
kind: "radial";
|
|
23
|
+
/** Centre as a fraction of the box, `[0.5, 0.5]` being the middle (the default). */
|
|
24
|
+
center: [number, number];
|
|
25
|
+
/** Outer radius as a fraction of the box's larger side (default 0.5, i.e. it touches the edges). */
|
|
26
|
+
radius: number;
|
|
27
|
+
stops: StopInput[];
|
|
28
|
+
}
|
|
29
|
+
export type GradientInput = LinearGradientInput | RadialGradientInput;
|
|
30
|
+
export declare const isGradientInput: (v: unknown) => v is GradientInput;
|
|
31
|
+
/**
|
|
32
|
+
* A linear gradient. `linearGradient("#fff", "#000")` runs top to bottom; pass `{ angle }` for any
|
|
33
|
+
* other direction, and `{ color, at }` for a stop that is not evenly spaced.
|
|
34
|
+
*/
|
|
35
|
+
export declare function linearGradient(...args: [...StopInput[]] | [{
|
|
36
|
+
angle?: number;
|
|
37
|
+
stops: StopInput[];
|
|
38
|
+
}]): LinearGradientInput;
|
|
39
|
+
/** A radial gradient, centred by default and reaching the edges. */
|
|
40
|
+
export declare function radialGradient(...args: [...StopInput[]] | [{
|
|
41
|
+
center?: [number, number];
|
|
42
|
+
radius?: number;
|
|
43
|
+
stops: StopInput[];
|
|
44
|
+
}]): RadialGradientInput;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a box-relative gradient against the box it paints, into the absolute page coordinates the
|
|
47
|
+
* IR wants. Called by the renderer, which is the first place the box's geometry is known.
|
|
48
|
+
*
|
|
49
|
+
* The angle follows CSS: 0 points to the top and it turns clockwise, so 90 goes to the right. The
|
|
50
|
+
* axis runs through the box centre and is scaled so it spans the box on that diagonal - which is why
|
|
51
|
+
* a 45-degree gradient reaches the corners rather than stopping short of them.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveGradient(g: GradientInput, x: number, y: number, width: number, height: number): Gradient;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { toColor } from "./color.js";
|
|
2
|
+
export const isGradientInput = (v) => typeof v === "object" && v !== null && "kind" in v && "stops" in v;
|
|
3
|
+
/**
|
|
4
|
+
* A linear gradient. `linearGradient("#fff", "#000")` runs top to bottom; pass `{ angle }` for any
|
|
5
|
+
* other direction, and `{ color, at }` for a stop that is not evenly spaced.
|
|
6
|
+
*/
|
|
7
|
+
export function linearGradient(...args) {
|
|
8
|
+
const first = args[0];
|
|
9
|
+
if (args.length === 1 && typeof first === "object" && first !== null && "stops" in first) {
|
|
10
|
+
return { kind: "linear", angle: first.angle ?? 180, stops: first.stops };
|
|
11
|
+
}
|
|
12
|
+
return { kind: "linear", angle: 180, stops: args };
|
|
13
|
+
}
|
|
14
|
+
/** A radial gradient, centred by default and reaching the edges. */
|
|
15
|
+
export function radialGradient(...args) {
|
|
16
|
+
const first = args[0];
|
|
17
|
+
if (args.length === 1 && typeof first === "object" && first !== null && "stops" in first) {
|
|
18
|
+
return {
|
|
19
|
+
kind: "radial",
|
|
20
|
+
center: first.center ?? [0.5, 0.5],
|
|
21
|
+
radius: first.radius ?? 0.5,
|
|
22
|
+
stops: first.stops,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { kind: "radial", center: [0.5, 0.5], radius: 0.5, stops: args };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Spread the stops that carry no `at` evenly, first to last, the way CSS does - and check the pinned
|
|
29
|
+
* ones, because they end up in a PDF stitching function's `/Bounds`, which the format requires to be
|
|
30
|
+
* strictly increasing inside the domain. A bad offset there is not a wrong-looking gradient, it is a
|
|
31
|
+
* malformed file, so it is refused by name here rather than emitted.
|
|
32
|
+
*/
|
|
33
|
+
function toStops(stops) {
|
|
34
|
+
if (stops.length < 2) {
|
|
35
|
+
throw new Error("A gradient needs at least two colour stops.");
|
|
36
|
+
}
|
|
37
|
+
const last = stops.length - 1;
|
|
38
|
+
const out = stops.map((s, i) => {
|
|
39
|
+
const pinned = typeof s === "object" && s !== null && "at" in s;
|
|
40
|
+
const at = pinned ? s.at : i / last;
|
|
41
|
+
if (!Number.isFinite(at) || at < 0 || at > 1) {
|
|
42
|
+
throw new Error(`Invalid gradient stop position ${at}: it must be a number between 0 and 1.`);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
offset: at,
|
|
46
|
+
color: toColor(pinned ? s.color : s),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
for (let i = 1; i < out.length; i++) {
|
|
50
|
+
if (out[i].offset <= out[i - 1].offset) {
|
|
51
|
+
throw new Error(`Gradient stops must move forward: stop ${i} sits at ${out[i].offset}, which is not after ` +
|
|
52
|
+
`${out[i - 1].offset}. (Two stops at the SAME position would be a hard colour edge in CSS; ` +
|
|
53
|
+
`PDF cannot express that in one shading, so it is refused rather than drawn wrongly.)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// A pinned first or last stop would otherwise be IGNORED - the shading's domain is always 0..1 and
|
|
57
|
+
// only the INTERIOR offsets reach /Bounds. Carrying the edge colour outwards is what CSS does.
|
|
58
|
+
if (out[0].offset > 0)
|
|
59
|
+
out.unshift({ offset: 0, color: out[0].color });
|
|
60
|
+
if (out[out.length - 1].offset < 1)
|
|
61
|
+
out.push({ offset: 1, color: out[out.length - 1].color });
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a box-relative gradient against the box it paints, into the absolute page coordinates the
|
|
66
|
+
* IR wants. Called by the renderer, which is the first place the box's geometry is known.
|
|
67
|
+
*
|
|
68
|
+
* The angle follows CSS: 0 points to the top and it turns clockwise, so 90 goes to the right. The
|
|
69
|
+
* axis runs through the box centre and is scaled so it spans the box on that diagonal - which is why
|
|
70
|
+
* a 45-degree gradient reaches the corners rather than stopping short of them.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveGradient(g, x, y, width, height) {
|
|
73
|
+
const stops = toStops(g.stops);
|
|
74
|
+
if (g.kind === "radial") {
|
|
75
|
+
const cx = x + g.center[0] * width;
|
|
76
|
+
const cy = y + g.center[1] * height;
|
|
77
|
+
const r = g.radius * Math.max(width, height);
|
|
78
|
+
return { type: "radial", x0: cx, y0: cy, r0: 0, x1: cx, y1: cy, r1: r, stops, extend: "pad" };
|
|
79
|
+
}
|
|
80
|
+
const rad = ((g.angle % 360) * Math.PI) / 180;
|
|
81
|
+
// CSS angles: 0 = up, clockwise. The engine lays out top-left down, so "up" is -y here.
|
|
82
|
+
const dx = Math.sin(rad);
|
|
83
|
+
const dy = -Math.cos(rad);
|
|
84
|
+
// Half the box's extent along the axis, so the line covers the whole box for any angle.
|
|
85
|
+
const half = (Math.abs(dx) * width + Math.abs(dy) * height) / 2;
|
|
86
|
+
const cx = x + width / 2;
|
|
87
|
+
const cy = y + height / 2;
|
|
88
|
+
return {
|
|
89
|
+
type: "linear",
|
|
90
|
+
x0: cx - dx * half,
|
|
91
|
+
y0: cy - dy * half,
|
|
92
|
+
x1: cx + dx * half,
|
|
93
|
+
y1: cy + dy * half,
|
|
94
|
+
stops,
|
|
95
|
+
extend: "pad",
|
|
96
|
+
};
|
|
97
|
+
}
|