Initial xentral_oss_20.3.c9ffacf
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf;
|
||||
|
||||
use Xentral\Components\Pdf\Merger\FpdiPdfMerger;
|
||||
use Xentral\Components\Pdf\Merger\GhostScriptPdfMerger;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'PdfMerger' => 'onInitPdfMerger',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PdfMerger
|
||||
*/
|
||||
public static function onInitPdfMerger()
|
||||
{
|
||||
return new PdfMerger(new FpdiPdfMerger(), new GhostScriptPdfMerger());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class FileExistsException extends RuntimeException implements PdfComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class FileNotFoundException extends RuntimeException implements PdfComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class FileNotWritableException extends RuntimeException implements PdfComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf\Exception;
|
||||
|
||||
use InvalidArgumentException as SplInvalidArgumentExceptionAlias;
|
||||
|
||||
class InvalidArgumentException extends SplInvalidArgumentExceptionAlias implements PdfComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface PdfComponentExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class PdfIncompatibleException extends RuntimeException implements PdfComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class ShellCommandFailedException extends RuntimeException implements PdfComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf\Merger;
|
||||
|
||||
use Xentral\Components\Pdf\Exception\FileExistsException;
|
||||
use Xentral\Components\Pdf\Exception\FileNotFoundException;
|
||||
use Xentral\Components\Pdf\Exception\FileNotWritableException;
|
||||
use Xentral\Components\Pdf\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Pdf\Exception\PdfComponentExceptionInterface;
|
||||
|
||||
abstract class AbstractPdfMerger
|
||||
{
|
||||
/**
|
||||
* @param array|string[] $sourceFiles
|
||||
* @param string $targetFile
|
||||
*
|
||||
* @throws PdfComponentExceptionInterface
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function mergeFiles(array $sourceFiles, $targetFile);
|
||||
|
||||
/**
|
||||
* @param array|string[] $sourceFiles
|
||||
* @param string $targetFile
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws FileExistsException
|
||||
* @throws FileNotWritableException
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function preCheckMergeFiles(array $sourceFiles, $targetFile)
|
||||
{
|
||||
if (empty($sourceFiles)) {
|
||||
throw new InvalidArgumentException('Can not merge PDFs. Required parameter "sourceFiles" is empty.');
|
||||
}
|
||||
if (empty($targetFile)) {
|
||||
throw new InvalidArgumentException('Can not merge PDFs. Required parameter "$targetFile" is empty.');
|
||||
}
|
||||
if (is_file($targetFile)) {
|
||||
throw new FileExistsException(sprintf(
|
||||
'Can not merge PDFs. Target file "%s" already exists.', $targetFile
|
||||
));
|
||||
}
|
||||
if (!$this->isFilePathWriteable($targetFile)) {
|
||||
throw new FileNotWritableException(sprintf(
|
||||
'Can not merge PDFs. Unable to create target file "%s" .', $targetFile
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($sourceFiles as $sourceFile) {
|
||||
if (!is_file($sourceFile)) {
|
||||
throw new FileNotFoundException(sprintf(
|
||||
'Can not merge PDFs. Source file "%s" does not exist.', $sourceFile
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filePath
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isFilePathWriteable($filePath)
|
||||
{
|
||||
$result = @fopen($filePath, 'wb');
|
||||
@unlink($filePath);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf\Merger;
|
||||
|
||||
use fpdi;
|
||||
use Xentral\Components\Pdf\Exception\FileExistsException;
|
||||
use Xentral\Components\Pdf\Exception\FileNotFoundException;
|
||||
use Xentral\Components\Pdf\Exception\FileNotWritableException;
|
||||
use Xentral\Components\Pdf\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Pdf\Exception\PdfIncompatibleException;
|
||||
|
||||
/**
|
||||
* Anti-Corruption-Layer für alte FPDI-Klasse
|
||||
*/
|
||||
final class FpdiPdfMerger extends AbstractPdfMerger
|
||||
{
|
||||
/**
|
||||
* @param string[] $sourceFiles Array with files pathes
|
||||
* @param string $targetFile Path to merged file
|
||||
*
|
||||
* @throws InvalidArgumentException If required parameter is invalid/empty
|
||||
* @throws FileNotWritableException If target file is not writable
|
||||
* @throws FileExistsException If target file already exists
|
||||
* @throws FileNotFoundException If one of the source files does not exist
|
||||
* @throws PdfIncompatibleException If XRef-Pointer is missing
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function mergeFiles(array $sourceFiles, $targetFile)
|
||||
{
|
||||
$this->preCheckMergeFiles($sourceFiles, $targetFile);
|
||||
|
||||
$fpdi = new fpdi('P', 'mm', 'A4');
|
||||
|
||||
foreach ($sourceFiles as $file) {
|
||||
if (!$this->hasPdfXrefError($file)) {
|
||||
throw new PdfIncompatibleException(sprintf(
|
||||
'Can not merge PDFs. Unable to find pointer to xref table in file "%s".', $file
|
||||
));
|
||||
}
|
||||
$pageCount = $fpdi->setSourceFile($file);
|
||||
|
||||
for ($pageNumber = 1; $pageNumber <= $pageCount; $pageNumber++) {
|
||||
$templateId = $fpdi->importPage($pageNumber);
|
||||
$size = $fpdi->getTemplateSize($templateId);
|
||||
|
||||
// Create empty page
|
||||
if ($size['w'] > $size['h']) {
|
||||
// Landscape orientation
|
||||
$fpdi->addPage('L', [$size['h'], $size['w']]);
|
||||
} else {
|
||||
// Portrait orientation
|
||||
$fpdi->addPage('P', [$size['w'], $size['h']]);
|
||||
}
|
||||
|
||||
// Import page
|
||||
$fpdi->useTemplate($templateId);
|
||||
}
|
||||
}
|
||||
|
||||
$fpdi->Output($targetFile, 'F');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to detect xref-Error
|
||||
*
|
||||
* @see \fpdi_pdf_parser::pdf_find_xref
|
||||
*
|
||||
* @param string $pdfFile Path to pdf file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPdfXrefError($pdfFile)
|
||||
{
|
||||
$resource = @fopen($pdfFile, 'rb');
|
||||
fseek($resource, -50, SEEK_END);
|
||||
$data = fread($resource, 50);
|
||||
|
||||
$foundXrefOffset = preg_match('/startxref\s*(\d+)\s*%%EOF\s*$/', $data, $matches);
|
||||
if (!$foundXrefOffset) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$xrefOffset = (int)$matches[1];
|
||||
fseek($resource, $xrefOffset);
|
||||
$data = trim(fgets($resource));
|
||||
|
||||
return $data === 'xref';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf\Merger;
|
||||
|
||||
use Xentral\Components\Pdf\Exception\PdfComponentExceptionInterface;
|
||||
use Xentral\Components\Pdf\Exception\ShellCommandFailedException;
|
||||
|
||||
final class GhostScriptPdfMerger extends AbstractPdfMerger
|
||||
{
|
||||
/**
|
||||
* @param array $sourceFiles
|
||||
* @param string $targetFile
|
||||
*
|
||||
* @throws PdfComponentExceptionInterface
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function mergeFiles(array $sourceFiles, $targetFile)
|
||||
{
|
||||
$this->preCheckMergeFiles($sourceFiles, $targetFile);
|
||||
if (!$this->isGhostscriptAvailable()) {
|
||||
throw new ShellCommandFailedException('Ghostscript command "gs" is missing or not callable.');
|
||||
}
|
||||
|
||||
$sourceFilesParam = '';
|
||||
foreach ($sourceFiles as $sourceFile) {
|
||||
$sourceFilesParam .= escapeshellarg($sourceFile) . ' ';
|
||||
}
|
||||
|
||||
$cmdTemplate = 'gs -dQUIET -dNOPAUSE -dBATCH ';
|
||||
$cmdTemplate .= '-dPDFSETTINGS=/prepress '; // High quality, color preserving, 300 dpi images
|
||||
$cmdTemplate .= '-dAutoRotatePages=/None -sDEVICE=pdfwrite -sOUTPUTFILE=%s %s';
|
||||
$cmd = sprintf($cmdTemplate, escapeshellarg($targetFile), $sourceFilesParam);
|
||||
|
||||
$this->executeShellCommand($cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $command
|
||||
*
|
||||
* @throws ShellCommandFailedException
|
||||
*
|
||||
* @return array|string[] Zeilenweise Ausgabe des Commands
|
||||
*/
|
||||
private function executeShellCommand($command)
|
||||
{
|
||||
if ($this->isFunctionDisabled('exec')) {
|
||||
throw new ShellCommandFailedException('PHP function "exec" is disabled by php.ini settings.');
|
||||
}
|
||||
|
||||
$output = [];
|
||||
@exec($command, $output, $returnVar);
|
||||
|
||||
switch ($returnVar) {
|
||||
case 0:
|
||||
// Kein Fehler
|
||||
break;
|
||||
case 1:
|
||||
throw new ShellCommandFailedException('General error: ' . implode(' ', $output));
|
||||
break;
|
||||
case 126:
|
||||
throw new ShellCommandFailedException('Can not execute "gs" command.');
|
||||
break;
|
||||
case 127:
|
||||
throw new ShellCommandFailedException('Command "gs" not found.');
|
||||
break;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private function isGhostscriptAvailable()
|
||||
{
|
||||
if ($this->isFunctionDisabled('shell_exec')) {
|
||||
throw new ShellCommandFailedException('PHP function "shell_exec" is disabled by php.ini settings.');
|
||||
}
|
||||
|
||||
$result = @shell_exec('gs');
|
||||
|
||||
return !empty($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $functionName PHP function name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFunctionDisabled($functionName)
|
||||
{
|
||||
$functionName = trim($functionName);
|
||||
$disabledFunctions = explode(',', ini_get('disable_functions'));
|
||||
foreach ($disabledFunctions as $disabledFunction) {
|
||||
if (trim($disabledFunction) === $functionName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Pdf;
|
||||
|
||||
use Xentral\Components\Pdf\Exception\FileExistsException;
|
||||
use Xentral\Components\Pdf\Exception\FileNotFoundException;
|
||||
use Xentral\Components\Pdf\Exception\FileNotWritableException;
|
||||
use Xentral\Components\Pdf\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Pdf\Exception\PdfIncompatibleException;
|
||||
use Xentral\Components\Pdf\Exception\ShellCommandFailedException;
|
||||
use Xentral\Components\Pdf\Merger\FpdiPdfMerger;
|
||||
use Xentral\Components\Pdf\Merger\GhostScriptPdfMerger;
|
||||
|
||||
final class PdfMerger
|
||||
{
|
||||
/** @var FpdiPdfMerger $fpdi */
|
||||
private $fpdi;
|
||||
|
||||
/** @var GhostScriptPdfMerger $gs */
|
||||
private $gs;
|
||||
|
||||
/**
|
||||
* @param FpdiPdfMerger $fpdi
|
||||
* @param GhostScriptPdfMerger $ghostScript
|
||||
*/
|
||||
public function __construct(FpdiPdfMerger $fpdi, GhostScriptPdfMerger $ghostScript)
|
||||
{
|
||||
$this->fpdi = $fpdi;
|
||||
$this->gs = $ghostScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sourceFiles Array with file pathes
|
||||
* @param string|null $targetFile Path to merged file; Path will be generated if null
|
||||
*
|
||||
* @throws InvalidArgumentException If required parameter is invalid/empty
|
||||
* @throws FileNotWritableException If target file is not writable
|
||||
* @throws FileExistsException If target file already exists
|
||||
* @throws FileNotFoundException If one of the source files does not exist
|
||||
* @throws PdfIncompatibleException If XRef-Pointer is missing
|
||||
* @throws ShellCommandFailedException IF GhostScript fails to execute a shell command
|
||||
*
|
||||
* @return string Path to merged file; Same as parameter $targetFile if not null
|
||||
*/
|
||||
public function merge(array $sourceFiles, $targetFile = null)
|
||||
{
|
||||
if ($targetFile === null) {
|
||||
$targetFile = realpath(sys_get_temp_dir()) . '/' . uniqid('pdfmerge_', false) . '.pdf';
|
||||
}
|
||||
|
||||
try {
|
||||
$this->fpdi->mergeFiles($sourceFiles, $targetFile);
|
||||
} catch (PdfIncompatibleException $exception) {
|
||||
$this->gs->mergeFiles($sourceFiles, $targetFile);
|
||||
}
|
||||
|
||||
return $targetFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filePath
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPdfXrefError($filePath)
|
||||
{
|
||||
return $this->fpdi->hasPdfXrefError($filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# PDF-Dateien zusammenführen
|
||||
|
||||
## Beispiel
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$sourceFiles = [
|
||||
'/tmp/example1.pdf',
|
||||
'/tmp/example2.pdf',
|
||||
'/tmp/example3.pdf',
|
||||
];
|
||||
$targetFile = '/tmp/merge.pdf';
|
||||
|
||||
$merger = $this->app->Container->get('PdfMerger');
|
||||
$merger->merge($sourceFiles, $targetFile);
|
||||
```
|
||||
|
||||
Im Fehlerfall wird eine Exception geworfen. Alle Exceptions implementieren
|
||||
`\Xentral\Components\Http\Exception\PdfComponentExceptionInterface`.
|
||||
|
||||
Der zweite Parameter der `merge`-Methode ist optional. Wenn `null` übergeben wird, wird eine zufällige Datei im
|
||||
System-Temp-Ordner erzeugt. Der Dateipfad der erzeugten Datei steht dann im Rückgabewert der `merge`-Methode.
|
||||
Reference in New Issue
Block a user