---
name: pptx-planta-progresso
description: Metodologia para construir um gerador de PPTX de progresso de obra (ou qualquer overlay colorido sobre uma planta/imagem técnica) a partir de uma imagem de planta com grelha de eixos, uma planilha de cronograma e um PPTX-modelo. Use quando o usuário pedir para pintar/marcar status (andamento/concluído) sobre uma planta em um PowerPoint, gerar um slide por período (quinzena/semana/mês) a partir de um cronograma, ou reproduzir esse tipo de relatório visual em um projeto novo (edificação, planta, cronograma diferentes dos já usados).
---

# Overlay de progresso sobre planta técnica em PPTX

Metodologia genérica extraída de um caso real (planta de fundações dividida
em quadrantes, cronograma por Geotecnia/Infraestrutura/Superestrutura,
1 slide por quinzena). Os nomes aqui são genéricos de propósito — adapte aos
nomes reais do projeto em mãos.

Esta skill é sobre **método**, não sobre um script fixo: os detalhes (nomes
de coluna, quantidade de eixos, cores) sempre vão mudar de projeto para
projeto; o que se repete é a sequência de passos e as pegadinhas abaixo.

## Visão geral do pipeline

```
imagem(ns) da planta  ──┐
                         ├─→ medir pixels dos eixos → mapear quadrante→retângulo
planilha de cronograma ─┘                                      │
                                                                 ▼
PPTX-modelo (1 slide-tipo) ──→ duplicar N vezes (1 por período) ──→ pintar
                                                                      cada
                                                                    quadrante
                                                                 conforme
                                                                  status
```

## Passo 1 — Medir os eixos da planta em pixels

Se a planta tem uma grelha de eixos numerados/rotulados (comum em plantas de
estrutura/fundação: colunas numéricas, linhas em letras), NÃO estime a
posição visualmente — meça programaticamente:

1. Carregar a imagem em escala de cinza (`PIL` + `numpy`).
2. Binarizar (`pixel < limiar`) e restringir a análise a uma FAIXA estreita
   onde só existem os círculos/marcas de eixo (topo da imagem para colunas,
   margem esquerda para linhas) — se usar a imagem inteira, hachuras e texto
   de dentro do desenho viram ruído e atrapalham a detecção.
3. Somar pixels escuros por coluna (ou por linha) dentro dessa faixa.
4. Agrupar posições consecutivas com valor > 0 (tolerância de alguns pixels
   de "buraco") em clusters; o centro de cada cluster (média ponderada pela
   intensidade) é a posição do eixo.
5. Filtrar clusters espúrios por LARGURA — um círculo de eixo tem uma largura
   característica (~mesma para todos); qualquer cluster muito mais estreito
   ou muito mais largo é ruído (seta, cota, texto cruzando a faixa) e deve
   ser descartado. Conferir sempre que o número de clusters batidos é igual
   ao número de eixos esperado.
6. Fazer o mesmo para linhas, cuidado com textos de cota grandes/rotacionados
   que caem na mesma faixa (ex. uma cota vertical entre dois eixos pode gerar
   um cluster fantasma — filtrar por largura resolve isso também).

Sempre medir na imagem **de fato usada no PPTX final** (extraia
`ppt/media/imageN.png` de dentro do `.zip` do `.pptx`, não confie numa cópia
enviada separadamente — arquivos "iguais" podem ter poucos pixels de
diferença de crop/exportação e isso já causou desalinhamento em produção).

## Passo 2 — Mapear "quadrante" (ou elemento) → retângulo/ponto em pixels

Se o cronograma se refere a células/trechos da planta por um código (ex.
`"10-11/A1-B"` = colunas 10 a 11, linhas A1 a B), parseie esse código com
regex e resolva os 2 pares de coordenadas usando os dicionários do passo 1.
Se a planta estiver dividida em mais de uma imagem (ex. planta muito larga
exportada em 2 metades), identifique de qual imagem cada elemento faz parte
(geralmente por faixa de coluna) — mas confirme que a granularidade do
cronograma nunca faz um elemento "atravessar" duas imagens; se atravessar,
esse caso precisa de tratamento especial.

## Passo 3 — Pixel → coordenada do PPTX (EMU)

