# Cadeia de Erros — Diário Oficial: Quebra de Colunas com PDF Inserido

## Visão Geral

**Data do ocorrido:** 29-Jun-2026  
**Módulo:** Diário Oficial Eletrônico  
**Funcionalidade:** Geração de PDF do diário com conteúdo do tipo `file` + `file_in=true` em layout de 2 colunas  
**Sintoma:** Ao criar um diário com 2 colunas e inserir um PDF como conteúdo (sem texto manual), ocorria quebra de espaço onde 1 página ocupava o topo de uma coluna e o restante ficava em branco, pulando para a próxima coluna ou página.

---

## Cadeia de Erros (3 problemas encadeados)

### 🔴 Problema 1 — Crash: `Call to a member function toArray() on null`

**Local:** `app/Repositories/Eloquent/Prefecture/DiarioOficial/DiarioOficialRepository.php:73`

```php
$dataConfig = DiarioConfig::query()->first();
Log::info("Configurações do diário obtidas.", ['config_data' => $dataConfig->toArray()]);
//                                                                       ^^^^^^^^^
//                                                                       CRASH se null
```

**Causa:** `DiarioConfig::query()->first()` retornou `null` porque não havia registro na tabela `diario_configs`. `toArray()` foi chamado em `null`.

**Impacto:** Impedia COMPLETAMENTE a geração de qualquer PDF. O erro era fatal.

**Correção:** Adicionar verificação de nulidade antes de chamar `toArray()`:

```php
Log::info("Configurações do diário obtidas.", ['config_data' => $dataConfig ? $dataConfig->toArray() : null]);
```

**Resiliência:** O restante do código já tratava `$dataConfig = null` via `data_get()` com valores padrão (`'center'`, fallback de imagem), então apenas o log precisava de correção.

---

### 🔴 Problema 2 — Falha ao converter PDF: compressão CrossReference não suportada

**Local:** `app/Services/FPdfService.php:convertPdfToImages()` (linha ~497)

```php
$tempPdf = new Fpdi();  // FPDI gratuito
$pageCount = $tempPdf->setSourceFile($pdfPath); // ← EXCEPTION: CrossReference
```

**Log do erro:**
```
This PDF document probably uses a compression technique which is not supported
by the free parser shipped with FPDI.
```

**Causa:** O FPDI (Free PDF Data Interface) distribuído gratuitamente com a biblioteca `setasign/fpdi` não suporta PDFs que usam **CrossReference Streams** (introduzido no PDF 1.5 / Adobe Acrobat 6). PDFs gerados por sistemas mais modernos frequentemente usam essa técnica de compressão.

**Impacto:** O método `convertPdfToImages()` retornava `['success' => false]`, e o PDF do diário era gerado **sem o conteúdo do PDF inserido** — apenas com cabeçalho, rodapé e título da área vazios.

**Correção:** Adicionar fallback para contagem de páginas via **Imagick** quando FPDI falha:

```php
// Try FPDI first (fast). Fall back to Imagick if PDF uses unsupported compression.
$pageCount = false;
try {
    $tempPdf = new Fpdi();
    $pageCount = $tempPdf->setSourceFile($pdfPath);
} catch (\Exception $e) {
    Log::warning("FPDI failed, falling back to Imagick...");
}

if ($pageCount === false || $pageCount === 0) {
    $imagickCount = new Imagick();
    $imagickCount->setResolution(72, 72);
    $imagickCount->readImage($pdfPath);
    $pageCount = $imagickCount->getNumberImages();
    $imagickCount->clear();
    $imagickCount->destroy();
}
```

**Resiliência:** O Imagick já era usado para TODA a conversão de páginas (loop com `readImage("{$pdfPath}[{$pageNum}]")`), então estender seu uso para contagem de páginas é natural e não adiciona nova dependência. O sistema agora funciona com qualquer PDF, independente da técnica de compressão.

---

### 🔴 Problema 3 — Quebra de espaço em colunas (o problema original)

**Local Múltiplo:** `app/Repositories/Eloquent/Prefecture/DiarioOficial/DiarioOficialRepository.php` + `app/Services/FPdfService.php`

