# Diário Oficial — Alterações (v6): estado anterior × atual

> **Data:** 2026-09-11
> **Branch:** `update_diario_v5_to_v6`
> **Referência de paridade (v5):** `diario-oficial-reimplementacao/`
> **Backlog detalhado:** `plan/2026-09-11-diario-upgrade-backlog.md`
> **Objetivo:** trazer as melhorias/otimizações do módulo v5 para a estrutura v6,
> **preservando** as features v6 (workflow 4 olhos/revisor, auditoria pública
> LGPD, `cover_image_id`, geração com polling/download).

Este documento descreve **tudo que foi modificado** no módulo Diário Oficial,
comparando o **estado anterior** (v6 original, `HEAD`) com o **estado atual**
(árvore de trabalho do branch). As seções estão organizadas por área funcional e
há uma tabela arquivo-a-arquivo no final.

---

## 1. Resumo executivo

| Área | Estado anterior (v6 original) | Estado atual |
|------|-------------------------------|--------------|
| Geração de PDF | Síncrona, sem progresso, grava direto, sem timeout | Resiliênte: timeout 0, progresso granular, escrita atômica, logs |
| Anexos `file_in=true` | Grade fixa **2×2 (4-up)** de imagens, força página nova | **Fluxo inline** contínuo, anexo do tamanho da coluna |
| Anexos `file_in=false` | Grade 2×2 de páginas importadas + `AddPage()` final | **Páginas inteiras** (1 origem = 1 saída), com fallback raster |
| Escopo de seção | Área + subárea; subárea podia causar quebra | **Somente área**; subárea não tem representação nem quebra |
| Tamanho do anexo | Caixa da grade (fixa) | Largura da coluna (~87×123mm em 2 col) → **2/coluna, 4/página** |
| Anexos ilegíveis ao FPDI | Conteúdo era perdido silenciosamente | Rasterização via **Ghostscript** funciona com qualquer PDF |
| Imagens no texto | `processImageContent` inline simples | `ImageProcessor` (mesmo cálculo da v5), tamanho fixo em mm |
| Capa | Upload sem transação, sem limite de dimensão | Transação + compressão limitada a 1366px de altura |
| Certificados | Sem validação de CRT/PKCS12 | Validação CRT/CN/PKCS12 + logs + rollback |
| Preview/UX CMS | Preview "preso" no 1º PDF; sem progresso | Botão "Processar PDF", polling 1s, progresso, gating |
| Portal | Filtro em memória | Filtro em **SQL** (data, termo, edição) |
| QR Code | `$_SERVER['SERVER_NAME']` (quebra em CLI, sem porta/esquema) | `config('app.url')` (APP_URL) |
| Infra | Sem Ghostscript; policy bloqueia PDF | Dockerfile com `ghostscript` + policy liberado |

---

## 2. Geração de PDF (`DiarioOficialRepository`)

### 2.1 Antes (v6 original)

```php
$pdfContent = $pdf->Output($pdf_name, 'S');
Storage::put($caminho, $pdfContent);            // escrita direta
```

- Sem `set_time_limit(0)` — podia estourar em anexos grandes.
- QR Code sem try/catch (uma falha derrubava a geração).
- Sem `generation_progress` (a tela não mostrava progresso).
- Assinatura digital sempre aplicada, mesmo em rascunho.
- `catch` genérico sem log.
- Anexo `file_in=true`: `gridBoxesForColumns()` → **grade 2×2 (4-up)** por
  página, com `AddPage()` ao final (órfã do cabeçalho/página em branco).
- Anexo `file_in=false`: grade 2×2 de páginas importadas (via FPDI) + `AddPage()`.
- `ChapterHeader($area, $subarea)` — o 2º argumento era ignorado.

### 2.2 Agora

- **Escrita atômica**: gera em `.tmp.<uniqid>.pdf` e só então substitui o destino
  (nunca deixa PDF parcial/corrompido).
