---
id: SVC-MEDIA
tipo: servico
onda: 2
status: concluido
unidades: [SVC-MEDIA]
fontes:
  - V2/Services/Media/
depende_de: [SVC-STORAGE, SVC-GALLERY, SVC-EXCELIMPORTER]
consumidores: [SVC-GALLERY, SVC-CONTENTRICHEDITOR, MOD-SUPPORT-STRUCTURALLANDINGEDITOR, MOD-LAYOUTS-BASICPAGE, MOD-PUBLICACOES-NOTICIAS, MOD-CONEXOES-ASSOCIADOS]
criticidade: alta
dono: SvcStorageMedia
---

# Serviço Media

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Recebe um `UploadedFile`, grava o original no storage e gera derivados conforme o tipo: variantes WebP responsivas (imagem), capa WebP + contagem de páginas (PDF), variantes CSV/HTML (planilha) ou nenhum derivado (áudio/documento). |
| Entrada | `MediaInputer::processUpload(UploadedFile $file, $path, $disk='public', $customFilename=null)`; comando `v2:redigest-images`; chamadas diretas aos otimizadores e ao `ImageRedigestService`. |
| Saída | Array com `original_name`, `path`, `disk`, `mime_type`, `size`, `type` (`image/pdf/audio/spreadsheet/document/unknown`), `variants` e `metadata`; arquivos no disco; snippets Blade regravados pelo comando. |
| Efeitos colaterais | Escrita do original e de variantes no `SVC-STORAGE`; downloads temporários S3; remoção da capa temporária de PDF; `Log::info/warning/error`; comando `--apply` atualiza `snippet`/`content` e limpa o cache de páginas. |
| Particularidade central | É o único pipeline de validação/otimização de mídia, mas **não cataloga**: quem insere em `v2_gallery_media` é o `SVC-GALLERY`. O parâmetro `$disk` é aceito e ignorado na gravação. |
| Como se integra | `MediaInputer` por autowire; views no namespace `v2-service-media::components.*`; comando Artisan registrado em `app/Console/Kernel.php:20`; consome `StorageService` e `ExcelTranspilerService`. |
| Se removido, quebra | Upload da Galeria (`GalleryService.php:58-75`), upload do editor rico (`ContentRichEditorController.php:36-42`), logos/banners do LSE (`LayoutBaseCmsController.php:88,243`) e a geração de snippets de imagem em BasicPage/SLE. |
| Superfície pública | `Services\MediaInputer`; `Services\ImageRedigestService`; `Services\Midia\{ImageOptimizer,PdfVisualOptimizer,AudioOptimizer,SpreadsheetOptimizer}`; comando `RedigestImages`. |

## 1. Efeito

### 1.1 Transformação que o serviço realiza

`processUpload()` valida o MIME detectado no servidor, grava o original em `<path>/<nome>.<ext>` e então roteia para um otimizador (`V2/Services/Media/Services/MediaInputer.php:134-228`). O retorno é sempre o array padronizado; `type` é definido antes da otimização, de modo que uma falha do otimizador deixa o original gravado e o tipo preenchido, com `variants` potencialmente vazio.

| Tipo de entrada | Original | Derivados | Metadados |
|---|---|---|---|
| `image/*` exceto SVG | `<path>/<nome>.<ext>` | `variants/<nome>-{360,720,1200,1920}w.webp` para larguras ≤ original; se nenhuma couber, `variants/<nome>.webp` com chave `original` | `fallback_local` |
| `image/svg+xml` | `<path>/<nome>.svg` | nenhum | `fallback_local` |
| `application/pdf` (ou extensão `.pdf`) | `<path>/<nome>.pdf` | capa da página 0 em WebP e variantes do **nome temporário** `temp_cover_<nome>` | `fallback_local`, `pages` |
| áudio (`audio/*` ou extensões de áudio) | `<path>/<nome>.<ext>` | nenhum (`optimizeAudio()` devolve `[]`) | `fallback_local` |
| planilha (`xlsx/xls/csv/ods`) | `<path>/<nome>.<ext>` | `<nome>_variant.csv` e `<nome>_variant.html` | `fallback_local` |
| outros MIME não bloqueados | `<path>/<nome>.<ext>` | nenhum | `type='document'`, `fallback_local` |

### 1.2 Efeitos sobre dados e sistema

| Efeito | Recurso afetado | Ponto no código | Fonte |
|---|---|---|---|
| Valida MIME no servidor contra whitelist + prefixos `image/`/`audio/` | request | `validateMimeType` | `V2/Services/Media/Services/MediaInputer.php:100-122` |
| Normaliza MIME de AVIF/planilha **depois** da validação | variável local | `processUpload` | `:143-155` |
| Valida áudio e aborta antes de gravar | request | `AudioOptimizer::validate` | `:161-176`; `V2/Services/Media/Services/Midia/AudioOptimizer.php:36-48` |
| Grava o original | disco resolvido pelo `StorageService` | `storeFileResiliently` | `:235-274` |
| Fallback local se o `put` lançar | disco `public` | `storeFileResiliently` | `:256-272` |
| Gera variantes WebP de imagem | `<dir>/variants` | `ImageOptimizer::generateVariants` | `V2/Services/Media/Services/Midia/ImageOptimizer.php:46-145` |
| Gera capa e variantes de PDF | `<dir>/temp_cover_*.webp` + `<dir>/variants/temp_cover_*` | `PdfVisualOptimizer` | `V2/Services/Media/Services/Midia/PdfVisualOptimizer.php:85-143` |
| Extrai `pages` do PDF via `pingImage` | memória | `extractMetadata` | `V2/Services/Media/Services/Midia/PdfVisualOptimizer.php:39-75` |
| Gera variantes CSV/HTML de planilha | mesmo diretório | `SpreadsheetOptimizer::generateVariants` | `V2/Services/Media/Services/Midia/SpreadsheetOptimizer.php:40-64` |
| Remove a capa temporária de PDF após gerar variantes | disco | `deleteSafe` | `V2/Services/Media/Services/Midia/PdfVisualOptimizer.php:124-125` |
| Remove download temporário S3 após otimizar | `/tmp` | `unlink` | `V2/Services/Media/Services/Midia/ImageOptimizer.php:124-127,139-143` |
| Regrava `snippet` de assets e `variants` de blobs | tabelas de outros módulos | `ImageRedigestService` | `V2/Services/Media/Services/ImageRedigestService.php:153-203,234-350` |
| Limpa cache de páginas após `--apply` | Redis (via comando externo) | `RedigestImages::clearPageCache` | `V2/Services/Media/Console/Commands/RedigestImages.php:97-100,156-159` |

Não há escrita em banco no pipeline de upload. A linha em `v2_gallery_media` é criada pelo chamador `GalleryService::upload()` (`V2/Services/Gallery/Services/GalleryService.php:65-77`) com `variants` e `metadata` serializados em JSON.

### 1.3 Contrato observável

`processUpload` devolve sempre as mesmas chaves (`:184-195`):

| Chave | Tipo | Conteúdo |
|---|---|---|
| `original_name` | string | `UploadedFile::getClientOriginalName()` |
| `path` | string\|null | chave relativa gravada; `null` quando áudio é rejeitado antes de salvar (`:166-175`) |
| `disk` | string | `StorageService::getDisk()` (primário) ou `'public'` no fallback de exceção; em rejeição de áudio, o `$disk` de entrada |
| `mime_type` | string | MIME detectado, após normalizações |
| `size` | int | `getSize()` do upload |
| `type` | enum | `image`, `pdf`, `audio`, `spreadsheet`, `document`, `unknown` |
| `variants` | array | mapa `largura` / `'csv'` / `'html'` / `'original'` => caminho |
| `metadata` | array | `fallback_local` sempre; `pages` em PDF; `error` em áudio rejeitado |

