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
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,
page_width, page_height):
"""
@ -172,36 +208,41 @@ def _gerar_pagina_autenticacao(assinaturas_info, codigo, url_verificacao,
y -= 10 * mm
# ---- 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
block_height = 22 * mm
block_spacing = 5 * mm
block_height = 20 * mm
block_spacing = 2 * mm
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):
col = idx % 2
row = idx // 2
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
if by - block_height < 80 * mm:
# Se vai ultrapassar a área do footer, para
if by_top - block_height < footer_y:
break
# Borda do bloco
c.setStrokeColorRGB(0.6, 0.6, 0.6)
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
text_x = bx + 3 * mm
text_y = by - 5 * mm
text_y = by_top - 3.5 * mm
c.setFont("Helvetica", 6)
c.setFillColorRGB(0.3, 0.3, 0.3)
c.drawString(text_x, text_y, "Assinado digitalmente por")
text_y -= 4 * mm
text_y -= 3.5 * mm
c.setFont("Helvetica-Bold", 7)
c.setFillColorRGB(0, 0, 0)
nome = assinatura.get('nome_assinante', 'N/A').upper()
@ -209,56 +250,58 @@ def _gerar_pagina_autenticacao(assinaturas_info, codigo, url_verificacao,
nome = nome[:40] + "..."
c.drawString(text_x, text_y, nome)
text_y -= 4 * mm
text_y -= 3 * mm
c.setFont("Helvetica", 6)
c.setFillColorRGB(0.3, 0.3, 0.3)
cargo = assinatura.get('cargo', '')
if cargo:
c.drawString(text_x, text_y, cargo)
text_y -= 3.5 * mm
text_y -= 3 * mm
data = assinatura.get('data_assinatura', '')
c.drawString(text_x, text_y, f"Data: {data}")
# Ajusta y para depois dos blocos
n_rows = (len(assinaturas_info) + 1) // 2
y -= n_rows * (block_height + block_spacing) + 5 * mm
text_y -= 3 * mm
c.setFont("Helvetica", 5)
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é
if y < 95 * mm:
y = 95 * mm
# ---- Footer fixo no rodapé ----
y_footer = footer_y
# ---- Código de Autenticação ----
# Código de Autenticação
c.setFont("Helvetica-Bold", 11)
c.setFillColorRGB(0, 0, 0)
c.drawCentredString(center_x, y, f"Código de Autenticação: {codigo}")
y -= 10 * mm
c.drawCentredString(center_x, y_footer,
f"Código de Autenticação: {codigo}")
y_footer -= 8 * mm
# ---- QR Code ----
# QR Code
try:
qr_buf = _gerar_qrcode_image(url_verificacao)
qr_img = ImageReader(qr_buf)
qr_size = 30 * mm
c.drawImage(qr_img, center_x - qr_size / 2, y - qr_size,
qr_size = 25 * mm
c.drawImage(qr_img, center_x - qr_size / 2, y_footer - qr_size,
width=qr_size, height=qr_size)
y -= qr_size + 5 * mm
y_footer -= qr_size + 3 * mm
except Exception as 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.setFillColorRGB(0.2, 0.2, 0.8)
c.drawCentredString(center_x, y, f"Verifique em: {url_verificacao}")
y -= 12 * mm
c.drawCentredString(center_x, y_footer,
f"Verifique em: {url_verificacao}")
y_footer -= 8 * mm
# ---- Aviso legal ----
# Aviso legal
c.setFont("Helvetica", 7)
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")
y -= 3.5 * mm
c.drawCentredString(center_x, y,
y_footer -= 3.5 * mm
c.drawCentredString(center_x, y_footer,
"Medida Provisória nº 2.200-2/2001.")
c.save()
@ -266,19 +309,31 @@ def _gerar_pagina_autenticacao(assinaturas_info, codigo, url_verificacao,
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,
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
# Monta texto do carimbo com cargo (se houver)
# Monta texto do carimbo com cargo (se houver) + hash
linhas = ['Assinado digitalmente por', '%(signer)s']
if cargo:
linhas.append(cargo)
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(
stamp_text='\n'.join(linhas),
@ -286,8 +341,9 @@ def _criar_stamp_style(nome_assinante, cargo):
font_size=7,
leading=10,
border_width=1,
**font_kwargs,
),
border_width=1,
border_width=0,
background=None,
background_opacity=0,
timestamp_format='%d/%m/%Y %H:%M',
@ -679,22 +735,15 @@ def materia_assinar_a1(request, pk):
existing_pdf_bytes = f.read()
# Calcular posição do campo de assinatura na página de autenticação
# Grade 2 colunas para blocos de assinatura
col = n_assinatura % 2
row = n_assinatura // 2
margin = 20 * mm
largura_carimbo = 70 * mm
altura_carimbo = 22 * mm
x_pos = margin + col * (largura_carimbo + margin)
# 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
# Usa o mesmo grid da página gerada na primeira assinatura
temp_pypdf = PdfFileReader(io.BytesIO(existing_pdf_bytes))
auth_page = temp_pypdf.getPage(temp_pypdf.getNumPages() - 1)
auth_page_width = float(auth_page.mediaBox.getWidth())
auth_page_height = float(auth_page.mediaBox.getHeight())
x1, y1, x2, y2 = _posicao_bloco_assinatura(
n_assinatura, auth_page_width, auth_page_height
)
signed_buffer = io.BytesIO()
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
# 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(
meta,
signer=signer,
@ -1295,20 +1347,15 @@ def docacessorio_assinar_a1(request, pk):
existing_pdf_bytes = f.read()
# Calcular posição na página de autenticação
col = n_assinatura % 2
row = n_assinatura // 2
margin = 20 * mm
largura_carimbo = 70 * mm
altura_carimbo = 22 * mm
x_pos = margin + col * (largura_carimbo + margin)
y_base = 500
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
# Usa o mesmo grid da página gerada na primeira assinatura
temp_pypdf = PdfFileReader(io.BytesIO(existing_pdf_bytes))
auth_page = temp_pypdf.getPage(temp_pypdf.getNumPages() - 1)
auth_page_width = float(auth_page.mediaBox.getWidth())
auth_page_height = float(auth_page.mediaBox.getHeight())
x1, y1, x2, y2 = _posicao_bloco_assinatura(
n_assinatura, auth_page_width, auth_page_height
)
signed_buffer = io.BytesIO()
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
# 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(
meta,
signer=signer,

Loading…
Cancel
Save