- `@set_time_limit(0)` na geração.
- QR Code com `try/catch` + `Log::warning`.
- **Progresso granular** (`saveGenerationProgress`): `processing_attachment`
  (anexo X de Y) e `processing_pages` (página X de Y), persistido a cada etapa.
- Assinatura condicionada a `isPublished` (`status` do diário).
- `catch` fatal com `Log::error` + limpeza dos temporários.
- **QR Code** agora usa `rtrim(config('app.url'), '/')` (APP_URL) em vez de
  `'https://' . $_SERVER['SERVER_NAME']`.
- **Escopo por área**: `groupBy('id_area')`; a subárea não influencia nada.
- **Quebra de seção**: cada nova área só inicia página nova se a atual já tiver
  conteúdo (evita página em branco).

### 2.3 Anexos — comportamento final

`printAnexoMateria($pdf, $materia, $idDiario, $columnsNumber, $inlineFlow)`

| Caso | Antes | Agora |
|------|-------|-------|
| `file_in=true` | Grade 2×2 de imagens (conversão Imagick), `AddPage()` final | **Inline**: páginas rasterizadas (JPEG) escritas via `writeHTML`+`selectColumn()` no fluxo de colunas; tamanho = largura da coluna |
| `file_in=false` | Grade 2×2 de páginas importadas | **Páginas inteiras**: 1 página de origem = 1 página de saída (FPDI `useTemplate`), sem cabeçalho/rodapé |
| Fallback | — | `file_in=true`: FPDI inline → grade → imagens. `file_in=false`: raster (`printImagesFullPage`) → grade |
| Subárea | Podia separar/forçar quebra | Ignorada; anexos consecutivos da mesma área compartilham o fluxo |

Métodos novos no repositório: `saveGenerationProgress`, `writeImagesInline`,
`printImagesFullPage`, `printImportedPagesInline`, `printImportedPagesFullPage`,
`printImagesFlow`, `inlineColumnLayout`, `resolveAnexoPath`. O merge por subárea
**deixou de ser usado** na geração (era a causa da quebra e do descarte de PDFs
que o FPDI não lê): `mergePdfs()` permanece apenas como código morto e
`cleanupMergedPdfs()` segue sendo chamado só para remover eventuais temporários
`_merged_*`.

### 2.4 Dimensionamento dos anexos (regra final)

- Anexo rasterizado (700×989px, A4) → `ImageProcessor::buildTagForColumn()`:
  - **2 colunas**: ~**87 × 123mm** → **2 anexos por coluna, 4 por página**.
  - **3 colunas**: ~58 × 82mm → até 3 por coluna.
  - **1 coluna**: ~141 × 200mm → 1 por página.
- Altura limitada a **123mm** em layout multi-coluna (`maxHeight = columnsNumber > 1 ? 123 : 200`)
  para garantir 2 por coluna.

---

## 3. Serviços de PDF

### 3.1 `app/Services/Pdf/ImageProcessor.php` (**novo**)

Trazido da implementação clássica (`diario-oficial-reimplementacao`). Calcula o
tamanho máximo do elemento de forma **proporcional e independente da resolução**:

- `calculateOptimalDimensions($w, $h)` — limita a 190mm de largura e 200mm de
  altura, retornando `width_mm`, `height_mm` e `width_percent`.
- `buildOptimizedImageTag($src, $dims)` — usado para imagens do texto corrido.
- `buildTagForColumn($src, $pxW, $pxH, $columnWidthMM, $maxHeightMM)` — usado
  para páginas de anexo; emite **`width`/`height` em mm**.

> **Nota técnica:** o **TCPDF 6.3 não implementa `max-width`/`max-height`**
> (0 ocorrências no `tcpdf.php`). Por isso o tamanho é emitido como `width`/`height`
> em mm — mantendo o efeito da implementação clássica (tamanho fixo, independente
> da resolução).

### 3.2 `app/Services/FPdfService.php`

