Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/lerichardv/patolab-platform/llms.txt

Use this file to discover all available pages before exploring further.

PatoLab generates signed pathology report PDFs by rendering a Blade HTML view through a headless Chromium browser using Spatie Browsershot. When a pathologist finalizes a specimen report, the system automatically produces a PDF at letter size (215.9 mm × 279.4 mm), embeds pathologist signatures as inline Base64 images, converts all report images to inline data URIs so Chromium can render them without network requests, and stores the result on the public storage disk. The same pipeline powers the in-editor PDF preview feature, which generates a temporary PDF without altering the stored finalized file.

Dependencies

DependencyLayerPurpose
spatie/browsershotPHP (Composer)Puppeteer-based HTML → PDF bridge
Chromium or Google ChromeOS binaryHeadless browser that renders the HTML
Node.js + npmOS binaryRequired by Browsershot’s Puppeteer bridge
Install the PHP package:
composer require spatie/browsershot

Installing Chromium

Browsershot requires a Chromium-compatible binary accessible on the server. The recommended approach for Ubuntu/Debian:
1

Install Chromium (open-source build)

apt-get install -y chromium-browser
2

Or install Google Chrome Stable

wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
dpkg -i google-chrome-stable_current_amd64.deb
# Fix any missing dependencies:
apt-get install -f -y
3

Verify the binary path

which google-chrome-stable
# Expected output: /usr/bin/google-chrome-stable

google-chrome-stable --version
# Expected output: Google Chrome 136.x.x.x
Update BROWSERSHOT_CHROME_PATH in your .env to match the actual binary path.

Environment Variables

The following variables configure the paths Browsershot uses to locate Node.js, npm, and Chrome. They are only applied in the production environment — in local development, Browsershot relies on system PATH discovery.
.env
BROWSERSHOT_INCLUDE_PATH='$PATH:/usr/local/bin:/usr/bin'
BROWSERSHOT_NODE_BINARY='/usr/local/bin/node'
BROWSERSHOT_NPM_BINARY='/usr/local/bin/npm'
BROWSERSHOT_CHROME_PATH='/usr/bin/google-chrome-stable'
These are consumed in ReportPdfService::generatePdfContent() via env() calls, applied conditionally:
app/Services/ReportPdfService.php
if (app()->environment('production')) {
    $browsershot
        ->setIncludePath(env('BROWSERSHOT_INCLUDE_PATH', '$PATH:/usr/local/bin:/usr/bin'))
        ->setNodeBinary(env('BROWSERSHOT_NODE_BINARY', '/usr/local/bin/node'))
        ->setNpmBinary(env('BROWSERSHOT_NPM_BINARY', '/usr/local/bin/npm'))
        ->setChromePath(env('BROWSERSHOT_CHROME_PATH', '/usr/bin/google-chrome-stable'));
}
When running PatoLab inside a Docker container, Chromium typically refuses to start without the --no-sandbox flag due to the absence of a user namespace. The ReportPdfService already applies this flag unconditionally via ->noSandbox() and ->addChromiumArguments(['no-sandbox', ...]), so PDF generation should work inside Docker without additional configuration. However, verify that the Chromium binary is installed in the container image — it is not included by default in most PHP base images.

ReportPdfService

The App\Services\ReportPdfService class contains two public methods and a set of private image-processing helpers.

generateAndStoreReport(Specimen $specimen): string

The finalization method — called automatically when a specimen’s status transitions to finalized. It generates the PDF, replaces any previously stored report file, saves the new file on the public storage disk, updates the specimen_reports.report_file column, and cleans up the temporary preview directory.
app/Services/ReportPdfService.php
public function generateAndStoreReport(Specimen $specimen): string
{
    $pdfContent = $this->generatePdfContent($specimen);
    $pdfPath = 'reports/report_' . $specimen->sequence_code . '_' . time() . '.pdf';

    if ($specimen->report->report_file) {
        Storage::disk('public')->delete($specimen->report->report_file);
    }

    Storage::disk('public')->put($pdfPath, $pdfContent);
    $specimen->report->update([
        'report_file' => $pdfPath,
    ]);

    Storage::disk('public')->deleteDirectory("temp_reports/{$specimen->sequence_code}");

    return $pdfPath;
}
Storage path: storage/app/public/reports/report_{sequence_code}_{timestamp}.pdf Database: The path is stored in specimen_reports.report_file. When a user requests a download, ReportEditorController::downloadPdf() checks this column first and streams the stored file directly, avoiding a re-render.

generatePdfContent(Specimen $specimen, &$pages = null)

The inner rendering method, also called directly by generateTempPdf() during editor preview. It:
  1. Loads all required relations (customerRelation, type, examination, category, referrerRelation, report, users.role)
  2. Reads each pathologist’s signature file from the public storage disk and converts it to a Base64 data URI embedded in $user->signature_base64
  3. Converts all <img> src attributes in every HTML report section to inline Base64 data URIs using convertImagesToBase64(), so Chromium never needs to make network requests during rendering
  4. Calls ReportPaginator::paginate() to compute the page layout
  5. Renders the pdf.report.body Blade view to an HTML string
  6. Passes that HTML string to Browsershot and returns the raw PDF binary content