#### Sub-problema 3A: Altura fixa das imagens (`max-height: 75mm` hardcoded)

**Arquivo:** `DiarioOficialRepository.php:387`

```php
$imageHtmlStrings[] = '<img src="..." style="max-height: 75mm; ..." />';
//                                                       ^^^^^^^
//                                                       SEMPRE 75mm
```

**Causa:** Cada imagem de página A4 era sempre limitada a `75mm` de altura, independentemente do espaço disponível na coluna. Com cabeçalho do capítulo (~94mm na 1ª página com logo lateral), a coluna oferecia apenas ~180mm úteis. Cada imagem consumia ~83mm (75mm + 8mm de `Ln()`), permitindo no máximo 2 imagens por coluna. Imagens excedentes iam para a próxima coluna, deixando espaço residual.

#### Sub-problema 3B: `writeHTML` com `$ln=true` duplicava espaçamento

**Arquivo:** `DiarioOficialRepository.php:259`

```php
$pdf->writeHTML($html, true, false, true, false, '');
//                      ^^^^
//                      $ln=true → TCPDF chama Ln($this->lasth) internamente
$pdf->Ln(8);  // + nosso Ln manual
```

**Causa:** O TCPDF, ao final do `writeHTML` com `$ln=true`, chama `Ln($this->lasth)`. Se `lasth` retivesse a altura da imagem renderizada, isso efetivamente dobrava o espaçamento vertical (75mm da imagem + 75mm do Ln interno + 8mm do Ln manual = ~158mm por imagem → apenas 1 por coluna).

**Mecanismo interno do TCPDF** (`tcpdf.php:18904-18917`):
```php
if ($this->num_columns > 1) {
    $this->selectColumn();  // NÃO reseta Y (mesma coluna)
}
if ($ln) {
    $this->Ln($this->lasth);  // Espaçamento extra
}
```

#### Sub-problema 3C: Ausência de `selectColumn()` antes de `writeHTML`

**Arquivo:** `DiarioOficialRepository.php:256-262`

`ChapterBody()` (usado para texto) chamava `selectColumn()` antes de `writeHTML()`. O loop de imagens **não** chamava, deixando o estado da coluna inconsistente, especialmente após `checkIfIsNeedBreakPage()`.

#### Sub-problema 3D: `resetColumns()` no fim do loop quebrava o fluxo

**Arquivo:** `DiarioOficialRepository.php:264-266`

```php
if ($columnsNumber > 1) {
    $pdf->resetColumns();  // Quebra o estado das colunas
}
```

Isso resetava as colunas antes que o próximo conteúdo (texto da mesma área) pudesse fluir naturalmente.

#### Sub-problema 3E: `checkIfIsNeedBreakPage()` não era ciente de colunas

**Arquivo:** `FPdfService.php:391-413`

O método calculava espaço disponível baseado na PÁGINA inteira, não na COLUNA atual. Em modo multi-coluna, usava `$this->lastPage()` + `$this->AddPage()` para quebrar, em vez de `selectColumn(next)` para avançar para a próxima coluna.

---

## Resolução (6 correções aplicadas)

| # | Correção | Arquivo | Risco | Efeito |
|---|----------|---------|:-----:|--------|
| 1 | `max-height` dinâmico baseado no espaço disponível | `DiarioOficialRepository.php:248-253` | Baixo | Imagens se adaptam ao espaço da coluna |
| 2 | `writeHTML($html, false, ...)` — `$ln=false` | `DiarioOficialRepository.php:270` | Baixo | Elimina dupla postergação de espaço |
| 3 | `selectColumn()` antes de cada `writeHTML` | `DiarioOficialRepository.php:266` | Baixo | Estado de coluna garantido |
| 4 | `resetColumns()` removido do loop de imagens | `DiarioOficialRepository.php:274` | Baixo | Fluxo de colunas não é interrompido |
| 5 | `checkIfIsNeedBreakPage()` ciente de colunas | `FPdfService.php:396-433` | Médio | Quebra avançando para próxima coluna, não página |
| 6 | FPDI fallback para Imagick na contagem de páginas | `FPdfService.php:495-518` | Baixo | PDFs modernos com CrossReference funcionam |