Erros observáveis:

| Situação | Resultado |
|---|---|
| MIME fora da whitelist e sem prefixo aceito | `\Exception` com `"Tipo de arquivo não suportado: {mime} (extensão .{ext}). Apenas imagens, PDFs, áudios e planilhas são permitidos."` (`:118-121`) |
| Áudio com MIME inválido | retorno `type='unknown'`, `path=null`, `metadata.error` com `"O formato do arquivo '{mime}' (extensão .{ext}) não é um formato de áudio suportado."` (`AudioOptimizer.php:46`) |
| Original gravado, otimização falha | `type` permanece o detectado, `variants` vazio/parcial, log `error`; **não lança** (`MediaInputer.php:224-226`) |
| Storage primário e fallback lançam | `\Exception` `"Falha ao salvar arquivo no armazenamento: {msg}"` (`:269-272`) |
| `ImageOptimizer` chamado direto e original ausente | `\Exception` `"Arquivo original não encontrado no storage: {path}"` (`ImageOptimizer.php:48-51`) |
| `ImageOptimizer` sem extensão Imagick | `\Exception` `"Extensão Imagick não encontrada no PHP."` (`ImageOptimizer.php:67-69`) |

### 1.4 O que o serviço deliberadamente NÃO faz

- Não cria/atualiza registro de galeria, não calcula `owner_module`, `is_shared`, `alt_text` ou `description`.
- Não autoriza usuário, módulo ou posse; isso é do controller/middleware da Galeria ou do editor.
- Não impõe tamanho máximo nem rate limit; o limite é do chamador (`max:10240` em `GalleryController.php:88-93` e `ContentRichEditorController.php:29-31`).
- Não converte áudio, não extrai duração/bitrate (`AudioOptimizer::optimizeAudio` é reservado, `AudioOptimizer.php:57-61`).
- Não converte SVG em WebP nem gera variantes para SVG (`MediaInputer.php:205-207`; `ImageOptimizer.php:53-59`).
- Não preserva EXIF/metadados: usa `stripImage()` (`ImageOptimizer.php:88,108`; `PdfVisualOptimizer.php:113`).
- Não remove variantes antigas ao sobrescrever a mesma imagem/nome.
- Não grava variante WebP do original para imagens ≥ 360 px; o original continua no formato de entrada.
- Não faz crop, resize do original ou marca d'água.
- Não atualiza `snippet` automaticamente após novo upload; isso exige o comando de redigestão.

## 2. Abordagem

### 2.1 Estratégia e por quê

`MediaInputer` é um **manager/fachada** que concentra validação, gravação resiliente e roteamento por MIME; cada formato tem uma estratégia própria (`ImageOptimizer`, `PdfVisualOptimizer`, `AudioOptimizer`, `SpreadsheetOptimizer`), todas com a mesma assinatura conceitual `(disk, path, [file])`. O objetivo é uma porta única para Galeria, editor e LSE, mantendo o pipeline independente do disco via `StorageService`.

```mermaid
flowchart TD
    A[UploadedFile] --> B{validateMimeType}
    B -- inválido --> X[Exception]
    B -- válido --> C[normaliza MIME xls/csv/avif]
    C --> D{isAudio?}
    D -- sim --> E[AudioOptimizer::validate]
    E -- inválido --> Y[type unknown, path null]
    E -- válido --> F[storeFileResiliently]
    D -- não --> F
    F -- put ok --> G{type por MIME/extensão}
    F -- put falha --> H[LocalAdapter('public')]
    H -- ok --> G
    H -- falha --> Z[Exception]
    G -- image --> I[ImageOptimizer::generateVariants]
    G -- pdf --> J[extractMetadata + generateCoverVariants]
    G -- audio --> K[optimizeAudio=[]]
    G -- spreadsheet --> L[convertToCsv/Html]
    G -- outro --> M[type=document]
    I/J/L -- exceção --> N[log error, mantém original]
    I/J/L -- ok --> O[array de resultado]
```

| Otimizador | Entrada | Saída | Biblioteca | Limites | Falha |
|---|---|---|---|---|---|
| `ImageOptimizer` | `$sourcePath` já gravado; extensão para o guard de SVG | `[360/720/1200/1920 => path]` ou `['original' => path]`; SVG → `[]` | `Imagick` (`new Imagick`, `thumbnailImage`, `setImageFormat('webp')`) | larguras `[360,720,1200,1920]`; só ≤ largura original; qualidade 75; sem crop | original ausente, Imagick ausente ou erro de encode → lança `\Exception` |
| `AudioOptimizer` | `UploadedFile` (validate) e `$path` (optimize) | `validate` → `null`; `optimizeAudio` → `[]` | nenhuma | MIME `audio/*` ou `octet-stream` + extensão de áudio | MIME inválido → `\Exception` com formato/extensão |
| `PdfVisualOptimizer` | `$sourcePath` PDF | `['pages'=>int]` e variantes da capa | `Imagick` (`pingImage`, `readImage('[0]')`, 150 DPI) | só página 0; capa WebP qualidade 90 | qualquer erro → loga e devolve `[]` |
| `SpreadsheetOptimizer` | `$sourcePath` + `UploadedFile` | `['csv'=>path,'html'=>path]` | `PhpSpreadsheet` via `ExcelTranspilerService` | primeira aba; CSV `;`/`"`/`\r\n`; HTML primeira linha como header | erro → loga e devolve variantes já geradas (parcial) |

### 2.2 Mecânica passo a passo

1. `getMimeType()`, extensão em minúsculas, nome original e tamanho são capturados (`MediaInputer.php:136-139`).
2. `validateMimeType()` aceita a whitelist, qualquer `image/*`, qualquer `audio/*` e `application/octet-stream` quando a extensão está em `knownExtensions`; caso contrário lança (`:100-122`).
3. Só **depois** da validação, MIME de planilhas é normalizado (`xls` HTML/texto → `application/vnd.ms-excel`; `csv` não-`text/csv` → `text/csv`) e AVIF `octet-stream` → `image/avif` (`:143-155`).
4. `$isAudio` é verdadeiro para prefixo `audio/`, `application/ogg`, `application/x-cdf` ou extensões `mp3,wav,ogg,m4a,aac,webm,wma,mp4,mpeg` (`:157-158`).
5. Se áudio, `AudioOptimizer::validate()` roda antes de qualquer gravação; rejeição retorna `type='unknown'` sem gravar (`:161-176`).
6. `storeFileResiliently()` monta `filename = customFilename.ext` ou `uniqid('media_').'_'.time().'.'.ext`, grava via `StorageService::put($contents, $savedPath, ['visibility'=>'public'])` e devolve `path`, `disk`, `fallback_local=false` (`:235-252`).
7. Se `put` lançar, cria `new StorageService()`, injeta `LocalAdapter('public')` via `setAdapter`, grava o mesmo `path` e devolve `disk='public'`, `fallback_local=true`; se também falhar, lança (`:256-272`).
8. Monta o array base com `type='unknown'`, `variants=[]`, `metadata.fallback_local` (`:184-195`).
9. Roteia: imagem → `generateVariants`; PDF → `extractMetadata` + `generateCoverVariants`; áudio → `optimizeAudio`; planilha → `generateVariants($disk,$path,$file)`; resto → `document` (`:197-223`).
10. Qualquer exceção nesse bloco é logada e engolida (`:224-226`); o array base é retornado (`:228`).
11. O chamador decide persistir: `GalleryService` insere em `v2_gallery_media` e serializa `variants`/`metadata` (`V2/Services/Gallery/Services/GalleryService.php:65-77`).

### 2.3 Trechos de código pivô

```php
// V2/Services/Media/Services/MediaInputer.php:100-122 — validação por MIME do servidor.
protected function validateMimeType($mimeType, $extension)
{
    if (in_array($mimeType, $this->allowedMimeTypes)) {
        return;
    }

    // ... (image/* e audio/* também retornam)
    if ($mimeType === 'application/octet-stream' && in_array($extension, $this->knownExtensions)) {
        return;
    }

    throw new \Exception(
        "Tipo de arquivo não suportado: {$mimeType} (extensão .{$extension}). " .
        "Apenas imagens, PDFs, áudios e planilhas são permitidos."
    );
}
```

O filtro é por **prefixo**: qualquer `image/*` e qualquer `audio/*` passam, mesmo que não estejam na lista. A lista só é decisiva para PDF, planilhas e tipos como `application/ogg`. O MIME considerado é o detectado pelo Symfony/finfo, não o `Content-Type` do cliente.

```php
// V2/Services/Media/Services/MediaInputer.php:143-158 — normalização e detecção de áudio.
if (in_array($extension, ['xlsx', 'xls', 'csv', 'ods'])) {
    if ($extension === 'xls' && ($mimeType === 'text/html' || $mimeType === 'application/xhtml+xml' || $mimeType === 'text/plain')) {
        $mimeType = 'application/vnd.ms-excel';
    } elseif ($extension === 'csv' && $mimeType !== 'text/csv') {
        $mimeType = 'text/csv';
    }
}

if ($extension === 'avif' && $mimeType === 'application/octet-stream') {
    $mimeType = 'image/avif';
}

$audioExtensions = ['mp3', 'wav', 'ogg', 'm4a', 'aac', 'webm', 'wma', 'mp4', 'mpeg'];
$isAudio = strpos($mimeType, 'audio/') === 0 || in_array($mimeType, ['application/ogg', 'application/x-cdf']) || in_array($extension, $audioExtensions);
```

A normalização de XLS/CSV acontece **depois** de `validateMimeType` (`:141`): um arquivo detectado como `text/html` ou `text/plain` já foi rejeitado antes de chegar aqui. Na prática, o bloco só é útil quando o MIME já passou pela whitelist/`octet-stream`, e a detecção de áudio por extensão pode classificar um `mp4` como áudio caso o MIME tenha passado.

```php
// V2/Services/Media/Services/MediaInputer.php:235-252 — nome e gravação do original.
protected function storeFileResiliently(UploadedFile $file, $path, $disk, $customFilename = null)
{
    $extension = $file->getClientOriginalExtension() ?: 'bin';
    $filename = $customFilename
        ? $customFilename . '.' . $extension
        : uniqid('media_') . '_' . time() . '.' . $extension;
    $savedPath = rtrim($path, '/') . '/' . $filename;

    try {
        Log::info("[MediaInputer] Tentando salvar arquivo via StorageService");
        $contents = file_get_contents($file->getRealPath());
        $this->storageService->put($contents, $savedPath, ['visibility' => 'public']);

        return [
            'path' => $savedPath,
            'disk' => $this->storageService->getDisk(),
            'fallback_local' => false
        ];
    } catch (\Exception $e) {
```

`$customFilename` é usado sem sanitização e recebe a extensão do cliente; passar `foto.jpg` produz `foto.jpg.jpg`. O `path` final não é normalizado além de `rtrim`. O fallback em `:256-271` cria um segundo `StorageService` e força `LocalAdapter('public')`; o `disk` retornado só é confiável quando `fallback_local=true`, porque o `S3Adapter` pode ter feito fallback interno sem lançar.

```php
// V2/Services/Media/Services/MediaInputer.php:201-223 — roteamento por tipo.
if (strpos($mimeType, 'image/') === 0) {
    $result['type'] = 'image';

    // SVG vetorial: não converte para WebP — mantém original
    if ($mimeType === 'image/svg+xml') {
        $result['variants'] = [];
        Log::info("[MediaInputer] SVG detectado, pulando geração de variantes WebP: {$originalName}");
    } else {
        $result['variants'] = $this->imageOptimizer->generateVariants($resolvedDisk, $savedPath);
    }
} elseif ($mimeType === 'application/pdf' || strtolower($extension) === 'pdf') {
    $result['type'] = 'pdf';
    $result['metadata'] = array_merge($result['metadata'], $this->pdfOptimizer->extractMetadata($resolvedDisk, $savedPath));
    $result['variants'] = $this->pdfOptimizer->generateCoverVariants($resolvedDisk, $savedPath);
}
// ... elseif ($isAudio), elseif ($isSpreadsheet), else => 'document'

```

A ordem importa: imagem vence PDF, áudio e planilha; PDF vence áudio/planilha. Um PDF com extensão `pdf` e MIME `application/octet-stream` cai no ramo PDF pela extensão. Um `image/svg+xml` tem `variants=[]` explícito, o que faz o componente `picture` cair no `<img>` original.

```php
// V2/Services/Media/Services/Midia/ImageOptimizer.php:79-98 — larguras, WebP e caminho.
foreach ($this->widths as $width) {
    if ($width > $originalWidth) {
        continue;
    }
    $clone = clone $image;
    $clone->thumbnailImage($width, 0);
    $clone->setImageFormat('webp');
    $clone->setImageCompressionQuality(75);
    $clone->stripImage();
    $targetPath = "{$targetDir}/{$baseName}-{$width}w.webp";
    // Grava via StorageService (transparente para S3 ou Local)
    $this->storageService->put($clone->getImageBlob(), $targetPath, ['visibility' => 'public']);
    $clone->clear();
    $clone->destroy();
    $variants[$width] = $targetPath;
}
```

`$this->widths` é `[360,720,1200,1920]` (`:26`). `thumbnailImage($width, 0)` preserva proporção, sem crop, e o original **não** é regravado. Se nenhuma largura couber, o bloco `:104-119` gera `<base>.webp` na largura original com chave `'original'`. Qualidade fixa 75 e `stripImage()` removem metadados.

```php
// V2/Services/Media/Services/Midia/PdfVisualOptimizer.php:104-122 — capa da página 0
// e variantes geradas a partir de um arquivo temporário.
$imagick->setResolution(150, 150);
$imagick->readImage($fullSourcePath . '[0]');
$imagick->setImageFormat('webp');
$imagick->setImageCompressionQuality(90);
$imagick->stripImage();

// Salva a capa temporária via StorageService para o ImageOptimizer processar
$this->storageService->put($imagick->getImageBlob(), $tempCoverPath, ['visibility' => 'public']);

$imagick->clear();
$imagick->destroy();

// Gera o kit de variantes responsivas (360w, 720w, etc)
$variants = $this->imageOptimizer->generateVariants($disk, $tempCoverPath);

// Remove a imagem base temporária
$this->storageService->deleteSafe($tempCoverPath);
```

O caminho temporário é `temp_cover_<basename>.webp` (`:97-98`), portanto as variantes ficam `.../variants/temp_cover_<basename>-360w.webp` — o prefixo `temp_cover_` **permanece** nos nomes finais. Se `generateVariants` lançar, o `deleteSafe` não é executado e a capa temporária fica órfã, pois o `catch` só limpa o PDF baixado (`:133-143`).

```php
// V2/Services/Media/Services/ImageRedigestService.php:44-58 — literal do snippet e escapes.
$altEscaped = $alt !== null && $alt !== '' ? addslashes($alt) : '';

// Disk vem de config, nunca de env() em runtime (compatível com config:cache).
$disk = $opts['disk'] ?? config('filesystems.media_disk', 'public');

// Monta o array de variants como literal PHP: [360 => 'path', 720 => 'path'].
$variantsLiteral = '[]';
if (!empty($variants)) {
    $pairs = [];
    foreach ($variants as $width => $path) {
        $pairs[] = $width . " => '" . addslashes((string) $path) . "'";
    }
    $variantsLiteral = '[' . implode(', ', $pairs) . ']';
}
```

O snippet produzido (`:63-70`) é exatamente `@include('v2-service-media::components.picture', [...])` com `originalPath`, `variants`, `disk`, `alt`, `class` e `lazy`. O `disk` é congelado no momento da geração; mudar `filesystems.media_disk` depois não altera snippets gravados. `addslashes` não é escape de Blade/PHP robusto para todos os casos, mas preserva o formato histórico.

```php
// V2/Services/Media/Services/ImageRedigestService.php:289-305 — reescrita cirúrgica.
$newVariants = self::variantsForOriginalPath($originalPath);
if (empty($newVariants)) {
    return $block; // sem variants atuais -> mantém
}
if (!preg_match($variantsLineRegex, $block, $vl)) {
    return $block; // sem linha de variants -> mantém
}
$currentLiteral = $vl[1];
$newLiteral = self::variantsLiteral($newVariants);
if ($currentLiteral === $newLiteral) {
    return $block; // idempotência
}
return str_replace(
    "'variants' => " . $currentLiteral,
    "'variants' => " . $newLiteral,
    $block
);
```

A reescrita só toca a linha `'variants'`; `alt`, `class`, `lazy`, `disk` e `originalPath` são preservados. Se a mídia não tiver variants (SVG/PDF ou JSON vazio), o bloco fica intacto — não há como "limpar" variants antigas de um SVG via comando. O casamento é por `file_path` exato e não filtra `deleted_at`.

### 2.4 Algoritmos e custos

- **I/O de imagem:** 1 `exists`, 1 download local (S3) ou `path`, 1 leitura Imagick, até 4 clonagens/gravações. Custo dominado por decodificação e encode WebP.
- **PDF:** `pingImage` lê só o cabeçalho para `pages`; a capa usa `readImage('[0]')` a 150 DPI, gerando uma imagem-base e até 4 variantes; a base é removida depois. No S3, o PDF é baixado duas vezes (metadados e capa), porque `extractMetadata` remove o temporário antes de `generateCoverVariants` (`PdfVisualOptimizer.php:45-63,91-93`).
- **Planilha:** `PhpSpreadsheet` carrega a planilha **duas vezes** (CSV e HTML) via `loadSpreadsheet`, com pico de memória proporcional ao arquivo; CSV usa delimitador `;`, enclosure `"`, EOL `\r\n` e apenas a primeira aba (`V2/Services/ExcelImporter/Services/ExcelTranspilerService.php:22-41`); HTML usa a aba ativa, primeira linha como `<th>` e escapa valores com `e()` (`:52-108`).
- **Redigest:** varre em chunks de 100 por PK, com cursor inicial nulo para aceitar UUID; por linha de asset faz 2 consultas (`find` + `variantsFor...`); por bloco de conteúdo faz 1 consulta por `originalPath`. Complexidade O(linhas + blocos).
- **Memória:** `file_get_contents` do upload + conteúdo completo no `put`; não há streaming no `MediaInputer`.
- **Concorrência:** sem locks; dois uploads com o mesmo `customFilename` sobrescrevem original e variantes.

## 3. Particularidades

### 3.1 O que é exclusivo deste serviço

