diff --git a/signaturefield.services.yml b/signaturefield.services.yml new file mode 100644 index 0000000..222f427 --- /dev/null +++ b/signaturefield.services.yml @@ -0,0 +1,3 @@ +services: + signaturefield.png_converter: + class: Drupal\signaturefield\SignaturePngConverter diff --git a/src/Plugin/Field/FieldFormatter/SignatureFormatter.php b/src/Plugin/Field/FieldFormatter/SignatureFormatter.php index 1444565..73a6618 100644 --- a/src/Plugin/Field/FieldFormatter/SignatureFormatter.php +++ b/src/Plugin/Field/FieldFormatter/SignatureFormatter.php @@ -14,6 +14,7 @@ use Drupal\Core\Form\FormStateInterface; * label = @Translation("Signature"), * field_types = { * "signature", + * "signature_file", * }, * ) */ @@ -78,6 +79,7 @@ class SignatureFormatter extends FormatterBase { public function viewElements(FieldItemListInterface $items, $langcode) { $element = []; + /** @var \Drupal\signaturefield\Plugin\Field\FieldType\SignatureItemInterface $item */ foreach ($items as $delta => $item) { $element[$delta] = [ '#theme' => 'image', @@ -85,7 +87,7 @@ class SignatureFormatter extends FormatterBase { '#height' => $this->getSetting('height'), '#alt' => $this->t('Signature'), '#attributes' => [ - 'src' => $item->value, + 'src' => $item->getImageSrc(), ], ]; } diff --git a/src/Plugin/Field/FieldType/SignatureFileItem.php b/src/Plugin/Field/FieldType/SignatureFileItem.php new file mode 100644 index 0000000..a7114a7 --- /dev/null +++ b/src/Plugin/Field/FieldType/SignatureFileItem.php @@ -0,0 +1,79 @@ + 'png', + 'file_directory' => 'signaturefield', + ] + parent::defaultFieldSettings(); + } + + /** + * {@inheritdoc} + * + * Hide parent configuration which is irrelevant to this field type. + */ + public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) { + $element = parent::storageSettingsForm($form, $form_state, $has_data); + $element['display_field']['#access'] = FALSE; + return $element; + } + + /** + * {@inheritdoc} + * + * Hide parent configuration which is irrelevant to this field type. + */ + public function fieldSettingsForm(array $form, FormStateInterface $form_state) { + $element = parent::fieldSettingsForm($form, $form_state); + $element['file_extensions']['#access'] = FALSE; + $element['max_filesize']['#access'] = FALSE; + $element['description_field']['#access'] = FALSE; + return $element; + } + + /** + * {@inheritdoc} + * + * Return the URL to the file. + */ + public function getImageSrc() { + $file_entity = $this->entity; + if ($file_entity === NULL) { + return NULL; + } + return $file_entity->createFileUrl(); + } + +} diff --git a/src/Plugin/Field/FieldType/SignatureItem.php b/src/Plugin/Field/FieldType/SignatureItem.php index c41096b..bb200d6 100644 --- a/src/Plugin/Field/FieldType/SignatureItem.php +++ b/src/Plugin/Field/FieldType/SignatureItem.php @@ -18,7 +18,7 @@ use Drupal\Core\TypedData\DataDefinition; * default_formatter = "signature", * ) */ -class SignatureItem extends FieldItemBase { +class SignatureItem extends FieldItemBase implements SignatureItemInterface { /** * {@inheritdoc} @@ -56,4 +56,11 @@ class SignatureItem extends FieldItemBase { ]; } + /** + * {@inheritdoc} + */ + public function getImageSrc() { + return $this->value; + } + } diff --git a/src/Plugin/Field/FieldType/SignatureItemInterface.php b/src/Plugin/Field/FieldType/SignatureItemInterface.php new file mode 100644 index 0000000..053160d --- /dev/null +++ b/src/Plugin/Field/FieldType/SignatureItemInterface.php @@ -0,0 +1,22 @@ +signaturePngConverter = $signature_png_converter; + $this->fileSystem = $file_system; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + /** @var \Drupal\signaturefield\SignaturePngConverter $signature_png_converter */ + $signature_png_converter = $container->get('signaturefield.png_converter'); + /** @var \Drupal\Core\File\FileSystemInterface $file_system */ + $file_system = $container->get('file_system'); + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $signature_png_converter, + $file_system + ); + } + + /** + * {@inheritdoc} + * + * Override parent to convert an existing file field's entity to a data url. + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $element = parent::formElement($items, $delta, $element, $form, $form_state); + /** @var \Drupal\signaturefield\Plugin\Field\FieldType\SignatureFileItem $item */ + $item = $items[$delta]; + $file_entity = $item->entity; + if ($file_entity === NULL) { + return $element; + } + // Convert the file to a data URL and set the default value. + $data_url = $this->signaturePngConverter->fileToPngDataUrl($file_entity); + $element['value']['#default_value'] = $data_url; + // Store this file in widget state for later validation during submission. + $this->storeFile($form, $form_state, $delta, $file_entity); + return $element; + } + + /** + * {@inheritdoc} + * + * Converts the Data URL(s) into file(s) in order to provide the proper + * value type for a file field. + * + * In order to prevent this from creating a brand new png file and file entity + * the existing file's Data URL is compared against the current value. + * + * @see FileWidget::massageFormValues() + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + /** @var \Drupal\Component\Uuid\UuidInterface $uuid_service */ + $new_values = []; + foreach ($values as $delta => $value) { + $data_url = $value['value']; + // Ignore processing empty values. + if (empty($data_url)) { + continue; + } + // Check if the existing file matches data url and return it instead. + $file = $this->getStoredFile($form, $form_state, $delta); + $existing_file_data_url = $file ? $this->signaturePngConverter->fileToPngDataUrl($file) : NULL; + // If the data URL is different, create a new file. + if ($existing_file_data_url !== $data_url) { + $file = $this->createFileForDataUrl($data_url); + $this->storeFile($form, $form_state, $delta, $file); + } + // Set the expected data value for a file field. + $new_values[$delta] = [ + 'target_id' => $file->id(), + ]; + } + return $new_values; + } + + /** + * Creates and saves a new png file with the provided Data URL. + * + * @param string $data_url + * The encoded data URL to build the png image with. + * + * @return \Drupal\file\FileInterface + * The saved file entity for the provided data url. + * + * @throws \Drupal\Core\File\Exception\FileWriteException + * - If the destination directory is not writable. + */ + protected function createFileForDataUrl(string $data_url) { + // Otherwise, create/save a new file. + $destination = $this->createSignatureFileItem()->getUploadLocation(); + // Ensure the destination is writable. + if (!$this->fileSystem->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY)) { + throw new FileWriteException("The destination directory '$destination' is not writable"); + } + $filename = $this->generateSignatureFileName(); + $file_uri = "$destination/$filename"; + $file_contents = $this->signaturePngConverter->dataUrlToPngFileContents($data_url); + return file_save_data($file_contents, $file_uri); + } + + /** + * Create a signature file field item. + * + * @return \Drupal\signaturefield\Plugin\Field\FieldType\SignatureFileItem + * An instantiated file item containing no specific data. + * + * @see \Drupal\media_library\Form\FileUploadForm::createFileItem() + */ + protected function createSignatureFileItem() { + $data_definition = FieldItemDataDefinition::create($this->fieldDefinition); + return new SignatureFileItem($data_definition); + } + + /** + * Builds the filename for the signature png file. + * + * This file name comprises two parts: + * 1. The current unix timestamp. + * 2. A random number between 1000-9000 to encourage no duplicate filenames. + * + * @return string + * The generated filename for the signature png file. + */ + protected function generateSignatureFileName() { + return time() . random_int(1000, 9999) . '.png'; + } + + /** + * Stores a file in the widget state for a given delta. + * + * File (IDs) are stored in the widget state to ensure that we do not create + * a new signature file unless the signature has changed. + * + * @param array $element + * The form or element array in which the widget is attached to. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The drupal form state. + * @param int|string $delta + * The delta in which the file should be stored. + * @param \Drupal\file\FileInterface $new_file + * The file to store in the widget state. + * + * @see SignatureFilePadWidget::getStoredFile() + */ + private function storeFile(array $element, FormStateInterface $form_state, $delta, FileInterface $new_file) { + $field_name = $this->fieldDefinition->getName(); + $widget_state = static::getWidgetState($element['#parents'], $field_name, $form_state); + $widget_state[self::STATE_FILE_KEY][$delta] = $new_file->id(); + static::setWidgetState($element['#parents'], $field_name, $form_state, $widget_state); + } + + /** + * Retrieves the file in the widget state for a given delta. + * + * @param array $element + * The form or element array in which the widget is attached to. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The drupal form state. + * @param int|string $delta + * The delta in which the file should be retrieved from. + * + * @return \Drupal\file\FileInterface|null + * The stored file or NULL if a file has not been stored for this delta. + * + * @see SignatureFilePadWidget::storeFile() + */ + private function getStoredFile(array $element, FormStateInterface $form_state, $delta) { + $field_name = $this->fieldDefinition->getName(); + $widget_state = static::getWidgetState($element['#parents'], $field_name, $form_state); + $fid = $widget_state[self::STATE_FILE_KEY][$delta] ?? NULL; + if ($fid === NULL) { + return NULL; + } + return File::load($fid); + } + +} diff --git a/src/SignaturePngConverter.php b/src/SignaturePngConverter.php new file mode 100644 index 0000000..36411eb --- /dev/null +++ b/src/SignaturePngConverter.php @@ -0,0 +1,44 @@ +getFileUri(); + $contents = file_get_contents($uri); + return $this->fileContentsToPngDataUrl($contents); + } + + /** + * {@inheritdoc} + */ + public function fileContentsToPngDataUrl(string $contents) { + // Encode the data into Base 64. + $base_64 = base64_encode($contents); + // Construct our base64 string. + return 'data:image/png;base64,' . $base_64; + } + + /** + * {@inheritdoc} + */ + public function dataUrlToPngFileContents(string $data_url) { + $contents = str_replace(['data:image/png;base64,', ' '], ['', '+'], $data_url); + return base64_decode($contents); + } + +} diff --git a/src/SignaturePngConverterInterface.php b/src/SignaturePngConverterInterface.php new file mode 100644 index 0000000..de1cb99 --- /dev/null +++ b/src/SignaturePngConverterInterface.php @@ -0,0 +1,47 @@ +