| Antes | Agora |
|-------|-------|
| `processImageContent()` próprio (regex + data URI + `width:190mm/height:200mm`) | Delega para `ImageProcessor::processImageContent()` |
| — | Novo `buildAnexoImageTag($imagePath, $columnWidthMM, $maxHeightMM = 200)` |

### 3.3 `app/Services/Pdf/PdfConverter.php`

| Antes | Agora |
|-------|-------|
| `Log::error` por página e seguia tentando | `Log::warning` + **aborta** em erro de policy (`security policy`/`IsCoderAuthorized`) |
| Retornava `success=true` com 0 imagens | Retorna `success=false` quando nenhuma página converteu |

> O container passou a ter **Ghostscript** (ver §7), então a conversão
> Imagick→JPEG funciona para PDFs comuns. O abort evita dezenas de erros em série
> quando a policy bloqueia.

---

## 4. Controllers e componentes

### 4.1 `app/Http/Controllers/Prefecture/DiarioController.php`

| Método | Antes | Agora |
|--------|-------|-------|
| `create`/`edit` | `$table->get(['id','edicao as name'])` (ordem indefinida) | `orderBy('id','desc')` (determinístico) |
| `show` | Sempre servia o arquivo existente (mesmo desatualizado) | Força regeração quando `generation_status ∉ {completed, processing}` |
| `attachCoverImage` | Upload + compressão sem transação | `DB::beginTransaction` + rollback + log; comprime só raster, limitando a altura em **1366px** (pula `webp/avif/gif/svg`) |
| `dispatchGeneration` | `save()` direto | `try/catch` com log (não quebra o request) |
| `refresh_PDF_preview` | Atualizava via model; não limpava `generation_progress` | Limpa `generation_progress` no início; atualização final via **query update** (`generation_status`, `generation_completed_at`, `generation_progress=null`, `generation_error`); log do erro |

### 4.2 `app/Http/Controllers/Portal/DiarioController.php`

| Antes | Agora |
|-------|-------|
| `filtroGET` carregava tudo e filtrava em memória; sem edição | Filtro em **SQL**: `data_inicial`/`data_final`, `termo` (ILIKE em `allareas`), **`edicao`**; somente `status = true`; `orderBy('edicao','DESC')` |

### 4.3 `app/Http/Controllers/Prefecture/DiarioCertificadosController.php`

| Antes | Agora |
|-------|-------|
| `openssl_pkcs12`/`x509` sem validação | Valida CRT gerado (existe/não vazio), `openssl_x509_read`, presença de CN e leitura do PKCS12; logs específicos; no erro faz `forceDelete` + limpa diretório |

### 4.4 `app/Http/Components/.../Preview/DiarioOficialComponent.php`

| Antes | Agora |
|-------|-------|
| `diario_certificados->first()` (incluía deletados) | `whereNull('deleted_at')->first()` |
| `certReturn` sem tratamento | `try/catch` + validação de arquivo/CN (log warning; nunca derruba o preview) |

---

## 5. Front-end (JS e views)

### 5.1 CMS — `create.blade.php`

**Antes:** botões "Visualizar" + "Salvar"; `preview-diario.js` separado; preview
prendia no 1º PDF gerado (em branco) e não refletia mudanças.

**Agora:**
- Botões **"Processar PDF"**, **"Visualizar"** (habilitado só quando pronto) e
  status textual (`#pdfStatus`).
- Polling de 1s em `generation-status`; barra/spinner; progresso granular
  ("Anexo 1/2 — Página 5 de 27").
- **Guarda contra status obsoleto**: compara `generation_started_at`; timeout de
  120s enquanto aguarda o início da geração nova.
- `API = window.location.origin` (corrige host/porta do iframe).
- Removido o include de `preview-diario.js` (handler conflitante).

### 5.2 Demais arquivos

