import PDFDocument from 'src/api/PDFDocument' ; import PDFFont from 'src/api/PDFFont' ; import { AppearanceMapping } from 'src/api/form/appearances' ; import { Color , colorToComponents , setFillingColor } from 'src/api/colors' ; import { Rotation , toDegrees , rotateRectangle , reduceRotation , adjustDimsForRotation , degrees , } from 'src/api/rotations' ; import { PDFRef , PDFWidgetAnnotation , PDFOperator , PDFName , PDFDict , MethodNotImplementedError , AcroFieldFlags , PDFAcroTerminal , AnnotationFlags , } from 'src/core' ; import { assertIs , assertMultiple , assertOrUndefined } from 'src/utils' ; import { ImageAlignment } from '../image' ; import PDFImage from '../PDFImage' ; import { drawImage , rotateInPlace } from '../operations' ; export interface FieldAppearanceOptions { x ? : number ; y ? : number ; width ? : number ; height ? : number ; textColor ? : Color ; backgroundColor ? : Color ; borderColor ? : Color ; borderWidth ? : number ; rotate ? : Rotation ; font ? : PDFFont ; hidden ? : boolean ; } export const assertFieldAppearanceOptions = ( options ? ) => { assertOrUndefined ( options ?. x , 'options.x' , [ 'number' ] ) ; assertOrUndefined ( options ?. y , 'options.y' , [ 'number' ] ) ; assertOrUndefined ( options ?. width , 'options.width' , [ 'number' ] ) ; assertOrUndefined ( options ?. height , 'options.height' , [ 'number' ] ) ; assertOrUndefined ( options ?. textColor , 'options.textColor' , [ [ Object , 'Color' ] , ] ) ; assertOrUndefined ( options ?. backgroundColor , 'options.backgroundColor' , [ [ Object , 'Color' ] , ] ) ; assertOrUndefined ( options ?. borderColor , 'options.borderColor' , [ [ Object , 'Color' ] , ] ) ; assertOrUndefined ( options ?. borderWidth , 'options.borderWidth' , [ 'number' ] ) ; assertOrUndefined ( options ?. rotate , 'options.rotate' , [ [ Object , 'Rotation' ] ] ) ; } ; /**
 * Represents a field of a [[PDFForm]].
 *
 * This class is effectively abstract. All fields in a [[PDFForm]] will
 * actually be an instance of a subclass of this class.
 *
 * Note that each field in a PDF is represented by a single field object.
 * However, a given field object may be rendered at multiple locations within
 * the document (across one or more pages). The rendering of a field is
 * controlled by its widgets. Each widget causes its field to be displayed at a
 * particular location in the document.
 *
 * Most of the time each field in a PDF has only a single widget, and thus is
 * only rendered once. However, if a field is rendered multiple times, it will
 * have multiple widgets - one for each location it is rendered.
 *
 * This abstraction of field objects and widgets is defined in the PDF
 * specification and dictates how PDF files store fields and where they are
 * to be rendered.
 */ export default class PDFField { /** The low-level PDFAcroTerminal wrapped by this field. */ readonly acroField : PDFAcroTerminal ; /** The unique reference assigned to this field within the document. */ readonly ref : PDFRef ; /** The document to which this field belongs. */ readonly doc : PDFDocument ; protected constructor ( acroField , ref , doc ) { assertIs ( acroField , 'acroField' , [ [ PDFAcroTerminal , 'PDFAcroTerminal' ] ] ) ; assertIs ( ref , 'ref' , [ [ PDFRef , 'PDFRef' ] ] ) ; assertIs ( doc , 'doc' , [ [ PDFDocument , 'PDFDocument' ] ] ) ; this . acroField = acroField ; this . ref = ref ; this . doc = doc ; } returns getName ( ) { return this . acroField . getFullyQualifiedName ( ) ?? '' ; } returns isReadOnly ( ) { return this . acroField . hasFlag ( AcroFieldFlags . ReadOnly ) ; } /**
     * Prevent PDF readers from allowing users to interact with this field or
     * change its value. The field will not respond to mouse or keyboard input.
     * For example:
     * ```js
     * const field = form.getField('some.field')
     * field.enableReadOnly()
     * ```
     * Useful for fields whose values are computed, imported from a database, or
     * prefilled by software before being displayed to the user.
     */ enableReadOnly ( ) { this . acroField . setFlagTo ( AcroFieldFlags . ReadOnly , true ) ; } /**
     * Allow users to interact with this field and change its value in PDF
     * readers via mouse and keyboard input. For example:
     * ```js
     * const field = form.getField('some.field')
     * field.disableReadOnly()
     * ```
     */ disableReadOnly ( ) { this . acroField . setFlagTo ( AcroFieldFlags . ReadOnly , false ) ; } returns isRequired ( ) { return this . acroField . hasFlag ( AcroFieldFlags . Required ) ; } /**
     * Require this field to have a value when the form is submitted.
     * For example:
     * ```js
     * const field = form.getField('some.field')
     * field.enableRequired()
     * ```
     */ enableRequired ( ) { this . acroField . setFlagTo ( AcroFieldFlags . Required , true ) ; } /**
     * Do not require this field to have a value when the form is submitted.
     * For example:
     * ```js
     * const field = form.getField('some.field')
     * field.disableRequired()
     * ```
     */ disableRequired ( ) { this . acroField . setFlagTo ( AcroFieldFlags . Required , false ) ; } returns isExported ( ) { return ! this . acroField . hasFlag ( AcroFieldFlags . NoExport ) ; } /**
     * Indicate that this field's value should be exported when the form is
     * submitted in a PDF reader. For example:
     * ```js
     * const field = form.getField('some.field')
     * field.enableExporting()
     * ```
     */ enableExporting ( ) { this . acroField . setFlagTo ( AcroFieldFlags . NoExport , false ) ; } /**
     * Indicate that this field's value should **not** be exported when the form
     * is submitted in a PDF reader. For example:
     * ```js
     * const field = form.getField('some.field')
     * field.disableExporting()
     * ```
     */ disableExporting ( ) { this . acroField . setFlagTo ( AcroFieldFlags . NoExport , true ) ; } ignore needsAppearancesUpdate ( ) { throw new MethodNotImplementedError ( this . constructor . name , 'needsAppearancesUpdate' ) ; } ignore defaultUpdateAppearances ( _font ) { throw new MethodNotImplementedError ( this . constructor . name , 'defaultUpdateAppearances' ) ; } protected markAsDirty ( ) { this . doc . getForm ( ) . markFieldAsDirty ( this . ref ) ; } protected markAsClean ( ) { this . doc . getForm ( ) . markFieldAsClean ( this . ref ) ; } protected isDirty ( ) { return this . doc . getForm ( ) . fieldIsDirty ( this . ref ) ; } protected createWidget ( options ) { const textColor = options . textColor ; const backgroundColor = options . backgroundColor ; const borderColor = options . borderColor ; const borderWidth = options . borderWidth ; const degreesAngle = toDegrees ( options . rotate ) ; const caption = options . caption ; const x = options . x ; const y = options . y ; const width = options . width + borderWidth ; const height = options . height + borderWidth ; const hidden = Boolean ( options . hidden ) ; const pageRef = options . page ; assertMultiple ( degreesAngle , 'degreesAngle' , 90 ) ; const widget = PDFWidgetAnnotation . create ( this . doc . context , this . ref ) ; const rect = rotateRectangle ( { x , y , width , height } , borderWidth , degreesAngle ) ; widget . setRectangle ( rect ) ; if ( pageRef ) widget . setP ( pageRef ) ; const ac = widget . getOrCreateAppearanceCharacteristics ( ) ; if ( backgroundColor ) { ac . setBackgroundColor ( colorToComponents ( backgroundColor ) ) ; } ac . setRotation ( degreesAngle ) ; if ( caption ) ac . setCaptions ( { normal : caption } ) ; if ( borderColor ) ac . setBorderColor ( colorToComponents ( borderColor ) ) ; const bs = widget . getOrCreateBorderStyle ( ) ; if ( borderWidth !== undefined ) bs . setWidth ( borderWidth ) ; widget . setFlagTo ( AnnotationFlags . Print , true ) ; widget . setFlagTo ( AnnotationFlags . Hidden , hidden ) ; widget . setFlagTo ( AnnotationFlags . Invisible , false ) ; if ( textColor ) { const da = this . acroField . getDefaultAppearance ( ) ?? '' ; const newDa = da + '\n' + setFillingColor ( textColor ) . toString ( ) ; this . acroField . setDefaultAppearance ( newDa ) ; } return widget ; } protected updateWidgetAppearanceWithFont ( widget , font , { normal , rollover , down } ) { this . updateWidgetAppearances ( widget , { normal : this . createAppearanceStream ( widget , normal , font ) , rollover : rollover && this . createAppearanceStream ( widget , rollover , font ) , down : down && this . createAppearanceStream ( widget , down , font ) , } ) ; } protected updateOnOffWidgetAppearance ( widget , onValue , { normal , rollover , down , } ) { this . updateWidgetAppearances ( widget , { normal : this . createAppearanceDict ( widget , normal , onValue ) , rollover : rollover && this . createAppearanceDict ( widget , rollover , onValue ) , down : down && this . createAppearanceDict ( widget , down , onValue ) , } ) ; } protected updateWidgetAppearances ( widget , { normal , rollover , down } ) { widget . setNormalAppearance ( normal ) ; if ( rollover ) { widget . setRolloverAppearance ( rollover ) ; } else { widget . removeRolloverAppearance ( ) ; } if ( down ) { widget . setDownAppearance ( down ) ; } else { widget . removeDownAppearance ( ) ; } } private createAppearanceStream ( widget , appearance , font ? ) { const { context } = this . acroField . dict ; const { width , height } = widget . getRectangle ( ) ; const Resources = font && { Font : { [ font . name ] : font . ref } } ; const stream = context . formXObject ( appearance , { Resources , BBox : context . obj ( [ 0 , 0 , width , height ] ) , Matrix : context . obj ( [ 1 , 0 , 0 , 1 , 0 , 0 ] ) , } ) ; const streamRef = context . register ( stream ) ; return streamRef ; } param widget param alignment param image returns protected createImageAppearanceStream ( widget , image , alignment ) { const { context } = this . acroField . dict ; const rectangle = widget . getRectangle ( ) ; const ap = widget . getAppearanceCharacteristics ( ) ; const bs = widget . getBorderStyle ( ) ; const borderWidth = bs ?. getWidth ( ) ?? 0 ; const rotation = reduceRotation ( ap ?. getRotation ( ) ) ; const rotate = rotateInPlace ( { ... rectangle , rotation } ) ; const adj = adjustDimsForRotation ( rectangle , rotation ) ; const imageDims = image . scaleToFit ( adj . width - borderWidth * 2 , adj . height - borderWidth * 2 ) ; const options = { x : borderWidth , y : borderWidth , width : imageDims . width , height : imageDims . height , rotate : degrees ( 0 ) , xSkew : degrees ( 0 ) , ySkew : degrees ( 0 ) , } ; if ( alignment === ImageAlignment . Center ) { options . x += ( adj . width - borderWidth * 2 ) / 2 - imageDims . width / 2 ; options . y += ( adj . height - borderWidth * 2 ) / 2 - imageDims . height / 2 ; } else if ( alignment === ImageAlignment . Right ) { options . x = adj . width - borderWidth - imageDims . width ; options . y = adj . height - borderWidth - imageDims . height ; } const imageName = this . doc . context . addRandomSuffix ( 'Image' , 10 ) ; const appearance = [ ... rotate , ... drawImage ( imageName , options ) ] ; const Resources = { XObject : { [ imageName ] : image . ref } } ; const stream = context . formXObject ( appearance , { Resources , BBox : context . obj ( [ 0 , 0 , rectangle . width , rectangle . height ] ) , Matrix : context . obj ( [ 1 , 0 , 0 , 1 , 0 , 0 ] ) , } ) ; return context . register ( stream ) ; } private createAppearanceDict ( widget , appearance , onValue ) { const { context } = this . acroField . dict ; const onStreamRef = this . createAppearanceStream ( widget , appearance . on ) ; const offStreamRef = this . createAppearanceStream ( widget , appearance . off ) ; const appearanceDict = context . obj ( { } ) ; appearanceDict . set ( onValue , onStreamRef ) ; appearanceDict . set ( PDFName . of ( 'Off' ) , offStreamRef ) ; return appearanceDict ; } }