# Solução Final — Geração de PDF sob Demanda

## Problemas Resolvidos

| # | Problema | Causa Raiz | Solução |
|:-:|----------|-------------|---------|
| 1 | Tela trava ao salvar | `generatePDF()` síncrono no `store()` | Save **sem gerar PDF**. Geração ocorre apenas na página de edição |
| 2 | PDF corrompido ao sair | Escrita direta sem proteção | Escrita atômica (temp + rename) |
| 3 | Regeneração ao entrar no CMS | `show()` chamava `generatePDF` sem verificar | `show()` checa `generation_status` e só stream se já existir |
| 4 | Crash do worker FPM (exit 64) | `exec()` + background orphan + shutdown function | Geração **síncrona no ciclo normal do Laravel** (controller → repository) |
| 5 | Crash do ImageMagick no 2º PDF | Ghostscript/Imagick corrompe estado interno em shutdown function | Geração roda no request normal, onde o C não crasha |
| 6 | Progresso não aparecia | `refresh_PDF_preview` não setava `generation_status = 'processing'` | Agora marca `processing` + `generation_progress` antes de gerar |

---

## Arquitetura Final

```
Salvar (store/update)
    ↓
Salva dados no banco           ← INSTANTÂNEO (sem geração de PDF)
Redirect para EDIT
    ↓
Página de edição:
  [Salvar]  [Processar PDF]  [Visualizar]
                ↓
        Usuário clica "Processar PDF"
                ↓
        Botão vira spinner + progresso
                ↓
        fetch(refresh-diario-preview?force=1)
                ↓
        Controller → repository → generatePdfFile()
                ↓
        A cada página: progressCallback salva no DB
                ↓
        Frontend: setInterval(1s) → generationStatus()
                ↓
        Quando 'completed': botão verde "PDF OK — Gerar novamente"
                ↓
        Visualizar → modal com iframe do PDF
```

### Por que não usar background?

Foram testadas 3 abordagens antes da solução final:

| Abordagem | Resultado |
|-----------|-----------|
| `exec()` + Artisan command | FPM crash exit code 64 |
| `register_shutdown_function` + `fastcgi_finish_request` | ImageMagick crash (segfault) no 2º PDF anexado |
| Cron + `diario:generate --pending` | Indisponível no ambiente |

A geração **síncrona no request normal** (controller → repository) é a única que funciona de forma estável com ImageMagick/Ghostscript.

---

## Fluxo de Geração (`generatePdfFile`)

```
generatePdfFile($id)
    │
    ├── Busca Diario, DiarioConfig, DiarioCertificado
    ├── Conta total de anexos (type='file')
    ├── Prepara dados (prefeitura, endereço, QR Code)
    ├── Instancia FPdfService
    │   └── Configura $progressCallback → salva progresso no DB
    │
    ├── Adiciona capa (se houver)
    │
    ├── Para cada ÁREA:
    │   ├── ChapterHeader
    │   └── Para cada MATÉRIA:
    │       ├── Atualiza progresso: anexo X de Y
    │       ├── Se type='file' + file_in:
    │       │   └── convertPdfToImages()  ← 100 DPI, resize 700px
    │       │       └── Para cada página:
    │       │           ├── new Imagick()
    │       │           ├── readImage("pdf[página]")
    │       │           ├── resizeImage(700px, Lanczos)
    │       │           ├── unsharpMaskImage
    │       │           ├── writeImage(JPEG q80)
    │       │           ├── clear() + destroy()
    │       │           └── progressCallback(página, total)
    │       └── Se type='file' + !file_in:
    │           └── FPDI importPage (merge página inteira)
    │
    ├── Salva PDF (escrita atômica)
    └── Retorna true/false
```

---

## Progresso em Tempo Real

### Backend (PHP)

O `progressCallback` é uma closure anexada ao `FPdfService` antes da geração:

```php
$pdf->progressCallback = function ($currentPage, $totalPages) use ($id, &$currentAttachment, $totalAttachments) {
    $d = Diario::find($id);
    if ($d) {
        $d->generation_progress = json_encode([
            'step' => 'processing_pages',
            'current_attachment' => $currentAttachment,
            'total_attachments' => $totalAttachments,
            'current_page' => $currentPage,
            'total_pages' => $totalPages,
        ]);
        $d->save();
    }
};
```