| Arquivo | Antes | Agora |
|---------|-------|-------|
| `form.blade.php` | Autor/e-mail/data vazios | Pré-preenche com `Sentinel::getUser()` e `now()` |
| `index.blade.php` (CMS) | JS sem versão | `?v=0.0.2` em `datatable.js`/`generation-status.js` |
| `modal/areas/index.blade.php` | `areas.js?v=1.0.0` | `?v=1.0.1` |
| `portal/.../index.blade.php` | Sem campo edição | Campo **"Edição"** + `diario_portal.js?v=0.0.2` |
| `portal/.../show-pdf.blade.php` | Sem metadados | Tags **OpenGraph** + bloco de assinatura (autor/e-mail/lei/data) |
| `areas.js` | Ordenação/erro frágeis | i18n local (sem CDN), `removeLoad` seguro, Swal de sucesso/erro, Sortable com guard, coluna vazia na grid |
| `datatable.js` | Badge sem atributos | `data-status` + `title` com `generation_error` |
| `generation-status.js` | `setInterval` fixo, polling infinito | IIFE, `setTimeout` recursivo, para em estado terminal, aplica `data-status`/`title` |
| `diario_portal.js` | Filtro sem edição, paginação frágil | Suporta edição, guard de array, paginação com `disabled`/`active` |
| `select-image.js` | Sem guards | Guards de `selectSize`/`navMenu`, sem `console.log` |

---

## 6. Banco de dados / modelo

- Sem novas migrations neste branch.
- SoftDeletes já existentes (`diarios`, `diario_has_areas`, `diario_certificados`)
  passaram a ser respeitados nas consultas alteradas (preview, portal, certificados).
- Durante a validação foram **soft-deletados** os diários de teste (edição ≥ 91000)
  e restaurados os diários-base (1, 19). O diário `39` é de teste do usuário.

---

## 7. Infra / ambiente

### 7.1 `docker/apache/Dockerfile`

> Arquivo em `.git/info/exclude` (`/docker/`), portanto **não versionado**.

| Antes | Agora |
|-------|-------|
| Pacote `imagemagick` apenas | Adicionado **`ghostscript`** |
| Policy do ImageMagick bloqueia PDF | `RUN sed` libera o coder PDF (`rights="read|write"`) |

**Ações necessárias no ambiente:**
1. `docker compose build app` (persistir Ghostscript/policy na imagem).
2. **`docker stop` + `docker start`** do container app — o **PHP-FPM cacheia o
   policy do ImageMagick**; sem reiniciar, o web cai no fallback FPDI
   (1 anexo por coluna, vetorial) mesmo com o CLI funcionando.

### 7.2 `config('app.url')` no QR Code

- Antes: `'https://' . $_SERVER['SERVER_NAME'] . '/diario-oficial/' . $id`
  (falha em CLI; ignora porta/esquema).
- Agora: `rtrim(config('app.url'), '/') . '/diario-oficial/' . $id`.

---

## 8. Testes / validação

### 8.1 `tests-e2e/` (novo diretório)

- Suíte Playwright CMS/Portal (`tests/diario-cms.spec.ts`, etc.), `BASELINE.md`,
  `.auth/cms.json`.
- **Gate visual** (`scripts/sniff-visuals.sh`, sniffCSS) com baselines em
  `sniff/baselines/` e capturas em `sniff/current/`.
- Harness de cenários (`scenarios.cjs`, `scenarios-refetch.cjs`) + fixtures
  (`fixtures/scenario/`: PDFs de 1/2/3/5/8/18 págs, 3 págs landscape, `inline.png`).

### 8.2 Resultados

- **14 cenários** (texto puro, texto+imagem, texto+anexo no início/meio/fim,
  anexo sozinho, múltiplos anexos, combinações área/subárea, 2 colunas, full-page):
  **nenhuma página em branco**, cabeçalhos de área no topo, quebra só entre áreas.
- **E2E: 12/12**.
- **Gate visual: OK** (após remover os diários de teste que poluíam o select de
  Complemento).
