As stated in the title i'm currently using Laravel DOMPDF that extends from DOMPDF library to generate PDF from HTML.
Issue : Images are displayed in the PDF as a black square with a cross inside
Context :
I'm retrieving from my DB an HTML file that was created using a text editor.
$template = Template::where('Title', $request->templateTitle)->firstOrFail();
This HTML contains placeholders (in the code dynamic-values) for words and images. Still from my DB i'm retrieving data to replace these placeholders
// Retrieve trainer & company data for dynamic replacements.
$trainer = Trainer::findOrFail($request->trainerId);
$company = Company::findOrFail($trainer->companies_id);
// Define the dynamic values. (Keep these keys in sync with your template)
$dynamicValues = [
'company_name' => $company->company_name,
'street_number' => $company->street_number,
'street_type' => $company->street_type,
'street_name' => $company->street_name,
'postal_code' => $company->postal_code,
'city' => $company->city,
'activity_declaration_number' => $company->activity_declaration_number,
'acquisition_region' => $company->acquisition_region,
'registration_number' => $company->registration_number,
'contact_first_name' => $company->contact_first_name,
'contact_last_name' => $company->contact_last_name,
'trainer_first_name' => $trainer->first_name,
'trainer_last_name' => $trainer->last_name,
'trainer_address' => $trainer->address,
'trainer_zip_code' => $trainer->zip_code,
'logo_path' => $company->logo_path,
];
// Get the initial content from the template.
$content = $template->Content;
// Replace all dynamic text placeholders.
foreach ($dynamicValues as $key => $value) {
$placeholder = '<span class="dynamic-value">' . $key . '</span>';
$content = str_replace($placeholder, $value, $content);
}
Then to replace placeholder images for a PDF i call :
$content = $this->processDynamicImages($content, $dynamicValues, 'pdf');
Which is just a function that remove images with empty src, retrieve the images marked as 'dynamic-image', replace the placeholder src by it's value and returns the full html:
private function processDynamicImages($html, array $dynamicValues, $format)
{
libxml_use_internal_errors(true);
$dom = new \DOMDocument();
$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$images = $dom->getElementsByTagName('img');
foreach ($images as $img) {
if (!($img instanceof \DOMElement)) {
continue;
}
if (empty($img->getAttribute('src'))) {
$img->parentNode->removeChild($img);
continue;
}
$class = $img->getAttribute('class');
if (strpos($class, 'dynamic-image') !== false) {
$src = $img->getAttribute('src');
foreach ($dynamicValues as $key => $value) {
if (stripos($src, $key) !== false) {
$newPath = ltrim($value, '/'); // e.g. "storage/companies/logos/..."
// Ensure forward slashes (they work with chroot)
$newPath = str_replace('\\', '/', $newPath);
$img->setAttribute('src', $newPath);
break;
}
}
}
}
$result = $dom->saveHTML();
libxml_clear_errors();
return $result;
}
Then we finish the pdf Creation by defining the chroot (the folder in which DOMPDF will be authorized to look for images)
$chroot = public_path();
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadHTML($content)->setOptions(['chroot' => $chroot, 'isRemoteEnabled'=>true]);
And defining the filename, filepath for storage and create the pdf :
$filename = $pdfFilename;
$filePath = storage_path('app/public/Trainers/contracts/' . $filename);
$pdf->save($filePath);
What i've found :
There is a file called Cache.php in a folder called Image in the vendor/dompdf/src library
In this file there is a function called resolve_url which, as stated at the top of the function is used to Resolve and fetch an image for use
After several tests with this function, i found the issue was coming from this foreach :
foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
[$result, $message] = $rule($full_url);
if (!$result) {
throw new ImageException("Error loading $url: $message", E_WARNING);
}
}
Where $allowed_protocols = $options->getAllowedProtocols() and getAllowedProtocols() returns
private $allowedProtocols = [
"data://" => ["rules" => []],
"file://" => ["rules" => []],
"http://" => ["rules" => []],
"https://" => ["rules" => []]
];
and [$result,$message] would return :
array:2 [▼ // vendor\dompdf\dompdf\src\Image\Cache.php:86
0 => false
1 => "Permission denied. The file could not be found under the paths specified by Options::chroot."
]
So i removed the foreach, just to find that i had to enable the extension php_gd in my php_ini and restart my computer and it worked.
Question:
Why does this foreach triggers an error ?
EDIT ON SWITI ANSWER
Thank you for your answer Switi
But as i stated in the body of my question, i've already installed php_gd by exactly doing what you are telling in your answer
Something caught my attention though, i didn't try the asset() for the src. So following up on your advice i replaced in my controller method processDynamicImages() , in if($format === 'pdf') this :
$newPath = str_replace('\\', '/', $newPath);
by that :
$newPath = str_replace('\\', '/', asset($newPath));
Now, two things changed.
- First the loading time for the answer coming from the server is way longer, i even had to add ini_set('max_execution_time', 90); to the beginning of my Controller to avoid a Maximum Execution Time error
- Second, in the Image/Cache.php in DOMPDF library, the foreach that checks if the image follows security rules is now returning :
array (0 => true,1 => NULL) instead of false and Permission denied before returning :
Dompdf\Exception\ImageException {#4598 ▼ // vendor\dompdf\dompdf\src\Image\Cache.php:191
#message: "Image not found"
#code: 2
#file: "
D:\Graphy Scope\CRM MASTERFORMA\Code\masterforma\vendor
\dompdf\dompdf\
src\Image\Cache.php
"
#line: 117
trace: {▶}
}
We're getting somewhere !
If i dd($full_url); i get http://localhost:8000/storage/companies/logos/QYMcuLqYwlhqZIXst1j13w79iI41wctYNenXVuf6.png which is the right path to the image, so i don't understand the image not found