- plano-de-ataque: skill de projeto, especifica deste repo (arquivos, formato da planilha, coordenadas de pixel da planta, cores) - pptx-planta-progresso: skill generica/reutilizavel com a metodologia (medicao de eixos em pixel, correcao de crop srcRect, duplicacao de slide em python-pptx, fidelidade de cor, empacotamento sem dependencias para Linux/Windows, validacao sem Office instalado) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 KiB
| name | description |
|---|---|
| pptx-planta-progresso | 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:
- Carregar a imagem em escala de cinza (
PIL+numpy). - 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. - Somar pixels escuros por coluna (ou por linha) dentro dessa faixa.
- 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.
- 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.
- 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:
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.
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:
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:
- Integridade estrutural:
zipfile.ZipFile(path).testzip()(sem corrupção) +lxml.etree.fromstringem cada.xml/.relsdo pacote (sem erro de parse). - Reconstrução visual manual: ler
left/top/width/height(EMU) de cada<p:pic>e forma viapython-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
.exede verdade. Alternativa que funciona: montar um Python "portátil" —- Baixar o zip embeddable oficial do python.org
(
python-<versão>-embed-amd64.zip). - Editar o
python<XY>._pthde dentro do zip para incluirLib\site-packagese descomentarimport site(por padrão o embeddable vem sem suporte a pacotes de terceiros). - 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 downloadsó baixa o arquivo, não executa nada — funciona cross-platform). - Extrair (unzip) cada
.whldentro deLib\site-packages. - Lançador: um
.batde uma linha chamandopython\python.exe script.py, compauseno 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
.zipdesse pacote, gere em um diretório TEMPORÁRIO fora da pasta que está sendo zipada, e só depois mova o.zippara dentro — gerar direto dentro da própria pasta faz o zip se autoincluir (bug real, já aconteceu).
- Baixar o zip embeddable oficial do python.org
(
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 "...".