- Verificação de dimensão: diário 39 com **4 anexos por página** (2 por coluna).

---

## 9. Tabela arquivo-a-arquivo

| Arquivo | Status | Resumo |
|---------|--------|--------|
| `app/Repositories/Eloquent/Prefecture/DiarioOficial/DiarioOficialRepository.php` | M | Escrita atômica, progresso, QR, escopo por área, fluxo inline de anexos, fallbacks, `buildAnexoImageTag` |
| `app/Services/FPdfService.php` | M | Delega `processImageContent` ao ImageProcessor; `buildAnexoImageTag` |
| `app/Services/Pdf/ImageProcessor.php` | **novo** | Cálculo de tamanho (clássico) independente de resolução |
| `app/Services/Pdf/PdfConverter.php` | M | Aborta em policy; `success=false` com 0 imagens |
| `app/Http/Controllers/Prefecture/DiarioController.php` | M | `show` força regen; capa em transação/1366px; progresso limpo; `orderBy` |
| `app/Http/Controllers/Portal/DiarioController.php` | M | Filtro em SQL (data/termo/edição) |
| `app/Http/Controllers/Prefecture/DiarioCertificadosController.php` | M | Validação CRT/CN/PKCS12 + logs |
| `app/Http/Components/.../Preview/DiarioOficialComponent.php` | M | Certificado não deletado + tolerância a erro |
| `public/assets/js/custom/contents/diario-oficial/areas.js` | M | i18n local, feedback, guards, Sortable |
| `public/assets/js/custom/contents/diario-oficial/datatable.js` | M | `data-status` + `title` de erro |
| `public/assets/js/custom/contents/diario-oficial/diario_portal.js` | M | Edição, guard de array, paginação |
| `public/assets/js/custom/contents/diario-oficial/generation-status.js` | M | Polling recursivo que para em terminal |
| `public/assets/js/custom/utilities/helpers/select-image.js` | M | Guards + `navMenu()` |
| `resources/views/prefecture/contents/diario-oficial/create.blade.php` | M | "Processar PDF", polling, progresso, gating |
| `resources/views/prefecture/contents/diario-oficial/form.blade.php` | M | Prefill autor/e-mail/data |
| `resources/views/prefecture/contents/diario-oficial/index.blade.php` | M | Versionamento de JS |
| `resources/views/prefecture/contents/diario-oficial/modal/areas/index.blade.php` | M | Versionamento de JS |
| `resources/views/portal/layouts/diario-oficial/index.blade.php` | M | Campo Edição |
| `resources/views/portal/layouts/diario-oficial/show-pdf.blade.php` | M | OpenGraph + assinatura |
| `docker/apache/Dockerfile` | M (não versionado) | Ghostscript + policy PDF |
| `tests-e2e/**` | **novo** | E2E, gate visual e matriz de cenários |

---

## 10. Observações e limitações

1. **Anexos inline são rasterizados** (JPEG): o PDF final não tem texto
   selecionável nas páginas de anexo (mesma característica da v5). O
   `file_in=false` permanece vetorial (FPDI) quando o PDF é compatível.
2. O parsing do FPDI (free) **não lê PDFs 1.5+/object streams**; por isso o
   caminho inline usa rasterização e o full-page tem fallback raster.
3. **Rebuild + restart do container** são obrigatórios para o Ghostscript/policy
   (item §7.1).
4. O gate visual é sensível aos diários ativos; os diários de teste devem ser
   removidos antes de rodá-lo (o select de Complemento é afetado).

---

## 11. Itens de backlog (Fase 4, não implementados)

- Remover orphans de adendo (`#adendo_container`/`areas.js`) e views mortas.
- Enum de tipos, fallback de extensão no `FileTrait`, `fillable`/casts do
  `Diario`, nome de rota, seeds de menu.
- Spike: avaliar o motor de colunas v5 (`LayoutManager`/`ImageProcessor`) vs a
  grade atual.
