diff --git a/deploy/drhider/drhider.py b/deploy/drhider/drhider.py index 0582592..abcbe06 100644 --- a/deploy/drhider/drhider.py +++ b/deploy/drhider/drhider.py @@ -207,6 +207,8 @@ class TwoPassObfuscator: """ # --- Распаковать ZIP-файлы --- files = self._expand_zips(files) + # --- Конвертировать PDF → DOCX --- + files = self._convert_pdfs_to_docx(files) try: # --- Проход 1: сбор сущностей --- @@ -236,10 +238,6 @@ class TwoPassObfuscator: pass elif fname in all_docx: obf_content = self._replace_in_docx(all_docx[fname]) - elif fname.endswith('.pdf'): - txt = all_texts.get(fname, '') - obf_content = self._replace_in_text(txt, fname) - fname = fname[:-4] + '.txt' else: txt = all_texts.get(fname, '') obf_content = self._replace_in_text(txt, fname) @@ -253,6 +251,46 @@ class TwoPassObfuscator: self._regex_replacements.clear() self._sorted_keys.clear() + def _convert_pdfs_to_docx(self, files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, str]]: + """Конвертировать PDF в DOCX через LibreOffice. При совпадении имён — _из_pdf.""" + import subprocess, tempfile + result = [] + existing_names = {f[0] for f in files} + for fname, content, ctype in files: + if not fname.lower().endswith('.pdf'): + result.append((fname, content, ctype)) + continue + try: + with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp: + tmp.write(content) + pdf_path = tmp.name + outdir = tempfile.mkdtemp() + subprocess.run(['libreoffice', '--headless', '--convert-to', 'docx', + '--outdir', outdir, pdf_path], timeout=30, capture_output=True) + docx_files = [f for f in os.listdir(outdir) if f.endswith('.docx')] + if docx_files: + with open(os.path.join(outdir, docx_files[0]), 'rb') as f: + docx_content = f.read() + new_name = fname[:-4] + '.docx' + if new_name in existing_names: + new_name = fname[:-4] + '_из_pdf.docx' + existing_names.add(new_name) + result.append((new_name, docx_content, ctype)) + else: + log.warning("PDF→DOCX failed for %s", fname) + result.append((fname, content, ctype)) + except Exception as e: + log.warning("PDF→DOCX error for %s: %s", fname, e) + result.append((fname, content, ctype)) + finally: + if os.path.exists(pdf_path): + os.unlink(pdf_path) + if os.path.exists(outdir): + for f in os.listdir(outdir): + os.unlink(os.path.join(outdir, f)) + os.rmdir(outdir) + return result + def _expand_zips(self, files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, str]]: """Распаковать ZIP-файлы, заменив их содержимым. Остальные файлы — как есть. Защита от ZIP-бомб: ratio + накопительный размер (как в compare/unzip.py).""" diff --git a/site/services/drhider.py b/site/services/drhider.py index 898f7ef..ebfea7a 100644 --- a/site/services/drhider.py +++ b/site/services/drhider.py @@ -207,6 +207,8 @@ class TwoPassObfuscator: """ # --- Распаковать ZIP-файлы --- files = self._expand_zips(files) + # --- Конвертировать PDF → DOCX --- + files = self._convert_pdfs_to_docx(files) try: # --- Проход 1: сбор сущностей --- @@ -236,10 +238,6 @@ class TwoPassObfuscator: pass elif fname in all_docx: obf_content = self._replace_in_docx(all_docx[fname]) - elif fname.endswith('.pdf'): - txt = all_texts.get(fname, '') - obf_content = self._replace_in_text(txt, fname) - fname = fname[:-4] + '.txt' else: txt = all_texts.get(fname, '') obf_content = self._replace_in_text(txt, fname) @@ -253,6 +251,46 @@ class TwoPassObfuscator: self._regex_replacements.clear() self._sorted_keys.clear() + def _convert_pdfs_to_docx(self, files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, str]]: + """Конвертировать PDF в DOCX через LibreOffice. При совпадении имён — _из_pdf.""" + import subprocess, tempfile + result = [] + existing_names = {f[0] for f in files} + for fname, content, ctype in files: + if not fname.lower().endswith('.pdf'): + result.append((fname, content, ctype)) + continue + try: + with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp: + tmp.write(content) + pdf_path = tmp.name + outdir = tempfile.mkdtemp() + subprocess.run(['libreoffice', '--headless', '--convert-to', 'docx', + '--outdir', outdir, pdf_path], timeout=30, capture_output=True) + docx_files = [f for f in os.listdir(outdir) if f.endswith('.docx')] + if docx_files: + with open(os.path.join(outdir, docx_files[0]), 'rb') as f: + docx_content = f.read() + new_name = fname[:-4] + '.docx' + if new_name in existing_names: + new_name = fname[:-4] + '_из_pdf.docx' + existing_names.add(new_name) + result.append((new_name, docx_content, ctype)) + else: + log.warning("PDF→DOCX failed for %s", fname) + result.append((fname, content, ctype)) + except Exception as e: + log.warning("PDF→DOCX error for %s: %s", fname, e) + result.append((fname, content, ctype)) + finally: + if os.path.exists(pdf_path): + os.unlink(pdf_path) + if os.path.exists(outdir): + for f in os.listdir(outdir): + os.unlink(os.path.join(outdir, f)) + os.rmdir(outdir) + return result + def _expand_zips(self, files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, str]]: """Распаковать ZIP-файлы, заменив их содержимым. Остальные файлы — как есть.""" result = [] diff --git a/site/templates/drhider.html b/site/templates/drhider.html index d788ec3..a99ce36 100644 --- a/site/templates/drhider.html +++ b/site/templates/drhider.html @@ -84,7 +84,7 @@ Сверка договоров | DrHider - v1.8 + v1.9 @@ -123,7 +123,7 @@

