Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev
This commit is contained in:
commit
dcbaf1b1af
@ -13,22 +13,23 @@ if (! defined('MERGED_CLAIM_FILE_TYPE')) {
|
|||||||
|
|
||||||
if (! function_exists('merge_ticket_pdfs')) {
|
if (! function_exists('merge_ticket_pdfs')) {
|
||||||
/**
|
/**
|
||||||
* Merge all active PDF rows in claim_files for a given ticket_master id
|
* Merge all active PDF/image rows in claim_files for a given ticket_master id
|
||||||
* into one combined PDF and register that PDF as a new claim_files row
|
* into one combined PDF and register that PDF as a new claim_files row
|
||||||
* with file_type = MERGED_CLAIM_FILE_TYPE.
|
* with file_type = MERGED_CLAIM_FILE_TYPE.
|
||||||
*
|
*
|
||||||
* Source rows are picked from claim_files where:
|
* Source rows are picked from claim_files where:
|
||||||
* - ticket_id = $ticket_master_id
|
* - ticket_id = $ticket_master_id
|
||||||
* - is_active = 1
|
* - is_active = 1
|
||||||
* - mime_type = 'application/pdf'
|
* - mime_type IN $opts['include_mime_types'] (default PDF/JPG/PNG)
|
||||||
* - file_type IN $opts['include_file_types'] (default [2, 3])
|
* - file_type IN $opts['include_file_types'] (default [1, 2, 3])
|
||||||
*
|
*
|
||||||
* @param int $ticket_master_id
|
* @param int $ticket_master_id
|
||||||
* @param array $opts {
|
* @param array $opts {
|
||||||
* @var bool $replace Default true. Soft-delete previous merged row before re-creating.
|
* @var bool $replace Default true. Soft-delete previous merged row before re-creating.
|
||||||
* @var int $created_by Override created_by user id on the inserted row.
|
* @var int $created_by Override created_by user id on the inserted row.
|
||||||
* @var int $ticket_type Default 1. Stored on the inserted claim_files row.
|
* @var int $ticket_type Default 1. Stored on the inserted claim_files row.
|
||||||
* @var array $include_file_types Default [2, 3].
|
* @var array $include_file_types Default [1, 2, 3].
|
||||||
|
* @var array $include_mime_types Default ['application/pdf', 'image/jpeg', 'image/png'].
|
||||||
* }
|
* }
|
||||||
* @return array {status, merged_file_id, file_name, pages, source_count, message}
|
* @return array {status, merged_file_id, file_name, pages, source_count, message}
|
||||||
*/
|
*/
|
||||||
@ -38,7 +39,8 @@ if (! function_exists('merge_ticket_pdfs')) {
|
|||||||
'replace' => true,
|
'replace' => true,
|
||||||
'created_by' => null,
|
'created_by' => null,
|
||||||
'ticket_type' => 1,
|
'ticket_type' => 1,
|
||||||
'include_file_types' => [2, 3],
|
'include_file_types' => [1, 2],
|
||||||
|
'include_mime_types' => ['application/pdf', 'image/jpeg', 'image/png'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = [
|
$result = [
|
||||||
@ -60,14 +62,13 @@ if (! function_exists('merge_ticket_pdfs')) {
|
|||||||
$rows = $claimFiles
|
$rows = $claimFiles
|
||||||
->where('ticket_id', $ticket_master_id)
|
->where('ticket_id', $ticket_master_id)
|
||||||
->where('is_active', 1)
|
->where('is_active', 1)
|
||||||
->where('mime_type', 'application/pdf')
|
|
||||||
->whereIn('file_type', $opts['include_file_types'])
|
->whereIn('file_type', $opts['include_file_types'])
|
||||||
->orderBy('id', 'ASC')
|
->orderBy('id', 'ASC')
|
||||||
->findAll();
|
->findAll();
|
||||||
|
|
||||||
if (empty($rows)) {
|
if (empty($rows)) {
|
||||||
$result['status'] = true;
|
$result['status'] = true;
|
||||||
$result['message'] = 'No PDF files to merge';
|
$result['message'] = 'No PDF/image files to merge';
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +76,7 @@ if (! function_exists('merge_ticket_pdfs')) {
|
|||||||
. 'uploads' . DIRECTORY_SEPARATOR
|
. 'uploads' . DIRECTORY_SEPARATOR
|
||||||
. 'claim_files' . DIRECTORY_SEPARATOR;
|
. 'claim_files' . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
$sourcePaths = [];
|
$sourceFiles = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$name = ! empty($row['url']) ? $row['url'] : ($row['file_name'] ?? '');
|
$name = ! empty($row['url']) ? $row['url'] : ($row['file_name'] ?? '');
|
||||||
if (empty($name)) {
|
if (empty($name)) {
|
||||||
@ -84,23 +85,47 @@ if (! function_exists('merge_ticket_pdfs')) {
|
|||||||
// url column may sometimes hold a full URL; we only care about the file basename on disk.
|
// url column may sometimes hold a full URL; we only care about the file basename on disk.
|
||||||
$full = $uploadDir . basename($name);
|
$full = $uploadDir . basename($name);
|
||||||
if (is_file($full) && is_readable($full)) {
|
if (is_file($full) && is_readable($full)) {
|
||||||
$sourcePaths[] = $full;
|
$mime = merge_ticket_pdf_resolve_mime($full, (string) ($row['mime_type'] ?? ''));
|
||||||
|
if (! in_array($mime, $opts['include_mime_types'], true)) {
|
||||||
|
log_message('error', "merge_ticket_pdfs | unsupported source mime {$mime} | claim_file_id={$row['id']} | path={$full}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceFiles[] = [
|
||||||
|
'path' => $full,
|
||||||
|
'mime' => $mime,
|
||||||
|
'id' => $row['id'] ?? null,
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
log_message('error', "merge_ticket_pdfs | missing PDF on disk | claim_file_id={$row['id']} | path={$full}");
|
log_message('error', "merge_ticket_pdfs | missing file on disk | claim_file_id={$row['id']} | path={$full}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($sourcePaths)) {
|
if (empty($sourceFiles)) {
|
||||||
$result['message'] = 'No readable PDF files on disk';
|
$result['message'] = 'No readable PDF/image files on disk';
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
$result['source_count'] = count($sourcePaths);
|
$result['source_count'] = count($sourceFiles);
|
||||||
|
log_message(
|
||||||
|
'error',
|
||||||
|
'merge_ticket_pdfs | source files resolved | ticket_id=' . $ticket_master_id . ' | sources=' . json_encode(array_map(static function ($sourceFile) {
|
||||||
|
return [
|
||||||
|
'id' => $sourceFile['id'],
|
||||||
|
'mime' => $sourceFile['mime'],
|
||||||
|
'file' => basename($sourceFile['path']),
|
||||||
|
];
|
||||||
|
}, $sourceFiles))
|
||||||
|
);
|
||||||
|
|
||||||
$tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
|
$tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
|
||||||
if (! is_dir($tempDir)) {
|
if (! is_dir($tempDir)) {
|
||||||
@mkdir($tempDir, 0775, true);
|
@mkdir($tempDir, 0775, true);
|
||||||
}
|
}
|
||||||
|
$mpdfCacheDir = $tempDir . DIRECTORY_SEPARATOR . 'mpdf';
|
||||||
|
if (! is_dir($mpdfCacheDir)) {
|
||||||
|
@mkdir($mpdfCacheDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
$mergedName = 'merged_' . $ticket_master_id . '_' . time() . '_' . bin2hex(random_bytes(5)) . '.pdf';
|
$mergedName = 'merged_' . $ticket_master_id . '_' . time() . '_' . bin2hex(random_bytes(5)) . '.pdf';
|
||||||
$mergedPath = $uploadDir . $mergedName;
|
$mergedPath = $uploadDir . $mergedName;
|
||||||
@ -112,26 +137,34 @@ if (! function_exists('merge_ticket_pdfs')) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$totalPages = 0;
|
$totalPages = 0;
|
||||||
foreach ($sourcePaths as $src) {
|
foreach ($sourceFiles as $sourceFile) {
|
||||||
|
$src = $sourceFile['path'];
|
||||||
try {
|
try {
|
||||||
|
if ($sourceFile['mime'] === 'application/pdf') {
|
||||||
$pageCount = $mpdf->setSourceFile($src);
|
$pageCount = $mpdf->setSourceFile($src);
|
||||||
for ($p = 1; $p <= $pageCount; $p++) {
|
for ($p = 1; $p <= $pageCount; $p++) {
|
||||||
$tplId = $mpdf->importPage($p);
|
$tplId = $mpdf->importPage($p);
|
||||||
$size = $mpdf->getTemplateSize($tplId);
|
$mpdf->AddPage();
|
||||||
$mpdf->AddPageByArray([
|
// adjustPageSize=true makes the output page match the imported page.
|
||||||
'orientation' => ($size['width'] > $size['height']) ? 'L' : 'P',
|
$mpdf->useTemplate($tplId, 0, 0, null, null, true);
|
||||||
'sheet-size' => [$size['width'], $size['height']],
|
|
||||||
]);
|
|
||||||
$mpdf->useTemplate($tplId);
|
|
||||||
$totalPages++;
|
$totalPages++;
|
||||||
}
|
}
|
||||||
|
log_message('error', "merge_ticket_pdfs | added PDF | file={$src} | pages={$pageCount}");
|
||||||
|
} elseif (in_array($sourceFile['mime'], ['image/jpeg', 'image/png'], true)) {
|
||||||
|
if (merge_ticket_pdf_add_image_page($mpdf, $src)) {
|
||||||
|
$totalPages++;
|
||||||
|
log_message('error', "merge_ticket_pdfs | added image | file={$src} | mime={$sourceFile['mime']}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_message('error', "merge_ticket_pdfs | unsupported source mime {$sourceFile['mime']} | {$src}");
|
||||||
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
log_message('error', "merge_ticket_pdfs | failed to import {$src} | " . $e->getMessage());
|
log_message('error', "merge_ticket_pdfs | failed to import {$src} | " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($totalPages === 0) {
|
if ($totalPages === 0) {
|
||||||
$result['message'] = 'All source PDFs failed to import';
|
$result['message'] = 'All source PDF/image files failed to import';
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,3 +225,98 @@ if (! function_exists('merge_ticket_pdfs')) {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! function_exists('merge_ticket_pdf_resolve_mime')) {
|
||||||
|
/**
|
||||||
|
* Resolve file type from stored MIME, filesystem MIME, and extension.
|
||||||
|
* Some uploads can be saved as image/jpg, empty MIME, or octet-stream in DB.
|
||||||
|
*/
|
||||||
|
function merge_ticket_pdf_resolve_mime(string $path, string $storedMime = ''): string
|
||||||
|
{
|
||||||
|
$storedMime = strtolower(trim($storedMime));
|
||||||
|
if ($storedMime === 'image/jpg') {
|
||||||
|
return 'image/jpeg';
|
||||||
|
}
|
||||||
|
if (in_array($storedMime, ['application/pdf', 'image/jpeg', 'image/png'], true)) {
|
||||||
|
return $storedMime;
|
||||||
|
}
|
||||||
|
|
||||||
|
$detectedMime = '';
|
||||||
|
if (function_exists('mime_content_type')) {
|
||||||
|
$detectedMime = strtolower((string) @mime_content_type($path));
|
||||||
|
if ($detectedMime === 'image/jpg') {
|
||||||
|
return 'image/jpeg';
|
||||||
|
}
|
||||||
|
if (in_array($detectedMime, ['application/pdf', 'image/jpeg', 'image/png'], true)) {
|
||||||
|
return $detectedMime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||||
|
if ($ext === 'pdf') {
|
||||||
|
return 'application/pdf';
|
||||||
|
}
|
||||||
|
if (in_array($ext, ['jpg', 'jpeg'], true)) {
|
||||||
|
return 'image/jpeg';
|
||||||
|
}
|
||||||
|
if ($ext === 'png') {
|
||||||
|
return 'image/png';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $detectedMime ?: ($storedMime ?: 'application/octet-stream');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('merge_ticket_pdf_add_image_page')) {
|
||||||
|
/**
|
||||||
|
* Add an uploaded image as a single PDF page, preserving portrait/landscape
|
||||||
|
* orientation and fitting the image proportionally inside the page.
|
||||||
|
*/
|
||||||
|
function merge_ticket_pdf_add_image_page(Mpdf $mpdf, string $imagePath): bool
|
||||||
|
{
|
||||||
|
$imageInfo = @getimagesize($imagePath);
|
||||||
|
$ext = strtolower(pathinfo($imagePath, PATHINFO_EXTENSION));
|
||||||
|
$imageType = $ext === 'png' || ((int) ($imageInfo[2] ?? 0) === IMAGETYPE_PNG) ? 'png' : 'jpg';
|
||||||
|
|
||||||
|
$imageWidthPx = (int) ($imageInfo[0] ?? 0);
|
||||||
|
$imageHeightPx = (int) ($imageInfo[1] ?? 0);
|
||||||
|
$hasSize = $imageWidthPx > 0 && $imageHeightPx > 0;
|
||||||
|
$isLandscape = $hasSize && $imageWidthPx > $imageHeightPx;
|
||||||
|
|
||||||
|
$pageWidth = $isLandscape ? 297 : 210;
|
||||||
|
$pageHeight = $isLandscape ? 210 : 297;
|
||||||
|
$margin = 0;
|
||||||
|
|
||||||
|
$availableWidth = $pageWidth - ($margin * 2);
|
||||||
|
$availableHeight = $pageHeight - ($margin * 2);
|
||||||
|
if ($hasSize) {
|
||||||
|
$scale = min($availableWidth / $imageWidthPx, $availableHeight / $imageHeightPx);
|
||||||
|
$drawWidth = $imageWidthPx * $scale;
|
||||||
|
$drawHeight = $imageHeightPx * $scale;
|
||||||
|
$x = ($pageWidth - $drawWidth) / 2;
|
||||||
|
$y = ($pageHeight - $drawHeight) / 2;
|
||||||
|
} else {
|
||||||
|
// Some PNGs fail getimagesize(), but mPDF can still render them.
|
||||||
|
// Use a portrait A4 fallback and let mPDF calculate image height.
|
||||||
|
log_message('error', "merge_ticket_pdfs | image size unavailable, using fallback page | {$imagePath}");
|
||||||
|
$drawWidth = $availableWidth;
|
||||||
|
$drawHeight = 0;
|
||||||
|
$x = $margin;
|
||||||
|
$y = $margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mpdf->AddPageByArray([
|
||||||
|
'orientation' => $isLandscape ? 'L' : 'P',
|
||||||
|
'sheet-size' => 'A4',
|
||||||
|
'margin-left' => 0,
|
||||||
|
'margin-right' => 0,
|
||||||
|
'margin-top' => 0,
|
||||||
|
'margin-bottom' => 0,
|
||||||
|
'margin-header' => 0,
|
||||||
|
'margin-footer' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mpdf->Image($imagePath, $x, $y, $drawWidth, $drawHeight, $imageType);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user