# Fluxo Completo de Geração de PDF

> _Atualizado para o **v6** (`update_diario_v5_to_v6`) em 2026-09-11. Alterações desta versão: `DIARIO_OFICIAL_ALTERACOES.md` e `plan/2026-09-11-diario-upgrade-backlog.md`._

## Gatilhos

O PDF é gerado **apenas** quando o usuário clica em **"Processar PDF"** na página de edição do CMS:

| Cenário | Local | Mecanismo |
|---------|-------|-----------|
| Clique em "Processar PDF" | `create.blade.php` (script inline) → `processPdf()` | `fetch(/refresh-diario-preview/{id}?force=1)` e polling de 1s |
| Visualizar ("Visualizar") | `create.blade.php` → `openPreview()` | Carrega o iframe `/cms/diario-oficial/{id}` (só habilitado quando `completed`) |
| Botão "Regenerar" na listagem | `DiarioController@refresh_PDF` | `generatePDF($id, true)` + redireciona para a listagem |
| Portal (se PDF não existe) | `Portal\DiarioController@showPDF` | Marca `pending` e mostra tela de espera |
| `show()` do CMS | `DiarioController@show` | Regenera se `generation_status ∉ {completed, processing}` |

**O PDF NÃO é gerado durante o save** (`store()`/`update()` apenas salvam os dados e marcam `generation_status = pending`).

---

## Fluxo Detalhado

```
1. Usuário clica "Processar PDF"
   │
   ├── 2. JS: setButton('processing'), desabilita "Visualizar"
   ├── 3. JS: fetch(/refresh-diario-preview/{id}?force=1)
   ├── 4. JS: setInterval(pollStatus, 1000)  → /cms/ajax/diario-generation-status/{id}
   │
   ├── 5. refresh_PDF_preview($id, Request):
   │   ├── Marca generation_status = 'processing'
   │   ├── Zera generation_progress / generation_error / generation_completed_at
   │   ├── Define generation_started_at = now()
   │   ├── Chama repository->generatePdfFile($id)  (síncrono)
   │   │
   │   │   └── generatePdfFile():
   │   │       ├── @set_time_limit(0) + Log::info
   │   │       ├── Busca Diario, DiarioConfig, DiarioCertificado
   │   │       ├── Agrupa matérias por ÁREA: groupBy('id_area')
   │   │       ├── Prepara dados (prefeitura, endereço, certificado)
   │   │       ├── QR Code = rtrim(config('app.url'),'/').'/diario-oficial/{id}' (try/catch)
   │   │       ├── Instancia FPdfService
   │   │       │   ├── isPublished = status do diário
   │   │       │   └── progressCallback → saveGenerationProgress
   │   │       ├── Adiciona capa (se houver) + AddPage()
   │   │       └── Para cada ÁREA:
   │   │           ├── Nova área inicia página nova se a atual tiver conteúdo
   │   │           ├── ChapterHeader(nome da área)
   │   │           └── Para cada MATÉRIA:
   │   │               ├── [Texto] → acumula HTML
   │   │               ├── [PDF + file_in=true]:
   │   │               │   ├── convertPdfToImages() → Imagick+Ghostscript
   │   │               │   │   (700px, JPEG q80; progresso página a página)
   │   │               │   └── writeImagesInline() → writeHTML + selectColumn()
   │   │               │       (fluxo de colunas contínuo; ~87×123mm em 2 col)
   │   │               │       Fallback: printImportedPagesInline → printImportedPagesGrid
   │   │               └── [PDF + file_in=false]:
   │   │                   └── printImportedPagesFullPage() (FPDI useTemplate)
   │   │                       Fallback: printImagesFullPage → grade
   │   │
   │   ├── Escrita ATÔMICA: .tmp.<uniqid>.pdf → Storage::move
   │   ├── cleanupMergedPdfs() (remove eventuais _merged_*)
   │   └── Atualiza via query: status 'completed'/'failed', progress = null
   │
   └── 6. Frontend (polling a cada 1s):
       ├── 'processing' + progress → "Anexo 1/2 — Página 5 de 27"
       ├── 'completed' → botão verde "PDF OK — Gerar novamente" + recarrega preview
       └── 'failed' → botão vermelho "Tentar novamente"
```

> A subárea **não** gera quebra de página nem de coluna: o único escopo de seção é a **área**.

---

## Estrutura de Arquivos Gerados

```
storage/app/public/diario-oficial/
├── content/{id}/
│   ├── pdf/
│   │   └── diário-{edicao}.pdf        ← PDF final (escrita atômica)
│   └── images/                         ← JPGs temporários (Imagick/Ghostscript)
│       ├── diario_{id}_materia_{id}_page_1.jpg
│       └── ...
├── covers/                             ← Imagens de capa (Archive)
└── headers/                            ← Imagens de cabeçalho (configurações)
```

