---
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 `` dentro do ``),
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 ``
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'.*?', xml, re.S):
print(re.search(r'name="([^"]+)"', m.group(0)).group(1),
re.search(r'', 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 ``) 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
`` 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--embed-amd64.zip`).
2. Editar o `python._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
--implementation cp --abi cp --only-binary=:all: `
(`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 "..."`.