Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added Form Annotation support #2845

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

natterstefan
Copy link

Tasks

  • fix dependency cycle (also mentioned here). @diegomura: I am not sure how though... #need-help

Notes

Docs

Copy link

changeset-bot bot commented Aug 18, 2024

⚠️ No Changeset found

Latest commit: d9a1e84

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Comment on lines +49 to +52
if (!!this._root.data.AcroForm) {
// Form is already initialized
return this;
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes an issue when using <Form /> twice on e.g. two pages.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not check this inside the render package itself?
That way, it would not interfere with this issue: #2613

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And what about removing the <Form /> component? Its sole purpose is initializing on the document. Shouldn't it be a property on the <Document /> then, or maybe even just a check inside the form components?

Comment on lines 11 to 16
export const Form = 'FORM';
export const FormField = 'FORM_FIELD';
export const TextInput = 'TEXT_INPUT';
export const FormPushButton = 'FORM_PUSH_BUTTON';
export const Picker = 'PICKER';
export const FormList = 'FORM_LIST';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of keeping the initial names of these components to keep them aligned with the form annotation methods of PDFKit?

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)

(src)

Comment on lines +22 to +26
const isRecursiveNode = (node) =>
node.type !== P.Text &&
node.type !== P.Svg &&
node.type !== P.Form &&
node.type !== P.FormField;
Copy link
Author

@natterstefan natterstefan Aug 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be solved by introducing a cleanUp function similar to the render function in the renderNode.js, which would be called after render the component and childs. This way a form field would not need to render its own childs.

Suggested change
const isRecursiveNode = (node) =>
node.type !== P.Text &&
node.type !== P.Svg &&
node.type !== P.Form &&
node.type !== P.FormField;
const isRecursiveNode = (node) =>
node.type !== P.Text &&
node.type !== P.Svg &&
node.type !== P.Form;
const cleanUpFns = {
[P.FormField]: cleanUpFormField
}
// inside renderNode():
const renderFn = renderFns[node.type];
if (renderFn) renderFn(ctx, node, options);
if (shouldRenderChildren) renderChildren(ctx, node, options);
const cleanUpFn = cleanUpFns[node.type];
if (cleanUpFn) cleanUpFn(ctx, node, options);
// renderFormField.js:
const renderFormField = (ctx, node, options) => {
const name = node.props?.name || '';
const formField = ctx.formField(name);
const option = options;
if(!option.formFields)
option.formFields = [formField]
else
option.formFields.push(formField)
};
const cleanUpFormField = (_ctx, _node, options) => {
options.formFields.pop()
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the form text it would then need something like this: options.formFields?.at(0)

Comment on lines +15 to +16
import renderForm from './form/renderForm';
import renderFormField from './form/renderFormField';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO

  • fix Dependency cycle

Comment on lines +48 to +50
<Text>Checkbox</Text>
{/* TODO: how to check automatically? */}
<Checkbox name="checkbox" style={{ height: '20px' }} />
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • example: how to set checked?

Copy link

@PhilippBloss PhilippBloss Aug 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be value or defaultValue, but then we face the problem of appearance. Like described in the pdfkit docs the appearance is not supported in the initial release and so pdfkit just used the NeedAppearances flag to ask the viewer for appearances. “The NeedAppearances flag may not be honored by all PDF viewers”~ from the appearance section. This means that we cannot set a value without providing appearances, which means we need to set the AS and AP values. My suggestion for the parseFormOptions.js would be:

const getAppearance = (ctx, data) => {
  let appearance = ctx.ref({
    Type: 'XObject',
    Subtype: 'Form',
    BBox: [0, 0, 11.1, 11.1],
    Resources: {
      ProcSet: ['PDF', 'Text', 'ImageB', 'ImageC', 'ImageI']
    }
  });
  appearance.initDeflate();
  appearance.write(data);
  appearance.end();
  return appearance
}

const parseCheckboxOptions = (ctx, node, formField) => {
  let onOption = node.props?.onState || 'Yes';
  let offOption = node.props?.offState || 'Off';

  let normalAppearance = {}
  normalAppearance[onOption] = getAppearance(ctx, '/Tx BMC\nq BT\n0 0 0 rg /F1 11.1 Tf\n1.8 1.8 Td (8) Tj\nET\nQ\nEMC');
  normalAppearance[offOption] = getAppearance(ctx, '/Tx BMC\nEMC\n');

  return clean({
    ...parseCommonFormOptions(node),
    backgroundColor: node.props?.backgroundColor || undefined,
    borderColor: node.props?.borderColor || undefined,
    parent: formField || undefined,
    value: '/' + node.props?.checked === true ? onOption : offOption,
    defaultValue: '/' + node.props?.checked === true ? onOption : offOption,
    AS: node.props?.checked === true ? onOption : offOption,
    AP: {
      N: normalAppearance
    }
  });
};

// typescript definition

interface CheckboxProps extends FormCommonProps {
    backGroundColor?: string;
    borderColor?: string;
    checked?: boolean;
    onState?: string;
    offState?: string;
  }

//renderChecbox.js
  parseCheckboxOptions(ctx, node, options.formField)

And yes AS needs to be without / and the values with /. If you provide a / for AS pdfkit writes two but not for the values for some reason.
The default naming Yes and Off comes from the PDF Reference 1.7 page 686 under “Check Boxes”. It is not required but recommended, so I gave the user the option to change it via props. I tested it with your example.

Edit:
Had to update the appearance because pdfkit wants the FlateDecode via function call not dictionary

@PhilippBloss
Copy link

Right now, push buttons don't have any purpose. They are only there to exist. They cannot hold any value. There should be an option for the user to specify an 8.6.4 action (from PDF Reference 1.7) for this button. Even if it's just on a basic level. Going too detailed e.g. with 8.5.3 actions would go beyond the scope of the pr. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants