/home/tuzdhajd/ablacktime.com/wp-content/plugins/ninja-forms/includes/Fields/Signature.php
<?php if ( ! defined( 'ABSPATH' ) ) exit;
/**
* Class NF_Fields_Signature
*
* Provides signature capture functionality with both typed and drawn signature options.
* This field is intended for collecting acknowledgments, confirmations, and user consent
* in a visual format. It is NOT designed to create legally binding signatures or meet
* legal requirements for electronic signatures (e.g., e-signature laws, digital certificates).
*
* Features:
* - Typed signatures: User types their name, displayed with custom fonts
* - Drawn signatures: User draws signature on a canvas
* - Method selection: Can be configured for typed-only, drawn-only, or both
*
* Data Storage:
* - Signatures are stored as JSON with the following structure:
* {
* "signature_type": "typed" | "drawn",
* "typed_name": "John Doe" (for typed),
* "signature_font": "dancing-script" (for typed),
* "signature_data": "data:image/png;base64,..." (for drawn),
* "canvas_dimensions": {"width": 400, "height": 150} (for drawn),
* "timestamp": "2024-01-01T12:00:00Z"
* }
*
* Export Formats:
* - CSV: "Method: typed/drawn | Signed: true/false | Value: [name if typed]"
* - PDF: Text representation (e.g., "John Doe (Typed signature - dancing-script font)")
* - Email/HTML: Full HTML display with styled text or image
*
* Usage in HTML fields:
* - Use merge tag format: {field:signature_field_key} or {field:signature_field_id}
* - In HTML emails: Displays styled text (typed) or base64 image (drawn)
* - In plain text emails: Shows text representation only
* - Example: <p>Customer Signature: {field:signature_1}</p>
*
* Repeater Field Support:
* - Fully supports signature fields within repeater fields
* - Maintains consistent formatting across all export types
*
* @since 3.x
*/
class NF_Fields_Signature extends NF_Abstracts_Input
{
protected $_name = 'signature';
protected $_type = 'signature';
protected $_section = 'common';
protected $_icon = 'pencil-square-o';
protected $_templates = 'signature';
protected $_test_value = '{"signature_type":"typed","typed_name":"John Doe","signature_font":"dancing-script"}';
// Constants for validation and limits
const MAX_SIGNATURE_SIZE = 137000; // ~100KB in base64
const MAX_NAME_LENGTH = 100;
const VALID_SIGNATURE_TYPES = array( 'drawn', 'typed' );
const VALID_FONTS = array( 'dancing-script', 'satisfy', 'cursive' );
protected $_settings = array(
'signature_method',
'signature_font',
'typed_placeholder',
'drawn_placeholder',
'canvas_width',
'canvas_height',
'pen_color',
'background_color'
);
protected $_settings_all_fields = array(
'key', 'label', 'label_pos', 'required', 'classes', 'manual_key', 'admin_label', 'help', 'description',
'signature_method',
'signature_font',
'typed_placeholder',
'drawn_placeholder',
'canvas_width',
'canvas_height',
'pen_color',
'background_color'
);
public function __construct()
{
parent::__construct();
$this->_nicename = esc_html__( 'Signature', 'ninja-forms' );
// Add default field settings
$this->_settings[ 'label_pos' ][ 'value' ] = 'above';
$this->_settings[ 'required' ][ 'value' ] = 0;
// Add filter to localize field settings
add_filter( 'ninja_forms_localize_field_' . $this->_name, array( $this, 'localize_field_settings' ) );
// Primary filter for merge tag display (handles all signature merge tag conversions)
add_filter( 'ninja_forms_merge_tag_value_' . $this->_type, array( $this, 'filter_merge_tag_value' ), 10, 2 );
// Add filter for CSV export
add_filter( 'ninja_forms_subs_export_field_value_' . $this->_type, array( $this, 'filter_csv_export_value' ), 10, 2 );
// Add filter for PDF export
add_filter( 'ninja_forms_pdf_field_value_' . $this->_type, array( $this, 'filter_pdf_value' ), 10, 2 );
// Add filter for PDF field value display - this is the main handler
add_filter( 'ninja_forms_pdf_field_value', array( $this, 'filter_pdf_field_display' ), 10, 3 );
// Disable wpautop for signature fields in PDF
add_filter( 'ninja_forms_pdf_field_value_wpautop', array( $this, 'disable_wpautop_for_signatures' ), 10, 3 );
// Add signature to the list of HTML-safe fields
add_filter( 'ninja_forms_get_html_safe_fields', array( $this, 'add_to_safe_fields' ), 10, 1 );
// Add filter for CPT custom columns display
add_filter( 'ninja_forms_custom_columns', array( $this, 'custom_columns' ), 10, 3 );
}
/**
* Localize field settings for frontend
*/
public function localize_field_settings( $settings )
{
// Ensure signature-specific settings are passed to frontend
$signature_settings = [
'signature_method',
'signature_font',
'typed_placeholder',
'drawn_placeholder',
'canvas_width',
'canvas_height',
'pen_color',
'background_color'
];
foreach ( $signature_settings as $setting ) {
if ( ! isset( $settings[ $setting ] ) && isset( $this->_settings[ $setting ][ 'value' ] ) ) {
$settings[ $setting ] = $this->_settings[ $setting ][ 'value' ];
}
}
// If there's a value that looks like JSON, process it for display
if ( isset( $settings['value'] ) && is_string( $settings['value'] ) && strpos( $settings['value'], '{' ) === 0 ) {
// Store the raw value for potential merge tag processing
$settings['raw_signature_value'] = $settings['value'];
}
return $settings;
}
/**
* Validate field
*/
public function validate( $field, $data )
{
$errors = parent::validate( $field, $data );
// Get field value
$value = $field[ 'value' ];
// If field is required and empty
if ( 1 == $field[ 'required' ] && empty( $value ) ) {
$errors[] = esc_html__( 'Please provide a signature', 'ninja-forms' );
return $errors;
}
// If not empty, validate signature data
if ( ! empty( $value ) ) {
$signature_data = json_decode( $value, true );
if ( ! is_array( $signature_data ) || ! isset( $signature_data[ 'signature_type' ] ) ) {
$errors[] = esc_html__( 'Invalid signature data', 'ninja-forms' );
return $errors;
}
// Validate signature type
if ( ! in_array( $signature_data[ 'signature_type' ], self::VALID_SIGNATURE_TYPES ) ) {
$errors[] = esc_html__( 'Invalid signature type', 'ninja-forms' );
return $errors;
}
// Validate based on signature type
if ( 'typed' === $signature_data[ 'signature_type' ] ) {
if ( empty( $signature_data[ 'typed_name' ] ) ) {
if ( 1 == $field[ 'required' ] ) {
$errors[] = esc_html__( 'Please provide a signature', 'ninja-forms' );
} else {
$errors[] = esc_html__( 'Please type your name', 'ninja-forms' );
}
}
// Check name length
if ( isset( $signature_data[ 'typed_name' ] ) && strlen( $signature_data[ 'typed_name' ] ) > self::MAX_NAME_LENGTH ) {
$errors[] = esc_html__( 'Name is too long', 'ninja-forms' );
}
} elseif ( 'drawn' === $signature_data[ 'signature_type' ] ) {
if ( empty( $signature_data[ 'signature_data' ] ) ||
! preg_match( '/^data:image\/(png|jpeg);base64,/', $signature_data[ 'signature_data' ] ) ) {
if ( 1 == $field[ 'required' ] ) {
$errors[] = esc_html__( 'Please provide a signature', 'ninja-forms' );
} else {
$errors[] = esc_html__( 'Please draw your signature', 'ninja-forms' );
}
}
// Check signature size
if ( isset( $signature_data[ 'signature_data' ] ) &&
strlen( $signature_data[ 'signature_data' ] ) > self::MAX_SIGNATURE_SIZE ) {
$errors[] = esc_html__( 'Signature data is too large', 'ninja-forms' );
}
}
}
return $errors;
}
/**
* Sanitize field value
*/
public function sanitize_field_value( $value )
{
if ( empty( $value ) ) {
return '';
}
// Decode JSON
$signature_data = json_decode( $value, true );
if ( ! is_array( $signature_data ) ) {
return '';
}
$sanitized = [];
// Validate and sanitize signature type
$sanitized[ 'signature_type' ] = in_array( $signature_data[ 'signature_type' ], self::VALID_SIGNATURE_TYPES )
? $signature_data[ 'signature_type' ]
: 'typed';
// Sanitize based on type
if ( 'drawn' === $sanitized[ 'signature_type' ] ) {
// Validate base64 image data
if ( isset( $signature_data[ 'signature_data' ] ) &&
preg_match( '/^data:image\/(png|jpeg);base64,(.+)/', $signature_data[ 'signature_data' ], $matches ) ) {
// Limit image data size
if ( strlen( $signature_data[ 'signature_data' ] ) < self::MAX_SIGNATURE_SIZE ) {
// Additional validation: verify base64 can be decoded
$decoded = base64_decode( $matches[2], true );
if ( false !== $decoded && ! empty( $decoded ) ) {
$sanitized[ 'signature_data' ] = $signature_data[ 'signature_data' ];
}
}
}
// Sanitize canvas dimensions
if ( isset( $signature_data[ 'canvas_dimensions' ] ) && is_array( $signature_data[ 'canvas_dimensions' ] ) ) {
$width = isset( $signature_data[ 'canvas_dimensions' ][ 'width' ] ) ?
absint( $signature_data[ 'canvas_dimensions' ][ 'width' ] ) : 400;
$height = isset( $signature_data[ 'canvas_dimensions' ][ 'height' ] ) ?
absint( $signature_data[ 'canvas_dimensions' ][ 'height' ] ) : 150;
// Clamp to reasonable ranges
$width = max( 100, min( 2000, $width ) );
$height = max( 50, min( 1000, $height ) );
$sanitized[ 'canvas_dimensions' ] = [
'width' => $width,
'height' => $height
];
}
} elseif ( 'typed' === $sanitized[ 'signature_type' ] ) {
// Sanitize typed name
if ( isset( $signature_data[ 'typed_name' ] ) ) {
$sanitized[ 'typed_name' ] = sanitize_text_field( $signature_data[ 'typed_name' ] );
}
// Sanitize font
if ( isset( $signature_data[ 'signature_font' ] ) ) {
$sanitized[ 'signature_font' ] = in_array( $signature_data[ 'signature_font' ], self::VALID_FONTS )
? $signature_data[ 'signature_font' ]
: 'dancing-script';
}
}
// Add metadata
if ( isset( $signature_data[ 'timestamp' ] ) ) {
// Validate timestamp format (ISO 8601 or Unix timestamp)
$timestamp = $signature_data[ 'timestamp' ];
if ( is_numeric( $timestamp ) || preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', $timestamp ) ) {
$sanitized[ 'timestamp' ] = sanitize_text_field( $timestamp );
}
}
return wp_json_encode( $sanitized );
}
/**
* Format value for display in the legacy admin interface
*
* Note: This method is used by the old WordPress admin submission view interface
* (WordPress Admin → Ninja Forms → Submissions → View individual submission).
* The modern submission management interface uses a React component instead,
* but this method is kept for backwards compatibility.
*
* @param array $field Field data
* @param string $value Field value (JSON string)
* @return string HTML formatted signature for display
*/
public function admin_form_element( $field, $value )
{
if ( empty( $value ) ) {
return '<div style="color: #999; font-style: italic;">' . esc_html__( 'Not signed', 'ninja-forms' ) . '</div>';
}
// Decode HTML entities if present
$decoded_value = html_entity_decode( $value, ENT_QUOTES, 'UTF-8' );
$signature_data = json_decode( $decoded_value, true );
// Try with stripslashes if needed
if ( ! is_array( $signature_data ) && is_string( $value ) ) {
$signature_data = json_decode( stripslashes( $value ), true );
}
// If still not valid, try original value
if ( ! is_array( $signature_data ) ) {
$signature_data = json_decode( $value, true );
}
if ( ! is_array( $signature_data ) || ! isset( $signature_data['signature_type'] ) ) {
return '<div style="color: #999; font-style: italic;">' . esc_html__( 'Invalid signature data', 'ninja-forms' ) . '</div>';
}
$output = '';
$method = $signature_data['signature_type'];
// Display typed signature
if ( 'typed' === $method && isset( $signature_data['typed_name'] ) && ! empty( $signature_data['typed_name'] ) ) {
$font = isset( $signature_data['signature_font'] ) ? $signature_data['signature_font'] : 'dancing-script';
$font_family = $this->get_signature_font_family( $font );
$output .= '<div style="margin-bottom: 15px;">';
$output .= '<div style="font-family: ' . esc_attr( $font_family ) . '; font-size: 32px; color: #000; padding: 15px; background: transparent; display: inline-block;">';
$output .= esc_html( $signature_data['typed_name'] );
$output .= '</div>';
$output .= '<div style="margin-top: 8px; font-size: 12px; color: #666;">';
$output .= '<strong>' . esc_html__( 'Type:', 'ninja-forms' ) . '</strong> ' . esc_html__( 'Typed signature', 'ninja-forms' );
$output .= '</div>';
$output .= '</div>';
}
// Display drawn signature
elseif ( 'drawn' === $method && isset( $signature_data['signature_data'] ) && ! empty( $signature_data['signature_data'] ) ) {
if ( preg_match( '/^data:image\/(png|jpeg);base64,/', $signature_data['signature_data'] ) ) {
$output .= '<div style="margin-bottom: 15px;">';
$output .= '<img src="' . esc_attr( $signature_data['signature_data'] ) . '" ';
$output .= 'alt="' . esc_attr__( 'Signature', 'ninja-forms' ) . '" ';
$output .= 'style="max-width: 400px; height: auto; display: block;" />';
$output .= '<div style="margin-top: 8px; font-size: 12px; color: #666;">';
$output .= '<strong>' . esc_html__( 'Type:', 'ninja-forms' ) . '</strong> ' . esc_html__( 'Drawn signature', 'ninja-forms' );
$output .= '</div>';
$output .= '</div>';
} else {
return '<div style="color: #999; font-style: italic;">' . esc_html__( 'Invalid signature image data', 'ninja-forms' ) . '</div>';
}
}
// Add non-editable notice
$output .= '<div style="font-size: 12px; color: #666; font-style: italic; margin-top: 8px;">';
$output .= esc_html__( 'Signatures are not editable', 'ninja-forms' );
$output .= '</div>';
return $output;
}
/**
* Filter merge tag value for display in various contexts
*
* This method handles signature display for:
* - HTML fields (via merge tags)
* - Email notifications
* - PDF documents
* - Any other merge tag usage
*
* @param mixed $value The field value
* @param array $field The field data (may contain additional field settings)
* @return string Formatted value for display
*/
public function filter_merge_tag_value( $value, $field )
{
if ( empty( $value ) ) {
return '';
}
// Handle HTML entity-encoded JSON (common in email contexts)
if ( is_string( $value ) && ( strpos( $value, '"' ) !== false || strpos( $value, '&' ) !== false ) ) {
$value = html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
}
$signature_data = json_decode( $value, true );
if ( ! is_array( $signature_data ) ) {
return '';
}
// Check if we need plain text output (for plain text emails only)
$is_plain_text = isset( $field['email_format'] ) && 'plain' === $field['email_format'];
// Also check if this is being used in a context that requires plain text
$context = isset( $field['merge_tag_context'] ) ? $field['merge_tag_context'] : 'default';
if ( $is_plain_text && 'email' === $context ) {
// Plain text format for emails only
if ( 'typed' === $signature_data[ 'signature_type' ] && ! empty( $signature_data[ 'typed_name' ] ) ) {
return $signature_data[ 'typed_name' ] . ' (Typed Signature)';
}
if ( 'drawn' === $signature_data[ 'signature_type' ] ) {
$timestamp = isset( $signature_data[ 'timestamp' ] ) ? ' - ' . $signature_data[ 'timestamp' ] : '';
return 'Drawn Signature' . $timestamp;
}
} else {
// Detect if we're in a PDF context
$is_pdf_context = $this->is_pdf_rendering_context();
// HTML format - return HTML for merge tags in HTML fields, emails, and PDFs
$options = $this->get_signature_html_options( $context );
// For PDF context with typed signatures, generate an image instead of HTML text
if ( $is_pdf_context && 'typed' === $signature_data['signature_type'] && ! empty( $signature_data['typed_name'] ) ) {
error_log( 'Ninja Forms Signature: PDF context detected, generating image for typed signature' );
$html = $this->get_signature_pdf_image_html( $signature_data, $options );
} else {
if ( $is_pdf_context ) {
error_log( 'Ninja Forms Signature: PDF context but not typed signature (type: ' . ( isset( $signature_data['signature_type'] ) ? $signature_data['signature_type'] : 'unknown' ) . ')' );
}
// Allow filter to override options
$options = apply_filters( 'ninja_forms_signature_merge_tag_options', $options, $field, $context );
// Generate HTML that will display properly in HTML fields
$html = $this->get_signature_email_html( $signature_data, $options );
}
// Check if we're in a repeater context (field data has different structure)
$is_repeater_context = isset( $field['id'] ) && strpos( $field['id'], '_' ) !== false;
// Wrap HTML for proper display (skip wrapper if in repeater context)
if ( ! empty( $html ) ) {
if ( $is_repeater_context ) {
return $html;
}
return $this->wrap_signature_html( $html, 'merge-tag' );
}
}
return '';
}
/**
* Filter CSV export value
*
* Formats signature data for CSV exports in a human-readable format:
* - Empty: "Method: none | Signed: false"
* - Typed: "Method: typed | Signed: true | Value: John Doe"
* - Drawn: "Method: drawn | Signed: true"
*
* @param mixed $value The field value (JSON string)
* @param object|array $field The field object or array (unused but required by filter signature)
* @return string Formatted value for CSV export
*/
public function filter_csv_export_value( $value, $field )
{
// Handle already processed values (e.g., from Email action)
if ( is_string( $value ) && ( strpos( $value, 'Method:' ) !== false || strpos( $value, 'Signed:' ) !== false ) ) {
return $value;
}
// Build CSV parts
$csv_parts = array();
// Handle empty values - field was not signed
if ( empty( $value ) ) {
$csv_parts[] = 'Method: none';
$csv_parts[] = 'Signed: false';
return implode( ' | ', $csv_parts );
}
// Try to decode JSON - handle potential escaping issues
$signature_data = json_decode( $value, true );
// If first decode fails, try with stripslashes
if ( ! is_array( $signature_data ) && is_string( $value ) ) {
$signature_data = json_decode( stripslashes( $value ), true );
}
// If still not valid, try html_entity_decode (common in email contexts)
if ( ! is_array( $signature_data ) && is_string( $value ) ) {
$decoded_value = html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
$signature_data = json_decode( $decoded_value, true );
}
// If we can't parse the data, check if it might be just the signature type string
if ( ! is_array( $signature_data ) ) {
// Sometimes the value might be just the signature type as a plain string
if ( in_array( $value, array( 'typed', 'drawn' ) ) ) {
$csv_parts[] = 'Method: ' . $value;
$csv_parts[] = 'Signed: false';
return implode( ' | ', $csv_parts );
}
$csv_parts[] = 'Method: error';
$csv_parts[] = 'Signed: false';
return implode( ' | ', $csv_parts );
}
// Get the method used - signature_type contains the actual method used during submission
$method = isset( $signature_data['signature_type'] ) ? $signature_data['signature_type'] : 'unknown';
$csv_parts[] = 'Method: ' . $method;
// Check if a value was actually provided based on the method
$has_value = false;
$typed_value = '';
if ( 'typed' === $method ) {
// For typed signatures, check if name was provided
if ( isset( $signature_data['typed_name'] ) && ! empty( $signature_data['typed_name'] ) ) {
$has_value = true;
$typed_value = $signature_data['typed_name'];
}
} elseif ( 'drawn' === $method ) {
// For drawn signatures, check if signature data exists
if ( isset( $signature_data['signature_data'] ) && ! empty( $signature_data['signature_data'] ) ) {
$has_value = true;
}
}
// Add signed status based on whether value exists
$csv_parts[] = 'Signed: ' . ( $has_value ? 'true' : 'false' );
// Add the value when method is typed
if ( 'typed' === $method && ! empty( $typed_value ) ) {
$csv_parts[] = 'Value: ' . $typed_value;
}
return implode( ' | ', $csv_parts );
}
/**
* Filter PDF export value - transforms signature data for PDF export
*
* This filter is specific to the signature field type and prepares
* the signature data in a format suitable for PDF generation.
*
* @param mixed $value The field value (JSON string)
* @param object $field The field object (unused but required by filter signature)
* @return array Formatted value for PDF export with type and value keys
*/
public function filter_pdf_value( $value, $field )
{
if ( empty( $value ) ) {
return [
'type' => 'empty',
'value' => ''
];
}
$signature_data = json_decode( $value, true );
if ( ! is_array( $signature_data ) ) {
return [
'type' => 'empty',
'value' => ''
];
}
// Get PDF-ready data using internal method
return $this->get_signature_pdf_data( $signature_data );
}
/**
* Filter PDF field display to show signature
*
* This is the main filter for controlling how signature fields appear in PDFs.
* It handles both the default PDF table (which uses MultiCell and doesn't support HTML)
* and custom templates (which can use WriteHTML for full HTML support).
*
* Note: The default PDF table generation doesn't support HTML, so we provide
* a text representation. For proper signature image display, users should
* use a custom PDF template or the document body feature.
*
* @param string $field_value The current field value
* @param string $original_value The original field value (JSON string)
* @param array $field The field data including type, id, etc.
* @return string Modified field value for PDF display
*/
public function filter_pdf_field_display( $field_value, $original_value, $field )
{
// Only process signature fields
if ( ! isset( $field['type'] ) || 'signature' !== $field['type'] ) {
return $field_value;
}
// If value is empty, return empty message
if ( empty( $original_value ) ) {
return esc_html__( 'Not signed', 'ninja-forms' );
}
// Try to decode JSON
$signature_data = json_decode( $original_value, true );
// If first decode fails, try with stripslashes
if ( ! is_array( $signature_data ) && is_string( $original_value ) ) {
$signature_data = json_decode( stripslashes( $original_value ), true );
}
// If we can't parse the data, return error message
if ( ! is_array( $signature_data ) || ! isset( $signature_data['signature_type'] ) ) {
return esc_html__( 'Invalid signature data', 'ninja-forms' );
}
// Handle based on signature type
if ( 'typed' === $signature_data['signature_type'] && ! empty( $signature_data['typed_name'] ) ) {
// For typed signatures, show name and font
$font = isset( $signature_data['signature_font'] ) ? $signature_data['signature_font'] : 'dancing-script';
// Return text representation since default PDF table doesn't support HTML
return sprintf(
'%s (Typed signature - %s font)',
$signature_data['typed_name'],
$font
);
} elseif ( 'drawn' === $signature_data['signature_type'] && ! empty( $signature_data['signature_data'] ) ) {
// For drawn signatures in default PDF table, we can only show text
// To display the actual image, users need to use a custom template
// or enable the "use_document_body" option
return esc_html__( 'Drawn signature captured', 'ninja-forms' );
}
return esc_html__( 'Not signed', 'ninja-forms' );
}
/**
* Disable wpautop for signature fields in PDF
*
* @param bool $apply_wpautop Whether to apply wpautop
* @param string $field_value The field value
* @param array $field The field data
* @return bool
*/
public function disable_wpautop_for_signatures( $apply_wpautop, $field_value, $field )
{
if ( isset( $field['type'] ) && 'signature' === $field['type'] ) {
return false;
}
return $apply_wpautop;
}
/**
* Add signature to the list of HTML-safe fields
*
* This ensures signature field values are not stripped of HTML
* when used in merge tags
*
* @param array $safe_fields Current list of safe fields
* @return array Modified list
*/
public function add_to_safe_fields( $safe_fields )
{
if ( ! in_array( 'signature', $safe_fields ) ) {
$safe_fields[] = 'signature';
}
return $safe_fields;
}
/**
* Get default HTML options for signature display
*
* @param string $context The display context (merge_tag, email, etc.)
* @return array HTML generation options
*/
private function get_signature_html_options( $context = 'merge_tag' )
{
$default_options = [
'max_width' => '400px',
'font_size' => '28px',
'typed_color' => '#000000',
'container_style' => 'display: inline-block;'
];
// Allow context-specific overrides
return apply_filters( 'ninja_forms_signature_html_options', $default_options, $context );
}
/**
* Detect if we're rendering in a PDF context
*
* Checks various indicators to determine if the current rendering
* is happening within a PDF generation process.
*
* @return bool True if in PDF context, false otherwise
*/
private function is_pdf_rendering_context()
{
// Check if this is being called from PDF generation actions
if ( doing_action( 'nf_pdf_before_template_part' ) || doing_action( 'nf_pdf_after_template_part' ) ) {
return true;
}
// Check the call stack for PDF-related classes
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 15 );
foreach ( $backtrace as $trace ) {
if ( isset( $trace['class'] ) && strpos( $trace['class'], 'NF_Pdf_Submissions' ) !== false ) {
return true;
}
if ( isset( $trace['file'] ) && strpos( $trace['file'], 'ninja-forms-pdf-submissions' ) !== false ) {
return true;
}
}
return false;
}
/**
* Generate HTML for signature display in PDF using an image
*
* Converts typed signature text to an image to ensure proper font rendering
* in PDF documents without affecting other text.
*
* @param array $signature_data Decoded signature JSON data
* @param array $options Display options
* @return string HTML with image tag or fallback text
*/
private function get_signature_pdf_image_html( $signature_data, $options = [] )
{
// Validate we have typed signature data
if ( 'typed' !== $signature_data['signature_type'] || empty( $signature_data['typed_name'] ) ) {
return '';
}
$font = isset( $signature_data['signature_font'] ) ? $signature_data['signature_font'] : 'dancing-script';
$text = $signature_data['typed_name'];
// Generate image with signature font
$image_options = [
'font_size' => 48,
'color' => isset( $options['typed_color'] ) ? $options['typed_color'] : '#000000',
'padding' => 15,
];
$image_data = $this->generate_signature_image( $text, $font, $image_options );
// If image generation failed, fall back to plain text
if ( false === $image_data ) {
return sprintf(
'<div style="font-family: serif; font-style: italic; font-size: 24px;">%s</div>',
esc_html( $text )
);
}
// Return image tag with appropriate styling
$max_width = isset( $options['max_width'] ) ? $options['max_width'] : '400px';
return sprintf(
'<img src="%s" alt="%s" style="max-width: %s; height: auto; display: block;" />',
esc_attr( $image_data ),
esc_attr( sprintf( __( 'Signature: %s', 'ninja-forms' ), $text ) ),
esc_attr( $max_width )
);
}
/**
* Generate HTML wrapper for signature content
* @param string $html The signature HTML content
* @param string $context The display context
* @return string Wrapped HTML
*/
private function wrap_signature_html( $html, $context = 'merge_tag' )
{
if ( empty( $html ) ) {
return '';
}
$class = 'nf-signature-' . sanitize_html_class( $context );
return sprintf(
'<div class="%s" style="display: block; margin: 10px 0;">%s</div>',
esc_attr( $class ),
$html
);
}
/**
* Convert signature data to HTML for email display
*
* @param array $signature_data Decoded signature JSON data
* @param array $options Display options
* @return string HTML output
*/
private function get_signature_email_html( $signature_data, $options = [] )
{
$defaults = [
'max_width' => '400px',
'font_size' => '28px',
'typed_color' => '#000000',
'container_style' => '',
];
$options = array_merge( $defaults, $options );
// For typed signatures
if ( 'typed' === $signature_data['signature_type'] && ! empty( $signature_data['typed_name'] ) ) {
$font = isset( $signature_data['signature_font'] ) ? $signature_data['signature_font'] : 'dancing-script';
$font_family = $this->get_signature_font_family( $font );
$style = sprintf(
'font-family: %s; font-size: %s; color: %s; padding: 10px 15px; background-color: transparent; display: inline-block; %s',
esc_attr( $font_family ),
esc_attr( $options['font_size'] ),
esc_attr( $options['typed_color'] ),
esc_attr( $options['container_style'] )
);
return sprintf(
'<div style="%s">%s</div>',
$style,
esc_html( $signature_data['typed_name'] )
);
}
// For drawn signatures
if ( 'drawn' === $signature_data['signature_type'] && ! empty( $signature_data['signature_data'] ) ) {
$style = sprintf(
'max-width: %s; height: auto; display: block; %s',
esc_attr( $options['max_width'] ),
esc_attr( $options['container_style'] )
);
return sprintf(
'<img src="%s" alt="%s" style="%s" />',
esc_attr( $signature_data['signature_data'] ),
esc_attr__( 'Signature', 'ninja-forms' ),
$style
);
}
return '';
}
/**
* Get font family CSS value
*
* @param string $font Font identifier
* @return string CSS font-family value
*/
private function get_signature_font_family( $font )
{
static $fonts = [
'dancing-script' => '"Dancing Script", cursive',
'satisfy' => '"Satisfy", cursive',
'cursive' => 'cursive',
];
return isset( $fonts[ $font ] ) ? $fonts[ $font ] : $fonts['dancing-script'];
}
/**
* Get font file path for signature font
*
* @param string $font Font identifier
* @return string|false Path to TTF font file or false if not found
*/
private function get_signature_font_file( $font )
{
// Get the Ninja Forms plugin directory
// __FILE__ is /includes/Fields/Signature.php, so we need to go up 2 levels
$plugin_dir = dirname( dirname( dirname( __FILE__ ) ) );
$font_dir = $plugin_dir . '/assets/fonts/signature/';
$font_files = [
'dancing-script' => 'dancing-script-400.ttf',
'satisfy' => 'satisfy-400.ttf',
'cursive' => 'dancing-script-400.ttf', // Fallback to Dancing Script
];
$font_file = isset( $font_files[ $font ] ) ? $font_files[ $font ] : $font_files['dancing-script'];
$full_path = $font_dir . $font_file;
return file_exists( $full_path ) ? $full_path : false;
}
/**
* Generate image from typed signature text
*
* Creates a PNG image with the signature text rendered in the selected font.
* This is used for PDF display to ensure fonts render correctly without affecting
* the rest of the PDF document.
*
* @param string $text The signature text to render
* @param string $font Font identifier (dancing-script, satisfy, cursive)
* @param array $options Image generation options (font_size, color, padding)
* @return string|false Base64 encoded PNG data URL or false on failure
*/
private function generate_signature_image( $text, $font = 'dancing-script', $options = [] )
{
// Check if GD library is available
if ( ! function_exists( 'imagecreatetruecolor' ) ) {
error_log( 'Ninja Forms Signature: GD library not available' );
return false;
}
$defaults = [
'font_size' => 48, // Font size in points
'color' => '#000000', // Text color
'padding' => 20, // Padding around text
'bg_color' => 'transparent', // Background color
];
$options = array_merge( $defaults, $options );
// Get font file path
$font_file = $this->get_signature_font_file( $font );
if ( ! $font_file ) {
error_log( 'Ninja Forms Signature: Font file not found for font: ' . $font );
return false;
}
// Log which font is being used
error_log( 'Ninja Forms Signature: Generating image with font: ' . $font . ' (' . basename( $font_file ) . ')' );
// Calculate text dimensions
$font_size = $options['font_size'];
$angle = 0;
// Get bounding box for text
$bbox = imagettfbbox( $font_size, $angle, $font_file, $text );
if ( ! $bbox ) {
return false;
}
// Calculate image dimensions
$text_width = abs( $bbox[4] - $bbox[0] );
$text_height = abs( $bbox[5] - $bbox[1] );
$padding = $options['padding'];
$img_width = $text_width + ( $padding * 2 );
$img_height = $text_height + ( $padding * 2 );
// Create image
$image = imagecreatetruecolor( $img_width, $img_height );
if ( ! $image ) {
return false;
}
// Handle transparency
imagesavealpha( $image, true );
$transparent = imagecolorallocatealpha( $image, 0, 0, 0, 127 );
imagefill( $image, 0, 0, $transparent );
// Parse color
$hex_color = ltrim( $options['color'], '#' );
$r = hexdec( substr( $hex_color, 0, 2 ) );
$g = hexdec( substr( $hex_color, 2, 2 ) );
$b = hexdec( substr( $hex_color, 4, 2 ) );
$text_color = imagecolorallocate( $image, $r, $g, $b );
// Calculate text position (centered vertically, padded horizontally)
$x = $padding;
$y = $padding + $text_height;
// Draw text
imagettftext( $image, $font_size, $angle, $x, $y, $text_color, $font_file, $text );
// Capture image as PNG
ob_start();
imagepng( $image, null, 9 ); // Max compression
$image_data = ob_get_clean();
// Clean up
imagedestroy( $image );
if ( ! $image_data ) {
return false;
}
// Convert to base64 data URL
return 'data:image/png;base64,' . base64_encode( $image_data );
}
/**
* Generate PDF-ready signature data
*
* @param array $signature_data Decoded signature JSON data
* @return array PDF-compatible data structure
*/
private function get_signature_pdf_data( $signature_data )
{
if ( ! is_array( $signature_data ) || ! isset( $signature_data['signature_type'] ) ) {
return [
'type' => 'empty',
'value' => ''
];
}
// For typed signatures
if ( 'typed' === $signature_data['signature_type'] && ! empty( $signature_data['typed_name'] ) ) {
$font = isset( $signature_data['signature_font'] ) ? $signature_data['signature_font'] : 'dancing-script';
return [
'type' => 'styled_text',
'value' => $signature_data['typed_name'],
'font_family' => $this->get_signature_font_family( $font ),
'font_size' => 24,
'color' => '#000000',
'style' => 'italic'
];
}
// For drawn signatures
if ( 'drawn' === $signature_data['signature_type'] && ! empty( $signature_data['signature_data'] ) ) {
// Try to extract just the base64 data part
if ( preg_match( '/^data:image\/(png|jpeg);base64,(.+)/', $signature_data['signature_data'], $matches ) ) {
return [
'type' => 'image',
'format' => $matches[1],
'data' => $matches[2],
'base64_full' => $signature_data['signature_data'],
'width' => 300,
'height' => 100,
'maintain_aspect_ratio' => true,
'alt' => 'Signature'
];
}
}
return [
'type' => 'empty',
'value' => ''
];
}
/**
* Format signature field display for CPT custom columns
*
* This method handles signature display in the WordPress CPT submissions table
* (edit.php?post_type=nf_sub). It displays a compact signature representation.
*
* @param mixed $value Field value
* @param object $field Field object
* @param int $sub_id Submission ID
* @return string Formatted HTML for table display
*/
public function custom_columns( $value, $field, $sub_id = 0 )
{
// Only process signature fields
if ( ! is_object( $field ) || $field->get_setting( 'type' ) !== 'signature' ) {
return $value;
}
// Handle empty value
if ( empty( $value ) || $value === '' ) {
return '<span style="color: #999; font-style: italic;">' . esc_html__( 'Not signed', 'ninja-forms' ) . '</span>';
}
// Decode HTML entities first (common in WordPress contexts)
$decoded_value = html_entity_decode( $value, ENT_QUOTES, 'UTF-8' );
// Try to decode JSON from decoded value
$signature_data = json_decode( $decoded_value, true );
// If that didn't work, try the original value
if ( ! is_array( $signature_data ) ) {
$signature_data = json_decode( $value, true );
}
// Handle invalid JSON
if ( ! is_array( $signature_data ) || ! isset( $signature_data['signature_type'] ) ) {
return '<span style="color: #999; font-style: italic;">' . esc_html__( 'Invalid signature', 'ninja-forms' ) . '</span>';
}
// Display typed signature
if ( 'typed' === $signature_data['signature_type'] && ! empty( $signature_data['typed_name'] ) ) {
$font = isset( $signature_data['signature_font'] ) ? $signature_data['signature_font'] : 'dancing-script';
$font_family = $this->get_signature_font_family( $font );
$output = '<div style="display: inline-block;">';
$output .= '<div style="font-family: ' . esc_attr( $font_family ) . '; font-size: 20px; color: #000; padding: 8px 12px; background: transparent;">';
$output .= esc_html( $signature_data['typed_name'] );
$output .= '</div>';
$output .= '<div style="font-size: 11px; color: #666; margin-top: 3px;">' . esc_html__( 'Typed signature', 'ninja-forms' ) . '</div>';
$output .= '</div>';
return $output;
}
// Display drawn signature
if ( 'drawn' === $signature_data['signature_type'] && ! empty( $signature_data['signature_data'] ) ) {
if ( preg_match( '/^data:image\/(png|jpeg);base64,/', $signature_data['signature_data'] ) ) {
$output = '<img src="' . esc_attr( $signature_data['signature_data'] ) . '" ';
$output .= 'alt="' . esc_attr__( 'Signature', 'ninja-forms' ) . '" ';
$output .= 'style="max-width: 150px; max-height: 50px; display: block;" />';
$output .= '<div style="font-size: 11px; color: #666; margin-top: 3px;">' . esc_html__( 'Drawn signature', 'ninja-forms' ) . '</div>';
return $output;
}
}
return '<span style="color: #999; font-style: italic;">' . esc_html__( 'Invalid signature data', 'ninja-forms' ) . '</span>';
}
}