mirror of
https://github.com/foliojs/pdfkit.git
synced 2025-12-08 20:15:54 +00:00
Merge pull request #1002 from jpravetz/master
Add AcroForms output support
This commit is contained in:
commit
05bc0f3e09
BIN
demo/out.pdf
BIN
demo/out.pdf
Binary file not shown.
85
demo/test-acroform.js
Executable file
85
demo/test-acroform.js
Executable file
@ -0,0 +1,85 @@
|
||||
var PDFDocument = require('../');
|
||||
var fs = require('fs');
|
||||
|
||||
// Create a new PDFDocument
|
||||
var doc = new PDFDocument();
|
||||
|
||||
doc.pipe(fs.createWriteStream('out-acroform.pdf'));
|
||||
|
||||
// Set some meta data
|
||||
doc.info['Title'] = 'Test Form Document';
|
||||
doc.info['Author'] = 'test-acroform.js';
|
||||
|
||||
doc.font('Helvetica'); // establishes the default font for forms
|
||||
doc.initForm();
|
||||
|
||||
let rootField = doc.formField('rootField');
|
||||
doc.font('Courier');
|
||||
let child1Field = doc.formField('child1Field', { parent: rootField });
|
||||
doc.font('Helvetica');
|
||||
let child2Field = doc.formField('child2Field', { parent: rootField });
|
||||
|
||||
let y = 10;
|
||||
doc.formText('leaf1', 10, y, 200, 20, {
|
||||
parent: child1Field,
|
||||
value: '1999-12-31',
|
||||
format: {
|
||||
type: 'date',
|
||||
param: 'yyyy-mm-dd'
|
||||
},
|
||||
align: 'center'
|
||||
});
|
||||
|
||||
y += 30;
|
||||
opts = {
|
||||
parent: child1Field,
|
||||
value: 32.98,
|
||||
format: {
|
||||
type: 'number',
|
||||
nDec: 2,
|
||||
currency: '$',
|
||||
currencyPrepend: true
|
||||
},
|
||||
align: 'right'
|
||||
};
|
||||
doc.formText('dollar', 10, y, 200, 20, opts);
|
||||
|
||||
y += 30;
|
||||
doc.formText('leaf2', 10, y, 200, 40, {
|
||||
parent: child1Field,
|
||||
multiline: true,
|
||||
align: 'right'
|
||||
});
|
||||
y += 50;
|
||||
doc.formText('leaf3', 10, y, 200, 80, {
|
||||
parent: child2Field,
|
||||
multiline: true
|
||||
});
|
||||
|
||||
y += 90;
|
||||
var opts = {
|
||||
backgroundColor: 'yellow',
|
||||
label: 'Test Button'
|
||||
};
|
||||
doc.formPushButton('btn1', 10, y, 100, 30, opts);
|
||||
|
||||
y += 40;
|
||||
opts = {
|
||||
borderColor: 'black',
|
||||
select: ['Select Option', 'github', 'bitbucket', 'gitlab'],
|
||||
value: 'Select Option',
|
||||
defaultValue: 'Select Option',
|
||||
align: 'center',
|
||||
edit: true
|
||||
};
|
||||
doc.formCombo('ch1', 10, y, 100, 20, opts);
|
||||
|
||||
y += 30;
|
||||
opts = {
|
||||
borderColor: '#808080',
|
||||
select: ['github', 'bitbucket', 'gitlab', 'sourcesafe', 'perforce'],
|
||||
sort: true
|
||||
};
|
||||
doc.formList('ch2', 10, y, 100, 45, opts);
|
||||
|
||||
doc.end();
|
||||
313
docs/forms.md
Normal file
313
docs/forms.md
Normal file
@ -0,0 +1,313 @@
|
||||
# Forms in PDFKit
|
||||
|
||||
Forms are an interactive feature of the PDF format. Forms make it possible to
|
||||
add form annotations such as text fields, combo boxes, buttons and actions.
|
||||
Before addings forms to a PDF you must call the document `initForm()` method.
|
||||
|
||||
- `initForm()` - Must be called before adding a form annotation to the document.
|
||||
|
||||
```javascript
|
||||
doc.font('Helvetica'); // establishes the default form field font
|
||||
doc.initForm();
|
||||
```
|
||||
|
||||
## Form Annotation Methods
|
||||
|
||||
Form annotations are added using the following document methods.
|
||||
|
||||
- `formText( name, x, y, width, height, options)`
|
||||
- `formPushButton( name, x, y, width, height, name, options)`
|
||||
- `formCombo( name, x, y, width, height, options)`
|
||||
- `formList( name, x, y, width, height, options)`
|
||||
|
||||
The above methods call the `formAnnotation` method with
|
||||
type set to one of `text`, `pushButton`, `radioButton`, `combo` or `list`.
|
||||
|
||||
- `formAnnotation( name, type, x, y, width, height, options)`
|
||||
|
||||
### `name` Parameter
|
||||
|
||||
Form annotations are each given a `name` that is used for identification. Field
|
||||
names are hierarchical using a period ('.') as a separaror (_e.g._
|
||||
_shipping.address.street_). More than one form field can have the same name.
|
||||
When this happens, the fields will have the same value. There is more
|
||||
information on `name` in the **Field Names** section below.
|
||||
|
||||
### `options` Parameter
|
||||
|
||||
#### Common Options
|
||||
|
||||
Form Annotation `options` that are common across all form annotation types are:
|
||||
|
||||
- `required` [_boolean_] - The field must have a value by the time the form is submitted.
|
||||
- `noExport` [_boolean_] - The field will not be exported if a form is submitted.
|
||||
- `readOnly` [_boolean_] - The user may not change the value of the field, and
|
||||
the field will not respond to mouse clicks. This is useful for fields that have
|
||||
computed values.
|
||||
- `value` [_number|string_] - The field's value.
|
||||
- `defaultValue` [_number|string_] - The default value to which the field
|
||||
reverts if a reset-form action is executed.
|
||||
|
||||
Some form annotations have `color` options. You can use an array of RGB values,
|
||||
a hex color, or a named CSS color value for that option.
|
||||
|
||||
- `backgroundColor` - field background color
|
||||
- `borderColor` - field border color
|
||||
|
||||
#### Text Field Options
|
||||
|
||||
- `align` [_string_] - Sets the alignment to `left`,
|
||||
`center` or `right`.
|
||||
- `multiline` [_boolean_] - Allows the field to have multiple lines of text.
|
||||
- `password` [_boolean_] - The text will be masked (_e.g._ with asterisks).
|
||||
- `noSpell` [_boolean_] - If set, text entered in the field is not spell-checked
|
||||
- `format` [_object_] - See the section on **Text Field Formatting** below.
|
||||
|
||||
```js
|
||||
doc.formText('leaf2', 10, 60, 200, 40, {
|
||||
multiline: true,
|
||||
align: 'right',
|
||||
format: {
|
||||
type: 'date',
|
||||
params: 'm/d'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Combo and List Field Options
|
||||
|
||||
- `sort` [_boolean_] - The field options will be sorted alphabetically.
|
||||
- `edit` [_boolean_] - (combo only) Allow the user to enter a value in the field.
|
||||
- `multiSelect` [_boolean_] - Allow more than one choice to be selected.
|
||||
- `noSpell` [_boolean_] - (combo only) If set and `edit` is true, text entered in the field is not spell-checked.
|
||||
- `select` [_array_] - Array of choices to display in the combo or list form field.
|
||||
|
||||
```js
|
||||
opts = {
|
||||
select: ['', 'github', 'bitbucket', 'gitlab'],
|
||||
value: '',
|
||||
defaultValue: '',
|
||||
align: 'left'
|
||||
};
|
||||
doc.formCombo('ch1', 10, y, 100, 20, opts);
|
||||
```
|
||||
|
||||
#### Button Field Options
|
||||
|
||||
- `label` [_string_] - Sets the label text. You can also set an icon, but for
|
||||
this you will need to 'expert-up' and dig deeper into the PDF Reference manual.
|
||||
|
||||
```js
|
||||
var opts = {
|
||||
backgroundColor: 'yellow',
|
||||
label: 'Test Button'
|
||||
};
|
||||
doc.formPushButton('btn1', 10, 200, 100, 30, opts);
|
||||
```
|
||||
|
||||
### Text Field Formatting
|
||||
|
||||
When needing to format the text value of a Form Annotation, the following
|
||||
`options` are available. This will cause predefined document JavaScript actions
|
||||
to automatically format the text. Refer to the section _Formatting scripts_ in
|
||||
[Acrobat Forms
|
||||
Plugin](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FIAC_API_FormsIntro%2FMethods1.htm)
|
||||
of the Acrobat SDK documentation for more information.
|
||||
|
||||
Add a format dictionary to `options`. The dictionary must contain a `type` attribute.
|
||||
|
||||
- `format` - generic object
|
||||
- `format.type` - value must be one of `date`, `time`, `percent`, `number`, `zip`, `zipPlus4`, `phone` or `ssn`.
|
||||
|
||||
When `type` is `date`, `time`, `percent` or `number` the format dictionary must
|
||||
contain additional parameters as described below.
|
||||
|
||||
#### Date format
|
||||
|
||||
- `format.param` (_string_) - specifies the value and display format and can include:
|
||||
- `d` - single digit day of month
|
||||
- `dd` - double digit day of month
|
||||
- `m` - month digit
|
||||
- `mm` - month double digit
|
||||
- `mmm` - abbreviated month name
|
||||
- `mmmm` - full month name
|
||||
- `yy` - two digit year
|
||||
- `yyyy` - four digit year
|
||||
- `hh` - hour for 12 hour clock
|
||||
- `HH` - hour for 24 hour clock
|
||||
- `MM` - two digit minute
|
||||
- `tt` - am or pm
|
||||
|
||||
```js
|
||||
// Date text field formatting
|
||||
doc.formText('field.date', 10, 60, 200, 40, {
|
||||
align: 'center',
|
||||
format: {
|
||||
type: 'date',
|
||||
param: 'mmmm d, yyyy'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Time format
|
||||
|
||||
- `format.param` - value must be a number between 0 and 3, representing
|
||||
the formats "14:30", "2:30 PM", "14:30:15" and "2:30:15 PM".
|
||||
|
||||
```js
|
||||
// Time text field formatting
|
||||
doc.formText('field.time', 10, 60, 200, 40, {
|
||||
align: 'center',
|
||||
format: {
|
||||
type: 'time',
|
||||
param: 2
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Number and percent format
|
||||
|
||||
- `format.nDec` [_number_] - the number of places after the decimal point
|
||||
- `format.sepComma` [_boolean_] - display a comma separator, otherwise do not display a separator.
|
||||
- `format.negStyle` [_string_] (number only) - the value must be one of `MinusBlack` , `Red`, `ParensBlack`, `ParensRed`
|
||||
- `format.currency` [_string_] (number only) - a currency symbol to display
|
||||
- `format.currencyPrepend` [_boolean_] (number only) - set to true to prepend the currency symbol
|
||||
|
||||
```js
|
||||
// Currency text field formatting
|
||||
doc.formText('leaf2', 10, 60, 200, 40, {
|
||||
multiline: true,
|
||||
align: 'right',
|
||||
format: {
|
||||
type: 'number',
|
||||
nDec: 2,
|
||||
sepComma: true,
|
||||
negStyle: 'ParensRed',
|
||||
currency: '$',
|
||||
currencyPrepend: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Field Names
|
||||
|
||||
Form Annotations are, by default, added to the root of the PDF document. A PDF
|
||||
form is organized in a name heirarchy, for example _shipping.address.street_.
|
||||
Capture this heirarchy either by setting the `name` of each _form annotation_ with
|
||||
the full hierarchical name (e.g. _shipping.address.street_) or by creating a
|
||||
hierarchy of _form fields_ and _form annotations_ and refering to a form field or form
|
||||
annotations parent using `options.parent`.
|
||||
|
||||
A _form field_ is an invisible node in the PDF form and is created using the
|
||||
document `formField` method. A form field must include the node's `name` (e.g.
|
||||
_shipping_) and may include other information such as the default font that is
|
||||
to be used by all child form annotations.
|
||||
|
||||
Using the `formField` method you might create a _shipping_ field that is added
|
||||
to the root of the document, an _address_ field that refers to the _shipping_
|
||||
field as it's parent, and a _street_ Form Annotation that would refer to the
|
||||
_address_ field as it's parent.
|
||||
|
||||
Create form fields using the document method:
|
||||
|
||||
- `formField( name, options )` - returns a reference to the field
|
||||
|
||||
-- _Example PDF using field hierarchy, three text fields and a push
|
||||
button_ --
|
||||
|
||||
```javascript
|
||||
doc.font('Helvetica'); // establishes the default font
|
||||
doc.initForm();
|
||||
|
||||
let rootField = doc.formField('rootField');
|
||||
let child1Field = doc.formField('child1Field', { parent: rootField });
|
||||
let child2Field = doc.formField('child2Field', { parent: rootField });
|
||||
|
||||
// Add text form annotation 'rootField.child1Field.leaf1'
|
||||
doc.formText('leaf1', 10, 10, 200, 40, {
|
||||
parent: child1Field,
|
||||
multiline: true
|
||||
});
|
||||
// Add text form annotation 'rootField.child1Field.leaf2'
|
||||
doc.formText('leaf2', 10, 60, 200, 40, {
|
||||
parent: child1Field,
|
||||
multiline: true
|
||||
});
|
||||
// Add text form annotation 'rootField.child2Field.leaf1'
|
||||
doc.formText('leaf1', 10, 110, 200, 80, {
|
||||
parent: child2Field,
|
||||
multiline: true
|
||||
});
|
||||
|
||||
// Add push button form annotation 'btn1'
|
||||
var opts = {
|
||||
backgroundColor: 'yellow',
|
||||
label: 'Test Button'
|
||||
};
|
||||
doc.formPushButton('btn1', 10, 200, 100, 30, opts);
|
||||
```
|
||||
|
||||
The output of this example looks like this.
|
||||
|
||||

|
||||
|
||||
### Advanced Form Field Use
|
||||
|
||||
Forms can be quite complicated and your needs will likely grow to sometimes need
|
||||
to directly specify the attributes that will go into the Form Annotation or
|
||||
Field dictionaries. Consult the **PDF Reference** and set these attributes in
|
||||
the `options` object. Any options that are not listed above will be added
|
||||
directly to the corresponding PDF Object.
|
||||
|
||||
## Font
|
||||
|
||||
The font used for a Form Annotation is set using the `document.font` method.
|
||||
Yes that's the same method as is used when setting the text font.
|
||||
|
||||
The `font` method must be called before `initForm` and may be called before `formField` or any of the form annotation methods.
|
||||
|
||||
```js
|
||||
doc.setFont('Courier');
|
||||
doc.formText('myfield', 10, 10, 200, 20);
|
||||
```
|
||||
|
||||
## Named JavaScript
|
||||
|
||||
In support of Form Annotations that execute JavaScript in PDF, you may use the
|
||||
following document method:
|
||||
|
||||
- `addNamedJavaScript( name, string )`
|
||||
|
||||
## Limitations
|
||||
|
||||
It is recommended that you test your PDF form documents across all platforms and
|
||||
viewers that you wish to support.
|
||||
|
||||
### Form Field Appearances
|
||||
|
||||
Form elements must each have an _appearance_ set using the `AP` attribute of the
|
||||
annotation. If this attribute is not set, the form element's value _may_ not be
|
||||
visible. Because appearances can be complex to generate, Adobe Acrobat has an
|
||||
option to build these apperances from form values and Form Annotation
|
||||
attributes when a PDF is first opened. To do this PDFKit always sets the Form dictionary's
|
||||
`NeedAppearances` attribute to true. This could mean that the PDF will be
|
||||
_dirty_ upon open, meaning it will need to be saved.
|
||||
|
||||
The `NeedAppearances` flag may not be honored by all PDF viewers.
|
||||
|
||||
Some form documents may not need to generate appearances. This may be the case
|
||||
for text Form Annotations that initially have no value. This is not true for
|
||||
push button widget annotations. Please test
|
||||
|
||||
### Document JavaScript
|
||||
|
||||
Many PDF Viewers, aside from Adobe Acrobat Reader, do not implement document
|
||||
JavaScript. Even Adobe Readers may not implement document JavaScript where it is
|
||||
not permitted by a device's app store terms of service (e.g. iOS devices).
|
||||
|
||||
### Radio and Checkboxes
|
||||
|
||||
Support for radio and checkboxes requires a more advanced attention to their
|
||||
rendered appearances and are not supported in this initial forms release.
|
||||
|
||||
---
|
||||
BIN
docs/images/acroforms.png
Normal file
BIN
docs/images/acroforms.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
185
jest.config.js
Normal file
185
jest.config.js
Normal file
@ -0,0 +1,185 @@
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// Respect "browser" field in package.json when resolving modules
|
||||
// browser: false,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/h2/_ymt4p095vs7zg32zp2lbtqr0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
// clearMocks: false,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: null,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
// coverageDirectory: null,
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: null,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: null,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: null,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: null,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: null,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: null,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: null,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: null,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
// testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: null,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: null,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: null,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
@ -17,6 +17,7 @@ import TextMixin from './mixins/text';
|
||||
import ImagesMixin from './mixins/images';
|
||||
import AnnotationsMixin from './mixins/annotations';
|
||||
import OutlineMixin from './mixins/outline';
|
||||
import AcroFormMixin from './mixins/acroform';
|
||||
|
||||
class PDFDocument extends stream.Readable {
|
||||
constructor(options = {}) {
|
||||
@ -186,6 +187,17 @@ class PDFDocument extends stream.Readable {
|
||||
this._root.data.Names.data.Dests.add(name, args);
|
||||
}
|
||||
|
||||
addNamedJavaScript(name, js) {
|
||||
if (!this._root.data.Names.data.JavaScript) {
|
||||
this._root.data.Names.data.JavaScript = new PDFNameTree();
|
||||
}
|
||||
let data = {
|
||||
JS: new String(js),
|
||||
S: 'JavaScript'
|
||||
};
|
||||
this._root.data.Names.data.JavaScript.add(name, data);
|
||||
}
|
||||
|
||||
ref(data) {
|
||||
const ref = new PDFReference(this, this._offsets.length + 1, data);
|
||||
this._offsets.push(null); // placeholder for this object's offset once it is finalized
|
||||
@ -259,6 +271,7 @@ Please pipe the document into a Node stream.\
|
||||
this._root.end();
|
||||
this._root.data.Pages.end();
|
||||
this._root.data.Names.end();
|
||||
this.endAcroForm();
|
||||
|
||||
if (this._security) {
|
||||
this._security.end();
|
||||
@ -321,5 +334,6 @@ mixin(TextMixin);
|
||||
mixin(ImagesMixin);
|
||||
mixin(AnnotationsMixin);
|
||||
mixin(OutlineMixin);
|
||||
mixin(AcroFormMixin);
|
||||
|
||||
export default PDFDocument;
|
||||
|
||||
389
lib/mixins/acroform.js
Normal file
389
lib/mixins/acroform.js
Normal file
@ -0,0 +1,389 @@
|
||||
const FIELD_FLAGS = {
|
||||
readOnly: 1,
|
||||
required: 2,
|
||||
noExport: 4,
|
||||
multiline: 0x1000,
|
||||
password: 0x2000,
|
||||
toggleToOffButton: 0x4000,
|
||||
radioButton: 0x8000,
|
||||
pushButton: 0x10000,
|
||||
combo: 0x20000,
|
||||
edit: 0x40000,
|
||||
sort: 0x80000,
|
||||
multiSelect: 0x200000,
|
||||
noSpell: 0x400000
|
||||
};
|
||||
const FIELD_JUSTIFY = {
|
||||
left: 0,
|
||||
center: 1,
|
||||
right: 2
|
||||
};
|
||||
const VALUE_MAP = { value: 'V', defaultValue: 'DV' };
|
||||
const FORMAT_SPECIAL = {
|
||||
zip: '0',
|
||||
zipPlus4: '1',
|
||||
zip4: '1',
|
||||
phone: '2',
|
||||
ssn: '3'
|
||||
};
|
||||
const FORMAT_DEFAULT = {
|
||||
number: {
|
||||
nDec: 0,
|
||||
sepComma: false,
|
||||
negStyle: 'MinusBlack',
|
||||
currency: '',
|
||||
currencyPrepend: true
|
||||
},
|
||||
percent: {
|
||||
nDec: 0,
|
||||
sepComma: false
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Must call if adding AcroForms to a document. Must also call font() before
|
||||
* this method to set the default font.
|
||||
*/
|
||||
initForm() {
|
||||
if (!this._font) {
|
||||
throw new Error('Must set a font before calling initForm method');
|
||||
}
|
||||
this._acroform = {
|
||||
fonts: {},
|
||||
defaultFont: this._font.name
|
||||
};
|
||||
this._acroform.fonts[this._font.id] = this._font.ref();
|
||||
|
||||
let data = {
|
||||
Fields: [],
|
||||
NeedAppearances: true,
|
||||
DA: new String(`/${this._font.id} 0 Tf 0 g`),
|
||||
DR: {
|
||||
Font: {}
|
||||
}
|
||||
};
|
||||
data.DR.Font[this._font.id] = this._font.ref();
|
||||
const AcroForm = this.ref(data);
|
||||
this._root.data.AcroForm = AcroForm;
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called automatically by document.js
|
||||
*/
|
||||
endAcroForm() {
|
||||
if (this._root.data.AcroForm) {
|
||||
if (
|
||||
!Object.keys(this._acroform.fonts).length &&
|
||||
!this._acroform.defaultFont
|
||||
) {
|
||||
throw new Error('No fonts specified for PDF form');
|
||||
}
|
||||
let fontDict = this._root.data.AcroForm.data.DR.Font;
|
||||
Object.keys(this._acroform.fonts).forEach(name => {
|
||||
fontDict[name] = this._acroform.fonts[name];
|
||||
});
|
||||
this._root.data.AcroForm.data.Fields.forEach(fieldRef => {
|
||||
this._endChild(fieldRef);
|
||||
});
|
||||
this._root.data.AcroForm.end();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_endChild(ref) {
|
||||
if (Array.isArray(ref.data.Kids)) {
|
||||
ref.data.Kids.forEach(childRef => {
|
||||
this._endChild(childRef);
|
||||
});
|
||||
ref.end();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates and adds a form field to the document. Form fields are intermediate
|
||||
* nodes in a PDF form that are used to specify form name heirarchy and form
|
||||
* value defaults.
|
||||
* @param {string} name - field name (T attribute in field dictionary)
|
||||
* @param {object} options - other attributes to include in field dictionary
|
||||
*/
|
||||
formField(name, options = {}) {
|
||||
let fieldDict = this._fieldDict(name, null, options);
|
||||
let fieldRef = this.ref(fieldDict);
|
||||
this._addToParent(fieldRef);
|
||||
return fieldRef;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates and adds a Form Annotation to the document. Form annotations are
|
||||
* called Widget annotations internally within a PDF file.
|
||||
* @param {string} name - form field name (T attribute of widget annotation
|
||||
* dictionary)
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} w
|
||||
* @param {number} h
|
||||
* @param {object} options
|
||||
*/
|
||||
formAnnotation(name, type, x, y, w, h, options = {}) {
|
||||
let fieldDict = this._fieldDict(name, type, options);
|
||||
fieldDict.Subtype = 'Widget';
|
||||
if (fieldDict.F === undefined) {
|
||||
fieldDict.F = 4; // print the annotation
|
||||
}
|
||||
|
||||
// Add Field annot to page, and get it's ref
|
||||
this.annotate(x, y, w, h, fieldDict);
|
||||
let annotRef = this.page.annotations[this.page.annotations.length - 1];
|
||||
|
||||
return this._addToParent(annotRef);
|
||||
},
|
||||
|
||||
formText(name, x, y, w, h, options = {}) {
|
||||
return this.formAnnotation(name, 'text', x, y, w, h, options);
|
||||
},
|
||||
|
||||
formPushButton(name, x, y, w, h, options = {}) {
|
||||
return this.formAnnotation(name, 'pushButton', x, y, w, h, options);
|
||||
},
|
||||
|
||||
formCombo(name, x, y, w, h, options = {}) {
|
||||
return this.formAnnotation(name, 'combo', x, y, w, h, options);
|
||||
},
|
||||
|
||||
formList(name, x, y, w, h, options = {}) {
|
||||
return this.formAnnotation(name, 'list', x, y, w, h, options);
|
||||
},
|
||||
|
||||
formRadioButton(name, x, y, w, h, options = {}) {
|
||||
return this.formAnnotation(name, 'radioButton', x, y, w, h, options);
|
||||
},
|
||||
|
||||
formCheckbox(name, x, y, w, h, options = {}) {
|
||||
return this.formAnnotation(name, 'checkbox', x, y, w, h, options);
|
||||
},
|
||||
|
||||
_addToParent(fieldRef) {
|
||||
let parent = fieldRef.data.Parent;
|
||||
if (parent) {
|
||||
if (!parent.data.Kids) {
|
||||
parent.data.Kids = [];
|
||||
}
|
||||
parent.data.Kids.push(fieldRef);
|
||||
} else {
|
||||
this._root.data.AcroForm.data.Fields.push(fieldRef);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_fieldDict(name, type, options = {}) {
|
||||
if (!this._acroform) {
|
||||
throw new Error(
|
||||
'Call document.initForms() method before adding form elements to document'
|
||||
);
|
||||
}
|
||||
let opts = Object.assign({}, options);
|
||||
if (type !== null) {
|
||||
opts = this._resolveType(type, options);
|
||||
}
|
||||
opts = this._resolveFlags(opts);
|
||||
opts = this._resolveJustify(opts);
|
||||
opts = this._resolveFont(opts);
|
||||
opts = this._resolveStrings(opts);
|
||||
opts = this._resolveColors(opts);
|
||||
opts = this._resolveFormat(opts);
|
||||
opts.T = new String(name);
|
||||
if (opts.parent) {
|
||||
opts.Parent = opts.parent;
|
||||
delete opts.parent;
|
||||
}
|
||||
return opts;
|
||||
},
|
||||
|
||||
_resolveType(type, opts) {
|
||||
if (type === 'text') {
|
||||
opts.FT = 'Tx';
|
||||
} else if (type === 'pushButton') {
|
||||
opts.FT = 'Btn';
|
||||
opts.pushButton = true;
|
||||
} else if (type === 'radioButton') {
|
||||
opts.FT = 'Btn';
|
||||
opts.radioButton = true;
|
||||
} else if (type === 'checkbox') {
|
||||
opts.FT = 'Btn';
|
||||
} else if (type === 'combo') {
|
||||
opts.FT = 'Ch';
|
||||
opts.combo = true;
|
||||
} else if (type === 'list') {
|
||||
opts.FT = 'Ch';
|
||||
} else {
|
||||
throw new Error(`Invalid form annotation type '${type}'`);
|
||||
}
|
||||
return opts;
|
||||
},
|
||||
|
||||
_resolveFormat(opts) {
|
||||
const f = opts.format;
|
||||
if (f && f.type) {
|
||||
let fnKeystroke;
|
||||
let fnFormat;
|
||||
let params = '';
|
||||
if (FORMAT_SPECIAL[f.type] !== undefined) {
|
||||
fnKeystroke = `AFSpecial_Keystroke`;
|
||||
fnFormat = `AFSpecial_Format`;
|
||||
params = FORMAT_SPECIAL[f.type];
|
||||
} else {
|
||||
let format = f.type.charAt(0).toUpperCase() + f.type.slice(1);
|
||||
fnKeystroke = `AF${format}_Keystroke`;
|
||||
fnFormat = `AF${format}_Format`;
|
||||
|
||||
if (f.type === 'date') {
|
||||
fnKeystroke += 'Ex';
|
||||
params = String(f.param);
|
||||
} else if (f.type === 'time') {
|
||||
params = String(f.param);
|
||||
} else if (f.type === 'number') {
|
||||
let p = Object.assign({}, FORMAT_DEFAULT.number, f);
|
||||
params = String(
|
||||
[
|
||||
String(p.nDec),
|
||||
p.sepComma ? '0' : '1',
|
||||
'"' + p.negStyle + '"',
|
||||
'null',
|
||||
'"' + p.currency + '"',
|
||||
String(p.currencyPrepend)
|
||||
].join(',')
|
||||
);
|
||||
} else if (f.type === 'percent') {
|
||||
let p = Object.assign({}, FORMAT_DEFAULT.percent, f);
|
||||
params = String([String(p.nDec), p.sepComma ? '0' : '1'].join(','));
|
||||
}
|
||||
}
|
||||
opts.AA = opts.AA ? opts.AA : {};
|
||||
opts.AA.K = {
|
||||
S: 'JavaScript',
|
||||
JS: new String(`${fnKeystroke}(${params});`)
|
||||
};
|
||||
opts.AA.F = {
|
||||
S: 'JavaScript',
|
||||
JS: new String(`${fnFormat}(${params});`)
|
||||
};
|
||||
}
|
||||
delete opts.format;
|
||||
return opts;
|
||||
},
|
||||
|
||||
_resolveColors(opts) {
|
||||
let color = this._normalizeColor(opts.backgroundColor);
|
||||
if (color) {
|
||||
if (!opts.MK) {
|
||||
opts.MK = {};
|
||||
}
|
||||
opts.MK.BG = color;
|
||||
}
|
||||
color = this._normalizeColor(opts.borderColor);
|
||||
if (color) {
|
||||
if (!opts.MK) {
|
||||
opts.MK = {};
|
||||
}
|
||||
opts.MK.BC = color;
|
||||
}
|
||||
delete opts.backgroundColor;
|
||||
delete opts.borderColor;
|
||||
return opts;
|
||||
},
|
||||
|
||||
_resolveFlags(options) {
|
||||
let result = 0;
|
||||
Object.keys(options).forEach(key => {
|
||||
if (FIELD_FLAGS[key]) {
|
||||
result |= FIELD_FLAGS[key];
|
||||
delete options[key];
|
||||
}
|
||||
});
|
||||
if (result !== 0) {
|
||||
options.Ff = options.Ff ? options.Ff : 0;
|
||||
options.Ff |= result;
|
||||
}
|
||||
return options;
|
||||
},
|
||||
|
||||
_resolveJustify(options) {
|
||||
let result = 0;
|
||||
if (options.align !== undefined) {
|
||||
if (typeof FIELD_JUSTIFY[options.align] === 'number') {
|
||||
result = FIELD_JUSTIFY[options.align];
|
||||
}
|
||||
delete options.align;
|
||||
}
|
||||
if (result !== 0) {
|
||||
options.Q = result; // default
|
||||
}
|
||||
return options;
|
||||
},
|
||||
|
||||
_resolveFont(options) {
|
||||
// add current font to document-level AcroForm dict if necessary
|
||||
if (this._acroform.fonts[this._font.id] === null) {
|
||||
this._acroform.fonts[this._font.id] = this._font.ref();
|
||||
}
|
||||
|
||||
// add current font to field's resource dict (RD) if not the default acroform font
|
||||
if (this._acroform.defaultFont !== this._font.name) {
|
||||
options.DR = { Font: {} };
|
||||
options.DR.Font[this._font.id] = this._font.ref();
|
||||
options.DA = new String(`/${this._font.id} 0 Tf 0 g`);
|
||||
}
|
||||
return options;
|
||||
},
|
||||
|
||||
_resolveStrings(options) {
|
||||
let select = [];
|
||||
function appendChoices(a) {
|
||||
if (Array.isArray(a)) {
|
||||
for (let idx = 0; idx < a.length; idx++) {
|
||||
if (typeof a[idx] === 'string') {
|
||||
select.push(new String(a[idx]));
|
||||
} else {
|
||||
select.push(a[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
appendChoices(options.Opt);
|
||||
if (options.select) {
|
||||
appendChoices(options.select);
|
||||
delete options.select;
|
||||
}
|
||||
if (select.length) {
|
||||
options.Opt = select;
|
||||
}
|
||||
|
||||
if (options.value || options.defaultValue) {
|
||||
let x = 0;
|
||||
}
|
||||
Object.keys(VALUE_MAP).forEach(key => {
|
||||
if (options[key] !== undefined) {
|
||||
options[VALUE_MAP[key]] = options[key];
|
||||
delete options[key];
|
||||
}
|
||||
});
|
||||
['V', 'DV'].forEach(key => {
|
||||
if (typeof options[key] === 'string') {
|
||||
options[key] = new String(options[key]);
|
||||
}
|
||||
});
|
||||
|
||||
if (options.MK && options.MK.CA) {
|
||||
options.MK.CA = new String(options.MK.CA);
|
||||
}
|
||||
if (options.label) {
|
||||
options.MK = options.MK ? options.MK : {};
|
||||
options.MK.CA = new String(options.label);
|
||||
delete options.label;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
};
|
||||
14
package.json
14
package.json
@ -57,7 +57,7 @@
|
||||
"website": "node docs/generate_website.js",
|
||||
"docs": "npm run pdf-guide && npm run website && npm run browser-demo",
|
||||
"prettier": "prettier {lib,tests,demo,docs}/**/*.js",
|
||||
"test": "jest -i",
|
||||
"test": "jest",
|
||||
"test:integration": "jest integration/ -i",
|
||||
"test:unit": "jest unit/ -i"
|
||||
},
|
||||
@ -71,15 +71,5 @@
|
||||
},
|
||||
"engine": [
|
||||
"node >= v6.0.0"
|
||||
],
|
||||
"jest": {
|
||||
"testPathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"<rootDir>/demo/"
|
||||
],
|
||||
"testURL": "http://localhost/",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/tests/unit/setupTests.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
277
tests/unit/acroform.spec.js
Normal file
277
tests/unit/acroform.spec.js
Normal file
@ -0,0 +1,277 @@
|
||||
import PDFDocument from '../../lib/document';
|
||||
import PDFSecurity from '../../lib/security';
|
||||
import { logData } from './helpers';
|
||||
import PDFFontFactory from '../../lib/font_factory';
|
||||
|
||||
// manual mock for PDFSecurity to ensure stored id will be the same accross different systems
|
||||
PDFSecurity.generateFileID = () => {
|
||||
return new Buffer('mocked-pdf-id');
|
||||
};
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
function joinTokens(...args) {
|
||||
let a = args.map(i => escapeRegExp(i));
|
||||
let r = new RegExp('^' + a.join('\\s*') + '$');
|
||||
return r;
|
||||
}
|
||||
|
||||
describe('acroform', () => {
|
||||
let doc;
|
||||
|
||||
beforeEach(() => {
|
||||
doc = new PDFDocument({
|
||||
info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }
|
||||
});
|
||||
});
|
||||
|
||||
test('named JavaScript', () => {
|
||||
const expected = [
|
||||
'2 0 obj',
|
||||
// '<<\n/Dests <<\n/Names []\n>>\n/JavaScript <<\n/Names [\n(name1) <<\n/JS (my javascript goes here)\n/S /JavaScript\n>>\n]\n>>\n>>',
|
||||
joinTokens(
|
||||
'<<',
|
||||
'/Dests',
|
||||
'<<',
|
||||
'/Names',
|
||||
'[',
|
||||
']',
|
||||
'>>',
|
||||
'/JavaScript',
|
||||
'<<',
|
||||
'/Names',
|
||||
'[',
|
||||
'(name1)',
|
||||
'<<',
|
||||
'/JS',
|
||||
'(my javascript goes here)',
|
||||
'/S',
|
||||
'/JavaScript',
|
||||
'>>',
|
||||
']',
|
||||
'>>',
|
||||
'>>'
|
||||
),
|
||||
'endobj'
|
||||
];
|
||||
const docData = logData(doc);
|
||||
doc.addNamedJavaScript('name1', 'my javascript goes here');
|
||||
expect(docData.length).toBe(0);
|
||||
doc.end();
|
||||
expect(docData).toContainChunk(expected);
|
||||
});
|
||||
|
||||
test('init no fonts', () => {
|
||||
doc.addPage();
|
||||
const docData = logData(doc);
|
||||
const font = PDFFontFactory.open(doc, 'tests/fonts/Roboto-Regular.ttf');
|
||||
doc.initForm();
|
||||
expect(docData.length).toBe(0);
|
||||
});
|
||||
|
||||
test('init standard fonts', () => {
|
||||
const expected = [
|
||||
'12 0 obj',
|
||||
joinTokens(
|
||||
'<<',
|
||||
'/FT',
|
||||
'/Tx',
|
||||
'/Ff',
|
||||
'4096',
|
||||
'/DR',
|
||||
'<<',
|
||||
'/Font',
|
||||
'<<',
|
||||
'/F3',
|
||||
'10 0 R',
|
||||
'>>',
|
||||
'>>',
|
||||
'/DA',
|
||||
'(/F3 0 Tf 0 g)',
|
||||
'/T',
|
||||
'(file0)',
|
||||
'/Subtype',
|
||||
'/Widget',
|
||||
'/F',
|
||||
'4',
|
||||
'/Type',
|
||||
'/Annot',
|
||||
'/Rect',
|
||||
'[10 292 602 692]',
|
||||
'>>'
|
||||
),
|
||||
'endobj'
|
||||
];
|
||||
|
||||
const docData = logData(doc);
|
||||
doc.registerFont('myfont1', 'tests/fonts/Roboto-Regular.ttf');
|
||||
|
||||
doc.font('Courier-Bold'); // establishes the default font
|
||||
doc.initForm();
|
||||
|
||||
doc
|
||||
.font('myfont1')
|
||||
.fontSize(25)
|
||||
.text('Test Doc', 0, 20, { width: 612, align: 'center' });
|
||||
doc
|
||||
.font('Courier')
|
||||
.fontSize(16)
|
||||
.text('Courier subheading', 0, 50, { width: 612, align: 'center' });
|
||||
|
||||
doc
|
||||
.font('myfont1')
|
||||
.formText('file0', 10, 100, 592, 400, { multiline: true });
|
||||
|
||||
expect(docData.length).toBe(3);
|
||||
expect(docData).toContainChunk(expected);
|
||||
});
|
||||
|
||||
test('push button', () => {
|
||||
const expected = [
|
||||
'10 0 obj',
|
||||
'<<\n/FT /Btn\n/Ff 65536\n/MK <<\n/CA (Test Button)\n/BG [1 1 0]\n>>\n/T (btn1)\n/Subtype /Widget\n/F 4\n/Type /Annot\n/Rect [20 742 120 772]\n>>',
|
||||
'endobj'
|
||||
];
|
||||
doc.initForm();
|
||||
const docData = logData(doc);
|
||||
let opts = {
|
||||
backgroundColor: 'yellow',
|
||||
label: 'Test Button'
|
||||
};
|
||||
doc.formPushButton('btn1', 20, 20, 100, 30, opts);
|
||||
expect(docData.length).toBe(3);
|
||||
expect(docData[0]).toBe(expected[0]);
|
||||
expect(docData[1]).toBe(expected[1]);
|
||||
expect(docData[2]).toBe(expected[2]);
|
||||
});
|
||||
|
||||
describe('text format', () => {
|
||||
test('number', () => {
|
||||
const expected = [
|
||||
'10 0 obj',
|
||||
'<<\n/FT /Tx\n/V 32.98\n/AA <<\n/K <<\n/S /JavaScript\n' +
|
||||
'/JS (AFNumber_Keystroke\\(2,1,"MinusBlack",null,"$",true\\);)\n>>\n' +
|
||||
'/F <<\n/S /JavaScript\n/JS (AFNumber_Format\\(2,1,"MinusBlack",null,"$",true\\);)\n>>\n>>\n' +
|
||||
'/T (dollars)\n/Subtype /Widget\n/F 4\n/Type /Annot\n/Rect [20 752 70 772]\n>>',
|
||||
'endobj'
|
||||
];
|
||||
doc.initForm();
|
||||
const docData = logData(doc);
|
||||
let opts = {
|
||||
value: 32.98,
|
||||
format: {
|
||||
type: 'number',
|
||||
nDec: 2,
|
||||
currency: '$',
|
||||
currencyPrepend: true
|
||||
}
|
||||
};
|
||||
doc.formText('dollars', 20, 20, 50, 20, opts);
|
||||
expect(docData.length).toBe(3);
|
||||
expect(docData).toContainChunk(expected);
|
||||
});
|
||||
test('date', () => {
|
||||
const expected = [
|
||||
'10 0 obj',
|
||||
'<<\n/FT /Tx\n/V (1999-12-31)\n/AA <<\n/K <<\n/S /JavaScript\n' +
|
||||
'/JS (AFDate_KeystrokeEx\\(yyyy-mm-dd\\);)\n>>\n' +
|
||||
'/F <<\n/S /JavaScript\n/JS (AFDate_Format\\(yyyy-mm-dd\\);)\n>>\n>>\n' +
|
||||
'/T (date)\n/Subtype /Widget\n/F 4\n/Type /Annot\n/Rect [20 752 70 772]\n>>',
|
||||
'endobj'
|
||||
];
|
||||
doc.initForm();
|
||||
const docData = logData(doc);
|
||||
let opts = {
|
||||
value: '1999-12-31',
|
||||
format: {
|
||||
type: 'date',
|
||||
param: 'yyyy-mm-dd'
|
||||
}
|
||||
};
|
||||
doc.formText('date', 20, 20, 50, 20, opts);
|
||||
expect(docData.length).toBe(3);
|
||||
expect(docData).toContainChunk(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test('flags', () => {
|
||||
const expected = [
|
||||
'10 0 obj',
|
||||
'<<\n/FT /Tx\n' +
|
||||
'/Ff 4206599\n/Q 1\n' +
|
||||
'/T (flags)\n/Subtype /Widget\n/F 4\n/Type /Annot\n/Rect [20 752 70 772]\n>>',
|
||||
'endobj'
|
||||
];
|
||||
doc.initForm();
|
||||
const docData = logData(doc);
|
||||
let opts = {
|
||||
required: true,
|
||||
noExport: true,
|
||||
readOnly: true,
|
||||
align: 'center',
|
||||
multiline: true,
|
||||
password: true,
|
||||
noSpell: true
|
||||
};
|
||||
doc.formText('flags', 20, 20, 50, 20, opts);
|
||||
expect(docData.length).toBe(3);
|
||||
expect(docData).toContainChunk(expected);
|
||||
});
|
||||
|
||||
test('field heirarchy', () => {
|
||||
const expected = [
|
||||
'13 0 obj',
|
||||
'<<\n/Parent 11 0 R\n/FT /Tx\n/T (leaf1)\n/Subtype /Widget\n/F 4\n/Type /Annot\n/Rect [10 742 210 782]\n>>',
|
||||
'endobj',
|
||||
'14 0 obj',
|
||||
'<<\n/Parent 11 0 R\n/FT /Tx\n/T (leaf2)\n/Subtype /Widget\n/F 4\n/Type /Annot\n/Rect [10 692 210 732]\n>>',
|
||||
'endobj',
|
||||
'15 0 obj',
|
||||
'<<\n/Parent 12 0 R\n/FT /Tx\n/T (leaf3)\n/Subtype /Widget\n/F 4\n/Type /Annot\n/Rect [10 642 210 682]\n>>',
|
||||
'endobj'
|
||||
];
|
||||
const expected2 = [
|
||||
'11 0 obj',
|
||||
'<<\n/Parent 10 0 R\n/T (child1Field)\n/Kids [13 0 R 14 0 R]\n>>',
|
||||
'endobj',
|
||||
'12 0 obj',
|
||||
'<<\n/Parent 10 0 R\n/T (child2Field)\n/Kids [15 0 R]\n>>',
|
||||
'endobj',
|
||||
'10 0 obj',
|
||||
'<<\n/T (rootField)\n/Kids [11 0 R 12 0 R]\n>>',
|
||||
'endobj',
|
||||
'9 0 obj',
|
||||
'<<\n/Fields [10 0 R]\n/NeedAppearances true\n/DA (/F1 0 Tf 0 g)\n/DR <<\n/Font <<\n/F1 8 0 R\n>>\n>>\n>>',
|
||||
'endobj'
|
||||
];
|
||||
|
||||
const docData = logData(doc);
|
||||
|
||||
doc.font('Helvetica'); // establishes the default font
|
||||
doc.initForm();
|
||||
|
||||
let rootField = doc.formField('rootField');
|
||||
let child1Field = doc.formField('child1Field', { Parent: rootField });
|
||||
let child2Field = doc.formField('child2Field', { Parent: rootField });
|
||||
doc.formText('leaf1', 10, 10, 200, 40, { Parent: child1Field });
|
||||
doc.formText('leaf2', 10, 60, 200, 40, { Parent: child1Field });
|
||||
doc.formText('leaf3', 10, 110, 200, 40, { Parent: child2Field });
|
||||
|
||||
expect(docData.length).toBe(expected.length);
|
||||
for (let idx = 0; idx < expected.length; ++idx) {
|
||||
expect(docData[idx]).toBe(expected[idx]);
|
||||
}
|
||||
|
||||
doc.end();
|
||||
|
||||
for (let idx = 0; idx < docData.length; ++idx) {
|
||||
if (docData[idx] === expected2[0]) {
|
||||
for (let jdx = 0; jdx < expected2.length; ++jdx) {
|
||||
expect(docData[idx + jdx]).toBe(expected2[jdx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -1,3 +1,4 @@
|
||||
import matcher from './toContainChunk';
|
||||
|
||||
|
||||
expect.extend(matcher);
|
||||
|
||||
@ -30,12 +30,16 @@ const failMessage = (utils, data, chunk, headIndex) => () => {
|
||||
};
|
||||
|
||||
export default {
|
||||
toContainChunk(data, chunk) {
|
||||
toContainChunk (data, chunk) {
|
||||
const headIndex = data.indexOf(chunk[0]);
|
||||
let pass = headIndex !== -1;
|
||||
if (pass) {
|
||||
for (let i = 1; i < chunk.length; ++i) {
|
||||
pass = pass && this.equals(data[headIndex + i], chunk[i]);
|
||||
if (chunk[i] instanceof RegExp) {
|
||||
pass = pass && chunk[i].test(data[headIndex + i]);
|
||||
} else {
|
||||
pass = pass && this.equals(data[headIndex + i], chunk[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user