Também há progresso por anexo (entre as chamadas de `convertPdfToImages`):

```php
$d->generation_progress = json_encode([
    'step' => 'processing_attachment',
    'current_attachment' => $currentAttachment,
    'total_attachments' => $totalAttachments,
]);
```

### Frontend (JavaScript)

```javascript
function processPdf() {
    setButton('processing', 'Gerando PDF...');
    fetch(API + '/refresh-diario-preview/' + ID + '?force=1').catch(function () {});
    stopPoll();
    pollTimer = setInterval(pollStatus, 1000);  // Polling a cada 1s
}
```

O polling consulta `generationStatus()` e exibe:

| Estado | UI |
|--------|----|
| `processing` + `current_page` | "Anexo 1/2 — Página 5 de 27" |
| `processing` + sem página | "Anexo 1 de 2..." |
| `completed` | Botão verde "PDF OK — Gerar novamente" |
| `failed` | Botão vermelho "Tentar novamente" |

---

## Escrita Atômica

```php
$caminhoTemp = $caminhoRelativo . '.tmp.' . uniqid() . '.pdf';
Storage::put($caminhoTemp, $pdfContent);

if (Storage::exists($caminhoRelativo)) {
    Storage::delete($caminhoRelativo);
}
Storage::move($caminhoTemp, $caminhoRelativo);
```

Garante que:
- Nunca haja PDF parcial no destino
- Se o processo for interrompido, apenas o `.tmp` é perdido
- O leitor nunca vê um arquivo corrompido

---

## Conversão de PDF para Imagens (`convertPdfToImages`)

| Parâmetro | Valor |
|-----------|-------|
| Resolução | 100 DPI |
| Resize | 700px largura (Lanczos) |
| Nitidez | Unsharp mask (1.0, 0.5, 1.5, 0.05) |
| Formato | JPEG, qualidade 80 |
| Renderização | 1 página por vez (novo Imagick a cada página) |

```php
$imagick = new \Imagick();
$imagick->setResolution(100, 100);
$imagick->readImage("{$pdfPath}[{$pageNum}]");
$imagick->resizeImage(700, $height, Imagick::FILTER_LANCZOS, 0.9);
$imagick->unsharpMaskImage(1.0, 0.5, 1.5, 0.05);
$imagick->writeImage($imagePath);
$imagick->clear();
$imagick->destroy();
```

Cada página usa um **novo objeto Imagick** para isolar crashes do Ghostscript. Se uma página falhar, as demais continuam (try/catch por página).

---

## Status de Geração

| Status | Significado | Como entra |
|--------|-------------|------------|
| `null` | Nunca foi gerado | Padrão |
| `pending` | Aguardando (legado) | `dispatchGeneration()` (não usado mais) |
| `processing` | PDF sendo gerado | `refresh_PDF_preview` → antes de `generatePdfFile` |
| `completed` | PDF gerado com sucesso | `refresh_PDF_preview` → após sucesso |
| `failed` | Erro na geração | `refresh_PDF_preview` → catch |

---

## Migrations

| # | Migration | Adiciona |
|:-:|-----------|----------|
| 1 | `...100002_add_generation_status_to_diarios_table` | `generation_status`, `generation_error`, `generation_started_at`, `generation_completed_at` |
| 2 | `...100003_update_view_diario_oficial_with_generation_status` | View `diario_oficial` com `generation_status` |
| 3 | `...100004_add_generation_progress_to_diarios_table` | `generation_progress` (JSON) |

---

## Arquivos Criados/Modificados

### Novos

| Arquivo | Descrição |
|---------|-----------|
| `app/Console/Commands/DiarioGeneratePdf.php` | Artisan command (fallback manual) |
| `public/assets/js/custom/contents/diario-oficial/generation-status.js` | Polling de badges na listagem CMS |
| `database/migrations/...100002_add_generation_status...php` | Colunas de status |
| `database/migrations/...100003_update_view_diario_oficial...php` | View atualizada |
| `database/migrations/...100004_add_generation_progress...php` | Coluna de progresso |

### Modificados

