Commit 8bf52738 authored by Carsten  Rose's avatar Carsten Rose
Browse files

implements #10751. Allow images to be concatenated for PDF download.

parent df2f71d6
Pipeline #3526 passed with stages
in 6 minutes and 35 seconds
...@@ -103,7 +103,7 @@ Setup in :ref:`configuration` ...@@ -103,7 +103,7 @@ Setup in :ref:`configuration`
* *download*: * *download*:
* During a download (especially by using wkhtml), temporary files are not deleted automatically. Also the * During a download (especially by using wkhtml), temporary files are not deleted automatically. Also the
``wkhtmltopdf`` and ``pdfunite`` command lines will be logged to :ref:`QFQ_LOG`. Use this only to debug problems on download. ``wkhtmltopdf``, ``pdfunite``, ``pdf2img`` command lines will be logged to :ref:`QFQ_LOG`. Use this only to debug problems on download.
.. _MAIL_LOG: .. _MAIL_LOG:
......
...@@ -42,6 +42,7 @@ The following features are only tested / supported on linux hosts: ...@@ -42,6 +42,7 @@ The following features are only tested / supported on linux hosts:
* General: QFQ is coded to run on Linux hosts, preferable on Debian derivates like Ubuntu. * General: QFQ is coded to run on Linux hosts, preferable on Debian derivates like Ubuntu.
* HTML to PDF conversion - command `wkhtmltopdf`. * HTML to PDF conversion - command `wkhtmltopdf`.
* Concatenation of PDF files - command `pdfunite`. * Concatenation of PDF files - command `pdfunite`.
* Convert of imges to PDF files - command `img2pdf`.
* PDF decrypt (used for merge with pdfunite) - command `qpdf`. * PDF decrypt (used for merge with pdfunite) - command `qpdf`.
* PDF decrypt (used for merge with pdfunite) - command `gs` - in case `qpdf` is not successful. * PDF decrypt (used for merge with pdfunite) - command `gs` - in case `qpdf` is not successful.
* Mime type detection for uploads - command `file`. * Mime type detection for uploads - command `file`.
...@@ -59,7 +60,7 @@ To normalize UTF8 input, *php-intl* package is needed by ...@@ -59,7 +60,7 @@ To normalize UTF8 input, *php-intl* package is needed by
* normalizer::normalize() * normalizer::normalize()
For the :ref:`download` function, the programs `pdfunite`, `qpdf`, `gs` and `file` are necessary to concatenate PDF files. For the :ref:`download` function, the programs `img2pdf`, `pdfunite`, `qpdf`, `gs` and `file` are necessary to concatenate PDF files.
Preparation for Ubuntu:: Preparation for Ubuntu::
...@@ -409,6 +410,8 @@ Extension Manager: QFQ Configuration ...@@ -409,6 +410,8 @@ Extension Manager: QFQ Configuration
+-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+ +-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+
| cmdPdfunite | pdfunite | PathFilename of pdfunite. Optional variables like LD_LIBRARY_PATH=... | | cmdPdfunite | pdfunite | PathFilename of pdfunite. Optional variables like LD_LIBRARY_PATH=... |
+-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+ +-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+
| cmdImg2pdf | img2pdf | PathFilename of img2pdf. Optional variables like LD_LIBRARY_PATH=... |
+-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+
| sendEMailOptions | -o tls=yes | General options. Check: http://caspian.dotconf.net/menu/Software/SendEmail | | sendEMailOptions | -o tls=yes | General options. Check: http://caspian.dotconf.net/menu/Software/SendEmail |
+-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+ +-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+
| documentation | http://docs.typo3.org... | Link to the online documentation of QFQ. Every QFQ installation also | | documentation | http://docs.typo3.org... | Link to the online documentation of QFQ. Every QFQ installation also |
......
...@@ -303,6 +303,7 @@ const ERROR_DOWNLOAD_FILE_NOT_READABLE = 1705; ...@@ -303,6 +303,7 @@ const ERROR_DOWNLOAD_FILE_NOT_READABLE = 1705;
const ERROR_DOWNLOAD_FOPEN_BLOCKED = 1706; const ERROR_DOWNLOAD_FOPEN_BLOCKED = 1706;
const ERROR_DOWNLOAD_JSON_CONVERT = 1707; const ERROR_DOWNLOAD_JSON_CONVERT = 1707;
const ERROR_DOWNLOAD_MERGE_FAILED = 1708; const ERROR_DOWNLOAD_MERGE_FAILED = 1708;
const ERROR_DOWNLOAD_CONVERT_FAILED = 1709;
// Excel // Excel
const ERROR_EXCEL_POSITION_ARGUMENT_EMPTY = 1800; const ERROR_EXCEL_POSITION_ARGUMENT_EMPTY = 1800;
...@@ -649,6 +650,7 @@ const SYSTEM_CMD_CONVERT = 'cmdConvert'; ...@@ -649,6 +650,7 @@ const SYSTEM_CMD_CONVERT = 'cmdConvert';
const SYSTEM_CMD_QPDF = 'cmdQpdf'; const SYSTEM_CMD_QPDF = 'cmdQpdf';
const SYSTEM_CMD_GS = 'cmdGs'; const SYSTEM_CMD_GS = 'cmdGs';
const SYSTEM_CMD_PDFUNITE = 'cmdPdfunite'; const SYSTEM_CMD_PDFUNITE = 'cmdPdfunite';
const SYSTEM_CMD_IMG2PDF = 'cmdImg2pdf';
// Thumbnail // Thumbnail
const SYSTEM_THUMBNAIL_DIR_SECURE = 'thumbnailDirSecure'; const SYSTEM_THUMBNAIL_DIR_SECURE = 'thumbnailDirSecure';
......
...@@ -101,6 +101,7 @@ class Download { ...@@ -101,6 +101,7 @@ class Download {
$this->qpdf = $this->store->getVar(SYSTEM_CMD_QPDF, STORE_SYSTEM); $this->qpdf = $this->store->getVar(SYSTEM_CMD_QPDF, STORE_SYSTEM);
$this->gs = $this->store->getVar(SYSTEM_CMD_GS, STORE_SYSTEM); $this->gs = $this->store->getVar(SYSTEM_CMD_GS, STORE_SYSTEM);
$this->pdfunite = $this->store->getVar(SYSTEM_CMD_PDFUNITE, STORE_SYSTEM); $this->pdfunite = $this->store->getVar(SYSTEM_CMD_PDFUNITE, STORE_SYSTEM);
$this->img2Pdf = $this->store->getVar(SYSTEM_CMD_IMG2PDF, STORE_SYSTEM);
if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_DOWNLOAD, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))) { if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_DOWNLOAD, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))) {
$this->downloadDebugLog = $this->store->getVar(SYSTEM_SQL_LOG, STORE_SYSTEM); $this->downloadDebugLog = $this->store->getVar(SYSTEM_SQL_LOG, STORE_SYSTEM);
...@@ -109,38 +110,47 @@ class Download { ...@@ -109,38 +110,47 @@ class Download {
/** /**
* Concatenate all named files to one PDF file. Return name of new full PDF. * Concatenate all named files to one PDF file. Return name of new full PDF.
* If a source file is an image, it will be converted to a PDF.
* *
* @param array $files * @param array $files Array of source files
* @param array $filesCleanLater Array of temporarily created files
* *
* @return string - fileName of concatenated file * @return string - fileName of concatenated file
* @throws \CodeException * @throws \CodeException
* @throws \DownloadException * @throws \DownloadException
* @throws \UserFormException * @throws \UserFormException
*/ */
private function concatPdfFiles(array $files) { private function concatFilesToPdf(array $files, array &$filesCleanLater) {
// Remove empty entries. Might happen if there was no upload // Remove empty entries. Might happen if there was no upload
$files = OnArray::removeEmptyElementsFromArray($files); $files = OnArray::removeEmptyElementsFromArray($files);
// Check that all files exist and are readable // Check that all files are of type 'application/pdf'
foreach ($files AS $filename) { foreach ($files as $key => $filename) {
// Check that all files exist and are readable
if (!is_readable($filename)) { if (!is_readable($filename)) {
throw new \DownloadException("Error reading file $filename. Not found or no permission", ERROR_DOWNLOAD_FILE_NOT_READABLE); throw new \DownloadException("Error reading file $filename. Not found or no permission", ERROR_DOWNLOAD_FILE_NOT_READABLE);
} }
}
if (count($files) === 0) {
return '';
}
// Check that all files are of type 'application/pdf'
foreach ($files AS $filename) {
$mimetype = mime_content_type($filename); $mimetype = mime_content_type($filename);
// If file is an image, convert to PDF
if (substr($mimetype, 0, 6) == 'image/') {
$tmpName = $this->doImgToPdf($filename);
$filesCleanLater[] = $tmpName; // remember to purge later.
$files[$key] = $tmpName; // replace original reference against temporary one.
$mimetype = 'application/pdf';
}
if ($mimetype != 'application/pdf') { if ($mimetype != 'application/pdf') {
throw new \DownloadException("Error concat file $filename. Mimetype 'application/pdf' expected, got: $mimetype", ERROR_DOWNLOAD_UNEXPECTED_MIME_TYPE); throw new \DownloadException("Error concat file $filename. Mimetype 'application/pdf' expected, got: $mimetype", ERROR_DOWNLOAD_UNEXPECTED_MIME_TYPE);
} }
} }
if (count($files) === 0) {
return '';
}
if (count($files) == 1) { if (count($files) == 1) {
return $files[0]; return $files[0];
} }
...@@ -176,6 +186,36 @@ class Download { ...@@ -176,6 +186,36 @@ class Download {
return $concatFile; return $concatFile;
} }
/**
* Convert an image to PDF
*
* @param $fileImage
* @throws \DownloadException
*/
private function doImgToPdf($fileImage) {
$filePdf = HelperFile::tempnam();
if (false === $filePdf) {
throw new \DownloadException('Error creating output file.', ERROR_DOWNLOAD_CREATE_NEW_FILE);
}
// img2pdf --pagesize A4 -o out.pdf *.jpg
$cmd = $this->img2Pdf . ' --pagesize A4 -o ' . $filePdf . ' ' . escapeshellarg($fileImage);
if ($this->downloadDebugLog != '') {
Logger::logMessage("Download: $cmd", $this->downloadDebugLog);
}
exec($cmd, $rcOutput, $rc);
if ($rc != 0) {
throw new \DownloadException (json_encode([ERROR_MESSAGE_TO_USER => "Failed to convert image to PDF.",
ERROR_MESSAGE_TO_DEVELOPER => "CMD: $cmd<br>RC: $rc<br>Output: " . implode("<br>", $rcOutput)])
, ERROR_DOWNLOAD_CONVERT_FAILED);
}
return $filePdf;
}
/** /**
* Fires the merge command. * Fires the merge command.
* If for any reason the command fails: check if the reason is 'unencrypted files'. * If for any reason the command fails: check if the reason is 'unencrypted files'.
...@@ -457,7 +497,7 @@ class Download { ...@@ -457,7 +497,7 @@ class Download {
$len = strlen(TMP_FILE_PREFIX); $len = strlen(TMP_FILE_PREFIX);
$ii = 1; $ii = 1;
foreach ($files AS $filename) { foreach ($files as $filename) {
$localName = substr($filename, strrpos($filename, '/') + 1); $localName = substr($filename, strrpos($filename, '/') + 1);
if (substr($localName, 0, $len) == TMP_FILE_PREFIX) { if (substr($localName, 0, $len) == TMP_FILE_PREFIX) {
...@@ -499,7 +539,8 @@ class Download { ...@@ -499,7 +539,8 @@ class Download {
*/ */
private function doElements(array $vars, $outputMode) { private function doElements(array $vars, $outputMode) {
$tmpFiles = array(); $srcFiles = array();
$filesCleanLater = array();
$workDir = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM); $workDir = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM);
HelperFile::chdir($workDir); HelperFile::chdir($workDir);
...@@ -525,7 +566,7 @@ class Download { ...@@ -525,7 +566,7 @@ class Download {
$tmpData = array(); $tmpData = array();
foreach ($elements as $element) { foreach ($elements as $element) {
$data = ''; $data = '';
$tmpFiles[] = $this->getElement($element, $downloadMode, $data); $srcFiles[] = $this->getElement($element, $downloadMode, $data);
if (!empty($data)) { if (!empty($data)) {
$tmpData[] = $data; $tmpData[] = $data;
} }
...@@ -534,7 +575,7 @@ class Download { ...@@ -534,7 +575,7 @@ class Download {
// Export, Concat File(s) // Export, Concat File(s)
switch ($downloadMode) { switch ($downloadMode) {
case DOWNLOAD_MODE_ZIP: case DOWNLOAD_MODE_ZIP:
$filename = $this->zipFiles($tmpFiles); $filename = $this->zipFiles($srcFiles);
if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) { if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
$vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename); $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
} }
...@@ -542,7 +583,7 @@ class Download { ...@@ -542,7 +583,7 @@ class Download {
case DOWNLOAD_MODE_EXCEL: case DOWNLOAD_MODE_EXCEL:
$excel = new Excel(); $excel = new Excel();
$filename = $excel->process($tmpFiles, $tmpData); $filename = $excel->process($srcFiles, $tmpData);
if (empty($filename) || !file_exists($filename)) { if (empty($filename) || !file_exists($filename)) {
throw new \DownloadException(json_encode( throw new \DownloadException(json_encode(
...@@ -560,7 +601,7 @@ class Download { ...@@ -560,7 +601,7 @@ class Download {
break; break;
case DOWNLOAD_MODE_FILE: case DOWNLOAD_MODE_FILE:
$filename = $tmpFiles[0]; $filename = $srcFiles[0];
if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) { if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
$vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename); $vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
} }
...@@ -568,11 +609,11 @@ class Download { ...@@ -568,11 +609,11 @@ class Download {
case DOWNLOAD_MODE_PDF: case DOWNLOAD_MODE_PDF:
$filename = $this->concatPdfFiles($tmpFiles); $filename = $this->concatFilesToPdf($srcFiles, $filesCleanLater);
// try to find a meaningful filename // try to find a meaningful filename
if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) { if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
if (count($tmpFiles) > 1) { if (count($srcFiles) > 1) {
$vars[DOWNLOAD_EXPORT_FILENAME] = DOWNLOAD_OUTPUT_FILENAME . ".pdf"; $vars[DOWNLOAD_EXPORT_FILENAME] = DOWNLOAD_OUTPUT_FILENAME . ".pdf";
} else { } else {
if (HelperFile::isQfqTemp($filename)) { if (HelperFile::isQfqTemp($filename)) {
...@@ -595,6 +636,8 @@ class Download { ...@@ -595,6 +636,8 @@ class Download {
ERROR_MESSAGE_TO_DEVELOPER => "File: $filename"]), ERROR_IO_FILE_EXIST); ERROR_MESSAGE_TO_DEVELOPER => "File: $filename"]), ERROR_IO_FILE_EXIST);
} }
$filesCleanLater[] = $filename;
switch ($outputMode) { switch ($outputMode) {
case OUTPUT_MODE_FILE: case OUTPUT_MODE_FILE:
...@@ -602,12 +645,10 @@ class Download { ...@@ -602,12 +645,10 @@ class Download {
case OUTPUT_MODE_COPY_TO_FILE: case OUTPUT_MODE_COPY_TO_FILE:
HelperFile::copy($filename, $vars[DOWNLOAD_EXPORT_FILENAME]); HelperFile::copy($filename, $vars[DOWNLOAD_EXPORT_FILENAME]);
HelperFile::cleanTempFiles([$filename]);
break; break;
case OUTPUT_MODE_DIRECT: case OUTPUT_MODE_DIRECT:
$this->outputFile($filename, $vars[DOWNLOAD_EXPORT_FILENAME]); $this->outputFile($filename, $vars[DOWNLOAD_EXPORT_FILENAME]);
HelperFile::cleanTempFiles([$filename]);
$filename = ''; $filename = '';
break; break;
...@@ -615,6 +656,8 @@ class Download { ...@@ -615,6 +656,8 @@ class Download {
throw new \CodeException('Unkown mode: ' . $outputMode, ERROR_UNKNOWN_MODE); throw new \CodeException('Unkown mode: ' . $outputMode, ERROR_UNKNOWN_MODE);
} }
HelperFile::cleanTempFiles($filesCleanLater);
return $filename; return $filename;
} }
......
...@@ -377,6 +377,7 @@ class Config { ...@@ -377,6 +377,7 @@ class Config {
SYSTEM_CMD_QPDF => 'qpdf', SYSTEM_CMD_QPDF => 'qpdf',
SYSTEM_CMD_GS => 'gs', SYSTEM_CMD_GS => 'gs',
SYSTEM_CMD_PDFUNITE => 'pdfunite', SYSTEM_CMD_PDFUNITE => 'pdfunite',
SYSTEM_CMD_IMG2PDF => 'img2pdf',
SYSTEM_THUMBNAIL_DIR_SECURE => SYSTEM_THUMBNAIL_DIR_SECURE_DEFAULT, SYSTEM_THUMBNAIL_DIR_SECURE => SYSTEM_THUMBNAIL_DIR_SECURE_DEFAULT,
SYSTEM_THUMBNAIL_DIR_PUBLIC => SYSTEM_THUMBNAIL_DIR_PUBLIC_DEFAULT, SYSTEM_THUMBNAIL_DIR_PUBLIC => SYSTEM_THUMBNAIL_DIR_PUBLIC_DEFAULT,
......
...@@ -14,6 +14,7 @@ use IMATHUZH\Qfq\Core\Helper\Logger; ...@@ -14,6 +14,7 @@ use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray; use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Sanitize; use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support; use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
/* /*
* Stores: * Stores:
......
...@@ -413,6 +413,7 @@ class StoreTest extends TestCase { ...@@ -413,6 +413,7 @@ class StoreTest extends TestCase {
SYSTEM_CMD_QPDF => 'qpdf', SYSTEM_CMD_QPDF => 'qpdf',
SYSTEM_CMD_GS => 'gs', SYSTEM_CMD_GS => 'gs',
SYSTEM_CMD_PDFUNITE => 'pdfunite', SYSTEM_CMD_PDFUNITE => 'pdfunite',
SYSTEM_CMD_IMG2PDF => 'img2pdf',
]; ];
$body = <<< EOT $body = <<< EOT
......
...@@ -37,6 +37,9 @@ cmdGs = gs ...@@ -37,6 +37,9 @@ cmdGs = gs
# cat=config/config; type=string; label=Command 'pdfunite':Default is 'pdfunite'. Will be used to merge PDFs. # cat=config/config; type=string; label=Command 'pdfunite':Default is 'pdfunite'. Will be used to merge PDFs.
cmdPdfunite = pdfunite cmdPdfunite = pdfunite
# cat=config/config; type=string; label=Command 'img2pdf':Default is 'img2pdf'. Will be used to convert images to PDFs.
cmdImg2pdf = img2pdf
# cat=config/email; type=string; label=Options for SendEMail:Default is empty. General options. Check: http://caspian.dotconf.net/menu/Software/SendEmail. E.g.: 'sendEMail=-o tls=yes' # cat=config/email; type=string; label=Options for SendEMail:Default is empty. General options. Check: http://caspian.dotconf.net/menu/Software/SendEmail. E.g.: 'sendEMail=-o tls=yes'
sendEMailOptions = sendEMailOptions =
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment