import os from urllib.parse import urlparse import requests from babel.dates import format_date from django.core.files.base import ContentFile from django.db import models, transaction from django.core.exceptions import ValidationError import fitz # PyMuPDF from asgiref.sync import async_to_sync class TipoDiarioOficial(models.Model): nome = models.CharField(max_length=100, unique=True) def __str__(self): return self.nome class Meta: verbose_name_plural = "Tipos de Diários Oficiais" class DiarioOficial(models.Model): data = models.DateField() arquivo = models.FileField(upload_to="diarios_oficiais/", blank=True, null=True) tipo = models.ForeignKey( TipoDiarioOficial, blank=True, null=True, on_delete=models.SET_NULL, related_name="diarios", ) numero = models.CharField(max_length=20, unique=True) link = models.URLField(blank=True, null=True, unique=True) def save(self, *args, **kwargs): updated = False super().save(*args, **kwargs) if self.link and not self.arquivo: self._download_pdf_from_link() updated = True if self.arquivo and not self.paginas.exists(): self._extract_pdf_pages() updated = True if updated: super().save(*args, **kwargs) def clean(self): super().clean() if not self.arquivo and not self.link: raise ValidationError("Informe um arquivo ou um link para o Diário.") def _validar_link(self): if not self.link.lower().endswith(".pdf"): raise ValidationError("O link deve apontar para um arquivo PDF.") def _download_pdf_from_link(self): try: response = requests.get(self.link) response.raise_for_status() parsed_url = urlparse(self.link) file_name = os.path.basename(parsed_url.path) or f"diario_{self.numero}.pdf" self.arquivo.save(file_name, ContentFile(response.content), save=True) except requests.RequestException as e: raise ValidationError(f"Não foi possível baixar o PDF: {e}") def _extract_pdf_pages(self): try: # Salvar temporariamente o PDF para abrir com o PyMuPDF with self.arquivo.open("rb") as pdf_file: temp_pdf_path = f"/tmp/diario_{self.id}.pdf" with open(temp_pdf_path, "wb") as temp_file: temp_file.write(pdf_file.read()) # Abrir e processar com fitz doc = fitz.open(temp_pdf_path) self._process_pdf_pages(doc) doc.close() # Remover arquivo temporário os.remove(temp_pdf_path) except Exception as pdf_error: raise ValidationError(f"Não foi possível processar o PDF: {pdf_error}") def _process_pdf_pages(self, doc): with transaction.atomic(): self.paginas.all().delete() for i, page in enumerate(doc): try: blocks = page.get_text("blocks") # Ordenar os blocos por coordenadas (y, x) para manter a ordem de leitura blocks.sort(key=lambda b: (b[1], b[0])) page_text = "" for block in blocks: text = block[4].strip() if text: page_text += text + "\n" # Crucial: Remove NULL bytes from the extracted text # PostgreSQL text fields cannot contain NUL (0x00) bytes cleaned_text = page_text.strip().replace('\x00', '') if cleaned_text: PageDiarioOficial.objects.create( diario=self, numero=i + 1, conteudo=cleaned_text, ) else: PageDiarioOficial.objects.create( diario=self, numero=i + 1, conteudo="[Conteúdo não extraído ou vazio]", ) except Exception as page_error: PageDiarioOficial.objects.create( diario=self, numero=i + 1, conteudo=f"[Erro na extração do texto: {str(page_error)}]", ) print(f"Erro ao processar a página {i+1} no Diario ID {self.id}: {page_error}") @property def data_formatada(self): return format_date(self.data, format="long", locale="pt_BR") @property def is_online(self): return bool(self.link) def __str__(self): tipo_nome = self.tipo.nome if self.tipo else "Sem Tipo" return f"Diário {tipo_nome} nº {self.numero}, {self.data_formatada}" class Meta: constraints = [models.UniqueConstraint(fields=["numero"], name="unique_numero")] verbose_name_plural = "Diários Oficiais" class PageDiarioOficial(models.Model): diario = models.ForeignKey( DiarioOficial, on_delete=models.CASCADE, related_name="paginas" ) layout_duas_colunas = models.BooleanField(default=False) numero = models.PositiveIntegerField() conteudo = models.TextField() class Meta: unique_together = ("diario", "numero") verbose_name = "Página de Diário Oficial" verbose_name_plural = "Páginas de Diários Oficiais" def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.diario: self.diario.save() def __str__(self): return f"Página {self.numero} do Diário {self.diario.numero}"