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`
* *download*:
* 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:
......
......@@ -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.
* HTML to PDF conversion - command `wkhtmltopdf`.
* 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 `gs` - in case `qpdf` is not successful.
* Mime type detection for uploads - command `file`.
......@@ -59,7 +60,7 @@ To normalize UTF8 input, *php-intl* package is needed by
* 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::
......@@ -409,6 +410,8 @@ Extension Manager: QFQ Configuration
+-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+
| 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 |
+-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+
| 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;
const ERROR_DOWNLOAD_FOPEN_BLOCKED = 1706;
const ERROR_DOWNLOAD_JSON_CONVERT = 1707;
const ERROR_DOWNLOAD_MERGE_FAILED = 1708;
const ERROR_DOWNLOAD_CONVERT_FAILED = 1709;
// Excel
const ERROR_EXCEL_POSITION_ARGUMENT_EMPTY = 1800;
......@@ -649,6 +650,7 @@ const SYSTEM_CMD_CONVERT = 'cmdConvert';
const SYSTEM_CMD_QPDF = 'cmdQpdf';
const SYSTEM_CMD_GS = 'cmdGs';
const SYSTEM_CMD_PDFUNITE = 'cmdPdfunite';
const SYSTEM_CMD_IMG2PDF = 'cmdImg2pdf';
// Thumbnail
const SYSTEM_THUMBNAIL_DIR_SECURE = 'thumbnailDirSecure';
......
......@@ -101,6 +101,7 @@ class Download {
$this->qpdf = $this->store->getVar(SYSTEM_CMD_QPDF, STORE_SYSTEM);
$this->gs = $this->store->getVar(SYSTEM_CMD_GS, 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))) {
$this->downloadDebugLog = $this->store->getVar(SYSTEM_SQL_LOG, STORE_SYSTEM);
......@@ -109,38 +110,47 @@ class Download {
/**
* 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
* @throws \CodeException
* @throws \DownloadException
* @throws \UserFormException
*/
private function concatPdfFiles(array $files) {
private function concatFilesToPdf(array $files, array &$filesCleanLater) {
// Remove empty entries. Might happen if there was no upload
$files = OnArray::removeEmptyElementsFromArray($files);
// Check that all files are of type 'application/pdf'
foreach ($files as $key => $filename) {
// Check that all files exist and are readable
foreach ($files AS $filename) {
if (!is_readable($filename)) {
throw new \DownloadException("Error reading file $filename. Not found or no permission", ERROR_DOWNLOAD_FILE_NOT_READABLE);
}
}
if (count($files) === 0) {
return '';
$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';
}
// Check that all files are of type 'application/pdf'
foreach ($files AS $filename) {
$mimetype = mime_content_type($filename);
if ($mimetype != 'application/pdf') {
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) {
return $files[0];
}
......@@ -176,6 +186,36 @@ class Download {
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.
* If for any reason the command fails: check if the reason is 'unencrypted files'.
......@@ -457,7 +497,7 @@ class Download {
$len = strlen(TMP_FILE_PREFIX);
$ii = 1;
foreach ($files AS $filename) {
foreach ($files as $filename) {
$localName = substr($filename, strrpos($filename, '/') + 1);
if (substr($localName, 0, $len) == TMP_FILE_PREFIX) {
......@@ -499,7 +539,8 @@ class Download {
*/
private function doElements(array $vars, $outputMode) {
$tmpFiles = array();
$srcFiles = array();
$filesCleanLater = array();
$workDir = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM);
HelperFile::chdir($workDir);
......@@ -525,7 +566,7 @@ class Download {
$tmpData = array();
foreach ($elements as $element) {
$data = '';
$tmpFiles[] = $this->getElement($element, $downloadMode, $data);
$srcFiles[] = $this->getElement($element, $downloadMode, $data);
if (!empty($data)) {
$tmpData[] = $data;
}
......@@ -534,7 +575,7 @@ class Download {
// Export, Concat File(s)
switch ($downloadMode) {
case DOWNLOAD_MODE_ZIP:
$filename = $this->zipFiles($tmpFiles);
$filename = $this->zipFiles($srcFiles);
if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
$vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
}
......@@ -542,7 +583,7 @@ class Download {
case DOWNLOAD_MODE_EXCEL:
$excel = new Excel();
$filename = $excel->process($tmpFiles, $tmpData);
$filename = $excel->process($srcFiles, $tmpData);
if (empty($filename) || !file_exists($filename)) {
throw new \DownloadException(json_encode(
......@@ -560,7 +601,7 @@ class Download {
break;
case DOWNLOAD_MODE_FILE:
$filename = $tmpFiles[0];
$filename = $srcFiles[0];
if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
$vars[DOWNLOAD_EXPORT_FILENAME] = basename($filename);
}
......@@ -568,11 +609,11 @@ class Download {
case DOWNLOAD_MODE_PDF:
$filename = $this->concatPdfFiles($tmpFiles);
$filename = $this->concatFilesToPdf($srcFiles, $filesCleanLater);
// try to find a meaningful filename
if (empty($vars[DOWNLOAD_EXPORT_FILENAME])) {
if (count($tmpFiles) > 1) {
if (count($srcFiles) > 1) {
$vars[DOWNLOAD_EXPORT_FILENAME] = DOWNLOAD_OUTPUT_FILENAME . ".pdf";
} else {
if (HelperFile::isQfqTemp($filename)) {
......@@ -595,6 +636,8 @@ class Download {
ERROR_MESSAGE_TO_DEVELOPER => "File: $filename"]), ERROR_IO_FILE_EXIST);
}
$filesCleanLater[] = $filename;
switch ($outputMode) {
case OUTPUT_MODE_FILE:
......@@ -602,12 +645,10 @@ class Download {
case OUTPUT_MODE_COPY_TO_FILE:
HelperFile::copy($filename, $vars[DOWNLOAD_EXPORT_FILENAME]);
HelperFile::cleanTempFiles([$filename]);
break;
case OUTPUT_MODE_DIRECT:
$this->outputFile($filename, $vars[DOWNLOAD_EXPORT_FILENAME]);
HelperFile::cleanTempFiles([$filename]);
$filename = '';
break;
......@@ -615,6 +656,8 @@ class Download {
throw new \CodeException('Unkown mode: ' . $outputMode, ERROR_UNKNOWN_MODE);
}
HelperFile::cleanTempFiles($filesCleanLater);
return $filename;
}
......
......@@ -377,6 +377,7 @@ class Config {
SYSTEM_CMD_QPDF => 'qpdf',
SYSTEM_CMD_GS => 'gs',
SYSTEM_CMD_PDFUNITE => 'pdfunite',
SYSTEM_CMD_IMG2PDF => 'img2pdf',
SYSTEM_THUMBNAIL_DIR_SECURE => SYSTEM_THUMBNAIL_DIR_SECURE_DEFAULT,
SYSTEM_THUMBNAIL_DIR_PUBLIC => SYSTEM_THUMBNAIL_DIR_PUBLIC_DEFAULT,
......
......@@ -14,6 +14,7 @@ use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
/*
* Stores:
......
......@@ -413,6 +413,7 @@ class StoreTest extends TestCase {
SYSTEM_CMD_QPDF => 'qpdf',
SYSTEM_CMD_GS => 'gs',
SYSTEM_CMD_PDFUNITE => 'pdfunite',
SYSTEM_CMD_IMG2PDF => 'img2pdf',
];
$body = <<< EOT
......
......@@ -37,6 +37,9 @@ cmdGs = gs
# cat=config/config; type=string; label=Command 'pdfunite':Default is 'pdfunite'. Will be used to merge PDFs.
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'
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