"""찾아보기(색인) 페이지에서 용어와 인쇄 페이지 번호를 추출.""" import re import pymupdf from .pdfutil import Line, page_lines PAGES_ONLY = re.compile(r"^\d{1,3}(?:\s*[,~\-–]\s*\d{1,3})*$") ENTRY = re.compile(r"^(.+?)\s*[\s_]\s*(\d{1,3}(?:\s*[,~\-–]\s*\d{1,3})*)$") XREF = re.compile(r"^(.+?)\s*(?:→|☞|⇒|=>|참조|see)\s*(.+)$") HEADINGS = {"찾아보기", "한글 찾아보기", "영문 찾아보기", "인물 찾아보기", "기타", "INDEX", "Index"} JAMO = set("ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅌㅍㅎㄲㄸㅃㅆㅉ가나다라마바사아자차카타파하") INDEX_PAT = re.compile(r"^(.+?)\s+(\d{1,3}(?:\s*[,~\-–]\s*\d{1,3})*)$") def find_index_pages(doc: pymupdf.Document) -> list[int]: """책 끝 60쪽 가운데 대부분의 줄이 '용어 페이지' 꼴인 쪽.""" pages = [] for i in range(max(0, len(doc) - 60), len(doc)): lines = page_lines(doc, i) merged = _merge_numbers(lines) if len(merged) < 15: continue hits = sum(bool(INDEX_PAT.match(l.text)) for l in merged) if hits / len(merged) > 0.6: pages.append(i) # 연속 구간만 사용 (본문 중간의 표 등 배제) if not pages: return [] run = [pages[-1]] for p in reversed(pages[:-1]): if run[0] - p <= 2: run.insert(0, p) else: break return run def _merge_numbers(lines: list[Line]) -> list[Line]: """'용어'와 '페이지 번호'가 별개 줄로 나뉜 경우(같은 높이) 하나로 합침.""" out: list[Line] = [] used = set() nums = [k for k, l in enumerate(lines) if PAGES_ONLY.match(l.text)] for k in nums: L = lines[k] best = None for j, T in enumerate(lines): if j == k or j in nums or abs(T.y - L.y) > 3.5: continue gap = L.x - T.x1 if -2 <= gap < 160 and (best is None or gap < best[0]): best = (gap, j) if best is not None: used.add(k) j = best[1] T = lines[j] lines[j] = Line(T.page, T.x, T.y, L.x1, T.size, T.font, f"{T.text} {L.text}") for k, l in enumerate(lines): if k not in used: out.append(l) return out def _pages(s: str) -> list[int]: out = [] for part in re.split(r"\s*,\s*", s): m = re.match(r"(\d+)(?:\s*[~\-–]\s*(\d+))?", part) if m: out.append(int(m.group(1))) return out def _columns(lines: list[Line]) -> list[list[Line]]: """x 시작점으로 단을 나눈 뒤 각 단을 위→아래로 정렬.""" xs = sorted({round(l.x) for l in lines}) cols: list[list[Line]] = [] starts: list[float] = [] for l in sorted(lines, key=lambda l: l.x): for ci, s in enumerate(starts): if abs(l.x - s) < 40: cols[ci].append(l) break else: starts.append(l.x) cols.append([l]) order = sorted(range(len(cols)), key=lambda c: starts[c]) return [sorted(cols[c], key=lambda l: l.y) for c in order] def extract_index(doc: pymupdf.Document, book: dict) -> list[dict]: pages = list(book["index_pages"]) if book.get("index_pages") else find_index_pages(doc) entries: list[dict] = [] section = "ko" for pi in pages: h = doc[pi].rect.height lines = [l for l in page_lines(doc, pi) if 40 < l.y < h - 40 and l.size < 20] lines = _merge_numbers(lines) for col in _columns(lines): pending: Line | None = None for l in col: t = l.text.strip() if t in HEADINGS or t.endswith("찾아보기"): if "인물" in t: section = "person" elif "영문" in t: section = "en" elif "한글" in t: section = "ko" pending = None continue if len(t) == 1 and (t in JAMO or t.isascii() and t.isalpha()): if section != "person": section = "en" if t.isascii() else "ko" pending = None continue if t == "●" or t.isdigit() and int(t) > 400: continue m = ENTRY.match(t) if not m: x = XREF.match(t) if x: entries.append({"term": x.group(1).strip(), "see": x.group(2).strip(), "pages": [], "section": section, "index_page": pi}) pending = None continue # 다음 줄로 이어지는 긴 용어 if pending is not None and abs(l.y - pending.y) < 20: pending = Line(l.page, pending.x, l.y, l.x1, l.size, l.font, pending.text + " " + t) else: pending = l continue term = m.group(1).strip() if pending is not None and abs(l.y - pending.y) < 20: term = pending.text + " " + term pending = None term = re.sub(r"\s*●\s*", "", term).strip(" ,") if not term or term.isdigit(): continue # 영문 색인에서 두 항목이 한 줄로 붙은 경우: "a 20, 101 b" → a / b split = re.match(r"^(.+?) (\d{1,3}(?:, ?\d{1,3})*) ([a-z].+)$", term) if section == "en" else None if split: entries.append({"term": split.group(1), "pages": _pages(split.group(2)), "section": section, "index_page": pi}) term = split.group(3) sec = section if sec == "person" and not re.search(r"[가-힣]", term): sec = "en" # 인물 찾아보기 뒤에 이어진 영문 색인 entries.append({"term": term, "pages": _pages(m.group(2)), "section": sec, "index_page": pi}) return entries