Browse Source

Fix(Assinatura): Padronizar visual, posicionamento e adicionar hash nos stamps

- Corrige fonte dos stamps subsequentes de Courier para Helvetica
- Elimina borda dupla (border_width=0 no outer, 1 no inner)
- Substitui y_base=500 fixo por grid dinâmico baseado nas dimensões da página
- Unifica col_width entre ReportLab e pyhanko (era 70mm vs calculado)
- Footer (código, QR, URL, aviso legal) em posição fixa no rodapé (68mm)
- Adiciona Hash do documento abaixo da Data em cada bloco de assinatura
- Suporta até 14 assinaturas por página de autenticação

Signed-off-by: rangelbruno <rangelbruno@users.noreply.github.com>
pull/3858/head
rangelbruno 6 months ago
parent
commit
39d821f52b
  1. 184
      sapl/materia/views_assinatura.py

184
sapl/materia/views_assinatura.py

@ -115,6 +115,42 @@ def _encontrar_logo():
return None return None
def _calcular_blocks_y_start(page_height):
"""Calcula posição Y onde blocos de assinatura começam na página de autenticação."""
from reportlab.lib.units import mm
y = page_height - 40 * mm
if _encontrar_logo():
y -= 3 * mm
y -= 6 * mm # título
y -= 4 * mm # subtítulo
y -= 10 * mm # gap separador
return y
def _posicao_bloco_assinatura(idx, page_width, page_height):
"""
Calcula posição (x1, y1, x2, y2) para bloco de assinatura no índice dado.
Usa o mesmo grid da página de autenticação (ReportLab).
"""
from reportlab.lib.units import mm
margin = 20 * mm
col_width = (page_width - 3 * margin) / 2
block_height = 20 * mm
block_spacing = 2 * mm
col_x = [margin, margin + col_width + margin]
y_start = _calcular_blocks_y_start(page_height)
col = idx % 2
row = idx // 2
x = col_x[col]
y_top = y_start - row * (block_height + block_spacing)
return (x, y_top - block_height, x + col_width, y_top)
def _gerar_pagina_autenticacao(assinaturas_info, codigo, url_verificacao, def _gerar_pagina_autenticacao(assinaturas_info, codigo, url_verificacao,
page_width, page_height): page_width, page_height):
""" """
@ -172,36 +208,41 @@ def _gerar_pagina_autenticacao(assinaturas_info, codigo, url_verificacao,
y -= 10 * mm y -= 10 * mm
# ---- Blocos de assinatura (grade 2 colunas) ---- # ---- Blocos de assinatura (grade 2 colunas) ----
# Usa mesmas dimensões de _posicao_bloco_assinatura para consistência
# com os stamps pyhanko das assinaturas subsequentes.
col_width = (page_width - 3 * margin) / 2 col_width = (page_width - 3 * margin) / 2
block_height = 22 * mm block_height = 20 * mm
block_spacing = 5 * mm block_spacing = 2 * mm
col_x = [margin, margin + col_width + margin] col_x = [margin, margin + col_width + margin]
# Posição mínima Y: footer fixo no rodapé
footer_y = 68 * mm
for idx, assinatura in enumerate(assinaturas_info): for idx, assinatura in enumerate(assinaturas_info):
col = idx % 2 col = idx % 2
row = idx // 2 row = idx // 2
bx = col_x[col] bx = col_x[col]
by = y - row * (block_height + block_spacing) by_top = y - row * (block_height + block_spacing)
# Se vai ultrapassar a área útil, para # Se vai ultrapassar a área do footer, para
if by - block_height < 80 * mm: if by_top - block_height < footer_y:
break break
# Borda do bloco # Borda do bloco
c.setStrokeColorRGB(0.6, 0.6, 0.6) c.setStrokeColorRGB(0.6, 0.6, 0.6)
c.setLineWidth(0.5) c.setLineWidth(0.5)
c.rect(bx, by - block_height, col_width, block_height) c.rect(bx, by_top - block_height, col_width, block_height)
# Conteúdo do bloco # Conteúdo do bloco
text_x = bx + 3 * mm text_x = bx + 3 * mm
text_y = by - 5 * mm text_y = by_top - 3.5 * mm
c.setFont("Helvetica", 6) c.setFont("Helvetica", 6)
c.setFillColorRGB(0.3, 0.3, 0.3) c.setFillColorRGB(0.3, 0.3, 0.3)
c.drawString(text_x, text_y, "Assinado digitalmente por") c.drawString(text_x, text_y, "Assinado digitalmente por")
text_y -= 4 * mm text_y -= 3.5 * mm
c.setFont("Helvetica-Bold", 7) c.setFont("Helvetica-Bold", 7)
c.setFillColorRGB(0, 0, 0) c.setFillColorRGB(0, 0, 0)
nome = assinatura.get('nome_assinante', 'N/A').upper() nome = assinatura.get('nome_assinante', 'N/A').upper()
@ -209,56 +250,58 @@ def _gerar_pagina_autenticacao(assinaturas_info, codigo, url_verificacao,
nome = nome[:40] + "..." nome = nome[:40] + "..."
c.drawString(text_x, text_y, nome) c.drawString(text_x, text_y, nome)
text_y -= 4 * mm text_y -= 3 * mm
c.setFont("Helvetica", 6) c.setFont("Helvetica", 6)
c.setFillColorRGB(0.3, 0.3, 0.3) c.setFillColorRGB(0.3, 0.3, 0.3)
cargo = assinatura.get('cargo', '') cargo = assinatura.get('cargo', '')
if cargo: if cargo:
c.drawString(text_x, text_y, cargo) c.drawString(text_x, text_y, cargo)
text_y -= 3.5 * mm text_y -= 3 * mm
data = assinatura.get('data_assinatura', '') data = assinatura.get('data_assinatura', '')
c.drawString(text_x, text_y, f"Data: {data}") c.drawString(text_x, text_y, f"Data: {data}")
# Ajusta y para depois dos blocos text_y -= 3 * mm
n_rows = (len(assinaturas_info) + 1) // 2 c.setFont("Helvetica", 5)
y -= n_rows * (block_height + block_spacing) + 5 * mm c.setFillColorRGB(0.4, 0.4, 0.4)
c.drawString(text_x, text_y, f"Hash: {codigo}")
# Piso mínimo para evitar sobreposição com rodapé # ---- Footer fixo no rodapé ----
if y < 95 * mm: y_footer = footer_y
y = 95 * mm
# ---- Código de Autenticação ---- # Código de Autenticação
c.setFont("Helvetica-Bold", 11) c.setFont("Helvetica-Bold", 11)
c.setFillColorRGB(0, 0, 0) c.setFillColorRGB(0, 0, 0)
c.drawCentredString(center_x, y, f"Código de Autenticação: {codigo}") c.drawCentredString(center_x, y_footer,
y -= 10 * mm f"Código de Autenticação: {codigo}")
y_footer -= 8 * mm
# ---- QR Code ---- # QR Code
try: try:
qr_buf = _gerar_qrcode_image(url_verificacao) qr_buf = _gerar_qrcode_image(url_verificacao)
qr_img = ImageReader(qr_buf) qr_img = ImageReader(qr_buf)
qr_size = 30 * mm qr_size = 25 * mm
c.drawImage(qr_img, center_x - qr_size / 2, y - qr_size, c.drawImage(qr_img, center_x - qr_size / 2, y_footer - qr_size,
width=qr_size, height=qr_size) width=qr_size, height=qr_size)
y -= qr_size + 5 * mm y_footer -= qr_size + 3 * mm
except Exception as e: except Exception as e:
logger.warning(f"Não foi possível gerar QR Code: {e}") logger.warning(f"Não foi possível gerar QR Code: {e}")
y -= 5 * mm y_footer -= 3 * mm
# ---- URL de verificação ---- # URL de verificação
c.setFont("Helvetica", 7) c.setFont("Helvetica", 7)
c.setFillColorRGB(0.2, 0.2, 0.8) c.setFillColorRGB(0.2, 0.2, 0.8)
c.drawCentredString(center_x, y, f"Verifique em: {url_verificacao}") c.drawCentredString(center_x, y_footer,
y -= 12 * mm f"Verifique em: {url_verificacao}")
y_footer -= 8 * mm
# ---- Aviso legal ---- # Aviso legal
c.setFont("Helvetica", 7) c.setFont("Helvetica", 7)
c.setFillColorRGB(0.4, 0.4, 0.4) c.setFillColorRGB(0.4, 0.4, 0.4)
c.drawCentredString(center_x, y, c.drawCentredString(center_x, y_footer,
"Documento assinado digitalmente nos termos da") "Documento assinado digitalmente nos termos da")
y -= 3.5 * mm y_footer -= 3.5 * mm
c.drawCentredString(center_x, y, c.drawCentredString(center_x, y_footer,
"Medida Provisória nº 2.200-2/2001.") "Medida Provisória nº 2.200-2/2001.")
c.save() c.save()
@ -266,19 +309,31 @@ def _gerar_pagina_autenticacao(assinaturas_info, codigo, url_verificacao,
return buf.read() return buf.read()
def _criar_stamp_style(nome_assinante, cargo): def _criar_stamp_style(nome_assinante, cargo, hash_doc=''):
""" """
Cria um TextStampStyle customizado para assinaturas subsequentes, Cria um TextStampStyle customizado para assinaturas subsequentes,
mantendo visual consistente com a página de autenticação gerada mantendo visual consistente com a página de autenticação gerada
na primeira assinatura. na primeira assinatura (Helvetica, borda simples, mesmo layout).
""" """
from pyhanko.stamp import TextStampStyle, TextBoxStyle from pyhanko.stamp import TextStampStyle, TextBoxStyle
# Monta texto do carimbo com cargo (se houver) # Monta texto do carimbo com cargo (se houver) + hash
linhas = ['Assinado digitalmente por', '%(signer)s'] linhas = ['Assinado digitalmente por', '%(signer)s']
if cargo: if cargo:
linhas.append(cargo) linhas.append(cargo)
linhas.append('Data: %(ts)s') linhas.append('Data: %(ts)s')
if hash_doc:
linhas.append(f'Hash: {hash_doc}')
# Usa Helvetica para consistência com a página de autenticação (ReportLab)
font_kwargs = {}
try:
from pyhanko.pdf_utils.font import SimpleFontEngineFactory
font_kwargs['font'] = SimpleFontEngineFactory(
name='/Helvetica', avg_width=600
)
except (ImportError, Exception):
pass # Usa fonte padrão se Helvetica não disponível
return TextStampStyle( return TextStampStyle(
stamp_text='\n'.join(linhas), stamp_text='\n'.join(linhas),
@ -286,8 +341,9 @@ def _criar_stamp_style(nome_assinante, cargo):
font_size=7, font_size=7,
leading=10, leading=10,
border_width=1, border_width=1,
**font_kwargs,
), ),
border_width=1, border_width=0,
background=None, background=None,
background_opacity=0, background_opacity=0,
timestamp_format='%d/%m/%Y %H:%M', timestamp_format='%d/%m/%Y %H:%M',
@ -679,22 +735,15 @@ def materia_assinar_a1(request, pk):
existing_pdf_bytes = f.read() existing_pdf_bytes = f.read()
# Calcular posição do campo de assinatura na página de autenticação # Calcular posição do campo de assinatura na página de autenticação
# Grade 2 colunas para blocos de assinatura # Usa o mesmo grid da página gerada na primeira assinatura
col = n_assinatura % 2 temp_pypdf = PdfFileReader(io.BytesIO(existing_pdf_bytes))
row = n_assinatura // 2 auth_page = temp_pypdf.getPage(temp_pypdf.getNumPages() - 1)
margin = 20 * mm auth_page_width = float(auth_page.mediaBox.getWidth())
largura_carimbo = 70 * mm auth_page_height = float(auth_page.mediaBox.getHeight())
altura_carimbo = 22 * mm
x1, y1, x2, y2 = _posicao_bloco_assinatura(
x_pos = margin + col * (largura_carimbo + margin) n_assinatura, auth_page_width, auth_page_height
# Posição a partir do topo da página de autenticação )
y_base = 500 # Posição base aproximada para os blocos (em pontos)
y_pos = y_base - row * (altura_carimbo + 5 * mm)
x1 = x_pos
y1 = y_pos - altura_carimbo
x2 = x_pos + largura_carimbo
y2 = y_pos
signed_buffer = io.BytesIO() signed_buffer = io.BytesIO()
with io.BytesIO(existing_pdf_bytes) as inf: with io.BytesIO(existing_pdf_bytes) as inf:
@ -723,7 +772,10 @@ def materia_assinar_a1(request, pk):
# Usa PdfSigner com stamp_style customizado para # Usa PdfSigner com stamp_style customizado para
# manter visual consistente com a página de autenticação # manter visual consistente com a página de autenticação
stamp_style = _criar_stamp_style(nome_assinante, cargo) hash_doc = materia.codigo_autenticacao or ''
stamp_style = _criar_stamp_style(
nome_assinante, cargo, hash_doc
)
pdf_signer = PdfSigner( pdf_signer = PdfSigner(
meta, meta,
signer=signer, signer=signer,
@ -1295,20 +1347,15 @@ def docacessorio_assinar_a1(request, pk):
existing_pdf_bytes = f.read() existing_pdf_bytes = f.read()
# Calcular posição na página de autenticação # Calcular posição na página de autenticação
col = n_assinatura % 2 # Usa o mesmo grid da página gerada na primeira assinatura
row = n_assinatura // 2 temp_pypdf = PdfFileReader(io.BytesIO(existing_pdf_bytes))
margin = 20 * mm auth_page = temp_pypdf.getPage(temp_pypdf.getNumPages() - 1)
largura_carimbo = 70 * mm auth_page_width = float(auth_page.mediaBox.getWidth())
altura_carimbo = 22 * mm auth_page_height = float(auth_page.mediaBox.getHeight())
x_pos = margin + col * (largura_carimbo + margin) x1, y1, x2, y2 = _posicao_bloco_assinatura(
y_base = 500 n_assinatura, auth_page_width, auth_page_height
y_pos = y_base - row * (altura_carimbo + 5 * mm) )
x1 = x_pos
y1 = y_pos - altura_carimbo
x2 = x_pos + largura_carimbo
y2 = y_pos
signed_buffer = io.BytesIO() signed_buffer = io.BytesIO()
with io.BytesIO(existing_pdf_bytes) as inf: with io.BytesIO(existing_pdf_bytes) as inf:
@ -1335,7 +1382,10 @@ def docacessorio_assinar_a1(request, pk):
# Usa PdfSigner com stamp_style customizado para # Usa PdfSigner com stamp_style customizado para
# manter visual consistente com a página de autenticação # manter visual consistente com a página de autenticação
stamp_style = _criar_stamp_style(nome_assinante, cargo) hash_doc = docacessorio.codigo_autenticacao or ''
stamp_style = _criar_stamp_style(
nome_assinante, cargo, hash_doc
)
pdf_signer = PdfSigner( pdf_signer = PdfSigner(
meta, meta,
signer=signer, signer=signer,

Loading…
Cancel
Save