---

## Conversão de PDF para Imagens

### Parâmetros do `PdfConverter`

| Parâmetro | Valor | Motivo |
|-----------|-------|--------|
| Resolução | 100 DPI | Equilíbrio qualidade/performance |
| Resize | 700px (Lanczos) | Tamanho suficiente para tela |
| Nitidez | Unsharp mask | Compensa blur do resize |
| Compressão | JPEG, qualidade 80 | Boa qualidade, tamanho pequeno |
| Renderização | **1 página por vez** | Isola crashes do Ghostscript |

### Isolamento por Página

```php
for ($pageNum = 0; $pageNum < $pageCount; $pageNum++) {
    try {
        $imagick = new \Imagick();
        $imagick->setResolution(100, 100);
        $imagick->readImage("{$pdfPath}[{$pageNum}]");
        // ... processamento ...
        $imagick->writeImage($imagePath);
    } catch (\Throwable $pageError) {
        Log::warning("[PdfConverter] Erro na página {$pageNum}: " . $pageError->getMessage());
        // policy do ImageMagick bloqueia PDF globalmente → aborta
    } finally {
        if (isset($imagick)) { $imagick->clear(); $imagick->destroy(); }
    }
}
```

Cada página cria um **novo** objeto `Imagick`. Se a policy do ImageMagick bloquear
o coder PDF (`security policy`/`IsCoderAuthorized`), a conversão **aborta** e o
`printAnexoMateria` cai no fallback de importação direta (FPDI). Se nenhuma página
converter, o retorno é `success = false`.

---

## Dimensionamento dos Anexos (`ImageProcessor`)

O `TCPDF 6.3` **não implementa** `max-width`/`max-height`; por isso o
`ImageProcessor` emite as dimensões como `width`/`height` em **mm**
(proporcionais à coluna, independentes da resolução):

| Colunas | Largura da coluna | Tamanho do anexo (A4 700×989px) | Resultado |
|:------:|:----------------:|:-------------------------------:|-----------|
| 2 | 89 mm | ~**87 × 123 mm** | **2 por coluna / 4 por página** |
| 3 | 58 mm | ~58 × 82 mm | até 3 por coluna |
| 1 | 190 mm | ~**141 × 200 mm** | 1 por página |

A altura é limitada a `maxHeight = columnsNumber > 1 ? 123 : 200` para garantir
2 anexos por coluna em layouts multi-coluna.

---

## Escrita Atômica

```php
// Gera em .tmp temporário
Storage::put($caminho . '.tmp.' . uniqid() . '.pdf', $pdfContent);

// Remove destino antigo (se existir)
if (Storage::exists($caminho)) {
    Storage::delete($caminho);
}

// Renomeia (atômico no mesmo filesystem)
Storage::move($caminhoTemp, $caminho);
```

---

## Sistema de Colunas

| Configuração | Efeito |
|-------------|--------|
| `columns_number = 1` | Uma coluna (largura total) |
| `columns_number = 2` | Duas colunas (`setEqualColumns(2, 89)`) |
| `columns_number = 3` | Três colunas (`setEqualColumns(3, 58)`) |

O TCPDF gerencia a quebra de colunas automaticamente via `selectColumn()` +
`writeHTML()`. Em `file_in=true`, o anexo é escrito nesse mesmo fluxo de colunas.

---

## Certificação Digital

A assinatura é aplicada **somente em edições publicadas** (`$pdf->isPublished`),
dentro de `FPdfService::Header()`:

```php
if ($this->isPublished) {
    $this->setSignature($certificate, $pkey, '', '', 2, $info);
}
```

---

## Progresso (generation_progress)

Estrutura JSON salva no banco a cada página/anexo:

```json
// Entre anexos:
{"step":"processing_attachment","current_attachment":2,"total_attachments":2}

// Durante páginas:
{"step":"processing_pages","current_attachment":1,"total_attachments":2,"current_page":5,"total_pages":27}
```

Atualizado via `saveGenerationProgress` (acionado pelo `progressCallback` do
`FPdfService`/`PdfConverter`) e lido pelo polling do frontend.

---

## Tratamento de Erros

| Erro | Causa | Tratamento |
|------|-------|------------|
| PDF não encontrado | Arquivo deletado | Geração sob demanda |
| Policy do ImageMagick | PDF bloqueado | Aborta conversão e usa fallback FPDI/grade |
| PDF moderno (FPDI não lê) | Object streams / PDF 1.5+ | `file_in=true`: rasterização (Ghostscript) |
| Crash do Ghostscript | Página corrompida | Try/catch por página + log |
| Falha no progresso | DB temporário | Silencioso (não interrompe) |
| Trava de geração | `processing` > 10 min | `generationStatus()` marca como `failed` |
| Erro fatal | Exceção na geração | `Log::error` + limpeza de temporários + `failed` |