É a única implementação de: conversão responsiva WebP, capa de PDF, variantes CSV/HTML, validação MIME de mídia e reconstrução de snippets `picture`. O `SVC-GALLERY` **cataloga e seleciona** (tabela `v2_gallery_media`, visibilidade por módulo, picker); o `SVC-MEDIA` **processa e entrega**. `MediaInputer` não conhece `owner_module`, `is_shared` nem permissões; `GalleryService` não implementa otimização, apenas chama o pipeline e persiste o resultado (`V2/Services/Gallery/Services/GalleryService.php:55-77`).

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-MEDIA-01` | Validação usa MIME detectado no servidor; qualquer `image/*`/`audio/*` passa. | Whitelist não restringe todos os subtipos. | `MediaInputer.php:100-122` |
| `RN-MEDIA-02` | Normalização de XLS/CSV ocorre depois da validação. | `text/html`/`text/plain` disfarçados são rejeitados antes de normalizar. | `MediaInputer.php:141-150` |
| `RN-MEDIA-03` | SVG nunca gera variantes WebP. | `variants=[]`; `picture` usa só o original. | `MediaInputer.php:205-207`; `ImageOptimizer.php:53-59` |
| `RN-MEDIA-04` | Variantes só são geradas para larguras ≤ largura original; se nenhuma, gera `original`. | Imagem de 300 px produz só `variants/<nome>.webp` com chave `'original'`. | `ImageOptimizer.php:79-119` |
| `RN-MEDIA-05` | Nomes de variantes usam `<basename>-<width>w.webp`; PDFs usam `temp_cover_<basename>`. | Consumidores precisam usar o caminho retornado, não reconstruir o nome. | `ImageOptimizer.php:90`; `PdfVisualOptimizer.php:97-98` |
| `RN-MEDIA-06` | Áudio é validado, mas não convertido. | `optimizeAudio()` devolve `[]`; formato/taxa permanecem. | `AudioOptimizer.php:57-61` |
| `RN-MEDIA-07` | Planilha gera CSV e HTML no mesmo diretório. | Variantes têm sufixo `_variant.csv`/`_variant.html`. | `SpreadsheetOptimizer.php:49-59` |
| `RN-MEDIA-08` | Falha de otimização não falha o upload. | Original permanece; `type` já está definido; `variants` pode estar vazio/parcial. | `MediaInputer.php:201-226` |
| `RN-MEDIA-09` | `$disk` é ignorado na gravação. | Quem decide é `StorageService::resolveAdapter()`. | `MediaInputer.php:235-246`; `StorageService.php:33-43` |
| `RN-MEDIA-10` | O pipeline não escreve em banco. | Persistência é responsabilidade do chamador (Gallery). | `MediaInputer.php:134-229`; `GalleryService.php:65-77` |
| `RN-MEDIA-11` | `customFilename` recebe a extensão do cliente sem sanitização. | `foto.jpg` → `foto.jpg.jpg`; `../x` pode escapar do diretório conforme driver. | `MediaInputer.php:237-241` |
| `RN-MEDIA-12` | `v2:redigest-images` é dry-run por padrão. | Sem `--apply`, apenas conta; com `--apply`, grava e limpa cache de páginas. | `RedigestImages.php:26,69,97-100` |
| `RN-MEDIA-13` | Redigest de assets usa `file_path`/`alt_text` do gallery, não `storage_path` do asset. | `snippet` reflete o estado atual da mídia. | `ImageRedigestService.php:183-188` |
| `RN-MEDIA-14` | Redigest de conteúdo troca **apenas** o array `variants`. | `alt`/`class`/`lazy`/`disk` gravados permanecem. | `ImageRedigestService.php:289-312` |
| `RN-MEDIA-15` | Consultas de redigest não filtram `deleted_at`. | Mídia arquivada ainda é usada para reconstruir snippets. | `ImageRedigestService.php:91,116-118,178` |
| `RN-MEDIA-16` | Qualidade fixa: imagem 75, capa PDF 90. | Sem configuração por chamador. | `ImageOptimizer.php:87,107`; `PdfVisualOptimizer.php:112` |

### 3.3 Casos-limite

- **Arquivo vazio/corrompido:** passa na validação se o MIME for aceito; Imagick falha, `MediaInputer` engole e devolve original com `variants=[]`.
- **Imagick ausente:** `ImageOptimizer` lança `"Extensão Imagick não encontrada no PHP."`; `MediaInputer` engole; upload "funciona" sem variantes. `PdfVisualOptimizer` também depende de `Imagick`, mas retorna `[]` em erro (`PdfVisualOptimizer.php:67-75,134-142`).
- **PDF protegido/ inválido:** `pingImage`/`readImage` falham; `pages` ausente e capa `[]`, mas o PDF original permanece.
- **Contagem de páginas:** `pages` é `getNumberImages()` imediatamente após `pingImage` (`PdfVisualOptimizer.php:50-55`); o valor para PDFs multi-página depende do delegate do Imagick e deve ser validado com amostras reais `[I]`.
- **Planilha malformada:** `SpreadsheetOptimizer` captura e devolve o que já gerou; se o CSV falhar, o HTML nem é tentado (`SpreadsheetOptimizer.php:48-63`).
- **Áudio inválido:** nada é gravado e o `GalleryService` lança `"Tipo de arquivo não suportado."` ao ver `type='unknown'` (`GalleryService.php:61-63`).
- **S3 indisponível:** `S3Adapter::put` pode gravar em `public` sem lançar; `fallback_local` fica `false` e `disk='s3'`, mas os bytes estão no local (`S3Adapter.php:22-37`; `MediaInputer.php:248-252`). Nesse cenário, `ImageOptimizer::generateVariants` chama `StorageService::exists` no adaptador S3; se o S3 responder `false` (objeto ausente) sem lançar, o fallback local não é consultado e a otimização falha com `"Arquivo original não encontrado no storage"`, que o `MediaInputer` engole — resultado: original no local, `disk='s3'`, `variants=[]`.
- **Imagem < 360 px:** gera uma única variante `original`; `picture` usa essa variante como WebP e o original como fallback.
- **GIF animado/TIFF:** `Imagick` + `thumbnailImage` opera na imagem corrente; não há tratamento explícito de múltiplos frames (`ImageOptimizer.php:71-98`) `[I]`.
- **Sobrescrita com `customFilename`:** original e variantes são sobrescritos; variantes de larguras que a nova imagem não gera continuam no disco.
- **Nome de arquivo com espaços/acentos:** preservado; a URL depende do driver codificar corretamente.
- **`mp4`/`mpeg`:** a extensão está na lista de áudio, mas `video/mp4` não está na whitelist e é rejeitado antes de `$isAudio`; só passa como `audio/mp4` ou `application/octet-stream` (`MediaInputer.php:56-81,100-122,157-158`).
- **`octet-stream` com extensão conhecida:** passa na validação, mas só cai em ramo de imagem se o MIME tiver sido normalizado para `image/*` (caso AVIF). Um TIFF/BMP/SVG detectado como `application/octet-stream` cai em `type='document'`, sem variantes (`MediaInputer.php:113-116,153-155,201-222`).
- **`variants` ausente/inválido no banco:** redigest devolve `[]` e não altera o bloco (`ImageRedigestService.php:391-414`).

### 3.4 Armadilhas de reimplementação

- **Esperar exceção quando a otimização falha.** O `MediaInputer` captura em `:224-226`; só falhas de validação MIME/áudio ou de gravação propagam.
- **Tratar `$disk` como destino.** Ele é repassado aos otimizadores (que o ignoram) e não é usado na gravação; use `config('filesystems.media_disk')` ou o retorno `disk`.
- **Procurar `variants['webp']`.** As chaves são `360/720/1200/1920`/`original` (e `csv`/`html`). O `ContentRichEditorController` procura `webp` (`ContentRichEditorController.php:40-42`) e nunca encontra, retornando o original.
- **Reconstruir nomes de variantes.** O caminho depende de `basename`, `variants/` e, no PDF, de `temp_cover_`; use o mapa retornado.
- **Passar JSON string para `picture`.** O componente só aceita array; string vira `[]` (`picture.blade.php:28`).
- **Redigest em conteúdo com formatação diferente.** O regex exige `@include(...` seguido de `\n])` no formato exato de `buildPictureSnippet` (`ImageRedigestService.php:243-250`); includes editados manualmente podem não casar.
- **Rodar redigest sem `--apply` e achar que gravou.** O default é dry-run (`RedigestImages.php:69`).
- **Usar `--module=BasicPage|SLE`.** A assinatura não usa união enumerada porque `|` é separador de shortcut no Symfony Console (`RedigestImages.php:44-49`); valores válidos são `BasicPage`, `SLE`, `all`.
- **Contar com limpeza de temporários.** Capa de PDF pode ficar órfã; `/tmp/lotep_s3_downloads` nunca é limpo pelo serviço.
- **Assumir idempotência do `snippet` com alt alterado.** Redigest de asset regrava se o snippet inteiro divergir (inclui alt); redigest de conteúdo **não** atualiza alt.

### 3.5 Dívidas e divergências

- **README contradiz o código.** Afirma "Propagação de erros: exceções são lançadas (não silenciadas)" (`V2/Services/Media/README.md:11`), mas `MediaInputer` captura e loga (`MediaInputer.php:224-226`); o README do Gallery ainda manda capturar `\Exception` de `seedFromPath` (`V2/Services/Media/CMS_INTEGRATION.md:75-86`), o que não acontece para erros de otimização.
- **Normalização inalcançável.** O bloco XLS/CSV pós-validação (`MediaInputer.php:143-150`) não cobre os casos que declara, porque a validação já rejeitou `text/html`/`text/plain`.
- **`allowedMimeTypes` inclui `application/x-cdf`**, tratado como áudio (`:56-81,158`); não há explicação no código. `[I]` provável legado/erro de digitação para CSV.
- **`$disk` ignorado nos otimizadores.** As assinaturas documentam "Ignorado" (`ImageOptimizer.php:41-42`; `PdfVisualOptimizer.php:35-36`; `SpreadsheetOptimizer.php:35`), mas o parâmetro continua sendo passado.
- **`media-viewer`/`components.audio` sem consumidores no repositório** (0 `@include`); podem ser contrato publicado para uso futuro. Ver `ui-contrato.md`.
- **`ContentRichEditorController` usa `variants['webp']`**, chave que o pipeline nunca produz.
- **`ImageRedigestService` afirma que `chunkById` quebra com UUID** e implementa cursor manual; as tabelas de asset usam PK UUID (`2026_07_06_000200_create_basic_page_assets_table.php:18`; `2026_07_06_000000_create_v2_sle_assets_table.php:19`), mas `v2_gallery_media` usa `increments('id')` (`2026_06_28_000000_create_v2_gallery_media_table.php:24`).
- **Sem transação entre original e variantes.** Qualquer falha parcial deixa arquivos órfãos.
- **Sem limpeza de variantes antigas** em reupload/rotação. `GalleryService::rotate` pode alterar a imagem sem regenerar todas as variantes (fora do escopo desta unidade; confirmar no `SVC-GALLERY`).

## 4. Integrabilidade

### 4.1 API pública

| Símbolo | Assinatura | Retorno | Exceções | Consumidores |
|---|---|---|---|---|
| `MediaInputer::processUpload` | `(UploadedFile $file, $path, $disk='public', $customFilename=null)` | `array` (8 chaves) | `\Exception` MIME, áudio? não (retorno), storage crítico | Gallery, editor, LSE |
| `ImageOptimizer::generateVariants` | `($disk, $sourcePath)` | `array [largura=>path]` ou `['original'=>path]` ou `[]` (SVG) | original ausente, Imagick ausente, falha Imagick | `MediaInputer`, `PdfVisualOptimizer` |
| `PdfVisualOptimizer::extractMetadata` | `($disk, $sourcePath)` | `['pages'=>int]` ou `[]` | nenhuma (catch) | `MediaInputer` |
| `PdfVisualOptimizer::generateCoverVariants` | `($disk, $sourcePath)` | `array [largura=>path]` ou `[]` | nenhuma (catch) | `MediaInputer` |
| `AudioOptimizer::validate` | `(UploadedFile $file)` | `null` | `\Exception` com mensagem de formato | `MediaInputer` |
| `AudioOptimizer::optimizeAudio` | `($disk, $path)` | `[]` | — | `MediaInputer` |
| `SpreadsheetOptimizer::generateVariants` | `($disk, $sourcePath, UploadedFile $file)` | `['csv'=>path,'html'=>path]` (parcial possível) | nenhuma (catch) | `MediaInputer` |
| `ImageRedigestService::buildPictureSnippet` | `(string $originalPath, array $variants, ?string $alt=null, array $opts=[])` | `string` | — | `BasicPageAssetManagerService`, `AssetManagerService` |
| `ImageRedigestService::variantsForGalleryMediaId` | `(int $id)` | `array` | — | redigest/consumidores |
| `ImageRedigestService::variantsForOriginalPath` | `(string $path)` | `array` | — | redigest |
| `ImageRedigestService::redigestAssetTable` | `(string $table, bool $dryRun=false)` | `int` alterados | erro de banco | comando |
| `ImageRedigestService::redigestContentBlobs` | `(string $table, string $contentColumn='content', ?array $areas=null, bool $dryRun=false)` | `int` alterados | erro de banco | comando |

### 4.2 Componentes Blade e contrato de UI

Os cinco componentes (`picture`, `media-input`, `media-group-input`, `media-viewer`, `audio`) são detalhados em [`ui-contrato.md`](ui-contrato.md). Resumo do contrato de dados:

- `picture` espera `originalPath` (string) e `variants` (array `largura => path`); usa `v2_storage_url()` para todas as URLs (`picture.blade.php:51-72`).
- `media-viewer` recebe `path`, `variants` array, `type` opcional; se `type` faltar, detecta pela extensão (`media-viewer.blade.php:18-34`).
- `media-input`/`media-group-input` não chamam `MediaInputer` diretamente: delegam upload à Galeria via picker ou `POST cms/v2/gallery` (`media-input.js:124-138`; `media-group-input.js:56-72`).

### 4.3 JS e ciclo de vida

`V2/Services/Media` não possui arquivos JS no manifest. Os componentes referenciam `public/assets/js/media-input.js`, `public/assets/js/media-group-input.js` e `vendor/cropperjs` (`media-input.blade.php:110-114`; `media-group-input.blade.php:23-26,132-136`). O contrato está no `ui-contrato.md`.

### 4.4 Ordem de inicialização e registro

1. `V2ServiceProvider::register()` descobre `V2/Services/*` e registra `V2/Services/Media/Views` no namespace `v2-service-media` (`V2/Core/Providers/V2ServiceProvider.php:168-188`).
2. `MediaInputer` é autowire: depende de `ImageOptimizer`, `PdfVisualOptimizer`, `AudioOptimizer`, `SpreadsheetOptimizer` e `StorageService`; todos resolvem por tipo.
3. `SpreadsheetOptimizer` depende de `ExcelTranspilerService` (namespace `V2\Services\ExcelImporter`), que precisa estar autoloadable.
4. O comando é registrado explicitamente em `app/Console/Kernel.php:20` (`\V2\Services\Media\Console\Commands\RedigestImages::class`).
5. `picture` depende de `v2_storage_url()` já carregado por `require_once` no `V2ServiceProvider` (`V2/Core/Providers/V2ServiceProvider.php:342-351`).
6. `media-input` depende de o layout CMS incluir `gallery-picker` com nome `global-media-picker` (`V2/layouts/cms/layouts/app.blade.php:139`).

### 4.5 Receitas de uso

```php
// 1. Ingestão direta (sem catálogo): use quando o consumidor só quer o arquivo e as variantes.
use V2\Services\Media\Services\MediaInputer;

public function __construct(MediaInputer $media) { $this->media = $media; }

$result = $this->media->processUpload($request->file('file'), 'noticias/audios', 'public');
// $result['type'] => image|pdf|audio|spreadsheet|document|unknown
// $result['path'] => 'noticias/audios/media_<uniqid>_<ts>.mp3'
// salve $result onde precisar; NÃO existe linha em v2_gallery_media.
```

```php
// 2. Ingestão catalogada (recomendada pelo CMS_INTEGRATION): Galeria persiste as variantes.
use V2\Services\Gallery\Services\GalleryService;
use V2\Services\Gallery\DTOs\GalleryConfig;

$config = new GalleryConfig('meu-modulo', ['image/*'], 10240, false);
$media = $gallery->upload($file, $config, 'Texto alternativo', 'Descrição', false, 'nome-fixo');
// $media['file_path'], $media['variants'] (JSON string), $media['metadata'] (JSON string)
```

```blade
{{-- 3. Renderizar imagem usando o mapa retornado/persistido. --}}
@include('v2-service-media::components.picture', [
    'originalPath' => $media->file_path,
    'variants' => is_string($media->variants) ? json_decode($media->variants, true) : ($media->variants ?? []),
    'alt' => $media->alt_text ?? '',
    'class' => 'img-fluid rounded',
    'lazy' => true,
])
```

```bash
# 4. Redigestão de snippets após mudança de variantes (dry-run por padrão).
php artisan v2:redigest-images --module=all --entity=all
php artisan v2:redigest-images --module=SLE --entity=content --area=header,footer --apply
```

### 4.6 Anti-uso

- **Chamar `processUpload` e esperar erro em caso de imagem corrompida.** O erro é logado e o upload retorna com `variants=[]`; valide/observe `variants` se o derivado for obrigatório (`MediaInputer.php:224-226`).
- **Procurar `variants['webp']` ou montar `path` de variante manualmente.** As chaves e nomes reais são `360/720/1200/1920`/`original` e `variants/<basename>-<width>w.webp` (`ImageOptimizer.php:90-116`).
- **Usar `MediaInputer` para mídia catalogada sem persistir.** Sem `GalleryService`, o arquivo fica no disco e nenhum seletor o encontra; a Galeria é quem insere em `v2_gallery_media`.
- **Passar `customFilename` com extensão ou path.** A extensão do cliente é anexada e não há sanitização (`MediaInputer.php:237-241`).

### 4.7 Custos e limites

- Limite de upload não existe no serviço; chamadores usam `max:10240` (10 MB) e `mimes` (`GalleryController.php:88-93`; `ContentRichEditorController.php:29-31`).
- O pipeline carrega original inteiro em memória e usa `Imagick` + `PhpSpreadsheet`; arquivos grandes podem estourar `memory_limit`/`max_execution_time`.
- `PDF` a 150 DPI pode gerar capa grande antes de reduzir; o número de páginas não limita o trabalho (só a página 0 é renderizada).
- `ImageOptimizer` grava até 4 arquivos por imagem; S3 tem 4 `put` adicionais.
- `redigestContentBlobs` processa 100 linhas por chunk e compila cada conteúdo com BladeCompiler antes de gravar; em bases grandes é O(N) com custo de regex.
- Sem throttle próprio.

## 5. Rotas e endpoints

Não se aplica: o serviço não registra rotas. O diretório não tem `Routes/`; as rotas usadas pelos widgets são da Galeria (`POST cms/v2/gallery`, nome `v2.gallery.store`) e do editor (`V2/Services/ContentRichEditor/Routes/cms.php`), portanto pertencem a outras unidades.

| Método | URI | Nome | Middleware | Controller@ação | Permissão | Efeito | Fonte |
|---|---|---|---|---|---|---|---|
| — | — | — | — | — | — | sem rotas próprias | `V2/Services/Media/` |

## 6. Dados e migrations

Não se aplica: a unidade não possui `Migrations/` nem models. Ela **lê e escreve** colunas de tabelas de outros donos:

| Tabela | Dono | Colunas usadas | Operação | Fonte |
|---|---|---|---|---|
| `v2_gallery_media` | `SVC-GALLERY` | `id`, `file_path`, `alt_text`, `variants`, `mime_type` | leitura (redigest) e escrita indireta via Gallery | `V2/Services/Media/Services/ImageRedigestService.php:91,116-118,178`; `V2/Services/Gallery/Migrations/2026_06_28_000000_create_v2_gallery_media_table.php:23-40` |
| `v2_basic_page_assets` (`BasicPage`) | `MOD-LAYOUTS-BASICPAGE` | `id` (UUID), `gallery_media_id`, `snippet` | leitura/update de `snippet` | `ImageRedigestService.php:153-203`; `2026_07_06_000200_create_basic_page_assets_table.php:17-30` |
| `v2_sle_assets` (`SLE`) | `MOD-SUPPORT-STRUCTURALLANDINGEDITOR` | `id` (UUID), `gallery_media_id`, `snippet` | leitura/update de `snippet` | `ImageRedigestService.php:153-203`; `2026_07_06_000000_create_v2_sle_assets_table.php:18-34` |
| `v2_basic_pages` | `MOD-LAYOUTS-BASICPAGE` | `id` (UUID), `content` | leitura/update de `content` | `RedigestImages.php:35-38`; `2026_06_29_092300_create_basic_pages_table.php:23-34` |
| `v2_structural_portal_contents` | `MOD-SUPPORT-STRUCTURALLANDINGEDITOR` | `id` (UUID), `area`, `content` | leitura/update de `content`, filtro `area` | `RedigestImages.php:35-38`; `2026_07_03_000000_create_v2_structural_portal_blocks_table.php:18-27` |

O `variants` de `v2_gallery_media` é JSON (`json`) e `metadata` é `jsonb` (`2026_06_28_000000_create_v2_gallery_media_table.php:34-36`); o model `GalleryMedia` não declara `$casts`, e o `ImageRedigestService` decodifica manualmente para evitar duplo decode (`ImageRedigestService.php:391-414`).

## 7. Configuração e permissões

| Chave | Default | Env | Efeito | Consumidores |
|---|---|---|---|---|
| `filesystems.media_disk` | `public` | `MEDIA_DISK` | gravado no snippet e usado pelo `StorageService` | `ImageRedigestService.php:48`; `GalleryService.php:55` |
| `filesystems.default` | `public` | `FILESYSTEM_DRIVER` | fallback do `StorageService` | `StorageService.php:35` |
| `filesystems.disks.public.url` | `/storage` | — | base de URL das variantes | `LocalAdapter.php:87` |

Permissões: não se aplica ao pipeline. O acesso ao upload é controlado pelos controllers/middlewares da Galeria/editor; o comando é de console.

## 8. Observabilidade e erros

| Situação | Log | Nível | Fonte |
|---|---|---|---|
| Tentativa de salvar | `[MediaInputer] Tentando salvar arquivo via StorageService` | `info` | `MediaInputer.php:244` |
| Falha do disco primário | `[MediaInputer] Falha ao salvar no disco primario, tentando fallback local: {msg}` | `warning` | `:254` |
| Falha crítica de storage | `[MediaInputer] Falha critica ao salvar arquivo (S3 e local falharam): {msg}` | `error` | `:270` |
| Áudio inválido | `[MediaInputer] Validação de áudio falhou para {nome}: {msg}` | `error` | `:165` |
| SVG ignorado | `[MediaInputer] SVG detectado, pulando geração de variantes WebP: {nome}` | `info` | `:207` |
| Falha de otimização | `[MediaInputer] Falha ao processar otimizações do arquivo {nome}: {msg}` | `error` | `:225` |
| Imagem: original ausente | `[ImageOptimizer] Arquivo original não encontrado: {path}` | `warning` | `ImageOptimizer.php:49` |
| Imagem: SVG ignorado | `[ImageOptimizer] SVG ignorado para variantes WebP: {path}` | `info` | `:57` |
| Imagem: variantes geradas | `[ImageOptimizer] Variantes WebP geradas com sucesso` + contagem | `info` | `:130-136` |
| Imagem: falha | `[ImageOptimizer] Falha ao processar imagem: {path} - Erro: {msg}` | `error` | `:138` |
| PDF: falha de metadados | `[PdfVisualOptimizer] Falha ao extrair metadados: {path} - Erro: {msg}` | `error` | `PdfVisualOptimizer.php:63` |
| PDF: falha de capa | `[PdfVisualOptimizer] Falha ao gerar capa: {path} - Erro: {msg}` | `error` | `:135` |
| Planilha: falha | `[SpreadsheetOptimizer] Erro ao gerar variantes de planilha: {msg}` | `error` | `SpreadsheetOptimizer.php:62` |
| Redigest por módulo/entidade | `[Redigest > Module={mod} > Entity={entity}] {n} registros ...` | `info` | `RedigestImages.php:138-146` |
| Blob não compila | `[Redigest] Blob de conteúdo não compilou (id=... tabela=...): {msg}` | `warning` | `ImageRedigestService.php:329-332` |
| Comando inválido | `Módulo inválido: {m}. Use BasicPage, SLE ou all.` / `Entidade inválida: {e}. Use assets, content ou all.` | console/error | `RedigestImages.php:56-66` |

Não há `ActivityLogger` no pipeline; a auditoria de upload é feita pelo `GalleryService` (`GalleryService.php:79-91`).

## 9. Consumidores (mapa de integração)

| Consumidor | O que usa | Como usa | Risco de quebra |
|---|---|---|---|
| `GalleryService` | `MediaInputer::processUpload` | grava original/variantes e insere `v2_gallery_media` | alto: todo upload catalogado |
| `ContentRichEditorController` | `processUpload` | upload de imagens/PDF/áudio/planilha no editor | alto: conteúdo do editor; usa chave `webp` inexistente |
| `LayoutBaseCmsController` (LSE) | `processUpload` | logos/banners do layout base | médio |
| `BasicPageAssetManagerService` | `buildPictureSnippet` | gera `snippet` de assets | alto: imagens de páginas básicas |
| `AssetManagerService` (SLE) | `buildPictureSnippet` | gera `snippet` de assets | alto: imagens do LSE |
| `RedigestImages` | `redigestAssetTable`, `redigestContentBlobs` | comando de manutenção | médio: corrige conteúdo legado |
| `Gallery`/templates Blade | caminhos de `variants` | `picture`/`v2_bg_image_css` | alto: imagens responsivas |
| Seeds (`SleHeaderSeeder`, `SleFooterSeeder`, `ProcessesGalleryImages`) | snippets/`view('...picture')` | conteúdo inicial com imagens | médio |

## 10. Lacunas

- `LG-MEDIA-01` — Não há configuração de qualidade/tamanho das variantes; 75/90 são fixos. Confirmar se há intenção de torná-los configuráveis (nenhuma chave em `config/`).
- `LG-MEDIA-02` — Não há teste no manifest para o pipeline; os testes de `tests/Feature/V2/Services/Media/` e `tests/Unit/V2/Services/Media/` existem fora da unidade. Confirmar cobertura real no CI.
- `LG-MEDIA-03` — O comportamento de GIF animado/TIFF no `Imagick` não é especificado; validar com amostras reais.
- `LG-MEDIA-04` — O motivo de `application/x-cdf` na whitelist não está documentado; confirmar se é legado de CSV.
- `LG-MEDIA-05` — Não há política de remoção de variantes órfãs (reupload/rotação/exclusão). Confirmar no `SVC-GALLERY`.
- `LG-MEDIA-06` — O `ImageRedigestService` depende do formato exato do snippet; conteúdos editados manualmente podem não ser redigeridos. Confirmar inventário de formatos em produção.
- `LG-MEDIA-07` — `RedigestImages` só cobre BasicPage e SLE; outros módulos com `snippet` não são redigeridos. Confirmar se existem.

## 11. Checklist de paridade

- [ ] `PC-MEDIA-01` — `processUpload` de imagem ≥ 360 px grava o original e variantes `360/720/1200/1920` (apenas ≤ largura), WebP qualidade 75, em `<dir>/variants/<base>-<w>w.webp` (`ImageOptimizer.php:79-98`).
- [ ] `PC-MEDIA-02` — Imagem < 360 px gera uma única variante `<dir>/variants/<base>.webp` com chave `'original'` (`ImageOptimizer.php:104-119`).
- [ ] `PC-MEDIA-03` — SVG é gravado e retorna `variants=[]` sem chamar Imagick (`MediaInputer.php:205-207`).
- [ ] `PC-MEDIA-04` — PDF retorna `metadata.pages` (via `pingImage`) e variantes de capa cujo caminho contém `temp_cover_`; a capa temporária é removida em sucesso (`PdfVisualOptimizer.php:40-72,95-125`).
- [ ] `PC-MEDIA-05` — Planilha gera `<base>_variant.csv` e `<base>_variant.html`; CSV usa `;` e apenas a primeira aba (`SpreadsheetOptimizer.php:49-59`; `ExcelTranspilerService.php:22-41`).
- [ ] `PC-MEDIA-06` — Áudio válido é gravado e `type='audio'` com `variants=[]`; áudio inválido retorna `type='unknown'`, `path=null`, `metadata.error` (`MediaInputer.php:161-175`; `AudioOptimizer.php:36-61`).
- [ ] `PC-MEDIA-07` — MIME fora da whitelist lança a mensagem exata `"Tipo de arquivo não suportado: ..."` (`MediaInputer.php:113-121`).
- [ ] `PC-MEDIA-08` — Falha de otimização não impede o upload: original permanece, `variants=[]`, log `error` (`MediaInputer.php:224-226`).
- [ ] `PC-MEDIA-09` — `$customFilename='foto'` grava `foto.<ext>`; `'foto.jpg'` grava `foto.jpg.<ext>` (`MediaInputer.php:237-241`).
- [ ] `PC-MEDIA-10` — `buildPictureSnippet` produz exatamente o `@include` com `originalPath`, `variants`, `disk`, `alt`, `class`, `lazy` (`ImageRedigestService.php:44-70`).
- [ ] `PC-MEDIA-11` — `v2:redigest-images` sem `--apply` não grava nada; com `--apply` grava e chama `v2:clear-page-cache` (`RedigestImages.php:69,97-100`).
- [ ] `PC-MEDIA-12` — Redigest de conteúdo preserva `alt`/`class`/`lazy`/`disk` e troca só o array `variants` (`ImageRedigestService.php:289-312`).
- [ ] `PC-MEDIA-13` — `variantsForGalleryMediaId` devolve `[]` para SVG/PDF, JSON inválido, `null`, `'{}'` e `'[]'` (`ImageRedigestService.php:391-414`).
- [ ] `PC-MEDIA-14` — `--module` inválido imprime `Módulo inválido: ... Use BasicPage, SLE ou all.` e retorna 1 (`RedigestImages.php:56-59`).

### 11.1 Checklist de extração (A–M)

| Item | Resposta |
|---|---|
| A1–A7 | Efeito e tipos na §1.1; efeitos na §1.2; contrato/erros na §1.3; limites na §1.4 e §4.7; perfis: o pipeline não distingue perfil, autorização é do chamador. |
| B1–B7 | Diagrama e mecânica na §2.1–2.2; trechos na §2.3; custos na §2.4; falhas parciais documentadas em §1.3, §2.4 e §3.3. |
| C1–C7 | Fonte única na §3.1; invariantes na §3.2; casos-limite na §3.3; armadilhas na §3.4; dívidas/divergências na §3.5. |
| D1–D9 | API na §4.1; componentes referenciados na §4.2; JS na §4.3; init na §4.4; receitas/anti-uso na §4.5–4.6; custos na §4.7; consumidores na §9; extensão: novo otimizador + ramo em `processUpload`. |
| E | Não se aplica: sem rotas próprias (§5). |
| F | Não se aplica: sem migrations próprias; leituras/escritas na §6. |
| G | Não se aplica: serviço sem tela própria; widgets no `ui-contrato.md`. |
| H | Mídia do portal: `picture`/`v2_bg_image_css`; ver `ui-contrato.md` e §4.2. |
| I | Contrato de UI no `ui-contrato.md`; resumo na §4.2. |
| J | Config na §7; sem permissões. |
| K | Logs/mensagens na §8; limites na §4.7. |
| L | Não se aplica: sem seeds próprios. |
| M | PCs na §11; lacunas na §10; evidências na §12. |

## 12. Evidências consultadas

- `V2/Services/Media/Services/MediaInputer.php:1-275`.
- `V2/Services/Media/Services/Midia/ImageOptimizer.php:1-149`.
- `V2/Services/Media/Services/Midia/PdfVisualOptimizer.php:1-144`.
- `V2/Services/Media/Services/Midia/AudioOptimizer.php:1-62`.
- `V2/Services/Media/Services/Midia/SpreadsheetOptimizer.php:1-67`.
- `V2/Services/Media/Services/ImageRedigestService.php:1-415`.
- `V2/Services/Media/Console/Commands/RedigestImages.php:1-160`.
- `V2/Services/Media/README.md:1-55`; `V2/Services/Media/CMS_INTEGRATION.md:1-121`.
- `V2/Services/ExcelImporter/Services/ExcelTranspilerService.php:22-112`.
- `V2/Services/Gallery/Services/GalleryService.php:55-125`; `V2/Services/Gallery/Controllers/GalleryController.php:86-109`.
- `V2/Services/ContentRichEditor/Controllers/ContentRichEditorController.php:25-53`.
- `V2/Modules/Support/StructuralLandingEditor/Controllers/LayoutBaseCmsController.php:80-93,235-248`.
- `V2/Core/Providers/V2ServiceProvider.php:168-192,342-351`; `app/Console/Kernel.php:20`.
- Migrations citadas na §6.