✅ Форматы файлов

@@ -139,7 +139,7 @@ .doc не обрабатывается: старые Word-файлы возвращаются без изменений.
- PDF → текст: таблицы, графика и форматирование теряются. + PDF → DOCX: конвертация через LibreOffice может незначительно изменить форматирование сложных PDF.
Сканы/изображения: текст на картинках не распознаётся (нет OCR). @@ -211,6 +211,44 @@ function render() { BTN.onclick = () => { if (!files.length) return; + // Проверить коллизии PDF→DOCX + const collisions = []; + const names = files.map(f => f.name); + files.forEach(f => { + if (f.name.toLowerCase().endsWith('.pdf')) { + const docxName = f.name.slice(0, -4) + '.docx'; + if (names.includes(docxName)) { + collisions.push({pdf: f.name, docx: docxName}); + } + } + }); + if (collisions.length) { + showCollisionModal(collisions); + return; + } + doSubmit(); +}; + +function showCollisionModal(collisions) { + const list = collisions.map(c => `
📄 ${esc(c.pdf)}${esc(c.docx)} (уже есть)
`).join(''); + document.getElementById('collisionList').innerHTML = list; + document.getElementById('collisionOverlay').classList.add('show'); + window._collisions = collisions; +} + +function closeCollision(skipAll) { + document.getElementById('collisionOverlay').classList.remove('show'); + if (skipAll) { + // Удалить PDF-файлы из списка + const pdfNames = window._collisions.map(c => c.pdf); + files = files.filter(f => !pdfNames.includes(f.name)); + render(); + } + // В любом случае отправляем (бэкенд сам переименует) + setTimeout(doSubmit, 100); +} + +function doSubmit() { BTN.disabled = true; setStatus('progress', '⏳ Отправка...'); const xhr = new XMLHttpRequest(); @@ -248,7 +286,24 @@ function fmt(n) { return n>1e6 ? (n/1e6).toFixed(1)+' MB' : n>1e3 ? (n/1e3).toFi // ── Модальное окно ───────────────────────────────────────────── function openModal() { document.getElementById('modalOverlay').classList.add('show'); } function closeModal() { document.getElementById('modalOverlay').classList.remove('show'); } -document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); }); +// ── Коллизия PDF→DOCX ────────────────────────────────────────── +document.addEventListener('keydown', e => { if (e.key === 'Escape') closeCollision(true); }); + + + diff --git a/site/templates/index.html b/site/templates/index.html index 8dd8fc4..d73ca6f 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -73,7 +73,7 @@
Nubes - Сверка договоров — LLM AI-driven Event Sourcing v1.0.195-flask + Сверка договоров — LLM AI-driven Event Sourcing v1.0.196-flask
○ Загрузка ○ Классификация