diff --git a/CRM/DataprocessorOutputExport/AbstractOutputExport.php b/CRM/DataprocessorOutputExport/AbstractOutputExport.php
index 2072b86c7f9b56db89c19bf75117fc91ed8d8705..beb54fa09cf7cff6fb942ee347eec0cbfca68d73 100644
--- a/CRM/DataprocessorOutputExport/AbstractOutputExport.php
+++ b/CRM/DataprocessorOutputExport/AbstractOutputExport.php
@@ -30,23 +30,26 @@ abstract class CRM_DataprocessorOutputExport_AbstractOutputExport implements Exp
   /**
    * Returns the directory name for storing temporary files.
    *
+   * @param array $configuration
    * @return String
    */
-  abstract public function getDirectory(): string;
+  abstract public function getDirectory(array $configuration=[]): string;
 
   /**
    * Returns the file extension.
    *
+   * @param array $configuration
    * @return String
    */
-  abstract public function getExtension(): string;
+  abstract public function getExtension(array $configuration=[]): string;
 
   /**
    * Returns the mime type of the export file.
    *
+   * @param array $configuration
    * @return string
    */
-  abstract public function mimeType(): string;
+  abstract public function mimeType(array $configuration=[]): string;
 
   /**
    * Run the export of the data processor.
diff --git a/CRM/DataprocessorOutputExport/AbstractSpreadsheet.php b/CRM/DataprocessorOutputExport/AbstractSpreadsheet.php
new file mode 100644
index 0000000000000000000000000000000000000000..77be882c5132d8cafd8d86f700545140a01c74b3
--- /dev/null
+++ b/CRM/DataprocessorOutputExport/AbstractSpreadsheet.php
@@ -0,0 +1,223 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+use Civi\DataProcessor\DataFlow\EndOfFlowException;
+use Civi\DataProcessor\DataFlow\InvalidFlowException;
+use Civi\DataProcessor\DataSpecification\DataSpecification;
+use Civi\DataProcessor\DataSpecification\FieldExistsException;
+use Civi\DataProcessor\Exception\DataFlowException;
+use Civi\DataProcessor\Output\DirectDownloadExportOutputInterface;
+use Civi\DataProcessor\ProcessorType\AbstractProcessorType;
+use Civi\DataProcessor\FileFormat\Fileformat;
+use CRM_Dataprocessor_ExtensionUtil as E;
+
+/**
+ * Abstract class to reuse for spreadsheet exports. Such as CSV and Xlsx.
+ */
+abstract class CRM_DataprocessorOutputExport_AbstractSpreadsheet extends CRM_DataprocessorOutputExport_AbstractOutputExport implements DirectDownloadExportOutputInterface {
+
+  /**
+   * Returns the File Format class for this spreadsheet format.
+   *
+   * @param array $configuration
+   * @return \Civi\DataProcessor\FileFormat\Fileformat
+   */
+  abstract protected function getFileFormatClass(array $configuration): Fileformat;
+
+  /**
+   * Returns the directory name for storing temporary files.
+   *
+   * @param array $configuration
+   * @return String
+   */
+  public function getDirectory(array $configuration=[]): string {
+    return 'dataprocessor_export_'.$this->getFileFormatClass($configuration)->getFileExtension();
+  }
+
+  /**
+   * Returns the file extension.
+   *
+   * @param array $configuration
+   * @return String
+   */
+  public function getExtension(array $configuration=[]): string {
+    return $this->getFileFormatClass($configuration)->getFileExtension();
+  }
+
+  /**
+   * Returns the alternate file name.
+   *
+   * @param array $configuration
+   * @return string
+   */
+  protected function getAlternateFileName(array $configuration=[]):? string {
+    if (isset($configuration['altfilename']) && ''!==$configuration['altfilename']) {
+      return $configuration['altfilename'];
+    }
+    return null;
+  }
+
+  /**
+   * Get the download name of the export file.
+   *
+   * @param $dataProcessor
+   * @param $outputBAO
+   *
+   * @return string
+   */
+  public function getDownloadName($dataProcessor, $outputBAO): string {
+    $configuration = [];
+    if (isset($outputBAO['configuration'])) {
+      $configuration = $outputBAO['configuration'];
+    }
+    $download_name = $this->getAlternateFileName($configuration);
+    if (empty($download_name)) {
+      $download_name = date('Ymdhis') . '_' . $dataProcessor['name'] . '.' . $this->getExtension($configuration);
+    }
+    return $download_name;
+  }
+
+  /**
+   * When this filter type has additional configuration you can add
+   * the fields on the form with this function.
+   *
+   * @param \CRM_Core_Form $form
+   * @param array $output
+   */
+  public function buildConfigurationForm(CRM_Core_Form $form, $output=array()) {
+    parent::buildConfigurationForm($form, $output);
+    try {
+      $form->add('text', 'altfilename', E::ts('Alternate File Name'), []);
+    } catch (CRM_Core_Exception $e) {
+    }
+    $configuration = array();
+    if ($output && isset($output['configuration'])) {
+      $configuration = $output['configuration'];
+    }
+    $defaults['altfilename'] = $this->getAlternateFileName($configuration);
+    $form->setDefaults($defaults);
+    $this->getFileFormatClass($configuration)->buildConfigurationForm($form, $configuration);
+    $form->assign('spreadsheet_file_format_configuration', $this->getFileFormatClass($configuration)->getConfigurationTemplateFileName());
+  }
+
+  /**
+   * When this filter type has configuration specify the template file name
+   * for the configuration form.
+   *
+   * @return false|string
+   */
+  public function getConfigurationTemplateFileName():? string {
+    return "CRM/DataprocessorOutputExport/Form/Configuration/Spreadsheet.tpl";
+  }
+
+
+  /**
+   * Process the submitted values and create a configuration array
+   *
+   * @param $submittedValues
+   * @param array $output
+   * @return array
+   */
+  public function processConfiguration($submittedValues, &$output): array {
+    $configuration = parent::processConfiguration($submittedValues, $output);
+    $this->getFileFormatClass($configuration)->processConfiguration($submittedValues, $configuration);
+
+    if (isset($submittedValues['altfilename']) && ''!==$submittedValues['altfilename']) {
+      $configuration['altfilename'] = CRM_Utils_String::munge($this->removeFileExtensionFromFileName($submittedValues['altfilename'])) . '.' . $this->getExtension($configuration);
+    }
+    return $configuration;
+  }
+
+  /**
+   * This function is called prior to removing an output
+   *
+   * @param array $output
+   * @return void
+   */
+  public function deleteOutput($output) {
+    // Do nothing
+  }
+
+
+  /**
+   * Returns the mime type of the export file.
+   *
+   * @param array $configuration
+   * @return string
+   */
+  public function mimeType(array $configuration=[]): string {
+    return $this->getFileFormatClass($configuration)->getMimetype();
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string
+   */
+  public function getTitleForExport($output, $dataProcessor): string {
+    return E::ts('Download as CSV');
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string|false
+   */
+  public function getExportFileIcon($output, $dataProcessor):? string {
+    $configuration = [];
+    if (isset($output['configuration'])) {
+      $configuration = $output['configuration'];
+    }
+    return $this->getFileFormatClass($configuration)->getExportFileIcon();
+  }
+
+  protected function createHeader($filename, AbstractProcessorType $dataProcessorClass, $configuration, $dataProcessor, $idField=null, $selectedIds=array(), $formValues=array()) {
+    $fields = new DataSpecification();
+    try {
+      $fields = $dataProcessorClass->getDataFlow()->getDataSpecification();
+    } catch (InvalidFlowException|FieldExistsException $e) {
+    }
+
+    $this->getFileFormatClass($configuration)->createHeader($fields, $filename, $configuration);
+  }
+
+  protected function exportRecords(string $filename, AbstractProcessorType $dataProcessor, array $configuration, string $idField=null, array $selectedIds=array(), array $formValues=array()) {
+    $fields = new DataSpecification();
+    try {
+      $fields = $dataProcessor->getDataFlow()->getDataSpecification();
+    } catch (InvalidFlowException|FieldExistsException $e) {
+    }
+
+    try {
+      while($record = $dataProcessor->getDataFlow()->nextRecord()) {
+        $rowIsSelected = true;
+        if (isset($idField) && is_array($selectedIds) && count($selectedIds)) {
+          $rowIsSelected = false;
+          $id = $record[$idField]->rawValue;
+          if (in_array($id, $selectedIds)) {
+            $rowIsSelected = true;
+          }
+        }
+        if ($rowIsSelected) {
+          $row = [];
+          foreach ($record as $column => $value) {
+            $row[$column] = $value->formattedValue;
+          }
+          $this->getFileFormatClass($configuration)->addRecord($fields, $filename, $row, $configuration);
+        }
+      }
+    } catch (EndOfFlowException|InvalidFlowException|DataFlowException $e) {
+      // Do nothing
+    }
+    $this->getFileFormatClass($configuration)->closeFile($fields, $filename, $configuration);
+  }
+
+
+}
diff --git a/CRM/DataprocessorOutputExport/CSV.php b/CRM/DataprocessorOutputExport/CSV.php
index eb89828d10a134d7920cda86dc12ba84864b0f09..3185f8021d24cd0c931a4a324968269af6b2e40e 100644
--- a/CRM/DataprocessorOutputExport/CSV.php
+++ b/CRM/DataprocessorOutputExport/CSV.php
@@ -4,201 +4,39 @@
  * @license AGPL-3.0
  */
 
-use Civi\DataProcessor\DataFlow\EndOfFlowException;
-use Civi\DataProcessor\DataFlow\InvalidFlowException;
-use Civi\DataProcessor\DataSpecification\DataSpecification;
-use Civi\DataProcessor\DataSpecification\FieldExistsException;
-use Civi\DataProcessor\Exception\DataFlowException;
-use Civi\DataProcessor\Output\DirectDownloadExportOutputInterface;
-use Civi\DataProcessor\ProcessorType\AbstractProcessorType;
 use Civi\DataProcessor\FileFormat\Fileformat;
-use CRM_Dataprocessor_ExtensionUtil as E;
 
-class CRM_DataprocessorOutputExport_CSV extends CRM_DataprocessorOutputExport_AbstractOutputExport implements DirectDownloadExportOutputInterface {
+class CRM_DataprocessorOutputExport_CSV extends CRM_DataprocessorOutputExport_AbstractSpreadsheet {
 
   /** @var Fileformat */
   private $fileFormatClass;
 
-  protected function getFileFormatClass(): Fileformat {
+  /**
+   * Returns the File Format class for this spreadsheet format.
+   *
+   * @param array $configuration
+   * @return \Civi\DataProcessor\FileFormat\Fileformat
+   */
+  protected function getFileFormatClass(array $configuration=[]): Fileformat {
     if (empty($this->fileFormatClass)) {
       $factory = dataprocessor_get_factory();
       $this->fileFormatClass = $factory->getFileFormatByName('csv');
     }
     return $this->fileFormatClass;
   }
-
-  /**
-   * Returns the directory name for storing temporary files.
-   *
-   * @return String
-   */
-  public function getDirectory(): string {
-    return 'dataprocessor_export_csv';
-  }
-
-  /**
-   * Returns the file extension.
-   *
-   * @return String
-   */
-  public function getExtension(): string {
-    return 'csv';
-  }
-
   /**
-   * Get the download name of the export file.
-   *
-   * @param $dataProcessor
-   * @param $outputBAO
+   * Returns the alternate file name.
    *
+   * @param array $configuration
    * @return string
    */
-  public function getDownloadName($dataProcessor, $outputBAO): string {
-    if (isset($outputBAO['configuration']['altcsvfilename']) && ''!==$outputBAO['configuration']['altcsvfilename']) {
-      $download_name = $outputBAO['configuration']['altcsvfilename'];
-    } else {
-      $download_name = date('Ymdhis') . '_' . $dataProcessor['name'] . '.' . $this->getExtension();
+  protected function getAlternateFileName(array $configuration=[]):? string {
+    if (isset($configuration['altfilename']) && ''!==$configuration['altfilename']) {
+      return $configuration['altfilename'];
+    } elseif (isset($configuration['altcsvfilename']) && ''!==$configuration['altcsvfilename']) {
+      return $configuration['altcsvfilename'];
     }
-    return $download_name;
+    return null;
   }
 
-  /**
-   * When this filter type has additional configuration you can add
-   * the fields on the form with this function.
-   *
-   * @param \CRM_Core_Form $form
-   * @param array $output
-   */
-  public function buildConfigurationForm(CRM_Core_Form $form, $output=array()) {
-    parent::buildConfigurationForm($form, $output);
-    try {
-      $form->add('text', 'altcsvfilename', E::ts('Alternate CSV File Name'), []);
-    } catch (CRM_Core_Exception $e) {
-    }
-    $configuration = array();
-    if ($output && isset($output['configuration'])) {
-      $configuration = $output['configuration'];
-    }
-    if (isset($configuration['altcsvfilename']) && $configuration['altcsvfilename']) {
-      $defaults['altcsvfilename'] = $configuration['altcsvfilename'];
-    } else {
-      $defaults['altcsvfilename'] = '';
-    }
-    $form->setDefaults($defaults);
-    $this->getFileFormatClass()->buildConfigurationForm($form, $configuration);
-    $form->assign('csv_file_format_configuration', $this->fileFormatClass->getConfigurationTemplateFileName());
-  }
-
-  /**
-   * When this filter type has configuration specify the template file name
-   * for the configuration form.
-   *
-   * @return false|string
-   */
-  public function getConfigurationTemplateFileName():? string {
-    return "CRM/DataprocessorOutputExport/Form/Configuration/CSV.tpl";
-  }
-
-
-  /**
-   * Process the submitted values and create a configuration array
-   *
-   * @param $submittedValues
-   * @param array $output
-   * @return array
-   */
-  public function processConfiguration($submittedValues, &$output): array {
-    $configuration = parent::processConfiguration($submittedValues, $output);
-
-    if (isset($submittedValues['altcsvfilename']) && ''!==$submittedValues['altcsvfilename']) {
-      $configuration['altcsvfilename'] = CRM_Utils_String::munge($this->removeFileExtensionFromFileName($submittedValues['altcsvfilename'])) . '.' . $this->getExtension();
-    }
-    $this->getFileFormatClass()->processConfiguration($submittedValues, $configuration);
-    return $configuration;
-  }
-
-  /**
-   * This function is called prior to removing an output
-   *
-   * @param array $output
-   * @return void
-   */
-  public function deleteOutput($output) {
-    // Do nothing
-  }
-
-
-  /**
-   * Returns the mime type of the export file.
-   *
-   * @return string
-   */
-  public function mimeType(): string {
-    return 'text/csv';
-  }
-
-  /**
-   * Returns the url for the page/form this output will show to the user
-   *
-   * @param array $output
-   * @param array $dataProcessor
-   * @return string
-   */
-  public function getTitleForExport($output, $dataProcessor): string {
-    return E::ts('Download as CSV');
-  }
-
-  /**
-   * Returns the url for the page/form this output will show to the user
-   *
-   * @param array $output
-   * @param array $dataProcessor
-   * @return string|false
-   */
-  public function getExportFileIcon($output, $dataProcessor):? string {
-    return '<i class="fa fa-file-excel-o">&nbsp;</i>';
-  }
-
-  protected function createHeader($filename, AbstractProcessorType $dataProcessorClass, $configuration, $dataProcessor, $idField=null, $selectedIds=array(), $formValues=array()) {
-    $fields = new DataSpecification();
-    try {
-      $fields = $dataProcessorClass->getDataFlow()->getDataSpecification();
-    } catch (InvalidFlowException|FieldExistsException $e) {
-    }
-
-    $this->getFileFormatClass()->createHeader($fields, $filename, $configuration);
-  }
-
-  protected function exportRecords(string $filename, AbstractProcessorType $dataProcessor, array $configuration, string $idField=null, array $selectedIds=array(), array $formValues=array()) {
-    $fields = new DataSpecification();
-    try {
-      $fields = $dataProcessor->getDataFlow()->getDataSpecification();
-    } catch (InvalidFlowException|FieldExistsException $e) {
-    }
-
-    try {
-      while($record = $dataProcessor->getDataFlow()->nextRecord()) {
-        $rowIsSelected = true;
-        if (isset($idField) && is_array($selectedIds) && count($selectedIds)) {
-          $rowIsSelected = false;
-          $id = $record[$idField]->rawValue;
-          if (in_array($id, $selectedIds)) {
-            $rowIsSelected = true;
-          }
-        }
-        if ($rowIsSelected) {
-          $row = [];
-          foreach ($record as $column => $value) {
-            $row[$column] = $value->formattedValue;
-          }
-          $this->getFileFormatClass()->addRecord($fields, $filename, $row, $configuration);
-        }
-      }
-    } catch (EndOfFlowException|InvalidFlowException|DataFlowException $e) {
-      // Do nothing
-    }
-    $this->getFileFormatClass()->closeFile($fields, $filename, $configuration);
-  }
-
-
 }
diff --git a/CRM/DataprocessorOutputExport/PDF.php b/CRM/DataprocessorOutputExport/PDF.php
index 67c0f026e18e58f50437014ff92cfe10ec896082..6f01177672abca388850e95ae8e8ae04ff40b4c5 100644
--- a/CRM/DataprocessorOutputExport/PDF.php
+++ b/CRM/DataprocessorOutputExport/PDF.php
@@ -16,18 +16,20 @@ class CRM_DataprocessorOutputExport_PDF extends CRM_DataprocessorOutputExport_Ab
   /**
    * Returns the directory name for storing temporary files.
    *
+   * @param array $configuration
    * @return String
    */
-  public function getDirectory(): string {
+  public function getDirectory(array $configuration=[]): string {
     return 'dataprocessor_export_pdf';
   }
 
   /**
    * Returns the file extension.
    *
+   * @param array $configuration
    * @return String
    */
-  public function getExtension(): string {
+  public function getExtension(array $configuration=[]): string {
     return 'pdf';
   }
 
@@ -224,9 +226,10 @@ class CRM_DataprocessorOutputExport_PDF extends CRM_DataprocessorOutputExport_Ab
   /**
    * Returns the mime type of the export file.
    *
+   * @param array $configuration
    * @return string
    */
-  public function mimeType(): string {
+  public function mimeType(array $configuration=[]): string {
     return 'application/pdf';
   }
 
diff --git a/Civi/DataProcessor/FileFormat/CSVFileFormat.php b/Civi/DataProcessor/FileFormat/CSVFileFormat.php
index 28742082a3691a7548a0d3959002d18c80b28780..47f3965e1eb1e946bdd23952c8a3911c5ae243a5 100644
--- a/Civi/DataProcessor/FileFormat/CSVFileFormat.php
+++ b/Civi/DataProcessor/FileFormat/CSVFileFormat.php
@@ -44,6 +44,14 @@ class CSVFileFormat implements Fileformat {
     return 'csv';
   }
 
+  /**
+   * @return string
+   */
+  public function getExportFileIcon(): string {
+    return '<i class="fa fa-file-excel-o">&nbsp;</i>';
+  }
+
+
   /**
    * Returns true when this configuration has additional configuration.
    *
diff --git a/Civi/DataProcessor/FileFormat/Fileformat.php b/Civi/DataProcessor/FileFormat/Fileformat.php
index 0f1d355430f0c6ef801bad74817c87a091791744..aab8c98118cba35fe00b0fe7d32d5226c66fa1d1 100644
--- a/Civi/DataProcessor/FileFormat/Fileformat.php
+++ b/Civi/DataProcessor/FileFormat/Fileformat.php
@@ -33,6 +33,11 @@ interface Fileformat {
    */
   public function getFileExtension(): string;
 
+  /**
+   * @return string
+   */
+  public function getExportFileIcon(): string;
+
   /**
    * Returns true when this configuration has additional configuration.
    *
diff --git a/templates/CRM/DataprocessorOutputExport/Form/Configuration/CSV.tpl b/templates/CRM/DataprocessorOutputExport/Form/Configuration/Spreadsheet.tpl
similarity index 64%
rename from templates/CRM/DataprocessorOutputExport/Form/Configuration/CSV.tpl
rename to templates/CRM/DataprocessorOutputExport/Form/Configuration/Spreadsheet.tpl
index 9a4e55848ccc3c392b32c57022d3401dd4ec8946..1611a00c4d476bda94407af230e6c39fdfa2c352 100644
--- a/templates/CRM/DataprocessorOutputExport/Form/Configuration/CSV.tpl
+++ b/templates/CRM/DataprocessorOutputExport/Form/Configuration/Spreadsheet.tpl
@@ -1,16 +1,16 @@
 {crmScope extensionKey='dataprocessor'}
   <div class="crm-section">
-      <div class="label">{$form.altcsvfilename.label}</div>
-      <div class="content">{$form.altcsvfilename.html}</div>
+      <div class="label">{$form.altfilename.label}</div>
+      <div class="content">{$form.altfilename.html}</div>
       <div class="clear"></div>
   </div>
-  {include file=$csv_file_format_configuration}
+  {include file=$spreadsheet_file_format_configuration}
   <div class="crm-section">
     <div class="label">{$form.anonymous.label}</div>
     <div class="content">{$form.anonymous.html}
       <p class="description">
-        {ts}Tick this box when you want to make the CSV available for non-logged in users. <br>
-        This could be necessary when another system is importing this csv file on a regular basis. E.g. a website with
+        {ts}Tick this box when you want to make the file available for non-logged in users. <br>
+        This could be necessary when another system is importing this file on a regular basis. E.g. a website with
         a public agenda of the upcoming events.
         <br><strong>Caution:</strong> when you check this box the data becomes available without logging so this might lead to a data breach.{/ts}</p>
     </div>