### Cálculo da altura dinâmica

```php
$availableColumnHeight = $pdf->getPageHeight() - $pdf->getBreakMargin() - $pdf->GetY();
$numImages = count($result['images']);
$maxImageHeight = min(75, ($availableColumnHeight - 8 * $numImages - 15) / max(1, $numImages));
$maxImageHeight = max(20, $maxImageHeight);
```

- Desconta 8mm de `Ln()` por imagem + 15mm de margem de segurança
- Distribui o espaço restante igualmente entre as imagens
- Mínimo de 20mm (imagem legível) e máximo de 75mm (tamanho original)
- Ex: 3 imagens em 180mm de coluna → (180 - 24 - 15) / 3 = 47mm cada → 3 × (47+8) = 165mm ≤ 180mm ✓

---

## Como o sistema está mais resiliente

### 1. Tolerância a ausência de configuração
`DiarioConfig` pode ser `null` sem causar crash. Todos os acessos passam por `data_get()` com fallbacks seguros.

### 2. Suporte a PDFs modernos
O FPDI tenta primeiro (rápido, eficiente). Se falhar (CrossReference), o **Imagick** assume a contagem de páginas. Como o Imagick já era usado para a conversão das imagens, não há nova dependência.

### 3. Colunas adaptativas
A altura máxima das imagens não é mais fixa em 75mm. É calculada dinamicamente baseada no espaço disponível na coluna, garantindo que N imagens caibam sem overflow.

### 4. Espaçamento controlado
`writeHTML` com `$ln=false` + `selectColumn()` explícito + `Ln(8)` manual = espaçamento previsível e sem duplicação.

### 5. Quebra de página inteligente
`checkIfIsNeedBreakPage()` agora avança para a próxima coluna (em vez de nova página) quando há espaço disponível, maximizando o uso do layout.

### 6. Fallback corrigido
O caminho de fallback (quando FPDI falha em importar páginas) agora calcula altura dinâmica corretamente e converte o array de HTML em string com `implode()` — corrigindo um bug pré-existente onde `$content` virava array e quebrava `writeHTML`.

---

## Arquivos Alterados

| Arquivo | Linhas alteradas | Descrição |
|---------|:----------------:|-----------|
| `app/Repositories/Eloquent/Prefecture/DiarioOficial/DiarioOficialRepository.php` | 73, 248-274, 312-319, 384-402 | Correção do crash, altura dinâmica, selectColumn, ln=false, fallback |
| `app/Services/FPdfService.php` | 396-433, 495-518 | checkIfIsNeedBreakPage() multi-coluna, fallback Imagick no convertPdfToImages |

---

## Testes Recomendados

1. **Diário sem `diario_configs`** → Deve gerar PDF com valores padrão (logo central, header img padrão)
2. **PDF moderno (CrossReference)** → Deve converter para imagens via Imagick, sem falha
3. **PDF de 1 página em 2 colunas** → Imagem na coluna 0, sem espaço residual excessivo
4. **PDF de 3 páginas em 2 colunas** → 2 imagens na coluna 0, 1 na coluna 1
5. **PDF de 5 páginas** → 2+2 na primeira página, 1 na próxima
6. **PDF + texto combinados** → Texto flui naturalmente entre imagens

---

### 🔴 Problema 4 — Última página desaparece ao inserir 2 PDFs com número ímpar de páginas

**Descoberto em:** 09-Jul-2026  
**Sintoma:** Ao inserir 2 cópias de um PDF com número ÍMPAR de páginas (ex: 27 páginas) em layout de 2 colunas, a última imagem da segunda cópia desaparece — o PDF gerado tem 15 páginas em vez de 16.  
**Não ocorre com:** 1 única cópia, ou PDFs com número PAR de páginas.

#### Causa Raiz

`checkIfIsNeedBreakPage(60)` no bloco de imagens era chamado enquanto o modo **multi-coluna** ainda estava ativo (herdado do loop anterior de imagens). Isso fazia a verificação de espaço operar no contexto da **coluna atual** em vez da **página inteira**:

**Fluxo incorreto (antes da correção):**

```
1ª CÓPIA (27 imagens, odd, 2 colunas):
  Página 8: [img27 na col 0] [col 1 vazia]
           → Y no fim da col 0

checkIfIsNeedBreakPage(60) → MODO 2-COLUNAS AINDA ATIVO:
  remainingInColumn(col 0) < 110mm → SIM
  current_col (0) < 1 → SIM → selectColumn(1)
  → Y vai para o TOPO da col 1 (MESMA página 8)
  → NÃO adiciona nova página! ❌

setEqualColumns(2, 89) para a 2ª cópia:
  → y = $this->y = topo da col 1 (página 8)
  → selectColumn(0) → Y = topo da col 0 (página 8)
  → 2ª cópia começa na MESMA página da 1ª 😱

RESULTADO: 15 páginas em vez de 16. Última imagem "some"
```

**Com páginas PAR:** a última página da 1ª cópia tem ambas colunas cheias → `current_col = 1 ≥ 1` → `AddPage()` → quebra correta para página nova. **Por isso o bug só aparece com ímpar.**

#### Correção Aplicada

`checkIfIsNeedBreakPage()` precisa rodar **sempre em modo single-coluna**, onde a verificação de espaço considera a **página inteira** (`availableHeight = pageHeight - bottomMargin - currentY`).

3 mudanças nos mesmos locais onde `checkIfIsNeedBreakPage` era chamado:

| # | Arquivo | Mudança | Linha |
|:-:|---------|---------|:-----:|
| 1 | `app/Services/Pdf/LayoutManager.php` — `chapterHeader()` | `resetColumns()` movido para **ANTES** de `checkIfIsNeedBreakPage()` | 69 → 70 |
| 2 | `app/Services/Pdf/LayoutManager.php` — `printChapter()` | `resetColumns()` movido para **ANTES** de `checkIfIsNeedBreakPage()` | 42 → 43 |
| 3 | `app/Repositories/.../DiarioOficialRepository.php` — `generatePdfFile()` | `$pdf->resetColumns()` adicionado **ANTES** de `checkIfIsNeedBreakPage(60)` | 311 → 312 |

**Nota de contexto:** O `FPdfService.php` foi refatorado em sub-serviços (LayoutManager, ImageProcessor, etc.) durante a correção do Problema 3. Os métodos `ChapterHeader()`, `PrintChapter()` e `checkIfIsNeedBreakPage()` agora residem em `LayoutManager.php`, e o `FPdfService` delega para eles.

#### Fluxo corrigido

```
resetColumns() → modo single-coluna, margens originais restauradas
checkIfIsNeedBreakPage(60) → availableHeight = pageHeight - bottomMargin - currentY
                           → se < 110mm: AddPage() (nova página)
                           → se ≥ 110mm: continua na mesma página
setEqualColumns(2, 89) → colunas frescas na página correta
                        → 2ª cópia sempre começa na página certa ✅
```

#### Arquivos Alterados

| Arquivo | Linhas | Descrição |
|---------|:------:|-----------|
| `app/Services/Pdf/LayoutManager.php` | 42-43, 69-70 | `resetColumns()` antes de `checkIfIsNeedBreakPage()` em `chapterHeader()` e `printChapter()` |
| `app/Repositories/Eloquent/Prefecture/DiarioOficial/DiarioOficialRepository.php` | 311-312 | `$pdf->resetColumns()` antes de `checkIfIsNeedBreakPage(60)` |

#### Como testar

1. Inserir 2 cópias do **mesmo PDF com 27 páginas** na mesma ÁREA e SUBÁREA, 2 colunas
2. Verificar log: `Finalizando PDF: 16 páginas geradas` (antes: 15)
3. Verificar log: `nova página (single-column)` entre as duas cópias (antes: `avançou para coluna 1`)
4. Visualizar PDF: última página deve conter a última imagem da segunda cópia
5. Repetir com PDF de páginas PAR (ex: 20): deve continuar funcionando como antes
6. Repetir com 1 única cópia: deve continuar funcionando (8 páginas para 27 imagens)