Cada imagem colocada num slide tem uma posição/tamanho (`off`/`ext`, em EMU)
no XML do PPTX. A conversão ingênua é interpolação linear:
`emu = off + (pixel / tamanho_da_imagem_em_px) * ext`.

**Gotcha crítico**: se a imagem tiver um corte aplicado no PowerPoint
(elemento `<a:srcRect t="…" b="…" l="…" r="…"/>` dentro do `<p:blipFill>`),
a interpolação tem que ser feita sobre a região VISÍVEL da imagem, não sobre
a imagem inteira — do contrário tudo fica deslocado (geralmente para baixo
ou para o lado que foi cortado). Sempre inspecionar o XML de cada `<p:pic>`
do slide-modelo antes de assumir que pixel 0 = canto exibido:

```python
import re
xml = open('slide_extraido.xml').read()
for m in re.finditer(r'<p:pic>.*?</p:pic>', xml, re.S):
    print(re.search(r'name="([^"]+)"', m.group(0)).group(1),
          re.search(r'<a:srcRect[^/]*/>', m.group(0)))
```

Se houver corte, a fórmula fica:
```
vis0 = corte_inicio_fraction * tamanho_px
vis1 = (1 - corte_fim_fraction) * tamanho_px
escala = ext / (vis1 - vis0)
emu = off + (pixel - vis0) * escala
```

Se a planta estiver em mais de uma imagem lado a lado/empilhada, ainda dá
para conferir que a escala px→EMU bate entre elas (compare a distância entre
dois eixos conhecidos em cada imagem, convertida para EMU) — se bater, dá
para usar um offset constante para alinhar uma imagem à outra visualmente
mesmo que tenham sido exportadas com recortes/margens diferentes.

## Passo 4 — Decidir COMO pintar cada tipo de status

Nem todo "status" deve ser um retângulo do tamanho do quadrante inteiro.
Dois padrões comuns, que podem coexistir na mesma planta:

- **Status de área** (ex. escavação/estaqueamento, algo que ocupa a área
  toda do trecho): retângulo cobrindo o quadrante inteiro, com transparência
  (alpha < 100%) para a planta de baixo continuar legível.
- **Status de ponto** (ex. pilar, bloco de fundação — elementos que existem
  em pontos discretos, normalmente nos vértices/interseções da grelha, não
  espalhados pela área): marcador pequeno (quadrado/círculo) centrado em
  cada vértice relevante, não um retângulo proporcional ao quadrante. Se um
  vértice é compartilhado por elementos vizinhos, faça DEDUPE por ponto
  (usar o status mais avançado entre os que compartilham aquele vértice —
  ex. "concluído" prevalece sobre "andamento") em vez de desenhar
  marcadores sobrepostos.

Peça sempre uma imagem de referência de como o cliente já visualiza isso
(ou já visualizou em outra ferramenta) antes de assumir qual dos dois
padrões usar — a diferença visual é grande e already causou retrabalho.

## Passo 5 — Fidelidade de cor

Se o usuário fornecer uma legenda de referência (imagem), AMOSTRE OS PIXELS
em vez de "chutar" a cor no olho — e use a imagem em maior resolução
disponível (uma captura de tela pequena/comprimida pode alterar o tom o
suficiente pra ficar "quase certo, mas não fiel", que foi motivo de reclamação
real numa entrega). Amostre separadamente o preenchimento (centro do ícone) e
a borda (poucos pixels para dentro da borda visível) — normalmente são cores
diferentes.

```python
import numpy as np
from PIL import Image
arr = np.array(Image.open('legenda.png').convert('RGB'))
patch = arr[y-5:y+5, x-5:x+5].reshape(-1, 3)
hexcolor = '#%02X%02X%02X' % tuple(patch.mean(axis=0).round().astype(int))
```

Exponha as cores (preenchimento, borda, opacidade) num **arquivo de
configuração separado do código** (JSON simples) para o cliente poder ajustar
sem depender de você re-editar script.

## Passo 6 — Duplicar o slide-modelo N vezes (1 por período)

`python-pptx` não tem API pública para duplicar slide. Receita:

```python
def duplicate_slide(prs, source_index):
    source = prs.slides[source_index]
    dest = prs.slides.add_slide(source.slide_layout)
    for shp in list(dest.shapes):
        shp._element.getparent().remove(shp._element)   # limpa placeholders da layout

    rid_map = {}
    for rId, rel in source.part.rels.items():
        if rel.reltype == RT.SLIDE_LAYOUT or rel.is_external:
            continue
        rid_map[rId] = dest.part.relate_to(rel.target_part, rel.reltype)

    for shp in source.shapes:
        new_el = copy.deepcopy(shp._element)
        # IMPORTANTE: reescrever r:embed/r:link direto no atributo XML,
        # nunca com replace de string — se o remapeamento de rIds for uma
        # permutação (ex. rId2 vira rId3 e rId3 vira rId2), um replace
        # sequencial de texto corrompe as duas referências.
        for el in new_el.iter():
            for attr in (qn('r:embed'), qn('r:link')):
                old = el.get(attr)
                if old in rid_map:
                    el.set(attr, rid_map[old])
        dest.shapes._spTree.append(new_el)
    return dest
```

Depois de duplicar, reordenar slides manipulando `prs.slides._sldIdLst`
diretamente (mover/remover elementos `<p:sldId>`) já que também não há API
pública para isso.

## Passo 7 — Validar sem PowerPoint/LibreOffice instalado

Quando não há como renderizar o `.pptx` de verdade:

1. **Integridade estrutural**: `zipfile.ZipFile(path).testzip()` (sem
   corrupção) + `lxml.etree.fromstring` em cada `.xml`/`.rels` do pacote
   (sem erro de parse).
2. **Reconstrução visual manual**: ler `left/top/width/height` (EMU) de cada
   `<p:pic>` e forma via `python-pptx`, converter para pixels, montar um PNG
   com Pillow (`Image.alpha_composite`) reproduzindo o slide. É assim que se
   detectam desalinhamentos e cores erradas ANTES de entregar — não confie
   só na leitura do XML.

## Passo 8 — Empacotar como "sem dependências" para o usuário final

- **Linux**: `pyinstaller --onefile --name NomeDoApp script.py` — direto.
- **Windows, sem acesso a uma máquina/VM Windows nem Wine**: não dá para
  compilar um `.exe` de verdade. Alternativa que funciona: montar um Python
  "portátil" —
  1. Baixar o zip *embeddable* oficial do python.org
     (`python-<versão>-embed-amd64.zip`).
  2. Editar o `python<XY>._pth` de dentro do zip para incluir
     `Lib\site-packages` e descomentar `import site` (por padrão o
     embeddable vem sem suporte a pacotes de terceiros).
  3. Baixar as wheels certas **mesmo rodando de Linux**, sem precisar de
     Wine, com `pip download --platform win_amd64 --python-version <XY>
     --implementation cp --abi cp<XY> --only-binary=:all: <pacotes>`
     (`pip download` só baixa o arquivo, não executa nada — funciona
     cross-platform).
  4. Extrair (unzip) cada `.whl` dentro de `Lib\site-packages`.
  5. Lançador: um `.bat` de uma linha chamando
     `python\python.exe script.py`, com `pause` no final pra não fechar a
     janela sozinho.
  - Deixe claro pro usuário que isso é uma alternativa válida a um `.exe`
    "de verdade" — funciona igual (zero instalação), mas é uma pasta em vez
    de um arquivo único.
  - Se for gerar um `.zip` desse pacote, gere em um diretório TEMPORÁRIO
    fora da pasta que está sendo zipada, e só depois mova o `.zip` para
    dentro — gerar direto dentro da própria pasta faz o zip se
    autoincluir (bug real, já aconteceu).

## Passo 9 — Descoberta automática de arquivos de entrada

Se o programa final vai rodar numa pasta com 2-3 arquivos de entrada
(config, modelo, dados), não force nomes fixos — detecte por extensão
(`glob.glob('*.xlsx')` etc.), e falhe com mensagem clara se encontrar 0 ou
mais de 1 candidato ("deixe só um arquivo .xlsx na pasta"). Isso deixa o
programa reutilizável em outro projeto só trocando os arquivos de entrada,
sem precisar editar nada.

## Nota sobre git neste tipo de ambiente sandbox

Ambientes de execução isolados (containers efêmeros) às vezes não têm
identidade de git configurada e não se deve rodar `git config --global`
(pode ser bloqueado por política, e além disso não é uma boa prática mexer
em config global do usuário). Use override só no comando do commit:
`git -c user.name="..." -c user.email="..." commit -m "..."`.
