bethania_porto_santos/.claude/skills/pptx-planta-progresso/SKILL.md
lronetto 8b15f2d455 Adiciona skills de Claude Code documentando o aprendizado do gerador de PPTX
- 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>
2026-07-13 10:24:25 +00:00

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:

  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:

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:

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