677 lines
30 KiB
PHP
677 lines
30 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use App\Http\Requests\LambdaRequest;
|
|
use League\Csv\Reader;
|
|
|
|
class ProcessVideoSubmissions extends Command
|
|
{
|
|
/**
|
|
* The name and signature of the console command.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'video:process-submissions {file : Path to the CSV file}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Process video submissions from CSV file (similar to customdataandvideoform)';
|
|
|
|
/**
|
|
* Execute the console command.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function handle()
|
|
{
|
|
$filePath = $this->argument('file');
|
|
|
|
if (!file_exists($filePath)) {
|
|
$this->error("File not found: {$filePath}");
|
|
return 1;
|
|
}
|
|
|
|
if (!is_readable($filePath)) {
|
|
$this->error("File is not readable: {$filePath}");
|
|
return 1;
|
|
}
|
|
|
|
$this->info("Processing CSV file: {$filePath}");
|
|
Log::info("===== ProcessVideoSubmissions START =====");
|
|
Log::info("File: {$filePath}");
|
|
|
|
try {
|
|
// Read CSV file
|
|
$csv = Reader::createFromPath($filePath, 'r');
|
|
$csv->setHeaderOffset(0);
|
|
|
|
$headers = $csv->getHeader();
|
|
$records = $csv->getRecords();
|
|
|
|
// Convert iterator to array for counting
|
|
$rowsArray = iterator_to_array($records);
|
|
$totalRows = count($rowsArray);
|
|
|
|
if ($totalRows === 0) {
|
|
$this->error("CSV file is empty or invalid");
|
|
return 1;
|
|
}
|
|
|
|
$this->info("Found {$totalRows} rows to process");
|
|
Log::info("Total rows: {$totalRows}");
|
|
|
|
// Normalize headers (trim and lowercase for matching)
|
|
$normalizedHeaders = [];
|
|
foreach ($headers as $index => $header) {
|
|
$normalizedHeaders[strtolower(trim($header))] = $header;
|
|
}
|
|
|
|
// Required columns - check for id, partition, and formdata
|
|
$requiredColumns = ['id', 'partition', 'formdata'];
|
|
$missingColumns = [];
|
|
foreach ($requiredColumns as $col) {
|
|
if (!isset($normalizedHeaders[$col])) {
|
|
$missingColumns[] = $col;
|
|
}
|
|
}
|
|
|
|
if (!empty($missingColumns)) {
|
|
$this->error("Missing required columns: " . implode(', ', $missingColumns));
|
|
$this->info("Available columns: " . implode(', ', array_keys($normalizedHeaders)));
|
|
return 1;
|
|
}
|
|
|
|
$successCount = 0;
|
|
$errorCount = 0;
|
|
$skippedCount = 0;
|
|
$errors = [];
|
|
|
|
$bar = $this->output->createProgressBar($totalRows);
|
|
$bar->start();
|
|
|
|
$rowIndex = 0;
|
|
foreach ($rowsArray as $row) {
|
|
$rowIndex++;
|
|
$bar->advance();
|
|
|
|
try {
|
|
// Extract basic fields
|
|
$submissionId = $this->getCellValue($row, $normalizedHeaders, 'id');
|
|
$partition = $this->getCellValue($row, $normalizedHeaders, 'partition');
|
|
$formdataRaw = $this->getCellValue($row, $normalizedHeaders, 'formdata');
|
|
|
|
// Extract optional fields
|
|
$city = $this->getCellValue($row, $normalizedHeaders, 'city');
|
|
$employeeCode = $this->getCellValue($row, $normalizedHeaders, 'employee_code');
|
|
$videoWidth = $this->getCellValue($row, $normalizedHeaders, 'video_width');
|
|
$videoHeight = $this->getCellValue($row, $normalizedHeaders, 'video_height');
|
|
$userId = $this->getCellValue($row, $normalizedHeaders, 'user_id');
|
|
$userName = $this->getCellValue($row, $normalizedHeaders, 'user_name');
|
|
$userAvatar = $this->getCellValue($row, $normalizedHeaders, 'user_avatar');
|
|
|
|
// Validate required fields
|
|
if (empty($submissionId)) {
|
|
$skippedCount++;
|
|
$errors[] = "Row " . ($rowIndex + 1) . ": Missing id (submission_id)";
|
|
continue;
|
|
}
|
|
|
|
if (empty($partition)) {
|
|
$skippedCount++;
|
|
$errors[] = "Row " . ($rowIndex + 1) . ": Missing partition";
|
|
continue;
|
|
}
|
|
|
|
if (empty($formdataRaw)) {
|
|
$skippedCount++;
|
|
$errors[] = "Row " . ($rowIndex + 1) . ": Missing formdata";
|
|
continue;
|
|
}
|
|
|
|
// Parse formdata (could be JSON string or already an array)
|
|
$formdata = null;
|
|
if (is_string($formdataRaw)) {
|
|
// Try to decode as JSON
|
|
$decoded = json_decode($formdataRaw, true);
|
|
if (json_last_error() === JSON_ERROR_NONE) {
|
|
$formdata = $decoded;
|
|
} else {
|
|
// If not JSON, try to parse as serialized PHP
|
|
$formdata = @unserialize($formdataRaw);
|
|
if ($formdata === false) {
|
|
$skippedCount++;
|
|
$errors[] = "Row " . ($rowIndex + 1) . ": Unable to parse formdata";
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
$formdata = $formdataRaw;
|
|
}
|
|
|
|
// Extract video_url from formdata.upload_video
|
|
$videoUrl = null;
|
|
if (is_array($formdata) && isset($formdata['upload_video'])) {
|
|
$videoUrl = $formdata['upload_video'];
|
|
} elseif (is_object($formdata) && isset($formdata->upload_video)) {
|
|
$videoUrl = $formdata->upload_video;
|
|
}
|
|
|
|
// Extract doctor_name from formdata.orignal_doctor_name
|
|
$doctorName = null;
|
|
if (is_array($formdata) && isset($formdata['orignal_doctor_name'])) {
|
|
$doctorName = $formdata['orignal_doctor_name'];
|
|
} elseif (is_object($formdata) && isset($formdata->orignal_doctor_name)) {
|
|
$doctorName = $formdata->orignal_doctor_name;
|
|
}
|
|
|
|
// Extract city from formdata if not already set
|
|
if (empty($city)) {
|
|
if (is_array($formdata) && isset($formdata['city'])) {
|
|
$city = $formdata['city'];
|
|
} elseif (is_object($formdata) && isset($formdata->city)) {
|
|
$city = $formdata->city;
|
|
}
|
|
}
|
|
|
|
// Extract employee_code from formdata if not already set
|
|
if (empty($employeeCode)) {
|
|
if (is_array($formdata) && isset($formdata['employee_code'])) {
|
|
$employeeCode = $formdata['employee_code'];
|
|
} elseif (is_object($formdata) && isset($formdata->employee_code)) {
|
|
$employeeCode = $formdata->employee_code;
|
|
}
|
|
}
|
|
|
|
// Validate extracted fields
|
|
if (empty($videoUrl)) {
|
|
$skippedCount++;
|
|
$errors[] = "Row " . ($rowIndex + 1) . ": Missing upload_video in formdata";
|
|
continue;
|
|
}
|
|
|
|
Log::info("Processing row " . ($rowIndex + 1) . ": submission_id={$submissionId}");
|
|
|
|
// Get video dimensions if not provided (skip if memory intensive)
|
|
// MediaConvert can auto-detect dimensions, so this is optional
|
|
if (empty($videoWidth) || empty($videoHeight)) {
|
|
// Try to get dimensions using ffprobe on URL directly (without downloading)
|
|
Log::info("Attempting to get video dimensions for: {$videoUrl}");
|
|
$dimensions = $this->getVideoDimensionsFromUrl($videoUrl);
|
|
Log::info("getVideoDimensionsFromUrl returned: " . print_r($dimensions, true));
|
|
if ($dimensions && isset($dimensions['width']) && isset($dimensions['height']) && $dimensions['width'] > 0 && $dimensions['height'] > 0) {
|
|
$videoWidth = (int)$dimensions['width'];
|
|
$videoHeight = (int)$dimensions['height'];
|
|
Log::info("Got video dimensions: {$videoWidth}x{$videoHeight} for {$videoUrl}");
|
|
} else {
|
|
// If we can't get dimensions, set to null and let MediaConvert auto-detect
|
|
Log::info("Could not get video dimensions for {$videoUrl}, MediaConvert will auto-detect");
|
|
$videoWidth = null;
|
|
$videoHeight = null;
|
|
}
|
|
} else {
|
|
Log::info("Using provided video dimensions: {$videoWidth}x{$videoHeight}");
|
|
}
|
|
|
|
Log::info("Before jobParams - videoWidth: " . var_export($videoWidth, true) . ", videoHeight: " . var_export($videoHeight, true));
|
|
|
|
// Process frame image with doctor name
|
|
$processedFrameUrl = null;
|
|
if ($doctorName) {
|
|
$processedFrameUrl = $this->processFrameImage($doctorName, $city, $videoWidth, $videoHeight, $employeeCode);
|
|
}
|
|
|
|
// Build user_info
|
|
$userInfo = null;
|
|
if ($userId) {
|
|
$userInfo = [
|
|
'id' => $userId,
|
|
'name' => $userName ?: '',
|
|
'avatar' => $userAvatar ?: ''
|
|
];
|
|
}
|
|
|
|
// Extract user_partition from partition (e.g., "rapport-vitalis-1769675014-..." -> "rapport-vitalis")
|
|
$userPartition = null;
|
|
if ($partition) {
|
|
$parts = explode('-', $partition);
|
|
if (count($parts) >= 2) {
|
|
$userPartition = $parts[0] . '-' . $parts[1];
|
|
}
|
|
}
|
|
|
|
// Build job parameters
|
|
$jobParams = [
|
|
'video_id' => "1770791678-532ce634-58ef-4662-89b3-8aad4ddcfcd8",
|
|
'video_file_url' => $videoUrl,
|
|
'reference_entity' => 'form_submission',
|
|
'reference_id' => $submissionId,
|
|
'reference_partition' => $partition,
|
|
'reference_to_update' => 'processed_video',
|
|
'reference_updated_flag' => 'has_video_processed',
|
|
'user_info' => $userInfo,
|
|
'user_partition' => $userPartition
|
|
];
|
|
|
|
// Only add video dimensions if we have them (MediaConvert can auto-detect)
|
|
if (!empty($videoWidth) && !empty($videoHeight)) {
|
|
$jobParams['video_width'] = (int)$videoWidth;
|
|
$jobParams['video_height'] = (int)$videoHeight;
|
|
Log::info("Added video dimensions to jobParams: {$videoWidth}x{$videoHeight}");
|
|
} else {
|
|
Log::info("Skipping video dimensions - videoWidth: " . var_export($videoWidth, true) . ", videoHeight: " . var_export($videoHeight, true));
|
|
}
|
|
|
|
// Add processed frame URL if available
|
|
if ($processedFrameUrl) {
|
|
$jobParams['processed_frame_url'] = $processedFrameUrl;
|
|
}
|
|
|
|
// Log::info("Calling vgreet/job_create for submission: {$submissionId}");
|
|
// Log::info("Job params: " . print_r($jobParams, true));
|
|
print_r($jobParams);
|
|
//Call vgreet job_create API
|
|
$vgreetJobResp = LambdaRequest::postrequest(
|
|
'vgreet',
|
|
'job/create',
|
|
$jobParams
|
|
);
|
|
|
|
Log::info("vgreet/job_create response: " . print_r($vgreetJobResp, true));
|
|
|
|
if (isset($vgreetJobResp->statusCode) && $vgreetJobResp->statusCode == 200) {
|
|
$successCount++;
|
|
$this->line("\n✓ Row " . ($rowIndex + 1) . " processed successfully");
|
|
} else {
|
|
$errorCount++;
|
|
$errorMsg = isset($vgreetJobResp->data) ? $vgreetJobResp->data : 'Unknown error';
|
|
$errors[] = "Row " . ($rowIndex + 1) . ": " . $errorMsg;
|
|
$this->line("\n✗ Row " . ($rowIndex + 1) . " failed: " . $errorMsg);
|
|
}
|
|
//exit;
|
|
} catch (\Exception $e) {
|
|
$errorCount++;
|
|
$errorMsg = $e->getMessage();
|
|
$errors[] = "Row " . ($rowIndex + 1) . ": " . $errorMsg;
|
|
Log::error("Error processing row " . ($rowIndex + 1) . ": " . $errorMsg);
|
|
Log::error($e->getTraceAsString());
|
|
}
|
|
}
|
|
|
|
$bar->finish();
|
|
$this->newLine(2);
|
|
|
|
// Display results
|
|
$this->info("Processing completed!");
|
|
$this->info("Successful: {$successCount}");
|
|
$this->info("Skipped: {$skippedCount}");
|
|
$this->info("Errors: {$errorCount}");
|
|
|
|
if (!empty($errors) && count($errors) <= 20) {
|
|
$this->warn("\nErrors:");
|
|
foreach ($errors as $error) {
|
|
$this->line($error);
|
|
}
|
|
} elseif (!empty($errors)) {
|
|
$this->warn("\nFirst 20 errors:");
|
|
foreach (array_slice($errors, 0, 20) as $error) {
|
|
$this->line($error);
|
|
}
|
|
$this->warn("... and " . (count($errors) - 20) . " more errors. Check logs for details.");
|
|
}
|
|
|
|
Log::info("===== ProcessVideoSubmissions END =====");
|
|
Log::info("Successful: {$successCount}, Skipped: {$skippedCount}, Errors: {$errorCount}");
|
|
|
|
return 0;
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error("Processing failed: " . $e->getMessage());
|
|
Log::error('ProcessVideoSubmissions error', [
|
|
'file' => $filePath,
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get cell value from row by normalized header name
|
|
*/
|
|
private function getCellValue($row, $normalizedHeaders, $headerName)
|
|
{
|
|
$normalized = strtolower(trim($headerName));
|
|
if (isset($normalizedHeaders[$normalized])) {
|
|
$originalHeader = $normalizedHeaders[$normalized];
|
|
// CSV records are associative arrays with original header names as keys
|
|
return isset($row[$originalHeader]) ? trim($row[$originalHeader]) : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get video dimensions using ffprobe on URL directly (without downloading)
|
|
* Also checks for rotation metadata and swaps dimensions if needed
|
|
*/
|
|
private function getVideoDimensionsFromUrl($videoUrl)
|
|
{
|
|
try {
|
|
$videoPath = null;
|
|
if (filter_var($videoUrl, FILTER_VALIDATE_URL)) {
|
|
// It's a URL, use ffprobe directly
|
|
$videoPath = $videoUrl;
|
|
} elseif (file_exists($videoUrl)) {
|
|
// It's a local file path
|
|
$videoPath = $videoUrl;
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
// Single ffprobe command to get dimensions and all rotation metadata in JSON format
|
|
// Use -show_entries to get only stream info without packets_and_frames
|
|
$ffprobeCmd = "ffprobe -v error -select_streams v:0 -show_entries stream=width,height:stream_tags=rotate:side_data=rotation,displaymatrix -of json " . escapeshellarg($videoPath);
|
|
Log::info("Running ffprobe command: {$ffprobeCmd}");
|
|
$output = @shell_exec($ffprobeCmd);
|
|
|
|
if (!$output) {
|
|
Log::warning("ffprobe returned no output for: {$videoPath}");
|
|
return null;
|
|
}
|
|
|
|
Log::info("ffprobe output length: " . strlen($output));
|
|
|
|
// Try to parse JSON - it might be valid but very large
|
|
$data = json_decode($output, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
Log::error("JSON decode error: " . json_last_error_msg());
|
|
// The JSON might be too large or have issues, try to extract just the streams part
|
|
// Look for "streams": [ pattern
|
|
$streamsStart = strpos($output, '"streams"');
|
|
if ($streamsStart !== false) {
|
|
// Find the opening bracket after "streams"
|
|
$bracketStart = strpos($output, '[', $streamsStart);
|
|
if ($bracketStart !== false) {
|
|
// Find the matching closing bracket
|
|
$depth = 0;
|
|
$bracketEnd = $bracketStart;
|
|
for ($i = $bracketStart; $i < strlen($output); $i++) {
|
|
if ($output[$i] === '[') $depth++;
|
|
if ($output[$i] === ']') {
|
|
$depth--;
|
|
if ($depth === 0) {
|
|
$bracketEnd = $i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ($bracketEnd > $bracketStart) {
|
|
// Extract just the streams array
|
|
$streamsJson = '{"streams":' . substr($output, $bracketStart, $bracketEnd - $bracketStart + 1) . '}';
|
|
$data = json_decode($streamsJson, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
Log::error("Still JSON decode error after streams extraction: " . json_last_error_msg());
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!isset($data['streams']) || !is_array($data['streams']) || empty($data['streams'])) {
|
|
Log::warning("No stream data found in ffprobe output. Data keys: " . implode(', ', array_keys($data ?? [])));
|
|
return null;
|
|
}
|
|
|
|
if (!isset($data['streams'][0])) {
|
|
Log::warning("Streams array is empty");
|
|
return null;
|
|
}
|
|
|
|
$stream = $data['streams'][0];
|
|
$width = isset($stream['width']) ? (int)$stream['width'] : null;
|
|
$height = isset($stream['height']) ? (int)$stream['height'] : null;
|
|
|
|
if (!$width || !$height) {
|
|
return null;
|
|
}
|
|
|
|
$originalWidth = $width;
|
|
$originalHeight = $height;
|
|
|
|
// Check for rotation in multiple places
|
|
$rotationValue = null;
|
|
|
|
// Check for rotation - prefer side_data over tags as it's more accurate
|
|
$rotationValue = null;
|
|
|
|
// First check side_data rotation (more accurate)
|
|
if (isset($stream['side_data_list'])) {
|
|
foreach ($stream['side_data_list'] as $sideData) {
|
|
if (isset($sideData['rotation'])) {
|
|
$rotationValue = (int)$sideData['rotation'];
|
|
Log::info("Found rotation in side_data: {$rotationValue}°");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to stream tags if side_data not found
|
|
if ($rotationValue === null && isset($stream['tags']['rotate'])) {
|
|
$rotationValue = (int)$stream['tags']['rotate'];
|
|
Log::info("Found rotation in stream tags: {$rotationValue}°");
|
|
}
|
|
|
|
// Debug: Log the full stream data if rotation not found
|
|
if ($rotationValue === null) {
|
|
Log::info("No rotation metadata found for video. Original dimensions: {$width}x{$height}");
|
|
Log::info("Stream tags: " . json_encode($stream['tags'] ?? []));
|
|
Log::info("Side data list: " . json_encode($stream['side_data_list'] ?? []));
|
|
}
|
|
|
|
// If rotation is 90 or 270 degrees (or -90 or -270), swap width and height
|
|
// Common rotation values: 90, 180, 270, -90, -180, -270
|
|
if ($rotationValue !== null) {
|
|
$normalizedRotation = $rotationValue % 360;
|
|
if ($normalizedRotation < 0) {
|
|
$normalizedRotation += 360;
|
|
}
|
|
|
|
Log::info("Rotation value: {$rotationValue}°, Normalized: {$normalizedRotation}°");
|
|
|
|
// 90 or 270 degrees means portrait orientation - swap dimensions
|
|
if ($normalizedRotation == 90 || $normalizedRotation == 270) {
|
|
$temp = $width;
|
|
$width = $height;
|
|
$height = $temp;
|
|
Log::info("Video rotation detected: {$rotationValue}° (normalized: {$normalizedRotation}°), swapped dimensions from {$originalWidth}x{$originalHeight} to {$width}x{$height}");
|
|
} else {
|
|
Log::info("Rotation is {$normalizedRotation}°, no swap needed (dimensions: {$width}x{$height})");
|
|
}
|
|
} else {
|
|
// If no rotation metadata found, check if height > width (portrait by dimensions)
|
|
// This is a fallback for videos without rotation metadata
|
|
if ($height > $width) {
|
|
Log::info("No rotation metadata found, but video appears portrait ({$width}x{$height})");
|
|
}
|
|
}
|
|
return [
|
|
'width' => $width,
|
|
'height' => $height
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error("Failed to get video dimensions from URL: " . $e->getMessage());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Process frame image with doctor name (similar to customdataandvideoform)
|
|
*/
|
|
private function processFrameImage($doctorName, $city, $videoWidth, $videoHeight, $employeeCode = null)
|
|
{
|
|
try {
|
|
$personalisation = [
|
|
"portrait_frame_image" => "/vgreet/frames/vitaliss_frame_portrait.png",
|
|
"landscape_video_dimensions" => [
|
|
"width" => 1422,
|
|
"height" => 800
|
|
],
|
|
"type" => "video_frame",
|
|
"portrait_video_dimensions" => [
|
|
"width" => 800,
|
|
"height" => 1422
|
|
],
|
|
"landscape_frame_image" => "/vgreet/frames/vitaliss_frame_landscape.png"
|
|
];
|
|
|
|
// Determine orientation
|
|
$orientation = null;
|
|
$frame_image = null;
|
|
$frame_width = null;
|
|
$frame_height = null;
|
|
$fontSize = 18;
|
|
|
|
if ($videoWidth && $videoHeight) {
|
|
if ($videoHeight > $videoWidth) {
|
|
$orientation = 'portrait';
|
|
$frame_image = isset($personalisation['portrait_frame_image'])
|
|
? (strpos($personalisation['portrait_frame_image'], 's3://') === 0
|
|
? $personalisation['portrait_frame_image']
|
|
: "s3://rapportapp" . $personalisation['portrait_frame_image'])
|
|
: "s3://rapportapp/vgreet/frames/vitaliss_frame_portrait.png";
|
|
$frame_width = isset($personalisation['portrait_video_dimensions']['width'])
|
|
? $personalisation['portrait_video_dimensions']['width']
|
|
: 800;
|
|
$frame_height = isset($personalisation['portrait_video_dimensions']['height'])
|
|
? $personalisation['portrait_video_dimensions']['height']
|
|
: 1422;
|
|
} else {
|
|
$orientation = 'landscape';
|
|
$frame_image = isset($personalisation['landscape_frame_image'])
|
|
? (strpos($personalisation['landscape_frame_image'], 's3://') === 0
|
|
? $personalisation['landscape_frame_image']
|
|
: "s3://rapportapp" . $personalisation['landscape_frame_image'])
|
|
: "s3://rapportapp/vgreet/frames/vitaliss_frame_landscape.png";
|
|
$frame_width = isset($personalisation['landscape_video_dimensions']['width'])
|
|
? $personalisation['landscape_video_dimensions']['width']
|
|
: 1422;
|
|
$frame_height = isset($personalisation['landscape_video_dimensions']['height'])
|
|
? $personalisation['landscape_video_dimensions']['height']
|
|
: 800;
|
|
}
|
|
}
|
|
|
|
if (!$frame_image) {
|
|
Log::warning("No frame image determined, skipping frame processing");
|
|
return null;
|
|
}
|
|
|
|
// Build doctor name text
|
|
$doctor_name_text = 'Doctor: ' . $doctorName;
|
|
if ($city) {
|
|
$doctor_name_text .= ' | City: ' . $city;
|
|
}
|
|
|
|
// Set font path
|
|
$fontPath = storage_path('app/fonts/Poppins-Medium.ttf');
|
|
if (!file_exists($fontPath)) {
|
|
Log::warning("Font file not found: {$fontPath}");
|
|
return null;
|
|
}
|
|
|
|
// Download frame image from S3
|
|
$s3_relative_path = str_replace('s3://rapportapp/', '', $frame_image);
|
|
$frameTmpPath = sys_get_temp_dir() . '/' . uniqid('frame_') . '.png';
|
|
|
|
try {
|
|
$frameContent = Storage::disk('s3')->get($s3_relative_path);
|
|
file_put_contents($frameTmpPath, $frameContent);
|
|
|
|
// Load image with transparency preservation
|
|
$im = imagecreatefrompng($frameTmpPath);
|
|
if (!$im) {
|
|
Log::warning("Could not load image from '{$frameTmpPath}'");
|
|
@unlink($frameTmpPath);
|
|
return null;
|
|
}
|
|
|
|
// Enable alpha blending and save alpha channel to preserve transparency
|
|
imagealphablending($im, false);
|
|
imagesavealpha($im, true);
|
|
|
|
$fontColorRGB = [255, 255, 255]; // white
|
|
|
|
// Get text bounding box to calculate center
|
|
$bbox = imagettfbbox($fontSize, 0, $fontPath, $doctor_name_text);
|
|
$textWidth = $bbox[2] - $bbox[0];
|
|
$centerX = ($frame_width / 2) - ($textWidth / 2);
|
|
|
|
// Enable alpha blending for text rendering
|
|
imagealphablending($im, true);
|
|
|
|
// Print text at (centerX, y=50)
|
|
$color = imagecolorallocate($im, $fontColorRGB[0], $fontColorRGB[1], $fontColorRGB[2]);
|
|
imagettftext($im, $fontSize, 0, $centerX, 50, $color, $fontPath, $doctor_name_text);
|
|
|
|
// Disable alpha blending before saving to preserve transparency
|
|
imagealphablending($im, false);
|
|
imagesavealpha($im, true);
|
|
|
|
// Save new image to temp location with transparency
|
|
$outputFrame = sys_get_temp_dir() . '/' . uniqid('doctor_frame_') . '.png';
|
|
imagepng($im, $outputFrame, 9); // Use compression level 9
|
|
imagedestroy($im);
|
|
|
|
Log::info("Doctor name '{$doctor_name_text}' printed on frame: {$outputFrame}");
|
|
|
|
// Upload processed frame back to S3
|
|
$file_doctorname = preg_replace('/[^A-Za-z0-9_-]/', '_', $doctorName);
|
|
$file_doctorname = trim($file_doctorname, '_');
|
|
$processedFrameName = 'processed_' . uniqid() . '_' . $file_doctorname . '.png';
|
|
|
|
$uploadedPath = Storage::disk('s3')->putFileAs(
|
|
'vgreet/frames',
|
|
new \Illuminate\Http\File($outputFrame),
|
|
$processedFrameName
|
|
);
|
|
|
|
// Get full S3 URL
|
|
$processedFrameS3Path = env('AWS_URL') . '/' . $uploadedPath;
|
|
Log::info("Processed frame uploaded to S3: {$processedFrameS3Path}");
|
|
|
|
// Clean up temp files
|
|
@unlink($outputFrame);
|
|
@unlink($frameTmpPath);
|
|
|
|
return $processedFrameS3Path;
|
|
|
|
} catch (\Exception $ex) {
|
|
Log::error("Failed to fetch or process frame image: " . $ex->getMessage());
|
|
@unlink($frameTmpPath ?? '');
|
|
return null;
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error("Error in processFrameImage: " . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|