--- 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 "..."`.