app/Services/ReportPdfService.php
public function generatePdfContent(Specimen $specimen, &$pages = null)
{
    // ... load relations, process signatures, convert images ...

    $htmlContent = view('pdf.report.body', compact(
        'specimen', 'report', 'customer', 'examination', 'referrer', 'pages'
    ))->render();

    $browsershot = Browsershot::html($htmlContent);

    if (app()->environment('production')) {
        $browsershot
            ->setIncludePath(env('BROWSERSHOT_INCLUDE_PATH', '$PATH:/usr/local/bin:/usr/bin'))
            ->setNodeBinary(env('BROWSERSHOT_NODE_BINARY', '/usr/local/bin/node'))
            ->setNpmBinary(env('BROWSERSHOT_NPM_BINARY', '/usr/local/bin/npm'))
            ->setChromePath(env('BROWSERSHOT_CHROME_PATH', '/usr/bin/google-chrome-stable'));
    }

    return $browsershot
        ->addChromiumArguments(['disable-crash-reporter', 'disable-dev-shm-usage', 'no-sandbox'])
        ->noSandbox()
        ->paperWidth('215.9mm')   // US Letter width
        ->paperHeight('279.4mm')  // US Letter height
        ->margins(0, 0, 0, 0)
        ->timeout(120)
        ->waitUntilNetworkIdle()
        ->pdf();
}
Chromium arguments applied unconditionally:
ArgumentReason
disable-crash-reporterPrevents crash reporter processes that cause hangs in server environments
disable-dev-shm-usageAvoids /dev/shm exhaustion in Docker and low-memory servers
no-sandboxRequired for running as root or inside containers

Pathologist Signatures

When rendering the PDF, each pathologist user assigned to the specimen is checked for a user_signature field. The service reads the signature image from the public storage disk and encodes it as a Base64 data URI before passing it to the Blade view:
app/Services/ReportPdfService.php
foreach ($specimen->users as $user) {
    $user->signature_base64 = null;
    if ($user->user_signature) {
        if (Storage::disk('public')->exists($user->user_signature)) {
            $fileContent = Storage::disk('public')->get($user->user_signature);
            $mime = Storage::disk('public')->mimeType($user->user_signature) ?: 'image/png';
            $user->signature_base64 = 'data:' . $mime . ';base64,' . base64_encode($fileContent);
        }
    }
}
Signatures that cannot be found on the local disk are fetched via HTTP as a fallback. If a pathologist has no signature set, the PDF is not blocked — PatoLab enforces signature presence at the state transition level (the finalized status transition is rejected if any assigned pathologist lacks a signature).

Image Optimization Before Upload

When a pathologist uploads images into the report editor (via ReportEditorController::uploadImage()), they pass through App\Services\ImageOptimizerService before being stored. This service compresses images to stay under 300 KB using PHP’s GD extension, reducing PDF file sizes and preventing Browsershot timeouts. The optimizer:
  • Accepts JPEG, PNG, GIF, and WebP uploads
  • Iteratively reduces scale (in 0.1 steps down to a minimum of 1000 px on the short edge) then JPEG quality (in steps of 10, minimum 10) until the file is under 300 KB
  • Saves the result as a JPEG with a random 40-character filename under public/report-images/{sequence_code}/
app/Services/ImageOptimizerService.php
public function optimizeAndStore(UploadedFile $file, string $folder, string $disk = 'public'): string
Images are stored at storage/app/public/report-images/{sequence_code}/{random}.jpg and served at /storage/report-images/{sequence_code}/{random}.jpg. The convertImagesToBase64() method in ReportPdfService maps these public storage URLs back to their local filesystem paths for inline embedding.

Temporary PDF Preview

The report editor UI offers a preview PDF button that generates a temporary PDF without finalizing the specimen. This calls ReportEditorController::generateTempPdf(), which:
  1. Calls ReportPdfService::generatePdfContent() (same pipeline as finalization)
  2. Cleans up any previous temp files: Storage::disk('public')->deleteDirectory("temp_reports/{$specimen->sequence_code}")
  3. Saves the new temp file at temp_reports/{sequence_code}/report_{timestamp}.pdf
  4. Returns a JSON response with the public URL and total page count
The temp_reports/{sequence_code}/ directory is automatically deleted in two situations: when a new preview is generated (replaced with fresh content), and when generateAndStoreReport() finalizes the report. You do not need to clean up temp files manually. However, if a server restart or error occurs mid-preview, orphaned temp files may accumulate in storage/app/public/temp_reports/ — these can be safely deleted at any time.

PDF Download Flow

User clicks "Download PDF"


ReportEditorController::downloadPdf()

        ├─ specimen_reports.report_file exists on disk?
        │         │ YES → stream stored file directly (fast path)
        │         │
        │         └─ NO → call ReportPdfService::generatePdfContent()
        │                  and stream raw PDF bytes inline


Response: application/pdf
         Content-Disposition: attachment; filename="reporte_{sequence_code}.pdf"

Troubleshooting

SymptomLikely CauseFix
Could not find chrome exceptionChrome binary not found at configured pathVerify BROWSERSHOT_CHROME_PATH and run which google-chrome-stable
PDF generation times outHeavy report with many imagesIncrease ->timeout(120) or pre-compress images via ImageOptimizerService
Blank PDF / missing imagesImages not embedded as Base64Ensure storage:link is run (php artisan storage:link) so local path resolution works
error while loading shared librariesChromium missing system libs in DockerRun apt-get install -y chromium-browser inside the container image
Node.js not found in PATHPATH not propagated to PHP processSet BROWSERSHOT_INCLUDE_PATH and BROWSERSHOT_NODE_BINARY explicitly

Build docs developers (and LLMs) love