diff --git a/convert.py b/convert.py index 9ff1df8..41d6069 100644 --- a/convert.py +++ b/convert.py @@ -13,6 +13,10 @@ import json import sys from pathlib import Path +if sys.platform == 'win32': + sys.stdout.reconfigure(encoding='utf-8', errors='replace') + sys.stderr.reconfigure(encoding='utf-8', errors='replace') + SUPPORTED = {'.pdf', '.hwp', '.hwpx', '.hml', '.html', '.htm'} SKIP_NAMES = {'README.md', 'CLAUDE.md', 'AGENT_GUIDE.md'} diff --git a/converters/hwp.py b/converters/hwp.py index f10131f..a8036e9 100644 --- a/converters/hwp.py +++ b/converters/hwp.py @@ -43,7 +43,7 @@ def _com_hwp_to_hml(hwp_path: Path, hml_path: Path, timeout: int = 15) -> bool: t.start() t.join(timeout) if t.is_alive(): - print(f' COM 타임아웃 ({timeout}초) — pyhwp로 전환') + print(f' COM 타임아웃 ({timeout}초) -> pyhwp로 전환') return result[0] diff --git a/converters/pdf.py b/converters/pdf.py index 7c04673..647326b 100644 --- a/converters/pdf.py +++ b/converters/pdf.py @@ -17,6 +17,35 @@ from pathlib import Path import fitz # PyMuPDF from PIL import Image +# marker-pdf가 Python 이스케이프로 LaTeX 백슬래시를 손상시키는 버그 복원. +# \frac → \x0c(formfeed) + rac : \x0c = \f 대체 +# \times → 완전히 사라짐(imes만 남음): \t = tab이 strip됨 +# \beta → \x08(backspace) + eta : \x08 = \b 대체 +_CTRL_TO_LETTER = [('\x0c', 'f'), ('\x09', 't'), ('\x08', 'b'), ('\x0b', 'v')] +# \t가 완전히 사라진 케이스의 알려진 suffix → 원래 명령어 +_DROPPED_T_SUFFIXES = { + 'imes': 'times', 'ext': 'text', 'heta': 'theta', + 'au': 'tau', 'op': 'top', 'ilde': 'tilde', +} + + +def _fix_marker_latex(text: str) -> str: + """marker-pdf가 손상시킨 LaTeX 백슬래시를 복구한다.""" + # 제어문자가 LaTeX 이스케이프 첫 글자를 대체한 경우 복원 + # e.g. \x0c + rac → \f + rac = \frac + for ctrl, letter in _CTRL_TO_LETTER: + text = text.replace(ctrl, '\\' + letter) + + # \t가 완전히 사라진 케이스: 수식 내에서 suffix 단독 출현 → 전체 명령어로 복원 + # e.g. $imes → $\times, {1.1 imes → {1.1 \times + for suffix, full_cmd in _DROPPED_T_SUFFIXES.items(): + text = re.sub( + rf'(?<=[${{\s\^_{{]){re.escape(suffix)}(?=[^a-zA-Z])', + rf'\\{full_cmd}', + text, + ) + return text + # ── 페이지 분류 ─────────────────────────────────────────────────────────────── @@ -62,14 +91,16 @@ def classify_page(page: fitz.Page, doc: fitz.Document) -> str: text_density = text_len / page_area * 10_000 # 면적 대비 문자 수 - # 벡터 드로잉 밀도 (flowchart, CAD export 등은 수백 개 드로잉 포함) - drawing_density = len(drawings) / page_area * 10_000 + # 벡터 드로잉: 밀도 + 절대 개수 모두 확인 + # (엔지니어링 대형 페이지는 밀도가 낮아도 드로잉 수가 많으면 다이어그램) + n_drawings = len(drawings) + drawing_density = n_drawings / page_area * 10_000 # 1) 텍스트가 충분하면 텍스트 계열 + # (drawing이 많아도 text_density > 4면 표 테두리/장식선으로 간주) if text_density > 4: if not images: return "text" - # 이미지가 있어도 작은 이미지(로고 등)면 text large_images = [ img for img in images if doc.extract_image(img[0])["width"] > 150 @@ -77,8 +108,10 @@ def classify_page(page: fitz.Page, doc: fitz.Document) -> str: ] return "text-with-photo" if large_images else "text" - # 2) 벡터 드로잉이 많으면 다이어그램 - if drawing_density > 1.5: + # 2) 텍스트가 적을 때 벡터 드로잉이 많으면 다이어그램 + # - 밀도 기준: 소형 페이지 + # - 절대 수 기준: 대형 엔지니어링 페이지 (도면, 플로우차트 등) + if drawing_density > 1.5 or n_drawings >= 40: return "diagram" # 3) 래스터 이미지가 있으면 다이어그램 여부 분석 @@ -170,6 +203,7 @@ def convert_pdf(pdf_path: Path, output_dir: Path) -> dict: converter = PdfConverter(artifact_dict=create_model_dict()) rendered = converter(str(pdf_path)) full_text, _, marker_images = text_from_rendered(rendered) + full_text = _fix_marker_latex(full_text) # marker 추출 이미지 저장 if marker_images: