- Tambah fields[name][ic_font_size] dalam form — baris: Warna | Saiz Font No IC | Align - Default: 70% daripada saiz font nama (sebelum ini hardcode 50%) - loadPreview() hantar ic_font_size terkini ke endpoint pratonton - writeIcBelow() baca ic_font_size dari config, fallback 70% jika tiada - Validasi updateConfig: ic_font_size nullable|integer|min:8|max:200 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
144 lines
5.5 KiB
PHP
144 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Certificate;
|
|
use App\Models\Program;
|
|
use App\Models\Participant;
|
|
use App\Models\CertificateTemplate;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Intervention\Image\ImageManager;
|
|
use Intervention\Image\Drivers\Gd\Driver;
|
|
use Intervention\Image\Encoders\JpegEncoder;
|
|
use Intervention\Image\Typography\FontFactory;
|
|
|
|
class CertificateService
|
|
{
|
|
private ImageManager $manager;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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);
|
|
}
|
|
}
|