| Arquivo | O que mudou |
|---------|-------------|
| `app/Http/Model/Prefecture/Diario.php` | `$fillable` com campos de generation; `$dates` |
| `app/Repositories/.../DiarioOficialRepository.php` | `generatePdfFile()` + `getPdfPath()` + escrita atômica + progress callback |
| `app/Http/Controllers/Prefecture/DiarioController.php` | `refresh_PDF_preview` marca processing; `generationStatus()` retorna progress; `show()` checa status |
| `app/Http/Controllers/Portal/DiarioController.php` | `showPDF()` checa `generation_status` |
| `app/Console/Kernel.php` | Command registrado |
| `routes/basepro.php` | Rota `diario-generation-status/{id}` |
| `resources/views/.../diario-oficial/create.blade.php` | Botão "Processar PDF" + progresso + polling |
| `resources/views/.../diario-oficial/index.blade.php` | Script generation-status.js |
| `resources/views/portal/.../show-pdf.blade.php` | Tela "Gerando..." com auto-refresh |
| `public/assets/js/.../datatable.js` | Badge de generation_status |
| `app/Services/FPdfService.php` | `progressCallback`, 100 DPI, resize 700px, isolamento por página |

---

---

## Melhorias Pós-Implementação (Jul-2026)

### Problema 7 — Quebra de página entre múltiplos anexos PDF mesmo escopo

| # | Problema | Causa Raiz | Solução |
|:-:|----------|-------------|---------|
| 7 | Quebra de página/coluna entre anexos PDF consecutivos na mesma área+subárea | Cada anexo era processado individualmente, com `checkIfIsNeedBreakPage(60)` entre eles | Merge de PDFs consecutivos mesmo escopo via `mergePdfs()` |
| 8 | Geração nunca completa ("loop infinito") com múltiplos anexos | TCPDF `writeHTML` decodificando base64 de 20+ imagens grandes → timeout do PHP | `set_time_limit(0)` + pular `processImageContent` (era um no-op para data URIs) |

### Solução 7 — Merge de PDFs consecutivos

Ver documento dedicado: [`17-MERGE_ANEXOS_PDF.md`](./17-MERGE_ANEXOS_PDF.md)

### Solução 8 — Performance de renderização de imagens

**Problema:** `generateImageContent()` criava `<img src="data:image/jpeg;base64,..." width="300">`. TCPDF `writeHTML` precisava decodificar cada base64 de volta — extremamente lento para 20+ imagens grandes (700px, qualidade 80). PHP morria por `max_execution_time` (30s default), `generation_status` ficava 'processing' para sempre, frontend polllava infinitamente.

**Correções aplicadas:**

| # | Mudança | Local | Efeito |
|:-:|---------|-------|--------|
| 1 | `@set_time_limit(0)` | `generatePdfFile()` linha 109 | Remove timeout para esta requisição |
| 2 | `processImageContent()` removido do loop | `generatePdfFile()` linhas 366, 421 | ImageProcessor era um no-op (data URIs não são otimizáveis), só consumia CPU |
| 3 | `generateImageContent()` mantido com base64 | `DiarioOficialRepository.php` método privado | TCPDF writeHTML só suporta data URIs em `<img src>`, não caminhos de arquivo absolutos |

### Fluxo final de renderização de imagens

```
convertPdfToImages() → salva JPEGs no disco (700px, 100 DPI, q80)
       ↓
generateImageContent() → lê cada JPEG → base64-encode
       ↓                 → <img src="data:image/jpeg;base64,..." width="300">
writeHTML (SEM processImageContent)
       ↓                 → TCPDF decodifica base64, renderiza a 300px = ~79mm
TCPDF AutoPageBreak      → gerencia quebras naturalmente entre páginas
```

---

## Comportamento por Tela

### CMS — Listagem (index)
- Badge colorido na coluna Status: "PDF OK" (verde) | "Gerando..." (laranja) | "Falha" (vermelho)
- Polling automático a cada 10s atualiza os badges

### CMS — Edição (create)
- **Salvar**: salva dados **sem gerar PDF**
- **Processar PDF**: botão explícito que gera o PDF com progresso em tempo real
- **Visualizar**: modal com iframe do PDF (habilitado após geração)

### Portal — Visualizador Público
- Se PDF existe → embed direto
- Se não existe → "Diário em processamento" com auto-reload
