manager = new ImageManager(new Driver()); } public function generate(Certificate $certificate): void { $certificate->update(['status' => 'generating']); try { $certificate->load(['participant', 'template', 'program']); $template = $certificate->template; if (! $template) { throw new \RuntimeException('Template sijil tidak dijumpai.'); } $templatePath = Storage::disk('local')->path($template->image_path); if (! file_exists($templatePath)) { throw new \RuntimeException('Fail template sijil tidak dijumpai di storage.'); } $image = $this->manager->decodePath($templatePath); $config = $template->config_json ?? []; $fields = $config['fields'] ?? []; if (isset($fields['name'])) { $this->writeText($image, $certificate->participant->name, $fields['name']); $this->writeIcBelow($image, $certificate->participant->no_kp, $fields['name']); } if (isset($fields['certificate_no']) && $certificate->certificate_no) { $this->writeText($image, $certificate->certificate_no, $fields['certificate_no']); } $outputDir = 'certificates/' . $certificate->program_id; $outputFile = $outputDir . '/' . $certificate->uuid . '.jpg'; Storage::disk('local')->makeDirectory($outputDir); $image->encode(new JpegEncoder(90)) ->save(Storage::disk('local')->path($outputFile)); $certificate->update([ 'file_path' => $outputFile, 'status' => 'generated', 'generated_at' => now(), 'error_message'=> null, ]); } catch (\Throwable $e) { $certificate->update([ 'status' => 'failed', 'error_message' => $e->getMessage(), ]); } } public function generatePreview(CertificateTemplate $template, string $sampleName, string $sampleNo = '', ?array $overrideFields = null, string $sampleIc = '800808-08-8888'): string { $templatePath = Storage::disk('local')->path($template->image_path); $image = $this->manager->decodePath($templatePath); $fields = $overrideFields ?? ($template->config_json['fields'] ?? []); if (isset($fields['name'])) { $this->writeText($image, $sampleName, $fields['name']); $this->writeIcBelow($image, $sampleIc, $fields['name']); } if (isset($fields['certificate_no'])) { $this->writeText($image, $sampleNo ?: 'ECT/2025/0001', $fields['certificate_no']); } return $image->encode(new JpegEncoder(85))->toString(); } // Tulis IC di bawah nama — auto-posisi Y, saiz font dari config atau fallback 70% private function writeIcBelow(\Intervention\Image\Interfaces\ImageInterface $image, string $ic, array $nameCfg): void { $nameFontSize = (int) ($nameCfg['font_size'] ?? 48); $icFontSize = isset($nameCfg['ic_font_size']) && (int) $nameCfg['ic_font_size'] > 0 ? (int) $nameCfg['ic_font_size'] : (int) round($nameFontSize * 0.7); $icY = (int) ($nameCfg['y'] ?? 0) + (int) round($nameFontSize * 1.5); $this->writeText($image, $ic, array_merge($nameCfg, [ 'font_size' => $icFontSize, 'y' => $icY, 'font_file' => $nameCfg['font_file'] ?? 'DejaVuSans.ttf', ])); } private function writeText(\Intervention\Image\Interfaces\ImageInterface $image, string $text, array $cfg): void { $fontFile = $this->resolveFontPath($cfg['font_file'] ?? 'DejaVuSans-Bold.ttf'); $fontSize = (int) ($cfg['font_size'] ?? 48); $fontColor = (string)($cfg['font_color'] ?? '#000000'); $align = (string)($cfg['align'] ?? 'center'); $valign = (string)($cfg['valign'] ?? 'top'); $x = (int) ($cfg['x'] ?? 0); $y = (int) ($cfg['y'] ?? 0); $image->text($text, $x, $y, function (FontFactory $font) use ($fontFile, $fontSize, $fontColor, $align, $valign) { $font->filename($fontFile); $font->size($fontSize); $font->color($fontColor); $font->align($align, $valign); // v4: satu kaedah untuk horizontal + vertical }); } private function resolveFontPath(string $filename): string { $custom = resource_path('fonts/' . $filename); if (file_exists($custom)) { return $custom; } return resource_path('fonts/DejaVuSans-Bold.ttf'); } public function buildCertificateNo(Program $program, Participant $participant, int $sequence): string { $year = now()->format('Y'); $prefix = strtoupper(substr(preg_replace('/[^A-Za-z]/', '', $program->title), 0, 4)); return sprintf('%s/%s/%04d', $prefix, $year, $sequence); } }