218 lines
8 KiB
Python
218 lines
8 KiB
Python
from PIL import Image, UnidentifiedImageError
|
||
from pathlib import Path
|
||
from pypdf import PdfReader, PdfWriter
|
||
from reportlab.pdfgen import canvas
|
||
from io import BytesIO
|
||
from pypdf.constants import UserAccessPermissions
|
||
import base64
|
||
from typing import Optional
|
||
from actions.ui.ui import info, ok, fail, status, progress
|
||
|
||
|
||
# Alias para manter compatibilidade e usar "error" na interface
|
||
def error(message: str):
|
||
fail(message)
|
||
|
||
|
||
class TifToPdfConverter:
|
||
"""
|
||
Converte um arquivo TIFF/SPD específico em PDF protegido e confidencial.
|
||
Totalmente em memória, com marca d'água e feedback visual via UI.
|
||
"""
|
||
|
||
def __init__(
|
||
self, input_file: str, output_folder: Optional[str] = "storage/output"
|
||
):
|
||
self.input_file = Path(input_file)
|
||
if not self.input_file.exists():
|
||
raise FileNotFoundError(f"Arquivo não encontrado: {self.input_file}")
|
||
|
||
# Pasta de saída padrão
|
||
self.output_folder = Path(output_folder)
|
||
self.output_folder.mkdir(parents=True, exist_ok=True)
|
||
|
||
self.watermark_text = "DISPONÍVEL APENAS PARA ATIVIDADE CORRECIONAL ONLINE"
|
||
self.confidential_metadata = {
|
||
"/Title": "Documento Confidencial",
|
||
"/Author": "Sistema MirrorSYNC",
|
||
"/Subject": "Documento protegido",
|
||
"/Keywords": "Confidencial, Corregedoria, MirrorSYNC, Orius",
|
||
"/Producer": "Orius Tecnologia",
|
||
"/Creator": "MirrorSYNC Automação",
|
||
}
|
||
|
||
# =========================================================
|
||
# Helper 1 — Converte imagem em PDF (BytesIO)
|
||
# =========================================================
|
||
def _convert_image_to_pdf(self) -> Optional[BytesIO]:
|
||
try:
|
||
with Image.open(self.input_file) as img:
|
||
frames = []
|
||
|
||
# Converte todas as páginas do TIFF para RGB
|
||
for frame in range(getattr(img, "n_frames", 1)):
|
||
img.seek(frame)
|
||
frame_rgb = img.convert("RGB")
|
||
frames.append(frame_rgb.copy())
|
||
|
||
if not frames:
|
||
info(f"Nenhuma página encontrada em {self.input_file}")
|
||
return None
|
||
|
||
# Buffer em memória
|
||
buffer = BytesIO()
|
||
|
||
# Salva todas as páginas como um único PDF
|
||
frames[0].save(
|
||
buffer,
|
||
format="PDF",
|
||
resolution=150,
|
||
save_all=True,
|
||
append_images=frames[1:],
|
||
quality=80,
|
||
optimize=True,
|
||
)
|
||
buffer.seek(0)
|
||
|
||
ok(
|
||
f"Imagem convertida para PDF em memória "
|
||
f"({len(frames)} página(s))."
|
||
)
|
||
return buffer
|
||
|
||
except UnidentifiedImageError:
|
||
error(f"Arquivo não reconhecido como imagem: {self.input_file}")
|
||
except Exception as e:
|
||
error(f"Erro ao converter {self.input_file}: {e}")
|
||
return None
|
||
|
||
# =========================================================
|
||
# Helper 2 — Gera PDF de marca d’água
|
||
# =========================================================
|
||
def _create_watermark_pdf(self, page_width: float, page_height: float) -> BytesIO:
|
||
buffer = BytesIO()
|
||
c = canvas.Canvas(buffer, pagesize=(page_width, page_height))
|
||
c.saveState()
|
||
|
||
# Fonte adaptativa (limites 24..72)
|
||
base_font = max(24, min(72, max(page_width, page_height) * 0.04))
|
||
c.setFont("Helvetica-Bold", base_font)
|
||
|
||
# Cor vermelha com transparência (~30%)
|
||
try:
|
||
c.setFillColorRGB(1, 0, 0) # vermelho
|
||
c.setFillAlpha(0.3)
|
||
except Exception:
|
||
# Fallback sem alpha (ainda vermelho)
|
||
c.setFillColorRGB(1, 0, 0)
|
||
|
||
# Centro e rotação para deixar na diagonal
|
||
x_center = page_width / 2.0
|
||
y_center = page_height / 2.0
|
||
c.translate(x_center, y_center)
|
||
c.rotate(45) # ângulo da marca d'água (diagonal)
|
||
|
||
# Linhas igualmente espaçadas ao longo do eixo Y rotacionado
|
||
span = max(page_width, page_height) * 0.4
|
||
y_positions = [-span, -span * 0.5, 0, span * 0.5, span]
|
||
|
||
for y in y_positions:
|
||
c.drawCentredString(0, y, self.watermark_text)
|
||
|
||
c.restoreState()
|
||
c.save()
|
||
buffer.seek(0)
|
||
return buffer
|
||
|
||
# =========================================================
|
||
# Helper 3 — Aplica marca d'água e confidencialidade
|
||
# =========================================================
|
||
def _apply_watermark_and_security(self, pdf_buffer: BytesIO) -> BytesIO:
|
||
reader = PdfReader(pdf_buffer)
|
||
writer = PdfWriter()
|
||
|
||
for page in reader.pages:
|
||
width = float(page.mediabox.width)
|
||
height = float(page.mediabox.height)
|
||
watermark_stream = self._create_watermark_pdf(width, height)
|
||
watermark_pdf = PdfReader(watermark_stream)
|
||
watermark_page = watermark_pdf.pages[0]
|
||
page.merge_page(watermark_page)
|
||
writer.add_page(page)
|
||
|
||
# Aplica metadados confidenciais
|
||
writer.add_metadata(self.confidential_metadata)
|
||
|
||
# Bloqueia ações do usuário
|
||
block_permissions = (
|
||
UserAccessPermissions.PRINT
|
||
| UserAccessPermissions.MODIFY
|
||
| UserAccessPermissions.EXTRACT
|
||
| UserAccessPermissions.ASSEMBLE_DOC
|
||
| UserAccessPermissions.PRINT_TO_REPRESENTATION
|
||
)
|
||
writer.encrypt(
|
||
user_password="",
|
||
owner_password="correcional",
|
||
permissions_flag=int(block_permissions),
|
||
)
|
||
|
||
output_buffer = BytesIO()
|
||
writer.write(output_buffer)
|
||
output_buffer.seek(0)
|
||
ok("Marca d’água e proteção aplicadas em memória.")
|
||
return output_buffer
|
||
|
||
# =========================================================
|
||
# Helper 4 — Converte PDF final para Base64
|
||
# =========================================================
|
||
def _to_base64(self, pdf_buffer: BytesIO) -> str:
|
||
b64 = base64.b64encode(pdf_buffer.getvalue()).decode("ascii")
|
||
ok("Arquivo convertido para Base64.")
|
||
return b64
|
||
|
||
# =========================================================
|
||
# Método principal — Processamento completo
|
||
# =========================================================
|
||
def convert(self, return_base64: bool = False) -> Optional[str]:
|
||
"""
|
||
Executa a conversão e retorna:
|
||
- Base64 (string), se return_base64=True
|
||
- Caminho do arquivo PDF salvo em disco, se return_base64=False
|
||
"""
|
||
|
||
info(f"📄 Iniciando conversão de: {self.input_file.name}")
|
||
|
||
# Barra de progresso única para todo o fluxo
|
||
with progress(f"Processando {self.input_file.name}...", total=3) as step:
|
||
|
||
# 1) Converter imagem em PDF
|
||
with status("Convertendo imagem para PDF..."):
|
||
pdf_buffer = self._convert_image_to_pdf()
|
||
step()
|
||
|
||
if not pdf_buffer:
|
||
error("Falha na conversão da imagem. Processo interrompido.")
|
||
return None
|
||
|
||
# 2) Aplicar marca d’água e segurança
|
||
with status("Aplicando marca d’água e proteção..."):
|
||
protected_buffer = self._apply_watermark_and_security(pdf_buffer)
|
||
step()
|
||
|
||
# 3) Gerar Base64 ou salvar em disco
|
||
if return_base64:
|
||
with status("Gerando conteúdo Base64..."):
|
||
result = self._to_base64(protected_buffer)
|
||
ok("Conversão concluída com sucesso (Base64 gerado).")
|
||
step()
|
||
return result
|
||
|
||
with status("Salvando arquivo PDF em disco..."):
|
||
output_path = self.output_folder / f"{self.input_file.stem}.pdf"
|
||
with open(output_path, "wb") as f:
|
||
f.write(protected_buffer.getvalue())
|
||
ok(f"Arquivo PDF salvo em: {output_path}")
|
||
step()
|
||
|
||
return str(output_path)
|